带你入门前端工程:微前端

什么是微服务?先看看维基百科的定义:

微服务(英语:Microservices)是一种软件架构风格,它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模块化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关 (Language-Independent/Language agnostic)的API集相互通信。

换句话说,就是将一个大型、复杂的应用分解成几个服务,每个服务就像是一个组件,组合起来一起构建成整个应用。

想象一下,一个上百个功能、数十万行代码的应用维护起来是个什么场景?

  1. 牵一发而动全身,仅仅修改一处代码,就需要重新部署整个应用。经常有“修改一分钟,编译半小时”的情况发生。
  2. 代码模块错综复杂,互相依赖。更改一处地方的代码,往往会影响到应用的其他功能。

如果使用微服务来重构整个应用有什么好处?

一个应用分解成多个服务,每个服务独自服务内部的功能。例如原来的应用有 abcd 四个页面,现在分解成两个服务,第一个服务有 ab 两个页面,第二个服务有 cd 两个页面,组合在一起就和原来的应用一样。

当应用其中一个服务出故障时,其他服务仍可以正常访问。例如第一个服务出故障了, ab 页面将无法访问,但 cd 页面仍能正常访问。

好处:不同的服务独立运行,服务与服务之间解耦。我们可以把服务理解成组件,就像本小书第 3 章《前端组件化》中所说的一样。每个服务可以独自管理,修改一个服务不影响整体应用的运行,只影响该服务提供的功能。

另外在开发时也可以快速的添加、删除功能。例如电商网站,在不同的节假日时推出的活动页面,活动过后马上就可以删掉。

难点:不容易确认服务的边界。当一个应用功能太多时,往往多个功能点之间的关联会比较深。因而就很难确定这一个功能应该归属于哪个服务。

PS:微前端就是微服务在前端的应用,也就是前端微服务。

微服务实践

现在我们将使用微前端框架 qiankun 来构建一个微前端应用。之所以选用 qiankun 框架,是因为它有以下几个优点:

  • 技术栈无关,任何技术栈的应用都能接入。
  • 样式隔离,子应用之间的样式互不干扰。
  • 子应用的 JavaScript 作用域互相隔离。
  • 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。

样式隔离

样式隔离的原理是:每次切换子应用时,都会加载该子应用对应的 css 文件。同时会把原先的子应用样式文件移除掉,这样就达到了样式隔离的效果。

我们可以自己模拟一下这个效果:

1<!-- index.html --> 2<!DOCTYPE html> 3<html lang="en"> 4<head> 5 <meta charset="UTF-8"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>Document</title> 8 <link rel="stylesheet" href="index.css"> 9<body> 10 <div>移除样式文件后将不会变色</div> 11</body> 12</html>
1/* index.css */ 2body { 3 color: red; 4}

现在我们加一段 JavaScript 代码,在加载完样式文件后再将样式文件移除掉:

1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Document</title> 7 <link rel="stylesheet" href="index.css"> 8<body> 9 <div>移除样式文件后将不会变色</div> 10 <script> 11 setTimeout(() => { 12 const link = document.querySelector('link') 13 link.parentNode.removeChild(link) 14 }, 3000) 15 </script> 16</body> 17</html>

这时再打开页面看一下,可以发现 3 秒后字体样式就没有了。

JavaScript 作用域隔离

主应用在切换子应用之前会记录当前的全局状态,然后在切出子应用之后恢复全局状态。假设当前的全局状态如下所示:

const global = { a: 1 }

在进入子应用之后,无论全局状态如何变化,将来切出子应用时都会恢复到原先的全局状态:

1// global 2{ a: 1 }

官方还提供了一张图来帮助我们理解这个机制:

好了,现在我们来创建一个微前端应用吧。这个微前端应用由三部分组成:

  • main:主应用,使用 vue-cli 创建。
  • vue:子应用,使用 vue-cli 创建。
  • react: 子应用,使用的 react 16 版本。

对应的目录如下:

1-main 2-vue 3-react

创建主应用

我们使用 vue-cli 创建主应用(然后执行 npm i qiankun 安装 qiankun 框架):

vue create main

如果主应用只是起到一个基座的作用,即只用于切换子应用。那可以不需要安装 vue-router 和 vuex。

改造 App.vue 文件

主应用必须提供一个能够安装子应用的元素,所以我们需要将 App.vue 文件改造一下:

1<template> 2 <div class="mainapp"> 3 <!-- 标题栏 --> 4 <header class="mainapp-header"> 5 <h1>QianKun</h1> 6 </header> 7 <div class="mainapp-main"> 8 <!-- 侧边栏 --> 9 <ul class="mainapp-sidemenu"> 10 <li @click="push('/vue')">Vue</li> 11 <li @click="push('/react')">React</li> 12 </ul> 13 <!-- 子应用 --> 14 <main class="subapp-container"> 15 <h4 v-if="loading" class="subapp-loading">Loading...</h4> 16 <div id="subapp-viewport"></div> 17 </main> 18 </div> 19 </div> 20</template> 21 22<script> 23export default { 24 name: 'App', 25 props: { 26 loading: Boolean, 27 }, 28 methods: { 29 push(subapp) { history.pushState(null, subapp, subapp) } 30 } 31} 32</script>

可以看到我们用于安装子应用的元素为 #subapp-viewport,另外还有切换子应用的功能:

1<!-- 侧边栏 --> 2<ul class="mainapp-sidemenu"> 3 <li @click="push('/vue')">Vue</li> 4 <li @click="push('/react')">React</li> 5</ul>

改造 main.js

根据 qiankun 文档说明,需要使用 registerMicroApps() 和 start() 方法注册子应用及启动主应用:

1import { registerMicroApps, start } from 'qiankun'; 2registerMicroApps([ 3 { 4 name: 'react app', // app name registered 5 entry: '//localhost:7100', 6 container: '#yourContainer', 7 activeRule: '/yourActiveRule', 8 }, 9 { 10 name: 'vue app', 11 entry: { scripts: ['//localhost:7100/main.js'] }, 12 container: '#yourContainer2', 13 activeRule: '/yourActiveRule2', 14 }, 15]); 16start();

所以现在需要将 main.js 文件改造一下:

1import Vue from 'vue' 2import App from './App' 3import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from 'qiankun' 4 5let app = null 6 7function render({ loading }) { 8 if (!app) { 9 app = new Vue({ 10 el: '#app', 11 data() { 12 return { 13 loading, 14 } 15 }, 16 render(h) { 17 return h(App, { 18 props: { 19 loading: this.loading 20 } 21 }) 22 } 23 }); 24 } else { 25 app.loading = loading 26 } 27} 28 29/** 30 * Step1 初始化应用(可选) 31 */ 32render({ loading: true }) 33 34const loader = (loading) => render({ loading }) 35 36/** 37 * Step2 注册子应用 38 */ 39 40registerMicroApps( 41 [ 42 { 43 name: 'vue', // 子应用名称 44 entry: '//localhost:8001', // 子应用入口地址 45 container: '#subapp-viewport', 46 loader, 47 activeRule: '/vue', // 子应用触发路由 48 }, 49 { 50 name: 'react', 51 entry: '//localhost:8002', 52 container: '#subapp-viewport', 53 loader, 54 activeRule: '/react', 55 }, 56 ], 57 // 子应用生命周期事件 58 { 59 beforeLoad: [ 60 app => { 61 console.log('[LifeCycle] before load %c%s', 'color: green', app.name) 62 }, 63 ], 64 beforeMount: [ 65 app => { 66 console.log('[LifeCycle] before mount %c%s', 'color: green', app.name) 67 }, 68 ], 69 afterUnmount: [ 70 app => { 71 console.log('[LifeCycle] after unmount %c%s', 'color: green', app.name) 72 }, 73 ], 74 }, 75) 76 77// 定义全局状态,可以在主应用、子应用中使用 78const { onGlobalStateChange, setGlobalState } = initGlobalState({ 79 user: 'qiankun', 80}) 81 82// 监听全局状态变化 83onGlobalStateChange((value, prev) => console.log('[onGlobalStateChange - master]:', value, prev)) 84 85// 设置全局状态 86setGlobalState({ 87 ignore: 'master', 88 user: { 89 name: 'master', 90 }, 91}) 92 93/** 94 * Step3 设置默认进入的子应用 95 */ 96setDefaultMountApp('/vue') 97 98/** 99 * Step4 启动应用 100 */ 101start() 102 103runAfterFirstMounted(() => { 104 console.log('[MainApp] first app mounted') 105})

这里有几个注意事项要注意一下:

  1. 子应用的名称 name 必须和子应用下的 package.json 文件中的 name 一样。
  2. 每个子应用都有一个 loader() 方法,这是为了应对用户直接从子应用路由进入页面的情况而设的。进入子页面时判断一下是否加载了主应用,没有则加载,有则跳过。
  3. 为了防止在切换子应用时显示空白页面,应该提供一个 loading 配置。
  4. 设置子应用的入口地址时,直接填入子应用的访问地址。

更改访问端口

vue-cli 的默认访问端口一般为 8080,为了和子应用保持一致,需要将主应用端口改为 8000(子应用分别为 8001、8002)。创建 vue.config.js 文件,将访问端口改为 8000:

1module.exports = { 2 devServer: { 3 port: 8000, 4 } 5}

至此,主应用就已经改造完了。

创建子应用

子应用不需要引入 qiankun 依赖,只需要暴露出几个生命周期函数就可以:

  1. bootstrap,子应用首次启动时触发。
  2. mount,子应用每次启动时都会触发。
  3. unmount,子应用切换/卸载时触发。

现在将子应用的 main.js 文件改造一下:

1import Vue from 'vue' 2import VueRouter from 'vue-router' 3import App from './App.vue' 4import routes from './router' 5import store from './store' 6 7Vue.config.productionTip = false 8 9let router = null 10let instance = null 11 12function render(props = {}) { 13 const { container } = props 14 router = new VueRouter({ 15 // hash 模式不需要下面两行 16 base: window.__POWERED_BY_QIANKUN__ ? '/vue' : '/', 17 mode: 'history', 18 routes, 19 }) 20 21 instance = new Vue({ 22 router, 23 store, 24 render: h => h(App), 25 }).$mount(container ? container.querySelector('#app') : '#app') 26} 27 28if (window.__POWERED_BY_QIANKUN__) { 29 // eslint-disable-next-line no-undef 30 __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; 31} else { 32 render() 33} 34 35function storeTest(props) { 36 props.onGlobalStateChange && 37 props.onGlobalStateChange( 38 (value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev), 39 true, 40 ) 41 props.setGlobalState && 42 props.setGlobalState({ 43 ignore: props.name, 44 user: { 45 name: props.name, 46 }, 47 }) 48} 49 50export async function bootstrap() { 51 console.log('[vue] vue app bootstraped') 52} 53 54export async function mount(props) { 55 console.log('[vue] props from main framework', props) 56 storeTest(props) 57 render(props) 58} 59 60export async function unmount() { 61 instance.$destroy() 62 instance.$el.innerHTML = '' 63 instance = null 64 router = null 65}

可以看到在文件的最后暴露出了 bootstrap mount unmount 三个生命周期函数。另外在挂载子应用时还需要注意一下,子应用是在主应用下运行还是自己独立运行:container ? container.querySelector('#app') : '#app'。

配置打包项

根据 qiankun 文档提示,需要对子应用的打包配置项作如下更改:

1const packageName = require('./package.json').name; 2module.exports = { 3 output: { 4 library: `${packageName}-[name]`, 5 libraryTarget: 'umd', 6 jsonpFunction: `webpackJsonp_${packageName}`, 7 }, 8};

所以现在我们还需要在子应用目录下创建 vue.config.js 文件,输入以下代码:

1// vue.config.js 2const { name } = require('./package.json') 3 4module.exports = { 5 configureWebpack: { 6 output: { 7 // 把子应用打包成 umd 库格式 8 library: `${name}-[name]`, 9 libraryTarget: 'umd', 10 jsonpFunction: `webpackJsonp_${name}` 11 } 12 }, 13 devServer: { 14 port: 8001, 15 headers: { 16 'Access-Control-Allow-Origin': '*' 17 } 18 } 19}

vue.config.js 文件有几个注意事项:

  1. 主应用、子应用运行在不同端口下,所以需要设置跨域头 'Access-Control-Allow-Origin': '*'。
  2. 由于在主应用配置了 vue 子应用需要运行在 8001 端口下,所以也需要在 devServer 里更改端口。

另外一个子应用 react 的改造方法和 vue 是一样的,所以在此不再赘述。

部署

我们将使用 express 来部署项目,除了需要在子应用设置跨域外,没什么需要特别注意的地方。

主应用服务器文件 main-server.js:

1const fs = require('fs') 2const express = require('express') 3const app = express() 4const port = 8000 5 6app.use(express.static('main-static')) 7 8app.get('*', (req, res) => { 9 fs.readFile('./main-static/index.html', 'utf-8', (err, html) => { 10 res.send(html) 11 }) 12}) 13 14app.listen(port, () => { 15 console.log(`main app listening at http://localhost:${port}`) 16})

vue 子应用服务器文件 vue-server.js:

1const fs = require('fs') 2const express = require('express') 3const app = express() 4const cors = require('cors') 5const port = 8001 6 7// 设置跨域 8app.use(cors()) 9app.use(express.static('vue-static')) 10 11app.get('*', (req, res) => { 12 fs.readFile('./vue-static/index.html', 'utf-8', (err, html) => { 13 res.send(html) 14 }) 15}) 16 17app.listen(port, () => { 18 console.log(`vue app listening at http://localhost:${port}`) 19})

react 子应用服务器文件 react-server.js:

1const fs = require('fs') 2const express = require('express') 3const app = express() 4const cors = require('cors') 5const port = 8002 6 7// 设置跨域 8app.use(cors()) 9app.use(express.static('react-static')) 10 11app.get('*', (req, res) => { 12 fs.readFile('./react-static/index.html', 'utf-8', (err, html) => { 13 res.send(html) 14 }) 15}) 16 17app.listen(port, () => { 18 console.log(`react app listening at http://localhost:${port}`) 19})

另外需要将这三个应用打包后的文件分别放到 main-static、vue-static、react-static 目录下。然后分别执行命令 node main-server.js、node vue-server.js、node react-server.js 即可查看部署后的页面。现在这个项目目录如下:

1-main 2-main-static // main 主应用静态文件目录 3-react 4-react-static // react 子应用静态文件目录 5-vue 6-vue-static // vue 子应用静态文件目录 7-main-server.js // main 主应用服务器 8-vue-server.js // vue 子应用服务器 9-react-server.js // react 子应用服务器

我已经将这个微前端应用的代码上传到了 github,建议将项目克隆下来配合本章一起阅读,效果更好。下面放一下 DEMO 的运行效果图:

小结

对于大型应用的开发和维护,使用微前端能让我们变得更加轻松。不过如果是小应用,建议还是单独建一个项目开发。毕竟微前端也有额外的开发、维护成本。

参考资料

带你入门前端工程 全文目录:

  1. 技术选型:如何进行技术选型?
  2. 统一规范:如何制订规范并利用工具保证规范被严格执行?
  3. 前端组件化:什么是模块化、组件化?
  4. 测试:如何写单元测试和 E2E(端到端) 测试?
  5. 构建工具:构建工具有哪些?都有哪些功能和优势?
  6. 自动化部署:如何利用 Jenkins、Github Actions 自动化部署项目?
  7. 前端监控:讲解前端监控原理及如何利用 sentry 对项目实行监控。
  8. 性能优化(一):如何检测网站性能?有哪些实用的性能优化规则?
  9. 性能优化(二):如何检测网站性能?有哪些实用的性能优化规则?
  10. 重构:为什么做重构?重构有哪些手法?
  11. 微服务:微服务是什么?如何搭建微服务项目?
  12. Severless:Severless 是什么?如何使用 Severless?

本文转自 https://segmentfault.com/a/1190000039110977,如有侵权,请联系删除。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

java将前端的json数组字符串转换为列表

记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang