V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
llej
V2EX  ›  程序员

如何实现模块化加载的前端和后端代码?

  •  
  •   llej · 22 天前 · 3494 次点击

    首先我定义一下我这里的模块概念:一个文件夹下的代码(也就是前端的话可能涉及多个页面以及组件等,后端同理)

    其实如果只是追求打包时动态加载不同的模块是很简单的,通过环境变量约束一下打包工具即可

    但我还想要有强类型支持,以及直接剪切文件夹就能新增和移除模块。

    架构畅想

    1. 使用 MonoRepo 的形式进行项目管理

    2. 每个顶级模块(包)都可能包含一个后端模块和一个前端模块(也就是可以是单纯的前端模块或后端模块)

    3. 存在一个基座包,这是整个项目的核心,所有的其他包都会依赖这个

    4. 非基座包的后端模块和前端模块都能直接引用到基座包中的后端依赖和前端依赖,而不需要特别专门的配置

    5. 同一个包内的前端模块可以直接引用到后端模块的 api (强类型)以及其他包的后端模块的 api

    6. 所有包的后端模块都能直接声明需要的 context ,然后编译时能够感知到基座包是否兜底的提供了所有 context (即编译时能够报错某个 context 缺失)

      1. 为什么基座包需要兜底:因为是从基座包启动的 http server ,他是入口也是出口
    7. 可以直接剪切文件夹就能新增包和移除包,如果包之间有依赖但对应的包被移除了则编译时应该报错

    如何实现?

    期待各位朋友的指点(❁´◡`❁)

    28 条回复    2025-08-15 11:13:37 +08:00
    JoeJoeJoe
        1
    JoeJoeJoe  
    PRO
       22 天前 via iPhone   ❤️ 1
    可以搜一下 python 的一个 erp 框架 叫 Odoo , 跟你说的有些差异 但是他的模块化做的很好
    tcper
        2
    tcper  
       22 天前
    你说的目录是源码结构,最终打包结构和源码显然不是一回事,严重怀疑没做过真实项目才有这种设想
    layxy
        3
    layxy  
       22 天前
    前端可以通过微前端实现,后端 java 倒是可以通过插件实现
    sentinelK
        4
    sentinelK  
       22 天前
    所以楼主说的和 jar 、dll 的引用,以及 js 的 import 有啥区别?
    楼主的意思是不想改配置?

    那你直接把你项目 src 中的某个文件夹、dll 、jar 直接删了不就行了……
    直接删了,IDE 直接标红,就满足了楼主说的“如果包之间有依赖但对应的包被移除了则编译时应该报错”
    llej
        5
    llej  
    OP
       22 天前
    @tcper 因为我需要分发源码给不同的客户,我不希望他们能得到全部的模块源码,我想只给他们定制的功能需要的模块部分
    wn990916
        6
    wn990916  
       22 天前
    根据不同的用户构建不同的版本呢?
    llej
        7
    llej  
    OP
       22 天前
    @sentinelK 是的,我不想改配置,所以我需要实现,copy 一个模块的文件夹进来(包含了对应模块的前端和后端代码)然后直接就能运行项目,前端路由这块我已经实现可以这样加载了,但是后端还没搞定,主要是我想要 ts 类型严谨,否则我直接 require 也能实现
    llej
        8
    llej  
    OP
       22 天前
    @wn990916 比如老用户之前用的老模块,那就不用管呀,接着用老的就行了
    sentinelK
        9
    sentinelK  
       22 天前
    @llej 那不是应该从 Git 管理的角度入手吗。每个用户是不同的 Git 分支。否则你如何做后续支持?每次支持难道你都要“手动删除文件夹”到和用户环境一致的程度吗
    weixind
        10
    weixind  
       22 天前   ❤️ 1
    前端没那么复杂,使用文件路由就可以了。

    文件路由打包的时候就是先读取你的文件夹,动态生成路由文件。

    node 的后端也可以用同样的方式处理,但是不一定有现成的方案。其他语言的后端不清楚。

    可以搜一下哪些 router 方案支持文件路由。要自己写的话可以参考 taro 的 h5 方案。
    llej
        11
    llej  
    OP
       22 天前
    @sentinelK 我会有一个环境变量文件的,比如 用户 a.env 的是包含了模块 a 模块 b ,那么我的脚本打包的时候就只包含这两,也有脚本一键导出只包含这两模块的源码发给他(有很多用户就是想要源码,我提供源码也可以跟他要价更高一些)

    不使用 git 分支管理不同用户(因为我在实现某个用户的功能的时候可能又改善了某些通用模块,那么我不停切换分支来修改合并代码太麻烦了)
    llej
        12
    llej  
    OP
       22 天前
    @weixind 嗯嗯,前端我已经实现了,现在就是 node 我想实现 ts 类型严谨,还没想出好办法
    dssxzuxc
        13
    dssxzuxc  
       22 天前   ❤️ 1
    @llej node 实现 ts 类型严谨具体是指什么,是前端调用了不存在的接口就类型报错吗,调用对应接口拿到入参类型和返回类型?
    你可以看一下 tPRC ,或者 hono 的 RPC ,虽然重构的工作量可能会很大。
    hono 的 RPC 是后端导出一个或者多个路由实例类型给前端使用,客户端底层是基于 fetch 实现,前端调用这个接口就像直接调用后端接口方法一样,带来的收益是无需专门为接口类型定义一堆的类型到处混乱地引用,因为接口本身已经有类型了。
    在你这个场景下,可以每个模块都导出一个路由实例,最后在基座包组装所有路由,再把类型导入到前端,最终得到包含所有接口路径、入参、返回类型的一个 http 客户端,前端只需要调用这个客户端就行,不存在的接口或者入参不对都会报错。你也可以让一部分路由只给同个顶级包下的前端模块引用,这样就实现了或许会用到的接口 public/private 修饰功能。
    zhuangzhuang1988
        14
    zhuangzhuang1988  
       22 天前   ❤️ 1
    学习下 kibana , 就是要系统的设计才行。

    举例 https://github.com/elastic/kibana/tree/main/x-pack/examples/alerting_example
    flytsuki
        15
    flytsuki  
       22 天前   ❤️ 1
    要不试试代码存数据库,启动项目时再编译
    SorcererXW
        16
    SorcererXW  
       22 天前
    后端为啥要模块化加载,都部署不就好了吗,能差多少呢,对应模块没有流量也没啥开销
    crysislinux
        17
    crysislinux  
       22 天前 via Android   ❤️ 1
    https://nx.dev/ 看看这个呢。一个组件也可以是一个 package ,你想分多细分多细。也不用分前端还是后端,只要 package 没有平台依赖的代码就随便用。
    realJamespond
        18
    realJamespond  
       22 天前   ❤️ 1
    每个用户建一个 git 仓库包含对应模块 git submodules 的库
    neoblackcap
        19
    neoblackcap  
       22 天前   ❤️ 1
    后端不就是动态加载嘛,你完全可以将模块写成独立的 dll, so 文件,然后自己写一个 loader 在程序启动的时候载入这些模块。
    对应做法就是,一个程序许可证( license )可以解码出对应的权限以及模块清单,然后运行时根据这个清单去查找对应的模块并载入就可以了
    yandif
        20
    yandif  
       22 天前   ❤️ 1
    试一下 elysia.js ,https://elysiajs.com/at-glance.html
    截屏 2025-08-14 13.55.12.png
    这个后端框架有个 eden 功能实现了这个强类型
    asdjgfr
        21
    asdjgfr  
       22 天前   ❤️ 1
    新建一个 ee 文件夹,每个客户代码都是一个私有 git 仓库,通过 git 的子模块引入:ee/zhangsan ee/lisi ,分发的时候只拉取某个客户的就可以了,git submodule update --init --recursive zhangsan
    llej
        22
    llej  
    OP
       22 天前
    @dssxzuxc 你说的很对,关于前后端交互我是自己实现了一个前端可以直接引用后端接口类型的 ts proxy ,类似于 tRPC ,不过我这里提到的类型严谨是关于后端的 context 传递,我是基于 Effect 实现的依赖倒置,这样我能够直接通过类型看到每个模块还有哪些 context 没有传入,但是我还没想到要怎么才能够做到“可剪切式”的添加和移除模块,这块我还比较混乱,说的也不够清晰。

    我最近的想法是编译前根据环境变量 生成对应的 import 模块的 ts 代码,这样就能直接解决类型问题
    llej
        23
    llej  
    OP
       22 天前
    @SorcererXW 我不希望将全部源码给用户
    nekochyan
        24
    nekochyan  
       22 天前   ❤️ 1
    听起来意思是依赖注入,其他模块想用另一个模块都通过基座包去获取;我们项目大概是这么实现的:每个模块是一个文件夹,文件夹下只可引用基座包,里面调用基座包注入自身模块,这样剪切文件夹就实现了新增和移除模块

    至于强类型支持就需要 ts 的类型体操了,可以写一个模块生成器实现在依赖注入,声明其注入的一定是某个类型

    下面是我们项目的简单示例

    模块生成器文件:

    // 声明所有模块都是 TypeModules 类型
    declare global {
    interface TypeModules { }
    }


    class _coreModule {
    static instance: _coreModule;
    moduleMap: { [key: string]: any } = {};
    static initInstance(): _coreModule {
    return _coreModule.instance = new _coreModule();
    }
    setModule<T extends keyof TypeModules>(name: T, module: TypeModules[T]) {
    this.moduleMap[name] = module;
    }
    getModule<T extends keyof TypeModules>(name: T): TypeModules[T] {
    return this.moduleMap[name] as TypeModules[T];
    }
    }
    export const coreModule = _coreModule.initInstance();


    /**
    * @desc: 模块生成器
    * @param {T} name 模块名称
    * @param {new () => TypeModules[T]} moduleClass 模块类
    * @return {TypeModules[T]} 模块实例
    */
    export function ModuleGenerate<T extends keyof TypeModules>(name: T, moduleClass: new () => TypeModules[T]): TypeModules[T] {
    // eg:具体的实现; coreModule 保存注入依赖
    const instance = new moduleClass();
    coreModule.setModule(name, instance);
    return instance;
    }



    测试文件:
    declare global {
    interface TypeModules {
    /**
    * @desc: [模块] 测试模块
    */
    testData: TestDataClass
    }
    }

    export class TestDataClass {
    // 具体实现
    }

    // 注入
    const testData = ModuleGenerate('testData', TestDataClass);


    其他 文件或模块中

    // testData 类型就是 TestDataClass ,完全不需要引入 测试模块的文件,但当测试模块删除时,这段代码编译时会报错
    let testData = coreModule.getModule('testData');
    duan602728596
        25
    duan602728596  
       22 天前   ❤️ 1
    既然是 MonoRepo ,直接可以模块间互相引用了吧,workspaces 或者 lerna 都可以,比如有
    - packages/a
    - packages/b
    直接 import {} from '@xxx/a',import {} from '@xxx/b'就可以了
    给每个子模块加个 package.json 就可以了

    声明需要的 context ,装 node_modules 就可能会报错了,或者编译时也会提示缺失模块的。或者写个脚本检查一下

    类型甚至可以自动生成,把模块加到 declare global {}里
    asmoker
        26
    asmoker  
       22 天前
    蹲一个~
    2han9wen71an
        27
    2han9wen71an  
       21 天前
    java 有 osgi
    dododada
        28
    dododada  
       21 天前
    nodejs 没写过,不过以前做数据库审计的时候,mysql 有个 plugin 机制,直接安装运行,你感兴趣可以研究看看
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1034 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 18:35 · PVG 02:35 · LAX 11:35 · JFK 14:35
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.