Life and freedom Ge Lin ——— Draw by Razzh

Ant-Design-Icons 的生成之旅(下)

Oct 26 · 8 min

上文中,我们已经看到了 Icons Ast 的生成过程,这篇文章讲述的是生成入口文件和将优化后的 SVGAST 转换成 SVG 文件的过程。

#Icons 入口文件生成

代码还是位于 gulpfile.ts 入口文件中:

// 3.1 generate entry file: src/index.ts
generateEntry({
  entryName: 'index.ts',
  from: ['src/asn/*.ts'],
  toDir: 'src',
  banner: '// This index.ts file is generated automatically.\n',
  template: `export { default as <%= identifier %> } from '<%= path %>';`,
  mapToInterpolate: ({ name: identifier }) => ({
    identifier,
    path: `./asn/${identifier}`
  })
})

作者封装了一个函数,用于将 src下的 asn (Abstract Node)文件生成导出文件 index.ts,进到方法里面看看:

export const generateEntry = ({
  from,
  toDir,
  template,
  mapToInterpolate,
  entryName,
  banner = ''
}: GenerateEntryOptions) =>
  function GenerateEntry() {
    return src(from)
      .pipe(
        useTemplate({
          template,
          mapToInterpolate
        })
      )
      .pipe(concat(entryName))
      .pipe(header(banner))
      .pipe(dest(toDir));
  };

总的来说使用了 useTemplate 函数来渲染传入的模版,并使用 gulp-concat 插件,将 pipe 中的文件合成到 index.ts 中,再使用 gulp-header 插件,将 banner 添加到文件的开头,最后使用 dest 方法将处理后的 index.ts 文件将输出到 src 目录下。

#AST To SVG File

// 3.2 generate inline SVG files
generateInline({
  from: ['src/asn/*.ts'],
  toDir: ({ _meta }) => `inline-svg/${_meta && _meta.theme}`,
  getIconDefinitionFromSource: (content: string): IconDefinition => {
    const extract = ExtractRegExp.exec(content);
    if (extract === null || !extract[1]) {
      throw new Error('Failed to parse raw icon definition: ' + content);
    }
    return new Function(`return ${extract[1]}`)() as IconDefinition;
  }
}),
// 3.3 generate inline SVG files with namespace
generateInline({
  from: ['src/asn/*.ts'],
  toDir: ({ _meta }) => `inline-namespaced-svg/${_meta && _meta.theme}`,
  getIconDefinitionFromSource: (content: string): IconDefinition => {
    const extract = ExtractRegExp.exec(content);
    if (extract === null || !extract[1]) {
      throw new Error('Failed to parse raw icon definition: ' + content);
    }
    return new Function(`return ${extract[1]}`)() as IconDefinition;
  },
  renderOptions: {
    extraSVGAttrs: { xmlns: 'http://www.w3.org/2000/svg' }
  }
})

之后生成 SVG 文件,看到这里其实还是有点好奇作者为啥这么做?因为我看到源码中已经有了 SVG 文件夹,但是我想了一下,作者之前使用 SVGO 对 SVG 文件夹下 的SVG 文件进行了optimize (优化)处理,所以需要让优化后的 SVGAST 重新渲染生成 SVG 文件,或许作者就是这样的出发点去编写的这段代码吧。

仔细观察它们传入的参数只有 renderOptions 不同,这一点作者也给出了注释,是专门为了生成 SVG 的命名空间的,关于命名空间的作用可能是为了更好的兼容浏览器的解析器,避免出现不必要的错误。

export const generateInline = ({
  from,
  toDir,
  getIconDefinitionFromSource,
  renderOptions = {}
}: GenerateInlineOptions) =>
  function GenerateInline() {
    return src(from)
      .pipe(
        useRender({
          getIconDefinitionFromSource,
          renderOptions
        })
      )
      .pipe(dest(toDir));
  };

主要是使用了 useRender 函数对 src/ans/.ts 文件做处理:

export const useRender = ({
  getIconDefinitionFromSource,
  renderOptions
}: RenderOptions) =>
  createTrasformStream((content, file) => {
    const def = getIconDefinitionFromSource(content);
    file.extname = '.svg';
    file.stem = def.name;
    file._meta = {
      theme: def.theme
    } as RenderCustomData;
    return renderIconDefinitionToSVGElement(def, renderOptions);
  });

由于 content 是一个 SVGAST 字符串,所以需要使用 getIconDefinitionFromSource 函数中利用正则 /({\s*".*});/ ,去匹配 {"icon": {...}}, 这样的内容,这个函数中有一个细节就是正则匹配到的是一个 String 类型的字符串,而我们需要的是对象类型的 SVGAST,作者使用了new Function 构造出了一个 SVGAST 对象,再使用 renderIconDefinitionToSVGElement 方法进行 AST to SVG 的操作。

function renderAbstractNodeToSVGElement(
  node: AbstractNode,
  options: HelperRenderOptions
): string {
  const targetAttrs =
    node.tag === 'svg'
      ? {
          ...node.attrs,
          ...(options.extraSVGAttrs || {})
        }
      : node.attrs;
  const attrs = Object.keys(targetAttrs).reduce((acc: string[], nextKey) => {
    const key = nextKey;
    const value = targetAttrs[key];
    const token = `${key}="${value}"`;
    acc.push(token);
    return acc;
  }, []);
  const attrsToken = attrs.length ? ' ' + attrs.join(' ') : '';
  const children = (node.children || [])
    .map((child) => renderAbstractNodeToSVGElement(child, options))
    .join('');
 
  if (children && children.length) {
    return `<${node.tag}${attrsToken}>${children}</${node.tag}>`;
  }
  return `<${node.tag}${attrsToken} />`;
}

这段代码比较长,总的来说就是通过递归将传入的 SVGAST 对象拼接成 SVG 字符串,并返回,再通过 dest 方法输出到各自的目录中。

#最后

最后其实我在读源码的过程中经常能看到 refactor 的字样,4.x是作者从2.x版本的重构而来的,所以我把分支切换到2.x看到整体的源码是使用 rxjs 编写,好奇为什么要在稳定版本的代码上重构呢?最后在 Issue 找到了答案。

image

这个重构的变动之一主要是为了优化 Issue 而做的按需导入,我看了一下2.x版本打包出来的 lib 文件夹

image

其中 dist.jsmanifest.js ,在新版本的 4.x 都被剔除了,并新增了用于 tree-shakinges 目录,都是为了减少打包后的体积而做的改变。 为什么从 rxjs 转变到 gulp?作者也给出了答案:

image

这样,我的 Ant-Design-Icons 的源码之旅也要结束了,研究完源码之后我觉得作者对每个函数的封装都恰到好处,函数式编程非常优雅,源码是值得借鉴和欣赏的,谢谢作者 HeskeyBaozi 的贡献,让我学到很多东西。

浙ICP备2024129591号-1
春秋(Live) - 张敬轩
--:-- / --:--
  1. 1春秋(Live)张敬轩
  2. 2不吐不快(live)张敬轩
  3. 3男孩最痛(live)张敬轩
  4. 4粤语残片(live)陈奕迅
  5. 5几分之几(live)卢广仲
  6. 6地球很危险古巨基
  7. 7樱花树下(live)张敬轩

春秋 (Live) - 张敬轩 (Hins Cheung)

词:林夕

曲:Edmond Tsang

那夜谁将酒喝掉

因此我讲得多了

然后你摇着我手拒绝我

动人像友情深了

我没权终止见面

只因你友善依然

仍用接近甜蜜那种字眼通电

没人应该 怨地怨天

得到这结局

难道怪罪神没有更伪善的祝福

我没有为你伤春悲秋不配有憾事

你没有共我踏过万里

不够剧情延续故事

头发未染霜 着凉亦错在我幼稚

应快活像个天使

有没有运气再扮弱者 玩失意

有没有道理为你落发

必须得到世人同意

心灰得极可耻 心伤得无新意

那一线眼泪 欠大志

爱若能堪称伟大 再难挨照样开怀

如令你发现为你而活到失败

令人不安 我品性坏

我没有为你伤春悲秋不配有憾事

你没有共我踏过万里

不够剧情延续故事

头发未染霜 着凉亦错在我幼稚

应快活像个天使

有没有运气再扮弱者玩失意

有没有道理为你落发

必须得到世人同意

心灰得极可耻 心伤得无新意

那一线眼泪 欠大志 太没意思

若自觉这叫痛苦未免过份容易

我没有被你改写一生怎配有心事

我没有被你害过恨过

写成情史 变废纸

春秋只转载要事

如果爱你欠意义

这眼泪 无从安置

我没有运气放大自私的失意

更没有道理在这日

你得到真爱制造恨意

想心酸 还可以 想心底 留根刺

至少要见面上万次