一、为什么要学习微前端
什么是微前端
微前端就是将不同的功能按照不同的维度拆分成多个子应用。通过主应用来加载这些子应用。
微前端的核心在于拆, 拆完后在合!
为什么去使用微前端
- 不同团队间开发同一个应用技术栈不同怎么破?
- 希望每个团队都可以独立开发,独立部署怎么破?
- 项目中还需要老的应用代码怎么破?
我们是不是可以将一个应用划分成若干个子应用,将子应用打包成一个个的lib。当路径切换时加载同的子应用。这样每个子应用都是独立的,技术栈也不用做限制了!从而解决了前端协同开发问题
怎样落地微前端
微前端的灵感来源于,计算机上的应用,每一次用户打开一个应用,就相当于打开了一个新的页面
- 2018年 Single-SPA诞生了, single-spa 是一个用于前端微服务化的 JavaScript 前端解决方案 (本身没有处理样式隔离, js 执行隔离) 实现了路由劫持和应用加载
- 2019年 qiankun 基于Single-SPA, 提供了更加开箱即用的 API ( single-spa + sandbox + import-html-entry ) 做到了,技术栈无关、并且接入简单(像iframe 一样简单)
总结:子应用可以独立构建,运行时动态加载,主子应用完全解耦,技术栈无关,靠的是协议接入(子应用必须导出 bootstrap、mount、unmount方法)
这里先回答下大家的问题:
- 这不是iframe吗?
如果使用 iframe , iframe 中的子应用切换路由时用户刷新页面就尴尬了
更多参考 Why Not Iframe - 应用之间怎么通信
- 基于URL来进行数据传递,但是传递消息能力弱
- 基于 CustomEvent 实现通信
- 基于props主子应用间通信
- 使用全局变量、 Redux 进行通信
- 公共依赖
- CDN - externals
- webpack 联邦模块
微前端架构具备以下几个核心价值:
- 技术栈无关
主框架不限制接入应用的技术栈,微应用具备完全自主权 - 独立开发、独立部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新 - 增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略 - 独立运行时
每个微应用之间状态隔离,运行时状态不共享
二、SingleSpa 实战
Single-Apa完整的项目请参考 gitHub
构建子应用
我们需要父应用加载子应用,需要暴露三个方法:
1. bootstrap
2. mount
3. unmount
1. 构建子应用
// 启动项目安卓依赖
vue create single-child
npm i --save single-spa-vue
// main.js中导入依赖
import singleSpaVue from 'single-spa-vue'
const appOptions = {
el: '#vue', // 挂载到父应用中的 id 为 vue 的标签中
router,
render: h => h(App)
}
const vueLifeCycle = singleSpaVue({ // 返回single-spa 的生命周期也就是 bootstrap/mount/unmount
Vue,
appOptions
});
// single规定的协议,父应用会调用这些方法
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;
// 这样做还有一个严重的问题,子应用无法启动了??
2. 配置子应用中的打包路径
// 配置vue.config.js
module.exports = {
configureWebpack: {
output: {
library: 'singleVue',
libraryTarg: 'umd'
},
devServer: {
port: 10000
}
}
};
3. 配置子应用的路由
const router = new VueRouter({
mode: 'history',
base: '/vue', // 配置路由的基础路径
routes
})
4. 父应用搭建
vue create single-parent
npm i --save single-spa // 注意这里是single-spa
5. 将子应用挂载到 id="vue"
的容器中
<div id="app">
<!-- 当路由切换到 /vue 时加载子应用 -->
<router-link to="/vue">加载vue引用</router-link>
<router-view/>
<!-- 子应用加载的位置 -->
<div id="vue"></div>
</div>
6. 配置父应用加载子应用
import { registerApplication, start } from 'single-spa'
async function loadScript(url) { // 异步加载子组件中的脚本
return new Promise((resolve, reject) => {
let script = document.createElement('script');
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
registerApplication(
'myVueApp',
async () => {
console.log('加载模块');
// 加载子应用中的脚本
await loadScript(`http://localhost:10000/js/chunk-vendors.js`)
await loadScript(`http://localhost:10000/js/app.js`)
// 这里需要要返回 bootstrap/mount/unmount
return window.singleVue
},
location => location.pathname.startsWith('/vue'), // 此路径用来判断当前路由切换到 /vue 的路径下,需要加载我们定义的子应用
{ a: 1 } // 选传,传给子应用 props 的参数,可以是对象或值
);
start(); // 启动应用
7. 配置子应用的路径
// 设置路径
if (window.singleSpaNavigate) { // 如果是父应用去应用,那会自动挂载一个属性为true
__webpack_public_path__ = 'http://localhost:10000/'
}
8. 希望子应用可以独立运行,在子应用中添加一个配置
if(!window.singleSpaNavigate){
delete appOptions.el; // 子应用中没有#vue,所以需要手动删除,挂载到 #app 中
new Vue(appOptions).$mount('#app');
}
singleSpa 缺陷
- 不能动态加载JS文件
- 样式不隔离
- 全局对象,没有JS沙箱的机制
三、qiankun 实战
乾坤完整的demo请参考:gitHub
特点
- 简单:任意 js 框架均可使用。微应用接入像使用接入一个 iframe 系统一样简单,但实际不是 iframe。
- 完备:几乎包含所有构建微前端系统时所需要的基本能力,如 样式隔离、js 沙箱、预加载等。
- 生产可用:已在蚂蚁内外经受过足够大量的线上系统的考验及打磨,健壮性值得信赖。
项目构建
1. 主应用搭建 qiankun-base
// 构建项目,下载依赖,只需要在主项目中安装 qiankun 即可
vue create qiankun-base
npm i --save qiankun
// 配置主项目的加载 main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import {registerMicroApps, start} from 'qiankun';
Vue.config.productionTip = false
Vue.use(ElementUI);
const apps = [
{
name: 'vueApp', // 应用的名字
entry: 'http://localhost:10000/', // 默认加载这个html,解析里面的js动态的执行(子应用必须支持跨域,内部使用的是 fetch)
container: '#vue', // 要渲染到的容器名id
activeRule: '/vue' // 通过哪一个路由来激活
},
{
name: 'reactApp',
entry: 'http://localhost:20000/',
container: '#react',
activeRule: '/react'
}
];
registerMicroApps(apps); // 注册应用
start(); // 开启应用
new Vue({
router,
render: h => h(App)
}).$mount('#app')
<!-- 设置容器 -->
<template>
<div>
<el-menu :router="true" mode="horizontal">
<!-- 主应用中也可以放自己的路由 -->
<el-menu-item index="/">首页</el-menu-item>
<!-- 引用其他的子应用 -->
<el-menu-item index="/vue">vue应用</el-menu-item>
<el-menu-item index="/react">react应用</el-menu-item>
</el-menu>
<router-view v-show="$route.name"></router-view>
<div id="vue"></div>
<div id="react"></div>
</div>
</template>
2. 搭建Vue子项目
vue create qiankun-vue
// 子项目中不需要安装任何依赖,父组件会给window设置一些环境变量
// mian.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
/*
new Vue({
router,
render: h => h(App)
}).$mount('#app')
*/
let instance = null;
function render(props) {
// props 组件通信
instance = new Vue({
router,
render: h => h(App)
}).$mount('#app') // 这里是挂载到自己的HTML中,基座会拿到这个挂载后的HTML,将其插入进去
}
if (!window.__POWERED_BY_QIANKUN__) { // 如果是独立运行,则手动调用渲染
render();
}
if(window.__POWERED_BY_QIANKUN__){ // 如果是qiankun使用到了,则会动态注入路径
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
// 根据 qiankun 的协议需要导出 bootstrap/mount/unmount
export async function bootstrap(props) {
};
export async function mount(props) {
render(props);
};
export async function unmount(props) {
instance.$destroy();
};
// 设置router路径
const router = new VueRouter({
mode: 'history',
base: '/vue',
routes
})
// 配置打包 vue.config.js
module.exports = {
devServer: {
port: 10000,
headers:{
'Access-Control-Allow-Origin': '*' // 允许跨域
}
},
configureWebpack: {
output: {
library: 'vueApp',
libraryTarget: 'umd'
}
}
};
3. 搭建React项目
npx create-react-app qiankun-react
npm i --save-dev react-app-rewired
// 入口配置 /src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
function render(){
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
}
if(!window.__POWERED_BY_QIANKUN__){
render();
}
export async function bootstrap(){
}
export async function mount() {
render()
}
export async function unmount(){
ReactDOM.unmountComponentAtNode( document.getElementById('root'));
}
// 配置启动 config-overrides.js
module.exports = {
webpack:(config)=>{
config.output.library = 'reactApp';
config.output.libraryTarget = 'umd';
config.output.publicPath = 'http://localhost:20000/';
return config;
},
devServer:(configFunction)=>{
return function (proxy,allowedHost){
const config = configFunction(proxy,allowedHost);
config.headers = {
"Access-Control-Allow-Origin":'*'
}
return config
}
}
}
添加react环境变量 .env
PORT=20000
WDS_SOCKET_PORT=20000
// 配置react路由
import { BrowserRouter, Route, Link } from "react-router-dom"
const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react" : "";
function App() {
return (
<BrowserRouter basename={BASE_NAME}>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Route path="/" exact render={() => <h1>hello home</h1>}></Route>
<Route path="/about" render={() => <h1>hello about</h1>}></Route>
</BrowserRouter>
);
}
完整的项目请参考 gitHub