热门标签 | 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


推荐阅读
  • 在尝试使用C# Windows Forms客户端通过SignalR连接到ASP.NET服务器时,遇到了内部服务器错误(500)。本文将详细探讨问题的原因及解决方案。 ... [详细]
  • 软件工程课堂测试2
    要做一个简单的保存网页界面,首先用jsp写出保存界面,本次界面比较简单,首先是三个提示语,后面是三个输入框,然 ... [详细]
  • 目录一、salt-job管理#job存放数据目录#缓存时间设置#Others二、returns模块配置job数据入库#配置returns返回值信息#mysql安全设置#创建模块相关 ... [详细]
  • 本文介绍了如何使用JavaScript的Fetch API与Express服务器进行交互,涵盖了GET、POST、PUT和DELETE请求的实现,并展示了如何处理JSON响应。 ... [详细]
  • 本文详细介绍了在使用 SmartUpload 组件进行文件上传时,如何正确配置和查找文件保存路径。通过具体的代码示例和步骤说明,帮助开发者快速解决上传路径配置的问题。 ... [详细]
  • Python + Pytest 接口自动化测试中 Token 关联登录的实现方法
    本文将深入探讨 Python 和 Pytest 在接口自动化测试中如何实现 Token 关联登录,内容详尽、逻辑清晰,旨在帮助读者掌握这一关键技能。 ... [详细]
  • 深入解析ESFramework中的AgileTcp组件
    本文详细介绍了ESFramework框架中AgileTcp组件的设计与实现。AgileTcp是ESFramework提供的ITcp接口的高效实现,旨在优化TCP通信的性能和结构清晰度。 ... [详细]
  • 本文探讨了在 SQL Server 中使用 JDBC 插入数据时遇到的问题。通过详细分析代码和数据库配置,提供了解决方案并解释了潜在的原因。 ... [详细]
  • ListView简单使用
    先上效果:主要实现了Listview的绑定和点击事件。项目资源结构如下:先创建一个动物类,用来装载数据:Animal类如下:packagecom.example.simplelis ... [详细]
  • 本文详细探讨了 PHP 中常见的 '未定义索引' 错误,包括其原因、解决方案及最佳实践。通过实例和代码片段,帮助开发者更好地理解和处理这一常见问题。 ... [详细]
  • Python3 中使用 lxml 模块解析 XPath 数据详解
    XPath 是一种用于在 XML 文档中查找信息的路径语言,同样适用于 HTML 文件的搜索。本文将详细介绍如何利用 Python 的 lxml 模块通过 XPath 技术高效地解析和抓取网页数据。 ... [详细]
  • 当unique验证运到图片上传时
    2019独角兽企业重金招聘Python工程师标准model:public$imageFile;publicfunctionrules(){return[[[na ... [详细]
  • 深入理解 .NET 中的中间件
    中间件是插入到应用程序请求处理管道中的组件,用于处理传入的HTTP请求和响应。它在ASP.NET Core中扮演着至关重要的角色,能够灵活地扩展和自定义应用程序的行为。 ... [详细]
  • Ulysses Mac v29:革新文本编辑与写作体验
    探索Ulysses Mac v29,这款先进的纯文本编辑器为Mac用户带来了全新的写作和编辑环境。它不仅具备简洁直观的界面,还融合了Markdown等标记语言的最佳特性,支持多种格式导出,并提供强大的组织和同步功能。 ... [详细]
  • 本文探讨了为何相同的HTTP请求在两台不同操作系统(Windows与Ubuntu)的机器上会分别返回200 OK和429 Too Many Requests的状态码。我们将分析代码、环境差异及可能的影响因素。 ... [详细]
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社区 版权所有