webpack4.0 从入门到进阶三

上两篇博客简单总结了webpack4.0的一些基础配置,写完之后对webpack有了一个入门的认识。这两天又继续对遗留的几个知识点进行了梳理,如两种开发模式的配置流程,pathpublicPath的区别与作用等。但是看一些关于webpack运行原理的博客时还是会一脸懵逼,完全不知所云,而且目前来讲我并没有真正的从0配置一个webpack的项目,所有的这些知识点还都没进入实践去检验。接下来写完这一两篇博客后,会着手开始react框架的学习,到时候结合项目,再去深入的理解webpack

生产模式&&开发模式

现代前端工程项目,不管是使用angular-cli还是vue-cli来搭建,都会有本地开发和线上生产两种模式,命令行运行npm run dev即可在本地起一个服务,方便本地开发调试。之前讲webpack4.0新增了mode模式,之后会简化两种模式的配置,但是我们还是先来看一下4.0之前是如何配置两种模式的配置文件的,这里以vue为例。

vue的最佳实践

vue-cli脚手架生成的工程项目结构非常清晰,值得反复看,反复理解,它关于生产和开发模式的配置主要是这些文件:

webpack.base.conf.js中是一些公共的配置,然后通过webpack-merge把这些公共配置项和环境特定的配置项 merge 起来,成为一个完整的配置项。比如在webpack.dev.conf.js中:

1
2
3
4
5
6
7
'use strict'
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')

const devWebpackConfig = merge(baseWebpackConfig, {
...
})

通过这种方式,开发和生产两种模式就可以有各种独有的wenpack配置项,更关键的是,每个配置中用 webpack.DefinePlugin 向代码注入了 NODE\_ENV 这个环境变量。这个变量在不同环境下有不同的值,比如 dev 环境下就是 development。这些环境变量的值是在 config 文件夹下的配置文件中定义的。Webpack 首先从配置文件中读取这个值,然后注入。比如这样:

1
2
3
4
5
6
//webpack.dev.conf.js
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env.js')
}),
]
1
2
3
4
5
6
7
8
//dev.env.js
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})

这样我们就可以根据当前环境做判断,执行不同逻辑的代码,比如这样:

1
2
3
4
5
6
7
8
//utils.js
// 根据环境使用不同资源地址assetsSubDirectory
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}

这种区分不同的环境,并给环境变量设置不同的值的实践,让我们开启了编译时按环境对代码进行针对性优化的可能。

模拟4.0之前

看的再多不如手写一遍。我们先来手动实现vue的模式。首先我们安装两个依赖:

1
npm i webpack-merge uglifyjs-webpack-plugin -D

然后新建webpack.dev.conf.jswebpack.prod.conf.js两个文件,同时将webpack.conf.js修改为webpack.base.conf.js:

我们将devServer配置移动到webpack.dev.conf.js中,同时新增一个devtool配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const path = require('path');
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.base.conf')

module.exports = merge(baseWebpackConfig, {
mode: 'development',
devServer: {//配置此静态文件服务器,可以用来预览打包后项目
inline:true,//打包后加入一个websocket客户端
contentBase: path.resolve(__dirname, 'dist'),//开发服务运行时的文件根目录
host: 'localhost',//主机地址
port: 4200,//端口号
compress: true,//开发服务器是否启动gzip等压缩
open:true, // 自动打开浏览器
},
devtool: 'cheap-module-eval-source-map'
})

使用webpack-merge合并基础配置和开发配置,这里就简单这么配置。接下来配置一下webpack.prod.conf.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.base.conf.js');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = merge(baseWebpackConfig, {
mode: 'production',
devtool: '#source-map',
plugins: [
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: true,
parallel: true
})
]
})

这里我们配置了一个不同的devtool配置,然后新引入一个uglifyjs-webpack-plugin插件,用来压缩混淆js文件。最后我们为了方便测试效果,将package.json中的命令这么修改:

1
2
3
4
5
6
...  
"scripts": {
"dev": "webpack --config webpack.dev.conf.js",
"build": "webpack --config webpack.prod.conf.js"
},
...

接下来我们分别运行npm run devnpm run build,观察生成的dist文件夹有什么不同:

npm run dev

npm run build

从这个简单的实践我们就实现了一个很粗糙的运行模式拆分。剩下的就是针对具体的项目需求来编写更加强大的webpack配置了,当然还是强烈推荐去看vue脚手架生成的项目结构,越看越觉得有收获。

webpack4.0模式初探

webpack4.0新增了mode配置,可选值有development/production/none,借用官方文档:

选项 配置
development 会将 process.env.NODE_ENV 的值设为 development。启用 NamedChunksPluginNamedModulesPlugin
production 会将 process.env.NODE_ENV 的值设为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPluginUglifyJsPlugin.
none 不选用任何默认优化选项

在不同的模式下,webpack会自动执行不同的插件:

mode: development

1
2
3
4
5
6
7
8
9
// webpack.dev.conf.js
module.exports = {
+ mode: 'development'
- plugins: [
- new webpack.NamedModulesPlugin(),
- new webpack.NamedChunksPlugin(),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
- ]
}

mode: production

1
2
3
4
5
6
7
8
9
10
// webpack.production.config.js
module.exports = {
+ mode: 'production',
- plugins: [
- new UglifyJsPlugin(/* ... */),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
- new webpack.optimize.ModuleConcatenationPlugin(),
- new webpack.NoEmitOnErrorsPlugin()
- ]
}

mode: none

1
2
3
4
5
6
// webpack.custom.config.js
module.exports = {
+ mode: 'none',
- plugins: [
- ]
}

如果你想要根据 webpack.config.js 中的 mode 变量去影响编译行为,那你必须将导出对象,改为导出一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var config = {
entry: './app.js'
//...
};

module.exports = (env, argv) => {

if (argv.mode === 'development') {
config.devtool = 'source-map';
}

if (argv.mode === 'production') {
//...
}

return config;
};

但是这个选项的加入,如何优雅的跟之前拆分文件的形式结合,还得看社区大神们的整合方式,不管怎样,以后遇到心里就有底了。

path && publicPath

先来看一下现在我们output配置:

1
2
3
4
5
6
7
8
9
10
11
const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: "[chunkhash].bundle.js",
publicPath: '/'
},
...
}

output配置中path指定了webpack打包后生成的文件输入到哪里,我们之前运行npm run build的时候,文件都是打包到了这里,这个比较好理解,不过需要注意的是,该路径必须为绝对路径,所以我们使用了path.resolve来输出。

简单了说完了,比较迷惑的publicPath来了,这个路径指什么呢?刚开始我也比较迷惑,path已经配置了,还要这个publicPath干什么?反正你静态资源(cssimage)都是以此为基准嘛,等等,真的是这样吗?

我们先来运行一下npm run dev,注意,这时候我们把命令调回到webpack-dev-server,同时,我们设置outputpublicPath的值为"/",该值不设置的话默认为空字符串。

1
"dev": "webpack-dev-server --config webpack.dev.conf.js"

服务启动后,打开控制台,观察htmlcssimage的加载路径:

这里无法一次看完,手动写一下,第一个localhostdocument文件路径是http://localhost:4200/,第二个style.cssstylesheet文件路径是http://localhost:4200/static/css/style.css?4cb..,第三个wp7.pngpng文件路径是http://localhost:4200/static/img/wp7.png

这里我们发现,所有的静态资源文件路径都是基于/,即我们设置的publicPath,这个配置指定了你上传所有打包文件的位置(相对于服务器根目录),不过要注意,有时候我们的项目可不一定就是在网站根目录下配置的啊,有时候我们做的项目可能在xxx.com/project/下面部署,如果你不相应的修改publicPath值为/project/,你会发现静态资源路径依然会基于/,即根目录,从而导致资源无法访问到。

我们来试一试,现在将publicPath改为"/project/",再来运行npm run dev,发现页面空白,显示Cannot GET/,控制台内也是404,这时候我们访问http://localhost:4200/project/,页面加载成功,再看控制台:

所有的资源路径都是在/project/下。

这里面其实涉及到了项目打包部署的问题,我对这方面了解的比较少,所以前面才会一直搞不明白这个publicPath是干什么的,最近翻了翻vue脚手架项目,又反复测试,才渐渐有点理解。不过这里面确实需要不断的实践才能更好的理解,就如官方文档中举的例子:

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
//...
output: {
// One of the below
publicPath: 'https://cdn.example.com/assets/', // CDN(总是 HTTPS 协议)
publicPath: '//cdn.example.com/assets/', // CDN(协议相同)
publicPath: '/assets/', // 相对于服务(server-relative)
publicPath: 'assets/', // 相对于 HTML 页面
publicPath: '../assets/', // 相对于 HTML 页面
publicPath: '', // 相对于 HTML 页面(目录相同)
}
};

上面还涉及到了资源在CDN的情况,理解了这些,以后就可以愉快的配合运维同学一起部署上线了,再遇到问题,查看官方文档就有迹可循了。

webpack系列暂时告一段落,但是绝不是结束,后续的运行原理还等着我去研究和学习,到现在为止,感觉继续单纯的纸上谈兵已经不能有较大的提升了,接下来就是到项目中去,结合项目进行学习,巩固和调整。

参考文章:官方中文文档Webpack 4 配置最佳实践