阅读 661

Vue的样式隔离是怎么实现的?(vue绑定样式)

使用vue的时候,在<style> 标签加上scoped就可以实现样式隔离,只会作用在当前组件。

<template>   <div class="about">     <h1>This is an about page</h1>   </div> </template> <style scoped>   .about {     color: red   } </style> 复制代码

chrome上面: scoped-style.png

我们通过调试源码来分析下这个是怎么实现的。

vue-loader源码流程分析

先上一张vue-loader工作流程图,先混个印象,回头再细看。

vue-loader.png

查看配置

不知道你是否看过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-loaderVueLoaderPlugin插件,缺一不可

VueLoaderPlugin

这个插件主要作用有以下几点

  • webpack中Compiler对象添加一个标识,vue-loader在执行的时候会监测是否注册了VueLoaderPlugin

  • 检测compilerrules中是否存在处理.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后进入webpackbuildModule阶段,此时会使用loaderRunner解析源文件。

下面两种请求都会经过vue-loader,区别就是是否带有resourceQuery

  1. import App from './App.vue

  2. import * from "./App.vue?vue&type=template&id=1511d40d&


import App from './App.vue

  • 解析resourceQuery,拿到loader的query参数,上面的解析结果是{}

  • 通过vue-template-compilerparseComponent.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函数用于格式化组件暴露的值。

image.png

  • 此时请求import App from './App.vuevue-loader返回的JSON字符串是这样的。它把一个请求重新拆分为三个请求,通过type=区分

image.png

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] } 复制代码

那么就是倒序串联执行

image.png

如果遇到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。

image.png

import * from "./App.vue?vue&type=template&id=1511d40d&时会发生下面这些事情

  • 先经过pitcher-loader处理,pitcher-loader会以下处理

    -!../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& 复制代码

    vue中template处理流程:

    • 过滤loader,移除eslint-loader和自身

    • 匹配当前的type, type为template,则会匹配template逻辑部分,重新序列化请求,加上cache-loader和templateLoader, 上面的请求被转为了,会经过三个inline-loader处理

image.png

其中-!, !, !!符号意思参考wepack-inline-loader

给template标签元素加上_scoped

  • 转换后的行内请求首先回到vue-loader处理, 之前已经计算过descriptor结果,缓存直接返回。

image.png

  • 进入selectBlock,  slectBlock会分别处理不同类型的type,loaderContext.callback可以知道vue-loader是一个异步loader, templateLoader会等待它完成后再调用。

image.png

  • template-loader会接收vue-loader的处理结果,他的作用是

    • vue-template-compiler经过parser、optimization、generate阶段会返回render函数,和静态节点staticRender

image.png

你可能发现scopedId也传给了compiler,是不是生成render函数已经带上了data-v-1511d40d,其实并没有。出于打包后体积考虑,compiler没有再render函数就添加上{attrs: {'data-v-1511d40d'}},而是把_scopedId运行时patch阶段再加上。

回到上面说的normalize component,当代码浏览器运行的时候,会发现$options有个_scopedId, 这个会在首次patch时候动态设置上。

image.png

自此我们已经知道标签上的_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。 image.pngvue-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-loader有点类似,它可以处理一些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文件的具体实现。

image.png

至此css样式隔离实现。最近看的京东那套微前端实现,它的css隔离也是类似的方式,只是不依赖postcss,自己撸了一个cssParser,通过正则暴力替换。

总结

因为vue-loader跟webpack是强相关的,因此需要了解一些webpack流程的知识,不然实在难啃。话说你们的vite上生产了吗?


作者:狗胜
链接:https://juejin.cn/post/7057854547229671432


文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐