「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」。
一.webpack核心工作原理
- 一般项目中会散落着各种资源文件
- 然后webpack会找到其中的一个文件,作为文件打包的入口。一般这个文件会是JavaScript文件
- 然后会顺着这个文件中的代码,比如import或者require等语句,去解析并推断这个资源所依赖的模块。然后去解析每个模块的依赖,最后就形成了项目中所有依赖关系的依赖树。
- 然后webpack会递归或者遍历这个资源树,找到对于节点的配置文件。最后根据配置文件中的rules属性,去找到这个模块的加载器,然后交给对应的加载器,去加载这个模块。
- 最后会把加载后的结果,放到bundle.js,打包结果之中。从而去实现整个项目的打包。
1.webpack中的loader工作原理
- 这里的需求是一个markdown文件的加载器,markdown-loader
- md一般是转换成html后,再呈现到页面上,所以,希望得到的结果,转换后的html
- 创建markdown-loader.js文件
// 所有的loader都需要导出一个函数
// 这个函数就是对所加载的资源一个处理过程
// 输入就是加载的资源文件内容
// 输出就是此次加工后的结果
// 通过source参数去接受输入,通过返回值去输出
module.exports = source => {
// 这里先打印source试试
console.log(source)
return 'hello~'
}
- 这里去配置下webpack.config.js
{
test:/.md$/,
// use参数不仅可以匹配模块名,也可以匹配路径
use:'./markdown-loader.js'
}
-
运行输出,确实是md文件的内容,但同时也出现一个解析错误,需要再加载一个loader
-
其实webpack的loader像一个管道,可以依次处理,染最终的输出必须是Javascript代码。而这里返回的是一个字符串(return 'hello~'),所以报错。
-
所以修改上述代码,返回标准的js代码
module.exports = source => {
console.log(source)
return 'console.log("hello~")'
}
- 运行webpack打包,没有报错,打开bundle.js文件,查看最后一个loader。发现console直接拼接到最后的代码上面了。
- 先安装一个markdown解析模块,yarn add marked --dev
const marked = require('marked')
module.exports = source => {
const html = marked(source)
// 因为需要直接导出html,但loader的结果又必须是js,所以可以用module.exports
// 这里通过stringfy进行转换,把可能存在的字符串问题转换成js理解的字符串
return `module.exports = ${JSON.stringfy(html)}`
}
- 进行编译,查看bundle.js。此时的编译结果就是我们所需要的了。
- 也可以用export default的方式导出
const marked = require('marked')
module.exports = source => {
const html = marked(source)
return `export default ${JSON.stringfy(html)}`
}
- 编译输出的结果也是可以的。webpack会自动转换导出过程中的ESmodule代码
- 试一下其他的情况,比如返回一个html,在用html-loader去转换成js
const marked = require('marked')
module.exports = source => {
const html = marked(source)
// 返回html,字符串交给下一个loader去处理
return html
- yarn add html-loader --dev
- 修改配置文件
{
test:/.md$/,
// use修改成数组,这样就会依次加载loader,执行顺序是从后往前。
use:[
'html-loader',
'./markdown-loader.js'
]
}
- 总结:loader的作用就是把资源文件进行输入到输出的一个转换。loader类似一种管道的概念,对同一个资源可以依次使用loader。
二.webpack插件机制介绍
-
loader专注实现资源模块的加载
-
plugin是解决除了资源加载以外,其他的自动化的工作。
-
比如打包前自动清除dist目录
-
拷贝静态文件至输出目录
-
压缩输出代码等
-
1. webpack常用插件的介绍--clean-webpack-plugin
-
自动清除输出目录的插件,clean-webpack-plugin
-
yarn add clean-webpack-plugin --dev
-
修改配置文件
const { CleanWebpakcPlugin } = require('clean-webpack-plugin')
const path = require('path')
module.exports = {
entry:'./src/main.js',
output:{
filename:'bundle.js',
path:path.join(__dirname,'dist')
},
publicPath:'dist/'
module:{
rules:[
test:/.css$/,
use:[
'style-loader',
'css-loader'
]
]
},
// 需要专门配置一个使用插件的属性,是一个数组。
// 添加一个插件,就是添加一个元素。
// 绝大多数插件导出的都是一个类型
// 所以使用它就是通过一个类型去创建一个实例
// 然后把这个实例放到数组之中
plugins:[
new CleanWebpakcPlugin()
]
}
- 进行编译,查看结果
2. webpack常用插件的介绍--html-webpack-plugin
-
自动去生成使用打包结果的html
-
之前都是通过硬编码的方式,写在项目的根目录之下。这种方式会有两个问题
- 1.如果需要发布,则既需要发布dist目录下的文件,也需要同时发布这个html。而且还需要确保上线过后,html里面的路径引用是正确的。
- 2.如果webpack打包的配置发生了变化,那么html中的script标签的引用路径,也同时需要去修改。
-
解决这两个问题的最好办法就是通过webpack自动去生成html文件。这样上线的时候,就只需要把dist发布出去就好了。
-
yarn add html-webpack-plugin
-
修改配置文件
const { CleanWebpakcPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
module.exports = {
entry:'./src/main.js',
output:{
filename:'bundle.js',
path:path.join(__dirname,'dist')
注释这个配置,否则生成的html文件引入的路径不对
// publicPath:'dist/'
},
module:{
rules:[
test:/.css$/,
use:[
'style-loader',
'css-loader'
]
]
},
plugins:[
new CleanWebpakcPlugin(),
new HtmlWebpackPlugin()
]
}
- 运行打包命令,会自动生成一段html文件放在dist目录下。
- 更详细的配置
plugins:[
new CleanWebpakcPlugin(),
new HtmlWebpackPlugin({
// 设置html的标题
title:'Webpack Plugin Sample',
// meta属性可以用对象的形式去设置标签
meta:{
viewport:'width=device-width'
}
})
]
-
最后编译的结果
-
更好的方式是添加一个html的模板,然后根据模版,插件在进行输出
-
新建src/index.html
-
对于需要动态生成的部分,用loadsh语法进行插入<%= %>
-
通过htmlWebpackPlugin.options去获取配置属性,比如htmlWebpackPlugin.options.title
-
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="container">
<h1><%= htmlWebpackPlugin.options.title %></h1>
</div>
</body>
</html>
- 指定模板
plugins:[
new CleanWebpakcPlugin(),
new HtmlWebpackPlugin({
title:'Webpack Plugin Sample',
meta:{
viewport:'width=device-width'
},
// 指定模板
template:'./src/index.html'
})
]
- 除了自定义内容,同时输出多个页面文件,也是一个常见的需求
plugins:[
new CleanWebpakcPlugin(),
// 用于生成index.html
new HtmlWebpackPlugin({
title:'Webpack Plugin Sample',
meta:{
viewport:'width=device-width'
},
template:'./src/index.html'
}),
// 那么就可以添加多个实例来添加多个页面
// 用于生成about.html
new HtmlWebpackPlugin({
// 指定输出的文件名
// 这个属性默认是index.html
filename:'about.html',
})
]
3. webpack常用插件总结
-
在项目中可能有一些不需要要打包编译的静态文件,它们最终也需要发布到线上
-
所以需要将这些静态文件复制到输出的目录
-
对于这种需求,可以通过copy-webpack-plugin来实现
-
yarn add copy-webpack-plugin --dev
-
修改配置文件
const { CleanWebpakcPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 导出插件
const CopyWebpackPlugin = require('copy-webpack-plugin')
const path = require('path')
module.exports = {
entry:'./src/main.js',
output:{
filename:'bundle.js',
path:path.join(__dirname,'dist')
注释这个配置,否则生成的html文件引入的路径不对
// publicPath:'dist/'
},
module:{
rules:[
test:/.css$/,
use:[
'style-loader',
'css-loader'
]
]
},
plugins:[
new CleanWebpakcPlugin(),
new HtmlWebpackPlugin(),
// 同时构造一个实例
// 参数需要传入一个数组
// 用来指定需要拷贝的文件路径
// 可以是通配符'public/**'
// 也可以是目录或者文件的相对路径
new CopyWebpackPlugin([
// 这里是一个目录,就是把整个public下的文件复制到输出目录
// 不会带着public这个目录
'public'
])
]
}
4. webpack开发一个插件
-
相比于loader,plugin的能力会更加宽泛
-
Plugin通过钩子机制来实现
-
在webpack工作的过程中会有很多的环节,为了便于插件的扩展,webpack几乎给每个环节都埋下了一个钩子。
-
这样开发任务的时候,在不同的节点上挂载不同的任务,就可以轻松的扩展webpack的能力。\
-
具体有哪些预定好的钩子,可以去参考官方的[api文档]
-
webpack要求我们插件必须是一个函数,或者一个包含apply的对象。
-
一般都会把这个插件定义为一个类型,然后在这个类型中定义一个apply的方法。使用的时候就是通过这个类型构建一个实例,然后去使用
// 定义一个MyPlugin类型
class MyPlugin {
// 类型中定义一个apply方法
// 这个方法会在webpack启动后,自动被调用
apply( compiler ){
// compiler参数是webpack构建中的核心对象,里面有所有构建的配置信息
// 同时也是通过这个对象去注册钩子函数
// 这个插件的需求是,清除构建打包后的js文件中,哪些没必要的注释
// 了解好需求后,就需要明白注册的时机。
// 从官网找到一个emit钩子,是即将要往文件输出内容的时候执行
console.log(compiler)
// 从compiler的hooks属性访问钩子
// 通过tap方法去注册一个钩子函数
// tap方法接受两个参数,一个是插件的名称,另一个挂载到钩子上的函数
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation是指此次打包过程中的上下文
// 所有打包过程中的结果,都会放到这个对象当中
// 通过assets属性,获取即将写入文件的资源信息
// 因为是一个对象,所以可以通过for in进行遍历
for( const name in compilation.assets){
// 这个对象的键就是每个文件的名称
// 可以通过console.log(name)来查看,并将MyPlugin new到配置中
// 文件的内容需要通过source方法来获取
// 进行打印尝试console.log( compilation.assets[name].source() )
if(name.endsWith('.js'){
// 判断文件名是否由js结尾
// 获取文件的内容
const contents = compilation.assets[name].source()
// 进行正则替换
const withoutComments = contents.replace(//**+*//g,'')
// 将替换完的结果,覆盖到原有的结果当中
compilation.assets[name] = {
// 对于新的对象,同样暴露一个source方法,去返回新的内容。
source: () => withoutComments,
// 同时还需要一个size方法,去返回内容的大小,webpack要求的必须方法
size: () => withoutComments.length
}
})
}
})
}
}
- 总结:插件是通过往webpack生命周期的钩子挂载函数实现扩展