分类 技术 下的文章

本文是该系列的第三篇。

在我们的即时消息应用中,消息表现为两个参与者对话的堆叠。如果你想要开始一场对话,就应该向应用提供你想要交谈的用户,而当对话创建后(如果该对话此前并不存在),就可以向该对话发送消息。

就前端而言,我们可能想要显示一份近期对话列表。并在此处显示对话的最后一条消息以及另一个参与者的姓名和头像。

在这篇帖子中,我们将会编写一些 端点 endpoint 来完成像“创建对话”、“获取对话列表”以及“找到单个对话”这样的任务。

首先,要在主函数 main() 中添加下面的路由。

router.HandleFunc("POST", "/api/conversations", requireJSON(guard(createConversation)))
router.HandleFunc("GET", "/api/conversations", guard(getConversations))
router.HandleFunc("GET", "/api/conversations/:conversationID", guard(getConversation))

这三个端点都需要进行身份验证,所以我们将会使用 guard() 中间件。我们也会构建一个新的中间件,用于检查请求内容是否为 JSON 格式。

JSON 请求检查中间件

func requireJSON(handler http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if ct := r.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
            http.Error(w, "Content type of application/json required", http.StatusUnsupportedMediaType)
            return
        }
        handler(w, r)
    }
}

如果 请求 request 不是 JSON 格式,那么它会返回 415 Unsupported Media Type(不支持的媒体类型)错误。

创建对话

type Conversation struct {
    ID                string   `json:"id"`
    OtherParticipant  *User    `json:"otherParticipant"`
    LastMessage       *Message `json:"lastMessage"`
    HasUnreadMessages bool     `json:"hasUnreadMessages"`
}

就像上面的代码那样,对话中保持对另一个参与者和最后一条消息的引用,还有一个 bool 类型的字段,用来告知是否有未读消息。

type Message struct {
    ID             string    `json:"id"`
    Content        string    `json:"content"`
    UserID         string    `json:"-"`
    ConversationID string    `json:"conversationID,omitempty"`
    CreatedAt      time.Time `json:"createdAt"`
    Mine           bool      `json:"mine"`
    ReceiverID     string    `json:"-"`
}

我们会在下一篇文章介绍与消息相关的内容,但由于我们这里也需要用到它,所以先定义了 Message 结构体。其中大多数字段与数据库表一致。我们需要使用 Mine 来断定消息是否属于当前已验证用户所有。一旦加入实时功能,ReceiverID 可以帮助我们过滤消息。

接下来让我们编写 HTTP 处理程序。尽管它有些长,但也没什么好怕的。

func createConversation(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Username string `json:"username"`
    }
    defer r.Body.Close()
    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    input.Username = strings.TrimSpace(input.Username)
    if input.Username == "" {
        respond(w, Errors{map[string]string{
            "username": "Username required",
        }}, http.StatusUnprocessableEntity)
        return
    }

    ctx := r.Context()
    authUserID := ctx.Value(keyAuthUserID).(string)

    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        respondError(w, fmt.Errorf("could not begin tx: %v", err))
        return
    }
    defer tx.Rollback()

    var otherParticipant User
    if err := tx.QueryRowContext(ctx, `
        SELECT id, avatar_url FROM users WHERE username = $1
    `, input.Username).Scan(
        &otherParticipant.ID,
        &otherParticipant.AvatarURL,
    ); err == sql.ErrNoRows {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    } else if err != nil {
        respondError(w, fmt.Errorf("could not query other participant: %v", err))
        return
    }

    otherParticipant.Username = input.Username

    if otherParticipant.ID == authUserID {
        http.Error(w, "Try start a conversation with someone else", http.StatusForbidden)
        return
    }

    var conversationID string
    if err := tx.QueryRowContext(ctx, `
        SELECT conversation_id FROM participants WHERE user_id = $1
        INTERSECT
        SELECT conversation_id FROM participants WHERE user_id = $2
    `, authUserID, otherParticipant.ID).Scan(&conversationID); err != nil && err != sql.ErrNoRows {
        respondError(w, fmt.Errorf("could not query common conversation id: %v", err))
        return
    } else if err == nil {
        http.Redirect(w, r, "/api/conversations/"+conversationID, http.StatusFound)
        return
    }

    var conversation Conversation
    if err = tx.QueryRowContext(ctx, `
        INSERT INTO conversations DEFAULT VALUES
        RETURNING id
    `).Scan(&conversation.ID); err != nil {
        respondError(w, fmt.Errorf("could not insert conversation: %v", err))
        return
    }

    if _, err = tx.ExecContext(ctx, `
        INSERT INTO participants (user_id, conversation_id) VALUES
            ($1, $2),
            ($3, $2)
    `, authUserID, conversation.ID, otherParticipant.ID); err != nil {
        respondError(w, fmt.Errorf("could not insert participants: %v", err))
        return
    }

    if err = tx.Commit(); err != nil {
        respondError(w, fmt.Errorf("could not commit tx to create conversation: %v", err))
        return
    }

    conversation.OtherParticipant = &otherParticipant

    respond(w, conversation, http.StatusCreated)
}

在此端点,你会向 /api/conversations 发送 POST 请求,请求的 JSON 主体中包含要对话的用户的用户名。

因此,首先需要将请求主体解析成包含用户名的结构。然后,校验用户名不能为空。

type Errors struct {
    Errors map[string]string `json:"errors"`
}

这是错误消息的结构体 Errors,它仅仅是一个映射。如果输入空用户名,你就会得到一段带有 422 Unprocessable Entity(无法处理的实体)错误消息的 JSON 。

{
    "errors": {
        "username": "Username required"
    }
}

然后,我们开始执行 SQL 事务。收到的仅仅是用户名,但事实上,我们需要知道实际的用户 ID 。因此,事务的第一项内容是查询另一个参与者的 ID 和头像。如果找不到该用户,我们将会返回 404 Not Found(未找到) 错误。另外,如果找到的用户恰好和“当前已验证用户”相同,我们应该返回 403 Forbidden(拒绝处理)错误。这是由于对话只应当在两个不同的用户之间发起,而不能是同一个。

然后,我们试图找到这两个用户所共有的对话,所以需要使用 INTERSECT 语句。如果存在,只需要通过 /api/conversations/{conversationID} 重定向到该对话并将其返回。

如果未找到共有的对话,我们需要创建一个新的对话并添加指定的两个参与者。最后,我们 COMMIT 该事务并使用新创建的对话进行响应。

获取对话列表

端点 /api/conversations 将获取当前已验证用户的所有对话。

func getConversations(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    authUserID := ctx.Value(keyAuthUserID).(string)

    rows, err := db.QueryContext(ctx, `
        SELECT
            conversations.id,
            auth_user.messages_read_at < messages.created_at AS has_unread_messages,
            messages.id,
            messages.content,
            messages.created_at,
            messages.user_id = $1 AS mine,
            other_users.id,
            other_users.username,
            other_users.avatar_url
        FROM conversations
        INNER JOIN messages ON conversations.last_message_id = messages.id
        INNER JOIN participants other_participants
            ON other_participants.conversation_id = conversations.id
                AND other_participants.user_id != $1
        INNER JOIN users other_users ON other_participants.user_id = other_users.id
        INNER JOIN participants auth_user
            ON auth_user.conversation_id = conversations.id
                AND auth_user.user_id = $1
        ORDER BY messages.created_at DESC
    `, authUserID)
    if err != nil {
        respondError(w, fmt.Errorf("could not query conversations: %v", err))
        return
    }
    defer rows.Close()

    conversations := make([]Conversation, 0)
    for rows.Next() {
        var conversation Conversation
        var lastMessage Message
        var otherParticipant User
        if err = rows.Scan(
            &conversation.ID,
            &conversation.HasUnreadMessages,
            &lastMessage.ID,
            &lastMessage.Content,
            &lastMessage.CreatedAt,
            &lastMessage.Mine,
            &otherParticipant.ID,
            &otherParticipant.Username,
            &otherParticipant.AvatarURL,
        ); err != nil {
            respondError(w, fmt.Errorf("could not scan conversation: %v", err))
            return
        }

        conversation.LastMessage = &lastMessage
        conversation.OtherParticipant = &otherParticipant
        conversations = append(conversations, conversation)
    }

    if err = rows.Err(); err != nil {
        respondError(w, fmt.Errorf("could not iterate over conversations: %v", err))
        return
    }

    respond(w, conversations, http.StatusOK)
}

该处理程序仅对数据库进行查询。它通过一些联接来查询对话表……首先,从消息表中获取最后一条消息。然后依据“ID 与当前已验证用户不同”的条件,从参与者表找到对话的另一个参与者。然后联接到用户表以获取该用户的用户名和头像。最后,再次联接参与者表,并以相反的条件从该表中找出参与对话的另一个用户,其实就是当前已验证用户。我们会对比消息中的 messages_read_atcreated_at 两个字段,以确定对话中是否存在未读消息。然后,我们通过 user_id 字段来判定该消息是否属于“我”(指当前已验证用户)。

注意,此查询过程假定对话中只有两个用户参与,它也仅仅适用于这种情况。另外,该设计也不很适用于需要显示未读消息数量的情况。如果需要显示未读消息的数量,我认为可以在 participants 表上添加一个unread_messages_count INT 字段,并在每次创建新消息的时候递增它,如果用户已读则重置该字段。

接下来需要遍历每一条记录,通过扫描每一个存在的对话来建立一个 对话切片 slice of conversations 并在最后进行响应。

找到单个对话

端点 /api/conversations/{conversationID} 会根据 ID 对单个对话进行响应。

func getConversation(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    authUserID := ctx.Value(keyAuthUserID).(string)
    conversationID := way.Param(ctx, "conversationID")

    var conversation Conversation
    var otherParticipant User
    if err := db.QueryRowContext(ctx, `
        SELECT
            IFNULL(auth_user.messages_read_at < messages.created_at, false) AS has_unread_messages,
            other_users.id,
            other_users.username,
            other_users.avatar_url
        FROM conversations
        LEFT JOIN messages ON conversations.last_message_id = messages.id
        INNER JOIN participants other_participants
            ON other_participants.conversation_id = conversations.id
                AND other_participants.user_id != $1
        INNER JOIN users other_users ON other_participants.user_id = other_users.id
        INNER JOIN participants auth_user
            ON auth_user.conversation_id = conversations.id
                AND auth_user.user_id = $1
        WHERE conversations.id = $2
    `, authUserID, conversationID).Scan(
        &conversation.HasUnreadMessages,
        &otherParticipant.ID,
        &otherParticipant.Username,
        &otherParticipant.AvatarURL,
    ); err == sql.ErrNoRows {
        http.Error(w, "Conversation not found", http.StatusNotFound)
        return
    } else if err != nil {
        respondError(w, fmt.Errorf("could not query conversation: %v", err))
        return
    }

    conversation.ID = conversationID
    conversation.OtherParticipant = &otherParticipant

    respond(w, conversation, http.StatusOK)
}

这里的查询与之前有点类似。尽管我们并不关心最后一条消息的显示问题,并因此忽略了与之相关的一些字段,但是我们需要根据这条消息来判断对话中是否存在未读消息。此时,我们使用 LEFT JOIN 来代替 INNER JOIN,因为 last_message_id 字段是 NULLABLE(可以为空)的;而其他情况下,我们无法得到任何记录。基于同样的理由,我们在 has_unread_messages 的比较中使用了 IFNULL 语句。最后,我们按 ID 进行过滤。

如果查询没有返回任何记录,我们的响应会返回 404 Not Found 错误,否则响应将会返回 200 OK 以及找到的对话。


本篇帖子以创建了一些对话端点结束。

在下一篇帖子中,我们将会看到如何创建并列出消息。


via: https://nicolasparada.netlify.com/posts/go-messenger-conversations/

作者:Nicolás Parada 选题:lujun9972 译者:PsiACE 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

如果你是一名系统管理员或者开发者,当你在终端工作时有时会需要编辑一个文件。在 Linux 系统中有几种文件编辑器,你可以根据需求选择合适的文件编辑器。在这里,我想推荐 Vim 编辑器。

为什么推荐 Vim 编辑器

相对于创建新文件,你更多是修改已经存在的文件。在这种情况下,Vim 快捷键可以有效地满足你的需求。

下列文章可以帮助你了解对文件和目录的操作。

什么是 Vim

Vim 是被 Linux 管理员和开发者广泛使用的最流行和功能强大的编辑器之一。它可以通过高度的自定义配置来提高文本编辑效率。它是在众多 Unix 默认安装的 Vi 编辑器的升级版。

Vim 通常被称为“程序员的编辑器”,但并不限于此,它也可用于编辑任何类型的文件。它具有许多功能,例如:多次撤销、多窗口和缓冲区、语法高亮、命令行编辑、文件名补全、可视选择等等。你可以使用 :help 命令来获取在线帮助。

理解 Vim 的模式

Vim 有两种模式,详细介绍如下:

命令模式: 当启动 Vim 编辑器后,默认处在命令模式下。你可以在文件中移动并且修改内容,剪切、复制和粘贴文件的一部分,同时发出命令执行更多操作(按 ESC 键进入命令模式)

插入模式: 插入模式用于在给定的文档位置插入文本(按 i 键进入插入模式)

我如何知道我正使用哪种 Vim 模式呢?

如果你正在使用插入模式,你会在编辑器的底部看到 INSERT。如果编辑器底部没有显示任何内容,或者在编辑器底部显示了文件名,则处于 “命令模式”。

命令模式下的光标移动

Vim 快捷键允许你使用不同的方式来移动光标:

  • G – 跳转到文件最后一行
  • gg – 跳转到文件首行
  • $ – 跳转到行末尾
  • 0(数字 0) – 跳转到行开头
  • w – 跳转到下一个单词的开始(单词的分隔符可以是空格或其他符号)
  • W – 跳转到下一个单词的开始(单词的分隔符只能是空格)
  • b – 跳转到下一个单词的末尾(单词的分隔符可以是空格或其他符号)
  • B – 跳转到下一个单词的末尾(单词的分隔符只能是空格)
  • PgDn 键 – 向下移动一页
  • PgUp 键 – 向上移动一页
  • Ctrl+d – 向下移动半页
  • Ctrl+u – 向上移动半页

插入模式:插入文字

下面的 Vim 快捷键允许你根据需要在光标的不同位置插入内容。

  • i – 在光标之前插入
  • a – 在光标之后插入
  • I – 在光标所在行的开头插入。当光标位于行中间时,这个键很有用
  • A – 在光标所在行的末尾插入。
  • o – 在光标所在行的下面插入新行
  • O – 在光标所在行的上面插入新行
  • ea – 在单词的末尾插入

拷贝、粘贴和删除一行

  • yy – 复制一行
  • p / P – 将内容粘贴到光标之后 / 之前
  • dd – 删除一行
  • dw – 删除一个单词

在 Vim 中搜索和替换匹配的模式

  • /模式 – 向后搜索给定的模式
  • ?模式 – 向前搜索给定的模式
  • n – 向后重复搜索之前给定的模式
  • N – 向前重复搜索之前给定的模式
  • :%s/旧模式/新模式/g – 将文件中所有的旧模式替换为新模式
  • :s/旧模式/新模式/g – 将当前行中所有的旧模式替换为新模式
  • :%s/旧模式/新模式/gc – 逐个询问是否文件中的旧模式替换为新模式

如何在 Vim 编辑器中跳转到特定行

你可以根据需求以两种方式达到该目的,如果你不知道行号,建议采用第一种方法。

通过打开文件并运行下面的命令来显示行号

:set number

当你设置好显示行号后,按 :n 跳转到相应的行号。例如,如果你想跳转到第 15 行,请输入:

:15

如果你已经知道行号,请使用以下方法在打开文件时直接跳转到相应行。例如,如果在打开文件时直接跳转到 20 行,请输入下面的命令:

$ vim +20 [文件名]

撤销操作/恢复上一次操作/重复上一次操作

  • u – 撤销更改
  • Ctrl+r – 恢复更改
  • . – 重复上一条命令

保存和退出 Vim

  • :w – 保存更改但不退出 vim
  • :wq – 写并退出
  • :q! – 强制退出

via: https://www.2daygeek.com/basic-vim-commands-cheat-sheet-quick-start-guide/

作者:Magesh Maruthamuthu 选题:lujun9972 译者:萌新阿岩 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

一份优质的文档可以让很多用户对你的项目路人转粉。

好的代码很多时候并不代表一切。或许你能用最精巧的代码解决了世界上最迫切需要解决的问题,但如果你作为一个开源开发者,没能用准确的语言将你的作品公之于世,你的代码也只能成为沧海遗珠。因此,技术写作和文档编写是很重要的技能。

一般来说,项目中的文档是最受人关注的部分,很多用户会通过文档来决定自己是否应该对某个项目开始学习或研究。所以,我们不能忽视技术写作和文档编写的工作,尤其要重点关注其中的“ 入门 Getting Started ”部分,这会对你项目的发展起到关键性的作用。

对于很多人来说,写作是一件令人厌烦甚至恐惧的事情。我们这些工程师出身的人,更多学习的是“写代码”而不是学习“为代码写文档”。不少人会把英语作为自己的第二语言或者第三语言,他们可能会对英语写作感到不自信甚至害怕(我的母语是汉语,英语是作为我的第二语言学习的,所以我也能感受到这种痛苦)。

但如果你希望自己的项目能在全球范围内产生一定的影响力,英语就是你必须使用的语言,这是一个无法避免的现实。但不必害怕,我在写这篇文章的时候就考虑到了这些可能带来的挑战,并给出了我的一些建议。

五条有用的写作建议

这五条建议你马上就可以用起来,尽管看起来似乎有些浅显,但在技术写作时却经常被忽视。

  1. 使用主动语态:感受一下主动语态下的“你可以这样更改配置(You can change these configurations by…)”和被动语态下的“配置可以这样更改(These configurations can be changed by…)”有什么不同之处。
  2. 使用简洁明了的句子:可以借助 Hemingway App 或者 Grammarly 这样的工具,尽管它们并不开源。
  3. 保持条理性:你可以在文档中通过写标题、划重点、引链接等方式,把各类信息划分为不同的部分,避免将所有内容都杂糅在一大段冗长的文字当中。
  4. 提高可读性:除了单纯的文字之外,运用图表也是从多种角度表达的手段之一。
  5. 注意拼写和语法:必须记得检查文档中是否有拼写错误或者语法错误。

只要在文档的写作和编辑过程中应用到这些技巧,你就能够和读者建立起沟通和信任。

  • 高效沟通:对于工程师们来说,阅读长篇大论的冗长文字,还不如去看小说。在阅读技术文档时,他们总是希望能够从中快速准确地获取到有用的信息。因此,技术文档的最佳风格应该是精简而有效的,不过这并不代表文档中不能出现类似幽默、emoji 甚至段子这些东西,这些元素可以当你的文档更有个性、更使人印象深刻。当然,具体的实现方式就因人而异了
  • 建立信任:你需要取得文档读者们的信任,这在一个项目的前期尤为重要。读者对你的信任除了来源于你代码的质量,还跟你文档编写的质量有关。所以你不仅要打磨代码,还要润色好相关的文档,这也是上面第 5 点建议拼写和语法检查的原因。

从编写“入门”文档开始

现在,最需要花费功夫的应该就是“入门”部分了,这是一篇技术文档最重要的部分,二八定律在这里得到了充分体现:访问一个项目的大部分流量都会落在项目文档上,而访问项目文档的大部分流量则会落在文档的“入门”部分中。因此,如果文档的“入门”部分写得足够好,项目就会吸引到很多用户,反之,用户会对你的项目敬而远之。

那么如何写好“入门”部分呢?我建议按照以下三步走:

  1. 任务化:入门指南应该以任务为导向。这里的任务指的是对于开发者来说可以完成的离散的小项目,而不应该包含太多涉及到体系结构、核心概念等的抽象信息,因此在“入门”部分只需要提供一个简单明了的概述就可以了。也不要在“入门”部分大谈这个项目如何优秀地解决了问题,这个话题可以放在文档中别的部分进行说明。总而言之,“入门”部分最好是给出一些主要的操作步骤,这样显得开门见山。
  2. 30 分钟内能够完成:这一点的核心是耗时尽可能短,不宜超过 30 分钟,这个时间上限是考虑到用户可能对你的项目并不了解。这一点很重要,大部分愿意浏览文档的人都是有技术基础的,但对你的项目也仅仅是一知半解。首先让这些读者尝试进行一些相关操作,在收到一定效果后,他们才会愿意花更多时间深入研究整个项目。因此,你可以从耗时这个角度来评估你的文档“入门”部分有没有需要改进之处。
  3. 有意义的任务:这里“有意义”的含义取决于你的开源项目。最重要的是认真思考并将“入门”部分严格定义为一项任务,然后交给你的读者去完成。这个项目的价值应该在这项有意义的任务中有所体现,不然读者可能会感觉这是一个浪费时间的行为。

提示:假如你的项目是一个分布式数据库,那么达到“整个集群在某些节点故障的情况下可以不中断地保持可用”的目标就可以认为是“有意义”的;假如你的项目是一个数据分析工具或者是商业智能工具,“有意义”的目标也可以是“加载数据后能快速生成多种可视化效果的仪表板”。总之,无论你的项目需要达到什么“有意义”的目标,都应该能在笔记本电脑上本地快速实现。

Linkerd 入门就是一个很好的例子。Linkerd 是一个开源的 Kubernetes 服务网格 Service Mesh ,当时我对 Kubernetes 了解并不多,也不熟悉服务网格。但我在自己的笔记本电脑上很轻松地就完成了其中的任务,同时也加深了对服务网格的理解。

上面提到的三步过程是一个很有用的框架,对一篇文档“入门”部分的设计和量化评估很有帮助。今后你如果想将你的开源项目产品化,这个框架还可能对 实现价值的时间 time-to-value 产生影响。

其它核心部分

认真写好“入门”部分之后,你的文档中还需要有这五个部分:架构设计、生产环境使用指导、使用案例、参考资料以及未来展望,这五个部分在一份完整的文档中是必不可少的。

  • 架构设计:这一部分需要深入探讨整个项目架构设计的依据,“入门”部分中那些一笔带过的关键细节就应该在这里体现。在产品化过程中,这个部分将会是产品推广计划的核心,因此通常会包含一些可视化呈现的内容,期望的效果是让更多用户长期参与到项目中来。
  • 生产环境使用指导:对于同一个项目,在生产环境中部署比在笔记本电脑上部署要复杂得多。因此,指导用户认真使用就尤为重要。同时,有些用户可能对项目很感兴趣,但对生产环境下的使用有所顾虑,而指导和展示的过程则正好能够吸引到这类潜在的用户。
  • 使用案例 社会认同 social proof 的力量是有目共睹的,所以很有必要列出正在生产环境使用这个项目的其他用户,并把这些信息摆放在显眼的位置。这个部分的浏览量甚至仅次于“入门”部分。
  • 参考资料:这个部分是对项目的一些详细说明,让用户得以进行详细的研究以及查阅相关信息。一些开源作者会在这个部分事无巨细地列出项目中的每一个细节和 边缘情况 edge case ,这种做法可以理解,但不推荐在项目初期就在这个部分花费过多的时间。你可以采取更折中的方式,在质量和效率之间取得平衡,例如提供一些相关社区的链接、Stack Overflow 上的标签或单独的 FAQ 页面。
  • 未来展望:你需要制定一个简略的时间表,规划这个项目的未来发展方向,这会让用户长期保持兴趣。尽管项目在当下可能并不完美,但要让用户知道你仍然有完善这个项目的计划。这个部分也能让整个社区构建一个强大的生态,因此还要向用户提供表达他们对未来展望的看法的交流区。

以上这几个部分或许还没有在你的文档中出现,甚至可能会在后期才能出现,尤其是“使用案例”部分。尽管如此,还是应该在文档中逐渐加入这些部分。如果用户对“入门”部分已经感觉良好,那以上这几个部分将会提起用户更大的兴趣。

最后,请在“入门”部分、README 文件或其它显眼的位置注明整个项目所使用的许可证。这个细节会让你的项目更容易通过最终用户的审核。

花 20% 的时间写作

一般情况下,我建议把整个项目 10% 到 20% 的时间用在文档写作上。也就是说,如果你是全职进行某一个项目的,文档写作需要在其中占半天到一天。

再细致一点,应该将写作纳入到常规的工作流程中,这样它就不再是一件孤立的琐事,而是日常的事务。文档写作应该随着工作进度同步进行,切忌将所有写作任务都堆积起来最后完成,这样才可以帮助你的项目达到最终目标:吸引用户、获得信任。


特别鸣谢云原生计算基金会的布道师 Luc Perkins 给出的宝贵意见。

本文首发于 COSS Media 并经许可发布。


via: https://opensource.com/article/20/3/documentation

作者:Kevin Xu 选题:lujun9972 译者:HankChow 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

跟随接下来的介绍,自己搭建一个三节点的 Kubernetes 集群。

我对在树莓派上搭建 Kubernetes 集群已经感兴趣很长时间了,只要照着网上的教程,我可以在由三个树莓派组成的集群中搭建一套 Kubernetes 并正常运行。但在这种情况下,主节点上的内存和 CPU 资源捉襟见肘,执行 Kubernetes 任务的时候往往性能不佳,想要升级 Kubernetes 就更不可能了。

这个时候,我非常激动地发现了 K3s 这个项目。K3s 被誉为在可用于资源受限环境下的轻量级 Kubernetes,它还针对 ARM 处理器做出了优化,这让 Kubernetes 集群可以在树莓派上运行得更好。在下文中,我们将会使用 K3s 来创建一个 Kubernetes 集群。

准备

要按照本文介绍的方式创建 Kubernetes 集群,首先需要准备:

  • 至少一个树莓派(包括 SD 卡和电源)
  • 网线
  • 将所有树莓派连接到一起的交换机或路由器

我们会通过在线安装的方式安装 K3s,因此还需要可以连接到互联网。

集群概览

在这个集群里,我们会使用三个树莓派。其中一个树莓派作为主节点,我们将它命名为 kmaster,并为其分配一个静态 IP 192.168.0.50(注:假设使用的私有网段是 192.168.0.0/24),而另外两个树莓派作为工作节点,分别命名为 knode1knode2,也分别分配 192.168.0.51192.168.0.52 两个 IP 地址。

当然,如果你实际的网络布局和上面不同,只要将文中所提及到的 IP 替换成你实际可用的 IP 就可以了。

为了不需要通过 IP 来引用某一个节点,我们将每个节点的主机名记录到 PC 的 /etc/hosts 文件当中:

echo -e "192.168.0.50\tkmaster" | sudo tee -a /etc/hosts
echo -e "192.168.0.51\tknode1" | sudo tee -a /etc/hosts
echo -e "192.168.0.52\tknode2" | sudo tee -a /etc/hosts

部署主节点

我们首先部署主节点。最开始的步骤当然是使用镜像安装最新的 Raspbian,这个步骤可以参考我的另一篇文章,在这里就不展开介绍了。在安装完成之后,启动 SSH 服务,将主机名设置为 kmaster,然后分配静态 IP 192.168.0.50

在主节点上安装 Raspbian 完成后,启动树莓派并通过 ssh 连接上去:

ssh pi@kmaster

在主节点上执行以下命令安装 K3s:

curl -sfL https://get.k3s.io | sh -

等到命令跑完以后,一个单节点集群就已经运行起来了。让我们检查一下,还在这个树莓派上执行:

sudo kubectl get nodes

就会看到这样的输出:

NAME     STATUS   ROLES    AGE    VERSION
kmaster  Ready    master   2m13s  v1.14.3-k3s.1

获取 连接令牌 join token

之后我们需要部署工作节点。在工作节点上安装 K3s 的时候,会需要用到连接令牌,它放置在主节点的文件系统上。首先把连接令牌保存出来以便后续使用:

sudo cat /var/lib/rancher/k3s/server/node-token

部署工作节点

通过 SD 卡在每个作为工作节点的树莓派上安装 Raspbian。在这里,我们把其中一个树莓派的主机名设置为 knode1,为其分配 IP 地址 192.168.0.51,另一个树莓派的主机名设置为 knode2,分配 IP 地址 192.168.0.52。接下来就可以安装 K3s 了。

启动主机名为 knode1 的树莓派,通过 ssh 连接上去:

ssh pi@knode1

在这个树莓派上,安装 K3s 的过程和之前差不多,但需要另外加上一些参数,表示它是一个工作节点,需要连接到一个已有的集群上:

curl -sfL http://get.k3s.io | K3S_URL=https://192.168.0.50:6443 \
K3S_TOKEN=刚才保存下来的连接令牌 sh -

K3S_TOKEN 的值需要替换成刚才保存下来的实际的连接令牌。完成之后,在主机名为 knode2 的树莓派上重复这个安装过程。

通过 PC 访问集群

现在如果我们想要查看或者更改集群,都必须 ssh 到集群的主节点才能使用 kubectl,这是比较麻烦的。因此我们会将 kubectl 放到 PC 上使用。首先,在主节点上获取一些必要的配置信息,sshkmaster 上执行:

sudo cat /etc/rancher/k3s/k3s.yaml

复制上面命令的输出,然后在你的 PC 上创建一个目录用来放置配置文件:

mkdir ~/.kube

将复制好的内容写入到 ~/.kube/config 文件中,然后编辑该文件,将

server: https://localhost:6443

改为

server: https://kmaster:6443

出于安全考虑,只对自己保留这个配置文件的读写权限:

chmod 600 ~/.kube/config

如果 PC 上还没有安装 kubectl 的话,就可以开始安装了。Kubernetes 官方网站上有各种平台安装 kubectl方法说明,我使用的是 Ubuntu 的衍生版 Linux Mint,所以我的安装方法是这样的:

sudo apt update && sudo apt install -y apt-transport-https
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee -a /etc/apt/sources.list.d/kubernetes.list
sudo apt update && sudo apt install kubectl

上面几个命令的作用是添加了一个包含 Kubernetes 的 Debian 软件库,获取 GPG 密钥以确保安全,然后更新软件包列表并安装 kubectl。如果 kubectl 有更新,我们将会通过 标准软件更新机制 standard software update mechanism 收到通知。

现在在 PC 上就可以查看 Kubernetes 集群了:

kubectl get nodes

输出大概会是这样:

NAME     STATUS  ROLES   AGE   VERSION
kmaster  Ready   master  12m   v1.14.3-k3s.1
knode1   Ready   worker  103s  v1.14.3-k3s.1
knode1   Ready   worker  103s  v1.14.3-k3s.1

至此,我们已经搭建了一个三节点的 Kubernetes 集群。

K3s 的彩蛋

如果执行 kubectl get pods --all-namespaces,就会看到其它服务的一些 Pod,比如 Traefik。Traefik 在这里起到是反向代理和负载均衡器的作用,它可以让流量从单个入口进入集群后引导到集群中的各个服务。Kubernetes 支持这种机制,但 Kubernetes 本身不提供这个功能,因此 Traefik 是一个不错的选择,K3s 安装后立即可用的优点也得益于此。

在后续的文章中,我们会继续探讨 Traefik 在 Kubernetes ingress 中的应用,以及在集群中部署其它组件。敬请关注。


via: https://opensource.com/article/20/3/kubernetes-raspberry-pi-k3s

作者:Lee Carpenter 选题:lujun9972 译者:HankChow 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

开源硬件解决方案可以为抵御新型冠状病毒的传播和痛苦做出贡献。

开源硬件运动长期以来一直主张维修权的重要性,完全拥有所购买的技术,并能够像音乐一样重新组合和复制部件。因此,在这个充满挑战的时期内,开源硬件为由冠状病毒大流行引起的一些问题提供了一些答案。

背景概述

首先,全球的硬件开发人员正在努力使用开源解决供应链中的弱点,在过去 30 年中,这种理念推动了新软件技术的大量涌现。过去在硬件运动方面的成功,如 RepRap ProjectOpen Source EcologyOpen Source Beehives,证明了这一点是可以做到的。

对于使用 3D 打印和其他技术按需生产安全设备和按需制造它的替换零件,创客们的兴趣日益增加。例如,香港理工大学实验室为医院工作人员提供 3D 打印面罩。意大利初创公司 Isinnova 与米兰 FabLab 合作,为受重灾的意大利北部提供用于呼吸机的 3D 打印替换阀。公司们还发布了设计以适应我们的物理接触需求,例如 Materialise 的 3D 打印免提开门器。这些更换零件和解决问题的示例是一个很好的起点,为挽救生命做出了努力。

另一种传统的硬件技术正在加速发展:缝纫。法新社报道说,全球急需口罩,来自世界卫生组织的指导也指明了其重要性。随着一次性口罩要优先供给于医护人员,捷克共和国的人们开始缝制自己的口罩。(重复使用的口罩确实会带来细菌问题。)Facebook 小组“捷克缝制口罩”开始在他们的国家解决这个问题,成千上万的成员开始用起了他们的家用缝纫机。

开源硬件设备和机械项目也越来越受欢迎。首先,有一些测试性设备具备高精度且功能强大。其次,在没有其他选择方案的情况下,有一些医疗设备(顶多)可以归类为现场级。这些项目将在下面详细概述。

为了解更多信息,我与总部位于芝加哥的 Tapster Robotics 的创始人兼首席执行官 Jason Huggins 进行了交谈。Tapster Robotics 使用 3D 打印、计算机数控(CNC)加工和 Arduino 等开源电子产品设计和制造台式机器人。他兼具技术知识和工业能力,具有很高的影响力。他想投入自己公司的资源来帮助这场斗争。

“基本上,我们现在正处于第二次世界大战的动员时刻。即使我不是医生,我们仍然应该遵循希波克拉底誓言。无论我做什么,我都不想让问题变得更糟”,Huggins 解释,他认为:“作为对策,世卫组织执行主任 Michael Ryan 博士发表了这样的评论:‘速度胜过完美’。”

哇!

这个人是疾病传播的全球权威。如果您是领导者(无论以何种身份),请注意。如果不是,也请注意。

pic.twitter.com/bFogaekehM

— Jim RichardsSh?wgram(@JIMrichards1010)2020 年 3 月 15 日

Huggins 在应需提供方面具有丰富的经验。他的努力有助于 Healthcare.gov 在挑战性的最初启动后得以扩展。他还创建了软件行业标准的测试框架 Selenium 和 Appium。有了这一经验,他的建议非常值得考虑。

我还与 Tyson Law 的西雅图律师 Mark Tyson 进行了交谈,他的合作对象是初创公司和小型企业。他在快速发展的行业中与敏捷公司合作有着直接的经验。在阐述整个问题时,Tyson 说到:

善良的撒玛利亚人法 Good Samaritan law 》保护志愿者(即“好撒玛利亚人”)免于因其在紧急情况下提供援助的决定而承担责任。尽管这些法律的具体内容因州而异,但它们具有共同的公共政策依据:即鼓励旁观者帮助遇到紧急情况的其他人。可以想象,除了传统的把车祸的受害者从伤害中拉出来之外,这种理论依据可以证明在不太传统的环境中应用这类法律的合理性。

对于这种特定情况,Tyson 指出:

“在采取行动之前,创客们明智的做法是与律师沟通一下,以针对特定的州进行风险评估。还应谨慎地要求大型机构(例如医院或保险公司)通过合同接受潜在的责任风险,例如,通过使用赔偿协议,使医院或其保险人同意赔偿创客们的责任。”

Tyson 明白情况的紧迫性和严重性。使用合同的这种选择并不意味着障碍。相反,这可能是一种帮助其大规模采用的方法,以更快地产生更大的变化。这取决于你或你的机构。

综上所述,让我们探索正在使用或正在开发中的项目(可能很快就可以部署)。

7 个与新冠病毒对抗的开源硬件项目

Opentrons

Opentrons 的开源实验室自动化平台由一套开源硬件、经过验证的实验室器具、消耗品、试剂和工作站组成。Opentrons 表示,其产品可以“在下订单后几天内每天自动进行多达 2400 个测试”的系统,可以极大地帮助提高新冠病毒测试规模。它计划在 7 月 1 日之前提升到多达 100 万个测试样本。

 title=

来自 Opentrons 网站,版权所有

该公司已经在与联邦和地方政府机构合作,以确定其系统是否可以在紧急使用授权下用于临床诊断。 Opentrons 在 Apache 2.0 许可证下共享。我最初是从与该项目有联系的生物学家 Kristin Ellis 那里得知它的。

Chai 的 Open qPCR

Chai 的 Open qPCR 设备使用聚合酶链反应(PCR)快速检测物品表面(例如,门把手和电梯按钮)上的拭子,以查看是否存在新型冠状病毒。这种在 Apache 2.0 许可证下共享的开源硬件使用 BeagleBone 低功耗 Linux 计算机。Chai 的 Open qPCR 提供的数据可以使公共卫生、公民和企业领导者做出有关清洁、缓解、关闭设施、接触追踪和测试的更明智的决策。

OpenPCR

OpenPCR 是 Chai Open qPCR 的创建者 Josh Perfetto 和 Jessie Ho 的 PCR 测试设备套件。与他们的前一个项目相比,这更像是一种 DIY 开源设备,但它具有相同的使用场景:使用环境测试来识别野外冠状病毒。正如该项目页面所指出的那样,“能够检测这些病原体的传统实时 PCR 设备通常花费超过 30,000 美元,而且不适合在现场使用。”由于 OpenPCR 是用户构建的工具包,并且在 GPLv3.0 许可证下共享,因此该设备旨在使分子诊断的访问大众化。

 title=

来自 OpenPCR 网站,版权所有

而且,就像任何优秀的开源项目一样,它也有一个衍生产品!瑞士的 Gaudi Labs 推出的 WildOpenPCR 也以 GPLv3.0 许可证共享。

PocketPCR

Gaudi Labs 的 PocketPCR 热循环仪可通过升高和降低小试管中液体的温度来激活生物反应。它可以通过简单的 USB 电源适配器供电,该适配器可以绑定到设备上,也可以单独使用,不使用计算机或智能手机时可使用预设参数。

 title=

来自 PocketPCR 网站,版权所有

与本文所述的其他 PCR 产品一样,此设备可能有助于对冠状病毒进行环境测试,尽管其项目页面并未明确说明。PocketPCR 在 GPLv3.0 许可证下共享。

Open Lung 低资源呼吸机

Open Lung 低资源呼吸机是一种快速部署的呼吸机,它以气囊阀罩(BVM)(也称为 Ambu 气囊)为核心组件。Ambu 气囊已批量生产,经过认证,体积小,机械简单,并且适用于侵入性导管和口罩。 Open Lung 呼吸机使用微电子技术来感测和控制气压和流量,以实现半自主运行。

 title=

Open Lung,GitLab

这个早期项目拥有一支由数百名贡献者组成的大型团队,领导者包括:Colin Keogh、David Pollard、Connall Laverty 和 Gui Calavanti。它是以 GPLv3.0 许可证共享的。

Pandemic 呼吸机

Pandemic 呼吸机是 DIY 呼吸机的原型。像 RepRap 项目一样,它在设计中使用了常用的硬件组件。该项目已由用户 Panvent 于 10 多年前上传到 Instructables,并且有六个主要的生产步骤。该项目是以 CC BY-NC-SA 许可证共享的。

Folding at Home

Folding at Home 是一个分布式计算项目,用于模拟蛋白质动力学,包括蛋白质折叠的过程以及与多种疾病有关的蛋白质运动。这是一个面向公民科学家、研究人员和志愿者的行动呼吁,类似于退役的 SETI@Home 项目使用家中的计算机来运行解码计算。如果你是具备强大计算机硬件功能的技术人员,那么这个项目适合你。

 title=

Vincent Voelz,CC BY-SA 3.0

Folding at Home 项目使用马尔可夫状态模型(如上所示)来建模蛋白质可能采取的形状和折叠途径,以寻找新的治疗机会。你可以在华盛顿大学生物物理学家 Greg Bowman 的帖子《它是如何运作的以及如何提供帮助》中找到有关该项目的更多信息。

该项目涉及来自许多国家和地区(包括香港、克罗地亚、瑞典和美国)的财团的学术实验室、贡献者和公司赞助者。 在 GitHub 上,在混合了 GPL 和专有许可证下共享,并且可以在 Windows、macOS 和 GNU/Linux(例如 Debian、Ubuntu、Mint、RHEL、CentOS、Fedora)。

许多其他有趣的项目

这些项目只是在开源硬件领域中解决或治疗新冠病毒活动中的一小部分。在研究本文时,我发现了其他值得探索的项目,例如:

这些项目遍布全球,而这种全球合作正是我们所需要的,因为病毒无视国界。新冠病毒大流行在不同时期以不同方式影响国家,因此我们需要一种分布式方法。

正如我和同事 Steven Abadie 在 OSHdata 2020 报告中所写的那样,开源硬件运动是全球性运动。参与该认证项目的个人和组织遍布全球 35 个国家地区和每个半球。

 title=

OSHdata,CC BY-SA 4.0 国际版

如果你有兴趣加入这场与全球开源硬件开发人员的对话,请加入开源硬件峰会的 Discord 服务器,并通过专用渠道进行有关新冠病毒的讨论。你在这里可以找到机器人专家、设计师、艺术家、固件和机械工程师、学生、研究人员以及其他共同为这场战争而战的人。希望可以看到你。


via: https://opensource.com/article/20/3/open-hardware-covid19

作者:Harris Kenny 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

以我的观点,每个人都该有一个 Linux 发行版,不管他们喜欢的是哪种,或者想用来做什么。

要是你刚刚开始使用 Linux,你可以使用这些最适合于初学者的 Linux 发行版;要是你是从 Windows 投奔过来的,你可以使用这些看起来像 Windows 一样的 Linux 发行版;要是你有台旧电脑想让它发挥余热,你可以使用这些轻量级 Linux 发行版。当然,无论你是在企业还是 SOHO,Linux 还可以用于个人工作开发,以及作为服务器嵌入式系统的操作系统,它可以用于各种用途

但在这个列表中,我们只关注最漂亮的 Linux 发行版。

7 款最漂亮的 Linux 发行版

等一下!Linux 发行版有漂亮这个分类吗?显然,你可以自己定制任何发行版的外观,并使用主题图标之类的来使其看起来更漂亮。

你说得对。但是在这里,我所说的是指不需要用户进行任何调整和自定义工作就看起来很漂亮的发行版。这些发行版提供友好的顺滑、流畅、开箱即用的桌面体验。

注意: 以下列表排名不分前后,名单纯属个人喜好,请使劲喷你的观点。

1、elementary OS

首先踏上 T 台的 elementary OS 是最漂亮的 Linux 发行版(如果非得说的话,可以是“之一”)。它靠着一种类似 macOS 的外观,而为 Linux 用户提供了出色的体验。如果你已经适应了 macOS,那么你在使用 elementary OS 时就不会有任何不适感。

此外,elementary OS 是基于 Ubuntu 的,因此,你可以很容易地找到大量的可以满足你的需要的应用程序。

不仅仅是外观和感受,elementary OS 总是努力地引入一些重要的变化。因此,你可以期待用户体验会随着每次更新而获得提升。

2、深度 Linux

深度 Linux(或许现在该叫统信 UOS?)是另外一个漂亮的 Linux 发行版,它起源于 Debian 的稳定版分支。对于某些人来说,其动画效果(外观和感受)可能有点过度了,虽然它看起来很漂亮。

它有它自己特色的 Deepin 桌面环境(DDE),蕴藏了多种基本功能,从而提供尽可能好的用户体验。它可能不像其它发行版的用户界面,但你很容易就可以习惯。

我个人对深度中的控制中心和特色配色方案印象深刻。你可以自己尝试一下,我觉得值得一试。

3、Pop!\_OS

Pop!\_OS 在提供了纯净的 GNOME 体验的同事,也成功地在 Ubuntu 之上打造出了一个出色的用户界面。

这正是我的最爱,我使用它作为我的主要桌面系统。Pop!\_OS 既不浮华,也没有一些花哨的动画。然而,它通过图标和主题的完美组合做到了最好,同时从技术角度提升了用户体验。

我不想发起一场 Ubuntu 和 Pop!\_OS 之间的争论,但是如果你已经习惯了 Ubuntu,Pop!\_OS 可能是一个可能更好的用户体验的绝佳选择。

4、Manjaro Linux

Manjaro Linux 是一个基于 Arch 的 Linux 发行版。虽然安装 Arch Linux 是一件稍微复杂的工作,而 Manjaro 则提供了一种更舒适、更流畅的 Arch 体验。

它提供各种各样的桌面环境版本,在下载时可供选择。不管你选择哪一个,你都仍然有足够的选择权来自定义外观和感觉或布局。

对我来说,这样一个开箱即用的、基于 Arch 的发行版看起来棒极了,你值得拥有!

5、KDE Neon

KDE Neon 是为那些想要采用简化的设计方案而仍可以得到丰富体验的用户所准备的。

它是一个基于 Ubuntu 的轻量级 Linux 发行版。顾名思义,它采用 KDE Plasma 桌面,看起来精美绝伦。

KDE Neon 给予你最新的、最好的 KDE Plasma 桌面及 KDE 应用程序。不像 Kubuntu 或其它基于 KDE 的发行版,你不需要等待数月来获取新的 KDE 软件

你可以在 KDE 桌面中获取很多内置的自定义选项,请按你的心意摆弄它!

6、Zorin OS

毫无疑问,Zorin OS 是一款令人印象深刻的 Linux 发行版,它努力提供了良好的用户体验,即便是它的精简版也是如此。

你可以尝试完整版或精简版(使用 Xfce 桌面)。这个用户界面是专门为习惯于 Windows 和 macOS 的用户定制的。虽然它是基于 Ubuntu 的,但它仍然提供了出色的用户体验。

如果你开始喜欢上了它的用户界面,那你也可以尝试使用 Zorin Grid 来管理运行在工作区/家庭中的 Zorin OS 计算机。而使用其终极版,你还可以控制桌面布局(如上图所示)。

7、Nitrux OS

Nitrux OS 在 Linux 发行版里算是个另类,它某种程度上基于 Ubuntu,但是不完全基于 Ubuntu 。

对于那些正在寻找在 Linux 发行版上使用全新方式的独特设计语言的用户来说,它专注于为他们提供一种良好的用户体验。它使用基于 KDE 的 Nomad 桌面。

Nitrux 鼓励使用 AppImage 应用程序。但是在这个基于 Ubuntu 的 Nitrux 中你也可以使用 Arch Linux 的 pacman 软件包管理器。真令人惊叹!

如果它不是你安装过(或尚未安装过)的完美的操作系统,但它确实看起来很漂亮,并且对大多数基本任务来说已经足够了。你可以阅读我们的 Nitrux 创始人的采访,可以更多地了解它。

这是一个稍微过时的 Nitrux 视频,但是它仍然看起来很好:

赠品:eXtern OS (处于‘停滞’开发阶段)

如果你想尝试一个实验性的 Linux 发行版,extern OS 是非常漂亮的。

它没有积极维护,因此不应该用于生产系统。但是,它提供独特的用户体验,尽管还不够完美无缺。

如果只是想尝试一个好看的 Linux 发行版,你可以试试。

总结

俗话说,情人眼里出西施。所以这份来自我眼中的最漂亮 Linux 发行版列表。你可以随意提出不同的意见 (当然要礼貌一点),并提出你最喜欢的 Linux 发行版。


via: https://itsfoss.com/beautiful-linux-distributions/

作者:Ankush Das 选题:lujun9972 译者:robsean 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出