虽然写了很长一段时间的Vue了,对于CSS Scoped的原理也大致了解,但一直未曾关注过其实现细节。最近在重新学习webpack,因此查看了vue-loader源码,顺便从vue-loader的源码中整理CSS Scoped的实现。
本文展示了vue-loader中的一些源码片段,为了便于理解,稍作删减。参考
Vue CSS SCOPED实现原理
Vue loader官方文档
相关概念
CSS Scoped的实现原理
在Vue单文件组件中,我们只需要在style标签上加上scoped属性,就可以实现标签内的样式在当前模板输出的HTML标签上生效,其实现原理如下
每个Vue文件都将对应一个唯一的id,该id可以根据文件路径名和内容hash生成
编译template标签时时为每个标签添加了当前组件的id,如<div></div>会被编译成<div data-v-27e4e96e></div>
编译style标签时,会根据当前组件的id通过属性选择器和组合选择器输出样式,如.demo{color: red;}会被编译成.demo[data-v-27e4e96e]{color: red;}
了解了大致原理,可以想到css scoped应该需要同时处理template和style的内容,现在归纳需要探寻的问题
渲染的HTML标签上的data-v-xxx属性是如何生成的
CSS代码中的添加的属性选择器是如何实现的
resourceQuery
在此之前,需要了解首一下webpack中的作用。在配置loader时,大部分时候我们只需要通过test匹配文件类型即可
{ test: /\.vue$/, loader: 'vue-loader' } // 当引入vue后缀文件时,将文件内容传输给vue-loader进行处理 import Foo from './source.vue'
resourceQuery提供了根据引入文件路径参数的形式匹配路径
{ resourceQuery: /shymean=true/, loader: path.resolve(__dirname, './test-loader.js') } // 当引入文件路径携带query参数匹配时,也将加载该loader import './test.js?shymean=true' import Foo from './source.vue?shymean=true'
vue-loader中就是通过resourceQuery并拼接不同的query参数,将各个标签分配给对应的loader进行处理。
loader.pitch
参考
webpack中loaders的执行顺序是从右到左执行的,如loaders:[a, b, c],loader的执行顺序是c->b->a,且下一个loader接收到的是上一个loader的返回值,这个过程跟"事件冒泡"很像。
但是在某些场景下,我们可能希望在"捕获"阶段就执行loader的一些方法,因此webpack提供了loader.pitch的接口。
一个文件被多个loader处理的真实执行流程,如下所示
a.pitch -> b.pitch -> c.pitch -> request module -> c -> b -> a
loader和pitch的接口定义大概如下所示
// loader文件导出的真实接口,content是上一个loader或文件的原始内容 module.exports = function loader(content){ // 可以访问到在pitch挂载到data上的数据 console.log(this.data.value) // 100 } // remainingRequest表示剩余的请求,precedingRequest表示之前的请求 // data是一个上下文对象,在上面的loader方法中可以通过this.data访问到,因此可以在pitch阶段提前挂载一些数据 module.exports.pitch = function pitch(remainingRequest, precedingRequest, data) { data.value = 100 }}
正常情况下,一个loader在execution阶段会返回经过处理后的文件文本内容。如果在pitch方法中直接返回了内容,则webpack会视为后面的loader已经执行完毕(包括pitch和execution阶段)。
在上面的例子中,如果b.pitch返回了result b,则不再执行c,则是直接将result b传给了a。
VueLoaderPlugin
接下来看看与vue-loader配套的插件:VueLoaderPlugin,该插件的作用是:
将在webpack.config定义过的其它规则复制并应用到 .vue 文件里相应语言的块中。
其大致工作流程如下所示
获取项目webpack配置的rules项,然后复制rules,为携带了?vue&lang=xx...query参数的文件依赖配置xx后缀文件同样的loader
为Vue文件配置一个公共的loader:pitcher
将[pitchLoder, ...clonedRules, ...rules]作为webapck新的rules
// vue-loader/lib/plugin.js const rawRules = compiler.options.module.rules // 原始的rules配置信息 const { rules } = new RuleSet(rawRules) // cloneRule会修改原始rule的resource和resourceQuery配置,携带特殊query的文件路径将被应用对应rule const clonedRules = rules .filter(r => r !== vueRule) .map(cloneRule) // vue文件公共的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 } } // 更新webpack的rules配置,这样vue单文件中的各个标签可以应用clonedRules相关的配置 compiler.options.module.rules = [ pitcher, ...clonedRules, ...rules ]