mustard-app
Version:
个人前端微应用建设中。。。
1,572 lines (1,540 loc) • 54 kB
JavaScript
const AppName = 'mustard-app'; // 元素名
const LocationPrefix = 'mApp-'; // location path 前缀
const MainMustardApp = 'main'; // 基座标识(禁用)
// 子应用状态
var IAppStatus;
(function (IAppStatus) {
IAppStatus[IAppStatus["create"] = 0] = "create";
IAppStatus[IAppStatus["loading"] = 1] = "loading";
IAppStatus[IAppStatus["mount"] = 2] = "mount";
IAppStatus[IAppStatus["unmount"] = 3] = "unmount";
IAppStatus[IAppStatus["destory"] = 4] = "destory";
IAppStatus[IAppStatus["error"] = -100] = "error"; // 子应用异常
})(IAppStatus || (IAppStatus = {}));
const APPStAtUSCNKEYS = ['create', 'loading', 'mount', 'unmount', 'destory', 'error'];
function isFunction(value) {
return value instanceof Function;
}
function isURL(value) {
return value instanceof URL;
}
/**
* 是否是子应用的state
* @param value
* @returns
*/
function isMustardState(value) {
return value?.isMustard === 'MustardApp';
}
/**
* 是否是生命周期的key
* @param value
* @returns
*/
function isIAppStatusKey(value) {
return APPStAtUSCNKEYS.includes(value);
}
/**
* 是否是远程类型资源
* @param dom
* @returns boolean
*/
function isRemotezElement(dom) {
return dom instanceof HTMLImageElement || dom instanceof HTMLVideoElement || dom instanceof HTMLAudioElement || dom instanceof HTMLSourceElement;
}
/**
* 是否是相对地址
* @param src
* @returns boolean
*/
function isRelativePath(src = '') {
return /^(\.){0,2}\//.test(src);
}
// 发布订阅系统中心
class EventCenter {
eventList = new Map();
/**
* 初始化
* @param name 事件名
* @param options 事件配置
*/
initEvent(name, options = {}) {
this.eventList.set(name, {
data: undefined,
sourceOfData: undefined,
assignment: false,
callbacks: new Set(),
repeatSend: new WeakSet(),
...options
});
}
/**
* 订阅事件
* @param name 事件名
* @param fn 事件
* @param param2.immediately 是否立即执行(对应消息dispatch过)
* @param param2.repeatSend 多次dispatch相同的data,只有第一次生效
*/
on(name, fn, { immediately, repeatSend } = {}) {
if (!isFunction(fn)) {
return;
}
const events = this.eventList.get(name);
if (!events) {
this.initEvent(name, {
callbacks: new Set([fn]),
repeatSend: new WeakSet(repeatSend ? [fn] : [])
});
}
else {
events.callbacks.add(fn);
repeatSend && events.repeatSend.add(fn);
immediately && events.assignment && fn(events.data, undefined, events.sourceOfData);
}
}
/**
* 注销订阅事件
* @param name 事件名
* @param fn 事件 不传递全部清空
*/
off(name, fn) {
const events = this.eventList.get(name);
if (events) {
fn ? events.callbacks.delete(fn) : events.callbacks.clear();
}
}
/**
* 发布消息
* @param name 事件名
* @param source 那个应用发送的消息
* @param data 数据
*/
dispatch(name, source, data) {
const events = this.eventList.get(name);
const isSameData = events?.data === data;
if (events) {
const oldData = events.data;
events.assignment = true;
for (const callback of events.callbacks) {
// 存在且值等于上一次不执行,其他的都执行
if (!(events.repeatSend.has(callback) && isSameData)) {
callback(data, oldData, source);
}
}
events.data = data;
events.assignment = true;
events.sourceOfData = source;
}
else {
this.initEvent(name, {
data: data,
sourceOfData: source,
assignment: true,
callbacks: new Set([])
});
}
}
}
function getEventGlobalLifeKeyByValue(value) {
return `globalLife_${value}`;
}
function getEventGlobalDataChangeKey() {
return 'globalDataChange';
}
function getEventDataKey(name) {
return `data_${name}`;
}
function getEventDataChangeKey(name) {
return `dataChange_${name}`;
}
function getEventLifeKeyByKey(name, key) {
return `life_${name}_${key}`;
}
function getEventLifeKeyByValue(name, value) {
return `life_${name}_${IAppStatus[value]}`;
}
function getEventBindKey(name, method) {
return `bind_${name}_${method}`;
}
// 订阅实例
const eventCenter$1 = new EventCenter();
function setGlobalEvents(options) {
Reflect.ownKeys(options).forEach(key => {
if (isFunction(options[key])) {
if (key === 'dataChange') {
eventCenter$1.off(getEventGlobalDataChangeKey());
eventCenter$1.on(getEventGlobalDataChangeKey(), options[key]);
}
else if (isIAppStatusKey(key)) {
eventCenter$1.off(getEventGlobalLifeKeyByValue(key));
eventCenter$1.on(getEventGlobalLifeKeyByValue(key), options[key]);
}
}
});
}
function globalDataChangeDispatch(name, data) {
eventCenter$1.dispatch(getEventGlobalDataChangeKey(), name, data);
}
function globalLifeDispatch(key, name) {
eventCenter$1.dispatch(getEventGlobalLifeKeyByValue(key), name);
}
// 基座通讯集合
class EventCenterBaseApp {
/**
* 向子应用发送data数据
* @param name 子应用名字
* @param data 发送数据
*/
dispatch(name, data) {
eventCenter$1.dispatch(getEventDataKey(name), MainMustardApp, data);
}
/**
* 订阅props修改事件
* @param name 子应用名字
* @param fn 事件
*/
onData(name, fn) {
eventCenter$1.on(getEventDataChangeKey(name), fn);
}
/**
* 订阅生命事件
* @param name 子应用名字
* @param life 生命周期映射
* @param fn 事件
*/
onLife(name, life, fn) {
eventCenter$1.on(getEventLifeKeyByKey(name, life), fn);
}
/**
* 订阅自定义事件
* @param name 子应用名字
* @param method 自定义方法名
* @param fn 事件
*/
onCustomize(name, method, fn) {
eventCenter$1.on(getEventBindKey(name, method), fn);
}
/**
* 取消订阅生命事件
* @param name 子应用名字
* @param fn 事件
*/
offData(name, fn) {
eventCenter$1.off(getEventDataChangeKey(name), fn);
}
/**
* 取消订阅生命事件
* @param name 子应用名字
* @param life 生命周期映射
* @param fn 事件
*/
offLife(name, life, fn) {
eventCenter$1.off(getEventLifeKeyByKey(name, life), fn);
}
/**
* 取消订阅自定义事件
* @param name 子应用名字
* @param method 自定义方法名
* @param fn 事件
*/
offCustomize(name, method, fn) {
eventCenter$1.off(getEventBindKey(name, method), fn);
}
}
// 子应用通讯类(注入到子应用的window.microApp)
class EventCenterMicroApp {
name;
constructor(name) {
this.name = name;
}
// 添加data事件监听
addDataListener(fn) {
eventCenter$1.on(getEventDataKey(this.name), fn, { immediately: true, repeatSend: true });
}
// 解除data监听
removeDataListener(fn) {
fn && eventCenter$1.off(getEventDataKey(this.name), fn);
}
// 解除所以的data监听事件
clearDateListener() {
eventCenter$1.off(getEventDataKey(this.name));
}
/**
* 发送data数据修改事件
* @param data 发送数据
*/
dispatch(data) {
globalDataChangeDispatch(this.name, data);
eventCenter$1.dispatch(getEventDataChangeKey(this.name), this.name, data);
}
/**
* 发送自定义事件
* @param method
*/
dispatchCustomize(method, data) {
eventCenter$1.dispatch(getEventBindKey(this.name, method), this.name, data);
}
}
// 子应用生命周期通讯类(内部调用)
class EventCenterMicorLife {
name;
constructor(name) {
this.name = name;
}
/**
* 发送生命周期
* @param state 子应用的生命周期
*/
dispatchLife(state) {
globalLifeDispatch(IAppStatus[state], this.name);
eventCenter$1.dispatch(getEventLifeKeyByValue(this.name, state), this.name);
}
}
class ProxyEventListener {
eventLis = new Map();
// 添加事件
addEventListener(type, listener, options) {
if (!this.eventLis.has(type)) {
this.eventLis.set(type, new Map());
}
const listeners = this.eventLis.get(type);
listeners?.set(listener, options);
return window.addEventListener(type, listener, options);
}
// 删除事件
removeEventListener(type, listener, options) {
if (!this.eventLis.has(type)) {
this.eventLis.set(type, new Map());
}
const listeners = this.eventLis.get(type);
if (listeners?.get(listener) === options) {
listeners?.delete(listener);
}
return window.removeEventListener(type, listener, options);
}
// 全部清除事件
clear() {
Array.from(this.eventLis.keys()).forEach(key => {
const listeners = this.eventLis.get(key);
if (listeners instanceof Map) {
Array.from(listeners.keys()).forEach(listener => {
window.removeEventListener(key, listener, listeners.get(listener));
});
}
});
}
}
const mustardAppInfos = window.mustardAppInfos = window?.mustardAppInfos ?? {
currentReadDocMAppName: '', // 子应用临时标识
appInstanceMap: new Map(), // 当前子应用的实例
getAppProxyWindow(appName) {
// eslint-disable-next-line no-use-before-define
const app = getAppFromInstance(appName);
if (app) {
return app.sandbox.proxyWindow;
}
return null;
}
};
/**
* 获取所以实例app.name
* @returns IApp[]
*/
function getAllApp() {
return Array.from(mustardAppInfos.appInstanceMap.keys());
}
/**
* 实例写入缓存
* @param name 子应用标识
* @param app 实例
* @returns
*/
function addInstance(name, app) {
return mustardAppInfos.appInstanceMap.set(name, app);
}
/**
* 删除实例
* @param name 子应用标识
*/
function removeInstance(name) {
mustardAppInfos.appInstanceMap.delete(name);
}
/**
* 获取实例
* @param name 子应用标识
* @returns IApp
*/
function getAppFromInstance(name) {
return mustardAppInfos.appInstanceMap.get(name);
}
/**
* 子应用是否存在
* @param name 子应用标识
* @returns
*/
function appIsExist(name) {
return mustardAppInfos.appInstanceMap.has(name);
}
/**
* 设置子应用标识
* 用于后续同步步骤的消费
* e.g document.querySelector
* @param appName
*/
function setReadDocumentName(appName) {
return mustardAppInfos.currentReadDocMAppName = appName;
}
/**
* 消费标识
* @returns
*/
function consumption() {
const name = mustardAppInfos.currentReadDocMAppName;
mustardAppInfos.currentReadDocMAppName = '';
return name;
}
let templateStyle;
function scopedStyleRule(rule, prefix, path) {
const { selectorText, cssText } = rule;
let str = '';
// 处理选择器
if (/^((html[\s>~,]+body)|(html|body|:root))$/.test(selectorText)) {
// 处理顶层选择器,如 body,html 都转换为 micro-app[name=xxx]
str = cssText.replace(/^((html[\s>~,]+body)|(html|body|:root))/, prefix) + '\n';
}
else {
// 选择器添加前缀
str = cssText.replace(/^[\s\S]*{/, cssHead => {
return cssHead.replace(/(^|,)([^,]+)/g, (_, $1, $2) => {
if (/^[\s]*((html|body|:root)|(html[\s>~]+body))/.test($2)) {
return `${$1} ${$2.replace(/^[\s]*((html[\s>~]+body)|(html|body|:root))/, prefix)}` + '\n';
}
return `${$1} ${prefix} ${$2} \n`;
});
});
}
// 处理样式里的相对地址
return str.replace(/url\(((".+?")|('.+?')|(.+?))\)/gi, (_, url) => {
const src = url.replace(/["']/g, '');
return `url(${getCompletePath(src, path)})`;
});
}
// 处理媒体查询和样式支持查询
function scopedStyleRuleOther(rule, prefix, packName, path) {
// eslint-disable-next-line no-use-before-define
const rules = scopedStyleRules(rule.cssRules, prefix, path);
return `@${packName} ${rule.conditionText} {\n${rules}\n}`;
}
function scopedStyleRules(cssRules, prefix, path) {
let rules = '';
if (cssRules?.length) {
Array.from(cssRules).forEach((cssRule) => {
switch (cssRule.type) {
case 1: // STYLE_RULE
rules += scopedStyleRule(cssRule, prefix, path) + '\n';
break;
case 4: // MEDIA_RULE 媒体查询
rules += scopedStyleRuleOther(cssRule, prefix, 'media', path) + '\n';
break;
case 12: // SUPPORTS_RULE 样式支持
rules += scopedStyleRuleOther(cssRule, prefix, 'supports', path) + '\n';
break;
default:
rules += cssRule.cssText;
break;
}
});
}
return rules;
}
function scopedCSSTextContent(textContent, appName, path) {
const prefix = `mustard-app[name='${appName}'] `; // 样式前缀
if (!templateStyle) {
templateStyle = document.createElement('style');
document.body.appendChild(templateStyle);
if (templateStyle?.sheet) {
templateStyle.sheet.disabled = false; // 样式标记不可用
}
}
if (textContent && templateStyle.sheet?.cssRules) {
templateStyle.textContent = textContent;
const _textContent = scopedStyleRules(templateStyle.sheet.cssRules, prefix, path);
templateStyle.textContent = '';
return _textContent;
}
return textContent;
}
function scopedCSS(styleEle, appName, path) {
const s = new Date();
styleEle.textContent = scopedCSSTextContent(styleEle.textContent, appName, path);
console.log('============', new Date().getTime() - s.getTime());
}
/**
* 获取虚拟路由key
* @param appName
* @returns
*/
function getLocationNameByAppName(appName) {
return LocationPrefix + appName;
}
/**
* 根据相对地址和当前页面地址返回具体资源路径
* @param relativePath 相对地址
* @param absolutePath 当前页面地址
* @returns
*/
function getCompletePath(relativePath, absolutePath) {
if (!absolutePath || !isRelativePath(relativePath))
return relativePath;
return new URL(relativePath, absolutePath).href;
}
/**
* 请求资源
* @param relativePath 相对地址
* @param absolutePath 当前页面地址
* @returns
*/
function fetchSource(relativePath, absolutePath) {
return fetch(getCompletePath(relativePath, absolutePath)).then((res) => {
return res.text();
});
}
/**
* 监听Dom变化
* @param dom 需要监听的dom元素
* @param config 需要监听的范围 e.g 属性变动/子节点变动
* @param callback 监听变动回调函数
*/
function mutationObserver(dom, config, callback) {
const observer = new MutationObserver((mutationsList, observer) => {
observer.disconnect();
callback(mutationsList, observer);
observer.observe(dom, config);
});
// 以上述配置开始观察目标节点
observer.observe(dom, config);
return observer;
}
/**
* 处理子应用的dom
* 1. 加上子应用标识 appName
* 2. 修改ownerDocument,代理到proxydocument
* 3. 特殊dom,特殊处理 e.g 1. 远程资源src 2. 动态style处理(实时加入前缀)
* @param dom
* @param _appName 子应用标识
* @returns
*/
function handleDom(dom, _appName) {
if (!dom)
return dom;
const appName = _appName ?? consumption();
if (appName && !dom?.appName) {
const app = getAppFromInstance(appName);
const proxyWindow = mustardAppInfos.getAppProxyWindow(appName);
const config = {
// 1. 子应用标识
// 2. 判断是否处理过的元素
appName: {
value: appName
},
ownerDocument: {
enumerable: true,
get() {
return proxyWindow?.document ?? document;
}
}
};
// 远程资源地址适配
if (isRemotezElement(dom)) {
mutationObserver(dom, { attributes: true, attributeFilter: ['src'] }, function ([mutations] = []) {
if (mutations.type === 'attributes') {
const target = mutations.target;
target.src = getCompletePath(target.getAttribute('src'), app.url);
}
});
}
// 动态style适配
if (dom instanceof HTMLStyleElement) {
mutationObserver(dom, { childList: true }, function ([mutations] = []) {
if (mutations.type === 'childList') {
mutations.target.textContent = scopedCSSTextContent(mutations.target.textContent, appName);
}
});
}
return Object.defineProperties(dom, config);
}
return dom;
}
/**
* 处理选择器
* e.g.
* 1. head -> mustard-app-head
* 2. body -> mustard-app-body
* @param selectors
*/
function handleSelectors(selectors) {
if (!selectors)
return '';
if (selectors?.trim() === 'head')
return 'mustard-app-head';
if (selectors?.trim() === 'body')
return 'mustard-app-body';
return selectors.split(',').map(_selector => _selector.replace(/(^|,|\s)(head|body)([^a-zA-Z]|$)/g, function (test) {
return test.replace(/(head|body)/, (_, $1) => `mustard-app-${$1}`);
})).join(',');
}
/**
* 获取相对地址
* 根据子应用的appName,从loaction.search 上读取对应数据
* @param appName
* @returns
*/
function getPath(appName) {
const search = location.search;
const searchParams = new URLSearchParams(search);
const href = searchParams.get(`${LocationPrefix}${appName}`) ?? '/';
return decodeURIComponent(href);
}
/**
* 获取地址的URL对象
* @param appName
* @param baseUrl
* @returns
*/
function getURL(appName, baseUrl) {
return new URL(getPath(appName), baseUrl);
}
/**
* 异步下一微任务运行
* @param fn 待运行的方法
*/
function nextTick(fn) {
Promise.resolve().then(() => {
isFunction(fn) && fn();
});
}
function proxyDocument(appName) {
return new Proxy(document, {
get: (target, key) => {
if (key === 'defaultView') {
return getAppFromInstance(appName)?.sandbox?.proxyWindow;
}
// 设置要消费的子应用标识
if (target[key] instanceof Function) {
return function (...args) {
setReadDocumentName(appName);
return target[key].call(target, ...args);
};
}
return target[key];
}
});
}
function changeDomPropety() {
// 修改Document原型链
const createElement = Document.prototype.createElement;
const createElementNS = Document.prototype.createElementNS;
const createTextNode = Document.prototype.createTextNode;
const createComment = Document.prototype.createComment;
const createDocumentFragment = Document.prototype.createDocumentFragment;
const caretRangeFromPoint = Document.prototype.caretRangeFromPoint;
Document.prototype.createElement = function (tagName, ...options) {
return handleDom(createElement.call(this, tagName, ...options));
};
Document.prototype.createElementNS = function (...options) {
return handleDom(createElementNS.call(this, ...options));
};
Document.prototype.createTextNode = function (...options) {
return handleDom(createTextNode.call(this, ...options));
};
Document.prototype.createComment = function (...options) {
return handleDom(createComment.call(this, ...options));
};
Document.prototype.createDocumentFragment = function (...options) {
return handleDom(createDocumentFragment.call(this, ...options));
};
caretRangeFromPoint && (Document.prototype.caretRangeFromPoint = function (...options) {
return handleDom(caretRangeFromPoint.call(this, ...options));
});
const getElementById = Document.prototype.getElementById;
const querySelector = Document.prototype.querySelector;
const querySelectorAll = Document.prototype.querySelectorAll;
const getElementsByClassName = Document.prototype.getElementsByClassName;
const getElementsByTagName = Document.prototype.getElementsByTagName;
const getElementsByName = Document.prototype.getElementsByName;
Document.prototype.getElementById = function (selectors) {
const appName = consumption();
if (appName) {
const miniRootDom = getAppFromInstance(appName)?.container;
const ele = miniRootDom?.querySelector(`#${selectors}`);
return handleDom(ele, appName); // 已经消费过唯一标识,需要手动传递标识
}
return getElementById.call(this, selectors);
};
Document.prototype.querySelector = function (selectors) {
const appName = consumption();
if (appName) {
const miniRootDom = getAppFromInstance(appName)?.container;
const ele = miniRootDom?.querySelector(handleSelectors(selectors));
return handleDom(ele, appName); // 已经消费过唯一标识,需要手动传递标识
}
return querySelector.call(this, selectors);
};
Document.prototype.querySelectorAll = function (selectors) {
const appName = consumption();
if (appName) {
const miniRootDom = getAppFromInstance(appName)?.container;
const eles = miniRootDom?.querySelectorAll(handleSelectors(selectors));
Array.from(eles).forEach((ele) => {
handleDom(ele, appName); // 已经消费过唯一标识,需要手动传递标识
});
return eles;
}
return querySelectorAll.call(this, selectors);
};
Document.prototype.getElementsByClassName = function (selectors) {
const appName = consumption();
if (appName) {
const miniRootDom = getAppFromInstance(appName)?.container;
const eles = miniRootDom?.getElementsByClassName(selectors);
Array.from(eles).forEach((ele) => {
handleDom(ele, appName); // 已经消费过唯一标识,需要手动传递标识
});
return eles;
}
return getElementsByClassName.call(this, selectors);
};
Document.prototype.getElementsByTagName = function (selectors) {
const appName = consumption();
if (appName) {
const miniRootDom = getAppFromInstance(appName)?.container;
const eles = miniRootDom?.getElementsByTagName(handleSelectors(selectors));
Array.from(eles).forEach((ele) => {
handleDom(ele, appName); // 已经消费过唯一标识,需要手动传递标识
});
return eles;
}
return getElementsByTagName.call(this, selectors);
};
Document.prototype.getElementsByName = function (name) {
const appName = consumption();
if (appName) {
const miniRootDom = getAppFromInstance(appName)?.container;
const eles = miniRootDom?.querySelectorAll(`[name=${name}]`);
Array.from(eles).forEach((ele) => {
handleDom(ele, appName); // 已经消费过唯一标识,需要手动传递标识
});
return eles;
}
return getElementsByName.call(this, name);
};
}
/* eslint-disable no-use-before-define */
function encodeState(data, appName, options) {
const app = getAppFromInstance(appName);
const index = getStateIndex(appName) + 1;
// todo origin
const { flushed = false, origin = app?.state?.origin } = options ?? {};
const allAppState = getAllAppState();
return {
...allAppState,
[appName]: {
data,
index,
origin,
flushed
}
};
}
function decodeState(appName) {
const state = history.state;
if (state?.[appName]) {
return state[appName];
}
}
function getAllAppState() {
const state = {
isMustard: 'MustardApp',
[MainMustardApp]: undefined
};
getAllApp().forEach(name => {
if (decodeState(name)) {
state[name] = decodeState(name);
}
});
return state;
}
function getStateIndex(appName) {
return decodeState(appName)?.index ?? 0;
}
function initState(appName, state, unused, url) {
let preState = history.state;
if (!isMustardState(preState)) {
preState = {
isMustard: 'MustardApp',
[MainMustardApp]: preState
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
history.replaceState({
...preState,
[appName]: {
index: 0,
origin: url
}
}, unused, undefined, true);
}
function navigateTo(appName, type, flushed) {
const navigateToMehtods = function (_state, _unused, _url) {
return history[type].call(history, _state, _unused, _url, true);
};
const pathKey = getLocationNameByAppName(appName);
return function (_state, _unused, _url) {
const app = getAppFromInstance(appName);
const preState = decodeState(appName);
if (!_url) {
// 不刷新
return navigateToMehtods(encodeState(_state, appName, {
flushed: type === 'replaceState' ? preState?.flushed : flushed
}), _unused);
}
// 处理_url:相对地址->绝对地址
const url = new URL(isURL(_url) ? _url.href : _url, app.url);
const state = encodeState(_state, appName, {
flushed: type === 'replaceState' ? preState?.flushed : flushed,
origin: !flushed ? app.state.origin : url.href // 不刷新页面 使用当前 app.state.origin,否则使用跳转的地址做文档来源
});
const { pathname: pathnameFromLocation, search: searchFromLocation = '', hash: hashFromLocation } = location;
const searchFromLocationParams = new URLSearchParams(searchFromLocation);
searchFromLocationParams.set(pathKey, encodeURIComponent(url.href)); // 设置app对应的地址
const searchParams = searchFromLocationParams.toString();
app.state = state[appName];
return navigateToMehtods(state, _unused, `${pathnameFromLocation}${searchParams ? '?' + searchParams : ''}${hashFromLocation}`);
};
}
function proxyHistory(appName) {
return new Proxy(history, {
get(target, key) {
if (key === 'pushState') {
return navigateTo(appName, 'pushState');
}
else if (key === 'replaceState') {
return navigateTo(appName, 'replaceState');
}
else if (key === 'state') {
return decodeState(appName)?.data;
}
else if (isFunction(target[key])) {
return target[key].bind(history);
}
return target[key];
}
});
}
// 修改全局history方法
function changeHistoryPropety() {
const pushState = History.prototype.pushState;
const replaceState = History.prototype.replaceState;
function changeState(type) {
const methodState = type === 'pushState' ? pushState : replaceState;
return function (_state, _unused, _url, _isMustard) {
if (_isMustard) {
return methodState.call(this, _state, _unused, _url);
}
else {
const allAppState = getAllAppState();
return methodState.call(this, { ...allAppState, [MainMustardApp]: _state }, _unused, _url);
}
};
}
History.prototype.pushState = changeState('pushState');
History.prototype.replaceState = changeState('replaceState');
}
/**
* 创建子应用的location
*/
function createLocation(appName) {
const assign = navigateTo(appName, 'pushState', true);
const replace = navigateTo(appName, 'replaceState', true);
class Location extends URL {
assign(url) {
const app = getAppFromInstance(appName);
assign('', '', url);
app && app.reload();
}
reload() {
const app = getAppFromInstance(appName);
app && app.reload();
}
replace(url) {
const app = getAppFromInstance(appName);
replace('', '', url);
app && app.reload();
}
toString() {
return this.href;
}
}
return (path, base) => {
return (base ? new Location('' + path, base) : new Location('' + path));
};
}
function proxyLocation(appName, url) {
const _createLocation = createLocation(appName);
const _location = _createLocation(getPath(appName), url);
return new Proxy(_location, {
get(target, key) {
target.href = getURL(appName, url).href;
return target[key];
},
set(target, key, value) {
target.href = getURL(appName, url).href;
const result = target[key] = value;
target.assign(target.href);
return result;
}
});
}
function proxyStorage(appName, _storage) {
const nullObject = JSON.stringify(Object.create(null));
const getItem = _storage.getItem.bind(_storage);
const setItem = _storage.setItem.bind(_storage);
const removeItem = _storage.removeItem.bind(_storage);
const app = getAppFromInstance(appName);
const origin = new URL(app.url)?.origin;
function _getAllItem() {
return JSON.parse(getItem(origin) ?? nullObject);
}
function _getItem(key) {
return _getAllItem()[key];
}
function _setItem(value) {
return setItem(origin, JSON.stringify(value));
}
class Storage {
constructor() {
const data = _getAllItem();
Reflect.ownKeys(data).forEach((key) => {
this[key] = data[key];
});
}
get length() {
const data = _getAllItem();
return Reflect.ownKeys(data)?.length;
}
clear() {
Reflect.ownKeys(this).forEach(key => {
Reflect.deleteProperty(this, key);
});
removeItem(origin);
}
getItem(key) {
return _getItem(key);
}
setItem(key, value) {
const data = _getAllItem();
this[key] = data[key] = value?.toString?.();
return _setItem(data);
}
removeItem(key) {
const data = _getAllItem();
Reflect.deleteProperty(this, key);
const result = Reflect.deleteProperty(data, key);
_setItem(data);
return result;
}
key(index) {
const data = _getAllItem();
return Reflect.ownKeys(data)[index];
}
}
const storage = new Storage();
return new Proxy(storage, {
get(target, key) {
if (key === 'length') {
return target.length;
}
else if (['clear', 'getItem', 'setItem', 'removeItem', 'key'].includes(key)) {
return target[key].bind(target);
}
else {
return _getItem(key);
}
},
set(target, key, value) {
if (key === 'length')
return value;
return target.setItem(key, value);
},
ownKeys() {
const data = _getAllItem();
return Reflect.ownKeys(data);
},
deleteProperty(target, key) {
return target.removeItem(key);
}
});
}
function proxyLocalStorage(appName) {
return proxyStorage(appName, localStorage);
}
function proxySessionStorage(appName) {
return proxyStorage(appName, sessionStorage);
}
class SandBox {
active = false; // 沙箱是否在运行
microWindow = {}; // 代理的对象
injectedKeys = new Set(); // 新添加的属性,在卸载时清空
name; // 沙箱标识同app标识一致
proxyEventListener; // 全局事件代理
proxyWindow; // window 代理
proxyDocument; // document 代理
proxyHistory; // history 代理
proxyLocation; // location 代理
proxyLocalStorage; // localStorage 代理
proxySessionStorage; // sessionStorage 代理
microApp; // 事件通讯
// todo
// url: MustardURL
constructor(name, url) {
this.name = name;
this.proxyLocation = proxyLocation(this.name, url);
this.proxyHistory = proxyHistory(this.name);
this.proxyLocalStorage = proxyLocalStorage(this.name);
this.proxySessionStorage = proxySessionStorage(this.name);
this.proxyDocument = proxyDocument(this.name);
this.proxyEventListener = new ProxyEventListener();
this.microApp = new EventCenterMicroApp(this.name);
this.proxyWindow = new Proxy(this.microWindow, {
// 取值
get: (target, key) => {
// 优先从代理对象上取值
if (Reflect.has(target, key)) {
return Reflect.get(target, key);
}
if (key === 'document') {
return this.proxyDocument;
}
if (key === 'addEventListener') {
return this.proxyEventListener.addEventListener.bind(this.proxyEventListener);
}
if (key === 'history') {
return this.proxyHistory;
}
if (key === 'location') {
return this.proxyLocation;
}
if (key === 'localStorage') {
return this.proxyLocalStorage;
}
if (key === 'sessionStorage') {
return this.proxySessionStorage;
}
if (key === 'microApp') {
return this.microApp;
}
// 否则兜底到window对象上取值
const rawValue = Reflect.get(window, key);
// 如果兜底的值为函数,则需要绑定window对象,如:console、alert等
if (typeof rawValue === 'function') {
const valueStr = rawValue.toString();
// 排除构造函数
if (!/^function\s+[A-Z]/.test(valueStr) && !/^class\s+/.test(valueStr)) {
return rawValue.bind(window);
}
}
// 其它情况直接返回
return rawValue;
},
// 设置变量
set: (target, key, value) => {
// 沙箱只有在运行时可以设置变量
if (this.active) {
Reflect.set(target, key, value);
// 记录添加的变量,用于后续清空操作
this.injectedKeys.add(key);
}
return true;
},
deleteProperty: (target, key) => {
// 当前key存在于代理对象上时才满足删除条件
if (Object.prototype.hasOwnProperty.call(target, key)) {
return Reflect.deleteProperty(target, key);
}
return true;
},
has(target, key) {
return key in target || key in window;
}
});
}
start() {
if (!this.active) {
this.active = true;
}
}
stop() {
if (this.active) {
this.microApp.clearDateListener();
this.active = false;
Array.from(this.injectedKeys.keys()).forEach(key => Reflect.deleteProperty(this.microWindow, key));
this.injectedKeys.clear();
this.proxyEventListener.clear();
}
}
// 修改js作用域
bindScope(code) {
return `
;(function(window, self){
const microApp = window.microApp;
const history = window.history;
const location = window.location;
const document = window.document;
const localStorage = window.localStorage;
const sessionStorage = window.sessionStorage;
const addEventListener = window.addEventListener;
${code}\n;
}).call(
mustardAppInfos.getAppProxyWindow('${this.name}'),
mustardAppInfos.getAppProxyWindow('${this.name}'),
mustardAppInfos.getAppProxyWindow('${this.name}'),
)
`;
}
}
// Unique ID creation requires a high quality random # generator. In the browser we therefore
// require the crypto API and do not support built-in fallback to lower quality random number
// generators (like Math.random()).
let getRandomValues;
const rnds8 = new Uint8Array(16);
function rng() {
// lazy load so that environments that need to polyfill have a chance to do so
if (!getRandomValues) {
// getRandomValues needs to be invoked in a context where "this" is a Crypto implementation.
getRandomValues = typeof crypto !== 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto);
if (!getRandomValues) {
throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported');
}
}
return getRandomValues(rnds8);
}
/**
* Convert array of 16 byte values to UUID string format of the form:
* XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
*/
const byteToHex = [];
for (let i = 0; i < 256; ++i) {
byteToHex.push((i + 0x100).toString(16).slice(1));
}
function unsafeStringify(arr, offset = 0) {
// Note: Be careful editing this code! It's been tuned for performance
// and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434
return byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]];
}
const randomUUID = typeof crypto !== 'undefined' && crypto.randomUUID && crypto.randomUUID.bind(crypto);
var native = {
randomUUID
};
function v4(options, buf, offset) {
if (native.randomUUID && !buf && !options) {
return native.randomUUID();
}
options = options || {};
const rnds = options.random || (options.rng || rng)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
rnds[6] = rnds[6] & 0x0f | 0x40;
rnds[8] = rnds[8] & 0x3f | 0x80; // Copy bytes to buffer, if provided
if (buf) {
offset = offset || 0;
for (let i = 0; i < 16; ++i) {
buf[offset + i] = rnds[i];
}
return buf;
}
return unsafeStringify(rnds);
}
// 请求link
function fetchLinkFormHtml(app, htmlDom) {
const head = htmlDom.querySelector('mustard-app-head');
const linkEntries = Array.from(app.source.links.entries());
// 通过fetch请求所有css资源
const fetchLinkPromise = [];
for (const [url, info] of linkEntries) {
fetchLinkPromise.push(info.isExternal ?
fetchSource(url, app.url) :
Promise.resolve(info.code));
}
Promise.all(fetchLinkPromise).then(res => {
res.forEach(code => {
const styleEle = document.createElement('style');
styleEle.textContent = code;
// 处理css,加上前缀 mustard-app[name='${appName}']
scopedCSS(styleEle, app.name);
head && head.appendChild(styleEle);
});
app.onLoad(htmlDom);
}).catch(error => app.error(error));
}
function fetchScriptFormHtml(app, htmlDom) {
const scriptEntries = Array.from(app.source.scripts.entries());
// 通过fetch请求所有css资源
const fetchPromise = [];
for (const [url, info] of scriptEntries) {
fetchPromise.push(info.isExternal ?
fetchSource(url, app.url) :
Promise.resolve(info.code));
}
Promise.all(fetchPromise).then(res => {
res.forEach((code, i) => {
// 将代码放入缓存,再次渲染时可以从缓存中获取
scriptEntries[i][1].code = code;
});
app.onLoad(htmlDom);
}).catch(error => app.error(error));
}
/**
* 递归并处理dom
* 收集静态的样式和js
* 处理远程资源 e.g img,video,audio
* @param parent
* @param app
*/
function extractSourceDom(parent, app) {
const children = Array.from(parent.children);
children?.length && children.forEach(child => extractSourceDom(child, app));
for (const dom of children) {
// const attrs = dom.getAttributeNames();
// attrs.forEach(attr => {
// // 处理dom上直接绑定的事件
// if(/^on[a-zA_Z]+/.test(attr)){
// const time = attr + v4();
// app.source.domClick += `
// this.${time} = function(){
// ${dom.getAttribute(attr)}}\n;
// `;
// dom.setAttribute(attr,`proxyWindow.${time}()`);
// }
// });
// link 记录并收集,并提取src
if (dom instanceof HTMLLinkElement) {
const href = dom.getAttribute('href');
if (dom.getAttribute('rel') === 'stylesheet' && href) {
app.source.links.set(href, {
code: '',
isExternal: true
});
}
parent.removeChild(dom);
}
else if (dom instanceof HTMLStyleElement) {
// style 记录并收集,并提取code
app.source.links.set(v4(), {
code: dom.textContent ?? ''
});
parent.removeChild(dom);
}
else if (dom instanceof HTMLScriptElement) {
// script 记录并收集,并提取code
const src = dom.getAttribute('src');
// 远程js
if (src) {
app.source.scripts.set(src, {
code: '',
isExternal: true // 是否远程js
});
}
else {
app.source.scripts.set(v4(), {
code: dom.textContent ?? ''
});
}
parent.removeChild(dom);
}
else if (isRemotezElement(dom)) {
// 远程资源相对地址处理
const src = dom.getAttribute('src');
if (isRelativePath(src)) {
dom.setAttribute('src', getCompletePath(src, app.url));
}
}
}
}
function loadHtml(app) {
fetchSource(app.url).then(html => {
html = html
.replace(/<head[^>]*>[\s\S]*?<\/head>/i, match => match.replace(/<head/i, '<mustard-app-head').replace(/<\/head>/i, '</mustard-app-head>')).replace(/<body[^>]*>[\s\S]*?<\/body>/i, match => match.replace(/<body/i, '<mustard-app-body').replace(/<\/body>/i, '</mustard-app-body>'));
// htmlText -> Dom
const Box = document.createElement('div');
Box.innerHTML = html;
// 提取静态js和link,处理style
extractSourceDom(Box, app);
if (app.source.links.size) {
fetchLinkFormHtml(app, Box);
}
else {
app.onLoad(Box);
}
if (app.source.scripts.size) {
fetchScriptFormHtml(app, Box);
}
else {
app.onLoad(Box);
}
}).catch(error => app.error(error));
}
function errorLog(e) {
const message = e instanceof Error ? e.message : e;
console.error(message);
}
function log(...args) {
// eslint-disable-next-line no-console
console.log(...args);
}
class App {
baseUrl;
url;
name;
container;
sandbox;
loadCount = 0;
status = IAppStatus.create;
state; // document 来源
microLifeCenter; // 生命周期通讯
// 存放动态资源
source = {
links: new Map(), // 存放links
scripts: new Map(), // 存放scripts
domClick: '\n;' // 存放 dom attrs 上的事件
};
constructor({ name, url: baseUrl, container }) {
this.name = name;
this.baseUrl = baseUrl;
this.container = container;
this.microLifeCenter = new EventCenterMicorLife(this.name);
this.init();
}
// 刷新 卸载->初始化
reload() {
this.unmount(true);
this.init();
}
// 初始化
init() {
this.status = IAppStatus.create;
this.microLifeCenter.dispatchLife(this.status);
// 初始化立刻存入 mustardAppInfos.appInstanceMap
addInstance(this.name, this);
this.loadCount = 0;
// 设置资源的真正地址
this.url = getURL(this.name, this.baseUrl).href;
this.source = {
links: new Map(), // 存放links
scripts: new Map(), // 存放scripts
domClick: '\n;' // 存放 dom attrs 上的事件
};
// 刷新页面时,为了保证 document.origin 正确,默认取history
this.state = decodeState(this.name);
if (!this.state) {
initState(this.name, '', '', this.url);
this.state = decodeState(this.name);
}
this.status = IAppStatus.loading;
this.microLifeCenter.dispatchLife(this.status);
// 加载对应的资源
loadHtml(this);
// 初始化砂箱
this.sandbox = new SandBox(this.name, this.url);
}
// 资源加载完时执行
onLoad(htmlDom) {
this.loadCount += 1;
// 第二次执行且组件未卸载时执行渲染
if (this.loadCount === 2 && this.status !== IAppStatus.unmount) {
// 执行mount方法
this.mount(htmlDom);
}
}
/**
* 资源加载完成后进行渲染
*/
mount(html) {
this.sandbox.start();
// 克隆DOM节点
const cloneHtml = html.cloneNode(true);
// 创建一个fragment节点作为模版,这样不会产生冗余的元素
const fragment = document.createDocumentFragment();
Array.from(cloneHtml.childNodes).forEach((node) => {
fragment.appendChild(node);
});
// 将格式化后的DOM结构插入到容器中
this.container.appendChild(fragment);
// 执行js
let scripts = '';
this.source.scripts.forEach((info) => {
scripts += info.code + '\n;';
});
{
(0, eval)(this.sandbox.bindScope(scripts + '\n;' + this.source.domClick));
}
// 标记应用为已渲染
this.status = IAppStatus.mount;
this.microLifeCenter.dispatchLife(this.status);
}
/**
* 卸载应用
* 执行关闭沙箱,清空缓存等操作
* @param destory 是否销毁应用
*/
unmount(destory) {
this.sandbox.stop(); // 暂停沙箱
this.status = IAppStatus.unmount;
this.microLifeCenter.dispatchLife(this.status);
if (destory) {
this.destory();
}
}
/**
* 销毁应用
*/
destory() {
this.sandbox = null;
this.container.innerHTML = '';
this.status = IAppStatus.destory;
this.microLifeCenter.dispatchLife(this.status);
removeInstance(this.name);
}
/**
* 子应用加载失败
* @param error 失败原因
*/
error(error) {
errorLog(error);
this.status = IAppStatus.error;
this.microLifeCenter.dispatchLife(this.status);
this.microLifeCenter = null;
}
}
// 监听实时地址与对应的子应用url不匹配问题
function addEventListenerUrl(callback) {
window.addEventListener('popstate', callback);
return function () {
window.removeEventListener('popstate', callback);
};
}
function checkUrl(name) {
const app = getAppFromInstance(name); // 路由history改变前的实例
const previousState = app.state; // history 切换前的 state
const currentState = decodeState(name); // history 切换后的 state
if (currentState && previousState) {
if (currentState.origin !== previousState.origin) {
log(`${name}: 域名不一致: ${previousState.origin}->${currentState.origin}`);
return app.reload();
}
else if (currentState.index < previousState.index && previousState.flushed) {
log(`${name}: history后退: ${previousState.origin}->${currentState.origin}`);
return app.reload();
}
else if (currentState.index > previousState.index && currentState.flushed) {
log(`${name}: history前进: ${previousState.origin}->${currentState.origin}`);
return app.reload();
}
}
// else{
// const origin = currentState?.origin || previousState?.origin;
// if(getURL(name, app.baseUrl)?.href !== origin) {
// log('checkUrl-one', name, getURL(name, app.baseUrl)?.href, origin);
// return app.reload();
// }
// }
app.state = decodeState(name); // 更新后自动刷新state
}
const eventCenter = new EventCenterBaseApp();
const rawSetAttribute = HTMLElement.prototype.setAttribute;
const rawAddEventListener = HTMLElement.prototype.addEventListener;
const rawRemoveEventListener = HTMLElement.prototype.removeEventListener;
class MustardApp extends HTMLElement {
url = ''; // 子应用资源地址
name = ''; // 子应用标识
// keepAlive:boolean = true; // dom移除是否保活
checkUrlStop; // URL 校验关闭
static get observedAttributes() {
return ['name', 'url'];
}
constructor() {
super();
}
// 组件刷新
reload() {
const app = getAppFromInstance(this.name);
if (app) {
app.reload();
}
}
// 子应用添加至页面
connectedCallback() {
if (!appIsExist(this.name) && this.url) {
nextTick(() => {
new App({
url: this.url,
name: this.name,
container: this
});
});
}
// 开启URL校验
this.checkUrlStop = addEventListenerUrl(() => {
checkUrl(this.name);
});
}
// 子应用从页面中移除
disconnectedCallback() {
// 关闭URL校验
this.checkUrlStop?.();
// getAppFromInstance(this.name)?.unmount(true);
}
// 子应用移动至新页面。
adoptedCallback() { }
/**
* 属性变化
* @param name 属性名
* @param oldValue 属性旧值
* @param newValue 属性新值
*/
attributeChangedCallback(name, oldValue, newValue) {
if (!this.name && name === 'name') {
if (newValue === MainMustardApp) {
throw new Error('子应用标识非法');
}
if (appIsExist(name)) {
throw new Error(`子应用标识已存在: ${name}`);
}
else {
this.name = newValue;
}
}
else if (!this.url && name === 'url') {
this.url = newValue;
}
}
setAttribute(key, value) {
if (/^mustard-app/i.test(this.tagName) && key === 'data') {
// 发送数据
eventCenter.dispatch(this.name, value);
}
else {
rawSetAttribute.call(this, key, value);
}
}
addEventListener(type, listener, options) {
if (isFunction(listener)) {
if (type === 'dataChange') {
eventCenter.onData(this.name, listener);
}
else if (isIAppStatusKey(type)) {
eventCenter.onLife(this.name, type, listener);
}
else {
rawAddEventListener.call(this, type, listener, options);
}
}
}
removeEventList