@ksconsole/qiankun-plus
Version:
A completed implementation of Micro Frontends
490 lines (475 loc) • 19.8 kB
JavaScript
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
import _asyncToGenerator from "@babel/runtime/helpers/esm/asyncToGenerator";
import _regeneratorRuntime from "@babel/runtime/regenerator";
/**
* @author Kuitos
* @homepage https://github.com/kuitos/
* @since 2018-08-15 11:37
*/
import { allSettledButCanBreak } from './allSettledButCanBreak';
import processTpl, { genLinkReplaceSymbol, genScriptReplaceSymbol } from './process-tpl';
import { defaultGetPublicPath, evalCode, getGlobalProp, getInlineCode, noteGlobalProps, readResAsString, requestIdleCallback } from './utils';
var styleCache = {};
var scriptCache = {};
var embedHTMLCache = {};
// 方便调试
window.__styleCache = styleCache;
window.__scriptCache = scriptCache;
window.__embedHTMLCache = embedHTMLCache;
if (!window.fetch) {
throw new Error('[import-html-entry] Here is no "fetch" on the window env, you need to polyfill it');
}
var defaultFetch = window.fetch.bind(window);
function defaultGetTemplate(tpl) {
return tpl;
}
// css 路径替换为绝对路径
function resolveCssUrls(cssContent, cssFileUrl) {
// 获取 CSS 文件所在的目录
var cssBaseUrl = cssFileUrl.substring(0, cssFileUrl.lastIndexOf('/') + 1);
// 正则表达式匹配 url() 引用
var urlRegex = /url\((['"]?)([^'")]+)\1\)/g;
// 替换相对路径为绝对路径
return cssContent.replace(urlRegex, function (match, quote, urlPath) {
// 忽略 data URI 和绝对路径
if (/^(data:|https?:|\/)/i.test(urlPath)) {
return match;
}
// 解析相对路径为绝对路径
var absoluteUrl = new URL(urlPath, cssBaseUrl).href;
return 'url('.concat(absoluteUrl, ')');
});
}
/**
* convert external css link to inline style for performance optimization
* @param template
* @param styles
* @param opts
* @return embedHTML
*/
function getEmbedHTML(template, styles) {
var opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
var _opts$fetch = opts.fetch,
fetch = _opts$fetch === void 0 ? defaultFetch : _opts$fetch;
var embedHTML = template;
return _getExternalStyleSheets(styles, fetch).then(function (styleSheets) {
embedHTML = styleSheets.reduce(function (html, styleSheet) {
var styleSrc = styleSheet.src;
var styleSheetContent = resolveCssUrls(styleSheet.value, styleSrc);
html = html.replace(genLinkReplaceSymbol(styleSrc), isInlineCode(styleSrc) ? "".concat(styleSrc) : "<style>/* ".concat(styleSrc, " */").concat(styleSheetContent, "</style>"));
return html;
}, embedHTML);
return embedHTML;
});
}
var isInlineCode = function isInlineCode(code) {
return code.startsWith('<');
};
function getExecutableScript(scriptSrc, scriptText) {
var opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
var proxy = opts.proxy,
strictGlobal = opts.strictGlobal,
_opts$scopedGlobalVar = opts.scopedGlobalVariables,
scopedGlobalVariables = _opts$scopedGlobalVar === void 0 ? [] : _opts$scopedGlobalVar;
var sourceUrl = isInlineCode(scriptSrc) ? '' : "//# sourceURL=".concat(scriptSrc, "\n");
// 将 scopedGlobalVariables 拼接成变量声明,用于缓存全局变量,避免每次使用时都走一遍代理
var scopedGlobalVariableDefinition = scopedGlobalVariables.length ? "const {".concat(scopedGlobalVariables.join(','), "}=this;") : '';
// 通过这种方式获取全局 window,因为 script 也是在全局作用域下运行的,所以我们通过 window.proxy 绑定时也必须确保绑定到全局 window 上
// 否则在嵌套场景下, window.proxy 设置的是内层应用的 window,而代码其实是在全局作用域运行的,会导致闭包里的 window.proxy 取的是最外层的微应用的 proxy
var globalWindow = (0, eval)('window');
globalWindow.proxy = proxy;
// TODO 通过 strictGlobal 方式切换 with 闭包,待 with 方式坑趟平后再合并
return strictGlobal ? scopedGlobalVariableDefinition ? ";(function(){with(this){".concat(scopedGlobalVariableDefinition).concat(scriptText, "\n").concat(sourceUrl, "}}).bind(window.proxy)();") : ";(function(window, self, globalThis){with(window){;".concat(scriptText, "\n").concat(sourceUrl, "}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);") : ";(function(window, self, globalThis){;".concat(scriptText, "\n").concat(sourceUrl, "}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);");
}
// for prefetch
function _getExternalStyleSheets(styles) {
var fetch = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultFetch;
return allSettledButCanBreak(styles.map(/*#__PURE__*/function () {
var _ref = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime.mark(function _callee(styleLink) {
return _regeneratorRuntime.wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
if (!isInlineCode(styleLink)) {
_context.next = 4;
break;
}
return _context.abrupt("return", getInlineCode(styleLink));
case 4:
return _context.abrupt("return", fetch(styleLink).then(function (response) {
if (response.status >= 400) {
throw new Error("".concat(styleLink, " load failed with status ").concat(response.status));
}
return response.text();
}).catch(function (e) {
try {
if (e.message.indexOf(styleLink) === -1) {
e.message = "".concat(styleLink, " ").concat(e.message);
}
} catch (_) {
// e.message 可能是 readonly,此时会触发异常
}
throw e;
}));
case 5:
case "end":
return _context.stop();
}
}, _callee);
}));
return function (_x) {
return _ref.apply(this, arguments);
};
}())).then(function (results) {
return results.map(function (result, i) {
if (result.status === 'fulfilled') {
result.value = {
src: styles[i],
value: result.value
};
}
return result;
}).filter(function (result) {
// 忽略失败的请求,避免异常下载阻塞后续资源加载
if (result.status === 'rejected') {
Promise.reject(result.reason);
}
return result.status === 'fulfilled';
}).map(function (result) {
return result.value;
});
});
}
// for prefetch
export { _getExternalStyleSheets as getExternalStyleSheets };
function _getExternalScripts(scripts) {
var fetch = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultFetch;
var entry = arguments.length > 2 ? arguments[2] : undefined;
var fetchScript = function fetchScript(scriptUrl, opts) {
return fetch(scriptUrl, opts).then(function (response) {
// usually browser treats 4xx and 5xx response of script loading as an error and will fire a script error event
// https://stackoverflow.com/questions/5625420/what-http-headers-responses-trigger-the-onerror-handler-on-a-script-tag/5625603
if (response.status >= 400) {
throw new Error("".concat(scriptUrl, " load failed with status ").concat(response.status));
}
return response.text();
}).catch(function (e) {
try {
if (e.message.indexOf(scriptUrl) === -1) {
e.message = "".concat(scriptUrl, " ").concat(e.message);
}
} catch (_) {
// e.message 可能是 readonly,此时会触发异常
}
throw e;
});
};
// entry js 下载失败应该直接 break
var shouldBreakWhileError = function shouldBreakWhileError(i) {
return scripts[i] === entry;
};
return allSettledButCanBreak(scripts.map(/*#__PURE__*/function () {
var _ref2 = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime.mark(function _callee2(script) {
var src, async, crossOrigin, fetchOpts;
return _regeneratorRuntime.wrap(function _callee2$(_context2) {
while (1) switch (_context2.prev = _context2.next) {
case 0:
if (!(typeof script === 'string')) {
_context2.next = 8;
break;
}
if (!isInlineCode(script)) {
_context2.next = 5;
break;
}
return _context2.abrupt("return", getInlineCode(script));
case 5:
return _context2.abrupt("return", fetchScript(script));
case 6:
_context2.next = 13;
break;
case 8:
// use idle time to load async script
src = script.src, async = script.async, crossOrigin = script.crossOrigin;
fetchOpts = crossOrigin ? {
credentials: 'include'
} : {};
if (!async) {
_context2.next = 12;
break;
}
return _context2.abrupt("return", {
src: src,
async: true,
content: new Promise(function (resolve, reject) {
return requestIdleCallback(function () {
return fetchScript(src, fetchOpts).then(resolve, reject);
});
})
});
case 12:
return _context2.abrupt("return", fetchScript(src, fetchOpts));
case 13:
case "end":
return _context2.stop();
}
}, _callee2);
}));
return function (_x2) {
return _ref2.apply(this, arguments);
};
}()), shouldBreakWhileError).then(function (results) {
return results.map(function (result, i) {
if (result.status === 'fulfilled') {
result.value = {
src: scripts[i],
value: result.value
};
}
return result;
}).filter(function (result) {
// 忽略失败的请求,避免异常下载阻塞后续资源加载
if (result.status === 'rejected') {
Promise.reject(result.reason);
}
return result.status === 'fulfilled';
}).map(function (result) {
return result.value;
});
});
}
export { _getExternalScripts as getExternalScripts };
function throwNonBlockingError(error, msg) {
setTimeout(function () {
console.error(msg);
throw error;
});
}
var supportsUserTiming = typeof performance !== 'undefined' && typeof performance.mark === 'function' && typeof performance.clearMarks === 'function' && typeof performance.measure === 'function' && typeof performance.clearMeasures === 'function';
/**
* FIXME to consistent with browser behavior, we should only provide callback way to invoke success and error event
* @param entry
* @param scripts
* @param proxy
* @param opts
* @returns {Promise<unknown>}
*/
function _execScripts(entry, scripts) {
var proxy = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : window;
var opts = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
var _opts$fetch2 = opts.fetch,
fetch = _opts$fetch2 === void 0 ? defaultFetch : _opts$fetch2,
_opts$strictGlobal = opts.strictGlobal,
strictGlobal = _opts$strictGlobal === void 0 ? false : _opts$strictGlobal,
success = opts.success,
_opts$error = opts.error,
error = _opts$error === void 0 ? function () {} : _opts$error,
_opts$beforeExec = opts.beforeExec,
beforeExec = _opts$beforeExec === void 0 ? function () {} : _opts$beforeExec,
_opts$afterExec = opts.afterExec,
afterExec = _opts$afterExec === void 0 ? function () {} : _opts$afterExec,
_opts$scopedGlobalVar2 = opts.scopedGlobalVariables,
scopedGlobalVariables = _opts$scopedGlobalVar2 === void 0 ? [] : _opts$scopedGlobalVar2;
return _getExternalScripts(scripts, fetch, entry).then(function (scriptsText) {
var geval = function geval(scriptSrc, inlineScript) {
var rawCode = beforeExec(inlineScript, scriptSrc) || inlineScript;
var code = getExecutableScript(scriptSrc, rawCode, {
proxy: proxy,
strictGlobal: strictGlobal,
scopedGlobalVariables: scopedGlobalVariables
});
evalCode(scriptSrc, code);
afterExec(inlineScript, scriptSrc);
};
function exec(scriptSrc, inlineScript, resolve) {
var markName = "Evaluating script ".concat(scriptSrc);
var measureName = "Evaluating Time Consuming: ".concat(scriptSrc);
if (process.env.NODE_ENV === 'development' && supportsUserTiming) {
performance.mark(markName);
}
if (scriptSrc === entry) {
noteGlobalProps(strictGlobal ? proxy : window);
try {
geval(scriptSrc, inlineScript);
var exports = proxy[getGlobalProp(strictGlobal ? proxy : window)] || {};
resolve(exports);
} catch (e) {
// entry error must be thrown to make the promise settled
console.error("[import-html-entry]: error occurs while executing entry script ".concat(scriptSrc));
throw e;
}
} else {
if (typeof inlineScript === 'string') {
try {
if (scriptSrc && scriptSrc.src) {
geval(scriptSrc.src, inlineScript);
} else {
geval(scriptSrc, inlineScript);
}
} catch (e) {
// consistent with browser behavior, any independent script evaluation error should not block the others
throwNonBlockingError(e, "[import-html-entry]: error occurs while executing normal script ".concat(scriptSrc));
}
} else {
// external script marked with async
inlineScript.async && inlineScript.content.then(function (downloadedScriptText) {
return geval(inlineScript.src, downloadedScriptText);
}).catch(function (e) {
throwNonBlockingError(e, "[import-html-entry]: error occurs while executing async script ".concat(inlineScript.src));
});
}
}
if (process.env.NODE_ENV === 'development' && supportsUserTiming) {
performance.measure(measureName, markName);
performance.clearMarks(markName);
performance.clearMeasures(measureName);
}
}
function schedule(i, resolvePromise) {
if (i < scriptsText.length) {
var script = scriptsText[i];
var scriptSrc = script.src;
var inlineScript = script.value;
exec(scriptSrc, inlineScript, resolvePromise);
// resolve the promise while the last script executed and entry not provided
if (!entry && i === scriptsText.length - 1) {
resolvePromise();
} else {
schedule(i + 1, resolvePromise);
}
}
}
return new Promise(function (resolve) {
return schedule(0, success || resolve);
});
}).catch(function (e) {
error();
throw e;
});
}
export { _execScripts as execScripts };
export default function importHTML(url) {
var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var fetch = defaultFetch;
var autoDecodeResponse = false;
var getPublicPath = defaultGetPublicPath;
var getTemplate = defaultGetTemplate;
var postProcessTemplate = opts.postProcessTemplate;
// compatible with the legacy importHTML api
if (typeof opts === 'function') {
fetch = opts;
} else {
// fetch option is availble
if (opts.fetch) {
// fetch is a funciton
if (typeof opts.fetch === 'function') {
fetch = opts.fetch;
} else {
// configuration
fetch = opts.fetch.fn || defaultFetch;
autoDecodeResponse = !!opts.fetch.autoDecodeResponse;
}
}
getPublicPath = opts.getPublicPath || opts.getDomain || defaultGetPublicPath;
getTemplate = opts.getTemplate || defaultGetTemplate;
}
return fetch(url).then(function (response) {
return readResAsString(response, autoDecodeResponse);
}).then(function (html) {
var assetPublicPath = getPublicPath(url);
var _processTpl = processTpl(getTemplate(html), assetPublicPath, postProcessTemplate),
template = _processTpl.template,
scripts = _processTpl.scripts,
entry = _processTpl.entry,
styles = _processTpl.styles;
return getEmbedHTML(template, styles, {
fetch: fetch
}).then(function (embedHTML) {
return {
template: embedHTML,
assetPublicPath: assetPublicPath,
getExternalScripts: function getExternalScripts() {
return _getExternalScripts(scripts, fetch);
},
getExternalStyleSheets: function getExternalStyleSheets() {
return _getExternalStyleSheets(styles, fetch);
},
execScripts: function execScripts(proxy, strictGlobal) {
var opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
if (!scripts.length) {
return Promise.resolve();
}
return _execScripts(entry, scripts, proxy, _objectSpread({
fetch: fetch,
strictGlobal: strictGlobal
}, opts));
}
};
});
});
}
export function importEntry(entry) {
var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var _opts$fetch3 = opts.fetch,
fetch = _opts$fetch3 === void 0 ? defaultFetch : _opts$fetch3,
_opts$getTemplate = opts.getTemplate,
getTemplate = _opts$getTemplate === void 0 ? defaultGetTemplate : _opts$getTemplate,
postProcessTemplate = opts.postProcessTemplate;
var getPublicPath = opts.getPublicPath || opts.getDomain || defaultGetPublicPath;
if (!entry) {
throw new SyntaxError('entry should not be empty!');
}
// html entry
if (typeof entry === 'string') {
return importHTML(entry, {
fetch: fetch,
getPublicPath: getPublicPath,
getTemplate: getTemplate,
postProcessTemplate: postProcessTemplate
});
}
// config entry
if (Array.isArray(entry.scripts) || Array.isArray(entry.styles)) {
var _entry$scripts = entry.scripts,
scripts = _entry$scripts === void 0 ? [] : _entry$scripts,
_entry$styles = entry.styles,
styles = _entry$styles === void 0 ? [] : _entry$styles,
_entry$html = entry.html,
html = _entry$html === void 0 ? '' : _entry$html;
var getHTMLWithStylePlaceholder = function getHTMLWithStylePlaceholder(tpl) {
return styles.reduceRight(function (html, styleSrc) {
return "".concat(genLinkReplaceSymbol(styleSrc)).concat(html);
}, tpl);
};
var getHTMLWithScriptPlaceholder = function getHTMLWithScriptPlaceholder(tpl) {
return scripts.reduce(function (html, scriptSrc) {
return "".concat(html).concat(genScriptReplaceSymbol(scriptSrc));
}, tpl);
};
return getEmbedHTML(getTemplate(getHTMLWithScriptPlaceholder(getHTMLWithStylePlaceholder(html))), styles, {
fetch: fetch
}).then(function (embedHTML) {
return {
template: embedHTML,
assetPublicPath: getPublicPath(entry),
getExternalScripts: function getExternalScripts() {
return _getExternalScripts(scripts, fetch);
},
getExternalStyleSheets: function getExternalStyleSheets() {
return _getExternalStyleSheets(styles, fetch);
},
execScripts: function execScripts(proxy, strictGlobal) {
var opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
if (!scripts.length) {
return Promise.resolve();
}
return _execScripts(scripts[scripts.length - 1], scripts, proxy, _objectSpread({
fetch: fetch,
strictGlobal: strictGlobal
}, opts));
}
};
});
} else {
throw new SyntaxError('entry scripts or styles should be array!');
}
}