微前端qiankun原理学习

更新时间:2023-05-18 20:24:56 阅读: 评论:0

微前端qiankun原理学习
前⾔
 这⾥重新提及⼀下,在上⼀篇我对于single-spa中提及了,single-spa帮住我们解决了⼦应⽤之间的调度问题,但是它留下了⼀个⼗分⼤的缺⼝。就是加载函数,下⾯官⽅⽂档的加载函数写法的截图:
官⽅⽂档中只是给出了⼀个简单的写法,但是光光靠这么写,是不够的。为什么不够:
1.这样写,⽆法避免全局变量window的污染。
2.css之间也会存在污染。
3.如果你有若⼲个⼦应⽤,你就要重复的去写这句话若⼲次,代码难看⽆法维护。
那么qiankun的出现,就是提供了⼀种⽅案帮住⽤户解决了这些问题,让⽤户做到开箱即⽤。不需要思考过多的问题。
这篇⽂章我们关注⼏个问题:
1. qiankun是如何完善single-spa中留下的巨⼤缺⼝,加载函数的缺⼝
2. qiankun通过什么策略去加载⼦应⽤资源
3. qiankun如何隔离⼦应⽤的js的全局环境
4. 沙箱的隔离原理是什么
5. qiankun如何隔离css环境
6. qiankun如何获得⼦应⽤⽣命周期函数
7. qiankun如何该改变⼦应⽤的window环境
同理qiankun我们也从两个函数去⼊⼿qiankun,registerMicroApps和start函数。
registerMicroApps
下⾯是registerMicroApps代码:
export function registerMicroApps<T extends object = {}>(
apps: Array<RegistrableApp<T>>,
lifeCycles?: FrameworkLifeCycles<T>,
) {
// Each app only needs to be registered once
//let microApps: RegistrableApp[] = [];
 //apps是本⽂件定义的⼀个全局数组,装着你在qiankun中注册的⼦应⽤信息。
 //microApps.some((registeredApp) => registeredApp.name === app.name));那么这句话返回的就是fal,取反就为true,
//然后把app的元素存⼊unregisteredApps中。所以其实整句话的含义就是在app上找出那些没有被注册的应⽤。其实就是变量名称unregisteredApps的含义。
const unregisteredApps = apps.filter((app) =>
!microApps.some((registeredApp) => registeredApp.name === app.name));
//这⾥就把未注册的应⽤和已经注册的应⽤进⾏合并
microApps = [...microApps, ...unregisteredApps];
unregisteredApps.forEach((app) => {
//解构出⼦应⽤的名字,激活的url匹配规规则,实际上activeRule就是⽤在single-spa的activeWhen,loader是⼀个空函数它是loadsh⾥⾯的东西,props传⼊⼦应⽤的值。
const { name, activeRule, loader = noop, props, ...appConfig } = app;
//这⾥调⽤的是single-spa构建应⽤的api
//name app activeRule props都是交给single-spa⽤的
registerApplication({
name,
//这⾥可以看出我开始说的问题,qiankun帮主我们定制了⼀套加载⼦应⽤的⽅案。整个加载函数核⼼的逻辑就是loadApp
//最后返回出⼀个经过处理的装载着⽣命周期函数的对象,和我上篇分析single-spa说到的加载函数的
写法的理解是⼀致的
app: async () => {
loader(true);
await frameworkStartedDefer.promi;
const { mount, ...otherMicroAppConfigs } = (
await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
)();
return {
mount: [async () => loader(true), ...toArray(mount), async () => loader(fal)],
...otherMicroAppConfigs,
};
},
activeWhen: activeRule,
customProps: props,
});
});
}
registerMicroApps其实只做了⼀件事情,根据⽤户传⼊的参数forEach遍历⼦应⽤注册数组,调⽤single-spa的registerApplication⽅法去注册⼦应⽤。
start函数
qiankun的start函数在single-spa的start函数的基础上增加了⼀些东西
export function start(opts: FrameworkConfiguration = {}) {
/
/let frameworkConfiguration: FrameworkConfiguration = {};它是本⽂件开头的全局变量记录着,框架的配置。
frameworkConfiguration = { prefetch: true, singular: true, sandbox: true, ...opts };
const { prefetch, sandbox, singular, urlRerouteOnly, ...importEntryOpts } = frameworkConfiguration;
if (prefetch) {
//⼦应⽤预加载的策略,⾃⾏在官⽅⽂档查看作⽤
doPrefetchStrategy(microApps, prefetch, importEntryOpts);
}
//检查当前环境是否⽀持proxy。因为后⾯沙箱环境中需要⽤到这个东西
if (sandbox) {
if (!window.Proxy) {
console.warn('[qiankun] Miss window.Proxy, proxySandbox will degenerate into snapshotSandbox');
frameworkConfiguration.sandbox = typeof sandbox === 'object' ? { ...sandbox, loo: true } : { loo: true };
if (!singular) {
console.warn(
'[qiankun] Setting singular as fal may cau unexpected behavior while your browr not support window.Proxy',
);
}
}
}
//startSingleSpa是single-spa的start⽅法的别名。这⾥本质就是执⾏single-spa的start⽅法启动应⽤。
startSingleSpa({ urlRerouteOnly });
}
总结:qiankun的start⽅法做了两件事情:
1.根据⽤户传⼊start的参数,判断预加载资源的时机。
2.执⾏single-spa的start⽅法启动应⽤。
加载函数
  从我上⼀篇对single-spa的分析知道了。在start启动应⽤之后不久,就会进⼊到加载函数。准备加载⼦应⽤。下⾯看看qiankun加载函数的源码。app: async () => {
loader(true);
await frameworkStartedDefer.promi;
const { mount, ...otherMicroAppConfigs } = (
//这⾥loadApp就是qiankun加载⼦应⽤的应对⽅案
await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
)();
return {
mount: [async () => loader(true), ...toArray(mount), async () => loader(fal)],
...otherMicroAppConfigs,
};
},
loadApp源码
export async function loadApp<T extends object>(
app: LoadableApp<T>,
configuration: FrameworkConfiguration = {},
lifeCycles?: FrameworkLifeCycles<T>,
): Promi<ParcelConfigObjectGetter> {
//从app参数中解构出⼦应⽤的⼊⼝entry,和⼦应⽤的名称。
const { entry, name: appName } = app;
//定义了⼦应⽤实例的id
const appInstanceId = `${appName}_${+new Date()}_${Math.floor(Math.random() * 1000)}`;
const markName = `[qiankun] App ${appInstanceId} Loading`;
if (v.NODE_ENV === 'development') {
//进⾏性能统计
performanceMark(markName);
}
const { singular = fal, sandbox = true, excludeAstFilter, ...importEntryOpts } = configuration;
//importEntry是import-html-entry库中的⽅法,这⾥就是qiankun对于加载⼦应⽤资源的策略
const { template, execScripts, astPublicPath } = await importEntry(entry, importEntryOpts);
...省略
}
下⾯看看importEntry的源码,它来⾃import-html-entry库。
export function importEntry(entry, opts = {}) {
//第⼀个参数entry是你⼦应⽤的⼊⼝地址
//第⼆个参数{prefetch: true}
//defaultFetch是默认的资源请求⽅法,其实就是window.fecth。在qiankun的start函数中,可以允许你传⼊⾃定义的fetch⽅法去请求资源。
//defaultGetTemplate是⼀个函数,传⼊⼀个字符串,原封不动的返回出来
const { fetch = defaultFetch, getTemplate = defaultGetTemplate } = opts;
//getPublicPath是⼀个函数,⽤来解析⽤户entry,转变为正确的格式,因为⽤户可能写⼊⼝地址写得奇形怪状,框架把不同的写法统⼀⼀下。
const getPublicPath = PublicPath || Domain || defaultGetPublicPath;
//没有写⼦应⽤加载⼊⼝直接报错
if (!entry) {
throw new SyntaxError('entry should not be empty!');
}
// html entry
if (typeof entry === 'string') {
//加载代码核⼼函数
泽的组词
return importHTML(entry, {
fetch,
getPublicPath,
getTemplate,
});
}
...省略
}
大学寒假开学时间
importHTML源码。
export default function importHTML(url, opts = {}) {
// 传⼊参数
//  entry, {
/
/    fetch,
//    getPublicPath,
//    getTemplate,
// }
let fetch = defaultFetch;
let getPublicPath = defaultGetPublicPath;
let getTemplate = defaultGetTemplate;
// compatible with the legacy importHTML api
if (typeof opts === 'function') {
fetch = opts;
} el {
fetch = opts.fetch || defaultFetch;
getPublicPath = PublicPath || Domain || defaultGetPublicPath;
getTemplate = Template || defaultGetTemplate;
}
//embedHTMCache是本⽂件开头定义的全局对象,⽤来缓存请求的资源的结果,下⼀次如果想要获取资源直接从缓存获取,不需要再次请求。
  //如果在缓存中找不到的话就去通过window.fetch去请求⼦应⽤的资源。但是这⾥需要注意,你从主应⽤中去请求⼦应⽤的资源是会存在跨域的。所以你在⼦应⽤中必须要进⾏跨域放⾏。配置下webpack的devServer的headers就可以
//从这⾥可以看出来qiankun是如何获取⼦应⽤的资源的,默认是通过window.fetch去请求⼦应⽤的资源。⽽不是简单的注⼊srcipt标签,通过fetch去获得了⼦应⽤的html资源信息,然后通过把信息转变为字符串的形式。
//然后把得到的html字符串传⼊processTpl⾥⾯进⾏html的模板解析
return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url)
//()下⾯的data就会变成⼀⼤串html respon.json()就是变成json对象
.then(respon => ())
.then(html => {
const astPublicPath = getPublicPath(url);
//processTpl这个拿到了⼦应⽤html的模板之后对微应⽤所有的资源引⼊做处理。
const { template, scripts, entry, styles } = processTpl(getTemplate(html), astPublicPath);
return getEmbedHTML(template, styles, { fetch }).then(embedHTML => ({
客岁//getEmbedHTML通过它的处理,就把外部引⽤的样式⽂件转变为了style标签,embedHTML就是处理后的html模板字符串
//embedHTML就是新⽣成style标签⾥⾯的内容
template: embedHTML,
astPublicPath,
getExternalScripts: () => getExternalScripts(scripts, fetch),
getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
//下⾯这个函数就是⽤来解析脚本的。从这⾥看来它并不是简单的插⼊script标签就完事了。⽽是
//通过在代码内部去请求资源,然后再去运⾏了别⼈的脚本内容
execScripts: (proxy, strictGlobal, execScriptsHooks = {}) => {
//proxy sandboxInstance.proxy
if (!scripts.length) {
solve();
}
return execScripts(entry, scripts, proxy, {
fetch,
strictGlobal,
beforeExec: execScriptsHooks.beforeExec,
afterExec: execScriptsHooks.afterExec,
});
},
}));
}));
}
HTML模板解析
processTpl源码:
const ALL_SCRIPT_REGEX = /(<script[\s\S]*?>)[\s\S]*?<\/script>/gi;
const SCRIPT_TAG_REGEX = /<(script)\s+((?!type=('|')text\/ng-template\3).)*?>.*?<\/\1>/is;
const SCRIPT_SRC_REGEX = /.*\ssrc=('|")?([^>'"\s]+)/;
const SCRIPT_TYPE_REGEX = /.*\stype=('|")?([^>'"\s]+)/;
const SCRIPT_ENTRY_REGEX = /.*\ntry\s*.*/;
const SCRIPT_ASYNC_REGEX = /.*\sasync\s*.*/;
const SCRIPT_NO_MODULE_REGEX = /.*\snomodule\s*.*/;
const SCRIPT_MODULE_REGEX = /.*\stype=('|")?module('|")?\s*.*/;
const LINK_TAG_REGEX = /<(link)\s+.*?>/isg;
const LINK_PRELOAD_OR_PREFETCH_REGEX = /\srel=('|")?(preload|prefetch)\1/;
const LINK_HREF_REGEX = /.*\shref=('|")?([^>'"\s]+)/;
const LINK_AS_FONT = /.*\sas=('|")?font\1.*/;
const STYLE_TAG_REGEX = /<style[^>]*>[\s\S]*?<\/style>/gi;
const STYLE_TYPE_REGEX = /\s+rel=('|")?stylesheet\1.*/;
const STYLE_HREF_REGEX = /.*\shref=('|")?([^>'"\s]+)/;
const HTML_COMMENT_REGEX = /<!--([\s\S]*?)-->/g;
const LINK_IGNORE_REGEX = /<link(\s+|\s+.+\s+)ignore(\s*|\s+.*|=.*)>/is;
const STYLE_IGNORE_REGEX = /<style(\s+|\s+.+\s+)ignore(\s*|\s+.*|=.*)>/is;
const SCRIPT_IGNORE_REGEX = /<script(\s+|\s+.+\s+)ignore(\s*|\s+.*|=.*)>/is;
//在函数所定义的⽂件开头有很多的正则表达式,它们主要是⽤来匹配得到的html的⼀些标签,分别有style标签,link标签,script标签。总之就是和样式,js有关的标签。同时它会特别的关注那些有外部引⽤的标签。
export default function processTpl(tpl, baURI) {
//tpl就是我们的html模板, baURI == localhost:8080
//这个script数组是⽤来存放在html上解析得到含有外部资源引⽤的script标签的资源引⽤地址
  let scripts = [];
//这个是⽤来存放外部引⽤的css标签引⽤路径地址
const styles = [];
let entry = null;
// Detect whether browr supports `<script type=module>` or not
const moduleSupport = isModuleScriptSupported();
//下⾯有若⼲个replace函数,开始对html字符串模板进⾏匹配修改
const template = tpl
/*
remove html comment first
*/
//匹配所有的注释直接移除
.replace(HTML_COMMENT_REGEX, '')
//匹配link
.replace(LINK_TAG_REGEX, match => {
/*
change the css link
*/
       //检查link⾥⾯的type是不是写着stylesheet,就是找样式表
const styleType = !!match.match(STYLE_TYPE_REGEX);
if (styleType) {
//匹配href,找你引⽤的外部css的路径学期计划表
const styleHref = match.match(STYLE_HREF_REGEX);
const styleIgnore = match.match(LINK_IGNORE_REGEX);
          //进⼊if语句说明你的link css含有外部引⽤
if (styleHref) {
//这⾥就是提取出了css的路径
const href = styleHref && styleHref[2];
let newHref = href;
          //我们在单个项⽬的时候,我们的css或者js的引⽤路径很有可能是个相对路径,但是相对路径放在微前端的是不适⽤的,因为你的主项⽬中根本不存在你⼦项⽬的资源⽂件,相对路径⽆法获取得到你的⼦应⽤的资源,只有通            //所以这⾥需要把你所有的相对路径都提取出来,然后根据你最
开始注册⼦应⽤时候传⼊的entry,资源访问的⼊⼝去把你的相对路径和绝对路径进⾏拼接,最后得到⼦应⽤资源的路径
    //hasProtocol这⾥是⽤来检验你写的href是不是⼀个绝对路径
//如果不是的话,他就帮你拼接上变为绝对路径+相对路径的形式。
if (href && !hasProtocol(href)) {
newHref = getEntirePath(href, baURI);
}
if (styleIgnore) {
return genIgnoreAstReplaceSymbol(newHref);同诸公登慈恩寺塔
}
            //把css外部资源的引⽤路径存⼊styles数组。供后⾯正式访问css资源提供⼊⼝
styles.push(newHref);
//这个genLinkReplaceSymbol函数就把你的link注释掉,并且写明你的css已经被import-html-entry⼯具注释掉了
//并且直接去掉你你⾃⼰的css。因为接⼊微前端。⾥⾯原本存在的⼀些资源引⼊是不需要的,因为它们的路径都是错误的。后⾯会有统⼀的资源引⼊的⼊⼝
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_st(match)) {
return genIgnoreAstReplaceSymbol('style file');
}
return match;
})
劳动法丧假//这⾥匹配script标签,处理和css标签类似,也是存放外部js引⽤的路径到scripts数组,然后把你的script标签注释掉
//const ALL_SCRIPT_REGEX = /(<script[\s\S]*?>)[\s\S]*?<\/script>/gi;
.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);
//获取type⾥⾯的值,如果⾥⾯的值是⽆效的就不需要处理,原封不动的返回
const matchedScriptType = matchedScriptTypeMatch && matchedScriptTypeMatch[2];
if (!isValidJavaScriptType(matchedScriptType)) {
return match;
}
// if it is a external script
if (SCRIPT_st(match) && scriptTag.match(SCRIPT_SRC_REGEX)) {
/*
collect scripts and replace the ref
*/
//获得entry字段
const matchedScriptEntry = scriptTag.match(SCRIPT_ENTRY_REGEX);
//获得src⾥⾯的内容
//const SCRIPT_SRC_REGEX = /.*\ssrc=('|")?([^>'"\s]+)/;
const matchedScriptSrcMatch = scriptTag.match(SCRIPT_SRC_REGEX);
let matchedScriptSrc = matchedScriptSrcMatch && matchedScriptSrcMatch[2];
if (entry && matchedScriptEntry) {
throw new SyntaxError('You should not t multiply entry script!');
} el {
// append the domain while the script not have an protocol prefix
//这⾥把src改为绝对路径
if (matchedScriptSrc && !hasProtocol(matchedScriptSrc)) {
matchedScriptSrc = getEntirePath(matchedScriptSrc, baURI);
}
entry = entry || matchedScriptEntry && matchedScriptSrc;
}
if (scriptIgnore) {
return genIgnoreAstReplaceSymbol(matchedScriptSrc || 'js file');
}
if (moduleScriptIgnore) {
return genModuleScriptReplaceSymbol(matchedScriptSrc || 'js file', moduleSupport);
}
//把这些script存⼊数组中,然后注释掉他们
if (matchedScriptSrc) {
//const SCRIPT_ASYNC_REGEX = /.*\sasync\s*.*/;
const asyncScript = !!scriptTag.match(SCRIPT_ASYNC_REGEX);
scripts.push(asyncScript ? { async: true, src: matchedScriptSrc } : matchedScriptSrc);
return genScriptReplaceSymbol(matchedScriptSrc, asyncScript);
}
return match;
} el {
if (scriptIgnore) {
return genIgnoreAstReplaceSymbol('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 the lines are comments.
const isPureCommentBlock = code.split(/[\r\n]+/).every(line => !im() || im().startsWith('//'));
if (!isPureCommentBlock) {
scripts.push(match);
}
return inlineScriptReplaceSymbol;
}
});
怀孕可以吃秋葵吗//过滤掉⼀些空标签
scripts = scripts.filter(function (script) {
// filter empty script
return !!script;
});
return {
template,
scripts,
styles,
// t the last script as entry if have not t
entry: entry || scripts[scripts.length - 1],
};
}
模板解析的过程稍微长⼀些,总结⼀下它做的核⼼事情:
1. 删除html上的注释。
2. 找到link标签中有效的外部css引⽤的路径,并且把他变为绝对路径存⼊styles数组,提供给后⾯资源统⼀引⼊作为⼊⼝
3. 找到script标签处理和link css类似。
4. 最后把处理过后的模板,css引⽤的⼊⼝数组,js引⽤的⼊⼝数组进⾏返回
现在回到importHTML函数中看看处理完模板后⾯做了什么事情。
export default function importHTML(url, opts = {}) {
   。。。省略
return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url)
/
/()下⾯的data就会变成⼀⼤串html respon.json()就是变成json对象,⾃⾏了解window.fetch⽤法
.then(respon => ())
.then(html => {
const astPublicPath = getPublicPath(url);
//processTpl这个拿到了html的模板之后对微应⽤所有的资源引⼊做处理
const { template, scripts, entry, styles } = processTpl(getTemplate(html), astPublicPath);
       //这⾥执⾏getEmbedHTML的作⽤就是根据刚刚模板解析得到的styles路径数组,正式通过fetch去请求获得css资源。
return getEmbedHTML(template, styles, { fetch }).then(embedHTML => ({
...省略
}));
}));
}
getEmbedHTML源码:function getEmbedHTML(template, styles, opts = {}) {
//template, styles, { fetch }
const { fetch = defaultFetch } = opts;
let embedHTML = template;
//getExternalStyleSheets这个函数的作⽤是什么?就是如果在缓存中有了style的样式的话。就直接从缓存获取,没有的话就正式去请求获取资源
return getExternalStyleSheets(styles, fetch)
//getExternalStyleSheets返回了⼀个处理样式⽂件的promi
.then(styleSheets => {
//styleSheets就是整个样式⽂件的字符串这⾥就是开始注⼊style标签,⽣成⼦应⽤的样式
embedHTML = duce((html, styleSrc, i) => {
          //这⾥genLinkReplaceSymbol的作⽤就是根据上⾯在处理html模板的时候把link css注释掉了,然后现在匹配回这个注释,就是找到这个注释的位置,然后替换成为style标签          //说明对于外部的样式引⽤最后通过拿到它的css字符串,然后把全部的外部引⽤都变成style标签的引⽤形式。
html = place(genLinkReplaceSymbol(styleSrc), `<style>/* ${styleSrc} */${styleSheets[i]}</style>`);
return html;
}, embedHTML);
return embedHTML;
});
}
// for prefetch
export function getExternalStyleSheets(styles, fetch = defaultFetch) {
  //第⼀个参数就是存放着有css路径的数组,第⼆个是fetch请求⽅法
return Promi.all(styles.map(styleLink => {
if (isInlineCode(styleLink)) {
// if it is inline style
return getInlineCode(styleLink);
} el {
// external styles
         //先从缓存中寻找,有的话直接从缓存中获取使⽤,没有的话就通过fetch去请求,最后把请求的到的css资源装变为字符串的形式返回      return styleCache[styleLink] ||
(styleCache[styleLink] = fetch(styleLink).then(respon => ()));
}
},
));
}
继续回到importHTML中。
export default function importHTML(url, opts = {}) {
...
return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url)
.then(respon => ())
.then(html => {
const astPublicPath = getPublicPath(url);
const { template, scripts, entry, styles } = processTpl(getTemplate(html), astPublicPath);
       //最后通过getEmbedHTML请求到了css资源并且把这些css资源通过style标签的形式注⼊到了html,重新把新的html返回回来
       //最后then中return了⼀个对象,但是注意现在并没有真正的去引⽤js的资源,js资源在loadApp后⾯进⾏引⼊
return getEmbedHTML(template, styles, { fetch }).then(embedHTML => ({
          //经过注释处理和样式注⼊的html模板
template: embedHTML,
astPublicPath,
          //获取js资源的⽅法5月6日
getExternalScripts: () => getExternalScripts(scripts, fetch),
getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
//下⾯这个函数就是⽤来解析脚本的。后⾯分析这段代码的作⽤
execScripts: (proxy, strictGlobal, execScriptsHooks = {}) => {
//proxy sandboxInstance.proxy
if (!scripts.length) {
solve();
}
return execScripts(entry, scripts, proxy, {
fetch,
strictGlobal,
beforeExec: execScriptsHooks.beforeExec,
afterExec: execScriptsHooks.afterExec,
});
},
}));
}));
}
这⾥总结⼀下整个importEntry做了什么:
1.  请求html模板,进⾏修改处理
2. 请求css资源注⼊到html中
3. 返回⼀个对象,对象的内容含有处理过后的html模板,通过提供获取js资源的⽅法getExternalScripts,和执⾏获取到的js脚本的⽅法execScripts。
回到loadApp⽅法继续解析后⾯的内容
export async function loadApp<T extends object>(

本文发布于:2023-05-18 20:24:56,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/89/914084.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:资源   加载   函数   标签   处理   模板   获取   请求
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图