mirror of
https://github.com/motajs/template.git
synced 2026-10-17 03:10:17 +08:00
refactor: 加载系统
This commit is contained in:
parent
d3598e9e27
commit
f5a6ed8cf6
@ -1,5 +1,5 @@
|
||||
import {
|
||||
ILoadProgressTotal,
|
||||
ILoadManager,
|
||||
LoadDataType,
|
||||
ILoadTask,
|
||||
LoadTask,
|
||||
@ -51,7 +51,7 @@ export class MotaAssetsLoader implements IMotaAssetsLoader {
|
||||
private readonly rowSplitter: ITextureSplitter<number>;
|
||||
|
||||
constructor(
|
||||
readonly progress: ILoadProgressTotal,
|
||||
readonly progress: ILoadManager,
|
||||
private readonly dataLoader: IMotaDataLoader,
|
||||
private readonly ac: IMotaAudioContext,
|
||||
private readonly sounds: ISoundPlayer<SoundIds>,
|
||||
@ -207,9 +207,11 @@ export class MotaAssetsLoader implements IMotaAssetsLoader {
|
||||
url,
|
||||
identifier: `@system-font/${font}`,
|
||||
dataType: LoadDataType.ArrayBuffer,
|
||||
// @ts-expect-error 需要重构
|
||||
processor: this.fontProcessor,
|
||||
progress: this.progress
|
||||
});
|
||||
task.setProcessor(this.fontProcessor);
|
||||
this.addCustomLoadTask(task, data => this.fontLoaded(font, data));
|
||||
});
|
||||
|
||||
@ -220,6 +222,7 @@ export class MotaAssetsLoader implements IMotaAssetsLoader {
|
||||
url,
|
||||
identifier: `@system-image/${image}`,
|
||||
dataType: LoadDataType.Blob,
|
||||
// @ts-expect-error 需要重构
|
||||
processor: this.imageProcessor,
|
||||
progress: this.progress
|
||||
});
|
||||
@ -238,6 +241,7 @@ export class MotaAssetsLoader implements IMotaAssetsLoader {
|
||||
url,
|
||||
identifier: `@system-sound/${sound}`,
|
||||
dataType: LoadDataType.Uint8Array,
|
||||
// @ts-expect-error 需要重构
|
||||
processor: this.audioProcessor,
|
||||
progress: this.progress
|
||||
});
|
||||
@ -251,6 +255,7 @@ export class MotaAssetsLoader implements IMotaAssetsLoader {
|
||||
url,
|
||||
identifier: `@system-tileset/${tileset}`,
|
||||
dataType: LoadDataType.Blob,
|
||||
// @ts-expect-error 需要重构
|
||||
processor: this.imageProcessor,
|
||||
progress: this.progress
|
||||
});
|
||||
@ -268,6 +273,7 @@ export class MotaAssetsLoader implements IMotaAssetsLoader {
|
||||
url,
|
||||
identifier: `@system-autotile/${key}`,
|
||||
dataType: LoadDataType.Blob,
|
||||
// @ts-expect-error 需要重构
|
||||
processor: this.imageProcessor,
|
||||
progress: this.progress
|
||||
});
|
||||
@ -294,6 +300,7 @@ export class MotaAssetsLoader implements IMotaAssetsLoader {
|
||||
url,
|
||||
identifier: `@system-material/${materialName}`,
|
||||
dataType: LoadDataType.Blob,
|
||||
// @ts-expect-error 需要重构
|
||||
processor: this.imageProcessor,
|
||||
progress: this.progress
|
||||
});
|
||||
@ -308,6 +315,7 @@ export class MotaAssetsLoader implements IMotaAssetsLoader {
|
||||
url: animatesUrl,
|
||||
identifier: '@system-animates',
|
||||
dataType: LoadDataType.Text,
|
||||
// @ts-expect-error 需要重构
|
||||
processor: this.textProcessor,
|
||||
progress: this.progress
|
||||
});
|
||||
@ -454,6 +462,7 @@ export class MotaAssetsLoader implements IMotaAssetsLoader {
|
||||
url: `loadList.json`,
|
||||
dataType: LoadDataType.JSON,
|
||||
identifier: '@system-loadList',
|
||||
// @ts-expect-error 需要重构
|
||||
processor: this.dataLoader.jsonProcessor,
|
||||
progress: { onProgress() {} }
|
||||
});
|
||||
@ -465,6 +474,7 @@ export class MotaAssetsLoader implements IMotaAssetsLoader {
|
||||
url: loadList.file,
|
||||
identifier: `@system-zip/${loadList.file}`,
|
||||
dataType: LoadDataType.ArrayBuffer,
|
||||
// @ts-expect-error 需要重构
|
||||
processor: this.zipProcessor,
|
||||
progress: this.progress
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {
|
||||
ILoadProgressTotal,
|
||||
ILoadManager,
|
||||
ILoadTask,
|
||||
ILoadTaskProcessor,
|
||||
LoadDataType
|
||||
@ -37,7 +37,7 @@ export interface ICompressedMotaAssetsLoadList {
|
||||
|
||||
export interface IMotaAssetsLoader {
|
||||
/** 加载进度对象 */
|
||||
readonly progress: ILoadProgressTotal;
|
||||
readonly progress: ILoadManager;
|
||||
/** 当前是否正在加载 */
|
||||
readonly loading: boolean;
|
||||
/** 当前是否已经加载完毕 */
|
||||
|
||||
@ -63,7 +63,7 @@ export class ClientCore extends CoreState implements IClientCore {
|
||||
readonly bgmPlayer: IBGMPlayer<BgmIds>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
super({});
|
||||
|
||||
//#region Layer 4
|
||||
|
||||
|
||||
@ -74,6 +74,7 @@ export const LoadScene = defineComponent<ILoadProps>(props => {
|
||||
loader.load().then(() => {
|
||||
loadEnd();
|
||||
});
|
||||
// @ts-expect-error 需要重构
|
||||
for await (const _ of loader.progress) {
|
||||
taskProgress.set(loader.progress.getLoadedTasks());
|
||||
byteProgress.set(loader.progress.getLoadedByte());
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {
|
||||
ILoadProgressTotal,
|
||||
ILoadManager,
|
||||
LoadDataType,
|
||||
ILoadTask,
|
||||
ILoadTaskProcessor
|
||||
@ -24,7 +24,7 @@ export class MotaDataLoader implements IMotaDataLoader {
|
||||
|
||||
readonly jsonProcessor: ILoadTaskProcessor<LoadDataType.JSON, any>;
|
||||
|
||||
constructor(readonly progress: ILoadProgressTotal) {
|
||||
constructor(readonly progress: ILoadManager) {
|
||||
this.jsonProcessor = new LoadJSONProcessor();
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {
|
||||
ILoadProgressTotal,
|
||||
ILoadManager,
|
||||
ILoadTask,
|
||||
ILoadTaskProcessor,
|
||||
LoadDataType
|
||||
@ -9,7 +9,7 @@ export type CustomLoadFunc<R> = (data: R) => Promise<void>;
|
||||
|
||||
export interface IMotaDataLoader {
|
||||
/** 加载进度对象 */
|
||||
readonly progress: ILoadProgressTotal;
|
||||
readonly progress: ILoadManager;
|
||||
|
||||
/** json 处理器 */
|
||||
readonly jsonProcessor: ILoadTaskProcessor<LoadDataType.JSON, any>;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { ILoadProgressTotal, LoadProgressTotal } from '@motajs/loader';
|
||||
import { ILoadManager, LoadManager } from '@motajs/loader';
|
||||
import {
|
||||
IRoleFaceBinder,
|
||||
IFaceManager,
|
||||
@ -48,7 +48,7 @@ import {
|
||||
IPathfindingSystem,
|
||||
PathfindingSystem
|
||||
} from '@user/data-system';
|
||||
import { ICoreState, ISaveableExecutor } from './types';
|
||||
import { ICoreState, ICoreStateConfig, ISaveableExecutor } from './types';
|
||||
import {
|
||||
CommonAuraConverter,
|
||||
EnemyLegacyBridge,
|
||||
@ -93,7 +93,7 @@ export class CoreState implements ICoreState {
|
||||
readonly pathfinding: IPathfindingSystem;
|
||||
|
||||
// Layer 3 用户层,也就是最顶层的内容,一般仅用于初始化以及仅供渲染端调用的顶层模块
|
||||
readonly loadProgress: ILoadProgressTotal;
|
||||
readonly loadProgress: ILoadManager;
|
||||
readonly dataLoader: IMotaDataLoader;
|
||||
|
||||
/** 可存档对象映射 */
|
||||
@ -106,7 +106,7 @@ export class CoreState implements ICoreState {
|
||||
ISaveableExecutor<any>
|
||||
> = new Map();
|
||||
|
||||
constructor() {
|
||||
constructor(config: Readonly<ICoreStateConfig>) {
|
||||
//#region L0 初始化
|
||||
|
||||
// 朝向
|
||||
@ -143,7 +143,7 @@ export class CoreState implements ICoreState {
|
||||
const heroState = new HeroState(this, dir8, heroAttribute);
|
||||
this.hero = heroState;
|
||||
|
||||
this.loadProgress = new LoadProgressTotal();
|
||||
this.loadProgress = new LoadManager();
|
||||
this.dataLoader = new MotaDataLoader(this.loadProgress);
|
||||
|
||||
// 怪物管理器
|
||||
@ -323,7 +323,3 @@ export class CoreState implements ICoreState {
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
export function createCoreState(): CoreState {
|
||||
return new CoreState();
|
||||
}
|
||||
|
||||
@ -7,4 +7,4 @@ import { CoreState } from './core';
|
||||
* 此对象是数据端状态,本身不负责任何渲染操作,仅会向渲染端发送数据让渲染端渲染,不要把渲染操作直接放到此对象上,
|
||||
* 否则可能导致录像验证失败。
|
||||
*/
|
||||
export const state = new CoreState();
|
||||
export const state = new CoreState({});
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { IMotaDataLoader, IStateBase } from '@user/data-base';
|
||||
import { ILoadProgressTotal } from '@motajs/loader';
|
||||
import { ILoadManager } from '@motajs/loader';
|
||||
import { IStateSystem } from '@user/data-system';
|
||||
import { ISaveableContent } from '@user/data-common';
|
||||
|
||||
export interface ICoreStateConfig {}
|
||||
|
||||
export interface ISaveableExecutor<T> {
|
||||
/**
|
||||
* 当数据读取后执行的函数,允许对其他存档对象进行读取
|
||||
@ -15,7 +17,7 @@ export interface ISaveableExecutor<T> {
|
||||
export interface ICoreState
|
||||
extends IStateSystem, ISaveableContent<ReadonlyMap<string, unknown>> {
|
||||
/** 加载进度对象 */
|
||||
readonly loadProgress: ILoadProgressTotal;
|
||||
readonly loadProgress: ILoadManager;
|
||||
/** 数据端加载对象 */
|
||||
readonly dataLoader: IMotaDataLoader;
|
||||
|
||||
|
||||
44
packages-user/data-state/test/tileLegacy.test.ts
Normal file
44
packages-user/data-state/test/tileLegacy.test.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TileStore } from '@user/data-common';
|
||||
import { LegacyTileData, TileLegacyBridge } from '../src/legacy/tile';
|
||||
|
||||
describe('TileLegacyBridge events-map contract', () => {
|
||||
// 验证旧样板事件对象会转换为 TileStore 可消费的默认事件映射
|
||||
it('converts legacy events into the TileStore event map', () => {
|
||||
const bridge = new TileLegacyBridge();
|
||||
const store = new TileStore<LegacyTileData>();
|
||||
store.attachLegacyConverter(bridge);
|
||||
const legacy: LegacyTileData = {
|
||||
id: 'yellowWall',
|
||||
cls: 'terrains',
|
||||
events: { 10: 'onEnter', 20: 'onTouch' },
|
||||
noPass: true
|
||||
};
|
||||
|
||||
const converted = store.fromLegacy(1, legacy);
|
||||
|
||||
expect(converted.events).toEqual({ 10: 'onEnter', 20: 'onTouch' });
|
||||
expect(store.getEvent(1)).toEqual(
|
||||
new Map([
|
||||
[10, 'onEnter'],
|
||||
[20, 'onTouch']
|
||||
])
|
||||
);
|
||||
expect(converted.eventPass).toBe(false);
|
||||
expect(converted).not.toHaveProperty('trigger');
|
||||
});
|
||||
|
||||
// 验证旧样板缺少事件输入时仍生成合法的空事件映射
|
||||
it('uses an empty events map when legacy input has no events', () => {
|
||||
const bridge = new TileLegacyBridge();
|
||||
const legacy: LegacyTileData = {
|
||||
id: 'whiteWall',
|
||||
cls: 'terrains'
|
||||
};
|
||||
|
||||
const converted = bridge.fromLegacy(2, legacy);
|
||||
|
||||
expect(converted.events).toEqual({});
|
||||
expect(converted).not.toHaveProperty('trigger');
|
||||
});
|
||||
});
|
||||
@ -29,7 +29,7 @@
|
||||
"27": "No autotile connection data, please ensure you have created autotile connection map.",
|
||||
"28": "Cannot compile map render shader.",
|
||||
"29": "Cannot get uniform location of map render shader.",
|
||||
"30": "",
|
||||
"30": "Load task needs a load starter binding before loading.",
|
||||
"31": "No asset data is specified when rending map.",
|
||||
"32": "Every layer added to map renderer must share the same size. Different layer: $1.",
|
||||
"33": "Map layer transfered to vertex generator must belong to the renderer that the generator use.",
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export * from './progress';
|
||||
export * from './starter';
|
||||
export * from './stream';
|
||||
export * from './task';
|
||||
export * from './types';
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
import { clamp } from 'lodash-es';
|
||||
import { ILoadProgressTotal, ILoadTask, LoadDataType } from './types';
|
||||
import { logger } from '@motajs/common';
|
||||
import { ILoadManager, ILoadManagerHooks, ILoadTask } from './types';
|
||||
import {
|
||||
Hookable,
|
||||
HookController,
|
||||
IHookController,
|
||||
logger
|
||||
} from '@motajs/common';
|
||||
|
||||
export class LoadProgressTotal<
|
||||
T extends LoadDataType = LoadDataType,
|
||||
R = any
|
||||
> implements ILoadProgressTotal<T, R> {
|
||||
export class LoadManager
|
||||
extends Hookable<ILoadManagerHooks>
|
||||
implements ILoadManager
|
||||
{
|
||||
/** 当前已经附着的加载任务 */
|
||||
private readonly attached: Map<ILoadTask<T, R>, number> = new Map();
|
||||
private readonly attached: Map<ILoadTask, number> = new Map();
|
||||
/** 当前已经加载完毕的任务 */
|
||||
readonly loadedTasks: Set<ILoadTask<T, R>> = new Set();
|
||||
readonly loadedTasks: Set<ILoadTask> = new Set();
|
||||
/** 当前已经添加的任务 */
|
||||
readonly addedTasks: Set<ILoadTask<T, R>> = new Set();
|
||||
readonly addedTasks: Set<ILoadTask> = new Set();
|
||||
|
||||
/** 总加载量 */
|
||||
private total: number = 0;
|
||||
@ -23,26 +28,37 @@ export class LoadProgressTotal<
|
||||
/** 兑现当前的 `nextPromise` */
|
||||
private nextResolve: () => void;
|
||||
|
||||
async *[Symbol.asyncIterator]() {
|
||||
while (true) {
|
||||
if (this.loadedTasks.size === this.addedTasks.size) {
|
||||
return;
|
||||
}
|
||||
yield this.nextPromise;
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const { promise, resolve } = Promise.withResolvers<void>();
|
||||
this.nextPromise = promise;
|
||||
this.nextResolve = resolve;
|
||||
}
|
||||
|
||||
addTask(task: ILoadTask<T, R>) {
|
||||
this.addedTasks.add(task);
|
||||
protected createController(
|
||||
hook: Partial<ILoadManagerHooks>
|
||||
): IHookController<ILoadManagerHooks> {
|
||||
return new HookController(this, hook);
|
||||
}
|
||||
|
||||
onProgress(task: ILoadTask<T, R>, loaded: number, total: number): void {
|
||||
addTask(task: ILoadTask) {
|
||||
this.addedTasks.add(task);
|
||||
task.addHook({
|
||||
onProgress: (loaded, total) => this.onProgress(task, loaded, total)
|
||||
});
|
||||
}
|
||||
|
||||
async *load(): AsyncIterable<number> {
|
||||
while (true) {
|
||||
if (this.loadedTasks.size === this.addedTasks.size) {
|
||||
return;
|
||||
}
|
||||
await this.nextPromise;
|
||||
yield this.loaded;
|
||||
}
|
||||
}
|
||||
|
||||
onProgress(task: ILoadTask, loaded: number, total: number): void {
|
||||
if (!this.addedTasks.has(task)) {
|
||||
logger.warn(95);
|
||||
return;
|
||||
|
||||
11
packages/loader/src/starter.ts
Normal file
11
packages/loader/src/starter.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { ILoadTaskRequest, ILoadTaskStarter } from './types';
|
||||
|
||||
export class WebLoadStarter implements ILoadTaskStarter {
|
||||
start(request: ILoadTaskRequest): Promise<Response> {
|
||||
return fetch(request.url, {
|
||||
method: request.method,
|
||||
body: request.body,
|
||||
headers: request.headers
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -2,26 +2,39 @@ import { sumBy } from 'lodash-es';
|
||||
import {
|
||||
ILoadDataTypeMap,
|
||||
ILoadTask,
|
||||
ILoadTaskHooks,
|
||||
ILoadTaskInit,
|
||||
ILoadTaskProcessor,
|
||||
ILoadTaskProgress,
|
||||
ILoadTaskStarter,
|
||||
IResponseLike,
|
||||
IResponseReaderLike,
|
||||
LoadDataType,
|
||||
RequestMethod
|
||||
} from './types';
|
||||
import {
|
||||
Hookable,
|
||||
HookController,
|
||||
IHookController,
|
||||
logger
|
||||
} from '@motajs/common';
|
||||
|
||||
/** 文字解码 */
|
||||
const loadTextDecoder = new TextDecoder();
|
||||
|
||||
export class LoadTask<T extends LoadDataType, R> implements ILoadTask<T, R> {
|
||||
export class LoadTask<T extends LoadDataType, R>
|
||||
extends Hookable<ILoadTaskHooks<T, R>>
|
||||
implements ILoadTask<T, R>
|
||||
{
|
||||
readonly dataType: T;
|
||||
readonly identifier: string;
|
||||
readonly url: string | URL;
|
||||
readonly processor: ILoadTaskProcessor<T, R>;
|
||||
readonly progress: ILoadTaskProgress<T, R>;
|
||||
readonly url: string;
|
||||
readonly method?: RequestMethod;
|
||||
readonly body?: BodyInit;
|
||||
readonly headers?: HeadersInit;
|
||||
|
||||
processor: ILoadTaskProcessor<T, R> | null = null;
|
||||
starter: ILoadTaskStarter | null = null;
|
||||
|
||||
contentLoaded: boolean = false;
|
||||
loadedByte: number = 0;
|
||||
totalByte: number = 0;
|
||||
@ -34,12 +47,12 @@ export class LoadTask<T extends LoadDataType, R> implements ILoadTask<T, R> {
|
||||
/** 加载结果 */
|
||||
private loadedData: R | null = null;
|
||||
|
||||
constructor(init: ILoadTaskInit<T, R>) {
|
||||
constructor(init: ILoadTaskInit<T>) {
|
||||
super();
|
||||
|
||||
this.dataType = init.dataType;
|
||||
this.identifier = init.identifier;
|
||||
this.url = this.resolveURL(init.url);
|
||||
this.processor = init.processor;
|
||||
this.progress = init.progress;
|
||||
this.url = init.url;
|
||||
this.method = init.method;
|
||||
this.body = init.body;
|
||||
this.headers = init.headers;
|
||||
@ -49,16 +62,26 @@ export class LoadTask<T extends LoadDataType, R> implements ILoadTask<T, R> {
|
||||
this.loadResolve = resolve;
|
||||
}
|
||||
|
||||
private resolveURL(url: string | URL) {
|
||||
if (typeof url === 'string') {
|
||||
return `${import.meta.env.BASE_URL}${url}`;
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
protected createController(
|
||||
hook: Partial<ILoadTaskHooks<T, R>>
|
||||
): IHookController<ILoadTaskHooks<T, R>> {
|
||||
return new HookController(this, hook);
|
||||
}
|
||||
|
||||
setProcessor(processor: ILoadTaskProcessor<T, R> | null): void {
|
||||
this.processor = processor;
|
||||
}
|
||||
|
||||
setStarter(starter: ILoadTaskStarter): void {
|
||||
this.starter = starter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当不使用流加载时,使用此方法直接处理响应对象
|
||||
* @param response 响应体对象
|
||||
*/
|
||||
private processUnstreamableResponse(
|
||||
response: Response
|
||||
response: IResponseLike
|
||||
): Promise<ILoadDataTypeMap[T]> {
|
||||
switch (this.dataType) {
|
||||
case LoadDataType.ArrayBuffer:
|
||||
@ -74,6 +97,10 @@ export class LoadTask<T extends LoadDataType, R> implements ILoadTask<T, R> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当使用流加载时,处理每次流加载的分块
|
||||
* @param chunks 流式加载的二进制分块
|
||||
*/
|
||||
private processStreamChunkResponse(
|
||||
chunks: Uint8Array<ArrayBuffer>[]
|
||||
): ILoadDataTypeMap[T] {
|
||||
@ -102,23 +129,44 @@ export class LoadTask<T extends LoadDataType, R> implements ILoadTask<T, R> {
|
||||
}
|
||||
}
|
||||
|
||||
private async processResponse(response: Response) {
|
||||
const reader = response.body?.getReader();
|
||||
const contentLength = response.headers.get('Content-Length') ?? '0';
|
||||
const total = parseInt(contentLength, 10);
|
||||
this.loadedByte = 0;
|
||||
this.totalByte = total;
|
||||
this.progress.onProgress(this, 0, total);
|
||||
if (!reader) {
|
||||
const data = await this.processUnstreamableResponse(response);
|
||||
this.loadedByte = this.totalByte;
|
||||
this.contentLoaded = true;
|
||||
this.progress.onProgress(this, this.loadedByte, this.totalByte);
|
||||
const processed = await this.processor.process(data, this);
|
||||
this.loadedData = processed;
|
||||
this.loadResolve(processed);
|
||||
return;
|
||||
/**
|
||||
* 处理加载数据
|
||||
* @param data 加载获取的原始数据
|
||||
*/
|
||||
private processData(data: ILoadDataTypeMap[T]): Promise<R> {
|
||||
if (this.processor) {
|
||||
return this.processor.process(data, this);
|
||||
} else {
|
||||
return Promise.resolve(data as R);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用非流式加载方式进行加载
|
||||
* @param response 响应体
|
||||
* @param total 加载总字节数
|
||||
*/
|
||||
private async *loadUnstream(
|
||||
response: IResponseLike,
|
||||
total: number
|
||||
): AsyncGenerator<number, ILoadDataTypeMap[T]> {
|
||||
const data = await this.processUnstreamableResponse(response);
|
||||
this.loadedByte = this.totalByte;
|
||||
this.contentLoaded = true;
|
||||
this.forEachHook(hook => hook.onProgress?.(total, total));
|
||||
yield total;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用流式加载方式进行加载
|
||||
* @param reader 流式读取器
|
||||
* @param total 加载总字节数
|
||||
*/
|
||||
private async *loadStream(
|
||||
reader: IResponseReaderLike,
|
||||
total: number
|
||||
): AsyncGenerator<number, ILoadDataTypeMap[T]> {
|
||||
let received = 0;
|
||||
const chunks: Uint8Array<ArrayBuffer>[] = [];
|
||||
while (true) {
|
||||
@ -129,23 +177,70 @@ export class LoadTask<T extends LoadDataType, R> implements ILoadTask<T, R> {
|
||||
}
|
||||
if (done) this.contentLoaded = true;
|
||||
this.loadedByte = received;
|
||||
this.progress.onProgress(this, received, total);
|
||||
if (done) break;
|
||||
this.forEachHook(hook => hook.onProgress?.(received, total));
|
||||
yield received;
|
||||
if (done) {
|
||||
this.loadedByte = total;
|
||||
yield total;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const data = this.processStreamChunkResponse(chunks);
|
||||
const processed = await this.processor.process(data, this);
|
||||
this.loadedData = processed;
|
||||
this.loadResolve(processed);
|
||||
return data;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
const response = await fetch(this.url, {
|
||||
method: this.method,
|
||||
body: this.body,
|
||||
headers: this.headers
|
||||
});
|
||||
this.processResponse(response);
|
||||
return;
|
||||
/**
|
||||
* 执行加载任务,自动决定使用流式加载或非流式加载
|
||||
* @param response 响应体
|
||||
* @param total 加载总字节数
|
||||
*/
|
||||
private loadTask(
|
||||
response: IResponseLike,
|
||||
total: number
|
||||
): AsyncGenerator<number, ILoadDataTypeMap[T]> {
|
||||
const reader = response.body?.getReader();
|
||||
if (reader) {
|
||||
return this.loadStream(reader, total);
|
||||
} else {
|
||||
return this.loadUnstream(response, total);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理响应体,并进行加载工作
|
||||
* @param response 响应体
|
||||
*/
|
||||
private async *processResponse(
|
||||
response: IResponseLike
|
||||
): AsyncIterable<number> {
|
||||
const contentLength = response.headers.get('Content-Length') ?? '0';
|
||||
const total = parseInt(contentLength, 10);
|
||||
this.loadedByte = 0;
|
||||
this.totalByte = total;
|
||||
this.forEachHook(hook => hook.onLoadStart?.(total));
|
||||
this.forEachHook(hook => hook.onProgress?.(0, total));
|
||||
|
||||
const iter = this.loadTask(response, total);
|
||||
const data = yield* iter;
|
||||
|
||||
const processed = await this.processData(data);
|
||||
this.loadedData = processed;
|
||||
this.loadResolve(processed);
|
||||
this.forEachHook(hook => hook.onLoadEnd?.(processed, data, total));
|
||||
}
|
||||
|
||||
async *start(): AsyncIterable<number> {
|
||||
if (this.loadedData) return;
|
||||
|
||||
if (!this.starter) {
|
||||
logger.error(30);
|
||||
return;
|
||||
}
|
||||
const response = await this.starter.start(this);
|
||||
yield* this.processResponse(response);
|
||||
|
||||
// 加载完毕后就可以清空钩子了
|
||||
this.forEachHook(hook => hook).forEach(v => this.removeHook(v));
|
||||
}
|
||||
|
||||
loaded(): Promise<R> {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
//#region 流传输
|
||||
|
||||
import { IHookable, IHookBase } from '@motajs/common';
|
||||
|
||||
export interface IStreamController {
|
||||
/** 当前是否正在加载 */
|
||||
readonly loading: boolean;
|
||||
@ -85,33 +87,6 @@ export const enum LoadDataType {
|
||||
JSON
|
||||
}
|
||||
|
||||
export interface ILoadDataTypeMap {
|
||||
[LoadDataType.ArrayBuffer]: ArrayBuffer;
|
||||
[LoadDataType.Uint8Array]: Uint8Array<ArrayBuffer>;
|
||||
[LoadDataType.Blob]: Blob;
|
||||
[LoadDataType.Text]: string;
|
||||
[LoadDataType.JSON]: any;
|
||||
}
|
||||
|
||||
export interface ILoadTaskProcessor<T extends LoadDataType, R> {
|
||||
/**
|
||||
* 处理加载内容
|
||||
* @param response 处理前加载结果
|
||||
* @param task 加载任务对象
|
||||
*/
|
||||
process(response: ILoadDataTypeMap[T], task: ILoadTask<T, R>): Promise<R>;
|
||||
}
|
||||
|
||||
export interface ILoadTaskProgress<T extends LoadDataType, R> {
|
||||
/**
|
||||
* 更新加载进度
|
||||
* @param task 加载任务对象
|
||||
* @param loaded 已加载的字节数
|
||||
* @param total 文件总计字节数,如果此值为零说明无法读取到 `Content-Length`
|
||||
*/
|
||||
onProgress(task: ILoadTask<T, R>, loaded: number, total: number): void;
|
||||
}
|
||||
|
||||
export const enum RequestMethod {
|
||||
GET = 'GET',
|
||||
POST = 'POST',
|
||||
@ -124,17 +99,17 @@ export const enum RequestMethod {
|
||||
PATCH = 'PATCH'
|
||||
}
|
||||
|
||||
export interface ILoadTaskInit<T extends LoadDataType, R> {
|
||||
/** 请求响应格式 */
|
||||
readonly dataType: T;
|
||||
/** 加载任务标识符 */
|
||||
readonly identifier: string;
|
||||
export interface ILoadDataTypeMap {
|
||||
[LoadDataType.ArrayBuffer]: ArrayBuffer;
|
||||
[LoadDataType.Uint8Array]: Uint8Array<ArrayBuffer>;
|
||||
[LoadDataType.Blob]: Blob;
|
||||
[LoadDataType.Text]: string;
|
||||
[LoadDataType.JSON]: any;
|
||||
}
|
||||
|
||||
export interface ILoadTaskRequest {
|
||||
/** 加载目标 URL */
|
||||
readonly url: string | URL;
|
||||
/** 加载的处理对象,用于处理加载结果等 */
|
||||
readonly processor: ILoadTaskProcessor<T, R>;
|
||||
/** 加载进度对象,用于监控加载进度 */
|
||||
readonly progress: ILoadTaskProgress<T, R>;
|
||||
readonly url: string;
|
||||
/** 请求模式 */
|
||||
readonly method?: RequestMethod;
|
||||
/** 请求体 */
|
||||
@ -143,21 +118,141 @@ export interface ILoadTaskInit<T extends LoadDataType, R> {
|
||||
readonly headers?: HeadersInit;
|
||||
}
|
||||
|
||||
export interface ILoadTask<T extends LoadDataType, R> extends ILoadTaskInit<
|
||||
T,
|
||||
R
|
||||
> {
|
||||
export interface ILoadTaskProcessor<T extends LoadDataType, R> {
|
||||
/**
|
||||
* 处理加载内容
|
||||
* @param response 处理前加载结果
|
||||
* @param task 加载任务对象
|
||||
*/
|
||||
process(response: ILoadDataTypeMap[T], task: ILoadTask<T, R>): Promise<R>;
|
||||
}
|
||||
|
||||
export interface IResponseReaderLike {
|
||||
/**
|
||||
* 进行下一个分块的读取
|
||||
*/
|
||||
read(): Promise<ReadableStreamReadResult<Uint8Array<ArrayBuffer>>>;
|
||||
}
|
||||
|
||||
export interface IResponseBodyLike {
|
||||
/**
|
||||
* 获取流式读取器
|
||||
*/
|
||||
getReader(): ReadableStreamDefaultReader;
|
||||
}
|
||||
|
||||
export interface IResponseHeadersLike {
|
||||
/**
|
||||
* 获取指定响应头的内容
|
||||
* @param name 响应头字段名称
|
||||
*/
|
||||
get(name: string): string | null;
|
||||
}
|
||||
|
||||
export interface IResponseLike {
|
||||
/** 请求地址 */
|
||||
readonly url: string;
|
||||
/** 响应体 */
|
||||
readonly body: IResponseBodyLike | null;
|
||||
/** 响应头 */
|
||||
readonly headers: IResponseHeadersLike;
|
||||
|
||||
/**
|
||||
* 输出为 `ArrayBuffer`
|
||||
*/
|
||||
arrayBuffer(): Promise<ArrayBuffer>;
|
||||
|
||||
/**
|
||||
* 输出为 `Blob`
|
||||
*/
|
||||
blob(): Promise<Blob>;
|
||||
|
||||
/**
|
||||
* 输出为 `Uint8Array`
|
||||
*/
|
||||
bytes(): Promise<Uint8Array<ArrayBuffer>>;
|
||||
|
||||
/**
|
||||
* 输出为 `JSON`
|
||||
*/
|
||||
json(): Promise<any>;
|
||||
|
||||
/**
|
||||
* 输出为纯文本
|
||||
*/
|
||||
text(): Promise<string>;
|
||||
}
|
||||
|
||||
export interface ILoadTaskStarter {
|
||||
/**
|
||||
* 进行加载请求,获取 `Response` 对象
|
||||
* @param request 请求配置
|
||||
*/
|
||||
start(request: ILoadTaskRequest): Promise<IResponseLike>;
|
||||
}
|
||||
|
||||
export interface ILoadTaskInit<
|
||||
T extends LoadDataType
|
||||
> extends ILoadTaskRequest {
|
||||
/** 请求响应格式 */
|
||||
readonly dataType: T;
|
||||
/** 加载任务标识符 */
|
||||
readonly identifier: string;
|
||||
}
|
||||
|
||||
export interface ILoadTaskHooks<T extends LoadDataType, R> extends IHookBase {
|
||||
/**
|
||||
* 当加载开始时触发
|
||||
* @param total 加载总字节数
|
||||
*/
|
||||
onLoadStart?(total: number): void;
|
||||
|
||||
/**
|
||||
* 当加载进度变动时触发
|
||||
* @param loaded 已加载的字节数
|
||||
* @param total 加载总字节数
|
||||
*/
|
||||
onProgress?(loaded: number, total: number): void;
|
||||
|
||||
/**
|
||||
* 当加载完成时触发
|
||||
* @param data 经过处理后的加载内容
|
||||
* @param raw 加载完毕时获取的原始数据
|
||||
* @param total 加载总字节数
|
||||
*/
|
||||
onLoadEnd?(data: R, raw: ILoadDataTypeMap[T], total: number): void;
|
||||
}
|
||||
|
||||
export interface ILoadTask<T extends LoadDataType = LoadDataType, R = any>
|
||||
extends ILoadTaskInit<T>, IHookable<ILoadTaskHooks<T, R>> {
|
||||
/** 当前是否加载完毕 */
|
||||
readonly contentLoaded: boolean;
|
||||
/** 已经加载的字节数 */
|
||||
readonly loadedByte: number;
|
||||
/** 该加载任务的总体字节数 */
|
||||
readonly totalByte: number;
|
||||
/** 当前任务使用的数据处理器 */
|
||||
readonly processor: ILoadTaskProcessor<T, R> | null;
|
||||
/** 当前任务使用的加载启动器 */
|
||||
readonly starter: ILoadTaskStarter | null;
|
||||
|
||||
/**
|
||||
* 设置当前任务使用的数据处理器
|
||||
* @param processor 数据处理器
|
||||
*/
|
||||
setProcessor(processor: ILoadTaskProcessor<T, R> | null): void;
|
||||
|
||||
/**
|
||||
* 设置当前任务使用的加载启动器
|
||||
* @param starter 加载启动器
|
||||
*/
|
||||
setStarter(starter: ILoadTaskStarter): void;
|
||||
|
||||
/**
|
||||
* 开始此加载计划,返回一个 `Promise`,当得到服务器的响应后兑现
|
||||
* @returns 异步迭代器,当加载进度变动时进行一次迭代,迭代值为当前加载的字节数
|
||||
*/
|
||||
start(): Promise<void>;
|
||||
start(): AsyncIterable<number>;
|
||||
|
||||
/**
|
||||
* 返回一个 `Promise`,当本计划加载完毕后兑现,兑现结果是加载结果
|
||||
@ -170,29 +265,43 @@ export interface ILoadTask<T extends LoadDataType, R> extends ILoadTaskInit<
|
||||
getLoadedData(): R | null;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 内置组件
|
||||
|
||||
export interface ILoadProgressTotal<
|
||||
T extends LoadDataType = LoadDataType,
|
||||
R = any
|
||||
> extends ILoadTaskProgress<T, R> {
|
||||
/** 已经添加的加载任务对象 */
|
||||
readonly addedTasks: Set<ILoadTask<T, R>>;
|
||||
/** 当前已经加载完毕的任务对象 */
|
||||
readonly loadedTasks: Set<ILoadTask<T, R>>;
|
||||
export interface ILoadManagerHooks extends IHookBase {
|
||||
/**
|
||||
* 当开始加载时触发
|
||||
*/
|
||||
onStartLoad?(): void;
|
||||
|
||||
/**
|
||||
* 迭代加载进度,当 `yield` 的值被兑现时,说明加载进度更新
|
||||
* 当加载进度发生变化时触发
|
||||
* @param loaded 已加载的字节数
|
||||
* @param total 加载总字节数,此数值可能不准确,因为必须等所有的加载全部连接完成时才能够得知总字节数
|
||||
*/
|
||||
[Symbol.asyncIterator](): AsyncGenerator<void, void, void>;
|
||||
onProgress?(loaded: number, total: number): void;
|
||||
|
||||
/**
|
||||
* 当所有加载任务完成时触发
|
||||
* @param total 加载的总字节数
|
||||
*/
|
||||
onLoadEnd?(total: number): void;
|
||||
}
|
||||
|
||||
export interface ILoadManager extends IHookable<ILoadManagerHooks> {
|
||||
/** 已经添加的加载任务对象 */
|
||||
readonly addedTasks: Set<ILoadTask>;
|
||||
/** 当前已经加载完毕的任务对象 */
|
||||
readonly loadedTasks: Set<ILoadTask>;
|
||||
|
||||
/**
|
||||
* 向该进度监听器添加加载任务对象
|
||||
* @param task 加载任务对象
|
||||
*/
|
||||
addTask(task: ILoadTask<T, R>): void;
|
||||
addTask(task: ILoadTask): void;
|
||||
|
||||
/**
|
||||
* 开始执行加载
|
||||
* @returns 加载的异步迭代器,当加载进度更新时迭代,迭代值为当前已加载的总字节数
|
||||
*/
|
||||
load(): AsyncIterable<number>;
|
||||
|
||||
/**
|
||||
* 获取总体已加载的字节数
|
||||
|
||||
Loading…
Reference in New Issue
Block a user