当前位置: 首页 > 图灵资讯 > 技术篇> 了解CSS Module作用域隔离原理

了解CSS Module作用域隔离原理

来源:图灵教育
时间:2023-06-05 09:37:28

CSS Module的背景

众所周知,Javascript的发展已经出现了许多模块化规范,如AMD、CMD、 Common JS、ESModule等,这些模块化规范可以隔离我们的JS。但是CSS并没有那么幸运,但是到目前为止还没有模块化规范,因为CSS是 根据选择器对元素进行全局匹配,因此,如果您在页面的两个不同位置定义相同的类名,则先定义的样式将被后定义所覆盖。因此,CSS的命名冲突一直困扰着前端人员。

前端开发者无法接受这种情况,因此CSS社区也诞生了各种CSS模块化解决方案(这不是标准),例如:

  • 命名方法: 命名规则人为约定
  • scoped: vue中常见的隔离方法
  • CSS Module: 每个文件都是独立的模块
  • CSS-in-JS: 这在reacttt中很常见、 JSX中

现在来看CSS Module它是目前最流行的解决方案之一,可以与CSS预处理器一起用于各种框架。

假如这篇文章对你有帮助,❤️关注+点赞❤️鼓励作者,文章微信官方账号首发,关注 前端南玖 第一次获得最新文章~

CSS Module

CSS Module的流行起源于React社区,它在社区中得到了迅速的应用。后来,由于Vue-cli对其集成后开箱即用的支持,将其推向了一个新的高度。

局部作用域

在w3c 规范中,CSS 始终是「全局生效的」。在传统的 web 在开发过程中,最头疼的是治疗 CSS 问题。由于全局性,款式明确定义,但不生效,原因可能是被其他款式定义强制覆盖。

产生局部作用域的唯一方法就是为风格取一个独特的名字,CSS Module也就是说,用这种方法实现作用域隔离。

在CSS Module可以使用:local(className)声明局部作用域的CSS规则。

:local(.qd_btn) {    border-radius: 8px;    color: #fff;}:local(.qd_btn):nth(1) {    color: pink;}:local(.qd_title) {    font-size: 20px;}

CSS Module会对:local()做包含的选择器localIdentName规则处理,即为其生成唯一的选择器名称,以达到作用域隔离的效果。

编译后,上述css将生成这样的代码:

了解CSS Module作用域隔离原理_CSS

这里的:export是CSS 为了解决导出问题,Module添加了新的伪类,稍后介绍

全局作用域

当然CSS Module也允许使用:global(className)声明一个全球作用域的规则。

:global(.qd_text) {    color: chocolate;}

而对于:global()包含选择器CSS Module因为CSS规则默认是全局,所以不会做任何处理。

也许很多人会好奇,我们似乎很少在开发过程中使用它:local(),例如,在vue中,我们只需要在style标签上添加module能自动达到作用域隔离的效果。

是的,为了方便我们的开发过程,postcss-modules-local-by-default默认情况下,插件已经帮助我们处理了这一步。只要我们打开CSS模块化,CSS就会默认添加到编译过程中:local()

Composing(组合)

组合是指一个选择器可以继承另一个选择器的规则。

继承当前文件的内容

:local(.qd_btn) {    border-radius: 8px;    color: #fff;}:local(.qd_title) {    font-size: 20px;    composes: qd_btn;}

了解CSS Module作用域隔离原理_JavaScript_02

继承其他文件

Composes 外部文件中的样式也可以继承

/* a.css */:local(.a_btn) {    border: 1px solid salmon;}

/** default.css **/.qd_box {    border: 1px solid #ccc;    composes: a_btn from 'a.css'}

以下代码将在编译后生成:

了解CSS Module作用域隔离原理_PostCSS_03

导入导出

从以上编译结果中,我们会发现有两种我们平时没用过的伪类::import:export

CSS Module 内部通过ICSS为了解决CSS的导入导出问题,对应上述两种新的伪类。

Interoperable CSS (ICSS) 是标准 CSS 的超集。

:import

语句:import允许从其他 CSS 文件导入变量。它执行以下操作:

  • 获取和处理依赖项
  • 根据导入的令牌分析依赖项的导出,并匹配它们localAlias
  • 搜索和替换当前文件中的某些地方(如下所述)localAlias依赖项的exportedValue.
:export

一个:export块定义了将导出给消费者的符号。可以认为,它在功能上等同于以下几点 JS:

module.exports = {"exportedKey": "exportedValue"}

语法有以下限制:export

  • 它必须在顶层,但可以在文件中的任何位置。
  • 若一个文件中有多个,则将键与值组合并导出。
  • 如果exportedKey重复一个特定项目,最后一个(按源顺序)优先。
  • AnexportedValue可以包含对 CSS 任何声明值有效的字符(包括空格)。
  • exportedValue不需要引用an ,它已被视为文字字符串。

输出可读性需要以下内容,但不是强制性的:

  • 应该只有一个:export
  • 它应该位于文件的顶部,但在任何地方:import块之后
CSS Module原理

对CSS有了大致的了解 在Module语法之后,我们可以看到它的内部实现和它的核心原理 —— 作用域隔离。

一般来说,我们在开发中使用它并不那么麻烦。例如,我们可以在vue项目中开箱即用。最重要的插件是css-loader,我们可以从这里开始探究竟。

你可以在这里思考,css-loader处理主要依靠哪些库?

我们必须知道,CSS Module事实上,这些新语法不是CSS 内置语法,必须编译

那么CSS编译我们首先想到的是哪个库呢?

postcs对吗?对于CSS来说,就像babel对于javascriptts一样

可以安装css-loader验证一下:

了解CSS Module作用域隔离原理_作用域_04

我们可以在这里看到一些与我们预期的一致性postcss-module开头的插件应该是CSS 核心插件Module。

从上面的插件名称中,我们应该能够看到哪一个是实现功能域隔离的

  • Postcss-modules-extract-imports:导入导出功能
  • Postcss-modules-local-by-default:局部作用域默认
  • Postcss-modules-scope:作用域隔离
  • Posts-modules-values:变量功能
编译流程

一般来说,整个过程类似于Babel编译javascript:parse ——> transform ——> stringier

了解CSS Module作用域隔离原理_JavaScript_05

与Babel不同的是,PostCSS本身只包括css分析器,css节点树API,source map生成器和css节点树拼接器。

css的组成单元是一个样式规则(rule),每个样式规则都包含一个或多个属性&值的定义。因此,PostCSS的执行过程是先读取CSS字符内容,得到一个完整的节点树。接下来,对节点树进行一系列转换操作(基于节点树API的插件)。最后,CSS节点树拼接器将转换后的节点树重新组成CSS字符。source可以在此期间生成 map显示转换前后的字符对应关系。

CSS还需要在编译过程中生成AST,就像Babel处理JS一样。

AST

PostCSSAST主要有以下四种:

  • rule: 选择器开头

#main {    border: 1px solid black;}

  • atrule: 以@开头

@media screen and (min-width: 480px) {    body {        background-color: lightgreen;    }}

  • decl: 具体风格规则

border: 1px solid black;

  • comment: 注释

/* 注释*/

类似于Babel,我们也可以使用工具更清楚地理解CSS 的 AST:

了解CSS Module作用域隔离原理_css_06

  • Root: 继承自 Container。AST 代表整个的根节点 css 文件
  • AtRule: 继承自 Container。以 @ 开头语句的核心属性是 params,例如: @import url('./default.css'),params 为url('./default.css')
  • Rule: 继承自 Container。有声明的选择器的核心属性是 selector,例如: .color2{},selector为.color2
  • Comment: 继承自 Node。标准注释/标准注释/* 注释 */ 节点包括一些通用属性:
  • type:节点类型
  • parent:父节点
  • source:存储节点的资源信息,计算 sourcemap
  • start:节点起始位置
  • end:节点的终止位置
  • raws:附加符号、分号、空间、注释等。存储节点 stringify 这些附加符号将在此过程中拼接
安装体验

npm i postcss postcss-modules-extract-imports postcss-modules-local-by-default postcss-modules-scope postcss-selector-parser

我们可以一个接一个地体验这些插件的功能。我们首先将这些主要插件串联起来,尝试效果,然后实现自己的功能Postcss-modules-scope插件

(async () => {    const css = await getCode('./css/default.css')    const pipeline = postcss([        postcssModulesLocalByDefault(),        postcssModulesExtractImports(),        postcssModulesScope()    ])    const res = pipeline.process(css)    console.log('【output】', res.css)})()

集成这些核心插件,我们会发现我们的css中的样式不需要再写了:local它还可以生成唯一的hash名称,也可以导入其他文件的样式。这主要取决于postcss-modules-local-by-defaultpostcss-modules-extract-imports两个插件。

/* default.css */.qd_box {    border: 1px solid #ccc;    composes: a_btn from 'a.css'}.qd_header {    display: flex;    justify-content: center;    align-items: center;    width: 100%;    composes: qd_box;}.qd_box {    background: coral;}

了解CSS Module作用域隔离原理_CSS_07

编写插件

现在让我们自己去实现类似postcss-modules-scope插件,其实原理很简单,就是遍历AST,为选择器生成一个唯一的名字,并与选择器的名字进行维护exports里面。

主要API

说到遍历AST,Post与Babel类似 CSS还为操作AST提供了许多API:

  • walk: 遍历所有节点信息
  • walkAtRules: 遍历所有atrule 类型节点
  • walkRules: 遍历所有rule类型节点
  • walkComments: 遍历所有 comment 类型节点
  • walkDecls: 遍历所有 decl类型节点

(postcss文档可查看更多内容)

有了这些API,我们处理AST非常方便

插件格式

编写PostCSS插件类似于Babel。我们只需要按照它的规范处理AST。我们不需要关心它的编译和目标代码的生成。

const plugin = (options = {}) => {  return {    postcssPlugin: 'plugin name',    Once(root) {      // 每个文件都会调用一次,类似Babel的visitor    }  }}plugin.postcss = truemodule.exports = plugin

核心代码

const selectorParser = require("postcss-selector-parser");// 随机生成选择器名称constt createScopedName = (name) => {    const randomStr = Math.random().toString(16).slice(2);    return `_${randomStr}__${name}`;}const plugin = (options = {}) => {    return {        postcssPlugin: 'css-module-plugin',        Once(root, helpers) {            const exports = {};            // 导出 scopedName            function exportScopedName(name) {                // css名称及其相应的作用域名城映射                const scopedName = createScopedName(name);                exports[name] = exports[name] || [];                if (exports[name].indexOf(scopedName) < 0) {                    exports[name].push(scopedName);                }                return scopedName;            }            // 本地节点,即需要作用域隔离的节点:local()            function localizeNode(node) {                switch (node.type) {                    case "selector":                        node.nodes = node.map(localizeNode);                        return node;                    case "class":                        return selectorParser.className({                            value: exportScopedName(                                node.value,                                node.raws && node.raws.value ? node.raws.value : null                            ),                        });                    case "id": {                        return selectorParser.id({                            value: exportScopedName(                                node.value,                                node.raws && node.raws.value ? node.raws.value : null                            ),                        });                    }                }            }            // 遍历节点            function traverseNode(node) {                // console.log('【node】', node)                if(options.module) {                    const selector = localizeNode(node.first, node.spaces);                    node.replaceWith(selector);                    return node                }                switch (node.type) {                    case "root":                    case "selector": {                        node.each(traverseNode);                        break;                    }                    // 选择器                    case "id":                    case "class":                        exports[node.value] = [node.value];                        break;                    // 伪元素                    case "pseudo":                        if (node.value === ":local") {                            const selector = localizeNode(node.first, node.spaces);                            node.replaceWith(selector);                            return;                        }else if(node.value === ":global") {                        }                }                return node;            }            // 所有rule类型的节点遍历            root.walkRules((rule) => {                const parsedSelector = selectorParser().astSync(rule);                rule.selector = traverseNode(parsedSelector.clone()).toString();                // 所有decl类型的节点 处理 composes                rule.walkDecls(/composes|compose-with/i, (decl) => {                    const localNames = parsedSelector.nodes.map((node) => {                        return node.nodes[0].first.first.value;                    })                    const classes = decl.value.split(/\s+/);                    classes.forEach((className) => {                        const global = /^global(()).exec(className);                        // console.log(exports, className, '-----')                        if (global) {                            localNames.forEach((exportedName) => {                                exports[exportedName].push(global[1]);                            });                        } else if (Object.prototype.hasOwnProperty.call(exports, className)) {                            localNames.forEach((exportedName) => {                                exports[className].forEach((item) => {                                    exports[exportedName].push(item);                                });                            });                        } else {                            console.log('error')                        }                    });                    decl.remove();                });            });            // 处理 @keyframes            root.walkAtRules(/keyframes$/i, (atRule) => {                const localMatch = /^:local\((.*)\)$/.exec(atRule.params);                if (localMatch) {                    atRule.params = exportScopedName(localMatch[1]);                }            });            // 生成 :export rule            const exportedNames = Object.keys(exports);            if (exportedNames.length > 0) {                const exportRule = helpers.rule({ selector: ":export" });                exportedNames.forEach((exportedName) =>                    exportRule.append({                        prop: exportedName,                        value: exports[exportedName].join(" "),                        raws: { before: "\n  " },                    })                );                root.append(exportRule);            }        },    }}plugin.postcss = truemodule.exports = plugin

使用

(async () => {    const css = await getCode('./css/index.css')    const pipeline = postcss([        postcssModulesLocalByDefault(),        postcssModulesExtractImports(),        require('./plugins/css-module-plugin')()    ])    const res = pipeline.process(css)    console.log('【output】', res.css)})()

欢迎关注微信官方账号。 「前端南玖」,如果你想一起进入前端交流群学习,请点击这里

我是南九,下次见!!!