这是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。页面显示正常,打开开发者工具可以看到发送了一个叫_webpackhmr的请求(请求路径可以配置,我们使用了默认值)。

修改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({value: this.state.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,发现控制台出现下面错误:

提示告诉我们:不能在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。说明组件状态没有被重置。

完毕。

同步博客原文链接

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

  1. webpack 配置react脚手架(二):热更新

    下面继续配置 webpack dev server    hot module replacement: 首先配置dev-server     安装     npm i webpack-dev-ser ...

  2. 详解H5中的history单页面,手动实现单页面开发,细说h5单页面原理

    就目前来看,前端的单页面开发占了很大一部分,一方面无刷新的切换增强了体验,并且浏览器记录依然存在,前进后退都没问题,在之前我们通地址栏中的hash改变来触发onhashchange方法来实现单页面应用 ...

  3. vue多页面与单页面开发的区别。

    进入一家新的公司,要开发移动端app项目,前端技术选型时前端组长选的是vue的多页面开发,当时很蒙,vue不是单页面开发吗?咋出来多页面的.接触之后才发现确实存在也挺简单的,省去了路由表的配置.那就给 ...

  4. Windows Phone 8初学者开发—第14部分:在运行时绑定到真实的数据

    原文 Windows Phone 8初学者开发—第14部分:在运行时绑定到真实的数据 第14部分:在运行时绑定到真实的数据 原文地址: http://channel9.msdn.com/Series/ ...

  5. vue搭建多页面开发环境

    自从习惯开发了单页面应用,对多页面的页面间的相互跳转间没有过渡效果.难维护极度反感.但是最近公司技术老大说,当一个应用越来越大的时候单页面模式应付不来,但是没讲怎么应付不来,所以还得自己去复习一遍这两 ...

  6. vue.js多页面开发环境搭建

    利用 vue-cli 搭建的项目大都是单页面应用项目,对于简单的项目,单页面就能满足要求.但对于有多个子项目的应用,如果创建多个单页面,显示有点重复,特别是 node_modules 会有多份相同的. ...

  7. Idea开发环境中搭建Maven并且使用Maven打包部署程序

    1.配置Maven的环境变量 a.首先我们去maven官网下载Maven程序,解压到安装目录,如图所示: b.配置M2_HOME的环境变量,然后将该变量添加到Path中 备注:必须要有JAVA_HOM ...

  8. 关于开发环境中的消息在download时没有下载下来时的问题

    业务场景:在开发环境改了一些代码,现在需要将这些代码(包括class和数据库对象)移植到开发环境,整理出了Objectlist(就是该模块定义了哪些数据库对象),然后上传到FTP服务器上时,再执行do ...

  9. webpack学习:uni运行时代码解读一 (页面初始化加载)

    uni的vue代码是如何在微信小程序里面执行的,对此比较感兴趣所以去调试学习了一波. 准备工作 // 在vue.config.js里打开非压缩的代码 module.exports = { config ...

随机推荐

  1. iOS网络编程笔记——Socket编程

    一.什么是Socket通信: Socket是网络上的两个程序,通过一个双向的通信连接,实现数据的交换.这个双向连路的一端称为socket.socket通常用来实现客户方和服务方的连接.socket是T ...

  2. 浅谈!SQL语句中LEFT JOIN ON WHERE和LEFT JOIN ON AND的区别

    今天的工作学习之路是一个数据库的小知识,当时没有区分出所以然,特此记录分享一下子. 众所周知,数据库的表都是单独存在的,但是当我们进行联合查询(多表查询)时,我们获得数据库返回的值时就好像在一张表里一 ...

  3. C#实现不影响当前线程情况下间隔一定的时间执行一段代码

    大家知道C#间隔一定时间去执行一段代码,常用的有 1. Thread.Sleep(多少毫秒); 2. 使用Timer控件间隔一定的时间,设置执行一次 以上两种方法,实现起来不难,弊端在于会阻塞当前线程 ...

  4. Fireworks快捷键大全和ps查看切图的坐标颜色

    记住后方便了许多

  5. HTML 部分常用属性、组成属性|...超链接、路径、锚点、列表、滚动、URL编码、表格、表单、GET和POST

    URL地址 就是我们所说的网址:www.jd.com 浏览器内核,渲染引擎 Ie内核:triteent 谷歌/欧鹏:blink 火狐:gecko 苹果:webkit 渲染引擎是出现兼容性的根本问题 - ...

  6. jquery-scrollstop

    $(window) .on("scrollstart", function() { // Paint the world yellow when scrolling starts. ...

  7. Redis应用----消息传递

    1.摘要 消息传递这一应用广泛存在于各个网站中,这个功能也是一个网站必不可少的.常见的消息传递应用有,新浪微博中的@我呀.给你评论然后的提示呀.赞赞赞提示.私信呀.甚至是发微博分享的新鲜事:知乎中的私 ...

  8. Mycat安装与使用

      1.下载:   https://github.com/MyCATApache/Mycat-download 具体下载哪个版本以发布为准,推荐1.4,1.5.   2.安装:   安全前,在Linu ...

  9. 【转】JavaScript中使用ActiveXObject操作本地文件夹的方法

    原文链接:http://www.jb51.net/article/48538.htm  

  10. String, StringBuilder, StringBuffer问题

    1. 区别 String为字符串常量,而StringBuilder和StringBuffer都是字符串变量,其中StringBuilder线程非安全,StringBuffer线程安全. 每次对 Str ...