背景
微前端架构模式早就在2016年由ThoughtWorks年提出,微前端是模仿服务端微服务的理念而应用于浏览器端,即将多个单一的单体应用组合起来拼凑成唯一应用,各个单体应用还可以独立开发、独立运行、独立部署,这也是微前端比较重要的特点。
我司要开发一个中后台项目,本身toB的中后台项目因为周期时间长会形成项目难以维护和项目过大的缺点,我是体验过要完成一个小需求,本来只需要半天的时间就可以完成,结果代码查找和打包部署直接教育了我的天真想法,而且最重要的原因我们是多个团队共同开发,为了让不同的团队之间可以独立开发和部署,互相不影响,必须选择微前端。
技术选型
微前端是指一种架构模式,他有很多种实现方式,具体有哪些我就不一一介绍了,建议大家参考前端架构从入门到微前端这本书。
我们选择的是前端微服务化的实现模式,前端微服务化是指每个前端应用完全独立,通过模块化的方式组合应用。
技术栈:single-spa + vue
准备工作
- 项目地址
- systemjs、相关模块化的知识
- single-spa框架
- vue相关技术栈
我司基本流程图:
主项目
1.安装依赖:
- Single-spa
- Systemjs
模块加载器,用来加载子项目初始化相关资源,但是子项目入口文件必须打包成<font color=red>umd</font>格式,也可以配合构建工具webpack中的externals属性,将子项目和主项目公共的模块(vue,vuex,vue-router,loadsh…)提取到主项目中,配置成外链可以实现按需加载,可以减少子项目打包的体积,前提这些公共模块版本必须保持一致。
2.子项目注册(microfrontend.js):
import { registerApplication, start } from 'single-spa';
///// 文件的相关路径需要替换成自己项目实际路径
/**
*
* 获取子项目app.js文件
*/
function getApplication(path) {
return window.System.import(`${path}?time=${new Date().getTime()}`).then((res) => {
if (res.default) {
return window.System.import(res.default['app.js']).then((ret) => ret.default);
}
});
}
/**
* name: 第一个参数表示应用名称,name必须是string类型
*
* app: 加载函数 可以是一个Promise类型的 加载函数,返回的Promise resolve之后的结果必须是一个可以被解析的应用,
* 这个应用其实是一个包含single-spa各个生命周期函数 的对象(e.g: vue打包 引入后的app.js)。
*
* activeWhen: 激活函数 一个纯函数 window.loaction作为第一个参数被调用,只有函数返回的值为true时,应用才会被激活。
* 通常情况下,activity function会根据 window.location 的path来决定是否需要被激活(我就是这样玩的)
*
* single-spa根据顶级路由查找应用,而每个应用会处理自身的子路由。以下场景,single-spa会调用应用的activity funtion
* 1.hashchange or popstate 事件触发时(vue-router hash或history路由模式都会触发)
* 2.pushState or replaceState被调用时
* 3.在single-spa 手动调用 triggerAppChange 方法
* 4.checkActivityFunctions 方法被调用时
*
* customProps
* 对象 可以表示自定义字段,子应用生命周期函数可以获取
* 函数 两个参数 应用的名称和window.location
*/
const development = process.env.NODE_ENV === 'development';
/// web 部署根域名
const baseUrl = process.env.VUE_APP_WEB_URL;
const configProject = [
{
name: 'app1',
app: window.System.import('app-demo1').then((res) => res.default), /// 子项目通过插件完成配置的引入方式
activeWhen: (location) => location.pathname.startsWith('/app1'),
customProps: {
// 对象
everything: 'just do it'
}
// customProps: (name, location) => {
// // 函数
// return {everything: 'just do it'};
// }
},
{
name: 'app2',
app: getApplication(development ? 'http://localhost:8993/manifest.json' : `${baseUrl}/app2/manifest.json`),
activeWhen: (location) => location.pathname.startsWith('/app2'),
customProps: {
// 对象
everything: 'just do it'
}
}
];
/// 注册子应用
configProject.forEach((element) => {
registerApplication(element);
});
// 暴露single-spa启动方法 可以控制子应用的激活时机
export function appStart() {
return start();
}
上面相关的xx.mainifest.json文件是什么?下面子项目模块有介绍
3.修改入口文件(main.js):
相关逻辑代码,全部代码可以查看项目。
// 引入启动方法
import { appStart } from './microfrontend';
// Single-spa启动方法在此处调用,是因为我们项目一般需要前置条件才会容许项目挂载 挂载应用
/// e.g 比如登录
// async function login() {
// await 相关接口请求
// appStart();
// }
appStart();
4.修改router.js文件:
vue-router默认路由模式是hash模式,我们需要修改成history模式。
5.import maps:
使用import maps共享依赖 配合webpack externals,使依赖包作为外部依赖,告诉你的应用不在node_modules里寻找,而是去运行时的模块中寻找; Chrome是目前唯一实现支持importmas的浏览器,想要在其他浏览器正常使用,就需要借助systemjs。
<!-- 配置导入映射的,模块必须符合以下三种规范之一:
1. System.register - https://github.com/systemjs/systemjs/blob/master/docs/system-register.md
2. UMD ( 推荐 )
3. Global variable
-->
e.g:
<script type="systemjs-importmap">
{
"imports": {
"vue": "https://cdn.jsdelivr.net/npm/vue@2.6.11",
"vue-router": "https://unpkg.com/vue-router@3.2.0/dist/vue-router.js",
"vuex": "https://unpkg.com/vuex@3.4.0",
"lodash": "https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js",
"dayjs": "https://unpkg.com/dayjs@1.8.21/dayjs.min.js",
"app-demo1": "http://localhost:8992/js/app.js?time=<%=version%>"
}
}
</script>
子项目
子项目(vue)的改造有两种方法,第一个是通过插件去修改相关配置(不推荐),第二个是自己动手修改相关配置,两种方式都在项目中使用了,分别代表的项目是app-demo1和app-demo2。
不推荐通过插件去修改相关配置,除了通过插件配置会让我们弱化对single-spa相关配置的认知,还有插件会修改一些必要配置之外,还会添加一些非必需配置(e.g:standalone-single-spa-webpack-plugin...)。
手动配置
1.安装相关依赖:
- single-spa-vue
single-spa-vue是一个针对vue项目的初始化、挂载、卸载的库,可以实现single—spa注册的应用、生命周期函数等功能。
- webpack-manifest-plugin
通过webpack-manifest-plugin在打包时自动生成资源列表json文件,内容是项目资源清单。这样主应用可以通过这个json文件获取打包后生成的app.hash.js入口文件。
2.修改入口文件(main.js):
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
// 新增
import singleSpaVue from 'single-spa-vue';
Vue.config.productionTip = false;
const appOptions = {
render: (h) => h(App),
router, // 路由
store // vuex
};
// 判断是微前端加载还是独立运行
if (!window.singleSpaNavigate) {
new Vue(appOptions).$mount('#app');
}
const vueLifecycles = singleSpaVue({
Vue, //(必传项) 主Vue对象
appOptions
});
// single-spa 生命周期函数 三个生命周期函数必须都有
export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;
3.修改vue.config.js文件(修改webpack配置):
const packageName = require('./package.json').name;
const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); // 资源清单 协助主应用加载资源
module.exports = {
// 设定好publicPath, 端口最好是一个固定值
publicPath: process.env.NODE_ENV === 'production' ? '/app-demo2' : 'http://localhost:8993',
configureWebpack: (config) => {
config.output.libraryTarget = 'umd'; // 打包格式为umd 配合模块加载工具加载项目
config.output.library = packageName;
config.output.jsonpFunction = `webpackJsonp_${packageName}`;// 防止全局变量webpackJsonp冲突
// 删除chunk-vendors.js文件,公共三方模块打包进app.js文件
config.optimization.splitChunks.cacheGroups = {};
//config.optimization.delete('splitChunks');
// 打包时移除这些通用库,配合systemjs从root加载
// 注意⚠️ 和上文importmap相关联
config.externals = ['vue', 'vue-router', 'vuex', 'lodash', 'dayjs'];
/// 主项目加载的xxx.mainifest.json文件
config.plugins.push(
new WebpackManifestPlugin({
fileName: 'manifest.json'
// filter: function(option) {
// return option.isInitial;
// }
})
);
},
devServer: {
headers: {
// 开发模式下解决微前端加载时跨域的问题
'Access-Control-Allow-Origin': '*'
}
},
css: {
// css不单独打包成一个文件,和js打包进一个文件 这个也会造成css污染问题,通过其他方式避免。
extract: false
}
};
插件
前提是通过vuecli创建的项目;通过插件修改的前提是子项目最好是新项目,因为插件会修改main.js文件,会造成代码丢失。
1.终端执行:
Vue add single-spa
这个插件帮助我们做了哪些事情呢???
- 安装single-spa-vue包
- 改造main.js文件
- 修改webpack相关配置, 至于具体修改了哪些配置,基本上和我们上面在vue.config.js文件修改的东西差不多,一些关键设置都是存在的,还有其他的一些不同之处。具体配置可以参考(配置)。
注意事项
single-spa虽然实现了完整的应用加载逻辑,但是应用之间的隔离需要我们自己去解决,在这方面上qiankun作为一个基于single-spa实现的框架,替我们解决了这部分的问题,后续会研究qiankun。
1.css样式隔离:
对于css污染来说,只要保证样式不会互相覆盖,可以借助打包工具或其他方式为每个子应用添加前缀的目的来规避。
2.js隔离:
项目基于打包工具进行模块化的打包方式,基本可以避免大多数的全局冲突,因为大多数都被编译成了闭包、内部变量和方法。
但是挂载到window上的变量还是有风险,为了降低风险还是需要制定开发规范:尽量避免挂载变量到window,不容许修改原始方法或对象。也可以参考一下qiankun 隔离的做法(通过快照的方式,具体可以自行查找)。
3.应用之间通信:
主应用和子应用,或者子应用和子应用之间如何通信? 要让两个隔离的应用做到通信,需要借助全局对象。我找到了一个非常好用的三方事件库eventemitter3,eventemitter3实例化后挂载到window下,应用之间派发/监听,就可以愉快的通信了。