深入探索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实现的表格分页实现代码
Jun 21 Javascript
使用Jquery获取带特殊符号的ID 标签的方法
Apr 30 Javascript
Javascript中设置默认参数值示例
Sep 11 Javascript
a标签的href与onclick事件的区别详解
Nov 12 Javascript
基于jQuery实现弹出可关闭遮罩提示框实例代码
Jul 18 Javascript
值得分享的Bootstrap Table使用教程
Nov 23 Javascript
ajax实现动态下拉框示例
Jan 10 Javascript
浅谈react-router HashRouter和BrowserRouter的使用
Dec 29 Javascript
Angular2学习笔记之数据绑定的示例代码
Jan 03 Javascript
浅谈ajax在jquery中的请求和servlet中的响应
Jan 22 jQuery
vue组件实现弹出框点击显示隐藏效果
Oct 26 Javascript
vue实现商品加减计算总价的实例代码
Aug 12 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
来自phpguru得Php Cache类源码
2010/04/15 PHP
PHP session有效期session.gc_maxlifetime
2011/04/20 PHP
在thinkphp5.0路径中实现去除index.php的方式
2019/10/16 PHP
利用javascript查看html源文件
2006/11/08 Javascript
关于图片验证码设计的思考
2007/01/29 Javascript
JS预览图像将本地图片显示到浏览器上
2013/08/25 Javascript
js 剪切板应用clipboardData详细解析
2013/12/17 Javascript
实例说明为什么不要行内使用javascript
2014/04/18 Javascript
javascript处理表单示例(javascript提交表单)
2014/04/28 Javascript
Bootstrap+jfinal实现省市级联下拉菜单
2016/05/30 Javascript
layer弹出层中H5播放器全屏出错的解决方法
2017/02/21 Javascript
彻底解决 webpack 打包文件体积过大问题
2017/07/07 Javascript
JS生成随机打乱数组的方法示例
2017/12/23 Javascript
JS滚轮控制图片缩放大小和拖动的实例代码
2018/11/20 Javascript
js canvas实现画图、滤镜效果
2018/11/27 Javascript
简单通过settimeout看javascript的运行机制
2019/05/10 Javascript
vue移动端模态框(可传参)的实现
2019/11/20 Javascript
Vue自定义组件的四种方式示例详解
2020/02/28 Javascript
解决echarts图表使用v-show控制图表显示不全的问题
2020/07/19 Javascript
Openlayers实现扩散的动态点(水纹效果)
2020/08/17 Javascript
使用beaker让Facebook的Bottle框架支持session功能
2015/04/23 Python
详解Python logging调用Logger.info方法的处理过程
2019/02/12 Python
python tkinter基本属性详解
2019/09/16 Python
使用python matplotlib 画图导入到word中如何保证分辨率
2020/04/16 Python
使用CSS3创建动态菜单效果
2015/07/10 HTML / CSS
澳大利亚快时尚鞋类市场:Billini
2018/05/20 全球购物
某个公司的Java笔面试题
2016/03/11 面试题
C#基础面试题
2016/10/17 面试题
机械电子工程专业推荐信范文
2013/11/20 职场文书
招商业务员岗位职责
2013/12/16 职场文书
自动一体化专业求职信
2014/03/15 职场文书
离婚协议书怎么写
2014/09/12 职场文书
转让协议书
2015/01/27 职场文书
2015年中秋节演讲稿
2015/03/20 职场文书
win10如何快速切换窗口 win10切换窗口快捷键分享
2022/07/23 数码科技
css让页脚保持在底部位置的四种方案
2022/07/23 HTML / CSS