本文會帶你簡單的認識一下webpack的loader,動手實現一個利用md轉成抽象語法樹,再轉成html字符串的loader。順便簡單的瞭解一下幾個style-loader,vue-loader,babel-loader的源碼以及工做流程。css
webpack容許咱們使用loader來處理文件,loader是一個導出爲function的node模塊。能夠將匹配到的文件進行一次轉換,同時loader能夠鏈式傳遞。 loader文件處理器是一個CommonJs風格的函數,該函數接收一個 String/Buffer 類型的入參,並返回一個 String/Buffer 類型的返回值。vue
方案1:node
// webpack.config.js
module.exports = {
...
module: {
rules: [{
test: /.vue$/,
loader: 'vue-loader'
}, {
test: /.scss$/,
// 先通過 sass-loader,而後將結果傳入 css-loader,最後再進入 style-loader。
use: [
'style-loader',//從JS字符串建立樣式節點
'css-loader',// 把 CSS 翻譯成 CommonJS
{
loader: 'sass-loader',
options: {
data: '$color: red;'// 把 Sass 編譯成 CSS
}
}
]
}]
}
...
}
複製代碼
方法2(右到左地被調用)webpack
// module
import Styles from 'style-loader!css-loader?modules!./styles.css';
複製代碼
當鏈式調用多個 loader 的時候,請記住它們會以相反的順序執行。取決於數組寫法格式,從右向左或者從下向上執行。像流水線同樣,挨個處理每一個loader,前一個loader的結果會傳遞給下一個loader,最後的 Loader 將處理後的結果以 String 或 Buffer 的形式返回給 compiler。git
import { getOptions } from 'loader-utils';
import { validateOptions } from 'schema-utils';
const schema = {
// ...
}
export default function(content) {
// 獲取 options
const options = getOptions(this);
// 檢驗loader的options是否合法
validateOptions(schema, options, 'Demo Loader');
// 在這裏寫轉換 loader 的邏輯
// ...
return content;
};
複製代碼
同步 loader,咱們能夠經過return
和this.callback
返回輸出的內容github
module.exports = function(content, map, meta) {
//一些同步操做
outputContent=someSyncOperation(content)
return outputContent;
}
複製代碼
若是返回結果只有一個,也能夠直接使用 return 返回結果。可是,若是有些狀況下還須要返回其餘內容,如sourceMap或是AST語法樹,這個時候能夠藉助webpack提供的api this.callback
web
module.exports = function(content, map, meta) {
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
return;
}
複製代碼
第一個參數必須是 Error 或者 null 第二個參數是一個 string 或者 Buffer。 可選的:第三個參數必須是一個能夠被這個模塊解析的 source map。 可選的:第四個選項,會被 webpack 忽略,能夠是任何東西【能夠將抽象語法樹(abstract syntax tree - AST)(例如 ESTree)做爲第四個參數(meta),若是你想在多個 loader 之間共享通用的 AST,這樣作有助於加速編譯時間。】。npm
異步loader,使用 this.async 來獲取 callback 函數。api
// 讓 Loader 緩存
module.exports = function(source) {
var callback = this.async();
// 作異步的事
doSomeAsyncOperation(content, function(err, result) {
if(err) return callback(err);
callback(null, result);
});
};
複製代碼
詳情請參考官網API
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
}
};
複製代碼
上述的例子是經過現成的插件把markdown文件裏的content轉成html字符串,可是若是沒有這個插件,改怎麼作呢?這個狀況下,咱們能夠考慮另一種解法,藉助 AST 語法樹,來協助咱們更加便捷地操做轉換。
markdown-ast
是將markdown文件裏的content轉成數組形式的抽象語法樹節點,操做 AST 語法樹遠比操做字符串要簡單、方便得多:
const md = require('markdown-ast');//經過正則的方法把字符串處理成直觀的AST語法樹
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經過正則切割的方法轉成抽象語樹
const md = require('markdown-ast');//md經過正則匹配的方法把buffer轉抽象語法樹
const hljs = require('highlight.js');//代碼高亮插件
// 利用 AST 做源碼轉換
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轉抽象語法樹操做",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
}
}
複製代碼
ast抽象語法數轉成html字符串
this.cacheable&&this.cacheable(false);
複製代碼
loader-runner
是一個很是實用的工具,用來開發、調試loader,它容許你不依靠 webpack 單獨運行 loader npm install loader-runner --save-dev
// 建立 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))
);
複製代碼
執行 node run-loader
做用:把樣式插入到DOM中,方法是在head中插入一個style標籤,並把樣式寫入到這個標籤的 innerHTML 裏 看下源碼。
先去掉option處理代碼,這樣就比較清晰明瞭了
style-loader.js
module.exports.pitch = function (request) {
const {stringifyRequest}=loaderUtils
var result = [
//1. 獲取css內容。2.// 調用addStyle把CSS內容插入到DOM中(locals爲true,默認導出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(';')
}
複製代碼
須要說明的是,正常咱們都會用default的方法,這裏用到pitch方法。pitch 方法有一個官方的解釋在這裏 pitching loader。簡單的解釋一下就是,默認的loader都是從右向左執行,用 pitching loader
是從左到右執行的。
{
test: /\.css$/,
use: [
{ loader: "style-loader" },
{ loader: "css-loader" }
]
}
複製代碼
爲何要先執行style-loader
呢,由於咱們要把css-loader
拿到的內容最終輸出成CSS樣式中能夠用的代碼而不是字符串。
addstyle.js
module.exports = function (content) {
let style = document.createElement("style")
style.innerHTML = content
document.head.appendChild(style)
}
複製代碼
首先看下跳過loader的配置處理,看下babel-loader輸出
transpile(source, options)
的code和map 再來看下
transpile
方法作了啥
const babel = require("babel-core")
module.exports = function (source) {
const babelOptions = {
presets: ['env']
}
return babel.transform(source, babelOptions).code
}
複製代碼
vue單文件組件(簡稱sfc)
<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配置
const VueloaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
...
module: {
rules: [
...
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
}
plugins: [
new VueloaderPlugin()
]
...
}
複製代碼
VueLoaderPlugin 做用:將在webpack.config定義過的其它規則複製並應用到 .vue 文件裏相應語言的塊中。
plugin-webpack4.js
const vueLoaderUse = vueUse[vueLoaderUseIndex]
vueLoaderUse.ident = 'vue-loader-options'
vueLoaderUse.options = vueLoaderUse.options || {}
// cloneRule會修改原始rule的resource和resourceQuery配置,
// 攜帶特殊query的文件路徑將被應用對應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單文件中的各個標籤能夠應用clonedRules相關的配置
compiler.options.module.rules = [
pitcher,
...clonedRules,
...rules
]
複製代碼
獲取webpack.config.js
的rules項,而後複製rules,爲攜帶了?vue&lang=xx...query
參數的文件依賴配置xx後綴文件一樣的loader 爲Vue文件配置一個公共的loader:pitcher 將[pitchLoder, ...clonedRules, ...rules]
做爲webapck新的rules。
再看一下vue-loader
結果的輸出
// vue-loader使用`@vue/component-compiler-utils`將SFC源碼解析成SFC描述符,,根據不一樣 module path 的類型(query 參數上的 type 字段)來抽離 SFC 當中不一樣類型的 block。
const { parse } = require('@vue/component-compiler-utils')
// 將單個*.vue文件內容解析成一個descriptor對象,也稱爲SFC(Single-File Components)對象
// descriptor包含template、script、style等標籤的屬性和內容,方便爲每種標籤作對應處理
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(loaderContext),
filename,
sourceRoot,
needMap: sourceMap
})
// 爲單文件組件生成惟一哈希id
const id = hash(
isProduction
? (shortFilePath + '\n' + source)
: shortFilePath
)
// 若是某個style標籤包含scoped屬性,則須要進行CSS Scoped處理
const hasScoped = descriptor.styles.some(s => s.scoped)
複製代碼
而後下一步將新生成的 js module 加入到 webpack 的編譯環節,即對這個 js module 進行 AST 的解析以及相關依賴的收集過程。
來看下源碼是怎麼操做不一樣type類型(template/script/style
)的,selectBlock 方法內部主要就是根據不一樣的 type 類型,來獲取 descriptor 上對應類型的 content 內容並傳入到下一個 loader 處理
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&"
複製代碼
總結一下vue-loader的工做流程
VueLoaderPlugin
在插件中,會複製當前項目webpack配置中的rules項,當資源路徑包含query.lang時經過resourceQuery匹配相同的rules並執行對應loader時 插入一個公共的loader,並在pitch階段根據query.type插入對應的自定義loadervue-loader
,.vue文件被解析成一個descriptor
對象,包含template、script、styles
等屬性對應各個標籤, 對於每一個標籤,會根據標籤屬性拼接src?vue&query
引用代碼,其中src爲單頁面組件路徑,query爲一些特性的參數,比較重要的有lang、type和scoped 若是包含lang屬性,會匹配與該後綴相同的rules並應用對應的loaders 根據type執行對應的自定義loader,template
將執行templateLoader
、style
將執行stylePostLoader
templateLoader
中,會經過vue-template-compiler
將template轉換爲render函數,在此過程當中, 會將傳入的scopeId
追加到每一個標籤的上,最後做爲vnode的配置屬性傳遞給createElemenet
方法, 在render函數調用並渲染頁面時,會將scopeId
屬性做爲原始屬性渲染到頁面上stylePostLoader
中,經過PostCSS解析style標籤內容