vue2
+ js
重构完成干预效果分析模块,且公司前端组件库目前维护了 vue2
组件库(集采、抗网、干预分析、新患教、处方评估模块已配套使用), vue3
组件库(处方点评配套使用),后续需求开发页面中已存在多数已经从点评剥离出来的且干预重构模块可以直接使用的组件在 vue3
组件库中,但此时已使用 vue2
组件库开发完成了分析模块以及项目整体的布局(侧边栏、全局状态管理、顶部导航栏、路由管理等);此时,存在以下几种方案:vue2
项目重构为 vue3
,或者将 vue3
组件库中的代码重构成 vue2
iframe
,开发两个项目,两个项目通过 iframe
发送信息进行交互、嵌套为什么不使用重构代码的方式
vue2
改成 vue3
,业务代码的改造工作量不大,主要是 vue2/vue3
使用的脚手架不可能是一致的,这样我们各自在将布局例如消息盒子、更多组件、侧边栏、顶部栏分别封装入 vue2/vue3
组件库时,写法肯定也是不一致的, vue2
改 vue3
等于是所有组件库中的组件重写、重新测试,时间成本过大;如若 vue3
组件改成vue2的写法后放入 vue2
组件库其实也是成本原因,如若有更好的方法,谁也不愿意多写一次代码。当然使用该种方法也是有好处,便于后续维护,能让别人看的清晰。为什么不使用iframe
iframe
,再去维护一个项目,这不是不合理的,甚至是最好的解决方案:不论是样式隔离、 js
隔离这类问题统统都能被完美解决;唯二的成本就是需要了解 iframe
的各种通信方式,以及无法应对各种交互时的力不从心; iframe
由于安全性考虑,并没有提供足够的权限获得交互方面的体验,如若后续增加较为繁琐的需求,怕是要推倒重来,以下列举为什么不选择 iframe
的核心点:
url
不同步。浏览器刷新 iframe url
状态丢失、后退前进按钮无法使用UI
不同步, DOM
结构不共享。想象一下屏幕右下角 1/4 的 iframe
里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize
时自动居中iframe
内外系统的通信、数据同步等需求,主应用的 cookie
要透传到根域名都不同的子应用中实现免登效果。为什么选择乾坤框架
vue2 + js + 内部vue2组件库
,剩余未改造模块计划采用 vue3 + ts + 内部vue3组件库
改造,该种情况满足乾坤的1对多模式:1个基座、多个子项目,虽然当前只存在一个子项目,且改子项目无单独部署、单独打包逻辑,所以无需多创建一个项目增加维护成本目录格式的确定:vue2项目作为基座,所以无需任何目录变动,只需要在根目录增加子项目的目录,例: sub
,故结构为 web-intervention/sub
基座中安装 qiankun
npm i qiankun -S
基座中需要腾出子应用的空间dom
:
src/layout/components/AppMain.vue
// 默认展示区域
// 通过乾坤进入的页面 只有在路由匹配时显示 且填入唯一的id便于后续注册
基座的路由改造:目前干预是只取了干预分析的路由进行 push
,后续可以针对除了干预分析模块其他的路由前端处理手动添加一层 sub/
,方便乾坤项目匹配,当然如果后端给的话那更好,主要是为了和基座路由进行区分并被乾坤解析
src/router/qiankun.js
{
path: '/sub/*',
name: 'sub',
component: () => import('@/layout/index')
}
接下来我们可以进行乾坤的注册
src/qiankun.js
// web-intervention/qiankun.js
import store from '@/store'
import { registerMicroApps } from 'qiankun'
/** location.pathname是否存在 routerPrefix */
function genActiveRule(routerPrefix, currentRoute = '') {
return location => location.pathname.indexOf(routerPrefix) !== -1
}
// 主应用共享出去的数据
const msg = {
state: store.state,
isQiankun: true,
system: 'main'
}
// 注册子应用
registerMicroApps(
[
{
/**
* name: 子服务有唯一性 - 这个需要与子服务webpack name一致
* entry: 子服务入口 - 通过该地址加载微应用
* container: 子服务挂载节点 - 微应用加载完成后将挂载在该节点上 - 与上述id一致
* activeRule: 子服务触发的路由规则 - 触发路由规则后将加载该微应用 - 与上述创建子服务路由前缀一致
* props 共享数据到子服务
* sandbox 开启沙箱
*/
name: 'sub', // 根据实际情况来
entry: process.env.NODE_ENV === 'production' ? '/pgintervene/sub/' : `//localhost:10200`,
container: '#sub',
activeRule: genActiveRule('/sub'),
props: msg, // 共享数据到子服务
sandbox: {
strictStyleIsolation: true
}
}
],
{
// 挂载前回调
beforeLoad: [
app => {
console.log('before load', app)
}
],
// 挂载后回调
beforeMount: [
app => {
console.log('before mount', app)
}
],
// 卸载后回调
afterUnmount: [
app => {
console.log('after unload', app)
}
]
}
)
从代码中我们可以看出注册函数在子模块在匹配到 sub
路由时,将子模块挂载到了 id
为 sub
的 dom
元素下,并且将基座的全局状态 store
(可能存在用户名、token
等)下发到了子模块,当然这里做了区分,如果是开发环境,子模块就是10200
端口启动的项目,否则生产环境是 /pgintervene/sub/
下的内容, pgintervene
是线上地址的前缀,也就是打包出来的包名
基座中引入注册文件
src/main.js
import './qiankun'
子项目增加public-path.ts并在main.ts中引用
src/public-path.ts
// @src/public-path.js
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line camelcase
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
子项目的配置,其中主要是 vue.config.js
与 main.ts
的修改
sub/src/main.ts
let instance = null
let Router = null
const baseUrl = process.env.NODE_ENV === 'production' ? '/pgintervene/sub' : '/sub'
function render(props : any = {}) {
const { container } = props
// 如果是乾坤环境
if (window.__POWERED_BY_QIANKUN__) {
const routes = router.routes
// 在 render 中创建 VueRouter,可以保证在卸载微应用时,移除 location 事件监听,防止事件污染
const base = window.__POWERED_BY_QIANKUN__ ? baseUrl : '/'
Router = createRouter({
history: createWebHistory(base),
routes,
})
instance = createApp(App)
.use(Router)
.use(ElementPlus)
.mount(container ? container.querySelector('#app') : '#app')
} else {
instance = createApp(App).use(router.router).use(ElementPlus).mount('#app')
}
}
// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
main.ts
中主要是为了区分三种情况进行区分
生产环境路由的前缀是 /pgintervene/sub
开发环境如果是从基座进入子项目路由前缀是 /sub
开发环境如果是直接打开子项目直接 render
且路由前缀直接就是 /
当然如果采取分包模式就不需要我这种方法了, nginx
做映射就好
其中还存在一些生命周期:
bootstrap
:只会在微应用初始化的时候调用一次,通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount
阶段被销毁的应用级别的缓存等;
mount
:应用每次进入都会调用 mount
方法,通常我们在这里触发应用的渲染方法;
unmount
:应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
vue.config.js
主要是为了解决打包的问题
publicPath
需要区分环境,代表的是文件的引用地址,打包改造主要也是和项目的目录接口相似,采用嵌套的模式:pgintervene/sub
,所以引用地址生产环境需要注意加上前置包名,开发环境就是当前微服务运行的环境即可,这样基座也不会丢失子模块的静态文件
sub/vue.config.js
publicPath: process.env.NODE_ENV === 'production' ? '/pgintervene/sub/' : `//localhost:${port}`,
outputDir
打包文件目录,根据当前目录设计,需要打包在基座打包文件的sub文件中
sub/vue.config.js
outputDir: '../pgintervene/sub',
devServer
本地代理中需要加入跨域头
sub/vue.config.js
headers: {
'Access-Control-Allow-Origin': '*',
},
configureWebpack
中需要加入官方配置
sub/vue.config.js
configureWebpack: {
output: {
// 子服务的包名,这里与主应用中注册的微应用名称一致
library: 'sub', // 根据实际情况来
// 将你的 library 暴露为所有的模块定义下都可运行的方式
libraryTarget: 'umd',
// 按需加载相关,设置为 webpackJsonp_子服务名称 即可
jsonpFunction: `webpackJsonp_sub`, // 根据实际情况来
},
}
至次,你应该能通过启动基座与子项目后,点击基座的 sub
菜单看到子模块了,但是项目中还存在很多问题需要修正。
打包优化、自动化优化:
由于项目未采用分包,基座项目原先采用的是 gitlab
的自动化,子项目打包在 config.js
中配置是打入基座中,所以无需在自动化再进行文件操作,我们只需改造基座的打包命令,分别代表了打包当前基座,打包子项目与同时打包基座与子项目
package.json
"build": "vue-cli-service build",
"build:sub": "cd sub && npm run build",
"build:prod": "vue-cli-service build && npm run build:sub",
自动化只需要修改npm 依赖包的安装位置
.gitlab-ci.yml
# 安装基座依赖
- npm install --registry=http://10.1.1.161:4837
- cd sub
# 安装子项目依赖
- npm install --registry=http://10.1.1.161:4837
- cd ..
- npm run build:prod
这样修改的好处就是每个工具都各尽其职,无需在 yml
中写过于繁琐的逻辑,且本地与云端打包逻辑完全一致,简单易懂, webpack
、 npm
、 yml
各自完成自己的一小部分即可
项目启动命令优化,项目开发大部分场景是要基座与子项目同时启动的,每次去切换目录进行启动过于繁琐,npm并不支持同时启动两个 node
项目,使用 concurrently
插件能解决不能同时启动的问题,与打包相同,存在三个不同功能的命令,注意该插件目前最新版存在解析问题,当前项目中退回到了早些版本
package.json
"start": "vue-cli-service serve",
"start:sub": "cd sub && npm run start",
"start:prod": "concurrently -r \"npm run start\" \"npm run start:sub\"",
样式隔离:
由于我们的组件库 vue2
是基于 element-ui
搭建,vue3
是基于 element-plus
搭建,存在大量相同命名的样式 class
,在一起运行时,样式或因层级优先不一致导致覆盖,经测试使用乾坤沙箱模式并无法解决父子关系的样式污染,只能解决子与子项目的污染,目前我们 vue3
组件库的 element-plus
版本过低还不支持重写 el
前缀,且即使修改了前缀也许我们的组件库并没有按照组件库原有的设计规范书写组件,故通过 postcss-change-css-prefix
插件修改基座组件样式前缀,内部应该是正则所有el前缀的样式进行替换,无需其他任何修改,经测试,能解决 element-ui
与 plus
的样式污染问题
postcss.config.js
const addCssPrefix = require('postcss-change-css-prefix')
module.exports = {
plugins: [
addCssPrefix({
prefix: 'el-',
replace: 'gp-'
})
]
}
webpack (vue.config.js)
module: {
rules: [
{
test: /\.js$/,
include: [
path.resolve(__dirname, './node_modules/web-vue2-front-end-lib/lib')
],
use: [
{
loader: 'change-prefix-loader',
options: {
prefix: 'el-',
replace: 'gp-'
}
}
]
}
]
}
lint
规范优化:
由于官方不再维护且推荐使用 eslint
,首先移除了子项目的 tslint
,基座本来使用的就是 eslint
,但配置的是js的规范,由于 eslint
的特性,子目录也会向上寻找 eslint
的规则,导致 ts
报了很多错,后续采用子模块单独安装 eslint
,且使用 root
限定范围,基座使用 eslint
的 js
规范(规则模仿了 element-ui
风格),子模块采用 eslint
的 ts
规范( element-plus
的风格)
node
版本同步
由于一些依赖兼容的 node
版本不一致,不可能运行一次就切换一次 node
版本,当然有较好的方案: nvm+bash
命令,通过不同项目目录定义 .nvmrc
限定 node
版本,进入终端通过 bash
命令自动化切换,但后续通过降低依赖版本解决了,该方案就没用到
原文链接:https://blog.stride.fun/blogs/category1/2023/61201.html