您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
京喜达工作台拆分微前端实战
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
京喜达工作台拆分微前端实战
自猿其说Tech
2022-02-24
IP归属:未知
306480浏览
前端
### 1 背景 京喜达工作台包含四个组的业务,仓/数据、基础资料/网格站、财务、订单/运输,业务代码越来越多导致项目越来越大,项目运行越来越慢,代码质量不可控,经常出现A业务上线影响了B的功能,所以开始拆分微前端。 ### 2 拆分目标 #### 2.1 可独立运行主框架 进入工作台,展示主框架的内容,包括顶部栏及顶部功能区,展示左侧菜单栏,内容区域显示home页,点击菜单栏,根据不同的路由加载不同子应用并展示在视图窗口 #### 2.2 可独立运行四个组业务子框架 进入工作台,展示主框架的内容,包括顶部栏及顶部功能区,展示左侧菜单栏,内容区域显示home 页,点击菜单栏,各组的菜单加载各自业务代码 ### 3 拆分方案 1. 微前端框架使用single-spa, 详情看文档https://zh-hans.single-spa.js.org/ 1. single-spa-vue 针对vue项目的初始化、挂载、卸载的库函数, 详情看官网https://zh-hans.single-spa.js.org/docs/ecosystem-vue 1. 主应用子应用间通信 vuex 1. 项目间公共逻辑,样式,组件。使用git submodule将公共部分单独拆分仓库,现有公用代码业务逻辑交织复杂,后续可以使用npm包的形式发布公共代码 ### 4 实施 #### 4.1 抽取公用工具、css、组件、图片等公用内容 将项目中的公用部分提取出来放在独立的git仓库,在项目中jxd-frame文件下引入公用代码 ![](//img1.jcloudcs.com/developer.jdcloud.com/2f7ac6f7-ce5a-42bc-893f-14411afca76620220224140250.png) 项目中保留对应目录的文件,主服务直接引用jxd-frame下对应文件的代码,各自子服务除引入jxd-frame下对应文件内容外,子服务的业务逻辑写在子服务的项目对应文件中 ##### 4.2 注册single-spa主应用配置及挂载 以下以订单子应用为例子,多个子应用可以统一注册,config 文件可以定义多个子应用 ##### 4.2.1 创建一个single-spa配置 注册single-spa应用首先要创建一个single-spa-config。查看文档以获取更多详细信息https://zh-hans.single-spa.js.org/docs/configuration/ **single-spa-config/order.ts** ```javascript /** * 订单和运输子应用配置 */ import { runScript, getManifest } from '../../func' export default { // name:表示应用名称,与子应用的app id值一致,代表将子应用ID为ordermanage的内容挂在到页面上 name: 'ordermanage', // activeWhen:路由配置到数组中,主应用路由切换时如果在下面的数组中,则加载子应用 activeWhen: ['/dailyoperate/tmsManager'], // app:加载函数,满足activeWhen路由后,执行app内的方法 app: async () => { try { // 开发环境直接请求本地启动的子应用server if (process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'uat') { await runScript('http://ip:port/js/chunk-vendors.js') await runScript('http://ip:port/js/app.js') return window.ordermanage } else { let ordermanage = null // 加载子应用的 manifest.json 文件并以script链接形式挂载到页面中 await getManifest(process.env.VUE_APP_ORDER_APP_URL + 'manifest.json' + '?v=' + new Date().getTime(), 'app').then(() => { ordermanage = window.ordermanage }) return ordermanage } } catch (e) { console.log('load child error', e) } } } ``` 其中 runScript与getManifest方法均为将js挂载到页面上 ```javascript /* * runScript:一个promise同步方法。可以代替创建一个script标签,然后加载服务 * */ export const runScript = async (url: any) => { return new Promise((resolve, reject) => { const script: any = document.createElement('script') script.src = url script.onload = resolve script.onerror = reject const firstScript: any = document.getElementsByTagName('script')[0] firstScript.parentNode.insertBefore(script, firstScript) }) } /* * getManifest:远程加载manifest.json 文件,解析需要加载的js * url: manifest.json 链接 * bundle:entry名称 * */ export const getManifest = async (url: any, bundle: any) => { const { data } = await axios.get(url) const { entrypoints, publicPath } = data const assets = entrypoints[bundle].assets try { for (let i = 0; i < assets.length; i++) { // 只加载JS下的内容 if (assets[i].indexOf('js/') >= 0) { await runScript(publicPath + assets[i]) } } } catch (error: any) { Message.error({ showClose: true, message: '加载子应用失败,请刷新重试' }) } } ``` ##### 4.2.2 注册single-spa应用 你需要先注册应用 https://zh-hans.single-spa.js.org/docs/building-applications/ ,这样single-spa才知道在什么时机,如何去初始化、下载、挂载和卸载各应用。我们一般情况下在single-spa的配置文件中进行注册,当然也可以有其他方式(不推荐)。如果在某个应用中注册其他应用,这两个应用不会存在嵌套关系,他们还是同级关系,应用的挂载和下载也还是会依赖各自的触发条件(activity functions)。我们通过调用registerApplication方法来注册应用。 **single-spa-config/single-spa-config.ts** ```javascript // single-spa-config.js import * as singleSpa from 'single-spa' // 导入single-spa // ordermanage 为 4.2.1中定义的配置文件 import { ordermanage } from './app' // 此处可以定义注册多个子应用 singleSpa.registerApplication(ordermanage) export default singleSpa ``` ##### 4.2.3 执行singleSpa.start() ```javascript import singleSpa from '@jxd-frame/single-spa-config/single-spa-config' // start()方法 必须被single-spa配置文件的js调用, // 这时应用才会被真正挂载。在start被调用之前,应用先被下载,但不会初始化/挂载/卸载。 singleSpa.start() ``` ##### 4.2.4 加载子应用后页面挂载逻辑 single-spa-config/single-spa-vue.ts 此处为single-spa-vue.js文件源码,在源码基础上做了优化处理,子应用的内容如果需要keep-alive缓存,则实例的display改为block, 由于篇幅问题,我只写出修改的部分,具体的源码可以查看官方文档single-spa-vue.js https://github.com/single-spa/single-spa-vue/blob/main/src/single-spa-vue.js ###### 1)mount函数的修改: ```javascript /** * 挂载应用的时候触发 * @param opts * @param mountedInstances * @param props * @returns */ function mount(opts: any, mountedInstances: any, props: any) { const instance: any = {} return Promise.resolve().then(() => { return resolveAppOptions(opts, props).then((appOptions: any) => { const oldMountedInstances: any = mountedInstances?.[props.name] // 挂载的时候如果需要保持原有状态,此处做特殊处理。如果dom没有内容,就走初始化逻辑 if (opts?.isKeepAlive && oldMountedInstances?.vueInstance && oldMountedInstances?.domEl?.innerHTML) { oldMountedInstances.domEl.style.display = 'block' return oldMountedInstances.vueInstance } // ...... return instance.vueInstance }) }) } ``` ###### 2)unmount函数的修改: 页面卸载时判断子应用是否需要keep-alive,如果需要则实例的display改为none ```javascript /** * 卸载应用的时候处理 * @param opts * @param mountedInstances * @param props * @returns */ function unmount(opts: any, mountedInstances: any, props: any) { return Promise.resolve().then(() => { const instance = mountedInstances[props.name] // 卸载的时候判断是否需要保持原始状态,如果需要,此处只做隐藏处理 if (opts?.isKeepAlive && instance?.domEl) { instance.domEl.style.display = 'none' } else { // ...... } }) } ``` #### 4.3 主应用修改 ##### 4.3.1 main.ts文件修改 ```javascript import Vue from 'vue' import store, { rootStoreModule } from '@/store' import router from './router' import singleSpaVue from '@jxd-frame/single-spa-config/single-spa-vue' Vue.config.productionTip = false const appOptions: any = { router, // 挂载store store, render: (h: (arg0: any) => any) => h(App) } // 父应用环境变量中增加 VUE_APP_PARENT = true 子应用的 VUE_APP_PARENT = false // 是否为父应用 // isParentApp() { // return process.env.VUE_APP_PARENT === 'true' // }, // 是否为独立app // isChildAloneApp() { // return !window.singleSpaNavigate && process.env.VUE_APP_PARENT === 'false' // }, // 如果不是single-spa模式 if (utils.isParentApp() || utils.isChildAloneApp()) { new Vue(appOptions).$mount('#app') } else { // 子应用模式 appOptions.el = '#app' } // singleSpaVue包装一个vue微前端服务对象 const vueLifecycles: any = singleSpaVue({ Vue, appOptions }) // 导出生命周期对象 export const bootstrap = vueLifecycles.bootstrap // 启动时 export const mount = vueLifecycles.mount // 挂载时 export const unmount = vueLifecycles.unmount // 卸载时 export default vueLifecycles ``` ##### 4.3.2 视图区 在view-router视图容器同级增加下面标签,子应用加载成功后内容会显示在id="ordermanager"容器内 ```javascript <template v-if="utils.isChildAloneApp() || utils.isParentApp()"> // 用于显示子应用内容 <div id="ordermanager" key="ordermanager"/> </template> ``` ##### 4.3.3 路由 主应用的路由要去掉single-spa-config菜单中activeWhen 参数中配置的路由,这样主应用切换路由后请求子应用加载的页面才是唯一的,否则主应用的路由会加载子应用的路由也会加载。 #### 4.4 子应用修改 ##### 4.4.1 vue.config.js 1)增加 stats-webpack-plugin 插件,将资源清单文件名称打包为mainfest.json,这样在主应用加载子应用时可以直接访问这个文件 stats-webpack-plugin 插件能够生成资源清单文件,被用于一些前端项目中,这个清单文件有两个用途: - 让服务器端能够根据Webpack配置的入口名称找到资源文件路径 - 在线上环境出现问题需要回滚时,可以通过回退清单文件来快速将前端资源切回上个版本,无需花时间重新构建前端资源 2)configureWebpack中增加output配置 ```javascript const StatsPlugin = require('stats-webpack-plugin') module.exports = { // 基本路径 此地址为调试模式使用,非调试模式 / 即可 publicPath: '//workbench.zhongyouex.com:2031/', configureWebpack: { // devtool: 'none', // 不打包sourcemap output: { library: 'ordermanage', // 导出名称 与single-spa中的name一致 libraryTarget: 'window' // 挂载目标 } }, // 调整内部的 webpack 配置。 chainWebpack: (config) => { config.plugin('ManifestPlugin').use(new StatsPlugin('manifest.json', { chunkModules: false, entrypoints: true, source: false, chunks: false, modules: false, assets: false, children: false, exclude: [/node_modules/] })) } } ``` ##### 4.4.2 main.ts 代码与主应用一致,唯一区别处为挂载id ```javascript // 如果不是single-spa模式 if (utils.isParentApp() || utils.isChildAloneApp()) { new Vue(appOptions).$mount('#app') } else { // 这里与single-spa的name保持一致 appOptions.el = '#ordermanage' } ``` 5 总结 以上为京喜达工作台微前端拆分总结,代码只总结了核心部分,具体的工程类逻辑没有体现,这次拆分只是将几个组的业务拆分开,能独立部署与集成部署,但是还存在很多不足,如子应用公共部分如何共用一份,主应用加载子应用时会有等待时间等,问题还有很多,我们后续也会持续优化,也欢迎大家一起交流探讨。 ------------ ###### 自猿其说Tech-京东物流技术发展部 ###### 作者:宁冲 张艳娟
原创文章,需联系作者,授权转载
上一篇:ClickHouse与Elasticsearch压测实践
下一篇:一个低代码报表配置平台实践
相关文章
前端十年回顾 | 漫画前端的前世今生
Taro小程序跨端开发入门实战
【技术干货】企业级扫描平台EOS关于JS扫描落地与实践!
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号