Loader(加载器) 是 webpack 的核心之一。它用于将不一样类型的文件转换为 webpack 可识别的模块。本文将尝试深刻探索 webpack 中的 loader,揭秘它的工做原理,以及如何开发一个 loader。javascript
webpack 只能直接处理 javascript 格式的代码。任何非 js 文件都必须被预先处理转换为 js 代码,才能够参与打包。loader(加载器)就是这样一个代码转换器。它由 webpack 的 loader runner
执行调用,接收原始资源数据做为参数(当多个加载器联合使用时,上一个loader的结果会传入下一个loader),最终输出 javascript 代码(和可选的 source map)给 webpack 作进一步编译。css
pre > normal > inline > post
。从右到左,从下到上
。内联 loader 能够经过添加不一样前缀,跳过其余类型 loader。html
!
跳过 normal loader。-!
跳过 pre 和 normal loader。!!
跳过 pre、 normal 和 post loader。这些前缀在不少场景下很是有用。java
loader 是一个导出一个函数的 node 模块。node
当只有一个 loader 应用于资源文件时,它接收源码做为参数,输出转换后的 js 代码。webpack
// loaders/simple-loader.js module.exports = function loader (source) { console.log('simple-loader is working'); return source; }
这就是一个最简单的 loader 了,这个 loader 啥也没干,就是接收源码,而后原样返回,为了证实这个loader被调用了,我在里面打印了一句话‘simple-loader is working’。git
测试这个 loader:
须要先配置 loader 路径
如果使用 npm 安装的第三方 loader,直接写 loader 的名字就能够了。可是如今用的是本身开发的本地 loader,须要咱们手动配置路径,告诉 webpack 这些 loader 在哪里。github
// webpack.config.js const path = require('path'); module.exports = { entry: {...}, output: {...}, module: { rules: [ { test: /\.js$/, // 直接指明 loader 的绝对路径 use: path.resolve(__dirname, 'loaders/simple-loader') } ] } }
若是以为这样配置本地 loader 并不优雅,能够在 webpack配置本地loader的四种方法 中挑一个你喜欢的。
执行webpack编译
能够看到,控制台输出 ‘simple-loader is working’。说明 loader 成功被调用。web
pitch
是 loader 上的一个方法,它的做用是阻断 loader 链。shell
// loaders/simple-loader-with-pitch.js module.exports = function (source) { console.log('normal excution'); return source; } // loader上的pitch方法,非必须 module.exports.pitch = function() { console.log('pitching graph'); // todo }
pitch 方法不是必须的。若是有 pitch,loader 的执行则会分为两个阶段:pitch
阶段 和 normal execution
阶段。webpack 会先从左到右执行 loader 链中的每一个 loader 上的 pitch 方法(若是有),而后再从右到左执行 loader 链中的每一个 loader 上的普通 loader 方法。
假如配置了以下 loader 链:
use: ['loader1', 'loader2', 'loader3']
真实的 loader 执行过程是:
在这个过程当中若是任何 pitch 有返回值,则 loader 链被阻断。webpack 会跳事后面全部的的 pitch 和 loader,直接进入上一个 loader 的 normal execution
。
假设在 loader2 的 pitch 中返回了一个字符串,此时 loader 链发生阻断:
style-loader 一般不会独自使用,而是跟 css-loader 连用。css-loader 的返回值是一个 js 模块,大体长这样:
// 打印 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;
这个模块在运行时上下文中执行后返回 css
代码 "\nbody {\n background: yellow;\n}\n"
。
style-loader 的做用就是将这段 css
代码转成 style
标签插入到 html
的 head
中。
js
脚本:在脚本中建立一个 style
标签,将 css
代码赋给 style
标签,再将这个 style
标签插入 html
的 head
中。css
代码,由于 css-loader 的返回值只能在运行时的上下文中执行,而执行 loader 是在编译阶段。换句话说,css-loader 的返回值在 style-loader 里派不上用场。css
代码的表达式,在运行时再获取 css (相似 require('css-loader!index.css')
)。inline loader
require css
文件,会产生循环执行 loader 的问题,因此咱们须要利用 pitch
方法,让 style-loader 在 pitch
阶段返回脚本,跳过剩下的 loader,同时还须要内联前缀 !!
的加持。注:pitch 方法有3个参数:
!
做为链接符组成的字符串。!
做为链接符组成的字符串。
能够利用
remainingRequest
参数获取 loader 链的剩余部分。
// loaders/simple-style-loader.js const loaderUtils = require('loader-utils'); module.exports = function(source) { // do nothing } module.exports.pitch = function(remainingRequest) { console.log('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); ` ) }
一个简易的 style-loader 就完成了。
webpack 配置
// 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' }) ] }
在 index.js 中引入一个 css 样式文件
// src/index.js require('./index.css'); console.log('Brovo!');
样式文件中将 body 的背景色设置为黄色
// src/index.css body { background-color: yellow; }
执行webpack
npm run build
能够看到命令行控制台打印了 'simple-style-loader is working',说明 webpack 成功调用了咱们编写的 loader。
在浏览器打开 dist 下的 index.html 页面,能够看到样式生效,并且成功插入到了页面头部!
说明咱们编写的 loader 发挥做用了。
成功!
开发 loader 必备:
1. loader-utils
这个模块中经常使用的几个方法:
2. 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; }
当配置项不符合要求,编译就会中断并在控制台打印错误信息:
异步 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); }
注: 异步回调 cb() 的第一个参数是
error
,要返回的结果放在第二个参数。
若是是处理图片、字体等资源的 loader,须要将 loader 上的 raw 属性设置为 true,让 loader 支持二进制格式资源(webpack默认是以 utf-8
的格式读取文件内容给 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}' `; } // 告诉 wepack 这个 loader 须要接收的是二进制格式的数据 module.exports.raw = true;
注:一般 raw 属性会在有文件输出需求的 loader 中使用。
在开发一些处理资源文件(好比图片、字体等)的 loader 中,须要拷贝或者生成新的文件,可使用内部的 this.emitFile()
方法.
用法:
// 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;
在这个例子中,loader 读取图片内容(buffer),将其重命名,而后调用this.emitFile()
输出到指定目录,最后返回一个模块,这个模块导出重命名后的图片地址。因而当require
图片的时候,就至关于 require 了一个模块,从而获得最终的图片路径。(这就是 file-loader 的基本原理)
为了让咱们的 loader 具备更高的质量和复用性,记得保持简单。也就是尽可能保持让一个 loader 专一一件事情,若是发现你写的 loader 比较庞大,能够试着将其拆成几个 loader 。
在 webpack 社区,有一份 loader 开发准则,咱们能够去参考它来指导咱们的 loader 设计:
文章源码获取:https://github.com/yc111/webp...
欢迎交流~
Happy New Year!
--
参考
https://webpack.js.org/concep...
https://webpack.js.org/api/lo...
https://webpack.js.org/contri...
https://github.com/webpack/we...
https://github.com/webpack-co...
https://www.npmjs.com/package...
https://www.npmjs.com/package...
欢迎转载,转载请注明出处:
https://champyin.com/2020/01/...