我们知道,一个组件的render方法返回的VNode,描述了组件对应的HTML标签和结构,HTML标签对应的DOM节点是从虚拟DOM节点构建的,一个Vnode包含了渲染DOM节点需要的基本属性。
那么,我们只需要了解到vnode上组件文件的哈希id的赋值过程,后面的问题就迎刃而解了。
// templateLoader.js const { compileTemplate } = require('@vue/component-compiler-utils') module.exports = function (source) { const { id } = query const options = loaderUtils.getOptions(loaderContext) || {} const compiler = options.compiler || require('vue-template-compiler') // 可以看见,scopre=true的template的文件会生成一个scopeId const compilerOptions = Object.assign({ outputSourceRange: true }, options.compilerOptions, { scopeId: query.scoped ? `data-v-${id}` : null, comments: query.comments }) // 合并compileTemplate最终参数,传入compilerOptions和compiler const finalOptions = {source, filename: this.resourcePath, compiler,compilerOptions} const compiled = compileTemplate(finalOptions) const { code } = compiled // finish with ESM exports return code + `\nexport { render, staticRenderFns }` }
关于compileTemplate的实现,我们不用去关心其细节,其内部主要是调用了配置参数compiler的编译方法
function actuallyCompile(options) { const compile = optimizeSSR && compiler.ssrCompile ? compiler.ssrCompile : compiler.compile const { render, staticRenderFns, tips, errors } = compile(source, finalCompilerOptions); // ... }
在Vue源码中可以了解到,template属性会通过compileToFunctions编译成render方法;在vue-loader中,这一步是可以通过vue-template-compiler提前在打包阶段处理的。
vue-template-compiler是随着Vue源码一起发布的一个包,当二者同时使用时,需要保证他们的版本号一致,否则会提示错误。这样,compiler.compile实际上是Vue源码中vue/src/compiler/index.js的baseCompile方法,追着源码一致翻下去,可以发现
// elementToOpenTagSegments.js // 对于单个标签的属性,将拆分成一个segments function elementToOpenTagSegments (el, state): Array<StringSegment> { applyModelTransform(el, state) let binding const segments = [{ type: RAW, value: `<${el.tag}` }] // ... 处理attrs、domProps、v-bind、style、等属性 // _scopedId if (state.options.scopeId) { segments.push({ type: RAW, value: ` ${state.options.scopeId}` }) } segments.push({ type: RAW, value: `>` }) return segments }
以前面的<div></div>为例,解析得到的segments为
[ { type: RAW, value: '<div' }, { type: RAW, value: 'class=demo' }, { type: RAW, value: 'data-v-27e4e96e' }, // 传入的scopeId { type: RAW, value: '>' }, ]
至此,我们知道了在templateLoader中,会根据单文件组件的id,拼接一个scopeId,并作为compilerOptions传入编译器中,被解析成vnode的配置属性,然后在render函数执行时调用createElement,作为vnode的原始属性,渲染成到DOM节点上。
stylePostLoader
在stylePostLoader中,需要做的工作就是将所有选择器都增加一个属性选择器的组合限制,
const { compileStyle } = require('@vue/component-compiler-utils') module.exports = function (source, inMap) { const query = qs.parse(this.resourceQuery.slice(1)) const { code, map, errors } = compileStyle({ source, filename: this.resourcePath, id: `data-v-${query.id}`, // 同一个单页面组件中的style,与templateLoader中的scopeId保持一致 map: inMap, scoped: !!query.scoped, trim: true }) this.callback(null, code, map) }
我们需要了解compileStyle的逻辑
// @vue/component-compiler-utils/compileStyle.ts import scopedPlugin from './stylePlugins/scoped' function doCompileStyle(options) { const { filename, id, scoped = true, trim = true, preprocessLang, postcssOptions, postcssPlugins } = options; if (scoped) { plugins.push(scopedPlugin(id)); } const postCSSOptions = Object.assign({}, postcssOptions, { to: filename, from: filename }); // 省略了相关判断 let result = postcss(plugins).process(source, postCSSOptions); }
最后让我们在了解一下scopedPlugin的实现,