导读
认识
2、特点
微前端架构有以下特点:
a)App大且含有多条业逻辑;
b) App是对应到不同的组织架构中;
c)多App间复用逻辑较多且有组合入口出现;
d) 不断迭代的App。
基于Vue的基座式微前端
1、基座运行环境
开发时基座的运行环境有以下两种方式。
a) 本地服务式 - 子应用开发者将基座拉取到本地,运行到本地服务中。
本地服务式是将基座和子应用放置于子应用开发者本地,由于同处于一个物理空间(磁盘)上,基座可以通过直接引用子应用的构建出口来解决引用热更新问题。这种做法解耦不是很彻底,需要子应用开发者本地启动基座服务,并且没法提供多个子应用同周期协调开发的环境。
b) 集中服务式 - 将基座维护在一个专供开发时应用的服务器容器中。
集中服务式开发体验较好且可以多方协调开发,但需要维护专供开发时的服务节点,并通过网络解决基座与子应用间的资源加载及热更新问题,设计难度较大,由于是网络传输资源所以耗时需要优化。
2、选型
为快速落地框架,我们选用本地服务式方案。下面主要介绍本地服务式各点实现。不过为了后续转集中服务式做准备,这里也会列出一部分实现思路。为提高代码可读性,我们命名基座为Voo。
3、本地服务式
开发时-本地服务式关系图
a) 子应用入口文件的包装
export default (Voo) => {
Voo.Vue.prototype[appName] =vuePrototypeExtension
return { router, store, App }
}
释:子应用暴露一个方法并返回router、store、App模块,供基座注册调用和回传基座对象(Voo);原型扩展方法需要增加namespace,方便提供给子应用本身和其他应用复用。
export default [
{ 'path': '/', 'redirect': 'home' },
{ 'path': 'home', 'name': 'Home', 'component': Home }
]
释:暴露一个routes数组,供基座动态插入,由于基座与子应用运行在同一个router对象中,所以遵循“以 / 开头的嵌套路径会被当作根路径。这可以让你充分的使用嵌套组件而无须设置嵌套的路径”的规则。
export default {state, mutations, actions, modules }
释:暴露一个store module对象,供基座动态注册。
c) 子应用脚手架的修改
webpackConfig.output.library ='[name]'
webpackConfig.output.libraryTarget= 'umd'
释:修改输出目标为umd便于runtime、开发时基座的引用
webpackConfig.output.jsonpFunction= appName
释:如果在同一网页中使用了多个来自不同编译过程(compilation)的webpack runtime,则需要修改此选项。
webpackConfig.plugins.push(newwebpack.BannerPlugin({
'banner': '/* eslint-disable */', 'raw': true
}))
释:向bundle中追加eslint注释,防止基座eslint校验不通过。
'devServer': {
'writeToDisk': true,
before(app,server) {
request.post('http://localhost:7777/__dev_subApp_register', {
'form': {
'id': [appName],
'resourcePath':path.resolve(__dirname, `dist/${[appName]}.js`)
}
}, (error, response, body) => {
if (error) {
console.error('[ error ]请先启动基座工程Voo')
} else {console.log(body)}
})
}
}
释:将构建目标写入到物理磁盘中,devServer默认是写入在虚拟内存中的,基座工程无法import。
在devServer启动前向基座工程的本地服务发送构建目标的物理路径,注意:此处路径是磁盘绝对路径,是可以通过import载入的。` 7777`是基座工程固定的端口,__dev_subApp_register是基座工程固定的子应用注册路由。
d) 基座脚手架的修改
'devServer': {
before(app, server) {
app.post('/__dev_subApp_register',(req, res) => {
const params =Object.assign(req.query, req.body)
const devSubAppRegisterInfo = `
/* eslint-disable */
export default (regiestSubApp, opts) => {
import('${params.resourcePath}').then((res) => {
const subApp =res.default(opts)
regiestSubApp({
id: '${params.id}',
subApp
})
})
}
`
fs.writeFileSync(`${__dirname}/__dev__subApp_register_info.js`,devSubAppRegisterInfo)
res.json({ 'code': 0, 'message': '开发时注册成功' })
})
}
}
释:编写子应用注册接口,在接收到子应用的注册请求后,将基座引用逻辑写入到__dev__subApp_register_info.js。
4、集中服务式
a) 配置服务,将nginx反向代理到基座服务;
b) 基座服务提供与子应用交互的接口,此处我们选用webpack的devServer进行描述。
1、在基座根目录创建 /subApps。
2、在before编写注册接口。当注册请求进入后将子应用/dist文件写入到/subApps中,如果/dist文件太大,可以采用压缩解压,如果子应用文件夹存在则更新。
c) /subApps目录结构
d) 动态读取 /subApps下所有文件,暴露出去
const requireSubApps =require.context('./', true, /\.js|.css$/)
export defaultrequireSubApps.keys().map((fileName) => {
return requireSubApps(fileName).default
})
e) 热更新
由于基座入口引用的是基座服务本地文件,所以,我们只需要在子应用代码发生改变时触发基座注册接口就行。实现如下:
before(app, server) {
registe();
app.post('/__dev_update', (req, res) => {registe()})
}
由于需要监听main.js入口以内所有模块的变化,所以将监听逻辑放到main.div.js,代码实现如下:
if (module.hot) {
module.hot.accept('./main.js', ()=> {
fetch('/__dev_update')
});
}
同时修改dev和prod环境下的打包入口。
configureWebpack(webpackConfig){
webpackConfig.entry =process.env.NODE_ENV === 'development'
? { [appName]: ['./main.dev.js'] }
: { [appName]: ['./src/main.js'] }
}
f) 优化
热更新时按需上传[hash].hot-updage.json,降低网络耗时。
1、子应用生命周期
由于子应用会被当做一个路由组件注册到基座中,所以子应用可以利用其root组件的vue生命周期。
2、沙盒化router、store、css、vue原型扩展
路由,将子应用注册到其ID为根的路由上,并将其暴露的路由注册到children上。
Voo.$router.addRoutes([
{'path': `/${ subApp.id}`, 'children': subApp.router, 'component': subApp.App}
])
Store module, 动态注册的store module本来就是具有作用域的,依照vuex文档即可。
Voo.$store.registerModule(id,{ 'namespaced': true, ...subApp.store })。
Css module, 通过postcss给子应用追加作用空间。
constpostcssNamespaceGlobal = postcss.plugin('postcss-namespace-global', ({namespace= ''}) => (root) => {
root.walk((node) => {
if(node.selector){
node.selector =(node.selector.split(',').map((selector) => {
if(selector.match(/^(\s*)(html|body)(\s*)$/)) {
return selector
}
return `${namespace}${selector}`
}
).join(
','
))
}
})
})
module.exports ={'plugins': [postcssNamespaceGlobal({namespace: `.${appName}`})]}
Vue原型扩展,给子应用用到的原型扩展方法规定到其ID对应的对象中。
3、 复用层
a) 类性质。调用时创建实例,因此runtime时互不干扰。但要根据是否需要锁定版本来确定复用内容的管理方案。
需要锁定版本,采用npm scope,管理在公司内部的npm服务上。注意:要规范好子应用npm安装重复问题。
不需要锁定版本,采用全局注册,例如全局注册的业务组件。注意:子应用注册需要scope。
b) 函数性质。调用和执行是同一组代码,这种复用内容性质是脆弱的。所以对设计者要求较高,且迭代应向下兼容。
4、 复用内容
a) UI组件库等第三方依赖:类性质,在基座中规定并回传给子应用。
b) ajax库及统一接口处理:函数性质,在基座中规定并回传给子应用。
c) 业务类组件:类、函数性质,采用全局注册回传给子应用。
d) 子应用特色业务组件:如chart、workflow等:类性质。托管npm。
5、 子应用拆分粒度
总结和展望
1、总体来说这一套架构解决了以下问题:
a) 解决业务繁多的项目分治;
b) 多团队协作开发,且团队内项目自治;
c) 对敏捷迭代的项目构建良好的基础;
d) 子应用开发者无需关注基座及其历史子应用业务,直接依赖基座预览开发效果,体验提升;
e) 子应用开发几乎无异于SPA,无学习门槛。
2、思考
此次是我们在微前端道路上的初探,输出也只是基于Vue技术栈的单一形态。所以围绕微前端概念我们还有很多事情要做。
在设计此架构前我也调研过很多应用微前端的文章,得出的结论竟然让我自己觉得有些矛盾。微前端的理念是为了解耦,但是往往很多使用者还希望通过微前端实现业务的高度复用。那么矛盾来了,复用就伴随着耦合。所以说没有银弹,我们要做的是解耦子应用的同时,尽可能的对复用层进行分类管理,结合业务场景定制化适合的微前端架构。
3、优势
a) 基于同技术栈的微前端,可以快速抽离复用层并无侵入性的投入使用;
b)基于vue,有效的利用了store、router动态注册特性,贴合58商业目前技术栈及存量项目;
c) 在本地式开发流程中子应用脚手架和基座脚手架之间的合作可以提供稳定的热更新方案。
4、规划
将目光放的再长远一些,那么我们还应该做以下规划。
d) 子应用跨技术栈,解耦更彻底,让微前端能应用到更大的聚合App上和组织架构中,当然复用层将变为一个挑战。
e) 为增加开发体验,基座采用集中服务化方案,例如:有子应用需求接入时就将基座部署到沙箱节点上,或者可以将基座应用设计为服务端渲染并提供一套开发专用带权限的接口。这样就可以解决专门为开发提供服务的问题,同时还可以封装注册表相关逻辑以管理子应用。
f) 注册表模块服务化,此项主要是为规范工程化管理。