- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
我们知道,Javascript发展到现在出现了众多模块化规范,比如AMD、CMD、 Common JS、ESModule等,这些模块化规范能够让我们的JS实现作用域隔离。但CSS却并没有这么幸运,发展到现在却一直没有模块化规范,由于CSS是 根据选择器去全局匹配元素的,所以如果你在页面的两个不同的地方定义了一个相同的类名,先定义的样式就会被后定义的覆盖掉。由于这个原因,CSS的命名冲突一直困扰着前端人员.
这种现状是前端开发者不能接受的,所以CSS社区也诞生了各种各样的CSS模块化解决方案(这并不是规范),比如:
现在来看 CSS Module 是目前最为流行的一种解决方案,它能够与CSS预处理器搭配使用在各种框架中.
如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新文章~ 。
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经过编译后会生成这样的代码:
这里的 :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() .
组合的意思就是一个选择器可以继承另一个选择器的规则.
:local(.qd_btn) {
border-radius: 8px;
color: #fff;
}
:local(.qd_title) {
font-size: 20px;
composes: qd_btn;
}
Composes 还可以继承外部文件中的样式 。
/* a.css */
:local(.a_btn) {
border: 1px solid salmon;
}
/** default.css **/
.qd_box {
border: 1px solid #ccc;
composes: a_btn from 'a.css'
}
编译后会生成如下代码:
从上面的这些编译结果我们会发现有两个我们平时没用过的伪类: :import 、 :export .
CSS Module 内部通过 ICSS 来解决CSS的导入导出问题,对应的就是上面两个新增的伪类.
Interoperable CSS (ICSS) 是标准 CSS 的超集.
语句 :import 允许从其他 CSS 文件导入变量。它执行以下操作:
localAlias
localAlias
依赖项的 exportedValue
. 一个 :export 块定义了将要导出给消费者的符号。可以认为它在功能上等同于以下 JS:
module.exports = {
"exportedKey": "exportedValue"
}
语法上有以下限制 :export :
exportedKey
重复某个特定项,则最后一个(按源顺序)优先。 exportedValue
可以包含对 CSS 声明值有效的任何字符(包括空格)。 exportedValue
不需要引用an ,它已被视为文字字符串。 以下是输出可读性所需要的,但不是强制的:
:export
块 :import
块之后 大概了解完CSS Module语法后,我们可以再来看看它的内部实现,以及它的核心原理 —— 作用域隔离.
一般来讲,我们平时在开发中使用起来没有这么麻烦,比如我们在vue项目中能够做到开箱即用,最主要的插件就是 css-loader ,我们可以从这里入手一探究竟.
这里大家可以思考下, css-loader 主要会依赖哪些库来进行处理?
我们要知道, CSS Module 新增的这些语法其实并不是CSS 内置语法,那么它就一定需要进行编译处理 。
那么编译CSS我们最先想到的是哪个库?
postcss对吧?它对于CSS就像Babel对于javascript 。
可以安装 css-loader 来验证一下
跟我们预期的一致,这里我们能看到几个以 postcss-module 开头的插件,这些应该就是实现CSS Module的核心插件.
从上面这些插件名称应该能看出哪个才是实现作用域隔离的吧 。
整个流程大体上跟Babel编译javascript类似:parse ——> transform ——> stringier 。
与Babel不同的是,PostCSS自身只包括css分析器,css节点树API,source map生成器以及css节点树拼接器.
css的组成单元是一条一条的样式规则(rule),每一条样式规则又包含一个或多个属性&值的定义。所以,PostCSS的执行过程是,先css分析器读取css字符内容,得到一个完整的节点树,接下来,对该节点树进行一系列转换操作(基于节点树API的插件),最后,由css节点树拼接器将转换后的节点树重新组成css字符。期间可生成source map表明转换前后的字符对应关系.
CSS在编译期间也是需要生成AST得,这点与Babel处理JS一样.
PostCSS的AST主要有以下这四种:
#main {
border: 1px solid black;
}
@
开头
@media screen and (min-width: 480px) {
body {
background-color: lightgreen;
}
}
border: 1px solid black;
/* 注释*/
与Babel类似,这些我们同样可以使用工具来更清晰地了解CSS 的 AST:
@import url('./default.css')
,params 为 url('./default.css')
.color2{}
,selector为 .color2
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-default 、 postcss-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;
}
现在我们就自己来实现一下类似 postcss-modules-scope 的插件吧,其实原理很简单,就是遍历AST,为选择器生成一个唯一的名字,并将其与选择器的名称维护在 exports 里面.
说到遍历AST,与Babel相似Post CSS也同样提供了很多API用于操作AST:
atrule
类型节点 rule
类型节点 comment
类型节点 decl
类型节点 (更多内容可在postcss文档上查看) 。
有了这些API我们处理AST就非常方便了 。
编写PostCSS插件与Babel类似,我们只需要按照它的规范进行处理AST就行,至于它的编译以及目标代码生成我们都不需要关心.
const plugin = (options = {}) => {
return {
postcssPlugin: 'plugin name',
Once(root) {
// 每个文件都会调用一次,类似Babel的visitor
}
}
}
plugin.postcss = true
module.exports = plugin
const selectorParser = require("postcss-selector-parser");
// 随机生成一个选择器名称
const 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 = true
module.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)
})()
原文首发地址 点这里 ,欢迎大家关注公众号 「前端南玖」 ,如果你想进前端交流群一起学习, 请点这里 。
我是南玖,我们下期见!!! 。
最后此篇关于了解CSSModule作用域隔离原理的文章就讲到这里了,如果你想了解更多关于了解CSSModule作用域隔离原理的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
这是我的本地域名 http://10.10.1.101/uxsurvey/profile/dashboard 在 Controller 中,我为用户列表设置了一个操作 redirect(control
要处理 Canonical URL,最佳做法是执行 301 重定向还是更好地为 www 和非 www 域使用相同的 IP 地址? 例如: 想要的规范 URL/域是 http://example.com
1 内网基础 内网/局域网(Local Area Network,LAN),是指在某一区域内有多台计算机互联而成的计算机组,组网范围通常在数千米以内。在局域网中,可以实现文件管理、应用软件共享、打印机
1 内网基础 内网/局域网(Local Area Network,LAN),是指在某一区域内有多台计算机互联而成的计算机组,组网范围通常在数千米以内。在局域网中,可以实现文件管理、应用软件共享、打印机
我想创建一个 weblogic 集群,其中有两个托管服务器,每个服务器在物理上独立的远程计算机上运行 根据weblogic文档 All Managed Servers in a cluster mus
我正在运行 grails 3.1.4,但在创建允许我将多个域对象绑定(bind)到其他几个域对象的模式时遇到了问题。作为我正在尝试做的一个例子: 我有三个类(class)。书籍、作者和阅读列表。 作者
我试图使用@count函数来根据它获取数据,但是在没有崩溃报告的情况下它以某种方式崩溃了。 这是代码 class PSMedia: Object { @objc dynamic var id
有谁知道是否有办法只输入字母字符而不输入数字?我想过这样的事情 CREATE DOMAIN countryDomain AS VARCHAR(100) CHECK( VALUE ??? );
我的代码: const checkoutUrl = 'https://example.com/checkout/*' window.onload = startup() function st
一些不是我编写的应用程序,也不是用 PHP 编写的,它为域 www.example.com 创建了一个 cookie。 我正在尝试替换该 cookie。所以在 PHP 中我做到了: setcookie
什么是 oauth 域?是否有任何免费的 oauth 服务?我可以将它用于 StackApps registration 吗? ?我在谷歌上搜索了很多,但找不到答案。 最佳答案 这是redirect_
自从 In October 2009, the Internet Corporation for Assigned Names and Numbers (ICANN) approved the cre
我使用 apache 作为我的应用程序 Web 服务器的代理,并希望即时更改与 sessionid cookie 关联的域名。 该cookie有一个与之关联的.company.com域,我想使用apa
我只想托管一个子域到cloudflare。我不想将主域名的域名服务器更改为他们的域名服务器。真的有可能吗? 最佳答案 是的,这是可能的,但是需要通过CloudFlare合作伙伴进行设置,或者您需要采用
When using socket in the UNIX domain, it is advisable to use path name for the directory directory m
想象两个共享一个域类的 Grails 应用程序。也许是 Book 域类。 一个应用程序被标识为数据的所有者,一个应用程序必须访问域数据。类似于亚马逊和亚马逊网络服务。 我想拥有的应用程序将使用普通的域
我有一个包含字段“URL”的表单。第一部分需要用户在文本框中填写。第二部分是预定义的,显示在文本框的右侧。 例如,用户在文本框中输入“test”。第二部分预定义为“.example.com”。因此,总
如果我要关闭并取消分配 azure 中的域 Controller ,从而生成新的 vm Generationid,我需要采取哪些步骤来恢复它? 最佳答案 what steps do I need to
我想尝试使用 Azure 作为托管提供商(我有一个域)。我读过那篇文章https://learn.microsoft.com/en-us/azure/app-service-web/web-sites
所以.... 我想知道是否有人可以在这方面协助我? 基本上,我已经创建了一个自托管的Docker容器,用作构建代理(Azure DevOps) 现在,我已经开始测试代理,并且由于我们的放置文件夹位于W
我是一名优秀的程序员,十分优秀!