Webpack 2: 生产力工具, 模块热替换(HMR)的几种方式

2018-01-10 更新

Webpacket 已经到3了, 这里添加更新说明,并说明结合babel-loader使用的简要步骤

参考资料

#0. 环境

node: v9.0.0
webpack: ^3.10.0 
webpack-dev-server: ^2.10.1
react-hot-loader": ^3.1.3
babel-core: ^6.26.0
babel-loader: ^7.1.2
babel-plugin-import: ^1.6.3
babel-preset-env: ^1.6.1
babel-preset-react: ^6.24.1

#1..babelrc中添加 react-hot-loader

// .babelrc
{
  "plugins": [
    "react-hot-loader/babel"
  ]
}

#2. webpacket.confg.js 中的设置

const path = require('path');
const webpack = require('webpack');
const moment = require('moment')

const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const WriteFilePlugin = require('write-file-webpack-plugin');

const publish_date = moment().format("YYYYMMDD")

let config = {
  devtool: 'eval-source-map',
  resolve: {
    extensions: ['.js', '.jsx', '.json']
  },
  entry: {
    app: [
      'react-hot-loader/patch',
      './src/index'
    ]
  },
  module: {
    rules: [
      {
        test: /\.jsx$/,
        exclude: /node_modules/,
        use: [{
          loader: 'babel-loader'
        }]
      },
      {
        test: /\.css$/,
        use: [
          { loader: 'style-loader' },
          { loader: 'css-loader' }
        ]
      },
      {
        test: /\.less$/,
        use: [
          { loader: 'style-loader' },
          { loader: "css-loader", },
          { loader: "less-loader" }
        ]
      },
    ]
  },
  output: {
    path: path.resolve(__dirname, 'dist', publish_date),
    filename: '[name].[hash].js',
    sourceMapFilename: '[name].[hash].map',
    publicPath: '/' + publish_date
  },
  plugins: [
    new WriteFilePlugin(),
    new HtmlWebpackPlugin({
      title: 'RBAC',
      inject: true,
      template: './index.html',
      filename: 'index.html',
      chunksSortMode: 'auto',
      minify: false,
      hash: false,
      xhtml: true,
      chunks: ['app'],
      cache: true,
      showErrors: true,
    }),
    new CopyWebpackPlugin([
      { from: 'src/assets', to: 'assets' }
    ]),
    new webpack.NamedModulesPlugin(),
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    contentBase: path.join(__dirname, 'dist', publish_date),
    hot: true,
    host: '0.0.0.0',
    port: 8000,
    publicPath: '/' + publish_date,
  }
}

module.exports = config;

devServerhot要设置为true

#3. 要在你的应用程序入口之前添加 react-hot-loader/patch, 如下:

let config = {
  devtool: 'eval-source-map',
  resolve: {
    extensions: ['.js', '.jsx', '.json']
  },
  entry: {
    app: [
      'react-hot-loader/patch',
      './src/index'
    ]
  },
  ...
  ...
  ...
}

#2 有完整的配置

#4. 入口文件要这样

// index.js
import React from 'react'
import ReactDOM from 'react-dom'
// 导入HMR
import { AppContainer } from 'react-hot-loader'
import App from './containers/App'

// 定义一个热加载回调函数用于重新渲染入口组件
const render = Component => {
  ReactDOM.render(
    <AppContainer>
      <Component />
    </AppContainer>,
    document.getElementById('root'),
  )
}

// 启动时调用
render(App)

// 当发送热加载时调用, Webpack打包的生产环境不会有 `module.hot`, 因此这块代码在开发环境
// 下面的if块里面的代码不会执行
if (module.hot) {
  module.hot.accept('./containers/App', () => {
    render(App)
  })
}

#5. .babelrc 中的 modules 属性

modules 属性必须要设置为 false, 否则HMR无效

{
  "presets": [
    ["env", { "modules": false }], 
    "react"
  ],
  "plugins": [
    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }],
    "react-hot-loader/babel"
  ]
}

#6. 最新如果你不用babel, 直接在配置文件中设置 react-hot-loader/webpack 加载器

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        use: ['react-hot-loader/webpack'],
      },
    ],
  },
}

更新完毕

命令行

命令行方式是最简单的方式, 如果项目只是纯Web前端, 使用这种方式是最便捷的. 只需要在package.json文件中的scripts里面添加下面一行就可以了.

直接命令行:

webpack-dev-server --content-base=www --inline --watch --hot --progress --config webpack.config.js

通过 npm run devyarn run dev:

// package.json
"scripts": {
  "dev": "webpack-dev-server --content-base=www --inline --watch --hot --progress --config webpack.config.js",
  ...
},

参数说明:

--content-base: 静态资源的目录, 为 output.path 设置的目录.

output: {   
   path: path.resolve(__dirname, 'dist'), 
}, 

--watch: 监视模式, Web执行完打包后不退出, 一直监听文件变化, Ctrl + S后自动构建变化的模块, 及其依赖模块.
--hot: 开启模块热替换.
--progress: 显示进度
--config: 指定配置文件 webpack.config.js 为默认文件名.

Webpack API

API的方式需要对webpack.config.js配置文件进行修改, 增加HotModuleReplacementPlugin插件.

Webpack 1.x

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

module.exports = {
  context: path.join(__dirname, 'js'),
  entry: [
    './index.js',
    'webpack/hot/dev-server',
    'webpack-dev-server/client?http://localhost:8080/',
  ],
  output: {
    path: path.join(__dirname, 'www'),
    filename: 'bundle.js',
    publicPath: '/',
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
  ],
};

Webpack 2.x

Webpack 2.x 需要一个额外配置 .babelrc, 增加 react-hot-loader/babel:

{
  "presets": [
    "es2015",
    "react",
    "stage-0"
  ],
  "plugins": [
    "react-hot-loader/babel"
  ]
}

webpack.config.js 模块加载器的配置, 和Webpack 1.x是不同的:

rules: [
  // Javascript模块加载器
  {
    test: /\.js|jsx$/,
    exclude: /(node_modules|bower_components)/,
    use: {
      loader: 'babel-loader',
      options: {
        cacheDirectory : true,
        presets: [
          ['es2015', {modules: false}]
        ],
        // ES7
        plugins: [
          // 模块动态导入
          'syntax-dynamic-import',
          'transform-async-to-generator',
          'transform-regenerator',
          // 运行时转换
          'transform-runtime'
        ]
      }
    }
  },

中间件

需要编写一个Express服务器, 并且把 webpack-dev-middleware 集成到自己的服务器中. 多用于需要高度定制的场景, 实际上之前的webpack-dev-server就是使用的 webpack-dev-middleware 来实现的, 这里可以证明.

const express = require('express');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const webpack = require('webpack');
const webpackConfig = require('./webpack.config.js');
const app = express();

const compiler = webpack(webpackConfig);

app.use(webpackDevMiddleware(compiler, {
  // 启用热更
  hot: true,
  filename: 'bundle.js',
  publicPath: '/assets/',
  stats: {
    colors: true,
  },
  // 路由需要的
  historyApiFallback: true,
}));

app.use(webpackHotMiddleware(compiler, {
  log: console.log,
  
  path: '/__webpack_hmr',
  heartbeat: 10 * 1000,
}));

app.get('/', function(req, res) {
  res.send('<body>Hello World<script src=\'assets/bundle.js\'></script></body>');
});

const server = app.listen(3000, function() {
  const host = server.address().address;
  const port = server.address().port;

  console.log('Example app listening at http://%s:%s', host, port);
});

对于这种形式的热更, 需要配合使用 webpack-hot-middleware.

Javascript 模块的热替换

需要在入口文件里面添加 module.hot.accept 配置, 下面给出入口文件的完整代码:

import React from 'react';
import ReactDOM from 'react-dom';

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

import App from './App';

import { AppContainer } from 'react-hot-loader';
const render = (Component) => {
  ReactDOM.render(
    <AppContainer>
      <Component />
    </AppContainer>,
    document.getElementById('App')
  );
}
render(App);
if (module.hot) {
  module.hot.accept('./App', () => { render(App) });
}

原先直接挂载App, 要使用HMR, 需要在外层包一个<AppContainer>容器.

多端同步刷新

这个视频, 是Chrome, Safari, Firefox 三端同步操作的示例.

对于需要支持全平台(移动, 桌面, Web)的开发, 可以使用 BrowerSync 这个神器. 它通过 Websocket 同步事件.

首先, 把模块包含进来:

const BrowserSyncPlugin          = require('browser-sync-webpack-plugin');

其次, 配置插件:

new BrowserSyncPlugin({
  host: 'localhost',
  port: 3000,
  // server: {            // 独立服务器模式, 这里我使用的代理模式, 注释掉了
  //   baseDir: ['dist']  // 监视的目录, 其中如果文件发生变化, 刷新页面
  // },
  proxy: 'http://localhost:8080/'
}, {
  name: 'dev',
  reload: false // 不让 BrowerSync 刷新页面, 让 webpack-dev-server 管理页面是否需要刷新.
}),

这里我使用代理模式, 因为除了 BrowerSync 的功能外, 我还要使用 webpack-dev-server HMR功能. 具体配置参考: https://github.com/Va1/browse...

参考:

https://github.com/search?utf...
https://github.com/webpack/we...
https://github.com/glenjamin/...
http://acgtofe.com/posts/2016...
https://github.com/Va1/browse...

1.7k 声望
262 粉丝
0 条评论
推荐阅读
Rust: 使用 sccache 进行编译加速
项目产生的 target 目录非常占用磁盘空间, cargo clean 清理后, 又需要重新编译, 非常耗费时间, 为此, 使用 sccache 来加速编译过程.

developerworks阅读 618

从零搭建 Node.js 企业级 Web 服务器(零):静态服务
过去 5 年,我前后在菜鸟网络和蚂蚁金服做开发工作,一方面支撑业务团队开发各类业务系统,另一方面在自己的技术团队做基础技术建设。期间借着 Node.js 的锋芒做了不少 Web 系统,有的至今生气蓬勃、有的早已夭折...

乌柏木140阅读 11.8k评论 10

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木60阅读 5.9k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs39阅读 6.1k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木39阅读 7k评论 6

【关于Javascript】--- 正则表达式篇
基础知识一、元字符 {代码...} 二、量词 {代码...} 三、集合 字符类 {代码...} 四、分支 {代码...} 五、边界 开始结束 {代码...} 六、修饰符 {代码...} 七、贪婪模式和非贪婪模式js默认贪婪模式即最大可能的匹配...

Jerry35阅读 2.9k

从零搭建 Node.js 企业级 Web 服务器(二):校验
校验就是对输入条件的约束,避免无效的输入引起异常。Web 系统的用户输入主要为编辑与提交各类表单,一方面校验要做在编辑表单字段与提交的时候,另一方面接收表单的接口也要做足校验行为,通过前后端共同控制输...

乌柏木32阅读 6k评论 9

1.7k 声望
262 粉丝
宣传栏