V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
wenerme
V2EX  ›  程序员

多年 CRM 开发的心路历程: Java -> Golang -> NodeJS

  •  
  •   wenerme ·
    wenerme · 97 天前 · 3708 次点击
    这是一个创建于 97 天前的主题,其中的信息可能已经有所发展或是发生改变。

    v1

    • 后端 - Java - 2019.11 - 2021.01
    • 前端 - NextJS v9 + Antd - 2019.11 - 2021.3
      • @reduxjs/toolkit

    :::tip 总结

    非常简单的尝试,很快就失败了,目标并不明确。 对 CRM 并不了解,过于盲目。 堆砌了一些基础前端组件。前端开发能力尚不成熟。

    :::

    v2

    • 2020.01 - 2020.06
    • 后端 - Node - nestjs
      • nestjs
      • objection - ORM
      • swagger
    • 前端 - Antd v4+NextJS v9
      • react-final-form
      • redux
      • tinacms

    :::tip 总结

    后端

    后端尝试构建基于 Schema 的 CRM ,但过于动态,过于灵活导致逻辑开发复杂。 对 NodeJS 后端开发并不足够了解,目标更像是一个 low-code 后端,但是支持 CRM 实体。 这个阶段对 CRM 逻辑有了一些了解。

    • 需要的是 先 CRM 再 low-code
    • 核心逻辑是确定的才能更好开发业务 - 不然要做太多假设
    • NodeJS 容器真的很大
    • 确定了要走 Schema 的方式 - 前端通过 Schema 做更多的事情

    前端

    前端选择 Antd ,发现阻力越来越大,实现定制化很难。 这时的前端开发能力还相对欠缺,对生态还不够了解。

    • 探索了前端想要达成的目标 - 窗口、多 Tab
    • 不要选择 antd 这种过于全面无法定制的组件库

    :::

    v3

    • 后端 - Go - 2020.11 - 2022.07
      • gorm + restful - 前期
      • ent + gqlgen - 中期
      • gorm - 非核心 CRM 业务
    • 前端 - 2020.11 - 2022.1
      • NextJS
      • BlueprintJS - 前期
      • graphql-codegen
      • urql - GraphQL
      • TailwindCSS - 后期
      • zustand - 后期
      • react-hook-form

    :::tip 总结

    这个阶段是历时最长的,除了核心的 CRM 还完成了其他的一些附属模块。

    后端

    后端使用 Golang 开发上进行了一些探索,前期 gorm+restful 方式代码量大且重复。 之后选择 ent+gqlgen+自定义生成代码。

    • GraphQL 对前端非常友好 - 但选择了先 GraphQL 而非先 GRPC 是比较失误的
    • 基于 ent 实现自定义生成逻辑
      • 生成了 schema - 但非标准 jsonschema - 且长期未从后端拉而是直接放在了前端
        • 改动困难
        • 自定义 schema 前期用起来爽,后期维护困难 - 记忆层面和熟悉度层面
      • 生成了 graphql 的 golang resolver 代码 - 通过解析 AST 的方式植入
        • 生成逻辑过于依赖 ent ,改动维护困难
        • 生成 golang resolver 逻辑维护困难
    • ent 生成过多的东西 - 代码库冗余
    • ent 升级有风险
      • 生成逻辑不兼容 - 主要问题 - 不敢随意升级 - 开发阻碍
      • 自身不兼容
    • 确定了要先 GRPC 的方式开发
    • 基于多 DB 后端的多租户逻辑不好维护
    • 优先设计的 On-Prime 模式,未考虑 SaaS ,后期尝到了苦头

    前端

    前期基于 Blueprint 快速实现大多功能,但还是因为经验不足,很多东西实现有缺陷。但实现了初期原型,达到了想要的结果。

    • Blueprint 提供 CSS 直接使用,非常方便 - 侵入性远远小于 Antd
    • Blueprint 升级需要改动所有的 CSS 前缀 - ⚠️
    • 后期选择 Tailwind 开发
    • 组件传递、自定义经验欠缺 - 做了很多不好维护的扩展
    • 基础 Schema 不标准 - 很多东西基于不标准的 schema 开发后结果是往更不好的方向发展
    • 优先考虑了用户使用而非管理使用,导致所有管理维护开通都需要人为 - 不可持续
    • 期望中的模块化一直未实现

    :::

    v4

    • 2022.06 -> 2022.11
    • 后端
      • 确定了要优先 GRPC ,且使用标准 Schema 。
        • 因此将 protobuf 的定义作为 Single-Source—Of—Truth Schema 。
        • 分离接口 Schema 和 DB - 之前是 ent schema -> graphql schema
      • 虽然选择 GRPC 但也要直接能给前端访问
        • 选择了 connect 协议
        • 实现了 connect-gateway
          • 实现了基于 Redis 服务发现
          • 实现了基于 grpc reflection 自动暴露服务
      • DB 确定不要通过 ORM 自动维护而是手动维护
        • 实现基于 PG RLS 的多租户逻辑而非多 DB 模式
        • 实现 CRM 时 DB Schema 的重要性等同接口
        • 业务沉淀为数据库模型
        • 也因此分离 API 模型和数据库模型
      • Golang -> NodeJS
        • 初期尝试用 Golang 实现
        • 核心问题 ⚠️
          • 开发一段 前端再回到 Golang 会 痛不欲生 - 上下文切换、开发思路切换
          • Golang 生成代码只能静态生成,NodeJS 可以动态生成
            • NodeJS 更加灵活 - PoC 阶段更有优势
          • 使用 NodeJS 可以和前端在同一个仓库 - 核心逻辑开发维护更容易
      • NodeJS
        • fastify+jsonschema+sequelize
        • 通过 grpc 生成 descriptor, definitoon
        • 启动后通过 descriptor 生成 jsonschema
        • 通过 jsonschema+definitoon 生成 grpc-service - nice-grpc
        • 实现标准的 CRUD 语义
        • fastify POST -> grpc 实现 - 类似 connect 语义
        • grpc-server -> grpc 实现
        • 未来: web -> connect-gateway -> grpc-server -> grpc 实现
    • 前端
      • daisyui+tailwindcss - 放弃 Blueprint
      • 不再直接选择成熟的 UI 框架,而是选择基于 CSS 的样式库
      • daisyui 提供了一套命名 class 的方式 - 有 Theme
      • 确定 管理功能优先于用户功能
      • 确定 功能能被看到才算开发完成
      • 模块化
        • 仍在尝试中 - 借鉴其他实现
        • 提供开放的 OSS 存储
        • 目前明确方向
          • 分离 os 逻辑和核心 route 逻辑 - 揉在一起困难

    v5

    • 2022.11 -> 2023.03
    • 方式的调整
      • 不只是独立于业务单纯的思考怎么做 CRM - 没有任何意义/产出的东西不可用
      • 业务层面将以前的思路以 DB Schema 的方式沉淀
      • 贴合现实做一些实际的事情
    • 后端
      • RPC over NATS
        • 简单易用
        • 能实现多租户
      • 自定义 RPC
        • 弱 Schema - 一个人维护不过来
      • 重新定义分层
        • DB -> Entity -> EntityService -> RemoteService -> Controller, tRPC, GraphQL
        • 层次更多,但是更清晰
        • 改动相对独立,互不影响
      • 依然全面的 NodeJS/TS/NextJS/NestJS
      • 尝试更加轻量的服务 - HonoJS
      • 尝试更加轻量的前端 - Vite
        • 不能 SSR/SSG/Server Action
        • 添加多个页面相对麻烦
      • 尝试 Typescript 生成 zod/typebox - Single-Source-Of-Truth
    • 前端
      • 大部分页面直接使用了 v4 的内容 - 逻辑结构上有一定调整
      • 证明 daisyui+tailwindcss 的移植性的确很好
      • 尝试引入了 shadcn
      • 核心功能能够做到模块化
      • 尝试重新恢复多窗口

    v6


    27 条回复    2024-09-24 17:34:57 +08:00
    raphaelsoul
        1
    raphaelsoul  
       97 天前   ❤️ 2
    很好的分享 技术栈和时间都和你差不多。
    但我这几年下来的感想是 不能再花太多精力在注重技术栈和翻来覆去的重构重写了。
    产品优先 出活优先
    cuijiajun
        2
    cuijiajun  
       97 天前
    学到了
    qsnow6
        3
    qsnow6  
       97 天前   ❤️ 1
    行业铁律“过早优化是原罪”,技术是为产品服务的,技术层面花里胡哨产品却不好好设计,最终就是被市场淘汰。
    Leviathann
        4
    Leviathann  
       97 天前
    js/ts 的生态确实是现在开发 web app 的最优解
    S4msara
        5
    S4msara  
       97 天前
    非常棒的分享,充分的说明了“技术以业务导向,技术为业务赋能”👍
    zzdgfv
        6
    zzdgfv  
       97 天前
    强,涉及太多了
    june4
        7
    june4  
       97 天前
    我这几年也全用 typescript 前后端一把梭,统一语言优势太大了。何况 js/ts 语言本身写起来很舒服。
    唯一缺点内存占用比 rust/go 大一些,对小鸡要放一堆程序不友好。
    horizon
        8
    horizon  
       97 天前   ❤️ 1
    真能折腾啊。。怎么坚持下来的
    你的 CRM 有链接吗,可以试用一下吗?
    CRM 中有工作流、审批流之类的吗,怎么实现的
    C0dEr
        9
    C0dEr  
       97 天前
    首先佩服 OP 的坚持和能力,但能不能至少给个架构,功能说明啥的,方便理解先?
    sunchuo
        10
    sunchuo  
       96 天前
    为啥不用 PHP 呢。
    sunchuo
        11
    sunchuo  
       96 天前
    看下来感觉 op 是想后端尽可能少写代码,前后端尽可能少的重复劳动。所以一开始朝着 lowcode 方向走了。
    我做过几乎一样的事情。

    但是后来发现。真正的业务逻辑千奇百怪,很难做到「不写代码」,哪怕是有 lowcode 平台能实现这些复杂业务逻辑,那配置的过程就相当于「写代码」了。可能要做一些取舍。不能沉迷于全部自动实现。😂


    我的实现大概是这样:

    1. 定义数据结构,包括:字段校验;状态机;筛选、搜索字段、字段变更事件、字段监听事件、持久化方式等。
    2. 基于数据结构,自动生成齐备的 curd restful 接口;也可以自定义接口,请求响应的 schema (可引用数据结构)。
    3. 可以通过定义的接口直接生成 openapi3.1 的 schema ,进而生成文档。
    4. 自动实现路由、参数校验、权限校验等。
    5. 任意接口可以自己接管、实现更具体的业务逻辑。
    6. 可以自动生成前端请求接口的 sdk 。前端不用调试接口,直接用。
    7. 基于接口 schema ,生成描述表单、列表、详情的 jsonschema ,然后前端实现类似 react-form-schema 、amis 的渲染引擎;也实现了前后端不分离的,生成 html 的服务端渲染引擎。

    8. 具体的业务逻辑还是手写。在合适的地方引用列表、表单、详情的组件(引擎渲染 jsonschema + sdk 的数据)。
    wenerme
        12
    wenerme  
    OP
       96 天前
    @horizon 如果写代码不折腾,不有趣,就只剩下 996 了。
    这是前端部分公共的内容 https://github.com/wenerme/wode/tree/main/packages/console
    这是后端部分的公共内容 https://github.com/wenerme/wode/tree/main/packages/nestjs

    这是一个假的 demo https://wode.vercel.app/console fake 的账号密码 admin admin
    wenerme
        13
    wenerme  
    OP
       96 天前
    @sunchuo

    > 看下来感觉 op 是想后端尽可能少写代码,前后端尽可能少的重复劳动。所以一开始朝着 lowcode 方向走了。
    我做过几乎一样的事情。

    是这样的,但 lowcode 根本不可满足业务需求,只能通过大量的代码定义去减少重复的工作内容。

    例如

    ```ts
    export const LeadResource = defineResource({
    name: 'Lead',
    idType: 'lead',
    title: '线索',
    icon: <ActiveToggleIcon icon={BsTelephone} iconActive={BsTelephoneFill} />,
    metadata: {},
    });
    ```

    这样能通过扩展和维护这个 Resource 构建大多元素,例如

    ```ts
    defineMetadata(LeadResource, ResourceListViewSelectorMetaKey, {
    views: [
    {
    label: '开放线索',
    value: 'open',
    query: {
    filters: [`state = "${LeadStatusType.state.Open}"`],
    },
    },
    ],
    });
    ```


    不少内容和你的实现都有类似的地方,比较有意思, 只不过我大方向选择的 GQL 。

    但我会尽量避免生成,而是通过动态去创建,目前主要用到生成的是 ts 的 interface 生成 zod 、typebox ( jsonschema ,但是有类型)。

    > 但我会尽量避免生成,而是通过动态去创建

    主要是方便修改复用,生成时怕的是生成后改不动会形成包袱。动态构建例如

    ```ts
    export function createListPayload<T extends object>(Type: Constructor<T>): Constructor<PageResponse<T>> {
    let name = getObjectName(Type);
    let key = `${name}ListPayload`;
    return computeIfAbsent(getTypeCache(), key, () => {
    @ObjectType(key)
    class ListPayload {
    @Field((type) => Int)
    total!: number;
    @Field((type) => [Type])
    data!: T[];
    }

    return ListPayload;
    });
    }
    ```

    对查询方法也适用,可以按需增加查询方法,例如 https://github.com/wenerme/wode/blob/f846c2158ff83ad7fcde781abd29ef7505f11258/packages/nestjs/src/type-graphql/resource/withBaseQuery.ts#L11
    wenerme
        14
    wenerme  
    OP
       96 天前
    @C0dEr

    > 但能不能至少给个架构,功能说明啥的,方便理解先?

    我的仓库里大多都是笔记性质的 https://wener.me/story/how-i-note / https://www.v2ex.com/t/1058208#reply2
    我一般用笔记来索引这些信息。

    > 架构,功能

    一般我会以总结的方式形成类似 Design XXX 这样的,design 目录下有不少这样的内容,我一般主要参考学习别人现有的,然后总结沉淀自己的。

    https://wener.me/notes/dev/design/schema
    https://wener.me/notes/dev/design/erp
    https://wener.me/notes/dev/design/ao-factory
    wenerme
        15
    wenerme  
    OP
       96 天前   ❤️ 1
    @qsnow6 有道理,但是被市场淘汰的是企业、公司,而不是折腾技术的个人,不折腾技术的个人反而会被行业淘汰。如果有公费折腾的机会,就应该好好利用。
    wenerme
        16
    wenerme  
    OP
       96 天前
    @raphaelsoul

    > 产品优先 出活优先



    > 行业铁律“过早优化是原罪”,技术是为产品服务的,技术层面花里胡哨产品却不好好设计,最终就是被市场淘汰。

    这样的论调都是类似的,都是站在公司的角度,而不是个人的角度。我觉得两者是相辅相成的。
    horizon
        17
    horizon  
       96 天前
    @wenerme
    你这个仓库挺难看懂的。。。说实话
    就你发了这么多链接,我发现还是你现在这个帖子还能看懂
    demo 里啥也没有啊。。。
    ixixi
        18
    ixixi  
       96 天前
    我也写过 crm 你们卖的咋样
    mark2025
        19
    mark2025  
       96 天前
    确定了要先 GRPC 的方式开发
    ========
    为啥呢?
    wenerme
        20
    wenerme  
    OP
       96 天前
    @mark2025

    > 确定了要先 GRPC 的方式开发
    > ========
    > 为啥呢?

    因为当时很 buy in buf[1] 那一套, 其实现在也还是能接受,如果是需要 rpc/server to server ,我还是可能会考虑 grpc ,或者实现一个简单的 rpc ,但目前减少了 server to server 这一层,部分逻辑还是保留,目前以 gql 直接暴露给前端为主。


    [1]: https://github.com/bufbuild/buf
    wenerme
        21
    wenerme  
    OP
       96 天前
    @ixixi

    > 我也写过 crm 你们卖的咋样

    没有成行的产品,都是内部用,然后我这边是以研发为主,crm 在用,但同时这套逻辑做了另外两个简单系统的后台,都不是 crm 的。
    wenerme
        22
    wenerme  
    OP
       96 天前
    @horizon

    > 你这个仓库挺难看懂的。。。说实话

    是的,因为主要是面向自己的,想要 to public ,需要做的事情就太多了,只是分享一些想法和思路。
    mark2025
        23
    mark2025  
       96 天前
    @wenerme crm 一般复杂在业务逻辑(包括多表关联查询统计),连接数量和流量我觉得还不需要一定是 rpc 。http 请求开发、调试方便多了。
    yrj
        24
    yrj  
       95 天前
    敢于折腾,并把过程总结出来,值得佩服。
    xiaoshan5733
        25
    xiaoshan5733  
       95 天前
    佩服,思考总结值得学习。我也经常在技术选型上花费大量时间,有时候确实容易陷入技术思维。OP 在做技术选型时一般会考虑哪些因素呢?
    wenerme
        26
    wenerme  
    OP
       94 天前
    @xiaoshan5733

    > 在做技术选型时一般会考虑哪些因素呢?

    先做个方面了解,逐步筛选,锚定一些核心的,再围绕核心的技术去做选择。
    一般我们说单有了锤子,看什么都是钉子。
    然后再增加不同领域的锤子,逐步叠加就好。

    例如数据库,刚开始的时候,数据库都是 mysql ,后来自己能做决定了,开始去了解,了解到 pg ,发现真的很不错。然后需要做分析的时候,也了解了一些例如 clickhouse 之类的等等,但是最终要得到一个 **trade off**,每个技术方向都是有价值的,但是做技术选择是有自己的背景的,多积累这样的 trade off ,会帮助之后做选择。
    因此大约在 pg 10 用到了现在 15 、16 ,当需要 OLAP 场景的时候,也尝试通过 pg 解决,当需要搜索,需要做 AI/向量搜索,也是在通过 pg 解决。这里锚定的点就是 pg ,锚定了这个基本点后,可以更加深入的去使用 pg 的一些功能,例如我不会介意直接在 pg 里写 js (用 pg v8 )。

    例如 OS ,选择了 alpine ,之后 os 相关的场景 物理机、镜像、容器等 全都是 alpine ,得到的回报值得投入。
    前端后端之类的同理,只是这些领域锚定的内容可能还会有变化,例如前端一开始 antd ,然后变为 tw 。后端确定 nodejs 后,一开始 nestjs rest 然后 gql 反复,但最终能 settle down 到一些固定的技术栈。这些也都是自己的技术投资,非常乐意沟通和分享这些内容。

    这些核心的点是不会变的,但是围绕这些点的技术是会不断发展的,新的的东西也在不断涌现,保持好奇和兴趣,从 程序员 变为 工程师。搞这些真的好玩 😄。

    到现在,原意分享,帮助别人去用 alpine 、pg 这些也是一种乐趣,非常有意思。
    hutoer
        27
    hutoer  
       65 天前
    RPC 我倒是觉得 https://github.com/moleculerjs/moleculer 比较简单
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3332 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 43ms · UTC 11:59 · PVG 19:59 · LAX 03:59 · JFK 06:59
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.