DoraCMS 从今年年初的时候开始有插件化构想,为什么会有这种想法?其实就目前发布的DoraCMS 2.1.3 来讲,对比之前的版本有很多优化,最重要的一点
是建立了比较清晰的代码结构,后台管理基于vue的spa模式,服务端 MVC 架构也非常明显,对比2.1.2 版本服务端抽离出了server 层,让数据操作更加便捷,避免混乱的各种查询。这是 2.1.3 的目录结构:
├─build // webpack 相关配置文件
│
├─client // 客户端文件(前台/后台)
│ │
│ ├─index // 前台组件
│ │
│ ├─manage // 后台组件
│ │
│ └─template // 初始模版
│
├─databak // 默认数据备份目录
│
│
├─logs // 日志目录
│
├─public // 静态文件目录
│ │
│ ├─admin // 后台vue编译后的文件目录
│ │
│ ├─apidoc // api文档目录
│ │
│ ├─plugins // 前台依赖的相关组件
│ │
│ ├─themes // 皮肤目录
│ │
│ ├─ueditor // ueditor插件目录
│ │
│ ├─upload // 文件上传目录
│ │
│ └─vendor // 后台静态dll目录
│
│
├─server // 服务端目录
│ │
│ ├─bootstrap // 前台渲染相关
│ │
│ ├─configs // 系统配置
│ │
│ ├─locales // 国际化
│ │
│ ├─middleware // 中间件
│ │
│ │
│ ├─lib // 核心层
│ │ ├─contorller // 控制器
│ │ │
│ │ ├─model // 数据模型
│ │ │
│ │ ├─service // 数据库操作
│ │ │
│ │ └─utils
│ │ ├─cache // redis缓存
│ │ │
│ │ ├─memoryCache // 内存缓存
│ │ │
│ │ ├─authPower.js // 资源鉴权
│ │ │
│ │ ├─authSession.js // session 鉴权
│ │ │
│ │ ├─authToken.js // token鉴权
│ │ │
│ │ ├─mime.js // 文件类型
│ │ │
│ │ ├─siteFunc.js // 公共方法
│ │ │
│ │ └─validatorUtil.js // 信息校验
│ │
│ │
│ └─routers // 路由
│
│
│
└─views // 前台模板
│
├─dorawhite // 主题目录
│
├─admin.html // 后台管理模板
│
└─adminUserLogin.html // 后台登录模板
先从基于vue后台管理说起,后台管理的界面以及逻辑在 /client/manage 下,每个模块非常清晰:

熟悉 DoraCMS 的童鞋都知道,开发完成后,执行
npm run build
就可以将我们开发的vue工程编译为浏览器可以识别的es5,发布到生产。非常方便,但是在开发过程中,我渐渐发现一些问题:
1、开发了很多模块后,编译速度非常慢。
2、有时候只改了一个vue文件,也需要重新编译,非常浪费资源
3、有些功能模块我现在不想用了,怎样友好的剥离掉?
这些是我们在实际开发场景下经常遇到的问题,于是通过google,我了解到了前端微服务,至今为止,我认为微服务将成为今后大型应用必不可少要考虑的架构。
什么是“微服务”,说通俗一点,我认为的微服务就是模块拆分,相互隔离解耦,互不影响,按需加载等;那么基于 DoraCMS 2.1.3 目前的架构,如何来做微服务呢?通过大量的调研和实践,我找到了一个比较合适的方案 : single-spa , 我做了如下尝试:
1、把每个模块独立出来,每个模块作为一个标准的小app抽离(基于 vue-cli)

2、通过 single-spa 提供的方法将每个模块串联起来,类似下面这样(DoraCMS 的实现相同)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Coexisting Vue Microfrontends</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="importmap-type" content="systemjs-importmap">
<script type="systemjs-importmap">
{
"imports": {
"navbar": "http://localhost:8080/app.js",
"app1": "http://localhost:8081/app.js",
"app2": "http://localhost:8082/app.js",
"single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js",
"vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js",
"vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js"
}
}
</script>
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js" as="script" crossorigin="anonymous" />
<link rel="preload" href="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js" as="script" crossorigin="anonymous" />
<script src="https://unpkg.com/import-map-overrides@1.7.2/dist/import-map-overrides.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/system.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/amd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-exports.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-register.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/use-default.min.js"></script>
</head>
<body>
<script>
(function() {
Promise.all([System.import('single-spa'), System.import('vue'), System.import('vue-router')]).then(function (modules) {
var singleSpa = modules[0];
var Vue = modules[1];
var VueRouter = modules[2];
Vue.use(VueRouter)
singleSpa.registerApplication(
'navbar',
() => System.import('navbar'),
location => true
);
singleSpa.registerApplication(
'app1',
() => System.import('app1'),
location => location.pathname.startsWith('/app1')
)
singleSpa.registerApplication(
'app2',
() => System.import('app2'),
location => location.pathname.startsWith('/app2')
)
singleSpa.start();
})
})()
</script>
<!-- See https://github.com/joeldenning/import-map-overrides#user-interface -->
<import-map-overrides-full show-when-local-storage="overrides-ui"></import-map-overrides-full>
</body>
</html>3、处理权限、路由等细节
经过架构变更之后,可以达到这样的效果:
1、每个模块相互独立了,编译出的js彼此分开。
2、按需编译,改哪个模块,编译哪个模块,借助jenkins,更新模块到服务器非常方便:

3、再也不用担心模块过多引起的编译速度过慢了,添加哪个编译哪个
4、后台的所有模块跟服务端基本没有关系了,独立开发,于是即使多个人同时开发后台管理模块,也相互不影响。
PS: single-spa 微服务架构已经应用到 DoraCMS 2.1.4 版本,有兴趣的童鞋在后面发布后可以clone下来试试。