实现一个 webpack loader 和 webpack plugin
loader
官网上的定义:
loader 是一个转换器,用于对源代码进行转换。
例如 babel-loader
可以将 ES6 代码转换为 ES5 代码;sass-loader
将 sass
代码转换为 css
代码。
一般 loader 的配置代码如下:
module: {
rules: [
{
test: /.js$/,
use: [
// loader 的执行顺序从下到上
{
loader: path.resolve('./src/loader2.js'),
},
{
loader: path.resolve('./src/loader1.js'),
},
]
}
]
},
rules 数组包含了一个个匹配规则和具体的 loader 文件。
上述代码中的 test: /.js$/
就是匹配规则,表示对 js 文件使用下面的两个 loader。
而 loader 的处理顺序是自下向上的,即先用 loader1 处理源码,然后将处理后的代码再传给 loader2。
loader2 处理后的代码就是最终的打包代码。
loader 的实现
loader 其实是一个函数,它的参数是匹配文件的源码,返回结果是处理后的源码。下面是一个最简单的 loader,它什么都没做:
module.exports = function (source) {
return source
}
这么简单的 loader 没有挑战性,我们可以写一个稍微复杂一点的 loader,它的作用是将 var
关键词替换为 const
:
module.exports = function (source) {
return source.replace(/var/g, 'const')
}
写完之后,我们来测试一下,测试文件为:
function test() {
var a = 1;
var b = 2;
var c = 3;
console.log(a, b, c);
}
test()
wepback.config.js
配置文件为:
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /.js$/,
use: [
{
loader: path.resolve('./src/loader1.js'),
},
]
}
]
},
}
运行 npm run build
,得到打包文件 bundle.js
,我们来看一看打包后的代码:
eval("function test() {rn const a = 1;rn const b = 2;rn const c = 3;rn console.log(a, b, c);rn}rnrntest()nn//# sourceURL=webpack:///./src/index.js?");
可以看到,代码中的 var
已经变成了 const
。
异步 loader
刚才实现的 loader 是一个同步 loader,在处理完源码后用 return
返回。
下面我们来实现一个异步 loader:
module.exports = function (source) {
const callback = this.async()
// 由于有 3 秒延迟,所以打包时需要 3+ 秒的时间
setTimeout(() => {
callback(null, `${source.replace(/;/g, '')}`)
}, 3000)
}
异步 loader 需要调用 webpack 的 async()
生成一个 callback,它的第一个参数是 error
,这里可设为 null
,第二个参数就是处理后的源码。当你异步处理完源码后,调用 callback
即可。
下面来试一下异步 loader 到底有没生效,这里设置了一个 3 秒延迟。我们来对比一下打包时间:
上图是调用同步 loader 的打包时间,为 141 ms;下图是调用异步 loader 的打包时间,为 3105 ms,说明异步 loader 生效了。
如果想看完整 demo 源码,请点击我的 github。
plugin
webpack 在整个编译周期中会触发很多不同的事件,plugin 可以监听这些事件,并且可以调用 webpack 的 API 对输出资源进行处理。
这是它和 loader 的不同之处,loader 一般只能对源文件代码进行转换,而 plugin 可以做得更多。plugin 在整个编译周期中都可以被调用,只要监听事件。
对于 webpack 编译,有两个重要的对象需要了解一下:
Compiler 和 Compilation 在插件开发中最重要的两个资源就是 compiler 和 compilation 对象。理解它们的角色是扩展 webpack 引擎重要的第一步。compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。 compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。 这两个组件是任何 webpack 插件不可或缺的部分(特别是 compilation),因此,开发者在阅读源码,并熟悉它们之后,会感到获益匪浅。
plugin 的实现
我们看一下官网的定义,webpack 插件由以下部分组成:
- 一个 JavaScript 命名函数。
- 在插件函数的 prototype 上定义一个 apply 方法。
- 指定一个绑定到 webpack 自身的事件钩子。
- 处理 webpack 内部实例的特定数据。
- 功能完成后调用 webpack 提供的回调。
简单的说,一个具有 apply 方法的函数就是一个插件,并且它要监听 webpack 的某个事件。下面来看一个简单的示例:
function Plugin(options) { }
Plugin.prototype.apply = function (compiler) {
// 所有文件资源都被 loader 处理后触发这个事件
compiler.plugin('emit', function (compilation, callback) {
// 功能完成后调用 webpack 提供的回调
console.log('Hello World')
callback()
})
}
module.exports = Plugin
写完插件后要怎么调用呢?
先在 webpack 配置文件中引入插件,然后在 plugins 选项中配置:
const Plugin = require('./src/plugin')
module.exports = {
...
plugins: [
new Plugin()
]
}
这就是一个简单的插件了。
下面我们再来写一个复杂点的插件,它的作用是将经过 loader 处理后的打包文件 bundle.js
引入到 index.html
中:
function Plugin(options) { }
Plugin.prototype.apply = function (compiler) {
// 所有文件资源经过不同的 loader 处理后触发这个事件
compiler.plugin('emit', function (compilation, callback) {
// 获取打包后的 js 文件名
const filename = compiler.options.output.filename
// 生成一个 index.html 并引入打包后的 js 文件
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="${filename}"></script>
</head>
<body>
</body>
</html>`
// 所有处理后的资源都放在 compilation.assets 中
// 添加一个 index.html 文件
compilation.assets['index.html'] = {
source: function () {
return html
},
size: function () {
return html.length
}
}
// 功能完成后调用 webpack 提供的回调
callback()
})
}
module.exports = Plugin
OK,执行一下,看看效果。
完美,和预测的结果一模一样。
完整 demo 源码,请看我的 github。
想了解更多的事件,请看官网介绍 compiler 钩子。
参考资料
- 一个MySQL优化案例的初步思路(r8笔记第87天)
- 一条直线上N个线段所覆盖的总长度
- Go 语言 数据库操作之插入数据实现
- (摘抄)GO语言中template的用法
- 大数据时代的技术hive:hive介绍
- hadoop2.6分布式部署时 livenodes等于1的原因
- Hadoop运行wordcount时报classnotfound错误的一个原因
- 在WINDOWS下交叉编译LINUX程序运行不了是为什么?
- Hibernate的缓存机制
- error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http:/
- 【Go 语言 redis内存数据库存储 demo】
- linux下基本操作记录mongodb
- ClassLoader究竟为何物?
- Oracle应用实战八(完结)——存储过程、函数+对象曹组
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 强化学习仿真环境搭建入门Getting Started with OpenAI gym
- 数据分析与数据挖掘 - 04科学计算
- SwiftUI:使用 @EnvironmentObject 从环境中读取自定义值
- JavaScript 启动性能瓶颈分析与解决方案
- 大白话理解Vuex
- 【Java面试总结】计算机网络
- volatile关键字 Krains 2020-08-26
- synchronized关键字 Krains 2020-08-25
- happens-before Krains 2020-08-26
- ReentrantLock可重入锁 Krains 2020-08-27
- Java中的线程 Krains 2020-08-24
- CAS Krains 2020-08-25
- 96. 不同的二叉搜索树 II Krains 2020-09-03 树
- 410. 分割数组的最大值 Krains 2020-08-29 20:21:39 动态规划二分查找
- 字典树 Krains 2020-09-01