热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Garfish微前端实现原理

Garfish微前端实现原理-近期有落地一些微前端业务场景,也遇到一些问题,看了下他们的实现发现目前无论是garfish还是qiankun对于这一块的实现都在不断的完善中,但是qi

近期有落地一些微前端业务场景,也遇到一些问题,看了下他们的实现发现目前无论是garfish还是qiankun对于这一块的实现都在不断的完善中,但是qiankun我也看了一下他们的实现,在一些case的处理上较garfish存在一定不足。所以本次是针对garfish的实现分析。下面会从资源加载入口,资源解析,沙箱环境,代码执行四大块进行分析,了解微前端的主要实现逻辑。

下文中对比qiankun的版本为2.4.0,如有不正确的还请评论指正。

文中涉及大量的代码分析,希望能够从实现层面更加直接的看出实现的逻辑,而不是通过几张图来解释概念。

如何解析资源(html入口)

获取资源内容

根据提供的url作为入口文件加载资源。加载的实现很简单,通过fetch拿到资源内容,如果是html资源入口会进行标签的序列化和相关处理,这个后面会看到。如果是js文件则会直接实例化一个js资源类,目的是保存加载到资源的类型,大小,代码字符串等基本信息。并会尝试缓存加载的资源。

下面是加载各类资源的实现,比如获取html文件、js文件、css文件。在整个流程中,这个方法会被多次使用来加载资源。

  // 加载任意的资源,但是都是会转为 string

  load(url: string, config?: RequestInit) {

 // 移除了部分代码只保留说明性的部分

      cOnfig= { mode: 'cors', ...config, ...requestConfig };

      this.loadings[url] = fetch(url, config)

        .then((res) => {

          // 响应码大于 400 的当做错误

          if (res.status >= 400) {

            error(`load failed with status "${res.status}"`);

          }

          const type = res.headers.get('content-type');

          return res.text().then((code) => ({ code, type, res }));

        })

        .then(({ code, type, res }) => {

          let manager;

          const blob = new Blob([code]);

          const size = Number(blob.size);

          const ft = parseContentType(type);

          // 对加载的资源进行分类处理

          // 下方new 的几个实例的目的都是保存代码块字符串和资源类型等一些基本信息

          if (isJs(ft) || /.js/.test(res.url)) {

            manager = new JsResource({ url, code, size, attributes: [] });

          } else if (isHtml(ft) || /.html/.test(res.url)) {

            manager = new HtmlResource({ url, code, size });

          } else if (isCss(ft) || /.css/.test(res.url)) {

            manager = new CssResource({ url, code, size });

          } else {

            error(`Invalid resource type "${type}"`);

          }



        // 所有的请求会存在一个promise map来维护,加载完成后清空

          this.loadings[url] = null;

          currentSize += isNaN(size) ? 0 : size;

          if (!isOverCapacity(currentSize) || this.forceCaches.has(url)) {

            // 尝试缓存加载的资源

            this.caches[url] = manager;

          }

          return manager;

        })

        .catch((e) => {

          const message = e instanceof Error ? e.message : String(e);

          error(`${message}, url: "${url}"`);

        });

      return this.loadings[url];

    }

  }

在html入口被加载的时候,这个方法便帮助我们获取到了入口html文件内容,接下载需要解析这个html文件。

序列化DOM树

因为html入口比较特殊,下面单独对这部分进行分析。如何解析并处理html文件的呢。首先我们在上一步获得了资源的文件内容。下一步是对加载的html资源进行ast解析,结构化dom,以便提取不同类型的标签内容。这里使用到了 himalaya 这个辅助库。在线尝试地址https://jew.ski/himalaya/, 解析内容格式如下,将dom文本解析文json结构。

结构化后进行深度优先遍历把link,style,script标签提取出来

// 调用方式

this.queryVNodesByTagNames(['link', 'style', 'script']) 



// 具体实现

// 实现代码截取 其中this.ast就是上面演示的parse的结果

private queryVNodesByTagNames(tagNames: Array) {

    const res: Record> = {};

    for (const tagName of tagNames) {

      res[tagName] = [];

    }

    const traverse = (vnode: VNode | VText) => {

      if (vnode.type === 'element') {

        const { tagName, children } = vnode;

        if (tagNames.indexOf(tagName) > -1) {

          res[tagName].push(vnode);

        }

        children.forEach((vnode) => traverse(vnode));

      }

    };

    this.ast.forEach((vnode) => traverse(vnode));

    return res;

  }

由于当前各个框架的实现基本都是有js生成dom并挂载到指定的元素上,因此这里只要把这三种加载资源的标签提取出来基本就完成了页面的加载。当然还需要配合微前端的加载方式改造下子系统入口,让挂载函数指向主应用提供的dom。至此我们完成了基本资源的提取。

构建运行环境

接下来就是实例化当前子应用了。我们需要子应用的运行时独立的环境不影响主应用的代码。因此子应用需要在指定的沙箱内运行,这也是微前端实现的核心部分。首先看下实例化子应用的代码

  // 每个子引用都会通过这个方法来实例化

  private createApp(

    appInfo: AppInfo,

    opts: LoadAppOptions,

    manager: HtmlResource,

    isHtmlMode: boolean,

  ) {

    const run = (resources: ResourceModules) => {

      // 这里是获取沙箱环境

      let AppCtor = opts.sandbox.snapshot ? SnapshotApp : App;

      if (!window.Proxy) {

        warn(

          'Since proxy is not supported, the sandbox is downgraded to snapshot sandbox',

        );

        AppCtor = SnapshotApp;

      }

      // 将app在沙箱内实例化以保证独立运行

      const app = new AppCtor(

        this.context,

        appInfo,

        opts,

        manager,

        resources, // 提供的html入口

        isHtmlMode,

      );

      this.context.emit(CREATE_APP, app);

      return app;

    };



    // 如果是 html, 就需要加载用到的资源

    const mjs = Promise.all(this.takeJsResources(manager as HtmlResource));

    const mlink = Promise.all(this.takeLinkResources(manager as HtmlResource));

    return Promise.all([mjs, mlink]).then(([js, link]) => run({ js, link }));

  }

这里只需要大致看一下一个子应用的大致创建和加载流程,基本就是一个上下文,一些资源信息。具体细节后续可以看看源码串下整体流程。接下来看下应用的运行上下文——沙箱的实现

代码的执行

在获取资源内容一节我们已经对script资源的获取进行了解析。但是这个部分代码具体是如何在沙箱环境执行的呢,在实例化app时会有一个方法execScript,实现如下,其中的code参数就是我们script获取的代码字符串。

  execScript(

    code: string,

    url?: string,

    options?: { async?: boolean; noEntry?: boolean },

  ) {

    try {

      (this.sandbox as Sandbox).execScript(code, url, options);

    } catch (e) {

      this.context.emit(ERROR_COMPILE_APP, this, e);

      throw e;

    }

  }

可以看到这部分的实现调用了沙箱中的execScript,这里先说下前置知识,基本所有的沙箱环境的代码执行都会使用with这个语法来处理代码的执行上下文,并且有着天然的优势。在vue中处理模板中访问变量this关键字的方式也采用了这个方式。

接下来看下具体的实现。

  execScript(code: string, url = '', options?: ExecScriptOptions) {

    // 省略一些次要代码,保留核心逻辑

    // 这里的context就是我们上面创建的代理window

    const cOntext= this.context;

    const refs = { url, code, context };



    // 这一步是创建一个script标签如果url存在,src为给定的url,否则code放到标签体内

    // 返回值为清空这个script 元素的引用函数

    const revertCurrentScript = setDocCurrentScript(this, code, url, async);



    try {

      const sourceUrl = url ? `//# sourceURL=${url}\n` : '';

      let code = `${refs.code}\n${sourceUrl}`;



      if (this.options.openSandbox) {

        // 如果是非严格模式则需要with包裹保证内部代码执行的上下文为代理后的window

        code = !this.options.useStrict

          ? `with(window) {;${this.attachedCode + code}}`

          : code;

        // 这个函数构造了代码执行环境

        evalWithEnv(code, {

          window: refs.context,

          ...this.overrideContext.overrides,

          unstable_sandbox: this,

        });

      } 

    } 



    revertCurrentScript();



    if (noEntry) {

      refs.context.module = this.overrideContext.overrides.module;

      refs.context.exports = context.module.exports;

    }

  }

接下来看下evalWithEnv的实现逻辑,这个函数的执行逻辑也很简单,就是把我们的代码内容放到一个构造出来的上下文中执行,上下文中的window,document等对象都是我们重写和代理过的,因此保证了环境的隔离。

export function internFunc(internalizeString) {

  const temporaryOb = {};

  temporaryOb[internalizeString] = true;

  return Object.keys(temporaryOb)[0];

}



export function evalWithEnv(code: string, params: Record) {

  const keys = Object.keys(params);

  // 不可使用随机值,否则无法作为常量字符串复用

  // 将我们代理过的全局变量挂到一个指定属性下

  const randomValKey = '__garfish__exec_temporary__';



  const vals = keys.map((k) => `window.${randomValKey}.${k}`);

  try {

    rawWindow[randomValKey] = params;

    // 数组首尾元素中间就是我们代码实际运行的位置

    // 可以看到首先绑定代理过的window作为上下文,然后参数指定了我们代理和重写的对象,

    // 这样代码内获取注入document对象时其实已经是代理过的了

    const evalInfo = [

      `;(function(${keys.join(',')}){`,

      `\n}).call(${vals[0]},${vals.join(',')});`,

    ];

    const internalizeString = internFunc(evalInfo[0] + code + evalInfo[1]);

    // (0, eval) 这个表达式会让 eval 在全局作用域下执行

    (0, eval)(internalizeString);

  } finally {

    delete rawWindow[randomValKey];

  }

}

到这里我们知道代码的执行环境使我们代理的window和重写的方法构造的,配合上面的with语句的特性则可以解决变量提升相关的问题。到这里我们完成了代码从加载到执行的路径分析。

结语

上面的分析大多为了讲解基本思路,阐述微前端的基本实现思想,在实际的执行过程中会有很多其他逻辑的判断以及加载优化,如果有兴趣的可以参考源码实现。目前garfish也在不断的完善过程中,因为很多场景需要用户验证,开发能考虑到的业务case毕竟有限,在写这篇文章的时候每天都会有近百个commit提交更新过来。可以看到优化场景还是挺多的。总的来说微前端确实很大程度上解决了项目迁移难,技术升级慢和难维护项目的问题。如果有上述痛点是可以尝试一下的。

Garfish 开源链接:https://github.com/modern-js-dev/garfish


推荐阅读
  • 深入理解:AJAX学习指南
    本文详细探讨了AJAX的基本概念、工作原理及其在现代Web开发中的应用,旨在为初学者提供全面的学习资料。 ... [详细]
  • spring boot使用jetty无法启动 ... [详细]
  • Maven + Spring + MyBatis + MySQL 环境搭建与实例解析
    本文详细介绍如何使用MySQL数据库进行环境搭建,包括创建数据库表并插入示例数据。随后,逐步指导如何配置Maven项目,整合Spring框架与MyBatis,实现高效的数据访问。 ... [详细]
  • 本文探讨了如何通过Service Locator模式来简化和优化在B/S架构中的服务命名访问,特别是对于需要频繁访问的服务,如JNDI和XMLNS。该模式通过缓存机制减少了重复查找的成本,并提供了对多种服务的统一访问接口。 ... [详细]
  • 本文将从基础概念入手,详细探讨SpringMVC框架中DispatcherServlet如何通过HandlerMapping进行请求分发,以及其背后的源码实现细节。 ... [详细]
  • publicclassBindActionextendsActionSupport{privateStringproString;privateStringcitString; ... [详细]
  • 本文介绍了SELinux的两种主要工作模式——强制模式和宽容模式,并提供了如何在CentOS 7中正确启用和配置SELinux的方法,以及在遇到登录问题时的解决策略。 ... [详细]
  • 调试利器SSH隧道
    在开发微信公众号或小程序的时候,由于微信平台规则的限制,部分接口需要通过线上域名才能正常访问。但我们一般都会在本地开发,因为这能快速的看到 ... [详细]
  • 近期尝试从www.hub.sciverse.com网站通过编程手段获取数据时遇到问题,起初尝试使用WebBrowser控件进行数据抓取,但发现使用GET方法翻页时,返回的HTML代码始终相同。进一步探究后了解到,该网站的数据是通过Ajax异步加载的,可通过HTTP查看详细的JSON响应。 ... [详细]
  • 问题场景用Java进行web开发过程当中,当遇到很多很多个字段的实体时,最苦恼的莫过于编辑字段的查看和修改界面,发现2个页面存在很多重复信息,能不能写一遍?有没有轮子用都不如自己造。解决方式笔者根据自 ... [详细]
  • 本文详细介绍了如何利用 Bootstrap Table 实现数据展示与操作,包括数据加载、表格配置及前后端交互等关键步骤。 ... [详细]
  • 处理Android EditText中数字输入与parseInt方法
    本文探讨了如何在Android应用中从EditText组件安全地获取并解析用户输入的数字,特别是用于设置端口号的情况。通过示例代码和异常处理策略,展示了有效的方法来避免因非法输入导致的应用崩溃。 ... [详细]
  • 在尝试加载支持推送通知的iOS应用程序的Ad Hoc构建时,遇到了‘no valid aps-environment entitlement found for application’的错误提示。本文将探讨此错误的原因及多种可能的解决方案。 ... [详细]
  • 本文详细介绍了在 CentOS 系统中如何创建和管理 SWAP 分区,包括临时创建交换文件、永久性增加交换空间的方法,以及如何手动释放内存缓存。 ... [详细]
  • 在1995年,Simon Plouffe 发现了一种特殊的求和方法来表示某些常数。两年后,Bailey 和 Borwein 在他们的论文中发表了这一发现,这种方法被命名为 Bailey-Borwein-Plouffe (BBP) 公式。该问题要求计算圆周率 π 的第 n 个十六进制数字。 ... [详细]
author-avatar
此人已死_0824
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有