MobX + 领域模型在区块链项目上的实践

我爱台南我爱台南~

网络请求库-KoAJAX

RESTful API with Token-based

export const service = new HTTPClient({
    baseURI: process.env.SERVER_URL,
    responseType: 'json'
}).use(async ({ request }, next) => {
    request.headers = {
        ['Authorization']: `Bearer ${localStorage.token || ''}`,
        'X-Network': localStorage.chainid || CHAINS[1].id + '',
        ...request.headers
    };

    try {
        await next();
    } catch (error) {
        if (error instanceof HTTPError && !error.message && error.body) {
            const { name, message } = error.body as Record<
                'name' | 'message',
                string
            >;
            error.message = message || name;
        }
        throw error;
    }
});

Request Example

  /**
     * graph user vote detail
     * @param proposal proposal id
     * @param userAddress voter address
     * @returns user vote detail
     */
    @logData
    @toggle('loading')
    async getUserVote(userAddress: string) {
        const { body } = await service.get<VoteCentral>(
            `proposal/${this.currentPoll}/vote/${userAddress}`
        );

        return (this.current = body);
    }

MobX-RESTful

API 层 Store 层解耦


    @toggle('uploading')
    async updateOne(data: Partial<NewData<D>>, id?: IDType) {
        const { body } = await (id
            ? this.client.patch<D>(`${this.baseURI}/${id}`, data)
            : this.client.post<D>(this.baseURI, data));

        return (this.currentOne = body);
    }

    @toggle('downloading')
    async getOne(id: IDType) {
        const { body } = await this.client.get<D>(`${this.baseURI}/${id}`);

        return (this.currentOne = body);
    }

    @toggle('uploading')
    async deleteOne(id: IDType) {
        await this.client.delete(`${this.baseURI}/${id}`);

        if (this.currentOne[this.indexKey] === id) this.clearCurrent();
    }
    
    
    //
    
    readAccessor = await KorisAccessor.create({
                chainId,
                apiUrl: formatApiUrl,
                config
            }));
    
   
      /**
     * query user joined current dac
     * @param eth_address user address
     */
    @action
    @logData
    @toggle('loading')
    async setIsJoin(eth_address: string, addr = this.currentDacAddr) {
        return (this.isJoin = await chain.readAccessor.contracts
            .DAC(addr)
            .isMember(eth_address));
    }
    

Request example

Simple list
export class RepositoryModel<
    D extends Repository = Repository,
    F extends Filter<D> = Filter<D>
> extends ListModel<D, F> {
    client = client;
    baseURI = 'orgs/idea2app/repos';

    async loadPage(page: number, per_page: number) {
        const { body } = await this.client.get<D[]>(
            `${this.baseURI}?${buildURLData({ page, per_page })}`
        );
        const [_, organization] = this.baseURI.split('/');
        const {
            body: { public_repos }
        } = await this.client.get<Organization>(`orgs/${organization}`);

        return { pageData: body, totalCount: public_repos };
    }
}
Preload List
export class PreloadRepositoryModel extends Buffer<Repository>(
    RepositoryModel
) {
    client = client;
    baseURI = 'orgs/idea2app/repos';

    loadPage = RepositoryModel.prototype.loadPage;
}
Multiple Source List
export class MultipleRepository extends Stream<Repository>(RepositoryModel) {
    client = client;

    async *getOrgRepos() {
        const {
            body: { public_repos }
        } = await this.client.get<Organization>('orgs/idea2app');

        this.totalCount = public_repos;

        for (let i = 1; ; i++) {
            const { body } = await this.client.get<Repository[]>(
                'orgs/idea2app/repos?page=' + i
            );
            if (!body[0]) break;

            yield* body;
        }
    }

    async *getUserRepos() {
        const {
            body: { public_repos }
        } = await this.client.get<User>('users/TechQuery');

        this.totalCount = public_repos;

        for (let i = 1; ; i++) {
            const { body } = await this.client.get<Repository[]>(
                'users/TechQuery/repos?page=' + i
            );
            if (!body[0]) break;

            yield* body;
        }
    }

    openStream() {
        return mergeStream(
            this.getOrgRepos.bind(this),
            this.getUserRepos.bind(this)
        );
    }
}

Store 设计

按照领域模型设计 Store,业务逻辑抽象为针对 Store 的增删改查

复杂 Store 分层处理

全局 root store(user)

负责 session

页面 Page Store (DAO Frame)
    @logData
    @toggle('loading')
    async getOne(daoId: string) {
        const {
            addr,
            name,
            desc,
            amount,
            members,
            logo,
            admin,
            admins,
            threshold,
            feeType,
            feeAmount,
            creator,
            memberHasBadge
        } = await this.getdaoHelperBasic(daoId);

        this.adminOf(daoId, addr, admin, +threshold);
        this.memberOf(addr, daoId, admin);
        this.informationOf(addr);
       
        return this.current;
    }

模块 Module Store (proposal)
  informationOf(daoAddress: string) {
        return (this.currentInformation = new InformationStore(daoAddress));
    }

    inviteOf(daoId: string, daoAddress: string) {
        return (this.currentInvite = new InviteStore(daoId, daoAddress));
    }

    operationOf(daoId: string, daoAddress: string, safeAddr: string) {
        return (this.currentOperation = new OperationStore(
            daoId,
            daoAddress,
            safeAddr
        ));
    }

    proposalOf(daoId: string, daoAddress?: string) {
        return (this.currentProposal = new ProposalStore(daoId, daoAddress));
    }