模块接口设计
从结果说起
API
JModule.define(moduleKey, {
init(jModuleInstance) {}, // jModuleInstance 为JModule 实例
routes: [], // 路由
store: {}, // vuex
imports: [], // 定义模块依赖
exports: {}, // 对外API
});
这里做一点解释:
- JModule 是一个模块管理器,加载到平台层,负责模块模块的管理、模块平台之间的通信等
- moduleKey 是识别模块的唯一标识,作用很大,后面细说
- init函数,可选配置,模块加载后会自动执行它,函数参数是模块实例,可以从实例获取到当前模块的域名等信息
- routes,可选配置,模块的路由配置,注册到平台的 vueRouter 进行工作
- store, 可选配置,数据中心,注册到平台的 Vuex 进行工作
- imports,可选配置,定义该模块依赖的其它模块
- exports,模块对外暴露的API, 可以是任意类型变量,比如function、component等
Demo
举个例子,开发一个研发流水线(pipeline)的模块:
- 它有列表和详情页面
- 它需要从代码仓库(coding模块)获取一些基础信息
- 它需要共享当前执行的流水线信息
- 它需要提供对外的接口,允许其它模块单独嵌入(不通过路由)流水线列表组件
应该会得到两个这样的模块配置:
// 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('...');
});
为什么设计这些字段
把问题具体化,应该有三个问题需要回答
- 这些字段的作用是什么?有多余吗?
- 这些字段够用吗?
- 为什么设计成这样?
这些字段的作用是什么?有多余吗?
这里总共涉及六个字段:moduleKey、init、routes、store、imports、exports。
moduleKey
-
作用
这是模块的标识符,用于识别一个模块。
-
它不重要
如果从“让模块代码顺利运行”这一个目标来讲,它其实不需要,我把代码加载了,该注册的路由注册了,代码就可以正确工作了,业务逻辑根本不需要它。不过,让代码运行,只是工程中的一部分工作。
-
它很重要 从管理的角度讲,它真的必不可少。比如:
a. 权限管理: 特定的人群只能访问特定的模块,需要它识别模块身份
b. 资源加载管理: 比如由于某些原因,多次要求执行加载某个模块的资源,我们可以通过模块标识进行模块加载状态管理,避免重复从服务器加载资源
c. 调试: 可以跟踪指定模块的资源加载进度以及执行异常,另外,当同一个平台注入了同一个模块的两套资源配置时,比如既有生产环境配置又因开发需要注入了本地资源配置,可以选择本地优先。
routes 和 store
-
作用
声明模块的路由信息和需要全局共享的数据
-
它不重要
一个模块有哪些页面,有哪些数据需要共享,是业务范畴的事情,业务是复杂多变的,它可能根本没有需要共享的数据,甚至根本不需要注册路由,比如我可能声明一个公共组件作为模块,或者它甚至没有视图,只是负责一些特定数据的加载,它其实可以不需要。所以这是可选字段。
-
它很重要
routes 很重要,对大多数场景而言,我们需要根据不同的地址显示不同的视图,路由就必不可少。而且业务范畴的事情,也应该在模块内管理,平台不会预测到一个模块有几个页面叫什么名字。store 的存在我倒是犹豫过,没有它并不会让业务开发不下去,如果需要共享数据,用 events 和 exports 事实上也可以达到相同的目的,但是从开发者习惯、易用性及历史项目改造难度的角度讲,store能避免一些额外的工作。
exports 和 imports
-
作用
模块间的依赖管理,声明模块对外提供服务的api, 以及依赖关系的外部模块的声明。可选
-
它不重要
没有这两个字段,事实上也确实有其它方式去满足功能开发的需求。没有imports,自己手动加载依赖的模块代码也可以工作;没有exports,往全局变量写点东西,其它模块也照样能访问到。如果只是为了实现功能,说它不重要,其实也没毛病。
-
它很重要
从代码可维护的角度讲,imports 声明依赖的模块,可以实现自动加载依赖的模块,不然可能会面临脑力记住依赖关系,手动加载依赖模块的尴尬场面。exports 除了对外暴露模块功能以外,可以告知开发者哪些是对外开放的功能,避免不兼容的修改导致其它模块不能正常工作。
init
-
作用
模块加载完之后自动执行的一个函数,可选
-
它不重要
绝大多数场景下,它确实没有存在必要。目前我也没有用到它了。
-
它很重要
这个字段存在的意义,主要是为了以后扩展功能,它可以拿到模块的实例信息,在早期的跨域解决方案中,模块内的http请求需要知道模块所在的域名,但在代码不允许使用域名硬编码的原则下,这个信息只有从模块实例才能获取到。后来为了研发方便调整了这部分方案,不过考虑到模块运行可能需要其它来源于外部配置的信息,所以保留了这个配置。
这些字段够用吗?
可能不一定够用,但我并没有遇到更复杂场景。从模块管理的角度讲,提供了moduleKey 作为身份识别;从模块自身功能完备性角度讲,一般业务场景中需要全局挂载使用的只有router和store了;从模块间的关系讲,定义了入口和出口,应该也齐活了。
为什么设计成这样?
这里我大概参考了一下angular的模块设计,angular是个自带模块设计的东西,所以借鉴了一下。或许有更好的方式。
有什么可以改进的?
或许可以改成这样
export default {
routes: [],
...
};
- JModule.define的使用其实可以去掉,在编译工具中自动加上它也是可以的
- moduleKey 可以根据项目内的路径自动生成,而不需要手动去填,比如:
/projectA/src/modules/b
可以自动提取为'projectA.b'作为moduleKey。