前端进阶之Webpack(一)
前言
不知不觉,工作两年了。
最近,参与搭建了一个微应用项目,由于使用的内部框架并不太成熟,而且是第一次参与构建大型项目,各种各样的坑,踩了个遍。
回想刚入行时,用vue-cli快速搭建项目,感觉搭项目也没啥难的,可谓是初生牛犊,不怕虎。
项目慢慢过渡到了迭代阶段,汇总一下热乎的知识,博客写了,就等于我没忘。
什么是Webpack?
在搭建项目的过程中,很多问题都可以总结成一个问题——兼容性问题。
剩下的问题,虽然五花八门,但始终围绕着“便捷”二字。
从作用上理解,Webpack是用来方便程序员开发,以及解决兼容性问题的工具。
从代码本质上理解,Webpack和其他依赖包没有区别,同样通过一个npm仓库进行管理,内部是nodejs语法。
不能少的node知识储备
node基础不错的朋友可以跳过这一节,但如果对node比较陌生的话,我建议还是看看,使用Webpack的过程中会用到。
模块化
一个依赖包就是一个模块,最基础的模块只需要两样东西:
- package.json:模块描述文件。
- [name].js:模块入口文件(模块的功能会被通过
module.exports导出)。

上传模块的步骤:
- 注册仓库账号,没有公司仓库的话,可以直接去官网www.npmjs.com/注册一个。
- npm adduser:登录账号。
- npm publish:上传模块至对应的仓库。

上传之后,模块就和我们常见的依赖包一样可以被下载。

下载后,通过require引入模块就可以使用内部功能,我在demo中只是简单抛出了一个对象{a: '111'},我们常用的依赖包功能自然会复杂的多。

Webpack多了不少文件,但本质和我的demo是一样的,是node编写的包,都属于用户编写的模块,而http、fs、path这些属于核心模块。
两者有什么区别?下面接着会讲。
模块引入
在node中引入模块,会经历三个步骤:
- 路径分析
- 文件定位
- 编译执行
一个用户编写的模块,通过require('node_test_demo_sunshaohe')引入项目,没有加任何路径和文件后缀,node会先认为它是一个node自带的核心模块,找不到的话,会一层一层向外循环遍历寻找同名的.js文件,还是找不到的话,会去寻找同名的.json文件,最后寻找同名的.node文件。
因此,外部引入一个.node或.json文件,最好写清楚路径和文件后缀。
核心模块在node源代码编译的过程中,就被编译成了二进制执行文件,而且在路径分析中会被优先判断,所以它的加载速度是最快的。
全局对象
由于自定义全局变量会造成环境污染,因此,需要尽可能利用node自带的全局对象。
__filename
__filename表示正在执行脚本的文件名(project/src/main.js),并附带绝对路径,如果在模块中,附带的是模块的路径(module/app.js)。
__dirname
__dirname表示执行脚本目录的绝对路径
process
在使用本地指令的时候,一般会用到process。
由于我做的是多语言项目,某些国家会有专门的定制版本,在启动指令中用不同的COUNTRY值来区分,在项目中可以用process.env.COUNTRY获取环境变量。
process.env一次只能获取一个环境变量,在想要一次获取多个环境变量的时候,可以使用process.argv,它会以数组的形式,返回所有环境变量。
除此之外,process还有许多其他的属性和方法,感兴趣的朋友可以自行了解。
核心模块
path
path是最常用的核心模块,用于处理文件路径。
path.join():用于连接路径,正确使用相应系统的分隔符,Unix是\,Windows是/。
eg:path.join('a', 'b', 'c')等价于a/b/c
path.resolve():用于解析路径,从右往左分析,最终得到一个绝对路径。
eg:path.resolve('/a/b', './c/d'),解析后得到/a/b/c/d
eg:path.resolve('/a/b', '/c/d'),解析后得到/c/d
eg:path.resolve('/a/b', '../c/d'),解析后得到/a/c/d
Webpack的打包流程
- 读取
Webpack的配置参数。 - 创建
Compiler对象并开始解析项目。 - 从入口开始解析,找到其依赖的模块,递归遍历分析,形成依赖关系树。
- 对不同文件类型的模块,使用对应的
loader进行编译,最终转换成JavaScript。 - 根据入口和依赖关系,输出成果物。
- 整个打包过程会发布事件,触发订阅了对应事件的
plugin。
配置参数包括webpack.config.js里的固定配置和指令中的变量,Webpack会整合后,传给Compiler。
以下是一个简易的Compliler类:
class Compiler(options) {
constructor(options) {
const {entry, output} = options; //options是配置参数
this.entry = entry;
this.output = output;
this.modules = [];
}
run(){} //Webpack打包
generate() {} //输出成果物
}
Webpack配置
Webpack默认会从webpack.config.js中读取配置参数,配置参数决定了Webpack会如何去打包,以及输出一个什么样的成果物。
context
基础目录context是一个绝对路径,Webpack解析相对路径时,会以context作为参考。
const path = require('path');
moduile.exports = {
context: path.resolve(__dirname, 'app')
}
entry
Webpack从入口entry开始打包,entry支持三种数据类型:
- String:
entry: './main.js' - Array:
entry: ['./main.js', './app.js'] - Object:
entry: {a: './main.js', b: './app.js'}
output
output配置影响成果物如何输出。
filename
output.filename配置输出文件的名称,如果只有一个输出文件,可以写成:
module.exports = {
output: {
filename: 'bundle.js'
}
}
有多个入口文件,需要输出多个文件时:
module.exports = {
output: {
filename: '[id][name][hash][chunkhash].js'
}
}
id:Chunk的唯一标识,从0开始。name:Chuunk的名称,由入口文件的名称决定。hash:Chunk唯一标识的hash值。chunkhash:Chunk内容的hash值。
hash和chunkhash的长度是可指定的,[hash: 8]代表取8位的hash值,默认是20位。
chunkFilename
output.chunkFilename配置无入口的Chunk在输出时的文件名称。

我用红框框住的就是无入口的Chunk,app.js则是入口文件的Chunk。
Webpack默认会把异步加载的组件提取出来,脱离入口依赖图,单独打包成一个Chunk。我们还可以根据需要制定拆分规则,具体配置后续会讲。
path
output.path配置输出文件的目录,必须是绝对路径。
const path = require('path');
module.exports = {
output: {
path: 'path.resolve(__dirname, 'dist')'
}
}
publicPath
output.publicPath配置线上资源的URL前缀。
const path = require('path');
module.exports = {
output: {
pubilicPath: 'http://example.com/assets/'
}
}
在项目中可以通过http://example.com/assets/image.png获取资源。
library和libraryTarget
当用Webpack去构建一个可以被其他项目导入使用的库时,就会用到library和libraryTarget。
简而言之,在一个项目中引用另一个项目的成果物。
ouput.library配置导出库的名称。
output.libraryTarget配置以何种方式导出库,共有七种:
umd:万能库,下面的每种库都有对应的使用方法,所有方法都可以用来调用umd库,不知道用什么的时候,用umd就对了。umd的兼容性自然是牺牲性能的,熟悉以后,做优化的时候,可以换成更合适的库。var:编写的库将通过var被赋值给通过library指定名称的变量。
module.exports = {
output: {
library: 'libraryName',
libraryTarget: 'var'
}
}
打包好的成果物,在另一个项目中使用:
// Webpack输出的代码
var LibraryName = lb_code;
// 使用库的方法
LibraryName.doSomething();
commonjs:编写的库将通过CommonJS规范导出。node是按照CommonJS规范编写的。
module.exports = {
output: {
library: 'libraryName',
libraryTarget: 'commonjs'
}
}
打包好的成果物,在另一个项目中使用:
// Webpack输出的代码
exports['LibraryName'] = lib_code;
// 使用库的方法
require('library-name-in-npm')['LibraryName'].doSomething();
commonjs2:编写的库将通过CommonJS2规范导出,与commonjs类似,在其基础上,可以通过module.exports的导出方式。
module.exports = {
output: {
library: 'libraryName',
libraryTarget: 'commonjs2'
}
}
打包好的成果物,在另一个项目中使用:
// Webpack输出的代码
module.exports = lib_code;
// 使用库的方法
require('library-name-in-npm')['LibraryName'].doSomething();
this:编写的库将通过this被赋值给通过library指定的名称。
module.exports = {
output: {
library: 'libraryName',
libraryTarget: 'this'
}
}
打包好的成果物,在另一个项目中使用:
// Webpack输出的代码
this['LibraryName'] = lib_code;
// 使用库的方法
this.LibraryName.doSomething();
window:编写的库将通过window被赋值给通过library指定的名称。
module.exports = {
output: {
library: 'libraryName',
libraryTarget: 'window'
}
}
打包好的成果物,在另一个项目中使用:
// Webpack输出的代码
window['LibraryName'] = lib_code;
// 使用库的方法
window.LibraryName.doSomething();
global:编写的库将通过global被赋值给通过library指定的名称。
module.exports = {
output: {
library: 'libraryName',
libraryTarget: 'global'
}
}
打包好的成果物,在另一个项目中使用:
// Webpack输出的代码
global['LibraryName'] = lib_code;
// 使用库的方法
global.LibraryName.doSomething();
optimization
optimization用于对打包结果进行优化,之前提到的拆包规则便是在这里配置。
minimizer
optimization.minimizer是一个数组,会执行用于代码压缩的插件。
const TerserPlugin = require('teser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
// 开启多进程进行打包
parallel: true;
terserOptions: {
// 代码压缩
compress: {
// 打包时自动去掉console.log
drop_console: true,
// 打包时自动去掉debugger
drop_debugger: true
}
}
})
]
}
}
splitChunks
optimization.splitChunks配置拆包规则。
module.exports = {
optimization: {
splitChunks: {
// 规则的适用对象,all表示所有,async表示异步(默认),initial表示同步
chunks: all,
// Chunk大小超过30kb时,才进行代码拆分
minSize: 30000,
// 如果Chunk大小超过50kb,那么Webpack会尝试对其进行拆分
maxSize: 50000,
// 一个模块至少要被引用3次,才会被单独打包成一个Chunk
minChunks: 3,
// 如果Chunk的异步请求数超过5个,那么Webpack会尝试对其进行拆分
maxAsyncRequests: 5,
// 入口点能被拆分的最大数量
maxInitialRequests: 1,
}
}
}
module
module配置处理模块的规则。
rules
module.rules是一个数组,读取Loader来解析对应类型的文件。
module.exports = {
module: {
rules: [
{
// 命中JavaScript文件
test: /\.js$/,
// 用babel-loader转换JavaScript文件
use: ['babel-loader'],
// 匹配范围是src下的文件
include: path.resolve(__dirname, 'src'),
// Loader的配置项
options: {
preset: ['es2015']
}
},
{
test: /\.scss$/,
// 使用一组Loader去处理scss文件
// 会先执行sass-loader,再执行css-loader,最后执行style-loader
// Webpack会按照配置从下到上,从右到左处理文件
use: ['style-loader', 'css-loader', 'sass-loader'],
// 搜索范围跳过node_modules下的文件
exclude: path.resolve(__dirname, 'node_modules')
}
]
}
}
resolve
resolve配置Webpack如何寻找模块对应的文件。
modules
resolve.modules是一个数组,告诉Webpack去哪些目录下寻找第三方模块。之前提到过Webpack加载非核心模块的时候,需要写路径,但平时项目里引用的时候,并没有写路径,是因为这里做了统一处理。
module.exports = {
resolve: {
modules: [path.resolve(__dirname, 'node_modules')]
}
}
alias
resolve.alias通过别名将原路径映射成一个新的路径。
module.exports = {
resolve: {
alias: {
// 项目里绝对路径常用的@便是在这里配置
'@': path.resolve(_dirname, 'src')
}
}
}
extensions
module.extensions是一个数组,用于拓展Webpack的文件支持项,之前说到过Webpack只能读取.js、.json、.node三种类型的文件,想要处理.ts、.vue、.jsx等类型的文件时,就需要做拓展。
module.exports = {
resolve: {
extensions: ['.ts', '.vue', '.jsx']
}
}
devtool
devtool用于配置souce-map的精细程度,精细程度越高,打出来的包也越大。souce-map是打包后运行中的代码与本地代码之间的一个映射,改bug时,通过映射可以知道是本地哪块的代码出了问题。
module.exports = {
// source-map/line-source-map/eval-source-map/hiden-source-map/cheap-source-map...
// 下面是我们项目采用的配置,大家可以作为参考,放弃列,映射精确到行。
devtool: 'cheap-module-eval-source-map'
}
devServer
只有安装了webpack-dev-server,通过devServer启服务时,配置才会生效。
proxy
proxy代理可以将浏览器到服务端的请求,转换成服务端与服务端之间的请求,绕过浏览器的同源性原则,从而避免跨域问题。
module.exports = {
devServer: {
proxy: 'http://localhost/demo:3000'
}
}
总结
本来想写一篇实战总结的,结果快写完了,才发现似乎成了一篇知识归纳,干脆拆成两篇。
转载自:https://juejin.cn/post/7136895529354526756