前言
开发的时候,为了更好的体验,我们往往会希望修改完代码,按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