从零开始搭建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框架已经搭建完成,剩下的工作就是结合工作需要,在里面添砖加瓦啦。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。