想要实现一个loader,需要首先了解loader的基本原理和用法。
1. 使用
loader是处理模块的解析器。
module: { rules: [ { test: /\.css$/, use: [ // 多个loader,从右向左解析,即css-loader开始 MiniCssExtractPlugin.loader, 'css-loader' ] } }
2.自定义loader的查找规则
很多时候,我们可以自己定义loader, 比如在根目录下新建一个loaders的文件夹,文件夹内实现各个loader的代码。但是webpack不识别这些loader,我们需要配置使webpack识别这些自定义的loader。
有四种方式:
1. resolveLoader.moduels
resolveLoader: { modules: ['node_modules', 'loaders'] // 先从node_modules中查找,没有从loaders文件夹中查找loader1.js }, module: { rules: [ { test: /\.js/, use: ['loader1'] } ] }
2.resolveLoader.alias
resolveLoader: { alias: {// 绝对路径 loader1: path.resolve(__dirname, 'loaders', 'loader1.js') } },
3.loader的绝对路径
module: { rules: [ { test: /\.js/, use: [path.resolve(__dirname, 'loaders', 'loader1.js')] } ] }
4.npm link(待解决)
3. loader的标准
1. 一个loader只实现一个功能,复合设计的单一功能原则。
2. loader的处理顺序。
当一个文件需要多个loader时,从最后的loader开始执行,其传入的参数是文件的原始内容。返回结果传入倒数第二个loader, 作为其入参,依次处理,直到第一个loader。
3. loaders 处理的最终结果(最后一个loader返回值)是一个字符串/Buffer。
4. loader类型
loader的加载顺序是按照pre->normal->inline->post的顺序执行
1.pre-前置loader
rule.enforce = pre;
{ test: /test\.js$/, loader: 'loader3', enforce: 'pre' },
2.normal-正常loader
没有任何特征的loader都是普通loader
3.inline-行内loader
// 对test.js使用loader1和loader2 import 'loader1!loader2!./test.js'; // 按照从右到左,先执行loader2
行内loader的一个应用场景是,loader中pitch的参数remainingRequest。其通过loaderUtils.stringifyRequest(this, XXXX)后,变为
"../loaders/css-loader.js!./style.css"
对于正常的.css文件,会根据webpack中的规则,从右向左加载。但是对于上面的行内loader,有三个标志符号指定哪些loader。
1)! 忽略普通loader
// 表示忽略webpack配置中的正常loader,然后按照loader类型的顺序加载 require("!" + "../loaders/css-loader.js!./style.css")
2. -! 忽略普通和前置loader
// 表示忽略webpack配置中的正常和前置loader require("-!" + "../loaders/css-loader.js!./style.css")
3. !! 只使用行内loader 忽略普通,前置,后置loader;
// 表示只使用行内loader, 忽略webpack配置中的loader require("!!" + "../loaders/css-loader.js!./style.css")
4.post-后置loader
{ test: /test\.js$/, loader: 'loader5', enforce: 'post' },
6. loaders的常见API
1. this.callback
当loader有单个返回值时可以直接使用return返回。当需要返回多个结果时,需要使用this.callback。
其预期参数如下:
this.callback( err: Error | null, content: string | Buffer, sourceMap?:SourceMap, // 可选传参 meta?:any //元数据,可以是任意值;当将AST作为参数传递时,可以提高编译速度 }
⚠️: 使用该方法时,loader必须返回undefined。
2. 越过loader(Pitching loader)
含义:
Pitching loader指的是loader上的pitch方法。
语法:
module.exports = function (content) { console.log(this.data); // {value: 42} return stringorBuffer; } /** * 对于请求index.js的rule * use: ['loader1','loader2', 'loader3'] * * @param {*} remainingRequest * 剩余的请求。 * 如果返回undefined,则按照remainingRequest的顺序访问下一个loader的pitch * 对于第一个被调用的pitch方法来说,其值为: loader2!loader3!index.js * * @param {*} precedingRequest * 前一个请求。 * 1. 如果返回一个非undefined值,则直接进入precedingRequest所在的loader方法, * 并且将pitch的返回值作为该loader方法的参数。 * 如果该loader不是FinalLoader,按照从右到左顺序依次执行 * 2. 有一个特殊情况,如果第一个pitch方法返回一个非undefined值, * 它必须是string|Buffer,因为它将作为该FinalLoader的返回值 * * @param {*} data * pitch中的数据。 * 初始值是空对象{},可以给其赋值,然后通过loader方法中的this.date共享该数据 */ module.exports.pitch = function(remainingRequest, precedingRequest, data) { data.value = 42; // 此处可以返回数据;但是如果是第一个pitch,只能返回string|Buffer,它就是最终结果 }
作用:
正常的loader加载顺序是从右到左。但是在执行loader之前,会从左到右的调用loader上的pitch方法,可以根据该方法的返回值,决定后续的loader要跳过不执行。其方法中传入的data数据可以通过loader方法中的this.data进行共享。
应用场景:
1 )最左侧的两个loader之间有关联关系;手动加载loader。
如:style-loader和css-loader
2 ) pitch阶段给data赋值,在执行阶段从this.data取值
3)通过pitch可以跳过某些loader
执行顺序:
use: [ 'a-loader', 'b-loader', 'c-loader' ] // 当所有的loader的pitch方法都返回undefined时,正确的执行顺序如下 |- a-loader `pitch` |- b-loader `pitch` |- c-loader `pitch` |- requested module is picked up as a dependency |- c-loader normal execution |- b-loader normal execution |- a-loader normal execution
如果某个loader的pitch方法返回一个非undefined的值,将会跳过剩余的loader。
// 如果上面的b-loader返回一个结果,则执行顺序为 |- a-loader `pitch` |- b-loader `pitch` returns a module |- a-loader normal execution
3. raw
设置loader的raw属性为true,则内容变为二进制形式。针对图片,文件等。
此时content.length就是文件的大小
7. loader工具库中常见方法
loader-utils: 内含各种处理loader的options的各种工具函数
schema-utils: 用于校验loader和plugin的数据结构
我们根据上面的要求,可以自己完成常见loader的实现。
1. loaderUtils.stringifyRequest(this, itemUrl)
将URL转为适合loader的相对路径
/Users/lyralee/Desktop/MyStudy/React/loaders/loaders/css-loader.js!/Users/lyralee/Desktop/MyStudy/React/loaders/src/style.css // 使用了loaderUtils.stringifyRequest(this, XXXX)方法后 "../loaders/css-loader.js!./style.css"
2. loaderUtils.getOptions(this)
获取loader的options对象
3. schemaUtils(schema, options)
校验options的格式
8.自模拟实现loader
1. babel-loader
简单的模拟实现babel-loader。它本身是基于@babel/core和其他插件和预设。
const babel = require('@babel/core'); const loaderUtils = require('loader-utils'); const path = require('path'); function loader(inputSource) { const loaderOptions = loaderUtils.getOptions(this); const options = { ...options, sourceMap: true, //是否生成映射 filename: path.basename(this.resourcePath) //从路径中获取目标文件名 } const {code, map, ast} = babel.transform(inputSource, loaderOptions); // 将内容传递给webpack /** * code: 处理后的字符串 * map: 代码的source-map * ast: 生成的AST */ this.callback(null, code, map, ast); } module.exports = loader;
2. banner-loader
给解析的模块添加注释信息。该loader主要用于学习schema-utils的用法。
const babel = require('@babel/core'); // 获取loader的options const loaderUtils = require('loader-utils'); // 校验loader的options const validationOptions = require('schema-utils'); const fs = require('fs'); /** * * @param {*} inputSource * 该方法只接受内容作为入参,要注意使用该插件的顺序, * 如果在其他返回多个参数的loader之后接受参数,会丢失内容 */ function loader(inputSource) { // 该loader启用缓存 this.cacheable(); // 用于异步操作中 const callback = this.async(); const schema = { type: 'object', properties: { text: { type: 'string' }, filename: { type: 'string'} } } const options = loaderUtils.getOptions(this); // 校验options格式是否符合自定义的格式schema validationOptions(schema, options); const { code } = babel.transform(inputSource); // 读取外部文件,作为注释的内容 fs.readFile(options.filename, 'utf8', (err, text) => { callback(null, options.text + text + code); }) } module.exports = loader;
按照loader中的要求,options必须含有两个字段,filename和text,否则会报错
{ loader: 'banner-loader', options: { text: '/***lyra code ***/', filename: path.resolve(__dirname, 'banner.txt') } }
3. less-loader
const less = require('less'); module.exports = function(content) { const callback = this.async(); less.render(content, {filename: this.resource}, (err, result) => { callback(null, result.css) }) }
4. css-loader
/** * 主要实现处理@import 和 url() 语法,基于postcss */ //通过js插件处理样式 const postcss = require('postcss'); // css选择器的词法分析器,用于解析和序列化css选择器 const Tokenizer = require("css-selector-tokenizer"); module.exports = function(content) { const callback = this.async(); const options = { importItems: [], urlItems: [] }; postcss([createPlugin(options)]).process(content).then(result => { const {importItems, urlItems} = options; let requires = importItems.map(itemUrl => ( `require(${itemUrl});` ) ).join(''); // require(url)返回一个打包后的绝对路径 let cssstring = JSON.stringify(result.css).replace(/_CSS_URL_(\d+)/g, function(match, g1) { // "background-image: url('" + require('" + url + "')"; return '"+ require("' + urlItems[+g1] + '").default + "'; }); cssstring = cssstring.replace(/@import\s+['"][^'"]+['"];/g, ''); callback(null, `${requires}module.exports=${cssstring}`); }) } // 自定义的js插件 function createPlugin({urlItems, importItems}) { return function(css) { // 遍历@import规则 css.walkAtRules(/^import$/, function(result) { importItems.push(result.params); }) // 遍历每一条样式 css.walkDecls(function(decl) { // 解析样式属性的值 const values = Tokenizer.parseValues(decl.value); values.nodes.forEach(value => { value.nodes.forEach(item => { if(item.type === 'url') { let url = item.url; item.url = "_CSS_URL_" + urlItems.length; urlItems.push(url); } }) }) // 将解析后值返回序列化 decl.value = Tokenizer.stringifyValues(values); }) } }
5.style-loader
const loaderUtils = require('loader-utils'); module.exports.pitch = function(remainingRquest, precedingRequest, data){ const script = ( ` const style = document.createElement('style'); style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRquest)}); document.head.appendChild(style); ` ) return script; }
6. file-loader
/** * 获取内容了;修改名称;在打包文件夹中输出 */ const { interpolateName, getOptions } = require('loader-utils'); module.exports = function(content) { const { name='[name].[hahs].[ext]' } = getOptions(this) || {}; const outFilename = interpolateName(this, name, {content}); this.emitFile(outFilename, content); return `module.exports=${JSON.stringify(outFilename)}` } // 内容二进制形式 module.exports.raw = true;
7.url-loader
/** * 当小于limit时,使用base64; * 当大于limit时,根据file-loader处理 */ const { getOptions } = require('loader-utils'); const fileLoader = require('file-loader'); const mime = require('mime'); module.exports = function(content) { const { limit=10*1024 } = getOptions(this) || {}; if (content.length < limit) { const base64 = `data:${mime.getType(this.resourcePath)};base64,${content.toString('base64')}` return `module.exports = "${base64}"` } return fileLoader.call(this, content) } module.exports.raw = true;