UNPKG

mustard-app

Version:

个人前端微应用建设中。。。

1,574 lines (1,540 loc) 54 kB
'use strict'; 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); } } }