什么是微前端?
微前端是一种设计思想,它将前端应用拆分成多个独立的小应用,每个小应用可以独立开发、部署和升级,同时它们可以共享公共的资源和组件。
有哪些微前端框架?
- Single-SPA:一个用于构建微前端应用的JavaScript库,它支持多种前端框架(如React、Vue、Angular等)。
- Qiankun:一个基于Single-SPA的微前端框架,它提供了更简单易用的API和更好的兼容性。
single-spa 实践
三种应用类型
- single-spa application/parcel:微前端架构中的微应用(子应用),可以使用Vue、React、Angular等框架。
- single-spa application与路由相关联,根据路由决定访问哪个微应用;
- 而single-spa parcel则不和路由关联,主要用于跨应用共享UI组件,重用UI
- single-spa root config:创建微前端容器应用(主应用)的方式,通过容器应用加载和管理普通的微应用(子应用)。根应用负责加载其他子应用,并作为单页应用(SPA)的容器,将不同的子应用集成在一个页面中,并为每个子应用创建一个独立的上下文。
- utility modules:公共模块应用,属于非渲染组件,用于跨应用共享JavaScript逻辑的微应用。
创建主应用 (脚手架的形式创建项目)
npm install --global create-single-spa # 全局安装create-single-spa,然后用create-single-spa命令创建应用
# 或者不想全局安装,可以使用下面的命令
npm init single-spa
# or
npx create-single-spa
# or
yarn create single-spa
#创建容器应用
npx create-single-spa
? Directory for new project single-spa
? Select type to generate single-spa root config
? Which package manager do you want to use? npm
? Will this project use Typescript? No
? Would you like to use single-spa Layout Engine No
? Organization name (can use letters, numbers, dash or underscore) wxz
进入项目目录并启动
cd single-spa
npm start
- 默认运行在 9000端口,访问 http://localhost:9000/ 即可看到默认Welcome页面。因为默认注册了一个
@single-spa/welcome子应用 - 具体api可看官方文档
默认文件 src\wxz-root-config.js
- 注册子应用的地方,
registerapplication的参数看官方文档 - 注意一下自己引用的
single-spa的版本和包类型,下面我是引用的 6.3.0版本的esm包。umd的包不是下面这样用的,会报错 registerApplication不是一个function
// src\wxz-root-config.js
import { registerApplication, start } from "single-spa";
// registerApplication: 注册子应用函数,每个子应用注册必须有三个参数:1. 子应用名称 2. 子应用入口,加载应用程序的代码 3. 子应用激活条件
registerApplication({
name: "@single-spa/welcome", // 注册子应用名称
app: () =>
import(
/* webpackIgnore: true */ // @ts-ignore-next
"https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
), // 子应用入口
activeWhen: ["/"], // 子应用激活条件
});
// registerApplication({
// name: "@wxz/navbar",
// app: () =>
// import(
// /* webpackIgnore: true */ // @ts-ignore-next
// "@wxz/navbar"
// ),
// activeWhen: ["/"],
// });
// 调用start函数之前,会加载所有注册的子应用,但不会初始化、挂载它们。start函数会启动应用,并开始监听路由变化,当路由变化时,会自动加载、挂载对应的子应用。
start({
urlRerouteOnly: true, // 是否可以通过 history.pushState 和 history.replaceState 改变路由,默认是false
});
默认文件 src\index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Root Config</title>
<!-- 如果有跨域错误可以把下面这行注释掉 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self' https: localhost:*; script-src 'unsafe-inline' 'unsafe-eval' https: localhost:*; connect-src https: localhost:* ws://localhost:*; style-src 'unsafe-inline' https:; object-src 'none';">
<meta name="importmap-type" use-injector />
<!-- If you wish to turn off import-map-overrides for specific environments (prod), uncomment the line below -->
<!-- More info at https://github.com/single-spa/import-map-overrides/blob/main/docs/configuration.md#domain-list -->
<!-- <meta name="import-map-overrides-domains" content="denylist:prod.example.com" /> -->
<!-- Shared dependencies go into this import map -->
<!-- 引用的公共库,可以把vue,vue-router,react,react-dom等cdn的形式引入放在这儿 -->
<script type="injector-importmap">
{
"imports": {
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.3/lib/es2015/esm/single-spa.min.js"
}
}
</script>
<link rel="preload" href="https://cdn.jsdelivr.net/npm/single-spa@6.0.3/lib/es2015/esm/single-spa.min.js" as="module">
<!-- Add your organization's prod import map URL to this script's src -->
<!-- <script type="injector-importmap" src="/importmap.json"></script> -->
<!-- 加载子应用路径 -->
<% if (isLocal) { %>
<script type="injector-importmap">
{
"imports": {
"@wxz/root-config": "//localhost:9000/wxz-root-config.js",
"@single-spa/welcome": "https://cdn.jsdelivr.net/npm/single-spa-welcome/dist/single-spa-welcome.min.js"
}
}
</script>
<% } %>
<script src="https://cdn.jsdelivr.net/npm/import-map-overrides@4.1.0/dist/import-map-overrides.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@single-spa/import-map-injector@1.1.0/lib/import-map-injector.js"></script>
</head>
<body>
<main></main>
<script>
window.importMapInjector.initPromise.then(() => {
import('@wxz/root-config');
});
</script>
<import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>
创建子应用
我的目录结构:在 src/applications 下创建子应用,所有子应用会放在 src/applications 下
注意 我创建子应用的时候,node版本用 20.17.0创建会失败,在填完 Organization name 后就退出终端了。换成 16.17.1 就创建成功了。
injector-importmap中添加包的时候,最后一个后面不要有逗号
创建一个没有使用任何框架基于webpack的demo
- 创建文件夹 webpackdemo, 进入该目录然后
npm init -y - 安装webpack和babel相关依赖,至少安装下面几个,够项目跑起来
{
"name": "@wxz/webpackdemo", // 用registerApplication注册子应用时,import入口时,官方建议用 子应用 package.json 中的name,为了统一可以改一下名字,都 组织名/子应用名
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"start": "webpack serve"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.26.0",
"babel-loader": "^8.3.0",
"webpack": "^5.97.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.0"
}
}
- 配置文件,webpackdemo 下 创建 webpack.config.js
// src\applications\webpackdemo\webpack.config.js
const singleSpaDefaults = require("webpack-config-single-spa"); //在webpackdemo子应用中没有安装,但在父级主应用中已经安装了
const { merge } = require("webpack-merge"); //在webpackdemo子应用中没有安装,但在父级主应用中已经安装了
const path = require("path");
module.exports = (webpackConfigEnv, argv) => {
const defaultConfig = singleSpaDefaults({
// 组织名称
orgName: "wxz",
// 项目名称
projectName: "webpackdemo",
webpackConfigEnv,
argv
});
// 使用merge方法配置微应用默认端口
return merge(defaultConfig, {
devServer: {
port: 9001
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/, // 排除node_modules中的js文件(这些文件不处理)
loader: "babel-loader"
}
]
}
});
};
- 配置 babel 创建
babel.config.js
module.exports = {
// 智能预设: 能够编译ES6的语法
presets: ["@babel/preset-env"]
};
- 在
webpackdemo/src下创建入口文件,入口文件命名规则组织名-子应用名.js, 组织名和项目名就是在webpack.config.js中配置的orgName和projectName.- 入口文件一定要导出
bootstrap、mount、unmount三个方法,用于主应用加载子应用。且必须返回promise,注册子应用的registerApplication函数的第二个参数。可以参考官方文档的简单例子。 这里我们换成async
- 入口文件一定要导出
let domEl = null;
export async function bootstrap(props) {
domEl = document.createElement("div");
domEl.id = "app1";
document.body.appendChild(domEl);
}
export async function mount(props) {
// 在这里通常使用框架将ui组件挂载到dom。请参阅https://single-spa.js.org/docs/ecosystem.html。
domEl.textContent = "webpackdemo is mounted!";
}
export async function unmount(props) {
// 在这里通常是通知框架把ui组件从dom中卸载。参见https://single-spa.js.org/docs/ecosystem.html
domEl.textContent = "";
}
- 去父应用中注册子应用
src\wxz-root-config.js
注册函数参数参考 官方文档
import { registerApplication, start } from "single-spa";
function pathPrefix(prefix) {
return function (location) {
return location.pathname.startsWith(`${prefix}`);
};
}
registerApplication({
name: "@single-spa/welcome",
app: () =>
import(
/* webpackIgnore: true */ // @ts-ignore-next
"https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
),
activeWhen: (location) => location.pathname === "/",
});
registerApplication({
name: "@wxz/webpackdemo",
app: () =>
import(
/* webpackIgnore: true */ // @ts-ignore-next
"@wxz/webpackdemo"
),
activeWhen: pathPrefix("/webpackdemo"),
});
start({
urlRerouteOnly: true,
});
- 在
src\index.ejs中引入子应用加载地址
<% if (isLocal) { %>
<script type="injector-importmap">
{
"imports": {
"@wxz/root-config": "//localhost:9000/wxz-root-config.js",
"@single-spa/welcome": "https://cdn.jsdelivr.net/npm/single-spa-welcome/dist/single-spa-welcome.min.js",
"@wxz/webpackdemo": "//localhost:9001/wxz-webpackdemo.js"
}
}
</script>
<% } %>
- 启动子应用,在
webpackdemo下执行npm run start,启动成功后, 在主应用根目录启动主应用npm run start,然后访问http://localhost:9000/webpackdemo,可以看到子应用已经成功加载到主应用中。访问http://localhost:9000可以看到 Welcome页
使用react框架的子应用
? Directory for new project reactapp
? Select type to generate single-spa application / parcel
? Which framework do you want to use? react
? Organization name (can use letters, numbers, dash or underscore) wxz
- 创建完
react,react-dom默认是最新的19.0.0版本,用这个版本试了一下会有各种问题,所以我降级到17.0.2版本
npm install react@17.0.2 react-dom@17.0.2
- 在
src/index.ejs里引共享包的地方,添加react和react-dom的引用 怎么说呢,这里引用一定要加,不加报错Failed to resolve module specifier "react"....巴拉巴拉的,但是加了后,不同的地址报不同的报错,最开始用的cdn.jsdelivr.net的,会报错 , 后来在 在issue上看到个相同报错, 换esm.sh的就好了。
<script type="injector-importmap">
{
"imports": {
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6.0.3/lib/es2015/esm/single-spa.min.js",
"react": "https://esm.sh/*react@17.0.2/index.js",
"react-dom": "https://esm.sh/*react-dom@17.0.2/index.js"
},
"scopes": {
"https://esm.sh/": {
"object-assign": "https://esm.sh/*object-assign@4.1.1/index.js",
"scheduler": "https://esm.sh/*scheduler@0.20.2/index.js"
}
}
}
</script>
- 配置文件
src\applications\reactapp\webpack.config.js中给个端口,随便定义,不冲突就行
const { merge } = require("webpack-merge");
const singleSpaDefaults = require("webpack-config-single-spa-react");
module.exports = (webpackConfigEnv, argv) => {
const defaultConfig = singleSpaDefaults({
orgName: "wxz",
projectName: "reactapp",
webpackConfigEnv,
argv,
});
return merge(defaultConfig, {
// modify the webpack config however you'd like to by adding to this object
devServer: {
port: 9002,
}
});
};
- 在
src/wxz-root-config.js中注册子应用
registerApplication({
name: "@wxz/reactapp",
app: () =>
import(
/* webpackIgnore: true */ // @ts-ignore-next
"@wxz/reactapp"
),
activeWhen: pathPrefix("/reactapp"),
});
- 在
src/index.ejs里引入子应用加载地址.
<script type="injector-importmap">
{
"imports": {
"@wxz/root-config": "//localhost:9000/wxz-root-config.js",
"@single-spa/welcome": "https://cdn.jsdelivr.net/npm/single-spa-welcome/dist/single-spa-welcome.min.js",
"@wxz/webpackdemo": "//localhost:9001/wxz-webpackdemo.js",
"@wxz/reactapp": "//localhost:9002/wxz-reactapp.js"
}
}
</script>
- 启动子应用,在启动主应用,然后访问
http://localhost:9000/reactapp,可以看到下面就成功了
使用vue框架的子应用
? Directory for new project vueapp
? Select type to generate single-spa application / parcel
? Which framework do you want to use? vue
? Organization name (can use letters, numbers, dash or underscore) wxz
- 唉,报错了,报错如下:
因为
vue-cli-plugin-single-spa内部用到了 systemjs-webpack-interop/SystemJSPublicPathWebpackPlugin: 它是用于处理 Webpack 打包后的模块加载路径的插件。主要用于在 SystemJS 中设置公共路径(public path),以确保模块能够正确加载。
但是 我用create-single-spa 最新版本去创建的项目,最新版是 5.0.5, 模板中默认是用import-map-injector use-injector 动态加载模块的,没有引入 system.js,在indx.ejs中引入一下system.js相关的包就不报错了
- 但是又有一个新的报错如下:
原因呢,import() 函数引入 vueapp 项目时,vue项目导出的的生命周期函数bootstrap、mount、unmount等没有拿到
暂时没找到原因, 所以试了 create-single-spa的版本降低到4.1.5,用这个版本创建项目,index.ejs中默认用的 system.js。
创建子应用: create-single-spa的版本降低到4.1.5,用这个版本创建项目
创建vue框架项目
- 首先创建容器应用, 创建成功后启动可才能会报错
export没有之类的,可能因为官方默认注册的welcom子应用更新了吧,不用管,删掉这个子应用的注册和引用 - 用
create-single-spa创建vue子应用,创建成功后: 1) 在wxz-root-config.js中注册 2) 在index.ejs中引入子应用加载地址,以及 添加vue依赖引用, 然后启动项目,但是报错了哈哈,报错如下:
创建:
? Directory for new project vueapp
? Select type to generate single-spa application / parcel
? Which framework do you want to use? vue
? Organization name (can use letters, numbers, dash or underscore) wxz
注册:rootconfig\src\wxz-root-config.js
// rootconfig\src\wxz-root-config.js
import { registerApplication, start } from "single-spa";
registerApplication({
name: "@wxz/vueapp",
app: () => System.import("@wxz/vueapp"),
activeWhen: ["/vueapp"],
});
start({
urlRerouteOnly: true,
});
引入:rootconfig\src\index.ejs
<script type="systemjs-importmap">
{
"imports": {
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js",
"vue": "https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js",
"vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.6.5/dist/vue-router.min.js"
}
}
</script>
<% if (isLocal) { %>
<script type="systemjs-importmap">
{
"imports": {
"@wxz/root-config": "//localhost:9000/wxz-root-config.js",
"@wxz/vueapp":"//localhost:9001/js/app.js"
}
}
</script>
<% } %>
3. 因为 vue-cli-plugin-single-spa 插件内打包输出是 umd, 修改成 system,在重新启动,就没有报错了
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
output: {
libraryTarget: "system"
},
externals: ["vue", "vue-router"]
}
})
创建react框架项目
? Directory for new project reactapp
? Select type to generate single-spa application / parcel
? Which framework do you want to use? react
? Which package manager do you want to use? npm
? Will this project use Typescript? No
? Organization name (can use letters, numbers, dash or underscore) wxz
? Project name (can use letters, numbers, dash or underscore) reactapp
- 注册、模板中添加加载地址, 添加
react、react-dom依赖, 然后启动,看到下图就成功了
import { registerApplication, start } from "single-spa";
registerApplication({
name: "@wxz/vueapp",
app: () => System.import("@wxz/vueapp"),
activeWhen: ["/vueapp"],
});
registerApplication({
name: "@wxz/reactapp",
app: () => System.import("@wxz/reactapp"),
activeWhen: ["/reactapp"]
});
start({
urlRerouteOnly: true,
});
<script type="systemjs-importmap">
{
"imports": {
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js",
"vue": "https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js",
"vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.6.5/dist/vue-router.min.js",
"react": "https://unpkg.com/react@17/umd/react.production.min.js",
"react-dom": "https://unpkg.com/react-dom@17/umd/react-dom.production.min.js",
"react-router-dom": "https://cdn.jsdelivr.net/npm/react-router-dom@5.2.0/umd/react-router-dom.min.js"
}
}
</script>
<script type="systemjs-importmap">
{
"imports": {
"@wxz/root-config": "//localhost:9000/wxz-root-config.js",
"@wxz/vueapp":"//localhost:9001/js/app.js",
"@wxz/reactapp":"//localhost:9002/wxz-reactapp.js"
}
}
</script>
create-single-spa@4.1.5的版本创建项目一路下来很顺利
子应用创建成功,每个子应用内部路由等和单独创建的一样使用
创建Parcel
- Parcel用来创建公共UI,涉及跨框架共享UI时,需要使用Parcel;如果所有应用都是同一个框架,官方建议使用相同框架创建组件去引用,而不是使用Parcel
- Parcel的定义可以使用任何single-spa支持的框架,它也是单独的应用,需要单独启动,但它不关联路由。
- Parcel应用的模块访问地址也需要被添加到import-map中,其它微应用通过System.import方法进行引用
- Parcel和微应用的区别是,Parcel需要手动挂载,微应用注册后访问会自动挂载
- 创建,假设用vue2框架
Directory for new project navparcel
? Select type to generate single-spa application / parcel
? Which framework do you want to use? vue
? Organization name (can use letters, numbers, dash or underscore) wxz
- 修改配置文件(
navparcel\vue.config.js)打包输出system
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
output: {
libraryTarget: "system"
},
externals: ["vue", "vue-router"]
}
})
rootconfig\src\index.ejs引入
<script type="systemjs-importmap">
{
"imports": {
"@wxz/root-config": "//localhost:9000/wxz-root-config.js",
"@wxz/vueapp":"//localhost:9001/js/app.js",
"@wxz/reactapp":"//localhost:9002/wxz-reactapp.js",
"@wxz/navparcel": "//localhost:9003/js/app.js"
}
}
</script>
- 写公共UI,先来个最简单的看效果
navparcel\src\App.vue
<template>
<div id="app">
logologologologologologologologologologologologologologologologologologologologologologo
</div>
</template>
<script>
export default {
name: 'App',
components: {
}
}
</script>
5. 启动 navparcel 项目
- 在reactapp中引用公共UI
- 在vueapp中引用公共UI
创建公共方法库
- 跨应用的js逻辑代码,放一些公共方法等
- 创建
Directory for new project tools
? Select type to generate in-browser utility module (styleguide, api cache, etc)
? Which framework do you want to use? none
? Which package manager do you want to use? npm
? Will this project use Typescript? No
? Organization name (can use letters, numbers, dash or underscore) wxz
? Project name (can use letters, numbers, dash or underscore) tools
- 添加要导出的公共方法
export function getUrlParams(url) {
let urls = url || window.location.href;
let params = {};
let urlStr = urls.split('?')[1];
if (urlStr) {
let arr = urlStr.split('&');
arr.forEach(item => {
let arr2 = item.split('=');
params[arr2[0]] = arr2[1];
});
}
return params;
}
export function formatTime(time) {
let date = new Date(time);
let year = date.getFullYear();
let month = date.getMonth() + 1;
let day = date.getDate();
let hour = date.getHours();
let minute = date.getMinutes();
let second = date.getSeconds();
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
- 在vueapp中使用,放哪用自己随意
async mounted() {
const { getUrlParams, formatTime } = await window.System.import('@wxz/tools');
console.log('getUrlParams', getUrlParams());
console.log('formatTime', formatTime('2025/01/21'));
}
- 在reactapp中使用