大前端时代下的微前端如何做

微前端的定义

微前端本质是是一种项目架构方案,是为了解决前端项目太过庞大,导致项目管理维护难、团队协作乱、升级迭代困难、技术栈不统一等等问题,有点类似微服务的概念,是将微服务理念扩展到前端开发的一种应用.

微前端出现在我们的视线的次数越来越多,因为to B 的发展越来越迅猛,导致中后台应用需求激增,如何将多项目集合成一个web主体就成为一个问题,当然也有不少童鞋们还会有疑惑🤔,究竟微前端是什么东西呢?

随这业界越来越关注复杂的现代化 Web 开发需要怎样的整体架构和组织结构这个问题。于是我们开始看到单体前端正在分解为更小、更简单的模块,这些模块可以各自独立开发、测试和部署,而它们组合在一起仍然对客户表现为一件单一完整的产品。我们将这种技术称为 微前端架构,其定义为:

“微前端是一种架构风格,其中众多独立交付的前端应用组合成一个大型整体。”

BigPipe 也好,微前端也好,都是一种概念,一种指导思想。

某后台

本质上应该就是一个微前端应用,左侧的菜单就是各个子应用的入口,切换菜单的同时就是在切换子应用,而整个主容器就是一个portal门户(可能包含用户登录机制 、菜单权限获取 、 全局异常处理等)

微前端的优点

  • 增量升级
  • 简单、解耦的代码库
  • 独立部署
  • 团队自治

微前端的落地方式

微前端它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用拆分为为多个小型前端应用聚集为一体的应用。

iFrame

iFrame 是微前端集成的最简单方式之一。可以说iFrame 里的页面是完全独立的,而且iFrame 页面中的静态资源(js、css)都是相互隔离的,互相不干扰,相当于一个独立的环境,具备沙箱隔离,可以让前端应用之间可以相互独立运行.

1
2
3
4
5
6
7
8
9
10
11
//通过切换url来切换不同业务项目

<iframe v-show="url" frameborder="0" id="contentIframe"></iframe>
createFrame(url) {
const iframe = document.getElementById('contentIframe');
const deviceWidth = document.body.clientWidth;
// const deviceHeight = document.body.clientHeight;
iframe.style.width = `${Number(deviceWidth) - 10}px`;
iframe.style.height = `${800}px`;
iframe.src = url;
}

当然iFrame 也有局限性👇:

  • 子项目需调整,需要隐藏自身页面中的导航(公共区域)
  • iFrame嵌入的视图控制难,有局限性
  • 刷新无法保存记录,也就意味着当浏览器刷新状态将消失,后退返回无效
  • iframe 阻塞主页面加载

Nginx路由分发方式

路由分发是指通过路由将不同业务拆分的子项目,结合反向代理的方式实现

路由分发方式也是比较简单的一种实现微前端的方式,将多个子项目聚合成一体,可以通过ngxin来配置不同路由的转发代理,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
http {
server {
listen 80;
server_name 192.168.0.1
location /web/monitor {
proxy_pass http://192.168.0.2/web/monitor;
}
location /web/admin {
proxy_pass http://192.168.0.3/web/admin;
}
location / {
proxy_pass /;
}
}
}

通过不同的路由请求,转发到不同的项目域名服务器下,这种方式好处在于团队协作方便、框架无关、项目独立部署维护

当然路由分发也有局限性:

  • web应用之间的复用性差
  • 每个独立的项目之间切换,需要重新加载,容易出现白屏影响用户体验

Single-SPA

官方号称“一个用于前端微服务化的JavaScript前端解决方案”,single-spa 听起来很高大上,它能兼容各种技术栈,并且在同一个页面中可以使用多种技术框架(React, Vue, Angular等任意技术框架),不用考虑因新的技术框架而去重构旧项目的代码,

大概的原理是,首先需要一个主应用(容器应用),需要先注册子应用,然后当url匹配到相应的子应用路由后,将会先请求子应用的资源,然后挂载子应用,同理,当url切换出该子应用路由时,将卸载该应用,以此达到切换子应用的效果,通过子应用生命周期boostrap(获取输出的资源文件) 、 mount、unmount的交替
聊聊Single-SPA 的优点:

  • 各项目独立开发、部署、迭代,互不影响效率高
  • 开发团队可以选择自己的技术并及时更新技术栈。
  • 相互之间的依赖性大大降低
  • 有利于CI/CD,更快的交付产品

qiankun (蚂蚁金服微前端框架)

qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

qiankun这名字起得溜啊,本质上就是在上一节提到得Single-SPA上做一些封装,让我们前端开发者用得更上手.

Single-SPA 实践

https://github.com/libin1991/single-spa-test-demo


single-spa.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { registerApplication, start } from 'single-spa'

registerApplication(
// Name of our single-spa application
'vue',
// loadingFunction
() => import ('./src/vue/main.js'),
// activityFunction
(location) => {
console.log('location.pathname --?',location.pathname)
return location.pathname.indexOf('vue') > -1 ? true : false
}
);

registerApplication(
// Name of our single-spa application
'react',
// loadingFunction
() => import ('./src/react/main.js'),
// activityFunction
(location) => location.pathname.indexOf('react') > -1 ? true : false
);

start();

webpack.config.js 入口文件改为 single-spa.config路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const path = require('path');
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');


module.exports = {
mode: 'development',
entry: {
// Set the single-spa config as the project entry point
'single-spa.config': './single-spa.config.js',
},
output: {
publicPath: '/dist/',
filename: '[name].js',
path: path.resolve(__dirname,'dist'),
},
module: {
rules: [
{
// Webpack style loader added so we can use materialize
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.js$/,
exclude: [path.resolve(__dirname, 'node_modules')],
loader: 'babel-loader'
},
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
node: {
fs: 'empty'
},
resolve: {
alias: {
vue: 'vue/dist/vue.js'
},
modules: [path.resolve(__dirname, 'node_modules')],
},
plugins: [
// A webpack plugin to remove/clean the output folder before building
new CleanWebpackPlugin(),
new VueLoaderPlugin(),
],
devtool: 'source-map',
externals: [],
devServer: {
host: 'localhost',
port: 1522,
hot: true, //开启热加载
historyApiFallback: true
}
}

Vue mian.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Vue from 'vue';
import App from './App.vue';
import singleSpaVue from 'single-spa-vue';

const vueLifecycles = singleSpaVue({
Vue,
appOptions: {
el: '#vuePage',
render: h => h(App),
},
});

//引导
export const bootstrap = vueLifecycles.bootstrap;
//挂载
export const mount = vueLifecycles.mount;
//卸载
export const unmount = vueLifecycles.unmount;

React mian.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import App from './App.js';

function domElementGetter() {
return document.getElementById("reactPage")
}

const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
domElementGetter,
})

export const bootstrap = [
reactLifecycles.bootstrap,
];

export const mount = [
reactLifecycles.mount,
];

export const unmount = [
reactLifecycles.unmount,
];

参考文献