CSS 模块化
css 默认是全局生效的,只要组件和样式里的选择器匹配则会应用,就会导致我们更改某个样式,所有匹配到的组件的样式都会发生变化,而在实际中我们很可能只想改变某个组件的部分样式,而不是影响整体,所以就诞生了模块化方案。
核心原理:每个样式的选择器(类名、id 等等)独一无二
BEM
block(块) - element(元素) - modifier(修饰符)
- 中划线 :仅作为连字符使用,表示某个块或者某个子元素的多单词之间的连接记号
- 双下划线:双下划线用来连接块和块的子元素
- 单下划线:单下划线用来描述一个块或者块的子元素的一种状态
<div class="article">
<div class="article_body">
<div class="tag">xxx</div>
<button class="article__button--primary"></button>
<button class="article__button--success"></button>
</div>
</div>
优缺点:
- 只使用一个类别选择器,解决复杂嵌套;类名与节点结构对应,清晰明了
- 较长且复杂
CSS Modules
原理:借助构建工具在打包时更改 css 中的类名保证其独一无二
// https://webpack.js.org/loaders/css-loader/#modules
{
loader: "css-loader",
options: {
modules: true,
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
}
// style.module.css
.header { }
.title { }
:global(.xxx) { }
// index.tsx
import moduleStyle from './style.module.css';
const Content = () => {
return <div className={moduleStyle.header}>
<h3 className={moduleStyle.title}>
test css modules
</h3>
</div>;
};
export default Content;
// 编译后
<div class="src-apps-personal-index__header--3uiGt">
<h3 class="src-apps-personal-index__title--3x4rH">
test css modules
</h3>
</div>
更多内容:css-tricks.com/css-modules…
Vue scoped
同样也是使得类名独一无二
<template>
<div class="container"></div>
</template>
<style scoped>
.container {
background-color: aqua;
font-size: 16px;
}
</style>
<!-- 编译后 -->
<div data-v-7ff7c89c class="container"></div>
.container[data-v-7ff7c89c] {
background-color: red;
}
vue-loader 源码解析:
// vue-loader-next\src\index.ts
// template 的处理
const hasScoped = descriptor.styles.some((s) => s.scoped)
const scopedQuery = hasScoped ? `&scoped=true` : ``
// duplicate tags when multiple components import the same css file
const idQuery = !style.src || style.scoped ? `&id=${id}` : ``
const query = `?vue&type=template${idQuery}${scopedQuery}${tsQuery}${attrsQuery}${resourceQuery}`
const styleRequest = stringifyRequest(src + query)
templateImport = `import { ${renderFnName} } from ${templateRequest}`
// vue-loader/templateLoader.js
const { id } = query
const compilerOptions = Object.assign({}, options.compilerOptions, {
scopeId: query.scoped ? `data-v-${id}` : null
})
const compiled = compileTemplate({ compilerOptions })
// style 的处理
const src = style.src || resourcePath
const attrsQuery = attrsToQuery(style.attrs, 'css')
// make sure to only pass id when necessary so that we don't inject
// duplicate tags when multiple components import the same css file
const idQuery = !style.src || style.scoped ? `&id=${id}` : ``
const inlineQuery = asCustomElement ? `&inline` : ``
const query = `?vue&type=style&index=${i}${idQuery}${inlineQuery}${attrsQuery}${resourceQuery}`
const styleRequest = stringifyRequest(src + query)
if (asCustomElement) {
stylesCode += `\nimport _style_${i} from ${styleRequest}`
} else {
stylesCode += `\nimport ${styleRequest}`
}
// core-main\packages\compiler-sfc\src\compileStyle.ts
const shortId = id.replace(/^data-v-/, '')
const longId = `data-v-${shortId}`
if (scoped) {
plugins.push(scopedPlugin(longId))
}
// core-main\packages\compiler-sfc\src\stylePluginScoped.ts
selector.each(n => {
const idToAdd = slotted ? id + '-s' : id
selector.insertAfter(
// If node is null it means we need to inject [id] at the start
// insertAfter can handle `null` here
node as any,
selectorParser.attribute({
attribute: idToAdd,
value: idToAdd,
raws: {},
quoteMark: `"`
})
)
}
CSS In Js
典型代表:styled-components
组件化的思想,将所有的相关直接放在对应的组件上,方便维护(随用随删),类名独一无二,简单的动态样式
Utilising tagged template literals (a recent addition to JavaScript) and the power of CSS, styled-components allows you to write actual CSS code to style your components. It also removes the mapping between components and styles – using components as a low-level styling construct could not be easier!.
利用标记的模板文字(ES6)和 CSS 的强大功能,styled-components 允许您编写实际的 CSS 代码来设置组件的样式。它还删除了组件和样式之间的映射——将组件用作低级样式构造再简单不过了!
import React from 'react';
import styled from 'styled-components';
// Create a <Title> react component that renders an <h1> which is
// centered, palevioletred and sized at 1.5em
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
// Create a <Wrapper> react component that renders a <section> with
// some padding and a papayawhip background
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;
export default () => {
// Use them like any other React component – except they're styled!
return <Wrapper>
<Title>Hello World, this is my first styled component!</Title>
</Wrapper>;
};
// 编译后
<style data-styled="active" data-styled-version="5.3.5">
.jeenfn{font-size:1.5em;text-align:center;color:palevioletred;}
.egDitv{padding:4em;background:papayawhip;}
</style>
<div id="root">
<section class="sc-gsnTZi egDitv">
<h1 class="sc-bczRLJ jeenfn">Hello World, this is my first styled component!</h1>
</section>
</div>
劣:
- 初学者的使用成本
- 代码可读性:类名虽独一无二但无规律
- 性能:工具本身体积(Unpacked Size:3.42 MB),运行时动态生成 css
原理:
const baseStyled = <Target extends WebTarget>(tag: Target) =>
constructWithOptions<'web', Target>(createStyledComponent, tag);
const styled = baseStyled as typeof baseStyled & {
[E in keyof JSX.IntrinsicElements]: WebStyled<E, JSX.IntrinsicElements[E]>;
};
// domElements = ['a', 'div', ...]
domElements.forEach(domElement => {
styled[domElement] = baseStyled(domElement);
});
export default styled;
const test = (x: any, ...y: any) => {
console.log(x, y);
}
test`
font-size: 14px;
color: ${(props: any) => props.color};
background: ${(props: any) => props.isBlack ? 'black' : 'orange'};
`
[
"\n font-size: 14px;\n color: ",
";\n background: ",
";\n "
]
[props => props.color, props => props.isBlack ? 'black' : 'orange']
// 到时直接将获得的值填入组装即可
function generateId(displayName?: string, parentComponentId?: string): string {
const name = typeof displayName !== 'string' ? 'sc' : escape(displayName);
// Ensure that no displayName can lead to duplicate componentIds
identifiers[name] = (identifiers[name] || 0) + 1;
const componentId = `${name}-${generateComponentId(
// SC_VERSION gives us isolation between multiple runtimes on the page at once
// this is improved further with use of the babel plugin "namespace" feature
SC_VERSION + name + identifiers[name]
)}`;
return parentComponentId ? `${parentComponentId}-${componentId}` : componentId;
}
const {
attrs = EMPTY_ARRAY,
componentId = generateId(options.displayName, options.parentComponentId),
displayName = generateDisplayName(target),
} = options;
const styledComponentId =
options.displayName && options.componentId
? `${escape(options.displayName)}-${options.componentId}`
: options.componentId || componentId;
const cssStatic = (
flatten(this.rules, executionContext, styleSheet, stylis) as string[]
).join('');
// 本质上就是保证x有意义(为数字类型),且为正整数,在有效的数组范围内(0 ~ 0xFFFFFFFF),且在无意义的情况下缺省值为0
// https://segmentfault.com/a/1190000014613703
// https://www.zhihu.com/question/20693429
const name = generateName(phash(this.baseHash, cssStatic) >>> 0);
// 生成对应的类名 - 独一无二
let dynamicHash = phash(this.baseHash, stylis.hash);
// 生成 style 标签和设置属性
const head = document.head as any as HTMLElement;
const parent = target || head;
const style = document.createElement('style');
const prevStyle = findLastStyleTag(parent);
const nextSibling = prevStyle !== undefined ? prevStyle.nextSibling : null;
style.setAttribute(SC_ATTR, SC_ATTR_ACTIVE);
style.setAttribute(SC_ATTR_VERSION, SC_VERSION);
parent.insertBefore(style, nextSibling);
// <style data-styled="active" data-styled-version="5.3.5">
export const CSSOMTag = class CSSOMTag implements Tag {
element: HTMLStyleElement;
sheet: CSSStyleSheet;
length: number;
constructor(target?: HTMLElement) {
const element = (this.element = makeStyleTag(target));
// Avoid Edge bug where empty style elements don't create sheets
element.appendChild(document.createTextNode(''));
this.sheet = getSheet(element);
this.length = 0;
}
insertRule(index: number, rule: string): boolean {
try {
this.sheet.insertRule(rule, index);
this.length++;
return true;
} catch (_error) {
return false;
}
}
// 再将所有的样式插入
this.tag.insertRule(ruleIndex, rules)
// sc-gsnTZi egDitv
propsForElement[
// handle custom elements which React doesn't properly alias
isTargetTag &&
domElements.indexOf(elementToBeCreated as unknown as Extract<typeof domElements, string>) === -1
? 'class'
: 'className'
] = (foldedComponentIds as string[])
.concat(
styledComponentId,
(generatedClassName !== styledComponentId ? generatedClassName : null) as string,
props.className,
attrs.className
)
.filter(Boolean)
.join(' ');
// 返回一系列处理后的组件
return createElement(elementToBeCreated, propsForElement);