一套兼容原生、VUE、React热更新的webpack配置

开始

  让我们从头开始。
  JS是一门入门很简单的语言,能够给新手很强的自信心,这对学习是很大的帮助。就连写JS的工具都不需要特别复杂,你可以直接在记事本中输入如下部分并保存为index.html,然后在浏览器打开就是一个网页了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
.root {
width: 300px;
height: 300px;
}
</style>
</head>
<body>
<div class="root"></div>
<script>
document.querySelector('.root').style.background = 'red';
</script>
</body>
</html>

  再进一步,就是找一个好用的文本编辑器,将HTML、JS、CSS分离,变得稍微专业一些。然后学习Vue、React、Angular等框架。这时已经可以正常进行项目开发了。
  这些框架各自提供了自己的脚手架,让我们可以实时看到自己写的代码产生的效果,但是如果我平时学习,难道也要分开好几个文件夹吗?那太麻烦了,可不可以在一套工程配置里实现兼容几种框架并且全部可以热更新?
  当然可以。

基础

  推荐一篇文章https://www.jianshu.com/p/42e11515c10f,笔者入门也是看到这篇文章。写的很好。如果你只想跟着一个人系统的学,那么可以看这里,也很详细!

  首先创建一个新的文件夹learnPack,进入文件夹,执行初始化

1
npm init

安装webpack,这里的版本是webpack4,每个大的版本之间是不能混用的

1
2
3
npm install webpack -g
npm install webpack -D
npm install webpack-cli -D

安装好后创建index.html、src/main.js、src/style.css:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-->index.html<-->
<!DOCTYPE html>
<html lang="en">
<head>
<title>learn webpack</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="./src/style.css">
</head>
<body>
<div class="root"></div>
<!-->注意这里,并没有创建dist文件夹<-->
<script src="./dist/main.js"></script>
</body>
</html>
1
2
3
4
5
// main.js
function init() {
document.querySelector('.root').innerHTML = 'hello webpack';
}
init();
1
2
3
4
5
/* style.css */
.root {
font-size: 100px;
color: aqua;
}

目录结构如下:
webpack

文件全部创建好后,开始打包!

1
2
// webpack已经全局安装过了,所以可以直接执行如下命令
webpack src/main.js

看到这些就说明打包成功了!
webpack
在目录中也看到了多了个dist文件夹,里面有我们的main.js,打开可以看到里面是处于压缩状态的,而且之前我们已经在index.html中引入了dist/main.js文件,在浏览器打开即可看到“hello webpack!”
webpack

那么,如果我想将打包文件输出至”build”文件夹呢?而且名称改为bundle怎么做?
现在,我们先添加一个webpack的配置文件:

1
2
3
4
5
6
7
8
9
10
// webpack.config.js
module.exports = {
// webpack是运行在node环境的,__dirname是node中的一个全局变量,表示当前文件的路径。
// 比如webppack.config.js在F:\learnPack目录下,则__dirname = F:\learnPack
entry: __dirname + "/src/main.js",//入口文件,告诉webpack从哪里开始分析打包
output: {
path: __dirname + "/build",//打包后的文件存放的地方
filename: "bundle.js"//打包后输出文件的文件名
}
}

有了这些配置,就可以在命令行直接执行webpack,webpack会自动识别这个文件里的配置
webpack
执行成功!

webpack中重要的几个概念是entry(入口)、output(输出)、loader(理解为解析器)、plugins(插件)。其中loader和plugins是可选的,我们这个简单的文件当然不需要。接下来搭建本地服务器,在线编辑并预览!

1
2
// 安装webpack自带的服务器
npm install webpack-dev-server -D

继续在webpack配置文件中添加服务配置devServer项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// webpack.config.js
console.log(__dirname);
module.exports = {
entry: __dirname + "/src/main.js",//入口文件
output: {
path: __dirname + "/build",//打包后的文件存放的地方
filename: "bundle.js"//打包后输出文件的文件名
},
devServer: {
contentBase: './', // 本地服务器所加载的index页面所在的目录
host: 'localhost', // host
port: '8080', // 端口
historyApiFallback: true, // 不跳转
inline: true, // 实时刷新
clientLogLevel: 'none',
compress: true, // 一切服务都启用 gzip 压缩
hot: true, // 启用 webpack 的模块热替换特性
hotOnly: true,
noInfo: true, // 不显示打包压缩的信息
index: 'index.html', // 模版页名称
progress: true, // 运行进度条
watchContentBase: false, // 观察 devServer.contentBase 下的文件。文件修改后,会触发一次完整的页面重载
open: true // 是否自动打开浏览器
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-->index.html<-->
<!DOCTYPE html>
<html lang="en">
<head>
<title>learn webpack</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="./src/style.css">
</head>
<body>
<div class="root"></div>
<!-->注意修改这里,启动本地服务后会自动匹配<-->
<script src="bundle.js"></script>
</body>
</html>

在package.json的scripts中添加start和buld指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "learnpack",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack-dev-server",
"build": "webpack"
},
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"webpack": "^4.33.0",
"webpack-cli": "^3.3.4",
"webpack-dev-server": "^3.7.1"
}
}

运行npm start 就可以在浏览器端 http://localhost:8000 看到运行结果啦!

在清楚了基础操作后建议将官方网站浏览一遍

热更新配置

  热更新(Hot Module Replacement)是 webpack-dev-server 最强大的功能之一。它能在不刷新整个页面的情况下,将修改的模块替换到运行中的页面里,保留应用的状态。比如你在表单填了一半、弹窗开到一半时改了样式代码,热更新可以只替换样式而保持表单状态不变。

启用 HMR

  在 devServer 中已经设置 hot: true 了,但这还不够,还需要添加 webpack.HotModuleReplacementPlugin 插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// webpack.config.js
const webpack = require('webpack');

module.exports = {
entry: __dirname + "/src/main.js",
output: {
path: __dirname + "/build",
filename: "bundle.js"
},
devServer: {
contentBase: './',
host: 'localhost',
port: '8080',
historyApiFallback: true,
inline: true,
hot: true,
hotOnly: true,
open: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}

  同时,在入口文件 main.js 中加入模块热替换的接口代码:

1
2
3
4
5
6
7
8
9
10
// main.js
function init() {
document.querySelector('.root').innerHTML = 'hello webpack';
}
init();

// 接受 HMR
if (module.hot) {
module.hot.accept();
}

  到此为止,我们已经有了一套支持热更新的基础配置。接下来分别配置原生 JS、Vue 和 React 三种场景。


场景一:原生 JS + 多页面

  如果你只是写写 demo、做做练习,不想每次都开一个脚手架,那用这套配置就够了。webpack 支持多入口,每个入口对应一个页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// webpack.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
// 多入口
entry: {
index: __dirname + '/src/index.js',
demo: __dirname + '/src/demo.js'
},
output: {
path: __dirname + '/build',
filename: '[name].bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
devServer: {
contentBase: './',
hot: true,
open: true
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
chunks: ['index'] // 只注入 index 的入口
}),
new HtmlWebpackPlugin({
template: './src/demo.html',
filename: 'demo.html',
chunks: ['demo']
})
]
}
1
2
3
4
5
6
7
// src/index.js
const div = document.createElement('div');
div.className = 'box';
div.textContent = 'Hello from index';
document.body.appendChild(div);

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

  每个页面独立一个入口,互不干扰。修改样式或逻辑后,只会热更新对应模块,不会丢失页面状态。


场景二:Vue 单文件组件

  Vue 推荐用单文件组件(.vue 文件)来组织代码。我们需要 vue-loadervue-template-compiler。需要注意的是,这两个包的大版本必须匹配——vue-loader@15 搭配 vue-template-compiler@2.x

1
npm install vue vue-loader vue-template-compiler -D
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// webpack.config.js
const webpack = require('webpack');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
entry: __dirname + '/src/main.js',
output: {
path: __dirname + '/build',
filename: 'bundle.js'
},
resolve: {
// 引入 Vue 时自动匹配 .vue 文件
extensions: ['.js', '.vue'],
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
}
]
},
devServer: {
hot: true,
open: true
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new VueLoaderPlugin(), // vue-loader v15 必须
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- src/App.vue -->
<template>
<div class="app">
<h1>{{ msg }}</h1>
</div>
</template>

<script>
export default {
data() {
return { msg: 'Hello Vue + HMR' }
}
}
</script>

<style scoped>
.app { color: #42b983; }
</style>
1
2
3
4
5
6
7
8
9
10
11
// src/main.js
import Vue from 'vue';
import App from './App.vue';

new Vue({
render: h => h(App)
}).$mount('#app');

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

  Vue 的 HMR 在 vue-loader 中已经内置了——修改 .vue 文件的 <template> 时,只会替换模板不重置状态;修改 <style> 时,只换样式不重渲染;只有在修改 <script> 的 data 或逻辑时,才会重新执行组件实例。


场景三:React + react-hot-loader

  React 的 HMR 需要额外的 react-hot-loader 配合。同时需要 Babel 来编译 JSX。

1
2
npm install react react-dom react-hot-loader -D
npm install @babel/core @babel/preset-env @babel/preset-react babel-loader -D
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// webpack.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
entry: __dirname + '/src/index.js',
output: {
path: __dirname + '/build',
filename: 'bundle.js'
},
resolve: {
extensions: ['.js', '.jsx']
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['react-hot-loader/babel']
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
devServer: {
hot: true,
open: true
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/App.js
import { hot } from 'react-hot-loader/root';
import React, { Component } from 'react';

class App extends Component {
state = { count: 0 };

render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={() => this.setState(s => ({ count: s.count + 1 }))}>
+
</button>
</div>
);
}
}

// 用 hot 包裹后,修改组件代码时保留 state 不丢失
export default hot(App);
1
2
3
4
5
6
7
8
9
10
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('app'));

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

  react-hot-loaderhot() 高阶组件是关键——它让组件在被替换时保留内部 state。你可以在页面上点了几次计数器的按钮,然后修改渲染逻辑,计数不会归零,这就是热更新的价值。


合一:如何在一个工程里同时兼容三套框架?

  把上面的 loader 规则和插件合并到同一个 webpack.config.js 中即可。Vue 和 React 在同一个工程里互不冲突,唯一的代价是多装了一些依赖和首次构建稍慢一些。最终的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
learnPack/
├── src/
│ ├── index.html # 原生 JS 页面
│ ├── index.js
│ ├── vue.html # Vue 页面
│ ├── vue.js
│ ├── App.vue
│ ├── react.html # React 页面
│ ├── react.js
│ └── App.js
├── package.json
└── webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 合一的 webpack.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
entry: {
index: __dirname + '/src/index.js',
vue: __dirname + '/src/vue.js',
react: __dirname + '/src/react.js'
},
output: {
path: __dirname + '/build',
filename: '[name].bundle.js'
},
resolve: {
extensions: ['.js', '.jsx', '.vue'],
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['react-hot-loader/babel']
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
devServer: {
hot: true,
open: true,
port: 8080
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: './src/index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
filename: 'vue.html',
template: './src/vue.html',
chunks: ['vue']
}),
new HtmlWebpackPlugin({
filename: 'react.html',
template: './src/react.html',
chunks: ['react']
})
]
}

  运行 npm start 后,访问 http://localhost:8080 看到原生 JS 页面,访问 http://localhost:8080/vue.html 看到 Vue 页面,访问 http://localhost:8080/react.html 看到 React 页面。三个框架共享同一套 webpack 配置,全部支持热更新。

  这个方案适合学习阶段用来对比不同框架的写法差异,也适合写技术 demo 分享。但生产项目还是推荐用框架各自的脚手架——它们经过更多实践检验,配置也更专业。