手写webpack系列一:了解认识loader-utils
Created By JishuBao on 2019-03-29 12:38:22
Recently revised in 2019-04-01 12:38:22
?
??歡迎大家來到技術寶的掘金世界,您的star是我寫文章最大的動力!GitHub地址 ?? ?
文章簡介:
1、webpack module簡介
2、rules的使用
3、loader-utils是什么?
4、api實現
5、未完待續
一、webpack module簡介
現在前端的技術日新月異,現在比較火的前端打包工具! webpack,我想各位小伙伴應該也有所了解,那么你知道如何手寫webpack嗎,作為手寫webpack系列的第一篇文章,我將帶領大家逐步走進webpack的世界。webpack中有一個及其重要的概念loader,那什么是loader呢?在webpack的官網中,我們可以看到這樣一行對Loader的定義,原來是處理js后綴文件名除外的文件。
webpack 可以使用 loader 來預處理文件。這允許你打包除 JavaScript 之外的任何靜態資源。你可以使用 Node.js 來很簡單地編寫自己的 loader。
在loader中,咱們可以通過關鍵詞this訪問當前執行環境的所有變量
1、同步回調時,可以執行this.callback(),默認第一個參數是err錯誤信息(不報錯時返回null),第二個參數是解析完模塊后的返回結果,第三個參數是sourceMap(可選),在鏈式調用時可將sourceMap傳給下一個loader;
2、異步回調時,可以執行this.async(),參數同上;
3、this.addDependency(filePath)可以把對應filePath的文件添加到webpack的依賴樹,webpack可以監測它的文件變動并刷新(filePath要是絕對路徑);
4、this.resolve()可以解析處理文件路徑;
5、this.query:獲取loader的配置選項。
了解或者使用過webpack的小伙伴都知道loader是使用在weboack的module里面的,用于處理module文件的。我們來看下module屬性對應的一些常用api!
- module.noParse
noParse 配置項可以讓 Webpack 忽略對部分沒采用模塊化的文件的遞歸解析和處理,這樣做的好處是能提高構建性能。 原因是一些庫例如 jQuery 、ChartJS 它們龐大又沒有采用模塊化標準,讓 Webpack 去解析這些文件耗時又沒有意義。noParse 是可選配置項,類型需要是 RegExp、[RegExp]、function 其中一個.例如想要忽略掉 jQuery 、ChartJS,可以使用如下代碼:
// 使用正則表達式 noParse: /jquery|chartjs/// 使用函數,從 Webpack 3.0.0 開始支持 noParse: (content)=> {// content 代表一個模塊的文件路徑// 返回 true or falsereturn /jquery|chartjs/.test(content); } 復制代碼- module.rules
配置模塊的讀取和解析規則,通常用來配置 Loader。其類型是一個數組,數組里每一項都描述了如何去處理部分文件。 配置一項 rules 時大致通過以下方式:
1.條件匹配:通過 test 、 include 、 exclude 三個配置項來命中 Loader 要應用規則的文件。
2.應用規則:對選中后的文件通過 use 配置項來應用 Loader,可以只應用一個 Loader 或者按照從后往前的順序應用一組 Loader,同時還可以分別給 Loader 傳入參數。
3.重置順序:一組 Loader 的執行順序默認是從右到左執行,通過 enforce 選項可以讓其中一個 Loader 的執行順序放到最前或者最后。 下面來通過一個例子來說明具體使用方法:
module: {rules: [{// 命中 JavaScript 文件test: /\.js$/,// 用 babel-loader 轉換 JavaScript 文件// ?cacheDirectory 表示傳給 babel-loader 的參數,用于緩存 babel 編譯結果加快重新編譯速度use: ['babel-loader?cacheDirectory'],// 只命中src目錄里的js文件,加快 Webpack 搜索速度include: path.resolve(__dirname, 'src')},{// 命中 SCSS 文件test: /\.scss$/,// 使用一組 Loader 去處理 SCSS 文件。// 處理順序為從后到前,即先交給 sass-loader 處理,再把結果交給 css-loader 最后再給 style-loader。use: ['style-loader', 'css-loader', 'sass-loader'],// 排除 node_modules 目錄下的文件exclude: path.resolve(__dirname, 'node_modules'),},{// 對非文本文件采用 file-loader 加載test: /\.(gif|png|jpe?g|eot|woff|ttf|svg|pdf)$/,use: ['file-loader'],},] } 復制代碼在 Loader 需要傳入很多參數時,你還可以通過一個 Object 來描述,例如在上面的 babel-loader 配置中有如下代碼:
use: [{loader:'babel-loader',options:{cacheDirectory:true,},// enforce:'post' 的含義是把該 Loader 的執行順序放到最后// enforce 的值還可以是 pre,代表把 Loader 的執行順序放到最前面enforce:'post'},// 省略其它 Loader ] 復制代碼上面的例子中 test include exclude 這三個命中文件的配置項只傳入了一個字符串或正則,其實它們還都支持數組類型,使用如下:
{test:[/\.jsx?$/,/\.tsx?$/],include:[path.resolve(__dirname, 'src'),path.resolve(__dirname, 'tests'),],exclude:[path.resolve(__dirname, 'node_modules'),path.resolve(__dirname, 'bower_modules'),] } 復制代碼數組里的每項之間是或的關系,即文件路徑符合數組中的任何一個條件就會被命中。
- parser
因為 Webpack 是以模塊化的 JavaScript 文件為入口,所以內置了對模塊化 JavaScript 的解析功能,支持 AMD、CommonJS、SystemJS、ES6。 parser 屬性可以更細粒度的配置哪些模塊語法要解析哪些不解析,和 noParse 配置項的區別在于 parser 可以精確到語法層面, 而 noParse 只能控制哪些文件不被解析。 parser 使用如下:
module: {rules: [{test: /\.js$/,use: ['babel-loader'],parser: {amd: false, // 禁用 AMDcommonjs: false, // 禁用 CommonJSsystem: false, // 禁用 SystemJSharmony: false, // 禁用 ES6 import/exportrequireInclude: false, // 禁用 require.includerequireEnsure: false, // 禁用 require.ensurerequireContext: false, // 禁用 require.contextbrowserify: false, // 禁用 browserifyrequireJs: false, // 禁用 requirejs}},] } 復制代碼二、rules的使用
根據上文我們已經基本了解了rule的使用,里面放一些loader處理對應的文件。隨意打開一個文件,這里已常用的loader file-loader舉例。
我們如果打開大部分loader的源碼,基本上你可以發現一個規律,基本上每個loader里面都有這樣一句,引入了loader-utils這個工具類。那么utils-loader究竟是什么呢?
三、loader-utils是什么?
loader-utils是一個webpack工具類,通過一些方法配合loader處理文件。讓我們一起來解讀一下。 本文旨在通過手寫loader-utils來了解loader-utils的內容。新建文件夾loader-utils,執行命令npm init初始化一個npm項目
npm init 復制代碼新建index.js作為webpack打包入口文件,內容為空。新建webpack.conf.js作為webpack配置文件,內容如下:
const path=require('path'); //path是node.js的一個模塊,提供了一些用于處理文件路勁的小工具 module.exports={entry:{main:"./index.js"//入口起點(entry point)指示 webpack 應該使用哪個模塊,來作為構建其內部依賴圖的開始。進入入口起點后,webpack 會找出有哪些模塊和庫是入口起點(直接和間接)依賴的},resolve:{},module:{rules:[{test:/\.js$/,//通過loader來預處理文件 允許你打包除了js之外的任何靜態資源use:[{loader:path.resolve('./loader-util.js'),options:{name:'wjb'}}]},]},plugins:[]} 復制代碼新建loader-utils.js文件作為webpack解析的loader。內容如下:
;const loaderUtils=require("loader-utils");function loader(content) {var publicPath="a";console.log('進入了loader內部');return `${publicPath};`; }module.exports = loader; 復制代碼修改package.json文件,新建dev和入口文件
{"name": "loader-util","version": "1.0.0","description": "","main": "index.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1","dev": "webpack --config webpack.conf.js"},"author": "","license": "ISC","dependencies": {"webpack": "^4.29.6"},"devDependencies": {"webpack-cli": "^3.3.0"} } 復制代碼執行命令npm run dev
npm run dev 復制代碼 會發現已經進入了你寫的這個loader里面了四、api實現
新建loader-equal-utils文件夾存放模仿loader-utils工具類的功能。新建index.js文件夾。
- parseQuery(解析被調用loader的配置選項)
將傳遞的字符串(例如loaderContext.resourceQuery)解析為查詢字符串,并返回一個對象。
const params = loaderUtils.parseQuery(this.resourceQuery); // resource: `file?param1=foo` if (params.param1 === "foo") {// do something } 復制代碼新建parseQuery.js文件,首先安裝json5,json5是json的一個超集,具體的不再贅述。
;const JSON5 = require('json5');const specialValues = {null: null,true: true,false: false, };function parseQuery(query) {if (query.substr(0, 1) !== '?') {//如果字符串不是以?開頭 就拋出錯誤throw new Error("A valid query string passed to parseQuery should begin with '?'");}query = query.substr(1);//將query變成去掉?的字符串if (!query) {//如果是空return {};}if (query.substr(0, 1) === '{' && query.substr(-1) === '}') {//如果是對象 返回解析return JSON5.parse(query);}const queryArgs = query.split(/[,&]/g);//將字符串以, &符號分割成字符串數組const result = {};//定義對象存儲數值queryArgs.forEach((arg) => {//遍歷數組const idx = arg.indexOf('=');//找到字符中是=的下標if (idx >= 0) {//當下表大于0,即存在=時let name = arg.substr(0, idx);//將=號之前 即namelet value = decodeURIComponent(arg.substr(idx + 1));//加密valueif (specialValues.hasOwnProperty(value)) {//當有值是true false 或者undefined時value = specialValues[value];//將value變成true false undefined}if (name.substr(-2) === '[]') {name = decodeURIComponent(name.substr(0, name.length - 2));if (!Array.isArray(result[name])) {result[name] = [];//將name鍵賦值為[]空數組值}result[name].push(value);//將resultpush成值} else {name = decodeURIComponent(name);result[name] = value;}} else {if (arg.substr(0, 1) === '-') {result[decodeURIComponent(arg.substr(1))] = false;} else if (arg.substr(0, 1) === '+') {result[decodeURIComponent(arg.substr(1))] = true;} else {result[decodeURIComponent(arg)] = true;}}});return result; }module.exports=parseQuery; 復制代碼在laoder-utils文件導入parseQuery,進行測試
更多參數解析:
-> Error ? -> {} ?flag -> { flag: true } ?+flag -> { flag: true } ?-flag -> { flag: false } ?xyz=test -> { xyz: "test" } ?xyz=1 -> { xyz: "1" } // numbers are NOT parsed ?xyz[]=a -> { xyz: ["a"] } ?flag1&flag2 -> { flag1: true, flag2: true } ?+flag1,-flag2 -> { flag1: true, flag2: false } ?xyz[]=a,xyz[]=b -> { xyz: ["a", "b"] } ?a%2C%26b=c%2C%26d -> { "a,&b": "c,&d" } ?{data:{a:1},isJSON5:true} -> { data: { a: 1 }, isJSON5: true } 復制代碼- parseString(是將字符串轉化為json對象)
新建parseString.js文件。
;function parseString(str){console.log(str)try {if (str[0] === '"') {console.log('我進入了1')console.log(str)console.log(JSON.parse(str))return JSON.parse(str);}if (str[0] === "'" && str.substr(str.length - 1) === "'") {//如果是以''包裹的字符串console.log(str)console.log('我進入了2')return parseString(str.replace(/\\.|"/g, (x) => (x === '"' ? '\\"' : x))//轉化為以""包裹的字符串.replace(/^'|'$/g, '"'));}return JSON.parse('"' + str + '"');} catch (e) {console.log('wobaocuole')console.log(e);return str;} }module.exports=parseString; 復制代碼在laoder-utils文件導入parseString,進行測試
- getOptions(檢索被調用loader的配置選項)
新建getOptions.js文件。
檢索加載程序調用選項的推薦方法:
1.如果this.query是字符串:嘗試解析查詢字符串并返回一個新對象
2.如果它不是有效的查詢字符串則拋出
3.如果this.query是對象,它只是返回this.query
4.在任何其他情況下,它只是返回 null
在laoder-utils文件導入getOptions,進行測試
- stringifyRequest(將一個請求轉化為非絕對路徑的可被require或import的字符串;)
新建stringifyRequest文件
將請求轉換為可在內部使用require()或import在避免絕對路徑時使用的字符串。JSON.stringify(...)如果您在加載器中生成代碼,請使用它。為什么這有必要?由于webpack在模塊路徑轉換為模塊ID之前計算哈希值,因此我們必須避免絕對路徑以確??绮煌幾g的一致哈希。
前置知識:
在laoder-utils文件導入stringifyRequest,進行測試
- urlToRequest(將url轉換成適合webpack環境的模塊請求)
新建urlToRequest文件
;//我們不能使用path的isAbsolute方法 因為他對正斜杠開頭的路徑同樣有效 const matchNativeWin32Path = /^[A-Z]:[/\\]|^\\\\/i;function urlToRequest(url, root){// 不能寫空的url字符串if (url === '') {return '';}const moduleRequestRegex = /^[^?]*~/;let request;if (matchNativeWin32Path.test(url)) {// 如果是絕對路徑 保持url不變request = url;}else if (root !== undefined && root !== false && /^\//.test(url)) {// 如果根路徑定義 且url是相對路徑switch (typeof root) {// 1. 如果根路徑是字符串 根路徑作為url路徑的前綴case 'string':// 特殊的: `~` 根轉換為模塊請求if (moduleRequestRegex.test(root)) {request = root.replace(/([^~/])$/, '$1/') + url.slice(1);} else {request = root + url;}break;// 2. 如果root為true,則絕對路徑允許// *當window路徑不是以`/`,一直支持絕對路徑case 'boolean':request = url;break;default:throw new Error("Unexpected parameters to loader-utils 'urlToRequest': url = " +url +', root = ' +root +'.');}}else if (/^\.\.?\//.test(url)) {//相對路徑request = url;} else {// 像一個相對request = './' + url;}// A `~` makes the url an moduleif (moduleRequestRegex.test(request)) {request = request.replace(moduleRequestRegex, '');}return request;}module.exports = urlToRequest; 復制代碼在loader-utils文件導入urlToRequest,進行測試
- getHashDigest(通過限制字符長度獲取文件部分哈希值)
- buffer:應該散列的內容
- hashType之一sha1,md5,sha256,sha512或任何其他的node.js支持的哈希類型
- digestType一hex,base26,base32,base36,base49,base52,base58,base62,base64
- maxLength 字符的最大長度
新建getHashDigest文件
;const baseEncodeTables = {//基本的類型26: 'abcdefghijklmnopqrstuvwxyz',32: '123456789abcdefghjkmnpqrstuvwxyz', // no 0lio36: '0123456789abcdefghijklmnopqrstuvwxyz',49: 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ', // no lIO52: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',58: '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ', // no 0lIO62: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',64: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_', };function encodeBufferToBase(buffer, base) {//buffer轉化為baseconst encodeTable = baseEncodeTables[base];if (!encodeTable) {//如果沒有 拋出錯誤 找不到base類型throw new Error('Unknown encoding base' + base);}const readLength = buffer.length;//緩存的長度const Big = require('big.js');Big.RM = Big.DP = 0;//初始化小數點的最大位數和最小位數let b = new Big(0);//初始化Bigfor (let i = readLength - 1; i >= 0; i--) {b = b.times(256).plus(buffer[i]);//b乘以256再加}let output = '';while (b.gt(0)) {output = encodeTable[b.mod(base)] + output;b = b.div(base);}Big.DP = 20;Big.RM = 1;return output; }function getHashDigest(buffer, hashType, digestType, maxLength) {hashType = hashType || 'md5';maxLength = maxLength || 9999;const hash = require('crypto').createHash(hashType);hash.update(buffer);if (digestType === 'base26' ||digestType === 'base32' ||digestType === 'base36' ||digestType === 'base49' ||digestType === 'base52' ||digestType === 'base58' ||digestType === 'base62' ||digestType === 'base64') {return encodeBufferToBase(hash.digest(), digestType.substr(4)).substr(0,maxLength);} else {return hash.digest(digestType || 'hex').substr(0, maxLength);} }module.exports = getHashDigest; 復制代碼在loader-utils文件導入getHashDigest,進行測試
- interpolateName(自定義資源名稱、hash等)
前置知識 path.parse() 方法返回一個對象,其屬性表示 path 的重要元素。 尾部的目錄分隔符將被忽略,參閱 path.sep。返回對象包含以下屬性 <string> root <string> base <string> name <string> ext <string>
在posix上: path.parse('/home/user/dir/file.txt'); // 返回: // { root: '/', // dir: '/home/user/dir', // base: 'file.txt', // ext: '.txt', // name: 'file' } ┌─────────────────────┬────────────┐ │ dir │ base │ ├──────┬ ├──────┬─────┤ │ root │ │ name │ ext │ " / home/user/dir / file .txt " └──────┴──────────────┴──────┴─────┘ ("" 行中的所有空格都應該被忽略,它們純粹是為了格式化) 在window上: path.parse('C:\\path\\dir\\file.txt'); // 返回: // { root: 'C:\\', // dir: 'C:\\path\\dir', // base: 'file.txt', // ext: '.txt', // name: 'file' } ┌─────────────────────┬────────────┐ │ dir │ base │ ├──────┬ ├──────┬─────┤ │ root │ │ name │ ext │ " C:\ path\dir \ file .txt " └──────┴──────────────┴──────┴─────┘ ("" 行中的所有空格都應該被忽略,它們純粹是為了格式化) 復制代碼新建interpolateName文件
;const path = require('path'); const emojisList = require('./emoj-list'); const getHashDigest = require('./getHashDigest');const emojiRegex = /[\uD800-\uDFFF]./;//判斷是否是emoj表情 const emojiList = emojisList.filter((emoji) => emojiRegex.test(emoji));//將不是emoj表情的排除掉 const emojiCache = {};//emoj表情緩存function encodeStringToEmoji(content, length) {//字符串轉emoj表情if (emojiCache[content]) {return emojiCache[content];}length = length || 1;//如果沒有指定長度就默認為1const emojis = [];//空數組do {//將表情存進emojis數組里面if (!emojiList.length) {throw new Error('Ran out of emoji');}const index = Math.floor(Math.random() * emojiList.length);emojis.push(emojiList[index]);emojiList.splice(index, 1);} while (--length > 0);const emojiEncoding = emojis.join('');//將數組轉化為字符串emojiCache[content] = emojiEncoding;return emojiEncoding;//表情組成的字符串 }function interpolateName(loaderContext, name, options) {let filename;if (typeof name === 'function') {//如果是函數filename = name(loaderContext.resourcePath);} else {filename = name || '[hash].[ext]';}const context = options.context;const content = options.content;const regExp = options.regExp;let ext = 'bin';let basename = 'file';let directory = '';let folder = '';if (loaderContext.resourcePath) {//如果存在路徑const parsed = path.parse(loaderContext.resourcePath);//返回一個對象let resourcePath = loaderContext.resourcePath;if (parsed.ext) {ext = parsed.ext.substr(1);}if (parsed.dir) {basename = parsed.name;resourcePath = parsed.dir + path.sep;//path.sep在window上是\ posix是/}if (typeof context !== 'undefined') {directory = path.relative(context, resourcePath + '_').replace(/\\/g, '/').replace(/\.\.(\/)?/g, '_$1');directory = directory.substr(0, directory.length - 1);} else {directory = resourcePath.replace(/\\/g, '/').replace(/\.\.(\/)?/g, '_$1');}if (directory.length === 1) {directory = '';} else if (directory.length > 1) {folder = path.basename(directory);}}let url = filename;if (content) {// Match hash templateurl = url// `hash` and `contenthash` are same in `loader-utils` context// let's keep `hash` for backward compatibility.replace(/\[(?:([^:\]]+):)?(?:hash|contenthash)(?::([a-z]+\d*))?(?::(\d+))?\]/gi,(all, hashType, digestType, maxLength) =>getHashDigest(content, hashType, digestType, parseInt(maxLength, 10))).replace(/\[emoji(?::(\d+))?\]/gi, (all, length) =>encodeStringToEmoji(content, parseInt(length, 10)));}url = url.replace(/\[ext\]/gi, () => ext).replace(/\[name\]/gi, () => basename).replace(/\[path\]/gi, () => directory).replace(/\[folder\]/gi, () => folder);if (regExp && loaderContext.resourcePath) {const match = loaderContext.resourcePath.match(new RegExp(regExp));match &&match.forEach((matched, i) => {url = url.replace(new RegExp('\\[' + i + '\\]', 'ig'), matched);});}if (typeof loaderContext.options === 'object' &&typeof loaderContext.options.customInterpolateName === 'function') {url = loaderContext.options.customInterpolateName.call(loaderContext,url,name,options);}return url; }module.exports = interpolateName; 復制代碼在loader-utils文件導入interpolateName,進行測試
- isUrlRequest(判斷是否是路徑)
新建isUrlRequest文件
;const path = require('path');function isUrlRequest(url, root) {// An URL is not an request if// 1. It's an absolute url and it is not `windows` path like `C:\dir\file`if (/^[a-z][a-z0-9+.-]*:/i.test(url) && !path.win32.isAbsolute(url)) {return false;}// 2. It's a protocol-relativeif (/^\/\//.test(url)) {return false;}// 3. It's some kind of url for a templateif (/^[{}[\]#*;,'§$%&(=?`′^°<>]/.test(url)) {return false;}// 4. It's also not an request if root isn't set and it's a root-relative urlif ((root === undefined || root === false) && /^\//.test(url)) {return false;}return true; }module.exports = isUrlRequest; 復制代碼在loader-utils文件導入isUrlRequest,進行測試
- getCurrentRequest(獲取當前請求)
在loader-utils文件導入getCurrentRequest,進行測試
- getRemainingRequest(獲取請求)
新建getRemainingRequest文件
;function getRemainingRequest(loaderContext) {if (loaderContext.remainingRequest) {return loaderContext.remainingRequest;}const request = loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map((obj) => obj.request).concat([loaderContext.resource]);return request.join('!'); }module.exports = getRemainingRequest; 復制代碼在loader-utils文件導入getRemainingRequest,進行測試
五、未完待續
揭開了loader-utils神秘的面紗 為我們手寫webpack打下了堅實的基礎
總結
以上是生活随笔為你收集整理的手写webpack系列一:了解认识loader-utils的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎么样让自己更加从容的面对生活
- 下一篇: 30天提升技术人的写作力-第一天