微前端是前端的一种架构类似于后端的微服务。
微前端主要功能是把一个功能复杂且庞大的前端项目拆成很多个微小的项目,每个小项目就是一个功能模块,每个功能模块可以单独上线不影响其他模块。
优点:
1.减少代码耦合度
2. 灵活部署
3.便于维护
缺点:
1.开发复杂,开发时候需要启动多个项目
2.跟踪和调试问题需要跨多个系统
实现方法:
1.使用 HTTP 服务器的路由来重定向多个应用
2.在不同的框架之上设计通讯、加载机制, 如 Single-SPA
3.通过组合多个独立应用、组件来构建一个单体应用
4.iFrame。使用 iFrame 及自定义消息传递机制
5.使用纯 Web Components 构建应用
注释:“实现的方法各有各的有缺点。在我看来最原生的html 写法才是耦合度最低的微前端”
今天主要讲的是目前使用比较多的single-spa 的方式实现微前端
微前端主要分为四个部分:
加载器:也就是微前端架构的核心,主要用来调度子应用,决定何时展示哪个子应用, 可以把它理解成电源。
包装器:有了加载器,可以把现有的应用包装,使得加载器可以使用它们,它相当于电源适配器。
主应用:一般是包含所有子应用公共部分的项目—— 它相当于电器底座
子应用:众多展示在主应用内容区的应用—— 它相当于你要使用的电器
注释:微前端的子应用和主应用是可以是各种框架如:react,angular,vue;个人目前很长一段时间都在使用vue所以下面就使用vue来讲
构建加载器和主项目
第一步:使用vue-cli 新建一个项目就叫parent.这个一步就不贴代码了,不知道vue-cli 怎么新建项目的可以去百度,度娘很强大
第二步:修改parent 项目 使其成为加载器。(加载器个人理解就是用来加载和挂载子项目代码的东西)(记得看代码中注释)
parent 项目vue-router 开启history模式
注册一个路由这个路由不填写component
字段,
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
// 子项目history模式下,父项目的模糊匹配。不建议这样做
// path: '/vue*',
path: '/vue',
name: 'vue'
},
{
path: '/two',
name: 'Two'
},
]
const router = new VueRouter({
mode: 'history',
routes
})
export default router
第三步:添加single-spa配置文件single-spa-config.js ,当然在这个之前需要先安装和了解一下single-spa 注册的api(记得看代码中注释)
npm install single-spa --save -d
singleSpa.registerApplication:这是注册子项目的方法。参数如下:
appName: 子项目名称
applicationOrLoadingFn: 子项目注册函数,用户需要返回 single-spa 的生命周期对象。
后面我们会介绍single-spa的生命周期机制
activityFn: 回调函数入参 location 对象,可以写自定义匹配路由加载规则。
singleSpa.start:这是启动函数。
// single-spa-config.js
import axios from 'axios'
import * as singleSpa from 'single-spa'; //导入single-spa
/*
* runScript:一个promise同步方法。可以代替创建一个script标签,然后加载服务
* */
const runScript = async (url) => { //把子项目js挂载到页面上
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.onload = resolve;
script.onerror = reject;
const firstScript = document.getElementsByTagName('script')[0];
firstScript.parentNode.insertBefore(script, firstScript);
});
};
/*
* getManifest:远程加载manifest.json 文件,解析需要加载的js
* url: manifest.json 链接
* bundle:entry名称
* */
const getManifest = (url, bundle) => new Promise(async (resolve) => { // 解析请求子项目打包出的manifest.json 得到想要js名称并通过 runScript 挂载到body元素里面
const { data } = await axios.get(url);
const { entrypoints, publicPath } = data;
const assets = entrypoints[bundle].assets;
for (let i = 0; i < assets.length; i++) {
await runScript(publicPath + assets[i]).then(() => {
if (i === assets.length - 1) {
resolve()
}
})
}
});
singleSpa.registerApplication( //注册微前端服务
'twoVue',
async () => {
// 注册用函数,
// return 一个singleSpa 模块对象,模块对象来自于要加载的js导出
// 如果这个函数不需要在线引入,只需要本地引入一块加载:
// () => import('xxx/main.js')
let singleVue = null;
await getManifest('/two/manifest.json', 'app').then(() => {// /two 是在vue.config里面设置的代理路径
singleVue = window.singleVueTwo;
});
return singleVue;
},
location => location.pathname.startsWith('/two') // 配置微前端模块前缀
);
singleSpa.registerApplication( //注册微前端服务
'singleDemo',
async () => {
// 注册用函数,
// return 一个singleSpa 模块对象,模块对象来自于要加载的js导出
// 如果这个函数不需要在线引入,只需要本地引入一块加载:
// () => import('xxx/main.js')
let singleVue = null;
await getManifest('/child/manifest.json', 'app').then(() => { // /child 是在vue.config里面设置的代理路径
singleVue = window.singleVue;
});
return singleVue;
},
location => location.pathname.startsWith('/vue') // 配置微前端模块前缀
);
singleSpa.start(); // 启动
第四步:把single-spa-config.js 文件加入到main.js 这个时候你们肯定很好奇main.js 是什么样子的,上代码
import Vue from 'vue'
import App from './App.vue'
import Ant from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';
import router from './router'
import VueRouter from 'vue-router'
import './single-spa-config.js'
console.log(moment)
Vue.config.productionTip = false
Vue.use(Ant);
new Vue({
router,
render: h => h(App)
}).$mount('#app')
第五步: 你们会发现mian.js跟平常的写法没有不同,但是平常一般是vue跟element-ui 相配,这个例子我们用到了ant-design-vue 和vue 想配合。当然使用组件的时候需要先安装 npm install ant-design-vue --save -d到这个一步就差一个需要就是app.vue组件上需要有给页面挂载的元素,就是添加一个 id 为我们设置的 那两个路由的id 就可以了,当然也可以自己另外起,话不多说上代码(记得看代码中注释)
<template>
<a-layout id="components-layout-demo-custom-trigger">
<a-layout-sider :trigger="null" collapsible v-model="collapsed">
<div class="logo" />
<a-menu theme="dark" mode="inline">
<a-sub-menu key="1">
<span slot="title">
<a-icon type="user" />
<span>
vue
<!-- <a href="/vue#">
vue
</a> -->
</span>
</span>
<a-menu-item key="1-1">
<a href="/vue" @click="goToChildRoute">
Home
</a>
</a-menu-item>
<a-menu-item key="1-2" >
<a href="/two" @click="goToChildRoute">
About
</a>
</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header style="background: #fff; padding: 0" />
<a-layout-content :style="{ margin: '24px 16px', padding: '24px', background: '#fff', minHeight: '280px' }">
<div class="content">
<!--这是右侧内容栏-->
<div id="single-vue" class="single-spa-vue">
<div id="vue"></div> <!--这个是挂载第一个子项目的元素-->
</div>
<div id="two"></div> <!--这个是挂载第二个子项目的元素-->
</div>
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script>
import { navigateToUrl } from 'single-spa';
export default {
data() {
return {
collapsed: false,
};
},
methods: {
goToChildRoute(e) {
// 官方指定跳转
e.preventDefault();
navigateToUrl(e);
}
}
}
</script>
<style>
#components-layout-demo-custom-trigger {
height: 100%;
}
#components-layout-demo-custom-trigger .trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
}
#components-layout-demo-custom-trigger .trigger:hover {
color: #1890ff;
}
#components-layout-demo-custom-trigger .logo {
height: 32px;
background: rgba(255, 255, 255, 0.2);
margin: 16px;
}
</style>
第五步:这些都构建好了就得看一下vue.config.js 得代理配置了,只要是在本地的时候去请求子项目服务会产生跨域,需要代理或者添加头部允许跨域,我是直接使用代理。那你肯定会想后面上线怎么办,后面上线的时候使用nginx的反向代理。上代码
module.exports = {
devServer: {
// disableHostCheck: true,
proxy: {
'/child': {
// target: 'http://10.0.30.96/mock/142/',
target: 'http://127.0.0.1:3000/',
// target: 'http://10.0.60.178:9005/',
changeOrigin: true,
pathRewrite: {
'^/child': '/',
},
},
'/two': {
// target: 'http://10.0.30.96/mock/142/',
target: 'http://127.0.0.1:4000/',
// target: 'http://10.0.60.178:9005/',
changeOrigin: true,
pathRewrite: {
'^/two': '/',
},
},
},
},
};
当然target 是我们后面启动的两个子项目的地址路径和端口。
到这边对主项目和加载器的改造就到这边了
注释:说错的地方还是忽略掉的地方各位看官请评论指出来
构建包装器和子应用
第一步: 没错依然是使用vue-cli 进行构建子项目
第二步:需要安装single-spa的vue 包装器依赖
npm install single-spa-vue --save -d
第三步: 使用single-spa-vue 改造mian.js 函数。引入single-spa-vue并传入vue对象和vue 挂载函数就可以生成一个包含sing-spa所需要的生命周期。话不多说,上代码(记得看代码中注释)
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from "single-spa-vue";
const vueOptions = {
el: "#vue", //这个地方使用 主项目中我们在app.vue 里面添加的用来挂载子项目的id
router,
render: h => h(App)
};
if (!window.singleSpaNavigate) { // 如果不是single-spa模式 那挂载的元素就用项目本身的 app
delete vueOptions.el;
new Vue(vueOptions).$mount('#app');
}
// singleSpaVue包装一个vue微前端服务对象
const vueLifecycles = singleSpaVue({
Vue,
appOptions: vueOptions
});
// 导出生命周期对象
export const bootstrap = vueLifecycles.bootstrap; // 启动时
export const mount = vueLifecycles.mount; // 挂载时
export const unmount = vueLifecycles.unmount; // 卸载时
export default vueLifecycles;
第四步:修改webpack 即 vue.config.js 需要 配置本地服务地址和端口,需要配置打包vuejs挂载目标和挂载对象名称,需要配置打包或者服务启动时候生成manifest.json安装 stats-webpack-plugin 插件。然后上代码(记得看代码中注释)
var StatsPlugin = require('stats-webpack-plugin');
module.exports = {
publicPath: "//localhost:3000/", //这个是本地服务的地址,就是主项目里面进行代理的地址和端口
// css在所有环境下,都不单独打包为文件。这样是为了保证最小引入(只引入js)
css: {
extract: false
},
configureWebpack: {
devtool: 'none', // 不打包sourcemap
output: {
library: "singleVue", // 导出vuejs 挂载到 window对象的名称。在single-spa 注册的时候有用到,可以返回主项目第三步查看注册函数singleSpa.registerApplication
libraryTarget: "window", //挂载目标
},
plugins: [
new StatsPlugin('manifest.json', {
chunkModules: false,
entrypoints: true,
source: false,
chunks: false,
modules: false,
assets: false,
children: false,
exclude: [/node_modules/]
}),
]
}
};
到这边就可以启动主项目和子项目查看效果了下面是效果截图
到这边一个初步的微前端就搭建完成了。
后面还需要一些东西的比如:
1. 样式隔离 可以使用postcss-selector-namespace 也可以自己用其他的办法
2.公共依赖的提取 方法很多网上很多是使用 system.js还有其他方法还可以直接把公共依赖挂载到window 上给子项目使用。可以研究看看自己更倾向于哪种方法
3.主项目如何和子项目通讯 可以通过主项目的store
4.如何整合angular 和react 项目
等等还有很多东西,有需要可以留下你的评论
注释:写的不好凑合看
再见