标签归档:开发经验

a computer screen with a drawing of two people talking to each other

Solution as a Service, not Software as a Service

在软件领域,我们有非常经典的 IaaS、PaaS、SaaS 模型,我们使用这个模型定义着我们的产品。

但另一方面,这个定义也局限了很多人的想法 —— SaaS just Software as a Service。

实际上,如果你是一个独立开发者 / 直面用户的岗位,你需要深刻的明白 —— SaaS 更大的价值不应该是 Software ,而应该是 Solution —— 用户从来不关心你是不是有 Software,用户只关心你的 Solution 能不能解决你的问题。

如果你不知道你的 Software 是在解决谁的问题 —— 不妨想想,是不是你没有找到你的Software 到底应该如何放在 Solution 当中 ,以及那个 Solution 对应的问题到底是什么。

持续关注用户的问题,尝试提供靠谱的 Solution 去解决他的问题 —— 而不是关注你的 Software。除了你,没有人真正在意你的 Software。

brown and black round board

OKR 要长远,但迭代要敏捷

飞书执行季 OKR 已经很久了,相比于过去的双月 OKR,我认为这确实是一个好的事情。季度 OKR 可以让我们在一个更长期的事情上来完成我们要达成的目标而无需担心自己所做的事情要更加的长远和长期,也期待更多的团队和协作方可以享受到季 OKR 的带来的长期。

但从另外一个方向来看,即使我们使用了季度 OKR,也需要关注执行的迭代。

OKR周期是我们达成目标的周期,而做事的手段则应该尽可能的敏捷和快速。快速判断、快速执行、快速复盘、快速修正。

目标大和周期长是我们要着眼于更重要、更长期的事情。而迭代的敏捷,则可以帮助我们更好更快的抵达目标。

person holding sticky note

Chinese-Calendar: 一个帮助你判断今天是不是工作日的 Pypi 包

在开发过程中,你可能会需要实现某些和工作日相关的特性(比如,工作日才发某些通知 /推送),这个时候,你可以借助于 chinese_calendar 这个包,来查看当前是否是工作日,你可以引入 chinese_calendar 这个包,来实现判断今天是否是工作日。

可以参考如下代码,is_workday_today 返回 True 时,就是工作日,就需要执行某些特定的逻辑。

from datetime import datetime
from chinese_calendar import  is_workday

# https://github.com/LKI/chinese-calendar
def is_workday_today():
    today = datetime.now();
    return is_workday(today)
Code language: Python (python)
34456427bc43e44f517b4eece861c6f5

CapRover 如何停止服务,并进行硬盘扩容/维护

在一开始使用 CapRover 时,我使用的是一个 10 GB 的数据盘,但在部署了诸多应用后,10GB 的数据盘已经无法满足我的需求,于是我就对其进行了扩容,扩容至 20GB。在完成扩容 & 重启后,仍需要执行 Linux 的扩容命令 resize2fs 来扩容硬盘。

但由于 CapRover 中运行的服务跑在这个数据盘上,并没有办法直接在这个数据盘上进行扩容(进程会持续读取文件),因此,需要先将 CapRover 上的服务暂停,暂停后进行扩容,并重新启动服务。

CapRover 底层是使用 Docker Swarm + Nginx 来进行的,因此,我们只需要使用 Docker Swarm 的命令,来停止服务运行即可。

1. 获取服务名称

首先,你需要先获取到当前所有在跑的服务,以便于稍后去暂停。执行 docker service ls 来获取到具体的服务名称。

d2b5ca33bd970f64a6301fa75ae2eb22 13

2. 拼接所需的命令

在 Docker Swarm 当中,并没有直接的 Start or Stop 概念,而是通过将 Replica 设置为 0 来实现关闭的能力。这个命令可以通过 docker service scale 服务名=服务数 来实现。因此,你需要将对应的服务设置为 0 来解决这个问题。你可以先行把开启和停止的命令拼接好,从而实现快速的启动和关闭,尽可能的减少宕机时间。

如果是有多个服务,可以直接拼接在后面,从而实现一次关闭 / 开启多个服务。

# docker service scale service_name=1 service_name_2=0
# 停止命令
docker service scale srv-captain--blog-ixiqin-com=0 srv-captain--mysql-8-production-db=0 srv-captain--pgsql-16-production=0 srv-captain--redis-server-production=0
# 启动命令
docker service scale srv-captain--blog-ixiqin-com=1 srv-captain--mysql-8-production-db=1 srv-captain--pgsql-16-production=1 srv-captain--redis-server-production=1
Code language: Bash (bash)

3. 执行命令,扩容硬盘

你可以先执行停止命令,然后执行扩容命令。完成扩容后,重新启动,即可完成整体的扩容。

black and white penguin toy

在你的 Github Actions 中添加一个 PostgreSQL 用于测试

在开发应用的时候,我们会选择使用 PostgreSQL 作为数据库进行开发,但在 Github Actions 环境下,默认是没有 PostgreSQL 作为数据库后端的,这个时候如果你想要测试一些和数据库相关的逻辑,就不得不面临两个选择:

  1. 使用一个和生产环境无关的数据库,比如 SQLite。
  2. 在 Github Actions 当中添加一个 PostgreSQL。

前者是大多数常规的做法,大概率也不会出现什么问题(毕竟作为 CURD 仔,我们用的大部分时候都是一些 ORM,很少裸写 SQL),不过依然存在一些概率是你写了一些 PostgreSQL Only 的 Query 无法覆盖到测试。

另外就是本文的核心了:在你的 Github Actions 当中添加一个 PostgreSQL

Github Actions Service

想要实现这个效果,我们依赖了 Github Actions Service Containers 这个能力。

服务容器是 Docker 容器,以简便、可携带的方式托管您可能需要在工作流程中测试或操作应用程序的服务。 例如,您的工作流程可能必须运行需要访问数据库和内存缓存的集成测试。

您可以为工作流程中的每个作业配置服务容器。 GitHub 为工作流中配置的每个服务创建一个新的 Docker 容器,并在作业完成后销毁该服务容器。 作业中的步骤可与属于同一作业的所有服务容器通信。 但是,你不能在组合操作中创建和使用服务容器。

GitHub

你可以选择你需要运行测试的环境中,找到对应的 Job,并在 Job 下新增一个 services ,即可为你的 job 设定一个依赖的服务容器,它可能是数据库 、 缓存之类的。比如我这里用的就是 PostgreSQL。

我的 Github Actions 完整参考:

  • services 是我运行的服务容器。
  • steps 是我的真正的测试流程。
name: Django CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

env:
  DEBUG: true
  SECRET_KEY: django-insecure-github-actions
  DB_NAME: postgres
  DB_USER: postgres
  DB_PASSWORD: postgres
  DB_HOST: localhost
  DB_PORT: 5432

jobs:
  build:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres
        env:
          POSTGRES_PASSWORD: postgres
        # Set health checks to wait until postgres has started
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    strategy:
      max-parallel: 4
      matrix:
        python-version: [3.12]

    steps:
    - uses: actions/checkout@v3
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v3
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install Dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
    - name: Run Tests
      run: |
        python manage.py test
Code language: PHP (php)
blue red and green letters illustration

Thinking in Component Tree

在开发前端应用的时候,我比较推荐在真正开始写代码之前试着画一画组件树 / 状态树。

在很多时候,可能你的设计师已经帮你做好了组件树,但在某些场景下,你的设计时并不会帮你拆解组件树,或者是你是直接和产品经理对接,他不会帮你拆解组件树。

这个时候,相比于写代码,我更推荐你先拆解组件树,在完成组件树之后,再开始你的 Coding。

d2b5ca33bd970f64a6301fa75ae2eb22 5

Figma / Sketch 之类的软件提供的分组能力、图层的能力,可以帮助你将组件合理的拆解、分组、归类。当你完成树的建设之后,可以试试看将不同的模块拆解,每个模块是否可以独立正常的运转。如果不可以,则说明你的状态拆解的可能是有问题的。

当你完成拆解之后,只需要按照你拆解出来的树组织你的 Component 即可。

text

使用 idb-kayval 作为前端数据存储

在前端留存一些状态,是在前端场景下提升性能的常规操作。最近我有一个场景需要在前端留存一个状态,借着这个机会,试了试 IndexedDB 来作为数据存储,拓展一下新的方向。

关于 Indexed DB

Chrome 在中提供了多种不同的存储,按下 F12 ,打开 DevTools ,找到应用 – 存储,你就会看到目前 Chrome 支持的多种存储方式。常用的主要就是本次存储空间(Local Storage)、会话存储空间(Session Storage)和 Indexed DB。这次我用的便是 Indexed DB。

d2b5ca33bd970f64a6301fa75ae2eb22 1

开发使用建议

由于 Indexed DB 提供的 API 整体比较裸,在实际应用开发时,可能并不好用,你可以根据自己的需要,选择使用不同的第三方开发库来开发你的应用。在这篇文章中,我使用了 idb-keyval 来作为我的开发库。

d2b5ca33bd970f64a6301fa75ae2eb22 3

用法

首先,使用 yarn add idb-keyval 来安装依赖,安装完成后,可以参考如下代码来在你的项目中引入 indexedDB.

import { set,get,keys } from 'idb-keyval';


// 下面演示了一个 get_books 函数,会将内容存储在 IndexedDB 的 your-keys 当中。
// 如果存在缓存,则直接使用缓存,不存在,则进行数据获取
function get_books(){
   // 使用 keys 获取当前 IndexedDB 当中的所有 Key,用于判断当前是否有缓存结果。
   const exists_keys = await keys();
   if(exists_keys.indexOf('your-keys') !== -1){
    console.log("use cached glossary")
    return await get('your-keys');
   }

   // fetch data
   let data = fetch_data();
   
   await set('your-keys',data)
   return data;
}
Code language: PHP (php)

使用前后的效果

在性能上,使用 Indexed DB 之后,根据你的数据获取的难度,会有不同的性能提升。比如这里我不使用缓存,单次数据获取需要花费 800ms,借助于 Indexed DB,时间可以被控制在 10ms 以内,从而得到一个不错性能。

d2b5ca33bd970f64a6301fa75ae2eb22 2
person holding sticky note

FastAPI 在使用 pytest 加载不同的配置文件,以实现测试环境单独运行某些配置项目

引言

Fast API 作为一个新兴的 Python Web 框架,不少的开发者都在使用 FastAPI 来开发应用。我最近也在试着使用它。

在开发应用时,我习惯使用 TDD 的方式进行开发,特别是开发飞书机器人时,由于飞书给我提供的请求是可预测的,特别适合以 TDD 的方式来定义行为并实现。

同时,我在使用其他语言开发的时候,也会用到使用 .env.env.testing 这样的配置文件来在不同的环境下加载不同的配置文件,达到在不同环境使用不同的变量来完成任务。

内容大纲

  1. 实现读取 .env 文件
  2. 实现在测试环境读取 .env.testing 文件

模块详细内容

实现读取 .env 文件

想要实现在 FastAPI 中读取 .env 文件,首先,你需要引入 pydantic_settings 包,来完成基本的配置文件的读取。

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    app_name: str = "Awesome API"

settings = Settings()
print(settings.app_name)

接下来你可以为这个 Settings 类新增 Config 配置,以实现读取本地的 .env 文件

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    app_name: str = "Awesome API"

    class Config:
        env_file = ".env"

settings = Settings()
print(settings.app_name)

通过上述代码,你的配置项目就会自动读取 .env 文件来加载环境变量,从而实现更加简单的环境变量管理。

实现在测试环境读取 .env.testing 文件

在测试时,为了避免使用线上的数据,你可以在测试时加载不同的环境变量文件。

如果你只有一个测试环境,可以有一个非常简单的方式来实现:在配置 env file 时,传入一个元组,会自动加载多个文件。

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    app_name: str = "Awesome API"

    class Config:
        env_file = (".env",".env.testing")

settings = Settings()
print(settings.app_name)

需要注意,这里实现的实际上是使用 pydantic-settings 自己的加载顺序来实现。默认情况下,靠后的文件优先级会更高(覆盖了前面的内容)。这样的好处是如果你有些配置测试环生产是一样的,可以在 .env.testing 中加入不同的部分,相同的部分直接复用生产环境的配置项目即可。

但由于使用的是顺序加载多个问题,如果你发现表现和你配置的不一致时,就要看看是不是有优先级更高的环境变量文件覆盖了默认的 .env 文件。

结论

借助 pydantic-settings 的一些配置,你可以快速实现 .env.env.testing 的加载,相比与判断环境变量,这样会比较简单,且可以实现在 .env.testing 中只维护变量,和线上保持一致的部分可以直接继承。


附加部分

black flat screen computer monitor

JSON to Pydantic

使用 FastAPI 后,你需要写完整的请求体和返回体,以便于生成 API 文档,对于完全从 0 开始构建的项目来说,是不难的,但如果你需要做的是一个项目的迁移,那么写这些类型定义可能需要耗费你大量的时间。

不过,JSON to Pydantic 可以帮助你解决这些问题:

d2b5ca33bd970f64a6301fa75ae2eb22 6

这个网站支持使用一个 JSON 作为 Input,并生成对应的 Pydantic 的代码,对于一些老旧项目迁移的工作来说,可以说是节省了大量的时间。

APILetter

聊聊 APILetter 的新计划

APILetter 从创刊号,到 S1E6,经历了一年的时间。

虽然在定更新节奏时,我就考虑到自己拖更的可能性,但确实没想到我拖更这么严重,在 2022 年,一口气更新了 3 篇,然后就是长达半年的拖更。不过,总算是把第六篇写完,算是给 Season 1 做个了结。

过去

APILetter 的出现,是源自我在研究 RESTFul 架构时发现的问题:国内有太多解释什么是 RESTFul 规范的文章,但你点进去看,篇篇都是复制粘贴。

而 API 是开发者生态中非常重要的一环,它不应该被草率的对待,开发者们值得用上更好的 API。既然没有人写关于 API 的严肃内容,那就从我开始吧。刚好我在研究相关的内容,那就写一些 API 到底应该是什么样的。

也正是抱着这个想法,我开始了一篇篇的创作,从 为什么是 RESTful API,到如何设计一个符合 RESTFul 风格规范的批量操作 OpenAPI;有务实的内容,也有务虚的内容。但不变的是希望让大家明白如何设计一个更好的面向开发者的 API。

但,Season 1 的内容也是杂乱无章的,聊过 RESTFul、聊过 API 文档、聊过 API 指标、聊过 API 报错,这样杂乱且没有主线的内容,作为博客来说,还算可以接受,但作为 Newsletter,可能就显得过于随意。因此,在 Season 2 开始,内容也会开始面向主题,我也会更早的设计不同的 Season 要讨论的话题,以便于让你可以有更好的阅读体验。

未来

在写第一篇邮件通讯时,我就注册了域名 APILetter.com,因为我知道这件事我应该会做很久,所以搞一个独立的站点是必然的事情,也是符合我习惯的事情 —— 每搞一个事情,就要给它一个独立的品牌。

而在刚开始写第一封邮件时,我是没有勇气使用独立的站点的,我总担心自己写完第一封就写不下去了。那搞一个独立的站点不过是浪费时间。所以我选择从竹白开始我的写作之旅。一年过去了,获得了还算不错的效果(至少比我想象中的要好一些)。

image
迁移到 Ghost 之前的数据

不论过程是否艰辛,总归我是完成了自己对自己的承诺,至少写完了一个 Season 的内容。

而 APILetter 完成了第一阶段的产出后,下一个阶段,我希望用更加品牌化的方式来运行这个项目,便启用了 APILetter.com 的域名),并搭建了 Ghost 来托管这个项目。

换到 Ghost,除了域名独立、程序独立、数据独立,相应的,自然也有一些好处 —— 比如可以 RSS 订阅了。如果你希望通过 RSS 的方式来订阅 APIletter, 也可以直接访问 https://www.apiletter.com/rss/ 来订阅。

Season 2:指标

在 Season 1 中,我花费了不少的篇幅来讲 OpenAPI 的设计问题。在 Season 2 ,我想专注于 OpenAPI、开发者体验中的指标问题,从企业和个人制定目标开始,到具体到某一个具体的 OpenAPI 指标评定。

目前预计会包含的内容:API 自身的指标定义、API 相关业务的指标定义、开发者体验中的一些指标定义。

除了这些指标,你还关注哪些指标?欢迎回复邮件告诉我。

总结

总之,APILetter 在新的一季里,我会尽量以更好的内容组织方式、更高的频率(但暂时承诺还是每月一封哈),来给大家分享我自己关于 API、关于开发者体验,以及一切与开发者有关的内容,希望可以帮到你。

如果你身边有对于开发者关系感兴趣或从事相关内容的朋友,欢迎你将 APILetter 介绍给他,帮助我获得更多的读者,以及,持续的写下去🥰。