从零开始搭建React同构应用(四):搭建Koa Server & 完善SSR
从零开始搭建React同构应用(四):搭建Koa Server & 完善SSR
上一篇我们使用了CLI的方式测试了SSR,这篇文章来讲如何在前文的基础上搭建一个Koa Server,实现真正意义上的SSR。
主要内容
Koa搭建
完善SSR逻辑
Koa搭建
我们使用Koa v2.0的版本;
npm i koa@next -S;
先搭建一个最简单的服务器
const Koa = require("koa"); const app = new Koa(); app.use(ctx => { ctx.body = 'Hello Koa'; }); app.listen(8088, _ => { console.log('server started') });
添加一个npm script
"scripts": { "start": "node --harmony server/index", //启动HTTP服务器 "watch": "webpack -d -w --progress --colors --bs", "test-server": "anywhere -p 18341 -d ./build", "dist": "cross-env NODE_ENV='production' webpack -p", "test-ssr": "node --harmony test/cli.js" },
执行
npm run start
这样一个最简单的Koa框架就搭建起来,下面就可以往里面填充东西了。
配置router
在添加router之前,我们需要加载webpack编译生成的HTML模板,这里我们没有使用EJS
,HBS
等Nodejs渲染引擎,我们而是使用cheerio来帮助我们操作HTML,cheerio
可以让我们在Node环境下像使用jQuery
一样来操作HTML,非常容易上手,这里是它的API,基本和jQuery无差别。
在server/index.js增加:
const cheerio = require("cheerio"); const fs = require("fs"); const path = require("path"); const Promise = require("bluebird"); const serve = require('koa-static-server'); const readFileAsync = Promise.promisify(fs.readFile); /** * 读取HTML模版,返回cheerio实例 * @param path * @return {Promise.<*>} */ async function loadHTMLTemplate(path) { try { let content = await readFileAsync(path); return cheerio.load(content); } catch (e) { console.error(e); return false; } }
我们使用koa-better-router中间件作为路由模块。我们添加一个router,在server/index.js增加:
const router = require('koa-better-router')().loadMethods(); router.get('/', async(ctx, next) => { let $ = await loadHTMLTemplate(path.resolve(__dirname, '../build/index.html')); if (!$) { return ctx.body = null; } return ctx.body = $.html(); }); app.use(router.middleware());
执行
npm run start
我们会发现CSS,JS等文件没有被加载进来,因为没有对应的路由,下面我们配置静态文件服务。
配置静态文件服务
我们不可能为所有的资源都写router,因此我们需要配置一个静态文件服务。这里我使用了koa-static-server中间件。
我们以build
目录作为资源文件根目录,在server/index.js增加:
const serve = require('koa-static-server'); const readFileAsync = Promise.promisify(fs.readFile); const RES_PATH = path.resolve(__dirname, '../build/'); //hfs app.use(serve({rootDir: RES_PATH}));
执行
npm run start
资源可以被正确载入了。
完善SSR逻辑
我们先添加一个API接口,方便模拟Node端的接口调用,在server/index.js增加:
//API接口 router.get('/api/todo_list', async(ctx, next) => { return ctx.body = ['11', '222']; });
我们还是以Index.jsx
为例:
将test/cli.js
中的代码copy过来。修改/
路由
const Koa = require("koa"); const app = new Koa(); const router = require('koa-better-router')().loadMethods(); const cheerio = require("cheerio"); const fs = require("fs"); const path = require("path"); const Promise = require("bluebird"); const serve = require('koa-static-server'); const readFileAsync = Promise.promisify(fs.readFile); const RES_PATH = path.resolve(__dirname, '../build/'); const fetch = require("isomorphic-fetch"); router.get('/', async(ctx, next) => { let $ = await loadHTMLTemplate(path.resolve(__dirname, '../build/index.html')); if (!$) { return ctx.body = null; } let IndexBundle = require("../build_server/index.bundle.js"); //fetch接口数据 let todoList = await(await fetch('http://localhost:8088/api/todo_list')).json(); let initialData = {todoList}; let instance = React.createElement(IndexBundle.default, initialData); let str = renderToString(instance); $('#wrap').html(str); //前后端数据要同步 let syncScript = `<script id="server-data">window._SERVER_DATA=${JSON.stringify(initialData)}</script>`; $('head').append(syncScript); return ctx.body = $.html(); });
这里要注意前后端数据要同步,我把Node端获取的数据放在window._SERVER_DATA
中了,前端渲染的时候会优先使用window._SERVER_DATA
来渲染。
if (process.browser) { //初始数据,用于和server render数据同步 let initialData = window._SERVER_DATA || {}; let store = createStore(reducers, initialData, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); let App = connect(_ => _)(Layout);//用connect包装一下,这里只用到mapStateToProps,而且不对state加以过滤 ReactDOM.render( <Provider store={store}> <App/> </Provider>, document.getElementById('wrap')); }
执行
npm run start
访问http://127.0.0.1:8088/
可以看到#wrap
中已经被填充渲染好的HTML文本了,Node端和前端的数据也同步了。 ^_^
至此,一个简单的SSR框架已经搭建完成,剩下的工作就是结合工作需要,在里面添砖加瓦啦。