我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情
webpack
对很多人来说都很熟悉,因为日常工作中我们都会用到,例如vue-cli
这些脚手架,都是用webpack
搭建出来的,极大的方便了我们的开发体验。但同时我们又对它感到很陌生,因为我们很少去真正的用到它,得益于社区的完善,大多数时候我们只需要下载一些插件再照着文档改改配置文件,就能满足我们日常的开发需要,这也是造就了webpack
对很多开发者来说是个既熟悉又陌生的东西。尽管如此,我们仍有必要学习学习webpack
的知识,毕竟说面试官也要考,谁会跟钱过不去呢。
接下来本文就带大家走进webpack
的世界,虽不能让你成为webpack
方面的高手,但也希望让你能对webpack
有所了解,哪怕只是一小部分。
本文的示例及讲解基于webpack5.0
webpack是什么
在讲webpack
是什么之前,让我们先把时间倒回到我们最初接触前端开发的时候,一开始我们还没有用上什么脚手架或打包工具之类的时候,都是一个html
文件走天下,需要js
就新建个js
文件然后再来个script
将其引入,css
也是一样,总之就是在html
里一把梭。
但随着需求越来越多,代码量也越来越多,一个html
文件也越来越臃肿,各种引用的文件错综复杂,且要按照一定的顺序用script
标签来引入,稍一不小心就报错了,例如a.js
用到了jquery
的API,不小心将a.js
的引入放在jquery
之前就报错了。
到后来,node.js
出现了,node
拥有对文件的操作能力,各类基于node
的打包工具也随之出现,如本文所讲的webpack
,除此之外还有rollup
、gulp
等等。
回到webpack
是什么的问题上,简而言之webpeck
是一个用于现代 JavaScript 应用程序的静态模块打包工具,它从你的应用入口出发,递归查找你所用到的模块并构建出一个依赖关系图,最终将所有的模块打包成一个或多个bundles,他们都是浏览器所能识别的静态资源文件。
webpack核心概念
前置准备
开始认识webpack的核心概念之前,这里我们先创建一个webpack的demo,方便后续的讲解。
先创建一个名为webpack-demo
的文件夹,进入该文件夹:
// 初始化
npm inin -y
// 下载webpack和webpack-cli
npm i webpack webpack-cli --D
复制代码
接着我们在项目下新建一个src/index.js,创建后的项目结构如下:
入口entry
entry
指明webpack使用哪个模块作为开始,来作为其构建内部依赖的起点。
随便在src/index.js里添加点东西:
console.log('hello world')
复制代码
在package.json里增加一条命令:
"scripts": {
"build": "webpack"
}
复制代码
运行npm run build
后可以看到,src/index.js下的内容已经被输出到dist/main.js下。
webpack入口默认是./src/index.js
,当然也可以自己指定入口文件,这时候需要创建一个webpack.config.js
并设置entry。
输出output
output
是webpack构建后文件输出的位置,以及文件的命名。主要输出文件的默认值是./dist/main.js
,其他生成文件默认在./dist
下。当然也是可以通过在配置中指定output的。
const path = require('path')
module.exports = {
entry: './src/index.js', // 指定入口文件
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.[hash].js'
}
}
复制代码
这里我们在配置里修改了输出的文件名规则,在后面加上了一串hash字符串。path是node自带的一个模块,这里可以不用install。
模式mode
webpack的模式有三种:development、production、none,默认值为production。
- development:一些没有用到的方法变量等会被保留,production则会自动移除
- production:会进行代码压缩等优化操作
- none:不做任何处理
使用命令行设置mode:
"scripts": {
"build": "webpack --mode=development"
}
复制代码
或者设置配置文件的mode字段:
module.exports = {
mode: 'development'
}
复制代码
模块转换器loader
webpack自身只理解js和json文件,loader的作用便是处理其他类型的文件,并将它们转换成webpack能识别的模块。例如css-loader用于转化css内容,ts-loader用于将ts转换为js。
我们试着新建一个样式文件并将其引入:
运行npm run build
后会发现报错了:
因为webpack只理解js和json文件,当我们没有指定loader去解析style.css这个文件时,webpack就当成js文件或json文件去解析了,遇到css的代码自然就解析出错了。
所以这时候如果我们修改style.css里的代码为js内容会发现是可以正常构建成功的,虽然这并不符合我们的预期。
// style.css
const name = '张三'
复制代码
当然这并不是我们的初衷,我们需要的是可以正常解析css文件里的内容。webpack提供一个module对象,通过module下的rules
属性可以使用特定的loader对特定的文件进行转换。
rules里包含两个必须的属性:
- test:声明需要转换的文件类型,该值通常是一个正则表达式。
- use:声明转换时所要使用的loader。use可以是一个字符串也可以是一个数组,use为数组时的执行顺序为从右向左。
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: 'css-loader'
}
]
}
}
复制代码
上面我们添加一个css-loader
对css文件进行转换,重新npm run build
构建后查看输出文件可以发现style.css文件可以被正常转换了。
插件plugin
plugin可以在webpack构建流程中的某个时机注入扩展逻辑来改变构建结果。
插件通过plugins字段传入,plugins是一个数组。
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
plugins: [
new CleanWebpackPlugin()
]
}
复制代码
clean-webpack-plugin
插件可以在每次构建输出的时候清空dist文件夹,避免上次打包的东西遗留下来形成堆积。
手写一个loader
之前我们已经使用css-loader
对css文件做了转换,但我们还没有应用这个css文件的内容,接下来我们以编写一个style-loader
为例,实现将css文件内容插入到html使其生效。
编写的loader需要是一个函数,因为函数里的this指向的是webpack的上下文,所以该函数不能是箭头函数,否则会导致不能使用this访问webpack内部的一些属性和方法。
// loader/style-loader.js
function styleLoader(source) {
console.log('source===', source)
return source
}
module.exports = styleLoader
复制代码
loader函数接收的参数是源文件的内容,该函数必须返回处理后的结果。
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
{
// 引入我们自定义的loader
loader: path.resolve(__dirname, './loader/style-loader.js')
},
'css-loader'
]
}
]
}
}
复制代码
使用该loader并进行构建,可以看到source的内容便是我们css文件里的内容。
loader函数必须返回结果,除了使用return之外,也可以使用this上的callback方法。
function styleLoader(source) {
console.log('source===', source)
// return source
this.callback(null, source)
}
复制代码
callback接收4个参数:
- error:必填,必须是一个Error或null。
- content:必填,处理后要返回的结果。
- map:选填,sourceMap相关内容。
- meta:选填,传递给下一个loader的额外信息。
如果loader函数里有异步操作,可以使用this.async来获取callback再进行结果返回,async表明了loader将会异步的回调。
function styleLoader(source) {
console.log('source===', source)
let callback = this.async()
setTimeout(() => {
callback(null, source)
}, 2000)
}
复制代码
接下来我们创建一个html文件:
然后使用html-webpack-plugin
自动在dist目录下生成一个index.html并引入我们打包后的js文件。
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html' // 打包后的文件名
})
]
}
复制代码
重新构建后打开dist/index.html发现style.css的内容并没有生效,body的背景色并没有变成红色的。
html, body {
height: 100%;
}
body {
background-color: red;
}
复制代码
这是因为style.css文件只是被解析转换到js文件里去了,我们需要让他插入到index.html里的style标签里才能生效,所以接下来我们实现这个功能。
// src/loader/style-loader.js
function styleLoader() {}
styleLoader.pitch = function(remainingRequest) {
// remainingRequest为:D:\webpack-demo\node_modules\css-loader\dist\cjs.js!D:\webpack-demo\src\style.css
// 将绝对路径转为相对路径
const relativeRequest =
remainingRequest
.split('!')
.map((part) => this.utils.contextify(this.context, part))
.join('!')
// relativeRequest的结果为:../node_modules/css-loader/dist/cjs.js!./style.css
return `
import content from '!!${relativeRequest}';
const style = document.createElement('style');
style.innerHTML = content;
document.head.appendChild(style);
`
}
module.exports = styleLoader
复制代码
这里我们使用了一个空的loader函数,处理逻辑写在了pitch
里,这是为什么?pitch
又是什么?
我们知道loader是从右往左执行的,在我们这个例子里是css-loader
=>style-loader
,如果我们按照这种正常的执行顺序,在style-loader里我们拿到的是已经经过css-loader
处理过的js字符串,我们要的是css内容,所以这不符合我们的要求。
好在webpack提供了Pitch Loader
,它与正常的loader不同,执行顺序是从左往右的,webpack在执行loader(从右到左)之前,会先从左到右的调用loader的pitch方法。拿我们这里的例子来说,其执行顺序为:
如果pitch
里有返回结果,则会跳过剩余的loader,只执行pitch
对应loader的loader函数。如果style-loader的pitch
有返回结果,上面的执行链路则变成:
pitch函数的第一个参数remainingRequest可以用来获取loader链的剩余请求,将其转换为相对路径后是../node_modules/css-loader/dist/cjs.js!./style.css
。
随后我们在返回的可执行js字符串中将其进行import,中间的!是webpack的语法,表示使用css-loader来导入后面的css文件,最前面的两个!!表示禁用前置的所以loader,在这里也就是style-loader,避免重复执行style-loader。
最后我们的pitch函数里返回创建style标签并插入html的逻辑,打开dist/index.html,引入的js文件执行这段代码后便可以看到css文件的内容被插入到了html。
以上就是style-loader简要的实现过程,相比官网的style-loader忽略了很多东西,感兴趣的同学阅读style-loader的源码。
手写一个plugin
接下来我们以编写一个clean-webpack-plugin
插件为例,一步一步认识编写插件的知识。
webpack插件需要是一个函数或类,里面需要定义一个apply方法,webpack会通过apply来启动插件。
class CleanWebpackPlugin {
constructor() {}
apply(complier) {
console.log('complier===', complier)
}
}
module.exports = CleanWebpackPlugin
复制代码
const CleanWebpackPlugin = require('./plugins/clean-webpack-plugin')
module.exports = {
plugins: [
new CleanWebpackPlugin()
]
}
复制代码
这里让我们先了解一下apply参数里的complier,complier是webpack的核心模块,complier继承自webpack的一个核心工具Tapable,Tapable上有三个方法可以用于触发钩子:
- tap():以同步的方式触发钩子。
- tapAsync():以异步的方式触发钩子。
- tapPromise():以异步的方式触发钩子,并返回Promise。
使用方式如下:
compiler.hooks.xxxHook.tap('CleanWebpackPlugin', (params) => {
/* ... */
})
复制代码
tap方法接收两个函数:插件名称和一个回调函数。
compiler的hooks的种类有:
environment
、afterEnvironment
、entryOption
、afterPlugins
、afterResolvers
、initialize
、beforeRun
、run
、watchRun
、normalModuleFactory
、contextModuleFactory
、beforeCompile
、compile
、thisCompilation
、compilation
、make
、afterCompile
、shouldEmit
、emit
、afterEmit
、assetEmitted
、done
、additionalPass
、failed
、invalid
、watchClose
、shutdown
、infrastructureLog
、log
。
回到我们要写的插件,我们想实现打包时自动清空dist目录,这时可以选择emit
钩子进行触发,emit
钩子表示会在输出asset到output之前执行,这符合我们的需求,当然你也可以选择更靠前的钩子。详细的钩子生命周期可以查看文档:compilier钩子。
const fs = require('fs')
/**
* 删除文件夹下的文件
* @param {*} path
*/
async function deleteDir(path) {
if (fs.existsSync(path)) {
const dirs = []
const files = await fs.readdirSync(path)
files.forEach(async (file) => {
const childPath = path + "/" + file
if (fs.statSync(childPath).isDirectory()) {
await deleteDir(childPath)
dirs.push(childPath)
} else {
await fs.unlinkSync(childPath)
}
})
dirs.forEach((fir) => fs.deleteDirSync(fir))
}
}
class CleanWebpackPlugin {
apply(complier) {
const { hooks, options } = complier
hooks.emit.tap('CleanWebpackPlugin', () => {
// 清除目录下文件
const dir = options.output.path
deleteDir(dir)
})
}
}
module.exports = CleanWebpackPlugin
复制代码
感谢
本次分享就到此结束了,感谢阅读!!!