我的编程空间,编程开发者的网络收藏夹
学习永远不晚

CSS中Scoped的实现原理是什么

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

CSS中Scoped的实现原理是什么

这篇“CSS中Scoped的实现原理是什么”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“CSS中Scoped的实现原理是什么”文章吧。

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中Rules.resourceQuery的作用。在配置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参数匹配时,也将加载该loaderimport './test.js?shymean=true'import Foo from './source.vue?shymean=true'

vue-loader中就是通过resourceQuery并拼接不同的query参数,将各个标签分配给对应的loader进行处理。

loader.pitch

参考

pitching-loader官方文档
webpack的pitching loader

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.jsconst rawRules = compiler.options.module.rules // 原始的rules配置信息const { rules } = new RuleSet(rawRules)// cloneRule会修改原始rule的resource和resourceQuery配置,携带特殊query的文件路径将被应用对应ruleconst clonedRules = rules   .filter(r => r !== vueRule)   .map(cloneRule) // vue文件公共的loaderconst 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]

因此,为vue单文件组件中每个标签执行的lang属性,也可以应用在webpack配置同样后缀的rule。这种设计就可以保证在不侵入vue-loader的情况下,为每个标签配置独立的loader,如

  1. 可以使用pug编写template,然后配置pug-plain-loader

  2. 可以使用scss或less编写style,然后配置相关预处理器loader

可见在VueLoaderPlugin主要做的两件事,一个是注册公共的pitcher,一个是复制webpack的rules。

vue-loader

接下来我们看看vue-loader做的事情。

pitcher

前面提到在VueLoaderPlugin中,该loader在pitch中会根据query.type注入处理对应标签的loader

  • 当type为style时,在css-loader后插入stylePostLoader,保证stylePostLoader在execution阶段先执行

  • 当type为template时,插入templateLoader

// pitcher.jsmodule.exports = code => codemodule.exports.pitch = function (remainingRequest) { if (query.type === `style`) {  // 会查询cssLoaderIndex并将其放在afterLoaders中  // loader在execution阶段是从后向前执行的  const request = genRequest([   ...afterLoaders,   stylePostLoaderPath, // 执行lib/loaders/stylePostLoader.js   ...beforeLoaders  ])  return `import mod from ${request}; export default mod; export * from ${request}` } // 处理模板 if (query.type === `template`) {  const preLoaders = loaders.filter(isPreLoader)  const postLoaders = loaders.filter(isPostLoader)  const request = genRequest([   ...cacheLoader,   ...postLoaders,   templateLoaderPath + `??vue-loader-options`, // 执行lib/loaders/templateLoader.js   ...preLoaders  ])  return `export * from ${request}` } // ...}

由于loader.pitch会先于loader,在捕获阶段执行,因此主要进行上面的准备工作:检查query.type并直接调用相关的loader

  • type=style,执行stylePostLoader

  • type=template,执行templateLoader

这两个loader的具体作用我们后面再研究。

vueLoader

接下来看看vue-loader里面做的工作,当引入一个x.vue文件时

// vue-loader/lib/index.js 下面source为Vue代码文件原始内容// 将单个*.vue文件内容解析成一个descriptor对象,也称为SFC(Single-File Components)对象// descriptor包含template、script、style等标签的属性和内容,方便为每种标签做对应处理const descriptor = parse({ source, compiler: options.compiler || loadTemplateCompiler(loaderContext), filename, sourceRoot, needMap: sourceMap})// 为单文件组件生成唯一哈希idconst id = hash( isProduction ? (shortFilePath + '\n' + source) : shortFilePath)// 如果某个style标签包含scoped属性,则需要进行CSS Scoped处理,这也是本章节需要研究的地方const hasScoped = descriptor.styles.some(s => s.scoped)

处理template标签,拼接type=template等query参数

if (descriptor.template) { const class="lazy" data-src = descriptor.template.class="lazy" data-src || resourcePath const idQuery = `&id=${id}` // 传入文件id和scoped=true,在为组件的每个HTML标签传入组件id时需要这两个参数 const scopedQuery = hasScoped ? `&scoped=true` : `` const attrsQuery = attrsToQuery(descriptor.template.attrs) const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}` const request = templateRequest = stringifyRequest(class="lazy" data-src + query) // type=template的文件会传给templateLoader处理 templateImport = `import { render, staticRenderFns } from ${request}`  // 比如,<template lang="pug"></template>标签 // 将被解析成 import { render, staticRenderFns } from "./source.vue?vue&type=template&id=27e4e96e&lang=pug&"}

处理script标签

let scriptImport = `var script = {}`if (descriptor.script) { // vue-loader没有对script做过多的处理 // 比如vue文件中的<script></script>标签将被解析成 // import script from "./source.vue?vue&type=script&lang=js&" // export * from "./source.vue?vue&type=script&lang=js&"}

处理style标签,为每个标签拼接type=style等参数

// 在genStylesCode中,会处理css scoped和css moudlestylesCode = genStylesCode( loaderContext, descriptor.styles,  id, resourcePath, stringifyRequest, needsHotReload, isServer || isShadow // needs explicit injection?)// 由于一个vue文件里面可能存在多个style标签,对于每个标签,将调用genStyleRequest生成对应文件的依赖function genStyleRequest (style, i) { const class="lazy" data-src = style.class="lazy" data-src || resourcePath const attrsQuery = attrsToQuery(style.attrs, 'css') const inheritQuery = `&${loaderContext.resourceQuery.slice(1)}` const idQuery = style.scoped ? `&id=${id}` : `` // type=style将传给stylePostLoader进行处理 const query = `?vue&type=style&index=${i}${idQuery}${attrsQuery}${inheritQuery}` return stringifyRequest(class="lazy" data-src + query)}

可见在vue-loader中,主要是将整个文件按照标签拼接对应的query路径,然后交给webpack按顺序调用相关的loader。

templateLoader

回到开头提到的第一个问题:当前组件中,渲染出来的每个HTML标签中的hash属性是如何生成的。

我们知道,一个组件的render方法返回的VNode,描述了组件对应的HTML标签和结构,HTML标签对应的DOM节点是从虚拟DOM节点构建的,一个Vnode包含了渲染DOM节点需要的基本属性。

那么,我们只需要了解到vnode上组件文件的哈希id的赋值过程,后面的问题就迎刃而解了。

// templateLoader.jsconst { 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.sclass="lazy" data-srcompile ? compiler.sclass="lazy" data-srcompile : 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/class="lazy" data-src/compiler/index.js的baseCompile方法,追着源码一致翻下去,可以发现

// elementToOpenTagSegments.js// 对于单个标签的属性,将拆分成一个segmentsfunction 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.tsimport 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的实现,

export default postcss.plugin('add-id', (options: any) => (root: Root) => { const id: string = options const keyframes = Object.create(null) root.each(function rewriteSelector(node: any) {  node.selector = selectorParser((selectors: any) => {   selectors.each((selector: any) => {    let node: any = null    // 处理 '>>>' 、 '/deep/'、::v-deep、pseudo等特殊选择器时,将不会执行下面添加属性选择器的逻辑    // 为当前选择器添加一个属性选择器[id],id即为传入的scopeId    selector.insertAfter(     node,     selectorParser.attribute({      attribute: id     })    )   })  }).processSync(node.selector) })})

由于我对于PostCSS的插件开发并不是很熟悉,这里只能大致整理,翻翻文档了,相关API可以参考Writing a PostCSS Plugin。

至此,我们就知道了第二个问题的答案:通过selector.insertAfter为当前styles下的每一个选择器添加了属性选择器,其值即为传入的scopeId。由于只有当前组件渲染的DOM节点上上面存在相同的属性,从而就实现了css scoped的效果。

以上就是关于“CSS中Scoped的实现原理是什么”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注编程网行业资讯频道。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

CSS中Scoped的实现原理是什么

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

CSS中Scoped的实现原理是什么

这篇“CSS中Scoped的实现原理是什么”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“CSS中Scoped的实现原理是什么
2023-07-04

Java中 AOP 的实现原理究竟是什么?(java中aop的实现原理是什么)

在Java开发中,AOP(Aspect-OrientedProgramming,面向切面编程)是一种重要的编程范式,它允许开发者将横切关注点(如日志记录、事务管理、性能监控等)从业务逻辑中分离出来,以提高代码的可维护性和可扩展性。那么,Java中AOP的实现原理是什么呢?
Java中 AOP 的实现原理究竟是什么?(java中aop的实现原理是什么)
Java2024-12-20

Java 中 Lock 的实现原理究竟是什么?(java中lock的实现原理是什么)

在Java编程中,Lock是用于线程同步的工具,它提供了比synchronized关键字更灵活的线程同步机制。本文将深入探讨Java中Lock的实现原理。一、synchronized关键字的局限性synchr
Java 中 Lock 的实现原理究竟是什么?(java中lock的实现原理是什么)
Java2024-12-22

Python中hook的实现原理是什么

在Python中,hook(钩子)是一种机制,允许开发者在特定事件(例如函数调用、异常发生等)发生时插入自定义的代码进行处理。实现原理主要基于Python的装饰器(Decorator)和元编程的概念。装饰器是Python中一种用来修饰函数或
2023-09-26

Golang中map的实现原理是什么

这篇“Golang中map的实现原理是什么”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Golang中map的实现原理是什么
2023-07-05

linux中fork的实现原理是什么

在Linux中,fork是创建新进程的系统调用之一。当调用fork系统调用时,操作系统会复制当前进程的所有资源(包括代码、数据、堆栈等)来创建一个新的进程,这个新的进程称为子进程。子进程是原始进程的副本,它从fork调用返回的地方开始执行。
2023-09-11

openfiler中iSCSI的实现原理是什么

这篇文章将为大家详细讲解有关openfiler中iSCSI的实现原理是什么,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。iSCSI概述iSCSI:Internet 小型计算机系统接口 (iS
2023-06-13

Golang中 WaitGroup的实现原理是什么

这篇文章给大家介绍Golang中 WaitGroup的实现原理是什么,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。1 前言WaitGroup是Golang应用开发过程中经常使用的并发控制技术。WaitGroup,可理解
2023-06-19

Linux中spin_lock的实现原理是什么

spin_lock是Linux中的一种锁机制,用于保护共享资源,避免多个线程同时访问共享资源导致的数据不一致性问题。spin_lock的实现原理是自旋等待。当一个线程需要获取spin_lock时,它会不断地尝试获取锁,而不是进入睡眠状态等待
2023-08-08

Java AOP 的实现原理究竟是什么?(java aop的实现原理是什么)

一、引言在Java开发中,AOP(Aspect-OrientedProgramming,面向切面编程)是一种重要的编程范式,它允许开发者将横切关注点(如日志记录、事务管理、性能监控等)从业务逻辑中分离出来,实现代码的模块化和可维护性。本文
Java AOP 的实现原理究竟是什么?(java aop的实现原理是什么)
Java2024-12-15

hooks的实现原理是什么

Hooks是React 16.8版本引入的一种新特性,它可以让我们在不编写class的情况下使用state和其他React的特性。Hooks的实现原理主要有两个方面:1. 使用链表来保存组件的状态:在React内部,使用一个链表来保存每个组
2023-10-10

chatgpt的实现原理是什么

本文小编为大家详细介绍“chatgpt的实现原理是什么”,内容详细,步骤清晰,细节处理妥当,希望这篇“chatgpt的实现原理是什么”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。chatgpt的原理ChatGPT
2023-02-21

SSH的实现原理是什么

本篇内容介绍了“SSH的实现原理是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!SSH是一种协议标准,它的主要目的是实现远程登录和提供安
2023-06-17

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录