0%

webpack配置——基础篇

webpack配置知识梳理

初识webpack

webpack 的几个重要的概念:entry,loader,plugin,output

entry:

单入口,多入口,多文件一个入口
webpack从入口开始解析依赖,打包产出

loader:

在module.rules 字段下来配置loader相关的规则
我们可以把 loader 理解为是一个转换器,负责把某种文件格式的内容转换成 webpack 可以支持打包的模块,它支撑着 webpack 来处理文件的多样性。

举个例子,在没有添加额外插件的情况下,webpack 会默认把所有依赖打包成 js 文件,如果入口文件依赖一个 .hbs 的模板文件以及一个 .css 的样式文件,那么我们需要 handlebars-loader 来处理 .hbs 文件,需要 css-loader 来处理 .css 文件(这里其实还需要 style-loader,后续详解),最终把不同格式的文件都解析成 js 代码,以便打包后在浏览器中运行。

plugin:

在配置中通过 plugins 字段添加新的 plugin
可以这么理解,模块代码转换的工作由 loader 来处理,除此之外的其他任何工作都可以交由 plugin 来完成。
例如,要使用压缩 JS 代码的 uglifyjs-webpack-plugin 插件,定义环境变量的 DefinePlugin,生成 CSS 文件的 ExtractTextWebpackPlugin 等。

output:

Webpack构建结果的文件名、路径等都是可以配置的,使用 output 字段。

webpack构建:

webpack 运行时默认读取项目下的 webpack.config.js 文件作为配置。
webpack 的配置其实是一个 Node.js 的脚本,这个脚本对外暴露一个配置对象,
因为是 Node.js 脚本,所以可玩性非常高,你可以使用任何的 Node.js 模块,如 path 模块,当然第三方的模块也可以。
Demo:

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
const path = require('path')
const UglifyPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
entry: './src/index.js',

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

module: {
rules: [
{
test: /\.jsx?/,
include: [
path.resolve(__dirname, 'src')
],
use: 'babel-loader',
},
],
},

// 代码模块路径解析的配置
resolve: {
modules: [
"node_modules",
path.resolve(__dirname, 'src')
],

extensions: [".wasm", ".mjs", ".js", ".json", ".jsx"],
},

plugin: [
new UglifyPlugin(),
// 使用 uglifyjs-webpack-plugin 来压缩 JS 代码
// 如果你留意了我们一开始直接使用 webpack 构建的结果,你会发现默认已经使用了 JS 代码压缩的插件
// 这其实也是我们命令中的 --mode production 的效果,后续的小节会介绍 webpack 的 mode 参数
],
}

有的时候我们开始一个新的前端项目,并不需要从零开始配置 webpack,而可以使用一些工具来帮助快速生成 webpack 配置。我们可以学习了解各脚手架所提供的 webpack 配置,有些情况下,还会尝试修改这些配置以满足特殊的需求。
e.g.
create-react-app
vue-cli
angular/devkit/build-webpack

webpack 的安装和使用和大多数使用 Node.js 开发的命令行工具一样,使用 npm 安装后执行命令即可,webpack 4.x 版本的零配置特性也让上手变得更加简单。

使用Webpack搭建前端基本开发环境

基本前端开发环境的需求:

  1. 构建我们发布需要的 HTML、CSS、JS 文件
  2. 使用 CSS 预处理器来编写样式
  3. 处理和压缩图片
  4. 使用 Babel 来支持 ES 新特性
  5. 本地提供静态服务以方便开发调试

展开:

1. 构建HTML并关联CSS

构建时 html-webpack-plugin 会通过我们配置的html模版为我们创建一个 HTML 文件,其中会引用构建出来的动态生成的JS 文件。如果需要添加多个页面关联,那么实例化多个 html-webpack-plugin。

1
2
3
4
5
6
7
8
9
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html', // 配置输出文件名和路径
template: 'assets/index.html', // 配置我们写好的文件模板
}),
],
}

2. 构建CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module: {
rules: [
// ...
{
test: /\.css/,
include: [
path.resolve(__dirname, 'src'),
],
use: [
'style-loader',
'css-loader',
],
},
],
}
}

我们在index.js 中引入index.css【import “./index.css”】然后构建,css相关loader会把css代码会转变为 JS,和 index.js 一起打包了。如果需要单独把 CSS 文件分离出来,我们需要使用 extract-text-webpack-plugin 插件。

  • css-loader 负责解析 CSS 代码,主要是为了处理 CSS 中的依赖,例如 @import 和 url() 等引用外部文件的声明;
  • style-loader 会将 css-loader 解析的结果转变成 JS 代码,运行时动态插入 style 标签来让 CSS 代码生效。

使用extract-text-webpack-plugin demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/,
// 因为这个插件需要干涉模块转换的内容,所以需要使用它对应的 loader
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
'css-loader’,
'less-loader’,
],
}),
},
],
},
plugins: [
// 引入插件,配置文件名,这里同样可以使用 [hash]
new ExtractTextPlugin('index.css'),
],
}

3. 处理图片等文件

file-loader 可以用于处理很多类型的文件,它的主要作用是直接输出文件,把构建后的文件路径返回。
css-loader虽然会解析样式中用 url() 引用的文件路径,但是图片对应的 jpg/png/gif 等文件格式,webpack 处理不了,此时就可以派file-loader上场了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
// 增加图片类型文件的解析配置
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {},
},
],
},
],
},
}

4. 构建JS ——Babel

Babel 是一个让我们能够使用 ES 新特性的 JS 编译工具,Babel 的相关配置可以在目录下使用 .babelrc 文件来处理。

5. 启动静态服务

我们可以使用 webpack-dev-server 在本地开启一个简单的静态服务来进行开发。
(1) 安装webpack-dev-server
(2)

1
2
3
4
"scripts": {
"build": "webpack --mode production",
"start": "webpack-dev-server --mode development"
}

(3)运行 npm run start 或者 yarn start,然后通过 http://localhost:8080/ 访问

Webpack如何解析代码模块路径

webpack构建的项目中,我们可以
使用import * as m from ‘./index.js’ 来引用代码模块 index.js。
使用import React from ‘react’引用第三方类库。

webpack是如何解析到对应文件模块的呢?

webpack 依赖 enhanced-resolve 来解析代码模块的路径,webpack 配置文件中和 resolve 相关的选项都会传递给 enhanced-resolve 使用,
(这个模块可以说是 Node.js 那一套模块路径解析的增强版本,有很多可以自定义的解析配置)
解析模块的规则大概如下:

  • 解析相对路径
  1. 查找相对当前模块(及当前模块内的模块)的路径下是否有对应文件或文件夹
  2. 是文件则直接加载
  3. 是文件夹则继续查找文件夹下的 package.json 文件
  4. 有 package.json 文件则按照文件中 main 字段的文件名来查找文件
  5. 无 package.json 或者无 main 字段则查找 index.js 文件
  • 解析模块名
    查找当前文件目录的父级目录及以上目录下的 node_modules 文件夹,看是否有对应名称的模块
  • 解析绝对路径(不建议使用)
    直接查找对应路径的文件

在 webpack 配置中,和模块路径解析相关的配置都在 resolve 字段下,常用解析路径规则的自定义配置如下:

resolve.alias

模糊匹配:

1
2
3
alias: {
utils: path.resolve(__dirname, 'src/utils') // 这里使用 path.resolve 和 __dirname 来获取绝对路径
}

下边的utils都会被替换为绝对路径

1
2
import ‘utils'
import 'utils/query.js'

精确匹配:

1
2
3
alias: {
utils$: path.resolve(__dirname, 'src/utils') // 只会匹配 import 'utils'
}

resolve.extensions

1
extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx'], // 这里的顺序代表匹配后缀的优先级,例如对于 index.js 和 index.jsx,会优先选择 index.js

这个配置可以定义在进行模块路径解析时,在不写文件后缀时webpack 会尝试帮你补全那些后缀名来进行查找。

resolve.modules

import React from ‘react’时,webpack 会类似 Node.js 一样进行路径搜索,搜索 node_modules 目录,这个目录就是使用 resolve.modules 字段进行配置的。

默认:

1
2
3
resolve: {
modules: ['node_modules'],
},

通常情况下,我们不会调整这个配置,但是如果可以确定项目内所有的第三方依赖模块都是在项目根目录下的 node_modules 中的话,那么可以在 node_modules 之前配置一个确定的绝对路径,这样配置在某种程度上可以简化模块的查找,提升构建速度:

1
2
3
4
5
6
resolve: {
modules: [
path.resolve(__dirname, 'node_modules'), // 指定当前目录下的 node_modules 优先查找
'node_modules', // 如果有一些类库是放在一些奇怪的地方的,你可以添加自定义的路径或者目录
],
}

resolve.mainFields

【对应上边模块解析规则第一项第4点】

1
2
3
4
5
6
7
resolve: {
// 配置 target === "web" 或者 target === "webworker" 时 mainFields 默认值是:
mainFields: ['browser', 'module', 'main'],

// target 的值为其他时,mainFields 默认值为:
mainFields: ["module", "main"],
}

因为通常情况下,模块的 package 都不会声明 browser 或 module 字段,所以便是使用 main了。
在 NPM packages 中,会有些 package 提供了两个实现,分别给浏览器和 Node.js 两个不同的运行时使用,这个时候就需要区分不同的实现入口在哪里,所以有时候会制定target。

resolve.mainFiles

当目录下没有 package.json 文件时,我们说会默认使用目录下的 index.js 这个文件,其实这个可以通过mainFiles配置的,

1
2
3
resolve: {
mainFiles: ['index'], // 你可以添加其他默认使用的文件名,index为约定俗称,通常不会修改
},

resolve.resolveLoader

用于配置解析 loader 时的 resolve 配置,这个配置很少用,遵循默认即可。

1
2
3
4
5
6
resolve: {
resolveLoader: {
extensions: ['.js', '.json’], // 默认
mainFields: ['loader', 'main’], // 默认
},
},

配置loader:

规则条件配置:

{ test: … } 匹配特定条件
{ include: … } 匹配特定路径
{ exclude: … } 排除特定路径
{ and: […] }必须匹配数组中所有条件
{ or: […] } 匹配数组中任意一个条件
{ not: […] } 排除匹配数组中所有条件

值可以是:
字符串:必须以提供的字符串开始,所以是字符串的话,这里我们需要提供绝对路径
正则表达式:调用正则的 test 方法来判断匹配
函数:(path) => boolean,返回 true 表示匹配
数组:至少包含一个条件的数组
对象:匹配所有属性值的条件

Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
rules: [
{
test: /\.jsx?/, // 正则
include: [
path.resolve(__dirname, 'src'), // 字符串,注意是绝对路径
], // 数组
// ...
},
{
test: {
js: /\.js/,
jsx: /\.jsx/,
}, // 对象,不建议使用
not: [
(value) => { /* ... */ return true; }, // 函数,通常需要高度自定义时才会使用
],
},
]

Webpack4.X 强化了模块规则,新增了模块类型的概念,相当于 webpack 内置一个更加底层的文件类型处理,暂时只有 JS 相关的支持,后续会再添加 HTML 和 CSS 等类型。
现阶段实现了以下 5 种模块类型:
javascript/auto:即 webpack 3 默认的类型,支持现有的各种 JS 代码模块类型 —— CommonJS、AMD、ESM
javascript/esm:ECMAScript modules,其他模块系统,例如 CommonJS 或者 AMD 等不支持,是 .mjs 文件的默认类型
javascript/dynamic:CommonJS 和 AMD,排除 ESM
javascript/json:JSON 格式数据,require 或者 import 都可以引入,是 .json 文件的默认类型
webassembly/experimental:WebAssembly modules,当前还处于试验阶段,是 .wasm 文件的默认类型

1
2
3
4
5
6
7
{
test: /\.js/,
include: [
path.resolve(__dirname, 'src'),
],
type: 'javascript/esm', // 这里指定模块类型
},

上述做法是可以帮助你规范整个项目的模块系统,但是如果遗留太多不同类型的模块代码时,建议还是直接使用默认的 javascript/auto。

Loader应用顺序:

rule中一个对象中的规则:配置多个loader, 执行是从后向前执行,对于css,除了 style-loader 和 css-loader,你可能还要配置 less-loader 然后再加个 postcss 的 autoprefixer 等。
rule中多个对象匹配到相通规则:无法保证规则执行先后顺序,可配置enforce字段为“pre” “post”, 分别对应前置类型或后置类型的 loader,不设置为普通类型
loader 按照前置 -> 行内 -> 普通 -> 后置的顺序执行。
当项目文件类型和应用的 loader 不是特别复杂的时候,通常建议把要应用的同一类型 loader 都写在同一个匹配规则中,这样更好维护和控制。
Demo:
eslint-loader 要检查的是人工编写的代码,如果在 babel-loader 之后使用,那么检查的是 Babel 转换后的代码,所以必须在 babel-loader 处理之前使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "eslint-loader”,
enforce: “pre”,
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
},
]

noParse

module.noParse 字段,可以用于配置哪些模块文件的内容不需要进行解析。对于一些不需要解析依赖(即无依赖) 的第三方大型类库等,可以通过这个字段来配置,以提高整体的构建速度。
使用 noParse 进行忽略的模块文件中不能使用 import、require、define 等导入机制。

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
// ...
module: {
noParse: /jquery|lodash/, // 正则表达式

// 或者使用 function
noParse(content) {
return /jquery|lodash/.test(content)
},
}
}

配置plugin:

(这里只介绍几个常用插件)

DefinePlugin

DefinePlugin 是 webpack 内置的插件,可以使用 webpack.DefinePlugin 直接获取。
这个插件用于创建一些在编译时可以配置的全局常量,这些常量的值我们可以在 webpack 的配置中去指定,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
// ...
plugins: [
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true), // const PRODUCTION = true
VERSION: JSON.stringify('5fa3b9'), // const VERSION = '5fa3b9'
BROWSER_SUPPORTS_HTML5: true, // const BROWSER_SUPPORTS_HTML5 = 'true'
TWO: '1+1', // const TWO = 1 + 1,
CONSTANTS: {
APP_VERSION: JSON.stringify('1.1.2') // const CONSTANTS = { APP_VERSION: '1.1.2' }
}
}),
],
}

在应用中访问变量:

1
2
console.log("Running App version " + VERSION);
if(!BROWSER_SUPPORTS_HTML5) require("html5shiv");

上面配置的注释已经简单说明了这些配置的效果,这里再简述一下整个配置规则。

  • 如果配置的值是字符串,那么整个字符串会被当成代码片段来执行,其结果作为最终变量的值,如上面的 “1+1”,最后的结果是 2
  • 如果配置的值不是字符串,也不是一个对象字面量,那么该值会被转为一个字符串,如 true,最后的结果是 ‘true’
  • 如果配置的是一个对象字面量,那么该对象的所有 key 会以同样的方式去定义
    这样我们就可以理解为什么要使用 JSON.stringify() 了,因为 JSON.stringify(true) 的结果是 ‘true’,JSON.stringify(“5fa3b9”) 的结果是 “5fa3b9”。

社区中关于 DefinePlugin 使用得最多的方式是定义环境变量,例如 PRODUCTION = true 或者 DEV = true 等。部分类库在开发环境时依赖这样的环境变量来给予开发者更多的开发调试反馈,例如 react 等。

【建议使用 process.env.NODE_ENV: … 的方式来定义 process.env.NODE_ENV,而不是使用 process: { env: { NODE_ENV: … } } 的方式,因为这样会覆盖掉 process 这个对象,可能会对其他代码造成影响。】

copy-webpack-plugin

1
2
3
4
5
6
7
8
9
10
11
12
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
// 有些文件没经过 webpack 处理,但是我们希望它们也能出现在 build 目录下,这时就可以使用 CopyWebpackPlugin 来处理了
plugins: [
new CopyWebpackPlugin([
{ from: 'src/file.txt', to: 'build/file.txt', }, // 顾名思义,from 配置来源,to 配置目标路径
{ from: 'src/*.ico', to: 'build/*.ico' }, // 配置项可以使用 glob
// 可以配置很多项复制规则
]),
],
}

extract-text-webpack-plugin

把依赖的 CSS 分离出来成为单独的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const ExtractTextPlugin = require('extract-text-webpack-plugin')

module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/,
// 因为这个插件需要干涉模块转换的内容,所以需要使用它对应的 loader
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: 'css-loader',
}),
},
],
},
plugins: [
// 引入插件,配置文件名,这里同样可以使用 [hash]
new ExtractTextPlugin('index.css'),
],
}

在上述的配置中,我们使用了 index.css 作为单独分离出来的文件名,但有的时候构建入口不止一个,extract-text-webpack-plugin 会为每一个入口创建单独分离的文件,因此最好这样配置:

1
2
3
plugins: [
new ExtractTextPlugin('[name].css’), // 这样确保在使用多个构建入口时,生成不同名称的文件。
],

ProvidePlugin

ProvidePlugin 也是一个 webpack 内置的插件,我们可以直接使用 webpack.ProvidePlugin 来获取。
该组件用于引用某些模块作为应用运行时的变量,从而不必每次都用 require 或者 import,其用法相对简单:

1
2
3
4
5
6
7
8
9
10
new webpack.ProvidePlugin({
identifier: 'module',
// ...
})

// 或者
new webpack.ProvidePlugin({
identifier: ['module', 'property'], // 即引用 module 下的 property,类似 import { property } from 'module'
// ...
})

在你的代码中,当 identifier 被当作未赋值的变量时,module 就会被自动加载了,而 identifier 这个变量即 module 对外暴露的内容。

注意,如果是 ES 的 default export,那么你需要指定模块的 default 属性:identifier: [‘module’, ‘default’],。

IgnorePlugin

也是一个 webpack 内置的插件,可以直接使用 webpack.IgnorePlugin 来获取。

这个插件用于忽略某些特定的模块,让 webpack 不把这些指定的模块打包进去。例如我们使用 moment.js,直接引用后,里边有大量的 i18n 的代码,导致最后打包出来的文件比较大,而实际场景并不需要这些 i18n 的代码,这时我们可以使用 IgnorePlugin 来忽略掉这些代码文件,配置如下:

1
2
3
4
5
6
module.exports = {
// ...
plugins: [
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]
}

IgnorePlugin 配置的参数有两个,第一个是匹配引入模块路径的正则表达式,第二个是匹配模块的对应上下文,即所在目录名。

更好地使用 webpack-dev-server

webpack-dev-server 的基础使用

webpack-dev-server 是一个 npm package,本质上也是调用 webpack,4.x 版本要指定 mode
建议把 webpack-dev-server 作为开发依赖安装,然后使用 npm scripts 来启动,

1
npm install webpack-dev-server --save-dev
1
2
3
4
5
6
{
// ...
"scripts": {
"start": "webpack-dev-server --mode development” // npm run start
}
}

webpack-dev-server 默认使用 8080 端口

使用了 html-webpack-plugin 来构建 HTML 文件,并且有一个 index.html 的构建结果,那么直接访问 http://localhost:8080/ 就可以看到 index.html 页面了。如果没有 HTML 文件的话,那么 webpack-dev-server 会生成一个展示静态资源列表的页面。

webpack-dev-server 的配置

可以通过 devServer 字段来配置 webpack-dev-server,如端口设置、启动 gzip 压缩等,常用配置如下:

  • public 字段用于指定静态服务的域名,默认是 http://localhost:8080/ ,当你使用 Nginx 来做反向代理时,应该就需要使用该配置来指定 Nginx 配置使用的服务域名。

  • port 字段用于指定静态服务的端口,如上,默认是 8080,通常情况下都不需要改动。

  • publicPath 字段用于指定构建好的静态文件在浏览器中用什么路径去访问,默认是 /,例如,对于一个构建好的文件 bundle.js,完整的访问路径是 http://localhost:8080/bundle.js,如果你配置了 publicPath: ‘assets/‘,那么上述 bundle.js 的完整访问路径就是 http://localhost:8080/assets/bundle.js。可以使用整个 URL 来作为 publicPath 的值,如 publicPath: http://localhost:8080/assets/如果你使用了HMR,那么要设置 publicPath 就必须使用完整的 URL。【建议将 devServer.publicPath 和 output.publicPath 的值保持一致】

  • proxy 用于配置 webpack-dev-server 将特定 URL 的请求代理到另外一台服务器上。当你有单独的后端开发服务器用于请求 API 时,这个配置相当有用。例如:

    1
    2
    3
    4
    5
    6
    proxy: {
    '/api': {
    target: "http://localhost:3000", // 将 URL 中带有 /api 的请求代理到本地的 3000 端口的服务上
    pathRewrite: { '^/api': '' }, // 把 URL 中 path 部分的 `api` 移除掉
    },
    }

    proxy 功能是使用 http-proxy-middleware 来实现的,更详细的 proxy 配置,可以参考官方文档 http-proxy-middleware。

  • contentBase 用于配置提供额外静态文件内容的目录,之前提到的 publicPath 是配置构建好的结果以什么样的路径去访问,而 contentBase 是配置额外的静态文件内容的访问路径,即那些不经过 webpack 构建,但是需要在 webpack-dev-server 中提供访问的静态资源(如部分图片等)。推荐使用绝对路径,【publicPath 的优先级高于 contentBase。】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 使用当前目录下的 public
    contentBase: path.join(__dirname, "public")

    // 也可以使用数组提供多个路径
    contentBase: [path.join(__dirname, "public"), path.join(__dirname, "assets")]
    * before 和 after 配置用于在 webpack-dev-server 定义额外的中间件,如
    before(app){
    app.get('/some/path', function(req, res) { // 当访问 /some/path 路径时,返回自定义的 json 数据
    res.json({ custom: 'response' })
    })
    }
  • before 在 webpack-dev-server 静态资源中间件处理之前,可以用于拦截部分请求返回特定内容,或者实现简单的数据 mock。

  • after 在 webpack-dev-server 静态资源中间件处理之后,比较少用到,可以用于打印日志或者做一些额外处理。

webpack-dev-middleware

中间件,简而言之就是在 Express 之类的 Web 框架中实现各种各样功能(如静态文件访问)的这一部分函数。多个中间件可以一起协同构建起一个完整的 Web 服务器。
webpack-dev-middleware 就是在 Express 中提供 webpack-dev-server 静态服务能力的一个中间件。
首先:npm install webpack-dev-middleware –save-dev
接着:创建一个 Node.js 服务的脚本文件,如 app.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const webpack = require('webpack')
const middleware = require('webpack-dev-middleware')
const webpackOptions = require('./webpack.config.js') // webpack 配置文件的路径

// 本地的开发环境默认就是使用 development mode
webpackOptions.mode = 'development'

const compiler = webpack(webpackOptions)
const express = require('express')
const app = express()

app.use(middleware(compiler, {
// webpack-dev-middleware 的配置选项
}))

// 其他 Web 服务中间件
// app.use(...)

app.listen(3000, () => console.log('Example app listening on port 3000!'))

然后:用node.js运行该文件:
node app.js # 使用刚才创建的 app.js 文件

使用 webpack-dev-server 的好处是相对简单,直接安装依赖后执行命令即可,而使用 webpack-dev-middleware 的好处是可以在既有的 Express 代码基础上快速添加 webpack-dev-server 的功能,同时利用 Express 来根据需要添加更多的功能,如 mock 服务、代理 API 请求等。

其实 webpack-dev-server 也是基于 Express 开发的,前面提及的 webpack-dev-server 中 before 或 after 的配置字段,也可以用于编写特定的中间件来根据需要添加额外的功能

实现一个简单的 mock 服务

而 webpack-dev-server 的 before 或 proxy 配置,又或者是 webpack-dev-middleware 结合 Express,都可以帮助我们来实现简单的 mock 服务。

基于 Express app 对webpack-dev-server实现一个简单 mock 功能的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.export = function mock(app) {
app.get('/some/path', (req, res) => {
res.json({ data: '' })
})

// ... 其他的请求 mock
// 如果 mock 代码过多,可以将其拆分成多个代码文件,然后 require 进来
}
然后应用到配置中的 before 字段:
const mock = require('./mock')

// ...
before(app) {
mock(app) // 调用 mock 函数
}

mock 函数同样可以应用到 Express 中去,提供与 webpack-dev-middleware 同样的功能。

由于 app.get(‘’, (req, res) => { … }) 的 callback 可以拿到 req 请求对象,其实可以根据请求参数来改变返回的结果,即通过参数来模拟多种场景的返回数据来协助测试多种场景下的代码应用。

当你单独实现或者使用一个 mock 服务时,你可以通过 proxy 来配置部分路径代理到对应的 mock 服务上去,从而把 mock 服务集成到当前的开发服务中去,相对来说也很简单。

当你和后端开发进行联调时,亦可使用 proxy 代理到对应联调使用的机器上,从而可以使用本地前端代码的开发环境来进行联调。当然了,连线上环境的异常都可以这样来尝试定位问题。

开发和生产环境的构建配置差异

问题:
webpack 4.x 和 3.x 如何在配置文件中区分环境来应用不同的配置选项(4.x 使用 mode 参数,3.x 使用 Node.js 的 process.env.NODE_ENV),
如何在应用代码运行时携带当前构建环境的相关信息,
以及如何利用 webpack-merge 这个工具来更好地维护不同构建环境中对应的构建需求配置。

当你指定使用 production mode 时,默认会启用各种性能优化的功能,包括构建结果优化以及 webpack 运行性能优化,而如果是 development mode 的话,则会开启 debug 工具,运行时打印详细的错误信息,以及更加快速的增量编译构建。

虽然 webpack 的 mode 参数已经给我们带来了一些很方便的环境差异化配置,但是针对一些项目情况,例如使用 css-loader 或者 url-loader 等,不同环境传入 loader 的配置也不一样,而 mode 并没有帮助我们做这些事情,因此有些配置还是需要手动区分环境后来进行调整。

在配置文件中区分 mode

webpack 4.x 的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package.json中:
"scripts": {
"build": "webpack --mode development",
"develop": "webpack --mode production"
}

webpack配置中:
module.exports = (env, argv) => ({
// ... 其他配置
optimization: {
minimize: false,
// 使用 argv 来获取 mode 参数的值, 之前webpack配置中直接对外暴露一个 JS 对象的方式无法获得,通过对外暴露一个函数,在函数中可以获取。
minimizer: argv.mode === 'production' ? [
new UglifyJsPlugin({ /* 你自己的配置 */ }),
// 仅在我们要自定义压缩配置时才需要这么做
// mode 为 production 时 webpack 会默认使用压缩 JS 的 plugin
] : [],
},
})

webpack 3.x中:(业界现在估计使用 webpack 3.x 版本的居多)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package.json中:
{
"scripts": {
"build": "NODE_ENV=production webpack",
"develop": "NODE_ENV=development webpack-dev-server"
}
}

webpack配置中:
const config = {
// ... webpack 配置
}

if (process.env.NODE_ENV === 'production') {
// 生产环境需要做的事情,如使用代码压缩插件等
config.plugins.push(new UglifyJsPlugin())
}

module.exports = config

运行时的环境变量

webpack 4.x做法:
我们使用 webpack 时传递的 mode 参数,是可以在我们的应用代码运行时,通过 process.env.NODE_ENV 这个变量获取的。这样方便我们在运行时判断当前执行的构建环境,使用最多的场景莫过于控制是否打印 debug 信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default function log(...args) {
if (process.env.NODE_ENV === 'development' && console && console.log) {
console.log.apply(console, args)
}
}

webpack 3.x做法:
module.exports = {
// ...
// webpack 的配置

plugins: [
new webpack.DefinePlugin({
// webpack 3.x 的 process.env.NODE_ENV 是通过手动在命令行中指定 NODE_ENV=... 的方式来传递的
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
}),
],
}

常见的环境差异配置

  • 生产环境可能需要分离 CSS 成单独的文件,以便多个页面共享同一个 CSS 文件
  • 生产环境需要压缩 HTML/CSS/JS 代码
  • 生产环境需要压缩图片
  • 开发环境需要生成 sourcemap 文件
  • 开发环境需要打印 debug 信息
  • 开发环境需要 live reload 或者 hot reload 的功能

webpack 4.x 的 mode 已经提供了上述差异配置的大部分功能,mode 为 production 时默认使用 JS 代码压缩,而 mode 为 development 时默认启用 hot reload,等等。这样让我们的配置更为简洁,我们只需要针对特别使用的 loader 和 plugin 做区分配置就可以了。

webpack 3.x 版本还是只能自己动手修改配置来满足大部分环境差异需求,所以如果你要开始一个新的项目,建议直接使用 webpack 4.x 版本。

拆分配置:

当配置越来越复杂时我们可以把 webpack 的配置按照不同的环境进行拆分,运行时直接根据环境变量加载对应的配置即可。基本的划分如下:
webpack.base.js:基础部分,即多个文件中共享的配置
webpack.development.js:开发环境使用的配置
webpack.production.js:生产环境使用的配置
webpack.test.js:测试环境使用的配置

通过webpack-merge对拆分进行合并:
demo:

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
webpack.base.js
module.exports = {
entry: '...',
output: {
// ...
},
resolve: {
// ...
},
module: {
// 这里是一个简单的例子,后面介绍 API 时会用到
rules: [
{
test: /\.js$/,
use: ['babel'],
},
],
// ...
},
plugins: [
// ...
],
}

webpack.development.js 需要添加 loader 或 plugin,就可以使用 webpack-merge 的 API
const { smart } = require('webpack-merge')
const webpack = require('webpack')
const base = require('./webpack.base.js')

module.exports = smart(base, {
module: {
rules: [
// 用 smart API,当这里的匹配规则相同且 use 值都是数组时,smart 会识别后处理
// 和上述 base 配置合并后,这里会是 { test: /\.js$/, use: ['babel', 'coffee'] }
// 如果这里 use 的值用的是字符串或者对象的话,那么会替换掉原本的规则 use 的值
{
test: /\.js$/,
use: ['coffee'],
},
// ...
],
},
plugins: [
// plugins 这里的数组会和 base 中的 plugins 数组进行合并
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
}),
],
})

Hot Module Replacement

早先时候的概念:Hot Reloading——当代码变更时通知浏览器刷新页面
HMR 可以理解为增强版的 Hot Reloading ——不用整个页面刷新,而是局部替换掉部分模块代码并且使其生效。
好处:HMR 既避免了频繁手动刷新页面,也减少了页面刷新时的等待,可以极大地提高前端页面开发效率。

配置HMR

安装好 webpack-dev-server后,webpack中添加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
const webpack = require('webpack')
module.exports = {
// ...
devServer: {
hot: true // dev server 的配置要启动 hot,或者在命令行中带参数开启
},
plugins: [
// ...
new webpack.NamedModulesPlugin(), // 用于启动 HMR 时可以显示模块的相对路径
new webpack.HotModuleReplacementPlugin(), // Hot Module Replacement 的插件
],
}

webpack 内部运行时,会维护一份用于管理构建代码时各个模块之间交互的表数据,webpack 官方称之为 Manifest,其中包括入口代码文件和构建出来的 bundle 文件的对应关系。

相关原理api自行翻看文档分析