From 7327a134b18f3a69bac1199131b8c78c39ecaee8 Mon Sep 17 00:00:00 2001 From: xion Date: Sat, 16 Nov 2024 19:16:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0container=E7=89=88?= =?UTF-8?q?=E6=9C=AC=EF=BC=8C=E4=BF=AE=E6=94=B9=E6=96=B0=E7=9A=84=E5=BC=95?= =?UTF-8?q?=E5=85=A5=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- .npmrc | 3 + package.json | 4 +- rollup.config.js | 16 +- src/code-url.ts | 78 ++++++ src/container-edit.ts | 36 ++- src/container-old.ts | 351 +++++++++++++++++++++++++++ src/container.css | 9 +- src/container.ts | 480 +++++++++++++------------------------ src/edit.ts | 4 +- src/handle/adapter.ts | 85 +++++++ src/index.ts | 2 + src/render.ts | 5 +- src/scheduler/index.ts | 1 + src/scheduler/scheduler.ts | 55 +++++ src/store/index.ts | 2 +- src/utils/get-tree.ts | 2 +- 17 files changed, 802 insertions(+), 335 deletions(-) create mode 100644 .npmrc create mode 100644 src/code-url.ts create mode 100644 src/container-old.ts create mode 100644 src/handle/adapter.ts create mode 100644 src/scheduler/index.ts create mode 100644 src/scheduler/scheduler.ts diff --git a/.gitignore b/.gitignore index 385b135..98a634a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ dist build .cache .DS_Store -*.log \ No newline at end of file +*.log + +.turbo \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..a4d9caf --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN} +@abearxiong:registry=https://npm.pkg.github.com +//registry.npmjs.org/:_authToken=${NPM_TOKEN} \ No newline at end of file diff --git a/package.json b/package.json index 526b344..a417c3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kevisual/container", - "version": "0.0.3", + "version": "1.0.0", "description": "", "main": "dist/container.js", "publishConfig": { @@ -37,6 +37,7 @@ "devDependencies": { "@rollup/plugin-commonjs": "^28.0.0", "@rollup/plugin-node-resolve": "^15.3.0", + "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.1.0", "@types/crypto-js": "^4.2.2", "@types/react": "^18.3.4", @@ -53,6 +54,7 @@ "dependencies": { "nanoid": "^5.0.7", "rollup-plugin-dts": "^6.1.1", + "scheduler": "^0.23.2", "zustand": "^4.5.5" } } \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js index aa40b3e..a60c5e5 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -5,6 +5,7 @@ import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import copy from 'rollup-plugin-copy'; import { dts } from 'rollup-plugin-dts'; +import terser from '@rollup/plugin-terser'; /** * @type {import('rollup').RollupOptions} @@ -18,6 +19,15 @@ const config1 = { plugins: [ resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块 commonjs(), // + terser({ + format: { + comments: false, // 移除注释 + }, + compress: { + drop_console: true, // 移除 console.log + drop_debugger: true, // 移除 debugger + }, + }), typescript({ allowImportingTsExtensions: true, noEmit: true, @@ -50,9 +60,9 @@ const config2 = { declaration: false, }), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件 // 复制/src/container.css 到dist/container.css - copy({ - targets: [{ src: 'src/container.css', dest: 'dist' }], - }), + // copy({ + // targets: [{ src: 'src/container.css', dest: 'dist' }], + // }), ], }; const config2Dts = { diff --git a/src/code-url.ts b/src/code-url.ts new file mode 100644 index 0000000..5f3d396 --- /dev/null +++ b/src/code-url.ts @@ -0,0 +1,78 @@ +import { MD5 } from 'crypto-js'; + +type CodeUrl = { + url: string; // 地址 + code?: string; // 代码 + id?: string; // codeId + hash?: string; +}; +export class CodeUrlManager { + urls: CodeUrl[] = []; + urlMap: { [key: string]: CodeUrl } = {}; + constructor() { + this.urls = []; + } + addCode(code: string, id: string, hash?: string) { + const urlMap = this.urlMap; + const resultContent = (content: CodeUrl) => { + const index = this.urls.findIndex((item) => item.id === id); + if (index === -1) { + this.urls.push(content); + } + return content; + }; + if (urlMap[hash]) { + // 可能id不同,但是hash相同。已经加载过了。 + const content = { + ...urlMap[hash], + id, + }; + return resultContent(content); + } + const _hash = hash || MD5(code).toString(); + if (urlMap[_hash]) { + const content = { + ...urlMap[_hash], + id, + }; + return resultContent(content); + } + const index = this.urls.findIndex((item) => item.id === id); + if (index > -1) { + // 没有共同hash的值了,但是还是找了id,这个时候应该移除之前 + const url = this.urls[index]; + const list = this.urls.filter((item) => item.hash === url.hash); + if (list.length <= 1) { + // 这个hash的值没有人用了 + URL.revokeObjectURL(url.url); + this.urlMap[url.hash] = null; + } + this.urls.splice(index, 1); + } + /** + * 全新代码 + * + */ + const url = URL.createObjectURL(new Blob([code], { type: 'application/javascript' })); + const content = { + id, + url, + code, + hash: _hash, + }; + + this.urls.push(content); + urlMap[_hash] = { ...content, id: null }; + return content; + } + removeUrl(id?: string, url?: string) { + const index = this.urls.findIndex((item) => item.url === url || item.id === id); + if (index > -1) { + const url = this.urls[index]; + URL.revokeObjectURL(url.url); + this.urlMap[url.hash] = null; + this.urls.splice(index, 1); + } + } +} +export const codeUrlManager = new CodeUrlManager(); diff --git a/src/container-edit.ts b/src/container-edit.ts index 8e67ead..c2fe43b 100644 --- a/src/container-edit.ts +++ b/src/container-edit.ts @@ -1,8 +1,10 @@ -import { Container, ContainerOpts } from './container'; +import { Container, ContainerOpts, createEmotion } from './container'; import { addListener } from './listener/dom'; + export type ContainerEditOpts = { edit?: boolean; mask?: boolean; + initCss?: string; } & ContainerOpts; let isListener = false; export class ContainerEdit extends Container { @@ -28,9 +30,12 @@ export class ContainerEdit extends Container { addListener(this.root); } } + if (opts.initCss && this.root) { + this.root.classList.add(kvContainerStyle); + } } - renderChildren(cid: string, parentElement?: any, pid?: any) { - const el = super.renderChildren(cid, parentElement, pid); + renderChildren(cid: string, parentElement?: any, pid?: any, isNew = true) { + const el = super.renderChildren(cid, parentElement, pid, isNew); if (el) { const computedStyle = window.getComputedStyle(el); @@ -72,3 +77,28 @@ export class ContainerEdit extends Container { } } } + +const css = `.kv-container.active { + background: #000 ; + border: 2px solid #195ca9; + z-index: 100; +} +.kv-container.dragging { + opacity: 0.5; /* 拖动时降低透明度 */ + box-sizing: content-box; + border: 2px dashed blue; +} +.kv-container.hover { + cursor: move; +} +.kv-container > .resizer, .kv-container > .drag-title { + display: none; +} +.kv-container.active > .resizer, .kv-container.active > .drag-title { + display: block; +} +`; + +export const emotion = createEmotion({ key: 'kv-container-style', speedy: true }); + +const kvContainerStyle = emotion.css(css); diff --git a/src/container-old.ts b/src/container-old.ts new file mode 100644 index 0000000..97cf093 --- /dev/null +++ b/src/container-old.ts @@ -0,0 +1,351 @@ +// @ts-nocheck +import createEmotion from '@emotion/css/create-instance'; +import EventEmitter from 'eventemitter3'; +import { ContainerEvent } from './event/continer'; +import { RenderCode } from './render'; +import { codeUrlManager } from './code-url'; + +const handleOneCode = async (data: RenderData) => { + const { code } = data; + const urlsBegin = ['http', 'https', 'blob', '//:']; + if (typeof code === 'string' && code && urlsBegin.find((item) => code.startsWith(item))) { + const importContent = await import(/* @vite-ignore */ code); + const { render, unmount, ...rest } = importContent || {}; + const _data = { + ...data, + code: { + render, + unmount, + ...rest, + }, + }; + return _data; + } + if (typeof code === 'string' && data.codeId) { + const codeId = data.codeId; + const { url, hash } = codeUrlManager.addCode(code, codeId, data.hash); + const importContent = await import(/* @vite-ignore */ url); + const { render, unmount, ...rest } = importContent || {}; + const _data = { + ...data, + codeId, + hash, + code: { + render, + unmount, + ...rest, + }, + }; + return _data; + } + return data; +}; + +const handleCode = async (data: RenderData[]) => { + const handleData = []; + for (let i = 0; i < data.length; i++) { + const item = data[i]; + const _data = await handleOneCode(item); + handleData.push(_data); + } + return handleData; +}; + +export type RenderData = { + id: string; // id不会重复 + children?: string[]; + parents?: string[]; + className?: string; + style?: React.CSSProperties | { [key: string]: string }; + code: RenderCode | string; + codeId?: string; + shadowRoot?: boolean; + hash?: string; + [key: string]: any; +}; + +export type ContainerOpts = { + root?: HTMLDivElement | string; + shadowRoot?: ShadowRoot; + destroy?: () => void; + data?: RenderData[]; + showChild?: boolean; +}; + +export class Container { + data: RenderData[]; + root: HTMLDivElement; + globalCss: any; + showChild: boolean; + event: EventEmitter; + entryId: string = ''; + loading: boolean = false; + loaded: boolean = false; + constructor(opts: ContainerOpts) { + const data = opts.data || []; + this.loadData.apply(this, [data]); + + const rootElement = typeof opts.root === 'string' ? document.querySelector(opts.root) : opts.root; + this.root = rootElement || (document.body as any); + this.globalCss = createEmotion({ key: 'css-global', speedy: true }).css; + this.showChild = opts.showChild ?? true; + const event = new EventEmitter(); + this.event = event; + const listening = this.root.dataset.listening; + if (listening !== 'true') { + this.root.addEventListener('onContainer', (e: ContainerEvent) => { + const { type, data } = e.data; + this.event.emit(type, data); + }); + } + } + async loadData(data: RenderData[], key: string = '') { + if (data.length === 0) { + this.data = data; + this.loaded = true; + return; + } + this.loading = true; + this.data = await handleCode(data); + this.loading = false; + this.event.emit('loadedData' + key); + this.loaded = true; + } + async getData({ id, codeId }: { id?: string; codeId?: string }) { + if (id && codeId) { + return this.data.find((item) => item.id === id && item.codeId === codeId); + } + if (id) { + return this.data.find((item) => item.id === id); + } + if (codeId) { + return this.data.find((item) => item.codeId === codeId); + } + return null; + } + async updateDataCode(data: RenderData[]) { + if (this.loading) { + console.warn('loading'); + return; + } + const _data = this.data.map((item) => { + const node = data.find((node) => node.codeId && node.codeId === item.codeId); + if (node) { + return { + ...item, + ...node, + }; + } + return item; + }); + await this.loadData(_data); + } + async updateData(data: RenderData[]) { + if (this.loading) { + return; + } + const _data = this.data.map((item) => { + const node = data.find((node) => node.id === item.id); + if (node) { + return { + ...item, + ...node, + }; + } + return item; + }); + console.log('updateData', _data.length, data.length); + await this.loadData(_data); + } + + renderChildren(cid: string, parentElement?: any, pid?: any): void | HTMLDivElement { + const data = this.data; + console.log('renderChildren', cid, data); + const globalCss = this.globalCss; + if (!parentElement) { + parentElement = this.root; + this.root.dataset.entryId = cid; + this.entryId = cid; + } + const renderChildren = this.renderChildren.bind(this); + const node = data.find((node: RenderData) => node.id === cid); + const event = this.event; + if (!node) { + console.warn('node not found', cid); + return; + } + const { style, code } = node; + const el = document.createElement('div'); + const root = parentElement.appendChild(el); + const parentIds = parentElement.dataset.parentIds || ''; + const shadowRoot = node.shadowRoot ? el.attachShadow({ mode: 'open' }) : null; + const { css, sheet, cache } = createEmotion({ key: 'css' }); + el.dataset.cid = cid; + el.dataset.pid = pid; + el.dataset.parentIds = parentIds ? parentIds + ',' + cid : cid; + if (shadowRoot) { + cache.insert = (selector: string, serialized: any, sheet: any, shouldCache: boolean) => { + const style = document.createElement('style'); + style.textContent = selector; + shadowRoot.appendChild(style); + }; + } + if (el.style) { + el.className = !shadowRoot ? globalCss(style as any) : css(style as any); + } + el.classList.add(cid, 'kv-container'); + const { render, unmount } = (code as RenderCode) || {}; + let renderRoot = node.shadowRoot ? shadowRoot : root; + const ctx = { + root: root, + shadowRoot, + renderRoot, + event, + container: this, + code: code as RenderCode, + data: node, + css: shadowRoot ? css : globalCss, + }; + if (render) { + render(ctx); + } else { + // no render, so no insert + } + + if (event) { + const destroy = (id: string) => { + if (id) { + if (id === cid || node?.parents?.find?.((item) => item === id)) { + unmount?.(ctx); // 销毁父亲元素有这个元素的 + event.off('destroy', destroy); // 移除监听 + } else { + // console.warn('destroy id not found, and not find parentIds', id, 'currentid', cid); + } + } else if (!id) { + unmount?.(ctx); // 所有的都销毁 + event.off('destroy', destroy); + } + // 不需要销毁子元素 + }; + event.on('destroy', destroy); + } + if (shadowRoot) { + const slot = document.createElement('slot'); + shadowRoot.appendChild(slot); + } + if (!this.showChild) { + return; + } + const childrenIds = node.children || []; + childrenIds.forEach((childId) => { + renderChildren(childId, root, cid); + }); + return el; + } + async destroy(id?: string) { + if (!id) { + this.root.innerHTML = ''; + } else { + const elements = this.root.querySelectorAll(`[data-cid="${id}"]`); + elements.forEach((element) => element.remove()); + } + this.event.emit('destroy', id); + await new Promise((resolve) => { + setTimeout(() => { + resolve(null); + }, 200); + }); + } + /** + * 只能用一次 + * @param entryId + * @param data + * @param opts + * @returns + */ + async render(entryId: string, data?: RenderData[], opts: { reload?: boolean } = {}) { + if (this.entryId && this.entryId !== entryId) { + await this.destroy(); + } else if (this.entryId) { + this.destroy(entryId); + } + const _data = data || this.data; + this.entryId = entryId; + if (opts?.reload) { + this.loading = true; + await this.loadData.apply(this, [_data]); + } + + if (this.loading || !this.loaded) { + this.event.once('loadedData', () => { + this.renderChildren(entryId); + }); + return; + } else { + this.renderChildren(entryId); + } + } + async hotReload(id: string) { + await this.destroy(id); + // const node = document.querySelector(`[data-cid="${id}"]`); + if (this.loading) { + this.event.once('loadedData', () => { + this.renderChildren(id); + }); + return; + } else { + this.renderChildren(id); + } + } + async reRender() { + await this.destroy(); + this.renderChildren(this.entryId); + } + async renderId(id: string) { + if (!this.entryId) { + this.render(id); + return; + } + if (id === this.entryId) { + this.reRender(); + return; + } + + const node = this.data.find((item) => item.id === id); + if (node?.parents && node.parents.length > 0) { + const parent = node.parents[node.parents.length - 1]; + const parentElement = this.root.querySelector(`[data-cid="${parent}"]`); + await this.destroy(id); + this.renderChildren(id, parentElement, parent); + } + } + close() { + this.destroy(); + const event = this.event; + event && event.removeAllListeners(); + } +} +export class ContainerOne extends Container { + constructor(opts: ContainerOpts) { + super(opts); + } + async renderOne({ code: RenderCode }) { + const newData = { codeId: 'test-reneder-one-code-id', id: 'test-render-one', code: RenderCode }; + console.log('loading', this.loading); + if (this.loading) { + return; + } + console.log('renderOne', newData); + if (this.data.length === 0) { + await this.loadData([newData]); + } else { + await this.updateData([newData]); + } + this.hotReload('test-render-one'); + } +} +export const mount = ({ render, unmount }, root: string | HTMLDivElement) => { + let _root = typeof root === 'string' ? document.querySelector(root) : root; + _root = _root || (document.body as any); + render({ root: _root }); +}; diff --git a/src/container.css b/src/container.css index 9caad27..9f93b48 100644 --- a/src/container.css +++ b/src/container.css @@ -1,5 +1,5 @@ .kv-container.active { - background: #000 !important; + background: #000; border: 2px solid #195ca9; z-index: 100; } @@ -11,10 +11,11 @@ .kv-container.hover { cursor: move; } -.kv-container > .resizer, .kv-container > .drag-title { +.kv-container > .resizer, +.kv-container > .drag-title { display: none; } -.kv-container.active > .resizer, .kv-container.active > .drag-title { +.kv-container.active > .resizer, +.kv-container.active > .drag-title { display: block; } - diff --git a/src/container.ts b/src/container.ts index 73b6763..afe3037 100644 --- a/src/container.ts +++ b/src/container.ts @@ -1,130 +1,12 @@ import createEmotion from '@emotion/css/create-instance'; import EventEmitter from 'eventemitter3'; -import { MD5 } from 'crypto-js'; import { ContainerEvent } from './event/continer'; import { RenderCode } from './render'; +import * as schedule from './scheduler'; +import { CodeUrlAdapter } from './handle/adapter'; +import { codeUrlManager } from './code-url'; -type CodeUrl = { - url: string; // 地址 - code?: string; // 代码 - id?: string; // codeId - hash?: string; -}; -export class CodeUrlManager { - urls: CodeUrl[] = []; - urlMap: { [key: string]: CodeUrl } = {}; - constructor() { - this.urls = []; - } - addCode(code: string, id: string, hash?: string) { - const urlMap = this.urlMap; - const resultContent = (content: CodeUrl) => { - const index = this.urls.findIndex((item) => item.id === id); - if (index === -1) { - this.urls.push(content); - } - return content; - }; - if (urlMap[hash]) { - // 可能id不同,但是hash相同。已经加载过了。 - const content = { - ...urlMap[hash], - id, - }; - return resultContent(content); - } - const _hash = hash || MD5(code).toString(); - if (urlMap[_hash]) { - const content = { - ...urlMap[_hash], - id, - }; - return resultContent(content); - } - const index = this.urls.findIndex((item) => item.id === id); - if (index > -1) { - // 没有共同hash的值了,但是还是找了id,这个时候应该移除之前 - const url = this.urls[index]; - const list = this.urls.filter((item) => item.hash === url.hash); - if (list.length <= 1) { - // 这个hash的值没有人用了 - URL.revokeObjectURL(url.url); - this.urlMap[url.hash] = null; - } - this.urls.splice(index, 1); - } - /** - * 全新代码 - * - */ - const url = URL.createObjectURL(new Blob([code], { type: 'application/javascript' })); - const content = { - id, - url, - code, - hash: _hash, - }; - - this.urls.push(content); - urlMap[_hash] = { ...content, id: null }; - return content; - } - removeUrl(id?: string, url?: string) { - const index = this.urls.findIndex((item) => item.url === url || item.id === id); - if (index > -1) { - const url = this.urls[index]; - URL.revokeObjectURL(url.url); - this.urlMap[url.hash] = null; - this.urls.splice(index, 1); - } - } -} -export const codeUrlManager = new CodeUrlManager(); -const handleOneCode = async (data: RenderData) => { - const { code } = data; - const urlsBegin = ['http', 'https', 'blob', '//:']; - if (typeof code === 'string' && code && urlsBegin.find((item) => code.startsWith(item))) { - const importContent = await import(/* @vite-ignore */ code); - const { render, unmount, ...rest } = importContent || {}; - const _data = { - ...data, - code: { - render, - unmount, - ...rest, - }, - }; - return _data; - } - if (typeof code === 'string' && data.codeId) { - const codeId = data.codeId; - const { url, hash } = codeUrlManager.addCode(code, codeId, data.hash); - const importContent = await import(/* @vite-ignore */ url); - const { render, unmount, ...rest } = importContent || {}; - const _data = { - ...data, - codeId, - hash, - code: { - render, - unmount, - ...rest, - }, - }; - return _data; - } - return data; -}; -const handleCode = async (data: RenderData[]) => { - const handleData = []; - for (let i = 0; i < data.length; i++) { - const item = data[i]; - const _data = await handleOneCode(item); - handleData.push(_data); - } - return handleData; -}; - +export { createEmotion }; export type RenderData = { id: string; // id不会重复 children?: string[]; @@ -141,31 +23,27 @@ export type RenderData = { export type ContainerOpts = { root?: HTMLDivElement | string; shadowRoot?: ShadowRoot; - destroy?: () => void; data?: RenderData[]; - showChild?: boolean; + adapter?: any; }; export class Container { data: RenderData[]; root: HTMLDivElement; globalCss: any; - showChild: boolean; event: EventEmitter; entryId: string = ''; - loading: boolean = false; - loaded: boolean = false; + adapter: CodeUrlAdapter; constructor(opts: ContainerOpts) { const data = opts.data || []; - this.loadData.apply(this, [data]); - + this.data = data; const rootElement = typeof opts.root === 'string' ? document.querySelector(opts.root) : opts.root; this.root = rootElement || (document.body as any); this.globalCss = createEmotion({ key: 'css-global', speedy: true }).css; - this.showChild = opts.showChild ?? true; const event = new EventEmitter(); this.event = event; const listening = this.root.dataset.listening; + this.adapter = opts.adapter || this.createAdapter(); if (listening !== 'true') { this.root.addEventListener('onContainer', (e: ContainerEvent) => { const { type, data } = e.data; @@ -173,68 +51,31 @@ export class Container { }); } } - async loadData(data: RenderData[], key: string = '') { - if (data.length === 0) { - this.data = data; - this.loaded = true; - return; - } - this.loading = true; - this.data = await handleCode(data); - this.loading = false; - this.event.emit('loadedData' + key); - this.loaded = true; + protected createAdapter() { + const adapter = new CodeUrlAdapter({ codeUrlManager }); + this.adapter = adapter; + return adapter; } - async getData({ id, codeId }: { id?: string; codeId?: string }) { - if (id && codeId) { - return this.data.find((item) => item.id === id && item.codeId === codeId); - } - if (id) { - return this.data.find((item) => item.id === id); - } - if (codeId) { - return this.data.find((item) => item.codeId === codeId); - } - return null; - } - async updateDataCode(data: RenderData[]) { - if (this.loading) { - console.warn('loading'); - return; - } - const _data = this.data.map((item) => { - const node = data.find((node) => node.codeId && node.codeId === item.codeId); - if (node) { - return { - ...item, - ...node, - }; - } - return item; - }); - await this.loadData(_data); - } - async updateData(data: RenderData[]) { - if (this.loading) { - return; - } - const _data = this.data.map((item) => { - const node = data.find((node) => node.id === item.id); - if (node) { - return { - ...item, - ...node, - }; - } - return item; - }); - console.log('updateData', _data.length, data.length); - await this.loadData(_data); - } - - renderChildren(cid: string, parentElement?: any, pid?: any): void | HTMLDivElement { + getData({ id }: { id: string }) { + const data = this.data; + const node = data.find((node) => node.id === id); + return node; + } + updateData(data: RenderData[]) { + this.data = this.data.map((node) => { + const find = data.find((item) => item.id === node.id); + return find || node; + }); + } + /** + * 渲染模块 + * @param cid + * @param parentElement + * @param pid + * @returns + */ + renderChildren(cid: string, parentElement?: any, pid?: any, isNew = true): void | HTMLDivElement { const data = this.data; - console.log('renderChildren', cid, data); const globalCss = this.globalCss; if (!parentElement) { parentElement = this.root; @@ -243,20 +84,64 @@ export class Container { } const renderChildren = this.renderChildren.bind(this); const node = data.find((node: RenderData) => node.id === cid); - const event = this.event; if (!node) { console.warn('node not found', cid); return; } + let el; + if (isNew) { + el = document.createElement('div'); + parentElement.appendChild(el); + const parentIds = parentElement.dataset.parentIds || ''; + + el.dataset.cid = cid; + el.dataset.pid = pid; + el.dataset.parentIds = parentIds ? parentIds + ',' + cid : cid; + // 代码渲染 + const { renderFn } = this.getContext({ renderEl: el, entryId: cid }); + schedule.addTask(renderFn, 'high'); + // 代码渲染结束 + } else { + el = parentElement.querySelector(`[data-cid="${cid}"]`); + } + + if (!el) { + console.warn('when no createNewElement el not found', cid); + return; + } + const childrenIds = node.children || []; + childrenIds.forEach((childId) => { + renderChildren(childId, el, cid); + }); + return el; + } + /** + * 渲染根节点 + * @param entryId + * @returns + */ + async renderRoot(entryId: string) { + if (this.entryId && this.entryId !== entryId) { + await this.destroy(); + } else if (this.entryId) { + await this.destroy(entryId); + } + this.renderChildren(entryId); + } + async reRender() { + await this.destroy(); + this.renderChildren(this.entryId); + } + protected getContext({ renderEl, entryId }: { renderEl: HTMLDivElement; entryId: string }) { + const node = this.data.find((node) => node.id === entryId); const { style, code } = node; - const el = document.createElement('div'); - const root = parentElement.appendChild(el); - const parentIds = parentElement.dataset.parentIds || ''; - const shadowRoot = node.shadowRoot ? el.attachShadow({ mode: 'open' }) : null; + const event = this.event; + const globalCss = this.globalCss; + const root = renderEl; + const adapter = this.adapter; + const containerThis = this; + const shadowRoot = node.shadowRoot ? renderEl.attachShadow({ mode: 'open' }) : null; const { css, sheet, cache } = createEmotion({ key: 'css' }); - el.dataset.cid = cid; - el.dataset.pid = pid; - el.dataset.parentIds = parentIds ? parentIds + ',' + cid : cid; if (shadowRoot) { cache.insert = (selector: string, serialized: any, sheet: any, shouldCache: boolean) => { const style = document.createElement('style'); @@ -264,64 +149,94 @@ export class Container { shadowRoot.appendChild(style); }; } - if (el.style) { - el.className = !shadowRoot ? globalCss(style as any) : css(style as any); - } - el.classList.add(cid, 'kv-container'); - const { render, unmount } = (code as RenderCode) || {}; - let renderRoot = node.shadowRoot ? shadowRoot : root; - const ctx = { - root: root, - shadowRoot, - renderRoot, - event, - container: this, - code: code as RenderCode, - data: node, - css: shadowRoot ? css : globalCss, - }; - if (render) { - render(ctx); - } else { - // no render, so no insert - } - - if (event) { - const destroy = (id: string) => { - if (id) { - if (id === cid || node?.parents?.find?.((item) => item === id)) { - unmount?.(ctx); // 销毁父亲元素有这个元素的 - event.off('destroy', destroy); // 移除监听 - } else { - // console.warn('destroy id not found, and not find parentIds', id, 'currentid', cid); - } - } else if (!id) { - unmount?.(ctx); // 所有的都销毁 - event.off('destroy', destroy); - } - // 不需要销毁子元素 - }; - event.on('destroy', destroy); + const cid = entryId; + renderEl.className = ''; + if (renderEl.style) { + renderEl.className = !shadowRoot ? globalCss(style as any) : css(style as any); } + renderEl.classList.add(cid, 'kv-container'); if (shadowRoot) { const slot = document.createElement('slot'); shadowRoot.appendChild(slot); } - if (!this.showChild) { + const renderFn = async () => { + const { code: newCode } = await adapter.handle(node as any); + const { render, unmount } = newCode || {}; + let renderRoot = node.shadowRoot ? shadowRoot : root; + const ctx = { + root: root, + shadowRoot, + renderRoot, + event, + container: containerThis, + code: newCode as RenderCode, + data: node, + css: shadowRoot ? css : globalCss, + }; + if (render) { + render(ctx); + } else { + // no render, so no insert + } + if (event) { + const destroy = (id: string) => { + if (id) { + if (id === cid || node?.parents?.find?.((item) => item === id)) { + unmount?.(ctx); // 销毁父亲元素有这个元素的 + event.off('destroy', destroy); // 移除监听 + } else { + // console.warn('destroy id not found, and not find parentIds', id, 'currentid', cid); + } + } else if (!id) { + unmount?.(ctx); // 所有的都销毁 + event.off('destroy', destroy); + } + // 不需要销毁子元素 + }; + event.on('destroy', destroy); + } + }; + return { renderFn, shadowRoot }; + } + // 更新渲染 + async render(entryId: string, el?: HTMLDivElement) { + if (schedule.getLoading()) { + console.log('schedule is loading'); + } + if (!this.entryId) { + console.warn('root is use renderRoot method, not render'); + return this.renderRoot(entryId); + } + const isRoot = this.entryId === entryId; + if (isRoot) { + await this.reRender(); return; } - const childrenIds = node.children || []; - childrenIds.forEach((childId) => { - renderChildren(childId, root, cid); - }); - return el; + let renderEl = el; + if (!el) { + renderEl = this.root.querySelector(`[data-cid="${entryId}"]`); + } + if (!renderEl) { + console.warn('render element not found', entryId); + return; + } else { + await this.destroy(entryId); + } + const parentElement = renderEl.parentElement; + const pid = renderEl.dataset.pid; + const { renderFn } = this.getContext({ renderEl, entryId }); + schedule.addTask(renderFn, 'high'); + this.renderChildren(entryId, parentElement, pid, false); } + async destroy(id?: string) { if (!id) { this.root.innerHTML = ''; } else { const elements = this.root.querySelectorAll(`[data-cid="${id}"]`); - elements.forEach((element) => element.remove()); + elements.forEach((element) => { + element.innerHTML = ''; + }); } this.event.emit('destroy', id); await new Promise((resolve) => { @@ -330,92 +245,23 @@ export class Container { }, 200); }); } - /** - * 只能用一次 - * @param entryId - * @param data - * @param opts - * @returns - */ - async render(entryId: string, data?: RenderData[], opts: { reload?: boolean } = {}) { - if (this.entryId && this.entryId !== entryId) { - await this.destroy(); - } else if (this.entryId) { - this.destroy(entryId); - } - const _data = data || this.data; - this.entryId = entryId; - if (opts?.reload) { - this.loading = true; - await this.loadData.apply(this, [_data]); - } - - if (this.loading || !this.loaded) { - this.event.once('loadedData', () => { - this.renderChildren(entryId); - }); - return; - } else { - this.renderChildren(entryId); - } - } - async hotReload(id: string) { - await this.destroy(id); - // const node = document.querySelector(`[data-cid="${id}"]`); - if (this.loading) { - this.event.once('loadedData', () => { - this.renderChildren(id); - }); - return; - } else { - this.renderChildren(id); - } - } - async reRender() { - await this.destroy(); - this.renderChildren(this.entryId); - } - async renderId(id: string) { - if (!this.entryId) { - this.render(id); - return; - } - if (id === this.entryId) { - this.reRender(); - return; - } - - const node = this.data.find((item) => item.id === id); - if (node?.parents && node.parents.length > 0) { - const parent = node.parents[node.parents.length - 1]; - const parentElement = this.root.querySelector(`[data-cid="${parent}"]`); - await this.destroy(id); - this.renderChildren(id, parentElement, parent); - } - } close() { this.destroy(); const event = this.event; event && event.removeAllListeners(); } } + export class ContainerOne extends Container { constructor(opts: ContainerOpts) { super(opts); } async renderOne({ code: RenderCode }) { const newData = { codeId: 'test-reneder-one-code-id', id: 'test-render-one', code: RenderCode }; - console.log('loading', this.loading); - if (this.loading) { - return; - } - console.log('renderOne', newData); - if (this.data.length === 0) { - await this.loadData([newData]); - } else { - await this.updateData([newData]); - } - this.hotReload('test-render-one'); + this.data = [newData]; + setTimeout(() => { + this.renderRoot('test-render-one'); + }, 1000); } } export const mount = ({ render, unmount }, root: string | HTMLDivElement) => { diff --git a/src/edit.ts b/src/edit.ts index 90db63c..511f446 100644 --- a/src/edit.ts +++ b/src/edit.ts @@ -1,7 +1,5 @@ export * from './container-edit'; -export * from './container'; - export * from './render'; export * from './event/unload'; @@ -9,3 +7,5 @@ export * from './event/unload'; export * from './event/emitter'; export * from './event/continer'; + +export * from './code-url'; \ No newline at end of file diff --git a/src/handle/adapter.ts b/src/handle/adapter.ts new file mode 100644 index 0000000..29b7ccd --- /dev/null +++ b/src/handle/adapter.ts @@ -0,0 +1,85 @@ +import type { CodeUrlManager } from '../code-url'; +type Fn = (args: any) => any; +export type RenderCode = { + render: Fn; + unmount?: Fn; + [key: string]: any; +}; + +type AdapterData = { + codeId?: string; + code?: string; + hash?: string; + codeUrl?: string; +}; +type CodeAdapterOpts = { + handle?: any; +} & T; +export class CodeAdapter { + handle: (data: AdapterData) => Promise; + constructor(opts: CodeAdapterOpts) { + this.handle = opts.handle; + } + setHandle(handle: (data: AdapterData) => Promise) { + this.handle = handle; + } +} +type Opts = { + codeUrlManager: CodeUrlManager; +}; +export class CodeUrlAdapter extends CodeAdapter { + codeUrlManager: CodeUrlManager; + constructor(opts: CodeAdapterOpts) { + super(opts); + this.handle = (data) => handleOneCode(data, this.codeUrlManager); + this.codeUrlManager = opts.codeUrlManager; + } + setCodeUrlManager(codeUrlManager: CodeUrlManager) { + this.codeUrlManager = codeUrlManager; + } +} +export const handleOneCode = async (data: AdapterData, codeUrlManager: CodeUrlManager) => { + const { code, codeId } = data; + const urlsBegin = ['http', 'https', 'blob']; + + let importUrl = ''; + // codeUrl优先级最高 + if (data?.codeUrl) { + importUrl = data.codeUrl; + } else if (typeof code === 'string' && code && urlsBegin.find((item) => code.startsWith(item))) { + importUrl = code; + } + if (importUrl) { + const importContent = await import(/* @vite-ignore */ importUrl); + const { render, unmount, ...rest } = importContent || {}; + const _data = { + ...data, + code: { + render, + unmount, + ...rest, + }, + }; + return _data; + } + if (typeof code === 'string' && codeId) { + const { url, hash } = codeUrlManager.addCode(code, codeId, data.hash); + const importContent = await import(/* @vite-ignore */ url); + const { render, unmount, ...rest } = importContent || {}; + const _data = { + ...data, + codeId, + hash, + code: { + render, + unmount, + ...rest, + }, + }; + return _data; + } else if (typeof code === 'string' && !codeId) { + console.error('codeId is required', data); + } + // 无需处理 + return data; +}; diff --git a/src/index.ts b/src/index.ts index 4497ca9..67b7354 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,3 +7,5 @@ export * from './event/unload'; export * from './event/emitter'; export * from './event/continer'; + +export * from './code-url'; diff --git a/src/render.ts b/src/render.ts index f2fe538..ab22e04 100644 --- a/src/render.ts +++ b/src/render.ts @@ -1,12 +1,13 @@ import EventEmitter from 'eventemitter3'; import type { Container } from './container'; -import type { ContainerEdit } from './container-edit'; +// import type { ContainerEdit } from './container-edit'; import { Emotion } from '@emotion/css/create-instance'; export type RenderContext = { root?: HTMLDivElement; shadowRoot?: ShadowRoot; - container?: Container | ContainerEdit; + // container?: Container | ContainerEdit; + container?: Container; event?: EventEmitter; code?: { render: Render; diff --git a/src/scheduler/index.ts b/src/scheduler/index.ts new file mode 100644 index 0000000..3521c5c --- /dev/null +++ b/src/scheduler/index.ts @@ -0,0 +1 @@ +export * from './scheduler'; diff --git a/src/scheduler/scheduler.ts b/src/scheduler/scheduler.ts new file mode 100644 index 0000000..c2a5c84 --- /dev/null +++ b/src/scheduler/scheduler.ts @@ -0,0 +1,55 @@ +// 定义任务队列 +const highPriorityTasks = []; +const lowPriorityTasks = []; +let isSchedulerRunning = false; // 调度器状态 + +export const fnvoid = () => {}; +export type Priority = 'high' | 'low'; +// 添加任务的辅助函数 +export const addTask = (callback: any, priority: Priority = 'low') => { + if (priority === 'high') { + highPriorityTasks.push(callback); + } else { + lowPriorityTasks.push(callback); + } + // 如果调度器未运行且有任务,启动调度 + if (!isSchedulerRunning) { + isSchedulerRunning = true; + requestIdleCallback(processTasks); + } +}; + +// 调度函数 +const processTasks: IdleRequestCallback = (deadline) => { + // 优先处理高优先级任务 + while (highPriorityTasks.length && deadline.timeRemaining() > 0) { + const task = highPriorityTasks.shift(); + task(); + } + + // 如果有剩余时间,再处理低优先级任务 + while (lowPriorityTasks.length && deadline.timeRemaining() > 0) { + const task = lowPriorityTasks.shift(); + task(); + } + + // 检查是否还有任务 + if (highPriorityTasks.length || lowPriorityTasks.length) { + requestIdleCallback(processTasks); + } else { + // 没有任务,停止调度器 + isSchedulerRunning = false; + } +}; +// 开始调度 +requestIdleCallback(processTasks); + +// 添加一些测试任务 +// addTask(() => console.log('Low priority task 1'), 'low'); +// addTask(() => console.log('High priority task 1'), 'high'); +// addTask(() => console.log('Low priority task 2'), 'low'); +// addTask(() => console.log('High priority task 2'), 'high'); + +export const getLoading = () => { + return isSchedulerRunning; +}; diff --git a/src/store/index.ts b/src/store/index.ts index 20efd0c..e998b3b 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,4 +1,4 @@ -import { handleCode, RenderData } from '../container'; +import { handleCode, RenderData } from '../container-old'; import { createStore } from 'zustand/vanilla'; export type ContainerStore = { loading: boolean; diff --git a/src/utils/get-tree.ts b/src/utils/get-tree.ts index c06e717..25e8137 100644 --- a/src/utils/get-tree.ts +++ b/src/utils/get-tree.ts @@ -1,4 +1,4 @@ -import { RenderData } from '../container'; +import { RenderData } from '../container-old'; export const getTree = (data: RenderData[], id: string) => { const node = data.find((node) => node.id === id);