使用vueSFC的时候,在<style> 标签加上scoped就可以实现样式隔离,只会作用在当前组件。
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style scoped>
.about {
color: red
}
</style>
chrome上面:
我们通过调试源码来分析下这个是怎么实现的。
vue-loader源码流程分析
先上一张vue-loader工作流程图,先混个印象,回头可细看。
查看配置
不知道你是否看过vue-cli默认的webpack配置, 可以通过vue inspect > config.js命令把项目的配置通过文件输出,@vue/cli 4.5.10默认配置如下。只关注对.vue文件的配置
module.exports = {
module: {
rules: [
/* config.module.rule('vue') */
{
test: /\.vue$/,
use: [
{
loader: 'D:\\work_space\\qiankun-example\\sub-vue\\node_modules\\cache-loader\\dist\\cjs.js',
options: {
// 之前打包结果的保存路径
cacheDirectory: 'D:\\work_space\\qiankun-example\\sub-vue\\node_modules\\.cache\\vue-loader',
cacheIdentifier: 'ffa56dac'
}
},
{
loader: 'D:\\work_space\\qiankun-example\\sub-vue\\node_modules\\vue-loader\\lib\\index.js',
options: {
compilerOptions: {
whitespace: 'condense'
},
cacheDirectory: 'D:\\work_space\\qiankun-example\\sub-vue\\node_modules\\.cache\\vue-loader',
cacheIdentifier: 'ffa56dac'
}
}
]
},
]
},
plugins: [
/* config.plugin('vue-loader') */
new VueLoaderPlugin(),
// ....
],
}
通过配置看出两点
vue-loader之后还有cache-loader,这个loader主要作用是缓存上次编译的结果,针对未被修改的文件,可以直接拉去缓存,大大缩短编译时间,也是性能提升最明显的一个loader。- 处理
vue文件同时需要vue-loader和VueLoaderPlugin插件,缺一不可
VueLoaderPlugin
这个插件主要作用有以下几点
- 给
webpack中Compiler对象添加一个标识,vue-loader在执行的时候会监测是否注册了VueLoaderPlugin - 检测
compiler的rules中是否存在处理.vue文件的loader,没有抛出错误 - 添加一个
RuleSet格式的全局pitcher-loader, 如import Foo from './foo.css?vue会匹配这个loader,带有vue参数 - 复制一份
Ruleset格式的rules覆盖之前的rules
源码片段分析:
sub-vue\node_modules\vue-loader\lib\plugin-webpack4.js:
// 添加一个标记,用于检测是是否已经配置了vueLoaderPlugin
if (compiler.hooks) {
// webpack 4
compiler.hooks.compilation.tap(id, compilation => {
const normalModuleLoader = compilation.hooks.normalModuleLoader
normalModuleLoader.tap(id, loaderContext => {
loaderContext[NS] = true
})
})
}
// 前置全局的pitcher-loader,用于处理vue中的template、script、style块
const pitcher = {
loader: require.resolve('./loaders/pitcher'),
resourceQuery: query => {
// 这个loader只有这种格式的请求才会被命中,query中存在vue字段
// import script from \"./App.vue?vue&type=script&lang=js
const parsed = qs.parse(query.slice(1))
return parsed.vue != null
},
options: {
// 缓存存放的路径
// sub-vue\node_modules\.cache\vue-loader
cacheDirectory: vueLoaderUse.options.cacheDirectory,
}
}
// 使用ruleset格式的rules替换原来的rule
compiler.options.module.rules = [
pitcher,
...clonedRules,
...rules
]
}
VueLoader
配置完插件,npm run build后进入webpack的buildModule阶段,此时会使用loaderRunner解析源文件。
下面两种请求都会经过vue-loader,区别就是是否带有resourceQuery
import App from './App.vueimport * from "./App.vue?vue&type=template&id=1511d40d&
import App from './App.vue
- 解析
resourceQuery,拿到loader的query参数,上面的解析结果是{} - 通过
vue-template-compiler的parseComponent把.vueSFC解析为三大块template、script、style,script还会生成mapping传递给后面的loader,这里用到了LRU缓存算法,因为后续相同的源文件还会继续调用parse方法,需要缓存防止性能浪费,同时还使用hash-sum模块计算出源文件对应的唯一的cacheKey,如果源文件被修改,缓存自然自然会失效。
// sub-vue\node_modules\@vue\component-compiler-utils\dist\parse.js
const cache = new (require('lru-cache'))(100);
function parse(options) {
// ...忽略
// 缓存编译结果
const cacheKey = hash(filename + source + JSON.stringify(compilerParseOptions));
let output = cache.get(cacheKey);
if (output)
return output;
output = compiler.parseComponent(source, compilerParseOptions);
- 因为
resourceQuery为空不会走这里的逻辑
// sub-vue\node_modules\vue-loader\lib\index.js
// 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
)
}
- 计算
css-scoped id, 用于css隔离,id就是类似data-v-039c5b43中的039c5b43
// sub-vue\node_modules\vue-loader\lib\index.js
const id = hash(
isProduction
? (shortFilePath + '\n' + source.replace(/\r\n/g, '\n'))
: shortFilePath
)
- 组装这次要返回的字符串,对template、script、style分别转换成带参数的请求,主要是加上
?vue&type=template&id=1511d40d&这样的resourceQuery。 如果是开发模式还会带上vue-loader自己实现模块热替换接口,同时还会带上运行时的一段normalize component函数用于格式化组件暴露的值。
- 此时请求
import App from './App.vue,vue-loader返回的JSON字符串是这样的。它把一个请求重新拆分为三个请求,通过type=区分
import * from "./App.vue?vue&type=template&id=1511d40d&
这个模块请求还是通过vue-loader解析,但是处理顺序会有区别。上面请求带有vue命中注册过的全局pitcher-loader,这个是一种特殊的loader,普通loader都是按照倒序执行的。但是如果存在pitcher-loader顺序会有变化,这里稍微说下区别
假如配置一个处理css的rule如下
{
test: /\.css$/,
use: [normalLoader1, normalLoader2, normalLoader3, normalLoader4]
}
那么就是倒序串联执行
如果遇到pitcher-loader
{
test: /\.css$/,
use: [normalLoader1, pitcherLoader, normalLoader3, normalLoader4]
}
从pitcher-loader先执行,跳过normalLoader3, normalLoader4,pticher-loader函数入参不再是源文件buffer,而是剩余请求normalLoader3, normalLoader4,把然后再到normalLoader1。具体的实现可以查看LoaderRunner源码sub-vue\node_modules\loader-runner\lib\LoaderRunner.js。一般来说,pitcher-loader后面不会有normalLoader,常见的pticher-loader就是开发模式使用的style-loader,把css通过style标签插入到head。
import * from "./App.vue?vue&type=template&id=1511d40d&时会发生下面这些事情
- 先经过pitcher-loader处理,pitcher-loader会以下处理
- 过滤loader,移除eslint-loader和自身
- 匹配当前的type, type为template,则会匹配template逻辑部分,重新序列化请求,加上cache-loader和templateLoader, 上面的请求被转为了,会经过三个inline-loader处理
vue中template处理流程:-!../node_modules/cache-loader/dist/cjs.js?{\\"cacheDirectory\\":\\"node_modules/.cache/vue-loader\\",\\"cacheIdentifier\\":\\"7e12f88b-vue-loader-template\\"} !../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../node_modules/cache-loader/dist/cjs.js??ref--0-0 !../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=template&id=1511d40d&
其中-!, !, !!符号意思参考wepack-inline-loader
给template标签元素加上_scoped
- 转换后的行内请求首先回到
vue-loader处理, 之前已经计算过descriptor结果,缓存直接返回。
- 进入selectBlock, slectBlock会分别处理不同类型的type,
loaderContext.callback可以知道vue-loader是一个异步loader,templateLoader会等待它完成后再调用。
template-loader会接收vue-loader的处理结果,他的作用是vue-template-compiler经过parser、optimization、generate阶段会返回render函数,和静态节点staticRender
你可能发现scopedId也传给了compiler,是不是生成render函数已经带上了data-v-1511d40d,其实并没有。出于打包后体积考虑,compiler没有再render函数就添加上{attrs: {'data-v-1511d40d'}},而是把_scopedId运行时patch阶段再加上。
回到上面说的normalize component,当代码浏览器运行的时候,会发现$options有个_scopedId, 这个会在首次patch时候动态设置上。
自此我们已经知道标签上的_scopedid是怎么来的,那么css属性选择器是怎么加上的呢?
给css样式加上_scopedId
上面的分析都是针对template的,我们还存在style要处理,同样的pitch-loader再处理src\\App.vue?vue&type=style&index=0&lang=css&, 把请求转换为行内请求,如下
"-!../node_modules/mini-css-extract-plugin/dist/loader.js??ref--6-oneOf-1-0!../node_modules/css-loader/dist/cjs.js??ref--6-oneOf-1-1
!../node_modules/vue-loader/lib/loaders/stylePostLoader.js
!../node_modules/postcss-loader/src/index.js??ref--6-oneOf-1-2
!../node_modules/cache-loader/dist/cjs.js??ref--0-0
!../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=style&index=0&lang=css"
npm run build模式,style会经过5个loader。
再
vue-loader处理的时候,跟处理template流程一样,只是单纯返回缓存。而_scpoedId就是在style-post-loader处理时加上的
// sub-vue\node_modules\vue-loader\lib\loaders\stylePostLoader.js
const qs = require('querystring')
const { compileStyle } = require('@vue/component-compiler-utils')
// 这是一个后置loader用来转换css, 用于加上css-scoped
module.exports = function (source, inMap) {
const query = qs.parse(this.resourceQuery.slice(1))
// 通过postcss解析器重新组装css
const { code, map, errors } = compileStyle({
source,
filename: this.resourcePath,
id: `data-v-${query.id}`,
map: inMap,
scoped: !!query.scoped,
trim: true
})
}
compileStyle方法时通过postcss能力返回新的css,postcss跟babel有点类似,它可以处理一些css语法,如变量,自动加上浏览器前缀之类。它也是可以把css解析为css-ast,然后通过插件机制对不同的节点类型进行编辑,通过generate重新生成新的css。而vue则通过自定义一个postcss-plugin来给css加上_scopedId和实现/deep/能力
一个postcss-plugin插件格式是这样的
module.exports = (opts = {}) => {
return {
postcssPlugin: '插件名字',
prepare (result) {
// 这里可以放一些公共的逻辑
return {
Declaration (node) {},
Rule (node) {},
AtRule (node) {}
}
}
}
}
css 的 AST 比 js 的简单多了,主要有这么几种:
- @符号样式开头的,比如@meida,媒体查询
- Rule,就是具体的css类名
- Dec就是具体的样式
padding: 5px这种
那么只要写一个插件,遍历这几种类型,修改css-ast,加上_scopedId即可,实现并不难。
具体可以查看\node_modules\@vue\component-compiler-utils\lib\stylePlugins\scoped.ts文件的具体实现。
至此css样式隔离实现。最近看的京东那套微前端实现,它的css隔离也是类似的方式,只是不依赖postcss,自己撸了一个cssParser,通过正则暴力替换。
总结
因为vue-loader跟webpack是强相关的,因此需要了解一些webpack流程的知识,不然实在难啃。话说你们的vite上生产了吗?