内容简介:也可以使用基本项目目录安装一些依赖,想尝试 React Hooks,所以安装了 next 版本,服务端用 koa
react-dom/server import { hydrate } from 'react-dom'
也可以使用 babel-core/register
让 React 代码能够运行在服务端,具体参考: segmentfault.com/a/119000001…
新建项目
$ mkdir customize-server-side-render $ cd customize-server-side-render # 初始化一个 package.json $ yarn init -y 复制代码
基本项目目录
|-- 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 ... 复制代码
安装一些依赖,想尝试 React Hooks,所以安装了 next 版本,服务端用 koa
$ yarn add react@next react-dom@next koa koa-router $ yarn add webpack webpack-cli ts-loader typescipt -D 复制代码
首先在 config 下面创建一个 paths.js,声明了有用到的 paths
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'), }; 复制代码
- webpack.base.js
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 配置
$ yarn add webpack-merge -D 复制代码
- webpack.client.js
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,打包出在客户端运行的脚本
打包客户端代码出现问题:
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 引入的库,这里都给他安装一下
$ yarn add object-assign prop-types scheduler -D 复制代码
依然出现上面的问题
猜测可能是没有引入 babel
的原因
最终结果并不是,是由于 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', }, ], }, }; 复制代码
启动 Node 服务
在 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 服务能在修改后自己重启。
编译 React 在服务端
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 commonjs
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\"" 复制代码
利用 HTML 模板文件
在 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'))); 复制代码
- index.html 中引入即可
<script type="text/javascript" src='/app.js'></script> 复制代码
这样子,客户端运行的时候就回去加载 public/app.js
,从而达到客户端激活的目的
- 修改 webpack.client.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, ); 复制代码
添加路由成功!
运用这些写了一个支持 React 服务端渲染的小库: github.com/wokeyi/serv… (随手点个star:grin:)
以上所述就是小编给大家介绍的《TypeScript + Webpack + Koa 搭建 React 服务端渲染》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- React服务端渲染(项目搭建)
- 使用 Puppeteer 搭建统一海报渲染服务
- 利用webpack4搭建vue服务器端渲染SSR(一)
- Octane渲染入门-渲染设置图文版
- 通过分析 WPF 的渲染脏区优化渲染性能
- React 服务器端渲染和客户端渲染效果对比
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。