對於webpack,一切皆模塊。所以,不管什麼文件,都須要轉換成js可識別模塊。你能夠理解爲,不管什麼後綴的文件,都看成js來使用(即便是img、ppt、txt文件等等)。可是直接看成js使用確定是不行的,需轉換爲一種能被js理解的方式才能看成js模塊來使用——這個轉換的過程由webpack的loader來處理。一個webpack loader 是一個導出爲函數的 js 模塊。webpack內部的
loader runner
會調用這個函數,而後把上一個 loader 產生的結果或者資源文件傳入進去,而後返回處理後的結果前端
下面會從基本使用開始出發,探究一個loader怎麼寫,並實現raw-loader
、json-loader
、url-loader
、bundle-loader
node
準備工做: 先安裝webpack
、webpack-cli
、webpack-dev-server
,後面的實踐用到什麼再裝什麼react
module.exports = {
module: {
rules: [
{
test: /\.js$/, // 匹配規則
use: ['babel-loader'] // require的loader路徑數組
}
]
}
}
複製代碼
寫了這個規則,只要匹配的文件名以.js
爲結尾的,那就會通過use裏面全部的loader處理webpack
raw-loader
來獲取整個txt文件裏面的字符串內容。除了使用統一webpack config配置的方式以外,咱們還能夠在引入的時候,用這樣的語法來引入:import txt from "raw-loader!./1.txt";
// txt就是這個文件裏面全部的內容
複製代碼
其實使用webpack.config文件統一配置loader後,最終也是會轉成這種方式使用loader再引入的。支持多個loader,語法: loader1!loader2!yourfilename
git
query替代optionsgithub
使用loadername! 前綴語法:raw-loader?a=1&b=2!./1.txt
,等價於webpack配置:web
{
test: /^1\.txt$/,
exclude: /node_modules/,
use: [
{ loader: "raw-loader", options: { a: '1', b: '2' } },
]
},
複製代碼
在寫本身的loader的時候,常常會使用loader-utils
(不須要特意安裝,裝了webpack一套就自帶)來獲取傳入參數json
const { getOptions } = require("loader-utils");
module.exports = function(content) {
const options = getOptions(this) || {};
// 若是是配置,返回的是options;若是是loadername!語法,返回根據query字符串生成的對象
// ...
};
複製代碼
下文爲了方便演示,會屢次使用此方法配置loader。若是沒用過這種方法的,就看成入門學習吧😊。搞起~api
一個loader是一個導出爲函數的 js 模塊,這個函數有三個參數:content, map, meta數組
咱們實現一個最最最簡單的,給代碼加上一句console的loader:
// console.js
module.exports = function(content, map, meta) {
return `${content}; console.log('loader exec')`;
};
複製代碼
webpack配置
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{ loader: "./loaders/console" }, // 加上本身寫的loader
]
}
]
},
複製代碼
'loader exec'
這兩個loader就是讀取文件內容,而後可使用import或者require導入原始文件全部的內容。很明顯,原文件被看成js使用的時候,缺乏了一個導出語句,loader作的事情就是加上導出語句。
好比有一個這樣的txt
this is a txt file
複製代碼
假如你把它看成js來用,import或者require進來的時候,執行this is a txt file
這句js,確定會報錯。若是想正常使用,那麼這個txt文件須要改爲:
export default 'this is a txt file'
複製代碼
最終的效果就是,不管是什麼文件,txt、md、json等等,都看成一個js文件來用,原文件內容至關於一個字符串,被導出了:
// 本身寫的raw-loader
const { getOptions } = require("loader-utils");
// 獲取webpack配置的options,寫loader的固定套路第一步
module.exports = function(content, map, meta) {
const opts = getOptions(this) || {};
const code = JSON.stringify(content);
const isESM = typeof opts.esModule !== "undefined" ? options.esModule : true;
// 直接返回原文件內容
return `${isESM ? "export default" : "module.exports ="} ${code}`;
};
複製代碼
raw-loader
和json-loader
幾乎都是同樣的,他們的目的就是把原文件全部的內容做爲一個字符串導出,而json-loader多了一個json.parse的過程
注意:看了一下官方的loader源碼,發現它們還會多一個步驟
JSON.stringify(content)
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
複製代碼
\u2028
和\u2029
是特殊字符,和\n
、\b
之類的相似,但它們特殊之處在於——轉義後直觀上看仍是一個空字符串。能夠看見它特殊之處:
即便你看得見中間有一個奇怪的字符,可是你再按下enter,仍是'ab'
,\u2028
字符串在直觀上來看至關於空字符串(實際上字符是存在的,卻沒有它的帶來的效果)。而對於除了2028和2029,好比\u000A
的\n
,是有換行的效果的(字符存在,也有它帶來的效果)。所以,對於低機率出現的字符值爲2028和2029的轉義是有必要的
Unicode 字符值 | 轉義序列 | 含義 | 類別 |
---|---|---|---|
\u0008 | \b | Backspace | |
\u0009 | \t | Tab | 空白 |
\u000A | \n | 換行符(換行) | 行結束符 |
\u000B | \v | 垂直製表符 | 空白 |
\u000C | \f | 換頁 | 空白 |
\u000D | \r | 回車 | 行結束符 |
\u0022 | " | 雙引號 (") | |
\u0027 | \‘ | 單引號 (‘) | |
\u005C | \ | 反斜槓 () | |
\u00A0 | 不間斷空格 | 空白 | |
\u2028 | 行分隔符 | 行結束符 | |
\u2029 | 段落分隔符 | 行結束符 | |
\uFEFF | 字節順序標記 | 空白 |
咱們前面已經實現了raw-loader
,這個loader是把原文件裏面的內容以字符串形式返回。可是問題來了,有的文件並非一個字符串就能夠解決的了的,好比圖片、視頻、音頻。此時,咱們須要直接利用原文件的buffer
。剛好,loader函數的第一個參數content,支持string/buffer
如何開啓buffer類型的content?
// 只須要導出raw爲true
module.exports.raw = true
複製代碼
url-loader
的流程就是,讀取配置,是否能夠轉、怎麼轉=>讀取原文件buffer=>buffer轉base64輸出 => 沒法轉換的走fallback流程。咱們下面實現一個簡易版本的url-loader
,僅僅實現核心功能
const { getOptions } = require("loader-utils");
module.exports = function(content) {
const options = getOptions(this) || {};
const mimetype = options.mimetype;
const esModule =
typeof options.esModule !== "undefined" ? options.esModule : true;
// base編碼組成:data:[mime類型];base64,[文件編碼後內容]
return `${esModule ? "export default" : "module.exports ="} ${JSON.stringify( `data:${mimetype || ""};base64,${content.toString("base64")}` )}`;
};
module.exports.raw = true;
複製代碼
而後,咱們隨便弄一張圖片,import進來試一下:
// loader路徑自行修改
// img就是一個base64的圖片路徑,能夠直接放img標籤使用
import img from "../../loaders/my-url-loader?mimetype=image!./1.png";
複製代碼
至於file-loader
,相信你們也有思路了吧,流程就是:讀取配置裏面的publicpath=>肯定最終輸出路徑=>文件名稱加上MD5 哈希值=>搬運一份文件,文件名改新的名=>新文件名拼接前面的path=>輸出最終文件路徑
官網對pitching loader介紹是: loader 老是從右到左地被調用。有些狀況下,loader 只關心 request 後面的元數據(metadata),而且忽略前一個 loader 的結果。在實際(從右到左)執行 loader 以前,會先從左到右調用 loader 上的 pitch 方法。其次,若是某個 loader 在 pitch 方法中返回一個結果,那麼這個過程會跳過剩下的 loader
pitch方法的三個參數:
loader從後往前執行這個過程,你能夠視爲順序入棧倒序出棧。好比命中某種規則A的文件,會經歷3個loader: ['a-loader', 'b-loader', 'c-loader']
會經歷這樣的過程:
pitch
方法pitch
方法pitch
方法若是b-loader
裏面有一個pitch方法,並且這個pitch方法有返回結果,那麼上面這個過程自從通過了b-loader
後,就不會再將c-loader
入棧
// b-loader
module.exports = function(content) {
return content;
};
// 沒作什麼,就透傳import進來再export出去
module.exports.pitch = function(remainingRequest) {
// remainingRequest路徑要加-! 前綴
return `import s from ${JSON.stringify( `-!${remainingRequest}` )}; export default s`;
};
複製代碼
b-loader的pitch方法有返回結果,會經歷這樣的過程:
pitch
方法pitch
方法(有返回結果,跳過c-loader)什麼狀況下須要跳過剩下的loader呢?最多見的,就是動態加載和緩存讀取了,要跳事後面loader的計算。
bundle-loader
是一個典型的例子
bundle-loader
實現的是動態按需加載,怎麼使用呢?咱們能夠對react最終ReactDom.render那一步改造一下,換成動態加載react-dom
,再體會一下區別
- import ReactDom from "react-dom";
+ import LazyReactDom from "bundle-loader?lazy&name=reactDom!react-dom";
+ LazyReactDom(ReactDom => {
+ console.log(ReactDom, "ReactDom");
ReactDom.render(<S />, document.getElementById("root"));
+});
複製代碼
能夠看見reactdom被隔離開來,動態引入
點開bundle-loader
源碼,發現它利用的是require.ensure
來動態引入,具體的實現也很簡單,具體看bundle-loader源碼。時代在變化,新時代的動態引入應該是動態import
,下面咱們本身基於動態import來實現一個新的bundle-loader
。(僅實現lazy引入的核心功能)
// 獲取ChunkName
function getChunkNameFromRemainingRequest(r) {
const paths = r.split("/");
let cursor = paths.length - 1;
if (/^index\./.test(paths[cursor])) {
cursor--;
}
return paths[cursor];
}
// 原loader不須要作什麼了
module.exports = function() {};
module.exports.pitch = function(remainingRequest, r) {
// 帶loadername!前綴的依賴路徑
const s = JSON.stringify(`-!${remainingRequest}`);
// 使用註釋webpackChunkName來定義chunkname的語法
return `export default function(cb) { return cb(import(/* webpackChunkName: "my-lazy-${getChunkNameFromRemainingRequest( this.resource )}" */${s})); }`;
};
複製代碼
用法和官方的bundle-loader
基本差很少,只是動態import返回一個promise,須要改一下使用方法:
import LazyReactDom from "../loaders/my-bundle!react-dom";
setTimeout(() => {
LazyReactDom(r => {
r.then(({ default: ReactDom }) => {
ReactDom.render(<S />, document.getElementById("root")); }); }); }, 1000); 複製代碼
上文咱們看見有在寫loader的時候使用this,這個this就是loader的上下文。具體可見官網
一堆上下文的屬性中,咱們拿其中一個來實踐一下: this.loadModule
loadModule(request: string, callback: function(err, source, sourceMap, module))
loadModule
方法做用是,解析給定的 request 到一個模塊,應用全部配置的 loader ,而且在回調函數中傳入生成的 source 、sourceMap和webpack內部的NormalModule
實例。若是你須要獲取其餘模塊的源代碼來生成結果的話,你可使用這個函數。
很明顯,這個方法其中一個應用場景就是,在已有代碼上注入其餘依賴
let's coding
背景:已有一個api文件api.js
const api0 = {
log(...args) {
console.log("api log>>>", ...args);
}
};
module.exports = api0;
複製代碼
但願效果:咱們使用下面這個a.js
js文件的時候,能夠直接使用api,且不報錯
// a.js
export default function a() {
return 1;
}
// 其餘代碼
// ...
api.log("a", "b");
複製代碼
所以,咱們須要構建的時候loader把api打進去咱們的代碼裏面:
// addapi的loader
module.exports = function(content, map, meta) {
// 涉及到加載模塊,異步loader
const callback = this.async();
this.loadModule("../src/api.js", (err, source, sourceMap, module) => {
// source是一個module.exports = require(xxx)的字符串,咱們須要require那部分
callback(
null,
`const api = ${source.split("=")[1]}; ${content};`,
sourceMap,
meta
);
});
return;
};
複製代碼
loader寫好了,記得去webpack配置裏面加上,或者使用loadername!的語法引入a.js(./loaders/addapi!./a.js
)
最後咱們能夠看見成功運行了api.js的log
平時也有一些熟悉的場景,某某某api、某某某sdk、公共utils方法、每個index頁面的pvuv上報等等,須要先把這些js加載執行完或者導入。若是咱們懶得一個個文件加import/require
語句,就能夠用這種方式瞬間完成。這種騷操做的前提是,保證後續同事接手項目難度低、代碼無坑。註釋、文檔、優雅命名都搞起來
loader的做用就是,讓一切文件,轉化爲本身所須要、能使用的js模塊運行起來。babel和loader雙劍合璧更增強大,能夠隨心所欲的修改代碼、偷懶等等。後續還會出webpack插件、babel相關的文章,你們一塊兒來學習交流~
關注公衆號《不同的前端》,以不同的視角學習前端,快速成長,一塊兒把玩最新的技術、探索各類黑科技