/* * vget.js * Copyright (C) 2024 veypi * * Distributed under terms of the MIT license. */ import EventBus from './vbus.js'; import axios from './axios.min.js' import vcss from './vcss.js' import vproxy from './vproxy.js'; import vmessage from './vmessage.js' async function FetchFile(url) { return fetch(url).then((response) => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.text(); }) } var cacheUrl = {} var pendingRequests = {}; let baseFile = '' const envMap = {} async function getEnv(scoped, temp) { scoped = scoped || '' if (!envMap[scoped]) { let baseURL = scoped.startsWith('http') ? scoped : window.location.origin + scoped envMap[scoped] = Object.assign({}, temp, { scoped: scoped, $G: vproxy.Wrap({}), $bus: new EventBus(), $axios: axios.create({ baseURL: baseURL, }), $message: vmessage, $router: null, $emit: null, }) if (scoped === $vhtml.scoped || $vhtml.scoped === null) { envMap[scoped].$router = $vhtml.$router } else { // 对于第三方组件,不配置路由 envMap[scoped].$router = { addRoutes: () => { }, beforeEnter: () => { } } } try { await (await import(baseURL + '/env.js')).default(envMap[scoped]) } catch (e) { console.warn('error loading ' + baseURL + '/env.js: ' + e) } } return envMap[scoped] } /** * @param {string} url * @return {Promise<{heads:HTMLCollection, body: HTMLElement, setup?:Element, scripts:Element[]}, scripts:Element>} */ async function FetchUI(url, env, ignorescoped) { if (!url || url === '/') { url = '/' } if (!url.startsWith('http') && !url.startsWith('@')) { if (!url.startsWith('/')) { url = '/' + url } } let scoped = env?.scoped if (scoped && url.startsWith('/')) { url = scoped + url } if (url.startsWith('@')) { url = url.slice(1) } if (cacheUrl[url]) { return Promise.resolve(cacheUrl[url]) } if (pendingRequests[url]) { return pendingRequests[url]; } let tempenv = {} const promise = fetch(url + "?random=" + Math.random()) .then(async (response) => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } for (const [key, value] of response.headers.entries()) { if (key.startsWith('vhtml-')) { tempenv[key.slice(6)] = value } } let scoped = tempenv.scoped || '' if (url.startsWith('http')) { scoped = new URL(url).origin + scoped tempenv.scoped = scoped } let packEnv = await getEnv(scoped, tempenv) Object.assign(tempenv, packEnv) // Object.seal(tempenv) return response.text() }) .then(txt => { // if (baseFile === txt) { // throw new Error(`HTTP error! status: 404`); // } if (baseFile == '') { baseFile = txt } return ParseUI(txt, tempenv, url, ignorescoped) }).then((parser) => { cacheUrl[url] = parser return parser }) .catch(err => { let errmsg = '404' if (err.message !== 'HTTP error! status: 404') { console.warn(err) } let dom404 = document.createElement('div') dom404.style.cssText = ` backgound:#aaa; height:100%; width: 100%; display:grid; place-items: center; ` dom404.innerHTML = `
404

${url}

` let parser = { heads: [], body: dom404, setup: '', scripts: [], styles: '', txt: '', tmp: '', env: tempenv, err: err, } cacheUrl[url] = parser return parser }) .finally(() => { delete pendingRequests[url]; }); pendingRequests[url] = promise; return promise; } function generateCompactUniqueString() { // 获取当前时间戳,精确到毫秒 const timestamp = new Date().getTime(); let shortenedTimestamp = timestamp.toString(36); if (shortenedTimestamp.length > 4) { shortenedTimestamp = shortenedTimestamp.substring(shortenedTimestamp.length - 4); } const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; let randomPart = ''; for (let i = 0; i < 4; i++) { randomPart += characters.charAt(Math.floor(Math.random() * characters.length)); } // 组合随机部分和时间戳部分,保证总长度为8位 return randomPart + shortenedTimestamp.padStart(4, '0'); } function sync_ref_owner_id(dom, id) { // 子组件根节点不设置data-v, 所以style class 不生效于body,只能通过body {}设置样式 // dom.setAttribute('data-v-' + id, '') Array.from(dom.childNodes).forEach((n) => { if (n.nodeType === 1) { n.setAttribute('vrefof', id) sync_ref_owner_id(n, id) } }) } async function ParseUI(txt, env, turl, ignorescoped) { if (turl === undefined) { turl = '#' + generateCompactUniqueString() } if (turl.endsWith('.html')) { turl = turl.slice(0, -5) } let tmp = new DOMParser().parseFromString(txt, 'text/html') if (tmp.body.hasAttribute('scoped') && !ignorescoped) { throw new Error(`HTTP error! status: 404`); } let target = { url: turl, heads: [], body: document.createElement('div'), setup: undefined, scripts: [], styles: '', txt: txt, env: env, tmp: tmp, customAttrs: {}, } target.heads = Array.from(tmp.querySelector('head')?.children) // target.heads.forEach(h => { // }) if (turl) { tmp.querySelectorAll('style').forEach((s) => { if (s.getAttribute('unscoped') === null) { target.styles += vcss.parse(s.innerHTML, turl) } else { target.styles += s.innerHTML } }) if (target.styles) { const style = document.createElement('style') style.innerHTML = target.styles style.setAttribute('vref', turl) document.head.appendChild(style) } } target.body.append(...tmp.querySelector('body').childNodes) // target.body = tmp.querySelector('body') target.body.querySelectorAll('script').forEach((s) => { let sinner = s.innerHTML.trim() if (sinner == '') { s.remove() return } if (s.hasAttribute('setup')) { target.setup = s } else if (!s.hasAttribute('no-vhtml')) { target.scripts.push(s) } s.remove() }) // target.body.classList = tmp.body.classList Array.from(tmp.body.attributes).forEach((e) => { if (/^[a-zA-Z]/.test(e.name)) { target.body.setAttribute(e.name, e.value) } else { target.customAttrs[e.name] = e.value } }) target.body.setAttribute('vref', turl) sync_ref_owner_id(target.body, turl) if (!ignorescoped) { await loadHeaders(target, env) } return target } async function loadHeaders(target, env) { for (let h of target.heads) { let nodeName = h.nodeName.toLowerCase() if (nodeName === 'link') { LoadLink(h, env) } else if (nodeName === 'script') { await LoadScript(h, env) } else if (nodeName === 'title') { target.title = h.innerText } else { } } } /** * @param {HTMLElement} dom */ function LoadScript(dom, env) { let src = dom.getAttribute('src') let key = dom.getAttribute('key') let scoped = env?.scoped if (scoped && src.startsWith('/')) { src = scoped + src } if (src.startsWith('@')) { src = src.slice(1) } if (src && document.querySelector(`script[src="${src}"]`)) { return } if (key && document.querySelector(`script[key="${key}"]`)) { return } let newDom = document.createElement('script') newDom.src = src newDom.key = key newDom.type = dom.getAttribute('type') || 'text/javascript' return new Promise((resolve, reject) => { newDom.onload = () => { resolve(newDom) }; newDom.onerror = () => reject(new Error(`Failed to load script ${src}`)); document.head.appendChild(newDom) }) } async function LoadLink(dom, env) { let src = dom.getAttribute('href') let key = dom.getAttribute('key') let scoped = env?.scoped if (scoped && src.startsWith('/')) { src = scoped + src } if (src.startsWith('@')) { src = src.slice(1) } if (src && document.querySelector(`link[href="${src}"]`)) { return } if (key && document.querySelector(`link[key="${key}"]`)) { return } dom.setAttribute('href', src) document.head.append(dom) } export default { FetchUI, FetchFile, LoadScript, LoadLink, ParseUI }