注意:本篇不是一个讲解HMR原理的文章,仅仅阐述如何实现接入HMR。

背景

随着部门业务的发展,业务逻辑的不断发展,为了更好的应对业务的多变性,提高项目可维护性,我们提出了基于React的组件化工作流。

在项目的前期,我们将原先的jquery组件逐步的迁移至React组件,一个个的业务模块被拆分为可维护的组件及可复用组件,对于React,由于独有的JSX语法,页面的html结构和js逻辑混搭在一起,通常是下面这样:

1
2
3
4
5
6
7
8
const Avatar = (props)=>{
var url = somehandle(props.url); // your js code ...
return (
<div className="m-avatar">
<img src={url} alt={props.alt || "用户头像"}/>
</div>
);
}

但是为了组件的统一维护性,需要将组件的样式也单独拆分到组件粒度并将组件的.jsx.scss文件放在一个目录里面,以上面讲到的Avatar组件为例:

1
2
3
//...avatar/index.jsx
require('./index.scss'); // 引入scss样式文件
//go you code...

组件的目录结构为:

1
2
3
4
5
6
--component
----avatar
------index.jsx (组件入口文件)
------index.scss(组件级样式文件)
------tool.js(组件使用的工具类)
------etc...

这个概念其实很早就有大神提出来了==>张云龙-前端工程-基础篇
当然,使用几乎无所不能的webpack可以达到我们的诉求。

痛处

在迁移中我发现,由于前端开发中通过webpack来加载.scss文件资源,每次修改组件样式文件时,webpack的watch机制就会触发资源的重新打包,进而将整个page页面刷新(通过使用livereload来监听js文件改动)。而在这之前,也就是样式没有拆分到组件粒度之前,样式文件按照page为单位维护,通过compass+livereload.js来完成实时编译.scss文件并热替换样式文件的“一条龙”服务。

现在写组件样式就很不爽了,就改了一个背景色,整个页面需要重新reload。所以有没有解决方案,让样式文件能够在不刷新浏览器的情况下被应用?

答案是有的,可以通过webpack的HMR(Hot-Module-Replacement)可以实现模块的热替换,这其中就包含css模块。

了解HMR

首先什么是HMR?原引官方文档的解释:

Hot Module Replacement (HMR) exchanges, adds, or removes modules while an application is running, without a full reload. This can significantly speed up development in a few ways:

  • Retain application state which is lost during a full reload.
  • Save valuable development time by only updating what’s changed.
  • Tweak styling faster – almost comparable to changing styles in the browser’s debugger.

原引中文文档翻译:

模块热替换(HMR - Hot Module Replacement)功能会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:

  • 保留在完全重新加载页面时丢失的应用程序状态。
  • 只更新变更内容,以节省宝贵的开发时间。
  • 调整样式更加快速 - 几乎相当于在浏览器调试器中更改样式。

在前端开发过程中,我们需要不断的调整样式,通过webpack的HMR我们可以很方便的在保存应用状态前提下快速修改代码,极大的节省了开发时间和提升开发效率。

面临的选择

通过查阅官方文档和相关资料,使用webpack的hmr有以下3种方式,可以根据情况作出选择:

下面简要概述这3种方式:

method-1 :webpack-dev-server CLI

第一种方法是通过命令行的方式来启用webpack的HMR,移动到相关project目录下,配置相关webpack明令,对于webpack-dev-serverCLI来说,可以接受通过--config配置传入的webpack.config.js配置文件。仅仅需要做以下事情即可:

1
2
3
cd your-project-path
npm install webpack webpack-dev-server --save-dev
webpack-dev-server --watch --hot -- other-options...

这种方式可以方便的用来快速开始一个webpack项目,在真实的开发项目中,建议不要试用这种方式。

更多的webpack CLI配置命令点击这里

method-2:webpack-dev-server API

第二种方法是通过webpack-dev-server来启用HMR,这种方式需要修改webpack配置项,简单来说就是webpack本地开启一个server,浏览器加载的boundle中包含一个javaScript runtime可以通过socket和webpack-dev-server通信,具体查看这篇文章来了解具体步骤。

method-3:webpack-hot-middleware

第三种方法是通过设置webpack-dev-middleware+webpack-hot-middleware来完成代码热替换的,与其他2种方法都必须要使用webpack-dev-server不同的地方在于,方法三适合哪些即有项目。对即有的项目来说,有自己的一套本地server(通常是Express),包含了其他一些针对本地server的处理,这个时候在强行再次起一个服务(webpack-dev-server)无疑是得不偿失的。通过webpack-dev-middlewarewebpack-hot-middleware中间件来整合webpack热替换到即有本地服务中,从而享受webpack HMR带来的各种开发优势。

将webpack HMR整合到Express

在选择实现方案的时候,考虑到由于已经存在前端本地服务(Express),所以选择通过webpack-hot-middleware的方式来接入webpack的HMR。
需要执行以下几个步骤:

步骤1:Express接入webpack-dev-middleware

首先,webpack-dev-mddleware是什么?

它是一个简单的webpack包装中间件,通过连接服务来提供从webpack打包好的文件。注意到它相比于将boundle打包成文件有以下优势:

  • 不会有硬盘文件的写入,它将打包好的文件存放在内存中。
  • 当在watch模式下有文件改动,webpack-dev-middle将会延迟boundle的响应直到新的boundle打包完成(也就是说,你不需要在刷新页面前等待文件的重新打包)
1
2
3
4
5
6
7
var app = express();
var compiler = webpack(your_webpackConfig);
app.use(require('webpack-dev-middleware')(compile, {
// required 打包好的boundle相对公共路径,此次设置为'/'
publicPath: your_webpackConfig.output.publicPath
// other options see ==> https://github.com/webpack/webpack-dev-middleware#usage
}));

步骤2:Express接入webpack-hot-middleware

什么是webpack-hot-middleware中间件?
通过webpack-hot-middleware中间件让你在既有server上添加热替换功能。

1
2
3
4
5
6
7
8
9
var app = express();
var compiler = webpack(your_webpackConfig);
app.use(require('webpack-hot-middleware'(compiler, {
log: console.log, // 控制台输出方法
path: '/__webpack_hmr', // webpack hmr server 路径
heartbeat: 10 * 1000,// 轮询通信间隔
// other options see ==>
// https://github.com/glenjamin/webpack-hot-middleware#documentation
}));

步骤3:修改webpack配置文件

首先,需要启用webpack的热加载功能,通过webpack自带的插件即可。

1
2
3
4
5
6
7
8
if(key == 'serve'){// 保证只在开发阶段试用webpack热替换
plugins = plugins.concat([
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin(),
// 启用热替换插件
new webpack.NoErrorsPlugin()
]);
}

其次,修改每个入口文件配置,链接到webpack热加载服务器。

1
2
3
4
5
6
7
8
9
10
11
var hotMiddlewareScript = 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=true';
// 详细的配置访问==>
//https://github.com/glenjamin/webpack-hot-middleware#config
//... 省略
if(key =='serve'){
for(var entry in entrys){
if(entrys.hasOwnProperty(entry)){
entrys[entry] = [entrys[entry], hotMiddlewareScript];
}
}
}

步骤4:修改js代码

为了完成webpack的热替换,还需要在你的应用代码中添加一些额外的代码,这些额外的代码做的事情是移除一些可能对应用产生副作用的影响。

由于通过stlye-loader来处理.css文件,而该loader自带模块热替换的功能,无需在应用代码中额外处理。

可以通过使用webpack提供的相关API来显示的处理,具体可以查看webpack官网

至此,已经完成了所有的配置修改工作,现在启动express服务器,修改某一个引入的.scss文件,你会发现页面在不刷新的情况下重新应用了相关的样式文件。

遇到的问题

在将webpack热替换接入到项目中时,还遇到了其他的一些问题:

  • 路径匹配问题(由于window和linux系统分盘符导致的打包文件路径问题)
  • 如何在不影响线上打包的情况下在开发环境下接入HMR
  • 如何模版文件中的js文件路径和webpack打包到内存中的路径保持一致

小结及反思

通过此次在项目中成功接入webpack HMR,更加明确及熟悉了前端架构优化的流程。总结了以下几点:

  • 首先提出诉求,我们想要达成的目标是什么?==> 热替换样式,不刷新页面
  • 调研实现方法有哪几种? ==> 3种,webpack-dev-server CLI / webpack-dev-server API / webpack-hot-middleware
  • 是否有合适方案/根据项目现有状况挑选一种最合适的实现方案==> webpack-dev-middle+webpack-hot-middleware
  • 实现注意事项有哪些?==>路径匹配/环境区分/etc

碍于篇幅,本篇仅仅解释了基于webpack的样式热替换,当然还可以实现让js文件热替换,可以查看webpack官网了解更多的loader和[HMR的API](https://doc.webpack-china.org/api/hot-module-replacement)。

参考文章

后续将会有另外的文章来讲解webpack热替换的原理,敬请期待:)