本文會(huì)帶你簡(jiǎn)單的認(rèn)識(shí)一下webpack的loader澄暮,動(dòng)手實(shí)現(xiàn)一個(gè)利用md轉(zhuǎn)成抽象語(yǔ)法樹名段,再轉(zhuǎn)成html字符串的loader阱扬。順便簡(jiǎn)單的了解一下幾個(gè)style-loader,vue-loader伸辟,babel-loader的源碼以及工作流程价认。
### loader簡(jiǎn)介
webpack允許我們使用loader來(lái)處理文件,loader是一個(gè)導(dǎo)出為function的node模塊自娩。可以將匹配到的文件進(jìn)行一次轉(zhuǎn)換渠退,同時(shí)loader可以鏈?zhǔn)絺鬟f忙迁。
loader文件處理器是一個(gè)CommonJs風(fēng)格的函數(shù),該函數(shù)接收一個(gè) String/Buffer 類型的入?yún)⑺槟耍⒎祷匾粋€(gè) String/Buffer 類型的返回值姊扔。
### loader 的配置的兩種形式
方案1:
```js
// webpack.config.js
module.exports = {
? ...
? module: {
? ? rules: [{
? ? ? test: /.vue$/,
? ? ? loader: 'vue-loader'
? ? }, {
? ? ? test: /.scss$/,
? ? ? // 先經(jīng)過(guò) sass-loader,然后將結(jié)果傳入 css-loader梅誓,最后再進(jìn)入 style-loader恰梢。
? ? ? use: [
? ? ? ? 'style-loader',//從JS字符串創(chuàng)建樣式節(jié)點(diǎn)
? ? ? ? 'css-loader',// 把? CSS 翻譯成 CommonJS
? ? ? ? {
? ? ? ? ? loader: 'sass-loader',
? ? ? ? ? options: {
? ? ? ? ? ? data: '$color: red;'// 把 Sass 編譯成 CSS
? ? ? ? ? }
? ? ? ? }
? ? ? ]
? ? }]
? }
? ...
}
```
方法2(右到左地被調(diào)用)
```js
// module
import Styles from 'style-loader!css-loader?modules!./styles.css';
```
當(dāng)鏈?zhǔn)秸{(diào)用多個(gè) loader 的時(shí)候,請(qǐng)記住它們會(huì)以相反的順序執(zhí)行梗掰。取決于數(shù)組寫法格式嵌言,從右向左或者從下向上執(zhí)行。像流水線一樣及穗,挨個(gè)處理每個(gè)loader摧茴,前一個(gè)loader的結(jié)果會(huì)傳遞給下一個(gè)loader,最后的 Loader 將處理后的結(jié)果以 String 或 Buffer 的形式返回給 compiler埂陆。
### 使用 loader-utils 能夠編譯 loader 的配置苛白,還可以通過(guò) schema-utils 進(jìn)行驗(yàn)證
```js
import { getOptions } from 'loader-utils';
import { validateOptions } from 'schema-utils';?
const schema = {
? // ...
}
export default function(content) {
? // 獲取 options
? const options = getOptions(this);
? // 檢驗(yàn)loader的options是否合法
? validateOptions(schema, options, 'Demo Loader');
? // 在這里寫轉(zhuǎn)換 loader 的邏輯
? // ...
? return content;?
};
```
- content: 表示源文件字符串或者buffer
- map: 表示sourcemap對(duì)象
- meta: 表示元數(shù)據(jù),輔助對(duì)象
### 同步loader
同步 loader焚虱,我們可以通過(guò)`return`和`this.callback`返回輸出的內(nèi)容
```js
module.exports = function(content, map, meta) {
? //一些同步操作
? outputContent=someSyncOperation(content)
? return outputContent;
}
```
如果返回結(jié)果只有一個(gè)购裙,也可以直接使用 return 返回結(jié)果。但是鹃栽,如果有些情況下還需要返回其他內(nèi)容躏率,如sourceMap或是AST語(yǔ)法樹,這個(gè)時(shí)候可以借助webpack提供的api `this.callback`
```js
module.exports = function(content, map, meta) {
? this.callback(
? ? err: Error | null,
? ? content: string | Buffer,
? ? sourceMap?: SourceMap,
? ? meta?: any
? );
? return;
}
```
第一個(gè)參數(shù)必須是 Error 或者 null
第二個(gè)參數(shù)是一個(gè) string 或者 Buffer谍咆。
可選的:第三個(gè)參數(shù)必須是一個(gè)可以被這個(gè)模塊解析的 source map禾锤。
可選的:第四個(gè)選項(xiàng),會(huì)被 webpack 忽略摹察,可以是任何東西【可以將抽象語(yǔ)法樹(abstract syntax tree - AST)(例如 ESTree)作為第四個(gè)參數(shù)(meta)恩掷,如果你想在多個(gè) loader 之間共享通用的 AST,這樣做有助于加速編譯時(shí)間供嚎』颇铮】峭状。
### 異步loader
異步loader,使用 this.async 來(lái)獲取 callback 函數(shù)逼争。
```js
// 讓 Loader 緩存
module.exports = function(source) {
? ? var callback = this.async();
? ? // 做異步的事
? ? doSomeAsyncOperation(content, function(err, result) {
? ? ? ? if(err) return callback(err);
? ? ? ? callback(null, result);
? ? });
};
```
詳情請(qǐng)參考[官網(wǎng)API](https://www.webpackjs.com/api/loaders/#%E5%90%8C%E6%AD%A5-loader)
### 開發(fā)一個(gè)簡(jiǎn)單的md-loader
```js
const marked = require("marked");
const loaderUtils = require("loader-utils");
module.exports = function (content) {
? ? this.cacheable && this.cacheable();
? ? const options = loaderUtils.getOptions(this);
? ? try {
? ? ? ? marked.setOptions(options);
? ? ? ? return marked(content)
? ? } catch (err) {
? ? ? ? this.emitError(err);
? ? ? ? return null
? ? }
};
```
上述的例子是通過(guò)現(xiàn)成的插件把markdown文件里的content轉(zhuǎn)成html字符串优床,但是如果沒(méi)有這個(gè)插件,改怎么做呢誓焦?這個(gè)情況下胆敞,我們可以考慮另外一種解法,借助 AST 語(yǔ)法樹杂伟,來(lái)協(xié)助我們更加便捷地操作轉(zhuǎn)換移层。
### 利用 AST 作源碼轉(zhuǎn)換
`markdown-ast`是將markdown文件里的content轉(zhuǎn)成數(shù)組形式的抽象語(yǔ)法樹節(jié)點(diǎn),操作 AST 語(yǔ)法樹遠(yuǎn)比操作字符串要簡(jiǎn)單赫粥、方便得多:
```js
const md = require('markdown-ast');//通過(guò)正則的方法把字符串處理成直觀的AST語(yǔ)法樹
module.exports = function(content) {
? ? this.cacheable && this.cacheable();
? ? const options = loaderUtils.getOptions(this);
? ? try {
? ? ? console.log(md(content))
? ? ? const parser = new MdParser(content);
? ? ? return parser.data
? ? } catch (err) {
? ? ? console.log(err)
? ? ? return null
? ? }
};
```
**md通過(guò)正則切割的方法轉(zhuǎn)成抽象語(yǔ)樹**

```js
const md = require('markdown-ast');//md通過(guò)正則匹配的方法把buffer轉(zhuǎn)抽象語(yǔ)法樹
const hljs = require('highlight.js');//代碼高亮插件
// 利用 AST 作源碼轉(zhuǎn)換
class MdParser {
constructor(content) {
? ? this.data = md(content);
? ? console.log(this.data)
this.parse()
}
parse() {
this.data = this.traverse(this.data);
}
traverse(ast) {
? ? console.log("md轉(zhuǎn)抽象語(yǔ)法樹操作",ast)
? ? let body = '';
? ? ast.map(item => {
? ? ? switch (item.type) {
? ? ? ? case "bold":
? ? ? ? case "break":
? ? ? ? case "codeBlock":
? ? ? ? ? const highlightedCode = hljs.highlight(item.syntax, item.code).value
? ? ? ? ? body += highlightedCode
? ? ? ? ? break;
? ? ? ? case "codeSpan":
? ? ? ? case "image":
? ? ? ? case "italic":
? ? ? ? case "link":
? ? ? ? case "list":
? ? ? ? ? item.type = (item.bullet === '-') ? 'ul' : 'ol'
? ? ? ? ? if (item.type !== '-') {
? ? ? ? ? ? item.startatt = (` start=${item.indent.length}`)
? ? ? ? ? } else {
? ? ? ? ? ? item.startatt = ''
? ? ? ? ? }
? ? ? ? ? body += '<' + item.type + item.startatt + '>\n' + this.traverse(item.block) + '</' + item.type + '>\n'
? ? ? ? ? break;
? ? ? ? case "quote":
? ? ? ? ? let quoteString = this.traverse(item.block)
? ? ? ? ? body += '<blockquote>\n' + quoteString + '</blockquote>\n';
? ? ? ? ? break;
? ? ? ? case "strike":
? ? ? ? case "text":
? ? ? ? case "title":
? ? ? ? ? body += `<h${item.rank}>${item.text}</h${item.rank}>`
? ? ? ? ? break;
? ? ? ? default:
? ? ? ? ? throw Error("error", `No corresponding treatment when item.type equal${item.type}`);
? ? ? }
? ? })
? ? return body
}
}
```
[完整的代碼參考這里](https://github.com/6fedcom/fe-blog/blob/master/webpack-loader/loaders/md-loader.js)
**ast抽象語(yǔ)法數(shù)轉(zhuǎn)成html字符串**

### loader的一些開發(fā)技巧
1. 盡量保證一個(gè)loader去做一件事情观话,然后可以用不同的loader組合不同的場(chǎng)景需求
2. 開發(fā)的時(shí)候不應(yīng)該在 loader 中保留狀態(tài)。loader必須是一個(gè)無(wú)任何副作用的純函數(shù)越平,loader支持異步频蛔,因此是可以在 loader 中有 I/O 操作的。
3. 模塊化:保證 loader 是模塊化的秦叛。loader 生成模塊需要遵循和普通模塊一樣的設(shè)計(jì)原則晦溪。
4. 合理的使用緩存
合理的緩存能夠降低重復(fù)編譯帶來(lái)的成本。loader 執(zhí)行時(shí)默認(rèn)是開啟緩存的书闸,這樣一來(lái)尼变, webpack 在編譯過(guò)程中執(zhí)行到判斷是否需要重編譯 loader 實(shí)例的時(shí)候,會(huì)直接跳過(guò) rebuild 環(huán)節(jié)浆劲,節(jié)省不必要重建帶來(lái)的開銷嫌术。
但是當(dāng)且僅當(dāng)有你的 loader 有其他不穩(wěn)定的外部依賴(如 I/O 接口依賴)時(shí),可以關(guān)閉緩存:
```js
this.cacheable&&this.cacheable(false);
```
5. `loader-runner` 是一個(gè)非常實(shí)用的工具牌借,用來(lái)開發(fā)度气、調(diào)試loader,它允許你不依靠 webpack 單獨(dú)運(yùn)行 loader
```npm install loader-runner --save-dev```
```js
// 創(chuàng)建 run-loader.js
const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");
runLoaders(
? {
? ? resource: "./readme.md",
? ? loaders: [path.resolve(__dirname, "./loaders/md-loader")],
? ? readResource: fs.readFile.bind(fs),
? },
? (err, result) =>
? ? (err ? console.error(err) : console.log(result))
);
```
執(zhí)行 `node run-loader`
### 認(rèn)識(shí)更多的loader
##### style-loader源碼簡(jiǎn)析
作用:把樣式插入到DOM中,方法是在head中插入一個(gè)style標(biāo)簽膨报,并把樣式寫入到這個(gè)標(biāo)簽的 innerHTML 里
看下源碼磷籍。
先去掉option處理代碼,這樣就比較清晰明了了

返回一段js代碼现柠,通過(guò)require來(lái)獲取css內(nèi)容院领,再通過(guò)addStyle的方法把css插入到dom里
自己實(shí)現(xiàn)一個(gè)簡(jiǎn)陋的`style-loader.js`
```js
module.exports.pitch = function (request) {
? const {stringifyRequest}=loaderUtils
? var result = [
? ? //1. 獲取css內(nèi)容。2.// 調(diào)用addStyle把CSS內(nèi)容插入到DOM中(locals為true够吩,默認(rèn)導(dǎo)出css)
? ? 'var content=require(' + stringifyRequest(this, '!!' + request) + ')’,
? ? 'require(' + stringifyRequest(this, '!' + path.join(__dirname, "addstyle.js")) + ')(content)’,
? ? 'if(content.locals) module.exports = content.locals’
? ]
? return result.join(';')
}
```
需要說(shuō)明的是比然,正常我們都會(huì)用default的方法,這里用到pitch方法周循。pitch 方法有一個(gè)官方的解釋在這里 pitching loader强法。簡(jiǎn)單的解釋一下就是万俗,默認(rèn)的loader都是從右向左執(zhí)行,用 `pitching loader` 是從左到右執(zhí)行的饮怯。
```js
{
? test: /\.css$/,
? use: [
? ? { loader: "style-loader" },
? ? { loader: "css-loader" }
? ]
}
```
為什么要先執(zhí)行`style-loader`呢闰歪,因?yàn)槲覀円裛css-loader`拿到的內(nèi)容最終輸出成CSS樣式中可以用的代碼而不是字符串。
`addstyle.js`
```js
module.exports = function (content) {
? let style = document.createElement("style")
? style.innerHTML = content
? document.head.appendChild(style)
}
```
##### babel-loader源碼簡(jiǎn)析
首先看下跳過(guò)loader的配置處理蓖墅,看下babel-loader輸出

上圖我們可以看到是輸出`transpile(source, options)`的code和map
再來(lái)看下`transpile`方法做了啥

babel-loader是通過(guò)babel.transform來(lái)實(shí)現(xiàn)對(duì)代碼的編譯的库倘,
這么看來(lái),所以我們只需要幾行代碼就可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的babel-loader
```js
const babel = require("babel-core")
module.exports = function (source) {
? const babelOptions = {
? ? presets: ['env']
? }
? return babel.transform(source, babelOptions).code
}
```
##### vue-loader源碼簡(jiǎn)析
vue單文件組件(簡(jiǎn)稱sfc)
```vue
<template>
? <div class="text">
? ? {{a}}
? </div>
</template>
<script>
export default {
? data () {
? ? return {
? ? ? a: "vue demo"
? ? };
? }
};
</script>
<style lang="scss" scope>
.text {
? color: red;
}
</style>
```
webpack配置
```js
const VueloaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
? ...
? module: {
? ? rules: [
? ? ? ...
? ? ? {
? ? ? ? test: /\.vue$/,
? ? ? ? loader: 'vue-loader'
? ? ? }
? ? ]
? }
? plugins: [
? ? new VueloaderPlugin()
? ]
? ...
}
```
**VueLoaderPlugin**
作用:將在webpack.config定義過(guò)的其它規(guī)則復(fù)制并應(yīng)用到 .vue 文件里相應(yīng)語(yǔ)言的塊中论矾。
`plugin-webpack4.js`
```js
const vueLoaderUse = vueUse[vueLoaderUseIndex]
? ? vueLoaderUse.ident = 'vue-loader-options'
? ? vueLoaderUse.options = vueLoaderUse.options || {}
? ? // cloneRule會(huì)修改原始rule的resource和resourceQuery配置于樟,
? ? // 攜帶特殊query的文件路徑將被應(yīng)用對(duì)應(yīng)rule
? ? const clonedRules = rules
? ? ? .filter(r => r !== vueRule)
? ? ? .map(cloneRule)
? ? // global pitcher (responsible for injecting template compiler loader & CSS
? ? // post loader)
? ? const pitcher = {
? ? ? loader: require.resolve('./loaders/pitcher'),
? ? ? resourceQuery: query => {
? ? ? ? const parsed = qs.parse(query.slice(1))
? ? ? ? return parsed.vue != null
? ? ? },
? ? ? options: {
? ? ? ? cacheDirectory: vueLoaderUse.options.cacheDirectory,
? ? ? ? cacheIdentifier: vueLoaderUse.options.cacheIdentifier
? ? ? }
? ? }
? ? // 更新webpack的rules配置,這樣vue單文件中的各個(gè)標(biāo)簽可以應(yīng)用clonedRules相關(guān)的配置
? ? compiler.options.module.rules = [
? ? ? pitcher,
? ? ? ...clonedRules,
? ? ? ...rules
? ? ]
```
獲取`webpack.config.js`的rules項(xiàng)拇囊,然后復(fù)制rules九府,為攜帶了`?vue&lang=xx...query`參數(shù)的文件依賴配置xx后綴文件同樣的loader
為Vue文件配置一個(gè)公共的loader:pitcher
將`[pitchLoder, ...clonedRules, ...rules]`作為webapck新的rules逝钥。
再看一下`vue-loader`結(jié)果的輸出

當(dāng)引入一個(gè)vue文件后,vue-loader是將vue單文件組件進(jìn)行parse撒顿,獲取每個(gè) block 的相關(guān)內(nèi)容关霸,將不同類型的 block 組件的 Vue SFC 轉(zhuǎn)化成 js module 字符串传黄。
```js
// vue-loader使用`@vue/component-compiler-utils`將SFC源碼解析成SFC描述符,,根據(jù)不同 module path 的類型(query 參數(shù)上的 type 字段)來(lái)抽離 SFC 當(dāng)中不同類型的 block。
const { parse } = require('@vue/component-compiler-utils')
// 將單個(gè)*.vue文件內(nèi)容解析成一個(gè)descriptor對(duì)象队寇,也稱為SFC(Single-File Components)對(duì)象
// descriptor包含template膘掰、script、style等標(biāo)簽的屬性和內(nèi)容佳遣,方便為每種標(biāo)簽做對(duì)應(yīng)處理
const descriptor = parse({
? source,
? compiler: options.compiler || loadTemplateCompiler(loaderContext),
? filename,
? sourceRoot,
? needMap: sourceMap
})
// 為單文件組件生成唯一哈希id
const id = hash(
? isProduction
? ? (shortFilePath + '\n' + source)
? : shortFilePath
)
// 如果某個(gè)style標(biāo)簽包含scoped屬性识埋,則需要進(jìn)行CSS Scoped處理
const hasScoped = descriptor.styles.some(s => s.scoped)
```
然后下一步將新生成的 js module 加入到 webpack 的編譯環(huán)節(jié),即對(duì)這個(gè) js module 進(jìn)行 AST 的解析以及相關(guān)依賴的收集過(guò)程零渐。
來(lái)看下源碼是怎么操作不同type類型(`template/script/style`)的窒舟,selectBlock 方法內(nèi)部主要就是根據(jù)不同的 type 類型,來(lái)獲取 descriptor 上對(duì)應(yīng)類型的 content 內(nèi)容并傳入到下一個(gè) loader 處理

這三段代碼可以把不同type解析成一個(gè)import的字符串
```js
import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&"
import script from "./App.vue?vue&type=script&lang=js&"
export * from "./App.vue?vue&type=script&lang=js&"
import style0 from "./App.vue?vue&type=style&index=0&lang=scss&scope=true&"
```
**總結(jié)一下vue-loader的工作流程**
1. 注冊(cè)`VueLoaderPlugin`
在插件中诵盼,會(huì)復(fù)制當(dāng)前項(xiàng)目webpack配置中的rules項(xiàng)惠豺,當(dāng)資源路徑包含query.lang時(shí)通過(guò)resourceQuery匹配相同的rules并執(zhí)行對(duì)應(yīng)loader時(shí)
插入一個(gè)公共的loader,并在pitch階段根據(jù)query.type插入對(duì)應(yīng)的自定義loader
2. 加載*.vue時(shí)會(huì)調(diào)用`vue-loader`,.vue文件被解析成一個(gè)`descriptor`對(duì)象风宁,包含`template洁墙、script、styles`等屬性對(duì)應(yīng)各個(gè)標(biāo)簽戒财,
對(duì)于每個(gè)標(biāo)簽热监,會(huì)根據(jù)標(biāo)簽屬性拼接`src?vue&query`引用代碼,其中src為單頁(yè)面組件路徑固翰,query為一些特性的參數(shù)狼纬,比較重要的有l(wèi)ang羹呵、type和scoped
如果包含lang屬性,會(huì)匹配與該后綴相同的rules并應(yīng)用對(duì)應(yīng)的loaders
根據(jù)type執(zhí)行對(duì)應(yīng)的自定義loader疗琉,`template`將執(zhí)行`templateLoader`冈欢、`style`將執(zhí)行`stylePostLoader`
3. 在`templateLoader`中,會(huì)通過(guò)`vue-template-compiler`將template轉(zhuǎn)換為render函數(shù)盈简,在此過(guò)程中凑耻,
會(huì)將傳入的`scopeId`追加到每個(gè)標(biāo)簽的上,最后作為vnode的配置屬性傳遞給`createElemenet`方法柠贤,
在render函數(shù)調(diào)用并渲染頁(yè)面時(shí)香浩,會(huì)將`scopeId`屬性作為原始屬性渲染到頁(yè)面上
4. 在`stylePostLoader`中,通過(guò)PostCSS解析style標(biāo)簽內(nèi)容
### 參考文獻(xiàn)
1. [webpack官網(wǎng)loader api](https://www.webpackjs.com/api/loaders/)
2. [手把手教你寫webpack yaml-loader](https://mp.weixin.qq.com/s/gTAq5K5pziPT4tmiGqw5_w)
3. [言川-webpack 源碼解析系列](https://github.com/lihongxun945/diving-into-webpack)
4. [從vue-loader源碼分析CSS Scoped的實(shí)現(xiàn)](https://juejin.im/post/5d8627355188253f3a70c22c)