一:背景
前几天在面试网易伏羲AI前端部门的时候,闻到了一个关于样式隔离的问题“css-modules 是如何做到样式隔离的,它的hash算法是什么”
问道这一个瞬间懵逼勒,说是通过它的组件名和样式名进行hash随机数,这样就可以做到样式的唯一性勒,他又问假如文件和样式都一致的话那怎么处理冲突讷。 额 不是很理解这一块的内容,于是结束之后就写了这一篇文章讲解一下在工程化体系中是如何做到样式隔离的。 先给出总结得结论:
二:样式隔离的方法
现在比较常用的四个方法分别是1. css-modules 2. styled-components 3. shadow-dom 4. vue的scoped用法。 在这里主要是对四个方法进行深刻理解和比较。
2.1 css-modules
2.1.1 介绍和使用用法
CSS Modules 指的是我们像 import js 一样去引入我们的 css 代码,代码中的每一个类名都是引入对象的一个属性, 编译时会将 css 类名 加上唯一 hash。 我们经常可以看到在class中会有这样的一个hash类名来保证样式不会被其他文件相同的样式类给影响。
于是我们就好奇这一个hash值到底是通过什么什么数据得到的讷。
于是我们开启一下探索。 这里使用create-react-app作为初始化脚本搭建demo
// 如果没有安装过create-react-app的请使用
// npm i -g create-react-app
create-react-app test-css-modules
在这里我们定义勒两个组件,同样的类名但是有的是不同的样式表现,一个渲染红色,一个渲染绿色
然后看一下渲染效果:
可以发现使用这一种css-modules模式之后原来的类名被替代勒,
这一个类名的处理是在css-loader中处理的默认的形式是([name][local][hash:base64:5])这样就能解释勒。
2.1.2 css-modules 的实现原理
接下来我们就要去查看css-module这一个转换是如何进行的,以及它的([name][local][hash:base64:5])这一种形式是如何保证它的唯一性的。
简单猜想下 name和local都比较容易看出来,主要是那一个hash是如何拼凑出来的,这一点需要我们查看源码进行探索。
首先我们需要去查看webpack中是如何处理css-module文件的,看代码知道这里对类名的处理调用了一个getCSSModuleLocalIdent程序。
然后我们看一下这一个程序做了什么样的事情
最后我们就知道hash值最后是通过它的文件地址进行唯一编码的而不是通过它的内容。
至此我们就了解从css-modules的使用再到它的编码方式的实现了~
2.2 使用styled-components
2.2.1 介绍和使用用法
import React from "react";
import styled from "styled-components";
const Button = styled.button`
color: red;
background: white;
`;
function C() {
return <Button>1111</Button>;
}
export default C;
实际渲染效果:
2.2.2 原理探索
2.2.2.1、Tagged Template Literals
标签模板字面量是ES6新增的特性,styled-components就是基于这个特性构建的。
先来看看标签模板是啥?
模板字符串可以紧跟在一个函数名后面,然后该函数会被调用来处理这个模板字符串。
看个例子
alert`hahaha`
// 等同于
alert(['hahaha'])
如果模板字符串里有变量,会稍微复杂一点
const a=1;
const b=2;
alertNew`hahaha${a}hello${b+1}bye`
// 等同于
alertNew(['hahaha','hello','bye'],1,3)
第一个参数是没有变量替换的部分组成的数组,后面的参数是各个变量被替换后的值
那回到styled-components语法上来
styled.button 只是 styled('button')的简写,它实际上相当于调用了一个 button 函数。
const Button = styled.button`
color: red;
font-size: 16px;
`;
// 等同于
const Button = styled('button')([ 'color: red;' + 'font-size: 16px;'])
2.2.2.2、创建组件过程
- styled构造函数实现大概长这样:
const styled = (tag: Target) => (...args) => {
return createStyledComponent(tag, {}, css(...args))
}
其中,css函数用来处理标签模板字面量生成RuleSet;createStyledComponent是个高阶组件,应用到传进来的Target上
- 在
createStyledComponent实现内部,会创建一个唯一componentId, 一般通过全局count++,再加上MurmurHash算法,来确保这个id唯一。
count++;
const componentId = 'sc-' + hash('sc' + count);
- 然后根据
componentId和css函数生成的RuleSet计算生成类名className - 使用stylis对样式预处理,添加浏览器对应前缀等
- 创建style标签,塞进head标签中,然后通过
StyleSheet对象将样式规则插入到DOM中
styleSheet.inject(
this.componentId,
stringifyRules(flatCSS,`.${className}`, undefined, componentId),
className,
)
- 通过React.createElement创建组件,绑定className
2.3 使用shadow-dom
2.3.1 介绍和使用用法
- Shadow DOM 是什么
Shadow DOM 是什么?我们先来打开 Chrome 的 DevTool,并在 'Settings -> Preferences -> Elements' 中把 ' Show user agent shadow DOM' 打上勾。然后,打开一个支持 HTML5 播放的视频网站。比如 Youtube:
可以看到 video 内部有一个 #shadow-root ,在 ShadowRoot 之下还能看到 div 这样的普通 HTML 标签。我们能知道 video 会有「播放/暂停按钮、进度条、视频时间显示、音量控制」等控件,那其实,就是由 ShadowRoot 中的这些子元素构成的。而我们最常用的 input 其实也附加了 Shadow DOM,比如,我们在 Chrome 中尝试给一个 Input 加上 placeholder ,通过 DevTools 便能看到,其实文字是在 ShadowRoot 下的一个 Id 为 palcehoder 的 div 中。
Shadow DOM 允许在文档(Document)渲染时插入一棵「子 DOM 树」,并且这棵子树不在主 DOM 树中,同时为子树中的 DOM 元素和 CSS 提供了封装的能力。Shadow DOM 使得子树 DOM 与主文档的 DOM 保持分离,子 DOM 树中的 CSS 不会影响到主 DOM 树的内容,如下图所示:
2.4 vue的scoped方法
2.4.1 介绍和使用用法
这里我简单创建了一个vue文件并在其中样式模块添加了使用scoped,添加之后可以发现在编译之后的文件有了两处改造
- dom结点上多了一个 【data-v-xxx】得属性值
- 相对应在style里面编写得css属性会加多一层属性选择器
2.4.2 原理探索
每个 Vue 文件都将对应一个唯一的 id,该 id 根据文件路径名和内容 hash 生成,通过组合形成scopeId。
编译 template 标签时,会为每个标签添加了当前组件的scopeId,如:
<div class="demo">test</div>
// 会被编译成:
<div class="demo" data-v-12e4e11e>test</div>
编译 style 标签时,会根据当前组件的 scopeId 通过属性选择器和组合选择器输出样式,如:
.demo{color: red;}
// 会被编译成:
.demo[data-v-12e4e11e]{color: red;}
这样就相当为我们配置的样式加上了一个唯一表示。 具体得实现可以查看这一篇文章 知乎 (zhihu.com)](zhuanlan.zhihu.com/p/394134606)
总结
好了,到这里就基本结束对组件间样式隔离的探索了,其实这一些点日常开发中我们都会使用到但是我们没有去深度探索它的一个实现原理,导致我们在面试过程中回答的坑坑巴巴得,希望能够借此文章来总结学习得样式模块化方案,也希望这一番总结能够为大家带来一些帮助吧。