什么是微服务?先看看维基百科的定义:css
微服务(英语:Microservices)是一种软件架构风格,它是以专一于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模块化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关 (Language-Independent/Language agnostic)的API集相互通讯。
换句话说,就是将一个大型、复杂的应用分解成几个服务,每一个服务就像是一个组件,组合起来一块儿构建成整个应用。html
想象一下,一个上百个功能、数十万行代码的应用维护起来是个什么场景?前端
- 牵一发而动全身,仅仅修改一处代码,就须要从新部署整个应用。常常有“修改一分钟,编译半小时”的状况发生。
- 代码模块错综复杂,互相依赖。更改一处地方的代码,每每会影响到应用的其余功能。
若是使用微服务来重构整个应用有什么好处?
一个应用分解成多个服务,每一个服务独自服务内部的功能。例如原来的应用有 abcd 四个页面,如今分解成两个服务,第一个服务有 ab 两个页面,第二个服务有 cd 两个页面,组合在一块儿就和原来的应用同样。vue
当应用其中一个服务出故障时,其余服务仍能够正常访问。例如第一个服务出故障了, ab 页面将没法访问,但 cd 页面仍能正常访问。node
好处:不一样的服务独立运行,服务与服务之间解耦。咱们能够把服务理解成组件,就像本小书第 3 章《前端组件化》中所说的同样。每一个服务能够独自管理,修改一个服务不影响总体应用的运行,只影响该服务提供的功能。react
另外在开发时也能够快速的添加、删除功能。例如电商网站,在不一样的节假日时推出的活动页面,活动事后立刻就能够删掉。webpack
难点:不容易确认服务的边界。当一个应用功能太多时,每每多个功能点之间的关联会比较深。于是就很难肯定这一个功能应该归属于哪一个服务。git
PS:微前端就是微服务在前端的应用,也就是前端微服务。github
微服务实践
如今咱们将使用微前端框架 qiankun 来构建一个微前端应用。之因此选用 qiankun 框架,是由于它有如下几个优势:web
- 技术栈无关,任何技术栈的应用都能接入。
- 样式隔离,子应用之间的样式互不干扰。
- 子应用的 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(子应用分别为 800一、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 的运行效果图:
小结
对于大型应用的开发和维护,使用微前端能让咱们变得更加轻松。不过若是是小应用,建议仍是单独建一个项目开发。毕竟微前端也有额外的开发、维护成本。
参考资料
带你入门前端工程 全文目录:
- 技术选型:如何进行技术选型?
- 统一规范:如何制订规范并利用工具保证规范被严格执行?
- 前端组件化:什么是模块化、组件化?
- 测试:如何写单元测试和 E2E(端到端) 测试?
- 构建工具:构建工具备哪些?都有哪些功能和优点?
- 自动化部署:如何利用 Jenkins、Github Actions 自动化部署项目?
- 前端监控:讲解前端监控原理及如何利用 sentry 对项目实行监控。
- 性能优化(一):如何检测网站性能?有哪些实用的性能优化规则?
- 性能优化(二):如何检测网站性能?有哪些实用的性能优化规则?
- 重构:为何作重构?重构有哪些手法?
- 微服务:微服务是什么?如何搭建微服务项目?
- Severless:Severless 是什么?如何使用 Severless?
本文同步分享在 博客“谭光志”(SegmentFault)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。