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

可莉 等级 379 0 1

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

微服务(英语: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 = 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')
}) 

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

  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 = 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 文件有几个注意事项:

  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 = 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-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 的运行效果图:

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

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

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

小结

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

参考资料

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

  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,如有侵权,请联系删除。

收藏
评论区

相关推荐

微信支付 (JSSDK支付)
官方文档 微信支付 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter7_7&index6 微信授权获取code https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JSSDK.html58 准备工作 微信公
微信小程序支付功能全流程实践
前言 微信小程序为电商类小程序,提供了非常完善、优秀、安全的支付功能。在小程序内可调用微信的API完成支付功能,方便、快捷。小程序开发者在开发小程序时,支付流程是必然要接触到,今天胡哥就小程序支付的全流程为大家一一细说,让小伙伴能快速得掌握小程序支付能力,避免踩坑! 知己知彼,方能百战不殆 小程序支付流程图 小程序支付交互流程图(https:/
微信小程序modal
首先创建一个组件component,组件命名可以为modal modal.wxml的内容为 <view class'modalmask' wx:if'{{show}}' bindtap'clickMask' <view class'modalcontent' <scrollview scrolly class'mainc
微信小程序new Date()转换时间异常问题
微信小程序苹果手机页面上显示时间异常,安卓机正常问题 image(https://imghelloworld.osscnbeijing.aliyuncs.com/imgs/b691e1230e2f15efbd81fe11ef734d4f.png) 错误代码 var date '20210306 17:00:00' var dateT
带你入门前端工程:微前端
什么是微服务?先看看维基百科(https://zh.wikipedia.org/wiki/%E5%BE%AE%E6%9C%8D%E5%8B%99)的定义: 微服务(英语:Microservices)是一种软件架构风格,它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模块化的方式组合出复杂的大型应用
金三银四了,掌握 JS 这 36 个概念,助你一臂之力
作者:Mahdhi Rezvi 译者:前端小智 来源:dmitripavlutin 点赞再看,微信搜索【大迁世界(https://mp.weixin.qq.com/s/sY9ufGGKfcdaAQ7KJQs3HA)】,B站关注【前端小智(https://space.bilibili.com/31089477)】这个没有大厂背景,但有着
vue h5 对接支付宝,微信支付,微信js支付
vue h5 实现支付(支付宝,微信) h5端实现支付难度不大,只是有些小的点需要注意下,其他的看文档撸就行了。 支付宝很简单,后端返回一个 html ,前端插入调用就行了,微信支付分两种:1、微信内支付(jsapi,微信内浏览器)2、微信外支付(h5支付)。 一、支付宝支付 // 前端啥都不用管,交给后端去干,返回 html 调用点击就好了 /
7个关于"this"面试题,你能回答上来吗?
作者:Shadeed 译者:前端小智 来源:dmitripavlutin 点赞再看,微信搜索【大迁世界(https://mp.weixin.qq.com/s/sY9ufGGKfcdaAQ7KJQs3HA)】,B站关注【前端小智(https://space.bilibili.com/31089477)】这个没有大厂背景,但有着一股向上积
微信小程序字符串展示为二维码
微信小程序中要将一个字符串展示为二维码的形式,需要引入该js文件 // Core code comes from https://github.com/davidshimjs/qrcodejsvar QRCode;(function () { / Get the type by string length
微信小程序 - 引入字体图标
网站图标要想做到清晰无锯齿,使用普通图片或者雪碧图都很难达到这个目的,一般我们都会引入字体图标(svg转font,使用图标像使用字体一样,详见《web页面使用字体图标》,那么如何在微信小程序中使用自定义图标呢?请看详细步骤:1、从上选择喜欢的图标加入购物车,在购物车弹窗中点击“下载代码”后,解压阿里图库 加入购物车购物车 下载代码图标文件内容2.、进入导入第
推荐几个微信小程序开发小技巧
前段时间在下开发了个微信小程序,开发过程中总结了一些我觉得对我有用的小技巧,提炼出来,相当于一个总结复盘,也希望可以帮助到大家。如果对大家确实有帮助,别忘了点赞哦 🌟 ~1\. 开发中可能遇到的坑以及 Tips本来想写个小技巧的,结果我总结了一堆坑,没上手之前完全想象不到微信小程序的开发体验是如此之差、如此之烂,从微信
计划助手V1.0-微信小程序(QQ小程序)-源代码分享
疫情期间在家感觉好无聊啊,于是利用空闲时间做了一个用来记录和管理小目标时间的小程序,命名为《小沙漏》。 QQ版本小程序同步上线,QQ小程序叫《时间小沙漏》,欢迎大家前来体验,后期也会添加其他的新功能哦 【区别】:微信小程序的代码与QQ小程序的源码是不一样的。 微信小程序的源码基于微信小程序云开发,需要在有网络的情况下使用,具有同步功能,所有记录在删除小
手写一个仿微信登录的nodejs程序
前言首先,我们看一下微信开放文档中的一张图:上面的一幅图中清楚地介绍了微信登录整个过程,下面对图上所示进行总结:一、二维码的获得1. 用户打开登录网页后,登录网页后台根据微信OAuth2.0协议向微信开发平台请求授权登录,并传递事先在微信开发平台中审核通过的AppID和AppSecrect等参数; 2. 微信开发平台对AppID等参数进行验证,并向
微信小程序体验composition-api(类似vue3)
微信小程序compositionapi用该是什么样子? 使用使用起来应该像是这个样子wxue(options) setup配置应该是包含一个setup选项是一个函数,返回的函数可以this.xxx调用,返回的数据可以this.data.xxx用到,如下import wxue, reactive from 'wxue'wxue( setup(option
想搞定大厂面试官?被逼无奈开始狂啃底层技术
Part 1微服务架构设计概述1.1 传统应用架构的问题1.2 微服务架构是什么1.3 微服务架构有哪些特点和挑战1.4 如何搭建微服务架构 Part 2微服务开发框架2.1 Spring Boot 是什么2.2 如何使用Spring Boot框架2.3 Spring Boot生产级特性 Part 3微服务网关3.1 Node.js 是什么3.2 如何使用

热门文章

了解Vuex状态管理模式Vue 包大小优化

最新文章

Vue 包大小优化了解Vuex状态管理模式