从零开始构建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

    原文作者:devlee
    原文地址: https://segmentfault.com/a/1190000011926131
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞