阅读 742

框架的诞生-一:我想要的框架

我想要的框架是什么样的?

上一篇文章: 《框架的诞生-零:为什么写框架?》里有讲 什么是框架。

框架是一个架子,在游戏程序中,抛开渲染层引擎框架,我们的指的框架就是支撑业务逻辑的架子,也是一个框框,规范和约束着开发人员。

每个框架都有着自己的边界,解决特定领域问题的。

那我们从去分析我遇到了什么问题,有什么需求,如何解决问题,如何实现需求。

无论是小项目还是大项目,或者是一个会不断扩张的项目,都希望能够项目代码结构清晰有条理,管理好不同模块。

我尝试过的方式

Manager of Managers

很多框架都用这种方式(包括我之前写的框架)

这是刘钢老师的《Unity 项目架构设计与开发管理》中讲到的一种相对较好的方式 ▼

优点是:类似于分级结构,各司其职;比如音频管理,场景管理,关卡管理等,每一个都是一个单例脚本,配合使用。结构相对清晰。可以复用

但我个人不太喜欢的点

  • 调用不方便,调用链太长,费手指。比如 UIManager.getInstance().showUI 或者 UIMgr.ins.showUI
    • 我想这样: m.uiMgr.showUI 少敲几个字母,也不想 import UIMgr
  • 无生命周期统一管理,单例初始化和依赖不太可控。惰性初始化,等到调用 getInstance 才初始化。
  • 控制台调试调用不方便,单例可能得单独绑定暴露到全局才能,每个模块都得这样做才行
  • 直接依赖不可动态替换
    • 有个模块我想在不同的平台切换成不同的实现,单例做不到。
  • 大多耦合引擎来开发,只能在同引擎项目进行复用

模块字典挂载到全局变量 window

这是我之前的框架使用的方式

就是将所有的模块初始化,然后注入一个模块字典

然后将字典挂载到全局的一个变量中。

export class Main {
    constructor(){
        const moduleMap = {

        };
        moduleMap["uiMgr"] = new UIMgr();
        moduleMap["netMgr"] = new NetMgr();

        window["aa"] = moduleMap;
    }
}
class UIMgr {

}
class NetMgr {

}
复制代码

优点:调用方便

缺点: 有点危险,别人知道了可以在控制台调用进行调试。

这些方式在模块管理上都有问题,先不考虑如何方便调用,先实现个如何管理模块的核心机制。

一个具有生命周期的模块化机制

pomelo 给我的灵感

这个想法的灵感来自 pomelo 一个网易开发的基于 nodejs 的分布式服务器框架。

pomelo 支持可插拔的 component 扩展架构

用户只要实现 component 相关的接口: start, afterStart, stop, 就可以加载自定义的组件:

app.load([name], comp, [opts])
复制代码

start, afterStart 这些生命周期 接口跟 cocos 和 Unity 的组件式接口很像。 主要是方便处理不同模块之间的依赖引用。比如:A 依赖了 B,但 B 还未初始化。

各自的初始化,都在 start 里处理,然后在 afterStart 里进行依赖调用。

可能对于不同的业务,这些生命周期可能不够用,可以根据具体业务进行扩展,满足自定义需求。

比如登录业务相关的:

C 模块依赖 A 和 B 登录后的数据状态,那么增加两个接口 onLoginInit,onAfterLoginInit。

那么 A 和 B 实现 onLoginInit 接口进行登录初始化,C 在 onAfterLoginInit 接口进行依赖调用。

怎么实现我想要的框架?

模块生命周期图

模块生命周期图

接口设计

declare global {
    namespace egf {

        interface IModule {
            /**模块名 */
            key?: string
            /**
             * 当初始化时
             */
            onInit?(app: IApp): void;
            /**
             * 所有模块初始化完成时
             */
            onAfterInit?(app: IApp): void;
            /**
             * 模块停止时
             */
            onStop?(): void;
        }
        type BootEndCallback = (isSuccess: boolean) => void;
        /**
         * 引导程序
         */
        interface IBootLoader {
            /**
             * 引导
             * @param app
             */
            onBoot(app: IApp, bootEnd: BootEndCallback): void;
        }
        /**
         * 主程序
         */
        interface IApp<ModuleMap = any> {
            /**
             * 程序状态
             * 0 未启动 1 引导中, 2 初始化, 3 运行中
             */
            state: number;
            /**
             * 模块字典
             */
            moduleMap: ModuleMap;
            /**
             * 引导
             * @param bootLoaders
             */
            bootstrap(bootLoaders: egf.IBootLoader[]): Promise<boolean>;
            /**
             * 初始化
             */
            init(): void;
            /**
             * 加载模块
             * @param module
             */
            loadModule(module: IModule | any, key?: keyof ModuleMap): void;
            /**
             * 停止
             */
            stop(): void;
            /**
             * 获取模块实例
             * @param moduleKey
             */
            getModule<T extends IModule = any>(moduleKey: keyof ModuleMap): T;
            /**
             * 判断有没有这个模块
             * @param moduleKey
             */
            hasModule(moduleKey: keyof ModuleMap): boolean;

        }
    }
}

// eslint-disable-next-line @typescript-eslint/semi
export { }
复制代码

Bootloader: CatLib 给我的灵感

这里有一个 bootloader 的东西我没有讲到,它的灵感来自 CatLib,一个我觉得很棒的 Unity 框架。

这个机制是什么呢?以开发测试环境和生产环境举例。

有一个 debugBootLoader,这个引导程序处理一些测试用的模块加载和初始化,杂七杂八的。

当你发布生产环境时,可以通过 debug 变量屏蔽加载这个引导程序,也可以通过编译工具剔除这段代码。

具体实现可以看:github.com/AILHC/EasyG…

怎么使用?

具体使用请看 demo 工程

cocoscreator2.x 的 demo github.com/AILHC/egf-c…

cocoscreator3d 的 demo github.com/AILHC/egf-c…

如何接入项目

//FrameworkLoader.ts
import { HelloWorld } from "../HelloWorld";
export class FrameworkLoader implements egf.IBootLoader {
    onBoot(app: egf.IApp, bootEnd: egf.BootEndCallback): void {
        const helloWorld = new HelloWorld();
        app.loadModule(helloWorld);
        bootEnd(true);
    }

}
//AppMain.ts
import { App } from "@ailhc/egf-core"
import { FrameworkLoader } from "./boot-loaders/FrameworkLoader";
import { setModuleMap, m } from "./ModuleMap";
/**
 * 这是一种启动和初始化框架的方式,在cocos加载脚本时启动
 * 不依赖场景加载和节点组件挂载
 */
export class AppMain {
    public static app: App<IModuleMap>;
    public static initFramework() {
        const app = new App<IModuleMap>();
        AppMain.app = app;
        app.bootstrap([new FrameworkLoader()]);
        setModuleMap(app.moduleMap);
        app.init();
        window["m"] = m;//挂在到全局,方便控制台调试,生产环境可以屏蔽=>安全
        m.helloWorld.say();
    }

}
AppMain.initFramework();

复制代码

接入项目很简单,new 一下,bootstrap,init 就可以了~

注入模块也很简单

//在UIMgr.ts开头增加个声明
declare global {
    interface IModuleMap {
        uiMgr:UIMgr
    }
}
//在初始化地方,注入实例
app.loadModule(UIMgr.getInstance(),"uiMgr");
复制代码

注入的模块是什么类型的,不限制,你可以将业务模块 比如 HeroModule 注入进去,那么业务模块之间就可以直接调用了。也不用担心 typescript 的循环引用了。

举个栗子(随便的):

// BattleModule.ts
m.hero.showHero(1);

//HeroModule.ts
m.battle.startTestBattle(1);

复制代码

就像服务端的 rpc 调用一样。

app.rpc.chat.chatRemote.kick(session, uid, player, function(data){
});
复制代码

至于怎么使得接口调用更方便,这个看个人的喜好,我呢,用了一点点魔法,让自己用着舒服又有点安全感。具体实现细节请看 demo

我想在 CocosCreator 和 C3d 中使用

由于我的工作中是用 Laya 的,项目也用了这个框架。但我私底下都是玩 CocosCreator 和 CocosCreator3d 的(为什么啊?你懂得 😉😉)

我不想在项目之间将源码拷贝来拷贝去,迭代更新同步麻烦。

如果能像 npm 包一样 安装就好了。而且核心模块是一个模块,其他模块也是一个模块。

于是我开发了一个模块编译发布的工具,开发之前以为很简单,实际上,踩坑了好久 😂。

这个模块编译发布工具有什么功能?

  • 编译模块成 iife、commonjs、systemjs 格式的 js 文件
  • 自动生成单个.d.ts 声明文件

这个 systemjs 格式的 js 文件可以让不支持 npm 包的 CocosCreator3d 可以像使用 npm 包一样使用。即使到时 Cocos3.0 支持 npm 了,使用方式也一模一样。使用 C3d1.2.0 发布 web,微信小游戏,验证运行没有问题。

import { App } from '@ailhc/egf-core';//像引用npm包一样引用
import { _decorator, Component, Node } from 'cc';
import { m, setModuleMap } from './ModuleMap';
import { FrameworkLoader } from './boot-loaders/FrameworkLoader';
const { ccclass, property } = _decorator;
@ccclass('AppMainComp')
export class AppMainComp extends Component {
    /* class member could be defined like this */
    // dummy = '';

    /* use `property` decorator if your want the member to be serializable */
    // @property
    // serializableDummy = 0;

    onLoad() {
        this._initFramework();
    }
    private _initFramework() {
        const app = new App<IModuleMap>();
        // new TestImport();
        app.bootstrap([new FrameworkLoader()]);
        // app.bootstrap([new FrameworkLoader2()]);
        setModuleMap(app.moduleMap);
        app.init();
        window["m"] = m;//挂在到全局,方便控制台调试,生产环境可以屏蔽=>安全
        m.helloWorld.say();
    }
    start() {

    }

    // update (dt) {}
}
复制代码

如何开发一个模块

  1. 克隆项目 git clone github.com/AILHC/EasyG…
  2. 复制 packages/package-template 项目,改文件夹名,改 package.json 里的项目名等信息
  3. npm install 初始化项目
  4. 然后用 typescript 进行开发,使用 index.ts 文件将所有代码 export(可以使用 export-typescript 插件自动化,插件版本必须是 0.0.5 之前的)
  5. 使用 egf build 进行编译发布

总结一下框架有什么特性

  • 轻量级模块化机制
  • 模块生命周期
    • 让模块的初始化有序,依赖可控
  • 可面向接口编程
    • 方便具体实现细节可替换,模块可动态替换
  • 友好的类型声明
    • 点一下就有类型提示,传字符串获取模块也有类型提示,很香的。
  • 基于 TypeScript 与引擎无关
  • 每个模块库都是一个 npm 包
  • 模块库可以导出多种 js 格式,让 laya,ccc,c3d 使用,甚至给 Unity、Unreal 用(xLua 作者的 Puerts 了解一下?)

这个框架可以做什么?

特性

  • 基于轻量级无依赖的模块机制,可以为不同项目量身定制框架,可大可小。也可以根据项目的不同阶段进行渐进式扩展。还可以在项目的不同阶段轻易地接入

  • 面向接口编程的模块,底层组件可以无感知替换

  • 基于模块开发工具,我们可以开发和发布一个单独的对核心零依赖的模块,给不同的项目使用。

    • 方便别的项目引用
    • 方便开源
    • 方便做单元测试
  • 基于模块化机制和配套开发工具,大家可以为公司或者个人建立自己的模块库,在不同项目按需复用。

架构设想 ▼

基于egf-core的架构设想图

谢谢大家阅读我的文章,希望大家能有所收获。

框架开发系列文章

最后

欢迎关注,更多内容持续更新

公众号搜索:玩转游戏开发

QQ 群: 1103157878

博客主页: ailhc.github.io/

github: github.com/AILHC

文章分类
前端
文章标签