优化单页面开发环境:webpack与react的运行时打包与热更新

这是Webpack+React系列配置过程记录的第三篇。其他内容请参考:

这篇文章将基于前面两篇文章进一步优化开发环境,实现单页面开发时的运行时打包与热更新。

调整文件布局

在第二篇文章中发现了框架代码文件的命名有些冲突,这里我们需要做一下调整,以便接下来的讲述不易出现问题。调整时需要小小地改动配置文件几个路径。文件布局调整前后对比如下:

项目布局调整

图片基本已经说明了情况。我们将在src目录下开发代码,而编译后的代码将存放在public目录中。开发过程中,我们使用server.js配置的服务器进行测试。

接下来开始本文的正题。

配置运行时打包

前面两篇文章中,我们每次改动代码都需要使用下面两条命令

npm run build
npm start

编译和运行代码。这让每次build都需要输入这么多字;而且每次都需要扫描所有文件,效率十分低。

所以这次我们要配置运行时打包,只要测试服务器启动后,就可以让每次改动的内容都被webpack监测到并且自动打包。webpack-dev-middleware这个express的中间件可以实现该需求。

安装

安装webpack-dev-middleware:

npm install --save-dev webpack-dev-middleware
配置与启用webpack-dev-middleware

这是express的中间件,因此需要配置测试服务器端的代码server.js:

var express = require('express');
var app = express();

app.use('/', require('connect-history-api-fallback')());
app.use('/', express.static('public'));

if (process.env.NODE_ENV !== 'production') {
  var webpack = require('webpack');
  var webpackConfig = require('./webpack.config.js');
  var webpackCompiled = webpack(webpackConfig);
  // 配置运行时打包
  var webpackDevMiddleware = require('webpack-dev-middleware');
  app.use(webpackDevMiddleware(webpackCompiled, {
    publicPath: "/",
    stats: {colors: true},
    lazy: false,
    watchOptions: {
        aggregateTimeout: 300,
        poll: true
    },
  }));
}

var server = app.listen(2000, function() {
  var port = server.address().port;
  console.log('Open http://localhost:%s', port);
});

server.js把webpack和express连接到了一起实现了运行时打包。我这里简单使用了webpack-dev-middleware的几个配置项:

  • publicPath:这个插件的唯一必填项。由于index.html请求的out.js存放的位置映射到服务器的URI路径是根,即“/”,所以我赋予了publicPath为:“/”。
  • stats:我设置了console统计日志带颜色输出。
  • lazy:指示是否懒人加载模式。true表示不监控源码修改状态,收到请求才执行webpack的build。false表示监控源码状态,配套使用的watchOptions可以设置与之相关的参数。

还有其他配置项,可以通过官网查阅按需配置。

接下来,我们需要删除之前使用npm run build命令生成的out.js。否则在验证效果时,由于server.js中静态服务器的static中间件优先捕获到关于out.js的请求,将直接返回结果给客户端,导致看不到运行时打包的效果。

那么index.html引用的out.js文件是哪里来的呢?就是webpack-dev-middleware这个中间件利用缓存方式生成的。

验证

使用npm start命令启动服务器,在浏览器访问index.html,可以看到页面正常显示。

修改src/index.js文件中的内容并保存。这时服务器后台执行自动打包,可以看到控制台输出了打包的日志,并不需要你再花时间敲那两行代码了。手动刷新浏览器页面就可以看到刚刚改动的内容。这告诉我们服务器已经可以实现运行时加载。

配置热更新

我们会注意到每次改动后还是需要我们刷新浏览器页面才能看到结果,还是未能让人满意。这时候可以配置热更新,让浏览器自动刷新页面。

热更新利用到的是名叫webpack-hot-middleware的依赖。它提供了用于express的中间件用于建立连接和传输更新;也提供了webpack的插件用于生成更新内容;同时还提供了用户端接口用于嵌入到js脚本中用于与express建立连接和应用更新。更详细的原理描述可以参考这里

我们需要根据这几个方面嵌入webpack-hot-middleware到我们的开发框架中。

安装

使用下面命令安装:

npm install --save-dev webpack-hot-middleware
配置服务器端

改动server.js文件,在express中增加一个中间件即可,改动后如下:

var express = require('express');
var app = express();

app.use('/', require('connect-history-api-fallback')());
app.use('/', express.static('public'));

if (process.env.NODE_ENV !== 'production') {
  var webpack = require('webpack');
  var webpackConfig = require('./webpack.config.js');
  var webpackCompiled = webpack(webpackConfig);
  // 配置运行时打包
  var webpackDevMiddleware = require('webpack-dev-middleware');
  app.use(webpackDevMiddleware(webpackCompiled, {
    publicPath: "/",
    stats: {colors: true},
    lazy: false,
    watchOptions: {
        aggregateTimeout: 300,
        poll: true
    },
  }));

  // 配置热更新
  var webpackHotMiddleware = require('webpack-hot-middleware');
  app.use(webpackHotMiddleware(webpackCompiled));
}

var server = app.listen(2000, function() {
  var port = server.address().port;
  console.log('Open http://localhost:%s', port);
});
在webpack中应用插件

修改webpack.config.js文件:

var path = require('path');
var webpack = require('webpack');

module.exports = {
  entry: ['webpack-hot-middleware/client', './src/index.js'],
  output: {
    filename: 'out.js',
    path: path.resolve(__dirname, 'public')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['env', 'stage-0', 'react'],
            plugins: [['import', {"libraryName": "antd", "style": "css"}]]
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ],
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  ]
};

注意改动中首先引入了webpack对象,然后修改了entry节点,最后添加了两个插件。这里两个插件中,webpack.HotModleReplacementPlugin是关于热更新的,webpack.NoEmitOnErrorsPlugin可以保证出错时页面不阻塞,且会在编译结束后报错。

前端脚本中配置热更新处理逻辑

热更新的处理逻辑webpack已经封装好了,只要在应用的入口文件中添加以下代码

...
if (module.hot) {
  module.hot.accept();
}

即可。我配置的是src/index.js。

验证

npm start启动服务器,浏览器访问index.html。页面显示正常,打开开发者工具可以看到发送了一个叫__webpack_hmr的请求(请求路径可以配置,我们使用了默认值)。

修改src/index.js中的某个内容并保存,将会看到控制台输出了打包日志,然后浏览器页面自动更新页面内容。效果如下:

热更新对比图

到这里热更新配置完毕。

让热更新后保留React的组件状态

React组件的状态对热更新有什么影响?我们先来看下面的一个例子。

在src目录下添加Counter.js文件,内容如下:

import React from 'react';

const COUNT_STEP = 1;

export default class Counter extends React.Component {

  constructor(props) {
    super(props);
    this.state = {value: 1};
  }

  componentDidMount() {
    this.timeout = setTimeout(this.handleTimeoutEvent.bind(this), 1000);
  }

  componentWillUnmount() {
    this.timeout && clearTimeout(this.timeout);
  }

  handleTimeoutEvent() {
    this.setState((prevState, props) => ({
      value: prevState.value + COUNT_STEP
    }), () => {
      this.timeout = setTimeout(this.handleTimeoutEvent.bind(this), 1000);
    });
  }

  render() {
    return (
      <div>
        <p> This is a counter: {this.state.value} </p>
      </div>
    );
  }
}

Counter.js定义了一个React组件,这个组件拥有一个状态值叫value,初始值为1。实际上,React组件的状态指的是存储在组件的成员变量state中的内容,value不过是我们测试的一个实例。

在组件挂在的时候建立了一个计时器,每秒钟增加以下value的值,增加量为COUNT_STEP。

然后我们修改一下index.js文件,修改内容如下:

...
import Counter from './Counter';

const BasicExample = () => (
  <Router>
    <div>
      <ul>
        <li><Link to="/">Home111</Link></li>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/topics">Topics</Link></li>
        <li><Link to="/counter">Counter</Link></li>
      </ul>

      <hr/>

      <Route exact path="/" component={Home}/>
      <Route path="/about" component={About}/>
      <Route path="/topics" component={Topics}/>
      <Route path="/counter" component={Counter}/>
    </div>
  </Router>
)
...

重新启动服务器,使用浏览器访问index.html。点击链接Counter页面显示了我们定义的Counter组件,发现内容逐步在递增1。

修改Counter.js文件中的COUNT_STEP为10,浏览器因为热更新而更新了页面,但是我们会发现Counter组件的状态值会被重置为1,然后重新开始递增10。

这是个小问题。但是放大这个问题到其他场景下,我们可以猜测,如果热更新后页面刷新了,那更新前的状态会被重置,更新前被打断的业务逻辑也无法继续,这明显是个bug。

解决这个问题可以使用react-hot-loader。

安装react-hot-loader

使用下面命令安装,官方文档强调要增加@next指定版本。我不太理解为什么。安装后看到添加的版本是3.0.0-beta.6

npm install --save-dev react-hot-loader@next
配置webpack使用react-hot-loader

需要修改webpack.config.js文件。

注意基于webpack2和react-hot-loader3的配置方式跟旧版本有所不同。我在旧的配置方式上被坑了很久,看这里才解决问题。

修改后的内容:

var path = require('path');
var webpack = require('webpack');

module.exports = {
  entry: [
    'react-hot-loader/patch',
    'webpack-hot-middleware/client',
    './src/index.js'
  ],
  output: {
    filename: 'out.js',
    path: path.resolve(__dirname, 'public')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['env', 'stage-0', 'react'],
            plugins: [
              ['react-hot-loader/babel'],
              ['import', {"libraryName": "antd", "style": "css"}]
            ]
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ],
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  ]
};
配置前端使用react-hot-loader

这里有个坑,且看我直接修改index.js文件:

...
import { AppContainer } from 'react-hot-loader';
import Counter from './Counter';
...
//ReactDOM.render(<BasicExample/>, document.getElementById('main'));
ReactDOM.render(
  <AppContainer>
    <BasicExample/>
  </AppContainer>,
  document.getElementById('main')
);
...

启动服务器,访问index.html,发现控制台出现下面错误:

react-hot-loader配置错误

提示告诉我们:不能在index.js中直接定义组件,然后又用AppContainer封装组件。方法很简单,把BasicExample抽离出来定义就可以了。

src目录下创建BasicExample.js文件,做一下简单的修改,内容如下:

import React from 'react';
import {
  BrowserRouter as Router,
  Route,
  Link
} from 'react-router-dom';
import Counter from './Counter';

export default class BasicExample extends React.Component {
  render() {
    return (
      <Router>
        <div>
          <ul>
            <li><Link to="/">Home122</Link></li>
            <li><Link to="/topics">Topics</Link></li>
            <li><Link to="/counter">Counter</Link></li>
          </ul>
          <hr/>
          <Route exact path="/" component={Home}/>
          <Route path="/topics" component={Topics}/>
          <Route path="/counter" component={Counter}/>
        </div>
      </Router>
    );
  }
}

const Home = () => (
  <div>
    <h2>Home</h2>
  </div>
)
const Topics = ({ match }) => (
  <div>
    <h2>Topics</h2>
    <ul>
      <li>
        <Link to={`${match.url}/props-v-state`}>
          Props v. State
        </Link>
      </li>
    </ul>

    <Route path={`${match.url}/:topicId`} component={Topic}/>
    <Route exact path={match.url} render={() => (
      <h3>Please select a topic.</h3>
    )}/>
  </div>
)
const Topic = ({ match }) => (
  <div>
    <h3>{match.params.topicId}</h3>
  </div>
)

index.js文件修改为:

import React from 'react';  // 必须引入
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import BasicExample from './BasicExample';

ReactDOM.render(
  <AppContainer>
    <BasicExample/>
  </AppContainer>,
  document.getElementById('main')
);

if (module.hot) {
  module.hot.accept();
}

注意尽管index.js中没有使用直接到React,我们仍必须引入React,不然会报错。猜测是后面引入的内容间接使用到了它。

验证

设置Counter.js中的COUNT_STEP为1。重新启动服务器,浏览器访问index.html,点击切换到counter页面,可以看到页面数值在递增1。

修改COUNT_STEP为10,看到页面数值没有重置为1,而是直接在原来的数值上递增10。说明组件状态没有被重置。

完毕。

本文来自作者同步博客

源码下载地址:https://pan.baidu.com/s/1qYnYk5A

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容