119.乾坤资源加载机制

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

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
可以看到

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);
}
}
}

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

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)
functionWrappedCode的结果,目前还不是很理解为什么要重新构建一个function,为了防止重写this的指向吗?

至此加载的模板中的js代码执行完毕(包含远程的和内嵌的代码),控制台打印出123,说明我们已经加载了打包好的资源并且执行了。因为单页应用的渲染和事件执行都在打包好的入口文件里面即app.js,所有执行了app.js中的代码后就可以跳转到我们的子应用了。
乾坤框架系列学习
01.学习微前端架构-乾坤
02.资源加载过程分析
03.乾坤css加载机制
04.乾坤js隔离机制
乾坤沙箱实现原理