前端微服务

介绍

前端领域一直在不断的发展,传统的 jQuery + Backbone + Bootstrap MVC 解决方案逐渐被 Angular、Ember、React、Vue 等 MVVM 框架替代,前后端分离和前端组件化的思想已经达到了顶峰。

在传统的系统中,通常会有一个站点,所有的业务都在这个站点上,随着业务复杂度的上升,打包的体积会迅速变大,发布时也会变慢。为了适应业务的复杂度往往需要更多的开发者、更细粒度的团队组织。分组开发时大家的模块解耦到各自完成,上线时糅合在一起运行,这时会产生出层出不穷的分支合并、代码回滚等,都会造成合作效率的骤降。

所有的业务聚合在一起还会造成频繁发布,每个业务都会产生一定的更新频率,每个业务都会导致整个项目一起升级、测试和上线,发布频率的总和会非常高、非常频繁。

以如此高的上线频率、版本迭代速度来看,开发者也极难追溯哪个版本对应哪个改动。

框架层出不穷,版本更是迭代不穷,难免会出现前端项目技术栈不统一、所用框架版本不统一。

比如:有的项目使用了 Vue,有的项目使用了 React,Vue 的项目已经稳定运行,若是没有新的功能加入,但却需要结合到其它的项目中时,对 Vue 的重构的成本会很高,这时就需要去兼容不同类型的前端框架。

一家大的公司也可能有很多的应用,这些应用代表了公司的组织架构,在用户眼里他们是一个产品。

聚合成为了一个技术趋势,体现在前端的聚合就是微服务化架构

那么问题来了,什么是前端微服务?

一个集成了不同业务的大型应用,将应用拆分成多个模块,每一个模块可以单独的开发、调试并上线,最后由应用提供统一的入口。

有什么优势?
  • 每个模块都是一个独立的个体,如果有某个模块出现问题了,不会导致整个应用挂掉。

  • 由于每个模块可以单独上线,因此上线会更快,有利于更新迭代。

  • 由于有了服务注册的功能,因此页面都可以通过配置化的方式来动态加载,对于功能的新增、回滚特别方便。

  • 框架无关 (每个模块都可以根据实际需求选择不同的框架)

会遇到什么样的难题?
  • 如何将不同业务模块集中到一个大的应用上,统一对外开放?

  • 如何给不同用户赋予权限,让其能够访问平台的特定业务模块,同时禁止其访问无权限的业务模块?

  • 如何快速接入新的模块,并对模块进行版本管理,保证功能同步?

实现方案

我们已经知道了什么是微服务,那我们该如何具体去实现呢?

独立开发

每个模块单独开发,不需要和其他模块保持完全一致的组织模型,也可以选择适合自己的框架。

独立开发的优点:

  • 业务模块分布式开发,代码仓库更易管理。

  • 模块内的代码高内聚,更专注于业务;

  • 新模块的接入不需要修改已有模块,不会影响其他模块的功能;

  • 业务模块移植性强,可单独部署,也可整合到大平台下。

独立部署

单体应用的一大问题是发布非常慢,导致每天上线不了几次,风险也很大。当业务多的时候,不管有多少更新都要一起发布。

独立部署解决了以上的问题,每次只需要发布对应模块的应用即可。

服务发现

单体应用拆分成多个模块之后,一个项目里的方法分开部署了,那主程序怎么知道有哪些模块,以及各个模块对应的配置信息( js / css 等配置信息)呢。

查找配置的模块信息的过程,就叫做服务发现

需要有个统一的注册机构,把提供服务的各个应用都查到。

假如:我们有三个不同的业务应用,用户如果想使用这三个业务,如何去切换这三个应用呢?

服务发现

当用户登录时,我们先调用接口根据用户的身份、权限来返回不同的模块配置信息,当用户点击对应的模块后,通过 nginx 配置反向代理,来进行路由分发,从而实现前端微服务。

具体怎么实现呢?

每个模块将自己的全量路由路径传入给主程序 ,而在主程序启动时,主程序会调用接口从后端拉取当前登录用户有权限的路由路径,当访问某模块的路由时,会与有权限的路由路径进行比对,比对失败的路由路径会自动导向无权限的页面视图。

至于路由的权限维护,可以做一个可视化配置路由的管理页面,权限的细化程度根据自己的业务情况自定义即可。

下面是通过接口返回的一个模块的配置信息,path 代表模块对应的路由地址,也就是说,当前端匹配到了路由为 /home 的时候,就会加载对应的 js 和 css 文件,并执行 js 文件渲染模块内容。

[{
    name: 'home',
    path: '/home',
    js: 'https://XXX/home.js',
    css: 'https:XXX/home.css'
}]
动态加载

当前端匹配到了一个路由,需要确定当前路由路径对应的是哪一个模块,若对应的模块尚未注入路由信息,需要动态加载模块资源包,待加载并执行了对应的 js 脚本资源包后,再继续执行后续的渲染逻辑。

模块的资源包可以有多种形式的打包方式,如 AMD、Commonjs、UMD 等。

  1. CommonJs

首先实现一个简单的模块功能:

import React from 'react';
import ReactDom from 'react-dom';


function App() {
 return React.createElement('div', null, 'hello world');
}


export const render = container => {
  ReactDom.render(React.createElement(App), container);
};

这段代码在页面上显示了 hello world ,并导出了 render 方法。

// 全局模块管理
const modules = {};

function loadModule() {
 const currentConfig = {
        name: 'home',
        path: '/home',
        js: './dist/main.js',
      };
 
 const { name, path, js } = currentConfig;
 
      modules[name] = {
        exports: {},
      };
 const ajax = new XMLHttpRequest();
      ajax.open('get', js);
      ajax.onload = function(event) {
 new Function('module', 'exports', this.responseText)(
       modules[name],
       modules[name].exports,
     );
 
        modules[name].exports.render(document.getElementById('app'));
      };
      ajax.send();
}

loadModule();

上面这段代码主要做了如下工作:

  1. 主程序加载的时候,根据配置信息,创建模块的 module 信息。

  2. 通过 xhr 加载拿到模块的 js 代码,并通过 new Function 的方式,将我们的模块 module 信息传进去执行 js 代码,这样 js 代码导出的内容就会挂载到 modules[name] 上。

  3. 调用模块导出的 render 方法来渲染模块内容。

注意:

  • 导出的模块必须选用 CommonJs 打包类型,否则无法将我们自己的 module 传进去。

  • 加载模块的时候使用 xhr 请求,这样才能拿到代码的 source code。

  1. AMD

AMD 语法:

define('module', [...deps], function () {
    ...
});

首先,定义一个简单的模块:

import React from 'react';
import ReactDom from 'react-dom';

function App() {
 return React.createElement('div', null, 'hello world');
}

const render = container => {
  ReactDom.render(React.createElement(App), container);
};

window.defineModule('home', {
  render,
});

通过 window.defineModule 这个方法来定义自己的模块。

const namespace = Symbol('namespace');
window[namespace] = {};

function defineModule(name, exports) {
 window[namespace][name] = exports;
}

function getModule(name) {
 return window[namespace][name];
}

window.defineModule = defineModule;

function loadModule() {
 const currentConfig = {
        name: 'home',
        path: '/home',
        js: './dist/main.js',
      };
 
 const { name, path, js } = currentConfig;
 
 const scriptEle = document.createElement('script');
      scriptEle.src = js;
      scriptEle.onload = () => {
 const module = getModule(name);
 module.render(document.getElementById('app'));
      };
 
 document.body.appendChild(scriptEle);
}

loadModule();

主程序通过这种方式定义模块,其他模块就可以通过依赖项注入的方式来使用该模块。

实现方法:

  1. 主程序通过定义一个 defineModule 方法,并将其挂载在 window 上来实现模块定义。

  2. 业务模块在开发的时候,通过 window.defineModule 方法来定义自己的模块,并将自己的 render 方法导出。

  3. 主程序在加载模块的时候,通过正常的创建 script 来加载。在加载完成后,根据模块的配置信息可以拿到模块的导出内容。

  4. 调用模块导出的 render 方法来渲染模块内容。

生命周期管理

在切换各模块时,上一个模块的 DOM 会被替换,但相关的事件并未正确清除。比如使用 React 框架的模块,当我们替换掉 DOM 内容时,并未正确触发 React 组件的 UnMount 事件。

所以,我们需要为模块添加 destroy 和 ready 接口:

class App {

  ready() {
    // 在当前模块切换进来时调用
  }

  destroy() {
    // 在当前模块切换出去时调用
  }

}

在切换模块时,需自动调用上一个模块的销毁接口,然后在渲染新的模块后,再自动调用当前模块的准备接口。

总结

当一个应用非常庞大且复杂时,可以考虑使用前端微服务这种方式来提高性能。

  1. 大的应用可以拆分成多个子模块独立开发。

  2. 多个独立的子模块可以单独发布。

  3. 可以通过接口的方式实现服务发现,可以根据用户不同的权限动态返回配置信息。

  4. 动态加载模块可以使用 new Function + CommonJs 和 AMD 的实现,具体哪种方法取决于个人。

  5. 在模块销毁的时候处理数据,在模块刚进入的时候做一下准备工作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容