微前端之IceStark介绍和基础使用
1. 介绍
阿里飞冰团队于 2019 年 6 月发布的微前端框架,内部服务于淘宝、飞猪、钉钉、阿里云等大型项目。
IceStark 支持以下特性:
- 框架无关(也是所有微前端框架都会解决的问题)
- 快速迁移:支持从 url,html 文本,entry应用入口文件等方式直接接入
- 完善的 js 沙箱,使微应用接入更安全
- 性能优秀(大家都优秀)
- 支持 ES Module
- 支持微模块(Single SPA 也有)
IceStark 本身并不是基于 Single SPA 进行二次开发,而是自身内部实现了一套微应用加载逻辑,但是在浏览器历史记录处理(前进与回退等事件需要渲染对应子应用)也借鉴了部分 Single SPA 的逻辑;并提供了与 SingleSPA 类似的配置 API,兼容 Single SPA 微应用。
2. 主应用
在 IceStark 的官方文档中,对主应用职责进行了说明:
- 负责整体系统 Layout 布局
- 注册和配置微应用
- 避免过多样式代码
- 减少公共 API 暴露,避免应用耦合
IceStark 主应用构建与 qiankun 比较相似,都通过 registerMicroApps 来注册微应用,通过 start 开启路由劫持来加载/卸载微应用。
应该是因为官方内部使用 React 更多一点,所以对 React 的应用注册提供了 AppRouter 和 AppRoute 来替换原有的 ReactRouter,直接注册微应用。
因为笔者对 React 不是很熟悉,大家可以看 官方文档 - React 主应用接入
首先,引入 IceStark
npm install @ice/stark --save
然后,创建 micro-apps.ts
import { registerMicroApps, start } from '@ice/stark';
import NProgress from 'nprogress'
import "nprogress/nprogress.css";
const getDivDom:HTMLElement = (id: string) => document.getElementById(id)
// 子应用
const microApps = [
{
name: 'sub-app-one',
entry: '//localhost:3001',
activeRule: '/sub-one'
}
]
// 生命周期处理
const lifeCycles = {
beforeMount: (app: any) => {
console.log("before mount app.name====>>>>>", app.name)
},
afterMount: (app: any) => {
console.log("after mount app.name====>>>>>", app.name)
},
afterUnmount: (app: any) => {
console.log("after unmount app.name====>>>>>", app.name)
}
}
// 子应用处理
const normalizeMicroApp = (apps = []) => {
return apps.map((app: any) => ({
container: getDivDom('#sub-container'),
...app
}))
}
const register = () => registerMicroApps(normalizeMicroApp(microApps), lifeCycles)
export default {
register,
start
}
这里有以下几点需要注意:
- IceStark 子应用生命周期与 qiankun 等不同,没有 beforeLoad,增加了 beforeUpdate 和 afterUpdate
- 子应用挂载节点配置 container 需要是一个 HTMLElement 节点,而不是字符串
- 子应用配置没有 loader 选项,如果要配置加载过度动画,需要配置到 start 方法。
// 这里对上面的 start 方法进行一下扩展
const startLoading = () => NProgress.start()
const endLoading = () => NProgress.done()
const startApp = (props: StartConfiguration) => {
start({
onAppEnter: startLoading,
onFinishLoading: endLoading,
onError: endLoading,
...props
})
}
3. 子应用
微应用接入与 qiankun 和 Single SPA 基本一致,都需要子应用打包成 umd 格式,在入口处导出声明周期函数,并且都需要给子应用配置基准路由,避免无法匹配。
IceStark 微应用不需要 bootstrap 初始化周期
3.1 Webpack 应用
使用 Vue CLI 和 CRA(create react app) 之类的脚手架创建的应用都算是 webpack 应用,这里还是以 Vue 作为例子。
首先,依然是修改 main.ts 入口文件
import Vue from 'vue';
import VueRouter from 'vue-router';
import isInIcestark from '@ice/stark-app/lib/isInIcestark';
import setLibraryName from '@ice/stark-app/lib/setLibraryName';
import App from './App.vue';
import routes from './router';
let router = null;
let instance = null;
function render(props = {}) {
const { container } = props;
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/',
mode: 'history',
routes,
});
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount(container || '#app');
}
// 注意:`setLibraryName` 的入参需要与 webpack 工程配置的 output.library 保持一致
setLibraryName('microApp');
export function mount(props) {
render(props)
}
export function unmount() {
instance && instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
router = null;
}
if (!isInIcestark()) {
render()
}
这里的 VueRouter 路由实例可以拆分出来单独实例化。IceStark 提供了 getBasename 方法用来查询 setLibraryName 时定义的微应用基准路由。
import Vue from 'vue';
import Router from 'vue-router';
import getBasename from '@ice/stark-app/lib/getBasename';
Vue.use(Router);
export default new Router({
mode: 'history',
base: getBasename(),
routes: [
// ...
],
});
最后,也是修改 webpack 配置
module.exports = {
output: {
// 设置模块导出规范为 umd
libraryTarget: 'umd',
// 与上文 setLibraryName 设置的名称一致
library: 'microApp',
}
}
3.2 Vite 子应用
Vite 子应用的应用实例化和路由配置与上面基本一致,这里不再赘述
根据官方文档,这里接入 Vite 子应用有两种方式:
1. 使用 Vite Lib 模式,指定入口为 main.ts
cimport { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: './src/main.ts',
formats: ['es'],
fileName: 'index'
},
rollupOptions: {
preserveEntrySignatures: 'exports-only',
}
},
})
但是这种方式会造成子应用无法单独运行(也可以改造成按照环境变量来设置不同的 lib 配置)
**2. 使用官方插件 vite-plugin-index-html **
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import htmlPlugin from 'vite-plugin-index-html';
export default defineConfig({
plugins: [
vue(),
htmlPlugin({
input: './src/main.ts', // 指定确定的入口文件
preserveEntrySignatures: "exports-only", // 确保入口文件导出生命周期函数
})
],
})
🚀 因为 Vite 应用启动时采用的是原生 ES Module,所以主应用在注册时需要声明脚本加载方式:
registerMicroApps([ { name: 'seller', activePath: '/seller', // ... loadScriptMode: 'import', // 指定加载 ES modules 类型微应用 } ])
4. 样式隔离
🚀🚀 样式隔离通常指子应用间的样式隔离,主应用如果没有过分区分的话,有可能会对子应用样式造成影响。
这里的隔离方式推荐以下几种:
- CSS Module:主应用与子应用都采用该方式,就可以避免主应用样式影响
- 主应用与微应用避免重复使用样式重置插件,建议提取到主应用即可
- 对于不同的应用间有不同UI库或者主题配置,建议修改类名前缀
不论是 qiankun 还是 IceStark,这类微前端框架通常都能做到 JS 的沙箱隔离,但是对于样式隔离的问题,大家采用的方案一般都是 Shadow DOM,但是 Shadow DOM 并不能解决 Dialog 这类直接插入到 body 节点下的组件样式,并且兼容性也待商榷。所以,采用 CSS Module 并约定一个良好的应用样式规范,才是避免样式冲突最好的方法。
转载自:https://juejin.cn/post/7129374473949544485