const Koa = require('koa'); const Router = require('koa-router'); const app = new Koa(); const router = new Router(); router.get('*', async (ctx) => { ctx.body = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>React SSR</title> </head> <body> <div></div> <script type="text/javascript" src="https://www.jb51.net/bundle.js"></script> </body> </html> `; }); app.use(router.routes()); app.listen(3000, '0.0.0.0');
上面*路由代表任意的url进来我们都默认渲染这个html,包括html中打包出来的js,你也可以用一些服务端模板引擎(如:nunjucks)来直接渲染html文件,在webpack打包时通过html-webpack-plugin来自动插入打包出来的js/css资源路径。
OK, 我们的简易koa server好了,接下来我们开始编写React SSR的入口文件AppSSR.js,这里我们需要使用StaticRouter来代替之前的BrowserRouter,因为在服务端,路由是静态的,用BrowserRouter的话是不起作用的,后面还会做一些配置来使得react-loadable运行在服务端。
提示: 你可以把整个node端的代码用ES6/JSX风格编写,而不是部分commonjs,部分JSX, 但这样的话你需要用webpack把整个服务端的代码编译成commonjs风格,才能使得它运行在node环境中,这里的话我们把React SSR的代码单独抽出去,然后在普通的node代码里去require它。因为可能在一个现有的项目中,之前都是commonjs的风格,把以前的node代码一次性转成ES6的话成本有点大,但是可以后期一步步的再迁移过去
OK, 现在在你的 AppSRR.js中:
import React from 'react'; //使用静态 static router import { StaticRouter } from 'react-router-dom'; import ReactDOMServer from 'react-dom/server'; import Loadable from 'react-loadable'; //下面这个是需要让react-loadable在服务端可运行需要的,下面会讲到 import { getBundles } from 'react-loadable/webpack'; import stats from '../build/react-loadable.json'; //这里吧react-router的路由设置抽出去,使得在浏览器跟服务端可以共用 //下面也会讲到... import AppRoutes from 'src/AppRoutes'; //这里我们创建一个简单的class,暴露一些方法出去,然后在koa路由里去调用来实现服务端渲染 class SSR { //koa 路由里会调用这个方法 render(url, data) { let modules = []; const context = {}; const html = ReactDOMServer.renderToString( <Loadable.Capture report={moduleName => modules.push(moduleName)}> <StaticRouter location={url} context={context}> <AppRoutes initialData={data} /> </StaticRouter> </Loadable.Capture> ); //获取服务端已经渲染好的组件数组 let bundles = getBundles(stats, modules); return { html, scripts: this.generateBundleScripts(bundles), }; } //把SSR过的组件都转成script标签扔到html里 generateBundleScripts(bundles) { return bundles.filter(bundle => bundle.file.endsWith('.js')).map(bundle => { return `<script type="text/javascript" src="https://www.jb51.net/${bundle.file}"></script>\n`; }); } static preloadAll() { return Loadable.preloadAll(); } } export default SSR;
当编译这个文件的时候,在webpack配置里使用target: "node" 和 externals,并且在你的打包前端app的webpack配置中,需要加入react-loadable的插件,app的打包需要在ssr打包之前运行,不然拿不到react-loadable需要的各组件信息,先来看app的打包:
//webpack.config.dev.js, app bundle const ReactLoadablePlugin = require('react-loadable/webpack') .ReactLoadablePlugin; module.exports = { //... plugins: [ //... new ReactLoadablePlugin({ filename: './build/react-loadable.json', }), ] }
在.babelrc中加入loadable plugin:
{ "plugins": [ "syntax-dynamic-import", "react-loadable/babel", ["import-inspector", { "serverSideRequirePath": true }] ] }
上面的配置会让react-loadable知道哪些组件最终在服务端被渲染了,然后直接插入到html script标签中,并在前端初始化时把SSR过的组件考虑在内,避免重复加载,下面是SSR的打包:
//webpack.ssr.js const nodeExternals = require('webpack-node-externals'); module.exports = { //... target: 'node', output: { path: 'build/node', filename: 'ssr.js', libraryExport: 'default', libraryTarget: 'commonjs2', }, //避免把node_modules里的库都打包进去,此ssr js会直接运行在node端, //所以不需要打包进最终的文件中,运行时会自动从node_modules里加载 externals: [nodeExternals()], //... }
然后在koa app.js, require它,并且调用SSR的方法: