{"id":19725438,"url":"https://github.com/easychen/metatoy2","last_synced_at":"2025-04-29T23:30:44.741Z","repository":{"id":145559151,"uuid":"596557112","full_name":"easychen/MetaToy2","owner":"easychen","description":"通用命令行和可视化代码生成工具 ","archived":false,"fork":false,"pushed_at":"2023-02-17T03:48:07.000Z","size":1743,"stargazers_count":19,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-05T20:11:14.476Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"EJS","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/easychen.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-02-02T12:56:27.000Z","updated_at":"2023-10-31T08:54:11.000Z","dependencies_parsed_at":"2023-09-08T01:16:27.417Z","dependency_job_id":null,"html_url":"https://github.com/easychen/MetaToy2","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easychen%2FMetaToy2","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easychen%2FMetaToy2/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easychen%2FMetaToy2/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/easychen%2FMetaToy2/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/easychen","download_url":"https://codeload.github.com/easychen/MetaToy2/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251599734,"owners_count":21615574,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-11T23:29:52.997Z","updated_at":"2025-04-29T23:30:44.724Z","avatar_url":"https://github.com/easychen.png","language":"EJS","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MetaToy2\n\n![](images/icon.png)\n\nMetaToy 是一个通用代码生成工具。它包含两部分：\n\n1. 命令行 MetaToy Cli, 采用GPLv3 开源\n1. 图形界面  MetaToy UI, 不开源但可以免费使用\n\n![](images/20230114120244.png)\n\n## 使用方式\n\n通常来讲，MetaToy 作为项目级生成器使用，每个项目可在跟目录建立 .metatoy 目录，并在其中放置配置文件和对应的模板。本文将按此进行说明。\n\n当然你也可以按其他方式使用，是要保证 metatoy.settings.jsonc 文件和其他目录的相对位置不变即可。\n\n## 命令行 MetaToy Cli\n\n运行 MetaToy Cli 需要 Node 环境(14+)\n\n一个典型的命令如下：\n\n```bash\nnode .metatoy/exec.mjs new api controller --name=Book --cn_name=书籍\n```\n\n其中 `api controller` 部分对应 `.metatoy/_template` 下的 `api/controller.tpl.ejs` 文件。\n\n如果想要生成 api 目录下的全部模板，那么可以 \n\n```bash\nnode .metatoy/exec.mjs new api --name=Book --cn_name=书籍\n```\n\n具体逻辑可以查看 `.metatoy/exec.mjs`\n\n#### 模版\n\n代码模板存放于 `.metatoy/_template` 下，可自行分目录存放，模板文件均需要以 `.tpl.ejs` 结尾。\n\n模板采用`ejs`语法，可[访问官网查看](https://github.com/mde/ejs)\n\n`.metatoy/_template` 目录中默认包含的是全栈引擎 MetaStack 的前后端模版，不需要的话可以直接删除。\n\n##### 头标记\n\n模板头部必须包含头标记，其格式如下：\n\n```\n/* @MT-TPL-FILE\n * @Desc: API接口\n * @To: api/app/Http/Controllers/\u003c%=OPT.TheName%\u003eController.php \n * @Replace: -\n */\n```\n\n其中 To 参数指明了生成后的模板写入哪个文件；Replace 则指定了文件已经存在时的行为。如果设置为 `@Replace: overwrite` 会每次重新生成全文，并覆盖原有文件。\n\n但这种模式下手工修改会丢失。所以当设置为 `@Replace: -` 时会开启智能覆盖模式。\n\n##### 区块标记\n\n智能覆盖模式不会直接覆盖整个文件，它会在模板中搜索区块标记，只覆盖目标文件中由同样标记包裹的部分内容。\n\n以下代码就包含了由 `@MT-TPL-LIST-START` 和 `@MT-TPL-LIST-END` 构成的区块标记。\n```php\npublic function search()\n{\n    /* @MT-TPL-LIST-START */\n    $\u003c%=OPT.the_name%\u003e_array = \u003c%=OPT.TheName%\u003e::all();\n    /* @MT-TPL-LIST-END */\n\n    return http_result( ['\u003c%=OPT.the_name%\u003e_array'=\u003e$\u003c%=OPT.the_name%\u003e_array] );\n    \n}\n```\n\n在第一次生成时，以上模板代码会被编译为：\n\n```php\npublic function search()\n{\n    /* @MT-TPL-LIST-START */\n    $recharge_cards_array = RechargeCards::all();\n    /* @MT-TPL-LIST-END */\n\n    return http_result( ['recharge_cards_array'=\u003e$recharge_cards_array] );\n\n}\n\n```\n\n这个时候如果我们想对返回值进行加工，可以在生成好的代码中的区块标记后手工添加代码：\n\n```php\npublic function search()\n{\n    /* @MT-TPL-LIST-START */\n    $recharge_cards_array = RechargeCards::all();\n    /* @MT-TPL-LIST-END */\n\n    // 这里是手工修改\n    if( $recharge_cards_array['creator_uid'] != auth()-\u003eid() )\n    return send_error( \"只能查询自己的充值卡\", \"AUTH\" );\n\n    return http_result( ['recharge_cards_array'=\u003e$recharge_cards_array] );\n\n}\n```\n\n第二次生成时，MetaToy 只会替换区块标签包裹部分的内容，手工修改得以保留。\n\n##### 区块标记的高级用法\n\n###### 追加模式\n\n默认情况下，区块标记会用新生成的内容替换原有内容。但在一些场景，比如路由、列表中，我们想要的不是替换，而是追加。这个时候可以设置区块的 Replace 模式为 append。\n\n以下是一个实际例子：\n```php\n/* @MT-TPL-ROUTE-LIST-START */\n/* @Replace: append */\n/* @SkipIf: Route::post('/\u003c%=OPT.the_name%\u003e */\n\n\u003c%_ for( action of ['save','update','remove','search'] ){_%\u003e\nRoute::post('/\u003c%=OPT.the_name%\u003e/\u003c%=H.low(action)%\u003e', 'App\\Http\\Controllers\\\u003c%=OPT.TheName%\u003eController@\u003c%=H.lc(action)%\u003e');    \n\u003c%_}_%\u003e\n\n/* @MT-TPL-ROUTE-LIST-END */\n```\n\n###### Skip\n\n追加模式必然会遇到一个问题，就是重复执行生成命令的时候，会出现重复追加。为了避免这个问题，我们又引入了 `SkipIf` 和 `SkipRegex` 两个设置。\n\n当之前生成的代码中包含字符串或者匹配正则时，会自动跳过追加过程。这就解决重复追加的问题。\n\n###### Wrap\n\n由于我们采用类似 `/* @MT-TPL-NAV-START */` 的注释来实现区块标记，在React中，直接使用注释会作为内容直接输出，因此我们需要用 `{}` 将其包裹起来。\n\n```jsx\n{/* @MT-TPL-NAV-START */\n/* @Replace: append */\n/* @Wrap: react */\n/* @SkipIf: to=\"/\u003c%=OPT.the_name%\u003e/list\" */\n\n\u003cNavLink to=\"/\u003c%=OPT.the_name%\u003e/list\" className=\"nav-link p-3 block hover:bg-blue-100 flex flex-row items-center\"\u003e\u003cFaRegDotCircle size={20}  className=\"m-2\"/\u003e\u003c%=H.as(OPT.TheName)%\u003e\u003c/NavLink\u003e\n\n/* @MT-TPL-NAV-END */}\n```\n\n当有多个内容时就会生成如下代码：\n\n```jsx\n{/* @MT-TPL-NAV-START */\n    \n    \u003cNavLink to=\"/recharge_cards/list\" className=\"nav-link p-3 block hover:bg-blue-100 flex flex-row items-center\"\u003e\u003cFiCreditCard size={20}  className=\"m-2\"/\u003eRecharge Cards\u003c/NavLink\u003e\n\n    \u003cNavLink to=\"/user/api/token\" className=\"nav-link p-3 block hover:bg-blue-100 flex flex-row items-center\"\u003e\u003cFiKey size={20}  className=\"m-2\"/\u003eAPI Token\u003c/NavLink\u003e\n\n/* @MT-TPL-NAV-END */}\n```\n在 HTML 中这很正常，但是在 React 中，一个组件的根对象不能有两个，因此我们只能进行包裹，这样才不会报错：\n\n```jsx\n{/* @MT-TPL-NAV-START */\n    \n    \u003c\u003e\n    \u003cNavLink to=\"/recharge_cards/list\" className=\"nav-link p-3 block hover:bg-blue-100 flex flex-row items-center\"\u003e\u003cFiCreditCard size={20}  className=\"m-2\"/\u003eRecharge Cards\u003c/NavLink\u003e\n\n    \u003cNavLink to=\"/user/api/token\" className=\"nav-link p-3 block hover:bg-blue-100 flex flex-row items-center\"\u003e\u003cFiKey size={20}  className=\"m-2\"/\u003eAPI Token\u003c/NavLink\u003e\n    \u003c/\u003e\n\n/* @MT-TPL-NAV-END */}\n```\n开启 `@Wrap: react` 参数，可以让 MetaToy 帮你自动包裹。\n\n```jsx\n{/* @MT-TPL-NAV-START */\n/* @Replace: append */\n\n/* @Wrap: react */ \u003c-- 这里\n\n/* @SkipIf: to=\"/\u003c%=OPT.the_name%\u003e/list\" */\n\n\u003cNavLink to=\"/\u003c%=OPT.the_name%\u003e/list\" className=\"nav-link p-3 hover:bg-blue-100 flex flex-row items-center\"\u003e\u003cFaRegDotCircle size={20}  className=\"m-2\"/\u003e\u003c%=H.as(OPT.TheName)%\u003e\u003c/NavLink\u003e\n\n/* @MT-TPL-NAV-END */}\n```\n\n\n\n#### 字段数据\n\n在模板中我们需要知道数据库字段在各个场景下的形态，比如是否参与验证、验证的rule。这些信息被放置一个JSONC文件（JSON格式，可写入注释）中 `.metatoy/metatoy.data.jsonc`。其基本格式为：\n\n```\n{\n    \"project\": \"metastack\",\n    \"version\": \"1.0\",\n    \"tables\": {\n        \"article\": {\n            \"description\": \"文章表\",\n            \"order\": 1,\n            \"scenarios\": {\n                \"default\": {\n                    \"description\": \"默认场景\",\n                    \"order\": 1,\n                    \"fields\": {\n                        \"title\": {\n                            \"cn_name\": \"标题\",\n                            \"type\": \"string\",\n                            \"validate\": \"required|string\",\n                            \"ui\": \"text-line\",\n                            \"ui_options\": {\n                                \"placeholder\": \"请输入标题\"\n                            }\n                        },\n                        ...\n```\n\n#### 帮助函数\n\n在模板中，我们需要对这些数据进行查询、循环和显示。这些代码有大量的重复，因此我们将其封装到 Helper 中，可以在 `helper.mjs`中进行修改和添加。\n\nfields 可能是模板中最常用的函数，它根据表名和场景获取字段数据。其定义如下：\n\n```js\nfields( meta, table, scenario=null, array=true )\n{\n        return this.section( meta, table, scenario, 'ALL', array );\n}\n```\n\n在模板中我们通过`H.fields`来使用它：\n\n```js\n\n\u003c%_ for( const field of H.fields(DATA,OPT.the_name,'create')??[])\n{ if( field.source_func ){_%\u003e\n$validated['\u003c%=field.name%\u003e'] = \u003c%=field.source_func%\u003e;\n\u003c%_ }} _%\u003e\n\n```\n\n其中 DATA 为字段和环境数据，DATA.DB 为  `.metatoy/metatoy.data.jsonc` 对应的 JSON 对象。\n\nOPT则是命令行传入的参数，典型的是 `--name` 和 `--cn_name`。为了方便使用，我们还对name进行了自动大小写处理：\n\n```\noptions.theName = helper.lc(options.name); // 小驼峰\noptions.TheName = helper.bc(options.name); // 大驼峰\noptions.the_name = helper.ul(options.name); // 小写+下划线分隔\n```\n\n可以通过 `OPT.the_name`等方式直接在模板中使用。\n\n\n#### 将数据绑定到字段\n\n手工编写 `.metatoy/metatoy.data.jsonc` 非常麻烦。尤其是进行字段修改后要同步维护它，这会导致我们有多份信息源，不符合元编程的精神。\n\n我们可以直接从数据库中读取字段信息，但这部分信息并不包含关于界面、验证和其他。\n\n一个好消息是，MySQL等数据库都提供了字段注释，我们可以把额外信息存放到里边。\n一个坏消息是，这个注释最长只支持255，这让我们很难存放大量信息（比如格式过滤函数代码）。\n\nMetatoy给出的解决方案是只在注释中存放一个唯一ID，而将ID对应的信息存放到一个Key-Value形式的文件中，这个唯一ID被称为MTID，而这个文件就是 `.metatoy/metatoy.kv.json`。\n\n如果你使用 MetaStack ，那么这个ID可以以 `@MTID=xxx-xxx-xxx`的方式手工添加进去，也可以使用我们提供的 Laravel 命令： \n\n```php\nphp artisan metatoy:mtid {table}\n```\n\n这个命令会监测数据表的注释，并生成一个 Migration，用于给所有不包含 @MTID 的字段自动添加MTID。\n\n你可以运行以下命令执行它。\n\n```php\nphp artisan migrate\n```\n\n#### 图形界面\n\n由于数据分处两地，这让我们使用起来非常不直观，因此我们开发了一个图形界面 MetaToyUI 来提升使用体验。该程序可以从仓库的 `release` 处获取。\n\n要使用 MetaToyUI，首先你需要在配置文件`.metatoy/metatoy.settings.jsonc`中设置一些基本信息：\n\n```jsonc\n{\n    // 数据库连接信息\n    \"mysql\":{\n        \"host\":\"localhost\",\n        \"port\":3306,\n        \"user\":\"root\",\n        \"password\":\"\",\n        \"database\":\"metastack\"\n    },\n    // 场景列表\n    \"scenarios\":[\n        {\"id\":\"default\",\"name\":\"默认\"}\n        // ...\n    ],\n    // 扩展字段和对应类型和数据\n    // type 支持 checkbox \n    \"extra_fields\":[\n        {\"id\":\"cn_name\",\"name\":\"中文名\",\"values\":null},\n        // ...\n    ]\n}\n```\n\n你可以参考现有文件进行修改。注意默认场景（default）是一个特殊场景，其他场景如果字段值为空，那么会从默认场景读取。\n\n然后启动 MetaToyUI，点击左侧 Logo 下的文件图标，载入刚才编辑的文件。注意MetaToyUI 会根据它来计算其他依赖文件的路径。\n\n![](images/20230114151535.png)  \n\n\n成功连接数据库后，MetaToyUI 会载入数据库字段以供选择和设置。\n\n![](images/20230114151936.png)  \n\n右侧黄色框选区域会列出 `.metatoy/_template` 目录下所有模板，可以选中多个以后点击「生成」按钮。\n\n生成提示结果会在按钮上方显示，如果觉得显示区域不够大，可以用快捷键 Cmd+Alt+I 调出 Devtools 查看日志。\n\n\u003e 特别提醒：MetaToyUI 并不能独立工作，它依赖 MetaToyCli，因此需要确保命令行环境可用。以及数据库字段必须有 MTID，否则在界面设置的信息将无法成功保存。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feasychen%2Fmetatoy2","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feasychen%2Fmetatoy2","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feasychen%2Fmetatoy2/lists"}