微内核架构在大型前端系统中的应用(微前端)

微内核架构在大型前端系统中的应用(微前端)

只讨论架构,不讨论框架。 提出除『微前端』以外另外一种建设高弹性架构的思路。javascript

一、名词解释

由一群尽量将数量最小化的软件程序组成,他们负责提供、实现一个操做系统所须要的各类机制和功能。这些最基础的机制,包括了底层地址空间管理,线程管理,与进程间通信。前端

二、设计理念

将系统的实现,与系统的基本操做规则区分开来。它实现的方式是将核心功能模块化,划分红几个独立的进程,各自运行,这些进程被称为服务。全部的服务进程,都运行在不一样的地址空间。vue

让服务各自独立,能够减小系统之间的耦合度,易于实现与除错,也能够增进可移植性。它能够避免单一组件失效,而形成整个系统崩溃,内核只须要重启这个组件,不至于影响其余服务器的功能,使系统稳定度增长。同时业务功能能够视须要,抽换或新增某些服务进程,使功能更有弹性java

就代码数量来看,通常来讲,由于功能简化,核心系统使用的代码比集成式系统更少。更少的代码意味更少的潜藏程序bugreact

三、具体应用

微内核架构在使用时主要考虑两个方面『核心系统』和『插件模块』。应用逻辑被划分为独立的『核心系统』和『插件模块』,这样就提供了良好的可扩展性灵活性,应用的新特性和基础业务逻辑也会被隔离webpack

1、核心系统

核心系统一般是一个能够独立运行的最小化模块,操做系统(Windows NT、Mac OS X)就是这么实现的。从商业应用的角度来看,核心系统为那些特定的场景、规则、复杂的条件判断提供了通用的业务逻辑,而插件模块则提供了更为具体的业务逻辑。能够增长或扩展核心系统以达到产生附加的业务逻辑的能力。git

2、插件模块

插件模块一般是一个专业处理额外特性的独立组件。一般,插件模块之间是没有依赖的,固然你也能够建立一个依赖其余插件模块的插件,但无论怎么样,让插件模块之间能够彼此通信又不产生依赖是一个很重要的问题。github

3、获取插件模块并判断可用性

核心系统须要知道每一个插件的可用性而且知道如何获取它们,一个一般的实现方式是使用一组注册表。注册表包括了每一个插件的基本信息,包括名称数据规范远程访问协议(取决于插件模块如何和核心系统进行链接)以及其余自定义数据。好比百度网盘中用于上传文件的上传插件提供了插件名称、数据规范(输入、输出数据)、数据格式(json、xml),若是这个插件是经过异步进行加载的,那么还会有一个具体远程HTTP访问协议地址。web

4、链接到核心系统

插件模块能够经过多种方式链接到核心系统,包括OSGI(open service gateway initiative)消息机制web服务以及点对点的绑定(对象实例化,既依赖注入)。使用何种方式主要取决于具体的应用场景和特殊需求(单机部署、分布式部署),微内核架构默认没有要求具体的实现方式,可是必须保证插件模块之间不能产生任何依赖。npm

5、通讯规范

插件模块和核心系统之间的通讯规范分为标准规范自定义规范,自定义规范一般是指某个插件模块是由第三方服务开发的。这种状况下,就须要在自定义规范和标准规范之间提供一个Adapter,这样核心系统就不须要关心每一个插件模块的具体实现。在设计标准规范以前制定一个版本策略很重要。

6、事件模式

核心系统提供了多种事件模式,主要包括经常使用的点对点模式、发布订阅模式。同时,事件的类型分为全局(系统级)事件、系统内部事件以及插件模块内部事件。因为点对点模式中发送者和接收者之间没有依赖关系而且一条消息只对应一个接收者,因此能够用做广播全局(系统级)事件,好比调起某个插件模块。而发布订阅模式中订阅者和发布者之间存在时间上的依赖性,能够用于系统内部事件和插件模块的内部事件。此外,核心模块也能够经过发布订阅模式向外发布某些属于业务基本操做规则的事件。

7、接口设计

当插件模块注册到核心系统以后,经过系统级事件能够调起具体的某插件模块。此时就须要核心模块提供属于基本操做规则的接口供插件模块使用,一样的,插件模块也必须按照通讯规范提供运行入口(相似于java的Main方法)数据规范(参数格式,返回的数据格式),以此保障插件模块能够在核心系统上正确运行。插件模块是独立于核心系统以外的,可是根据具体的需求(提供单纯的数据服务处理系统数据和信息)可能会须要操做核心模块的系统服务作一些定制化功能,此时核心系统须要提供一个上下文对象(Context),且插件模块与外部进行交互只能经过此上下文对象。上下文对象提供了基础操做(调起其余插件模块、调起系统服务、获取系统信息)的API和事件。

四、在前端系统中使用

把前端系统当成一个操做系统,业务基本操做的业务逻辑抽象成一个能够独立运做的系统内核,而不属于业务基本操做的业务逻辑都当成一个应用程序,完成安装、卸载、禁用、调用以及开机启动等功能。

在功能愈来愈多,依赖愈来愈负责的大型前端系统中,若是在项目初期没有很好地考虑后期兼容的灵活性、扩展性以及弹性,很容易出现项目难以维护或者谁都不想碰的尴尬场面,因此初期的设计很重要。

目前的大型前端单页面系统使用的都是根据业务划分独立组件,进行解耦和复用,最后经过组件进行堆叠、编译、上线。这样虽然完成依赖良好的组件化设计考虑到了系统的扩展性和灵活性以及弹性,可是整个系统仍是牢牢绑在一块儿的,并无根据基础业务和附加业务进行很好的拆分。固然不少优秀的前端工程师也考虑到了这一方面,提出来微前端的概念。不过微前端仍是一个比较新的技术概念,没有通过不少大型前端系统的实践。而微内核架构已经在操做系统和不少的产品的后端服务及前端APP中通过了不少的实践。

1、定义核心模块和系统服务

上面提到核心模块是一个能够独立运行起来,包含系统基本操做规则的最小化模块。没有任何插件模块依然能够正常运行并处理基本的业务逻辑,因此在大型前端系统中将基础页面以及基础功能单独包装起来,组成一个最小化的模块,称之为core system。而这个core system能够经过包方式在多个系统间进行复用(NPM、bower、bundle、js chunk)。同时,将那些和业务相关的操做按照类型和场景封装为多个系统服务,并挂载(依赖注入)到核心系统中,称之为system service。须要注意的是core system可操做system service,而system service不可操做core system

此外,core system根据具体的模块规范(AMD、CMD、CommonJS、ESM、SystemJS、UMD)向外部暴露了可交互的API和事件,称之为标准接口。后续在编写插件模块时要严格按照标准接口进行开发。

2、定义插件模块

插件模块是一个独立于核心系统的专业处理不属于系统基本操做的业务的模块(组件),好比网盘中的上传、下载、分享等功能。每一个插件模块必须遵守定义好的标准接口通讯规范进行开发,并且每一个插件模块都是相互独立的,因此没有对每一个插件的实现细节作过多要求,如A插件模块使用React开发,B插件模块使用Vue开发,C模块使用jQuery开发。

每一个插件模块都应该提供一个包含本插件模块签名信息(Mainfest)的JSON文件,签名信息包括了这个插件的名称、数据规范、依赖、远程访问地址(异步加载的js下载地址)和其余自定义字段。在前端加载核心系统时将该Mainfest文件注册进去,完成核心系统和插件模块的链接。

每一个插件模块都应该提供一个统一名称的运行入口,好比start方法。也能够按照标准接口提供插件的生命周期事件,方便更细粒度的控制。

3、注册和调起

每一个插件都提供了各自的Mainfest签名文件可执行文件(JS文件、CSS文件)。因此当服务器接收到浏览器请求时能够将所要求的插件Manifest进行merge,合并成一个大的JSON结构,而后返回给浏览器。浏览器接收后,执行核心系统并注册Manfiest信息,而后启动。在注册过程当中能够按照需求完成开机启动(默认执行)预加载以及后台运行等不一样类型的操做。

在业务逻辑和插件内部逻辑中能够能存在调起其余插件模块的需求,因为插件模块之间不产生依赖而且独立于核心系统,因此没法直接进行调起。不过因为注册表点对点事件模式的存在,能够经过核心系统向外暴露的API传入插件名称和组插件模块ID等信息进行调起。在调起以前先判断该插件在是否已注册,是否已加载(同步加载、异步加载),是否为单例和互斥,参数信息和数据格式,保证它能够正确的调起。

在插件运行过程当中出现异常时,经过系统级事件通知核心模块。核心模块根据签名信息中的标识选择重启关闭该插件模块。

4、多入口管理

在复杂的前端系统中同一个功能可能会存在过个入口的状况,好比上传、下载、分享等功能都是经过不一样位置的按钮点击进行调起。一般,将具体功能(插件模块)插件模块入口的UI展现进行隔离。首先,在基础页面结构中按照需求进行分块,分红不一样的功能块,如菜单栏、右键菜单、列表项、右侧区域、左侧区域,并为这些区域定义惟一的名称和ID。在须要进行入口展现的插件模块的Manifest中,标识入口的区域和展现方式(按钮、图片、引导块、菜单项、下拉菜单)。

核心系统在注册表注册完毕后,解析那些须要展现入口的的字段并交给专门渲染插件模块入口系统服务,这样就经过配置完成了多入口的管理,在后续需求变更和修改时,只须要更改Manifest文件便可,更加完善了系统的扩展性、灵活性、弹性。

五、技术选型

架构是独立于框架和类库的存在。

微内核架构的核心就是使业务的基本操做和专业处理额外特性的操做相隔离,提升系统的扩展性、灵活性和弹性。因此在技术选型时咱们须要考虑三个方面:核心系统、系统服务、插件模块。

核心系统一般包含一个项目所须要的基本功能,包括基本的展现页面、交互操做、业务处理,代码量一般不多;系统服务提供业务处理的通用功能,好比列表操做、弹框、提示、异步化接口处理等,一般将系统中通用的需求抽象到这一层中;因此,这两个方面可使用目前常见的react或vue经过webpack工具进行规范化开发,但如何向外部暴露核心系统的API和事件给插件模块调用是一个十分重要的问题。

插件模块更倾向于一个专业处理额外特性的lib库,因此推荐使用rollup或者webpack的lib模式进行开发和打包,产出一个『干净』的bundle(也能够发布到NPM中,实现独立发布和维护)。须要注意的是,若是这个bundle按照定义好的标准规范进行开发,那么它能够在任意一个微内核架构下运行,达到跨系统的能力。就像按照X86规范编写的程序能够在任意一个X86架构的系统上运行同样。

调起插件模块时如何异步加载插件模块bundle?

1、插件模块开发阶段

方案一:source code 插件模块的代码放置在一个根文件夹中,经过源代码进行开发和编译。每次更改后经过rollup或webpack产出一个bundle与Manifest文件,而后将它们上线更新便可。

这种模式下,插件模块的代码更新后,对应的Manifest文件也会更新,因此核心系统加载到插件模块也会被更新,不须要基础业务逻辑执行任何操做。 优势:不须要更新并上线基础业务代码。 缺点:没有版本号的管理功能以及不方便测试。

方案二:npm install 插件模块发布到github、gitlab等其余托管平台中,经过npm进行安装到基础业务逻辑中。插件模块每次更改后须要从新发布到托管平台,并在须要在业务逻辑中更新版本号从新执行npm install xxx,而后从新编译业务代码进行上线。

插件模块更新后,不须要像方案一那样上线插件模块。而是更新业务逻辑的依赖,安装最新版本的插件模块。 优势:能够经过版本号加载不一样的阶段的插件模块以及方便测试。 缺点:更改后须要从新安装插件模块,并对依赖此插件模块的业务逻辑从新进行编译和上线。回归成本大,除了回归插件模块还要回归其余基础业务逻辑(固然也能够像方案一那样作,可是这样就抛弃了npm的最大优势 -> 版本号管理)。


2、获取插件模块的Manifest签名信息

方案一:服务器渲染直出到HTML中 服务器收到浏览器的页面请求时,将该页面须要的插件模块的Manifest签名文件进行Merge操做,而后统一输出到HTML中并返回给浏览器。

方案二:经过异步化获取 经过script标签的async和defer功能或AJAX,异步从服务器获取Merge以后的Manifest签名信息集合。


3、远程访问协议

核心系统调起插件模块时,能够经过插件声明的远程访问协议的HTTP地址,进行异步加载。

方案一:Manifest签名文件 在Manifest签名信息中放置插件模块的远程访问协议,好比上传插件模块的签名示例:

{
	// 插件名称
    "name": "upload",
    // 组
    "group": "com.xxx.xxx",
    // 预加载插件模块资源
    "preload": true,
    // 数据规范,要求输入的参数
    "arguments": {
	    // 核心系统提供的上下文对象
        "ctx": {
            "type": "Object",
            "required": true
        },
        // 须要上传的文件信息
        "file": {
            "type": "Object",
            "required": false
        }
    },
    // 远程访问协议
    "entrance": "http://www.a.com/static/plugin-bundles/upload-0.0.1.min.js"
}
复制代码

方案二:异步化接口 + import()

该方案是系统插件模块的远程访问协议不放置在插件模块的Manifest中,而是额外经过异步化接口请求获得远程访问协议。而后经过webpack提供的require.ensure()或esm的import()加载插件资源。

// ctx为核心系统上下文对象
ctx.loadPlugInAdapter = (pluginName, groud) => {
	// 经过接口请求上传插件模块的远程访问协议
    fetchEntrance(pluginName, group).then(url => {
	    // 核心系统执行插件模块
	    ctx.invoke(pluginName, url);
    });
}

// 调起插件模块
ctx.loadPlugInAdapter('upload', 'com.xxx.xxx');
复制代码

最后

架构和框架是独立的,本文仅仅是提出一种架构思路,并且这个架构也在百度的某款用户量很大的复杂前端产品中得以应用。基于这一套弹性架构并结合Vue/React的现代化开发理念,能够很好的完成高复杂度的前端系统。但愿本文能够给大家提供了除微前端以外的构建高弹性前端系统的另一种思路。

相关文章
相关标签/搜索