上一篇咱們使用了CLI的方式測試了SSR,這篇文章來說如何在前文的基礎上搭建一個Koa Server,實現真正意義上的SSR。html
demo在這前端
主要內容node
Koa搭建react
完善SSR邏輯webpack
新建server/index.js:git
咱們使用Koa v2.0的版本;github
npm i koa@next -S;
先搭建一個最簡單的服務器web
const Koa = require("koa"); const app = new Koa(); app.use(ctx => { ctx.body = 'Hello Koa'; }); app.listen(8088, _ => { console.log('server started') });
添加一個npm scriptnpm
"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" },
執行json
npm run start
這樣一個最簡單的Koa框架就搭建起來,下面就能夠往裏面填充東西了。
在添加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
資源能夠被正確載入了。
咱們先添加一個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框架已經搭建完成,剩下的工做就是結合工做須要,在裏面添磚加瓦啦。