摘要:夾在中間的被鏈?zhǔn)秸{(diào)用,他們拿到上個的返回值,為下一個提供輸入。最終把返回值和傳給。前面我們說過,也是一個模塊,它導(dǎo)出一個函數(shù),該函數(shù)的參數(shù)是的源模塊,處理后把返回值交給下一個。
文:小 boy(滬江網(wǎng)校Web前端工程師)本文原創(chuàng),轉(zhuǎn)載請注明作者及出處
經(jīng)常逛 webpack 官網(wǎng)的同學(xué)應(yīng)該會很眼熟上面的圖。正如它宣傳的一樣,webpack 能把左側(cè)各種類型的文件(webpack 把它們叫作「模塊」)統(tǒng)一打包為右邊被通用瀏覽器支持的文件。webpack 就像是魔術(shù)師的帽子,放進去一條絲巾,變出來一只白鴿。那這個「魔術(shù)」的過程是如何實現(xiàn)的呢?今天我們從 webpack 的核心概念之一 —— loader 來尋找答案,并著手實現(xiàn)這個「魔術(shù)」??赐瓯疚?,你可以:
知道 webpack loader 的作用和原理。
自己開發(fā)貼合業(yè)務(wù)需求的 loader。
什么是 Loader ?在擼一個 loader 前,我們需要先知道它到底是什么。本質(zhì)上來說,loader 就是一個 node 模塊,這很符合 webpack 中「萬物皆模塊」的思路。既然是 node 模塊,那就一定會導(dǎo)出點什么。在 webpack 的定義中,loader 導(dǎo)出一個函數(shù),loader 會在轉(zhuǎn)換源模塊(resource)的時候調(diào)用該函數(shù)。在這個函數(shù)內(nèi)部,我們可以通過傳入 this 上下文給 Loader API 來使用它們?;仡櫼幌骂^圖左邊的那些模塊,他們就是所謂的源模塊,會被 loader 轉(zhuǎn)化為右邊的通用文件,因此我們也可以概括一下 loader 的功能:把源模塊轉(zhuǎn)換成通用模塊。
Loader 怎么用 ?知道它的強大功能以后,我們要怎么使用 loader 呢?
1. 配置 webpack config 文件既然 loader 是 webpack 模塊,如果我們要使其生效,肯定離不開配置。我這里收集了三種配置方法,任你挑選。
單個 loader 的配置增加 config.module.rules 數(shù)組中的規(guī)則對象(rule object)。
let webpackConfig = { //... module: { rules: [{ test: /.js$/, use: [{ //這里寫 loader 的路徑 loader: path.resolve(__dirname, "loaders/a-loader.js"), options: {/* ... */} }] }] } }多個 loader 的配置
增加 config.module.rules 數(shù)組中的規(guī)則對象以及 config.resolveLoader。
let webpackConfig = { //... module: { rules: [{ test: /.js$/, use: [{ //這里寫 loader 名即可 loader: "a-loader", options: {/* ... */} }, { loader: "b-loader", options: {/* ... */} }] }] }, resolveLoader: { // 告訴 webpack 該去那個目錄下找 loader 模塊 modules: ["node_modules", path.resolve(__dirname, "loaders")] } }其他配置
也可以通過 npm link 連接到你的項目里,這個方式類似 node CLI 工具開發(fā),非 loader 模塊專用,本文就不多討論了。
2. 簡單上手配置完成后,當(dāng)你在 webpack 項目中引入模塊時,匹配到 rule (例如上面的 /.js$/)就會啟用對應(yīng)的 loader (例如上面的 a-loader 和 b-loader)。這時,假設(shè)我們是 a-loader 的開發(fā)者,a-loader 會導(dǎo)出一個函數(shù),這個函數(shù)接受的唯一參數(shù)是一個包含源文件內(nèi)容的字符串。我們暫且稱它為「source」。
接著我們在函數(shù)中處理 source 的轉(zhuǎn)化,最終返回處理好的值。當(dāng)然返回值的數(shù)量和返回方式依據(jù) a-loader 的需求來定。一般情況下可以通過 return 返回一個值,也就是轉(zhuǎn)化后的值。如果需要返回多個參數(shù),則須調(diào)用 this.callback(err, values...) 來返回。在異步 loader 中你可以通過拋錯來處理異常情況。Webpack 建議我們返回 1 至 2 個參數(shù),第一個參數(shù)是轉(zhuǎn)化后的 source,可以是 string 或 buffer。第二個參數(shù)可選,是用來當(dāng)作 SourceMap 的對象。
3. 進階使用通常我們處理一類源文件的時候,單一的 loader是不夠用的(loader 的設(shè)計原則我們稍后講到)。一般我們會將多個 loader 串聯(lián)使用,類似工廠流水線,一個位置的工人(或機器)只干一種類型的活。既然是串聯(lián),那肯定有順序的問題,webpack 規(guī)定 use 數(shù)組中 loader 的執(zhí)行順序是從最后一個到第一個,它們符合下面這些規(guī)則:
順序最后的 loader 第一個被調(diào)用,它拿到的參數(shù)是 source 的內(nèi)容
順序第一的 loader 最后被調(diào)用, webpack 期望它返回 JS 代碼,source map 如前面所說是可選的返回值。
夾在中間的 loader 被鏈?zhǔn)秸{(diào)用,他們拿到上個 loader 的返回值,為下一個 loader 提供輸入。
我們舉個例子:
webpack.config.js
{ test: /.js/, use: [ "bar-loader", "mid-loader", "foo-loader" ] }
在上面的配置中:
loader 的調(diào)用順序是 foo-loader -> mid-loader -> bar-loader。
foo-loader 拿到 source,處理后把 JS 代碼傳遞給 mid,mid 拿到 foo 處理過的 “source” ,再處理之后給 bar,bar 處理完后再交給 webpack。
bar-loader 最終把返回值和 source map 傳給 webpack。
用正確的姿勢開發(fā) Loader了解了基本模式后,我們先不急著開發(fā)。所謂磨刀不誤砍柴工,我們先看看開發(fā)一個 loader 需要注意些什么,這樣可以少走彎路,提高開發(fā)質(zhì)量。下面是 webpack 提供的幾點指南,它們按重要程度排序,注意其中有些點只適用特定情況。
1.單一職責(zé)一個 loader 只做一件事,這樣不僅可以讓 loader 的維護變得簡單,還能讓 loader 以不同的串聯(lián)方式組合出符合場景需求的搭配。
2.鏈?zhǔn)浇M合這一點是第一點的延伸。好好利用 loader 的鏈?zhǔn)浇M合的特型,可以收獲意想不到的效果。具體來說,寫一個能一次干 5 件事情的 loader ,不如細分成 5 個只能干一件事情的 loader,也許其中幾個能用在其他你暫時還沒想到的場景。下面我們來舉個例子。
假設(shè)現(xiàn)在我們要實現(xiàn)通過 loader 的配置和 query 參數(shù)來渲染模版的功能。我們在 “apply-loader” 里面實現(xiàn)這個功能,它負(fù)責(zé)編譯源模版,最終輸出一個導(dǎo)出 HTML 字符串的模塊。根據(jù)鏈?zhǔn)浇M合的規(guī)則,我們可以結(jié)合另外兩個開源 loader:
jade-loader 把模版源文件轉(zhuǎn)化為導(dǎo)出一個函數(shù)的模塊。
apply-loader 把 loader options 傳給上面的函數(shù)并執(zhí)行,返回 HTML 文本。
html-loader 接收 HTMl 文本文件,轉(zhuǎn)化為可被引用的 JS 模塊。
事實上串聯(lián)組合中的 loader 并不一定要返回 JS 代碼。只要下游的 loader 能有效處理上游 loader 的輸出,那么上游的 loader 可以返回任意類型的模塊。3.模塊化
保證 loader 是模塊化的。loader 生成模塊需要遵循和普通模塊一樣的設(shè)計原則。
4.無狀態(tài)在多次模塊的轉(zhuǎn)化之間,我們不應(yīng)該在 loader 中保留狀態(tài)。每個 loader 運行時應(yīng)該確保與其他編譯好的模塊保持獨立,同樣也應(yīng)該與前幾個 loader 對相同模塊的編譯結(jié)果保持獨立。
5.使用 Loader 實用工具請好好利用 loader-utils 包,它提供了很多有用的工具,最常用的一個就是獲取傳入 loader 的 options。除了 loader-utils 之外包還有 schema-utils 包,我們可以用 schema-utils 提供的工具,獲取用于校驗 options 的 JSON Schema 常量,從而校驗 loader options。下面給出的例子簡要地結(jié)合了上面提到的兩個工具包:
import { getOptions } from "loader-utils"; import { validateOptions } from "schema-utils"; const schema = { type: object, properties: { test: { type: string } } } export default function(source) { const options = getOptions(this); validateOptions(schema, options, "Example Loader"); // 在這里寫轉(zhuǎn)換 source 的邏輯 ... return `export default ${ JSON.stringify(source) }`; };loader 的依賴
如果我們在 loader 中用到了外部資源(也就是從文件系統(tǒng)中讀取的資源),我們必須聲明這些外部資源的信息。這些信息用于在監(jiān)控模式(watch mode)下驗證可緩存的 loder 以及重新編譯。下面這個例子簡要地說明了怎么使用 addDependency 方法來做到上面說的事情。
loader.js:
import path from "path"; export default function(source) { var callback = this.async(); var headerPath = path.resolve("header.js"); this.addDependency(headerPath); fs.readFile(headerPath, "utf-8", function(err, header) { if(err) return callback(err); //這里的 callback 相當(dāng)于異步版的 return callback(null, header + " " + source); }); };模塊依賴
不同的模塊會以不同的形式指定依賴。比如在 CSS 中我們使用 @import 和 url(...) 聲明來完成指定,而我們應(yīng)該讓模塊系統(tǒng)解析這些依賴。
如何讓模塊系統(tǒng)解析不同聲明方式的依賴呢?下面有兩種方法:
把不同的依賴聲明統(tǒng)一轉(zhuǎn)化為 require 聲明。
通過 this.resolve 函數(shù)來解析路徑。
對于第一種方式,有一個很好的例子就是 css-loader。它把 @import 聲明轉(zhuǎn)化為 require 樣式表文件,把 url(...) 聲明轉(zhuǎn)化為 require 被引用文件。
而對于第二種方式,則需要參考一下 less-loader。由于要追蹤 less 中的變量和 mixin,我們需要把所有的 .less 文件一次編譯完畢,所以不能把每個 @import 轉(zhuǎn)為 require。因此,less-loader 用自定義路徑解析邏輯拓展了 less 編譯器。這種方式運用了我們剛才提到的第二種方式 —— this.resolve 通過 webpack 來解析依賴。
如果某種語言只支持相對路徑(例如 url(file) 指向 ./file)。你可以用 ~ 將相對路徑指向某個已經(jīng)安裝好的目錄(例如 node_modules)下,因此,拿 url 舉例,它看起來會變成這樣:url(~some-library/image.jpg)。代碼公用
避免在多個 loader 里面初始化同樣的代碼,請把這些共用代碼提取到一個運行時文件里,然后通過 require 把它引進每個 loader。
絕對路徑不要在 loader 模塊里寫絕對路徑,因為當(dāng)項目根路徑變了,這些路徑會干擾 webpack 計算 hash(把 module 的路徑轉(zhuǎn)化為 module 的引用 id)。loader-utils 里有一個 stringifyRequest 方法,它可以把絕對路徑轉(zhuǎn)化為相對路徑。
同伴依賴如果你開發(fā)的 loader 只是簡單包裝另外一個包,那么你應(yīng)該在 package.json 中將這個包設(shè)為同伴依賴(peerDependency)。這可以讓應(yīng)用開發(fā)者知道該指定哪個具體的版本。
舉個例子,如下所示 sass-loader 將 node-sass 指定為同伴依賴:
"peerDependencies": { "node-sass": "^4.0.0" }Talk is cheep
以上我們已經(jīng)為砍柴磨好了刀,接下來,我們動手開發(fā)一個 loader。
如果我們要在項目開發(fā)中引用模版文件,那么壓縮 html 是十分常見的需求。分解以上需求,解析模版、壓縮模版其實可以拆分給兩給 loader 來做(單一職責(zé)),前者較為復(fù)雜,我們就引入開源包 html-loader,而后者,我們就拿來練手。首先,我們給它取個響亮的名字 —— html-minify-loader。
接下來,按照之前介紹的步驟,首先,我們應(yīng)該配置 webpack.config.js ,讓 webpack 能識別我們的 loader。當(dāng)然,最最開始,我們要創(chuàng)建 loader 的 文件 —— src/loaders/html-minify-loader.js。
于是,我們在配置文件中這樣處理:
webpack.config.js
module: { rules: [{ test: /.html$/, use: ["html-loader", "html-minify-loader"] // 處理順序 html-minify-loader => html-loader => webpack }] }, resolveLoader: { // 因為 html-loader 是開源 npm 包,所以這里要添加 "node_modules" 目錄 modules: [path.join(__dirname, "./src/loaders"), "node_modules"] }
接下來,我們提供示例 html 和 js 來測試 loader:
src/example.html:
Document
src/app.js:
var html = require("./expamle.html"); console.log(html);
好了,現(xiàn)在我們著手處理 src/loaders/html-minify-loader.js。前面我們說過,loader 也是一個 node 模塊,它導(dǎo)出一個函數(shù),該函數(shù)的參數(shù)是 require 的源模塊,處理 source 后把返回值交給下一個 loader。所以它的 “模版” 應(yīng)該是這樣的:
module.exports = function (source) { // 處理 source ... return handledSource; }
或
module.exports = function (source) { // 處理 source ... this.callback(null, handledSource) return handledSource; }
注意:如果是處理順序排在最后一個的 loader,那么它的返回值將最終交給 webpack 的 require,換句話說,它一定是一段可執(zhí)行的 JS 腳本 (用字符串來存儲),更準(zhǔn)確來說,是一個 node 模塊的 JS 腳本,我們來看下面的例子。
// 處理順序排在最后的 loader module.exports = function (source) { // 這個 loader 的功能是把源模塊轉(zhuǎn)化為字符串交給 require 的調(diào)用方 return "module.exports = " + JSON.stringify(source); }
整個過程相當(dāng)于這個 loader 把源文件
這里是 source 模塊
轉(zhuǎn)化為
// example.js module.exports = "這里是 source 模塊";
然后交給 require 調(diào)用方:
// applySomeModule.js var source = require("example.js"); console.log(source); // 這里是 source 模塊
而我們本次串聯(lián)的兩個 loader 中,解析 html 、轉(zhuǎn)化為 JS 執(zhí)行腳本的任務(wù)已經(jīng)交給 html-loader 了,我們來處理 html 壓縮問題。
作為普通 node 模塊的 loader 可以輕而易舉地引用第三方庫。我們使用 minimize 這個庫來完成核心的壓縮功能:
// src/loaders/html-minify-loader.js var Minimize = require("minimize"); module.exports = function(source) { var minimize = new Minimize(); return minimize.parse(source); };
當(dāng)然, minimize 庫支持一系列的壓縮參數(shù),比如 comments 參數(shù)指定是否需要保留注釋。我們肯定不能在 loader 里寫死這些配置。那么 loader-utils 就該發(fā)揮作用了:
// src/loaders/html-minify-loader.js var loaderUtils = require("loader-utils"); var Minimize = require("minimize"); module.exports = function(source) { var options = loaderUtils.getOptions(this) || {}; //這里拿到 webpack.config.js 的 loader 配置 var minimize = new Minimize(options); return minimize.parse(source); };
這樣,我們可以在 webpack.config.js 中設(shè)置壓縮后是否需要保留注釋:
module: { rules: [{ test: /.html$/, use: ["html-loader", { loader: "html-minify-loader", options: { comments: false } }] }] }, resolveLoader: { // 因為 html-loader 是開源 npm 包,所以這里要添加 "node_modules" 目錄 modules: [path.join(__dirname, "./src/loaders"), "node_modules"] }
當(dāng)然,你還可以把我們的 loader 寫成異步的方式,這樣不會阻塞其他編譯進度:
var Minimize = require("minimize"); var loaderUtils = require("loader-utils"); module.exports = function(source) { var callback = this.async(); if (this.cacheable) { this.cacheable(); } var opts = loaderUtils.getOptions(this) || {}; var minimize = new Minimize(opts); minimize.parse(source, callback); };
你可以在這個倉庫查看相關(guān)代碼,npm start 以后可以去 http://localhost:9000 打開控制臺查看 loader 處理后的內(nèi)容。
總結(jié)到這里,對于「如何開發(fā)一個 loader」,我相信你已經(jīng)有了自己的答案。總結(jié)一下,一個 loader 在我們項目中 work 需要經(jīng)歷以下步驟:
創(chuàng)建 loader 的目錄及模塊文件
在 webpack 中配置 rule 及 loader 的解析路徑,并且要注意 loader 的順序,這樣在 require 指定類型文件時,我們能讓處理流經(jīng)過指定 laoder。
遵循原則設(shè)計和開發(fā) loader。
最后,Talk is cheep,趕緊動手?jǐn)]一個 loader 耍耍吧~
參考Writing a loader推薦: 翻譯項目Master的自述: 1. 干貨|人人都是翻譯項目的Master 2. iKcamp出品微信小程序教學(xué)共5章16小節(jié)匯總(含視頻) 3. 開始免費連載啦~每周2更共11堂iKcamp課|基于Koa2搭建Node.js實戰(zhàn)項目教學(xué)(含視頻)| 課程大綱介紹
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/92782.html
摘要:組件結(jié)構(gòu)同組件結(jié)構(gòu)通過方法獲取元素的大小及其相對于視口的位置,之后對提示信息進行定位??梢杂脕磉M行一些復(fù)雜帶校驗的彈窗信息展示,也可以只用于簡單信息的展示??梢酝ㄟ^屬性來顯示任意標(biāo)題,通過屬性來修改顯示區(qū)域的寬度。 手把手教你擼個vue2.0彈窗組件 在開始之前需要了解一下開發(fā)vue插件的前置知識,推薦先看一下vue官網(wǎng)的插件介紹 預(yù)覽地址 http://haogewudi.me/k...
摘要:組件結(jié)構(gòu)同組件結(jié)構(gòu)通過方法獲取元素的大小及其相對于視口的位置,之后對提示信息進行定位。可以用來進行一些復(fù)雜帶校驗的彈窗信息展示,也可以只用于簡單信息的展示??梢酝ㄟ^屬性來顯示任意標(biāo)題,通過屬性來修改顯示區(qū)域的寬度。 手把手教你擼個vue2.0彈窗組件 在開始之前需要了解一下開發(fā)vue插件的前置知識,推薦先看一下vue官網(wǎng)的插件介紹 預(yù)覽地址 http://haogewudi.me/k...
摘要:畫字首先我在畫布上畫了個點,用這些點來組成我們要顯示的字,用不到的字就隱藏起來。星星閃爍效果這個效果實現(xiàn)很簡單,就是讓星星不停的震動,具體就是讓點的目的地坐標(biāo)不停的進行小范圍的偏移。 哈哈哈哈?。?!當(dāng)我說在寫這邊文章的時候,妹子已經(jīng)追到了,哈哈哈哈哈?。?! 其實東西是一年前寫的,妹子早就追到手了,當(dāng)時就是用這個東西來表白的咯,二話不說,先看效果(點擊屏幕可顯示下一句) showImg(...
閱讀 2997·2021-11-25 09:43
閱讀 2449·2021-11-24 09:39
閱讀 2794·2021-09-23 11:51
閱讀 1488·2021-09-07 10:11
閱讀 1514·2019-08-27 10:52
閱讀 1993·2019-08-26 12:13
閱讀 3419·2019-08-26 11:57
閱讀 1458·2019-08-26 11:31