feat: 更新container版本,修改新的引入方式
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -4,3 +4,5 @@ build | ||||
| .cache | ||||
| .DS_Store | ||||
| *.log | ||||
|  | ||||
| .turbo | ||||
							
								
								
									
										3
									
								
								.npmrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.npmrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| //npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN} | ||||
| @abearxiong:registry=https://npm.pkg.github.com | ||||
| //registry.npmjs.org/:_authToken=${NPM_TOKEN} | ||||
| @@ -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" | ||||
|   } | ||||
| } | ||||
| @@ -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 = { | ||||
|   | ||||
							
								
								
									
										78
									
								
								src/code-url.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/code-url.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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(); | ||||
| @@ -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); | ||||
|   | ||||
							
								
								
									
										351
									
								
								src/container-old.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										351
									
								
								src/container-old.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 }); | ||||
| }; | ||||
| @@ -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; | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										480
									
								
								src/container.ts
									
									
									
									
									
								
							
							
						
						
									
										480
									
								
								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) => { | ||||
|   | ||||
| @@ -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'; | ||||
							
								
								
									
										85
									
								
								src/handle/adapter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/handle/adapter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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<T = any> = { | ||||
|   handle?: any; | ||||
| } & T; | ||||
| export class CodeAdapter { | ||||
|   handle: (data: AdapterData) => Promise<any>; | ||||
|   constructor(opts: CodeAdapterOpts) { | ||||
|     this.handle = opts.handle; | ||||
|   } | ||||
|   setHandle(handle: (data: AdapterData) => Promise<any>) { | ||||
|     this.handle = handle; | ||||
|   } | ||||
| } | ||||
| type Opts = { | ||||
|   codeUrlManager: CodeUrlManager; | ||||
| }; | ||||
| export class CodeUrlAdapter extends CodeAdapter { | ||||
|   codeUrlManager: CodeUrlManager; | ||||
|   constructor(opts: CodeAdapterOpts<Opts>) { | ||||
|     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; | ||||
| }; | ||||
| @@ -7,3 +7,5 @@ export * from './event/unload'; | ||||
| export * from './event/emitter'; | ||||
|  | ||||
| export * from './event/continer'; | ||||
|  | ||||
| export * from './code-url'; | ||||
|   | ||||
| @@ -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<T extends { [key: string]: any } = {}> = { | ||||
|   root?: HTMLDivElement; | ||||
|   shadowRoot?: ShadowRoot; | ||||
|   container?: Container | ContainerEdit; | ||||
|   // container?: Container | ContainerEdit; | ||||
|   container?: Container; | ||||
|   event?: EventEmitter; | ||||
|   code?: { | ||||
|     render: Render; | ||||
|   | ||||
							
								
								
									
										1
									
								
								src/scheduler/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/scheduler/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| export * from './scheduler'; | ||||
							
								
								
									
										55
									
								
								src/scheduler/scheduler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/scheduler/scheduler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
| }; | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user