微前端

前言

在软件开发中,逐渐出现了类,模块化,组件化,设计模式等来解耦和拆分咱们的代码,使得代码更易读,易维护。而微前端架构其实也是一种新的思想来帮助咱们更好的拆分一些如今方式没法解决的问题而已。html

什么是微前端

  • 微服务的架构思想在前端的映射和落地
  • 针对复杂且大型的web前端的总体架构和组织结构问题,将单体的前端拆分红更小,更简单的模块,使其能够独立开发,测试和部署,最后将其整合到一块儿。

设计理念

相似与操做系统,将系统的实现与系统的基本操做规则区分开来。将核心功能模块化,划分红几个独立的进程,各自运行,全部的服务进程都运行在不一样的地址空间,让服务各自独立。
设计理念.png前端

落地到浏览器

落地到浏览器.png

如上图所示:微前端落地到浏览器,浏览器将承载一个html页面,在页面中安装,启动相应的服务。web

微前端的核心价值

  • 不受技术栈的约束,可使用任何技术栈来研发独立的模块
  • 自动同步更新,独立开发,部署,测试
  • 将大型web应用拆分,使其更易维护,测试,同时使模块与模块以前更加独立,解决部分紧耦合的问题

微前端带来的问题

  • 部署
  • 服务拆分标准
  • 拆的太多会过于分散,拆的太少会过于密集

拆分方式

我的感受除去微前端的一些技术实现,它主要难点就在于如何拆分应用,在拆分的同时还需结合团队规模等问题:npm

  • 按功能维度拆分
  • 按业务逻辑拆分
  • 按前端路由拆分

应用场景

并非全部应用都适合使用微前端架构,在一个简单的单体应用中使用,反而拔苗助长。
微前端主要应用在大型的互联网应用,该应用可能具有几个特色:系统庞大到不少人去开发,页面数量达到某个量级,在这种状况下可能会致使系统难以维护,代码量逐渐增大,同时也使得协做方面难以管控,包括测试,回归,而且在工程化方面编译显的耗时。总结有如下几点状况bootstrap

  • 业务愈来愈多
  • 组件愈来愈多
  • 文件愈来愈多
  • 打包编译速度愈来愈慢
  • 开发启动速度愈来愈慢
  • 定位文件愈来愈慢

微前端落地的几种实现方式

  • npm:子系统以NPM包的形式发布,打包构建的时候集成到主系统一块儿打包发布。
  • iframe:子工程之间彻底独立,以iframe的方式集成到主系统,这样也能使用不能的技术栈去实现。
  • 使用现有框架:single-spa,Mooa,qiankun

微前端基本原理

基本原理.png

和微服务同样,微前端的独立部署是关键。减小服务间的耦合性,不管前端代码部署在哪里,每一个微前端都有本身持续交付pipeline,进行构建,测试,部署到生产环境中。最后将多个子系统集成到主系统中。设计模式

微前端架构

微前端架构.png

基座工程:数组

  • 路由控制层: 根据url变化来调其不一样的子应用
  • 应用注册: 注册每一个子应用的信息
  • 生命周期管理: 获得每一个应用的生命周期,如安装,卸载等管理
  • 应用加载器:加载对应的子应用
  • 服务发现:获得每一个子应用的服务,入口文件等信息

子应用:promise

  • 子应用之间相互隔离并独立运行
  • Manifest:记录的该应用入口文件,地址等信息
  • 生命周期:暴露生命周期函数工基座工程管理

single-spa的生命周期管理

生命周期.png

  • not_loaded: 还未加载,默认状态
  • load_source_code: 加载模块中
  • not_bootstrapped: 加载完成,可是还未启动
  • bootstrapping: 正在执行的bootstrap生命周期
  • not_mounted: 未装载
  • mounting:正在装载
  • mounted: 已装载
  • updating: 更新
  • unloading: 清楚加载
  • unmounting: 卸载

路由控制层

路由控制层主要监控路由的变动,经过路由变动来控制子系统是否须要加载。子系统路由发生变化首先会有主系统拦截路由变动时间,决定是否加载子系统,若是路由不须要切换子系统,则将该事件交还给子系统处理。
屏幕快照 2020-05-10 下午9.14.38.png浏览器

应用注册

应用注册.png

应用注册其实就是相似于平时的帐号注册,须要填写一些基本信息,而在微前端中所指的应用注册主要是指app名称,以及对应子系统配置文件的url。而在single-spa中registerApplication主要包含三个参数,appName,app,activeWhen缓存

  • appName:app名称
  • activeWhen:返回true则加载应用
  • app:加载子应用

    在主应用中会注册多个子应用,而这些子应用的信息及状态会进行保存和管理。

模块加载器

模块加载器.png

注册对应的子系统,当路由规则匹配到某个子系统的时候会先去加载该子系统的manifest文件来获取该子系统的信息,经过该文件去加载对应的子系统。

single-spa实现思路

实现思路.png

single-spa执行队列有两个入口,一是经过监听路由的变化,二是register函数。
每次触发会先判断是否已启动,若是未启动则执行loadApps去加载须要加载的app。若是已启动则调用performAppChanges函数去mount app。
在执行期间,若是有新的app进来也就是队列发生了变动,会将新的app缓存待执行完当前的再循环执行下一次,这个操做由finishUpAndReturn这个函数内部作判断来完成。

single-spa主要有一下几个模块:

  • applcation : app的注册,生命周期过滤,任务超时等处理函数
  • lifecycles:应用的生命周期管理
  • navigation:监听全局路由变化,执行队列的核心函数。
  • parcels:挂在parcel的核心函数,返回parcel的各个生命周期钩子

下面来大体看一下几个核心函数大体的实现

registerApplication

该函数接收4个参数:

  • appNameOrConfig:app名称或一个包含这4个参数的对象,若是是对象的话则下面三个参数就不用传了
  • appOrLoadAppFn:子系统的bundle代码以及生命周期函数
  • activeWhen:什么时候激活子应用去mount,返回一个boolean
  • customProps:自定义传递给子系统的属性
export function registerApplication(
  appNameOrConfig,
  appOrLoadApp,
  activeWhen,
  customProps
) {
  const registration = sanitizeArguments(
    appNameOrConfig,
    appOrLoadApp,
    activeWhen,
    customProps
  );

  apps.push(
    assign(
      {
        loadErrorTime: null,
        status: NOT_LOADED,
      },
      registration
    )
  );
  
  reroute();
}
  • sanitizeArguments主要为了格式化参数,以支持另一种对象的方式传递
  • 而后将该注册的应用添加到一个数组中,并给该应用一个初始化的状态为NOT_LOADED
  • 最后执行reroute函数

路由监听

function urlReroute() {
  reroute([], arguments);
}

window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
  • 经过监听hashChange和popstate事件来执行reroute函数
const originalAddEventListener = window.addEventListener;

window.addEventListener = function(eventName, fn) {
    if (typeof fn === "function") {
      if (
        routingEventsListeningTo.indexOf(eventName) >= 0 &&
        !find(capturedEventListeners[eventName], listener => listener === fn)
      ) {
        capturedEventListeners[eventName].push(fn);
        return;
      }
    }

    return originalAddEventListener.apply(this, arguments);
  };
  • 改写addEventListener监听函数,使每次路由变化事件先由single-spa接管,而后再执行原监听函数交还给系统路由。
  • 这里会先将监听函数保存在capturedEventListeners[eventName]数组中,在调用完reroute以后再去执行capturedEventListeners这个队列里面的事件函数,这样可以保证single-spa每次先执行

reroute

reroute.png

let appChangeUnderway = false,
peopleWaitingOnAppChange = [];

function reroute(pendingPromises = [], eventArguments) {
  .......
}

函数接收两个参数:

  • pendingPromises:执行reroute期间,再次调用reroute函数所产生的app
  • eventArguments:路由监听事件的event参数

下面都是reroute函数里面的代码:

if (appChangeUnderway) {
    return new Promise((resolve, reject) => {
      peopleWaitingOnAppChange.push({
        resolve,
        reject,
        eventArguments
      });
    });
  }
  • 若是reroute在执行期间再次被调用,则会先将数据缓存到peopleWaitingOnAppChange当中,当reroute当前次调用结束后递归执行,以保证执行顺序
let wasNoOp = true;

  if (isStarted()) {
    appChangeUnderway = true;
    return performAppChanges();
  } else {
    return loadApps();
  }
  • wasNoOp: 等于true的时候表示app没有发生变动,也就是没有发生状态的变化。
  • isStarted: 判断是否已经启动
  • 若是启动了则执行performAppChanges,并将appChangeUnderway=true
  • 不然执行loadApps
function loadApps() {
    return Promise.resolve().then(() => {
      const loadPromises = getAppsToLoad().map(toLoadPromise);

      if (loadPromises.length > 0) {
        wasNoOp = false;
      }

      return (
        Promise.all(loadPromises)
          .then(callAllEventListeners)
          .then(() => [])
          .catch(err => {
            callAllEventListeners();
            throw err;
          })
      );
    });
  }
  • getAppsToLoad:根据一些条件来筛选出须要加载的app
  • toLoadPromise:将每一个app都封装到一个promise中,返回一个数组
  • 经过promise.All执行每一个promise,主要判断每一个app是否有bootstrap,mount,unmount生命周期,有则将app状态修改成NOT_BOOTSTRAPPED,没有则更改成SKIP_BECAUSE_BROKEN
  • 执行callAllEventListeners函数,来调用拦截下来的原生事件。
function performAppChanges() {
    return Promise.resolve().then(() => {
      const unloadPromises = getAppsToUnload().map(toUnloadPromise);

      const unmountUnloadPromises = getAppsToUnmount()
        .map(toUnmountPromise)
        .map(unmountPromise => unmountPromise.then(toUnloadPromise));

      const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);
      if (allUnmountPromises.length > 0) {
        wasNoOp = false;
      }

      const unmountAllPromise = Promise.all(allUnmountPromises);

      const appsToLoad = getAppsToLoad();

      /* We load and bootstrap apps while other apps are unmounting, but we
       * wait to mount the app until all apps are finishing unmounting
       */
      const loadThenMountPromises = appsToLoad.map(app => {
        return toLoadPromise(app)
          .then(toBootstrapPromise)
          .then(app => {
            return unmountAllPromise.then(() => toMountPromise(app));
          });
      });
      if (loadThenMountPromises.length > 0) {
        wasNoOp = false;
      }

      /* These are the apps that are already bootstrapped and just need
       * to be mounted. They each wait for all unmounting apps to finish up
       * before they mount.
       */
      const mountPromises = getAppsToMount()
        .filter(appToMount => appsToLoad.indexOf(appToMount) < 0)
        .map(appToMount => {
          return toBootstrapPromise(appToMount)
            .then(() => unmountAllPromise)
            .then(() => toMountPromise(appToMount));
        });
      if (mountPromises.length > 0) {
        wasNoOp = false;
      }
      return unmountAllPromise
        .catch(err => {
          callAllEventListeners();
          throw err;
        })
        .then(() => {
          /* Now that the apps that needed to be unmounted are unmounted, their DOM navigation
           * events (like hashchange or popstate) should have been cleaned up. So it's safe
           * to let the remaining captured event listeners to handle about the DOM event.
           */
          callAllEventListeners();

          return Promise.all(loadThenMountPromises.concat(mountPromises))
            .catch(err => {
              pendingPromises.forEach(promise => promise.reject(err));
              throw err;
            })
            .then(finishUpAndReturn);
        });
    });
  }
  • unloadPromises: 先拿到须要unload的app,而后封装到一个执行unload的promise中
  • unmountUnloadPromise: 拿到须要unmount的app,封装到一个promise中,再将unmountPromise的结果封装到uploadPromise中
  • allUnmountPromises: 将unmount和unload合并
  • unmountAllPromise: 执行allUnmountPromises,这里不等待执行完成直接执行下面代码
  • getAppsToLoad:加载须要load的app
  • loadThenMountPromises: 将须要load的app封装到loadPromise中,执行该promise,完成后封装到BootstrapPromise中,再接着执行BootStrapPromise,最后等上面unmountAllPromise的执行完毕后将app封装到toMountPromise中去
  • mountPromises:拿到全部须要mount的app,返回的是一个promise
  • 最后在unmountAllPromise执行完后调用callAllEventListeners,而后去挂在app,mount完后执行finishUpAndReturn来看队列中是否还有等待的任务,递归执行reroute

总结

能不用则不用!!!!!

相关文章
相关标签/搜索