微前端实践

在 toB 的前端开发工做中,咱们每每就会遇到以下困境:css

  1. 工程愈来愈大,打包愈来愈慢
  2. 团队人员多,产品功能复杂,代码冲突频繁、影响面大
  3. 心里想作 SaaS 产品,但客户老是要作定制化

不一样的团队可能有不一样的方法去解决这些问题。在前端开发突飞猛进、前端工程化蓬勃发展的今天,我想给你们介绍下另外一种尝试——微前端。html

微前端是什么

那什么是微前端?微前端主要是借鉴后端微服务的概念。简单地说,就是将一个巨无霸(Monolith)的前端工程拆分红一个一个的小工程。别小看这些小工程,它们也是“麻雀虽小,五脏俱全”,彻底具有独立的开发、运行能力。整个系统就将由这些小工程协同合做,实现全部页面的展现与交互。前端

能够跟微服务这么对比着去理解:vue

微服务 微前端
一个微服务就是由一组接口构成,接口地址通常是 URL。当微服务收到一个接口的请求时,会进行路由找到相应的逻辑,输出响应内容。 一个微前端则是由一组页面构成,页面地址也是 URL。当微前端收到一个页面 URL 的请求时,会进行路由找到相应的组件,渲染页面内容。
后端微服务会有一个网关,做为单一入口接收全部的客户端接口请求,根据接口 URL 与服务的匹配关系,路由到对应的服务。 微前端则会有一个加载器,做为单一入口接收全部页面 URL 的访问,根据页面 URL 与微前端的匹配关系,选择加载对应的微前端,由该微前端进行进行路由响应 URL。

这里要注意跟 iframe 实现页面嵌入机制的区别。微前端没有用到 iframe,它很纯粹地利用 JavaScript、MVVM 等技术来实现页面加载。后面咱们将介绍相关的技术实现。webpack

为何要用微前端

在介绍具体的改造方式以前,我想跟你们先说明下咱们当时面临的问题,以及改造后的对比,以便你们以此为对照,评判或决定使用。主要包括打包速度、页面加载速度、多人多地协做、SaaS 产品定制化、产品拆分这几个角度。web

首先是打包速度。在 6 个月前,咱们的 B 端工程那会儿仍是一个 Monolith。当时已经有 20 多个依赖、60 多个公共组件、200 多个页面,对接 700 多个接口。咱们使用了 Webpack 2,并启用 DLL Plugin、HappyPack 4。在个人我的主机上使用 4 线程编译,大概要 5 分钟。而若是不拆分,算下来如今咱们已经有近 400 个页面,对接1000 多个接口。
这个时间意味着什么?它不只会耽误咱们开发人员的时间,还会影响整个团队的效率。上线时,在 Docker、CI 等环境下,耗时还会被延长。若是部署后出几个 Bug,要线上当即修复,那就不知道要熬到几点了。
在使用微前端改造后,目前咱们已经有 26 个微前端工程,平均打包时间在 30-45 秒之间(注意,这里尚未应用 DLL + HappyPack)。vue-router

页面加载速度其实影响到并非很大,由于通过 CDN、gzip 后,资源的大小还能接受。这里只是给你们看一些直观的数据变化。6 个月前,打包生成的 app.js 有 5MB(gzip 后 1MB),vendor.js 有 2MB(gzip 后 700KB),app.css 有 1.5MB(gzip 后 250KB)。这样首屏大概要传输 2MB 的内容。拆分后,目前首屏只须要传输 800KB 左右。bootstrap

在协做上,咱们在全国有三个地方的前端团队,这么多人在同一个工程里开发,遭遇代码冲突的几率会很频繁,并且冲突的影响面比较大。若是代码中出现问题,致使 CI 失败,全部其余人的代码提交与更新也都会被阻塞。使用微前端后,这样的风险就平摊到各个工程上去了。windows

再者就是定制化了。咱们作的额是一款 toB 的产品,作成 SaaS 标准版产品大概是全部从业者的愿望。但总体市场环境与产品功能所限,常常要面临一些客户要求作本地化与定制化的要求。本地化就会有代码安全方面的考量,最好是不给客户源代码,最差则是只给客户购买功能的源代码。而定制化从易到难则能够分为独立新模块、改造现有模块、替换现有模块。
经过微前端技术,咱们能够很容易达到本地化代码安全的下限——只给客户他所购买的模块的前端源码。定制化里最简单的独立新模块也变得简单:交付团队增长一个新的微前端工程便可,不须要揉进现有研发工程中,不占用研发团队资源。而定制化中的改造现有模块也能够比较好地实现:好比说某个标准版的页面中须要增长一个面板,则能够经过一个新的微前端工程,一样响应该页面的 URL(固然要控制好顺序),在页面的恰当位置插入一个新的 DOM 节点便可。后端

最后就是产品拆分方面的考量了。咱们的产品比较大,有几块功能比较独立、有特点。若是说未来须要独立成一个子产品,有微前端拆分做为铺垫,腾挪组合也会变得更加容易些。

其余目标

有了以上的一些缘由与诉求,在决定进行微前端改造前,还须要设定一些额外的小目标:

  • 不能对现有的前端开发方式带来太大变化,至少要有平滑过渡的机制。
  • 每一个为前端工程都要求能够独立运行,至少在本地开发时要能作到。
  • 微前端在加载时,要实现预加载,并能够自由调整预加载顺序,甚至是根据用户的偏好来实现智能化、个性化的加载顺序。

如何改造现有工程

“Talk is cheap,show me the code“。下面就让咱们一块儿来看看具体的改造吧!咱们的微前端工程能够划分为 portal 工程、业务工程、common 工程这几类。

portal 工程

portal,顾名思义,就是入口。这也就是上面所说的微前端加载器。当用户打开浏览器,首次进入咱们的页面时,不论是什么 URL,首先加载的就是 portal。portal 里会配置全部业务工程的地址、匹配哪些 URL、须要加载哪些资源。如:

// 业务工程的名称
customer: {
    // URL 匹配模式
    matchUrlHash: ['^/customer'],
    // 微前端地址
    target: 'http://localhost:8101/mfe-customer/index.html',
    // 资源匹配模式
    resourcePatterns: ['/app.*.css$', '/vendor.*.css$', '/manifest.*.js$', '/vendor.*.js$', '/app.*.js$'],
}
复制代码

portal 会定时、异步、并发地下载业务工程的资源,并将它们进行注册,此时并不会加载这些业务工程。这里之因此要业务工程的地址(target)、资源(resourcePatterns),是为了加载时肯定地知道其所包含的 app.js、vendor.js、app.css 等资源的路径。由于业务工程每次有变动,app.js 等资源路径上都会带有新的文件内容哈希值(Hash),致使路径不可预测。而它的 index.html 的路径是固定的。咱们读取该 HTML,解析其内容,经过正则就能匹配到 app.js 等资源的路径。

portal 在运行时,会监听 URL 变化。目前咱们只支持 URL Hash(如 #/customer)。当 Hash 发生变动时,匹配到业务工程,而后执行卸载、加载的工做。这个机制主要是利用 single-spa 来实现,但原理就是这么简单。

import { registerApplication } from 'single-spa';
registerApplication('customer', 
    // 下载微前端工程,获取三个函数钩子:bootstrap、mount、unmount
    () => {
        const html = fetch(mfeConfig.target);
        const {cssUrls, jsUrls} = match(html, mfeConfig.resourcePatterns);
        loadCss(cssUrls);
        loadJs(jsUrls);
        return windows['mfe:customer'];
    },
    // 对当前浏览器 URL Hash 进行匹配,若是匹配(返回 true),则加载该微前端(调用 mount);不然卸载(调用 unmount)
    () => {
        return match(window.location.hash, mfeConfig.matchUrlHash);
    },
    mfeConfig.customProps
);
复制代码

业务工程

业务工程就是普通的微前端工程,通常一个模块一个工程。业务工程要扮演两个角色,一个是可独立运行的前端工程,一个是受 portal 控制的运行时。前者主要用于咱们本地开发,后者则是线上集成时使用。在独立运行时,它跟原来的前端工程没有什么区别。以 Vue 工程为例,照样使用 new Vue({el: '#app'}) 来启动、渲染页面。

new Vue({
    el: '#app',
    i18n,
    router,
    store,
    template: '<App/>',
    components: { App }
});
复制代码

而当受控运行时,则是利用 UMD 方式输出几个钩子函数,包括初始化、加载、卸载。

if(!window.IS_IN_MFE){ // 独立运行时
    new Vue({...})
} else { // 受控运行时
    module.exports = {
        bootstrap(){ // 注册时执行
        },
        mount(customProps){ // 加载时执行
            return Promise.resolve().then(()=>{
            instance = new Vue({...})
            })
        },
        unmount(){ // 卸载时执行
            return Promise.resolve().then(()=>{
            instance.$destroy()
            })
        }
    }
}
复制代码

线上环境的 Webpack 配置:

output: {
    libraryTarget: "umd",
    library: 'mfe:customer'
}
复制代码

而区分是否受控,则能够经过判断一个全局变量来实现。如 window.IS_IN_MFE,portal 工程在运行时会将其设置为 true

为了支持本地多个工程同时开发,咱们须要为每一个微前端工程指定一个肯定的、独占的端口号。好比从 8100 开始,逐一递增。同时,为了支持线上部署,咱们还须要给每一个微前端工程指定一个肯定的、独占的基础路径(前缀)。这样相同域名下能够用不一样路径进行独立访问。路径统一以 /mfe- 开头,如 /mfe-customer。这也就是上面 portal 里业务工程的配置示例里所展示的那样。

特殊业务工程:mfe-navs

咱们产品的页面结构分为顶部栏、侧边栏、中间内容区三大块。顶部栏和侧边栏在页面跳转过程当中,基本上保持不变。因此咱们也将它们剥离出来做为一个独立的微前端业务工程,叫作 mfe-navs。它会匹配全部的 URL,也就是说访问任意 URL 时,都会加载它,并且还要保证先加载它。当它加载完毕后,会在页面内提供一个中间内容区的锚点 DOM(#app:),供其余业务工程加载时挂载。

Common 工程

上面能够看到,每个业务工程都是一个独立的前端工程,因此里面会有一些相同的依赖,如 Vue、moment、lodash 等。若是将这些内容都打包到各自的 vendor.js 里,则势必会致使代码冗余太多,浏览器运行内存压力增大。咱们把这些公共依赖、公共组件、CSS、Fonts 等都放到一个工程里,由该工程进行打包,将依赖、组件 export,并以 UMD 的方式注入到全局。

main.js:

import Vue from 'vue'; // 公共依赖
import VueRouter from 'vue-router';
import VueI18n from 'vue-i18n';
import '@/css/icon-font/iconfont.css';
import ContentSelector from '@/components/ContentSelector'; // 公共组件

Vue.use(VueI18n); // 你们都要这么作,咱们就代劳吧!

module.exports = {
    'vue': Vue,
    'vue-router': VueRouter,
    'content-selector': ContentSelector,
};
复制代码

Webpack 配置:

output: {
    libraryTarget: "umd",
    library: 'mfe:common'
}
复制代码

业务工程则经过 Webpack 外部依赖(external)的方式引入到工程中。这样业务工程打包时就不会包含这些公共代码了。

var externalModules = ['vue', 'vue-router', 'content-selector'];

module.exports = { // webpack 配置项
    // ...
    externals: (context, request, callback)=>{
        if(externalModules.includes(request)){
            callback(null, 'root window["mfe:common"]["'+request+'"]')
        } else {
            callback();
        }
    },
}
复制代码

结语

以上就是咱们微前端改造与实践方面的一些经验。前路漫漫,这里面还存在不少待完善的地方,如 History 模式支持、i18n 更好地集成、各个业务工程的加载顺序优化及个性化等。除了这些纯粹技术上的探索,在拥有微前端、微服务这些架构的基础上,团队也能够考虑进行垂直拆分:一个小组独立负责一块业务,它有本身的微前端工程和微服务工程。从技术管理到人员管理,将它们糅合在一块儿统一考虑,这也是咱们软件工程的探索方向。期待这些可以对你们带来一些思考和帮助!

相关文章
相关标签/搜索