从零开始构建react应用(七)代码修改自动应用
前言
开发的时候,为了更好的体验,我们往往会希望修改完代码,按ctrl+s保存代码后,浏览器就呈现出我们最新的修改结果。利用目前的一些工具,我们可以实现这样的效果,本文主要讲解客户端代码修改后热重载
、服务端代码修改后重启node进程
和node进程重启后自动刷新浏览器
这三部分的具体实现过程。
客户端热重载
我们利用webpack来实现客户端热重载的功能。
安装依赖
以下依赖后续会使用到:
nom install react-hot-loader webpack-hot-middleware @types/react-hot-loader @types/webpack-hot-middleware @types/webpack-env
PS: @types/webpack-env主要用于给webpack里诸如module,require这些变量进行类型补充定义,而不是默认使用node端的定义
创建基于koa的hot中间件
和dev中间件一样,webpack-hot-middleware中间件也需要经过改造以用于koa:
// ./src/webpack/koa-webpack-hot-middleware.ts import * as Koa from 'koa'; import * as webpack from 'webpack'; import * as webpackHotMiddleware from 'webpack-hot-middleware'; import * as stream from 'stream'; export default (compiler: webpack.Compiler, opts?: webpackHotMiddleware.Options) => { const hotMiddleware = webpackHotMiddleware(compiler, opts); return (ctx: Koa.Context, next: () => Promise<any>): any => { const streamer = new stream.PassThrough(); ctx.body = streamer; const res: any = {}; res.write = streamer.write.bind(streamer); res.writeHead = (state: number, headers?: any) => { ctx.state = state; ctx.set(headers); }; return hotMiddleware(ctx.req, res, next); }; };
这里主要是修改了res的两个方法,使得原hot中间件调用res的这两个方式时,可以作用到ctx对象的相关属性上去。
现在来应用它:
./src/webpack/webpack-dev-server.ts ... import koaWebpackHotMiddleware from './koa-webpack-hot-middleware'; ... export default (app: Koa, serverCompilerDone) => { ... app.use(koaWebpackDevMiddleware(clientCompiler, devMiddlewareOptions)); app.use(koaWebpackHotMiddleware(clientCompiler)); ... }; ...
需要在dev中间件后使用。
修改clientDevConfig
按照官方介绍,使用webpack-hot-middleware的话,我们需要在webpack配置上做一些改动:
./src/webpack/client.ts ... ((clientDevConfig.entry as any).client as string[]).unshift( 'webpack-hot-middleware/client', ); // 热重载配置 ((clientDevConfig.entry as any).vendor as string[]).unshift( 'react-hot-loader/patch', ); // 热重载配置 ... const tsRule = getTsRule('./src/webpack/tsconfig.client.json'); (tsRule.use as object[]).unshift({ loader: 'react-hot-loader/webpack', }); ... (clientDevConfig.module as webpack.NewModule).rules.push( ... tsRule, ... ); ... clientDevConfig.plugins.push( ... new webpack.HotModuleReplacementPlugin(), // 热重载配置 ... ); ...
主要涉及入口文件,规则,插件这三部分内容。
给根节点包裹AppContainer
根据react-hot-loader官方介绍,我们需要给根节点包裹其提供的AppContainer组件,介于同构,所以前后端都需要加。
这个是服务端的:
./src/server/bundle.tsx ... import { AppContainer } from 'react-hot-loader'; ... export default { ... render() { ... const html = renderToString( <AppContainer> <AppProvider context={context}> <AppContent /> </AppProvider> </AppContainer>, ); ... }, ... }; ...
客户端的稍微复杂一些,因为涉及热重载所以renderApp函数需要做些改造,从无参改为接收组件作为参数:
./src/client/index.tsx ... import { AppContainer } from 'react-hot-loader'; ... function renderApp(Comp) { ReactDOM.hydrate( <AppContainer warnings={false}> <Comp /> </AppContainer>, document.getElementById('app'), ); } ... window.onload = () => { renderApp(App); if (module.hot) { module.hot.accept('./component/app', () => renderApp(require('./component/app').default)); } }; ...
这样,我们就实现了客户端的热重载效果,打开浏览器,我们修改AppContent组件里的hello world字符串,会发现浏览器无刷新的呈现了最新结果,修改样式文件也会实时应用变更内容。
服务端自动重启
利用nodemon,可以很方便的实现重启node进程。
安装依赖
npm install nodemon --save-dev
配置nodemon
我们在根目录下新建nodemon的配置文件nodemon.json:
./nodemon.json { "watch": [ "./dist/config", "./dist/server", "./dist/webpack" ] }
我们监听dist目录下的三个文件夹,这三个文件夹内容涉及服务端代码。一旦我们修改了ts文件,ts编译成的js就会发生相应的修改,从而被nodemon监听到。
修改我们的启动命令:
{ ... "scripts": { ... "dev": "nodemon ./dist/server/index.js", ... }, ... }
将node修改为nodemon即可。
这样就实现了修改服务端代码,自动重启了。
浏览器自动刷新
虽然按照上述方法,我们实现了服务端自动重启,但是我们已经打开浏览器并感知不到服务端重启这个事件,我们想要实现感知可以利用webpack-hot-middleware(下简称whm)来实现。
原理解析
从浏览器控制台的打印信息可以看到,当服务端重启,whm客户端会丢失链接,并定时重新尝试链接,直到成功。我们可以利用这一特性来实现浏览器自动刷新,我们在服务端启动时设置一个hmrKey值,并在服务端bundle完成后通过whm的publish方法向浏览器定时推送该值,浏览器则进行监听,将该值存于本地,一旦服务端重启,hmrKey改变,浏览器接收到新的hmrKey值则进行刷新页面操作。
输出whm实例
我们自己写的koa-webpack-hot-middleware输出的是一个function,现在我们要利用whm的实例,所以我们将其作为function的属性一并输出。
// ./src/webpack/koa-webpack-hot-middleware.ts ... export default (...) => { ... const koaWebpackHotMiddleware = (...) => { ... }; ... (koaWebpackHotMiddleware as any).hotMiddleware = hotMiddleware; ... return koaWebpackHotMiddleware; ... }; ...
将whm实例作为serverCompilerDone回调参数
./src/webpack/webpack-dev-server.ts ... export default (...) => { ... let hotMiddleware; ... clientCompiler.plugin('done', () => { ... serverCompiler.plugin('done', () => serverCompilerDone.call(null, hotMiddleware)); ... }; ... const koaWebpackHotMiddlewareObject = koaWebpackHotMiddleware(clientCompiler, { heartbeat: 1000, }); ... hotMiddleware = (koaWebpackHotMiddlewareObject as any).hotMiddleware; ... app.use(koaWebpackHotMiddlewareObject); ... }; ...
我们先定义whm实例变量名,配置完clientCompiler后,再利用其获得whm实例值,最后给whm实例变量赋值。
hmrKey发送
在入口处设定,并通过whm实例发送给浏览器:
// ./src/server/index.ts ... const hmrKey = Math.random() * 100000 + ''; let hmrKeyT; ... if (isDev) { ... webpackDevServer(app, (hotMiddleware) => { ... if (hotMiddleware && typeof hotMiddleware.publish === 'function') { global.clearInterval(hmrKeyT); hmrKeyT = global.setInterval(() => { hotMiddleware.publish({ action: 'bundled', hmrKey }); }, 1000); } ... }); // 仅在开发环境使用 ... } ...
hmrKey接收
客户端入口进行监听逻辑:
// ./src/client/index.tsx ... window.onload = () => { ... if (module.hot) { ... let hmrKey; ... /* tslint:disable no-submodule-imports */ const hotClient = require('webpack-hot-middleware/client'); ... hotClient.subscribe((e) => { if (e.action === 'bundled') { if (hmrKey && (hmrKey !== e.hmrKey)) { window.location.reload(); } else { hmrKey = e.hmrKey; } } }); ... } }; ...
修改重连等待时长
根据官方建议该值为心跳时长两倍,上面我们设置1秒一次心跳,这里我们设置为两秒:
// ./src/webpack/client.ts ... ((clientDevConfig.entry as any).client as string[]).unshift( 'webpack-hot-middleware/client?timeout=2000', ); // 热重载配置 ...
其它解决方案
业界常用的有browser-sync,我也尝试了一下,但是对于我们这个koa+webpack体系的app并不是太适合,它依赖express,还得额外加gulp来进行流程控制,进行一大堆繁杂的配置,另外它的功能实在太强大,所以这里我就自己想了上面这个解决方案。
Thanks
By devlee