前端模块化架构设计与实现(二|模块接口设计)

2,065 阅读6分钟

模块接口设计

从结果说起

API

JModule.define(moduleKey, {
    init(jModuleInstance) {}, // jModuleInstance 为JModule 实例
    routes: [], // 路由
    store: {}, // vuex
    imports: [], // 定义模块依赖
    exports: {}, // 对外API
});

这里做一点解释:

  1. JModule 是一个模块管理器,加载到平台层,负责模块模块的管理、模块平台之间的通信等
  2. moduleKey 是识别模块的唯一标识,作用很大,后面细说
  3. init函数,可选配置,模块加载后会自动执行它,函数参数是模块实例,可以从实例获取到当前模块的域名等信息
  4. routes,可选配置,模块的路由配置,注册到平台的 vueRouter 进行工作
  5. store, 可选配置,数据中心,注册到平台的 Vuex 进行工作
  6. imports,可选配置,定义该模块依赖的其它模块
  7. exports,模块对外暴露的API, 可以是任意类型变量,比如function、component等

Demo

举个例子,开发一个研发流水线(pipeline)的模块:

  1. 它有列表和详情页面
  2. 它需要从代码仓库(coding模块)获取一些基础信息
  3. 它需要共享当前执行的流水线信息
  4. 它需要提供对外的接口,允许其它模块单独嵌入(不通过路由)流水线列表组件

应该会得到两个这样的模块配置:

// coding 模块配置
JModule.define('coding', {
    exports: {
        loadHooks() {},
    },
});

// pipeline 模块配置
JModule.define('pipeline', {
    routes: [{
        path: '/list',
        name: 'PipelineList',
        component: ListView,
    }, { ... }],
    store: {
        currentPipeline: {}, // vuex 里的模块概念
    },
    imports: ['coding'], // 它依赖coding提供的API, 
    exports: {
        ListView,
    },
});

// pipeline 业务代码
JModule.require('coding.loadHooks').then((loadHooks) => {
    loadHooks('...');
});

为什么设计这些字段

把问题具体化,应该有三个问题需要回答

  1. 这些字段的作用是什么?有多余吗?
  2. 这些字段够用吗?
  3. 为什么设计成这样?

这些字段的作用是什么?有多余吗?

这里总共涉及六个字段:moduleKey、init、routes、store、imports、exports。

moduleKey

  1. 作用

    这是模块的标识符,用于识别一个模块。

  2. 它不重要

    如果从“让模块代码顺利运行”这一个目标来讲,它其实不需要,我把代码加载了,该注册的路由注册了,代码就可以正确工作了,业务逻辑根本不需要它。不过,让代码运行,只是工程中的一部分工作。

  3. 它很重要 从管理的角度讲,它真的必不可少。比如:

    a. 权限管理: 特定的人群只能访问特定的模块,需要它识别模块身份

    b. 资源加载管理: 比如由于某些原因,多次要求执行加载某个模块的资源,我们可以通过模块标识进行模块加载状态管理,避免重复从服务器加载资源

    c. 调试: 可以跟踪指定模块的资源加载进度以及执行异常,另外,当同一个平台注入了同一个模块的两套资源配置时,比如既有生产环境配置又因开发需要注入了本地资源配置,可以选择本地优先。

routes 和 store

  1. 作用

    声明模块的路由信息和需要全局共享的数据

  2. 它不重要

    一个模块有哪些页面,有哪些数据需要共享,是业务范畴的事情,业务是复杂多变的,它可能根本没有需要共享的数据,甚至根本不需要注册路由,比如我可能声明一个公共组件作为模块,或者它甚至没有视图,只是负责一些特定数据的加载,它其实可以不需要。所以这是可选字段。

  3. 它很重要

    routes 很重要,对大多数场景而言,我们需要根据不同的地址显示不同的视图,路由就必不可少。而且业务范畴的事情,也应该在模块内管理,平台不会预测到一个模块有几个页面叫什么名字。store 的存在我倒是犹豫过,没有它并不会让业务开发不下去,如果需要共享数据,用 events 和 exports 事实上也可以达到相同的目的,但是从开发者习惯、易用性及历史项目改造难度的角度讲,store能避免一些额外的工作。

exports 和 imports

  1. 作用

    模块间的依赖管理,声明模块对外提供服务的api, 以及依赖关系的外部模块的声明。可选

  2. 它不重要

    没有这两个字段,事实上也确实有其它方式去满足功能开发的需求。没有imports,自己手动加载依赖的模块代码也可以工作;没有exports,往全局变量写点东西,其它模块也照样能访问到。如果只是为了实现功能,说它不重要,其实也没毛病。

  3. 它很重要

    从代码可维护的角度讲,imports 声明依赖的模块,可以实现自动加载依赖的模块,不然可能会面临脑力记住依赖关系,手动加载依赖模块的尴尬场面。exports 除了对外暴露模块功能以外,可以告知开发者哪些是对外开放的功能,避免不兼容的修改导致其它模块不能正常工作。

init

  1. 作用

    模块加载完之后自动执行的一个函数,可选

  2. 它不重要

    绝大多数场景下,它确实没有存在必要。目前我也没有用到它了。

  3. 它很重要

    这个字段存在的意义,主要是为了以后扩展功能,它可以拿到模块的实例信息,在早期的跨域解决方案中,模块内的http请求需要知道模块所在的域名,但在代码不允许使用域名硬编码的原则下,这个信息只有从模块实例才能获取到。后来为了研发方便调整了这部分方案,不过考虑到模块运行可能需要其它来源于外部配置的信息,所以保留了这个配置。

这些字段够用吗?

可能不一定够用,但我并没有遇到更复杂场景。从模块管理的角度讲,提供了moduleKey 作为身份识别;从模块自身功能完备性角度讲,一般业务场景中需要全局挂载使用的只有router和store了;从模块间的关系讲,定义了入口和出口,应该也齐活了。

为什么设计成这样?

这里我大概参考了一下angular的模块设计,angular是个自带模块设计的东西,所以借鉴了一下。或许有更好的方式。

有什么可以改进的?

或许可以改成这样

export default {
    routes: [],
    ...
};
  1. JModule.define的使用其实可以去掉,在编译工具中自动加上它也是可以的
  2. moduleKey 可以根据项目内的路径自动生成,而不需要手动去填,比如:/projectA/src/modules/b 可以自动提取为'projectA.b'作为moduleKey。