Trello clone with Phoenix and React (第二章节)

这篇文章属于基于Phoenix Framework 和React的Trello系列

项目设置

现在我们已经选择了框架,让我们开始创建新的Phoenix项目。在这之前,请确保我们的系统已经按照官方提供的安装指南安装好了ElixirPhoenix

Webpack打包静态资源

对比Ruby on Rails,Phoenix没有自己的资源管理,而使用Brunch作为其资源打包工具,Brunch使用起来很现代和灵活。更有趣的事情是,可以不使用Brunch,我们可以使用Webpack。我以前也没有用过Brunch,后面就使用Webpack打包。

node.js需要作为Phoenix可选项,如果Phoenix 使用Brunch作为静态资源管理,就需要安装node.js。同时Webpack也需要node.js,因此必须确保node.js安装正确。

第一步:不使用Brunch创建新的Phoenix项目:

  1. $ mix phoenix.new --no-brunch phoenix_trello
  2. ...
  3. ...
  4. ...
  5. $ cd phoenix_trello

现在我们创建了新的项目,项目没有使用资源打包工具。

第二步:创建package.json文件,并安装Webpack作为dev依赖:

  1. $ npm init
  2. ...按照提示的默认值确认
  3. ...
  4. ...
  5. $ npm install webpack --save-dev

现在package.json文件中可以看到与下面相似的内容:

  1. {
  2. "name": "phoenix_trello",
  3. "devDependencies": {
  4. "webpack": "^1.12.9"
  5. },
  6. "dependencies": {
  7. },
  8. }

在项目中我们将使用一系列依赖,在这里就不列出他们,大家可以查看项目仓库中的的源文件。(备注:大家可以复制这个文件到项目文件夹,然后执行npm install就行)

第三步:我们需要添加webpack.config.js配置文件,便于Webpack打包资源:

  1. 'use strict';
  2. var path = require('path');
  3. var ExtractTextPlugin = require('extract-text-webpack-plugin');
  4. var webpack = require('webpack');
  5. // helpers for writing path names
  6. // e.g. join("web/static") => "/full/disk/path/to/hello/web/static"
  7. function join(dest) { return path.resolve(__dirname, dest); }
  8. function web(dest) { return join('web/static/' + dest); }
  9. var config = module.exports = {
  10. // our application's entry points - for this example we'll use a single each for
  11. // css and js
  12. entry: {
  13. application: [
  14. web('css/application.sass'),
  15. web('js/application.js'),
  16. ],
  17. },
  18. // where webpack should output our files
  19. output: {
  20. path: join('priv/static'),
  21. filename: 'js/application.js',
  22. },
  23. resolve: {
  24. extensions: ['', '.js', '.sass'],
  25. modulesDirectories: ['node_modules'],
  26. },
  27. // more information on how our modules are structured, and
  28. //
  29. // in this case, we'll define our loaders for JavaScript and CSS.
  30. // we use regexes to tell Webpack what files require special treatment, and
  31. // what patterns to exclude.
  32. module: {
  33. noParse: /vendor\/phoenix/,
  34. loaders: [
  35. {
  36. test: /\.js$/,
  37. exclude: /node_modules/,
  38. loader: 'babel',
  39. query: {
  40. cacheDirectory: true,
  41. plugins: ['transform-decorators-legacy'],
  42. presets: ['react', 'es2015', 'stage-2', 'stage-0'],
  43. },
  44. },
  45. {
  46. test: /\.sass$/,
  47. loader: ExtractTextPlugin.extract('style', 'css!sass?indentedSyntax&includePaths[]=' + __dirname + '/node_modules'),
  48. },
  49. ],
  50. },
  51. // what plugins we'll be using - in this case, just our ExtractTextPlugin.
  52. // we'll also tell the plugin where the final CSS file should be generated
  53. // (relative to config.output.path)
  54. plugins: [
  55. new ExtractTextPlugin('css/application.css'),
  56. ],
  57. };
  58. // if running webpack in production mode, minify files with uglifyjs
  59. if (process.env.NODE_ENV === 'production') {
  60. config.plugins.push(
  61. new webpack.optimize.DedupePlugin(),
  62. new webpack.optimize.UglifyJsPlugin({ minimize: true })
  63. );
  64. }

这里需要指出的是,我们需要两个不同webpack入口文件,一个用于javascript,另一个用于stylesheets,都位于 web/static文件夹。我们的输出文件位于private/static文件夹。同时,为了使用了S6/7 和 JSX 特性,我们使用Babel来设计。

最后一步:当我们启动服务器时,告诉Phoenix需要每次启动Webpack,这样Webpack可以监控我们开发过程中的任何更改,并产生页面需要的最终资源文件。下面是在config/dev.exs添加一个监视:

  1. # config/dev.exs
  2. config :phoenix_trello, PhoenixTrello.Endpoint,
  3. http: [port: 4000],
  4. debug_errors: true,
  5. code_reloader: true,
  6. cache_static_lookup: false,
  7. check_origin: false,
  8. watchers: [
  9. node: ["node_modules/webpack/bin/webpack.js", "--watch", "--color"]
  10. ]
  11. ...

如果我们现在运行服务器,可以看到Webpakc已经在运行并监视着项目:

  1. $ mix phoenix.server
  2. [info] Running PhoenixTrello.Endpoint with Cowboy using http on port 4000
  3. Hash: 93bc1d4743159d9afc35
  4. Version: webpack 1.12.10
  5. Time: 6488ms
  6. Asset Size Chunks Chunk Names
  7. js/application.js 1.28 MB 0 [emitted] application
  8. css/application.css 49.3 kB 0 [emitted] application
  9. [0] multi application 40 bytes {0} [built]
  10. + 397 hidden modules
  11. Child extract-text-webpack-plugin:
  12. + 2 hidden modules

还有一件事需要做,如果我们查看 private/static/js目录,可以发现phoenix.js文件。这个文件包含了我们需要使用的 websockets和channels,因此把这个文件复制到web/static/js文件夹中,这样便于我们使用。

前端基本结构

现在我们可以编写代码了,让我们开始创建前端应用结构,需要以下npm包:

  • bourbon , bourbon-neat, 我最喜欢的Sass mixin库
  • history 用于管理history .
  • react 和 react-dom.
  • redux 和 react-redux 用于状态处理.
  • react-router 路由库.
  • redux-simple-router 在线变更路由.

我不打算在stylesheets上面浪费更多的时间,后面始终需要修改它们。但是我需要提醒的是,我经常使用css-burrito创建合适的文件结构来组织Sass文件,这是我个人认为非常有用的。

我们需要配置Redux store,创建如下文件:

  1. //web/static/js/store/index.js
  2. import { createStore, applyMiddleware } from 'redux';
  3. import createLogger from 'redux-logger';
  4. import thunkMiddleware from 'redux-thunk';
  5. import reducers from '../reducers';
  6. const loggerMiddleware = createLogger({
  7. level: 'info',
  8. collapsed: true,
  9. });
  10. const createStoreWithMiddleware = applyMiddleware(thunkMiddleware, loggerMiddleware)(createStore);
  11. export default function configureStore() {
  12. return createStoreWithMiddleware(reducers);
  13. }

基本上配置store需要中间件。

  • reduxRouterMiddleware 用于分配路由的 actions 到 store。
  • redux-thunk 用于处理 async 动作.
  • redux-logger 用于记录一切动作和浏览器控制台的状态变化

同时,需要传递所有reducer state,创建如下的基础文件:

  1. //web/static/js/reducers/index.js
  2. import { combineReducers } from 'redux';
  3. import { routeReducer } from 'redux-simple-router';
  4. import session from './session';
  5. export default combineReducers({
  6. routing: routeReducer,
  7. session: session,
  8. });

需要指出的是,我们只需要两个reducer,routeReducer自动设置路由管理状态变化,session reducer如下所示:

  1. //web/static/js/reducers/session.js
  2. const initialState = {
  3. currentUser: null,
  4. socket: null,
  5. error: null,
  6. };
  7. export default function reducer(state = initialState, action = {}) {
  8. return state;
  9. }

初始化状态包含currentUser对象,这些对象用于用户验证,以及需要连接channels部分的socket,和验证用户过程中出现的error便于追溯问题。

有了这些准备,可以编写application.js文件,和渲染Root组件:

  1. //web/static/js/application.js
  2. import React from 'react';
  3. import ReactDOM from 'react-dom';
  4. import createBrowserHistory from 'history/lib/createBrowserHistory';
  5. import { syncReduxAndRouter } from 'redux-simple-router';
  6. import configureStore from './store';
  7. import Root from './containers/root';
  8. const store = configureStore();
  9. const history = createBrowserHistory();
  10. syncReduxAndRouter(history, store);
  11. const target = document.getElementById('main_container');
  12. const node = <Root routerHistory={history} store={store}/>;
  13. ReactDOM.render(node, target);

我们创建history和配置store,在主应用布局中渲染Root组件,将作为一个Redux Provider作用于路由。

  1. //web/static/js/containers/root.js
  2. import React from 'react';
  3. import { Provider } from 'react-redux';
  4. import { Router } from 'react-router';
  5. import invariant from 'invariant';
  6. import { RoutingContext } from 'react-router';
  7. import routes from '../routes';
  8. export default class Root extends React.Component {
  9. _renderRouter() {
  10. invariant(
  11. this.props.routingContext || this.props.routerHistory,
  12. '<Root /> needs either a routingContext or routerHistory to render.'
  13. );
  14. if (this.props.routingContext) {
  15. return <RoutingContext {...this.props.routingContext} />;
  16. } else {
  17. return (
  18. <Router history={this.props.routerHistory}>
  19. {routes}
  20. </Router>
  21. );
  22. }
  23. }
  24. render() {
  25. return (
  26. <Provider store={this.props.store}>
  27. {this._renderRouter()}
  28. </Provider>
  29. );
  30. }
  31. }

现在定义我们基本的路由文件:

  1. //web/static/js/routes/index.js
  2. import { IndexRoute, Route } from 'react-router';
  3. import React from 'react';
  4. import MainLayout from '../layouts/main';
  5. import RegistrationsNew from '../views/registrations/new';
  6. export default (
  7. <Route component={MainLayout}>
  8. <Route path="/" component={RegistrationsNew} />
  9. </Route>
  10. );

我们的应用将包含在MainLayout组件和Root路径中,路径将渲染registrations视图。文件最终版本可能有些复杂,后面将涉及到用户验证机制,这会在下一章节讲到。

最后,我们添加html容器,在主Phoenix应用布局中呈现root组件:

  1. <!-- web/templates/layout/app.html.eex -->
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="utf-8">
  6. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  7. <meta name="viewport" content="width=device-width, initial-scale=1">
  8. <meta name="description" content="">
  9. <meta name="author" content="ricardo@codeloveandboards.com">
  10. <title>Phoenix Trello</title>
  11. <link rel="stylesheet" href="<%= static_path(@conn, "/css/application.css") %>">
  12. </head>
  13. <body>
  14. <main id="main_container" role="main"></main>
  15. <!main role="main">
  16. <!%= render @view_module, @view_template, assigns %>
  17. <!/main>
  18. <script src="<%= static_path(@conn, "/js/application.js") %>"></script>
  19. </body>
  20. </html>

注意:link 和script标记,可以参考Webpack打包生产的静态资源。

因为我们是在前端管理路由,需要告诉Phoenix处理任何通过index动作产生的http请求,这个动作位于PageControler中,用于渲染主布局和Root组件:

  1. # master/web/router.ex
  2. defmodule PhoenixTrello.Router do
  3. use PhoenixTrello.Web, :router
  4. pipeline :browser do
  5. plug :accepts, ["html"]
  6. plug :fetch_session
  7. plug :fetch_flash
  8. plug :protect_from_forgery
  9. plug :put_secure_browser_headers
  10. end
  11. scope "/", PhoenixTrello do
  12. pipe_through :browser # Use the default browser stack
  13. get "*path", PageController, :index
  14. end
  15. end

现在就是这样。下一章节将介绍如何创建第一个数据库迁移,User模型和创建新用户所需的所有功能。在此期间,你可以查看运行演示和下载最终的源代码:

演示 源代码