想要實現一個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;