# Cod 起源和用武之地

Cod [kɒd] 是一套动态 tr 解决方案，包括 [**Egg**](https://eggjs.org/zh-cn/)\*\* 插件**和**服务配置平台\*\*两部分。通过 cod ，可以直接在前端消费 tr 服务([sofa-rpc)](https://www.yuque.com/egg/nodejs/dklip5)，增加、修改接口不需要修改和发布代码。

本文将从 cod 的诞生开始，到 cod 动态 tr 实现原理，详细介绍 cod 诞生的过程，cod 方案可以用于解决什么问题，以及 cod 的未来。

## 起源

有一天和同事们讨论一个技术方案，有人偶然提及 common.invokeTr ，说这个技术方案用的非常多，很好用。我乍一听，感觉这个 common.invokeTr 有点黑科技。

common.invokeTr 做的事情就是转发 tr 接口，在 BFF 中配置白名单，然后就可以直接在前端请求 tr 接口了。做的事情并不复杂，但这个想法我觉得非常有意思。这给了我一个新的思路，一直以来，我都在尝试[引入 TypeScript](https://www.yuque.com/eward/blog/wvu1hw) ，通过 [tr-snapshot](https://www.yuque.com/eward/blog/iikc68) 简化测试用例开发成本。

这些技术方案在复杂应用（全站 node）场景下有用。但 BFF 所做的事情，大都只是请求转发、简单数据处理，BFF 本身就是一个转发层。common.invokeTr 这种方式，是**从减少代码量的角度来提高开发效率的**。如何让代码写的更快，写的更好维护，都没有减少代码开发量来的更有效。

当时，我隐隐约约感觉到，**BFF 代码可以通过某种方式进行抽象，大部分代码是可以简化，是不需要开发的**。

后面有很长一段时间，我开始关注这个问题，开始思考如何减少 BFF 代码量。从公司内部来看，还是有挺多人思考，并且在这方面尝试突破了。

最初，我想到的是和 [graphql](https://graphql.org/) 结合，经过一段时间开发，然后 model-bus 诞生了。

## model-bus

model-bus 设计了一套类似于 graphql 的查询语法，相对于 graphql 更加简单，只支持单层数据结构查询，我称之为 [simpleql](https://github.com/shepherdwind/simpleql) 。

之所以要重新设计一套查询语句，而不是直接使用 graphql ，是因为 graphql 必须首先定义模型结构，这导致接入成本非常搞。而我们的 BFF 应用，数据都是来源于外部服务，我们并不需要关注模型的结构。另外，graphql 只支持数据结构描述，如果要描述一些逻辑、依赖关系就无能为力的。因此，我重新设计了一套语法，这样方便扩展，更适合 BFF 技术体系。

### simpleql

简单来说，simpleql 的解析过程，就是遍历查询语句字符串，遇到关键字，进入到下一个状态，并且把上一个状态数据记录下来。字符串遍历一遍之后，就可以得到语法树结构了。

一般情况，大家一听到说要写一套新的语法，就会比较担心其中可能存在性能问题。实际上，**simpleql 语法解析的复杂度是 O(n) ，只需要遍历一遍字符串就够了**。

比如一条简单的查询语句 `foo: Fengdie(insmutual_clause) { a, b}`  解析，通过状态图可以了解具体过程。

 ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS9iNDA3NjBkYy05M2NmLTRlOTctOTFhNC01MmFiNTE3NGE3YWMvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzODQwMCwiZXhwIjoxNzkwNTI0ODAwfQ.SvA1rMeSJLi2tS9jydvs6ZXFdMOuCZOxoSh467znBXk) 这个图表示一个模型语法的描述，可能的路径也是可以枚举的，大致分为一下四种情况

```graphql
foo: Fengdie // 1 -> 7
foo: Fengdie(insmutual_clause) // 1 -> 2 -> 6
foo: Fengdie { a, b, c: originField } // 1 -> 4 -> 5
foo: Fengdie(insmutual_clause) { a, b, c: originField } // 1 -> 2 -> 3 -> 5
```

simpleql 的解析过程，也是遵循上面状态机来解析的，有兴趣的可以看看[源码](https://github.com/shepherdwind/simpleql)。

### 模型调度

simpleql 是查询语句，只是用来描述数据模型以及数据之间的关系。最终，需要调用 BFF 服务，来完成数据查询，最终给输出数据，这个过程称之为模型调用。

我们可以把 simpleql 描述的查询语句，看成一系列任务。每一个 model ，都需要找到对应的模型处理器，模型处理器来负责真正的模型查询。

首先，在 model-bus 中，暴露出一个方法 `defineModel` ，通过这个方法来统一注册模型。举个简单的例子

```typescript
import { defineModel } from '@alipay/model-bus';

export default class InsiopService extends Service {
  @defineModel(DataType.InsIopData, 'scene')
  async getInsIopData(query: { userId?: string; source: string, scene: string }) {
    const { moduleList, scm } = await this.getPageRenderData(params);
    return { scm, moduleList };
  }
}
```

然后需要进行真正的调用了，一开始对查询语句解析，得到多个执行任务，这一批任务会并发查询。查询结束后，统一返回。

有时候，我们会有模型之间相互依赖，这种情况，第一次任务执行，在查找依赖数据的时候，会直接 throw 一个特定的异常，在模型调度的过程中，捕获这些异常，然后把这部分有依赖的任务放入到一个临时的数组中。等待本次任务完成，任务还没有全部完成，会自动执行上次等待中的任务，一直到所有任务完成，最终返回数据。说起来有些枯燥，画个流程图说明一下

 ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS83MTc1YTJmYy03ZjdlLTQ0ZDgtYTkzNi01NGRlZjZhN2I1ZTAvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzODQwMCwiZXhwIjoxNzkwNTI0ODAwfQ.A62jZWDMAOOeXPA4V8LSwvKCf4kBVb_i-ORa1Kvg9FE)

### 问题

model-bus 完成了第一步，把 BFF 面向页面开发，转向为面向模型开发，这样模型的复用率会更好一些。搞定之后，开始在 insxhbbff 中尝试，把几个页面接口简化了。

但仔细思考后，发现这个方案解决的查询聚合的问题，在一些中后台应用中真正需要的是，**在不发布应用的情况下，直接转发 tr 接口，这种情况下，BFF 这一层是多余的**。

仔细想想，目前 model-bus 本身并没有比 common.invokeTr 多少提升，就多了支持聚合查询能力。聚合查询有用的前提是模型复用度足够高，实际情况可复用的模型并不多。

然后问题变成了如何实现动态 tr ，对这个问题进一步探索，最终诞生了 cod 。

## Cod

### 动态 tr

cod 最核心需要解决的问题就是 tr 请求动态化。也就是在不发布 BFF 应用的情况，能够请求到 Java 应用提供的 tr 服务。

一开始，我的大致思路是，动态配置加上固定的插件代码，替代 [jar2proxy](https://github.com/eggjs/jar2proxy) 生成的 js 代码，实现调用 tr 服务。那样，新增 tr 服务只需依赖配置下发，没有代码的修改，就不需要发布应用了。

一开始觉得可能会很复杂，尝试一下之后发现，实现动态 tr 请求还是很简单的。我写了一个最简单的 demo

```javascript
const rpcClient = app.trClient || app.hsfClient;
consumer = rpcClient.createConsumer({
  id: 'com.alipay.insxhbprod.common.service.facade.BrandFacade:1.0',
  appname: 'insxhbprod',
  targetAppName: 'insxhbprod',
  proxyName: 'BrandFacade',
});

const args = [
  {
    \$class: 'com.alipay.insxhbprod.common.service.facade.model.request.BrandStatisticsQueryRequest',
    \$: { userId: '2088302539975403' },
  },
];
consumer.invoke('queryBrandStatisticsInfo', args, { ctx: this.ctx });
```

从上面的例子可以看出，只需要把 jar 包解析，拿到服务 id 和入参数据结构，然后动态发布到 BFF 应用服务器上，就可以实现动态 tr 了。

于是，我们还缺少一个平台来做服务结构配置下发。然后我们成立了一个小组，开始搞起来。

### 命名

一个产品的诞生，从有名字开始。

想了很久，我们觉得给这个平台命名 Cod [kɒd] 。cod 作为名词是鳕鱼的意思，同时 cod 和 code 比较接近，只少了一个 e ，寓意 code less 。

基于以上寓意，我找到外援(我老婆)帮设计了一个好看的 logo ：

 ![logo.png](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS8zZThmZDVjYy05YTE5LTRkNjMtOThjZS02ZjE4MDVjZGY0Y2EvaW1hZ2UucG5nIiwidHlwZSI6ImF0dGFjaG1lbnQiLCJpYXQiOjE3OTA0Mzg0MDAsImV4cCI6MTc5MDUyNDgwMH0.P8KVmr8F2-uqbhRNX43JHQS40Mx8JmmdKyq5d15aieI " =160x180")

再经过大家一两个月的密集开发，最终，我们完成了 cod 平台管理站点 。

### 实现原理

cod 平台主要模型包括 应用-> Jar 包 -> 服务 三层。

 ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS8xZjM4MTk4Yi0yNGU5LTQwZWItOTFmYi1hMzc1NzFjZDQxY2UvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzODQwMCwiZXhwIjoxNzkwNTI0ODAwfQ.GQeEk_51ApYD-zp0xV8cFxzvHbAL9zXnFWWEeNHnJqM) 其中 Jar 包维度上功能最复杂，最核心的配置发布流程，和 jar 解析、服务新增。jar 包解析过程，基于 jar2proxy ，然后重新构建成 cod 需要的数据结构。

发布过程，基于 basement 的 filesync 服务，直接把配置文件发布到 oss 。BFF 端，通过内置的 fengdie-pull 插件，拉取配置数据，最终 tr 调用在 model-bus 插件中执行，配置数据从 fengdie 区块读取。整体实现原理如下图所示

 ![image.png](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS80ZTM4MTFjMi0xNDkxLTQ3OTgtYjM3Ny0xZDVkMmUxMjQyZTQvaW1hZ2UucG5nIiwidHlwZSI6ImF0dGFjaG1lbnQiLCJpYXQiOjE3OTA0Mzg0MDAsImV4cCI6MTc5MDUyNDgwMH0.Wn9s8vHBaXhHmMvOsOShtsqQPY2eKfMi88qPbUfvWvo " =560x332")

最终通过 cod 平台加上 model-bus 组合，实现了动态 tr 能力。tr 接口发布从原来的代码开发流程，转变为区块配置发布流程。

### 效率

tr 接口转发，这样一个场景，我们真正做到了高效。下面新增 facade 服务两者开发流程对比图

 ![image.png](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS82OTZlZmVhMS01NGI1LTRhODQtYjIzMC04YTM5ZmRjMWE3NTgvaW1hZ2UucG5nIiwidHlwZSI6ImF0dGFjaG1lbnQiLCJpYXQiOjE3OTA0Mzg0MDAsImV4cCI6MTc5MDUyNDgwMH0.Kh5273usJ6vQ25ey9v7KaGFcxB5_hYb3eIP3UYx1EDk)

## 未来

现在我们的整套方案，包括两部分能力

* cod 实现动态 tr ，方便快捷支持 tr 接口转发类场景
* model-bus 支持模型聚合查询

我们还打算做一个本地 sdk 工具，通过这个命令行工具，可以生成调用代码和接口类型，这样可以进一步简化开发者使用成本。整体架构图如下  ![image.png](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS81MGI5YTNmOS04ZjgxLTQwZGItYmEzNS1kZTVhZmQ2Y2M4MjgvaW1hZ2UucG5nIiwidHlwZSI6ImF0dGFjaG1lbnQiLCJpYXQiOjE3OTA0Mzg0MDAsImV4cCI6MTc5MDUyNDgwMH0.zpDyZ4PlewVnkjyQy4bkoZysfrA7RpjjCwdcybvlAe4 " =480x313")

总体 cod 方案，解决了动态 tr 能力。并且在此之上，会尝试覆盖更多的业务场景，把更多简单的 controller 层代码转换为配置。一言蔽之就是 **To write code less and get more things done** 。

### 处理器

通过处理器，我们可以对返回数据进行一定的处理。处理器的存在，最初是为了解决类似下面场景

```javascript
if (contractNo) {
  const c = await this.contractService.detail({
    source,
    contractNo,
  });

  this.ctx.body = {
    ...this._cookContract(c),
    clauseList: this.app.fengdie.insmutual_clause.cancer,
  };
} else {
  const contractList = await this.contractService.list({
    source,
    brandCode,
  });

  const toRenewalContract = contractList.filter(c =>
    c.status === 'JOINED' &&
    c.autoRenewStatus === 'WAIT_OPEN' &&
    c.supportAutoRenew,
  )
  .sort((a, b) => moment(a.terminateTime).valueOf() - moment(b.terminateTime).valueOf())[0];

  this.ctx.body = {
    ...this._cookContract(toRenewalContract),
    clauseList: this.app.fengdie.insmutual_clause.cancer,
  };
}
```

这种情况，实际上是调用 tr 查询合约列表，然后对数据进行一定的处理，这个数据处理，可以通过处理器来描述

```javascript
{
  simpleQuery: `
    \${ contractNo ? 
      'contract: Contract(contractNo: \$contractNo)' :
      'contract: ContractList | filter(\$filter) | sort(\$sort) | get(0)'
    },
    clauseList: Fengdie(insmutual_clause) | get(cancer),
  `,
  params: {
    filter: `item.status === "JOINED" 
                        && item.autoRenewStatus === "WAIT_OPEN" 
                        && item.supportAutoRenew`,
    sort: 'a.terminateTime > b.terminateTime ? 1 : -1'
  }
}
```

这个里面使用了 model-bus 内置的几个处理器 filter 、sort 和 get 。处理器的参数是简单 js 表达式，通过 [simple-evaluate](https://github.com/shepherdwind/simple-evaluate) 解析，是绝对安全的表达式，所以可以直接在前端写查询语法。simple-evaluate 的解析过程和 simpleql 类似，可以参考[这篇文章](https://www.yuque.com/eward/blog/bgc0gx)。

有了处理器，model-bus 能够做的事情还是能够强大很多，另外，我们未来我们还打算开发自定义处理器，让开发者在 BFF 中写个性化的处理器。当然，新增处理器，是需要发布应用的。目前我们遵循的原则是，**Cod 只动态下发配置，如果需要修改代码，就应该走发布流程。** \*\*

### 复杂场景

实际上，model-bus 提供一些简单的语法描述能力，但可以组合成非常复杂查询逻辑。我们也在不断尝试在更多的场景中应用。下面有一个相对复杂的案例（代码不展示了）

分为三个步骤

* 并发两个 tr 查询 isSamePerson 和 hasInsUser 数据
* 如果数据 ok ，继续查询 userNo 数据
* 把上一个 userNo 数据作为下一个函数的参数，继续查最后一个 tr

这个通过 simpleql 语法可以这样描

```javascript
{
  simpleQuery: `
    isSamePerson: Cod(method: IdentityService.isSamePerson) | get(model),
    hasInsUser: Cod(method: InsUserQueryFacade.getInsUser) | get(model),
    userNo: Cod(\$if: "isSamePerson && hasInsUser", method: InsUserFacade.addUser) | get(data)
    _attachment: Empty(\$\$userNo) | assign(\$attachQuery) 
    attachment: Cod(method: insUserAuthFacade.uploadCertificateAttachment, params: \$\$_attachment)
  `,
  params: {
    attachQuery: {
      certType: CertTypeEnum.IDENTITY_CARD,
      sourceEvent: SourceEventEnum.QUERY,
      certUse: CertUseEnum.XHB,
      bizData: { policyNo },
    },
  },
}
```

这样场景，实现是没有问题的，但还是会有一些比较麻烦之处

* 在前端要写太多 tr 入参数据，需要前端理解太多的服务端逻辑
* 这样的语法，理解或者解释成本都是有一些高的
* 没法写测试用例，稳定性不那么可靠

解决的方案，可能是把这一些语法配置放在 Cod 平台上，或者允许开发者在 Cod 平台上拖拽加配置的可视化操作，完成这种复杂场景的配置。

未来，我们会首先在 model-bus 中实现更加复杂场景支持能力，经过足够的实践经验，合适的情况，会把这些配置能力直接做到 Cod 平台上，这就是所谓的服务编排，目前我们还是专注于代码(simpleql)编排。

---

**Documents**

- [基于实践谈谈软件架构设计](https://iling.fun/s/blog/doc/5z65lqo5a6e6le16lci6lci6l2v5lu25p625p6e6k66k6h-tsGrQnNSNu)
- [rspack 使用体验过程](https://iling.fun/s/blog/doc/rspack-u8S4Mtjkfz)
- [E2E test with Playwright](https://iling.fun/s/blog/doc/e2e-test-with-playwright-JLFKAvWQ0U)
- [Vim everywhere](https://iling.fun/s/blog/doc/vim-everywhere-v6WZyLzGA1)
- [滴答 FaaS 研发平台小结](https://iling.fun/s/blog/doc/faas-XQxhEO3e5r)
- [Cod 服务编排之路](https://iling.fun/s/blog/doc/cod-0CFsOfiX20)
- [Cod 起源和用武之地](https://iling.fun/s/blog/doc/cod-1aAA2rVJZY)
- [2019 年度总结](https://iling.fun/s/blog/doc/2019-RhbTD2SiCn)
- [凤蝶区块历史及其问题排查记录](https://iling.fun/s/blog/doc/5yek6j225yy65z2x5y6g5yy5yk5yw26zeu6aky5o6s5pl6k6w5b2v-WoaaJ9QHpq)
- [如何用单元测试](https://iling.fun/s/blog/doc/5aac5l2v55so5y2v5ywd5rwl6kv-3zwG1cJvTn)
- [小钱袋 Node 应用开发经验总结](https://iling.fun/s/blog/doc/node-QpFfYCuoE0)
- [简单 JS 算数表达式求值算法的实现探索](https://iling.fun/s/blog/doc/js-FXcWkpW2vw)
- [凤蝶可视化编辑实现原理](https://iling.fun/s/blog/doc/5yek6j225yv6keg5yyw57yw6l6r5a6e546w5y6f55cg-ZsBj0CS1ev)
- [凤蝶服务端渲染经验总结二](https://iling.fun/s/blog/doc/5yek6j225pyn5yqh56uv5riy5pt57up6aqm5oc757ut5lqm-a4Kd59d7Td)
- [凤蝶服务端渲染经验总结](https://iling.fun/s/blog/doc/5yek6j225pyn5yqh56uv5riy5pt57up6aqm5oc757ut-GBhTESymbn)
- [Node.js 中 sourcemap 使用问题总结](https://iling.fun/s/blog/doc/nodejs-sourcemap-duex3wDhWt)
- [如何实现一个 mvvm 组件](https://iling.fun/s/blog/doc/mvvm-8TXAVSXe77)
- [webpack 热加载原理探索](https://iling.fun/s/blog/doc/webpack-7Wjn9z1fUB)
- [再谈 vim](https://iling.fun/s/blog/doc/vim-UBf5u57ko7)
- [How to realize velocity template interpreters](https://iling.fun/s/blog/doc/how-to-realize-velocity-template-interpreters-4s1KEznjYR)
- [joycss](https://iling.fun/s/blog/doc/joycss-bGvBvDGF7k)