利用 webpack 打包能在 node 運行的 React 代碼,利用 react-dom/server
將 React 代碼渲染成 html 字符串返回給客戶端javascript
利用 webpack 打包瀏覽器運行的 React 代碼,在客戶端用 import { hydrate } from 'react-dom'
hydrate 激活(添加事件等)css
也可使用 babel-core/register
讓 React 代碼可以運行在服務端,具體參考:segmentfault.com/a/119000001…html
$ mkdir customize-server-side-render
$ cd customize-server-side-render
# 初始化一個 package.json
$ yarn init -y
複製代碼
基本項目目錄java
|-- customize-server-side-render
|-- config webpack 打包配置文件和路徑配置文件
|-- paths 路徑配置文件
|-- webpack.base.js 公用的 webpack 打包配置
|-- webpack.client.js 打包給客戶端使用的腳本
|-- webpack.server.js 打包給 node 使用的腳本
|-- src 源碼
|-- App.tsx
|-- index.tsx 客戶端啓動入口
!-- server.tsx 服務端啓動入口
|-- server koa 啓動 http 服務代碼
|-- public 靜態資源
|-- dist webpack 打包後的文件
|-- package.json
|-- tsconfig.json
|-- tslint.json
...
複製代碼
安裝依賴node
$ yarn add react react-dom koa koa-router
$ yarn add webpack webpack-cli ts-loader typescipt -D
複製代碼
首先在 config 下面建立一個 paths.js,聲明瞭有用到的 pathsreact
const path = require('path');
function resolveResource(filename) {
return path.resolve(__dirname, `../${filename}`);
}
module.exports = {
clientEntry: resolveResource('src/index.tsx'),
serverEntry: resolveResource('src/server.tsx'),
sourceDir: resolveResource('src'),
distDir: resolveResource('dist'),
};
複製代碼
const paths = require('./paths');
module.exports = {
mode: 'development',
output: {
path: paths.distDir,
filename: '[name].js',
},
resolve: {
extensions: ['.ts', '.tsx'],
},
module: {
rules: [
{
test: /\.tsx?/,
loader: 'ts-loader',
exclude: /node_modules/,
}
],
},
};
複製代碼
利用 webpack-merge
合併 webpack 配置webpack
$ yarn add webpack-merge -D
複製代碼
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base');
const paths = require('./paths');
module.exports = merge(baseConfig, {
target: 'web',
entry: paths.clientEntry,
})
複製代碼
運行 webpack --config config/webpack.client.js,打包出在客戶端運行的腳本git
ERROR in ./node_modules/react-dom/cjs/react-dom.development.js
Module not found: Error: Can't resolve 'object-assign' in '/Users/logan/Projects/backend/customize-server-side-render/node_modules/react-dom/cjs' @ ./node_modules/react-dom/cjs/react-dom.development.js 19:14-38 @ ./node_modules/react-dom/index.js @ ./src/index.tsx ... 複製代碼
打包時出現了一些依賴未安裝的問題,是開發版本的 react 引入的庫,這裏都給他安裝一下github
$ yarn add object-assign prop-types scheduler -D
複製代碼
依然出現上面的問題,猜想多是沒有引入 babel
的緣由web
最終結果並非,是因爲 resolve.extensions 中我只配置了 ts 和 tsx 結尾的文件類型,可是沒有 js 和 jsx 結尾的。修改 webpack.base.js
const paths = require('./paths');
module.exports = {
output: {
path: paths.distDir,
filename: '[name].js',
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
},
module: {
rules: [
{
test: /\.tsx?/,
include: paths.sourceDir,
exclude: /node_modules/,
loader: 'ts-loader',
},
],
},
};
複製代碼
在 server
下面建立一個 index.js
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
router.get('/', (ctx) => {
ctx.body = 'Hello world Koa';
});
app.use(router.routes());
app.listen(3000);
console.log('Application is running on http://127.0.0.1:3000');
複製代碼
運行 node server/index.js
,看見服務啓動正常,可是修改了 server
下面的 index.js
沒法本身重啓 node 服務,因此準備利用 nodemon
運行
$ yarn add nodemon -D
複製代碼
修改啓動腳本爲
$ nodemon server/index.js
複製代碼
OK, node 服務能在修改後本身重啓。
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base');
const paths = require('./paths');
module.exports = merge(baseConfig, {
target: 'node',
entry: {
'server-entry': paths.serverEntry,
},
})
複製代碼
將打包後的 server-entry.js
在 server/index.js 中引入, 利用 react-dom/server 模塊中的 renderToString 方法渲染成 html
const ReactDOMServer = require('react-dom/server');
const serverEntry = require('../dist/server-entry');
const str = ReactDOMServer.renderToString(serverEntry);
複製代碼
可是發現 require 進來的 serverEntry 只是一個空對象。
webpack-node-externals
插件,webpack 將不打包 path, fs 等原生 node 模塊下面的模塊commonjs
,webpack.server.js 以下:const merge = require('webpack-merge');
const nodeExternals = require('webpack-node-externals');
const baseConfig = require('./webpack.base');
const paths = require('./paths');
module.exports = merge(baseConfig, {
target: 'node',
entry: {
'server-entry': paths.serverEntry,
},
output: {
libraryTarget: 'commonjs',
},
externals: [nodeExternals()],
})
複製代碼
而後 require server-entry 的方式變爲:
const serverEntry = require('../dist/server-entry').default;
複製代碼
而後就能夠看見瀏覽器上顯示出了 的內容,可是每次運行都要 yarn dev:client
、yarn dev:server
、yarn dev
,並且還不能用 &&
鏈接,由於 yarn dev:client
中 webpack --watch
會卡在當前進程,因此能夠用 npm-run-all
一次運行三個腳本
$ yarn add npm-run-all -D
複製代碼
最終啓動腳本變爲:
"start": "npm-run-all --parallel \"dev\" \"dev:client\" \"dev:server\""
複製代碼
在 public
下面新建一個 index.html
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Customize Server side render</title>
</head>
<body>
<!-- 服務端會替換 <slot />,也可使用 koa-views 等插件實現 -->
<div class="app-container"><slot /></div>
</body>
</html>
複製代碼
修改 server/index.js
內容:
const Koa = require('koa');
const Router = require('koa-router');
// 新增
const fs = require('fs');
const path = require('path');
const ReactDOMServer = require('react-dom/server');
const serverEntry = require('../dist/server-entry').default;
const app = new Koa();
const router = new Router();
// 新增
const template = fs.readFileSync(path.resolve(__dirname, '../public/index.html'), 'utf8');
router.get('*', (ctx) => {
// 新增
const str = ReactDOMServer.renderToString(serverEntry);
ctx.body = template.replace('<slot />', str);
ctx.type = 'html';
});
app.use(router.routes());
app.listen(3000);
console.log('Application is running on http://127.0.0.1:3000');
複製代碼
可是 react-dom/server
模塊只是將 jsx 渲染成 html,可是他沒有 document 等 html 元素,因此他並無綁定點擊事件等,因此須要將代碼在瀏覽器端再運行一遍(瀏覽器激活)
將瀏覽器再運行一次的原理就是,將 webpack.client.js
的 output 中 path 設置爲 public 目錄,而後將 public 目錄設置爲 koa 中的靜態資源目錄。
public
設置爲靜態資源目錄const koaStatic = require('koa-static');
const app = new Koa();
// 這句必定要在 router.get('*') 以前,否則請求到 router.get('*') 中直接返回了,不會再找 public 中的靜態資源
app.use(koaStatic(path.resolve(__dirname, '../public')));
複製代碼
<script type="text/javascript" src='/app.js'></script>
複製代碼
這樣子,客戶端運行的時候就回去加載 public/app.js
,從而達到客戶端激活的目的
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base');
const paths = require('./paths');
module.exports = merge(baseConfig, {
target: 'web',
entry: {
app: paths.clientEntry,
},
output: {
// 指向 public 目錄
path: paths.publicDir,
},
});
複製代碼
可是,這樣子訪問 http://localhost: 3000
時,他走的不是 router.get('/')
, 而是 public/index.html,這個有不少種方式解決,好比修改 public/index.html
-> public/template.html
等。
安裝依賴
$ yarn add style-loader css-loader scss-loader node-sass -D
複製代碼
客戶端打包沒問題,可是 style-loader 須要 window 對象,可是 webpack.server.js
是打包給 node 用的,沒有 window ,會報錯
webpack:///./node_modules/style-loader/lib/addStyles.js?:23
return window && document && document.all && !window.atob;
^
ReferenceError: window is not defined
at eval (webpack:///./node_modules/style-loader/lib/addStyles.js?:23:2)
at eval (webpack:///./node_modules/style-loader/lib/addStyles.js?:12:46)
at module.exports (webpack:///./node_modules/style-loader/lib/addStyles.js?:80:88)
at eval (webpack:///./src/components/Container/style.scss?:16:140)
at Object../src/components/Container/style.scss (/Users/logan/Projects/backend/customize-server-side-render/dist/server-entry.js:165:1)
at __webpack_require__ (/Users/logan/Projects/backend/customize-server-side-render/dist/server-entry.js:20:30)
at eval (webpack:///./src/components/Container/index.tsx?:4:69)
at Module../src/components/Container/index.tsx (/Users/logan/Projects/backend/customize-server-side-render/dist/server-entry.js:154:1)
at __webpack_require__ (/Users/logan/Projects/backend/customize-server-side-render/dist/server-entry.js:20:30)
at eval (webpack:///./src/App.tsx?:6:79)
複製代碼
因此將樣式 loader 拆開,在 webpack.server.js
中用 isomorphic-style-loader 代替 style-loader
服務端渲染時,不能使用 BrowswerRouter
或者 HashRouter
,而是 StaticRouter
,參考地址:
能夠看到,StaticRouter
須要用到請求參數中的 path
甚至 context
,所以須要對結構作一些改變,讓 node 啓動的入口直接引入 <App />
,而不是經過 require
加載 webpack 打包過的
src
下面新建 server
目錄,新建 index.tsx
,這樣服務端的內容也可以使用 typescript
把 server/index.js
內容轉入 src/server/index.tsx
,安裝 @types/node
本來用 require
引入的方式都改成 import
修改 paths
下面的 serverEntry
,修改 src/server/index.tsx
下面引用的文件路徑,利用 typescript
之後,路勁引用就不用 path.resolve(__dirname, 'path/to/file')
,直接項目目錄下文件夾開始就行,若是引用 project/public
下面的 public
目錄,直接 public 便可。
修改後的 src/server/index.tsx
爲:
import * as React from 'react';
import * as fs from 'fs';
import Koa from 'koa';
import Router from 'koa-router';
import koaStatic from 'koa-static';
import * as ReactDOMServer from 'react-dom/server';
import App from '../App';
const app = new Koa();
const router = new Router();
const template = fs.readFileSync('public/template.html', 'utf8');
app.use(koaStatic('public', {
gzip: true,
maxage: 10,
}));
router.get('*', (ctx) => {
const str = ReactDOMServer.renderToString(<App />); ctx.body = template.replace('<slot />', str); ctx.type = 'html'; }); app.use(router.routes()); app.listen(3000); console.log('Application is running on http://127.0.0.1:3000'); 複製代碼
修改 renderToString
的過程
const str = ReactDOMServer.renderToString(
<StaticRouter location={ctx.req.url} context={{}}> <App /> </StaticRouter>
);
複製代碼
這是服務端添加了 Router
,可是這樣子直接運行的話,瀏覽器會報錯:
You should not use <Route> or withRouter() outside a <Router>
複製代碼
這是由於服務端添加了 StaticRouter
,可是客戶端外層卻並無添加一個 Router
修改 src/index.tsx
import * as React from 'react';
import { hydrate } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
hydrate(
(
<BrowserRouter> <App /> </BrowserRouter>
),
document.querySelector('.app-container') as HTMLElement,
);
複製代碼
添加路由成功!