按执行流程一步步看vue-loader 源码
通常配置webpack 时,咱们会配置一个 loader 和 一个 plugin
// webpack.config.jsconst VueLoaderPlugin = require('vue-loader/lib/plugin')// ...{ test: /\.vue$/, loader: 'vue-loader'},// ...plugins: [ new VueLoaderPlugin(),]
当咱们运行 webpack 时, 首先会进入 vue-loader/lib/plugin
在apply办法内先挂载了一个钩子,
// vue-loader/lib/plugin.js class VueLoaderPlugin { apply (compiler) { compiler.hooks.compilation.tap(id, compilation => { let normalModuleLoader if (Object.isFrozen(compilation.hooks)) { // webpack 5 normalModuleLoader = require('webpack/lib/NormalModule').getCompilationHooks(compilation).loader } else { normalModuleLoader = compilation.hooks.normalModuleLoader } normalModuleLoader.tap(id, loaderContext => { loaderContext[NS] = true }) }) // ... }}
而后读取webpack配置内的所有rule 配置, 并应用 foo.vue 文件名作为测试,查找出能匹配 vue 文件的Rule所在索引 , 并取出相应 rule
// vue-loader/lib/plugin.jsconst rawRules = compiler.options.module.rulesconst { rules } = new RuleSet(rawRules)// find the rule that applies to vue fileslet vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue`))if (vueRuleIndex < 0) { vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue.html`))}const vueRule = rules[vueRuleIndex]
找到 找到 vue-loader 在 rule.use 内的索引, 而后取出相应的loader 配置, 并写入 ident 属性,
// vue-loader/lib/plugin.jsconst vueUse = vueRule.use// get vue-loader optionsconst vueLoaderUseIndex = vueUse.findIndex(u => { return /^vue-loader|(\/|\\|@)vue-loader/.test(u.loader)})// 取出 vue-loader 配置, 参考如下/* { loader:'vue-loader' options:undefined }*/const vueLoaderUse = vueUse[vueLoaderUseIndex]vueLoaderUse.ident = 'vue-loader-options'vueLoaderUse.options = vueLoaderUse.options || {}
克隆出所有的 rule , 在所有规定之前退出一个 vue的 pitcher loader,这个loader 的 resourceQuery 匹配 query 上有 vue的文件,
最初合并这些重写rules
// vue-loader/lib/plugin.jsconst clonedRules = rules .filter(r => r !== vueRule) .map(cloneRule)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 }}// replace original rulescompiler.options.module.rules = [ pitcher, ...clonedRules, ...rules]
最初会进入一开始挂上的钩子, 针对 compilation.hooks.normalModuleLoader 再挂上一个钩子
// vue-loader/lib/plugin.jscompiler.hooks.compilation.tap(id, compilation => { let normalModuleLoader if (Object.isFrozen(compilation.hooks)) { // webpack 5 normalModuleLoader = require('webpack/lib/NormalModule').getCompilationHooks(compilation).loader } else { normalModuleLoader = compilation.hooks.normalModuleLoader } normalModuleLoader.tap(id, loaderContext => { loaderContext[NS] = true }) })
最初触发 compilation.hooks.normalModuleLoader 钩子, 并对 loaderContext 的 'vue-loader' 属性为 true
// vue-loader/lib/plugin.jsconst NS = 'vue-loader'// .....normalModuleLoader.tap(id, loaderContext => { loaderContext[NS] = true})
继续执行,进入了vue-loader 外部, 将this存储在 loaderContext , 并提取出外部属性, 并将vue单文件组件内容别离解析成 template 、 script 、 style 内容
// vue-loader/lib/index.js const loaderContext = this const stringifyRequest = r => loaderUtils.stringifyRequest(loaderContext, r) const { target, request, minimize, sourceMap, rootContext, resourcePath, resourceQuery } = loaderContext const rawQuery = resourceQuery.slice(1) // 提取 问号前面的query const inheritQuery = `&${rawQuery}` const incomingQuery = qs.parse(rawQuery) const options = loaderUtils.getOptions(loaderContext) || {} const isServer = target === 'node' const isShadow = !!options.shadowMode const isProduction = options.productionMode || minimize || process.env.NODE_ENV === 'production' const filename = path.basename(resourcePath) const context = rootContext || process.cwd() const sourceRoot = path.dirname(path.relative(context, resourcePath)) // 将 vue 但文件解析成 const descriptor = parse({ source, compiler: options.compiler || loadTemplateCompiler(loaderContext), filename, sourceRoot, needMap: sourceMap })
解析出组件内容后, 会判断 query 参数是否有type 属性, 因为有type 属性的话就示意是第二次进入这个loader, 这个咱们前面再说
// vue-loader/lib/index.js if (incomingQuery.type) { return selectBlock( descriptor, loaderContext, incomingQuery, !!options.appendExtension )}
取出入口文件的目录相对路径, 和目录相对路径 + query 参数, 并依据这个门路生成一个 hash 字符串
而后就是做一些个性的判断,如 是否用了 scoped, 是否应用了 functional 组件等等
// vue-loader/lib/index.js // module id for scoped CSS & hot-reload const rawShortFilePath = path .relative(context, resourcePath) .replace(/^(\.\.[\/\\])+/, '') const shortFilePath = rawShortFilePath.replace(/\\/g, '/') + resourceQuery const id = hash( isProduction ? (shortFilePath + '\n' + source) : shortFilePath ) const hasScoped = descriptor.styles.some(s => s.scoped) const hasFunctional = descriptor.template && descriptor.template.attrs.functional
接下来就是生成template模版了, 原本的 vue组件内容, 通过一下的解决后变成引入一个新的 import 语句
// vue-loader/lib/index.js 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}` }
生成的 import 语句参考如下:
import { render, staticRenderFns } from "./index.vue?vue&type=template&id=3cf90f21&"
同样的,解决完template 完后处理 script
// vue-loader/lib/index.js // 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 ) }
生成的 import 语句
import script from "./index.vue?vue&type=script&lang=js&"export * from "./index.vue?vue&type=script&lang=js&"
最初解决 style
// vue-loader/lib/index.js let stylesCode = `` if (descriptor.styles.length) { stylesCode = genStylesCode( loaderContext, descriptor.styles, id, resourcePath, stringifyRequest, needsHotReload, isServer || isShadow // needs explicit injection? ) }
生成的 import 语句如下:
import style0 from "./index.vue?vue&type=style&index=0&lang=less&"
三个模块都解决完后, 最初要做的就是将他们合并起来生成最终 code , 并返回
// vue-loader/lib/index.js 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 ) } code += `\nexport default component.exports` return code
生成的code 字符串参考如下
import { render, staticRenderFns } from "./index.vue?vue&type=template&id=3cf90f21&"import script from "./index.vue?vue&type=script&lang=js&"export * from "./index.vue?vue&type=script&lang=js&"import style0 from "./index.vue?vue&type=style&index=0&lang=less&"/* normalize component */import normalizer from "!../../../node_modules/.pnpm/vue-loader@15.7.0_css-loader@2.1.1+webpack@4.41.2/node_modules/vue-loader/lib/runtime/componentNormalizer.js"var component = normalizer( script, render, staticRenderFns, false, null, null, null )export default component.exports
就这样, 一个咱们平时书写的 .vue文件通过 vue-loader 的第一次解决后 生成了如上的 code 代码, 通过生成了3个新的 import 语句,再次引入本身 .vue文件然而携带不同的 type 参数,交给webpack 。 webpack 接管到这个 code 后,发现 这个.vue文件原来还有 import. 援用了其余三个文件,它会持续查找这个三个文件, 也就是再通过 loader,而后loader 就能够通过 type 进行判断,返回相应的内容。
好,咱们持续往下走. 因为webpack 发现还有新的 import 文件, 这时候就触发了之前在 plugin中增加的 pitcher loader 了, 还记得吗,他的规定是这样的
// vue-loader/lib/plugin.jsconst pitcher = { loader: require.resolve('./loaders/pitcher'), // 匹配规定 resourceQuery: query => { const parsed = qs.parse(query.slice(1)) // 匹配 ?vue 文件 return parsed.vue != null }, options: { cacheDirectory: vueLoaderUse.options.cacheDirectory, cacheIdentifier: vueLoaderUse.options.cacheIdentifier } }
于是咱们就进入了 pitcher loader 外部,
外部首先取出loader 的参数, cacheDirectory
, cacheIdentifier
都是 plugin 给它传的。
解析出query 参数, 并判断 type 参数是否存在, 验证了.vue 文件时,会将 eslint-loader 给过滤掉, 防止反复触发
并且紧接着会将 pitcher loader. 本身给过滤掉。 再判断是否应用了 null-loader , 应用了的话就间接退出了
// vue-loader/lib/loaders/pitcher.jsmodule.exports.pitch = function (remainingRequest) { const options = loaderUtils.getOptions(this) const { cacheDirectory, cacheIdentifier } = options const query = qs.parse(this.resourceQuery.slice(1)) let loaders = this.loaders // 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)) { // 防止反复linter 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) } } // remove self loaders = loaders.filter(isPitcher) // do not inject if user uses null-loader to void the type (#1239) if (loaders.some(isNullLoader)) { return }// ...}
接下来定义了一个 genRequest 的函数, 这个函数的作用呢就是接管一个 loaders 数组,而后依据数组内的loader 它会生成 内联的loader 门路,
// vue-loader/lib/loaders/pitcher.js const genRequest = loaders => { const seen = new Map() const loaderStrings = [] loaders.forEach(loader => { const identifier = typeof loader === 'string' ? loader : (loader.path + loader.query) const request = typeof loader === 'string' ? loader : loader.request if (!seen.has(identifier)) { seen.set(identifier, true) // loader.request contains both the resolved loader path and its options // query (e.g. ??ref-0) loaderStrings.push(request) } }) return loaderUtils.stringifyRequest(this, '-!' + [ ...loaderStrings, this.resourcePath + this.resourceQuery ].join('!')) }
生成款式参考:
"-!../../../node_modules/.pnpm/vue-loader@15.7.0_css-loader@2.1.1+webpack@4.41.2/node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../../node_modules/.pnpm/vue-loader@15.7.0_css-loader@2.1.1+webpack@4.41.2/node_modules/vue-loader/lib/index.js??vue-loader-options!./index.vue?vue&type=template&id=3cf90f21&"
而后重要的就是依据不同的 query.type 做不同的解决, 并针对 style 和 template 注入了 stylePostLoader
和 templateLoader
。
如果没命中 style , template , 自定义模块,剩下的就是script 了。下面生成的3个import 援用进入到这里后, 又生成了3个携带了内联 loader 地址的内容。因为下面咱们将本身的 picther loader 曾经删除了 ,所以下次就不会再进入这里了
// vue-loader/lib/loaders/pitcher.js const templateLoaderPath = require.resolve('./templateLoader')const stylePostLoaderPath = require.resolve('./stylePostLoader') // Inject style-post-loader before css-loader for scoped CSS and trimming 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 ]) // console.log(request) return `import mod from ${request}; export default mod; export * from ${request}` } } // for templates: inject the template compiler & optional cache 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).replace(/\\/g, '/'), cacheIdentifier: hash(cacheIdentifier) + '-vue-loader-template' })}`] : [] const preLoaders = loaders.filter(isPreLoader) const postLoaders = loaders.filter(isPostLoader) const request = genRequest([ ...cacheLoader, ...postLoaders, templateLoaderPath + `??vue-loader-options`, ...preLoaders ]) // console.log(request) // the template compiler uses esm exports return `export * from ${request}` } // if a custom block has no other matching loader other than vue-loader itself // or cache-loader, we should ignore it if (query.type === `custom` && shouldIgnoreCustomBlock(loaders)) { 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}`
pitcher loader 的工作完结 ,咱们持续往下走
此时又回到了 失常的loader 外部, 这部分通过的步骤都完全相同, 惟一不同的是这次接管到的 request 是pitcher loader 交给咱们的携带了内联 loader 的request
// vue-loader/lib/index.js const loaderContext = this const stringifyRequest = r => loaderUtils.stringifyRequest(loaderContext, r) const { target, request, minimize, sourceMap, rootContext, resourcePath, resourceQuery } = loaderContext const rawQuery = resourceQuery.slice(1) // 提取 问号前面的query const inheritQuery = `&${rawQuery}` const incomingQuery = qs.parse(rawQuery) const options = loaderUtils.getOptions(loaderContext) || {} const isServer = target === 'node' const isShadow = !!options.shadowMode const isProduction = options.productionMode || minimize || process.env.NODE_ENV === 'production' const filename = path.basename(resourcePath) const context = rootContext || process.cwd() const sourceRoot = path.dirname(path.relative(context, resourcePath)) // 将 vue 但文件解析成 script style template 数据 const descriptor = parse({ source, compiler: options.compiler || loadTemplateCompiler(loaderContext), filename, sourceRoot, needMap: sourceMap })
往下走,这次咱们因为携带了 query.type 就会进入到 selectBlock 这个办法, 并且将返回这个办法所返回的后果
// vue-loader/lib/index.js if (incomingQuery.type) { return selectBlock( descriptor, loaderContext, incomingQuery, !!options.appendExtension ) }
selectBlock 这个办法也很简略, 就是针对不同的query.type 来返回已解析好的对应的 descriptor 对象上的内容, 调用 loaderContext.callback 传入内容,交给webpack
// vue-loader/lib/select.jsmodule.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-loader/lib/plugin 注入 pitcher loader ➡️ vue-loader 第一次命中.vue 文件 ➡️ 因为没有 query.type 所以生成了三个新的import 援用并携带了 query.type ➡️ 因为新的援用携带了 query.type 所以命中了 pitcher loader ➡️ pitcher -loader 执行过程中将本人从 loader 中删除, 并针对 style, template 注入了 专门的loader 进行解决 生成内联 loader 援用 ➡️ 交给 vue-loader ➡️ vue-loader接管到pitcher loader 解决后的援用, 依据不同的type 返回了不同内容, 比方 template 是render函数 ➡️ 因为 pitcher loader 结构了内联 loader , 所以返回的内容又会被这些 内联的loader 给挨个解决
第一次写源码系列文章,写的不是很好,摸索中