Webpack 配置

记录 webpack 常用的配置。

解析路径

webpack 依赖 enhanced-resolve 来解析代码模块的路径。
在 webpack 配置中,和模块路径解析相关的配置都在 resolve 字段下:

1
2
3
4
5
6
7
module.exports = {
resolve: {
alias: {
utils: path.resolve(__dirname, 'lib/utils')
}
}
}

resolve.alias

当我们有某个模块,引用比较多,例如import './lib/utils/helper.js',这种引用比较麻烦,我们可以通过配置别名来应用:

1
2
3
alias: {
utils: path.resolve(__dirname, 'lib/utils')
}

这种配置是模糊匹配,模块路径中携带了utils就可以,然后就可以直接引用:

1
import 'utils/helper.js'

如果需要进行精确匹配可以使用:

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

Resolve Alias官方文档

resolve.extensions

1
2
3
resolve: {
extensions: [".wasm", ".mjs", ".js", ".json", ".jsx"],
},

上面的示例中,数组extensions里的顺序代表匹配后缀的优先级,例如,src目录下有index.jsxindex.js

1
import App from './src/index'

会优先匹配index.jsextensions数组中没有的后缀,则不会匹配。

resolve.modules

对于通过npm安装的第三方模块,webpack的加载机制与nodejs类似,它会搜索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.mainFiles

当目录下没有 package.json 文件时,会默认使用目录下的index.js,可以使用resolve.mainFiles字段,默认配置是:

1
2
3
resolve: {
mainFiles: ['index'],
},

通常情况下无须修改这个配置。

loader 配置

loader 用于处理不同的文件类型。

test

通过module.rules 字段来配置相关的规则:

1
2
3
4
5
6
7
8
9
10
11
12
module: {
// ...
rules: [
{
test: /\.jsx?/,
include: [
path.resolve(__dirname, 'lib') // 指定需要经过 loader 处理的文件路径
],
use: 'babel-loader',
},
],
}

两个最关键的因素: 匹配条件, 使用的 loader

  • test属性,用于匹配文件路径的正则表达式,通常都是匹配文件类型后缀。include 也属于条件
  • use属性,指定使用哪个loader

匹配条件通常都使用请求资源文件的绝对路径来进行匹配,在官方文档中称为resource
上面的代码中的 testincluderesource.testresource.include 的简写,你也可以这么配置:

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

其他匹配条件

一般配置 loader 的匹配条件时,配置test字段就足够了,但是有时需要一些特殊的配置,webpack 提供了多种匹配条件:

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

匹配条件的值可以是,字符串,正则表达式,函数( (path) => boolean,返回 true 表示匹配 ),数组,对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module: {
// ...
rules: [
{
test: /\.jsx?/,
include: [
path.resolve(__dirname, 'lib') // 指定需要经过 loader 处理的文件路径
],
not: [
(value) => { /* ... */ return true; },
]
use: 'babel-loader',
},
],
}

use

上面的例子中提到,使用use字段指定使用哪个loader,use的值除了可以是字符串,还可以是数组,或者对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rules: [
{
test: /\.less/,
use: [
'style-loader', // 使用字符串指定 loader
{
loader: 'css-loader',
options: {
importLoaders: 1
}
} // 使用对象指定 loader,可以传递 loader 配置等
],
}
],

如果只需要一个loader,也可以这样:use: { loader: 'babel-loader', options: { ... } }

loader 执行顺序

两种情况:

在一个rule中配置了多个loader,那么执行顺序从是最后配置的loader开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
rules: [
{
test: /\.less/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1
}
},
{
loader: 'less-loader',
options: {
noIeCompat: true
}
},
],
}
]

上面的示例,一个style.less文件会经过 less-loader => css-loader => style-loader 处理,然后打包。

在不同的rule,匹配了同种类型的文件:

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

上面的示例中,多了一个enforce字段,这个字段作用就是保证loader的执行顺序。pre代表了前置,保证了eslint-loaderbabel-loader前执行。

enforce字段有下面两种类型:

  • pre,表示前置类型的loader
  • post,表示后置类型的loader

没有enforce字段,就是普通类型。
这些loader的执行顺序为 前置 => 普通 => 后置

noParse

noParse 让 webpack 忽略对某些模块文件的解析。可以用来优化 webpack 的构建速度。
noParse类型可以是正则表达式,也可以一个函数。

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)
},
}
}

被忽略的模块中,不应该有importdefinerequire等模块化语句,包含这些语句的模块需要 webpack 解析,否则无法再浏览器端运行。

配置 plugin

插件是 webpack 的支柱功能。插件解决了loader无法实现的事情。

常用的插件

DefinePlugin

DefinePlugin 是内置的插件,使用webpack.DefinePlugin直接获取。
DefinePlugin用于创建一些在编译时可以配置的全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
// ...
plugins: [
new webpack.DefinePlugin({
ENV: 'production',
TEST: '1+1',
CONSTANTS: {
VERSION: JSON.stringify('1.0.0')
}
}),
],
}

配置好之后,可以在代码中直接访问:

1
console.log("ENV: ", ENV);

配置规则:

  • 如果配置的值是字符串,那么字符串会被当成代码片段来执行,其结果作为最终变量的值,如上面的TEST: "1+1",最后的结果是 2
  • 如果不是字符串,也不是一个对象字面量,那么该值会被转为一个字符串,如 true,最后的结果是 'true'
  • 如果一个对象字面量,那么该对象的所有key会以同样的方式去定义

ProvidePlugin

内置的插件,使用webpack.ProvidePlugin来获取。
用于引用某些模块作为应用运行时的变量,从而不需要每次使用require或者import去引用:

1
2
3
4
5
6
7
8
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
"window.jQuery": "jquery",
identifier: 'module',
})
]

上面的示例,将$jQuery两个变量都指向对应的jquery模块,然后在源码中可以使用下面的方式调用:

1
2
$('#test');
jQuery('#test');

上面的情况在使用bootstrap时就需要配置,否则后报错jQuery is not a function
Angular会寻找window.jQuery来决定jQuery是否存在。

上面的示例,当 identifier 被当作未赋值的变量时,module 就会被自动加载,而 identifier 这个变量即 module 对外暴露的内容。
注意,如果是ES6default export,那么需要指定模块的default属性:identifier: ['module', 'default']。.

IgnorePlugin

内置的插件,使用webpack.IgnorePlugin来获取。
用于忽略某些特定的模块,让 webpack 不把这些指定的模块打包进去。
例如moment.js,里边有大量的i18n的代码,这下locale会导致打包出来的文件较大,实际上我们并不需要这些i18n的代码,这时可以使用IgnorePlugin来忽略掉这些代码文件:

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

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

extract-text-webpack-plugin

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: 'css-loader',
fallback: 'style-loader'
})
}
]
},
plugins: [
new ExtractTextPlugin({
filename: '[name].css',
allChunks: true
})
]

copy-webpack-plugin

用来复制文件的插件:

1
2
3
new CopyWebpackPlugin([
{ from: 'src/assets/favicon.ico', to: 'favicon.ico', }, // from 配置来源,to 配置目标路径
]),

更多插件plugins in awesome-webpack

webpack-dev-server

webpack-dev-server可以快速的启动一个开发环境,支持热重载。
webpack-dev-server官方文档

安装

1
2
3
4
5
6
//全局
npm install webpack-dev-server -g
//安装到项目中
npm install webpack-dev-server --save-dev

使用

webpack-dev-server本质上也是调用 webpack ,在4.x需要指定mode,添加npm脚本:

1
2
3
"scripts": {
"start:dev": "webpack-dev-server --mode development"
}

webpack-dev-server默认端口是8080,运行npm run start:dev,就可以访问http://localhost:8080/了。如果使用了html-webpack-plugin来构建HTML文件,
并且有一个index.html的构建结果,就可以看到index.html页面,如果没有HTML文件的话,会生成一个展示静态资源列表的页面。

index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>ChatOps Configuration</title>
<link rel="stylesheet" type="text/css" href="/dist/app.css">
</head>
<body>
<div id="app" class="container">
</div>
<script type="text/javascript" src="/dist/app.js"></script>
</body>
</html>

配置

可以在 webpack 的配置文件中通过devServer字段来配置webpack-dev-server

1
2
3
4
5
devServer: {
contentBase: path.join(__dirname, "dist"),
compress: true,
port: 9000
}

上面的示例,所有来自dist/目录的文件都做gzip压缩,dev server 的端口为9000

常用配置选项

  • public字段用于指定静态服务的域名,默认是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.publicPathoutput.publicPath的值最好保持一致。
  • proxy,代理,给特定URL的配置代理。例如:

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

    proxy功能基于http-proxy-middleware实现,http-proxy-middleware官方文档

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

    1
    2
    3
    4
    5
    // 使用当前目录下的 public
    contentBase: path.join(__dirname, "public")
    // 也可以使用数组提供多个路径
    contentBase: [path.join(__dirname, "public"), path.join(__dirname, "assets")]

    publicPath的优先级高于contentBase

  • beforewebpack-dev-server静态资源中间件处理之前,可以用于拦截部分请求返回特定内容,或者实现简单的数据mock

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    devServer: {
    before(app){
    app.get('/chatops/config/bots', function(req, res) {
    res.json({
    code: 200,
    data:[{
    HUBOT_NAME: 'test',
    status: 'init'
    }],
    msg: 'success'
    })
    })
    }
    }
  • afterwebpack-dev-server静态资源中间件处理之后,比较少用到,可以用于打印日志或者做一些额外处理。

mock server

mock的数据过多,可以创建一个mock.js文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = function(app){
app.get('/chatops/config/bots', function(req, res) {
res.json({
code: 200,
data:[{
HUBOT_NAME: 'test',
status: 'init'
}],
msg: 'success'
})
})
// ... 其他路由 mock
}

然后在配置文件中引入:

1
2
3
4
5
6
const mock = require('./mock.js');
devServer: {
before(app){
mock(app);
}
}

如果你单独使用了一个mock sever时,可以配置proxy将部分路径代理到对应的mock sever

联调时,也可以使用 proxy 代理到对应的服务上去。

webpack-dev-middleware

webpack-dev-middleware是一个Express中间件,可以把webpack-dev-serverExpress集成。
使用webpack-dev-middleware可以mock数据,方便开发。还可以实现代理API。

安装webpack-dev-middleware
1
npm install webpack-dev-middleware --save-dev
Express集成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const webpack = require('webpack')
const middleware = require('webpack-dev-middleware')
const webpackOptions = require('./build/webpack.base.config.js')
// 开发环境
webpackOptions.mode = 'development'
const compiler = webpack(webpackOptions)
const express = require('express')
const app = express()
app.use(middleware(compiler, {
// webpack-dev-middleware 的配置
}))
// app.use(...)
app.listen(8080, () => {
console.log('Start server on port 8080.')
})

然后运行:

1
node app.js