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