写了个小玩意来展示我做的小玩意(禁止套娃
HGAME 完结撒花~
长达 4 周的 HGAME 2020 结束啦!因为是放寒假,所以每天几乎都睡到 12 才醒,醒来一看手机消息总会有几个学弟来问题。 陆陆续续下来,协会的招新工作也差不多接近尾声,一些有潜力的新生后面也要开始关注起来了。
在准备 HGAME 的空余时间呢,我依旧是在瞎写东西。Week2 在老家的时候把上学期开的 Pie-Baker 这个坑填了不少,估计以后可以写篇文章介绍一下。她是一个用 Go 编写的任务编排执行程序,有点像 iOS 上的 Workflow。你可以编写 JSON 格式的文件编排任务,然后 Pie-Baker 会解析执行。后期如果配上可视化的前端页面,用户只需用鼠标拖动就可以编排任务,像极了 Workflow。
回到深圳后,有天晚上看到博客的关于我页面,突然来了个新的点子。打算专门做个站来展示我以前写过的小玩意儿。但转而一想,如果又是一套 Gin + Vue 的增删改查未免也太无趣了点。
因而这次我便写了个静态网页生成器,我只需要写好一个个项目的 Markdown,她就可以生成对应的页面,甚至还有按照编程语言分类的页面。美其名曰:Ego
,因为刚开始写的时候尝试去读了 Hugo 的部分源码哈哈哈。
『My ego is big』
Ego 现在已经上线了!欢迎大家参观:https://p.github.red/
因为是全静态页面,这个网站目前是部署在七牛云 CDN 上的。为了方便上传部署,我使用了七牛的 Go SDK 给 Ego 加上了上传文件至七牛云的功能。 下面就开始具体聊聊 Ego 开发中我所遇到的坑吧~
最初开始写的时候,为了避免走太多弯路,我的想法是参考阅读一下前辈 Hugo 的代码。Hugo 发展到今天,已经是一个很成熟的静态网页生成器了,其支持的功能也是五花八门。去繁求简,我翻到了 Hugo 的第一个 commit,打算从她的第一版的核心代码开始看。
看了一个晚上后,我便开始按照 Hugo 最初的结构开始写:Page
一个结构体,其里面有一些例如RenderedContent
RawMarkdown
Draft
等等属性,然后又有Node
这个奇怪的结构体;这些看似有用的属性,我总感觉后面会有用到,因此全部也都照着这么写了。
直到写到后面我自己都无法理解啥是啥了,遂决定抛弃 Hugo 的那一套,最终还是按自己的想法来了。
以下便是 Ego 最后的目录结构了:
├── about.go “关于”页面
├── cli.go 命令行相关
├── config.go 全局的配置
├── data 用户的数据(我的 Markdown 就是放在这下面的)
│ ├── about.md “关于”页面内容
│ ├── config.toml 全局配置文件
│ └── project 项目文件夹
│ └── cube 这是我的示例项目 Cube
│ ├── cube.md 项目的简介(与文件夹同名)
│ └── history.yml 项目的更新历史记录文件
├── funcs.go Go template 自定义函数
├── helpers.go 常用的函数
├── language.go “编程语言分类”页面
├── main.go 入口
├── model.go 定义的结构体
├── page.go 单独一个页面的相关方法
├── precheck.go 运行前的预检(检查文件、目录是否存在)
├── project.go “项目详细”页面
├── public 渲染导出的页面
│ ├── ...
│
├── qiniu.go 七牛云相关
├── render.go 页面渲染相关
├── server.go 内置 HTTP 服务器,方便导出后快速预览
└── templates 模板文件
├── about.html “关于”页面模板
├── assets 静态文件(包括用户自己的图片)
│ ├── ...
├── index.html “主页”模板
├── language.html “编程语言分类”页面模板
├── layouts 每个页面渲染都要载入的模板
│ ├── footer.html 页头
│ ├── header.html 页尾
│ ├── include.html 引入的静态文件
│ └── profile.html 个人信息
└── project.html “项目”页面模板
可以看到这个 Ego 是很有“针对性”的,她就只能生成我这么一个项目展示网站,不像 Hugo 功能那么广泛而抽象,可以用于生成各种展示型站点。
Go 模板渲染
整个 Ego 的核心其实就是 Go 的template
包,这是 Go 自带的模板引擎。除了 Hugo 外,很多 Go Web 框架的模板引擎也是基于template
包的,比如 Beego。
我在之前的文章中也提到过:从一个项目看 beego 的 MVC
使用的方法其实很简单:
{
...
tpl := template.New(templateName).Funcs(r.FunctionMaps) // New 一个新的模板,并加入自定义的模板函数
...
}
func (p *Page) Render() ([]byte, error) {
tpl, err := p.Tpl.ParseFiles(append([]string{"./templates/" + p.TemplateName}, p.Layouts...)...) // 解析页面模板(其中 Layouts 是每个页面渲染都要载入的公有模板)
if err != nil {
return nil, err
}
var wr bytes.Buffer
p.Params["Title"] = p.Title // 设置标题
err = tpl.Execute(&wr, p.Params) // 将数据 p.Params 绑定到模板中,渲染!
if err != nil {
return nil, err
}
p.Content = wr.Bytes() // []bytes
return p.Content, nil
}
template
包里的相关方法虽然支持链式调用,但是这里的调用是有顺序要求的。首先是New()
指定模板名,一般与文件名相同。在使用ParseFiles()
解析模板文件前,需要先调用Funcs()
载入自定义的模板函数。
比如设置数据不进行 HTML 实体化的unescaped
函数,通过 Go 反射来计算切片或字典元素数量的count
函数。
最后使用Execute()
将绑定的数据送入模板中,渲染出 HTML 文本。
可以注意到这里的wr
我是转成[]bytes
而非string
类型进行存储的。最初我看 Hugo 也大量使用[]bytes
而不是string
,因此猜测[]bytes
可能会带来性能上的提升。在查阅了相关资料后,发现其实还是需要根据实际应用场景来选择。
string
在 Go 底层的定义其实是一个结构体:
type stringStruct struct {
str unsafe.Pointer
len int
}
存储了字符串的数组指针以及长度。
而byte
是uint8
的别名,因此[]byte
其实是个uint8
的切片。在之前写 RoarCTF Go 题的 writeup 里聊到了 Go 底层对 Slice 的定义:
type slice struct {
array unsafe.Pointer
len int
cap int
}
一个指向数据数组的指针,一个len
存储切片的长度,一个cap
存储切片的容量,后面append
的时候容量不够了会以 2 的倍数扩容,同时因为指针指向的是大小不可变的数组,因此会开辟一块新的数组存放数据。
这两种类型虽然都是指向一个数组,但是string
类型所指向的数组是只读的。这意味着只要你修改一个string
,它就必须分配一次内存,用一个新的数组来存储新的数据。而对slice
类型进行操作时,是可以修改其底层数组的内容的。只要不是因为拼接了一大串内容导致底层数组容量不够了需要扩容,像改几个字这种操作,是可以一直对指针指向的那一个数组进行操作的。
但是话说回来,string
类型却可以直接使用等号进行比较,但[]bytes
又有切片的那一套特性,可以轻松对单个字符进行修改。所以各有各的好,还是要按照实际使用场景来看。
模板中的 range
在 Ego 的project.html
中,有嵌套的两层循环:
{{ range $intro := .HistoryKey }}
<at-timeline-item>
<p>{{ $intro }}</p>
{{ range $item := index $.History $intro }}
<p>{{ $item }}</p>
{{ end }}
</at-timeline-item>
{{ end }}
外层是循环HistoryKey
这个切片中的元素intro
,内层再通过intro
作为下标取到History
这个 Map 中的元素。
这么做的原因是因为 Map 是无序的,每次渲染时 range 的元素顺序都不一样,因此我这里先对 Map 的 key 做了一波排序,再用排序好的 key 去取 value。
问题就在于这里用了两层循环,其中内层循环的History
是在执行Execute()
函数时传入的变量,按理来说直接使用.History
即可取得。但在循环中直接这么写却会报错。
需要在前面加上$
才行。这个问题当时也是困扰了我很久,甚至一度怀疑这是 Go 模板引擎的 bug 哈哈哈。
最后还是在 Stack Overflow 上找到了答案:https://stackoverflow.com/questions/43263280/go-template-cant-evaluate-field-x-in-type-y-x-not-part-of-y-but-stuck-in-a
Go template
包的文档里也进行的描述:
When execution begins, $ is set to the data argument passed to Execute, that is, to the starting value of dot.
即告诉我们在 range
if
with
等结构里,需要用$
来获取那些从Execute()
函数传入的变量。
还是循环的问题
下面一个也还是循环的问题,是我在写 Ego 的上方导航栏时遇到的问题,精简后的有问题代码如下:
users := []string{"aaa", "bbb", "ccc"}
test := make([]*string, 0)
for _, user := range users {
test = append(test, &user)
}
for k, v := range test {
fmt.Println(k, *v)
}
这里是通过一个循环将users
切片里的每个值的地址都传入了test
这个切片中。test
切片中的每个元素都是string
指针。最后的for
循环输出test
切片的值。
预期结果应该是:
0 aaa
1 bbb
2 ccc
然而实际结果却是:
0 ccc
1 ccc
2 ccc
怎么回事???前面两个指针的值都被第三个给覆盖了???
为了方便看出问题所在,我在for
循环中将user
的地址打印出来:
users := []string{"aaa", "bbb", "ccc"}
test := make([]*string, 0)
for _, user := range users {
fmt.Println(&user) // 这里打印一下 user 的地址
test = append(test, &user)
}
for k, v := range test {
fmt.Println(k, *v)
}
输出:
0xc000010210
0xc000010210
0xc000010210
0 ccc
1 ccc
2 ccc
原来for
循环体内每次循环的元素地址都是一样的!!每次循环都是将当前元素的值赋值给同一个变量。每次append
的都是user
的地址,而最后一次循环是将ccc
的值赋值给user
变量,所以最后就都是ccc
啦。
不过仔细想想也确实,Go 里面的一切都是值传递,而user
又是个string
类型的变量,每次循环都只是一个赋值的操作。
后来在网上找了一番,Stack Overflow 上的这个问题正好就是:https://stackoverflow.com/questions/39546804/differences-between-pointer-and-value-slice-in-for-range-loop
这个问题和回答都很棒,值得各位去细细阅读品味一番。他这里给出的例子比我这里更复杂一些。
在他这个例子中,print
方法是输入结构体指针*field
的。因此在对类型为field
的结构体调用这个方法时,虽然也是v.print()
,但这里其实是一个语法糖,写全了应该是:
(*field).print(&v)
可以看到这里就是在循环体里对迭代的变量v
取了地址,因此取到的都是同一个变量v
,就会出现我上面遇到的问题。
不过因为他这里是通过go
起协程来执行输出的,所以可以time.Sleep(100)
来放慢循环执行的速度,虽然每次都是取到同一个指针&v
,但是指针的值还是本次循环变量的值,还未改变。所以也能获得正确的输出。(当然这种做法只是扩展一下,加time.Sleep()
当然不能算是解决这个问题的办法。)
再说说七牛的事
Ego 会将最后渲染生成完的静态文件上传到七牛云仓库上,只需要在运行 Ego 时加上-p
参数即可。(是不是很方便嘻嘻)
七牛云算是国内我挺喜欢的一家云服务商,主要还是他们的 CDN 简单不做作,不像阿里云 OSS 那种买了存储包还要买流量包,并且价格也还算亲民。同时七牛后端的技术栈也是以 Go 为主。因此他们自家的产品当然也提供了 Go 语言的 SDK。
然而…… 你敢想象七牛云上传文件居然不支持批量上传!!官方文档很简单直白的给出的解决方案是:给单文件上传套个循环来实现批量上传……
不过有一说一,上传的速度还是挺快的,两秒的样子整个站就上去了。这还是在没开 CDN 上传加速的情况下。 前些天在 @Moesang 大佬的建议下,给博客的图片上了 webp,其实也就是七牛云的图像处理服务。然后实名认证的用户每个月有 20T 的免费额度,这么一算那就是不仅不要钱我还血赚。
最后再聊几句
寒假结束了呀,明天就要开始在家上网课了。其实回顾整个寒假,带回来的书几乎就没看过,更多的时间还是在瞎写东西。协会的线下 AWD 平台我已经写了 90%,自认为写得还不算差。然后在老家的时候填了下 Pie-Baker 的坑,之后就是这个 Ego 了。仔细一想其实学到的新东西很少。 有时候明知自己还很弱,差得很远,但是却一直没有动力去学。感觉总是在原地踏步啥的…… 不往远了说,就说平时写复杂一些东西,整个代码结构或者逻辑就挺乱的,或者说实现总是挺笨拙的。因此我现在不确定自己是否能开发一些大型的项目。 嘛,总之心情挺丧的,开学就是大二下了,我感觉自己在这个时间点已经落后了。无奈归无奈,但只能硬着头皮赶啊。
最后用最喜欢的一首歌精神支柱结束吧:
真っ暗で明かりもない 崩れかけたこの道で
在这暗无光明的崩坏道路上
あるはずもないあの时の希望が见えた気がした
我却感觉看到了 不应存在的往日的希望
どうして
为什么
ブラックロックシューター 懐かしい记忆
Black★Rock Shooter 令人怀念的记忆
ただ楽しかったあの顷を
那单纯感到快乐的时候
ブラックロックシューター でも动けないよ
Black★Rock Shooter 但我已无法动弹了
暗を駆ける星に愿いを
向着划过黑暗的星星许愿
もう一度だけ走るから
再一次就好 我要向前奔跑
喜欢这篇文章?为什么不打赏一下呢?