深入探索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 相关文章推荐
How to Auto Include a Javascript File
Feb 02 Javascript
JS预览图像将本地图片显示到浏览器上
Aug 25 Javascript
巧用replace将文字表情替换为图片
Apr 17 Javascript
JS来动态的修改url实现对url的增删查改
Sep 05 Javascript
javascript中tostring()和valueof()的用法及两者的区别
Nov 16 Javascript
Bootstrap CSS布局之按钮
Dec 17 Javascript
Angularjs中ng-repeat-start与ng-repeat-end的用法实例介绍
Dec 31 Javascript
Node.js+jade+mongodb+mongoose实现爬虫分离入库与生成静态文件的方法
Sep 20 Javascript
JavaScript代码执行的先后顺序问题
Oct 29 Javascript
JS实现的缓冲运动效果示例
Apr 30 Javascript
select2 ajax 设置默认值,初始值的方法
Aug 09 Javascript
从0到1搭建element后台框架优化篇(打包优化)
May 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
杏林同学录(四)
2006/10/09 PHP
Blitz templates 最快的PHP模板引擎
2010/04/06 PHP
php header功能的使用
2013/10/28 PHP
ThinkPHP的I方法使用详解
2014/06/18 PHP
php字符串的替换,分割和连接方法
2016/05/23 PHP
OAuth认证协议中的HMACSHA1加密算法(实例)
2017/10/25 PHP
JavaScript建立一个语法高亮输入框实现思路
2013/02/26 Javascript
jquery判断小数点两位和自动删除小数两位后的数字
2014/03/19 Javascript
使用getBoundingClientRect方法实现简洁的sticky组件的方法
2016/03/22 Javascript
浅谈js里面的InttoStr和StrtoInt
2016/06/14 Javascript
微信小程序 教程之WXSS
2016/10/18 Javascript
vue视图不更新情况详解
2019/05/16 Javascript
一文快速详解前端框架 Vue 最强大的功能
2019/05/21 Javascript
微信小程序事件流原理解析
2019/11/27 Javascript
[04:28]2014DOTA2国际邀请赛 采访小兔子LGD挺进钥匙体育馆
2014/07/14 DOTA
python实现巡检系统(solaris)示例
2014/04/02 Python
Django1.7+python 2.78+pycharm配置mysql数据库教程
2014/11/18 Python
Python的mysql数据库的更新如何实现
2017/07/31 Python
Opencv实现抠图背景图替换功能
2019/05/21 Python
python中struct模块之字节型数据的处理方法
2019/08/27 Python
关于HTML5的安全问题开发人员需要牢记的
2012/06/21 HTML / CSS
Monnier Frères美国官网:法国知名奢侈品网站
2016/11/22 全球购物
学生出入校管理制度
2014/01/16 职场文书
索桥的故事教学反思
2014/02/06 职场文书
宿舍保安职务说明书
2014/02/25 职场文书
机关工会开展学习雷锋活动总结
2014/03/01 职场文书
公司办公室岗位职责
2014/03/19 职场文书
环保倡议书50字
2014/05/15 职场文书
2014年学校总务处工作总结
2014/12/08 职场文书
2015年幼儿园班主任工作总结
2015/05/12 职场文书
刑事上诉状(量刑过重)
2015/05/23 职场文书
2016入党积极分子考察评语
2015/12/01 职场文书
唱歌比赛拉拉队口号
2015/12/25 职场文书
《月光曲》教学反思
2016/02/16 职场文书
再谈python_tkinter弹出对话框创建
2022/03/20 Python
MySQL示例讲解数据库约束以及表的设计
2022/06/16 MySQL