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

微服务(英语:Microservices)是一种软件架构格调,它是以专一于繁多责任与性能的小型性能区块 (Small Building Blocks) 为根底,利用模块化的形式组合出简单的大型应用程序,各性能区块应用与语言无关 (Language-Independent/Language agnostic)的API集互相通信。

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

设想一下,一个上百个性能、数十万行代码的利用保护起来是个什么场景?

  1. 牵一发而动全身,仅仅批改一处代码,就须要重新部署整个利用。常常有“批改一分钟,编译半小时”的状况产生。
  2. 代码模块盘根错节,相互依赖。更改一处中央的代码,往往会影响到利用的其余性能。

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

一个利用分解成多个服务,每个服务单独服务外部的性能。例如原来的利用有 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 = nullfunction 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')})

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

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

更改拜访端口

vue-cli 的默认拜访端口个别为 8080,为了和子利用保持一致,须要将主利用端口改为 8000(子利用别离为 8001、8002)。创立 vue.config.js 文件,将拜访端口改为 8000:

module.exports = {    devServer: {        port: 8000,    }}

至此,主利用就曾经革新完了。

创立子利用

子利用不须要引入 qiankun 依赖,只须要暴露出几个生命周期函数就能够:

  1. bootstrap,子利用首次启动时触发。
  2. mount,子利用每次启动时都会触发。
  3. 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 = falselet router = nulllet instance = nullfunction 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.jsconst { 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 文件有几个注意事项:

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

另外一个子利用 react 的革新办法和 vue 是一样的,所以在此不再赘述。

部署

咱们将应用 express 来部署我的项目,除了须要在子利用设置跨域外,没什么须要特地留神的中央。

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

const fs = require('fs')const express = require('express')const app = express()const port = 8000app.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-staticvue-staticreact-static 目录下。而后别离执行命令 node main-server.jsnode vue-server.jsnode 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

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

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