摘要:相關的內容為這樣對于一個處理的第二階段也就結束了,通過去攔截不同類型的,并返回新的,跳過后面的的執行,同時在內部會剔除掉,這樣在進入到下一個處理階段的時候,不在使用的范圍之內,因此下一階段便不會經由來處理。
文章首發于個人github blog: Biu-blog,歡迎大家關注~
Webpack 系列文章:
Webpack Loader 高手進階(一)
Webpack Loader 高手進階(二)
Webpack Loader 高手進階(三)
前2篇文章主要通過源碼分析了 loader 的配置,匹配和加載,執行等內容,這篇文章會通過具體的實例來學習下如何去實現一個 loader。
這里我們來看下 vue-loader(v15) 內部的相關內容,這里會講解下有關 vue-loader 的大致處理流程,不會深入特別細節的地方。
git clone git@github.com:vuejs/vue-loader.git
我們使用 vue-loader 官方倉庫當中的 example 目錄的內容作為整篇文章的示例。
首先我們都知道 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 來說是一個入口處理器。
在實際運用過程中,我們先來看下有關 Vue 的 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 文件里的 塊,說到這里我們就一起先來看看這個 plugin 里面到底做了哪些工作。
VueLoaderPlugin我們都清楚 webpack plugin 的裝載過程是在整個 webpack 編譯周期中初始階段,我們先來看下 VueLoaderPlugin 內部源碼的實現:
// 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 主要完成了以下三部分的工作:
判斷是否有給.vue或.vue.html進行 module.rule 的配置;
判斷對于.vue或.vue.html配置的 module.rule 是否有 vue-loader;
拓展開發者的 module.rule 配置,加入 vue-loader 內部提供的 pitcher 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 具體做的工作后文會分析。
Step 1接下來我們看下 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} ` + `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() + ` ` 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 += ` component.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 += ` export 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。
Step 2這樣 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 主要是做了哪些工作呢:
剔除 eslint loader;
剔除 pitcher loader 自身;
根據不同 type query 參數進行攔截處理,返回對應的內容,跳過后面的 loader 執行的階段,進入到 module parse 階段
// 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 來處理。
Step 3接下來進入到第三個階段,編譯返回的新的 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 源碼的分析,大家應該對 loader 有了更加深入的理解,也希望大家活學活用,利用 loader 機制去完成更多貼合實際需求的開發工作。
文章首發于個人github blog: Biu-blog,歡迎大家關注~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/102787.html
摘要:在一個構建過程中,首先根據的依賴類型例如調用對應的構造函數來創建對應的模塊。 文章首發于個人github blog: Biu-blog,歡迎大家關注~ Webpack 系列文章: Webpack Loader 高手進階(一)Webpack Loader 高手進階(二)Webpack Loader 高手進階(三) Webpack loader 詳解 loader 的配置 Webpack...
webpack的loaders是一大特色,也是很重要的一部分。這遍博客我將分類講解一些常用的laodershowImg(https://segmentfault.com/img/remote/1460000005742040); 一、loaders之 預處理 css-loader 處理css中路徑引用等問題 style-loader 動態把樣式寫入css sass-loader scss編譯器 ...
摘要:如果函數沒有返回值的話,那么進入到下一個的函數的執行階段。這也是異步化的一種方式如果執行后有返回值,執行開始下一個執行以上就是對于在構建過程中執行流程的源碼分析。 文章首發于個人github blog: Biu-blog,歡迎大家關注~ Webpack 系列文章: Webpack Loader 高手進階(一)Webpack Loader 高手進階(二)Webpack Loader 高手...
摘要:基本環境搭建就不展開講了一插件篇自動補全前綴官方是這樣說的,也就是說它是一個自動檢測兼容性給各個瀏覽器加個內核前綴的插件。 上一篇博客講解了webpack環境的基本,這一篇講解一些更深入的內容和開發技巧。基本環境搭建就不展開講了showImg(http://static.xiaomo.info/images/webpack.png); 一、插件篇 1. 自動補全css3前綴 autop...
摘要:,我想大家應該都知道或者聽過,是前端一個工具可以讓各個模塊進行加載預處理再進行打包。 webpack,我想大家應該都知道或者聽過,Webpack是前端一個工具,可以讓各個模塊進行加載,預處理,再進行打包。現代的前端開發很多環境都依賴webpack構建,比如vue官方就推薦使用webpack.廢話不多說,我們趕緊開始吧. 第一步、安裝webpack 新建文件夾webpack->再在web...
閱讀 3233·2021-09-07 10:10
閱讀 3579·2019-08-30 15:44
閱讀 2577·2019-08-30 15:44
閱讀 2981·2019-08-29 15:11
閱讀 2219·2019-08-28 18:26
閱讀 2744·2019-08-26 12:21
閱讀 1113·2019-08-23 16:12
閱讀 3009·2019-08-23 14:57