什么是微前端
微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一应用转变为把多个小型前端应用聚合为一的应用。各个前端应用还可以独立开发、独立部署。同时,它们也可以进行并行开发—这些组件可以通过NPM,GIT或者SVN来管理。
微前端架构特点
- 应用自治,即每个子应用之间不存在依赖关系,替换其中一个子应用而整体不受影响
- 单一职责,即微前端架构应该满足单一职责的原则
- 技术栈无关,即每个子应用可以用不同的前端框架去编写
微前端技术拆分方式
1.路由分发式
通过HTTP服务器的反向代理功能,将请求路由映射到对应的应用上
2.前端微服务化
在不同的框架上设计通信和加载机制,以在一个页面内加载对应的应用
3.微应用
通过软件工程的方式,在部署构建环境中,把多个独立的应用组合成一个应用
4.微件化
将业务功能拆分成独立的chunk代码,加载即可运行
5.前端容器化
iframe
6.应用组件化
Web Components技术
前端微服务化
这里我们将使用第二种方式,使用前端微服务化的方式将基于create-react-app
脚手架搭建的react应用以及vue-cli
搭建的vue应用合并在一个基座应用上,做到子应用可以独立开发,分别部署的同时也能做到合并在一起成为一个聚合应用
普通SPA对比前端微服务化
普通单体应用
微前端架构
基座模式
在这里,我们将通过一个主应用,来管理其它子应用,它主要实现了以下的功能
- 应用发现。让主应用可以寻找到其它应用。
- 应用注册。即提供新的微前端应用,向应用注册表注册的功能。
- 第三方应用注册。即让第三方应用,可以接入到系统中。
- 访问权限等相关配置。
创建主应用
single-spa
single-spa
是一款专为微前端实现设计的框架
github地址: https://github.com/CanopyTax/single-spa
qiankun
qiankun
是蚂蚁金服为其底层前端框架umi
开发的,基于single-spa
实现的,更适用于生产的微前端框架
github地址: https://github.com/umijs/qiankun
注册基座
你的基座应用可以用任何脚手架搭建,比如webpack
或者parcel
首先yarn add qiankun
index.js1
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
46import { registerMicroApps, start } from 'qiankun';
import react from 'react'
import ReactDOM from 'react-dom'
function Framework() {
function goto(title, href) {
window.history.pushState({}, title, href);
}
return (
<header>
<nav>
<ol>
<li><a onClick={() => goto('react app', '/react')}>react</a></li>
<li><a onClick={() => goto('vue app', '/vue')}>vue</a></li>
</ol>
</nav>
</header>
)
}
function render({ appContent, loading }) {
const container = document.getElementById('container');
ReactDOM.render(<Framework loading={loading} content={appContent}/>, container);
}
function genActiveRule(routerPrefix) {
return (location) => location.pathname.startsWith(routerPrefix);
}
registerMicroApps(
[
{
name: 'react app', // app name registered
entry: '//localhost:3000',
render,
activeRule: genActiveRule('/react') },
{
name: 'vue app',
entry: `//localhost:8080`,
render,
activeRule: genActiveRule('/vue')
},
],
);
start({ prefetch: true, jsSandbox: true })
这里,Framework
是你的基座框架,使用的是react
的方式实现,也可以使用vue
,angular
,下面给出vue
的写法,主要是render函数的区别1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25let app = null
function render({ appContent, loading }) {
if (!app) {
app = new Vue({
el: '#container',
data() {
return {
content: appContent,
loading,
};
},
render(h) {
return h(Framework, {
props: {
content: this.content,
loading: this.loading,
},
});
},
});
} else {
app.content = appContent;
app.loading = loading;
}
}
genActiveRule
方法用于基座匹配路由对应的子应用,返回的是布尔值registerMicroApps
注册子应用,参数是一个数组,每个数组成员对象都有四个属性,分别是name
,entry
,render
,activeRule
,其中,entry
是子应用部署所对应的地址
最后,别忘了运行start
这样我们的基座就写好,下面我们来改造一个使用vue-cli
脚手架搭建的vue项目,使其成为一个微前端子应用
子应用生命周期
上面基座中被注册的子应用如何被基座进行挂载以及监听呢,就需要子应用设置生命周期方法
子应用生命周期lifecycles
生命周期函数是single-spa将在已注册的应用程序上调用的函数或函数数组。single-spa通过从已注册应用程序的主文件中查找特定的命名导出来调用它们
single-spa lifecycles
设置生命周期
那么在哪里设置呢,需要在子应用的入口文件处设置,spa项目一般就是/src目录下的index.js
文件
一般在入口文件中,我们都会进行将实例挂载到dom上的操作,首先,我们就要将这一部分内容注释,基座会调用对应生命周期方法来实例化子应用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//index.js
export async function bootstrap() {
console.log('vue app bootstraped');
}
export async function mount(props) {
console.log(props);
instance = new Vue({
el: '#vueRoot',
render: h => h(App),
})
}
export async function unmount() {
ReactDOM.unmountComponentAtNode(document.getElementById('vueRoot'));
}
// 不执行实例化方法
/*
new Vue({
el: '#vueRoot',
render: h => h(App),
})
*/
入口文件不会实例化到dom对象,而是会导出相应的生命周期方法,由基座来进行对应的生命周期操作
webpack的相关配置注意项
上面的配置基本算是已经完成了,但是还是不够的,在实际开发中会遇到很多问题,这在框架文档中并不会列出来,因为大部分问题并不属于框架本身的问题,这个时候就要根据报错信息一一排查
下面是笔者在实践中遇到问题的解决办法
cra(create-react-app)配置
如果使用cra搭建的脚手架,一般会用到customize-cra
这个辅助工具,来完成webpack的配置,github地址:https://github.com/arackaf/customize-cra
下面给出示例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
59
60
61
62
63
64
65
66const {
override,
fixBabelImports,
addLessLoader,
useEslintRc,
addDecoratorsLegacy,
overrideDevServer,
} = require("customize-cra")
const variables = require("./src/styles/variables")
const antdThemes = require("./src/styles/antd")
const themes = {}
Object.keys(antdThemes).forEach(key => {
themes["@" + key] = variables[key]
})
const rewiredMap = () => config => {
config.devtool =
config.mode === "development" ? "cheap-module-source-map" : false
return config
}
const devServerConfig = () => config => {
return {
...config,
before(app) {
app.use((req, res, next) => {
res.append('Access-Control-Allow-Origin', '*')
next()
})
}
}
}
const outputChange = () => config => {
config.output.library = "packageName"
config.output.libraryTarget = "umd"
config.output.jsonpFunction = "webpackJsonp_packageName"
config.output.umdNamedDefine = true
config.output.publicPath = 'http://localhost:3000/'
// config.optimization = {}
return config
}
module.exports = {
webpack: override(
outputChange(),
// disableChunk(),
fixBabelImports("import", {
libraryName: "antd",
libraryDirectory: "es",
style: true
}),
addLessLoader({
javascriptEnabled: true,
modifyVars: themes,
data: Object.keys(variables)
.map(key => `@${key}:${variables[key]}`)
.join(`\n`),
localIdentName: "[local]--[hash:base64:5]"
}),
addDecoratorsLegacy(),
useEslintRc(),
rewiredMap()
),
devServer: overrideDevServer(
devServerConfig()
)
}
webpack导出umd
首先,需要将webpack的导出选项设置为umd通用模式
umd原理:
1.先判断是否支持Node.js模块格式(exports是否存在),存在则使用Node.js模块格式。
2.再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
3.前两个都不存在,则将模块公开到全局(window或global)。1
2
3
4
5output: {
library: packageName,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
}
跨域
由于主服务与子服务是运行在不同的端口下,所以需要解决跨域问题1
2
3
4
5
6
7// webpack-dev-server设置示例
before(app) {
app.use((req, res, next) => {
res.append('Access-Control-Allow-Origin', '*')
next()
})
}
请求地址问题
需要将子项目的webpack的publicPath配置项设为绝对地址,这样在请求资源时不会因为路径而导致子应用died
子项目css样式隔离
CSS Modules
如果使用antd
,https://pro.ant.design/docs/style-cn