深入探索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 相关文章推荐
jQuery html() in Firefox (uses .innerHTML) ignores DOM changes
Mar 05 Javascript
面向对象Javascript核心支持代码分享
May 23 Javascript
给文字加上着重号的JS代码
Nov 12 Javascript
js与jquery回车提交的方法
Feb 03 Javascript
jQuery判断元素是否显示 是否隐藏的简单实现代码
May 19 Javascript
jQuery表单验证插件解析(推荐)
Jul 21 Javascript
郁闷!ionic中获取ng-model绑定的值为undefined如何解决
Aug 27 Javascript
React Native 集成jpush-react-native的示例代码
Aug 16 Javascript
基于Vue2.X的路由和钩子函数详解
Feb 09 Javascript
详解KOA2如何手写中间件(装饰器模式)
Oct 11 Javascript
js删除对象/数组中null、undefined、空对象及空数组方法示例
Nov 14 Javascript
vue实现购物车列表
Jun 30 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 HTML代码串 截取实现代码
2009/06/29 PHP
JoshChen_php新手进阶高手不可或缺的规范介绍
2013/08/16 PHP
php文件管理基本功能简单操作
2017/01/16 PHP
PHP测试框架PHPUnit组织测试操作示例
2018/05/28 PHP
用JAVASCRIPT如何给&amp;lt;textarea&amp;gt;&amp;lt;/textarea&amp;gt;赋值
2007/04/20 Javascript
判断脚本加载是否完成的方法
2009/05/26 Javascript
HTML5附件拖拽上传drop &amp; google.gears实现代码
2011/04/28 Javascript
jQuery实现form表单reset按钮重置清空表单功能
2012/12/18 Javascript
新增加的内容是如何将div的scrollbar自动移动最下面
2014/01/02 Javascript
JS实现浏览器状态栏文字闪烁效果的方法
2015/10/27 Javascript
JS实现日期时间动态显示的方法
2015/12/07 Javascript
node网页分段渲染详解
2016/09/05 Javascript
JavaScript模块化之使用requireJS按需加载
2017/04/12 Javascript
JavaScript对象_动力节点Java学院整理
2017/06/23 Javascript
详解Vue用自定义指令完成一个下拉菜单(select组件)
2017/10/31 Javascript
node.js将MongoDB数据同步到MySQL的步骤
2017/12/10 Javascript
webpack配置导致字体图标无法显示的解决方法
2018/03/06 Javascript
koa上传excel文件并解析的实现方法
2018/08/09 Javascript
Angular PWA使用的Demo示例
2019/01/31 Javascript
微信小程序实现弹出菜单动画
2019/06/21 Javascript
layer 关闭指定弹出层的例子
2019/09/25 Javascript
vue+render+jsx实现可编辑动态多级表头table的实例代码
2020/04/01 Javascript
微信小程序实现简单的select下拉框
2020/11/23 Javascript
python client使用http post 到server端的代码
2013/02/10 Python
从零开始学Python第八周:详解网络编程基础(socket)
2016/12/14 Python
python 杀死自身进程的实现方法
2019/07/01 Python
pytorch 加载(.pth)格式的模型实例
2019/08/20 Python
adidas马来西亚官网:adidas MY
2020/09/12 全球购物
大专学生推荐信范文
2013/11/19 职场文书
儿子婚宴答谢词
2014/01/09 职场文书
大二学生职业生涯规划书
2014/02/05 职场文书
股权转让意向书
2014/04/01 职场文书
承诺函格式模板
2015/01/21 职场文书
应届毕业生求职简历自我评价
2015/03/02 职场文书
毕业设计致谢词
2015/05/14 职场文书
PostgreSQL存储过程实用脚本(二):创建函数入门
2021/04/05 PostgreSQL