友链朋友圈

cenyuio个人博客

GO 的 Web 开发系列(五)—— 使用 Swagger 生成一份好看的接口文档

<p>经过前面的文章,已经完成了 Web 系统基础功能的搭建,也实现了 API 接口、HTML 模板渲染等功能。接下来要做的就是使用 <code>Swagger</code> 工具,为这些 Api 接口生成一份好看的接口文档。</p><h2 id="一-写注释">一、写注释</h2><p>注释是 <code>Swagger</code> 的灵魂,<code>Swagger</code> 是通过特定格式的注释生成接口文档的。</p><h3 id="1-1-基础注释">1.1 基础注释</h3><p>这部分基础注释对全接口文档通用,指定接口文档的基础信息,可添加在 <code>main</code> 函数上。</p><table><thead><tr><th>注释</th><th>说明</th><th>示例</th></tr></thead><tbody><tr><td>title</td><td><strong>必填</strong> 应用程序的名称。</td><td>// @title Swagger Example API</td></tr><tr><td>version</td><td><strong>必填</strong> 提供应用程序API的版本。</td><td>// @version 1.0</td></tr><tr><td>description</td><td>应用程序的简短描述。</td><td>// @description This is a sample server celler server.</td></tr><tr><td>tag.name</td><td>标签的名称。</td><td>// @tag.name This is the name of the tag</td></tr><tr><td>tag.description</td><td>标签的描述。</td><td>// @tag.description Cool Description</td></tr><tr><td>tag.docs.url</td><td>标签的外部文档的URL。</td><td>// @tag.docs.url<a href="https://example.com/">https://example.com</a></td></tr><tr><td>tag.docs.description</td><td>标签的外部文档说明。</td><td>// @tag.docs.description Best example documentation</td></tr><tr><td>termsOfService</td><td>API的服务条款。</td><td>// @termsOfService<a href="http://swagger.io/terms/">http://swagger.io/terms/</a></td></tr><tr><td>contact.name</td><td>公开的API的联系信息。</td><td>// @contact.name API Support</td></tr><tr><td>contact.url</td><td>联系信息的URL。 必须采用网址格式。</td><td>// @contact.url<a href="http://www.swagger.io/support">http://www.swagger.io/support</a></td></tr><tr><td>contact.email</td><td>联系人/组织的电子邮件地址。 必须采用电子邮件地址的格式。</td><td>// @contact.email<a href="mailto:support@swagger.io">support@swagger.io</a></td></tr><tr><td>license.name</td><td><strong>必填</strong> 用于API的许可证名称。</td><td>// @license.name Apache 2.0</td></tr><tr><td>license.url</td><td>用于API的许可证的URL。 必须采用网址格式。</td><td>// @license.url<a href="http://www.apache.org/licenses/LICENSE-2.0.html">http://www.apache.org/licenses/LICENSE-2.0.html</a></td></tr><tr><td>host</td><td>运行API的主机(主机名或IP地址)。</td><td>// @host localhost:8080</td></tr><tr><td>BasePath</td><td>运行API的基本路径。</td><td>// @BasePath /api/v1</td></tr><tr><td>accept</td><td>API 可以使用的 MIME 类型列表。 请注意,Accept 仅影响具有请求正文的操作,例如 POST、PUT 和 PATCH。 值必须如“<a href="https://github.com/swaggo/swag/blob/master/README_zh-CN.md#mime%E7%B1%BB%E5%9E%8B">Mime类型</a>”中所述。</td><td>// @accept json</td></tr><tr><td>produce</td><td>API可以生成的MIME类型的列表。值必须如“<a href="https://github.com/swaggo/swag/blob/master/README_zh-CN.md#mime%E7%B1%BB%E5%9E%8B">Mime类型</a>”中所述。</td><td>// @produce json</td></tr><tr><td>query.collection.format</td><td>请求URI query里数组参数的默认格式:csv,multi,pipes,tsv,ssv。 如果未设置,则默认为csv。</td><td>// @query.collection.format multi</td></tr><tr><td>schemes</td><td>用空格分隔的请求的传输协议。</td><td>// @schemes http https</td></tr><tr><td>externalDocs.description</td><td>Description of the external document.</td><td>// @externalDocs.description OpenAPI</td></tr><tr><td>externalDocs.url</td><td>URL of the external document.</td><td>// @externalDocs.url<a href="https://swagger.io/resources/open-api/">https://swagger.io/resources/open-api/</a></td></tr><tr><td>x-name</td><td>扩展的键必须以x-开头,并且只能使用json值</td><td>// @x-example-key {"key": "value"}</td></tr></tbody></table><h3 id="1-2-API-接口注释">1.2 API 接口注释</h3><p>这部分注释用于声明一个接口,可将这部分注释添加到相应的接口方法上。</p><table><thead><tr><th>注释</th><th>描述</th></tr></thead><tbody><tr><td>description</td><td>操作行为的详细说明。</td></tr><tr><td>description.markdown</td><td>应用程序的简短描述。该描述将从名为<code>endpointname.md</code>的文件中读取。</td></tr><tr><td>id</td><td>用于标识操作的唯一字符串。在所有API操作中必须唯一。</td></tr><tr><td>tags</td><td>每个API操作的标签列表,以逗号分隔。</td></tr><tr><td>summary</td><td>该操作的简短摘要。</td></tr><tr><td>accept</td><td>API 可以使用的 MIME 类型列表。 请注意,Accept 仅影响具有请求正文的操作,例如 POST、PUT 和 PATCH。 值必须如“<a href="https://github.com/swaggo/swag/blob/master/README_zh-CN.md#mime%E7%B1%BB%E5%9E%8B">Mime类型</a>”中所述。</td></tr><tr><td>produce</td><td>API可以生成的MIME类型的列表。值必须如“<a href="https://github.com/swaggo/swag/blob/master/README_zh-CN.md#mime%E7%B1%BB%E5%9E%8B">Mime类型</a>”中所述。</td></tr><tr><td>param</td><td>用空格分隔的参数。<code>param name</code>,<code>param type</code>,<code>data type</code>,<code>is mandatory?</code>,<code>comment</code> <code>attribute(optional)</code></td></tr><tr><td>security</td><td>每个API操作的<a href="https://github.com/swaggo/swag/blob/master/README_zh-CN.md#%E5%AE%89%E5%85%A8%E6%80%A7">安全性</a>。</td></tr><tr><td>success</td><td>以空格分隔的成功响应。<code>return code</code>,<code>{param type}</code>,<code>data type</code>,<code>comment</code></td></tr><tr><td>failure</td><td>以空格分隔的故障响应。<code>return code</code>,<code>{param type}</code>,<code>data type</code>,<code>comment</code></td></tr><tr><td>response</td><td>与success、failure作用相同</td></tr><tr><td>header</td><td>以空格分隔的头字段。<code>return code</code>,<code>{param type}</code>,<code>data type</code>,<code>comment</code></td></tr><tr><td>router</td><td>以空格分隔的路径定义。<code>path</code>,<code>[httpMethod]</code></td></tr><tr><td>deprecatedrouter</td><td>与router相同,但是是deprecated的。</td></tr><tr><td>x-name</td><td>扩展字段必须以<code>x-</code>开头,并且只能使用json值。</td></tr><tr><td>deprecated</td><td>将当前API操作的所有路径设置为deprecated</td></tr></tbody></table><h3 id="1-3-类型枚举">1.3 类型枚举</h3><p>以上接口注解中用到的枚举类型的介绍。</p><p>Mime 类型枚举:</p><p><code>swag</code> 接受所有格式正确的 MIME 类型, 即使匹配 <code>*/*</code>。除此之外,<code>swag</code> 还接受某些 MIME 类型的别名。</p><table><thead><tr><th>Alias</th><th>MIME Type</th></tr></thead><tbody><tr><td>json</td><td>application/json</td></tr><tr><td>xml</td><td>text/xml</td></tr><tr><td>plain</td><td>text/plain</td></tr><tr><td>html</td><td>text/html</td></tr><tr><td>mpfd</td><td>multipart/form-data</td></tr><tr><td>x-www-form-urlencoded</td><td>application/x-www-form-urlencoded</td></tr><tr><td>json-api</td><td>application/vnd.api+json</td></tr><tr><td>json-stream</td><td>application/x-json-stream</td></tr><tr><td>octet-stream</td><td>application/octet-stream</td></tr><tr><td>png</td><td>image/png</td></tr><tr><td>jpeg</td><td>image/jpeg</td></tr><tr><td>gif</td><td>image/gif</td></tr></tbody></table><p>参数类型枚举:</p><table><thead><tr><th>参数类型</th><th>描述</th></tr></thead><tbody><tr><td>query</td><td>请求的 url 参数</td></tr><tr><td>path</td><td>放在请求路径中的参数</td></tr><tr><td>header</td><td>请求 header 中的参数</td></tr><tr><td>body</td><td>请求 body 中的参数</td></tr><tr><td>formData</td><td><code>x-www-form-urlencoded</code> 请求是的表单参数</td></tr></tbody></table><p>数据类型枚举:</p><table><thead><tr><th>数据类型</th><th>对应实际类型</th></tr></thead><tbody><tr><td>string</td><td>string</td></tr><tr><td>integer</td><td>int, uint, uint32, uint64</td></tr><tr><td>number</td><td>float32</td></tr><tr><td>boolean</td><td>bool</td></tr><tr><td>结构体</td><td>结构体类型</td></tr></tbody></table><p>更多用法内容可参考官方文档:<a href="https://github.com/swaggo/swag/blob/master/README_zh-CN.md">https://github.com/swaggo/swag/blob/master/README_zh-CN.md</a></p><h3 id="1-4-示例">1.4 示例</h3><p><strong>通用 API 示例:</strong></p><p>以下注释指定了文档的基本信息,以及基于 <code>apikey</code> 方式的一种安全校验方式:</p><pre><code class="language-go">// @title Aurora Admin-API 文档// @version v0.0.1// @description Aurora 建站// @contact.name nineya// @contact.url https://www.nineya.com// @contact.email 361654768@qq.com// @schemes http https// @host localhost:8888// @BasePath /api/admin// @produce json// @securityDefinitions.apikey admin// @in header// @name Admin-Authorization</code></pre><blockquote><p>注解不能放在 <code>@securityDefinitions</code> 相关注解的后面,否则将不会被解析</p></blockquote><p><strong>接口 API 示例:</strong></p><pre><code class="language-go">// @summary Upload attachment by id// @description Upload attachment by id// @tags attachment// @accept json// @produce json// @param id path id true "Attachment id"// @param param body request.UpdateAttachmentParam true "Attachment name and team information"// @success 200 {object} response.Response// @security admin// @router /attachment/{id} [put]</code></pre><h2 id="二-文档生成">二、文档生成</h2><p>使用命令下载 swag:</p><pre><code class="language-bash">go install github.com/swaggo/swag/cmd/swag@latest</code></pre><p>使用命令生成 swag 所需的文件:</p><pre><code class="language-bash">swag init</code></pre><p>这将会扫描源程序,解析注释并生成 <code>docs</code> 文件夹和文档信息文件。</p><p><img src="https://blog.nineya.com/upload/2024/02/image.png" alt="go-swagger"></p><h2 id="三-与-Gin-集成">三、与 Gin 集成</h2><p>下载安装 <code>gin-swagger</code> :</p><pre><code class="language-bash">go get -u github.com/swaggo/gin-swaggergo get -u github.com/swaggo/files</code></pre><p>导入 <code>docs</code>下的文件:</p><pre><code class="language-go">import ( _ "go-project-name/docs")</code></pre><p>添加 Gin 路由:</p><pre><code class="language-go">swaggerGroup := router.Group("swagger")swaggerGroup.GET("/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))</code></pre><p>通过 <code>/swagger/index.html</code> 可以访问到文档:</p><p><img src="https://blog.nineya.com/upload/2024/02/image-1706785148451.png" alt="访问 swagger 接口文档"></p><h2 id="四-多文档">四、多文档</h2><p>我们一个系统可能包含开发者、用户、管理员多种角色,我们需要为不同的角色分别创建接口文档。</p><h3 id="4-1-生成文档">4.1 生成文档</h3><p>要生成多份文档,在生成文档时就不能直接执行 <code>swag init</code> 命令了,需要指定更多的参数:</p><pre><code class="language-bash">swag init -hNAME: swag init - Create docs.goUSAGE: swag init [command options] [arguments...]OPTIONS: --generalInfo value, -g value API通用信息所在的go源文件路径,如果是相对路径则基于API解析目录 (默认: "main.go") --dir value, -d value API解析目录 (默认: "./"),多个目录可用逗号分隔 --exclude value 解析扫描时排除的目录,多个目录可用逗号分隔(默认:空) --propertyStrategy value, -p value 结构体字段命名规则,三种:snakecase,camelcase,pascalcase (默认: "camelcase") --output value, -o value 文件(swagger.json, swagger.yaml and doc.go)输出目录 (默认: "./docs") --parseVendor 是否解析vendor目录里的go源文件,默认不 --parseDependency 是否解析依赖目录中的go源文件,默认不 --markdownFiles value, --md value 指定API的描述信息所使用的markdown文件所在的目录 --generatedTime 是否输出时间到输出文件docs.go的顶部,默认是 --codeExampleFiles value, --cef value 解析包含用于 x-codeSamples 扩展的代码示例文件的文件夹,默认禁用 --parseInternal 解析 internal 包中的go文件,默认禁用 --parseDepth value 依赖解析深度 (默认: 100) --instanceName value 设置文档实例名 (默认: "swagger")</code></pre><p>可分别为每份文档执行以下命令方法生成多份文档:</p><pre><code class="language-bash">swag init -g 通用API所在Go文件 -d API解析目录 --exclude 排除的目录 -o 文档输出目录 --instanceName 文档实例名</code></pre><p>举例小玖的结构体对象所在目录为 <code>internal/application/param</code>,<code>Admin</code> 接口所在目录为<code>internal/application/router/api/admin</code>,<code>Content</code> 接口所在目录为 <code>internal/application/router/api/content</code>。</p><p><strong>Admin 文档命令:</strong></p><pre><code class="language-bash">swag init -g index.go -d internal/application/router/api/admin,internal/application/param -o ./docs/admin --instanceName=admin</code></pre><p><strong>Content 文档命令:</strong></p><pre><code class="language-bash">swag init -g index.go -d internal/application/router/api/content,internal/application/param -o ./docs/content --instanceName=content</code></pre><p><strong>注意:</strong></p><p><code>-g</code> 参数的路径相对于 <code>-d</code> 参数的第一个路径。</p><p>如果 <code>-d</code> 指定的路径下没有 Go 文件,会有 <code>execute go list command, exit status 1, stdout:, stderr:no Go files in ...</code> 错误提示,无影响。</p><h3 id="4-2-与-Gin-集成">4.2 与 Gin 集成</h3><p>导入 <code>docs</code> 下的文件:</p><pre><code class="language-go">import ( _ "go-project-name/docs/admin" _ "go-project-name/docs/content")</code></pre><p>添加 Gin 路由:</p><pre><code class="language-go">swaggerGroup := router.Group("swagger")swaggerGroup.GET("/admin/*any", ginSwagger.WrapHandler( swaggerFiles.NewHandler(), func(config *ginSwagger.Config) { config.InstanceName = "admin" }))swaggerGroup.GET("/content/*any", ginSwagger.WrapHandler( swaggerFiles.NewHandler(), func(config *ginSwagger.Config) { config.InstanceName = "content" }))</code></pre><p>通过 <code>/swagger/admin/index.html</code> 和 <code>/swagger/content/index.html</code> 可以分别访问到两份文档。</p>

玖涯博客

GO 的 Web 开发系列(四)—— 静态资源文件访问的几种方式

<p>Web 开发过程中,不可避免会包含有 <code>js</code> / <code>css</code> 等静态资源文件,在 Gin 框架中如何优雅的访问这些静态资源呢?</p><h2 id="一-访问外部静态文件">一、访问外部静态文件</h2><p>静态资源不打包进可执行文件内部,与可执行文件放在同一目录下,这时候 Gin 的 API 可以直接访问这些文件。</p><pre><code class="language-go">router := gin.Default()router.Static("/admin", "resource/admin")</code></pre><p>通过 <code>http://127.0.0.1:8080/admin/</code> 就可以访问 <code>resource/admin</code> 目录下的资源文件了。</p><p>但这种访问方式其实限制也比较多,如果我们希望通过不同的业务字段进行判断,进而实现不同资源文件的响应,那就需要自己增加 <code>GET</code> 方法进行实现。</p><pre><code class="language-go">router.GET("admin/*filePath", func(c *gin.Context) { // 拿到请求 url url := c.Request.RequestURI // 这里可以通过 c.Param 等拿到参数,进行相关的业务判断,然后决定是否响应文件 // ... // 响应文件 c.File("resource/" + c.Request.RequestURI)})</code></pre><h2 id="二-访问内部静态文件">二、访问内部静态文件</h2><p>以上方式虽然简单好用,但是需要将资源文件放在可执行文件外部。在一些特殊场景下,我们可能希望把资源文件放在可执行文件内部。</p><p>这时候就需要借助 <code>Embed</code> 功能,这个功能是 Go 内置的静态文件打包工具需要 Go 1.16 版本以上才可以支持,只需要几行代码即可进行简单配置。</p><pre><code class="language-go">package mainimport "embed"//go:embed resource/admin/*var f embed.FSfunc main() { router := gin.Default() router.StaticFS("/admin", http.FS(f))}</code></pre><p>通过如上代码就可以将资源文件打包到可执行程序内,并通过 Gin 进行访问。但是,这个访问方式路径为 <code>http://localhost:8080/admin/resource/admin/</code></p><p>在如上代码中加以修改,去掉 <code>resource/admin/</code> 路径:</p><pre><code class="language-go">package mainimport "embed"//go:embed resource/admin/*var f embed.FSfunc main() { router := gin.Default() st, _ := fs.Sub(f, "resource/admin") router.StaticFS("/admin", http.FS(st))}</code></pre><p>上述代码中,静态资源文件访问方式路径为 <code>http://localhost:8080/admin/</code></p><h2 id="三-文件无法访问问题">三、文件无法访问问题</h2><p>可以发现通过上述方式访问静态文件时,子目录下文件名为 <code>index.html</code> 的文件无法正确访问。</p><p>这是因为 <code>index.html</code> 文件的访问都会被 301 重定向到对应的目录路径。</p><p><img src="https://blog.nineya.com/upload/2024/01/image-1706615763059.png" alt="目录重定向"></p><p>解决方法的话,小玖找了网上的一些教程,最终也没有个明确可行的方案。最终看了看 Gin,决定不使用 <code>StaticFS</code> 函数实现静态资源访问,仿照函数的逻辑自己实现。</p><p>关键部分源码:</p><pre><code class="language-go|StaticFS函数源码">// StaticFS works just like `Static()` but a custom `http.FileSystem` can be used instead.// Gin by default uses: gin.Dir()func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes { if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") { panic("URL parameters can not be used when serving a static folder") } handler := group.createStaticHandler(relativePath, fs) urlPattern := path.Join(relativePath, "/*filepath") // Register GET and HEAD handlers group.GET(urlPattern, handler) group.HEAD(urlPattern, handler) return group.returnObj()}</code></pre><pre><code class="language-hgo|createStaticHandler函数源码">func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc { absolutePath := group.calculateAbsolutePath(relativePath) fileServer := http.StripPrefix(absolutePath, http.FileServer(fs)) return func(c *Context) { if _, noListing := fs.(*onlyFilesFS); noListing { c.Writer.WriteHeader(http.StatusNotFound) } file := c.Param("filepath") // Check if file exists and/or if we have permission to access it f, err := fs.Open(file) if err != nil { c.Writer.WriteHeader(http.StatusNotFound) c.handlers = group.engine.noRoute // Reset index c.index = -1 return } f.Close() fileServer.ServeHTTP(c.Writer, c.Request) }}</code></pre><p>通过以上源码可以看到,其实 <code>StaticFS</code> 内部也是封装了个访问静态文件资源的函数,最终通过注册 <code>GET</code> 和 <code>HEAD</code> 路由实现文件访问。</p><p>参照以上逻辑,自己实现静态资源访问,并对“/”结尾的请求特殊处理:</p><pre><code class="language-go">package mainimport "embed"//go:embed resource/admin/*var f embed.FSfunc main() { router := gin.Default() // 选中对应的子目录 st, _ := fs.Sub(f, "resource/admin") fss := http.FS(st) // 新建文件服务 fileServer := http.StripPrefix("/admin", http.FileServer(fss)) // 创建静态文件资源处理函数 handlerFunc := func(c *gin.Context) { file := c.Param("filepath") // 如果 / 结尾,则访问 index.html 文件 if strings.HasSuffix(file, "/") { file = file + "index.html" } // Check if file exists and/or if we have permission to access it fi, err := fss.Open(file) if err != nil { c.Writer.WriteHeader(http.StatusNotFound) return } fi.Close() fileServer.ServeHTTP(c.Writer, c.Request) } // Register GET and HEAD handlers router.GET("/admin/*filepath", handlerFunc) router.HEAD("/admin/*filepath", handlerFunc)}</code></pre><p>这样就可以通过路径直接访问 <code>index.html</code> 文件了。</p>

玖涯博客

GO 的 Web 开发系列(三)—— 通过 Gin 实现 Html 模板和 Api 接口路由

<p>Gin 的路由其实就是 Java 中 <code>@RequestMapping</code> 注解的内容,只是 Go 需要在一个代码模块中统一对所有路由进行配置。</p><h2 id="一-路由组配置">一、路由组配置</h2><p>一般情况下,通过路由需要完成两个事情,一是通过 HTML 模板完成前端页面的渲染,二是实现 Api 接口调用。</p><p>这两部分功能是完全不一样的,在 Gin 中可以用两个不同的路由组来隔离逻辑。</p><pre><code class="language-go">router := gin.Default()// 指定模板文件所在的路径router.LoadHTMLGlob("templates/*.html")// 渲染html的路由组htmlGroup := router.Group("")// api 处理的路由组apiGroup := router.Group("api")</code></pre><p>再更近一步,可以将 API 路由组再细分为公共 API 和私有 API。</p><pre><code class="language-go">// 公共API路由组publicGroup := apiGroup.Group("public")// 私有API路由组,绑定鉴权中间件privateGroup := apiGroup.Group("")privateGroup.Use(middleware.AdminAuth)</code></pre><h2 id="二-API-接口实现">二、API 接口实现</h2><p>创建一个简单的 GET 接口,并返回一条字符串数据内容。</p><pre><code class="language-go">apiGroup.GET("/health", func(c *gin.Context) {c.String(200, "ok health")})</code></pre><p>返回一个 JSON 数据:</p><pre><code class="language-go">apiGroup.GET("/health", func(c *gin.Context) {c.AbortWithStatusJSON(200, map[string]string{"msg": "ok health"})})</code></pre><h2 id="三-HTML-模板渲染">三、HTML 模板渲染</h2><p>渲染模板用的是 Go 内置的 <code>html.template</code> 工具包,Gin 对该工具包进行了集成,在渲染 Html 模板前需要先在 <code>*gin.Engine</code> 路由上指定模板文件所在的位置。</p><blockquote><p>必须在 <code>*gin.Engine</code> 路由上指定,不能在路由组上指定。</p></blockquote><p><strong>有两个方法可以指定模板位置:</strong></p><p><code>router.LoadHTMLGlob</code> 函数可以指定一个路径,模糊匹配下的模板文件。</p><pre><code class="language-go">// 指定模板所在的目录router.LoadHTMLGlob("templates/*")// 指定模板文件为html文件router.LoadHTMLGlob("templates/*.html")// 当模板在多级目录下时router.LoadHTMLGlob("templates/**/*.html")</code></pre><p><code>router.LoadHTMLFiles</code> 函数用于直接指定模板文件名称,可以同时指定多个文件。</p><pre><code class="language-go">router.LoadHTMLFiles("templates/index.html", "templates/page/index.html")</code></pre><p><strong>模板接口:</strong></p><p>通过如下方法就可以指定渲染模板。</p><pre><code class="language-go">htmlGroup.GET("/index.html", func(c *gin.Context) {c.HTML(http.StatusOK, "index.html", gin.H{"title": "玖涯菜菜子",})})</code></pre><h2 id="四-HTML-模板的缺陷">四、HTML 模板的缺陷</h2><p>通过章节三的方法可以实现模板,但是这个方法是存在一些问题的,或者说是存在一些限制。<strong>因为 Gin 只是简单集成了这个模板框架,并没有怎么考虑一些细节问题。</strong></p><p><mew-subtitle>模板访问问题</mew-subtitle></p><p>注册了 <code>templates/page/index.html</code> 模板,但是通过 <code>templates/page/index.html</code> 或者 <code>page/index.html</code> 会发现访问不到模板,只能通过 <code>index.html</code> 访问。</p><p><strong>原因:</strong> 在注册模板时以文件名作为模板名称,所以c.HTML函数中只要模板文件名,不带模板所在的目录。<strong>如果不同目录出现多个同名文件,模板将相互覆盖。</strong></p><p>如上例子中,<code>router.LoadHTMLFiles("templates/index.html", "templates/page/index.html")</code> 这两个文件中,<code>templates/index.html</code> 文件会被覆盖的。</p><p><strong>解决方法:</strong> 在模板中通过 <code>define</code> 关键字为模板中的代码片断赋予一个新名称,然后通过这个名称访问模板。</p><pre><code class="language-go">{{ define "test/index.html" }}test{{ end }}</code></pre><p><mew-subtitle>多次注册模板只有最后一次注册能生效</mew-subtitle></p><p>执行了多次 <code>router.LoadHTMLGlob</code> 或 <code>router.LoadHTMLFiles</code> 函数,只有最后一次注册的内容能够生效。</p><p><strong>原因:</strong> 每执行一次上面的方法都会重置 Gin 的 <code>HTMLRender</code> 属性,而这个属性存储着模板信息,所以每一次调这个函数都会重置模板状态。</p><p><strong>解决方法:</strong> 通过官方接口无法解决,需要自己继承 <code>HTMLRender</code> 写一个 HTML 渲染的接口实现,在一个章节细说。</p><p><mew-subtitle>LoadHTMLGlob 指定多级目录会报错</mew-subtitle></p><p><strong>原因:</strong> 使用 <code>**/*</code> 这种方式指定了两级目录,如果不存在两级目录,程序运行将会出错。</p><p><strong>解决方法:</strong> 通过官方的接口无法解决,可自己继承 <code>HTMLRender</code> 写一个 HTML 渲染的接口解决。</p><p>以上问题可通过继承 <code>HTMLRender</code> 自定义 Gin 的模板渲染工具解决,小玖在后面的章节中详细介绍。<img alt="tushe" class="emoji" src="/themes/dream/source/img/emoji/tushe.png" title="tushe"></p>

玖涯博客

几大开源免费的 JavaScript 富文本编辑器测评

MarkDown 编辑器用的时间长了,发现发现富文本编辑器用起来是真的舒服。 一直以来写博客都是用的 MarkDown 编辑器,MarkDown 文档简单方便,使用几个简单的符号就可以定义出样式统一的富文本内容。写博客的时间长了,小玖就越来越排斥用富文本编辑器了。 现在回过头来用富文本编辑器,体验是真的不错。样式定义灵活,所见即所得,可以轻松实现在 MarkDown 文档上无法实现的样式,简直不要太舒服。 于是忍不住就多调研了几款比较常见的富文本编辑器,简单分享一下。 一、WangEditor 官网:https://www.wangeditor.com/ 这是小玖接触的第一款富文本编辑器,很久以前做的一个论坛系统用的就是这个编辑器,轻量、样式简洁美观,曾经很受小玖喜欢。 优势嘛,就是简洁美观。 缺点也很明显,这个编辑器功能不够丰富,也不支持插件。字体颜色只能选预定义的几个颜色,表格不能合并单元格,图片不能悬浮在文字侧边显示。 还有就是,作者不打算再继续维护了。 但如果需求简单的话,这款编辑器还是不错的。 二、Jodit 官网:https://xdsoft.net/jodit/ 一个轻量的富文本编辑器,功能相比较于 WangEditor 更加完善,个人感觉比 WangEditor 好些。 三、Quill 官网:https://quilljs.com/ 仓库:https://github.com/quilljs/quill 功能非常简洁,在官网的 Demo 中看,貌似连插入表格的选项都没有看到。然后也没有位置排版相关的功能,图片也不能放大缩小,总而言之,功能很少。 优点的话,也是简洁美观,容易上手操作。有一些插件可以扩展编辑功能,但还是很有限。 四、CKEditor 官网:https://ckeditor.com/ckeditor-5/ 这款编辑器可扩展性比较强,插件的功能也比...

玖涯博客

GO 的 Web 开发系列(二)—— Web 项目的技术框架选择和项目搭建

<p>Web 系统搭建第一步,选择技术框架,完成 <code>Hello World</code>。</p><blockquote><p>小玖用的 Go 版本是 1.21.6</p></blockquote><p>介绍项目的技术选型个结构,不包括代码。</p><h2 id="一-技术框架选择">一、技术框架选择</h2><p><strong>Web 框架:</strong> github.com/gin-gonic/gin v1.9.1</p><p>调研了 Go 开发者一圈,给小玖推荐这个框架的人最多 <img alt="tushe" class="emoji" src="/themes/dream/source/img/emoji/tushe.png" title="tushe"></p><p>主要优点是轻量容易定制。</p><p><strong>ORM 框架:</strong> xorm.io/xorm v1.3.6</p><p>最初小玖选的 <code>gorm</code>,因为网上的评价一致是:<strong>gorm 是国人开发,用户更多,文档更完善,更简单好用;xorm 的优点只是性能好点。</strong></p><blockquote><p>但是后面查问题时发现,<code>gorm</code> 网上相关的资料也不是那么多,所以改为了使用 <code>xorm</code>。<br>(搜索 gorm 的问题,出来了很多 xorm 的相关回答)</p></blockquote><p>主要是这个框架自带缓存功能,想偷懒一下~</p><p><strong>配置文件处理工具:</strong> github.com/spf13/viper v1.18.2</p><p>这个工具可以同时处理 <code>yaml</code>、<code>json</code> 和命令行参数等多种配置方式,所以选择了它。</p><p>以下这些工具包,根据网上使用人数,以及功能是否完善、丰富做的选择。</p><p><strong>日志框架:</strong> go.uber.org/zap v1.26.0</p><p><strong>BCrypt 生成工具:</strong> golang.org/x/crypto v0.18.0</p><p><strong>JWT 工具:</strong> github.com/dgrijalva/jwt-go v3.2.0+incompatible</p><p><strong>Redis 连接工具:</strong> github.com/go-redis/redis/v8 v8.11.5</p><p><strong>MySQL 连接工具:</strong> github.com/go-sql-driver/mysql v1.7.0</p><p><strong>UUID 生成工具:</strong> github.com/google/uuid v1.4.0</p><p><strong>验证码生成工具:</strong> github.com/mojocn/base64Captcha v1.3.6</p><p><strong>内存缓存工具:</strong> github.com/patrickmn/go-cache v2.1.0+incompatible</p><p>在项目新建完成后,目录下会生成一个 <code>go.mod</code> 文件,用于管理依赖包,在该文件中输入以下内容:</p><pre><code class="language-go">module aurorago 1.21.6require (github.com/dgrijalva/jwt-go v3.2.0+incompatiblegithub.com/gin-gonic/gin v1.9.1github.com/go-redis/redis/v8 v8.11.5github.com/go-sql-driver/mysql v1.7.0github.com/google/uuid v1.4.0github.com/mojocn/base64Captcha v1.3.6github.com/patrickmn/go-cache v2.1.0+incompatiblegithub.com/spf13/viper v1.18.2go.uber.org/zap v1.26.0golang.org/x/crypto v0.18.0xorm.io/xorm v1.3.6)</code></pre><h2 id="二-项目包结构设计">二、项目包结构设计</h2><p>一般而言 <code>main.go</code> 文件是 Go 项目的启动文件,放在项目根目录,其他文件自己根据项目情况新建目录存放就可以了。</p><p>但是建议还是将源程序用一个目录统一存放,避免目录结构混乱。这个统一存放源文件的目录类似 <code>java</code> 的 <code>mian</code> 目录。</p><blockquote><p>需要注意不同的包(目录)的源程序文件不能循环引用。</p></blockquote><p>小玖参考了一些其他项目的包设计,然后根据功能需要,设计的项目包目录结构如下:</p><pre><code>internal:项目源码根目录|- application:业务逻辑相关的程序包|--- model:模型层源文件包目录,包含了模型结构体|--- param:web接口出入参的结构体源文件包目录|--- router:web路由接口文件包目录(可以理解为controller),里面根据业务功能划分子包|--- service:服务层源文件包目录,里面根据业务功能划分子包|- config:配置文件相关|- global:这个包用于存放一些公共逻辑和变量|- initialize:项目初始化各种框架的包,比如xorm、redis、gin|--- middleware:gin中间件的包|- utils:放工具方法的包resource:存放静态文件,比如vue管理后台源码test:测试程序文件所在的包application.yaml:程序的配置文件go.mod:依赖管理文件main.go:程序入口</code></pre><p>这样在目录结构上将静态文件和 Go 源文件区分开了,各个功能模块的逻辑都较为清晰,小玖用的算是比较舒服,仅供参考。</p><p>在确定了包结构后,就可以根据各个模块的职责往里面添代码,从网上找找这些框架的配置教程即可。</p><p>一些比较核心的逻辑小玖会在后面的章节中继续介绍。</p>

玖涯博客

GO 的 Web 开发系列(一)—— Java 与 Go 做 Web 程序的区别

<p>小玖是奋斗一线的 Java 开发,准备自己开发一个建站程序,出于某些原因,小玖最终将建站程序的开发语言选择了 Go。</p><p>对于 Go 小玖其实之前并没有太多接触过,也只能算是个初学者吧,所以对 Go 的 WEB 开发从初学的角度总结经验,希望日后能对初学 Go 语言的小伙伴有所帮助。但本系列不会从零基础开始介绍,是针对有一定程序开发基础的同学,Go 的一些基础语法就不会详细介绍。</p><h2 id="一-为什么选择-Go">一、为什么选择 Go</h2><p>可能很多伙伴会有疑问,Java 的 SpringBoot 一整套框架做 Web 开发效率高,技术成熟,文档和解决方案都很成熟,为啥小玖要选择 Go 呢?</p><p>Go 和 Java 其实差别还是比较大的,<strong>这其实是综合考虑的结果,因为 Go 能解决一些小玖认为非常重要的问题,而 Java 却刚好难以解决或者说无法解决。</strong></p><ol><li>Java 占用内存高,SpringBoot 框架一启动,直接把内存占满(对小玖的弱鸡服务器跑起来真的非常吃力啦);</li><li>Java 的虚拟机机制有些鸡肋,无法动态的去调整内存,比如 <code>-Xmx</code> 等虚拟机参数;</li><li>Java 编译的程序需要依赖 JVM 环境,而 Go 编译后的二进制文件可以直接运行(便于建站程序日后的交付);</li><li>Java 编译的 Class 容易反编译破解,虽然有一些类似通过自定义 <code>ClassLoader</code> 进行加密的方案,但安全性还是差了很多。小玖了解到的最安全的方案还得是通过 <code>JNI</code> 去调二进制文件做加密和验证,太麻烦了。</li></ol><p>基于以上这些问题,小玖找了好久的解决方案,比如 <code>GraalVM</code> 等等的都去了解过,这些方案总是有些局限性,所以,最终选择了 Go。</p><p>Go 占用的内存少,而且编译成的二进制文件源码安全性也更有保障。</p><p>Go 有以上技术优势,也存在一些劣势,尤其是在生态这块和 Java 差距很大。</p><h2 id="二-Java-与-Go-语言上的差异">二、Java 与 Go 语言上的差异</h2><p><mew-message type="warning">非 Go 语言专家,只谈谈自己使用以来的一些感受。</mew-message></p><p><mew-subtitle>Go 的语法比 Java 更简洁</mew-subtitle></p><p>Go 的语法更简短,不像 Java 那么长。但这是次要的,更多的是一种说不上来的感觉,用起来了就会有这种感受。</p><p>也许举个例子更能说明这种情况,在 Java 里 <code>try</code> 的语法糖帮助实现了关闭数据流。</p><pre><code class="language-java">try (InputStream bin = getInputStream();) { // 各种操作} catch (IOException e) { e.printStackTrace();}</code></pre><p>不用自己关闭数据流,这样用起来就很爽!</p><p>而在 Go 中这样类似的语法特别多,就让人用的非常的舒服。举例如 Go 的函数可以直接返回多个参数,接口只要实现了方法就可以自动继承,等等……</p><p><mew-subtitle>Go 的异常处理比 Java 差</mew-subtitle></p><p>Go 的语法中更喜欢直接返回 <code>error</code> 的实体对象,而不是抛出异常。</p><p>这就导致了一个问题,在每一个可能抛出异常的地方都要判断一些有没有 <code>error</code> 实体返回,然后再决定是否执行后面的逻辑。</p><pre><code class="language-go">_, err := session.Insert(&amp;param.Site)if err != nil { return err}</code></pre><p>这就导致逻辑里会有很多这种异常判断。</p><blockquote><p>Go 也支持 <code>panic</code> 函数抛出异常,不太明白为啥 Go 不提倡用这个,可能因为异常捕获更加麻烦。</p></blockquote><p>Go 的异常捕获需要用到 <code>defer</code> + <code>recover</code> 一起实现才行。</p><pre><code class="language-go">defer func() { if err := recover(); err != nil { response.FailWithError(err.(error), c) }}()</code></pre><p><mew-subtitle>Go 的并发处理比 Java 强</mew-subtitle></p><p>Go 的多线程实现比 Java 简洁,并发能力也更强。</p><p>不过小玖体会不深,还没有真正自己用 Go 写过复杂的并发逻辑。</p><p><mew-subtitle>Go 与 Java 接口和实体类的不同</mew-subtitle></p><p>Go 的实现了接口里的所有方法就自动实现了接口。实体类叫结构体,与 <code>c</code> 语言的那种结构体相似。</p><p>Go 有包的概念,没有太多类的概念,属于同一个包的变量和函数都算一个整体,可以在导入包的时候导入。然后包不能循环导入,A包导入了B包,B包就不能再导入A包了。</p><h2 id="三-Java-与-Go-技术框架的差异">三、Java 与 Go 技术框架的差异</h2><p><mew-subtitle>Go 的 Web 框架多且乱</mew-subtitle></p><p>目前 Go 主流的 Web 框架有 <code>Gin</code>、<code>Beego</code>、<code>Echo</code>、<code>Iris</code> 等等,非常多。</p><p>Go 主流的的 <code>ORM</code> 框架有 <code>gorm</code>、<code>xorm</code>、<code>BeegoORM</code> 等等……</p><p>这与 Java 的 <code>SpringBoot + MyBatis</code> 一套吃遍一圈的套路完全不一样,小玖在刚入门时最大的疑问就是,这些框架有什么不同,他们分别有什么特性?</p><p>咨询过很多 Go 的开发,但是貌似没有几个人回答的上来,给我的回答都是这些框架都差不多,大同小异,顺手的选一个用就行了。</p><p><mew-subtitle>Go 的框架更加轻量</mew-subtitle></p><p>小玖也不知道怎么说轻量这个词,往好的说就是程序更简洁,程序启动更快,占用的资源更少。</p><p>然后代码上直接主要逻辑,没有用建造者、观察者等等的设计模式,代码量更少,框架里没有各种判断等等复杂的逻辑。</p><p><img src="https://blog.nineya.com/upload/2024/01/image-1705822033199.png" alt="Gin 代码片段"></p><p>看源码比 Java 的框架轻松好多。</p><blockquote><p>学的老久的 Java 都没把 Spring 摸透,但刚用 Go 小玖就已经可以看懂 Gin 的源码,然后在上面定制功能了。</p></blockquote><p>但往差的说就是 Go 的 Web 框架功能更少,少掉的那些判断和注入逻辑用到的那就是要自己写咯。</p><blockquote><p>比如 <code>SpringBoot</code> 套装用上之后函数上加个注解就可以直观的实现权限校验,而 Go 貌似都是一个一个接口自己做的判断。</p></blockquote><h2 id="四-Java-与-Go-生态的差异">四、Java 与 Go 生态的差异</h2><p>Java 在国内的占有率是非常高的,<code>SpringBoot</code> 一套框架家喻户晓,开发时有各种文档,技术方案上有各种教程,功能实现上 <code>SpringBoot</code> 集成了各种框架自动化配置,遇到问题了还有各种解决教程和大佬解答。</p><p>但是 Go 的话,小玖开发这么些天以来,体验并不好。简单的问题会有一些博主的踩坑经验,一些比较深入的问题基本上没有找到有用的相关的文档和介绍,用 Google 也没有什么收获。</p><p><img src="https://blog.nineya.com/upload/2024/01/image-1705821897896.png" alt="Gin 动态删除路由问题"></p><p>可能,更多时候看源码是唯一的出路。</p><p>好在 Go 的框架大都更加简洁,很容易找到关键代码,源码阅读起来相对轻松。</p><h2 id="五-总结">五、总结</h2><p>总结的话,Java 的生态比 Go 好,在业务系统开发中,很多功能都有 Java 有很成熟的解决方案,开发速度快且稳定,就算遇到问题了也能找到前人的案例,能够快速解决。</p><p>Go 的话性能更优秀,更适合做一些偏底层的应用研发,然后也更方便对外交付。</p><p>以上,个人的理解!</p>

玖涯博客

呕心沥血大作,万事顺意玖涯定制款红包封面来啦~

<p>万事顺意玖涯定制款微信红包封面来啦~</p><blockquote><p>Wow So Dreamy</p></blockquote><p>在元旦,小玖发布了 Halo-Plus &amp; Dream 第一份正式年报。</p><p>年报中,小玖讲述了23年来 Dream 和 Halo-Plus 的迭代历程和自己的想法,同时也启动了跨年送封面的活动。</p><p><strong>非常感谢小伙伴们的喜欢和支持。</strong> 但遗憾的是,没能在元旦时定制一款好看的封面。</p><p>痛定思痛,一连好几夜,小玖纯手工手绘了一款红包封面。</p><h2 id="一-绘画过程简览">一、绘画过程简览</h2><p><img src="https://blog.nineya.com/upload/2024/01/KQ882%7D3UUT4_%7B7B7~7PPS1S.png" alt="手绘过程1"></p><p><img src="https://blog.nineya.com/upload/2024/01/image.png" alt="手绘过程2"></p><p><img src="https://blog.nineya.com/upload/2024/01/image-1705073569654.png" alt="手绘过程3"></p><p><img src="https://blog.nineya.com/upload/2024/01/image-1705073631004.png" alt="手绘过程4"></p><p><img src="https://blog.nineya.com/upload/2024/01/image-1705073640646.png" alt="手绘过程5"></p><p><img src="https://blog.nineya.com/upload/2024/01/image-1705073660048.png" alt="手绘过程6"></p><h2 id="二-封面成品一览">二、封面成品一览</h2><p>最终成品红包封面如下图。</p><p>威武霸气的龙,配上活泼可爱的小松鼠,用上这款封面,这红包发的就会很有排场!</p><p><img src="https://blog.nineya.com/upload/2024/01/image-1705073750285.png" alt="封面成品1"></p><p><img src="https://blog.nineya.com/upload/2024/01/image-1705073768556.png" alt="封面成品2"></p><p>虽然是第一次做手绘,但是相信还是非常精美的,希望大家喜欢。</p><h2 id="三-新年送福">三、新年送福</h2><p>与元旦时的活动方式相同,同样是 24 份,公众号后台回复 <strong>“万事顺意”</strong> 领取,先到先得。</p>

玖涯博客

寻觅2024:Dream + Halo-Plus 的第一份正式年报——追寻梦的方向,聆听心的声音

<p>自去年发布了 <code>逐梦2023</code> 之后,越来越有一种感觉,是应该写一封年报的,记录这一年来的变化,所以有了这篇文章——Dream + Halo-Plus 的第一份正式年报。</p><p>年关将至,回顾这一年,有许多事项出乎了我的意料。其中包括了 <code>Halo 2.0</code> 定位的改变、我自身个人博客内容的改变,以及我对博客样式风格的一些审美的转变。这一系列的原因导致了我这一年来没有按计划的进行主题的升级和适配,或许计划在大多时候都赶不上变化,犹如逐梦一场。</p><h2 id="一-与-Halo-2-x-的诀别">一、与 Halo 2.x 的诀别</h2><p>这里是一封与 <code>Halo</code> 官方的诀别信,也许让不少博主感受到沮丧,因为我没能做到曾经说过的拥抱 <code>Halo 2.0</code> 的诺言,对于 <code>Halo 2.x</code> 的适配也比预计的时间晚了些许。</p><p>一直以来,支持我更新的动力是我自身一直在更新维护自己的博客系统,开源是为了方便更多热爱个人博客的博主,让他们可以免费地使用与定制主题。同时也希望能够得到一些有建设性的意见和建议,帮助主题的迭代和发展。但随着对 <code>Halo 2.x</code> 更深入的了解,慢慢发现 <code>Halo 2.x</code> 的几次迭代都与我想要的博客系统已经渐行渐远了,<code>Halo 2.x</code> 在官网上的定位也由 <strong>开源个人博客系统</strong> 更新为了 <strong>开源建站工具</strong>,这个转变让我对升级 2.0 产生了诸多疑虑。</p><p><img src="https://blog.nineya.com/upload/2023/03/image.png" alt="Halo 定位转变"></p><p>现今的 <code>Halo 2.x</code> 功能越来越丰富,提供了用户管理、插件机制等等,有点像另一个 <code>WordPress</code>,这与我想要的 <strong>轻量、快速的个人博客系统</strong> 这个理念背道而驰。同时也带来了一些困恼,比如 <strong>不同插件的兼容性、主题与插件的职责划分</strong> 等等……</p><blockquote><p>个人认为,个人博客系统应简而美,专注于写作,是一套写作工具的完美结合,从而给博主提供最佳的写作体验。而非是多个工具、插件之间的选择,不同工具插件各有特性,难以完美结合。</p></blockquote><p>所以,最后我决定继续使用 <code>Halo 1.x</code> 系统,并且 <code>clone</code> 了 <code>Halo 1.x</code> 相关的开源库,选择在此基础上自己继续开发。</p><p>对于 <code>Halo 2.x</code>,今年度也做了一些适配工作,关于主题后续的优化、更新迭代我也将继续支持,但维护两个不同模板引擎的主题精力始终有限,更新的速度可能依旧是比较缓慢。更多的维护可能需要依靠于喜爱 <code>Dream</code> 、有意愿参与主题维护的个人博主。</p><h2 id="二-年初的阶段性更新">二、年初的阶段性更新</h2><p>自从发觉 <code>Halo 2.x</code> 不是很符合自己的预期,年初时对于 <code>Halo 2.x</code> 的适配一直处于观望状态。这段时间我的个人博客需求也有了些转变,增加了更多生活类、论述性的文章。针对主题而言,也添加了文艺风的博文模板,该博文模板增大了正文字体,并自动将正文转为短笺。由于 23 年第一个版本的发布临近情人节,顺应节日氛围,增加了恋爱墙侧边栏功能。</p><blockquote><p>立志成为一个有趣的、对生活有独特见解的人。</p></blockquote><p><img src="https://blog.nineya.com/upload/2023/03/image-1679066394077.png" alt="文艺风博文模板"></p><p><code>Halo 2.x</code> 给我也提供了一些设计灵感,特别是在评论区模块。借鉴于 <code>Halo 2.x</code> 的感悟,在评论区添加了管理 API,博主登录后支持在评论区便捷的使用博主账号进行评论回复、进行回收和删除评论操作,并实现了 <code>mew-hide</code> 评论后显示自定义标签。</p><p><img src="https://blog.nineya.com/upload/2023/03/image-1679067108473.png" alt="新版评论区"></p><p>在 3 月 17 日,完成了本阶段的最后一次样式和功能上的更新,发布了 <code>2.2.0</code> 版本,该版本实现了置顶文章轮播功能,建设性的添加了模块化的布局,这改变了最初的从 <code>Icarus</code> 沿用而来的三列/两列经典博客布局。对于博主而言,主题的样式配置有了更高的自由度,不同样式风格俨然成了不同的主题模样,帮助博主实现更多的个性化的定制。</p><p><img src="https://blog.nineya.com/upload/2023/03/image-1679067736984.png" alt="模块化主题风格"></p><p>除了主题在对外样式上的更新,统一了主题提示文字描述,对主题后台设置界面也做了优化升级。通过 <code>description</code> 属性进行脚本注入,实现对主题设置界面进行修改,添加了背景图和右下角悬浮的主题交流群快捷入群方式。</p><blockquote><p>这是曾经最令我满意的一个想法,因为我觉得实现非常的巧妙。</p></blockquote><p><img src="https://blog.nineya.com/upload/2023/03/image-1679067860495.png" alt="新的主题设置界面样式"></p><h2 id="三-适配-Halo-2-0">三、适配 Halo 2.0</h2><p>在3月21日新建了 <code>halo-theme-dream2.0</code> 仓库,正式开始了主题的 <code>Halo 2.0</code> 适配,经过一个星期不分昼夜的更新,在3月27日完成了主题的第一个版本 <code>1.0.0-alpha.1</code>。虽然当时还有不少小问题,但基本上所有可以适配的功能都完成了适配(因为有些功能当时还不知道 2.x 已经支持了,例如友链)。</p><p>最后的适配效果如下图,因为日志、相册和友链这些功能 <code>Halo 2.0</code> 当时还未支持,整体而言功能上还是少了一些。</p><p><img src="https://blog.nineya.com/upload/2023/04/image.png" alt="Dream2.0预览图"></p><p>当然,在后续迭代中,日志、相册这类插件都已陆续适配,功能性上与 <code>Dream 1.x</code> 差别已经不大,后续迭代上两份主题都会同步更新。</p><blockquote><p>至年底,Dream 2.0 与 Dream 在功能性上基本上别无二致。</p></blockquote><h2 id="四-推出-Halo-Plus">四、推出 Halo-Plus</h2><p>对于 <code>Halo 1.x</code> 版本而言,博客程序已经失去了官方的维护。所以,在对 <code>Dream</code> 主题进行迭代的同时,也将包含对 <code>Halo</code> 程序的维护,这个个人维护的 <code>Halo</code> 博客系统便是 <code>Halo-Plus</code>。</p><p><img src="https://blog.nineya.com/upload/2023/09/image-1694488623946.png" alt="Halo-Plus"></p><p><code>Halo-Plus</code> 由 <code>Halo 1.6.1</code> 继续开发而来,相比于 <code>Halo 1.6.1</code> 会在功能上进行优化,修复已知的缺陷,从而提升用户体验,但在功能上不会有太大的改动或新增。</p><p>迭代中也有涉及到主题相关 API 的变更,但目前而言,<code>Halo-Plus</code> 依旧能够兼容 <code>Halo 1.x</code> 的主题。后续升级如果无法实现主题兼容,届时 <code>README</code> 文档上也将添加相关版本说明,并且可能会推出相对应的一键升级主题安装包的脚本。</p><h2 id="五-Halo-Plus-迭代">五、Halo-Plus 迭代</h2><p>到本年底,<code>Halo-Plus</code> 从 <code>1.0.0</code> 迭代到了 <code>1.1.4</code>,共计 8 个版本,增加了一些功能,也进行了一些遗留问题的修复。</p><p><strong>功能迭代情况:</strong></p><p>1.0.0:附件支持分组展示、隐藏文章支持在文章列表中展示。</p><p>1.0.1:增加了列表中继器、代码输入框和复选框三种主题配置项类型。</p><p>1.0.2:支持上传附件时指定附件分组,主题配置输入框支持下拉文本提示。</p><p><img src="https://blog.nineya.com/upload/2023/12/image-1702622006784.png" alt="上传附件"></p><p><img src="https://blog.nineya.com/upload/2023/12/image-1702621956757.png" alt="输入框下拉文本提示"></p><p>1.1.0:友链管理界面支持检测友链连通性。</p><p><img src="https://blog.nineya.com/upload/2023/12/image-1702622143545.png" alt="死链检测"></p><p>1.1.1:优化分页功能,支持显示首页、尾页。</p><p>1.1.2:更换博客默认编辑器,采用 <code>Vditor</code> 作为编辑。</p><p><img src="https://blog.nineya.com/upload/2023/12/image-1702622344646.png" alt="内置 Vditor 编辑器"></p><p>1.1.3:修复了 <code>Vditor</code> 编辑器的一些适配性问题,将依赖的工具包内置到 <code>Halo-Plus</code> 安装包中。</p><p>1.1.4:支持了 <code>Emoji</code> 表情,同时主题可以通过 <code>editorOptions</code> 控制编辑器,实现编辑器中使用主题提供的表情和自定义标签。</p><p><img src="https://blog.nineya.com/upload/2023/12/image-1703361050898.png" alt="编辑器表情"></p><h2 id="六-主题迭代说明">六、主题迭代说明</h2><p>对于 <code>Halo 2.0</code> 的主题,所有开发和维护都将在 <code>halo-theme-dream2.0</code> 仓库中进行,持续进行维护。<code>Dream2.0</code> 的定位将持续保持为 <strong>个人博客</strong>,对于一些个人博客不需要的功能或插件,主题不会考虑进行适配。</p><p>后续的开发路径大概是,在 <code>Halo-Plus</code> 和 <code>Dream</code> 上先有一个开发迭代的思路,然后再开发和适配到 <code>Dream2.0</code> 上,大概率不会主动去适配一些 2.x 的插件。</p><blockquote><p>也鼓励支持和喜爱 <code>Dream</code> 的博主按照主题的代码风格和规范提交 <code>PR</code>。</p></blockquote><p>对于使用 <code>Halo 1.x</code> 官方版本的用户,或许已经迎来了 <code>Dream</code> 的终章,感谢你一路的支持。如果你依旧热爱 <code>Dream</code> 主题,可以选择使用 <code>Dream</code> 主题适配的 <code>Halo-Plus</code> 迭代版本,或者升级 <code>Halo 2.x</code>。</p><h2 id="七-图标细节优化">七、图标细节优化</h2><p>一直以来对 <code>Font Awesome</code> 图标不甚满意,<code>Font Awesome 4.7</code> 的图标线条粗细不一,缺乏统一的风格。最终在群友的介绍下,将图标库跟换为了 <code>RemixIcon</code>,这是一个风格统一且内容丰富的图标库。</p><p><img src="https://blog.nineya.com/upload/2023/09/image.png" alt="RemixIcon图标风格"></p><h2 id="八-喜迎新春">八、喜迎新春</h2><p>今年度最后一个主题大版本 <code>3.2.0</code> 首次提供了“庆典”主题风格,从国庆节后开始开发,历时两个月,于十二月正式发布。</p><p>恭祝新春,也祝愿大家在新的一年里平安顺遂。</p><p><img src="https://blog.nineya.com/upload/2023/12/image.png" alt="庆典主题风格"></p><p>于此同时也增加了一个非常不错的字体:阿里巴巴刀隶体。</p><p><img src="https://blog.nineya.com/upload/2023/11/image.png" alt="阿里巴巴刀隶体"></p><p>增加了更多的鼠标样式。</p><p><img src="https://blog.nineya.com/upload/2023/11/image-1699975723156.png" alt="鼠标样式"></p><p>在这次迭代中在分页导航上做了,<code>Dream 2.0</code> 上新增了 <code>最新评论</code> 侧边模块,优化了 评论区开启关闭控制、侧边统计信息配置等功能。</p><h2 id="九-寻觅2024---结语">九、寻觅2024 &amp; 结语</h2><p>下一年度迭代计划:暂无</p><p>或许会继续优化 <code>Halo-Plus</code> 的编辑器,或许会添加一些 <code>Widget</code> 模块,或许会重构一些页面,或许……我自己也不知道 <img alt="xiaoguai" class="emoji" src="/themes/dream/source/img/emoji/xiaoguai.png" title="xiaoguai">。似乎,能做的都已完成,但却始终没有找到最好的自己。</p><p>迷茫,是常态。亦或也是无限的可能?</p><p>敬请期待吧!<img alt="meigui" class="emoji" src="/themes/dream/source/img/emoji/meigui.png" title="meigui"></p><p><mew-quote name="玖涯博客">追寻梦的方向,聆听心的声音。</mew-quote></p><h2 id="十-致谢---宣传">十、致谢 &amp; 宣传</h2><p>在今年 <code>5月11日</code> 小玖注册了微信公众号 <code>玖涯菜菜子</code>。</p><p>本想细细经营,可因为懒惰,这半年多时间里很少发布有深度的技术和资讯文章,大多是主题版本迭代的说明。</p><p>但截止到现在,<code>玖涯菜菜子</code> 公众号还是积累到了 <code>130+</code> 的粉丝数了。</p><p>跨年之夜,小玖准备了 24 份 <code>微信红包封面</code> 赠送给各位粉丝,祝大家新的一年光辉璀璨。</p><blockquote><p>红包封面如下图任选一款,在公众号后台回复 <code>跨年送封面 + 红包封面序号</code> 领取,先到先得。</p></blockquote><p><mew-subtitle>红包封面1-2</mew-subtitle></p><p><img src="https://blog.nineya.com/upload/2023/12/%E7%BA%A2%E5%8C%85%E5%B0%81%E9%9D%A21-2.png" alt="红包封面1-2"></p><p><mew-subtitle>红包封面3-4</mew-subtitle></p><p><img src="https://blog.nineya.com/upload/2023/12/%E7%BA%A2%E5%8C%85%E5%B0%81%E9%9D%A23-4.png" alt="红包封面3-4"></p><p><mew-subtitle>红包封面5-6</mew-subtitle></p><p><img src="https://blog.nineya.com/upload/2023/12/%E7%BA%A2%E5%8C%85%E5%B0%81%E9%9D%A25-6.png" alt="红包封面5-6"></p><p><mew-subtitle>红包封面7-8</mew-subtitle></p><p><img src="https://blog.nineya.com/upload/2023/12/%E7%BA%A2%E5%8C%85%E5%B0%81%E9%9D%A27-8.png" alt="红包封面7-8"></p><p><mew-subtitle>红包封面9-10</mew-subtitle></p><p><img src="https://blog.nineya.com/upload/2023/12/%E7%BA%A2%E5%8C%85%E5%B0%81%E9%9D%A29-10.png" alt="红包封面9-10"></p><p>起初小玖想定制一款红包封面,可时间上来不及了,明年争取换个更好的。</p>

玖涯博客

微信红包封面分销代理开通方法

<h2 id="一-下载微店店长版APP">一、下载微店店长版APP</h2><p><strong>下载微店店长版APP,注册登录后,实名认证,才能分销(实名教程看下图)</strong></p><p><img src="https://blog.nineya.com/upload/2023/12/%E5%BE%AE%E4%BF%A1%E5%BA%97%E9%95%BF%E7%89%88%E5%BC%80%E5%BA%97.png" alt="微信店长版开店步骤"></p><h2 id="二-扫码分销">二、扫码分销</h2><p><strong>实名认证后需要用微店店长版app扫分销码</strong></p><p><img src="https://blog.nineya.com/upload/2023/12/%E6%89%AB%E7%A0%81%E5%88%86%E9%94%80%E6%AD%A5%E9%AA%A4.png" alt="扫码分销步骤"></p><p><strong>分销码如下:</strong></p><p><img src="https://blog.nineya.com/upload/2023/12/image-1703914449096.png" alt="微信红包封面分销码"></p><p><img src="https://blog.nineya.com/upload/2023/12/image-1703913580251.png" alt=""></p><h2 id="三-认证微信支付">三、认证微信支付</h2><p><img src="https://blog.nineya.com/upload/2023/12/%E8%AE%A4%E8%AF%81%E5%BE%AE%E4%BF%A1%E6%94%AF%E4%BB%98%E6%AD%A5%E9%AA%A4.png" alt="认证微信支付步骤"></p><h2 id="四-分享店铺">四、分享店铺</h2><p><strong>分享店铺(生成你自己店铺的链接,然后推广这个链接就可以了)</strong></p><blockquote><p>不要用小程序分享,红包封面是虚拟商品,小程序可能打不开链接</p></blockquote><p><img src="https://blog.nineya.com/upload/2023/12/%E5%88%86%E4%BA%AB%E5%BA%97%E9%93%BA%E6%AD%A5%E9%AA%A4.png" alt="分享店铺步骤"></p>

玖涯博客

通过 Nginx 代理实现网页内容替换

<p>突发奇想,用 <code>Nginx</code> 代理一个网站,把网站的一些关键字替换掉,蛮有意思的。</p><p>如下图:</p><p><img src="https://blog.nineya.com/upload/2023/12/image-1703261740026.png" alt="百度百科"></p><h2 id="一-编译安装-Nginx">一、编译安装 Nginx</h2><p>一般 <code>Nginx</code> 中不包含 <code>subs_filter</code> 文本替换的模块,需要自己手动编译安装,步骤如下。</p><p>克隆 <code>subs_filter</code> 仓库:</p><pre><code class="language-bash">git clone http://github.com/yaoweibin/ngx_http_substitutions_filter_module.git</code></pre><p>正常下载 <code>Nginx</code> 源码包解压,并安装相关编译环境:</p><pre><code class="language-bash"># 解压安装包tar -zxvf nginx-1.22.1.tar.gz# 安装编译环境yum -y install gcc gcc-c++ pcre pcre-devel openssl openssl-devel zlib zlib-devel</code></pre><p>预编译 <code>Nginx</code>,需要携带上 <code>--with-http_sub_module</code> 参数,并通过 <code>--add-module</code> 指定刚刚拉取的 <code>subs_filter</code> 仓库地址。</p><p>本文中仓库地址为:/root/install/ngx_http_substitutions_filter_module/</p><p>所以需要携带上关键参数:<code>--with-http_sub_module --add-module=/root/install/ngx_http_substitutions_filter_module/</code></p><p>完整预编译命令如下:</p><pre><code class="language-bash">./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_stub_status_module --with-http_gzip_static_module --with-http_sub_module --with-http_v2_module --add-module=/root/install/ngx_http_substitutions_filter_module/</code></pre><p>正常步骤安装 <code>Nginx</code>。</p><pre><code class="language-bash"># 编译并安装make &amp;&amp; make install # 检查是否编译正确,如果返回值是 0,就是执行成功;0 以外的值,就是失败。echo $?# 软连接二进制文件到命令目录ln -s /usr/local/nginx/sbin/nginx /usr/local/sbin/nginx</code></pre><h2 id="二-配置实现">二、配置实现</h2><p><code>sub_filter</code> 命令用于指定替换的文本:<code>sub_filter $目标文本 $替换文本;</code>。</p><p><code>sub_filter_once</code> 命令指定替换文本的次数,默认为 <code>on</code>,仅替换一次,修改为 <code>off</code> 可进行全文替换。</p><blockquote><p>需要注意源站是否有进行 <code>gzip</code> 压缩,<code>sub_filter</code> 仅能对未压缩的网页进行替换。</p></blockquote><pre><code class="language-conf">server{ listen 80; listen 443 ssl http2; server_name vm.nineya.com; index index.php index.html index.htm default.php default.htm default.html; ...省略一堆关于https的配置... location / { # 需要进行字符替换的源站点 proxy_pass http://baike.baidu.com; # 指定源站不要进行压缩 proxy_set_header Accept-Encoding ''; sub_filter_once off; sub_filter 马云 玖涯; } }</code></pre><h2 id="三-字符替换不生效">三、字符替换不生效</h2><p>按上面的 <code>conf</code> 配置,实际上是无法生效的,因为 <code>Accept-Encoding</code> 配置对于 <code>baike.baidu.com</code> 并没有生效,百度百科依旧反回了经过 <code>gzip</code> 压缩的内容。</p><p>该问题可通过两次代理解决,先用一次代理去除源站的 <code>gzip</code> 压缩,然后再用第二次代理进行文本替换。</p><p>配置参考如下:</p><pre><code class="language-conf">server{ listen 80; listen 443 ssl http2; server_name vm.nineya.com; index index.php index.html index.htm default.php default.htm default.html; ...省略一堆关于https的配置... # 代理去除源站的gzip压缩 location /test { gzip off; proxy_pass https://baike.baidu.com/; proxy_set_header HOST 'baike.baidu.com'; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; } # 给外部调用,进行文本替换 location / { proxy_pass https://vm.nineya.com/test; proxy_set_header Accept-Encoding ''; sub_filter_once off; sub_filter 马云 玖涯; }}</code></pre>

玖涯博客