深入探索VueJS Scoped CSS 实现原理


Posted in Javascript onSeptember 23, 2019

使用VueJS进行应用开发, 脱离不了对应用间的模块进行拆分, 将大块界面拆解为组件的过程. 我们可以很方便的在单文件中使用<template>块维护组件的视图, 使用<script>维护组件的逻辑部分, 使用<style>维护组件的样式. 在我们编写 VueJS 组件样式时, 不得忽略的一点就是样式污染.

样式污染产生原因

提及样式污染, 主要要追溯到Webpack对CSS文件的打包过程, 这里我们以Vue-Element-Admin中的Webpack配置项举例:

const webpackConfig = merge(baseWebpackConfig, {
 plugins: [
 new MiniCssExtractPlugin({
     filename: utils.assetsPath('css/[name].[contenthash:8].css'),
     chunkFilename: utils.assetsPath('css/[name].[contenthash:8].css')
    }),
 ]
})

Webpack 使用 MiniCssExtractPlugin 插件, 将文件(如Vue单文件组件)中的CSS代码, 经过处理后, 分离到形如app.hash1234.css的单独的CSS文件:

深入探索VueJS Scoped CSS 实现原理

如果没有加入防止样式污染的措施的同时, 项目中存在了大量的同名 ClassName, 那么可能会产生意想不到的CSS选择器权重覆盖. 这可能使后文件中某部分选择器权重更高的类影响整个应用, 而此过程通常发生在组件的编写中, 所以一般称之为组件样式污染.

Webpack & Vue SFC Object

对于 Vue 项目而言, 使用 Webpack 将极大的优化了工作流程, 因为通过Vue Loader, Vue 单文件组件能很好的融合进 Webpack 工作流中. 通过跟踪源码, 可以发现, 我们写的单文件组件都被处理为了SFC对象, 即包含了单个HTML模块, 单个脚本模块, 一个或多个样式模块, 一个或多个自定义模块的对象:

// vue-loader/index.js
const descriptor = parse({
  source,
  compiler: options.compiler || loadTemplateCompiler(),
  filename,
  sourceRoot,
  needMap: sourceMap
})

// vuejs/component-compiler-utils/index.js
function parse(options) {
  const { compiler } = options
  output = compiler.parseComponent(source, compilerParseOptions)
  return output
}

// vue.js
function parseComponent(content, options) {
 // ...
  var sfc = {
    template: null,
    script: null,
    styles: [],
    customBlocks: []
  }
  // ...
  return sfc
}

我们可以将SFC结构融合到Webpack进行开发的过程成中, 主要有这几点影响:

  • 允许为 Vue 组件的每个部分使用其它的 webpack loader,例如在 <style>的部分使用 Sass Loader , 在 <customBlocks>的部分使用自定义 Loader
  • 使用 webpack loader 将 <style>和 <template> 中引用的资源当作模块依赖来处理
  • 模拟 Scoped CSS
  • 在开发过程中使用热重载来保持状态

以下主要介绍Scoped CSS的原理.

Scoped CSS

大白话版本之 Scoped CSS 原理

通过 Webpack 调用 VueJS 中相应 Loader , 给组件HTML模板添加自定义属性 (Attribute) data-v-x, 以及给组件内CSS选择器添加对应的属性选择器 (Attribute Selector) [data-v-x], 达到组件内样式只能生效与组件内HTML的效果, 代码效果如下:

<div class='lionad' data-v-lionad></div>
<style>
.lionad[data-v-lionad] {
 background: @tiger-orange;
}
</style>

源码跟踪

Webpack 使用其它 CSS Loader 处理 VueJS 中对应 CSS 代码之前, Vue Loader 已经替我们做了一层简单的处理, 如果组件中 style 块包含了 scoped 属性:

<!-- 某个VueJS组件中 -->
<template>
  <div class='lionad'></div>
</template>
<style lang="scss" scoped>
  .lionad {
    background: @tiger-orange;
  }
</style>

下代码即判断当前SFC对象样式块中是否有scoped属性, 并插入用于 query 中, 顺带一提, 每个单文件组件被解析后, 都会生成对应组件ID, ID主要以生产/开发环境做区分, 通过文件路径+源码或是文件路径的值作为哈希特征值的形式生成, 如下:

// vue-loader/index.js
const id = hash(isProduction (shortFilePath + '\n' + source) : shortFilePath)
const hasScoped = descriptor.styles.some(s => s.scoped)
const query = `? vue&type=template${idQuery}${scopedQuery}`
const request = templateRequest = stringifyRequest(src + query)
templateImport = `import { render, staticRenderFns } from ${request}`

HTML模板处理

在用于处理SFC结构中HTML模板的 templateLoader 中, 我们可以得知, query 中所设置的参数将合并为 loader options 经由 Webpack 转交 templateLoader 再转交 @vue/component-compiler-utils.compileTemplate 处理:

// vue-loader/templateLoader.js
const query = qs.parse(this.resourceQuery)
const { id } = query
const compilerOptions = Object.assign({}, options.compilerOptions, {
  scopeId: query.scoped ? `data-v-${id}` : null
})
const compiled = compileTemplate({ compilerOptions })

实际 compileTemplate 函数在处理内容时, 编译函数使用的是 query 中的 compiler 或 vue-template-compiler, 后者会将模板文本转换成为 JavaScript 渲染函数, 大致如下:

  1. 从HTML模版转换为AST(虚拟语法树)
  2. AST优化,处理静态模版与动态模板
  3. 生成JS函数,用于在运行时运行时生成纯HTML

代码分别对应:

// vue-template-compiler/build.js/createCompilerCreator
var ast = parse(template.trim(), options)
optimize(ast, options)
var code = generate(ast, options)

先前我们的组件ID在 parse 阶段解析开始标签时就会被推入内部储存的数据结构中:

function elementToOpenTagSegments (el, state) {
 var segments = [{ type: RAW, value: ("<" + (el.tag)) }]
 // _scopedId
 if (state.options.scopeId) {
  segments.push({ type: RAW, value: (" " + (state.options.scopeId)) })
 }
 segments.push({ type: RAW, value: ">" })
 return segments
}

先前我们的HTML模板 <div class='lionad'></div> 中开始标签会被转换成如下数据结构:

[
  { type: RAW, value: '<div' },
  { type: RAW, value: 'class=lionad' },
  { type: RAW, value: 'data-v-xxxxxx' },
  { type: RAW, value: '>' },
]

样式模板处理

与 HTML Template 解析的过程类似, 通过 Webpack 将样式模板转交 stylePostLoader 进行处理, 处理逻辑主要引用了 @vue/component-compiler-utils 中的 compileStyle 部分, 后者对样式模板进行解析的过程中, 将会对含 scoped 标记的模板引入插件 stylePlugins/scoped.js, scoped.js 将 data-v-xxxxxx 添加到选择器末尾的过程如下:

selectors.each((selector) => {
  selector.each((n) => {
    if (n.value === '::v-deep' || n.value === '>>>' || n.value === '/deep/') {
      return false;
    }
  });
  selector.insertAfter(node, selectorParser.attribute({
    attribute: id
  }))
})

题外话, 通过以上代码, 我们发现当当前处理到三种特定类型选择器会终止循环, 停止将 data-v-xxx 添加到选择器末尾:

  1. 伪类 ::v-deep
  2. 选择器 >>>
  3. 选择器 /deep/

我们可以利用这个特征, 在组件中写样式穿透, 即内部组件影响外部组件样式 (ε=ε=ε=┏(?ロ?;)┛ 主动样式污染), 当然这在特定的情境下是有用的, 比如当我们想主动覆盖第三方UI组件框架的样式, 却不想引入新的CSS文件, 或不想写非 Scoped CSS 模板的时候.

最后

本人前端菜得捉急, 文中不详尽或有错的地方, 欢迎各位大佬斧正. 如果本文对你有所帮助, 那是再好不过, 看到这里都是真爱啊

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Javascript Select操作大集合
May 26 Javascript
JavaScript设计模式之外观模式实例
Oct 10 Javascript
jQuery控制div实现随滚动条滚动效果
Jun 07 Javascript
Highcharts入门之基本属性
Aug 02 Javascript
JavaScript提高加载和执行效率的方法
Feb 03 Javascript
vue.js实例todoList项目
Jul 07 Javascript
详解vue-router导航守卫
Jan 19 Javascript
浅谈layui框架自带分页和表格重载的接口解析问题
Sep 11 Javascript
JS实现的进制转换,浮点数相加,数字判断操作示例
Nov 09 Javascript
解决Vue打包上线之后部分CSS不生效的问题
Nov 12 Javascript
vue实现图片上传功能
May 28 Javascript
微信小程序轮播图swiper代码详解
Dec 01 Javascript
小程序实现锚点滑动效果
Sep 23 #Javascript
React Native 混合开发多入口加载方式详解
Sep 23 #Javascript
Node.js HTTP服务器中的文件、图片上传的方法
Sep 23 #Javascript
node 文件上传接口的转发的实现
Sep 23 #Javascript
layui 上传文件_批量导入数据UI的方法
Sep 23 #Javascript
Electron 调用命令行(cmd)
Sep 23 #Javascript
layui文件上传控件带更改后数据传值的方法
Sep 23 #Javascript
You might like
个人站长制做网页常用的php代码
2007/03/03 PHP
深入聊聊Array的sort方法的使用技巧.详细点评protype.js中的sortBy方法
2007/04/12 Javascript
JavaScript 全面解析各种浏览器网页中的JS 执行顺序
2009/02/17 Javascript
jQuery ajax(复习)—Baidu ajax request分离版
2013/01/24 Javascript
Javascript遍历Html Table示例(包括内容和属性值)
2014/07/08 Javascript
jQuery打印图片pdf、txt示例代码
2014/07/22 Javascript
jquery制作 随机弹跳的小球特效
2015/02/01 Javascript
js实现简单的可切换选项卡效果
2015/04/10 Javascript
浅析jquery如何判断滚动条滚到页面底部并执行事件
2016/04/29 Javascript
jQuery实现简单日期格式化功能示例
2017/09/19 jQuery
JS中使用textPath实现线条上的文字
2017/12/25 Javascript
浅谈React 服务器端渲染的使用
2018/05/08 Javascript
微信小程序实现人脸识别
2018/05/25 Javascript
在Vue项目中取消ESLint代码检测的步骤讲解
2019/01/27 Javascript
深入学习JavaScript中的bom
2019/05/27 Javascript
layer.msg()去掉默认时间,实现手动关闭的方法
2019/09/12 Javascript
Python各类图像库的图片读写方式总结(推荐)
2018/02/23 Python
opencv python 图像去噪的实现方法
2018/08/31 Python
Python实现定时自动关闭的tkinter窗口方法
2019/02/16 Python
python将字母转化为数字实例方法
2019/10/04 Python
python 基于dlib库的人脸检测的实现
2019/11/08 Python
tensorflow模型继续训练 fineturn实例
2020/01/21 Python
python GUI库图形界面开发之PyQt5信号与槽事件处理机制详细介绍与实例解析
2020/03/08 Python
python高级特性简介
2020/08/13 Python
python中not、and和or的优先级与详细用法介绍
2020/11/03 Python
CSS3 文字动画效果
2020/11/12 HTML / CSS
艺术应用与设计专业个人的自我评价
2013/11/19 职场文书
办公室岗位职责
2014/02/12 职场文书
今冬明春火灾防控工作方案
2014/05/29 职场文书
咖啡店创业计划书范文
2014/09/15 职场文书
助学贷款贫困证明
2014/09/23 职场文书
私人房屋买卖协议书
2014/10/04 职场文书
党建工作目标管理责任书
2015/01/29 职场文书
上级领导检查欢迎词
2015/09/30 职场文书
浅谈MySQL中的六种日志
2022/03/23 MySQL
使用MybatisPlus打印sql语句
2022/04/22 SQL Server