文章

css modules原理学习

css modules原理学习

整体思路

  1. 识别 :local 包裹的类名,将其进行hash化,保证不污染全局
  2. :local 包裹的类名放在 :export 中,这个是icss的规范,算是css规范的超集。这样就相当于 module.exports ,外面使用时,可以通过 styles.xxx 的方式来拿到hash后的类名。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
     .guang {
         color: blue;
     }
     ._input_css_amSA5i__dong{
         color: green;
     }
     ._input_css_amSA5i__dongdong{
         color: green;
     }
     ._input_css_amSA5i__dongdongdong{
         color: red;
     }
     @keyframes _input_css_amSA5i__guangguang {
         from {
             width: 0;
         }
         to {
             width: 100px;
         }
     }
    	
     :export {
       dong: _input_css_amSA5i__dong;
       dongdong: _input_css_amSA5i__dongdong;
       dongdongdong: _input_css_amSA5i__dongdongdong _input_css_amSA5i__dong _input_css_amSA5i__dongdong;
       guangguang: _input_css_amSA5i__guangguang;
     }
    

AST 解析

前面提高的,识别 :local()global() 标记,并且对其进行hash化,就是通过AST实现的。

这些可以通过 astexplorer.net 来可视化的查看转义后的AST:

Untitled.png

整个转换的过程是:

具体代码这里 https://github.com/QuarkGluonPlasma/postcss-plugin-exercize ,或者展开下面:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
const selectorParser = require("postcss-selector-parser");
function generateScopedName(name) {
    const randomStr = Math.random().toString(16).slice(2);
    return `_${randomStr}__${name}`;
};
const plugin = (options = {}) => {
    return {
        postcssPlugin: "my-postcss-modules-scope",
        Once(root, helpers) {
            const exports = {};
            function exportScopedName(name) {
                const scopedName = generateScopedName(name);

                exports[name] = exports[name] || [];

                if (exports[name].indexOf(scopedName) < 0) {
                    exports[name].push(scopedName);
                }
                return scopedName;
            }
            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) {
                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;
                        }
                }
                return node;
            }
            // 处理 :local 选择器
            root.walkRules((rule) => {
                const parsedSelector = selectorParser().astSync(rule);
                rule.selector = traverseNode(parsedSelector.clone()).toString();
                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);

                        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 {
                            throw decl.error(
                                `referenced class name "${className}" in ${decl.prop} not found`
                            );
                        }
                    });

                    decl.remove();
                });
            });
            // 处理 :local 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;

参考文档

手写 Css-Modules 来深入理解它的原理-51CTO.COM

bookmark

link_preview

bookmark

本文由作者按照 CC BY 4.0 进行授权