大家好, 最近换到了新部门,在做智能平台相关的内容。我接到的第一个任务就是把以前前端的项目重构一次。
说是重构,不如说是重写一遍。因为原来的项目是 ant-design-vue + vue 全家桶
,要切换成 ant-design + ant-design-pro + react 全家桶
。
更让人头疼的是,产品经理并不会让我们有大把大把时间专门搞重构,我们要边重构边做需求。在这样的挑战下,我想到了微前端解决方案,下面就跟大家分享这次 微前端在重构上的落地实践吧。
这次实践我简化了一下,放在 Github 上,大家可以自行 clone 来玩玩。
首先,来讲讲技术栈,老项目主要用了下面的技术:
框架
Vue
vuex
vue-router
样式
scss
UI
ant-design-vue
ant-design-pro for vue
脚手架
vue-cli
新项目需要用到的技术有:
框架
React
redux + redux-toolkit
react-router
新式
less
UI
react-design-react
react-design-pro for react
脚手架
团队内部自创脚手架
可以看到两个项目除了业务之外,几乎没什么交集了。
老项目作为主应用,通过 qiankun 去加载新项目(子应用)里的页面。
这样一来就可以避免 “我要一整个月都做重构” 的局面,而是可以做到一个页面一个页地慢慢迁移。最终等所有页面都在新项目写好之后,直接把老项目下掉,新项目就可以从幕后站出来了。相当于从重写的第一天开始,老项目就成替身了。
如果只看上面画的架构图,会觉得:啊,不就引入一个 qiankun 就完事了么?实际上还有很细节和问题需要注意的。
上图的架构有一个问题就是,当每次点击侧边栏的 MenuItem
时,都会加载一次微应用的子页面,也即:
微应用子页面之间的切换,其实就是在微应用里路由切换嘛,大可不需要通过重新加载一次微应用来做微应用子页面的切换。
所以,我想了一个办法:我在 <router-view>
旁边放了一个组件 Container
。进入主应用后,这个组件先直接把微应用整个都加载了。
<a-layout>
<!-- 页面 -->
<a-layout-content>
<!-- 子应用容器 -->
<micro-app-container></micro-app-container>
<!-- 主应用路由 -->
<router-view/>
</a-layout-content>
</a-layout>
当展示老页面时,把这个 Container
高度设为 0
,要展示新页面时,再把 Container
高度自动撑开。
// micro-app-container
<template>
<div class="container" :style="{ height: visible ? '100%' : 0 }">
<div id="micro-app-container"></div>
</div>
</template>
<script>
import { registerMicroApps, start } from 'qiankun'
export default {
name: "Container",
props: {
visible: {
type: Boolean,
defaultValue: false,
}
},
mounted() {
registerMicroApps([
{
name: 'microReactApp',
entry: '//localhost:3000',
container: '#micro-app-container',
activeRule: '/#/micro-react-app',
},
])
start()
},
}
</script>
这样一来,当进入老项目时,这个 Container
自动被 mounted 后就会地去加载子应用了。当在切换新页面时,本质上是在子应用里做路由切换,而不是从 A 应用切换到 B 应用了。
由于新的项目(子应用)里的页面要供给老项目(主应用)来使用的,所以子应用也应该有两套布局:
第一套标准的管理后台布局,有 Sider
,Header
还有 Content
,另一套侧作为子应用时,只展示 Content
部分的布局。
// 单独运行时的布局
export const StandaloneLayout: FC = () => {
return (
<AntLayout className={styles.layout}>
<Sider/>
<AntLayout>
<Header />
<Content />
</AntLayout>
</AntLayout>
)
}
// 作为子应用时的布局
export const MicroAppLayout = () => {
return (
<Content />
)
}
单独运行时的布局
作为微应用时的布局
最后通过 window.__POWERED_BY_QIANKUN__
就可以切换不同的布局了。
import { StandaloneLayout, MicroAppLayout } from "./components/Layout";
const Layout = window.__POWERED_BY_QIANKUN__ ? MicroAppLayout : StandaloneLayout;
function App() {
return (
<Layout/>
);
}
qiankun 是默认开启 JS 隔离(沙箱),关闭 CSS 样式隔离的。为什么这么做呢?因为 CSS 的隔离是不能无脑做去做的,下面来讲讲这方面的问题。
qiankun 一共提供了两种 CSS 隔离方法(沙箱):严格沙箱 以及 实验性沙箱。
开启代码:
start({
sandbox: {
strictStyleIsolation: true,
}
})
严格沙箱主要通过 ShadowDOM
来实现 CSS 样式隔离,效果是当子应用被挂在到 ShadowDOM
上,主子应用的样式 完完全全 地被隔离,无法互相影响。你说:这不是很好么?No No No。
这种沙箱的优点也成为了它自己的缺点:除了样式的硬隔离,DOM 元素也直接硬隔离了,导致子应用的一些 Modal
、Popover
、Drawer
组件会因为找不到主应用的 body
而丢失,甚至跑到整个屏幕之外。
还记得我刚说主应用和子应用都用了 ant-design 么?ant-design 的 Modal
、Popover``Drawer
的实现方式就是要挂在到 document.body
上的,这么一隔离,它们一挂在整个元素起飞了。
开启代码:
start({
sandbox: {
experimentalStyleIsolation: true,
}
})
这种沙箱实现方式就是给子应用的样式加后缀标签,有点像 Vue 里的 scoped
,通过名字来做样式 “软隔离”,比如像这样:
其实这种方式已经很好地做了样式隔离,但是主应用里经常有人喜欢写 !important
来覆盖 ant-design 的组件原样式:
.ant-xxx {
color: white: !important;
}
而 !importnant
的优先级是最高的,如果微应用也用了这个 .ant-xxx
类,就很容易被主应用的样式影响了。所以在加载微应用时,还需要处理 ant-design 之间的样式冲突问题。
ant-design 提供了一个非常好的类名前缀功能:用 prefixCls
来做样式隔离,我自然也用上了:
// 自定义前缀
const prefixCls = 'cmsAnt';
// 设置 Modal、Message、Notification rootPrefixCls
ConfigProvider.config({
prefixCls,
})
// 渲染
function render(props: any) {
const { container, state, commit, dispatch } = props;
const value = { state, commit, dispatch };
const root = (
<ConfigProvider prefixCls={prefixCls}>
<HashRouter basename={basename}>
<MicroAppContext.Provider value={value}>
<App />
</MicroAppContext.Provider>
</HashRouter>
</ConfigProvider>
);
ReactDOM.render(root, container
? container.querySelector('#root')
: document.querySelector('#root'));
}
@ant-prefix: cmsAnt; // 引入来改变全局变量值
但是不知道为什么,在 less 文件中改了 ant-prefix
变量后,ant-design-pro 的样式还是老样子,有的组件样式改变了,有的没变化。
最后,我是通过 less-loader
的 modifyVars
在打包时来更新全局的 ant-prefix
less 变量才搞定的:
var webpackConfig = {
test: /.(less)$/,
use: [
...
{
loader: 'less-loader',
options: {
lessOptions: {
modifyVars: {
'ant-prefix': 'cmsAnt'
},
sourceMap: true,
javascriptEnabled: true,
}
}
}
]
}
具体 Issue 看 Issue: ant-design 改了 prefixCls 后 ant-design-pro 不生效。
老项目(主应用)用到了 vuex 全局状态管理,所以新项目页面(子应用)里有时需要更改主应用里的状态,这里我用了 qiankun 的 globalState 来处理。
首先在 Container 里创建了 globalActions
,再监听 vuex 状态变更,每次变更都通知子应用,同时把 vuex 的 commit
和 dispatch
函数传给子应用:
import {initGlobalState, registerMicroApps, start} from 'qiankun'
const globalActions = initGlobalState({
state: {},
commit: null,
dispatch: null,
});
export default {
name: "Container",
props: {
visible: {
type: Boolean,
defaultValue: false,
}
},
mounted() {
const { dispatch, commit, state } = this.$store;
registerMicroApps([
{
name: 'microReactApp',
entry: '//localhost:3000',
container: '#micro-app-container',
activeRule: '/#/micro-react-app',
// 初始化时就传入主应用的状态和 commit, dispatch
props: {
state,
dispatch,
commit,
}
},
])
start()
// vuex 的 store 变更后再次传入主应用的状态和 commit, dispatch
this.$store.watch((state) => {
console.log('state', state);
globalActions.setGlobalState({
state,
commit,
dispatch
});
})
},
}
子应用里接收主应用传来的 state
,commit
以及 dispatch
函数,同时新起一个 Context,把这些东西都放到 MicroAppContext
里。(Redux 因为不支持存放函数这种 nonserializable 的值,所以只能先存到 Context 里)
// 渲染
function render(props: any) {
const { container, state, commit, dispatch } = props;
const value = { state, commit, dispatch };
const root = (
<HashRouter basename={basename}>
<MicroAppContext.Provider value={value}>
<App />
</MicroAppContext.Provider>
</HashRouter>
);
ReactDOM.render(root, container
? container.querySelector('#root')
: document.querySelector('#root'));
}
// mount 时监听 globalState,只要一改再次渲染 App
export async function mount(props: any) {
console.log('[micro-react-app] mount', props);
props.onGlobalStateChange((state: any) => {
console.log('[micro-react-app] vuex 状态更新')
render(state);
})
render(props);
}
这样一来,子应用也可以通过 commit
,和 dispatch
来更改主应用的值了。
const OrderList: FC = () => {
const { state, commit } = useContext(MicroAppContext);
return (
<div>
<h1 className="title">【微应用】订单列表</h1>
<div>
<p>主应用的 Counter: {state.counter}</p>
<Button type="primary" onClick={() => commit('increment')}>【微应用】+1</Button>
<Button danger onClick={() => commit('decrement')}>【微应用】-1</Button>
</div>
</div>
)
}
当然了,这样的实践也是我自己 “发明” 的,不知道这是不是一个好的实践,我只能说这样能 Work。
另一个问题就是当子应用隐式使用全局变量时,import-html-entry
执行 JS 时会直接爆炸。比如微应用有如下 <script>
的代码:
var x = {}; // 报错,要改成 window.x = {};
x.a = 1 // 报错,要改成 window.x.a = 1;
function a() {} // 要改成 window.a = () => {}
a() // 报错,要改成 window.a()
在主应用加载微应用后,上面的 x
和 a
全都会报 xxx is undefined
,这是因为 qiankun 在加载微应用时,会执行这部分 JS 代码,而此时 var 声明的变量不再是全局变量,其他的文件无法获取到。
解决方法就是使用 window.xxx
来显式定义/使用全局变量。具体可见 Issue: 子应用全局变量 undefined
只要主子应用都用上了 Hash 路由,那么很大概率会遇到这个问题。
比如你主应用有 /micro-app/home
和 /micro-app/user
两个路由,actvieRule
为 /#/micro-app
,子应用也有对应的 /micro-app/home
和 /micro-app/user
两个路由。
那么如果 在主应用里 从 /micro-app/home
切换到 /micro-app/user
,会发现子应用的路由并没有改变。但如果你 在主应用的子应用里 去切换,那么就能切换成功。
这是因为在主应用切换路由时不是通过 location.url
这种可以触发 hash change 事件的方式来变更路由,而 react-router 只监听了 hash change 事件,所以当主应用切换路由时,没有触发 hash change 事件,导致子应用的监听不到路由变化,也就不会做页面切换了。
具体可见:Issue: 加载子应用正常,但主应用切换路由,子应用不跳转,浏览器返回前进可触发子应用跳转。
解决方法很简单,下面三选一:
主应用在加载子应用时还是需要不少时间的,所以最好要展示一个加载中的状态,qiankun 正好提供了一个 loader
回调来让我们控制子应用的加载状态:
<div class="container" :style="{ height: visible ? '100%' : 0 }">
<a-spin v-if="loading"></a-spin>
<div id="micro-app-container"></div>
</div>
registerMicroApps([
{
name: 'microReactApp',
entry: '//localhost:3000',
container: '#micro-app-container',
activeRule: '/#/micro-react-app',
props: {
state,
dispatch,
commit,
},
loader: (loading) => {
this.loading = loading // 控制加载状态
}
},
])
start()
总的来说,微前端在解构巨石应用的帮助真的很大。像我们这种要重构整个应用的情况,部门肯定不会先暂停业务,给开发一整个月来专门重构的,只能在评新需求的时候多给你一两天时间而已。
微前端就可以解决重构的过程中边做新需求边重构的问题,使得新老页面都能共存,不会一下子整个业务都停掉来做重构工作。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/kJZ90-dU02bZFVtUsUoldQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。