编写一个loader
在平时自己由零搭建项目时,虽然基础配置都比较熟悉,比如配置 file-loader, url-loader, css-loader 等,配置不难,但究竟是怎么起作用的呢,如何编写一个 Webpack Loader。
loader 通常指打包的方案,即按什么方式来处理打包,打包的时候它可以拿到模块源代码,经过特定 loader 的转换后返回新的结果。
比如 sass-loader 可以把 SCSS 代码转换成 CSS 代码
保持功能单一
项目中可能会配置很多,但要记住,要保持一个 Loader 的功能单一,避免做多种功能,只需完成一种功能转换即可。
所以如 less 文件转换成 css 文件,也不是一步到位,而是 less-loader, css-loader, style-loader 几个 loader 的链式调用才能完成转换。
模块
因为 Webpack 本身是运行在 Node.js 之上的,一个 loader 其实就是一个 node 模块,这个模块导出的是一个函数,即:
module.exports = function (source) {
// source 为 compiler 传递给 Loader 的一个文件的原内容
// 处理...
return source; // 需要返回处理后的内容
};
这个导出的函数的工作就是获得处理前的原内容,对原内容执行处理后,返回处理后的内容。
替换字符串的 loader
比如打包时,想要替换源文件的字符串,这时可以考虑使用 Loader,因为 loader 就是获得源文件内容然后对其进行处理,再返回。
比如 src 目录下有三个文件:
export const msg1 = "学习框架";
export const msg2 = "深入理解JS";
import { msg1 } from "./msg1";
import { msg2 } from "./msg2";
function print() {
console.log(`输出:${msg1}, ${msg2}`);
}
print();
做的事情则是把 msg1 和 msg2 两个文件导入,然后输出两个字符串。
现在要做的事也很简单,把"框架"转为"React 框架", "JS"转为"JavaScript"。
新建 src/loaders/replaceLoader.js 文件,
module.exports = function (source) {
const handleContent = source
.replace("框架", "React框架")
.replace("JS", "JavaScript");
return handleContent;
};
source 是源文件内容
就这样,loader 写完了!!!
使用 Loader
在根目录下新建文件 webpack.config.js
const path = require("path");
module.exports = {
mode: "production",
entry: "./src/index.js",
module: {
rules: [
{
test: /\.js$/,
use: "./src/loaders/replaceLoader.js",
},
],
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
},
};
执行 npx webpack, 查看打包结果 dist/main.js
(() => {
"use strict";
console.log("输出:学习React框架, 深入理解JavaScript");
})();
替换成功!
需要注意的是,use 里面填写的 loader 是去 node_modules 目录里面找的,由于是自定义的 loader,所以不能直接写 use: 'replaceLoader',但直接写路径的方式未免难看点,可以通过 webpack 来配置:
module.exports = {
resolveLoader: {
modules: ["node_modules", "./src/loaders"], // node_modules找不到,就去./src/loaders找
},
module: {
rules: [
{
test: /\.js$/,
use: "replaceLoader",
},
],
},
};
获取 loader 的 options
其实就是写一个功能函数,如果 loader 可以传入参数呢
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'replaceLoader',
options: {
params: 'replaceString',
},
},
},
],
},
这个时候可以使用 this.query 来获取,通过 this.query.params 就能拿到,这里需要注意的是,this 上下文是有用的,所以这个 loader 导出函数不能是箭头函数。
但 webpack 更推荐 loader-utils 模块来获取,它提供了许多有用的工具,最常用的一种工具是获取传递给 loader 的选项。
首先要安装 npm i -D loader-utils
修改 src/loaders/replaceLoader.js
const { getOptions } = require("loader-utils");
module.exports = function (source) {
console.log(getOptions(this)); // { params: 'replaceString' }
console.log(this.query.params); // replaceString
const handleContent = source
.replace("框架", "React框架")
.replace("JS", "JavaScript");
return handleContent;
};
这里需要注意的是,getOptions(this)参数传入的是 this,也就是说
打印结果
{ params: 'replaceString' }
{ params: 'replaceString' }
{ params: 'replaceString' }
this.callback()
有些场景下还需要返回其他东西比如 sourceMap
module.exports = function (source) {
// 告诉 Webpack 返回的结果
this.callback(null, source, sourceMaps);
};
另外也不需要 return 了,所以也可使用此 API 替代 return
const { getOptions } = require("loader-utils");
module.exports = function (source) {
const handleContent = source
.replace("框架", "React框架")
.replace("JS", "JavaScript");
this.callback(null, handleContent);
};
自定义 loader 应用场景
- 在所有 function 外面加一层 try catch 代码块捕获错误,避免手动繁琐添加。
- 实现中英文替换:可以将文字用占位符如{{ title }}包裹,检测到占位符则根据环境变量替换为中英文。
常用 loader
raw-loader:加载文件原始内容(utf-8)
file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)
url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值时返回其 publicPath,小于阈值时返回文件 base64 形式编码 (处理图片和字体)
source-map-loader:加载额外的 Source Map 文件,以方便断点调试
svg-inline-loader:将压缩后的 SVG 内容注入代码中
image-loader:加载并且压缩图片文件
json-loader 加载 JSON 文件(默认包含)
handlebars-loader: 将 Handlebars 模版编译成函数并返回
babel-loader:把 ES6 转换成 ES5
ts-loader: 将 TypeScript 转换成 JavaScript
awesome-typescript-loader:将 TypeScript 转换成 JavaScript,性能优于 ts-loader
sass-loader:将 CSS 代码注入 JavaScript 中,通过 DOM 操作去加载 CSS
css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
postcss-loader:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀
eslint-loader:通过 ESLint 检查 JavaScript 代码
tslint-loader:通过 TSLint 检查 TypeScript 代码
mocha-loader:加载 Mocha 测试用例的代码
coverjs-loader:计算测试的覆盖率
vue-loader:加载 Vue.js 单文件组件
i18n-loader: 国际化
cache-loader: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里
编写一个plugin
按本人的理解,Webpack 插件的作用就是在 webpack 运行到某个时刻的时候,帮做一些事情。
在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
官方解释是:
插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建流程中。
webpack 插件的组成:
- 一个 JS 命名函数或一个类(可以想下平时使用插件就是 new XXXPlugin()的方式)
- 在插件类/函数的 (prototype) 上定义一个 apply 方法。
- 通过 apply 函数中传入 compiler 并插入指定的事件钩子,在钩子回调中取到 compilation 对象
- 通过 compilation 处理 webpack 内部特定的实例数据
- 如果是插件是异步的,在插件的逻辑编写完后调用 webpack 提供的 callback
写一个插件,生成一个版权的文件。
基本雏形
function CopyrightWebpackPlugin() {}
CopyrightWebpackPlugin.prototype.apply = function (compiler) {};
module.exports = CopyrightWebpackPlugin;
也可以写成类的形式:
class CopyrightWebpackPlugin {
apply(compiler) {
console.log(compiler);
}
}
module.exports = CopyrightWebpackPlugin;
webpack 在启动之后,在读取配置的过程中会先执行 new CopyrightWebpackPlugin(options)操作,初始化一个 CopyrightWebpackPlugin 实例对象。在初始化 compiler 对象之后,会调用上述实例对象的 apply 方法并将 compiler 对象传入。
在 apply 方法中,通过 compiler 对象来监听 webpack 生命周期中广播出来的事件,也可以通过 compiler 对象来操作 webpack 的输出。
Compiler 和 Compilation
在插件开发中最重要的两个对象是 compiler 和 compilation 对象。
-
compiler 对象代表了完整的 webpack 环境配置,在初始化 compiler 对象之后,通过调用插件实例的 apply 方法,作为其参数传入。这个对象在启动 webpack 时被一次性建立,并包含了 webpack 环境的所有的配置信息,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
-
compilation 对象会作为 plugin 内置事件回调函数的参数,一个 compilation 对象包含了当前的模块资源、编译生成资源、变化的文件以及被跟踪依赖的状态信息。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 compilation 将被创建。compilation 对象也提供了很多事件回调供插件做扩展。通过 compilation 也能读取到 compiler 对象。
编码
下面代码为生成一个版权 txt 文件,新建文件 src/plugins/copyright-webpack-plugin.js:
class CopyrightWebpackPlugin {
apply(compiler) {
// emit 钩子是生成资源到 output 目录之前执行,emit 是一个异步串行钩子,需要用 tapAsync 来注册
compiler.hooks.emit.tapAsync(
"CopyrightWebpackPlugin",
(compilation, callback) => {
// 回调方式注册异步钩子
const copyrightText = "版权归 JackySummer 所有";
// compilation存放了这次打包的所有内容
// 所有待生成的文件都在它的 assets 属性上
compilation.assets["copyright.txt"] = {
// 添加copyright.txt
source: function () {
return copyrightText;
},
size: function () {
// 文件大小
return copyrightText.length;
},
};
callback(); // 必须调用
}
);
}
}
module.exports = CopyrightWebpackPlugin;
webpack 中许多对象扩展自 Tapable 类。这个类暴露 tap, tapAsync 和 tapPromise 方法,可以使用这些方法,注入自定义的构建步骤,这些步骤将在整个编译过程中不同时机触发。
使用 tapAsync 方法来访问插件时,需要调用作为最后一个参数提供的回调函数。
在 webpack.config.js
const path = require("path");
const CopyrightWebpackPlugin = require("./src/plugins/copyright-webpack-plugin");
module.exports = {
mode: "production",
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
},
plugins: [new CopyrightWebpackPlugin()],
};
执行 webpack 命令,就会看到 dist 目录下生成 copyright.txt 文件
如果在配置文件使用 plugin 时传入参数该怎么获得呢,可以在插件类添加构造函数拿到:
plugins: [
new CopyrightWebpackPlugin({
name: 'jacky',
}),
],
在 copyright-webpack-plugin.js 中
class CopyrightWebpackPlugin {
constructor(options = {}) {
console.log("options", options); // options { name: 'jacky' }
}
}
常见 Plugins
define-plugin:定义环境变量 (Webpack4 之后指定 mode 会自动配置)
ignore-plugin:忽略部分文件
html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)
web-webpack-plugin:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用
uglifyjs-webpack-plugin:不支持 ES6 压缩 (Webpack4 以前)
terser-webpack-plugin: 支持压缩 ES6 (Webpack4)
webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度
mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代 extract-text-webpack-plugin)
serviceworker-webpack-plugin:为网页应用增加离线缓存功能
clean-webpack-plugin: 目录清理
ModuleConcatenationPlugin: 开启 Scope Hoisting
speed-measure-webpack-plugin: 可以看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时)
webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)