惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tor Project blog
月光博客
月光博客
P
Proofpoint News Feed
大猫的无限游戏
大猫的无限游戏
N
News and Events Feed by Topic
The Cloudflare Blog
博客园_首页
NISL@THU
NISL@THU
量子位
A
Arctic Wolf
Y
Y Combinator Blog
Spread Privacy
Spread Privacy
Engineering at Meta
Engineering at Meta
F
Fortinet All Blogs
酷 壳 – CoolShell
酷 壳 – CoolShell
Cyberwarzone
Cyberwarzone
The GitHub Blog
The GitHub Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
F
Full Disclosure
C
Cisco Blogs
Security Latest
Security Latest
T
The Exploit Database - CXSecurity.com
T
Tenable Blog
PCI Perspectives
PCI Perspectives
S
Security Affairs
Forbes - Security
Forbes - Security
Hugging Face - Blog
Hugging Face - Blog
C
CERT Recently Published Vulnerability Notes
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 聂微东
H
Hacker News: Front Page
S
Securelist
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
D
Darknet – Hacking Tools, Hacker News & Cyber Security
罗磊的独立博客
S
SegmentFault 最新的问题
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
S
Security @ Cisco Blogs
The Last Watchdog
The Last Watchdog
小众软件
小众软件
Hacker News - Newest:
Hacker News - Newest: "LLM"
Google DeepMind News
Google DeepMind News
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
IT之家
IT之家
Last Week in AI
Last Week in AI
爱范儿
爱范儿
AWS News Blog
AWS News Blog
MongoDB | Blog
MongoDB | Blog

木匣子

以 Maildir 方式备份邮件 后疫情之中国行 我的 Covid 经历 创建自签名 SSL 证书 写在 2021 年末 在 macOS 上使用 eGPU 的体验 给 Apollo-Link 打补丁 II 动森二维码生成器:服装类(后篇) 动森二维码生成器:服装类(前篇) 动森二维码生成器 for Aseprite 动森图鉴+开发笔记 给 Apollo-Link 打补丁 「攻略」JetBrains Quest S1E3 「攻略」JetBrains Quest S1E2 「攻略」JetBrains Quest S1E1 助你同时维护多个项目的终端神器:Tmux 中文博客的字体选型 写在 2019 年末 Edge 浏览器无法访问 VirtualBox 内的网站
使用 Webpack Loader 加载 Icon Font 映射
Lingjia · 2020-02-03 · via 木匣子

最近在做的新项目是使用 React 构建一个新的网站,实现新的需求的同时慢慢将旧网站迁移过来。其中的一部分工作是建立一个可重用的前端组件库。

实现一个前端组件库需要非常多的工作量,这里有一份详细的 Checklist 可供参考。除此之外,我们还需要为这些可重用组件建立一份文档,这样大家就可以照着文档去使用这些组件了。在对比了一些文档工具后,我选择了 Storybook 这款非常小清新的可视化组件文档生成器。它支持各种主流框架。

集成 Storybook 到项目的过程遇到了不少坑,不过这篇博客我们暂时不讨论这些,有空的话我再另开一篇文章吧。本文我想聊聊写文档的时候遇到的一些需求。

¶背景

在项目中我们使用了一款自制的图标字体(Icon Font),以字体的形式将网站常用的图标打包成 Web Font,然后再在页面中使用。

从设计师同事那获得的素材文件如下:

~/Downloads/racing20_march
├── fonts
│   ├── racing20.eot
│   ├── racing20.svg
│   ├── racing20.ttf
│   └── racing20.woff
├── icons-reference.html
└── styles.css

其中 icons-reference.html 是说明文档,内里介绍了如何使用这个字体,以及一个图标名称及对应字符的映射关系。

所谓映射(Mapping),可以从 styles.css 中看到一些例子:

.icon-article:before {
  content: "\61";
}
.icon-calendar:before {
  content: "\64";
}
...

article 图标对应的字符是 \61 即字母 a

不过使用的时候我们并不需要关心这个映射。只要知道想用这个图标的话,引用对应的英文名即可:

<i class="icon icon-article"></i>

¶需求

我们要做的正是将这个说明文档中的映射关系集成到我们的 Storybook 组件文档中去。以便在文档中显示所有图标,还可以直接点击图标复制组件代码,方便引用。

一个简单的方法就是手动创建这个列表,把映射关系整理到一个数组中。但是考虑到后期的维护,如新增图标或者映射有变化,就需要重新校对这个列表,是一件很麻烦的事。

既然如此,何不一开始就将其自动化?我们只需要写一个脚本将这个 styles.css 中的映射关系提取出来,就可以为我所用。另外这个 styles.css 作为唯一数据源,更新起来也很方便,直接将设计师提供的新文件覆盖旧文件即可。符合 Single Source of Truth 原则。

¶设计

通过自顶向下设计,我希望在 Storybook 里直接引用这个 css 文件,然后得到一个映射关系的数组:

import charsets from './fonts/racing20/styles.css';
// charsets = [{key: 'article': value: '\\61'}, ...]

显然我们可以写一个自定义的 Webpack Loader 来完成这个工作。而这个功能非常特殊,其它地方也用不到,所以我们可以直接使用 inline loader 来简化配置:

import charsets from 'icon-font-loader!./fonts/racing20/styles.css';

由于我们输入的 css 将直接生成 javascript 数组,我们不希望它被当作普通 css 文件进行额外的处理。我们需要使用额外的修饰符来标记这个文件:

import charsets from '!icon-font-loader!./fonts/racing20/styles.css';

最前面的 ! 表示略过 Webpack 配置文件中针对该类文件的标准 Loader 。这样,该文件只会被我们的自定义 Loader 处理。

¶实现

实现一个自定义 Loader 非常的简单,可以从官方的文档开始,也可以参考一些简单的现成的 Loader,例如 json5-loader

简而言之只要写一个函数,接受一个字符串类型的 source 参数,并生成一个 Javasciprt 模块的源文件即可。

module.exports = function loader(source) {
  let charsets = [];

  try {
    // parse the charsets from css file
  } catch (error) {
    this.emitError(error);
  }

  return `module.exports = ${JSON.stringify(charsets)};`;
};

styles.css 文件将被读入到字符串中。而这个 css 文件非常规则(参考上文的映射),使得我们可以使用简单的正则表达式直接提取出:

const parser = /.icon-([a-z-]+):before {\s+content: "(\\\w+)";\s+}/gm;
let ret = null;
while ((ret = parser.exec(source))) {
    charsets.push({
    key: ret[1],
    value: ret[2],
    });
}

但考虑到 css 本身的结构特性,我们还可以使用更加强大的解析器直接将 css 文件转化成语法树(AST)来提取我们需要的信息。解析出来的 ast 大概如下:

{
  "type": "stylesheet",
  "stylesheet": {
    "rules": [
      {
        "type": "rule",
        "selectors": [
          ".icon-article:before"
        ],
        "declarations": [
          {
            "type": "declaration",
            "property": "content",
            "value": "\"\\61\"",
            "position": {
              "start": {
                "line": 43,
                "column": 3
              },
              "end": {
                "line": 43,
                "column": 17
              }
            }
          }
        ],
        "position": {
          "start": {
            "line": 42,
            "column": 1
          },
          "end": {
            "line": 44,
            "column": 2
          }
        }
      },
      ...

我们仍然需要用正则表达式去提取 selector/content 中有用的部分,但不再需要担心空格和换行带来的困扰。

const css = require('css');

const regKey = new RegExp(`\\.icon-([a-z-]+):before`);
const regValue = new RegExp(/"(\\\w+)"/);
const ast = css.parse(source);
charsets = ast.stylesheet.rules
    .filter(r => r.type === 'rule' && r.selectors[0].startsWith(`.icon-`))
    .map(r => {
        const selector = r.selectors[0];
        const key = selector.match(regKey)[1];
        const content = r.declarations.find(d => d.property === 'content');
        const value = content.value.match(regValue)[1];
        return { key, value };
    });

为了使这个脚本更加健壮,我们可以作如下改进:

  • 增加 prefix 配置
  • 更丰富的的选择器名字:[a-zA-Z0-9-]
  • 支持两种伪元素选择器::::
const options = getOptions(this) || {};
const { prefix } = options;
const regKey = new RegExp(`\\.${prefix}([a-zA-Z0-9-]+)::?before`);

由于在制作 Icon Font 的时候,设计师可以对不同的图标文件提供不同的前缀(prefix),我们可以将其作为一个配置项:

import charsets from '!icon-font-loader?prefix=icon-!./fonts/racing20/styles.css';

这样我们就可以在 Storybook 中遍历展示这个字体中的所有图标了:

const IconList = () => charsets.map(char =>
    <ClickToCopySnippet>
        <Icon type={char.key}/>
        <Text>{char.value}</Text>
    </ClickToCopySnippet>);

完整的 Loader 脚本见此。由于是本地使用,所以不必发布到 npm 上。只需要在 Webpack 配置中设置一下 resolveLoader ,让 Webpack 知道我们的 Loader 在何处即可。

module.exports = {
  // ...
  resolveLoader: {
    modules: ['node_modules', 'internals/webpack/loaders'],
  },
  // ...
}