119.乾坤资源加载机制

98 阅读4分钟

通过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中的代码后就可以跳转到我们的子应用了。