什么是微前端?
微前端就是将不同的功能按照不同的维度拆分成多个子应用。通过主应用(基座)来加载这些子应用。 微前端的核心在于拆,拆完后在合
为什么要去用他?
- 不同的团队开发同一个应用,如何使用不同的技术栈?
- 每个团队都可以独立开发,独立部署
- 项目中一些老的应用代码
微前端的实现思想就是将一个应用划分成若干个子应用,将子应用打包成一个个的lib。当路径切换 时加载不同的子应用。这样每个子应用都是独立的,技术栈也不用做限制了!从而解决了前 端协同开发问题
实现微前端的几种方案
方案 | 描述 | 优点 | 缺点 |
---|---|---|---|
Nginx路由转发 | 通过Nginx配置反向代理来实现不同路径映射到不同应用 | 简单,快速,易配置 | 在切换应用时会触发浏览器刷新,影响体验 |
iframe嵌套 | 父应用单独是一个页面,每个子应用嵌套一个iframe | 实现简单,子应用之间自带沙箱,天然隔离,互不影响 | frame的样式显示、兼容性等都具有局限性;太过简单而显得low |
npm包形式 | 子工程以NPM包的形式发布源码;打包构建发布还是由基座工程管理,打包时集成。 | 打包部署慢,不能单独部署 | 打包部署慢,不能单独部署 |
通用中心路由基座式 | 子工程可以使用不同技术栈;子工程之间完全独立,无任何依赖;统一由基座工程进行管理,按照DOM节点的注册、挂载、卸载来完成。 | 不限定技术栈,单独部署 | 通信方式不够灵活 |
特定中心路由基座式 | 子业务线之间使用相同技术栈;基座工程和子工程可以单独开发单独部署;子工程有能力复用基座工程的公共基建。 | 通信方式多,单独部署 | 限定技术栈 |
目前主流的微前端框架
- Single-SPA single-spa 是一个用于前端微服务化的 JavaScript 前端解决方案 (本身没有处理样式隔离, js 执行隔离) 实现了路由劫持和应用加载
- qiankun 基于Single-SPA, 提供了更加开箱即用的 API ( single-spa + sandbox+ import-html-entry ) 做到了,技术栈无关、并且接入简单(像iframe 一样简单)
single-spa
基座和子项目都使用vue技术栈。react其实也是一个道理:基座提供注册逻辑、子应用提供三个协议接入方法和打包格式
基座(vue)
在基座里我们调用single-spa提供给我们的registerApplication和start的方法 1.registerApplication参数有四个个appNameOrConfig、appOrLoadApp、activeWhen、customProps。分别对应的是注册的子项目名和一些配置。加载子项目时执行的fn、执行的规则、和通信的参数
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import {registerApplication,start} from 'single-spa';
// 动态加载url
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);
})
}
// singleSpa 缺陷 不够灵活 不能动态加载js文件
// 样式不隔离 没有js沙箱的机制
registerApplication('myVueApp',
async ()=>{
//当匹配成功的时候,加载子应用的js
await loadScript(`http://localhost:10000/js/chunk-vendors.js`);
await loadScript(`http://localhost:10000/js/app.js`)
return window.singleVue; // 子应用打包umd格式。bootstrap mount unmount
},
//当匹配到/vue的时候执行上面的方法
location => location.pathname.startsWith('/vue'),
)
//启动应用
start();
new Vue({
router,
render: h => h(App)
}).$mount('#app')
子项目
子项目最重要的就是提供三个方法 bootstrap、mount、unmount 和 打包格式
main.js
这里借助了社区提供的single-spa-vue
、react就使用single-spa-react
。它会默认导出三个方法。
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue';
const appOptions = {
el:'#vue', // 挂载到父应用中的id为vue的标签中
router,
render: h => h(App)
}
const vueLifeCycle = singleSpaVue({
Vue,
appOptions
})
// 如果是微前端模式下,single-spa会在window上挂在一个singleSpaNavigate的属性。
// 这时候我们需要将public_path改成子项目中的地址。
if(window.singleSpaNavigate){
__webpack_public_path__ = 'http://localhost:10000/'
}
//这个是让子项目能够单独的运行
if(!window.singleSpaNavigate){
delete appOptions.el;
new Vue(appOptions).$mount('#app');
}
// 协议接入 我定好了协议 父应用会调用这些方法
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;
配置子项目的打包方式
vue.config.js
module.exports = {
configureWebpack:{
output:{
library:'singleVue',
libraryTarget:'umd'
},
devServer:{
port:10000
}
}
}
single-spa 的缺陷
- 从上图可以看出,基座在匹配到路径时候需要手动的添加js,如果子项目打包出一万个js,那....
- 样式不隔离
- 没有js沙箱的机制
qiankun
向接入iframe一样简单 qiankun是通过fetch方法直接把html插入到容器里,所以子项目需要允许跨域才行
基座
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";
Vue.use(ElementUI);
// 从qiankun中导出两个方法
import { registerMicroApps, start } from "qiankun";
const apps = [
{
name: "vueApp", // 应用的名字
entry: "//localhost:10000",//加载的html路径
container: "#vue", // 容器名
activeRule: "/vue", // 激活的路径
},
{
name: "reactApp",
entry: "//localhost:20000",
container: "#react",
activeRule: "/react",
},
];
registerMicroApps(apps); // 注册应用
start(); // 开启
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
子项目
import Vue from 'vue'
import App from './App.vue'
import router from './router'
// Vue.config.productionTip = false
let instance = null
function render(props) {
instance = new Vue({
router,
render: h => h(App)
}).$mount('#app'); // 这里是挂载到自己的html中 基座会拿到这个挂载后的html 将其插入进去
}
if (window.__POWERED_BY_QIANKUN__) { // 动态添加publicPath
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) { // 默认独立运行
render();
}
// 子组件的协议就ok了
export async function bootstrap(props) {
};
export async function mount(props) {
render(props)
}
export async function unmount(props) {
instance.$destroy();
}
子项目打包配置
module.exports = {
devServer:{
port:10000,
// 需要允许跨域
headers:{
'Access-Control-Allow-Origin':'*'
}
},
configureWebpack:{
output:{
library:'vueApp',
libraryTarget:'umd'
}
}
}
应用通信
- 基于URL来进行数据传递,但是传递消息能力弱
- 基于 CustomEvent 实现通信
- 基于props主子应用间通信
- 使用全局变量、 Redux 进行通信
css隔离方案
子应用之间样式隔离:
- Dynamic Stylesheet 动态样式表,当应用切换时移除老应用样式,添加新应用样式
主应用和子应用之间的样式隔离:
- BEM (Block Element Modifier) 约定项目前缀
- CSS-Modules 打包时生成不冲突的选择器名
- Shadow Dom 真正意义上的隔离
js沙箱机制
- 快照沙箱,在应用沙箱挂载或卸载时记录快照,在切换时依据快照恢复环境 (无法支持多实
例)
- Proxy 代理沙箱,不影响全局环境
快照沙箱
- 1.激活时将当前window属性进行快照处理
- 2.失活时用快照中的内容和当前window属性比对
- 3.如果属性发生变化保存到 modifyPropsMap 中,并用快照还原window属性
- 4.在次激活时,再次进行快照,并用上次修改的结果还原window
class SnapshotSandbox{
constructor(){
this.proxy = window;
this.modifyPropsMap = {};
this.active();
}
active(){ // 激活
this.windowSnapshot = {}; // 拍照
for(const prop in window){
if(window.hasOwnProperty(prop)){
this.windowSnapshot[prop] = window[prop];
}
}
Object.keys(this.modifyPropsMap).forEach(p=>{
window[p] = this.modifyPropsMap[p];
})
}
inactive(){ // 失活
for(const prop in window){
if(window.hasOwnProperty(prop)){
if(window[prop] !== this.windowSnapshot[prop]){
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop]
}
}
}
}
}
代理沙箱
每个应用都创建一个proxy来代理window,好处是每个应用都是相对独立,不需要直接更改全局window属性!
class ProxySandbox {
constructor() {
const rawWindow = window;
const fakeWindow = {}
const proxy = new Proxy(fakeWindow, {
set(target, p, value) {
target[p] = value;
return true
},
get(target, p) {
return target[p] || rawWindow[p];
}
});
this.proxy = proxy
}
}
基于jquery项目的微前端重构
项目背景
项目中使用的是backbonejs
的jq框架,还包含bootstrap
等一系列依赖。虽说jquery一把梭,但维护起来着实有点繁琐。于是就产生了用微前端重构的想法。
- 项目中使用的路由模式是hash模式
首先由于本项目是一个类似于后台管理系统的界面,左侧有很多菜单。如果jquery项目作为子项目可能改动会比较大,于是就直接将jq项目作为基站。当点击左侧路由的时候,通过匹配机制去加载子应用然后嵌入到容器内。
jquery
首先引入qiankun
import {registerMicroApps, addGlobalUncaughtErrorHandler, start} from 'qiankun';
mian.js
function genActiveRule(routerPrefix) {
return location => location.hash.startsWith('#' + routerPrefix);
}
$(function() {
// 布局
createLayout();
// 路由
createRoute();
// 前往上次的页面
gotoLastPage();
registerMicroApps([
{
name: 'micro',
entry: '//xxx.com/home/vue/',
container: '#main-app',
activeRule: genActiveRule('/vue')
}
]);
addGlobalUncaughtErrorHandler(event => {
console.error(event);
const {message: msg} = event;
// 加载失败时提示
if (msg && msg.includes('died in status LOADING_SOURCE_CODE')) {
console.error('子应用加载失败,请检查应用是否可运行');
}
});
start();
monitorOrders.init();
});
首先entry的内容就是我们子应用打包部署后能访问到的地址 增加了一个qiankun提供的错误提示
子项目(vue)
main.js
import Vue from "vue";
import VueRouter from "vue-router";
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
// import "./public-path";
import App from "./App.vue";
import routes from "./routes";
Vue.use(VueRouter);
Vue.use(ElementUI);
Vue.config.productionTip = false;
let instance = null;
let router = null;
/**
* 渲染函数
* 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行
*/
function render() {
console.log("window.__POWERED_BY_QIANKUN__", window.__POWERED_BY_QIANKUN__);
// 在 render 中创建 VueRouter,可以保证在卸载微应用时,移除 location 事件监听,防止事件污染
router = new VueRouter({
// 运行在主应用中时,添加路由命名空间 /vue
base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/",
mode: "hash",
routes,
});
// 挂载应用
instance = new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
}
// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log("VueMicroApp bootstraped");
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
console.log("VueMicroApp mount", props);
render(props);
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
console.log("VueMicroApp unmount");
instance.$destroy();
instance = null;
router = null;
}
vue.config.js
const path = require("path");
function resolve(dir) {
return path.join(__dirname, dir);
}
const stage = process.env.ENV === "testing";
const publicPath =
process.env.ENV === "production"
? xxxx:xxx
let output = {
library: "Micro",
// 将你的 library 暴露为所有的模块定义下都可运行的方式
libraryTarget: "umd",
// 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
jsonpFunction: `webpackJsonp_customerMicro`,
};
let cssExtract = {};
if (stage) {
output.filename = `js/[name].js?v=[hash]`;
output.chunkFilename = `js/[name].js?v=[hash]`;
cssExtract = {
filename: "css/[name].css?v=[hash]",
chunkFilename: "css/[name].css?v=[hash]",
};
}
module.exports = {
publicPath,
devServer: {
// 监听端口
port: 10200,
// 关闭主机检查,使微应用可以被 fetch
disableHostCheck: true,
// 配置跨域请求头,解决开发环境的跨域问题
headers: {
"Access-Control-Allow-Origin": "*",
},
},
css: {
extract: cssExtract,
},
configureWebpack: () => {
const conf = {
resolve: {
alias: {
"@": resolve("src"),
},
},
output: output,
};
return conf;
},
chainWebpack(config) {
config.plugins.delete("preload");
config.plugins.delete("prefetch");
config.module
.rule("vue")
.use("vue-loader")
.loader("vue-loader")
.tap((options) => {
options.compilerOptions.preserveWhitespace = true;
return options;
})
.end();
config.when(process.env.NODE_ENV === "development", (config) => config.devtool("cheap-source-map"));
},
};
我上面的配置是区分了测试环境和正式环境的。大家也可以根据实际的情况来。基站那边的entry也可以根据环境来。
微前端资源系列集合
感谢大家
1.如果文章对你有帮助,请动动你的小手指点个赞👍吧,你的赞是我创作的动力。
2.关注公众号「前端精英」!不定期推送高质量文章哦。