Webpack Loader 与 Plugin
Webpack简介
Webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。当 Webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个依赖图,然后将项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。
Webpack的一些核心概念
- Entry:入口,指示
Webpack应该使用哪个模块,来作为构建其内部 依赖图(dependency graph) 的开始。 - Output:输出结果,告诉
Webpack在哪里输出它所创建的 bundle,以及如何命名这些文件。 - Module:模块,在
Webpack里一切皆模块,一个模块对应着一个文件。Webpack会从配置的Entry开始递归找出所有依赖的模块。 - Chunk:代码块,一个
Chunk由多个模块组合而成,用于代码合并与分割。 - Loader:模块代码转换器,让
webpack能够去处理除了JS、JSON之外的其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。 - Plugin:扩展插件。在
webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的api改变输出结果。常见的有:打包优化,资源管理,注入环境变量。 - Mode:模式,告知
webpack使用相应模式的内置优化 - Browser Compatibility:浏览器兼容性,
Webpack支持所有符合ES5标准 的浏览器(IE8以上版本)
回归正题,loader 和 plugin 究竟是什么?
我们可以通过loader和plugin机制去进一步扩展能力,按照项目需要去实现个性化的功能。
Webpack 是由nodejs编写的前端资源加载/打包工具,由nodejs提供了强大的文件处理,IO能力。
Loader 和 Plugin 在 Webpack 里是支柱能力。在整个构建流程中,Loader 和 Plugin 对编译结果起着决定性的作用。
Loader
简介
Webpack 中提供了一种处理多种文件格式的机制,这就是 Loader,我们可以把Loader当成一个转换器,它可以将某种格式的文件转换成Webpack支持打包的模块。平时在 webpack.config.js 里经常会看见 rules 这个字段,其实就是通过正则匹配的方式来找到 Loader 需要处理的文件,并通过 Loader 对文件进行处理。
在Webpack中,一切皆模块,我们常见的Javascript、CSS、Less、Typescript、Jsx、图片等文件都是模块,不同模块的加载是通过模块加载器来统一管理的,这里的模块加载器指的就是 Loader 。当我们需要使用不同的 Loader 来解析不同类型的文件时,我们可以在module.rules字段下配置相关规则。
Loader特点
loader本质上是一个函数,output=loader(input);input可为工程源文件的字符串,也可是上一个loader转化后的结果;- 第一个
loader的传入参数只有一个:资源文件(resource file)的内容; loader支持链式调用,webpack打包时是按照数组从后往前的顺序将资源交给loader处理的。- 支持同步或异步函数。
代码结构
代码结构通常如下:
// source:资源输入,对于第一个执行的 loader 为资源文件的内容;后续执行的 loader 则为前一个 loader 的执行结果
// sourceMap: 可选参数,代码的 sourcemap 结构
// data: 可选参数,其它需要在 Loader 链中传递的信息,比如 posthtml/posthtml-loader 就会通过这个参数传递参数的 AST 对象
const loaderUtils = require('loader-utils');
module.exports = function(source, sourceMap?, data?) {
// 获取到用户给当前 Loader 传入的 options
const options = loaderUtils.getOptions(this);
// TODO: 此处为转换source的逻辑
return source;
};我自己写了一个Loader……
目标是匹配到项目中 i18n 方法($t(''))中的 key 值并打印出来:
module.exports = function (source) {
// 此处拿到的 source,就是文件内容
// 此处正则用于匹配 $t(xxx)
const reg = /\$t\(.*?\)/g
source = source.replace(reg, word => {
// 有可能是 $t('')/$t("")
if (["'", '"'].includes(word[3])) {
// 假设字符串内容是test,此处 str = 'test'/"test"
let str = word
// 切割掉引号,得到实际的内容
str = word.slice(4).slice(0, -2)
// 将内容打印出来
console.log(str);
}
// 此处没做处理,直接返回,因此不会对源文件做任何影响
return word
})
// 此处没做处理,直接返回,因此不会对源文件做任何影响
return source
}常用的Loader
babel-loader
输入:
javascript-> 处理:babel-> 输出:浏览器支持的低版本语法ts-loader
输入:
TypeScript-> 处理:tsc-> 输出:javascript代码markdown-loader
输入:
markdown文件 -> 处理:markdown-loader-> 输出:webpack支持的模块...more than
总结
Loader 其实就是一个加载、处理器,通过规则加载指定的文件,处理后输出成符合需求的内容。
Plugin
简介
Webpack就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。
Webpack通过Tapable来组织这条复杂的生产线。Webpack在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。Webpack的事件流机制保证了插件的有序性,使得整个系统扩展性很好。——「深入浅出 Webpack」
理解
我们都知道 node.js 有一个引以为傲的机制,就是它的事件驱动。node.js,包括 JavaScript,本质上都是一个单线程的语言,通过一条流水线从上至下地运行代码。但当我们需要异步的时候,问题就来了:要怎样才能在对应的时间做对应的事呢?node.js 给出的答案正是事件驱动,通过触发事件,执行回调函数,即可实现这种需求。
Webpack 本质上也是基于 node.js 写的,因此它的 plugin 也采用了事件的机制,我们写的 plugin 只需要关心 Webpack 广播出的众多事件中所需的那些,并传递我们的处理函数,Webpack 就会在指定的事件触发时,执行我们传入的函数,以实现流水线的拓展。
简而言之,plugin可以在webpack运行到某个时刻帮你做一些事情。 plugin会在webpack初始化时,给相应的生命周期函数绑定监听事件,直至webpack执行到对应的那个生命周期函数,plugin绑定的事件就会触发。
代码结构
以下是一个例子,用来说明 plugin 的代码结构。
plugin本质上是一个对外导出的class,类中包含一个固定方法名apply。
apply函数的第一个参数就是compiler,我们编写的插件逻辑就是在apply函数下面进行编写.
既然程序中已经获取了compiler参数,理论上我们就可以在compiler的各个钩子函数中绑定监听事件。比如下面代码会在emit阶段绑定一个监听事件。
主程序一旦执行到emit阶段,绑定的回调函数就会触发。通过上面的介绍可知,主程序处于emit阶段时,compilation已经将代码编译构建完了,下一步会将内容输出到文件系统。
此时compilation.assets存放着即将输出到文件系统的内容,如果这时候我们操作compilation.assets数据,势必会影响最终打包的结果。
下面代码直接在compilation.assets上新增一个属性名copyright.txt,并定义好了文件内容和长度。
这里需要引起注意,由于程序中使用tapAsync(异步序列)绑定监听事件,那么回调函数的最后一个参数会是next,异步任务执行完成后需要调用next,主程序才能进入到下一个任务队列。
最终打包后的目标文件夹下会多出一个copyright.txt文件,里面存放着字符串this is my copyright。
//copyRight.js
// 导出一个 class
class CopyRightPlugin {
// 固定 apply 方法
apply(compiler){
// 处于 emit 阶段时,代码已经编译构建完
compiler.hooks.emit.tapAsync("CopyRightPlugin",(compilation, next) => {
// 下一步将内容输出到文件系统
setTimeout(() => { // 模拟ajax获取版权信息
// 此时 compilation.assets 存放着即将输出到文件系统的内容
// 操作这个属性,会影响最终打包的结果
compilation.assets['copyright.txt'] = {
source: function() {
return "this is my copyright"; // 文件内容
},
size: function() {
return 20; // 文件大小
}
}
// 因为是异步序列,因此要调用 next 方法才会进入到下一个任务队列
next();
},1000)
})
}
}
module.exports = CopyRightPlugin;