微前端的概念最早由 thoughtworks 在 2016 年提出。其核心思路是借鉴后端微服务架构理念,将一个单体的庞大的前端应用拆分为多个简单独立的前端工程。每个前端工程可以独立开发、测试、部署。最终再由一个容器应用,将拆分后的微前端工程组合为一个整体,面向用户提供服务。
微前端的架构方式所带来的好处也是显而易见的:
- 降低代码耦合
- 微前端可独立部署
- 团队可以按照业务垂直拆分更高效
常见的实现方式
微前端的概念最近一两年很火。但必须指出的是,微前端并不是一门新技术,而是一种新的架构方式。社区开发者们充分发挥聪明才智,提供了不少实现思路。
iframe
对于前端同学来讲,最容易想到的就是 iframe 了。iframe 天然具备微前端的基因。我们只需将单体的前端应用,按照业务模块进行拆分,分别部署。最后通过 iframe 进行动态加载即可。 一个简单的实现如下:
<html>
<head>
<title>微前端-ifame</title>
</head>
<body>
<h1>我是容器</h1>
<iframe id="mfeLoader"></iframe>
<script type="text/javascript">
const routes = {
'/': 'https://app.com/index.html',
'/app1': 'https://app1.com/index.html',
'/app2': 'https://app2.com/index.html',
};
const iframe = document.querySelector('#mfeLoader');
iframe.src = routes[window.location.pathname];
</script>
</body>
</html>
复制代码
优点
- 实现简单
- 天然具备隔离性
缺点
- 主页面和 iframe 共享最大允许的 HTTP 链接数。
- iframe 阻塞主页面加载。
- 浏览器的后退按钮无效
服务端模板组合
有年代感的前端程序员对这种方式一定不陌生。常见的实现方式是,服务端根据路由动态渲染特定页面的模板文件。架构图如下:
容器模板代码如下:
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>微前端-服务端模板</title>
</head>
<body>
<h1>容器应用</h1>
<!--# include file="$PAGE.html" -->
</body>
</html>
复制代码
通过 Nginx 服务器根据 url 路径动态设置要加载的模板:
server {
listen 8080;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
ssi on;
rewrite ^/$ http://localhost:8080/app redirect;
location /app {
set $PAGE 'app';
}
location /app1 {
set $PAGE 'app1';
}
location /app2 {
set $PAGE 'app2';
}
error_page 404 /index.html;
}
复制代码
优点
- 实现简单
- 技术栈独立
缺点
- 需要额外配置 Nginx
- 前后端分离不彻底
微前端框架 single-spa
对于时刻将 “没有 js 做不到的事情” 视为座右铭的前端程序员们是不可能不造轮子的,鼎鼎大名的 single-spa 就这么被造出来了。
借助 single-spa,开发者可以为不同的子应用使用不同的技术栈,比如子应用 A 使用 vue 开发,子应用 B 使用 react 开发,完全没有历史债务。
single-spa 的实现原理并不难,从架构上来讲可以分为两部分:子应用和容器应用。
子应用与传统的单页应用的区别在于
- 不需要 HTML 入口文件,
- js 入口文件导出的模块,必须包括 bootstrap、mount 和 unmount 三个方法。
容器应用主要负责注册应用,当 url 命中子应用的路由时激活并挂载子应用,或者当子应用不处于激活状态时,将子应用从页面中移除卸载。其核心方法有两个:
registerApplication
注册并下载子应用start
启动处于激活状态的子应用。
以下是 single-spa 的简单示例:
容器应用代码
<html>
<body>
<script src="single-spa-config.js"></script>
</body>
</html>
复制代码
single-spa-config.js
代码如下:
import * as singleSpa from 'single-spa';
const appName = 'app1';
const app1Url = 'http://app1.com/app1.js'
singleSpa.registerApplication('app1',() => loadJS(app1Url), location => location.pathname.startsWith('/app1'))
singleSpa.start();
复制代码
loadJS 方法是伪代码,表示加载 app1.js。开发者需要自己实现,或者借助 systemJS 来实现。
子应用代码:
//app1.js
let domEl;
export function bootstrap(props) {
return Promise
.resolve()
.then(() => {
domEl = document.createElement('div');
domEl.id = 'app1';
document.body.appendChild(domEl);
});
}
export function mount(props) {
return Promise
.resolve()
.then(() => {
domEl.textContent = 'App 1 is mounted!'
});
}
export function unmount(props) {
return Promise
.resolve()
.then(() => {
domEl.textContent = '';
})
}
复制代码
优点
- 纯前端解决方案
- 可以使用多种技术栈
- 完善的生态
缺点
- 上手成本高
- 需要改造现有应用
- 跨应用的联调变得复杂
适用场景
以上介绍了三种常见的微前端架构方式。天生喜爱新事物的前端同学,早就想要一试了。那么问题来了。哪些场景适合微前端架构?采用哪种微前端实现方式?我的看法很简单:
- 业务模块相对独立的复杂单体应用
- 综合考虑团队技术能力和业务现状选择适合的方式
总结
以上介绍了三种常见的微前端实现方式:
- 使用 iframe 组合
- 服务端模板渲染组合
- 微前端框架 single-spa