0.9.0 • Published 4 months ago

@tuya-sat/medusa v0.9.0

Weekly downloads
-
License
ISC
Repository
-
Last release
4 months ago

一体化的微前端框架,支持绝大部分流行的微前端技术

安装 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错误

目前大概出现的有如下几种原因:

  1. 子项目无法访问
  2. 子项目跨域了
  3. 子项目配置了envTag。
  4. 子项目配置了sso,被redirect到登录页去了

使用说明

0.8.42增加模块联邦的hack配置

  1. 主项目使用

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>
  )
}
  1. 子项目使用

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打包的项目

  1. 主项目使用

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>;
  };
  1. 子项目使用

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())

主项目是任意项目,子项目是飞冰打包的子项目

  1. 主项目使用

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>;
  };
  1. 子项目(飞冰项目正常开发就好)

主项目是任意项目,子项目是乾坤打包的子项目

  1. 主项目使用

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>;
  };
  1. 子项目(乾坤项目正常开发就好)

主项目是任意项目,子项目是京东微前端的项目

  1. 主项目使用

router.tsx

  import {Route, Router} from '@tuya-sat/medusa';

  const AppRouter = () => {
    return <Router LoadingComponent={<div>这里是自定义loading...</div>}>
      <Route framework="zoe" ></Route>
    </Router>;
  };
  1. 子项目(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>
   }

配置与方法

  1. appId: 整个路由的appId,只有在多个路由系统一起工作的时候需要填写,用于区分
  2. urlMapPrefix: 调试url参数的前缀,默认为_tyPathMap
  3. LoadingComponent: 框架获取子项目过程中的loading组件
  4. ErrorComponent: 子项目加载失败时的自定义错误组件
  5. prefetch: boolean | string[] 是否启动prefetch功能,可以传递数组,数组参数为子路由指定的appId, 可以和Route的prefetchUrl结合使用
  6. autoPopState: 调用pushState或者replaceSate时,是否自动调用popState。因为react-router是基于popState的,所以主项目pushState并不会使得react-router生效。但是qiankun基于single-spa。single-spa实现了自动pop的逻辑。所以为了兼容,做了这个参数。酌情添加。
  7. onAppEnter: 子应用开始执行
  8. onAppStarted: 子应用js执行完毕(不保证一定mount,子应用代码有可能是异步函数)
  9. fetch: 需和window.fetch保持一致

  1. path: 子路由匹配路径,可是路由正则
  2. exact: 路由地址是否完全一致才匹配
  3. assets: {js: Array, css: Array} 子项目的资源地址。飞冰的加载方式,推荐只开发的时候用,因为线上静态文件的文件名一般会变
  4. manifest: 清单描述文件,可通过webpack插件生成的资源描述文件地址,加载此文件,可保证js和css都是最新的
  5. next: next子项目路径
  6. html: html文件的路径,一般为webpack-html-plugin生成,可保证js和css是最新的
  7. rootId: 手动指定当前路由容器的id,如果设置了,请保证子应用的加载id和填写的一致
  8. basename: 透传给子应用的basename, 支持正则表达式,参数为path中的参数, 不填则默认取path的最后一个/之前的字串
  9. peer: 可以和微前端混用,设置后此路由下的为纯react组件
  10. globalVars: 有些情况子项目的变量确实需要注册到主window, 比如next子项目的hotreload功能,有个变量如果放在沙箱内会导致无法动态更新
  11. credentials: 某些子项目需要带cookie,比如next子项目需要sso
  12. appId: 区别每个子项目的id, next下推荐使用,并保持与webpack hack传入的appName一致
  13. autoUnmount: boolean: 是否自动执行container._reactRootContainer.unmount(),用于卸载React的生命周期。请看react源码!
  14. onUrlFix?: (url: string) => string | undefined: 处理静态资源url,比如某些情况下,页面独立访问时会加载一些额外的js,可以在作为子项目时移除
  15. prefetchUrl?: string: 有些时候,我们的路径是由正则表达式拼接出来的,直接访问是访问不了的,所以给一个真实url用于替换
  16. cssScope?: boolean: 是否对css限定作用范围,next项目暂未开启功能
  17. props: Record<string, any>: 初始加载的时候,传递给子应用的数据,目前只有qiankun有效
  18. excludeAssetFilter: (assetUrl: string) => boolean: 指定部分特殊的动态加载的微应用资源(css/js) 不被 qiankun 劫持处理
  19. injectGlobals: Record<string, any>: 初始注入一些全局变量至沙箱
  20. initHtmlStr: string: 初始挂载到容器节点的html片段,由子应用自己代码里清除或保留
  21. getTemplate: (tpl: string) => string: 获取的子应用html,可进行替换
  22. 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路径的合成

加载流程

  1. 主项目获取location.href。得到hash,path,query等参数

  2. 通过hash或者path匹配子路由列表中的path, 如果如果没匹配到则显示loading,匹配到则只返回第一个匹配到的路由

  3. 通过匹配到的子路有中的配置,获取js和css列表。

如果是next或html的地址,则会先用fetch方法抓取网页,并解析网页中的link style以及script标签。link和style标签下的内容会被插入到head中的style标签下,script内容会由js沙箱托管并运行。

  1. 子项目通过获取getMountCode方法,获取当前路由处于哪个div容器下,并由react或者vue等框架自己挂载到下面。

  2. 当调用了pushState或popState等方法导致url发生变化,主项目会重新计算路由匹配,若匹配到同一个子路由则主项目不会有任何变化,若匹配到不同路由,则上一个路由触发销毁方法。

首先,移除子项目中插入到head中style标签,然后销毁js沙箱。

tips: 若某些style或css由子项目插入,则无能为力了,所以尽量避免这些操作。

  1. 继续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',
  },