带你入门前端工程:微前端
Posted 谭光志
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了带你入门前端工程:微前端相关的知识,希望对你有一定的参考价值。
什么是微服务?先看看维基百科的定义:
微服务(英语:Microservices)是一种软件架构风格,它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模块化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关 (Language-Independent/Language agnostic)的API集相互通信。
换句话说,就是将一个大型、复杂的应用分解成几个服务,每个服务就像是一个组件,组合起来一起构建成整个应用。
想象一下,一个上百个功能、数十万行代码的应用维护起来是个什么场景?
- 牵一发而动全身,仅仅修改一处代码,就需要重新部署整个应用。经常有“修改一分钟,编译半小时”的情况发生。
- 代码模块错综复杂,互相依赖。更改一处地方的代码,往往会影响到应用的其他功能。
如果使用微服务来重构整个应用有什么好处?
一个应用分解成多个服务,每个服务独自服务内部的功能。例如原来的应用有 abcd 四个页面,现在分解成两个服务,第一个服务有 ab 两个页面,第二个服务有 cd 两个页面,组合在一起就和原来的应用一样。
当应用其中一个服务出故障时,其他服务仍可以正常访问。例如第一个服务出故障了, ab 页面将无法访问,但 cd 页面仍能正常访问。
好处:不同的服务独立运行,服务与服务之间解耦。我们可以把服务理解成组件,就像本小书第 3 章《前端组件化》中所说的一样。每个服务可以独自管理,修改一个服务不影响整体应用的运行,只影响该服务提供的功能。
另外在开发时也可以快速的添加、删除功能。例如电商网站,在不同的节假日时推出的活动页面,活动过后马上就可以删掉。
难点:不容易确认服务的边界。当一个应用功能太多时,往往多个功能点之间的关联会比较深。因而就很难确定这一个功能应该归属于哪个服务。
PS:微前端就是微服务在前端的应用,也就是前端微服务。
微服务实践
现在我们将使用微前端框架 qiankun 来构建一个微前端应用。之所以选用 qiankun 框架,是因为它有以下几个优点:
- 技术栈无关,任何技术栈的应用都能接入。
- 样式隔离,子应用之间的样式互不干扰。
- 子应用的 javascript 作用域互相隔离。
- 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
样式隔离
样式隔离的原理是:每次切换子应用时,都会加载该子应用对应的 css 文件。同时会把原先的子应用样式文件移除掉,这样就达到了样式隔离的效果。
我们可以自己模拟一下这个效果:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="index.css">
<body>
<div>移除样式文件后将不会变色</div>
</body>
</html>
/* index.css */
body {
color: red;
}
现在我们加一段 JavaScript 代码,在加载完样式文件后再将样式文件移除掉:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="index.css">
<body>
<div>移除样式文件后将不会变色</div>
<script>
setTimeout(() => {
const link = document.querySelector(\'link\')
link.parentNode.removeChild(link)
}, 3000)
</script>
</body>
</html>
这时再打开页面看一下,可以发现 3 秒后字体样式就没有了。
JavaScript 作用域隔离
主应用在切换子应用之前会记录当前的全局状态,然后在切出子应用之后恢复全局状态。假设当前的全局状态如下所示:
const global = { a: 1 }
在进入子应用之后,无论全局状态如何变化,将来切出子应用时都会恢复到原先的全局状态:
// global
{ a: 1 }
官方还提供了一张图来帮助我们理解这个机制:
好了,现在我们来创建一个微前端应用吧。这个微前端应用由三部分组成:
- main:主应用,使用 vue-cli 创建。
- vue:子应用,使用 vue-cli 创建。
- react: 子应用,使用的 react 16 版本。
对应的目录如下:
-main
-vue
-react
创建主应用
我们使用 vue-cli 创建主应用(然后执行 npm i qiankun
安装 qiankun 框架):
vue create main
如果主应用只是起到一个基座的作用,即只用于切换子应用。那可以不需要安装 vue-router 和 vuex。
改造 App.vue
文件
主应用必须提供一个能够安装子应用的元素,所以我们需要将 App.vue
文件改造一下:
<template>
<div class="mainapp">
<!-- 标题栏 -->
<header class="mainapp-header">
<h1>QianKun</h1>
</header>
<div class="mainapp-main">
<!-- 侧边栏 -->
<ul class="mainapp-sidemenu">
<li @click="push(\'/vue\')">Vue</li>
<li @click="push(\'/react\')">React</li>
</ul>
<!-- 子应用 -->
<main class="subapp-container">
<h4 v-if="loading" class="subapp-loading">Loading...</h4>
<div id="subapp-viewport"></div>
</main>
</div>
</div>
</template>
<script>
export default {
name: \'App\',
props: {
loading: Boolean,
},
methods: {
push(subapp) { history.pushState(null, subapp, subapp) }
}
}
</script>
可以看到我们用于安装子应用的元素为 #subapp-viewport
,另外还有切换子应用的功能:
<!-- 侧边栏 -->
<ul class="mainapp-sidemenu">
<li @click="push(\'/vue\')">Vue</li>
<li @click="push(\'/react\')">React</li>
</ul>
改造 main.js
根据 qiankun 文档说明,需要使用 registerMicroApps()
和 start()
方法注册子应用及启动主应用:
import { registerMicroApps, start } from \'qiankun\';
registerMicroApps([
{
name: \'react app\', // app name registered
entry: \'//localhost:7100\',
container: \'#yourContainer\',
activeRule: \'/yourActiveRule\',
},
{
name: \'vue app\',
entry: { scripts: [\'//localhost:7100/main.js\'] },
container: \'#yourContainer2\',
activeRule: \'/yourActiveRule2\',
},
]);
start();
所以现在需要将 main.js
文件改造一下:
import Vue from \'vue\'
import App from \'./App\'
import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from \'qiankun\'
let app = null
function render({ loading }) {
if (!app) {
app = new Vue({
el: \'#app\',
data() {
return {
loading,
}
},
render(h) {
return h(App, {
props: {
loading: this.loading
}
})
}
});
} else {
app.loading = loading
}
}
/**
* Step1 初始化应用(可选)
*/
render({ loading: true })
const loader = (loading) => render({ loading })
/**
* Step2 注册子应用
*/
registerMicroApps(
[
{
name: \'vue\', // 子应用名称
entry: \'//localhost:8001\', // 子应用入口地址
container: \'#subapp-viewport\',
loader,
activeRule: \'/vue\', // 子应用触发路由
},
{
name: \'react\',
entry: \'//localhost:8002\',
container: \'#subapp-viewport\',
loader,
activeRule: \'/react\',
},
],
// 子应用生命周期事件
{
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)
},
],
afterUnmount: [
app => {
console.log(\'[LifeCycle] after unmount %c%s\', \'color: green\', app.name)
},
],
},
)
// 定义全局状态,可以在主应用、子应用中使用
const { onGlobalStateChange, setGlobalState } = initGlobalState({
user: \'qiankun\',
})
// 监听全局状态变化
onGlobalStateChange((value, prev) => console.log(\'[onGlobalStateChange - master]:\', value, prev))
// 设置全局状态
setGlobalState({
ignore: \'master\',
user: {
name: \'master\',
},
})
/**
* Step3 设置默认进入的子应用
*/
setDefaultMountApp(\'/vue\')
/**
* Step4 启动应用
*/
start()
runAfterFirstMounted(() => {
console.log(\'[MainApp] first app mounted\')
})
这里有几个注意事项要注意一下:
- 子应用的名称
name
必须和子应用下的package.json
文件中的name
一样。 - 每个子应用都有一个
loader()
方法,这是为了应对用户直接从子应用路由进入页面的情况而设的。进入子页面时判断一下是否加载了主应用,没有则加载,有则跳过。 - 为了防止在切换子应用时显示空白页面,应该提供一个
loading
配置。 - 设置子应用的入口地址时,直接填入子应用的访问地址。
更改访问端口
vue-cli 的默认访问端口一般为 8080,为了和子应用保持一致,需要将主应用端口改为 8000(子应用分别为 8001、8002)。创建 vue.config.js
文件,将访问端口改为 8000:
module.exports = {
devServer: {
port: 8000,
}
}
至此,主应用就已经改造完了。
创建子应用
子应用不需要引入 qiankun 依赖,只需要暴露出几个生命周期函数就可以:
bootstrap
,子应用首次启动时触发。mount
,子应用每次启动时都会触发。unmount
,子应用切换/卸载时触发。
现在将子应用的 main.js
文件改造一下:
import Vue from \'vue\'
import VueRouter from \'vue-router\'
import App from \'./App.vue\'
import routes from \'./router\'
import store from \'./store\'
Vue.config.productionTip = false
let router = null
let instance = null
function render(props = {}) {
const { container } = props
router = new VueRouter({
// hash 模式不需要下面两行
base: window.__POWERED_BY_QIANKUN__ ? \'/vue\' : \'/\',
mode: \'history\',
routes,
})
instance = new Vue({
router,
store,
render: h => h(App),
}).$mount(container ? container.querySelector(\'#app\') : \'#app\')
}
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
} else {
render()
}
function storeTest(props) {
props.onGlobalStateChange &&
props.onGlobalStateChange(
(value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
true,
)
props.setGlobalState &&
props.setGlobalState({
ignore: props.name,
user: {
name: props.name,
},
})
}
export async function bootstrap() {
console.log(\'[vue] vue app bootstraped\')
}
export async function mount(props) {
console.log(\'[vue] props from main framework\', props)
storeTest(props)
render(props)
}
export async function unmount() {
instance.$destroy()
instance.$el.innerHTML = \'\'
instance = null
router = null
}
可以看到在文件的最后暴露出了 bootstrap
mount
unmount
三个生命周期函数。另外在挂载子应用时还需要注意一下,子应用是在主应用下运行还是自己独立运行:container ? container.querySelector(\'#app\') : \'#app\'
。
配置打包项
根据 qiankun 文档提示,需要对子应用的打包配置项作如下更改:
const packageName = require(\'./package.json\').name;
module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: \'umd\',
jsonpFunction: `webpackJsonp_${packageName}`,
},
};
所以现在我们还需要在子应用目录下创建 vue.config.js
文件,输入以下代码:
// vue.config.js
const { name } = require(\'./package.json\')
module.exports = {
configureWebpack: {
output: {
// 把子应用打包成 umd 库格式
library: `${name}-[name]`,
libraryTarget: \'umd\',
jsonpFunction: `webpackJsonp_${name}`
}
},
devServer: {
port: 8001,
headers: {
\'Access-Control-Allow-Origin\': \'*\'
}
}
}
vue.config.js
文件有几个注意事项:
- 主应用、子应用运行在不同端口下,所以需要设置跨域头
\'Access-Control-Allow-Origin\': \'*\'
。 - 由于在主应用配置了 vue 子应用需要运行在 8001 端口下,所以也需要在
devServer
里更改端口。
另外一个子应用 react 的改造方法和 vue 是一样的,所以在此不再赘述。
部署
我们将使用 express 来部署项目,除了需要在子应用设置跨域外,没什么需要特别注意的地方。
主应用服务器文件 main-server.js
:
const fs = require(\'fs\')
const express = require(\'express\')
const app = express()
const port = 8000
app.use(express.static(\'main-static\'))
app.get(\'*\', (req, res) => {
fs.readFile(\'./main-static/index.html\', \'utf-8\', (err, html) => {
res.send(html)
})
})
app.listen(port, () => {
console.log(`main app listening at http://localhost:${port}`)
})
vue 子应用服务器文件 vue-server.js
:
const fs = require(\'fs\')
const express = require(\'express\')
const app = express()
const cors = require(\'cors\')
const port = 8001
// 设置跨域
app.use(cors())
app.use(express.static(\'vue-static\'))
app.get(\'*\', (req, res) => {
fs.readFile(\'./vue-static/index.html\', \'utf-8\', (err, html) => {
res.send(html)
})
})
app.listen(port, () => {
console.log(`vue app listening at http://localhost:${port}`)
})
react 子应用服务器文件 react-server.js
:
const fs = require(\'fs\')
const express = require(\'express\')
const app = express()
const cors = require(\'cors\')
const port = 8002
// 设置跨域
app.use(cors())
app.use(express.static(\'react-static\'))
app.get(\'*\', (req, res) => {
fs.readFile(\'./react-static/index.html\', \'utf-8\', (err, html) => {
res.send(html)
})
})
app.listen(port, () => {
console.log(`react app listening at http://localhost:${port}`)
})
另外需要将这三个应用打包后的文件分别放到 main-static
、vue-static
、react-static
目录下。然后分别执行命令 node main-server.js
、node vue-server.js
、node react-server.js
即可查看部署后的页面。现在这个项目目录如下:
-main
-main-static // main 主应用静态文件目录
-react
-react-static // react 子应用静态文件目录
-vue
-vue-static // vue 子应用静态文件目录
-main-server.js // main 主应用服务器
-vue-server.js // vue 子应用服务器
-react-server.js // react 子应用服务器
我已经将这个微前端应用的代码上传到了 github,建议将项目克隆下来配合本章一起阅读,效果更好。下面放一下 DEMO 的运行效果图:
小结
对于大型应用的开发和维护,使用微前端能让我们变得更加轻松。不过如果是小应用,建议还是单独建一个项目开发。毕竟微前端也有额外的开发、维护成本。
参考资料
- Microservices
- 可能是你见过最完善的微前端解决方案
- qiankun
带你入门前端工程 全文目录:
- 技术选型:如何进行技术选型?
- 统一规范:如何制订规范并利用工具保证规范被严格执行?
- 前端组件化:什么是模块化、组件化?
- 测试:如何写单元测试和 E2E(端到端) 测试?
- 构建工具:构建工具有哪些?都有哪些功能和优势?
- 自动化部署:如何利用 Jenkins、Github Actions 自动化部署项目?
- 前端监控:讲解前端监控原理及如何利用 sentry 对项目实行监控。
- 性能优化(一):如何检测网站性能?有哪些实用的性能优化规则?
- 性能优化(二):如何检测网站性能?有哪些实用的性能优化规则?
- 重构:为什么做重构?重构有哪些手法?
- 微服务:微服务是什么?如何搭建微服务项目?
- Severless:Severless 是什么?如何使用 Severless?
以上是关于带你入门前端工程:微前端的主要内容,如果未能解决你的问题,请参考以下文章