内容简介:也可以使用基本项目目录安装一些依赖,想尝试 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 服务器端渲染和客户端渲染效果对比
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Data Structures and Algorithm Analysis in Java
Mark A. Weiss / Pearson / 2011-11-18 / GBP 129.99
Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course wa......一起来看看 《Data Structures and Algorithm Analysis in Java》 这本书的介绍吧!