使用 TypeScript 和 Promises 的内容异步 Loading/Unloading

Async Loading/Unloading of content using TypeScript and Promises

我已经使用 TypeScript、Knockout、Generic Promises for TypeScript (https://github.com/pragmatrix/Promise) and async (https://github.com/caolan/async) 创建了异步 loading/unloading 内容框架。

虽然逻辑正常工作并且事件以正确的顺序触发和发生,但在加载 NavigationItem 时 UI 不会更新新选择并且不会在新项目上开始加载。谁能看出这是为什么?

核心逻辑在NavigationItemclass:

export class NavigationItem {
    constructor(public Data: INavigationData) {
        this.data = ko.observable(Data);
        this.data.subscribe(n => Data = n);
        this.status = ko.observable(NavigationItemStatus.Unloaded);
        this.isLoading = ko.computed(() => this.status() == NavigationItemStatus.Loading);
        this.isLoaded = ko.computed(() => this.status() == NavigationItemStatus.Loaded);
        this.isUnloaded = ko.computed(() => this.status() == NavigationItemStatus.Unloaded);
        this.isUnloading = ko.computed(() => this.status() == NavigationItemStatus.Unloading);
    }
    public data: KnockoutObservable<INavigationData>;
    public status: KnockoutObservable<NavigationItemStatus>;
    public isLoading: KnockoutComputed<boolean>;
    public isLoaded: KnockoutComputed<boolean>;
    public isUnloading: KnockoutComputed<boolean>;
    public isUnloaded: KnockoutComputed<boolean>;
    public closed: Lind.Events.ITypedEvent<NavigationItem> = new Lind.Events.TypedEvent();
    public navigationItemAdded: Lind.Events.ITypedEvent<NavigationItem> = new Lind.Events.TypedEvent();
    private queue: AsyncQueue<boolean> = async.queue((s, c) => {
        if (s)
            this.loadWorker().done(() => c());
        else
            this.unloadWorker().done(() => c());
    }, 1);
    load() : Promise<boolean>{
        var d = defer<boolean>();
        this.queue.push(true, () => d.resolve(true));
        return d.promise();
    }
    unload() : Promise<boolean>{
        var d = defer<boolean>();
        this.queue.push(false, () => d.resolve(true));
        return d.promise();
    }
    private unloadWorker(): Promise<boolean> {
        var d = defer<boolean>();
        this.doUnload().done(s => this.onUnloaded(s, d));
        this.onUnloading();
        return d.promise();
    }
    private loadWorker(): Promise<boolean>{
        var d = defer<boolean>();
        if (this.isLoaded())
        {
            this.unload();
            this.load();
            d.resolve(false);
        }
        else {
            this.doLoad().done(s => this.onLoaded(s, d));
            this.onLoading();
        }
        return d.promise();
    }
    private onLoaded(loadStatus: boolean, promise: P.Deferred<boolean>) {
        this.status(NavigationItemStatus.Loaded);
        promise.resolve(loadStatus);
    }
    private onUnloaded(unloadStatus: boolean, promise: P.Deferred<boolean>) {
        this.status(NavigationItemStatus.Unloaded);
        promise.resolve(unloadStatus);
    }
    private onLoading() {
        this.status(NavigationItemStatus.Loading);
    }
    private onUnloading() {
        this.status(NavigationItemStatus.Unloading);
    }
    doLoad(): Promise<boolean> {
        var d = defer<boolean>();
        d.resolve(true);
        return d.promise();
    }
    doUnload(): Promise<boolean> {
        var d = defer<boolean>();
        d.resolve(true);
        return d.promise();
    }
    close() {
        if(this.status() != NavigationItemStatus.Unloaded)
            this.unload();
        this.closed.trigger(this);
    }
    addNavigationItem(navigationItem : NavigationItem) {
        this.navigationItemAdded.trigger(navigationItem);
    }
}

当调用 load() 时,它会将一个加载工作人员排入队列,当调用 unload() 时,它会将一个卸载工作人员排入队列,该队列的并发性为 1。NavigationItemCollection class 扩展了 NavigationItem,公开了一个可观察数组并实现 doLoad 和 doUnload。

export class NavigationItemCollection<T> extends NavigationItem {
    constructor(data: INavigationData) {
        super(data);
        this.items = ko.observableArray<T>();
    }
    public items: KnockoutObservableArray<T>;
    doLoad(): Promise<boolean> {
        var d = defer<boolean>();
        super.doLoad().done(() => {
            this.getItems().done(i => {
                if (i != null) {
                    for (var k: number = 0; k < i.length; k++) {
                        this.items.push(i[k]);
                    }
                }
                d.resolve(true);
            });
        });
        return d.promise(); 
    }
    doUnload(): Promise<boolean> {
        var d = defer<boolean>();
        super.doUnload().done(() => {
            this.items.removeAll();
            d.resolve(true);
        });
        return d.promise();
    }
    getItems(): Promise<T[]> {
        var d = defer<T[]>();
        d.resolve(null);
        return d.promise();
    }
}

RepositoryNavigationItem class 然后实现 NavigationItemCollection 并实现 getItems()。

export class RepositoryNavigationItem<TViewModel, TEntity> extends ViewModels.Navigation.NavigationItemCollection<TViewModel>{
    constructor(data: ViewModels.Navigation.INavigationData, public Repository: Northwind.Repository.IRepositoryGeneric<TEntity>) {
        super(data);
    }
    getItems(): Promise<TViewModel[]> {
        var d = defer<TViewModel[]>();
        this.Repository.GetAll().done(i => {
            var vms: TViewModel[] = [];
            if (i != null) {
                for (var k: number = 0; k < i.length; k++) {
                    vms.push(this.createViewModel(i[k]));
                }
            }
            d.resolve(vms);
        });
        return d.promise();
    }
    createViewModel(entity : TEntity): TViewModel {
        return null;
    }
}
export class ProductsNavigationItem extends RepositoryNavigationItem<Northwind.Product, Northwind.IProduct>{
    createViewModel(entity: Northwind.IProduct): Northwind.Product {
        return Northwind.Product.Create(entity);
    }
}

存储库实现如下:

export class Repository<TEntity> implements IRepositoryGeneric<TEntity>{
    constructor(public ServiceLocation: string) { }
    GetAll(): Promise<TEntity[]> {
        var d = defer<TEntity[]>();
        $.ajax({
            type: "GET",
            url: this.ServiceLocation + "GetAll",
            success: data => d.resolve(<TEntity[]>data),
            error: err => d.resolve(null)
        });
        return d.promise();
    }
}

MainWindowViewModel 然后实例化 NavigationItems(使用 IoC 依赖项注入)并在选择和取消选择 NavigationItems 时控制 load/unload 控制流。

export class MainWindowViewModel {
    constructor(private Container: Lind.IoC.IContainer, navigationData: ViewModels.Navigation.INavigationData[]) {
        this.navigationItems = ko.observableArray<ViewModels.Navigation.NavigationItem>();
        this.selectedNavigationItem = ko.observable<ViewModels.Navigation.NavigationItem>();
        this.selectedNavigationItemType = ko.computed(() => {
            var navItem = this.selectedNavigationItem();
            if (navItem != null)
                return navItem.data().Name;
            return "Loading";
        });
        this.selectedNavigationItem.subscribe(n => {
            if (n != null)
                n.unload();
        }, this, "beforeChange");
        this.selectedNavigationItem.subscribe(n => {
            if(n != null)
                n.load();
        });
        for (var i: number = 0; i < navigationData.length; i++) {
            var navItem = Container.Resolve<ViewModels.Navigation.NavigationItem>(typeof ViewModels.Navigation.NavigationItem, navigationData[i].Name,
                [new Lind.IoC.ConstructorParameterFactory("data", () => navigationData[i])]);
            navItem.closed.add(this.onNavigationItemClosed);
            navItem.navigationItemAdded.add(this.onNavigationItemAdded);
            this.navigationItems.push(navItem);
        }
        this.selectedNavigationItem(this.navigationItems.peek()[0]);
    }
    public navigationItems: KnockoutObservableArray<ViewModels.Navigation.NavigationItem>;
    public selectedNavigationItem: KnockoutObservable<ViewModels.Navigation.NavigationItem>;
    public selectedNavigationItemType: KnockoutComputed<string>;
    private onNavigationItemClosed(item: ViewModels.Navigation.NavigationItem) {
        item.closed.remove(this.onNavigationItemClosed);
        item.navigationItemAdded.remove(this.onNavigationItemAdded);
        this.navigationItems.remove(item);
        if (this.selectedNavigationItem() == item)
            this.selectedNavigationItem(this.navigationItems.peek()[0]);
    }
    private onNavigationItemAdded(item: ViewModels.Navigation.NavigationItem) {
        item.navigationItemAdded.add(this.onNavigationItemAdded);
        item.closed.add(this.onNavigationItemClosed);
        this.navigationItems.push(item);
        this.selectedNavigationItem(item);
    }
}

然后视图如下:

<div id="rightNav" style="float:left">
    <ul data-bind="foreach: navigationItems">
        <li>
            <div data-bind="style:{ background: isLoading() == true ? 'yellow' : (isLoaded() == true ? 'green' : (isUnloading() == true ? 'gray' : (isUnloaded() == true ? 'white' : 'red')))}">
                <span><a data-bind="text:data().DisplayName, click: $parent.selectedNavigationItem"></a><button data-bind="visible:data().IsCloseable == true, click: close" >x</button></span>
            </div>
        </li>
    </ul>
</div>
<div id="leftContent" data-bind="template: { name: selectedNavigationItemType(), data: selectedNavigationItem }"></div>
<script type="text/html" id="Products">
    <table>
        <thead>
            <tr>
                <th>Name</th>
                <th>Supplier</th>
                <th>Category</th>
                <th>Unit Price</th>
                <th>Units in Stock</th>
                <th>Discontinued</th>
            </tr>
        </thead>
        <tbody data-bind="foreach: items">
            <tr>
                <td><span data-bind="text:productName" /></td>
                <td><span data-bind="text: supplier().companyName" /></td>
                <td><span data-bind="text: category().categoryName" /></td>
                <td><span data-bind="text: unitPrice" /></td>
                <td><span data-bind="text: unitsInStock" /></td>
                <td><input type="checkbox" data-bind="checked: discontinued" disabled="disabled" /></td>
            </tr>
        </tbody>
    </table>
</script>

我添加了一个模拟存储库并使用模拟视图对其进行了测试:

module Northwind.Repository.Mock {
export class MockRepository<TEntity> implements IRepositoryGeneric<TEntity>{
    constructor(public ServiceLocation: string) { }
    Delete(id: number): Promise<boolean> {
        var d = defer<boolean>();
        d.resolve(null);
        return d.promise();
    }
    GetAll(): Promise<TEntity[]> {
        var d = defer<TEntity[]>();
        setTimeout(() => {
            d.resolve(null);
        }, 5000);
        return d.promise();
    }
    Get(id: number): Promise<TEntity> {
        var d = defer<TEntity>();
        d.resolve(null);
        return d.promise();
    }
    Add(entity: TEntity): Promise<TEntity> {
        var d = defer<TEntity>();
        d.resolve(null);
        return d.promise();
    }
    Update(entity: TEntity): Promise<boolean> {
        var d = defer<boolean>();
        d.resolve(false);
        return d.promise();

    }
}
}

当正确模拟此功能时,看起来 jQuery ajax 调用以某种方式阻塞,这是怎么回事?

这是一个 IE 错误!什么鬼?