​1 基础回顾​

首先我们先回顾一下webpack常见配置,因为后面会用到,所以简单介绍一下。

​1.1 webpack常见配置​

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49



​// 入口文件​

​entry: {​

​app: ​​​​'./src/js/index.js'​​​​,​

​},​

​// 输出文件​

​output: {​

​filename: ​​​​'[name].bundle.js'​​​​,​

​path: path.resolve(__dirname, ​​​​'dist'​​​​),​

​publicPath: ​​​​'/'​​   //确保文件资源能够在 http://localhost:3000 下正确访问

​},​

​// 开发者工具 source-map​

​devtool: ​​​​'inline-source-map'​​​​,​

​// 创建开发者服务器​

​devServer: {​

​contentBase: ​​​​'./dist'​​​​,​

​hot: ​​​​true​​        ​​// 热更新​

​},​

​plugins: [​

​// 删除dist目录​

​new​​ ​​CleanWebpackPlugin([​​​​'dist'​​​​]),​

​// 重新穿件html文件​

​new​​ ​​HtmlWebpackPlugin({​

​title: ​​​​'Output Management'​

​}),​

​// 以便更容易查看要修补(patch)的依赖​

​new​​ ​​webpack.NamedModulesPlugin(),​

​// 热更新模块​

​new​​ ​​webpack.HotModuleReplacementPlugin()​

​],​

​// 环境​

​mode: ​​​​"development"​​​​,​

​// loader配置​

​module: {​

​rules: [​

​{​

​test: /\.css$/,​

​use: [​

​'style-loader'​​​​,​

​'css-loader'​

​]​

​},​

​{​

​test: /\.(png|svg|jpg|gif)$/,​

​use: [​

​'file-loader'​

​]​

​}​

​]​

​}​


这里面我们重点关注 module和plugins属性,因为今天的重点是编写loader和plugin,需要配置这两个属性。

​1.2 打包原理​

  • 识别入口文件
  • 通过逐层识别模块依赖。(Commonjs、amd或者es6的import,webpack都会对其进行分析。来获取代码的依赖)
  • webpack做的就是分析代码。转换代码,编译代码,输出代码
  • 最终形成打包后的代码

这些都是webpack的一些基础知识,对于理解webpack的工作机制很有帮助。

​2 loader​

OK今天第一个主角登场

​2.1 什么是loader?​

loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中

  • 处理一个文件可以使用多个loader,loader的执行顺序是和本身的顺序是相反的,即最后一个loader最先执行,第一个loader最后执行。
  • 第一个执行的loader接收源文件内容作为参数,其他loader接收前一个执行的loader的返回值作为参数。最后执行的loader会返回此模块的JavaScript源码

​2.2 手写一个loader​

需求:

  • 处理.txt文件
  • 对字符串做反转操作
  • 首字母大写

例如:abcdefg转换后为Gfedcba

OK,我们开始

1)首先创建两个loader(这里以本地loader为例)

为什么要创建两个laoder?理由后面会介绍

reverse-loader.js

?


1

2

3

4

5

6

7

8



​module.exports = ​​​​function​​ ​​(src) {​

​if​​ ​​(src) {​

​console.log(​​​​'--- reverse-loader input:'​​​​, src)​

​src = src.split(​​​​''​​​​).reverse().join(​​​​''​​​​)​

​console.log(​​​​'--- reverse-loader output:'​​​​, src)​

​}​

​return​​ ​​src;​

​}​


uppercase-loader.js

?


1

2

3

4

5

6

7

8

9

10



​module.exports = ​​​​function​​ ​​(src) {​

​if​​ ​​(src) {​

​console.log(​​​​'--- uppercase-loader input:'​​​​, src)​

​src = src.charAt(0).toUpperCase() + src.slice(1)​

​console.log(​​​​'--- uppercase-loader output:'​​​​, src)​

​}​

​// 这里为什么要这么写?因为直接返回转换后的字符串会报语法错误,​

​// 这么写import后转换成可以使用的字符串​

​return​​ ​​`module.exports = ​​​​'${src}'​​​​`​

​}​


看,loader结构是不是很简单,接收一个参数,并且return一个内容就ok了。

然后创建一个txt文件

2)mytest.txt

?


1



​abcdefg​


3)现在开始配置webpack

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20



​module.exports = {​

​entry: {​

​index: ​​​​'./src/js/index.js'​

​},​

​plugins: [...],​

​optimization: {...},​

​output: {...},​

​module: {​

​rules: [​

​...,​

​{​

​test: /\.txt$/,​

​use: [​

​'./loader/uppercase-loader.js'​​​​,​

​'./loader/reverse-loader.js'​

​]​

​}​

​]​

​}​

​}​


这样就配置完成了

4)我们在入口文件中导入这个脚本

为什么这里需要导入呢,我们不是配置了webapck处理所有的.txt文件么?

因为webpack会做过滤,如果不引用该文件的话,webpack是不会对该文件进行打包处理的,那么你的loader也不会执行

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25



​import _ from ​​​​'lodash'​​​​;​

​import txt from ​​​​'../txt/mytest.txt'​

​import ​​​​'../css/style.css'​

​function​​ ​​component() {​

​var​​ ​​element = document.createElement(​​​​'div'​​​​);​

​var​​ ​​button = document.createElement(​​​​'button'​​​​);​

​var​​ ​​br = document.createElement(​​​​'br'​​​​);​


​button.innerHTML = ​​​​'Click me and look at the console!'​​​​;​

​element.innerHTML = _.join(​​​​'【'​​ ​​+ txt + ​​​​'】'​​​​);​

​element.className = ​​​​'hello'​

​element.appendChild(br);​

​element.appendChild(button);​


​// Note that because a network request is involved, some indication​

​// of loading would need to be shown in a production-level site/app.​

​button.onclick = e => import(​​​​/* webpackChunkName: "print" */​​ ​​'./print'​​​​).then(module => {​

​var​​ ​​print = module.​​​​default​​​​;​


​print();​

​});​


​return​​ ​​element;​

​}​

​document.body.appendChild(component());​


package.json配置

?


1

2

3

4

5

6

7

8

9

10



​{​

​...,​

​"scripts"​​​​: {​

​"test"​​​​: ​​​​"echo \"Error: no test specified\" && exit 1"​​​​,​

​"build"​​​​: ​​​​"webpack --config webpack.prod.js"​​​​,​

​"start"​​​​: ​​​​"webpack-dev-server --open --config webpack.dev.js"​​​​,​

​"server"​​​​: ​​​​"node server.js"​

​},​

​...​

​}​


然后执行命令

?


1



​npm run build​


这样我们的loader就写完了。

现在回答为什么要写两个loader?

看到执行的顺序没,我们的配置的是这样的

?


1

2

3

4



​use: [​

​'./loader/uppercase-loader.js'​​​​,​

​'./loader/reverse-loader.js'​

​]​


正如前文所说, 处理一个文件可以使用多个loader,loader的执行顺序是和本身的顺序是相反的

我们也可以自己写loader解析自定义模板,像vue-loader是非常复杂的,它内部会写大量的对.vue文件的解析,然后会生成对应的html、js和css。

我们这里只是讲述了一个最基础的用法,如果有更多的需要,可以查看《loader官方文档》

​3 plugin​

​3.1 什么是plugin?​

在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

plugin和loader的区别是什么?

对于loader,它就是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss或A.less转变为B.css,单纯的文件转换过程

plugin是一个扩展器,它丰富了wepack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务。

​3.2 一个最简的插件​

/plugins/MyPlugin.js(本地插件)

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15



​class MyPlugin {​

​// 构造方法​

​constructor (options) {​

​console.log(​​​​'MyPlugin constructor:'​​​​, options)​

​}​

​// 应用函数​

​apply (compiler) {​

​// 绑定钩子事件​

​compiler.plugin(​​​​'compilation'​​​​, compilation => {​

​console.log(​​​​'MyPlugin'​​​​)​

​))​

​}​

​}​


​module.exports = MyPlugin​


webpack配置

?


1

2

3

4

5

6

7

8

9

10

11



​const MyPlugin = require(​​​​'./plugins/MyPlugin'​​​​)​

​module.exports = {​

​entry: {​

​index: ​​​​'./src/js/index.js'​

​},​

​plugins: [​

​...,​

​new​​ ​​MyPlugin({param: ​​​​'xxx'​​​​})​

​],​

​...​

​};​


这就是一个最简单的插件(虽然我们什么都没干)

  • webpack 启动后,在读取配置的过程中会先执行 new MyPlugin(options) 初始化一个 MyPlugin 获得其实例。
  • 在初始化 compiler 对象后,再调用 myPlugin.apply(compiler) 给插件实例传入 compiler 对象。
  • 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。
  • 并且可以通过 compiler 对象去操作 webpack。

看到这里可能会问compiler是啥,compilation又是啥?

Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;

Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。

​Compiler 和 Compilation 的区别在于:​

Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

​3.3 事件流​

  • webpack 通过 Tapable 来组织这条复杂的生产线。
  • webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
  • webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似。

绑定事件

?


1

2

3



​compiler.plugin(​​​​'event-name'​​​​, params => {​

​...     ​

​});​


触发事件

?


1



​compiler.apply('event-name',params)​


3.4 需要注意的点

  • 只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。
  • 传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。
  • 有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 webpack,才会进入下一处理流程 。例如:

?


1

2

3

4

5

6

7



​compiler.plugin('emit',function(compilation, callback) {​

​...​


​// 处理完毕后执行 callback 以通知 Webpack ​

​// 如果不执行 callback,运行流程将会一直卡在这不往下执行 ​

​callback();​

​});​


关于complier和compilation,webpack定义了大量的钩子事件。开发者可以根据自己的需要在任何地方进行自定义处理。

《compiler钩子文档》

《compilation钩子文档》

​3.5 手写一个plugin​

场景:

小程序mpvue项目,通过webpack编译,生成子包(我们作为分包引入到主程序中),然后考入主包当中。生成子包后,里面的公共静态资源wxss引用地址需要加入分包的前缀:/subPages/enjoy_given。

在未编写插件前,生成的资源是这样的,这个路径如果作为分包引入主包,是没法正常访问资源的。

所以需求来了:

修改dist/static/css/pages目录下,所有页面的样式文件(wxss文件)引入公共资源的路径。

因为所有页面的样式都会引用通用样式vender.wxss

那么就需要把@import "/static/css/vendor.wxss"; 改为:@import "/subPages/enjoy_given/static/css/vendor.wxss";复制代码

OK 开始!

1)创建插件文件 CssPathTransfor.js

CssPathTransfor.js

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32



​class CssPathTransfor {​

​apply (compiler) {​

​compiler.plugin(​​​​'emit'​​​​, (compilation, callback) => {​

​console.log(​​​​'--CssPathTransfor emit'​​​​)​

​// 遍历所有资源文件​

​for​​ ​​(​​​​var​​ ​​filePathName ​​​​in​​ ​​compilation.assets) {​

​// 查看对应的文件是否符合指定目录下的文件​

​if​​ ​​(/static\/css\/pages/i.test(filePathName)) {​

​// 引入路径正则​

​const reg = /\/static\/css\/vendor\.wxss/i​

​// 需要替换的最终字符串​

​const finalStr = ​​​​'/subPages/enjoy_given/static/css/vendor.wxss'​

​// 获取文件内容​

​let content = compilation.assets[filePathName].source() || ​​​​''​


​content = content.replace(reg, finalStr)​

​// 重写指定输出模块内容​

​compilation.assets[filePathName] = {​

​source () {​

​return​​ ​​content;​

​},​

​size () {​

​return​​ ​​content.length;​

​}​

​}​

​}​

​}​

​callback()​

​})​

​}​

​}​

​module.exports = CssPathTransfor​


看着挺多,实际就是遍历compilation.assets模块。对符合要求的文件进行正则替换。

2)修改webpack配置

?


1

2

3

4

5

6

7

8

9

10

11

12

13



​var​​ ​​baseWebpackConfig = require(​​​​'./webpack.base.conf'​​​​)​

​var​​ ​​CssPathTransfor = require(​​​​'../plugins/CssPathTransfor.js'​​​​)​


​var​​ ​​webpackConfig = merge(baseWebpackConfig, {​

​module: {...},​

​devtool: config.build.productionSourceMap ? ​​​​'#source-map'​​ ​​: ​​​​false​​​​,​

​output: {...},​

​plugins: [​

​...,​

​// 配置插件​

​new​​ ​​CssPathTransfor(),​

​]​

​})​


插件编写完成后,执行编译命令