[QuickPlugin插件]:导表工具-1.0.0

# 简介

通用导表工具,支持导出 TS(类型及数据安全), JSON 格式,支持 number、string、boolean、array(可嵌套) 类型的数据格式

# 详细介绍

打开方式

image

插件面板

image

  • 输入路径:xlsx 存在的目录
  • 输出路径:导出配置的目录(注意导出前会清空此目录,需要设置一个独立的文件夹)

表格格式


第一行:属性说明
第二行:导出的属性名
第三行:导出的类型
第四行:数据(数组类型依靠 JSON 解析)

注意表名需为 c_ 开头才会识别为配置表,可自行修改

导出的 TS 示例

绝对的类型及数据安全,防止外部修改

/* eslint-disable */

export type type_example_config<
	T = Record<
		number,
		{
			/** ID */
			id_n: number;
			/** test */
			test_n: number;
			/** test2 */
			test_s: string;
			/** test3 */
			test_b: boolean;
			/** test4 */
			test: [number, string[], number[][]];
		}
	>
> = {
	readonly [P in keyof T]: T[P] extends Function
		? T[P]
		: type_example_config<T[P]>;
};

/** 示例配置/c_example */
export const example_config: type_example_config = new Proxy(
	{
		[1]: {
			id_n: 1,
			test_n: 1,
			test_s: "2",
			test_b: true,
			test: [
				1,
				["你好", "世界"],
				[
					[0, 1, 2],
					[3, 4, 5],
				],
			],
		},
		[2]: {
			id_n: 2,
			test_n: 4,
			test_s: "5",
			test_b: false,
			test: [
				1,
				["你好", "世界"],
				[
					[0, 1, 2],
					[3, 4, 5],
				],
			],
		},
	},
	{
		get(target, key): any {
			if (!freeze_tab[key]) {
				freeze_tab[key] = true;
				deep_freeze(target[key]);
			}

			return target[key];
		},
		set() {
			return false;
		},
	}
);

const freeze_tab: Record<PropertyKey, boolean> = {};
function deep_freeze<T extends object>(object_: T): T {
	const prop_name_ss = Object.getOwnPropertyNames(object_);

	prop_name_ss.forEach((v_s) => {
		const value = object_[v_s as keyof T];

		if (value && typeof value === "object") {
			deep_freeze(value);
		}
	});

	return Object.freeze(object_);
}

# 下载

xlsx-tool.zip (245.5 KB)

# 相关

QuickPlugin

3赞

安装 QuickPlugin 即可快速开发插件和享受发帖即发布插件,避免重启编辑器和繁琐的重启插件开发过程,以及漫长的插件审核过程

image

个人还是比较推崇我目前使用的方式:用特定格式(文件名对应zlib压缩后的csv表数据)生成 tables.bin 文件(这点需要额外的工具实现),不仅具有一定的安全性,还能减少零散加载所带来的性能消耗,当业务用到某个表数据时从压缩数据中读取并删除,能有效减少内存占用,也能轻易远程更新表数据。
你这表头定义了数据类型,而我的实现可以手动编码映射为复杂对象,还能对表数据进行预处理,比如某些复杂的任务数据,策划数值表配置了前置ID,我们可以预处理所有任务数据的前后置关系,或者说一对多的关系,无需在业务逻辑中每次进行重复的逻辑处理,当然缺点就是会增加一定量的额外编码,不过也能减少业务代码,整体而言优点胜过缺点。

import { Facade, IConvertor, KdField, KdNumeric, StringUtils } from "../../core/Kaka";
import CsvConfManager from "../CsvConfManager";
import { ToLangValue, ToString } from "../CsvConvertors";
import { ConfOther, ConfOtherManager } from "./ConfOtherManager";
import { ConfSkill, ConfSkillManager } from "./ConfSkillManager";

export class ToSkill implements IConvertor<ConfSkill> {
    public cast(v: string): ConfSkill {
        if (!v || v == '') return undefined;
        return ConfSkillManager.getInstance().get(Number(v));
    }
}

export class ConfItem {
    public id: number;
    @KdField("name", ToLangValue)
    public name: string;
    @KdField("describe", ToLangValue)
    public describe: string;
    @KdField("icon", ToString)
    public icon: string;
    public type: number;
    public targetType: number;
    @KdField("skillId", ToSkill)
    public skill: ConfSkill;
    public dailyNum: number;
    @KdField("access", ToLangValue)
    public access: string;
}

/**
 * 道具数据管理
 * 
 * @author zkpursuit
 */
@KdNumeric("item.csv", ConfItem)
export class ConfItemManager extends CsvConfManager<ConfItem> {

    public static readonly lingShiId: number = 101;
    public static readonly lingShiIcon: string = '101';

    private static instance: ConfItemManager;
    public static getInstance(): ConfItemManager {
        if (this.instance == null) {
            this.instance = Facade.getInstance().retrieveProxy(ConfItemManager);
        }
        return this.instance;
    }

    private list: ConfItem[] = [];
    private map: Map<number, ConfItem> = new Map<number, ConfItem>();
    private map1: Map<number, ConfItem[]> = new Map<number, ConfItem[]>();

    protected parseBefore(): void {
        this.list.length = 0;
        this.map.clear();
        this.map1.clear();
    }
    protected cacheObject(info: ConfItem) {
        if (info.access === 'null') {
            info.access = null;
        } else if (info.access && info.access !== '') {
            let confOther: ConfOther = ConfOtherManager.getInstance().get();
            if (info.id === confOther.relief[1]) {
                info.access = StringUtils.format(info.access, confOther.relief[0]);
            } else if (info.dailyNum !== 0) {
                info.access = StringUtils.format(info.access, Math.abs(info.dailyNum));
            }
        }
        this.list.push(info);
        this.map.set(info.id, info);
        let arr: ConfItem[] = this.map1.get(info.targetType);
        if (!arr) {
            arr = [];
            this.map1.set(info.targetType, arr);
        }
        arr.push(info);
    }
    protected parseAfter(): void {
        //console.log(this);
    }

    public get(id: number): ConfItem {
        return this.map ? this.map.get(id) : null;
    }

    public getList(targetType?: number): ConfItem[] {
        return (!targetType || targetType < 0) ?  this.list : this.map1.get(targetType);
    }

}

我之所以写类型就是为了避免落后的手写代码读配置的操作,你这种方式我前个公司早就体验过了,非常的不方便

1,改了配置就要重新同步类型,读写等

2,我这个插件生成的TS在运行和编码时非常的方便,而且不需要额外的加载逻辑

3,插件的导出类型是做的可扩展方式

所以唯一的缺点就是没有压缩数据,这个也可以通过开发时使用TS,运行时使用二进制数据解决,当然目前我没有扩展导出和加载二进制数据

如果项目有很多配置数据,我觉得我预期的方案是

  • 打包时候全部打包为一个或者多个二进制文件
  • 打包时候同步有声明文件导出,方便编码时有提示
  • 运行时直接加载完这个二进制文件只需要映射不需要任何再解析就可以直接使用
  • 如果有以上逻辑,在其中加入加密和解密就更好.
1赞

如果是个人或者小型项目就没必要了,直接导出ts或者json就完事.

是的,发布时使用二进制和d.ts或者ts包装配合使用,这是最好的方式,但是不可避免会增加加载逻辑。这里我还没写,目前够用了

我不太了解. 打包时候这块内存其实和ts的属性结构已经映射好了,而且所有数据都是固定 好长度了,不可变. 所以加载的时候理应都直接内存映射就可以使用. 类似c++和c语言.

使用二进制你再怎么也得有加载和读取操作,加载只需要一次,读取是多次,所以这里要额外的代码支持

对啊,那只是基本的文件读入内存,不需要再解析这块内存数据.

是否是直接一块arrybuffer类似的承载这块数据,然后再和定义好的结构映射后,就可以直接使用了. 不太了解,不知道具体怎么实现.我觉得应该不会太复杂.

是这样的,我懒得去做,需要的时候我再去做,两种方式,
1,加载后解析所有数据并缓存
2,加载后缓存二进制然后动态解析

我更倾向于你说的也就是第二种

好像这就是序列化和反序列化…

你说的都是小问题,我们研发的项目几百个数值表,最初就是用的你这种(唯一的区别就是字段没定义类型,都是string),把表数据导入项目,后期远程动态更表非常不方便,而且表数据没有预处理,很多业务逻辑需要对配置数据重复的进行筛查过滤,导致不必要的性能损耗。

像上面这个任务表,你这种方式就是导入为json并限定类型,没有预处理,其它业务逻辑就得做处理,本质上是得不偿失的,预处理有个好处就是可以把每行数据映射为对象,并严格关联数据关系,预处理本质上就是给数据加索引的过程。

是支持导出JSON并不是说推荐使用JSON,支持导出JSON是为了服务器端需要使用的数据
客户端主要使用TS

另外你说的小问题在开发时会遇到
1,策划改了配置不说

2,改了配置类型漏改代码

3,生成数据时没有类型校验导致配置的数据错误需要反复核查

而上面这些问题添加了类型就解决了,而且我这个插件的类型并不像其他工具那么复杂

不不不,你不能仅考虑前端使用,毕竟不是所有项目都是单机运行的,针对你提出的三个问题:
1,策划改了配置,前后端需要同步改逻辑,不然生产事故谁负责?
2,绝大部分情况,改了配置,前端后端必然需要修改逻辑,而且修改完成后还需进行前后端验证,这是一种严格的开发部署流程,不能随性而为,不然生产事故谁负责?
3,数值表每行数据都映射为各个字段具有严格数据类型的对象,是不需要做类型校验的,反而预处理可以做整体数据校验,比如某些表配了某个奖励ID,但奖励表却没有配置这个ID对应的数据。

所以如果没有通知,那么你们只能在运行时报错才知道配置错了,而有些配置是很少出现在运行时的比如新手引导,这就是一个问题

而配置使用类型则可以在没有通知的情况下代码编辑器就会提示类型错误,这就是一个优点

另外我不知道你说的预处理是怎么做类型校验的,但是如果不能在从表格转配置数据时提示数据配置错误那就是一个Bug或者隐藏的生产事故

其实没必要纠结导表时做验证,既然是开发阶段,在csv数据表加载后ConfManager就可以做数据校验,虽然在运行期,但不影响开发流程,同样也能快速定位配置错误,也不会打断开发进度,你说的影响微乎其微,虽说你在导表时做了类型限定,但绝大部分情况,除开纯显示的文本数据,其它数值表数据修改后都需要修改业务逻辑,这种情况与是否在运行期没有太大关系,都需要程序员严格把控自己写的代码。

所以这就是区别,导出TS不需要手写任何代码,但是你的方案就需要手写所有配置的声明和校验代码。而人不可能不出错。

导出TS和导出时校验数据从一开始就杜绝了人为的错误

导出时校验和运行时校验有很大的区别,运行时校验时间成本和人力成本都会增加

你可以使用你的运行时校验,但是导出时校验一定优于运行时校验这是不可反驳的

优于,我不敢苟同,我举个例子,如果有要求验证策划数值表配置纰漏,比如一些配置表配置了某个奖励,而奖励表中没有配置这个数据,要校验出来,你这种做法是不是得每个引用了这个奖励ID的表都得遍历奖励表所有数据才能判定?没有数据预处理,取某个ID的奖励数据,还是得遍历所有数据,数据预处理你可以视作关系型数据库给数据建立索引,我只要在预处理那块,将奖励数据映射为:
Map<奖励ID, 奖励数据 ConfRewards>
是不是通过奖励ID取奖励数据比全表遍历的性能提升很多?同理,预处理还能进行更复杂的数据索引,特别是大型项目,这种性能优化绝对是有必要的。

一股脑的将配置表映射为JSON数组是最为不可取的,虽然你的工具将配置表转码为JSON做了类型校验,我个人觉得意义并不大,因为策划和技术对每个表的每个字段类型修改必然是需要相互了解的,比如你的逻辑用number[1, 2],策划要是改成string,虽然能通过你的类型校验,但你的逻辑是必须要同步修改的,否则在你所说技术不知策划改动的情况下,异常仍然将在运行期出现。

再比如如下数值表:

要将 access 字段中的通配符替换为 dailyNum 数据,没有数据预处理,兄台该如何应对?难道在每个业务逻辑那里编写代码替换吗?

再比如,我将道具表映射为这个对象
item1_20250604151039
只要道具表解析完成,注释部分的所有依赖关系将一次性产生,无需在业务逻辑处各种遍历,而你的方式就必须在业务逻辑中实时处理。

再比如,线上远程更新数值表,兄台如何应对?难道改某个字段数值都需要热更新 bundle 包?