2022年07月06日 天气晴
import { Route, RouteProps } from 'react-router-dom'
import useCurrentProduct from '@/hooks/useCurrentProduct'
import { hasPermission } from '@/apis/product'
import { useAsync } from 'react-use'
import { useRecoilValue, useSetRecoilState } from 'recoil'
import { curEnvData, microAppSuspense } from '@/recoil/global'
import { isNoEmptyObj } from '@/utils'
import NoAuth from '../NoAuth/NoAuth'
// 单例
let prev = ''
interface PermissionResponse {
hasRole: boolean
users: Array<PermissionUser>
authUrl: string | null
}
export function ProxyRoute(props: RouteProps) {
const curProduct = useCurrentProduct()
const envData = useRecoilValue(curEnvData)
const toggleSuspense = useSetRecoilState(microAppSuspense)
const { value } = useAsync(async() => {
if (prev !== curProduct?.key && isNoEmptyObj(envData)) {
prev = curProduct.key
if (curProduct?.backstageCode) {
const env = Reflect.get(envData, 'key')
if (env) {
try {
const { hasRole, users, authUrl } = await hasPermission<PermissionResponse>({
backstageCode: curProduct?.backstageCode!,
appCode: curProduct?.key,
env: env
})
const userList = users?.map((user) => {
return {
openId: user.openId,
realName: user.realName,
userName: user.userName
}
})
if (!hasRole) {
const data = {
env,
userInfo: userList,
authUrl
}
toggleSuspense(false)//备注:!!!这里是因为在挂载子应用时有一个生命周期,当生命周期结束时toggleSuspense设置为false,但是此时用户无权限,也就不需要挂载子应用,所以直接将toggleSuspense设置为false
return <NoAuth data={data} />
}
} catch (e) { }
}
}
}
}, [curProduct, envData])
return (value || <Route {...props} />)
}
微前端应用拦截时,有的用户没有权限,当hasRole为false时证明用户没有权限,最开始的写法:
if (!hasRole)
{
const data = {
env,
userInfo: userList,
authUrl
}
const params = encodeURI(encodeURI(JSON.stringify(data)))
return <Redirect to={`/noauth?authdata=${params}`} />
}
最开始是将无权限页面所需要的信息通过路由参数传递过去,但是当无权限时跳转到的无权限页面路由就没有了标识子应用的appCode,所以无法得知当前是什么子应用,也就无法加载该子应用的环境列表。(这种方法真是又难用又不对啊!!哈哈哈哈哈!) 最开始想法:将noauth路由中加上当前子应用对应的appCode,但是这样很麻烦,而且无法排除子应用也可能会有、noauth/${appCode}这样的路径,这种情况下就无法确定是加载子应用的路由还是加载基座应用的路由。 后来改成直接返回响应的无权限组件NoAuth,并将无权限组件需要的参数通过props传递。
此处为子应用加载生命周期:
import { useCallback, useEffect, useMemo, useState } from 'react'
import { plugins, fetchType, lifeCyclesType } from '@micro-app/types'
import { isDev, microAppSuspense } from '@/recoil'
import { useSetRecoilState, useRecoilValue } from 'recoil'
import useCurrentProduct from './useCurrentProduct'
import { baseAppSubject, createMapObject, microAppDebug, isNoEmptyObj } from '@/utils'
const FLAG = '${项目名}'//此处为项目名称
// const microAppHomeReg = /^.+cloudFront\/\w+\/\w+\/(.*\.\w*)/
const microAppHomeReg = /\/$/
function noMicroAppHtmlText() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>
</head>
<body style="display: flex; align-items: center; justify-content: center;">
<lottie-player
src="https://assets6.lottiefiles.com/temp/lf20_dzWAyu.json"
background="transparent"
speed="1"
style="width: 80vw; height: 80vh;"
loop
autoplay
>
</lottie-player>
</body>
</html>`
}
const replaceCdn = (html: string) => {
const srcReg = /src="[^"]+"/g
const cdnFlag = 'https://cdn.jsdelivr.net/npm/'
const matched = html.match(srcReg)
if (matched) {
try {
matched.forEach(text => {
if (text.includes(cdnFlag)) {
const address = text.slice(5, -1)
const [pkg, version] = address.replace(cdnFlag, '').split('/')?.[0]?.split('@')
html = html.replace(address, `https://cdn.bootcdn.net/ajax/libs/${pkg}/${version}/${pkg}.min.js`)
}
})
} catch {}
}
return html
}
export function useMicroApp() {
const dev = useRecoilValue(isDev)
const curProduct = useCurrentProduct()
const toggleSuspense = useSetRecoilState(microAppSuspense)
const [modules, setModules] = useState(createMapObject<Record<keyof AppMap, NonNullable<plugins['modules']>[string]>>())
const fetch: fetchType = useCallback(async(url) => {
// 将子应用的log-monitor方法拦截
if (url.includes('log-monitor')) return ''
try {
// 此fetch不止用于加载html,所有静态资源都会通过这个来加载,因此通过正则匹配是否是静态文件
if (!microAppHomeReg.test(url)) {
return await window.fetch(url).then(res => res.text())
} else {
toggleSuspense(true)
// catch 兼容伪路径
const html2text = await window.fetch(url).then(res => res.text()).catch(() => FLAG)
if (!html2text.includes(FLAG)) {
// 兼容jsdelivr cdn
return replaceCdn(html2text)
} else {
return noMicroAppHtmlText()
}
}
} catch (e) {
return noMicroAppHtmlText()
}
}, [])
const localViteLoader: (app: AppConfig) => (typeof modules)[keyof AppMap][number]['loader'] = (app) => {
const key = app.key
return (code, url) => {
if (dev) {
code = code.replace(RegExp(`(from|import)(\\s*['"])(/${key}/)`, 'g'), (all) => {
//! 如果本地开发地址404,修改这儿 `${app.devOptions.main}/${key}`
return all.replace(`/${key}/`, `${app.devOptions.main}`)
})
if (/(import\()(\s*['"])(\.\.?\/)/g.test(code)) {
code = code.replace(/(import\()(\s*['"])(\.\.?\/)/g, (all, $1, $2, $3) => {
return all.replace($3, `${app.devOptions.main}${key}/`)
})
}
if (/@vite\/client$/.test(url)) {
code = code.replace(
`const socket = new WebSocket(\`\${socketProtocol}://\${socketHost}\`, 'vite-hmr');`,
`const socket = new WebSocket(\`${app.devOptions.main.replace(/^http/, 'ws')}${app.key
}/\`, 'vite-hmr');`
)
}
}
return code
}
}
const lifeCycles = useMemo(() => {
const lifecycleNames = ['created', 'beforemount', 'mounted', 'unmount', 'error'] as const
return lifecycleNames.reduce((ret, cur) => {
Reflect.set(ret, cur, (e: LifeCyclesEvent) => {
if (cur === 'mounted') {
toggleSuspense(false)//正常来说这里子应用到挂载阶段,才将toggle关闭(关闭loading)
}
baseAppSubject.next({
lifecycle: cur,
microApp: {
key: e.detail.name
}
})
})
return ret
}, createMapObject<lifeCyclesType>())
}, [baseAppSubject])
useEffect(() => {
if (isNoEmptyObj(curProduct)) {
setModules({
[curProduct.key]: [{
loader: curProduct.devOptions.microAppOptions?.inline
? localViteLoader(curProduct)
: (v: string) => v
}]
} as typeof modules)
}
}, [curProduct])
useEffect(() => {
microAppDebug('modules changed >>> %o', modules)
}, [modules])
return [modules, {
fetch,
lifeCycles
}] as const
}
总结:从问题的根本出发,不过是不同情况加载不同的组件。