@tuya-sat/medusa
v0.9.0
Published
Micro frontend framework, support qiankun/ice-stark/next.js/micro-zoe/cra
Downloads
1
Readme
一体化的微前端框架,支持绝大部分流行的微前端技术
安装 yarn add @tuya-sat/medusa
目前支持的框架
| 模块 | 进度 | | ---- | ---- | | next.js(原生next.js) | 已支持 | | ice-stark(飞冰) | 已支持 | | qiankun(乾坤) | 已支持 | | micro-zoe(京东微前端) | 已支持 | | webpack5模块联邦 | 已支持 |
Q&A (尽量使用新版,目前接入项目增多,经常性会针对性的做一些更新)
请尽量避免在子项目中使用document.createElement('script')这种方式来动态引入远程脚本
动态添加的script标签在加载完成之后它的运行上下文就会变成全局,沙箱就会失效。美杜莎会捕获head和body里面的这一行为,重新放入沙箱,所以你非要使用的话,请append到head或body中
为什么本地开发好好的,已发布到日常就访问不了,报xx.forEach(xxx
错误
目前大概出现的有如下几种原因:
- 子项目无法访问
- 子项目跨域了
- 子项目配置了envTag。
- 子项目配置了sso,被redirect到登录页去了
使用说明
0.8.42增加模块联邦的hack配置
- 主项目使用
webpack.config.js
const {mfHackWebpack} = require('@tuya-sat/medusa/lib/mf-hack-webpack')
module.exports = (config, isDev, isServer) => {
...
mfHackWebpack(config, isDev, isServer)({
name: 'host',
remotes: {
// remote地址根据开发,日常,预发,线上
remote: isDev ? 'http://localhost:3118/_next/static/remoteEntry.js' :
`https://static1.tuyacn.com/static/${子项目AppID}/_next/static/${env(环境)}/remoteEntry.js`
}
})
return config
}
home.tsx
const RemoteApp = React.lazy<React.FC<any>>(() => import('remote/Module1'))
const AttendanceApp: React.FC<any> = (props) => {
if (isServer) {
return (
<div />
)
}
return (
<Suspense fallback={'loading...'}>
<RemoteApp />
</Suspense>
)
}
- 子项目使用
webpack.config.js
const {mfHackWebpack} = require('@tuya-sat/medusa/lib/mf-hack-webpack')
module.exports = (config, isDev, isServer) => {
...
mfHackWebpack(config, isDev, isServer)({
name: appId,
filename: 'static/${env}/remoteEntry.js',
exposes: {
'./Module1': '../client/components/module1',
},
useExternals: !(isDev || isServer),
commonChunks: {
test: (module) => {
return (
module.resource &&
!module.resource.includes('client/components/module1')
)
}
},
// 如果主和子都有ant。最后使用prefix把ant的样式进行隔离
antd: {
prefix: 'a-ant'
},
publicPath: isDev ? 'http://localhost:3118/_next/' : `https://static1.tuyacn.com/static/${appId}/_next/`
})
return config
}
主项目是普通webpack打包的项目,子项目是普通webpack打包的项目
- 主项目使用
router.tsx
import {Route, Router} from '@tuya-sat/medusa';
const AppRouter = () => {
return <Router LoadingComponent={<div>这里是自定义loading...</div>}>
<Route path="/path/(.*)" html="http://xxxx/sub" ></Route>
</Router>;
};
- 子项目使用
entry.tsx
import {isInMicroApp, getMountedNode} from '@tuya-sat/medusa/client'
const App:React.FC = (props) => {
return <>xxxx</>
}
const getDOM = () => {
if (isInMicroApp()) {
return getMountedNode()
}
return document.getElementById('app')
}
ReactDOM.render(<App />, getDOM())
主项目是任意项目,子项目是飞冰打包的子项目
- 主项目使用
router.tsx
import {Route, Router} from '@tuya-sat/medusa';
const AppRouter = () => {
return <Router LoadingComponent={<div>这里是自定义loading...</div>}>
<Route path="/path/(.*)" assets={{js: ['http://xxxx/sub.js']}} framework="icestark" ></Route>
</Router>;
};
- 子项目(飞冰项目正常开发就好)
主项目是任意项目,子项目是乾坤打包的子项目
- 主项目使用
router.tsx
import {Route, Router} from '@tuya-sat/medusa';
const AppRouter = () => {
return <Router LoadingComponent={<div>这里是自定义loading...</div>}>
<Route path="/path/(.*)" html="http://xxx.xx/index.html" framework="qiankun" ></Route>
</Router>;
};
- 子项目(乾坤项目正常开发就好)
主项目是任意项目,子项目是京东微前端的项目
- 主项目使用
router.tsx
import {Route, Router} from '@tuya-sat/medusa';
const AppRouter = () => {
return <Router LoadingComponent={<div>这里是自定义loading...</div>}>
<Route framework="zoe" ></Route>
</Router>;
};
- 子项目(zoe 项目正常开发即可)
路由系统
不是必须的功能,但是如果你要使用路由系统,请确保主项目子项目全都使用!
import {useRouter} from '@tuya-sat/medusa'
function App() {
const {pathname, asPath, isMatch, push, replace} = useRouter()
useEffect(() => {
console.log(pathname)
}, [])
return <div>
<button onClick={() => {
push('/another_route')
}}>子项目内部跳转</button>
<button onClick={() => {
push('/another_route', '/anotherProject/another_route', {autoBasename: false})
}}>项目间跳转</button>
</div>
}
配置与方法
- appId: 整个路由的appId,只有在多个路由系统一起工作的时候需要填写,用于区分
- urlMapPrefix: 调试url参数的前缀,默认为_tyPathMap
- LoadingComponent: 框架获取子项目过程中的loading组件
- ErrorComponent: 子项目加载失败时的自定义错误组件
- prefetch: boolean | string[] 是否启动prefetch功能,可以传递数组,数组参数为子路由指定的appId, 可以和Route的prefetchUrl结合使用
- autoPopState: 调用pushState或者replaceSate时,是否自动调用popState。因为react-router是基于popState的,所以主项目pushState并不会使得react-router生效。但是qiankun基于single-spa。single-spa实现了自动pop的逻辑。所以为了兼容,做了这个参数。酌情添加。
- onAppEnter: 子应用开始执行
- onAppStarted: 子应用js执行完毕(不保证一定mount,子应用代码有可能是异步函数)
- fetch: 需和window.fetch保持一致
- path: 子路由匹配路径,可是路由正则
- exact: 路由地址是否完全一致才匹配
- assets: {js: Array, css: Array} 子项目的资源地址。飞冰的加载方式,推荐只开发的时候用,因为线上静态文件的文件名一般会变
- manifest: 清单描述文件,可通过webpack插件生成的资源描述文件地址,加载此文件,可保证js和css都是最新的
- next: next子项目路径
- html: html文件的路径,一般为webpack-html-plugin生成,可保证js和css是最新的
- rootId: 手动指定当前路由容器的id,如果设置了,请保证子应用的加载id和填写的一致
- basename: 透传给子应用的basename, 支持正则表达式,参数为path中的参数, 不填则默认取path的最后一个/之前的字串
- peer: 可以和微前端混用,设置后此路由下的为纯react组件
- globalVars: 有些情况子项目的变量确实需要注册到主window, 比如next子项目的hotreload功能,有个变量如果放在沙箱内会导致无法动态更新
- credentials: 某些子项目需要带cookie,比如next子项目需要sso
- appId: 区别每个子项目的id, next下推荐使用,并保持与webpack hack传入的appName一致
- autoUnmount: boolean: 是否自动执行container._reactRootContainer.unmount(),用于卸载React的生命周期。请看react源码!
- onUrlFix?: (url: string) => string | undefined: 处理静态资源url,比如某些情况下,页面独立访问时会加载一些额外的js,可以在作为子项目时移除
- prefetchUrl?: string: 有些时候,我们的路径是由正则表达式拼接出来的,直接访问是访问不了的,所以给一个真实url用于替换
- cssScope?: boolean: 是否对css限定作用范围,next项目暂未开启功能
- props: Record<string, any>: 初始加载的时候,传递给子应用的数据,目前只有qiankun有效
- excludeAssetFilter: (assetUrl: string) => boolean: 指定部分特殊的动态加载的微应用资源(css/js) 不被 qiankun 劫持处理
- injectGlobals: Record<string, any>: 初始注入一些全局变量至沙箱
- initHtmlStr: string: 初始挂载到容器节点的html片段,由子应用自己代码里清除或保留
- getTemplate: (tpl: string) => string: 获取的子应用html,可进行替换
- extraOptions.nextPopstateMatch: 在next接收到popState事件时,是否判断as的前缀与当前routePrefix一致,如果不一致则阻塞。
appHistory
实际上就是window.history,包装了下
0.8.18: 提供了next框架下,直接调用router.push的封装
eventBus
主项目子项目共用的消息处理器
import {eventBus} from '@tuya-sat/medusa'
eventBus.emit('event-name', 'args')
eventBus.on('event-name', (...args) => {})
AppLink
微前端框架跳转
function isInMicroApp()
判断当前子项目是否在微前端框架内,因为有些时候子项目也需要独立运行与访问
function getMountedNode(id?, appId?)
如果指定id,则直接执行document.getElementById 如果指定appId,则获取此app下的domId
function getBasename(appId?)
获取主项目透传下的basename, 一般在子项目本身也有路由的情况下使用
function registerRedux(store: ReduxStore)
在主项目里使用,比如主项目使用了redux。则在createStore那里,调用此方法
function subscribeRedux(listener)
在子项目使用,主项目的store发生变化,listener都会触发调用
function dispatch(state, merge: boolean, namespace?: string)
主项目子项目都可使用,是微前端框架自带的数据管理器,第一个参数是state, 第二个参数代表是合并原油state还是替换, namespace为命名空间,独立state
tips: 如果你的子项目是乾坤,在调用props.onGlobalStateChange的时候也可以收到数据。但namespace必须为空
function subscribe(listener, namespace)
function registerLifecycle(config: {mount?: (props) => void, unmount?: (props) => void})
在子项目使用,需要手动处理挂载和卸载的时候用。
function registerPathChange(callback: (path: string) => void)
在子项目使用,在当前路由匹配的情况下,路由变化会触发此回调
hook useBrowserHistory
在主子项目都可使用,用于监听所有的浏览器的地址栏的变化
function urlJoin(list: Array, endsWithSlash?: boolean)
一个公共方法,用于url路径的合成
加载流程
主项目获取location.href。得到hash,path,query等参数
通过hash或者path匹配子路由列表中的path, 如果如果没匹配到则显示loading,匹配到则只返回第一个匹配到的路由
通过匹配到的子路有中的配置,获取js和css列表。
如果是next或html的地址,则会先用fetch方法抓取网页,并解析网页中的link style以及script标签。link和style标签下的内容会被插入到head中的style标签下,script内容会由js沙箱托管并运行。
子项目通过获取getMountCode方法,获取当前路由处于哪个div容器下,并由react或者vue等框架自己挂载到下面。
当调用了pushState或popState等方法导致url发生变化,主项目会重新计算路由匹配,若匹配到同一个子路由则主项目不会有任何变化,若匹配到不同路由,则上一个路由触发销毁方法。
首先,移除子项目中插入到head中style标签,然后销毁js沙箱。
tips: 若某些style或css由子项目插入,则无能为力了,所以尽量避免这些操作。
- 继续3-5的逻辑
调试与开发
由于微前端就意味着多个工程项目,很多时候我们只需要开发一个子项目,多个一起开会很麻烦。所以我们在主项目支持资源替换的方式来简化开发。
比如: 在我们把主项目发到日常或线上后,在主项目url后加上
?_tyPathMap=http://localhost:3000/sourceMap.json
sourceMap.json内容如下:
{
"/path1/(.*)": {
// 这里的配置和Route传入的props一致,都可以替换
"next": "http://localhost:3000/test1"
}
}
则可以把所有匹配到/path1/(.*)的子项目换成本地,这样就不需要打开主项目及其它子项目了。
优化及建议
有些时候我们主项目和子项目都用到了React或者都用到了Vue,如果都加载完整的包肯定会浪费资源。因此可以在主项目中把React和Vue挂载到window下。然后在子项目中的webpack中配置external
externals: {
react: 'React',
'react-dom': 'ReactDOM',
},