koa2-react-isomorphic-boilerplate
keywords: koa2,react,redux,isomorphic app
Getting Start in dev mode
git clone https://github.com/wssgcg1213/koa2-react-isomorphic-boilerplate
cd koa2-react-isomorphic-boilerplate
npm install # 国内可以使用 cnpm 加速, 教育网可使用 rednpm (https://npm.mirror.cqupt.edu.cn)加速
npm start # dev mode
open in browser
Deploy
npm test # pass unit tests npm run build # build to dist npm run production # or pm2 start
目录结构
.├── app│ ├── actions│ ├── common│ ├── components│ ├── containers│ │ └── App.jsx # React App │ ├── reducers│ ├── routes.js # 路由配置文件 │ └── store│ └── configureStore.js├── bin│ ├── development.js│ └── production.js├── package.json├── platforms│ ├── browser # 浏览器相关 │ │ └── index.js # 浏览器 APP 入口 │ ├── common│ │ └── config # 配置 │ │ ├── default.js│ │ └── index.js│ └── server # 服务端相关 │ ├── controllers│ │ ├── indexCtrl.js│ │ ├── serverRenderCtrl.js│ │ └── usersCtrl.js│ ├── index.js # 服务端入口 │ ├── middlewareRegister.js│ ├── models│ ├── routes # 服务端路由 │ │ ├── api.js│ │ ├── index.js│ │ └── render.js│ ├── services│ └── templates # 服务端模板 │ ├── 404.ejs│ ├── 422.ejs│ ├── 500.ejs│ └── index.ejs├── pm2.json├── public # public │ ├── favicon.ico│ └── robots.txt├── test│ └── test.js├── webpack.build.js└── webpack.development.js
同构的优势
- 首屏性能
- SEO / 搜索引擎爬虫支持
- 无缝的用户体验
实现的细节
目录结构
从 commits 可以看出, 目录的结构中途修改了多次, 对于目前的结构我也觉得有不合理的地方, 欢迎来提 issue 讨论. 由于在同构应用中, 有三种代码: 客户端 only ,服务端 only , 共用代码 , 这使得良好的目录结构显得更加重要. 最后我是参考了 ultimate-hot-reloading-example 这个项目的目录结构, 又做了适当的修改.
ServerSideRender
有人提出 React 是一种架构模式, 无论是内建的 DOM、Native还是React Canvas都是的一种基于React模式的具体实现. 那么 HTML string 其实也是 react 模式的一种实现, 只不过产出的是一堆字符串. React 在前几个版本中就把 render 函数从 react 中单独独立出来(react-dom), 服务端渲染依赖的就是 react-dom/server 中的renderToString 方法.
const appHtml =
路由
由于服务端需要做这么几件事情:
- react render
- api server
这些都需要路由来做分发
在服务端对于 /api 的请求, 全部交给 koa-router 来处理, 对于其他请求则交给 react-router 来处理
// api server through koa-router if ctxpath return await ctx next // others react-router to render await ctx next
koa-router 的用法在 platforms/server/routes/api.js 中, react-router 的用法的核心是 match 函数
const redirectLocation renderProps = await if redirectLocation ctx else if renderProps await else await
上面的 renderCtrl 是负责服务端渲染的方法. require 的 routes 文件就是 react-router 的配置文件, 该文件在前后端路由中是共用的.
// routes.js <Router history=browserHistory> <Route path="/" component=App> <Route path="picture" component=Picture /> <Route path="counter" component=Counter /> </Route></Router>
数据层
React 有丰富的数据层框架, 我用的是现在比较流行的 redux.
在服务端前置拉取数据的逻辑参考了 ReactJS 服务端同构实践【QQ音乐web团队】这篇文章的实现方式.
即在 react component 中规定了一个 fetch 的静态方法,在 Server Rendering 时,和前端相比组件没有完整的生命周期,只会走到 componentWillMount, 所以在前端的 componentDidMount 方法中去判断是否在前端做 fetch; 在做服务端渲染的时候遍历需要渲染的 components 去同步调用静态方法得到数据并吐出在页面上作为redux 的 initialState.
let prefetchTasks = for let component of renderPropscomponents if component && componentWrappedComponent && componentWrappedComponentfetch const _tasks = componentWrappedComponent if Array prefetchTasks = prefetchTasks else if _tasksthen prefetchTasks await PromiseallprefetchTasks await ctx
Component:
static { const fetchTasks = fetchTasks return fetchTasks } { const loaded success = thisprops if !loaded || loaded && !success thisconstructor } { const location: pathname = thisprops const headerCurrent = pathname === '/' ? 'home' : pathname return <div> <Header current=headerCurrent/> <Main>thispropschildren</Main> <Footer /> </div> }
模板
模板引擎是用的简单的 ejs
在 server render 的时候把 initial State 获取并吐出在页面中
<%- app %>
在前端的入口文件中使用 window.__REDUX_STATE__
作为初始 state
const store = ReactDOM
前后端兼容
前后端逻辑区分
虽然是同构应用, 大部分逻辑是共用的, 但是在服务端和浏览器端具体的实现肯定是不同的, 比如 superagent / isomorphic-fetch 这些库在服务端是使用的 http.request 方法, 而在前端使用 XHR 来实现. 那么在编写代码中, 前端的 url 可以使用相对路径, 而服务端只能使用完整的 http 请求路径.
const fetchStateUrl = __SERVER__ ? `http://localhost:/api/state` : '/api/state'
可以通过 webpack 的 definePlugin 来做, 但是由于我在 development 模式下是通过 node + babel-register + webpack-dev-middleware 直接运行的应用, 对于服务端运行的代码 webpack 就无能为力了. 我写了一个 babel 的插件 https://github.com/wssgcg1213/babel-plugin-inline-replace-variables 来直接对 js 的 identifier 进行替换.
在打包前端 bundle 的时候得益于 uglifyjs 插件的处理, 不可达代码会被清除, 这样也不需要担心 bundle.js 体积增大.
Nodejs require 静态资源文件的处理
react 的 component 中充满了import './component.less', import img from './img.png'
这样的语法, 但在 node 中是会报错的, 因为 isomorphic-tools 的文档太冗杂, 我没有采取它那一套方案, 而是使用了babel-plugin-transform-require-ignore
来忽略 css/less, 转而在 webpack-dev-middleware 中使用 style-loader 打包成 js bundle 输出, 这样也能同时支持 hot module replacement; 对于图片使用asset-require-hook
这个包来使 require 直接返回文件路径.
总之, 开发模式下的服务端对 require 进行了魔改使它支持了引入 ES6+React+Stylesheets+Images 的能力.
总结
搞两个流程图来看看
开发模式:
线上模式:
Example
使用这个模式开发了
https://github.com/CQUPTMirror/mirror-web-isomorphic/
线上地址 https://mirror.cqupt.edu.cn