美文网首页
微前端 -- 乾坤(二)源码篇

微前端 -- 乾坤(二)源码篇

作者: nucky_lee | 来源:发表于2022-04-13 18:07 被阅读0次

    HTML Entry

    HTML Entry 是由 import-html-entry 库实现的,通过 http 请求加载指定地址的首屏内容即 html 页面,然后解析这个 html 模版得到 template, scripts , entry, styles

    { 
      template: 经过处理的脚本,link、script 标签都被注释掉了, 
      scripts: [脚本的http地址 或者 { async: true, src: xx } 或者 代码块], 
      styles: [样式的http地址], 
      entry: 入口脚本的地址,要不是标有 entry 的 script 的 src,要不就是最后一个 script 标签的 src
    }
    

    然后远程加载 styles 中的样式内容,将 template 模版中注释掉的 link 标签替换为相应的 style 元素。

    然后向外暴露一个 Promise 对象

    { 
      // template 是 link 替换为 style 后的 templatetemplate: embedHTML,
      // 静态资源地址assetPublicPath,
      // 获取外部脚本,最终得到所有脚本的代码内容
      getExternalScripts: () => getExternalScripts(scripts, fetch),
        // 获取外部样式文件的内容
        getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
        // 脚本执行器,让 JS 代码(scripts)在指定 上下文 中运行
        execScripts: (proxy, strictGlobal) => {
          if (!scripts.length) {
            return Promise.resolve();
          }
          return execScripts(entry, scripts, proxy, { fetch, strictGlobal });
        }
    }
    

    qiankun 就用了这个对象中的 template、assetPublicPath 和 execScripts 三项,

    将 template 通过 DOM 操作添加到主应用中,执行 execScripts 方法得到微应用导出的生命周期方法,并且还顺便解决了 JS 全局污染的问题,因为执行 execScripts 方法的时候可以通过 proxy 参数指定 JS 的执行上下文。

    这就是 HTML Entry 的原理。

    乾坤是怎样加载资源的?

    加载微应用的时候要获取微应用的js、css、html等资源,qiankun使用了依赖库import-html-entry

    loadApp中执行了这一行代码:

    const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);

    这里的importEntry来自于一个依赖库import-html-entry

    importEntry

    https://segmentfault.com/a/1190000022275991

    https://juejin.cn/post/6885212507837825038

    功能

    l 加载css/js资源,并且将加载的资源嵌入到html中去;

    l 获取scripts资源上的exports对象

    类型

    l Entry(参数entry的类型,必传)

    // 代码片段1,所属文件:

    src/index.js
    
    export function importEntry(entry, opts = {}) {
      const { 
        fetch = defaultFetch, 
        getTemplate = defaultGetTemplate, 
        postProcessTemplate } = opts;
      const getPublicPath = opts.getPublicPath || opts.getDomain || defaultGetPublicPath;
      // 省略一些不太关键的代码...
      if (typeof entry === 'string') {
        return importHTML(entry, {
          fetch,getPublicPath,getTemplate,postProcessTemplate,
        });
      } // 此处省略了许多代码... 占位1}
    
    importHTML
    /**
    * 加载指定地址的首屏内容
    * return Promise<{ 
        // template 是 link 替换为 style 后的 template
        template: embedHTML,
        // 静态资源地址
        assetPublicPath,
        // 获取外部脚本,最终得到所有脚本的代码内容
        getExternalScripts: () => getExternalScripts(scripts, fetch),
        // 获取外部样式文件的内容
        getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
        // 脚本执行器,让 JS 代码(scripts)在指定 上下文 中运行
        execScripts: (proxy, strictGlobal) => {
          if (!scripts.length) {
            return Promise.resolve();
          }
          return execScripts(entry, scripts, proxy, { fetch, strictGlobal });
        },
    }> */
    
    export default function importHTML(url, opts = {}) {
      // 三个默认的方法
      let fetch = defaultFetch;
      let getPublicPath = defaultGetPublicPath;
      let getTemplate = defaultGetTemplate;
      // 通过 fetch 方法请求 url,这也就是 qiankun 为什么要求你的微应用要支持跨域的原因
      return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url)
        // response.text() 是一个 html 模版
      .then(response => response.text())
      .then(html => {
        // 获取静态资源地址
        const assetPublicPath = getPublicPath(url);
      /**
      * 从 html 模版中解析出外部脚本的地址或者内联脚本的代码块 和 link 标签的地址
      * {
      * template: 经过处理的脚本,link、script 标签都被注释掉了,
      * scripts: [脚本的http地址 或者 { async: true, src: xx } 或者 代码块],
      * styles: [样式的http地址],
      * entry: 入口脚本的地址,要不是标有 entry 的 script 的 src,要不就是最后一个 script 标签的 src
      * }
    */
    
    const { template, scripts, entry, styles } = processTpl(getTemplate(html), assetPublicPath);
    
    // getEmbedHTML 方法通过 fetch 远程加载所有的外部样式,然后将对应的 link 注释标签替换为 style,即外部样式替换为内联样式,然后返回 embedHTML,即处理过后的 HTML 模版return 
    
    getEmbedHTML(template, styles, { fetch }).then(embedHTML => ({
    
      // template 是 link 替换为 style 后的 template
      template: embedHTML,
      // 静态资源地址
      assetPublicPath,
    
      // 获取外部脚本,最终得到所有脚本的代码内容
      getExternalScripts: () => getExternalScripts(scripts, fetch),
    
      // 获取外部样式文件的内容
      getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
    
      // 脚本执行器,让 JS 代码(scripts)在指定 上下文 中运行
      execScripts: (proxy, strictGlobal) => {
    
      if (!scripts.length) {
        return Promise.resolve();
      }
    
      return execScripts(entry, scripts, proxy, { fetch, strictGlobal });
      },
      }));
    }));
    }
    
    

    例子:

    这是fetch到的html

    "<!DOCTYPE html>
    
    <html lang="en">
    
     <head>
    
     <meta charset="utf-8" />
    
     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    
     <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    
     <link rel="icon" href="/base/favicon.ico" />
    
     <title>micro-app-base</title>
    
     <link href="/base/static/js/about.js" rel="prefetch"><link href="/base/static/js/app.js" rel="preload" as="script"><link href="/base/static/js/chunk-vendors.js" rel="preload" as="script"></head>
    
     <body>
    
     <noscript>
    
     <strong
    
     >We're sorry but micro-app-base doesn't work properly without JavaScript enabled. Please
    
     enable it to continue.</strong
    
     >
    
     </noscript>
    
     <div id="microAppBase"></div>
    
     <!-- built files will be auto injected -->
    
     <script type="text/javascript" src="/base/static/js/chunk-vendors.js"></script><script type="text/javascript" src="/base/static/js/app.js"></script></body>
    
     <link rel="stylesheet" href="[https://css.znlh.work/index.css](https://css.znlh.work/index.css)"></link>
    
    <style>
    
     .hidden {
    
     display: none;
    
     }
    
    </style>
    
    </html>
    
    "
    

    经过processTpl处理后:

    template: 经过处理的脚本,link、script 标签都被注释掉了

    "<!DOCTYPE html>
    
    <html lang="en">
    
     <head>
    
     <meta charset="utf-8" />
    
     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    
     <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    
     <link rel="icon" href="/base/favicon.ico" />
    
     <title>micro-app-base</title>
    
     <!-- prefetch/preload link /base/static/js/about.js replaced by import-html-entry --><!-- prefetch/preload link /base/static/js/app.js replaced by import-html-entry --><!-- prefetch/preload link /base/static/js/chunk-vendors.js replaced by import-html-entry --></head>
    
     <body>
    
     <noscript>
    
     <strong
    
     >We're sorry but micro-app-base doesn't work properly without JavaScript enabled. Please
    
     enable it to continue.</strong
    
     >
    
     </noscript>
    
     <div id="microAppBase"></div>
    
     <!-- script [http://localhost:3071/base/static/js/chunk-vendors.js](http://localhost:3071/base/static/js/chunk-vendors.js) replaced by import-html-entry --><!-- script [http://localhost:3071/base/static/js/app.js](http://localhost:3071/base/static/js/app.js) replaced by import-html-entry --></body>
    
     <!-- link [https://css.znlh.work/index.css](https://css.znlh.work/index.css) replaced by import-html-entry --></link>
    
    <style>
    
     .hidden {
    
     display: none;
    
     }
    
    </style>
    
    </html>"
    

    scripts: [脚本的http地址 或者 { async: true, src: xx } 或者 代码块]

    ['http://localhost:3071/base/static/js/chunk-vendors.js', 'http://localhost:3071/base/static/js/app.js']

    styles: [样式的http地址]

    ['https://res.wx.qq.com/open/libs/weui/2.4.1/weui.min.css']

    entry: 入口脚本的地址

    "http://localhost:3071/base/static/js/app.js"

    getEmbedHTML

    外部样式转换成内联样式

    将html中的
    <link rel="stylesheet" href="[https://css.znlh.work/index.css](https://css.znlh.work/index.css)"></link>

    转换为:

    <style>/* [https://css.znlh.work/index.css](https://css.znlh.work/index.css) */@charset "UTF-8";:root{--el-color-white:#ffffff;--el-color-black:#000000;--el-color-primary:#409eff;--el-color-primary-light-1:#53a8ff;--el-color-primary-light-2:#66b1ff;--el-color-primary-light-3:#79bbff;:…"

    execScripts

    该方法的作用就是指定一个 proxy(默认是 window)对象,然后执行该模板文件中所有的 JS,并返回 JS 执行后 proxy 对象的最后一个属性(见下图 )。

    61C43E20-F08B-4a68-951A-525CE8CA4A75.png

    在微前端架构中,这个对象一般会包含一些子应用的生命周期钩子函数,主应用可以通过在特定阶段调用这些生命周期钩子函数,进行挂载和销毁子应用的操作。

    主应用挂载子应用 HTML 模板

    执行注册子应用时传入的 render 函数,将 HTML Template 和 loading 作为入参,render 函数的内容一般是将 HTML 挂载在指定容器中

    initialAppWrapperElement = createElement(appContent, strictStyleIsolation, scopedCSS, appInstanceId);
    initialContainer = 'container' in app ? app.container : undefined;
    render({ 
      element: initialAppWrapperElement,
      loading: true, 
      container: initialContainer
    }, 'loading');
    
    function createElement(
      appContent, 
      strictStyleIsolation, 
      scopedCSS, 
      appInstanceId) {  
        var containerElement = document.createElement('div');  
        containerElement.innerHTML = appContent; 
        // appContent always wrapped with a singular div  
        var appElement = containerElement.firstChild;  
        return appElement;
    }
    

    在这个阶段,主应用已经将子应用基础的 HTML 结构挂载在了主应用的某个容器内,接下来还需要执行子应用对应的 mount 方法(如 Vue.$mount)对子应用状态进行挂载。

    相关文章

      网友评论

          本文标题:微前端 -- 乾坤(二)源码篇

          本文链接:https://www.haomeiwen.com/subject/xnbasrtx.html