分类: 编程

  • Go并发编程精讲:goroutine/channel原理与避坑指南

    Go并发编程精讲:goroutine/channel原理与避坑指南

    深入理解 GMP 调度模型

    Go 运行时调度器将 goroutine 映射到 OS 线程上执行,GMP 代表 G(Goroutine)、M(Machine/OS 线程)、P(Processor,执行上下文)。P 的数量默认等于 CPU 核心数,可以通过 GOMAXPROCS 调整,这是 Go 并发性能的关键参数。

    goroutine 的生命周期与状态

    goroutine 有多个状态:waiting(阻塞在 channel 或系统调用)、runnable(就绪等待调度)、running(正在执行)。理解这些状态有助于分析死锁和性能问题。

    channel 的底层实现

    channel 内部有环形队列和两个等待队列(sendq 和 recvq)。向已满的无缓冲 channel 发送会阻塞,goroutine 被挂在 sendq 上;接收时会从 recvq 唤醒一个 sender 或从队列取数据。

    常见并发陷阱

    • 向已关闭的 channel 发送数据会 panic,但接收会返回零值
    • 多个 goroutine 同时等待同一个 channel,只有 1 个会被唤醒——用 sync.WaitGroup 而非 channel 控制退出
    • for range channel 会一直阻塞直到 channel 关闭
    • context.WithCancel 是优雅退出多个 goroutine 的标准方式

    小结

    理解 GMP 调度和 channel 底层原理,才能真正写出高效的并发代码。遇到性能问题时,用 runtime.NumGoroutine() 和 pprof 分析 goroutine 的状态分布是第一步。

    来源:https://www.dnote.cn

  • 用 Taro 开发小程序:答题卡网格布局的兼容性问题总结

    用 Taro 开发小程序:答题卡网格布局的兼容性问题总结

    背景

    在做一个刷题小程序时,需要实现一个答题卡界面——用网格展示所有题目状态(未答/已答/当前题),支持点击跳转。在 Web 上几行 CSS 搞定,在小程序里却踩了不少坑。

    问题一:gap 属性在小程序里不生效

    这是最常见的问题。CSS gap 属性用于设置网格/弹性盒子的间距,在主流浏览器都支持,但在微信小程序、支付宝小程序里均不支持。解决方案:

    /* ❌ 小程序不支持 */
    .grid { display: grid; gap: 16px; }
    
    /* ✅ 改用 margin 模拟 */
    .grid { display: grid; }
    .grid-item { margin-right: 16px; margin-bottom: 16px; }
    /* 最后一项去掉右边距 */
    .grid-item:last-child { margin-right: 0; }

    问题二:display: grid 在某些机型的兼容情况

    iOS 微信大部分版本支持 grid,但 Android 端部分机型(尤其是低端机)的 UC/QQ 浏览器内核不支持。推荐降级方案:用 flex + 百分比宽度实现网格。

    /* ✅ 兼容写法:flex 模拟网格 */
    .row { display: flex; flex-wrap: wrap; }
    .cell { width: 48px; height: 48px; margin: 0 4px 8px 0; }

    问题三::nth-child 在某些场景下失效

    nth-child 在动态列表(数据来自接口)时表现正常,但在用 v-for 遍历固定数量的数字时,微信开发者工具和部分真机行为不一致。解决方案是改用 v-if 或加辅助 class。

    实战答题卡组件代码

    const AnswerCard = ({ questions, currentIndex, onJump }) => {
      return (
        <View className='answer-card'>
          <View className='row'>
            {questions.map((q, i) => (
              <View
                key={q.id}
                className={`cell ${i === currentIndex ? 'current' : ''} ${q.answered ? 'done' : 'undone'}`}
                onClick={() => onJump(i)}
              >
                {i + 1}
              </View>
            ))}
          </View>
        </View>
      )
    }

    小结

    小程序开发的本质是与 CSS 兼容层打交道。遇到 display、gap、nth-child 等属性,先查 caniuse,再想降级方案。开发阶段多用真机调试,开发者工具的结果不一定准确。

    来源:https://www.dnote.cn

  • Docker 容器化 Go 服务的几个实战细节

    Docker 容器化 Go 服务的几个实战细节

    多阶段构建:让镜像小到 10MB 以内

    Go 是静态编译语言,只需要编译后的二进制文件,不需要运行时环境。多阶段构建可以分离构建环境和运行环境,最终镜像只包含二进制文件。

    # 阶段1:构建
    FROM golang:1.23-alpine AS builder
    WORKDIR /app
    COPY go.mod go.sum ./
    RUN go mod download
    COPY . .
    RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-w -s' -o server .
    
    # 阶段2:运行
    FROM alpine:3.19
    RUN addgroup -S appgroup && adduser -S appuser -G appgroup
    WORKDIR /app
    COPY --from=builder /app/server .
    RUN chmod +x server
    USER appuser
    CMD ["./server"]

    利用 Docker BuildKit 加速构建

    # 启用 BuildKit
    export DOCKER_BUILDKIT=1
    
    # 并行下载依赖,大幅加速
    docker build --progress=plain -t myapp:latest .

    健康检查:让 K8s/Compose 知道服务已就绪

    在 Dockerfile 里加上 HEALTHCHECK,让 orchestrator 在服务真正能接收请求时才将其加入负载均衡。

    HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
      CMD wget -qO- http://localhost:8080/health || exit 1

    环境变量注入的坑

    Go 程序默认会在 ENV 改变时重新加载,但 Docker 层会缓存 ENV。用 ENTRYPOINT 脚本或 Viper 动态读取,比硬编码 ENV 常量更灵活。

    小结

    Docker 化 Go 服务的核心是:镜像要小(多阶段构建)、构建要快(BuildKit)、健康要可感知。做到这三点,生产部署就没什么大问题了。

    来源:https://www.dnote.cn

  • 用 Taro 开发小程序:答题卡网格布局的兼容性问题总结

    背景

    在做一个刷题小程序时,需要实现一个答题卡界面——用网格展示所有题目状态(未答/已答/当前题),支持点击跳转。在 Web 上几行 CSS 搞定,在小程序里却踩了不少坑。

    问题一:gap 属性在小程序里不生效

    这是最常见的问题。CSS gap 属性用于设置网格/弹性盒子的间距,在主流浏览器都支持,但在微信小程序、支付宝小程序里均不支持。解决方案:

    /* ❌ 小程序不支持 */
    .grid { display: grid; gap: 16px; }
    
    /* ✅ 改用 margin 模拟 */
    .grid { display: grid; }
    .grid-item { margin-right: 16px; margin-bottom: 16px; }
    /* 最后一项去掉右边距 */
    .grid-item:last-child { margin-right: 0; }

    问题二:display: grid 在某些机型的兼容情况

    iOS 微信大部分版本支持 grid,但 Android 端部分机型(尤其是低端机)的 UC/QQ 浏览器内核不支持。推荐降级方案:用 flex + 百分比宽度实现网格。

    /* ✅ 兼容写法:flex 模拟网格 */
    .row { display: flex; flex-wrap: wrap; }
    .cell { width: 48px; height: 48px; margin: 0 4px 8px 0; }

    问题三::nth-child 在某些场景下失效

    nth-child 在动态列表(数据来自接口)时表现正常,但在用 v-for 遍历固定数量的数字时,微信开发者工具和部分真机行为不一致。解决方案是改用 v-if 或加辅助 class。

    实战答题卡组件代码

    const AnswerCard = ({ questions, currentIndex, onJump }) => {
      return (
        <View className='answer-card'>
          <View className='row'>
            {questions.map((q, i) => (
              <View
                key={q.id}
                className={`cell ${i === currentIndex ? 'current' : ''} ${q.answered ? 'done' : 'undone'}`}
                onClick={() => onJump(i)}
              >
                {i + 1}
              </View>
            ))}
          </View>
        </View>
      )
    }

    小结

    小程序开发的本质是与 CSS 兼容层打交道。遇到 display、gap、nth-child 等属性,先查 caniuse,再想降级方案。开发阶段多用真机调试,开发者工具的结果不一定准确。

    来源:https://www.dnote.cn

  • Go 并发编程:Goroutine 与 Channel 实战指南

    为什么 Go 的并发模型值得专门学

    Go 的并发哲学来自 C.A.R. Hoare 的 CSP 理论,与传统线程模型完全不同。理解这套模型,是写出高性能 Go 服务的基础。

    Goroutine 到底是什么

    goroutine 是由 Go 运行时管理的轻量级线程,创建成本极低(约 2KB 栈空间),可以轻松创建数万个 goroutine 而不耗尽系统资源。与 OS 线程的 1:1 模型不同,Go 使用 M:N 调度——M 个 OS 线程上运行 N 个 goroutine,由 Go 运行时 GOMAXPROCS 控制并行度。

    一个简单的 goroutine 示例

    // 启动一个 goroutine
    go func() {
        fmt.Println("running in goroutine")
    }()
    
    // 主 goroutine 继续执行,不等待子 goroutine
    fmt.Println("main exits")

    Channel:goroutine 之间的通信桥梁

    channel 是 goroutine 间传递数据的管道,有带缓冲(buffered)和不带缓冲(unbuffered)两种。推荐优先使用无缓冲 channel,这样发送与接收必须配对,强制调用方思考同步问题,避免隐藏的竞态。

    ch := make(chan int)         // 无缓冲
    ch <- 42                       // 发送(阻塞直到有人接收)
    value := <-ch                  // 接收
    
    // 带缓冲 channel(队列)
    bufCh := make(chan int, 100)  // 最多缓存 100 条,不阻塞

    select:多路复用的并发协调

    select 关键字让一个 goroutine 可以同时监听多个 channel,就跟 select 系统调用监听多个 fd 一样。配合 default case 可以实现非阻塞发送/接收。

    select {
    case msg := <-ch1:
        fmt.Println("received from ch1:", msg)
    case msg := <-ch2:
        fmt.Println("received from ch2:", msg)
    case <-time.After(time.Second):
        fmt.Println("timeout")
    default:
        fmt.Println("no message available")
    }

    常见踩坑与最佳实践

    • 不要用 channel 传递所有数据——共享内存(sync.Mutex/sync.Map)有时更简单
    • 永远不要关闭由别人发送的 channel,避免 panic
    • 使用 context.Context 来控制 goroutine 的生命周期,而不是用 channel 硬传退出信号
    • 在生产环境加上 -race 参数跑单元测试,检查数据竞争

    总结

    Go 的并发模型用起来简单,但要真正用好,需要理解 goroutine 的调度原理、channel 的阻塞语义,以及 context 在多层调用中的传递方式。掌握这些,你就能写出既高效又安全的并发代码。

    来源:dnote.cn

  • WordPress 6.9.4 正式发布:更新内容速览

    WordPress 6.9.4 已于 2026年04月15日 正式发布。以下是本次更新的主要内容整理,供站长参考。

    ⚠️ 安全版本:本次更新包含安全修复,建议所有用户尽快升级。

    本次 6.9.4 是一个安全维护版本,修复了 10 个安全问题。WordPress 安全团队发现此前版本中部分安全修复未完全生效,因此紧急发布 6.9.4 补丁。

    🔒 安全修复

    • 路径穿越漏洞(PclZip 组件)
    • 授权绕过漏洞
    • XXE(XML 外部实体注入)漏洞

    如何升级

    登录 WordPress 后台 → 仪表盘 → 更新,点击「立即更新」即可完成升级。建议升级前先备份数据库和文件。

    参考链接

    本文由自动化脚本监测到 WordPress 新版本后自动生成,仅供参考。

  • Dnote网站深度诊断报告:发现的问题与优化建议

    Dnote网站深度诊断报告:发现的问题与优化建议

    前言

    最近对个人博客网站 dnote.cn 进行了一次全面的结构分析和诊断,发现了一些值得关注的问题。本文将详细记录发现的问题并提供相应的优化建议。

    一、网站基础数据

    • 文章总数:100篇
    • 分类数:3个(编程95篇、投资18篇、未分类3篇)
    • 标签数:22个
    • 页面数:3个(关于、小玩意儿、搜索)
    • 媒体文件:50个

    二、发现的问题

    1. 内容分类严重失衡

    在100篇文章中,编程类占据了95篇,比例高达95%,而投资类仅有18篇。这反映出内容定位过于单一,建议:

    • 将部分技术文章重新归类,细化分类标签
    • 考虑增加新的分类维度,如”产品”、”创业”、”生活”
    • 将”未分类”的3篇文章尽快归类

    2. SEO基础优化不足

    通过分析发现以下SEO问题:

    • 缺少结构化数据:未发现JSON-LD富媒体标记,错失搜索展示机会
    • 内链锚文本单一:多为”查看更多→”,应使用关键词锚文本
    • 文章摘要较短:部分文章描述信息不足,影响搜索结果吸引力

    3. 性能优化空间

    • 未使用CDN:静态资源未通过CDN分发
    • 缺少Gzip压缩:建议开启以减少传输体积
    • 缺少浏览器缓存配置:应设置合理的缓存策略

    4. 标签使用混乱

    22个标签中,部分标签存在重复或过细的问题,如同时存在”SQL”和”MySQL”、”Docker”和”Linux”等,建议进行标签整合。

    5. 文章日期异常

    所有文章显示发布日期为2026年3月13日(部分甚至显示2025年),这可能是:

    • 批量导入时的伪日期
    • 系统时间配置问题
    • WordPress时区设置问题

    建议核查并修正,避免搜索引擎误判内容为过期内容。

    三、优化建议汇总

    优先级 问题 建议方案
    分类失衡 重新划分分类,增加3-5个新分类
    日期异常 检查WordPress时区设置,修正文章日期
    SEO元标签 添加JSON-LD结构化数据
    内链优化 使用关键词作为锚文本
    性能优化 接入CDN,配置Gzip和缓存

    四、结语

    整体而言,网站技术架构合理,WordPress运行稳定,内容质量较高。主要问题集中在内容组织结构和SEO基础优化上。解决这些问题后,网站的搜索可见性和用户体验都会有明显提升。

    后续我会逐步进行优化并记录改进效果,敬请期待。

  • 在electron中基于容器和服务提供者扩展应用核心能力

    在electron中基于容器和服务提供者扩展应用核心能力

    应用自身可能提供多种不同的能力,结合服务提供者概念和容器,我们可以实现类似插件的扩展机制,并且通过容器来统一管理服务对象,方便后续扩展。

    一、容器

    容器提供注册同步/异步工厂的方法、同步/异步获取指定服务对象的方法,移除某服务的方法和清空全部服务对象的方法。

    export class Container {
      private instances: Map<string, any> = new Map()
      private factories: Map<string, () => any> = new Map()
      private asyncFactories: Map<string, () => Promise<any>> = new Map()
      private resolvingPromises: Map<string, Promise<any>> = new Map()
      private singleton: Set<string> = new Set()
    
      // 同步注册工厂(默认为单例)
      register<T>(token: string, factory: () => T, options: { singleton?: boolean } = { singleton: true }) {
        this.factories.set(token, factory)
        if (options.singleton) {
          this.singleton.add(token)
        } else {
          this.singleton.delete(token)
        }
      }
    
      // 异步注册工厂(默认为单例)
      registerAsync<T>(token: string, factory: () => Promise<T>, options: { singleton?: boolean } = { singleton: true }) {
        this.asyncFactories.set(token, factory)
        if (options.singleton) {
          this.singleton.add(token)
        } else {
          this.singleton.delete(token)
        }
      }
    
      // 同步解析
      resolve<T>(token: string): T {
        // 如果是单例且已存在实例,直接返回
        if (this.singleton.has(token) && this.instances.has(token)) {
          return this.instances.get(token)
        }
    
        // 查找工厂函数
        const factory = this.factories.get(token)
        if (!factory) {
          // 如果没有工厂但有实例,可能是异步实例已完成
          if (this.instances.has(token)) {
            return this.instances.get(token)
          }
          throw new Error(`Service not found: ${token}`)
        }
    
        // 创建实例
        const instance = factory()
    
        // 如果是单例,保存实例
        if (this.singleton.has(token)) {
          this.instances.set(token, instance)
        }
    
        return instance
      }
    
      // 异步解析
      async resolveAsync<T>(token: string): Promise<T> {
        // 如果是单例且已存在实例,直接返回
        if (this.singleton.has(token) && this.instances.has(token)) {
          return this.instances.get(token)
        }
    
        // 检查是否已经有正在解析的Promise
        let resolvingPromise = this.resolvingPromises.get(token)
        if (resolvingPromise) {
          // 如果有,直接返回这个Promise的结果
          return resolvingPromise
        }
    
        // 先检查是否有异步工厂
        const asyncFactory = this.asyncFactories.get(token)
        if (asyncFactory) {
          // 创建一个新的解析Promise并存储
          resolvingPromise = (async () => {
            try {
              const resolved = await asyncFactory()
              // 如果是单例,保存实例
              if (this.singleton.has(token)) {
                this.instances.set(token, resolved)
              }
              return resolved
            } finally {
              // 解析完成后从resolvingPromises中移除
              this.resolvingPromises.delete(token)
            }
          })()
    
          // 如果是单例,存储Promise以避免重复创建
          if (this.singleton.has(token)) {
            this.resolvingPromises.set(token, resolvingPromise)
          }
          return resolvingPromise
        }
    
        // 再检查是否有同步工厂
        const factory = this.factories.get(token)
        if (factory) {
          try {
            const resolved = factory()
            // 如果是单例,保存实例
            if (this.singleton.has(token)) {
              this.instances.set(token, resolved)
            }
            return resolved
          } catch (error) {
            if (error instanceof Error) {
                throw new Error(`Failed to resolve service '${token}': ${error.message}`)
            } else {
                throw error
            }
          }
        }
    
        // 最后检查是否已有实例(可能是之前异步解析完成的)
        if (this.instances.has(token)) {
          return this.instances.get(token)
        }
    
        throw new Error(`Service not found: ${token}`)
      }
    
      // 检查是否存在指定token的服务
      has(token: string): boolean {
        return this.instances.has(token) ||
               this.factories.has(token) ||
               this.asyncFactories.has(token)
      }
    
      // 清除指定服务的实例(不移除工厂)
      clearInstance(token: string): void {
        this.instances.delete(token)
        this.resolvingPromises.delete(token)
      }
    
      // 移除服务(包括工厂和实例)
      remove(token: string): void {
        this.instances.delete(token)
        this.factories.delete(token)
        this.asyncFactories.delete(token)
        this.resolvingPromises.delete(token)
        this.singleton.delete(token)
      }
    }
    

    二、服务提供者

    服务提供者接口比较简单,区分register和boot主要是为了确保boot中可以调用任何其他提供者。

    import type { Container } from "../container"
    
    export default interface ServiceProvider {
        /**
         * 应用启动后立即调用
         * 建议仅用于注册服务,不要执行其他操作,其他操作可以放在boot方法中
         *
         * @param container
         */
        register?(container: Container): void
    
        /**
         * boot函数在所有服务注册完成后调用
         * 可以执行一些初始化操作
         *
         * @param container
         */
        boot?(container: Container): void | Promise<void>
    }
    

    三、应用对象

    import { Container } from './container'
    
    export default class APP {
    
        /**
         * 应用实例,用来保持单例
         */
        private static _ins: APP
    
        /**
         * 容器对象
         */
        private container: Container
    
        /**
         * 服务提供者列表
         */
        private providers: ServiceProvider[] = []
    
        /**
         * 运行应用
         */
        static run() {
            if (APP._ins == null) {
                APP._ins = new APP()
            }
    
            return APP._ins
        }
    
        constructor() {
            // 初始化容器并注册服务提供者
            this.container = new Container()
    
            // 注册服务
            this.registerProviders()
    
            // 启动服务
            this.bootProviders()
        }
    
        registerProviders() {
            for (const provider of this.providers) {
                provider.register?.(this.container)
            }
        }
    
        bootProviders() {
            return Promise.all(
                this.providers.map(provider => provider.boot?.(this.container))
            )
        }
    }
    
  • Caddy配置sts(Strict-Transport-Security)

    Caddy配置sts(Strict-Transport-Security)

    example.com {
        # 启用 HSTS,有效期 1 年(31536000 秒)
        header {
            Strict-Transport-Security max-age=31536000
        }
    
        # 其他配置...
        reverse_proxy localhost:8080
    }
    
  • 创建strapi插件

    创建strapi插件

    需要开发一个插件来实现一些自定义功能,官方文档写的实在是难以理解了。

    1. 创建基础的目录结构

    # 作为项目根目录
    mkdir xxx
    
    # 进入项目目录
    cd xxx
    
    # 创建strapi 项目
    npx create-strapi@latest strapi
    
    # 创建插件
    npx @strapi/sdk-plugin init strapi-plugin
    

    2. 配置工作区

     # xxx目录中添加workspace配置
    {
         "workspaces": [
             "strapi",
             "strapi-plugin"
         ],
         "dependencies": [
             "strapi-plugin": "workspace:*"
         ]
     }
    

    3. strapi目录引入依赖

     {
         ...
         "dependencies": {
             ...
             "strapi-plugin": "0.0.0"
         }
     }
    

    4. 插件目录监听文件修改

    # 根目录执行
    npm run watch --workspace=strapi-plugin
    

    5. 运行strapi

    npm run develop --workspace=strapi
    
  • Parse Platform任务调度

    Parse Platform任务调度

    说起来也挺奇怪的,Parse Platform本身基于js实现,Node生态里的任务调度实现起来并不麻烦,Parse却选择了不集成,使用第三方提供的任务调度。这里使用node-cron来实现Parse Server中的Job调度。

    一、Cloud Code定义Job

    Parse.Cloud.job("test", async (request) =>  {
        // params: passed in the job call
        // headers: from the request that triggered the job
        // log: the ParseServer logger passed in the request
        // message: a function to update the status message of the job object
        const { message } = request
        message('done')
    })
    

    二、定义一个调用Job的函数,方便后面使用

    async function callParseJob(parseServerUrl: string, name: string, data?: any) {
      console.log(`node-cron: callParseJob ${name}`)
      const options: RequestInit = {
        method: 'POST',
        headers: {
          'X-Parse-Application-Id': process.env.APP_ID ?? '',
          'X-Parse-Master-Key': process.env.MASTER_KEY ?? ''
        }
      }
      if (data != null) {
        options.body = JSON.stringify(data)
      }
      const res = await fetch(`${parseServerUrl}/jobs/${name}`, options)
      console.log(`node-cron: job ${name} finished, ${res.statusText}`)
      return await res.json()
    }
    

    三、使用node-cron调度Job

    这里选择通过单独的进程来运行调度程序

    if (cluster.isPrimary) {
      cron.schedule('* * * * *', async () => await callParseJob(process.env.SERVER_URL ?? '', 'test'))
      cluster.fork() // 根据cpu个数fork子进程
    
      cluster.on('exit', function(worker, code, signal) {
        console.log('worker ' + worker.process.pid + ' exited')
        cluster.fork() //新建一个worker
      })
    } else {
      const app = express()
        //...启动Server相关的代码,这里省略
    }
    
  • 为什么mongoDB在国内发展的没redis好?

    为什么mongoDB在国内发展的没redis好?

    mongo用户应该集中在前端转全栈,通过Node操作mongodb,可以保持之前对于JSON的理解,不用接触传统数据库的知识。

    国内用Node作为后端的用户就不多,Serverless发展也一般,搞Node全栈的集中在独立开发者或者大型公司的某个团队,这个体量确实也不是很大。

    如果是纯后端,我很难理解引入Mongo的意义,完全可以把Postgres的表搞成id+json的形式来模拟Mongo无架构,甚至微软搞了一个DocumentDB,兼容mongo协议,就是基于PostgreSQL的。从扩展性,功能,性能任何方面看,这样用pg都是优于采用MongoDB的。

    DocumentDB:开源公告 – Microsoft 开源博客

    Redis不一样,

    1. 大部分应用考虑扩展和性能的情况下,都需要一个缓存中间件,这个首先mongo就不适合
    2. 简单消息队列
    3. 发布订阅
    4. 不提供内置有序集合的语言,有序集合也省的自己写了
    5. 高频读写的小数据也可以开持久化作为主数据库

     

    https://www.zhihu.com/question/478485839/answer/1965357311661965488

  • Caddy配置CloudBeaver使用Authelia认证

    Caddy配置CloudBeaver使用Authelia认证

    Cloudbeaver反向代理认证配置文档:https://github.com/dbeaver/cloudbeaver/wiki/Reverse-proxy-header-authentication

    每个地方单独设置账号就太麻烦了,这里记录Caddy配置Cloudbeaver使用Authelia认证的方式。重点其实就是传递到后端时添加X-User和X-Team两个请求头,caddy推荐的authelia配置使用的是Remote-User和Remote-Groups。

    forward_auth authelia:9091 {
        uri /api/authz/forward-auth
        
        ## The following commented line is for configuring the Authelia URL in the proxy. We strongly suggest
        ## this is configured in the Session Cookies section of the Authelia configuration.
        # uri /api/authz/forward-auth?authelia_url=https://auth.example.com/
        copy_headers {
            # Remote-User Remote-Groups Remote-Email Remote-Name
            Remote-User>X-User
            Remote-Groups>X-Team
        }
    }
    

     

  • 阿里云OSS与Cloudflare R2价格对比

    阿里云OSS与Cloudflare R2价格对比

    场景:存储位置美西硅谷,存储量200GB,时间一年,写操作3万/月,读操作30万/月,每月传输200GB流量。

    1. 阿里云

    1. 存储费用:100GB资源包+100GB按量 = 162 + 0.136 元/GB/月 * 100 * 12 = 162 + 163.2 = 325.2(每月免费5GB,单价合¥0.136 元/GB/月
    2. 写操作:0(地域500万次/月免费,超出0.1/万次)
    3. 读操作:0(地域2000万次/月免费,超出0.1/万次)
    4. 流量费用:200 * 0.5 * 12 = 1200
    5. 合计:1525.2

    2. Cloudflare

    1. 存储费用:(200 – 10) * 0.015 * 12 = $34.2(每月免费10GB,合¥0.109/GB/月)
    2. 写操作:0(地域100万次/月免费,超出$4.5/百万次,合0.33/万次)
    3. 读操作:0(地域1000万次/月免费,超出0.36/百万次, 合0.026/万次)
    4. 流量费用:0
    5. 合计折合人民币7.13 * 34.2 = 243.846

    3. 腾讯云

    1. 存储费用:212.16(0.089 元/GB/月
    2. 读写操作:0.01/月/万次 * (30 + 3)万次 * 12 = 3.96
    3. 流量费用:200GB/月资源包 = 1029
    4. 合计: 1245.12

    境外走Cloudflare的CDN的话,三方都没有流量费,伴随使用量增加,估计最终Cloudflare整体的价格相对会更低一些,但是需要增加到一定的用量以后。

  • 优化WordPress后台加载速度

    优化WordPress后台加载速度

    1. PHP预加载脚本

    <?php
    
    /**
    *使用OPcache优化WordPress的预加载脚本。
    *将此文件放在WordPress安装的根目录中。
     */
    
    // Define the base path for WordPress
    define('WP_ROOT_DIR', __DIR__);
    
    // Define ABSPATH (required by WordPress core files)
    if ( ! defined( 'ABSPATH' ) ) {
        define( 'ABSPATH', WP_ROOT_DIR . '/' );
    }
    
    // Define WPINC (required by wp-includes/functions.php)
    if ( ! defined( 'WPINC' ) ) {
        define( 'WPINC', 'wp-includes' );
    }
    
    // Define WP_DEBUG (required by wp-includes/functions.php)
    if ( ! defined( 'WP_DEBUG' ) ) {
        define( 'WP_DEBUG', false );
    }
    
    // 仅预加载最基本的核心文件
    require WP_ROOT_DIR . '/wp-includes/default-constants.php';
    require WP_ROOT_DIR . '/wp-includes/rewrite.php';
    require WP_ROOT_DIR . '/wp-includes/theme.php';
    require WP_ROOT_DIR . '/wp-includes/post.php';
    require WP_ROOT_DIR . '/wp-includes/meta.php';
    require WP_ROOT_DIR . '/wp-includes/user.php';
    require WP_ROOT_DIR . '/wp-includes/cache.php';
    require WP_ROOT_DIR . '/wp-includes/capabilities.php';
    require WP_ROOT_DIR . '/wp-includes/shortcodes.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-query.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-widget.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-roles.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-user.php';
    require WP_ROOT_DIR . '/wp-includes/class-wp-post.php';
    
    // 后台管理模块的类
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-list-table.php';
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-media-list-table.php';
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-users-list-table.php';
    require WP_ROOT_DIR . '/wp-admin/includes/class-wp-themes-list-table.php';
    
    // Preload database-related files
    require WP_ROOT_DIR . '/wp-includes/wp-db.php';
    // require WP_ROOT_DIR . '/wp-includes/class-wpdb.php'; // 如果不需要,可以注释
    
  • Go语言获取指定年份生肖

    Go语言获取指定年份生肖

    根据给定年份,返回生肖字符串,公元前使用负值即可。(比如2022年,调用使用GetShengXiao(2022),公元前21年,调用使用GetShengXiao(-21))。
    
    
    // 获取生肖索引
    func GetShengXiaoIndex(year int) int {
        // 不存在0年
        if year == 0 {
            panic("err: invalid year")
        }
    
        // 公元前补1
        if year < 0 {
            year += 1
        }
    
        idx := (year - 4) % 12
    
        if idx < 0 {
            idx += 12
        }
    
        return idx
    }
    
    // 根据给定年份获取生肖
    func GetShengXiao(idx int) string {
        return [12]string{"鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"}[idx]
    }
    
  • WordPress添加关键词和描述标签

    WordPress添加关键词和描述标签

    关键词和描述标签作为SEO的基础配置,在wordpress中没有默认添加,这里记录一下在Wordpress中自动添加关键词和描述标签的方法。

    一、实现代码

    实现思路是使用标签来作为关键词,使用文章摘要作为页面描述,找到使用主题的functions.php文件,添加以下代码即可实现。

    /**
     * 添加SEO相关的Keywords和Description标签
     */
    function add_seo_meta_tags() {
        if (is_home()) {
            $tags = get_tags([
                'number' => 15,
                'orderby' => 'count',
                'order' => 'DESC'
            ]);
            $tags = array_map(function ($item) {
                return $item->name;
            }, $tags);
            if (count($tags) > 0) {
                ?>
                <meta name="keywords" content="<?php echo get_bloginfo('name'); ?>,<?php echo implode(',', $tags); ?>">
                <?php
            }
            ?>
                <meta name="description" content="<?php echo get_bloginfo('description'); ?>">
            <?php
        } else if (is_category() || is_tag()) {
            ?>
                <meta name="keywords" content="<?php echo single_cat_title(); ?>">
                <meta name="description" content="<?php echo strip_tags(category_description()); ?>">
            <?php
        } else if (is_singular()) {
            ?>
                <meta name="description" content="<?php echo strip_tags(get_the_excerpt()); ?>">
            <?php
    
            $tags = array_map(function ($item) {
                return $item->name;
            }, get_the_tags() ?: []);
            if (count($tags) > 0) {
                ?>
                    <meta name="keywords" content="<?php echo implode(',', $tags); ?>">
                <?php
            }
        }
    }
    add_action( 'wp_head', 'add_seo_meta_tags' );
    

    二、使用的前置要求

    请注意,需要保证主题的header.php调用过wp_head()函数,类似下面:

    <!DOCTYPE html>
    <html <?php language_attributes(); ?> class="no-js">
    <head>
    <meta charset="<?php bloginfo( 'charset' ); ?>">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    
    <?php wp_head(); ?>
    </head>
    

    OK,这样我们就给所有页面都添加上关键词和描述标签了~

    相关链接:

    1. wordpress关于meta标签的说明
  • 使用SSH转发服务器端口到本地

    使用SSH转发服务器端口到本地

    注意远程地址、远程端口号、本地地址、本地端口号需要按照实际情况修改。

    function forwardPort() {
        PROC_NAME="ssh -fR $1"
        ProcNumber=`ps -ef |grep -w "$PROC_NAME"|grep -v grep|wc -l`
        if [ $ProcNumber -le 0 ];then
            echo "$1 is not forward.."
            ssh -fCNR $1:localhost:$1 root@0.0.0.0 -p 1234 -o ServerAliveInterval=30
        else
            echo "$1 has forwarded.."
        fi
    }
    for port in 1234 4567
    do
        forwardPort $port
    done
    

    注意,如果需要外网访问转发的端口,需要在远程服务器的配置文件(/etc/ssh/sshd_config)中添加:

    AllowTcpForwarding yes
    
  • 使用Joplin作为博客后端-使用marked转换joplin笔记中资源文件的地址

    使用Joplin作为博客后端-使用marked转换joplin笔记中资源文件的地址

    joplin笔记中资源文件默认使用的是id,但是如果渲染到网页,需要转换为网络地址,记录一下处理过程。

    export async function transformMarkdown(markdown?: string) {
        if (markdown == null || markdown === '') {
            return ''
        }
    
        const renderer = new marked.Renderer()
    
        marked.use({
            async: true,
            async walkTokens(token) {
                if (token.type === "image") {
                    const id = token.href.replace(':/', '')
                    const resource = await joplinResource(id)
                    token.href = `${process.env.SITE_URL}/resources/${id}.${resource.file_extension}`
                    token.title = resource.title
                }
            },
            renderer: {
                image({ href, text, title }) {
                    return `<img src="${href}" alt="${text}" title="${title}" loading="lazy" />`
                }
            }
        })
    
        return await marked.parse(markdown, {
            renderer,
            gfm: true,
            breaks: false,
            pedantic: false
        })
    }
    
  • 使用Joplin作为博客后端-基于Joplin Terminal部署服务端REST API

    使用Joplin作为博客后端-基于Joplin Terminal部署服务端REST API

    Joplin Server本身虽然有接口,但是并不能直接获取笔记数据,了解后发现Joplin Terminal是支持Data API的,这样的话,可以通过在服务器部署一套Joplin Terminal程序来从服务器提供笔记数据,唯一的问题可能就是会导致服务器存储两份笔记数据,不过也不是很大的问题,这里以通过Docker容器部署为例。

    一、安装Joplin Terminal

    # 创建数据存储文件夹
    mkdir -p joplin/cli
    
    cd joplin/cli
    
    npm i joplin
    
    # 同步配置参考官方文档 https://joplinapp.org/help/apps/terminal/
    
    # 同步数据
    npx joplin sync
    

    二、启动Joplin的Data API Server

    修改项目的package.json,增加一个启动脚本。

    {
      "type": "module",
      "scripts": {
        "joplin": "joplin server start",
        ...
      },
      "dependencies": {
        "joplin": "^3.4.1"
      }
    }
    
    npm run joplin
    

    三、docker compose配置

    services:
        ...
        joplin-api:
            image: node:22.12.0
            restart: unless-stopped
            # 这个应该根据实际的uid和gid设置,与挂载目录的/home/test有关
            user: 1000:1000
            volumes:
                # 挂载这个文件主要是为了保持权限一致,避免本地修改容器不能运行
                - /etc/passwd:/etc/passwd:ro
                # 挂载项目代码
                - ./joplin/cli:/app
                # joplin数据保存位置
                - ./joplin/data:/home/test/.config/joplin
                - ./joplin/.npm:/home/test/.npm/
            # 这个start命令参考下面
            command: ["sh", "-c", "chdir /app && npm start"]
    

    四、转发请求,配置定时任务

    joplin terminal启动的data api默认是写死的绑定127.0.0.1,需要本地启动一个反向代理服务器来做一下请求转发,同时也配置一下定时任务。
    我这里服务器用的h3,用什么其实都可以,感觉JS的Web框架语法都差不太多,这里选h3主要是为了资源占用少。

    import { H3 } from "h3"
    import { createServer } from 'node:http'
    import { toNodeHandler } from "h3/node"
    import cron from 'node-cron'
    import { exec } from "node:child_process"
    
    /**
     * 运行node-cron定时任务
     * /30 * * * /path/to/joplin sync
     */
    cron.schedule('*/15 * * * *', joplinSync)
    
    /**
     * 同步数据
     */
    export async function joplinSync() {
      exec('/app/node_modules/.bin/joplin sync', (_, stdout) => {
        console.log('joplin sync: ', stdout)
      })
    }
    
    export const app = new H3()
    
    /**
     * 反向代理外界请求到本机的41184端口
     * joplin Data API Server默认监听41184,因为Docker环境,也不太可能出现冲突的问题
     */
    app.use(
      async (event) => {
        const { url } = event.req
    
        const proxyUrl = new URL(url)
        proxyUrl.protocol = 'http'
        proxyUrl.host = 'localhost:41184'
        const proxy = await fetch(proxyUrl.toString())
        return proxy.body
      }
    )
    
    createServer(toNodeHandler(app)).listen(3000, '0.0.0.0')
    

    五、完善启动脚本

    {
      "type": "module",
      "scripts": {
        "proxy": "node --experimental-strip-types index.ts",
        "joplin": "joplin server start",
        "start": "npm run proxy & npm run joplin"
      },
      "dependencies": {
        "h3": "^2.0.1-rc.2",
        "joplin": "^3.4.1",
        "node-cron": "^4.2.1"
      },
      "devDependencies": {
        "@types/node": "^24.7.1"
      }
    }
    

    六、后续

    后面其实就简单了,感觉直接开放到公共网络不是很安全,最好还是通过容器间通信使用,真正的权限鉴定放在h3这一层。