1、什么是微前端?为什么微前端方案?
2016年底在ThoughtWorks 技术雷达中提出,将多个独立的前端应用聚合在一起组成一个全新的应用,各个应用之间彼此独立,互不干扰,应用内高内聚,应用间松耦合;对于用户来说就是一个完整的应用,但是在技术角度上是由一个个独立的应用组合组合而成;
- 拆分、聚合
- 源码交付,自由组合 – 拆
- 应用接入 – 合
- 需求体量较大,避免巨石应用 – 拆
2、什么是qiankun、为什么qiankun?
- qiankun 是一个基于 single-spa 的微前端实现库,解决了应用加载,以及js/css污染问题,更简单好用。
- 方案成熟可靠、口碑较好,部门内部也有不少项目生产实践
- 接入简单,不会对原有工程造成很大“破坏”。
- 避免巨石应用,方便应用接入
- 与emp 相比更适合当前技术底座
3、带来怎样的收益,价值在哪里?
- 技术栈无关、易扩展:主框架不限制接入应用的技术栈,项目无限扩展
- 独立开发、独立部署:微应用仓库独立,前后端可独立开发,独立部署互不干扰
- 增量升级:微前端是一种非常好的实施渐进式重构的手段和策略
- 对团队来说,快速实践产出文档,形成组织过程资产
4、缺点
- 依赖不共享,重复加载资源
- 通过主应用共享依赖,子应用独立运行时需要启动主应用
1. qiankun本地实践
1.1 SwitchyOmega+whistle代理
前言:前端开发过程中经常会遇到需要对请求做拦截,然后通过代理工具或者其他方式转发到指定的服务器上。最常见的场景就是在单点登录的情况下,需要用域名来访问,带上登录token,后端才能识别成合法的用户请求。下文介绍一下chrome插件SwitchyOmega和基于nodejs的代理工具whistle的使用方法
- whistle
whistle(读音[ˈwɪsəl],拼音[wēisǒu])基于Node实现的跨平台web调试代理工具,类似的工具有Windows平台上的Fiddler,主要用于查看、修改HTTP、HTTPS、Websocket的请求、响应,也可以作为HTTP代理服务器使用,不同于Fiddler通过断点修改请求响应的方式,whistle采用的是类似配置系统hosts的方式,一切操作都可以通过配置实现,支持域名、路径、正则表达式、通配符、通配路径等多种匹配方式,且可以通过Node模块扩展功能。
# 安装 npm install -g whistle # 启动 w2 start
- SwitchyOmega
作为chrome的浏览器插件,安装后可以随时方便地使用。支持设置不同的场景模式,并且可以按照预定规则进行自动切换场景。直接在chrome网上应用店搜索就可以安装:
项目分为三大层,底层调度层,核心层和业务层。
- 底层调度层:主要借助single-spa做应用的调度,加载,卸载;qiankun主要提供js隔离,样式隔离,以及应用注册和应用通信功能等上层功能。和业务无关
- 核心层:主要接入qiankun框架,抽离出系统中的公共模块,例如登录注册,头部header、侧边菜单sidebar等功能,负责鉴权、全局事件处理,应用分发等
- 业务层:主要负责业务,根据主系统的要求,做系统的改造,接入微前端
1.2 项目配置
将普通的项目改造成 qiankun 主应用基座,需要进行四步操作:
- 当前qiankun版本 “qiankun”: “^2.4.10”,
- 创建微应用容器 - 用于承载微应用,渲染显示微应用;
- 注册微应用 - 设置微应用激活条件,微应用地址等等;
- 创建子应用 - 微前端接入配置;
- 启动 qiankun - 主应用基座中启动。
1.2.1 创建微应用容器
app.vue 中通过$route.name
判断是否为项目路由
<section v-show="!$route.name" id="qianKunBox" class="in-qiankun"></section>
<router-view v-show="$route.name"></router-view>
1.2.2 注册微应用
vue.config.js
改造配置文件 用于给主应用接入时进行注册,对应主应用注册子应用时的 registerMicroApps 中的某一个自入口的 name ``` javascript const packageName = require(‘./package.json’).name
module.exports = {
// … other configs
configureWebpack: {
// 让主应用能正确识别微应用暴露出来的一些信息
output: {
library: ${packageName}
,
libraryTarget: ‘umd’,
jsonpFunction: webpackJsonp_${packageName}
}
},
// … other configs
}
* src/qiankun/config.js 子应用注册信息
``` javascript
const { VUE_APP_ROUTER_BASE } = process.env;
const props = {
header: false,
sidebar: false,
isParentSlider: true,
};
export const subAppRouter = [
{
name: 'hrss-web-portal',
entry: `${VUE_APP_ROUTER_BASE}/sub/portal/?t=${Date.now()}`,
container: '#qianKunBox',
activeRule: `${VUE_APP_ROUTER_BASE}/portal`,
props: {
...props,
baseRouter: `${VUE_APP_ROUTER_BASE}/portal`,
isParentSlider: false,
},
},
];
- 注册子应用信息 ``` javascript import ‘core-js/stable’; import ‘regenerator-runtime/runtime’; import ‘@/core’; import Vue from ‘vue’; import { registerMicroApps, setDefaultMountApp, start } from ‘qiankun’; import App from ‘./app.vue’; import router from ‘./router’; import store from ‘./store’; import { topbarConfig, sideBarConfig } from ‘./config’; import { dealApps } from ‘@/qiankun’; import MUTATIONTYPES from ‘./store/mutations-type’;
const { VUE_APP_ROUTER_BASE } = process.env; const storeGlobalTypes = Vue.prototype.$storeGlobalTypes;
let appMain = null;
// 初始化应用渲染
const initRender = (props = {}) => {
const { userInfo } = props;
if (!appMain) {
appMain = new Vue({
router,
store,
render: (h) => h(App),
}).$mount(‘#app’);
}
// 此处省略大段设置 用户信息、菜单代码…
return dealApps(appMain);
};
// 注册微前端
function register(apps) {
registerMicroApps(apps, {
beforeLoad: [
(app) => {
console.log(‘[LifeCycle] before load %c%s’, ‘color: green;’, app.name);
},
],
beforeMount: [
(app) => {
console.log(‘[LifeCycle] before mount %c%s’, ‘color: green;’, app.name);
setTimeout(() => {
store.commit(index/${MUTATIONTYPES.SET_APP_LOAD}
, true);
}, 1);
console.log(‘main beforeMount ‘, app);
store.commit(global/${storeGlobalTypes.SET_SIDEBAR_STATUS}
, app.props.isParentSlider); // 设置顶栏
},
],
afterUnmount: [
(app) => {
console.log(‘[LifeCycle] after unmount %c%s’, ‘color: green;’, app.name);
},
],
});
// 设置默认打开的微前端 应用 TODO 如果路径存在则跳转,否则走默认路径
setDefaultMountApp(${VUE_APP_ROUTER_BASE}/portal
);
// 启动qiankun start({ sandbox: { strictStyleIsolation: false }, }); }
const apps = initRender({ userInfo: { isLogin: true, info: { name: ‘nickname’, }, // 用户中心可操作列表 menu: [], }, }); // 微前端主应用,微应用注册 register(apps);
#### 1.2.3 创建子应用
* 子应用导出钩子函数
``` javascript
// eslint-disable-next-line require-await
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
// eslint-disable-next-line require-await
export async function mount(props) {
console.log('[vue] vue app mount', props);
initRender(props);
}
// eslint-disable-next-line require-await
export async function unmount() {
instance.$destroy();
instance = null;
}
2. 依赖共享优化
虽然qiankun微前端方案,能带来应用独立部署独立开发的好处,但是独立部署时,各子应用将重复引入相同依赖包,使项目整体包体积。这里可以将chunk-vendor中的依赖包通过webpack的externals忽略,再通过外链方式引入html模板中,依赖包可通过copy-webpack-plugin从node_modules取出。
- 复用主应用公共依赖
- 子应用间共享依赖
具体实现
1. 主应用提取”公共“依赖
externals: {
vue: 'Vue',
vuex: 'Vuex',
axios: 'axios',
'tdesign-vue': 'TDesign',
'vue-router': 'VueRouter',
// vue: {
// commonjs: 'vue',
// commonjs2: 'vue',
// amd: 'vue',
// root: 'Vue', // 外链cdn加载挂载到window上的变量名
// },
plugins: [
new CopyWebpackPlugin({
patterns: [
{ from: path.join(__dirname, '/node_modules/vue/dist/vue.min.js'), to: './lib/vue.min.js' },
{
from: path.join(__dirname, '/node_modules/vue-router/dist/vue-router.min.js'),
to: './lib/vue-router.min.js',
},
{ from: path.join(__dirname, '/node_modules/vuex/dist/vuex.min.js'), to: './lib/vuex.min.js' },
{ from: path.join(__dirname, '/node_modules/axios/dist/axios.min.js'), to: './lib/axios.min.js' },
{ from: path.join(__dirname, '/node_modules/tdesign-vue/dist/tdesign.min.js'), to: './lib/tdesign.min.js' },
{ from: path.join(__dirname, '/node_modules/tdesign-vue/dist/tdesign.min.css'), to: './lib/tdesign.min.css' },
],
}),
2. 主应用外链引入
<script ignore src="<%= VUE_APP_CDN_PATH%>/lib/vue.min.js"></script>
<script ignore src="<%= VUE_APP_CDN_PATH%>/lib/vue-router.min.js"></script>
<script ignore src="<%= VUE_APP_CDN_PATH%>/lib/vuex.min.js"></script>
<script ignore src="<%= VUE_APP_CDN_PATH%>/lib/axios.min.js"></script>
<script ignore src="<%= VUE_APP_CDN_PATH%>/lib/tdesign.min.js"></script>
3. main.js 加载子应用前注入依赖
const commonCssLibs = ['tdesign.min.css'];
const commonJsLibs = ['vue.min.js', 'vue-router.min.js', 'vuex.min.js', 'axios.min.js', 'tdesign.min.js'];
const loadScript = (url) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.setAttribute('ignore', 'true'); // 避免重复加载
script.onerror = () => {
console.error(`加载失败${url},请刷新重试`);
};
document.head.appendChild(script);
};
const loadLink = (url) => {
const script = document.createElement('link');
script.rel = 'stylesheet';
script.type = 'text/css';
script.href = url;
script.setAttribute('ignore', 'true'); // 避免重复加载
script.onerror = () => {
console.error(`加载失败${url},请刷新重试`);
};
document.head.appendChild(script);
};
// 注册子应用,加载之前注入依赖
registerMicroApps(apps, {
beforeLoad: [
(app) => {
commonJsLibs.forEach((libName) => {
const libUrl = `${VUE_APP_ROUTER_BASE}/lib/${libName}`;
loadScript(libUrl);
});
commonCssLibs.forEach((libName) => {
const libUrl = `${VUE_APP_ROUTER_BASE}/lib/${libName}`;
loadLink(libUrl);
});
console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
},
],
}
4. 子应用打包忽略公共依赖
externals: {
vue: 'Vue',
vuex: 'Vuex',
axios: 'axios',
'tdesign-vue': 'TDesign',
'vue-router': 'VueRouter',
// vue: {
// commonjs: 'vue',
// commonjs2: 'vue',
// amd: 'vue',
// root: 'Vue', // 外链cdn加载挂载到window上的变量名
// },