前端进阶之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