系列做者:肖磊javascript
GitHub: github.com/CommanderXLcss
前2篇文章:webpack loader詳解1和webpack loader詳解2主要經過源碼分析了 loader 的配置,匹配和加載,執行等內容,這篇文章會經過具體的實例來學習下如何去實現一個 loader。html
這裏咱們來看下 vue-loader(v15) 內部的相關內容,這裏會講解下有關 vue-loader 的大體處理流程,不會深刻特別細節的地方。vue
git clone git@github.com:vuejs/vue-loader.git
複製代碼
咱們使用 vue-loader 官方倉庫當中的 example 目錄的內容做爲整篇文章的示例。java
首先咱們都知道 vue-loader 配合 webpack 給咱們開發 Vue 應用提供了很是大的便利性,容許咱們在 SFC(single file component) 中去寫咱們的 template/script/style,同時 v15 版本的 vue-loader 還容許開發在 SFC 當中寫 custom block。最終一個 Vue SFC 經過 vue-loader 的處理,會將 template/script/style/custom block 拆解爲獨立的 block,每一個 block 還能夠再交給對應的 loader 去作進一步的處理,例如你的 template 是使用 pug 來書寫的,那麼首先使用 vue-loader 獲取一個 SFC 內部 pug 模板的內容,而後再交給 pug 相關的 loader 處理,能夠說 vue-loader 對於 Vue SFC 來講是一個入口處理器。node
在實際運用過程當中,咱們先來看下有關 Vue 的 webpack 配置:webpack
const VueloaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
...
module: {
rules: [
...
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
}
plugins: [
new VueloaderPlugin()
]
...
}
複製代碼
一個就是 module.rules 有關的配置,若是處理的 module 路徑是以.vue
形式結尾的,那麼會交給 vue-loader 來處理,同時在 v15 版本必須要使用 vue-loader 內部提供的一個 plugin,它的職責是將你定義過的其它規則複製並應用到 .vue
文件裏相應語言的塊。例如,若是你有一條匹配 /\.js$/
的規則,那麼它會應用到 .vue
文件裏的 <script>
塊,說到這裏咱們就一塊兒先來看看這個 plugin 裏面到底作了哪些工做。git
咱們都清楚 webpack plugin 的裝載過程是在整個 webpack 編譯週期中初始階段,咱們先來看下 VueLoaderPlugin 內部源碼的實現:github
// vue-loader/lib/plugin.js
class VueLoaderPlugin {
apply() {
...
// use webpack's RuleSet utility to normalize user rules
const rawRules = compiler.options.module.rules
const { rules } = new RuleSet(rawRules)
// find the rule that applies to vue files
// 判斷是否有給`.vue`或`.vue.html`進行 module.rule 的配置
let vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue`))
if (vueRuleIndex < 0) {
vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue.html`))
}
const vueRule = rules[vueRuleIndex]
...
// 判斷對於`.vue`或`.vue.html`配置的 module.rule 是否有 vue-loader
// get the normlized "use" for vue files
const vueUse = vueRule.use
// get vue-loader options
const vueLoaderUseIndex = vueUse.findIndex(u => {
return /^vue-loader|(\/|\\|@)vue-loader/.test(u.loader)
})
...
// 建立 pitcher 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
}
}
// 拓展開發者的 module.rule 配置,加入 vue-loader 內部提供的 pitcher loader
// replace original rules
compiler.options.module.rules = [
pitcher,
...clonedRules,
...rules
]
}
}
複製代碼
這個 plugin 主要完成了如下三部分的工做:web
.vue
或.vue.html
進行 module.rule 的配置;.vue
或.vue.html
配置的 module.rule 是否有 vue-loader;咱們看到有關 pitcher loader 的 rule 匹配條件是經過resourceQuery
方法來進行判斷的,即判斷 module path 上的 query 參數是否存在 vue,例如:
// 這種類型的 module path 就會匹配上
'./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&'
複製代碼
若是存在的話,那麼就須要將這個 loader 加入到構建這個 module 的 loaders 數組當中。以上就是 VueLoaderPlugin 所作的工做,其中涉及到拓展後的 module rule 裏面加入的 pitcher loader 具體作的工做後文會分析。
接下來咱們看下 vue-loader 的內部實現。首先來看下入口文件的相關內容:
// vue-loader/lib/index.js
...
const { parse } = require('@vue/component-compiler-utils')
function loadTemplateCompiler () {
try {
return require('vue-template-compiler')
} catch (e) {
throw new Error(
`[vue-loader] vue-template-compiler must be installed as a peer dependency, ` +
`or a compatible compiler implementation must be passed via options.`
)
}
}
module.exports = function(source) {
const loaderContext = this // 獲取 loaderContext 對象
// 從 loaderContext 獲取相關參數
const {
target, // webpack 構建目標,默認爲 web
request, // module request 路徑(由 path 和 query 組成)
minimize, // 構建模式
sourceMap, // 是否開啓 sourceMap
rootContext, // 項目的根路徑
resourcePath, // module 的 path 路徑
resourceQuery // module 的 query 參數
} = loaderContext
// 接下來就是一系列對於參數和路徑的處理
const rawQuery = resourceQuery.slice(1)
const inheritQuery = `&${rawQuery}`
const incomingQuery = qs.parse(rawQuery)
const options = loaderUtils.getOptions(loaderContext) || {}
...
// 開始解析 sfc,根據不一樣的 block 來拆解對應的內容
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(),
filename,
sourceRoot,
needMap: sourceMap
})
// 若是 query 參數上帶了 block 的 type 類型,那麼會直接返回對應 block 的內容
// 例如: foo.vue?vue&type=template,那麼會直接返回 template 的文本內容
if (incomingQuery.type) {
return selectBlock(
descriptor,
loaderContext,
incomingQuery,
!!options.appendExtension
)
}
...
// template
let templateImport = `var render, staticRenderFns`
let templateRequest
if (descriptor.template) {
const src = descriptor.template.src || resourcePath
const idQuery = `&id=${id}`
const scopedQuery = hasScoped ? `&scoped=true` : ``
const attrsQuery = attrsToQuery(descriptor.template.attrs)
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}`
const request = templateRequest = stringifyRequest(src + query)
templateImport = `import { render, staticRenderFns } from ${request}`
}
// script
let scriptImport = `var script = {}`
if (descriptor.script) {
const src = descriptor.script.src || resourcePath
const attrsQuery = attrsToQuery(descriptor.script.attrs, 'js')
const query = `?vue&type=script${attrsQuery}${inheritQuery}`
const request = stringifyRequest(src + query)
scriptImport = (
`import script from ${request}\n` +
`export * from ${request}` // support named exports
)
}
// styles
let stylesCode = ``
if (descriptor.styles.length) {
stylesCode = genStylesCode(
loaderContext,
descriptor.styles,
id,
resourcePath,
stringifyRequest,
needsHotReload,
isServer || isShadow // needs explicit injection?
)
}
let code = `
${templateImport}
${scriptImport}
${stylesCode}
/* normalize component */
import normalizer from ${stringifyRequest(`!${componentNormalizerPath}`)}
var component = normalizer(
script,
render,
staticRenderFns,
${hasFunctional ? `true` : `false`},
${/injectStyles/.test(stylesCode) ? `injectStyles` : `null`},
${hasScoped ? JSON.stringify(id) : `null`},
${isServer ? JSON.stringify(hash(request)) : `null`}
${isShadow ? `,true` : ``}
)
`.trim() + `\n`
if (descriptor.customBlocks && descriptor.customBlocks.length) {
code += genCustomBlocksCode(
descriptor.customBlocks,
resourcePath,
resourceQuery,
stringifyRequest
)
}
...
// Expose filename. This is used by the devtools and Vue runtime warnings.
code += `\ncomponent.options.__file = ${
isProduction
// For security reasons, only expose the file's basename in production.
? JSON.stringify(filename)
// Expose the file's full path in development, so that it can be opened
// from the devtools.
: JSON.stringify(rawShortFilePath.replace(/\\/g, '/'))
}`
code += `\nexport default component.exports`
return code
}
複製代碼
以上就是 vue-loader 的入口文件(index.js)主要作的工做:對於 request 上不帶 type 類型的 Vue SFC 進行 parse,獲取每一個 block 的相關內容,將不一樣類型的 block 組件的 Vue SFC 轉化成 js module 字符串,具體的內容以下:
import { render, staticRenderFns } from "./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&"
import script from "./source.vue?vue&type=script&lang=js&"
export * from "./source.vue?vue&type=script&lang=js&"
import style0 from "./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&"
/* normalize component */
import normalizer from "!../lib/runtime/componentNormalizer.js"
var component = normalizer(
script,
render,
staticRenderFns,
false,
null,
"27e4e96e",
null
)
/* custom blocks */
import block0 from "./source.vue?vue&type=custom&index=0&blockType=foo"
if (typeof block0 === 'function') block0(component)
// 省略了有關 hotReload 的代碼
component.options.__file = "example/source.vue"
export default component.exports
複製代碼
從生成的 js module 字符串來看:將由 source.vue 提供 render函數/staticRenderFns,js script,style樣式,並交由 normalizer 進行統一的格式化,最終導出 component.exports。
這樣 vue-loader 處理的第一個階段結束了,vue-loader 在這一階段將 Vue SFC 轉化爲 js module 後,接下來進入到第二階段,將新生成的 js module 加入到 webpack 的編譯環節,即對這個 js module 進行 AST 的解析以及相關依賴的收集過程,這裏我用每一個 request 去標記每一個被收集的 module(這裏只說明和 Vue SFC 相關的模塊內容):
[
'./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&',
'./source.vue?vue&type=script&lang=js&',
'./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&',
'./source.vue?vue&type=custom&index=0&blockType=foo'
]
複製代碼
咱們看到經過 vue-loader 處理到獲得的 module path 上的 query 參數都帶有 vue 字段。這裏便涉及到了咱們在文章開篇提到的 VueLoaderPlugin 加入的 pitcher loader。若是遇到了 query 參數上帶有 vue 字段的 module path,那麼就會把 pitcher loader 加入處處理這個 module 的 loaders 數組當中。所以這個 module 最終也會通過 pitcher loader 的處理。此外在 loader 的配置順序上,pitcher loader 爲第一個,所以在處理 Vue SFC 模塊的時候,最早也是交由 pitcher loader 來處理。
事實上對一個 Vue SFC 處理的第二階段就是剛纔提到的,Vue SFC 會經由 pitcher loader 來作進一步的處理。那麼咱們就來看下 vue-loader 內部提供的 pitcher loader 主要是作了哪些工做呢:
// vue-loader/lib/loaders/pitcher.js
module.export = code => code
module.pitch = function () {
...
const query = qs.parse(this.resourceQuery.slice(1))
let loaders = this.loaders
// 剔除 eslint loader
// if this is a language block request, eslint-loader may get matched
// multiple times
if (query.type) {
// if this is an inline block, since the whole file itself is being linted,
// remove eslint-loader to avoid duplicate linting.
if (/\.vue$/.test(this.resourcePath)) {
loaders = loaders.filter(l => !isESLintLoader(l))
} else {
// This is a src import. Just make sure there's not more than 1 instance
// of eslint present.
loaders = dedupeESLintLoader(loaders)
}
}
// 剔除 pitcher loader 自身
// remove self
loaders = loaders.filter(isPitcher)
if (query.type === 'style') {
const cssLoaderIndex = loaders.findIndex(isCSSLoader)
if (cssLoaderIndex > -1) {
const afterLoaders = loaders.slice(0, cssLoaderIndex + 1)
const beforeLoaders = loaders.slice(cssLoaderIndex + 1)
const request = genRequest([
...afterLoaders,
stylePostLoaderPath,
...beforeLoaders
])
return `import mod from ${request}; export default mod; export * from ${request}`
}
}
if (query.type === 'template') {
const path = require('path')
const cacheLoader = cacheDirectory && cacheIdentifier
? [`cache-loader?${JSON.stringify({ // For some reason, webpack fails to generate consistent hash if we // use absolute paths here, even though the path is only used in a // comment. For now we have to ensure cacheDirectory is a relative path. cacheDirectory: path.isAbsolute(cacheDirectory) ? path.relative(process.cwd(), cacheDirectory) : cacheDirectory, cacheIdentifier: hash(cacheIdentifier) + '-vue-loader-template' })}`]
: []
const request = genRequest([
...cacheLoader,
templateLoaderPath + `??vue-loader-options`,
...loaders
])
// the template compiler uses esm exports
return `export * from ${request}`
}
// if a custom block has no other matching loader other than vue-loader itself,
// we should ignore it
if (query.type === `custom` &&
loaders.length === 1 &&
loaders[0].path === selfPath) {
return ``
}
// When the user defines a rule that has only resourceQuery but no test,
// both that rule and the cloned rule will match, resulting in duplicated
// loaders. Therefore it is necessary to perform a dedupe here.
const request = genRequest(loaders)
return `import mod from ${request}; export default mod; export * from ${request}`
}
複製代碼
對於 style block 的處理,首先判斷是否有 css-loader,若是有的話就從新生成一個新的 request,這個 request 包含了 vue-loader 內部提供的 stylePostLoader,並返回一個 js module,根據 pitch 函數的規則,pitcher loader 後面的 loader 都會被跳過,這個時候開始編譯這個返回的 js module。相關的內容爲:
import mod from "-!../node_modules/vue-style-loader/index.js!../node_modules/css-loader/index.js!../lib/loaders/stylePostLoader.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&"
export default mod
export * from "-!../node_modules/vue-style-loader/index.js!../node_modules/css-loader/index.js!../lib/loaders/stylePostLoader.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&"
複製代碼
對於 template block 的處理流程相似,生成一個新的 request,這個 request 包含了 vue-loader 內部提供的 templateLoader,並返回一個 js module,並跳事後面的 loader,而後開始編譯返回的 js module。相關的內容爲:
export * from "-!../lib/loaders/templateLoader.js??vue-loader-options!../node_modules/pug-plain-loader/index.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&"
複製代碼
這樣對於一個 Vue SFC 處理的第二階段也就結束了,經過 pitcher loader 去攔截不一樣類型的 block,並返回新的 js module,跳事後面的 loader 的執行,同時在內部會剔除掉 pitcher loader,這樣在進入到下一個處理階段的時候,pitcher loader 不在使用的 loader 範圍以內,所以下一階段 Vue SFC 便不會經由 pitcher loader 來處理。
接下來進入到第三個階段,編譯返回的新的 js module,完成 AST 的解析和依賴收集工做,並開始處理不一樣類型的 block 的編譯轉換工做。就拿 Vue SFC 當中的 style / template block 來舉例,
style block 會通過如下的流程處理:
source.vue?vue&type=style -> vue-loader(抽離 style block) -> stylePostLoader(處理做用域 scoped css) -> css-loader(處理相關資源引入路徑) -> vue-style-loader(動態建立 style 標籤插入 css)
template block 會通過如下的流程處理:
source.vue?vue&type=template -> vue-loader(抽離 template block ) -> pug-plain-loader(將 pug 模塊轉化爲 html 字符串) -> templateLoader(編譯 html 模板字符串,生成 render/staticRenderFns 函數並暴露出去)
咱們看到通過 vue-loader 處理時,會根據不一樣 module path 的類型(query 參數上的 type 字段)來抽離 SFC 當中不一樣類型的 block。這也是 vue-loader 內部定義的相關規則:
// vue-loader/lib/index.js
const qs = require('querystring')
const selectBlock = require('./select')
...
module.exports = function (source) {
...
const rawQuery = resourceQuery.slice(1)
const inheritQuery = `&${rawQuery}`
const incomingQuery = qs.parse(rawQuery)
...
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(),
filename,
sourceRoot,
needMap: sourceMap
})
// if the query has a type field, this is a language block request
// e.g. foo.vue?type=template&id=xxxxx
// and we will return early
if (incomingQuery.type) {
return selectBlock(
descriptor,
loaderContext,
incomingQuery,
!!options.appendExtension
)
}
...
}
複製代碼
當 module path 上的 query 參數帶有 type 字段,那麼會直接調用 selectBlock 方法去獲取 type 對應類型的 block 內容,跳過 vue-loader 後面的處理流程(這也是與 vue-loader 第一次處理這個 module時流程不同的地方),並進入到下一個 loader 的處理流程中,selectBlock 方法內部主要就是根據不一樣的 type 類型(template/script/style/custom),來獲取 descriptor 上對應類型的 content 內容並傳入到下一個 loader 處理:
module.exports = function selectBlock ( descriptor, loaderContext, query, appendExtension ) {
// template
if (query.type === `template`) {
if (appendExtension) {
loaderContext.resourcePath += '.' + (descriptor.template.lang || 'html')
}
loaderContext.callback(
null,
descriptor.template.content,
descriptor.template.map
)
return
}
// script
if (query.type === `script`) {
if (appendExtension) {
loaderContext.resourcePath += '.' + (descriptor.script.lang || 'js')
}
loaderContext.callback(
null,
descriptor.script.content,
descriptor.script.map
)
return
}
// styles
if (query.type === `style` && query.index != null) {
const style = descriptor.styles[query.index]
if (appendExtension) {
loaderContext.resourcePath += '.' + (style.lang || 'css')
}
loaderContext.callback(
null,
style.content,
style.map
)
return
}
// custom
if (query.type === 'custom' && query.index != null) {
const block = descriptor.customBlocks[query.index]
loaderContext.callback(
null,
block.content,
block.map
)
return
}
}
複製代碼
經過 vue-loader 的源碼咱們看到一個 Vue SFC 在整個編譯構建環節是怎麼樣一步一步處理的,這也是得益於 webpack 給開發這提供了這樣一種 loader 的機制,使得開發者經過這樣一種方式去對項目源碼作對應的轉換工做以知足相關的開發需求。結合以前的2篇(webpack loader詳解1和webpack loader詳解2)有關 webpack loader 源碼的分析,你們應該對 loader 有了更加深刻的理解,也但願你們活學活用,利用 loader 機制去完成更多貼合實際需求的開發工做。