首先基于webpack的打包构建,所以暂不考虑parcel。
webpack处理.vue文件配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | module: { rules: [ { test: /\.vue$/, loader: "vue-loader", options: vueLoaderConfig, }, .... ], plugins: [ new VueLoaderPlugin(), //千万不要忘了这个呦 ... ], } |
在探究vue-loader之前,请先阅读该文章:深入Webpack-编写Loader
好了,通过上面配置我们知道,当webpack识别到.vue的文件后,会交给vue-loader处理。
compiler-sfc 是如何处理.vue文件的
在这里声明以下,我先将
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | export function parse( source: string, { sourceMap = true, filename = 'component.vue', sourceRoot = '', pad = false, compiler = require('@vue/compiler-dom') }: SFCParseOptions = {} ): SFCParseResult { //生成sourceKey const sourceKey = source + sourceMap + filename + sourceRoot + pad + compiler.parse const cache = sourceToSFC.get(sourceKey) if (cache) { return cache } const descriptor: SFCDescriptor = { filename, template: null, script: null, styles: [], customBlocks: [] } const errors: CompilerError[] = [] //通过@vue/compiler-dom的compiler进行包装,此处对template的内容并未做处理 const ast = compiler.parse(source, { // there are no components at SFC parsing level isNativeTag: () => true, // preserve all whitespaces isPreTag: () => true, getTextMode: (tag, _ns, parent) => { // 对于除去template的顶级标签保留为文本。 // 简单来说就是对scrpit和style标签不做处理。 if (!parent && tag !== 'template') { return TextModes.RAWTEXT } else { return TextModes.DATA } }, onError: e => { errors.push(e) } }) // 接下来就是将ast的`template`,`script`,`style`转换为Block,最终传递出去给webpack处理 ast.children.forEach(node => { if (node.type !== NodeTypes.ELEMENT) { return } if (!node.children.length && !hasSrc(node)) { return } switch (node.tag) { case 'template': if (!descriptor.template) { descriptor.template = createBlock( node, source, pad ) as SFCTemplateBlock } else { warnDuplicateBlock(source, filename, node) } break case 'script': if (!descriptor.script) { descriptor.script = createBlock(node, source, pad) as SFCScriptBlock } else { warnDuplicateBlock(source, filename, node) } break case 'style': descriptor.styles.push(createBlock(node, source, pad) as SFCStyleBlock) break default: descriptor.customBlocks.push(createBlock(node, source, pad)) break } }) //以下是生成sourceMap逻辑 if (sourceMap) { const genMap = (block: SFCBlock | null) => { if (block && !block.src) { block.map = generateSourceMap( filename, source, block.content, sourceRoot, pad ? 0 : block.loc.start.line - 1 ) } } genMap(descriptor.template) genMap(descriptor.script) descriptor.styles.forEach(genMap) } const result = { descriptor, errors } sourceToSFC.set(sourceKey, result) return result } |
- 当webpack调用vue-loader后,将.vue读取出来交给``compiler-sfc`解析。
- 调用
@vue/compiler-dom的compiler 将.vue 文件转换成AST (抽象语法树)
1.1. 对于template 的内容此时并没有做处理
1.2 对于script 和style 及自定义顶级标签不做处理,保留为文本格式。 - 对生成的AST转化为
SFCDescriptor 形式descriptor 。(template 和script 只允许存在一个) - 对source进行处理map。
- 返回包含
descriptor 的结果。
通过源码分析vue-loader处理流程
由于代码太长我直接进行裁剪
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | const loader: webpack.loader.Loader = function(source: string) { const loaderContext = this ... // check if plugin is installed ... const { mode, target, sourceMap, rootContext, resourcePath, resourceQuery } = loaderContext ... // 调用`compiler-sfc`的parse生成SFCdescriptor const { descriptor, errors } = parse(source, { filename: resourcePath, sourceMap }) ... // 重点来了 // template let templateImport = `` 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}${resourceQuery}` templateRequest = stringifyRequest(src + query) // 在这里将经过`compiler-sfc`处理过的`template`对象 // 转化成webpack的import形式,同时将所需要参数拼接在后面 templateImport = `import { render } from ${templateRequest}` //在此处断点,可观察到 //templateImport = ` import { render } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true"` } // script let scriptImport = `const script = {}` if (descriptor.script) { const src = descriptor.script.src || resourcePath const attrsQuery = attrsToQuery(descriptor.script.attrs, 'js') const query = `?vue&type=script${attrsQuery}${resourceQuery}` const scriptRequest = stringifyRequest(src + query) // 在这里将.vue里面的`script`转换成webpack的import形式 scriptImport = `import script from ${scriptRequest}\n` + `export * from ${scriptRequest}` } // styles let stylesCode = `` let hasCSSModules = false if (descriptor.styles.length) { descriptor.styles.forEach((style: SFCStyleBlock, i: number) => { const src = style.src || resourcePath const attrsQuery = attrsToQuery(style.attrs, 'css') // make sure to only pass id when necessary so that we don't inject // duplicate tags when multiple components import the same css file const idQuery = style.scoped ? `&id=${id}` : `` const query = `?vue&type=style&index=${i}${idQuery}${attrsQuery}${resourceQuery}` const styleRequest = stringifyRequest(src + query) ... //对css module处理 ... //此处转化成类似import "xxx.css/less/sass"形式 stylesCode += `\nimport ${styleRequest}` // TODO SSR critical CSS collection }) } // 将刚才上面的import语句转成通用的code,以便交给webpack的其他流程进行处理。 let code = [ templateImport, scriptImport, stylesCode, templateImport ? `script.render = render` : `` ] .filter(Boolean) .join('\n') // attach scope Id for runtime use //这里是个重点,style设置了scoped的话,会将id先暂存添加到,等运行时在做处理。 if (hasScoped) { code += `\nscript.__scopeId = "data-v-${id}"` } ... // 对customBlocks处理 ... // finalize code += `\n\nexport default script` return code } |
我们再看loader将.vue文件生成如下格式的js,
1 2 3 4 5 6 7 8 9 10 11 12 | import { render } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true" import script from "./App.vue?vue&type=script&lang=js" export * from "./App.vue?vue&type=script&lang=js" import "./App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css" script.render = render script.__scopeId = "data-v-7ba5bd90" /* hot reload */ ... script.__file = "src/App.vue" export default script |
最终输出了如下图形式的文件路径。具体内容,可以自己找个demo自行查看对应的文件内容。以便加深理解。
image.png
根据上面内容我们总结下
当webpack识别到有导入".vue"文件,则将.vue读取到,交给了vue的loader。
- vue-loader先调用
compiler-sfc 对文件内容转换为webpack模块化的import形式 。 - 这样.vue文件就成了标准的js形式,最后其他的loader对
./App.vue?vue&type=(template|script|style) 进行处理
不知道我有没有解释清楚。。。
现在我们再来了解VueLoaderPlugin做了什么?
VueLoaderPlugin做了什么
阅读之前请先阅读:Webpack原理-编写Plugin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | class VueLoaderPlugin implements webpack.Plugin { static NS = NS apply(compiler: webpack.Compiler) { const rawRules = compiler.options.module!.rules // use webpack's RuleSet utility to normalize user rules const rules = new RuleSet(rawRules).rules as webpack.RuleSetRule[] // 查找到对`.vue`配置的rule let vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue`)) if (vueRuleIndex < 0) { vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue.html`)) } const vueRule = rules[vueRuleIndex] // get the normlized "use" for vue files const vueUse = vueRule.use as webpack.RuleSetLoader[] // 获取`vue-loader`的options const vueLoaderUseIndex = vueUse.findIndex(u => { return /^vue-loader|(\/|\\|@)vue-loader/.test(u.loader || '') }) const vueLoaderUse = vueUse[vueLoaderUseIndex] const vueLoaderOptions = (vueLoaderUse.options = vueLoaderUse.options || {}) as VueLoaderOptions // 复制配置的rule const clonedRules = rules.filter(r => r !== vueRule).map(cloneRule) // 重点来了 // 对于`.vue`文件的`template`的compiler const templateCompilerRule = { loader: require.resolve('./templateLoader'), test: /\.vue$/, resourceQuery: (query: string) => { const parsed = qs.parse(query.slice(1)) return parsed.vue != null && parsed.type === 'template' }, options: vueLoaderOptions } // for each rule that matches plain .js files, also create a clone and // match it against the compiled template code inside *.vue files, so that // compiled vue render functions receive the same treatment as user code // (mostly babel) const matchesJS = createMatcher(`test.js`) const jsRulesForRenderFn = rules .filter(r => r !== vueRule && matchesJS(r)) .map(cloneRuleForRenderFn) // 此pitcher将会拦截到`.vue`,并处理`style`和`custom`等形式的 // loaders matched for src imports) const pitcher = { loader: require.resolve('./pitcher'), resourceQuery: (query: string) => { const parsed = qs.parse(query.slice(1)) return parsed.vue != null } } // 替换原始的rules compiler.options.module!.rules = [ pitcher, ...jsRulesForRenderFn, templateCompilerRule, ...clonedRules, ...rules ] } } |
上面的代码比较长,不过我们了解到,VueLoaderPlugin做了什么:
- 找到
.vue 配置的rule,并获取到该rule配置的options。 - 通过option重新生成一份对于.vue?template的templateLoader,这里的templateLoader会对识别到的.vue的template,进行处理为render函数形式。
- 然后通过pitcher对style处理加入scope处理,具体参考下文
./pitcher做了什么
./templateLoader 做了什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import { compileTemplate } from '@vue/compiler-sfc' const TemplateLoader: webpack.loader.Loader = function(source, inMap) { source = String(source) const loaderContext = this ... // 此处是将scopeId传入,最终处理style的scope const scopeId = query.scoped ? `data-v-${query.id}` : null //此loader才会将template转换成h形式的render方法,参考下图。 const compiled = compileTemplate({ source, inMap, filename: loaderContext.resourcePath, compiler: options.compiler, compilerOptions: { ...options.compilerOptions, scopeId }, transformAssetUrls: options.transformAssetUrls || true }) ... const { code, map } = compiled loaderContext.callback(null, code, map) } |
image.png
./pitcher 做了什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // 负责拦截vue的 `style``script`或自定义标签 pitcher.pitch = function() { const context = this as webpack.loader.LoaderContext const rawLoaders = context.loaders.filter(isNotPitcher) let loaders = rawLoaders ... const query = qs.parse(context.resourceQuery.slice(1)) ... // 在css-loader之前添加style-post-loader // 来处理style的scope 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) 返回一个处理`style`加入了`style-post-loader`的rules return genProxyModule( [...afterLoaders, stylePostLoaderPath, ...beforeLoaders], context ) } } // 处理`.vue`的`custom`及cache if (query.type === `custom` && shouldIgnoreCustomBlock(loaders)) { return `` } return genProxyModule(loaders, context, query.type !== 'template') } |
./stylePostLoader 做了什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import { compileStyle } from '@vue/compiler-sfc' const StylePostLoader: webpack.loader.Loader = function(source, inMap) { const query = qs.parse(this.resourceQuery.slice(1)) const { code, map, errors } = compileStyle({ source: source as string, filename: this.resourcePath, id: `data-v-${query.id}`, map: inMap, scoped: !!query.scoped, trim: true }) if (errors.length) { this.callback(errors[0]) } else { this.callback(null, code, map) } } |
image.png
好了,本文到此结束了。文章顺序比较乱,为此参阅以下流程图。
vue-loader处理流程