跳到主要内容

· 阅读需 5 分钟
wen

背景

img

  • 获取两个切片之间的公共元素还是一个比较常见的需求,但是在 Code Review 的过程中,我发现还是会有一些人会用双重循环来实现。(这样的时间复杂度是 O(n^2),效率比较低)
  • 最近 Golang 用的多,顺便分享一下 Golang 中如何获取两个切片之间的公共元素的方法。

❌ 用双重循环实现的代码例子:

func intersection(nums1 []int, nums2 []int) []int {
var result []int

// 双重循环 O(n^2)
for _, v1 := range nums1 { // O(n) 外循环
for _, v2 := range nums2 { // O(n) 内循环
if v1 == v2 {
result = append(result, v1)
}
}
}
return result
}

改善方案

把上面例子中的内循环中的元素查找改成用 set* 来实现,这样内循环部分的时间复杂度就可以降低到 O(1),整体的时间复杂度就可以降低到 O(n)

Javascript 中的 set 解释

Set 对象是值的合集(collection)。集合(set)中的元素只会出现一次,即集合中的元素是唯一的。

规范要求集合的实现是"对合集中的元素的平均访问时间与集合中元素的数量呈次线性关系"。

因此,它可以在内部表示为哈希表(查找的时间复杂度为 O(1))、搜索树(查找的时间复杂度为 O(log(N)))或任何其他的时间复杂度低于 O(N) 的数据结构。

参考链接

set 来实现的例子

func intersection(nums1 []int, nums2 []int) []int {
var result []int

// 把 nums2 转换成 set,这样在 nums2 中查找元素的时间复杂度就变成了 O(1)。
// 考虑性能优化的话,可以把 nums1 和 nums2 中的元素数量进行比较,把数量多的那个切片转换成 set。
set := make(map[int]struct{}) // golang 中的 没有 set,用 map 来实现。struct{} 是一个空结构体,用来节省内存。
for _, v := range nums2 {
set[v] = struct{}{}
}

// 遍历 nums1,如果 nums1 中的元素在 nums2 中存在,就把它加入到 result 中
for _, v := range nums1 {
if _, ok := set[v]; ok {
result = append(result, v)
}
}
}

使用第三方库实现

考虑性能优化以及各种类型的切片,我们可以使用下面的第三方库来实现。

deckarep/golang-set

如其名,Golang 的 set 实现。

import (
"fmt"
mapset "github.com/deckarep/golang-set/v2"
)

func main() {
set1 := mapset.NewSet[string]()
set1.Add("a")
set1.Add("b")
set1.Add("c")

set2 := mapset.NewSet[string]()
set2.Add("c")
set2.Add("d")
set2.Add("e")

// 交集
intersectionSet := set1.Intersect(set2)
fmt.Println(intersectionSet) // Set{c}

// 除了交集,还支持并集、差集、对称差集等操作
// 并集
unionSet := set1.Union(set2)
fmt.Println(unionSet) // Set{a, b, c, d, e}

// 差集
diffSet := set1.Difference(set2)
fmt.Println(diffSet) // Set{a, b}

// 对称差集
symDiffSet := set1.SymmetricDifference(set2)
fmt.Println(symDiffSet) // Set{a, b, d, e}
}

samber/lo

如果你除了要对切片进行交集操作,还需要对切片等进行排序、分组等操作,那么可以考虑使用 samber/lo 这个库。

你可以把它理解成 lodash 的 Golang 版本。

import (
"github.com/samber/lo"
)

func main() {
// 交集
lo.Intersection([]int{1, 2, 3}, []int{2, 3, 4}) // return []int{2, 3}

// 并集
lo.Union([]int{1, 2, 3}, []int{2, 3, 4}) //return []int{1, 2, 3, 4}

// 差集
lo.Difference([]int{1, 2, 3}, []int{2, 3, 4}) // return []int{1}, []int{4}
}

· 阅读需 2 分钟
wen

Symmetric API testing 是啥?

这个概念应该是源自于 Gopher Academy Blog

作者在维护一个 Golang 的 Twitter API 客户端,为了对 Twitter 的 API 进行测试,所以作者提出了 Symmetric API testing 的概念。

简单地说就是保存 API 的返回结果,然后在测试的时候,用保存的结果来进行测试。

这样就不用编写 mock 和 测试用例了。

至于名字为什么叫 Symmetric, 是相对于传统的需要编写 mock 和 测试用例的方式 Asymmetric 而言的。

其实个人觉得把它叫做 SnapShot Testing 更为合适。

怎么实现?

除了手动保存 API 的返回结果,还可以使用 go-vcr 这个库来实现。

大概的代码如下:

r, err := recorder.New("<filename>")
if err != nil {
return err
}
defer r.Stop()
client.Transport = r
res, err := client.Get("http://api.twitter.com/...")
if err != nil {
return err
}

这里提供了完整的 示例代码

Reference

Symmetric API Testing

Symmetric API Testing という、手間なく堅牢に外部 API Client をテストする手法

go-vcr を使った Symmetric API Testing のメモ

· 阅读需 3 分钟
wen

背景&需求

在 Golang 中,我们经常会遇到需要解析 JSON 数据的场景,比如从 HTTP 请求中获取 JSON 数据,或者从文件中读取 JSON 数据。

通常我们会提前定义好对应的结构体,然后才能将 JSON 数据解析到结构体中。

比如:

type User struct {
Name string `json:"name"`
Age int `json:"age"`
}

func main() {
jsonStr := `{"name": "wen", "age": 18}`
var user User
json.Unmarshal([]byte(jsonStr), &user)
fmt.Println(user)
}

但是有时候我们并不知道 JSON 数据的结构,或者 JSON 数据的结构会经常变化,这时候我们就无法提前定义好对应的结构体。

解决方案

可以使用 map[string]any (Golang1.18 之前的话 map[string]interface{} ) 来解析 JSON 数据,这样就不需要提前定义结构体了。

func main() {
jsonStr := `{"name": "wen", "age": 18}`
var user map[string]any
json.Unmarshal([]byte(jsonStr), &user)
fmt.Println(user)

// 获取具体的值
fmt.Println(user["name"])
fmt.Println(user["age"])
}

扩展

如果觉得 map[string]any 这种方式解析速度比较慢,可以使用 jsonparser 这个库来解析,速度会快很多。

我用 User 结构体来测试了一下,解析速度快了 8-9 倍左右 🚀

其他的比较大的 JSON 数据,解析速度也会快很多,具体可以看下这里的 benchmark

NameIterationsns/op
BenchmarkEncodingJsonInterfaceUser-122540230460.6 ns/op
BenchmarkJsonParserUser-122141329655.91 ns/op
查看测试代码
// Just for emulating field access, so it will not throw "evaluated but not used"
func nothing(_ ...interface{}) {}

// 使用 jsonparser
func BenchmarkJsonParserUser(b *testing.B) {
for i := 0; i < b.N; i++ {
jsonparser.Get(user, "name")
jsonparser.Get(user, "age")
nothing()
}
}

// 使用 map[string]any
func BenchmarkEncodingJsonInterfaceUser(b *testing.B) {
for i := 0; i <details b.N; i++ {
var data interface{}
json.Unmarshal(user, &data)
m := data.(map[string]interface{})

nothing(m["name"].(string), m["age"])
}
}

· 阅读需 3 分钟
wen

0. 背景&需求

我的博客是使用 Cloudflare Pages 部署的,

但是 Cloudflare Pages 并没有提供部署状态通知的功能,所以我想到了使用 Github Actions 来实现这个功能。

1. 前提

  • 已经在 Github 上创建了你的网站的仓库
  • 已经在 Cloudflare Pages 上设置好了你的 Github 仓库
    • 比如我是设置成了 main 分支更新时自动部署

2. 方法

3. 实现

arddluma/cloudflare-pages-slack-notification 这个 Github Action 已经实现这个了我们所要的功能。

我们只需要准备好这个 Github Action 中使用的变量。

- name: Await CF Pages and send Slack notification
uses: arddluma/cloudflare-pages-slack-notification@v4
with:
# Cloudflare API token
apiToken: ${{ secrets.CF_API_TOKEN }}
# CloudFlare account ID
accountId: ${{ secrets.CF_ACC_ID }}
# CloudFlare Pages project name
project: ${{ secrets.CF_PAGES_PROJECT }}
# Create Slack Incoming webhook and add as variable https://hooks.slack.com/...
slackWebHook: ${{ secrets.SLACK_WEBHOOK }}
# Add this if you want to wait for a deployment triggered by a specfied commit
commitHash: ${{ steps.push-changes.outputs.commit-hash }}

3.1 获得变量

1️⃣ 2️⃣ 3️⃣ 是 Github Actions 通过 Cloudflare Pages 的 API 来获取部署状态所需要的参数。

4️⃣ 是 Github Actions 发送消息给 Slack 所需要的 webhook URL。

3.2 设置变量

在你的网站的 Github 仓库中,点击 Settings -> Secrets and variables -> New repository secret,然后设置上面的变量。

img

4. Github Actions 代码

github/workflows/cloudflare-pages.yml
name: Detect Cloudflare pages deployment status and notify to Slack
on:
push:
branches:
- main
paths-ignore:
- .github/**
pull_request:
branches:
- main
types: [closed]
paths-ignore:
- .github/**
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Await CF Pages and send Slack notification
id: cf-pages
uses: arddluma/cloudflare-pages-slack-notification@v4
with:
# Clouodflare API token
apiToken: ${{ secrets.CF_API_TOKEN }}
# CloudFlare account ID
accountId: ${{ secrets.CF_ACC_ID }}
# CloudFlare Pages project name
project: ${{ secrets.CF_PAGES_PROJECT }}
# Create Slack Incoming webhook and add as variable https://hooks.slack.com/...
slackWebHook: ${{ secrets.SLACK_WEBHOOK }}
# Add this if you want to wait for a deployment triggered by a specfied commit
commitHash: ${{ steps.push-changes.outputs.commit-hash }}

效果

  • Github Action Build Progress: img

  • Slack notification: img

Reference

Setup Cloudflare Pages Slack notifications

· 阅读需 2 分钟
wen

问题

Docuaurus 的文档(Docs)默认的 URL 路径名是 /docs,如:https://thewang.net/docs

如果你想把它改成 /api,如:https://thewang.net/api ,该怎么做呢?

解决方法

docusaurus.config.js 中,

  • 找到 docs 的配置项,添加 routeBasePath 属性,值为你想要的 URL 路径名,如:api
  • 找到 navbar 的配置项,
docusaurus.config.js
  presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
+ routeBasePath: 'api',
sidebarPath: require.resolve('./sidebars.js'),
...
docusaurus.config.js
     navbar: {
...
items: [
{ to: '/blog', label: 'Blog', position: 'left' },
// API docs
{
to: '/api',
type: 'docSidebar',
label: 'API',
...
},

补充

那那些已经公开的文档(Docs)的 URL 路径不能访问了該怎么办呢?

可以安装 docusaurus/plugin-client-redirects 插件,来实现重定向解决。

  npm install @docusaurus/[email protected] // 注意和你的 docusaurus 版本对应
docusaurus.config.js
...
+ plugins: [
+ [
+ '@docusaurus/plugin-client-redirects',
+ {
+ createRedirects(existingPath) {
+ if (existingPath.includes('/api')) {
+ // Redirect from /docs to /weekly
+ return [
+ existingPath.replace('/api', '/docs'),
+ ];
+ }
+ return undefined; // Return a falsy value: no redirect created
+ },
+ },
+ ]
+ ],

presets: [
...
PRODUCTION ONLY

This plugin is always inactive in development and only active in production because it works on the build output.

@docusaurus/plugin-client-redirects 只在生产环境下生效。

· 阅读需 2 分钟
wen

背景

以前经常用 plantuml 来生成各种示意图。

自从 Github 支持了 mermaid 语法后,就慢慢的转向了 mermaid。

今天就来记录一下如何使用 mermaid 来绘制 Gitflow 的示意图。

提示

Mermaid 是一个基于 JavaScript 的图表绘制工具,

它使用简单的文本描述来定义图表,然后将文本转换为图表。

Mermaid 支持流程图、序列图、类图、状态图、Git 图、甘特图等多种图表。

官方还提供了一个在线编辑器

1. Gitflow 的 Mermaid 代码

gitGraph LR:
commit id: "1:init"
branch "hotfix/{ticket_number}" order: 1
branch "release/{x}" order: 2
branch develop order: 3
commit id: "2:init"
branch "feature/{ticket_number}" order: 4
checkout "feature/{ticket_number}"
commit id: "3:commit-x"
checkout develop
merge "feature/{ticket_number}" id: "4:merge-feat" type: HIGHLIGHT

# release
checkout "release/{x}"
merge develop id: "5:release-x"
checkout main
merge "release/{x}" id: "6:merge-release" tag: "v.1.0.0" type: HIGHLIGHT

# hotfix
checkout "hotfix/{ticket_number}"
commit id: "7:commit-hotfix"
checkout develop
merge "hotfix/{ticket_number}" id: "8:merge-hotfix" type: HIGHLIGHT

2. Mermaid 代码的渲染结果

似乎 本博客( Docusaurus )的渲染结果 和官方提供的在线编辑器 生成的结果(↓)稍微有点不一样。

不过,大家可以通过修改 Mermaid 的配置 调整显示效果。

img

· 阅读需 4 分钟
wen

需求

  • Golang 中将结构体的数组保存到 CSV 文件中
  • CSV 文件的列顺序和结构体中的字段顺序不同
user.go
type User struct {
Name string
Age int
}

↓↓↓

AgeName
20"Musk"

调查

比较有人气的的 Golang CSV 库有:

但是两者都不支持指定结构体字段的顺序,而且也没有提供相应的 writer 接口,所以也无法自己覆盖接口实现。

本来想 fork 上面的其中一个库改造下,但搜索发现了 shigetaichi/xsv 这个库,在 gocsv 的代码的基础上实现了指定结构体字段顺序的功能。

字段的顺序可以通过 xsvSortOrder 属性指定一个数组,数组中的值是结构体中相应字段的索引(starting from 0)。

比如, 如果想要将结构体按照 Age, Name 的顺序输出,可以这样指定:

type User struct {
Name string `csv:"name"`
Age int `csv:"age"`
}

xsvWrite.SortOrder = []int{1, 0}

但是个人觉得字段顺序的指定方式不太友好。 既然已经有了 csv tag,为什么不直接使用 csv tag 来指定字段的顺序呢?

比如:

type User struct {
Name string `csv:"name,order:1"`
Age int `csv:"age,order:0"`
}

我的方案

代码

代码
package main

import (
"fmt"
"log"
"os"
"reflect"
"strconv"
"strings"

"github.com/shigetaichi/xsv"
)

type User struct {
Name string `csv:"name, order:1"`
Age int `csv:"age, order:0"`
}

func main() {
users := []*User{
{Name: "Alice", Age: 20},
{Name: "Bob", Age: 30},
}

// Create a csv file to write
file, err := os.OpenFile("users.csv", os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm)
if err != nil {
panic(err)
}
defer file.Close()

xsvWrite := xsv.NewXsvWrite[*User]()
// Get the order of the fields in the struct and set it to the writer
orders := getOrderOfFields(reflect.TypeOf(User{}))
xsvWrite.SortOrder = orders

// Write the users to the csv file
err = xsvWrite.SetFileWriter(file).Write(users)
if err != nil {
log.Println(err)
return
}
}

// Get the order of the fields in specified struct
func getOrderOfFields(structType reflect.Type) []int {
// Create a slice of int with the same length as the number of fields in the struct
var res []int
// Iterate through the struct fields
for i := 0; i < structType.NumField(); i++ {
// Get the field
field := structType.Field(i)

// Get the "order" tag value
order := getTagValue(field, "csv", "order")
if order >= 0 {
res = append(res, order)
}
}

return res
}

// getTagValue は指定されたフィールドの指定されたタグの値を取得する
func getTagValue(field reflect.StructField, tag string, tagField string) int {
tagValue, _ := field.Tag.Lookup(tag)
// Split the tag string by ","
tagParts := strings.Split(tagValue, ",")

// Iterate through the tag parts to find the "order" value
res := -1
for _, part := range tagParts {
part = strings.TrimSpace(part)
// Check if the part starts with "order:"
prefix := fmt.Sprintf("%s:", tagField)
if strings.HasPrefix(part, prefix) {
// Extract the numeric value after "order:"
valueStr := strings.TrimPrefix(part, prefix)
value, err := strconv.Atoi(valueStr)
if err == nil {
res = value
}
break
}
}

return res
}

保存的 CSV 文件

agename
20Alice
30Bob

· 阅读需 4 分钟
wen

背景

在使用 Golang 的时候,经常会遇到需要将结构体转换为 JSON 的情况,

但是在转换的时候,JSON 的字段顺序并不是我们想要的,这时候就需要我们自己来指定 JSON 的字段顺序。

解决方案

一个比较简单的方案是利用结构体的 tag 来指定 JSON 的字段顺序,

然后在转换的时候,将结构体的字段按照 tag 中的顺序进行排序。

type User struct {
Name string `json:"name,order:2"`
Age int `json:"age,order:1"`
}

代码实现

package main

import (
"encoding/json"
"fmt"
"reflect"
"sort"
"strconv"
"strings"

//orderedmap "github.com/wk8/go-ordered-map/v2"
"github.com/iancoleman/orderedmap"
)

type User struct {
Name string `json:"name,order:3"`
Age int `json:"age,order:2"`
Score int `json:"score,order:1"`
}

type Address struct {
City string `json:"city,order:10"`
Street string `json:"street,order:9"`
ZipCode string `json:"zip_code,order:8"`
}

func main() {
user := User{
Name: "Wen",
Age: 30,
Score: 100,
}
address := Address{
City: "Hangzhou",
Street: "XiHuDaDao",
ZipCode: "10001",
}

// User構造体を指定した順序でJSONに変換
userJSON, err := MarshalJSONWithOrder(user)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("User JSON:", string(userJSON))

// Address構造体を指定した順序でJSONに変換
addressJSON, err := MarshalJSONWithOrder(address)
if err != nil {
fmt.Println("Error:", err)
return
}

fmt.Println("Address JSON:", string(addressJSON))
}

// MarshalJSONWithOrder は構造体を指定した順序でJSONに変換する
// 構造体での指定方法: `json:"{struct field name},order:{integer}"`
// 指定例: `json:"name,order:10"`
func MarshalJSONWithOrder(obj interface{}) ([]byte, error) {
val := reflect.ValueOf(obj)
typ := reflect.TypeOf(obj)

// ソート用のスライス
var fields []fieldWithOrder

// フィールドの数だけループ
for i := 0; i < val.NumField(); i++ {
fieldName := typ.Field(i).Name

order := getTagValue(typ.Field(i), "json", "order")
fields = append(fields, fieldWithOrder{
Name: fieldName,
Order: order,
})
}

// フィールドのソート
sort.Slice(fields, func(i, j int) bool {
return fields[i].Order < fields[j].Order
})

// ソート後の順序に従ってJSONを生成
//result := orderedmap.New[string, any]()
result := orderedmap.New()
for _, f := range fields {
//result[f.Name] = val.FieldByName(f.Name).Interface()
result.Set(f.Name, val.FieldByName(f.Name).Interface())
}

// マーシャリング
return json.Marshal(result)
}

// getTagValue は指定されたフィールドの指定されたタグの値を取得する
func getTagValue(field reflect.StructField, tag string, tagField string) int {
tagValue, _ := field.Tag.Lookup(tag)

// Split the tag string by ","
tagParts := strings.Split(tagValue, ",")

// Iterate through the tag parts to find the "order" value
res := -1
for _, part := range tagParts {
// Check if the part starts with "order:"
prefix := fmt.Sprintf("%s:", tagField)
if strings.HasPrefix(part, prefix) {
// Extract the numeric value after "order:"
orderStr := strings.TrimPrefix(part, prefix)
order, err := strconv.Atoi(orderStr)
if err == nil {
res = order
}
}
}

return res
}

// fieldWithOrder はソート用の構造体
type fieldWithOrder struct {
Name string
Order int
}

NOTE

  • Ordered Map 的使用

    Golang 中的 map 是无序的,如果需要有序的 map, 可以使用 wk8/go-ordered-map 或者 iancoleman/orderedmap。 由于后者的性能似乎比较好,代码中采用的是后者。

  • ChatGPT 的使用

    代码大部分是用 ChatGPT 生成的,但是生成的代码中有几个问题,例如上面的 Ordered Map,我尝试让 ChatGPT 修改了几次,都没有成功。 最后还是自己手动修改了代码。

    感觉 ChatGPT 对于代码的细节部分的理解还有待改善,而且有时候还胡说八道(现阶段生成式 AI 的通病)。

· 阅读需 5 分钟
wen

背景

Docusaurus 默认没有集成评论系统。

在之前的博客中用过 Giscus ,

它是一个基于 GitHub Discussions 的评论系统,可以很方便地集成到 GitHub Pages、VuePress、Docusaurus 等静态网站生成器中。

这次打算把它添加到 Docusaurus。

提示

如果不想依存 Github 的服务,可以开卡 Waline 评论系统,可以添加自己的后端服务。

准备工作

提示

可以点击流程图中带下划线的 node 跳转到相关页面

步骤

  • 流程图

1. 配置 Giscus

Giscus 的网站上配置你的评论系统,并复制你的配置代码(高亮部分会在步骤 3 中用到)。

giscus config
<script src="https://giscus.app/client.js"
data-repo="your_org_name/your-repo"
data-repo-id="your-repo-id"
data-category="Announcements"
data-category-id="your-category-id"
... // other options
async>
</script>

2. 创建评论组件

2.1. 安装依赖

npm install @giscus/react mitt

# or
yarn add @giscus/react mitt

2.2. 创建组件

点击查看代码
src/components/GiscusComments/index.tsx
import React from "react";
import { useThemeConfig, useColorMode } from "@docusaurus/theme-common";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import { ThemeConfig } from "@docusaurus/preset-classic";
import BrowserOnly from "@docusaurus/BrowserOnly";
import Giscus, { GiscusProps } from "@giscus/react";

interface CustomThemeConfig extends ThemeConfig {
giscus: GiscusProps & { darkTheme: string };
}

const defaultConfig: Partial<GiscusProps> & { darkTheme: string } = {
id: "comments",
mapping: "title",
reactionsEnabled: "1",
emitMetadata: "0",
inputPosition: "top",
lang: "zh-CN",
theme: "light",
darkTheme: "dark",
};

export default function Comment(): JSX.Element {
const themeConfig = useThemeConfig() as CustomThemeConfig;
const { i18n } = useDocusaurusContext();

// merge default config
const giscus = { ...defaultConfig, ...themeConfig.giscus };

if (!giscus.repo || !giscus.repoId || !giscus.categoryId) {
throw new Error(
"You must provide `repo`, `repoId`, and `categoryId` to `themeConfig.giscus`."
);
}

giscus.theme =
useColorMode().colorMode === "dark" ? giscus.darkTheme : giscus.theme;
giscus.lang = i18n.currentLocale;

return (
<BrowserOnly fallback={<div>Loading Comments...</div>}>
{() => <Giscus {...giscus} />}
</BrowserOnly>
);
}

用 Docusaurus 的 swizzle 命令对博客页面对应组件进行修改(添加前面创建的评论组件)。

npm run swizzle @docusaurus/theme-classic BlogPostPage -- --eject --typescript
# or
yarn run swizzle @docusaurus/theme-classic BlogPostPage -- --eject --typescript

以上命令会在 src/theme/BlogPostPage/index.tsx 中生成一些文件,我们只需要修改 index.tsx 文件即可。

以下高亮部分为修改代码:

点击查看代码
src/theme/BlogPostPage/index.tsx
import React, {type ReactNode} from 'react';
import clsx from 'clsx';
import {HtmlClassNameProvider, ThemeClassNames} from '@docusaurus/theme-common';
import {BlogPostProvider, useBlogPost} from '@docusaurus/theme-common/internal';
import BlogLayout from '@theme/BlogLayout';
import BlogPostItem from '@theme/BlogPostItem';
import BlogPostPaginator from '@theme/BlogPostPaginator';
import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata';
import TOC from '@theme/TOC';
import type {Props} from '@theme/BlogPostPage';
import Unlisted from '@theme/Unlisted';
import type {BlogSidebar} from '@docusaurus/plugin-content-blog';
import Comment from '../../components/GiscusComments';

function BlogPostPageContent({
sidebar,
children,
}: {
sidebar: BlogSidebar;
children: ReactNode;
}): JSX.Element {
const {metadata, toc} = useBlogPost();
const {nextItem, prevItem, frontMatter, unlisted} = metadata;
const {
hide_table_of_contents: hideTableOfContents,
toc_min_heading_level: tocMinHeadingLevel,
toc_max_heading_level: tocMaxHeadingLevel,
hide_comment: hideComment,
} = frontMatter;
return (
<BlogLayout
sidebar={sidebar}
toc={
!hideTableOfContents && toc.length > 0 ? (
<TOC
toc={toc}
minHeadingLevel={tocMinHeadingLevel}
maxHeadingLevel={tocMaxHeadingLevel}
/>
) : undefined
}>
{unlisted && <Unlisted />}
<BlogPostItem>{children}</BlogPostItem>

{(nextItem || prevItem) && (
<BlogPostPaginator nextItem={nextItem} prevItem={prevItem} />
)}
{!hideComment && <Comment />}
</BlogLayout>
);
}

...

2.3. 配置 Docusaurus

docusaurus.config.js 中添加配置项:

docusaurus.config.js
module.exports = {
...
themeConfig: {
...
// giscus options
giscus: {
repo: 'your_org/your-blog', // edit this
repoId: 'your_repo_id', // edit this
category: 'Announcements',
categoryId: 'your_category_id', // edit this
}
...
},
...
}

3. 重启 Docusaurus

npm start
# or
yarn start

参考

Docusaurus 添加评论功能

评论服务

· 阅读需 3 分钟
wen

背景

最近参与的一个项目,需要经常地进入 AWS ECS 的 Fargate 服务中的容器中。

使用 awscli 的 ECS Exec 可以非常方便地连接到运行中的容器。

img

其大概的原理如下图所示:

ECS Exec 把 SSM Agent 绑定到容器,从而可以通过 Systems Manager (SSM) 的会话管理器(Session Manager)访问容器。

img

所以执行该命令,除了需要安装 AWS CLI 之外,还需要安装 session-manager-plugin

对于团队里的一部分成员来说,安装这些工具还是比较麻烦的,所以我写了一个脚本,可以一键连接到 ECS 的容器中。

脚本

connect-to-aws-ecs.sh
#!/bin/bash -eu

# check if aws-cli is installed
if ! type aws >/dev/null 2>&1; then
echo "aws-cli is not installed"
echo "installing aws-cli...."
brew install awscli
echo "aws-cli is installed successfully. please set your credentials by 'aws configure' command"
fi

# check if session-manager-plugin is installed
if ! type session-manager-plugin >/dev/null 2>&1; then
echo "session-manager-plugin is not installed"
echo "installing session-manager-plugin...."
# Download and unzip the Session Manager plugin installer
mkdir -p $HOME/session-manager-plugin/bin
curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/mac/sessionmanager-bundle.zip" -o "$HOME/session-manager-plugin/bin/sessionmanager-bundle.zip"
unzip -d $HOME/session-manager-plugin/bin $HOME/session-manager-plugin/bin/sessionmanager-bundle.zip
sudo $HOME/session-manager-plugin/bin/install -i /usr/local/sessionmanagerplugin -b /usr/local/bin/session-manager-plugin

echo "session-manager-plugin is installed successfully."
fi

# set your container name and cluster name
container_name="your-container-name"
cluster_name="your-ecs-cluster-name"

# 执行脚本时,使用 `--profile` 指定的 AWS 配置文件
aws_profile="default"
while [ "$#" -gt 0 ]; do
case "$1" in
--profile=*)
aws_profile="${1#*=}"
;;
*)
# exit when unknown option is specified
echo "未知のオプション: $1"
exit 1
;;
esac
shift
done
echo "aws_profile: $aws_profile"

# fetch task arn
task_arn=$(aws ecs list-tasks \
--profile "$aws_profile" \
--cluster $cluster_name \
--desired-status RUNNING \
--query 'taskArns[0]' \
--output text)

# spit task id from task arn
task_id=$(echo "$task_arn" | awk -F/ '{print $NF}')
echo "task_id: $task_id"

# enter container with session manager
aws ecs execute-command \
--profile "$aws_profile" \
--cluster $cluster_name \
--task "$task_id" \
--container $container_name \
--command "/bin/sh" \
--interactive

使用方法

  1. 将上述脚本保存为 connect-to-aws-ecs.sh 文件
  2. 执行 chmod +x connect-to-aws-ecs.sh 赋予脚本执行权限
  3. 修改脚本中的 container_namecluster_name 为你自己的值
  4. 执行 ./connect-to-aws-ecs.sh 即可连接到 ECS 的容器中

Reference

使用 ECS Exec 访问 Fargate 上的容器

Amazon ECS Exec を使ってみる