Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

前端图标库

Oak 前端图标库围绕 OakIcon 组件工作。它解决的不是“怎么在某个 React 页面里临时引入一个图标”,而是让页面配置、菜单配置、web 运行时和小程序编译都能用同一套字符串图标名。

最重要的规则只有一条:

  • 页面、菜单、namespace 配置里只写字符串;
  • 真正的图标库实现写在 oak.config.ts、web 初始化 options 或 namespace config 里;
  • 渲染层统一把字符串交给 OakIcon

这样做之后,oak-cli 才能在编译时生成路由、菜单、web 按需加载和小程序图标样式。

基本用法

业务代码里统一使用 OakIcon

import OakIcon from '@oak-frontend-base/components/icon';

<OakIcon name="oak:setup_fill" size={18} />
<OakIcon name="trip:hotel" size={18} />
<OakIcon name="antd:ShopOutlined" size={18} />

在页面、菜单、namespace 配置里也只写字符串:

icon: 'antd:ShopOutlined'

不要在 CreatePageConfig(...)CreateNamespaceConfig(...) 中写 ReactNode,例如不要写:

icon: <ShopOutlined />

这些配置需要保持可序列化。否则路由生成、菜单生成和按需图标加载都会失去静态分析基础。

图标名怎么写

Oak 当前支持三类常见写法。

1. Oak 内置图标

旧项目里常见的写法仍然可用:

icon: 'setup_fill'

这等价于:

icon: 'oak:setup_fill'

新代码更推荐显式写 oak:

icon: 'oak:setup_fill'
icon: 'oak:tasklist'

oak:* 来自 oak-frontend-base 内置 iconfont,web 和小程序都支持。

2. 自定义 font 图标

业务自己的字体图标使用 <library>:<name>

icon: 'trip:hotel'
icon: 'trip:order'

其中 trip 是图标库名,hotel / order 是业务图标名。

3. Web React 图标

web 端可以使用 React 图标库,例如 Ant Design Icons:

icon: 'antd:ShopOutlined'
icon: 'antd:SettingOutlined'

type: 'react' 只支持 web。小程序不能直接渲染 React 图标组件。

oak.config.ts 中配置

跨端通用配置写在项目根目录 oak.config.tsfrontend.iconLibraries 下。

使用已有 iconfont 样式

如果项目已经有 iconfont 的 CSS / Less 文件,可以这样配置:

import { CreateCompilerConfig } from '@xuchangzju/oak-cli/lib/createConfig';

export default CreateCompilerConfig({
    frontend: {
        iconLibraries: [
            {
                name: 'trip',
                type: 'font',
                fontFamily: 'trip-iconfont',
                fontFamilyClass: 'trip-iconfont',
                classPrefix: 'trip-icon-',
                style: '@project/assets/icons/trip/iconfont.less',
                subset: 'auto',
            },
        ],
    },
});

这些字段的含义是:

  • name:图标库名,对应图标字符串里的 trip:
  • type: 'font':字体图标库,web 和小程序都能用;
  • fontFamily:匹配 @font-face 中的字体名;
  • fontFamilyClass:运行时加到非 oak 图标上的字体 class;
  • classPrefix:glyph class 前缀;
  • style:已有 iconfont 样式文件;
  • subset:小程序端是否裁剪未使用的 glyph。

fontUrl + icons 生成样式

如果项目不想维护完整 iconfont 样式,也可以只给字体文件和 glyph 映射:

import { CreateCompilerConfig } from '@xuchangzju/oak-cli/lib/createConfig';

export default CreateCompilerConfig({
    frontend: {
        iconLibraries: [
            {
                name: 'trip',
                type: 'font',
                fontFamily: 'trip-iconfont',
                fontFamilyClass: 'trip-iconfont',
                classPrefix: 'trip-icon-',
                fontUrl: '@project/assets/icons/trip/iconfont.woff2',
                fontFormat: 'woff2',
                icons: {
                    hotel: '\\e600',
                    order: '\\e601',
                },
                subset: 'auto',
            },
        ],
    },
});

fontUrl 只说明字体文件在哪里,icons 才说明业务图标名和 glyph content 的对应关系。只配置字体文件,Oak 不能自动猜出 hotelorder 这些业务名。

运行时 trip:hotel 会生成类似这样的 class:

oak-icon trip-iconfont trip-icon-hotel oak-icon__primary

按平台配置

如果 web 和小程序使用不同图标库,可以放到 frontend.targets 下:

import { CreateCompilerConfig } from '@xuchangzju/oak-cli/lib/createConfig';

export default CreateCompilerConfig({
    frontend: {
        targets: {
            web: {
                iconLibraries: [
                    {
                        name: 'antd',
                        type: 'react',
                        packageName: '@ant-design/icons',
                        platforms: ['web'],
                    },
                ],
            },
            wechatMp: {
                iconLibraries: [
                    {
                        name: 'trip',
                        type: 'font',
                        fontFamilyClass: 'trip-iconfont',
                        classPrefix: 'trip-icon-',
                        style: '@project/assets/icons/trip/iconfont.less',
                        subset: 'auto',
                    },
                ],
            },
        },
    },
});

targets.web 只影响 web 编译,targets.wechatMp 只影响小程序编译。

在 namespace 中配置

某些后台 namespace 自己需要一套图标库时,可以写在:

web/src/app/namespaces/<namespace>/index.config.ts

示例:

import { CreateNamespaceConfig } from '@oak-frontend-base/config';

export default CreateNamespaceConfig({
    iconLibraries: [
        {
            name: 'antd',
            type: 'react',
            packageName: '@ant-design/icons',
            platforms: ['web'],
        },
    ],
    menu: {
        groups: [
            {
                name: 'HotelOps',
                icon: 'antd:ShopOutlined',
                order: 1,
            },
        ],
    },
});

namespace config 里的 iconLibraries 会参与 web 初始化。菜单仍然只写字符串。

在页面菜单中使用

页面菜单项通常写在页面旁边的 index.config.ts

import { CreatePageConfig } from '@oak-frontend-base/config';

export default CreatePageConfig({
    menu: {
        name: 'hotelArchive',
        icon: 'antd:ShopOutlined',
        group: 'HotelOps',
        order: 1,
    },
});

菜单渲染组件只需要把这个字符串交给 OakIcon

<OakIcon name={icon || ''} size={18} />

不要在菜单数据里提前构造 React 组件。菜单数据应该保持平台无关和可序列化。

Web React 图标按需加载

React 图标库有两种接法。

1. 手工注册已导入组件

如果只用几个图标,可以在 web 初始化时显式导入:

import {
    SettingOutlined,
    ShopOutlined,
} from '@ant-design/icons';

initialize(features, appName, routers, {
    namespaceConfigs,
    iconLibraries: [
        {
            name: 'antd',
            type: 'react',
            platforms: ['web'],
            icons: {
                SettingOutlined,
                ShopOutlined,
            },
        },
    ],
});

这种方式最直接,但要避免:

import * as Icons from '@ant-design/icons';

全量导入图标库会明显增加入口包体积。

2. 让 oak-cli 生成按需 loader

更推荐在 oak.config.ts 里声明 packageName

{
    name: 'antd',
    type: 'react',
    packageName: '@ant-design/icons',
    platforms: ['web'],
}

oak-cli 会扫描静态图标字符串,例如:

icon: 'antd:ShopOutlined'

然后生成类似下面的动态 import:

import('@ant-design/icons/ShopOutlined')

默认 import 规则是:

{packageName}/{iconName}

如果图标包路径不同,可以配置 importPath

{
    name: 'custom',
    type: 'react',
    packageName: '@scope/icons',
    importPath: '{packageName}/icons/{iconName}',
    platforms: ['web'],
}

这个机制是编译期静态扫描,不是运行时任意字符串 import。动态拼接图标名不保证有对应 loader:

// 不推荐:编译器无法稳定收集所有图标
icon: `antd:${name}`

小程序 font 图标裁剪

小程序只能处理 oak:*type: 'font' 图标库,不处理 type: 'react'

小程序编译时,oak-cli 会把 font 图标样式合并进:

components/icon/index.wxss

同时它会扫描当前小程序入口图里的页面和组件,收集静态使用的图标名,例如:

<oak-icon name="trip:hotel" />

或者:

<OakIcon name="trip:hotel" />

subset 控制裁剪策略:

  • subset: 'auto':推荐。静态可判断时只保留用到的 glyph;发现动态图标名时回退整库并打印 warning;
  • subset: true:强制按静态扫描结果裁剪;
  • subset: false:保留整库 CSS 和字体。

如果图标名有动态拼接,优先用 subset: 'auto'subset: false,避免小程序运行时才用到的 glyph 被裁掉。

常见排错

图标不显示时,先按下面顺序检查。

通用检查

  • 图标名是否是静态字符串;
  • 图标名前缀是否和图标库 name 一致;
  • oak.config.ts、namespace config 修改后是否重启了 dev server;
  • 页面、菜单、namespace 配置里是否误写了 ReactNode;
  • fontUrl 是否同时配了 icons 映射。

Web 图标不显示

  • type: 'react' 的依赖包是否已安装,例如 @ant-design/icons
  • packageName 对应的单图标入口是否真实存在;
  • 单图标入口不是 {packageName}/{iconName} 时,是否配置了 importPath
  • 手工注册时是否只导入了用到的图标组件;
  • type: 'font' 时,web 端是否能拿到对应样式或编译生成的 styleText

小程序图标不显示

  • 小程序是否误用了 antd:* 这类 React 图标;
  • style 指向的 iconfont 样式文件是否能被 oak-cli 解析;
  • fontUrl 指向的字体文件是否存在;
  • 动态图标名是否被 subset: true 裁掉;
  • 构建日志中是否出现 [oak-vite-mp-icon] warning。

当前限制

  • type: 'react' 只支持 web;
  • type: 'image' 目前只是类型预留,还不是完整的跨端图标资源方案;
  • type: 'react' + packageName 依赖编译期静态扫描,动态拼接图标名不会自动生成任意 import;
  • 小程序图标裁剪只扫描当前小程序入口图相关文件,不会无条件扫描项目里所有源码。