icestark 是飞冰团队针对大型系统提供的微前端解决方案,我们提供了独立插件 build-plugin-icestark 帮助 icejs 应用快速接入微前端解决方案。

框架应用

通过模板快速创建一个微前端的框架应用(主应用):

  1. $ npm init ice icestark-framework @icedesign/stark-layout-scaffold
  2. $ cd icestark-framework
  3. $ npm install
  4. $ npm start

如果不是通过模板创建,则需要按照下面的步骤进行改造:

添加插件 build-plugin-icestark

安装插件依赖:

  1. $ npm i --save-dev build-plugin-icestark

build.json 里引入插件:

  1. {
  2. "plugins": {
  3. ["build-plugin-icestark", {
  4. "type": "framework",
  5. // 防止与微应用的 webpackJSONP 冲突
  6. "uniqueName": "frameworkJsonp",
  7. }],
  8. ["build-plugin-fusion", {
  9. "themeConfig": {
  10. // 防止与微应用里的基础组件 css prefix 冲突
  11. "css-prefix": "next-icestark-"
  12. }
  13. }],
  14. }
  15. }

应用入口改造

应用入口 src/app.tsx 中配置框架应用的一些运行时信息:

  1. import { runApp } from 'ice'
  2. +import { ConfigProvider } from '@alifd/next';
  3. +import NotFound from '@/components/NotFound';
  4. +import BasicLayout from '@/layouts/BasicLayout';
  5. const appConfig = {
  6. app: {
  7. rootId: 'ice-container',
  8. + addProvider: ({ children }) => (
  9. + <ConfigProvider prefix="next-icestark-">{children}</ConfigProvider>
  10. + ),
  11. },
  12. router: {
  13. + type: 'browser',
  14. },
  15. icestark: {
  16. + Layout: BasicLayout,
  17. + getApps: async () => {
  18. + const apps = [{
  19. + path: '/seller',
  20. + title: '商家平台',
  21. + url: [
  22. + '//ice.alicdn.com/icestark/child-seller-react/index.js',
  23. + '//ice.alicdn.com/icestark/child-seller-react/index.css',
  24. + ],
  25. + }];
  26. + return apps;
  27. + },
  28. + appRouter: {
  29. + NotFoundComponent: NotFound,
  30. + },
  31. },
  32. };
  33. runApp(appConfig);

完整配置说明见运行时参数

微应用/子应用

通过模板快速创建一个微应用:

  1. # 创建微应用
  2. $ npm init ice icestark-child @icedesign/stark-child-scaffold
  3. $ cd icestark-child
  4. $ npm install
  5. $ npm start

如果不是通过模板创建,则需要按照下面的步骤进行改造:

添加插件 build-plugin-icestark

安装插件依赖:

  1. $ npm i --save-dev build-plugin-icestark

build.json 里引入插件:

  1. {
  2. "plugins": {
  3. ["build-plugin-icestark", {
  4. "type": "child",
  5. "umd": true
  6. }]
  7. }
  8. }

应用入口改造

在应用入口 src/app.ts 中配置微应用相关的信息:

  1. import { runApp } from 'ice'
  2. const appConfig = {
  3. app: {
  4. rootId: 'ice-container',
  5. },
  6. router: {
  7. + type: 'browser',
  8. },
  9. };
  10. runApp(appConfig)

只需要这么简单,你的 SPA 应用就可以变成微应用了。

使用 Vite 模式

icejs@2.0.0 + build-plugin-icestark@2.4.0 开始支持微应用使用 Vite 模式,构建出 ES Module 的产物格式

@ice/stark@2.6.0 开始支持加载 ES Module 格式的微应用

在微应用的 build.json 中添加配置:

  1. {
  2. + "vite": true,
  3. "plugins": [
  4. ["build-plugin-icestark", {
  5. + "type": "child",
  6. - "umd": true,
  7. }]
  8. ]
  9. }

同时框架应用中需要针对对应微应用配置 import 选项以支持 ES Module 格式的加载:

  1. import { runApp } from 'ice';
  2. runApp({
  3. app: {
  4. rootId: 'ice-container',
  5. },
  6. icestark: {
  7. Layout: BasicLayout,
  8. getApps: async () => {
  9. const apps = [{
  10. path: '/seller',
  11. title: '商家平台',
  12. + loadScriptMode: 'import',
  13. url: [],
  14. }];
  15. return apps;
  16. },
  17. },
  18. });

插件参数

运行时参数

运行时参数配置在入口文件 appConfig.icestark 字段中,使用方式如下:

  1. import { runApp } from 'ice';
  2. const appConfig = {
  3. app: {
  4. getApps: async () => {
  5. return [];
  6. },
  7. appRouter: {
  8. },
  9. },
  10. };
  11. runApp(appConfig);

所有参数配置如下:

Layout

  • 类型:Component

框架应用独有字段,框架应用对应的布局组件。

getApps

  • 类型:Function
  • 默认值:() => []

框架应用独有字段,用于获取微应用列表,单个微应用的完整配置字段请参考 AppConfig

appRouter

框架应用独有字段,可传入 icestark 运行时的钩子函数和可选配置。主要有:

  • NotFoundComponent,匹配不到任何微应用路由时的状态。
  • LoadingComponent,加载过程中的 Loading 状态。
  • ErrorComponent,加载出现错误时的状态。

更多配置详见文档

AppRoute

框架应用独有字段,微应用渲染组件,可替换 icestark 内部实现的渲染组件,或将其封装 HoC 组件提供更多能力。非特殊场景不建议使用。

type

废弃字段,推荐通过构建时参数配置。

  • 类型:child | framework

构建时参数

构建时参数配置在 build.json 中,如下使用方式:

  1. {
  2. "plugins": [
  3. ["build-plugin-icestark", {
  4. "type": "child",
  5. }]
  6. ]
  7. }

所有参数配置如下:

type

  • 类型:child | framework
  • 默认值:framework

标识应用类型,框架应用或微应用。

umd

  • 类型:boolean
  • 默认值:false

仅对微应用生效,是否构建为 UMD 格式的微应用。若配置 umd 参数,则 type 默认为 child

library

  • 类型:string

构建为 UMD 规范微应用相关字段,标识 UMD 微应用全局导出的变量名。

uniqueName

  • 类型:string
  • 默认:-

开启 splitChunk 或懒加载功能时,防止 webpack runtimes 冲突时使用。建议框架应用开启。

若使用 webpack5 构建应用,则无需启用该字段

常见问题

如何监听微应用切换

icestark 通过 onRouteChangeonAppEnteronAppLeave 来监听微应用间的切换,在 icejs 研发框架下可以通过在对应的 Layout 中实现相关钩子的监听。Layout 中接收 props 属性如下:

  • pathname:微应用路由切换信息,对应 onRouteChange
  • appEnter:渲染微应用的信息, onAppEnter
  • appLeave:卸载微应用的信息,对应 onAppLeave

在 Layout 使用相关属性时,结合对应属性是否发生变更来执行相应操作:

  1. const BasicLayout = ({ pathname, appLeave, appEnter, children }) => {
  2. useEffect(() => {
  3. console.log(`微应用路由发生变化:${pathname}`);
  4. }, [pathname]);
  5. useEffect(() => {
  6. console.log(`卸载微应用:${appLeave.path}`);
  7. }, [appLeave]);
  8. useEffect(() => {
  9. console.log(`渲染微应用:${appEnter.path}`);
  10. }, [appEnter]);
  11. return (
  12. <div>
  13. {children}
  14. </div>
  15. );
  16. };

动态修改微应用列表

初始化微应用列表可以如上文介绍在应用入口 src/app.ts 中配置 getApps 属性即可,如果需要动态修改微应用列表,可以通过 Layout 接收的 updateApps 属性进行修改:

  1. const BasicLayout = ({ updateApps, children }) => {
  2. useEffect(() => {
  3. updateApps([{
  4. path: '/seller',
  5. title: '商家平台',
  6. url: [
  7. '//ice.alicdn.com/icestark/child-seller-react/index.js',
  8. '//ice.alicdn.com/icestark/child-seller-react/index.css',
  9. ],
  10. }]);
  11. }, []);
  12. return (
  13. <div>
  14. {children}
  15. </div>
  16. );
  17. }

UMD 规范微应用

icestark 从 1.6.0 开始支持并推荐使用 UMD 规范的微应用,在微应用层面可以更少的降低跟主应用的耦合:

  • 微应用依赖的 build-plugin-icestark 版本需要高于 2.0.0 才能支持构建出 UMD 规范的微应用
  • 主应用依赖的 @ice/stark 版本需要高于 1.6.0 才能支持渲染 UMD 规范的微应用

微应用导出 UMD 规范的产物

build.json 中配置 umd 属性即可导出标准 UMD 规范的微应用:

  1. {
  2. "plugins": [
  3. ["build-plugin-icestark", {
  4. "umd": true
  5. }]
  6. ]
  7. }

向微应用透传 props

icestark 2.x 支持框架应用通过 props 自定义传递给微应用的参数。

  1. // 框架应用
  2. const appConfig = {
  3. ...
  4. icestark: {
  5. type: 'framework',
  6. Layout: BasicLayout,
  7. getApps: async () => {
  8. const apps = [{
  9. path: '/seller',
  10. title: '商家平台',
  11. url: [
  12. '//ice.alicdn.com/icestark/child-seller-react/index.js',
  13. '//ice.alicdn.com/icestark/child-seller-react/index.css',
  14. ],
  15. + props: {
  16. + name: 'micro-child'
  17. + }
  18. }];
  19. return apps;
  20. },
  21. ...
  22. },
  23. };
  24. runApp(appConfig);

在微应用中,可以通过页面级组件 的 props 获取框架应用传递的参数。

  1. function About(props) {
  2. const { frameworkProps: { name } } = props;
  3. return <div>{name}</div>;
  4. }

微应用自定义生命周期函数

插件 build-plugin-icestark 会默认为 ice.js 微应用提供生命周期函数。在一些业务场景下,需要自定义生命周期函数,则可以下面的示例进行配置:

  1. import { runApp } from 'ice';
  2. import { isInIcestark } from '@ice/stark-app';
  3. import ReactDOM from 'react-dom';
  4. // 微应用 app.tsx
  5. const appConfig = {
  6. router: {
  7. type: 'browser',
  8. },
  9. icestark: {
  10. type: 'child',
  11. },
  12. };
  13. if (!isInIcestark()) {
  14. runApp(appConfig);
  15. }
  16. // 自定义 mount 生命周期函数
  17. export function mount () {
  18. runApp(appConfig)
  19. }
  20. // 自定义 unmount 生命周期函数
  21. export function unmount ({ container }) {
  22. ReactDOM.unmountComponentAtNode(container)
  23. }

微应用 bundle 加载失败

前端应用如果做了按需加载,按需加载的 bundle 默认是根据当前域名拼接地址,如果前端资源部署在非当前域名(比如 CDN)下,则需要通过手动配置 publicPath 来实现,可参考文档

微应用开发时请求本地 Mock 接口

通常情况下,代码中的接口请求地址都是写成类似 /api/xxx 的相对地址,请求时会根据当前域名进行拼接,如果微应用嵌入主应用进行开发,在域名变化后依旧想使用微应用的 Mock 接口或者代理配置,可以设置 baseURL 来请求非当前域名的接口地址。

  1. import { runApp } from 'ice';
  2. const appConfig = {
  3. ...
  4. request: {
  5. baseURL: '//127.0.0.1:4444',
  6. }
  7. };
  8. runApp(appConfig);

微应用本地开发如何调试

单独微应用开发时只能看到自身的内容,无法关注到在主应用下的表现,这时候本地如果需要再启动一个主应用,开发起来就很繁琐。针对这种情况,我们推荐通过主应用的日常/线上环境调试本地微应用。

在主应用中注册微应用时,如果 url 里携带了类似 ?__env__=local 的 query,则将微应用的 url 转换为对应的本地服务地址,这样就可以方便调试微应用了。大体代码如下(可根据具体需求调整):

  1. // src/app.jsx
  2. import React from 'react';
  3. import { AppRouter, AppRoute } from '@ice/stark';
  4. import urlParse from 'url-parse';
  5. import BasicLayout from '@/layouts/BasicLayout';
  6. const urlQuery = urlParse(location.href, true).query || {};
  7. function getBundleUrl(name, version) {
  8. let jsUrl = `//g.alicdn.com/${name}/${version}/index.min.js`;
  9. let cssUrl = `//g.alicdn.com/${name}/${version}/index.min.css`;
  10. if (urlQuery.env === 'local') {
  11. jsUrl = `//127.0.0.1:${urlQuery.port}/build/js/index.js`;
  12. cssUrl = `//127.0.0.1:${urlQuery.port}/build/css/index.css`;
  13. }
  14. return [cssUrl, jsUrl];
  15. }
  16. const apps = [{
  17. title: '通用页面',
  18. url: getBundleUrl('seller', '0.1.0'),
  19. // ...
  20. }]

应用启用 lazy 后,chunk 加载失败

多个微应用均开启 lazy 加载页面,建议通过开启 sandbox 隔离微应用 windows 全局变量。如果无法开启 sandbox,则需要在主应用 onAppLeave 的阶段清空 webpackJsonp 配置:

  1. const onAppLeave = (appConfig) => {
  2. window.webpackJsonp = [];
  3. };

或建议通过构建时参数 uniqueName 隔离多个微应用的 webpack runtimes。

注意,若使用 webpack5 构建应用,则 webpack5 会默认使用 package.jsonname 作为 uniqueName,因此也无需在 onAppLeave 阶段移除 window.webpackJsonp

Error: Invariant failed: You should not use <withRouter(Navigation) /> outside a <Router>

因为 jsx 嵌套层级的关系,在主应用的 Layout 里没法使用 react-router 提供的 API,比如 withRouter, Link, useParams 等,具体参考文档 主应用中路由跳转

启用 HashRouter

官方推荐 BrowserRouter 作为微前端的路由模式。在某些情况下,你可以通过以下方式适配 HashRouter 路由模式。

  1. 修改主应用的路由模式

src/app.ts 中增加以下配置,将 router 修改为 hash

  1. import { runApp } from 'ice';
  2. const appConfig = {
  3. router: {
  4. - type: 'browser',
  5. + type: 'hash',
  6. }
  7. };
  8. runApp(appConfig);
  1. 为微应用设置 hashType 为 true
  1. import { runApp } from 'ice';
  2. const appConfig: IAppConfig = {
  3. icestark: {
  4. type: 'framework',
  5. Layout: FrameworkLayout,
  6. getApps: async () => {
  7. const apps = [{
  8. path: '/seller',
  9. title: '商家平台',
  10. sandbox: true,
  11. + hashType: true,
  12. url: [
  13. '//dev.g.alicdn.com/nazha/ice-child-react/0.0.1/js/index.js',
  14. '//dev.g.alicdn.com/nazha/ice-child-react/0.0.1/css/index.css',
  15. ],
  16. }, {
  17. path: '/waiter',
  18. title: '小二平台',
  19. sandbox: true,
  20. + hashType: true,
  21. url: [
  22. '//ice.alicdn.com/icestark/child-waiter-vue/app.js',
  23. '//ice.alicdn.com/icestark/child-waiter-vue/app.css',
  24. ],
  25. }];
  26. return apps;
  27. },
  28. },
  29. };
  30. runApp(appConfig);
  1. 修改 FrameworkLayout 中的逻辑

此外,你可能需要自行修改 FrameworkLayout 中的逻辑,路由信息会通过 routeInfo 字段返回。

  1. import * as React from 'react';
  2. import BasicLayout from '../BasicLayout';
  3. import UserLayout from '../UserLayout';
  4. interface RouteInfo {
  5. hash: string;
  6. pathname: string;
  7. query: object;
  8. routeType: 'pushState' | 'replaceState',
  9. }
  10. const { useEffect } = React;
  11. export default function FrameworkLayout(props: {
  12. children: React.ReactNode;
  13. appLeave: { path: string };
  14. appEnter: { path: string };
  15. routeInfo: RouteInfo;
  16. }) {
  17. const { children, appLeave, appEnter, routeInfo } = props;
  18. // 如果是 HashRouter 模式
  19. const isHashRouter = true;
  20. const { hash = '', pathname } = routeInfo;
  21. const path = isHashRouter ? hash.replace('#', '') : pathname;
  22. const Layout = hash === '/login' ? UserLayout : BasicLayout;
  23. useEffect(() => {
  24. console.log('== app leave ==', appLeave);
  25. if (appLeave.path === '/angular' && window.webpackJsonp) {
  26. // remove webpackJsonp added by Angular app
  27. delete window.webpackJsonp;
  28. }
  29. }, [appLeave]);
  30. useEffect(() => {
  31. console.log('== app enter ==', appEnter);
  32. }, [appEnter]);
  33. return (
  34. <Layout pathname={path}>{children}</Layout>
  35. );
  36. }
  1. 微应用改造

微应用的同样需要改造成 HashRouter 路由模式。

  1. 应用间跳转

应用间跳转可以通过 AppLinkappHistory,并设置 hashTypetrue

  1. import { AppLink, appHistory } from '@ice/stark-app';
  2. // 示例1
  3. const navItem = <AppLink to="/seller" hashType>{item.name}</AppLink>);
  4. // 示例2
  5. appHistory.push('/seller', true);

如何解决 Script Error 错误

“ Script error. ” 是一个常见错误,但由于该错误不提供完整的报错信息(错误堆栈),问题排查往往无从下手。icestark 的 scriptAttributes 参数支持为加载的 <script /> 资源添加 crossorigin="anonymous" 来解决这个问题。

更多有关 icestark 的内容请访问 👉 官网