import-html-entry源码分析

1. 为什么要分析import-html-entry

在qiankun中,子应用的入口文件是通过import-html-entry来加载的,作为qiankun一个重要的依赖,我们有必要了解import-html-entry的实现原理,才能更好的理解qiankun。

2. 一个简单的例子

这里的例子是import-html-entry库中的一个例子,这里我稍微做了下改动,用src中的index.js为入口文件,便于我们分析代码。

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
<!--<script src="../dist/import-html-entry.js"></script>-->
<script type="module">
  import importHTML from '../src/index.js';
    importHTML('./template.html').then(res => {
       console.log(res.template);
       res.execScripts().then(exports => {
          console.log(exports);
       });
    });

</script>
</body>
</html>

至于template.html文件,和库中保持一致

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>test</title>
    <link href="https://unpkg.com/antd@3.13.6/dist/antd.min.css" rel="stylesheet">
    <link href="https://unpkg.com/bootstrap@4.3.1/dist/css/bootstrap-grid.min.css" rel="stylesheet">
</head>
<body>

<script src="./a.js"></script>
<script ignore>alert(1)</script>
<script src="./b.js"></script>
<script src="https://unpkg.com/react@16.4.2/umd/react.production.min.js"></script>
<script src="https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js"></script>
</body>
</html>

a.jsb.js文件内容也无变化,如下

javascript 复制代码
// a.js
window.React;
console.log('a');
javascript 复制代码
// b.js
console.log('b');

在浏览器控制台输出的结果如下

stdout 复制代码
// 控制台输出
<!DOCTYPE html>
...
<style>
    // antd.min.css antd样式
    ...
</style>
<style>
    // bootstrap-grid.min.css bootstrap样式
    ...
</style>
<!--   script http://localhost:63342/import-html-entry/example/a.js replaced by import-html-entry -->
<!-- ignore asset js file replaced by import-html-entry -->
<!--   script http://localhost:63342/import-html-entry/example/b.js replaced by import-html-entry -->
<!--   script https://unpkg.com/react@16.4.2/umd/react.production.min.js replaced by import-html-entry -->
<!--   script https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js replaced by import-html-entry -->
</html>
a
b
{$mobx:...}

接下来的旅程,我们将从这个例子开始,一步步分析import-html-entry的实现原理。

3. 源码分析

3.1 import-html-entry概述

  • 以html文件为入口,加载html依赖的资源,比如css,js等
  • 从入口脚本中获取导出的内容

在我们上面的例子中,导出的内容就是mobx

3.2 import-html-entry的实现

我们以示例中的代码为突破口,来深入分析import-html-entry的执行机制,

3.2.1 importHTML

那么首先要分析的就是importHTML('./template.html')这个函数,源码如下:

js 复制代码
export default function importHTML(url, opts = {}) {
       let fetch = defaultFetch;
       let autoDecodeResponse = false;
       let getPublicPath = defaultGetPublicPath;
       let getTemplate = defaultGetTemplate;
       const { postProcessTemplate } = opts;

       // compatible with the legacy importHTML api
       if (typeof opts === 'function') {
             fetch = opts;
       } else {
             // fetch option is availble
             if (opts.fetch) {
                // ...
             }
             // 赋值 getPublicPath  getTemplate
             getPublicPath = opts.getPublicPath || opts.getDomain || defaultGetPublicPath;
             getTemplate = opts.getTemplate || defaultGetTemplate;
       }
// 获取html资源,返回一个promise
       return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url)
             .then(response => readResAsString(response, autoDecodeResponse))
             .then(html => {

                   const assetPublicPath = getPublicPath(url);
                   const { template, scripts, entry, styles } = processTpl(getTemplate(html), assetPublicPath, postProcessTemplate);
// 返回一个promise
                   return getEmbedHTML(template, styles, { fetch }).then(embedHTML => ({
                         template: embedHTML,
                         assetPublicPath,
                         getExternalScripts: () => getExternalScripts(scripts, fetch),
                         getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
                         execScripts: (proxy, strictGlobal, opts = {}) => {
                               if (!scripts.length) {
                                     return Promise.resolve();
                               }
                               return execScripts(entry, scripts, proxy, {
                                     fetch,
                                     strictGlobal,
                                     ...opts,
                               });
                         },
                   }));
             }));
}

importHTML在这里做了两件事情:

  1. 参数的标准化处理
  2. 使用fetch获取html资源,并且resolve的结果为一个对象,对象包含template 、assetPublicPath 、getExternalScripts 、getExternalStyleSheets 、execScripts5个属性,
processTpl

在返回的结果中,我们依赖processTpl的调用结果,这里我们再分析下processTpl这个函数:

javascript 复制代码
export default function processTpl(tpl, baseURI, postProcessTemplate) {

    // 初始化scripts,styles,entry
       let scripts = [];
       const styles = [];
       let entry = null;
       const moduleSupport = isModuleScriptSupported();

       // 对模板资源进行资源提取
       const template = tpl

             // 1. 移除注释
             .replace(HTML_COMMENT_REGEX, '')
             // 2. 提取css
             .replace(LINK_TAG_REGEX, match => {
                   /*
                   change the css link
                   */
                   const styleType = !!match.match(STYLE_TYPE_REGEX);
                   if (styleType) {

                         const styleHref = match.match(STYLE_HREF_REGEX);
                         const styleIgnore = match.match(LINK_IGNORE_REGEX);

                         if (styleHref) {

                               const href = styleHref && styleHref[2];
                               let newHref = href;

                               if (href && !hasProtocol(href)) {
                                  // baseUrl:"http://localhost:63342/import-html-entry/example/"
                                     newHref = getEntirePath(href, baseURI);
                               }
                               if (styleIgnore) {
                                     return genIgnoreAssetReplaceSymbol(newHref);
                               }

                               newHref = parseUrl(newHref);
                               // 将css资源提取处理塞入styles数组中
                               styles.push(newHref);
                               return genLinkReplaceSymbol(newHref);
                         }
                   }

                   const preloadOrPrefetchType = match.match(LINK_PRELOAD_OR_PREFETCH_REGEX) && match.match(LINK_HREF_REGEX) && !match.match(LINK_AS_FONT);
                   if (preloadOrPrefetchType) {
                         const [, , linkHref] = match.match(LINK_HREF_REGEX);
                         return genLinkReplaceSymbol(linkHref, true);
                   }

                   return match;
             })
          // 匹配style标签
             .replace(STYLE_TAG_REGEX, match => {
                   if (STYLE_IGNORE_REGEX.test(match)) {
                         return genIgnoreAssetReplaceSymbol('style file');
                   }
                   return match;
             })
          // 匹配script标签
             .replace(ALL_SCRIPT_REGEX, (match, scriptTag) => {
                   const scriptIgnore = scriptTag.match(SCRIPT_IGNORE_REGEX);
                   const moduleScriptIgnore =
                         (moduleSupport && !!scriptTag.match(SCRIPT_NO_MODULE_REGEX)) ||
                         (!moduleSupport && !!scriptTag.match(SCRIPT_MODULE_REGEX));
                   // in order to keep the exec order of all javascripts

                   const matchedScriptTypeMatch = scriptTag.match(SCRIPT_TYPE_REGEX);
                   const matchedScriptType = matchedScriptTypeMatch && matchedScriptTypeMatch[2];
                   if (!isValidJavaScriptType(matchedScriptType)) {
                         return match;
                   }

                   // if it is a external script
                   if (SCRIPT_TAG_REGEX.test(match) && scriptTag.match(SCRIPT_SRC_REGEX)) {
                         /*
                         collect scripts and replace the ref
                         */

                         const matchedScriptEntry = scriptTag.match(SCRIPT_ENTRY_REGEX);
                         const matchedScriptSrcMatch = scriptTag.match(SCRIPT_SRC_REGEX);
                         let matchedScriptSrc = matchedScriptSrcMatch && matchedScriptSrcMatch[2];

                         if (entry && matchedScriptEntry) {
                               throw new SyntaxError('You should not set multiply entry script!');
                         }

                         // 将baseUrl和scriptSrc进行拼接
                         if (matchedScriptSrc) {
                               // append the domain while the script not have a protocol prefix
                            // 拼接端口号
                               if (!hasProtocol(matchedScriptSrc)) {
                                     matchedScriptSrc = getEntirePath(matchedScriptSrc, baseURI);
                               }

                               matchedScriptSrc = parseUrl(matchedScriptSrc);
                         }

                         entry = entry || matchedScriptEntry && matchedScriptSrc;

                         if (scriptIgnore) {
                               return genIgnoreAssetReplaceSymbol(matchedScriptSrc || 'js file');
                         }

                         if (moduleScriptIgnore) {
                               return genModuleScriptReplaceSymbol(matchedScriptSrc || 'js file', moduleSupport);
                         }

                         if (matchedScriptSrc) {
                               const asyncScript = !!scriptTag.match(SCRIPT_ASYNC_REGEX);
                               const crossOriginScript = !!scriptTag.match(SCRIPT_CROSSORIGIN_REGEX);
                               // 将scriptSrc插入scripts数组中
                               scripts.push((asyncScript || crossOriginScript) ? { async: asyncScript, src: matchedScriptSrc, crossOrigin: crossOriginScript } : matchedScriptSrc);
                               return genScriptReplaceSymbol(matchedScriptSrc, asyncScript, crossOriginScript);
                         }

                         return match;
                   } else {
                         if (scriptIgnore) {
                               return genIgnoreAssetReplaceSymbol('js file');
                         }

                         if (moduleScriptIgnore) {
                               return genModuleScriptReplaceSymbol('js file', moduleSupport);
                         }

                         // if it is an inline script
                         const code = getInlineCode(match);

                         // remove script blocks when all of these lines are comments.
                         const isPureCommentBlock = code.split(/[\r\n]+/).every(line => !line.trim() || line.trim().startsWith('//'));

                         if (!isPureCommentBlock) {
                               scripts.push(match);
                         }

                         return inlineScriptReplaceSymbol;
                   }
             });

       scripts = scripts.filter(function (script) {
             // filter empty script
             return !!script;
       });

       let tplResult = {
             template,
             scripts,
             styles,
             // set the last script as entry if have not set
             entry: entry || scripts[scripts.length - 1],
       };
       if (typeof postProcessTemplate === 'function') {
             tplResult = postProcessTemplate(tplResult);
       }

       return tplResult;
}

上面的代码比较多,但是核心要处理的事情比较清晰,我们可以看到,processTpl函数主要做了以下几件事情:

  1. 移除注释
  2. 提取css
  3. 提取script
  4. 提取entry入口文件:如果没有指定入口文件,那么默认使用最后一个script作为入口文件

那么最后我们得到的tplResult就是一个对象,在我们这个示例中,这个函数最终返回的如下:

json 复制代码
{
    "template": "<!DOCTYPE html>
             <html lang="en">
                <head>
                   <meta charset="UTF-8">
                   <title>test</title>
                   <!--  link https://unpkg.com/antd@3.13.6/dist/antd.min.css replaced by import-html-entry -->
                   <!--  link https://unpkg.com/bootstrap@4.3.1/dist/css/bootstrap-grid.min.css replaced by import-html-entry -->
                </head>
                <body>
             
                   <!--   script http://localhost:63342/import-html-entry/example/a.js replaced by import-html-entry -->
                   <!-- ignore asset js file replaced by import-html-entry -->
                   <!--   script http://localhost:63342/import-html-entry/example/b.js replaced by import-html-entry -->
                   <!--   script https://unpkg.com/react@16.4.2/umd/react.production.min.js replaced by import-html-entry -->
                   <!--   script https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js replaced by import-html-entry -->
                </body>
             </html>
    ",
    "scripts": [
       "http://localhost:63342/import-html-entry/example/a.js",
       "http://localhost:63342/import-html-entry/example/b.js",
       "https://unpkg.com/react@16.4.2/umd/react.production.min.js",
       "https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js"
    ],
    "styles": [
       "https://unpkg.com/antd@3.13.6/dist/antd.min.css",
       "https://unpkg.com/bootstrap@4.3.1/dist/css/bootstrap-grid.min.css"
    ],
    "entry": "https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js"
}

回过头我们继续分析在执行完processTpl函数后,我们调用了getEmbedHTML函数,最后将它的结果resolve出去

getEmbedHTML

这个函数是用来加载css的,我们来看一下它的实现:

js 复制代码
function getEmbedHTML(template, styles, opts = {}) {
       const { fetch = defaultFetch } = opts;
       let embedHTML = template;
// 使用fetch函数加载css
       return getExternalStyleSheets(styles, fetch)
          // 这里的styleSheets是所有外链css的内容数组,['antd v3.13.6 css...', 'bootstrap v4.3.1 css...']
             .then(styleSheets => {
                   embedHTML = styles.reduce((html, styleSrc, i) => {
                      // 将css外链替换为style标签
                         html = html.replace(genLinkReplaceSymbol(styleSrc), isInlineCode(styleSrc) ? `${styleSrc}` : `<style>/* ${styleSrc} */${styleSheets[i]}</style>`);
                         return html;
                   }, embedHTML);
                   return embedHTML;
             });
}
export function getExternalStyleSheets(styles, fetch = defaultFetch) {
       return Promise.all(styles.map(styleLink => {
                   if (isInlineCode(styleLink)) {
                         // if it is inline style
                         return getInlineCode(styleLink);
                   } else {
                         // external styles
                         return styleCache[styleLink] ||
                               (styleCache[styleLink] = fetch(styleLink).then(response => response.text()));
                   }

             },
       ));
}

getEmbedHTML的目的就是为了将我们的外链css转换为style标签中的css。

目前为止,importHTML已经分析完成,我们总结下他的执行流程:

  1. 通过fetch函数获取html模板
  2. 通过processTpl函数处理模板,提取出css和script
  3. 通过getEmbedHTML函数将外链css转换为style标签中的css
  4. 将处理后的结果resolve出去

接下来我们将继续分析execScripts函数

3.2.2 execScripts

这个函数是用来执行script的,源码如下:

js 复制代码
export function execScripts(entry, scripts, proxy = window, opts = {}) {
       const {
             fetch = defaultFetch, strictGlobal = false, success, error = () => {
             }, beforeExec = () => {
             }, afterExec = () => {
             },
             scopedGlobalVariables = [],
       } = opts;

       return getExternalScripts(scripts, fetch, error)
             .then(scriptsText => {

                   const geval = (scriptSrc, inlineScript) => {
                         const rawCode = beforeExec(inlineScript, scriptSrc) || inlineScript;
                         const code = getExecutableScript(scriptSrc, rawCode, { proxy, strictGlobal, scopedGlobalVariables });

                         evalCode(scriptSrc, code);

                         afterExec(inlineScript, scriptSrc);
                   };

                   function exec(scriptSrc, inlineScript, resolve) {

                         const markName = `Evaluating script ${scriptSrc}`;
                         const measureName = `Evaluating Time Consuming: ${scriptSrc}`;

                         

                         if (scriptSrc === entry) {
                            noteGlobalProps(strictGlobal ? proxy : window);
                            // bind window.proxy to change `this` reference in script
                            geval(scriptSrc, inlineScript);
                            const exports = proxy[getGlobalProp(strictGlobal ? proxy : window)] || {};
                            resolve(exports);
                         } else {
                            if (typeof inlineScript === 'string') {
                               // bind window.proxy to change `this` reference in script
                               geval(scriptSrc, inlineScript);
                            } else {
                                  // external script marked with async
                               // 处理其他形式的script标签
                            }
                         }
                   }

                   function schedule(i, resolvePromise) {

                         if (i < scripts.length) {
                               const scriptSrc = scripts[i];
                               const inlineScript = scriptsText[i];

                               exec(scriptSrc, inlineScript, resolvePromise);
                               // resolve the promise while the last script executed and entry not provided
                               if (!entry && i === scripts.length - 1) {
                                     resolvePromise();
                               } else {
                                     schedule(i + 1, resolvePromise);
                               }
                         }
                   }

                   return new Promise(resolve => schedule(0, success || resolve));
             });
}

execScripts有这样几个入参:

  • entry:入口script,在本例中为最后一个script,https://unpkg.com/mobx@5.0.3
  • scripts:所有的script,包括外链script和内联script
  • proxy:代理对象,用来代理window对象,默认为window
  • opts:配置项,包括fetch函数、strictGlobalsuccesserrorbeforeExecafterExecscopedGlobalVariables

execScripts的目的就是为了执行所有的script,首先是调用了getExternalScripts,这个函数不展开分析了,是用fetch来获取script链接的内容,之后内部定义了三个函数:geval,exec,schedule,下面我们按照执行顺序来分析这三个函数

  • schedule: 用来执行所有的script,这里使用了递归的方式,每次执行一个script,直到所有的script都执行完毕,这里有一个细节,就是在执行最后一个script的时候,如果没有entry,那么就会resolve出去,这里的entry就是我们的入口script,如果没有入口script,那么就会在最后一个script执行完毕后resolve出去

  • exec:在每一轮递归中执行script,这个函数有三个入参:scriptSrc是js代码对应的src链接地址,inlineScript是使用fetch获取到的js代码,resolve就是外层的resole,函数内部的逻辑有两条线:

    • 当scriptSrc等于entry的时候,说明是入口script,我们稍后再分析这里的逻辑
    • 当scriptSrc不等于entry的时候,说明是其他的script,,那么就会执行geval函数
  • geval:用来绑定window.proxy,然后执行script,这里有两行代码比较关键:

    js 复制代码
    const code = getExecutableScript(scriptSrc, rawCode, { proxy, strictGlobal, scopedGlobalVariables });
    
    evalCode(scriptSrc, code);
getExecutableScript

第一步调用getExecutableScript函数,这个函数的作用是将script的内容包裹在一个函数中,这个函数的作用是为了将script的作用域隔离开,这样就不会污染全局作用域了

js 复制代码
function getExecutableScript(scriptSrc, scriptText, opts = {}) {
    const { proxy, strictGlobal, scopedGlobalVariables = [] } = opts;

    const sourceUrl = isInlineCode(scriptSrc) ? '' : `//# sourceURL=${scriptSrc}\n`;

    // 将 scopedGlobalVariables 拼接成变量声明,用于缓存全局变量,避免每次使用时都走一遍代理
    const scopedGlobalVariableDefinition = scopedGlobalVariables.length ? `const {${scopedGlobalVariables.join(',')}}=this;` : '';

    // 通过这种方式获取全局 window,因为 script 也是在全局作用域下运行的,所以我们通过 window.proxy 绑定时也必须确保绑定到全局 window 上
    // 否则在嵌套场景下, window.proxy 设置的是内层应用的 window,而代码其实是在全局作用域运行的,会导致闭包里的 window.proxy 取的是最外层的微应用的 proxy
    const globalWindow = (0, eval)('window');
    globalWindow.proxy = proxy;
    // TODO 通过 strictGlobal 方式切换 with 闭包,待 with 方式坑趟平后再合并
    return strictGlobal
       ? (
          scopedGlobalVariableDefinition
             ? `;(function(){with(this){${scopedGlobalVariableDefinition}${scriptText}\n${sourceUrl}}}).bind(window.proxy)();`
             : `;(function(window, self, globalThis){with(window){;${scriptText}\n${sourceUrl}}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`
       )
       : `;(function(window, self, globalThis){;${scriptText}\n${sourceUrl}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`;
}

这里有一个骚操作,const globalWindow = (0, eval)('window');,这里的作用在代码的注释例写的也很清楚,至于深层次的原因(eval的直接调用和间接调用),大家可以自行百度了解一下。之后就return了一个 模板字符串,乍一看这一段代码很难懂,我们不妨代入我们要执行的代码,并且把他格式化一下看看:

js 复制代码
;(function(window, self, globalThis){
  // 这里是我们要执行的代码
  console.log('hello world')
}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);
  1. 首先这是一个自执行函数
  2. 其次,这个自执行函数的函数体是一个匿名函数,通过bind绑定了window.proxy,这里的window.proxy就是我们传入的代理对象
  3. 最后,这个匿名函数的入参是window,self,globalThis,他们都是指向window.proxy的,也就是我们传入的代理对象
evalCode

我们继续分析第二步,这里调用evalCode函数,这个函数的作用是执行script,这里有一个细节,就是在执行script的时候,会将script的内容包裹在一个函数中,这个函数的作用是为了将script的作用域隔离开,这样就不会污染全局作用域了

js 复制代码
export function evalCode(scriptSrc, code) {
    const key = scriptSrc;
    if (!evalCache[key]) {
       const functionWrappedCode = `(function(){${code}})`;
       evalCache[key] = (0, eval)(functionWrappedCode);
    }
    const evalFunc = evalCache[key];
    evalFunc.call(window);
}

evalCode的最后就是执行我们的script代码

小结:目前我们已经分析了一个script从拉取到执行的全过程

我们再来看看入口script的逻辑,入口script的逻辑和其他script的逻辑是一样的,只是多了一些额外的逻辑,我们来看看这些额外的逻辑:

js 复制代码
// 这是额外的逻辑
noteGlobalProps(strictGlobal ? proxy : window);
// bind window.proxy to change `this` reference in script
geval(scriptSrc, inlineScript);
// 这也是额外的逻辑
const exports = proxy[getGlobalProp(strictGlobal ? proxy : window)] || {};
resolve(exports);
noteGlobalProps
  • noteGlobalProps:这个函数的作用是获取window上的最后一个属性
js 复制代码
export function noteGlobalProps(global) {
    // alternatively Object.keys(global).pop()
    // but this may be faster (pending benchmarks)
    firstGlobalProp = secondGlobalProp = undefined;

    for (let p in global) {
       if (shouldSkipProperty(global, p))
          continue;
       if (!firstGlobalProp)
          firstGlobalProp = p;
       else if (!secondGlobalProp)
          secondGlobalProp = p;
       lastGlobalProp = p;
    }

    return lastGlobalProp;
}
  • 然后是继续执行geval,因为是入口文件,且mobx会在全局添加一个变量mobx
  • 第三步就是获取我们需要暴露出去的全局变量,这里用到了getGlobalProp,下面的代码中我们的lastProp就是mobx
js 复制代码
export function getGlobalProp(global) {
    let cnt = 0;
    let lastProp;
    let hasIframe = false;

    for (let p in global) {
       if (shouldSkipProperty(global, p))
          continue;
       if (!hasIframe && (cnt === 0 && p !== firstGlobalProp || cnt === 1 && p !== secondGlobalProp))
          return p;
       cnt++;
       lastProp = p;
    }

    if (lastProp !== lastGlobalProp)
       return lastProp;

那么最后理所当然的返回了mobx,也就是我们的全局变量

4总结

大结:import-html-entry的大致流程就分享到这里了,代码并不多,但是eval的使用确实秀到了我,大写的佩服,还有半个小时下班,各位看官周末愉快,下周见!

复制代码
相关推荐
M_emory_18 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito21 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
mon_star°1 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184551 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
文军的烹饪实验室2 小时前
ValueError: Circular reference detected
开发语言·前端·javascript
Martin -Tang3 小时前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发3 小时前
解锁微前端的优秀库
前端
王解4 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁4 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis