From 53764692fd8740ebcf778786c33b0626f1c89d9b Mon Sep 17 00:00:00 2001 From: "wuji.xwt" Date: Sun, 16 Aug 2020 14:13:58 +0800 Subject: [PATCH] feat: load assets for preview --- packages/plugin-sample-preview/src/index.tsx | 27 ++- packages/utils/src/asset.ts | 173 +++++++++++++++++++ packages/utils/src/create-defer.ts | 17 ++ packages/utils/src/script.ts | 54 ++++++ 4 files changed, 267 insertions(+), 4 deletions(-) create mode 100644 packages/utils/src/create-defer.ts create mode 100644 packages/utils/src/script.ts diff --git a/packages/plugin-sample-preview/src/index.tsx b/packages/plugin-sample-preview/src/index.tsx index 27cb93fcc..e5a4609e3 100644 --- a/packages/plugin-sample-preview/src/index.tsx +++ b/packages/plugin-sample-preview/src/index.tsx @@ -2,7 +2,7 @@ import React, { useState, ComponentType } from 'react'; import { Button, Dialog } from '@alifd/next'; import { PluginProps, NpmInfo } from '@ali/lowcode-types'; import { Designer } from '@ali/lowcode-designer'; -import { buildComponents } from '@ali/lowcode-utils'; +import { buildComponents, assetBundle, AssetList, AssetLevel, AssetLoader } from '@ali/lowcode-utils'; import ReactRenderer from '@ali/lowcode-react-renderer'; import './index.scss'; @@ -16,15 +16,34 @@ const SamplePreview = ({ editor }: PluginProps) => { const designer = editor.get(Designer); if (designer) { const assets = await editor.get('assets'); + const { packages } = assets; + const { componentsMap, schema } = designer; + console.info('save schema:', designer, assets); const libraryMap = {}; - assets.packages.forEach(({ package, library }) => { + const libraryAsset: AssetList = []; + packages.forEach(({ package, library, urls }) => { libraryMap[package] = library; + if (urls) { + libraryAsset.push(urls); + } }); + + const vendors = [ + assetBundle(libraryAsset, AssetLevel.Library), + ]; + console.log('libraryMap&vendors', libraryMap, vendors); + + // TODO asset may cause pollution + const assetLoader = new AssetLoader(); + assetLoader.load(libraryAsset); + const components = buildComponents(libraryMap, componentsMap); + console.log('components', components); + setData({ - schema: designer.schema.componentsTree[0], - components: buildComponents(libraryMap, designer.componentsMap), + schema: schema.componentsTree[0], + components, }); setVisible(true); } diff --git a/packages/utils/src/asset.ts b/packages/utils/src/asset.ts index 79ab83531..f6f842160 100644 --- a/packages/utils/src/asset.ts +++ b/packages/utils/src/asset.ts @@ -1,3 +1,7 @@ +import { isCSSUrl } from './is-css-url'; +import { createDefer } from './create-defer'; +import { load, evaluate } from './script'; + export interface AssetItem { type: AssetType; content?: string | null; @@ -88,3 +92,172 @@ export function assetItem(type: AssetType, content?: string | null, level?: Asse id, }; } + +export class StylePoint { + private lastContent: string | undefined; + private lastUrl: string | undefined; + private placeholder: Element | Text; + + constructor(readonly level: number, readonly id?: string) { + let placeholder: any; + if (id) { + placeholder = document.head.querySelector(`style[data-id="${id}"]`); + } + if (!placeholder) { + placeholder = document.createTextNode(''); + const meta = document.head.querySelector(`meta[level="${level}"]`); + if (meta) { + document.head.insertBefore(placeholder, meta); + } else { + document.head.appendChild(placeholder); + } + } + this.placeholder = placeholder; + } + + applyText(content: string) { + if (this.lastContent === content) { + return; + } + this.lastContent = content; + this.lastUrl = undefined; + const element = document.createElement('style'); + element.setAttribute('type', 'text/css'); + if (this.id) { + element.setAttribute('data-id', this.id); + } + element.appendChild(document.createTextNode(content)); + document.head.insertBefore(element, this.placeholder.parentNode === document.head ? this.placeholder.nextSibling : null); + document.head.removeChild(this.placeholder); + this.placeholder = element; + } + + applyUrl(url: string) { + if (this.lastUrl === url) { + return; + } + this.lastContent = undefined; + this.lastUrl = url; + const element = document.createElement('link'); + element.onload = onload; + element.onerror = onload; + + const i = createDefer(); + function onload(e: any) { + element.onload = null; + element.onerror = null; + if (e.type === 'load') { + i.resolve(); + } else { + i.reject(); + } + } + + element.href = url; + element.rel = 'stylesheet'; + if (this.id) { + element.setAttribute('data-id', this.id); + } + document.head.insertBefore(element, this.placeholder.parentNode === document.head ? this.placeholder.nextSibling : null); + document.head.removeChild(this.placeholder); + this.placeholder = element; + return i.promise(); + } +} + +function parseAssetList(scripts: any, styles: any, assets: AssetList, level?: AssetLevel) { + for (const asset of assets) { + parseAsset(scripts, styles, asset, level); + } +} + +function parseAsset(scripts: any, styles: any, asset: Asset | undefined | null, level?: AssetLevel) { + if (!asset) { + return; + } + if (Array.isArray(asset)) { + return parseAssetList(scripts, styles, asset, level); + } + + if (isAssetBundle(asset)) { + if (asset.assets) { + if (Array.isArray(asset.assets)) { + parseAssetList(scripts, styles, asset.assets, asset.level || level); + } else { + parseAsset(scripts, styles, asset.assets, asset.level || level); + } + return; + } + return; + } + + if (!isAssetItem(asset)) { + asset = assetItem(isCSSUrl(asset) ? AssetType.CSSUrl : AssetType.JSUrl, asset, level)!; + } + + let lv = asset.level || level; + + if (!lv || AssetLevel[lv] == null) { + lv = AssetLevel.App; + } + + asset.level = lv; + if (asset.type === AssetType.CSSUrl || asset.type == AssetType.CSSText) { + styles[lv].push(asset); + } else { + scripts[lv].push(asset); + } +} + +export class AssetLoader { + async load(asset: Asset) { + const styles: any = {}; + const scripts: any = {}; + AssetLevels.forEach(lv => { + styles[lv] = []; + scripts[lv] = []; + }); + parseAsset(scripts, styles, asset); + const styleQueue: AssetItem[] = styles[AssetLevel.Environment].concat( + styles[AssetLevel.Library], + styles[AssetLevel.Theme], + styles[AssetLevel.Runtime], + styles[AssetLevel.App], + ); + const scriptQueue: AssetItem[] = scripts[AssetLevel.Environment].concat( + scripts[AssetLevel.Library], + scripts[AssetLevel.Theme], + scripts[AssetLevel.Runtime], + scripts[AssetLevel.App], + ); + await Promise.all( + styleQueue.map(({ content, level, type, id }) => this.loadStyle(content, level!, type === AssetType.CSSUrl, id)), + ); + await Promise.all(scriptQueue.map(({ content, type }) => this.loadScript(content, type === AssetType.JSUrl))); + } + + private stylePoints = new Map(); + private loadStyle(content: string | undefined | null, level: AssetLevel, isUrl?: boolean, id?: string) { + if (!content) { + return; + } + let point: StylePoint | undefined; + if (id) { + point = this.stylePoints.get(id); + if (!point) { + point = new StylePoint(level, id); + this.stylePoints.set(id, point); + } + } else { + point = new StylePoint(level); + } + return isUrl ? point.applyUrl(content) : point.applyText(content); + } + + private loadScript(content: string | undefined | null, isUrl?: boolean) { + if (!content) { + return; + } + return isUrl ? load(content) : evaluate(content); + } +} diff --git a/packages/utils/src/create-defer.ts b/packages/utils/src/create-defer.ts new file mode 100644 index 000000000..e7997365a --- /dev/null +++ b/packages/utils/src/create-defer.ts @@ -0,0 +1,17 @@ +export interface Defer { + resolve(value?: T | PromiseLike): void; + reject(reason?: any): void; + promise(): Promise; +} + +export function createDefer(): Defer { + const r: any = {}; + const promise = new Promise((resolve, reject) => { + r.resolve = resolve; + r.reject = reject; + }); + + r.promise = () => promise; + + return r; +} diff --git a/packages/utils/src/script.ts b/packages/utils/src/script.ts new file mode 100644 index 000000000..81841ff6d --- /dev/null +++ b/packages/utils/src/script.ts @@ -0,0 +1,54 @@ +import { createDefer } from './create-defer'; + +export function evaluate(script: string) { + const scriptEl = document.createElement('script'); + scriptEl.text = script; + document.head.appendChild(scriptEl); + document.head.removeChild(scriptEl); +} + +export function load(url: string) { + const node: any = document.createElement('script'); + + // node.setAttribute('crossorigin', 'anonymous'); + + node.onload = onload; + node.onerror = onload; + + const i = createDefer(); + + function onload(e: any) { + node.onload = null; + node.onerror = null; + if (e.type === 'load') { + i.resolve(); + } else { + i.reject(); + } + // document.head.removeChild(node); + // node = null; + } + + // node.async = true; + node.src = url; + + document.head.appendChild(node); + + return i.promise(); +} + +export function evaluateExpression(expr: string) { + // eslint-disable-next-line no-new-func + const fn = new Function(expr); + return fn(); +} + +export function newFunction(args: string, code: string) { + try { + // eslint-disable-next-line no-new-func + return new Function(args, code); + } catch (e) { + console.warn('Caught error, Cant init func'); + return null; + } +}