跳到主要内容
版本:Next

前端生成组件模版原理-Golang版

图片

结构体如下

package dto

type RequestBody struct {
WidgetName string `json:"widgetName"`
WidgetId string `json:"widgetId"`
Menu []map[string]interface{} `json:"menu"`
MenuMap map[string]map[string]interface{} `json:"menuMap"`
Type int `json:"type"`
}

type MenuConfig struct {
Menu []map[string]interface{}
MenuMap map[string]map[string]interface{}
}

type ErrorType struct {
string
int
error
}

CreateService 函数功能说明

CreateService 是一个用于自动生成前端组件模块(widget)的服务函数,核心流程如下:

路径初始化

  • 获取项目中 src/template/menu.ts、widgets 等路径。

模板复制

  • 使用 otiai10/copytemplate 文件夹复制到 src/,生成 widget 临时文件夹。

文件内容与名称替换

  • 遍历临时目录,将文件中 MyPluginName 等关键词替换为实际组件名。
  • 对应文件名同时重命名。

目录移动与清理

  • 删除中间过程路径;
  • 将临时 widget 目录迁移到 widgets/ 目录。

菜单更新

  • 构建并追加新的菜单项及映射至 menu.ts 文件;
  • 自动生成符合 TypeScript 类型声明的结构。

构建记录追加

  • 根据类型将组件 ID 写入 .build.local.build.prod 文件,用于后续追踪构建状态。
package service

import (
"app/dto"
"app/utils"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/gin-gonic/gin"
cp "github.com/otiai10/copy"
)

// 创建service
func CreateService(c *gin.Context, body dto.RequestBody) {
// 复制文件到上一层
absPath, _ := filepath.Abs("../src")
tplAbsPath, _ := filepath.Abs("../src/MyPluginName")
sourceAbsPath, _ := filepath.Abs("./template")
menuAbsPath, _ := filepath.Abs("../src/config/menu.ts")
widgetPath := filepath.ToSlash(absPath)
sourcePath := filepath.ToSlash(sourceAbsPath)
tplPath := filepath.ToSlash(tplAbsPath)
upWidget := utils.CapitalizeFirstLetter(body.WidgetId)
s, _ := filepath.Abs("../src/" + upWidget)
t, _ := filepath.Abs("../src/widgets/" + upWidget)
widgetSourcePath := filepath.ToSlash(s)
widgetTargetPath := filepath.ToSlash(t)
menuPath := filepath.ToSlash(menuAbsPath)

cpErr := cp.Copy(sourcePath, widgetPath)
if cpErr != nil {
utils.HandlerErr(c, cpErr)
return
}

// 扫描复制的文件夹
var matchDirs []string
filepath.WalkDir(tplPath, func(path string, d os.DirEntry, err error) error {
if err != nil {
utils.HandlerErr(c, err)
return err
}
matchDirs = append(matchDirs, path)
return nil
})
entityName := body.WidgetId + "Entity"
for _, dir := range matchDirs {
// 替换文件内容
utils.ReplaceFileContent(dir, []string{body.WidgetId, entityName, "MyPluginName", "MyEntityName"}, body.WidgetName)
// 修改文件名称
if strings.Contains(dir, "MyPluginName") {
if !strings.Contains(dir, "MyEntityName") {
utils.RenameFile(dir, body.WidgetId, "MyPluginName")
} else {
utils.RenameFile(dir, body.WidgetId, "MyPluginName", entityName, "MyEntityName")
}
}

}
// 删除目录
fileErr := os.RemoveAll(tplAbsPath)
if fileErr != nil {
utils.HandlerErr(c, fileErr)
return
}
// 移动目录,
// err := os.Rename(widgetSourcePath, widgetTargetPath)
err := cp.Copy(widgetSourcePath, widgetTargetPath)
if err != nil {
utils.HandlerErr(c, err)
return
}
// 删除目录
moveErr := os.RemoveAll(widgetSourcePath)
if moveErr != nil {
utils.HandlerErr(c, moveErr)
return
}
// 更新 menu.ts
newMenuMap := map[string]interface{}{
"icon": "p",
"name": body.WidgetName,
"notPage": false,
"patchName": body.WidgetId,
"path": "/information-base/" + body.WidgetId,
}
body.Menu = append(body.Menu, newMenuMap)
body.MenuMap[body.WidgetId] = newMenuMap
menuStr, menuErr := generateMenuStr(body.Menu, 1)
if menuErr != nil {
utils.HandlerErr(c, menuErr)
}
menuMapStr, menuMapErr := generateMenuStr(body.MenuMap, 2)
if menuMapErr != nil {
utils.HandlerErr(c, menuMapErr)
}
menuSumStr := menuStr + menuMapStr
// 写入文件
os.WriteFile(menuPath, []byte(menuSumStr), 0777)
appendFileContent(body.WidgetId, body.Type)
}

func generateMenuStr[T any](menuConfig T, menuType int) (string, error) {
menuJson, menuErr := json.Marshal(menuConfig)
var menuKey string
var menuTypeKey string
if menuType == 1 {
menuKey = "menu"
menuTypeKey = "Record<string,any>[]"
} else {
menuKey = "menuMap"
menuTypeKey = "Record<string,any>"
}
str := fmt.Sprintf("export const %s: %s = ", menuKey, menuTypeKey)
menuStr := str + string(menuJson) + "\n"
return menuStr, menuErr
}

func appendFileContent(widgetId string, fileType int) (string, error) {
absPath, _ := filepath.Abs("../.build.local")
prodPath, _ := filepath.Abs("../.build.prod")
local := filepath.ToSlash(absPath)
prod := filepath.ToSlash(prodPath)

var name string
if fileType == 1 {
name = local
} else {
name = prod
}
// 打开文件,如果文件不存在则创建
file, err := os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0777)
if err != nil {

return widgetId, err
}
defer file.Close()

// 追加内容到文件
_, err = file.WriteString("\n" + widgetId + "\n")
if err != nil {
return widgetId, err
}
return widgetId, err
}

package utils

import (
"net/http"
"os"
"path/filepath"
"strings"
"unicode"

"github.com/gin-gonic/gin"
cp "github.com/otiai10/copy"
)

// 统一处理错误
func HandlerErr(c *gin.Context, err error) error {
c.JSON(http.StatusBadRequest, gin.H{"message": err})
return err
}

// 首字母大写
func CapitalizeFirstLetter(s string) string {
if s == "" {
return s
}
runes := []rune(s)
runes[0] = unicode.ToUpper(runes[0])
return string(runes)
}

// 首字母小写
func DecapitalizeFirstLetter(s string) string {
if s == "" {
return s
}
runes := []rune(s)
runes[0] = unicode.ToLower(runes[0])
return string(runes)
}

func isDirectory(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false // 出错(例如路径不存在),默认返回 false
}
return info.IsDir() // 如果是目录,返回 true
}

// 读取文件后,并替换文件内容,同步
func ReplaceFileContent(sourceFile string, configArr []string, widgetName string) {
name := configArr[0]
entityName := configArr[1]
oldName := configArr[2]
oldEntityName := configArr[3]
sFile, _ := filepath.Abs(sourceFile)
// 插件名
upName := CapitalizeFirstLetter(name)
lowName := DecapitalizeFirstLetter(name)
// 旧插件名
upOldName := CapitalizeFirstLetter(oldName)
lowOldName := DecapitalizeFirstLetter(oldName)
// 新实体名
upEntityName := CapitalizeFirstLetter(entityName)
lowEntityName := DecapitalizeFirstLetter(entityName)
// 旧实体名
upOldEntityName := CapitalizeFirstLetter(oldEntityName)
lowOldEntityName := DecapitalizeFirstLetter(oldEntityName)
// tFile, _ := filepath.Abs(targetFile)
content, err := os.ReadFile(sFile)
if err != nil {
return
}

newContentStr := strings.ReplaceAll(string(content), "${{widgetName}}", widgetName)
newStr := strings.ReplaceAll(string(newContentStr), upOldName, upName)
pluginStr := strings.ReplaceAll(newStr, lowOldName, lowName)
entityStr := strings.ReplaceAll(pluginStr, upOldEntityName, upEntityName)
entityLowStr := strings.ReplaceAll(entityStr, lowOldEntityName, lowEntityName)
// 写入到文件中
os.WriteFile(sourceFile, []byte(entityLowStr), 0777)
}

// 修改文件名称
func RenameFile(dir string, targetName string, oldName string, entity ...string) {
slashDir := filepath.ToSlash(dir)
targetDir := strings.ReplaceAll(slashDir, oldName, targetName)
if len(entity) == 0 {

if strings.Contains(targetDir, "MyPluginName") || strings.Contains(targetDir, "MyEntityName") {
return
}
if !isDirectory(slashDir) {
cp.Copy(slashDir, targetDir)
}
} else {
targetEntityDir := strings.ReplaceAll(targetDir, entity[1], entity[0])
if strings.Contains(targetEntityDir, "MyPluginName") || strings.Contains(targetEntityDir, "MyEntityName") {
return
}
if !isDirectory(slashDir) {
cp.Copy(slashDir, targetEntityDir)
}
}
}