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

定义feature

feature 是 Oak 前端运行时中一个非常重要、但又很容易被忽视的概念。

简单地说,feature 就是被多个组件共享的前端状态与服务对象。它通常用来封装:

  • 某类前端状态;
  • 一组对 cache / localStorage / token / aspect 的组合调用;
  • 某种可跨页面复用的业务工具能力。

如果说 aspect 更像“后端业务服务入口”,那么 feature 更像“前端业务服务对象”。

feature 不是业务一致性层

首先要建立一个很重要的边界:

feature 只运行在前端,不承担系统底层一致性职责。

这意味着:

  • 需要保证正确性的约束,仍然应该写在 checker / trigger / 权限体系里;
  • feature 负责的是前端组织、状态复用和调用便利性。

Oak 自带的基础 features

oak-frontend-base/src/initialize.ts 负责创建基础 features。当前 Oak 生成的 src/initialize.server.ts 再按依赖组合结果合入依赖 features,最后调用项目 src/features/index.tscreate(...) 并合入项目 features。旧的 initialize.frontend.ts 只用于历史 DebugConnector 场景。

这些基础 features 里,最重要的包括:

  • cache
  • runningTree
  • localStorage
  • locales
  • message
  • notification
  • navigator
  • port
  • location
  • environment
  • style
  • theme
  • geo
  • contextMenuFactory
  • subscriber
  • socket

因此,很多业务 feature 实际上就是在这些基础能力之上再做一层更贴近业务语义的封装。

自定义 feature 写在哪里

通常有两个关键文件:

  • src/features/index.ts
  • src/initializeFeatures.ts

src/features/index.ts

这里负责创建 feature 实例。当前 oak-general-business/template/featuresIndex.ts 给出的项目模板是:

export function create(features: BasicFeatures<EntityDict> & Ogb0FeatureDict<EntityDict>) {
    const { cache, localStorage, token } = features;
    const sample = new Sample(cache);
    const console = new Console(cache, localStorage, token, () => ({
        id: 1,
        name: 1,
    }));
    const aspect = createService<EntityDict, MergeAspectDict>(cache);
    return {
        sample,
        console,
        aspect: aspect as typeof aspect & Feature,
    };
}

从这个例子中可以看到,自定义 feature 的常见依赖来源主要是:

  • cache
  • localStorage
  • token
  • 依赖模块提供的 feature

src/initializeFeatures.ts

这里负责初始化 feature。当前 general-business 项目模板中会初始化依赖 feature,并加载项目 locale:

export default async function initialize(features: FeatureDict & BasicFeatures<EntityDict> & Ogb0FeatureDict<EntityDict>) {
    await initializeOgb0Features(features, accessConfiguration, undefined, [Qiniu]);
    features.locales.loadServerData(['projectName-l-common', 'projectName-l-error', 'projectName-l-menu']);
}

这个文件的职责通常包括:

  • 调用依赖模块的 initialize(...)
  • 注册 selection / operation rewriter;
  • 加载服务端 i18n 数据;
  • 注册文件存储、SDK、全局配置等前端启动逻辑。

什么时候应该抽一个 feature

下面这些情况,通常值得单独抽 feature:

  • 一段逻辑会被多个页面复用;
  • 这段逻辑依赖多个基础 feature 组合调用;
  • 这段逻辑有自己的状态,需要跨组件共享;
  • 你不希望把复杂调用链直接堆在组件里。

例如 oak-general-business/src/features/index.ts 中,就把 tokenapplicationextraFilewechatSdkhumanVerifyinvite 等能力封装成了 feature。theme 则是 oak-frontend-base 的基础 feature,不属于 general-business。组件层只需要使用合并后的 feature 字典,不需要知道内部具体如何操作 cachelocalStorage

在组件里如何使用 feature

OakComponent 本身就支持声明依赖的 features,并且组件实例上可以通过 this.features.xxx 访问。

同时,在 formData 中也可以拿到:

formData({ data, features }) {
    return {
        rows: data,
        currentUserId: features.token.getCurrentUserId(),
    };
}

因此,feature 是 Oak 组件和运行时能力之间最自然的桥梁。

一个很实用的经验

当你发现一个组件里开始出现下面这种代码味道时:

  • 同时操作 cachelocalStoragetoken
  • 同样一段查询和转换在多个页面里重复;
  • 一大段“为了页面方便”而写的业务工具函数堆在组件文件里;

这通常就意味着,你应该把它抽成一个 feature 了。

feature 和 aspect 的关系

二者最常见的搭配方式是:

  • aspect 负责后端业务入口;
  • feature 负责前端如何组织和调用这些入口。

这是一种非常自然的分层:

  • 后端复杂动作写成 aspect;
  • 前端再把这些 aspect 包装成更顺手的业务对象。

这样组件代码会轻很多,也更容易维护。