美文网首页
119.乾坤资源加载机制

119.乾坤资源加载机制

作者: wo不是黄蓉 | 来源:发表于2022-11-08 15:59 被阅读0次
乾坤资源加载机制.jpg

通过import-html-entry请求资源

资源加载主要流程图,只看第二个节点的即可


微应用.png
 const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);

加载entry方法

function importEntry(entry,opts={}){
    const {fetch = defaultFect,getTemplate} = opts  const {
        fetch = defaultFetch,
        getTemplate = defaultGetTemplate,
        postProcessTemplate,
    } = opts;
    //entry不存在,提示报错
    if(!entry) return
    //针对单页应用的情况
    if(typeof entry === "string"){
        //加载html
        return importHTML(entry,{fetch,getPublicPath,getTemplate,postProcessTemplate})
    }
    //针对子应用是多出口的情况,加载多个入口文件
        if (Array.isArray(entry.scripts) || Array.isArray(entry.styles)) {
        const { scripts = [], styles = [], html = "" } = entry;
        //1.处理style
        //2.处理scripts
        //3.模板文件,处理逻辑和一个入口的子应用一样的,返回内容也是一样的
        return getEmbedHTML(
            getTemplate(
                getHTMLWithScriptPlaceholder(getHTMLWithStylePlaceholder(html))
            ),
            styles,
            { fetch }
        ).then((embedHTML) => ({
            template: embedHTML,
            assetPublicPath: getPublicPath(entry),
            getExternalScripts: () => getExternalScripts(scripts, fetch),
            getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
            execScripts: (proxy, strictGlobal, opts = {}) => {
                if (!scripts.length) {
                    return Promise.resolve();
                }
                return execScripts(scripts[scripts.length - 1], scripts, proxy, {
                    fetch,
                    strictGlobal,
                    ...opts,
                });
            },
        }));
    } else {
        throw new SyntaxError("entry scripts or styles should be array!");
    }
    
}

importHTML 函数

export default function importHTML(url, opts = {}) {
    let fetch = defaultFetch;
    let autoDecodeResponse = false;
    let getPublicPath = defaultGetPublicPath;
    let getTemplate = defaultGetTemplate;
    const { postProcessTemplate } = opts;
    //...处理fetch逻辑
    return (
        embedHTMLCache[url] ||
        (embedHTMLCache[url] = fetch(url)
            .then((response) => readResAsString(response, autoDecodeResponse))
            .then((html) => {
                const assetPublicPath = getPublicPath(url);
            //根据模板字符串和publicPath解析出模板,js脚本和style
                const { template, scripts, entry, styles } = processTpl(
                    getTemplate(html),
                    assetPublicPath,
                    postProcessTemplate
                );

            //将获取到的模板文件,scripts,styles以对象的方式返回
                return getEmbedHTML(template, styles, { fetch }).then((embedHTML) => ({
                    template: embedHTML,
                    assetPublicPath,
                    getExternalScripts: () => getExternalScripts(scripts, fetch),
                    getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
                    execScripts: (proxy, strictGlobal, opts = {}) => {
                        return execScripts(entry, scripts, proxy, {
                            fetch,
                            strictGlobal,
                            ...opts,
                        });
                    },
                }));
            }))
    );
}

readResAsString函数,处理fetch的请求结果,将结果转换成string

processTpl 函数,

export default function processTpl(tpl, baseURI, postProcessTemplate) {
    let scripts = [];
    const styles = [];
    let entry = null;
    const moduleSupport = isModuleScriptSupported();

    const template = tpl
    //...省略
    //1.去掉掉注释代码
    //2.处理link标签,解析远程样式
    //3.处理行内样式
    //4.处理script标签

    //返回所有执行的script脚本数组,模板、style标签
    let tplResult = {
        template,
        scripts,
        styles,
        // set the last script as entry if have not set
        entry: entry || scripts[scripts.length - 1],
    };

    return tplResult;
}

测试processTpl ,开启一个项目,使用fetch获取请求到的资源,然后调用processTpl方法,其中正则表达式s修饰符是es6的语法,如果使用的是vue-cli创建的vue2项目,编译会报错,提示内容是Invalid regular expression: invalid group,需要使用@babel/preset-env解决,安装 npm install -D @babel/core @babel/preset-env babel-loader @babel/plugin-transform-runtime

.babelrc文件配置

{
  "presets": ["@babel/preset-env"],
  "plugins": [
    "@babel/plugin-transform-runtime",
    "@babel/plugin-proposal-optional-chaining"
  ],
  "env": {
    "esm": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "modules": false
          }
        ]
      ],
      "plugins": [
        [
          "@babel/plugin-transform-runtime",
          {
            "useESModules": true
          }
        ],
        "@babel/plugin-proposal-optional-chaining"
      ]
    }
  }
}

调用processTpl测试代码:

import processTpl from "./import-html/process-tpl.js";
let template = "";
const url = "http://localhost:8080";

fetch(url)
  .then(response => {
    response.text().then(res => {
      const { template, scripts, entry, styles } = processTpl(res, url);
      console.log(template, scripts, entry, styles);
    });
  })
  .catch(err => {
    console.log(err);
  });

分别打印的template, scripts, entry, styles内容如下,template返回的模板文件,scripts中返回的js脚本,包含在模板文件中中的js代码和远程加载的js文件,styles中返回所有使用Link标签加载的css文件,可以看出来内置的style标签里面的内容和行内样式都没有打印出来,style处理只针对的是远程加的css文件。

为什么不用单独解析行内样式和内嵌样式?

我想应该是这些样式不需要处理,可以直接在页面加载的时候直接及逆行渲染,如果引用的是外联样式,可以发现打包好的模板文件把引入的代码给注释掉了,在getEmbedHTML可以看到

1667823480447.png
getEmbedHTML ,opts是importHTML 中传的fetch方法,这个方法的作用是返回style标签替换后的模板文件
function getEmbedHTML(template, styles, opts = {}) {
    const { fetch = defaultFetch } = opts;
    let embedHTML = template;
    //获取远程的style属性
    return getExternalStyleSheets(styles, fetch).then((styleSheets) => {
        embedHTML = styles.reduce((html, styleSrc, i) => {
            //将远程加载的css文件使用内嵌的方式加入到模板里面
            html = html.replace(
                genLinkReplaceSymbol(styleSrc),
                isInlineCode(styleSrc)
                    ? `${styleSrc}`
                    : `<style>/* ${styleSrc} */${styleSheets[i]}</style>`
            );
            return html;
        }, embedHTML);
        return embedHTML;
    });
}

回到importHTML方法,可以看到返回结果就是模板文件,scripts,styles

再往上返回到importEntry方法,可以看到返回结果是importHTML方法的执行结果,也就是将模板文件,scripts,styles这些内容返回。

看一下execScripts 方法

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){
            if(scriptSrc === entry){
               geval(scriptSrc,inlineScript)
                const exports = proxy[getGlobalProp(strictGlobal ? proxy : window)] || {}
                resolve(exports)
            }else{
                if(typeof inlineScript === "string"){
                    geval(scriptSrc,inlineScript)
                }else{
                    inlineScript.async && inlineScript?.content.then((downloadedScriptText)=>{
                    geval(inlineScript.src,downloadedScriptText)
                })
                }
            }
        }
        function schedule(i, resolvePromise) {
                    //循环执行scripts
            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))
    })
}

getExternalScripts函数,如果script是内嵌代码,将去掉标签后的可执行代码返回,具体表现在getInlineCode这个函数,从getExternalScripts函数的执行结果也可以看出来

如果是带src属性的js代码,则调用fetchScripts函数获取js代码,获取到的代码本身就是可执行的代码。

getExternalScripts执行完后调用schedule函数,看schedule 函数的代码

function getExternalScripts( 
    scripts,
    fetch = defaultFetch,
    errorCallback = () => {}){
        return Promise.all(scripts.map(script)=>{
            if(typeof script === "string"){
                 //内嵌js代码
                if(isInlineCode(script)){
                    //返回可执行的代码
                    return getInLineCode(script)
                }else{
                    //加载远程脚本代码
                    return fetchScript(script)
                }
            }
        })
}

schedule 函数执行过程见下图

    function schedule(i, resolvePromise) {
      //循环执行scripts
      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);
        }
      }
    }
1667890347322.png

exec函数

function exec(scriptSrc, inlineScript, resolve){
    //...省略    主要是执行geval方法      
}
//geval函数
const geval = (scriptSrc, inlineScript) => {
      const rawCode = beforeExec(inlineScript, scriptSrc) || inlineScript;
      //返回的代码内容为一个自执行函数
      const code = getExecutableScript(scriptSrc, rawCode, {
        proxy,
        strictGlobal,
        scopedGlobalVariables
      });

      evalCode(scriptSrc, code);

      afterExec(inlineScript, scriptSrc);
    };

getExecutableScript函数返回结果,可以看到是将解析出来的代码改为一个执行函数,至于为什么要传proxy而不直接是window,是为了改变this指向,在当前我的里面proxy指向的就是window


1667891388709.png

getExecutableScript函数,直接看返回结果即可,接下来就是执行evalCode 方法,

function getExecutableScript(scriptSrc, scriptText, opts = {}) {
  const { proxy, strictGlobal, scopedGlobalVariables = [] } = opts;
  return strictGlobal
    ? scopedGlobalVariableFnParameters
      ? `;(function(){with(window.proxy){(function(${scopedGlobalVariableFnParameters}){;${scriptText}\n${sourceUrl}}).bind(window.proxy)(${scopedGlobalVariableFnParameters})}})();`
      : `;(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);`;
}

evalCode 函数

export function evalCode(scriptSrc, code) {
   const key = scriptSrc;
   //对需要执行的script代码进行缓存,防止重复执行
   if (!evalCache[key]) {
       //已经构建了一个自执行函数了,为什么还要重新构建呢?这里还不是很理解
       const functionWrappedCode = `(function(){${code}})`;
       //间接调用eval函数,返回一个可以计算的值
       evalCache[key] = (0, eval)(functionWrappedCode);
   }
   const evalFunc = evalCache[key];
   //执行获取到的js代码,构建一个自执行函数,不用使用eval,看源码上面配置的使用with,还有待踩的坑,就不关注了
   evalFunc.call(window);
}

(0,eval)这行代码怎么理解?参考下面两篇文章

(0, eval)(functionWrappedCode)

eval的一些理解

functionWrappedCode的结果,目前还不是很理解为什么要重新构建一个function,为了防止重写this的指向吗?

1667893021581.png

至此加载的模板中的js代码执行完毕(包含远程的和内嵌的代码),控制台打印出123,说明我们已经加载了打包好的资源并且执行了。因为单页应用的渲染和事件执行都在打包好的入口文件里面即app.js,所有执行了app.js中的代码后就可以跳转到我们的子应用了。

乾坤框架系列学习
01.学习微前端架构-乾坤
02.资源加载过程分析
03.乾坤css加载机制
04.乾坤js隔离机制
乾坤沙箱实现原理

相关文章

  • 119.乾坤资源加载机制

    通过import-html-entry请求资源 资源加载主要流程图,只看第二个节点的即可 加载entry方法 im...

  • webkit资源加载机制

    资源加载器 1. 特定资源加载器,加载特定资源,比如image对应ImageLoader 2. 缓存机制的资源加载...

  • cocos creator基础-(十三)cc.Loader使用

    1: 掌握cc.loader加载本地资源; 2: 掌握cc.loader加载远程资源; 3: 掌握资源释放的机制与...

  • 《WebKit技术内幕》知识提炼 —— 资源加载和网络栈

    WebKit 资源加载机制 资源 HTML 支持的资源主要包括:HTML、JavaScript、CSS、图片、SV...

  • Android资源加载机制

    转载请说明出处 https://www.jianshu.com/p/0ffbfcf97902 Android资源加...

  • Android资源加载机制

    参考1参考2 获取资源的方式 先通过Context.getResources();获取Resources对象,有了...

  • WebKit架构(二)

    WebKit资源加载机制 资源 网络和资源加载是网页的加载和渲染过程中的第一步,也是必不可少的一步。网页本身就是一...

  • 第四章 资源加载和网络栈

    WebKit资源加载机制 1. 资源 HTML支持的类型主要包括:HTML、JavaScript、CSS样式表、图...

  • 类加载机制(一)

    加载机制系列类加载机制(一)类加载机制(二)类加载机制(三) 类加载机制 1.JVM把class文件加载到内存,对...

  • 插件化技术

    核心:类加载机制和反射机制插件中代码的加载和主工程的相互调用插件中资源的加载和主工程的互相访问四大组件生命周期管理

网友评论

      本文标题:119.乾坤资源加载机制

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