为何做出这样的技术选型
由于我自己的服务器资源紧张,也不想在这上面多花钱,就想到webassembly这个技术。全部编译成静态资源托管到cloudflare的page上面,就如同我这个博客一样。 面对这样的需求我一下子就想到对wasm支持比较好的rust、golang和kotlin,但是由于我对rust不怎么感兴趣也没学过就成了golang与kotlin二选一的结局了。最后考虑到kotlin跨平台目前使用的前端UI还是jetpack,我自己对android开发不熟悉,最后选了golang的go-app来实现这一需求。至于js呢,js对于二进制文件的处理以及io方面的操作不是很方便,加上go标准库的优秀以及wasm运行效率上区别,还有就是我想用些有趣的方式去实现。
重要功能的实现
模板
模板资源就是一个个的特殊的java项目,里面的关键位置使用了golang标准库中的template来实现替换,并在编译时将这些文件一起编译到可执行文件中。在调用替换方法后会将前端传来的参数在里面一一替换。
不同的模板自己实现自己的生成逻辑,最后统一实现生成函数接口,给调用者。
示例代码
统一生成zip压缩包二进制文件接口
type TemplateData interface { GenZip() ([]byte, error)}func (data WebTemplateData) GenZip() ([]byte, error) { var buf bytes.Buffer zw := zip.NewWriter(&buf) defer zw.Close()
// 遍历assets/gradle/web/2目录下所有文件 err := fs.WalkDir(distFS, fmt.Sprintf("assets/gradle/web/%d", data.SpringBootVersion), func(path string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr }
// 计算相对路径(去除assets/gradle/web/2前缀) relPath := strings.TrimPrefix(path, fmt.Sprintf("assets/gradle/web/%d/", data.SpringBootVersion)) if relPath == path { // 处理根目录情况 relPath = "" }
// 替换文件路径 if strings.Contains(relPath, "moduleName") { relPath = strings.ReplaceAll(relPath, "moduleName", data.ModuleName) }
if d.IsDir() { // 跳过根目录的创建(relPath为空时不创建) if relPath != "" { // 创建目录条目(zip目录需要以/结尾) _, err := zw.Create(relPath + "/") return err } return nil } // 判断文件是否需要替换 var fileData []byte if strings.Contains(relPath, "build.gradle.kts") { strData, err := data.GenBuildKts() if err != nil { return err } fileData = []byte(strData) } else if strings.Contains(relPath, "settings.gradle.kts") { strData, err := data.GenSettingsKts() if err != nil { return err } fileData = []byte(strData) } else if strings.Contains(relPath, "application.properties") { strData, err := data.GenApplication() if err != nil { return err } fileData = []byte(strData) } else if strings.Contains(relPath, "run.sh") { strData, err := data.GenRunSh() if err != nil { return err } fileData = []byte(strData) } else if strings.Contains(relPath, "start_web.sh") { strData, err := data.GenStartWebSh() if err != nil { return err } fileData = []byte(strData) } else if strings.Contains(relPath, "dk_product_component.yaml.j2") { strData, err := data.GenBlueprint() if err != nil { return err } fileData = []byte(strData) } else if strings.Contains(relPath, "README.md") { strData, err := data.GenReadme() if err != nil { return err } fileData = []byte(strData) } else { // 读取文件内容 fileDataS, err := distFS.ReadFile(path) if err != nil { return err } fileData = fileDataS }
info, err := d.Info() if err != nil { return err } // 创建文件头 fh, err := zip.FileInfoHeader(info) if err != nil { return err } fh.Name = relPath // 设置文件在zip中的路径 fh.Method = zip.Deflate // 使用默认压缩算法
// 写入文件内容 fileWriter, err := zw.CreateHeader(fh) if err != nil { return err } _, err = fileWriter.Write(fileData) return err })
if err != nil { return nil, err }
// 确保所有数据写入buffer if err := zw.Close(); err != nil { return nil, err }
return buf.Bytes(), nil}模板内容替换逻辑
func (data WebTemplateData) GenBuildKts() (string, error) { // 读取嵌入的模板文件 buildBytes, err := distFS.ReadFile(fmt.Sprintf("assets/gradle/web/%d/build.gradle.kts", data.SpringBootVersion)) if err != nil { return "", err // 改为返回错误而非 panic }
// 解析模板内容 tpl, err := template.New("buildGradle").Parse(string(buildBytes)) if err != nil { return "", err }
var result strings.Builder if err := tpl.Execute(&result, data); err != nil { return "", err } return result.String(), nil // 返回生成后的内容和错误
}UI
UI使用了go-app这个项目来操作dom元素,其中的难点是css样式和二进制文件保存。其中样式借助tailwind来写组件,再将他们排列组合。二进制文件的保存就很麻烦了,首先在dom树中添加了一个a标签,url上填入了压缩包的base64编码的字符串,在模拟点击触发使得浏览器下载压缩包,然后再移除这个元素。
示例代码
UI 组件
以button为例看起来其实和大部分的前端ui框架差不多
package compose
import "github.com/maxence-charriere/go-app/v10/pkg/app"
// Button 通用按钮组件(Tailwind版,类名拆分)// btnType 可选值:"primary"(默认)、"secondary"、"tertiary"、"warning"、"danger"func Button(name, btnType string) app.HTMLButton { button := app.Button(). Type("submit"). Text(name). // 通用基础样式(每个类名单独作为字符串参数) Class( "rounded-md", "px-4", "py-2", "text-sm", "font-medium", "border", "border-transparent", "cursor-pointer", "transition-all", "duration-200", "ease-in-out", )
// 根据类型设置具体样式(每个类名单独作为字符串参数) switch btnType { case "primary": // 主按钮 button = button.Class( "bg-[#5072a7]", "text-white", "hover:bg-[#466492]", "active:bg-[#3d577c]", )
case "secondary": // 次要按钮 button = button.Class( "bg-white", "text-[#6c757d]", "border-[#6c757d]", "hover:bg-[#f8f9fa]", "active:bg-[#e9ecef]", )
case "tertiary": // 第三级按钮 button = button.Class( "bg-transparent", "text-[#5072a7]", "hover:bg-[#f0f4f8]", "active:bg-[#e6edf3]", )
case "warning": // 警告按钮 button = button.Class( "bg-[#ffc107]", "text-[rgba(0,0,0,0.85)]", "hover:bg-[#ffca2c]", "active:bg-[#ffb700]", )
case "danger": // 危险按钮 button = button.Class( "bg-[#dc3545]", "text-white", "hover:bg-[#e04b59]", "active:bg-[#c82333]", )
default: // 默认主按钮 button = button.Class( "bg-[#5072a7]", "text-white", "hover:bg-[#466492]", "active:bg-[#3d577c]", ) } return button}操作dom元素保存二进制文件
package template
import ( "encoding/base64"
"github.com/maxence-charriere/go-app/v10/pkg/app")
func SaveZipDataLocally(ctx app.Context, e app.Event, zipBytes []byte, fileName string) {
// 将ZIP文件内容编码为Base64 b64Data := base64.StdEncoding.EncodeToString(zipBytes)
// 创建Data URL dataURL := "data:application/zip;base64," + b64Data
// 确保DOM操作在安全的上下文中执行 ctx.Defer(func(ctx app.Context) { // 创建下载链接 a := app.Window().Get("document").Call("createElement", "a") a.Set("href", dataURL) a.Set("download", fileName)
// 添加到DOM并触发点击 app.Window().Get("document").Get("body").Call("appendChild", a) a.Call("click")
// 移除链接 a.Call("remove")
})}积累
- 熟悉goalng的一些偏门用法。
- 更加熟悉外包项目中的模板渲染。
- 脱离框架后发现自己还挺行的。
踩坑
- 前端组建和样式太难写了,最后还是借助了tailwind来写样式。
- wasm中调用浏览器事件的难度超乎我的想象。
- 野心太大想要同时维护native app和web app,导致工作量太大了,使得功能迭代太慢了。
- 互联网上可以借鉴的文章太少了只能自己一点点尝试。
有成功帮我摸到鱼吗
目前来说有但是不多,摸鱼的时间被我拿去优化这个生成器和学习zig去了。虽然提高了工作效率,但是也有可能导致需要完成的工作量变得更多,工资也不会涨。
吐槽
用vscode在windwos上写go是真的卡,我自己电脑的配置本身也不低了,对比goland和zed简直卡暴了,开箱即用还得是goland。