Trelloã¯ç§ã®ãæ°ã«å
¥ãã®ã¢ããªã®1ã€ã§ãã ç§ã¯ãããéå§ä»¥æ¥äœ¿çšããŠããããã®åäœæ¹æ³ãã·ã³ãã«ããšæè»æ§ããšãŠãæ°ã«å
¥ã£ãŠããŸãã æ°ãããã¯ãããžãŒã®ç ç©¶ãéå§ãããã³ã«ãå®éã®åé¡ã解決ããããã«ç ç©¶ãããã¹ãŠãå®è·µã§ããæ¬æ Œçãªã¢ããªã±ãŒã·ã§ã³ãäœæãããããã®ãœãªã¥ãŒã·ã§ã³ã確èªããããšã奜ã¿ãŸãã ãã®ããã Elixirãšãã®Phoenixãã¬ãŒã ã¯ãŒã¯ã®å匷ãå§ãããšããåºäŒã£ããã®çŽ æŽãããè³æããã¹ãŠå®éã«äœ¿çšããã·ã³ãã«ã ãæ©èœçãªTrelloã®ç®èº«ãå®è£
ããæ¹æ³ã®ã¬ã€ããšããŠå
±æããå¿
èŠãããããšã«æ°ä»ããŸããã
ç®æ¬¡ïŒåŒ·èª¿è¡šç€ºãããŠããçŸåšã®è³æïŒ 翻蚳è
ããã®ã¡ã¢å¹Žã®åãããšãªã¯ãµãŒãšãã§ããã¯ã¹ãã¬ãŒã ã¯ãŒã¯ã«ç²Ÿéããããšã決ããŠãç§ã¯ãšãªã¯ãµãŒããã§ããã¯ã¹ãReactã䜿çšããTrelloã¯ããŒã³ã®å®è£
ã«é¢ããè峿·±ãã·ãªãŒãºã®èšäºã§ãããã«åºäŒããŸããã ç§ã«ã¯ããªãé¢çœããã§ããã·ã¢èªã®ç¿»èš³ã¯èŠã€ãããŸããã§ããããå
±æããããšæããŸããã æåŸã«ãäž¡æã翻蚳ã«å°éããŸããã
ç§ã¯Reactãšã³ã·ã¹ãã ã«å®å
šã«äžæ
£ãã§ããããšã«æ³šæããå¿
èŠããããŸãããã®éšåã¯çŸç¶ã®ãŸãŸã§ãã ããã«å ããŠããšãªã¯ãµãŒ/ãã§ããã¯ã¹ã®ããã€ãã®ç¬éã¯ãã®éã«å€åããŸãã-ãããžã§ã¯ãã¯ãŸã ç«ã¡äžãã£ãŠããŸããã ãŸããAngular2 <-> Phoenix Channels <-> Elixir / Phoenix Framework bunchããã£ãŠããã®ã§ãAngular2ã䜿çšããŠããã³ããšã³ããå®è£
ããããã«é¢ããèšäºãå
¬éããããã®å°æ¥ã®æéãèŠã€ããããšèããŠããŸãã
ç§ã®æèŠã§ã¯ãå
ã®ãµã€ã¯ã«ã§ã¯èšäºãããã¯ãçããããããããã§ã®1ã€ã®åºçç©ã«ã¯è€æ°ã®éšåãå«ãŸããå
ã®ãªã³ã¯ã¯å°èŠåºãã®æšªã«ãããŸãã
ç°è°ãããå Žåã¯ãçšèªã®å
ã®ååãæäŸããŸãã翻蚳ã«ççŸãããå Žåã¯ãä»£æ¿æãã容赊ãã ããã ãšã©ãŒãã¿ã€ããã¹ãäžæ£ç¢ºãã®ä¿®æ£ãæè¿ããŸãã
ãããŠãåºæãè€è£œããããšããizeã³ç³ãäžããŸãããã¿ãã¬ã®äžã§ãããã£ããã®åã«äœè
ããã¡ã¢ãšåºæãå
¥ããããšã¯ã§ããŸããã§ããã å°å
¥ãããéèŠã§ãããšæ±ºå®ããŸããã
æè¡ã¹ã¿ãã¯ã®ç޹ä»ãšéžæ
ãªãªãžãã«
Trelloã¯ç§ã®ãæ°ã«å
¥ãã®ã¢ããªã®1ã€ã§ãã ç§ã¯ãããéå§ä»¥æ¥äœ¿çšããŠããããã®åäœæ¹æ³ãã·ã³ãã«ããšæè»æ§ããšãŠãæ°ã«å
¥ã£ãŠããŸãã æ°ãããã¯ãããžãŒã®ç ç©¶ãéå§ãããã³ã«ãå®éã®åé¡ã解決ããããã«ç ç©¶ãããã¹ãŠãå®è·µã§ããæ¬æ Œçãªã¢ããªã±ãŒã·ã§ã³ãäœæãããããã®ãœãªã¥ãŒã·ã§ã³ã確èªããããšã奜ã¿ãŸãã ãã®ããã Elixirãšãã®Phoenixãã¬ãŒã ã¯ãŒã¯ã®å匷ãå§ãããšããåºäŒã£ããã®çŽ æŽãããè³æããã¹ãŠå®éã«äœ¿çšããã·ã³ãã«ã ãæ©èœçãªTrelloã®ç®èº«ãå®è£
ããæ¹æ³ã®ã¬ã€ããšããŠå
±æããå¿
èŠãããããšã«æ°ä»ããŸããã
ç§ãã¡ã¯äœãããã€ããã§ãã
å®éãæ¢åã®ãŠãŒã¶ãŒããã°ã€ã³ããŠè€æ°ã®ããŒããäœæããããããä»ã®ãŠãŒã¶ãŒãšå
±æããŠãªã¹ããã«ãŒãã远å ã§ãã1ããŒãžã®ã¢ããªã±ãŒã·ã§ã³ãäœæããŸãã ããŒãã衚瀺ãããšãæ¥ç¶ãããŠãããŠãŒã¶ãŒã衚瀺ããã倿Žã¯ããã«èªåçã«-Trelloã¹ã¿ã€ã«ã§-åãŠãŒã¶ãŒã®ãã©ãŠã¶ãŒã«åæ ãããŸãã
çŸåšã®æè¡ã¹ã¿ãã¯
ãã§ããã¯ã¹ã¯npmã䜿çšããŠéçãªãœãŒã¹ã管çãã BrunchãŸãã¯Webpackã䜿çšããŠããã«ããããåéããã®ã§ãåäžã®ã³ãŒãããŒã¹ãç¶æããªããããã³ããšã³ããšããã¯ãšã³ããå®å
šã«åé¢ããã®ã¯éåžžã«ç°¡åã§ãã ãããã£ãŠãããã¯ãšã³ãã«ã¯æ¬¡ã®ãã®ã䜿çšããŸãã
- ãšãªãã·ã«
- Phoenixãã¬ãŒã ã¯ãŒã¯
- ãšã¯ã
- PostgreSQL
ãããŠã1ããŒãžã®ããã³ããšã³ãã¢ããªã±ãŒã·ã§ã³ãäœæããã«ã¯ïŒ
- Webpack
- ã¹ã¿ã€ã«ã·ãŒãã®ãµã¹
- åå¿ãã
- Reactã«ãŒã¿ãŒ
- Redux
- ES6 / ES7 JavaScript
ããã«ããã€ãã®ElixiräŸåé¢ä¿ãšnpmããã±ãŒãžã䜿çšããŸãããããã»ã¹ã®åŸåã§ãããã«ã€ããŠèª¬æããŸãã
ãã®ã¹ã¿ãã¯ã¯ãªãã§ããïŒ
Elixirã¯ã Erlangã«åºã¥ããéåžžã«é«éã§åŒ·åãªé¢æ°åèšèªã§ãããRubyã«éåžžã«é¡äŒŒãã䜿ããããæ§æãåããŠããŸãã 圌ã¯éåžžã«ä¿¡é Œæ§ãé«ãã䞊ååŠçã«ç¹åããŠãããä»®æ³ãã·ã³ã®ãããã§ErlangïŒ Erlang VM ã BEAM-ãããTranslator ïŒã¯æ°åã®äžŠåããã»ã¹ã«å¯Ÿå¿ã§ããŸãã ç§ã¯ElixirãåããŠäœ¿çšããã®ã§ããŸã åŠã¶ã¹ãããšããããããããŸããããã§ã«åŠãã ããšãããéåžžã«å°è±¡çã§ãããšèšããŸãã
çŸåšãElixirã§æã人æ°ã®ããWebãã¬ãŒã ã¯ãŒã¯ã§ããPhoenixã䜿çšããŸããããã¯ã Rails Webéçºã§å°å
¥ããããã€ã³ããæšæºã®äžéšãå®è£
ããã ãã§ãªããäžèšã®éçãªãœãŒã¹ã®ç®¡çæ¹æ³ãªã©ãä»ã®å€ãã®ã¯ãŒã«ãªæ©èœãæäŸããŸãããããŠãç§ã«ãšã£ãŠæãéèŠãªããšã¯ãè€éãã远å ã®å€éšäŸåé¢ä¿ã®ãªãwebsocketã䜿çšããçµã¿èŸŒã¿ã®ãªã¢ã«ã¿ã€ã æ©èœã§ãïŒãããŠãä¿¡ããããŸãããæèšã®ããã«æ©èœããŸã ïŒã
åæã«ã React ã react-routerãããã³Reduxã䜿çšããŸããããã¯ããã®çµã¿åããã䜿çšããŠåäžããŒãžã®ã¢ããªã±ãŒã·ã§ã³ãäœæãããã®ç¶æ
ã管çããã®ã倧奜ãã ããã§ãã CoffieScriptããã€ãã®ããã«äœ¿çšãã代ããã«ãæ°ããå¹ŽïŒ èšäºã¯2016幎1æåæ¬-çŽTranslator ïŒã«ES6ãšES7ã§äœæ¥ãããã®ã§ããããéå§ããŠåå ããçµ¶å¥œã®æ©äŒã§ãã
æçµçµæ
ã¢ããªã±ãŒã·ã§ã³ã¯ã4ã€ã®ç°ãªããã¥ãŒã§æ§æãããŸãã æåã®2ã€ã¯ãç»é²ç»é¢ãšãã°ã€ã³ç»é¢ã§ãã

ã¡ã€ã³ç»é¢ã«ã¯ããŠãŒã¶ãŒèªèº«ã®ããŒããšãä»ã®ãŠãŒã¶ãŒãæ¥ç¶ããããŒãã®ãªã¹ããå«ãŸããŸãã

ãããŠæåŸã«ãããŒãã®ãã¬ãŒã³ããŒã·ã§ã³ã§ã¯ããã¹ãŠã®ãŠãŒã¶ãŒã誰ãããã«æ¥ç¶ããŠãããã確èªã§ãããªã¹ããšã«ãŒãã管çã§ããŸãã

ããããååãªè©±ã 2çªç®ã®ããŒãã®æºåãéå§ã§ããããã«ãããã§ãããŸããããæ°ãããã§ããã¯ã¹ãããžã§ã¯ãã®äœææ¹æ³ããã©ã³ãã®ä»£ããã«Webpackã䜿çšããããã«å€æŽããå¿
èŠããããã®ãããã³ããšã³ãã®ãã¬ãŒã ã¯ãŒã¯ã®æ§ææ¹æ³ã«ã€ããŠèª¬æããŸãã
Phoenix Frameworkãããžã§ã¯ãã®åæã»ããã¢ãã
ãªãªãžãã«
ãããã£ãŠãçŸåšã®ãã¯ãããžãŒã¹ã¿ãã¯ãéžæããããæ°ããPhoenixãããžã§ã¯ããäœæããããšããå§ããŸãããã ãããè¡ãåã«ã ElixirãšPhoenixããã§ã«ã€ã³ã¹ããŒã«ãããŠããå¿
èŠãããããã ã€ã³ã¹ããŒã«æé ã«ã€ããŠã¯å
¬åŒãµã€ãã䜿çšããŠãã ãã ã
Webpackã䜿çšããéçãªãœãŒã¹
Ruby on Railsãšã¯ç°ãªãã Phoenixã«ã¯ç¬èªã®ãªãœãŒã¹åŠçãã€ãã©ã€ã³ããããŸããïŒã¢ã»ãããã€ãã©ã€ã³ã äžéšã®ãã·ã¢èªRailsãªãœãŒã¹ã¯çšèªãããã¡ã€ã«ãã€ãã©ã€ã³ããšããŠç¿»èš³ããŸã-ã»ãŒç¿»èš³è
ïŒããããŠæè»ã Brunchã䜿çšããå¿
èŠããªãã®ã¯çŽ æŽãããããšã§ãããããæãŸãããªãå Žåã¯ã Webpackã䜿çšã§ããŸãã ç§ã¯Brunchãæ±ã£ãããšããªãã®ã§ã代ããã«Webpackã䜿çšããŸãã
Phoenixã«ã¯ããã©ã³ãã«å¿
èŠãªnode.jsããªãã·ã§ã³ã®äŸåé¢ä¿ãšããŠå«ãŸããŠããŸãããWebpackã«ã¯node.jsãå¿
èŠãªã®ã§ãåŸè
ãã€ã³ã¹ããŒã«ããŠãã ããã
ãã©ã³ããªãã§æ°ããPhoenixãããžã§ã¯ããäœæããŸãã
$ mix phoenix.new
ããŠãä»ã§ã¯ãªãœãŒã¹æ§ç¯ããŒã«ã®ãªãæ°ãããããžã§ã¯ãããããŸãã æ°ããpackage.json
ãã¡ã€ã«ãäœæããéçºçšã®äŸåé¢ä¿ãšããŠWebpackãã€ã³ã¹ããŒã«ããŸãïŒ deväŸåé¢ä¿-ã³ã¡ã³ã ãã©ã³ã¹ã¬ãŒã¿ãŒ ïŒïŒ
$ npm init ... ( Enter ) ... ... $ npm i webpack --save-dev
package.json
ã¯æ¬¡ã®ããã«ãªããŸãã
{ "name": "phoenix_trello", "devDependencies": { "webpack": "^1.12.9" }, "dependencies": { }, }
ãããžã§ã¯ãã®å Žåã倿°ã®äŸåé¢ä¿ãå¿
èŠãªã®ã§ãããã§ãã¹ãŠãã¹ã¯ããŒã«ããã®ã§ã¯ãªãããããžã§ã¯ããªããžããªã®ãœãŒã¹ãã¡ã€ã«ãèŠãŠãããããpackage.json
ã³ããŒããŠãã ããã æ¬¡ã®ã³ãã³ããå®è¡ããŠããã¹ãŠã®ããã±ãŒãžãã€ã³ã¹ããŒã«ããå¿
èŠããããŸãã
$ npm install
ãŸãã webpack.config.js
æ§æãã¡ã€ã«ã远å ããŠã webpack.config.js
ãªãœãŒã¹ã®åéæ¹æ³ãæç€ºããå¿
èŠããããŸãã
'use strict'; var path = require('path'); var ExtractTextPlugin = require('extract-text-webpack-plugin'); var webpack = require('webpack'); function join(dest) { return path.resolve(__dirname, dest); } function web(dest) { return join('web/static/' + dest); } var config = module.exports = { entry: { application: [ web('css/application.sass'), web('js/application.js'), ], }, output: { path: join('priv/static'), filename: 'js/application.js', }, resolve: { extesions: ['', '.js', '.sass'], modulesDirectories: ['node_modules'], }, module: { noParse: /vendor\/phoenix/, loaders: [ { test: /\.js$/, exclude: /node_modules/, loader: 'babel', query: { cacheDirectory: true, plugins: ['transform-decorators-legacy'], presets: ['react', 'es2015', 'stage-2', 'stage-0'], }, }, { test: /\.sass$/, loader: ExtractTextPlugin.extract('style', 'css!sass?indentedSyntax&includePaths[]=' + __dirname + '/node_modules'), }, ], }, plugins: [ new ExtractTextPlugin('css/application.css'), ], }; if (process.env.NODE_ENV === 'production') { config.plugins.push( new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({ minimize: true }) ); }
ããã§ã¯ã2ã€ã®webpackãšã³ããªãã€ã³ããå¿
èŠã§ããããšã瀺ããŸãã1ã€ã¯JavaScriptçšããã1ã€ã¯ã¹ã¿ã€ã«ã·ãŒãçšã§ãäž¡æ¹ãšãweb/static
ãã£ã¬ã¯ããªã«ãããŸãã åºåãã¡ã€ã«ã¯priv/static
äœæããpriv/static
ã ES6 / 7ããã³JSXã®ããã€ãã®æ©èœã䜿çšããããããããã®ç®çã®ããã«äœæãããããã€ãã®ããªã»ããã§Babelã䜿çšããŸãã
æåŸã®ã¹ãããã¯ãéçºãµãŒããŒãèµ·åãããã³ã«Webpack ãèµ·åããããPhoenixã«æç€ºããããšã§ããããã«ãããWebpackã¯éçºããã»ã¹äžã«å€æŽã远跡ããããã³ããšã³ããã¥ãŒã«ãã£ãŠåç
§ããã察å¿ãããªãœãŒã¹ãã¡ã€ã«ãçæã§ããŸãã ãããè¡ãã«ã¯ããobserverãã®èª¬æãconfig/dev.exs
ã
config :phoenix_trello, PhoenixTrello.Endpoint, http: [port: 4000], debug_errors: true, code_reloader: true, cache_static_lookup: false, check_origin: false, watchers: [ node: ["node_modules/webpack/bin/webpack.js", "--watch", "--color"] ] ...
éçºãµãŒããŒãèµ·åãããšã Webpackãæ©èœãã倿Žã远跡ããŠããããšãããããŸãã
$ mix phoenix.server [info] Running PhoenixTrello.Endpoint with Cowboy using http on port 4000 Hash: 93bc1d4743159d9afc35 Version: webpack 1.12.10 Time: 6488ms Asset Size Chunks Chunk Names js/application.js 1.28 MB 0 [emitted] application css/application.css 49.3 kB 0 [emitted] application [0] multi application 40 bytes {0} [built] + 397 hidden modules Child extract-text-webpack-plugin: + 2 hidden modules
å¥ã®ããšã priv/static/js
ãã£ã¬ã¯ããªã調ã¹ããšã phoenix.js
ãã¡ã€ã«ãphoenix.js
ãŸãã ãã®ãã¡ã€ã«ã«ã¯websocket
ãšchannels
ã䜿çšããããã«å¿
èŠãªãã®ããã¹ãŠå«ãŸããŠãããããå¿
èŠã«å¿ããŠæ¥ç¶ã§ããããã«web/static/js
ãœãŒã¹ã䜿çšããŠããŒã¹ãã£ã¬ã¯ããªã«ç§»åããŸã
ã¡ã€ã³ã®ããã³ããšã³ãæ§é
ããã§ãããã°ã©ãã³ã°ãéå§ããããã®ãã¹ãŠãã§ããŸããã ãŸããããã³ããšã³ãã¢ããªã±ãŒã·ã§ã³æ§é ãäœæããããšããå§ããŸããããããã«ã¯ãç¹ã«æ¬¡ã®ããã±ãŒãžãå¿
èŠã§ãã
- bourbonãšbourbon-neat ãSassçšã®ãæ°ã«å
¥ãã®ããã¯ã¹ã€ã³ã©ã€ãã©ãª
- JavaScriptããå±¥æŽã管çããããã®å±¥æŽ
- åå¿ããŠåå¿ãã
- ç¶æ
管çã®ããã®reduxããã³react-redux
- ã«ãŒãã£ã³ã°ïŒã«ãŒãã£ã³ã°ïŒã®ã©ã€ãã©ãªãšããŠã®react-router
- ç¶æ
ã®ã«ãŒã倿Žãä¿åããããã®redux-simple-router
ã¹ã¿ã€ã«ã·ãŒãã«ã€ããŠã¯ãŸã ä¿®æ£ããŠãããããæéãç¡é§ã«ããã€ããã¯ãããŸããããéåžžã¯css-burittoã䜿çšããŸããããã¯å人çãªæèŠãšããŠãé©åãªSassãã¡ã€ã«æ§é ãäœæããã®ã«éåžžã«äŸ¿å©ã§ãã
ReduxãªããžããªïŒreduxã¹ãã¢ïŒãæ§æããå¿
èŠããããããæ¬¡ã®ãã¡ã€ã«ãäœæããŸãã
//web/static/js/store/index.js import { createStore, applyMiddleware } from 'redux'; import createLogger from 'redux-logger'; import thunkMiddleware from 'redux-thunk'; import { syncHistory } from 'react-router-redux'; import reducers from '../reducers'; const loggerMiddleware = createLogger({ level: 'info', collapsed: true, }); export default function configureStore(browserHistory) { const reduxRouterMiddleware = syncHistory(browserHistory); const createStoreWithMiddleware = applyMiddleware(reduxRouterMiddleware, thunkMiddleware, loggerMiddleware)(createStore); return createStoreWithMiddleware(reducers); }
å®éã3ã€ã®ããã«ãŠã§ã¢ã¬ã€ã€ãŒãæã€ã¹ãã¢ãã»ããã¢ããããŠããŸãã
- ã«ãŒã¿ãŒã¢ã¯ã·ã§ã³ããªããžããªã«æž¡ãreduxRouterMiddleware
- éåæã¢ã¯ã·ã§ã³ãæž¡ãããã®redux-thunk
- ãã©ãŠã¶ã³ã³ãœãŒã«ãžã®ã¢ã¯ã·ã§ã³ãšç¶æ
ã®å€æŽãèšé²ããredux-logger
ãŸããç¶æ
ã¬ãã¥ãŒãµãŒã®çµã¿åãããæž¡ãå¿
èŠãããããããã®ãã¡ã€ã«ã®åºæ¬ããŒãžã§ã³ãäœæããŸãã
//web/static/js/reducers/index.js import { combineReducers } from 'redux'; import { routeReducer } from 'redux-simple-router'; import session from './session'; export default combineReducers({ routing: routeReducer, session: session, });
åºçºç¹ãšããŠå¿
èŠãªã®ã¯ãã«ãŒãã£ã³ã°ã®å€æŽãèªåçã«ç¶æ
ã«éä¿¡ããrouterReducer
ãæ¬¡ã®ãããªsession
2ã€ã®ã³ã³ããŒã¿ãŒïŒ routerReducer
ïŒã®ã¿ã§ãã
//web/static/js/reducers/session.js const initialState = { currentUser: null, socket: null, error: null, }; export default function reducer(state = initialState, action = {}) { return state; }
åŸè
ã®åæç¶æ
ã«ã¯ã蚪åè
ã®èªèšŒåŸã«æž¡ãcurrentUser
ãªããžã§ã¯ãããã£ãã«ãžã®æ¥ç¶ã«äœ¿çšããsocket
ãããã³ãŠãŒã¶ãŒèªèšŒäžã®åé¡ã远跡ããããã®error
ãå«ãŸããŸãã
ãããå®äºããããã¡ã€ã³ã®application.js
ãã¡ã€ã«ã«ç§»åããŠã Root
ã³ã³ããŒãã³ããã¬ã³ããªã³ã°application.js
ãŸãã
//web/static/js/application.js import React from 'react'; import ReactDOM from 'react-dom'; import { browserHistory } from 'react-router'; import configureStore from './store'; import Root from './containers/root'; const store = configureStore(browserHistory); const target = document.getElementById('main_container'); const node = <Root routerHistory={browserHistory} store={store}/>; ReactDOM.render(node, target);
ãã©ãŠã¶ãŒã®å±¥æŽãå«ããªããžã§ã¯ããäœæãããªããžããªãŒãæ§æããæåŸã«ã¡ã€ã³ã¢ããªã±ãŒã·ã§ã³ãã³ãã¬ãŒãã«Root
ã³ã³ããŒãã³ããæç»ããŸãããããRoot
ã®Reduxã¢ããã¿ãŒïŒã©ãããŒïŒ Provider
ã«ãªãroutes
ã
//web/static/js/containers/root.js import React from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router'; import invariant from 'invariant'; import routes from '../routes'; export default class Root extends React.Component { _renderRouter() { invariant( this.props.routerHistory, '<Root /> needs either a routingContext or routerHistory to render.' ); return ( <Router history={this.props.routerHistory}> {routes} </Router> ); } render() { return ( <Provider store={this.props.store}> {this._renderRouter()} </Provider> ); } }
次ã«ãéåžžã«åçŽãªã«ãŒããã¡ã€ã«ã«ã€ããŠèª¬æããŸãã
//web/static/js/routes/index.js import { IndexRoute, Route } from 'react-router'; import React from 'react'; import MainLayout from '../layouts/main'; import RegistrationsNew from '../views/registrations/new'; export default ( <Route component={MainLayout}> <Route path="/" component={RegistrationsNew} /> </Route> );
ã¢ããªã±ãŒã·ã§ã³ã¯MainLayout
ã³ã³ããŒãã³ãå
ã«MainLayout
ãã MainLayout
ã¯ç»é²ç»é¢ãæç»ããŸãã ãã®ãã¡ã€ã«ã®æçµããŒãžã§ã³ã¯ãåŸã§å®è£
ããèªèšŒã¡ã«ããºã ã®ããã«å€å°è€éã«ãªããŸãããããã«ã€ããŠã¯åŸã§èª¬æããŸãã
æåŸã«ãã¡ã€ã³ã®Phoenixã¢ããªã±ãŒã·ã§ã³ãã³ãã¬ãŒãã«Root
ã³ã³ããŒãã³ããæç»ããhtmlã³ã³ããã远å ããå¿
èŠããããŸãã
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content=""> <meta name="author" content="ricardo@codeloveandboards.com"> <title>Phoenix Trello</title> <link rel="stylesheet" href="<%= static_path(@conn, "/css/application.css") %>"> </head> <body> <main id="main_container" role="main"></main> <script src="<%= static_path(@conn, "/js/application.js") %>"></script> </body> </html>
ãªã³ã¯ããã³ã¹ã¯ãªããã¿ã°ã¯ã Webpackã«ãã£ãŠçæãããéçãªãœãŒã¹ãåç
§ããããšã«æ³šæããŠãã ããã
ããã³ããšã³ãã«ãŒãã£ã³ã°ã管çããã®ã§ãã¡ã€ã³ãã³ãã¬ãŒããšRoot
ã³ã³ããŒãã³ãã®ã¿ãæç»ããPageController
ã³ã³ãããŒã©ãŒã®ã¢ã¯ã·ã§ã³ïŒã¢ã¯ã·ã§ã³ïŒ index
ã€ãã³ããã³ãã©ãŒã«httpãªã¯ãšã¹ããéä¿¡ããããã«Phoenixã«æç€ºããå¿
èŠããããŸãã
# master/web/router.ex defmodule PhoenixTrello.Router do use PhoenixTrello.Web, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end scope "/", PhoenixTrello do pipe_through :browser # Use the default browser stack get "*path", PageController, :index end end
ä»ã®ãšãããã¹ãŠã§ãã æ¬¡ã®åºçç©ã§ã¯ãããŒã¿ããŒã¹ã User
ã¢ãã«ãããã³æ°ãããŠãŒã¶ãŒã¢ã«ãŠã³ããäœæããæ©èœã®æåã®ç§»è¡ãäœæããæ¹æ³ã«ã€ããŠèª¬æããŸãã
ãŠãŒã¶ãŒã¢ãã«ãšJWTèªèšŒ
ãªãªãžãã«
ãŠãŒã¶ãŒç»é²
ãããžã§ã¯ããå®å
šã«æ§æãããã®ã§ãããŒã¿ããŒã¹ãç§»è¡ããããã®User
ã¢ãã«ãšæé ãäœæããæºåãã§ããŸããã ãã®ããŒãã§ã¯ããããè¡ãæ¹æ³ãšã蚪åè
ãæ°ãããŠãŒã¶ãŒã¢ã«ãŠã³ããäœæã§ããããã«ããæ¹æ³ã«ã€ããŠèª¬æããŸãã
ãŠãŒã¶ãŒã¢ãã«ãšç§»è¡
ãã§ããã¯ã¹ã¯ãããŒã¿ããŒã¹ãšã®ããåãã®ä»²ä»ãšããŠEctoã䜿çšããŸãã Railsã®å ŽåãEctoã¯ActiveRecordsã«äŒŒãŠãããšèšããŸãããç°ãªãã¢ãžã¥ãŒã«éã§åæ§ã®æ©èœãå
±æããŠããŸãã
å
ã«é²ãåã«ãããŒã¿ããŒã¹ãäœæããå¿
èŠããããŸãïŒ ãã ãããã®åã«config/dev.exs
-translator commentã§ããŒã¿ããŒã¹æ¥ç¶èšå®ãæ§æããå¿
èŠããããŸã ïŒã
$ mix ecto.create
次ã«ãæ°ããç§»è¡ããã³Ectoã¢ãã«ãäœæããŸãã ã¢ãã«ãžã§ãã¬ãŒã¿ã¯ãã¢ãžã¥ãŒã«ã®ååãã¹ããŒã ã«ååãä»ããããã®è€æ°åœ¢ãããã³ãã©ãŒã :
å¿
é ãã£ãŒã«ã:
ãã©ã¡ãŒã¿ãŒãšããŠåãåããŸãã
$ mix phoenix.gen.model User users first_name:string last_name:string email:string encrypted_password:string
çµæã®ç§»è¡ãã¡ã€ã«ãèŠããšãRailsç§»è¡ãã¡ã€ã«ãšã®é¡äŒŒæ§ãããã«ããããŸãã
# priv/repo/migrations/20151224075404_create_user.exs defmodule PhoenixTrello.Repo.Migrations.CreateUser do use Ecto.Migration def change do create table(:users) do add :first_name, :string, null: false add :last_name, :string, null: false add :email, :string, null: false add :crypted_password, :string, null: false timestamps end create unique_index(:users, [:email]) end end
ãã£ãŒã«ãã®ã³ã³ãã³ãã«null
çŠæ¢ã远å ããé»åã¡ãŒã«ãã£ãŒã«ãã®äžæã®ã€ã³ããã¯ã¹ãã远å ããŸããã ããã¯ãä»ã®å€ãã®éçºè
ãè¡ãããã«ãã¢ããªã±ãŒã·ã§ã³ã«äŸåããããããããŒã¿ã®æŽåæ§ã«å¯Ÿãã責任ãããŒã¿ããŒã¹ã«ç§»ãããããã§ãã ããã¯å人çãªå¥œã¿ã®åé¡ã ãšæããŸãã
ããã§ã¯ãããŒã¿ããŒã¹ã«users
ããŒãã«ãäœæããŸãããã
$ mix ecto.migrate
User
ã¢ãã«ã詳ããèŠãŠã¿ãŸãããã
# web/models/user.ex defmodule PhoenixTrello.User do use Ecto.Schema import Ecto.Changeset schema "users" do field :first_name, :string field :last_name, :string field :email, :string field :encrypted_password, :string timestamps end @required_fields ~w(first_name last_name email) @optional_fields ~w(encrypted_password) def changeset(model, params \\ :empty) do model |> cast(params, @required_fields, @optional_fields) end end
2ã€ã®äž»èŠãªã»ã¯ã·ã§ã³ã衚瀺ãããŸãã
- ããŒãã«ãã£ãŒã«ãã«é¢é£ãããã¹ãŠã®ã¡ã¿ããŒã¿ãé
眮ãããã¹ããŒããããã¯
- ã¢ããªã±ãŒã·ã§ã³ã§äœ¿çšããæºåãã§ããåã«ãããŒã¿ã«é©çšããããã¹ãŠã®ãã§ãã¯ãšå€æãå®çŸ©ã§ãã倿Žã»ãã颿°ã
ãæ³šæ 翻蚳è
ïŒ
Ectoã®ææ°ããŒãžã§ã³ãæŽæ°ãããŸããã ããšãã°ã空ã®ã¢ãã ã¯å»æ¢äºå®ãšããŠããŒã¯ãããŠããããã代ããã«ç©ºã®é£æ³é
åïŒãããïŒ %{}
䜿çšããå¿
èŠããããŸãããŸããcast / 4颿°ã¯cast / 3ããã³validate_required / 3ãã³ãã«ã«çœ®ãæããããšããå§ãããŸã ã åœç¶ãææ°ã®Phoenixãžã§ãã¬ãŒã¿ãŒã¯ãããã®æšå¥šäºé
ã«åŸããŸãã
, , , null email. User
, , . encrypted_field
, .
:
# web/models/user.ex defmodule PhoenixTrello.User do # ... schema "users" do # ... field :password, :string, virtual: true # ... end @required_fields ~w(first_name last_name email password) @optional_fields ~w(encrypted_password) def changeset(model, params \\ :empty) do model |> cast(params, @required_fields, @optional_fields) |> validate_format(:email, ~r/@/) |> validate_length(:password, min: 5) |> validate_confirmation(:password, message: "Password does not match") |> unique_constraint(:email, message: "Email already taken") end end
, :
password
, , .password
email
- , 5 ;
password_confirmation
- email
. encrypted_password
. comeonin , mix.exs :
# mix.exs defmodule PhoenixTrello.Mixfile do use Mix.Project # ... def application do [mod: {PhoenixTrello, []}, applications: [ # ... :comeonin ] ] end #... defp deps do [ # ... {:comeonin, "~> 2.0"}, # ... ] end end
:
$ mix deps.get
comeonin User
encrypted_password
changeset :
# web/models/user.ex defmodule PhoenixTrello.User do # ... def changeset(model, params \\ :empty) do model # ... |> generate_encrypted_password end defp generate_encrypted_password(current_changeset) do case current_changeset do %Ecto.Changeset{valid?: true, changes: %{password: password}} -> put_change(current_changeset, :encrypted_password, Comeonin.Bcrypt.hashpwsalt(password)) _ -> current_changeset end end end
, . , comeonin encrypted_password
, .
ã«ãŒã¿ãŒ
, User
, , router.ex
:api
:
# web/router.ex defmodule PhoenixTrello.Router do use PhoenixTrello.Web, :router #... pipeline :api do plug :accepts, ["json"] end scope "/api", PhoenixTrello do pipe_through :api scope "/v1" do post "/registrations", RegistrationController, :create end end #... end
, POST
/api/v1/registrations
(action) :create
RegistrationController
, json ⊠, :)
ã³ã³ãããŒã©ãŒ
, . , . , , , , front-end json jwt . â , , .
jwt, Guardian, . mix.exs:
# mix.exs defmodule PhoenixTrello.Mixfile do use Mix.Project #... defp deps do [ # ... {:guardian, "~> 0.9.0"}, # ... ] end end
mix deps.get
config.exs:
# config/confg.exs #... config :guardian, Guardian, issuer: "PhoenixTrello", ttl: { 3, :days }, verify_issuer: true, secret_key: <your guardian secret key>, serializer: PhoenixTrello.GuardianSerializer
GuardianSerializer
, Guardian, :
# lib/phoenix_trello/guardian_serializer.ex defmodule PhoenixTrello.GuardianSerializer do @behaviour Guardian.Serializer alias PhoenixTrello.{Repo, User} def for_token(user = %User{}), do: { :ok, "User:#{user.id}" } def for_token(_), do: { :error, "Unknown resource type" } def from_token("User:" <> id), do: { :ok, Repo.get(User, String.to_integer(id)) } def from_token(_), do: { :error, "Unknown resource type" } end
, RegistrationController
:
# web/controllers/api/v1/registration_controller.ex defmodule PhoenixTrello.RegistrationController do use PhoenixTrello.Web, :controller alias PhoenixTrello.{Repo, User} plug :scrub_params, "user" when action in [:create] def create(conn, %{"user" => user_params}) do changeset = User.changeset(%User{}, user_params) case Repo.insert(changeset) do {:ok, user} -> {:ok, jwt, _full_claims} = Guardian.encode_and_sign(user, :token) conn |> put_status(:created) |> render(PhoenixTrello.SessionView, "show.json", jwt: jwt, user: user) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> render(PhoenixTrello.RegistrationView, "error.json", changeset: changeset) end end end
(pattern matching), create
"user"
. User . , Guardian ( encode_and_sign
) , jwt json . , , json , .
JSON
Phoenix JSON - Poison . Phoenix, - . â User
, :
# web/models/user.ex defmodule PhoenixTrello.User do use PhoenixTrello.Web, :model # ... @derive {Poison.Encoder, only: [:id, :first_name, :last_name, :email]} # ... end
json (channel), . !
back-end, , front-end, , , React Redux . .