目录
1. 什么是 Loader
1.1 Loader 工作原理
1.2 Loader 执行顺序
1.3 内联 Loader 前缀
2. 如何开发 Loader
2.1 Loader 长什么样子
2.2 配置本地 Loader 的四种方法
2.2.1 在配置 rules 时,指定 Loader 的绝对路径
2.2.2 在 resolveLoader 里,配置 alias 别名属性
2.2.3 在 resolveLoader 里,配置 modules 属性
2.2.4 使用 npm link 引用 Loader
2.2.5 配置完成后,测试下 2.1 中的简单 Loader
2.3 带 pitch 的 Loader —— 阻断 Loader 链
2.4 手写 mini style-loader
2.4.1 style-loader 需求描述
2.4.2 mini style-loader 设计思路
2.4.3 mini style-loader 代码编写
2.4.4 使用 mini style-loader
2.4.5 在 Vue 项目中使用 Loader
3. 开发 Loader 必备工具包
4. 开发异步 Loader
5. Loader raw 设为 true,用于支持 二进制格式资源
6. file-loader 基本原理、输出文件
7. Loader 开发约定
8. 参考文章
1. 什么是 Loader
1.1 Loader 工作原理
Webpack 只能直接处理 JavaScript 代码
任何非 JavaScript 文件,都必须被预先处理为 JavaScript 代码,才可以参与打包
Loader(加载器)就是这样一个代码转换器:
- 它由 Webpack 的 loader runner 执行调用,接收原始资源数据作为参数
- 当多个加载器联合使用时,上一个 Loader 的结果,会传入下一个 Loader
- 最终输出 JavaScript 代码(和可选的 source map)给 Webpack 做进一步编译
1.2 Loader 执行顺序
- pre: 前置 Loader
- normal: 普通 Loader
- inline: 内联 Loader
- post: 后置 Loader
执行优先级为:pre > normal > inline > post
相同优先级的 Loader 执行顺序为:从右到左,从下到上
举个栗子~
use: ['loader1', 'loader2', 'loader3'],执行顺序为 loader3 → loader2 → loader1
1.3 内联 Loader 前缀
内联 Loader 可以通过添加不同前缀,跳过其他类型 Loader
- ! 跳过 normal loader
- -! 跳过 pre 和 normal loader
- !! 跳过 pre、 normal 和 post loader
1.4 常用的 Loader
名称 | 作用 |
---|---|
style-loader | 用于将 css 编译完成的样式,挂载到页面 style 标签上 |
css-loader | 用于识别 .css文件, 须配合 style-loader 共同使用 |
sass-loader/less-loader | css 预处理器 |
postcss-loader | 用于补充 css 样式各种浏览器内核前缀 |
url-loader | 处理图片类型资源,可以转 base64 |
vue-loader | 用于编译 .vue 文件 |
worker-loader | 通过内联 loader 的方式,使用 web worker 功能 |
style-resources-loader | 全局引用对应的 css,避免页面再分别引入 |
2. 如何开发 Loader
2.1 Loader 长什么样子
Loader 本质是一个 Node 模块,该模块导出一个函数
函数接收 source (源文件),返回处理后的source
比如下面的 Loader,接收源码,打印文字,原样返回源码
// loaders/simple-loader.js
// Loader 本质是一个 Node 模块,该模块导出一个函数
module.exports = function loader (source) {
console.log('Lyrelion simple-loader is working');
return source;
}
2.2 配置本地 Loader 的四种方法
为了测试 2.1 中的 Loader,需要在 webpack.config.js 里,配置 Loader
Webpack 默认会去 node_modules 里找所有第三方模块
通过 npm 或者 yarn 安装的 Loader,配置时只需直接使用 Loader 的名字,不用关心 Loader 的路径(因为他们都会安装在 node_modules 目录下)
如果使用本地自己开发的 Loader,也就是他们不在 node_modules 里,就需要告诉 Webpack Loader 的位置
在 Webpack4.0 里,有四种方法配置本地 Loader
2.2.1 在配置 rules 时,指定 Loader 的绝对路径
module.exports = {
// xxx
module: {
rules: [
{
test: /\.js$/,
// 在这里配置绝对路径
use: path.resolve(__dirname, 'loaders/myLoader.js')
}
]
}
}
2.2.2 在 resolveLoader 里,配置 alias 别名属性
module.exports = {
// xxx
resolveLoader: {
// 配置 resolveLoader.alias
alias: {
myLoader: path.resolve(__dirname, 'loaders/myLoader.js')
}
},
module: {
rules: [
{
test: /\.js$/,
use: 'myLoader'
}
]
}
}
2.2.3 在 resolveLoader 里,配置 modules 属性
将可能放置 Loader 的目录,存放到 resolveLoader.modules 数组中,告诉 Webpack
当 Webpack 在默认目录下,找不到指定 Loader 时,会自动去 resolveLoader.modules 数组 中查找
module.exports = {
// xxx
resolveLoader: {
// 配置 resolveLoader.modules
modules: ['node_modules', path.resolve(__dirname, 'loaders']
},
module: {
rules: [
{
test: /\.js$/,
use: 'myLoader'
}
]
}
}
2.2.4 使用 npm link 引用 Loader
基本步骤:
- 把 Loader 从当前项目抽离出来,构建独立工程
- 在 Loader 工程目录下,执行 npm link,在全局 link 中添加 loader
- 回到原项目目录,执行 npm link xxx (xxx 为 Loader 的名称),实现 require Loader
- 最后,在原项目中,直接使用 Loader 名称即可 (跟 npm install 的 Loader 一样用法)
关于 npm link,可以参考下面的文章:
使用 npm link 测试本地编写的 node 模块 / 引入全局安装的 node 模块_Lyrelion的博客-CSDN博客使用 npm link 测试本地编写的 node 模块 / 引入全局安装的 node 模块https://blog.csdn.net/Lyrelion/article/details/128506812
2.2.5 配置完成后,测试下 2.1 中的简单 Loader
在 webpack.config.js 中,输入以下内容:
// webpack.config.js
const path = require('path');
module.exports = {
entry: {...},
output: {...},
module: {
rules: [
{
test: /\.js$/,
// 指明 Loader 的绝对路径
use: path.resolve(__dirname, 'loaders/simple-loader')
}
]
}
}
执行打包命令 yarn build,输出结果如下:
2.3 带 pitch 的 Loader —— 阻断 Loader 链
pitch 是 Loader 上的一个方法(非必须的),它的作用 —— 阻断 Loader 链
如果有 pitch,Loader 的执行会分为两个阶段:
- pitch 阶段 —— Webpack 会先 从左到右 执行 Loader 链中,每个 Loader 上的 pitch 方法(如果有)
- normal execution 阶段 —— Webpack 会再 从右到左 执行 Loader 链中,每个 Loader 上的普通 Loader 方法
举个栗子~~ 假设配置了下面的 Loader
use: ['loader1', 'loader2', 'loader3']
Loader 执行过程:
在这个过程中,如果任何 pitch 有返回值,则 Loader 链被阻断
Webpack 会跳过后面所有的的 pitch 和 loader,直接进入上一个 Loader 的 normal execution 阶段
也就是说,假设在 loader2 的 pitch 中返回了一个字符串,此时 Loader 链发生阻断:
pitch 方法有三个参数:
- remainingRequest:loader 链中排在自己后面的 loader 以及资源文件的绝对路径以
!
作为连接符组成的字符串 - precedingRequest:loader 链中排在自己前面的 loader 的绝对路径以
!
作为连接符组成的字符串 - data:每个 loader 中存放在上下文中的固定字段,可用于 pitch 给 loader 传递数据
2.4 手写 mini style-loader
// 将 css 内容,通过 style 标签插入到页面中
// source —— 要处理的 css 源文件
function loader(source) {
let style = `
let style = document.createElement('style');
style.setAttribute("type", "text/css");
style.innerHTML = ${source};
document.head.appendChild(style)`;
return style;
}
module.exports = loader;
2.4.1 style-loader 需求描述
style-loader 通常不会独自使用,而是跟 css-loader 连用
css-loader 的返回值是一个 JavaScript 模块,大致长这样:
// 打印 css-loader 的返回值
// Imports
var ___CSS_LOADER_API_IMPORT___ = require("../node_modules/css-loader/dist/runtime/api.js");
exports = ___CSS_LOADER_API_IMPORT___(false);
// Module
exports.push([module.id, "\nbody {\n background: yellow;\n}\n", ""]);
// Exports
module.exports = exports;
这个模块在运行时,返回一段字符串代码 —— “\nbody {\n background: yellow;\n}\n”
style-loader 的作用:将这段 css 代码,转成 style 标签,插入到 html 的 head 中
2.4.2 mini style-loader 设计思路
style-loader 最终需返回一个 JavaScript 脚本:在脚本中创建一个 style 标签,将 css 代码赋给 style 标签,再将这个 style 标签插入 html 的 head 中
难点:获取 css 代码;因为 css-loader 的返回值只能在 运行时 的上下文中执行,而执行 Loader 是在编译阶段。换句话说,css-loader 的返回值在 style-loader 里派不上用场
曲线救国方案:使用获取 css 代码的表达式,在运行时再获取 css (类似 require('css-loader!index.css'))
在处理 css 的 loader 中又去调用 inline loader require css 文件,会产生循环执行 loader 的问题:
- 需要利用 pitch 方法,让 style-loader 在 pitch 阶段返回脚本,跳过剩下的 loader
- 同时还需要内联前缀 !! 的加持
注:pitch 方法有3个参数:
- remainingRequest:loader 链中排在自己后面的 loader 以及资源文件的绝对路径以
!
作为连接符组成的字符串 - precedingRequest:loader 链中排在自己前面的 loader 的绝对路径以
!
作为连接符组成的字符串 - data:每个 loader 中存放在上下文中的固定字段,可用于 pitch 给 loader 传递数据
可以利用 remainingRequest 参数获取 loader 链的剩余部分
2.4.3 mini style-loader 代码编写
// loaders/simple-style-loader.js
const loaderUtils = require('loader-utils');
module.exports = function (source) {
// do nothing
}
/**
* @param {*} remainingRequest loader 链中排在自己后面的 loader 以及资源文件的绝对路径以!作为连接符组成的字符串
* @returns
*/
module.exports.pitch = function (remainingRequest) {
console.log('Lyrelion simple-style-loader is working');
// 在 pitch 阶段返回脚本
return (
`
// 创建 style 标签
let style = document.createElement('style');
/**
* 利用 remainingRequest 参数获取 loader 链的剩余部分
* 利用 ‘!!’ 前缀跳过其他 loader
* 利用 loaderUtils 的 stringifyRequest 方法将模块的绝对路径转为相对路径
* 将获取 css 的 require 表达式赋给 style 标签
*/
style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)});
// 将 style 标签插入 head
document.head.appendChild(style);
`
)
}
2.4.4 使用 mini style-loader
webpack.config.js
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {...},
output: {...},
// 手动配置 loader 路径
resolveLoader: {
modules: [path.resolve(__dirname, 'loaders'), 'node_modules']
},
module: {
rules: [
{
// 配置处理 css 的 loader
test: /\.css$/,
use: ['simple-style-loader', 'css-loader']
}
]
},
plugins: [
// 渲染首页
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
}
src/index.css
body {
background: pink;
}
src/index.js
require('./index.css');
// let p = require('./nodejs.png');
// console.log(p);
console.log('----------- Lyrelion 测试 Loader -----------');
目录整体结构:
打包后的页面效果展示:
2.4.5 在 Vue 项目中使用 Loader
在 vue.config.js 引入 Loader:
const MyStyleLoader = require('./simple-style-loader')
在 configureWebpack 中,添加配置:
module.exports = {
configureWebpack: {
module: {
rules: [
{
// 对 main.css 文件使用 MyStyleLoader 处理
test: /main.css/,
loader: MyStyleLoader
}
]
}
}
};
3. 开发 Loader 必备工具包
loader-utils
该模块中,常用的几个方法:
- getOptions 获取 loader 的配置项
- interpolateName 处理生成文件的名字
- stringifyRequest 把绝对路径处理成相对根目录的相对路径
schema-utils
该模块用于验证 loader option 配置的合法性
使用方法:
// loaders/simple-loader-with-validate.js
const loaderUtils = require('loader-utils');
const validate = require('schema-utils');
module.exports = function(source) {
// 获取 loader 配置项
let options = loaderUtils.getOptions(this) || {};
// 定义配置项结构和类型
let schema = {
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
// 验证配置项是否符合要求
validate(schema, options);
return source;
}
4. 开发异步 Loader
异步 Loader 的开发(例如:需要读取文件的操作),需要通过 this.async() 获取异步回调,然后手动调用它
使用方法:
// loaders/simple-async-loader.js
module.exports = function(source) {
console.log('async loader');
let cb = this.async();
setTimeout(() => {
console.log('ok');
// 在异步回调中手动调用 cb 返回处理结果
cb(null, source);
}, 3000);
}
PS:异步回调 cb() 的第一个参数是 error,第二个参数是返回结果
5. Loader raw 设为 true,用于支持 二进制格式资源
Webpack 默认是以 utf-8 的格式读取文件内容给 Loader
如果是用于处理 图片、字体 等资源的 Loader,需要将 Loader 上的 raw 属性设置为 true,让 loader 支持二进制格式资源
使用方法:
// loaders/simple-raw-loader.js
module.exports = function(source) {
// 将输出 buffer 类型的二进制数据
console.log(source);
// todo handle source
let result = 'results of processing source'
return `
module.exports = '${result}'
`;
}
// 告诉 webpack 这个 loader 需要接收的是二进制格式的数据
module.exports.raw = true;
6. file-loader 基本原理、输出文件
在开发一些处理资源文件(比如图片、字体等)的 Loader 中,需要拷贝或生成新的文件,可以使用内部的 this.emitFile() 方法
file-loader 基本原理:
- Loader 读取图片内容(buffer),将其重命名
- 调用 this.emitFile() 输出到指定目录
- 返回一个模块,这个模块导出重命名后的图片地址
- 最终实现了:当 require 图片的时候,就相当于 require 了一个模块,从而得到图片路径
基本用法:
// loaders/simple-file-loader.js
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// 获取 loader 的配置项
let options = loaderUtils.getOptions(this) || {};
// 获取用户设置的文件名或者制作新的文件名
// 注意第三个参数,是计算 contenthash 的依据
let url = loaderUtils.interpolateName(this, options.filename || '[contenthash].[ext]', {content: source});
// 输出文件
this.emitFile(url, source);
// 返回导出文件地址的模块脚本
return `module.exports = '${JSON.stringify(url)}'`;
}
module.exports.raw = true;
7. Loader 开发约定
Loader 的本质是一个 node 模块,这个模块导出一个函数,这个函数上可能还有一个 pitch 方法
了解了 Loader 的本质、Loader 链的执行机制,其实就已经具备了 Loader 开发基础了
开发 Loader 不难上手,但是要开发一款高质量的 Loader,仍需不断实践
在 Webpack 社区,有一份 loader 开发准则:
- 保持简单
- 利用多个 loader 链
- 模块化输出
- 确保 loader 是无状态的
- 使用 loader-utils 包
- 标记加载程序依赖项
- 解析模块依赖关系
- 提取公共代码
- 避免绝对路径
- 使用 peerDependency 对等依赖项
8. 参考文章
揭秘webpack loader | ChampYin's BlogLoader(加载器) 是 webpack 的核心之一。它用于将不同类型的文件转换为 webpack 可识别的模块。本文将尝试深入探索 webpack 中的 loader,揭秘它的工作原理,以及如何开发一个 loader。https://champyin.com/2020/01/28/%E6%8F%AD%E7%A7%98webpack-loader/