# 技术详解: 利用CI同步文章以及多端发布

news2024/12/23 3:45:07

Image

技术详解: 利用CI同步文章以及多端发布

  • 技术详解: 利用CI同步文章以及多端发布
    • 前言
    • 文章的同步
    • 实现的细节
      • 思路
      • 文章元数据的定义和提取
      • 修改文章的优化
      • 本地图片资源上传CDN并替换本地link
    • 终于到了 CI 的部分了
    • 最后来一些碎碎念

前言

前几天我更新了一篇简单技术总结之后,不少人都对里面的技术细节感兴趣,问我具体是怎么实现的?

于是为了给群友答疑解惑,接下来陆续会聊聊我自己的实现,这篇文章的这个方案已经运行了很多年了,很久没有更新,居然有一天会被人问到,也是比较欣喜的。当然,出于自身水平的限制,目前肯定有更好的方案,所以也欢迎提出建议和意见。

文章的同步

今天先来解答小伙伴第一个小问题,文章同步CI。

目前我实现的效果是,在一个 github私有仓库里,写文章,格式为 markdown,提交到云端后,就自动呈现在我的博客网站和博客小程序里。

同时,如果要对已经存在的文章,进行内容上的修改,或者隐藏和显示,所做的也只需要修改 markdown 文件内容,然后 git commit & push 就可以了。

实现的细节

思路

看到这个方案,下意识就想到了一种实现思路:即我们只需要上传文章内容到数据库,然后在 h5weapp 应用里面,各自装一个适配的 markdown 渲染器,再各自调用后端接口,获取内容直接渲染呈现就行。

于是立即动手开干,先设计数据库,再实现一下后端,最后再写个前端,然后就实现完成了。在实现的时候就会发现,创建一篇文章,并保存到数据库是很容易的,流程无非就是 fs 读取一下内容,提交到 createArticle 接口就完成了。然而,这个方案并没有解决文章的修改和删除问题。

那么,如何让修改后的文章pushremote的时候,也让程序知道是哪一篇文章被修改了,从而进行相应的数据库操作呢?显然基于文章标题或者内容的查询都是不妥的,因为它们都有被修改的可能。

所以这种情况下,我们必须给每一篇文章,设置一个唯一的 UNIQUE ID,这个 UNIQUE ID 不必是数据主键,只需要给它一个唯一性的约束即可。

新的问题接踵而至,这个 UNIQUE ID 应该在哪里进行体现,从而让我们写的文件扫描读取脚本,在获取内容的同时获取到它呢?

文章元数据的定义和提取

这个问题第一眼,我们可能会想到这样的解决方案:这些信息可以体现在文件的名称上呀!比如我们写了一篇文章,文件名叫 如何让霸道富婆爱上我_20230302_520_true.md,我们约定文件名格式为:${title}_${date}_${unique_id}_${valid}.md。这样在扫描的时候,除了获取文件的内容之外,还可以获取到这些信息,再把它们插入到数据库表里,这样似乎就解决了修改和隐藏显示的问题了!

然而这个方案却有一个很大的缺陷,即扩展性很差。这体现在,一旦文件元数据(metadata)多起来,很有可能会出现这样的文件名: 论答辩自产自销造就新时代经济永动机_20230303_666_true_1_999_fuck_your.md,这种文件名称,本身就是一坨答辩。而且每次要修改元数据提交,还会被 git 认为是新创建的文件,显然不好。

所以,我们不应该从文件名提取内容,而应该把这些文章的元数据,放在 md 文件内容中的一块指定区域里,比如放在文章开头。再通过 json/yml 这种格式,来把文章的元数据体现出来。然后再用特殊的分隔线,分隔元数据区域和内容区域。这样我们就可以在代码里,对文件内容块进行各自的处理,元数据区域用 JSON.parse/ js-yaml parse 进行解析,内容区域以字符串形式处理。这有没有让你想到 *.vue 文件的 templatescriptstyle 区域块?

当然这个markdown提取分离方式,我之前找的时候也发现 gray-matter 这个 npm 包可以满足这样的需求。所以推荐使用它,将它加入你的文件扫描脚本中,去提取和分离元数据。

这里也给出一个示例:

---yml
unique_id: 20220330
title: 'icebreaker的垃圾话学习指南'
date: 2022-03-30
description: '不得不学会的垃圾话'
authors: 
  - icebreaker
tags: 
  - 'Trash-talk'
---

# icebreaker的垃圾话学习指南

...{{content}}...

修改文章的优化

按照上面的做法,我们看似解决了 CRUD 的问题,但是实现后我们会发现,这个做法效率太低了。比如你现在有 1000 篇文章,你改了 500 篇,难道你要让服务端依次把这 1000 篇的文章所有的元数据和内容,一个一个从数据库里取出来,然后和你 git 仓库里的文章,一个字段,一个字段进行比对,然后再把有改动的记录下来,执行 UPDATE 语句?

试想一下,你写了一本 绘声绘色的小黄书 准备发布,好几万个字,发布完了,一分钟后违规通知接踵而至,你需要修改内容。你改完之后,提交发布了,服务器去数据库里,取出原先那巨大的字符串到内存里,再和你提交的另外一坨巨大字符串(也在内存里)做是否相等的判断,而且这个行为还要重复很多次。服务器回复:地铁老人手机.jpg

显然效率太低了,我们需要更精准的做法。所以我们在进行修改文章的时候,不应该拉出这么多的数据去进行比对,而是要充分利用 文件摘要算法 进行效率上的优化。

所以这里我选择 MD5,来对所有的元数据和内容做一个摘要,并且在一开始创建文章的时候,就把 Digest(摘要)存入数据库中,这样在修改的时候,就只要比较摘要是否相等这个结果,相等就跳过,不相等就执行 UPDATE

用代码实现一下,就是:

const klaw = require('klaw')
const matter = require('gray-matter')
const md5 = require('md5')
const normalizeNewline = require('normalize-newline')

async function getArticles () {
  const articles = []
  for await (const { path: klawPath, stats } of klaw(
    path.resolve(__dirname, 'path/to/articles')
  )) {
    if (path.extname(klawPath) === '.md' && stats.isFile()) {
      const str = await fs.readFile(klawPath, {
        encoding: 'utf-8'
      })
      const { content, data, orig } = matter(normalizeNewline(str))
      const digest = md5(orig)
      articles.push(createArticle({
        authors: data.authors,
        // 数据库字段叫 md5 放的就是 digest 摘要
        md5: digest,
        content,
        uniqueId: data.uniqueId,
        description: data.description,
        tags: data.tags,
        title: data.title
      }))
    }
  }
  return articles
}

这里有一个坑点,为什么需要 normalizeNewline?这源自于 winlinux/unix 这种默认的 EOL(End of Line) 的不同(其实就是老生常谈的\r\n\n问题),这会导致在不同的系统上,文件摘要计算结果的不一致。所以我们需要预先 normalize 一下,这是使用 windows 会遇到的坑点之一,之二是 BOM

接下来要做的,就是把这些文章结果进行分拣,分拣出哪些需要 insert,哪些需要 updatedelete 的。这里可以在服务端写一个同步前的预检接口,返回数据库里文章数据,构建成一个这样结构: Map<uniqueId,md5>,这样就可以把所有本地的文章和这个 Map 进行执行策略上的映射:

  • 假如这个 Map 中,没有当前文章这个 uniqueId, 则意味着这条数据要新增。
  • 假如有这个 uniqueId ,但是这个文章的 valid 标志位被设置为了 false, 则意味着要 softDelete 或者置一个状态的标识位,让它不显示。
  • 假如有这个 uniqueId,且数据是有效的(valid不为false),但是 md5 digest 的值不相等,则意味着这条数据要更新。
  • 否则直接跳过,什么都不做

以上便是本地和远程数据库同步的逻辑。

本地图片资源上传CDN并替换本地link

我们可以用 regex(正则),或者 markdown ast 进行文件内容的匹配提取,匹配 ![alt](href) 成功后,获取后面的本地引用地址,fs 读取之后,上传到你的 oss,然后你的 oss 又关联了 cdn,那对应的 cdn 地址不就有了吗?本地替换一下就行。

推荐有闲钱的同学,可以这样搞。不想花钱的,可以去使用一些免费图床。

终于到了 CI 的部分了

没想到文章的 CRUD 就写了这么多的篇幅,终于到了 Github Action 的配置了,也很简单,核心就执行几段脚本,看我注释就知道都干了啥:

name: 'sync-article'

on:
  # 允许手动触发
  workflow_dispatch:
  # 只有 main 分支的 content 下有 md 文件改动,才触发
  push:
    branches:
      - main
    paths:
      - 'content/**/*.md'

jobs:
  # 同步 job
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          # node_modules 缓存 
          node-version: 16
          cache: yarn
          cache-dependency-path: 'yarn.lock'
      - run: yarn --production
      # 扫描提取仓库文章,然后对数据库进行 CRUD 操作
      - run: yarn sync
      # (可选) 重新部署 website 上传到 oss 静态网站并刷新 cdn 缓存
      - run: yarn website:deploy

最后来一些碎碎念

可能你看到我这篇文章,是在我的小程序上,其实我的小程序因为一些审核的原因,已经快一年没有更新了,同时我也关闭了评论系统,为了内容安全。

还有获取文章内容的接口,一定要用 token 保护好,入参也尽量使用uuid这种无法找到规律的字符串,不然很容易就被爬接口一下子全爬下来了(当然直接抓html还是可以的,你可以放一些垃圾在里面)。

还有,假如你使用的是那种,nextjs/nuxtjs 里一些基于文件系统的CMS npm包(比如@nuxtjs/content),然后你还要开源的话,你应该把你的文章放入另外一个私有仓库,然后通过git submodule 把它添加进来,接着在运行和部署的时候,通过软连接,放入这类内容CMS包的指定文件夹中,这点 Github Action 也是能做到的。

      - name: Checkout Self Repo
        uses: actions/checkout@v3
        with:
          submodules: 'true'
          token: ${{ secrets.PAT }}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/386013.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用C语言写一个自己的shell-Part Ⅲ--built-in commands

Part Ⅲ–Built-in commands Just as we have mentioned in part Ⅱ&#xff0c;the exec family of functions can’t perform built-in commands like cd. It’s like The reason for this is that cd is not a system command like ls or pwd.We need to write and inv…

云原生下最火的API网关-APISIX

文章目录一、APISIX是什么&#xff1f;二、APISIX有哪些功能&#xff1f;三、APISIX对比Spring Cloud Gateway、Zuul等其他网关有哪些优势&#xff1f;四、从0到1部署APIXSIX步骤1&#xff1a;准备环境步骤2&#xff1a;安装依赖步骤3&#xff1a;安装APISIX步骤4&#xff1a;配…

成都待慕电商:抖音虚假宣传虚构被比较价格违规细则

为了保护抖音消费者权益&#xff0c;规范创作者商品分享推广秩序&#xff0c;抖音平台制定《「虚假宣传-虚构被比较价格」违规细则》。 来看详细内容&#xff1a;一、什么是“被比较价格”&#xff1f;被比较价格&#xff1a;指创作者通过价格比较的方式宣传商品价格优惠时&…

opencv:运用cv2给视频加水印

前一段时间学了一下计算机视觉的相关知识,今天想了想,觉得可以利用cv2这个模块给视频加上水印,这样从一些方面也可以表明这个视频的原创。 1. 实现原理 小编的实现原理就是使用cv2模块读取视频文件,然后获取到每一张图片,在图片上的相应位置写上相应的字符串(小编最初打算…

MR虚拟直播是什么?

阿酷tony / 2023-3-3 / 长沙MR虚拟直播是通过机器人、虚拟人物、虚拟舞台或虚拟现实技术&#xff0c;将主播和场景实时转化的互动直播方式。MR技术&#xff08;混合现实技术&#xff09;结合大数据、人工智能等技术&#xff0c;可以在虚拟直播中实现更多的自由度和互动性&#…

认识BUG

如何描述 bug一个合格的 bug 描述应该包括以下几个部分&#xff1a;发现问题的版本开发人员需要知道出现问题的版本&#xff0c;才能够获取对应版本的代码来重现故障&#xff0c;并且版本的标识也有利于统计和分析每个版本的质量。问题出现的环境环境分为硬件环境和软件环境&am…

GUI 之 Tkinter编程

GUI 图形界面&#xff0c;Tkinter 是 Python 内置的 GUI 库&#xff0c;IDLE 就是 Tkinter 设计的。 1. Tkinter 之初体验 import tkinter as tkroot tk.Tk() # 创建一个窗口root.title(窗口标题)# 添加 label 组件 theLabel tk.Label(root, text文本内容) theLabel.p…

学习使用Android开发者者文档

Android Lint 错误信息中可看到不兼容代码所属的API级别。也可以在ANdroid开发者文档中查看各API级别特有的类和函数。 越早熟悉使用开发者文档越有利于开发&#xff0c;当然我们不可能记住所有的Android SDK中海量信息&#xff0c;因此学会查阅SDK文档&#xff0c;不断学习新的…

ContextLoaderListener监听器和SSM整合

ContextLoaderListener监听器Spring提供了监听器ContextLoaderListener&#xff0c;实现ServletContextListener接口&#xff0c;可监听ServletContext的状态&#xff0c;在web服务器的启动&#xff0c;读取Spring的配置文件&#xff0c;创建Spring的IOC容器。web应用中必须在w…

关于linux采用桥连接网络模式

关于linux&#xff08;centos&#xff09;采用桥连接网络模式 下载安装VmWare&#xff0c;并创建centos虚拟机 找到自己的虚拟机&#xff0c;点击编辑虚拟机设置-网络适配器-桥接模式 点击编辑-虚拟网络编辑器 点击更改设置-自动桥接 进入系统-修改网络配置文件 #进入到…

一个使用 react+vite3+ts+react-router-dom6v Hooks Admin搭建的轻量级后台管理模板。

react18-vite3-ts-antd4react-router-dom6v 前言 之所以搭这个模板&#xff0c;对于工作上业务需求老是变来变去&#xff0c;就觉得很烦&#xff0c;干脆搭了个admin模板&#xff0c;这样自己熟悉&#xff0c;好根据业务的需求进行一个修改。很多人会说后端管理系统模板都差不…

一文看懂REE OS、TEE OS、CA以及TA概念、架构、流程

目录 一、概念 二、使能方式 三、TEE软件框架 四、TEE软件流程 一、概念 REE&#xff08;Rich Execution Environment&#xff09;&#xff1a;比如Android系统&#xff0c;是一个开放的环境&#xff0c;容易收到恶意软件的攻击&#xff0c;比如敏感数据被窃取、数字版权被…

Mask R-cnn 代码运行报错总结

Mask R-cnn 代码运行报错总结环境版本1. 数据集下载与参数配置2. 运行报错开始报错1报错2报错3报错4报错5报错6参考文章 文章1文章2 环境版本 TensorFlow 2.1.0Python 3.7keras 2.3.1 1. 数据集下载与参数配置 下载链接 https://github.com/matterport/Mask_RCNN/releases …

MySQL存储引擎详解及对比和选择

什么是存储引擎&#xff1f; MySQL中的数据用各种不同的技术存储在文件(或者内存)中。这些技术中的每一种技术都使用不同的存储机制、索引技巧、锁定水平并且最终提供广泛的不同的功能和能力。通过选择不同的技术&#xff0c;你能够获得额外的速度或者功能&#xff0c;从而改善…

Java关键字、标识符、变量数据类型

文章目录关键字标识符标识符的命名规则标识符的命名规范变量变量的数据类型整数类型浮点类型浮点型精度字符类型布尔类型关键字 定义&#xff1a;被 Java 语言赋予了特殊含义&#xff0c;用做专门用途的字符串&#xff08;或单词&#xff09;HelloWorld 案例 中&#xff0c;出现…

7年测试工程师,裸辞掉17K的工作,想跳槽找更好的,还是太高估自己了....

14年大学毕业后&#xff0c;在老师和朋友的推荐下&#xff0c;进了软件测试行业&#xff0c;这一干就是7年时间&#xff0c;当时大学本来就是计算机专业&#xff0c;虽然专业学的一塌糊涂&#xff0c;但是当年的软件测试属于新兴行业&#xff0c;人才缺口比较大&#xff0c;而且…

南卡Neo骨传导运动耳机正式发布,打造音质最强款骨传导耳机

最近中国专业骨传导领先品牌NANK南卡发布全新Neo系列骨传导运动耳机&#xff0c;全新来袭的南卡Neo骨传导运动耳机主打音质使用体验&#xff0c;耳机配置上做到更为强劲升级优化&#xff0c;支持一体化机身&#xff0c;首发无线充设计&#xff0c;IPX6等级防水&#xff0c;升级…

Android---进程间通信机制2

Service Manager(SM)&#xff1a;大管家。管理系统服务的 Ibinder。 1 如何启动 service_manager 服务 SM注册&#xff1a; 1 binder_open(): 打开驱动(设置大小128K)&#xff0c;内存映射 2 binder_become_context_manager(): 设置 SM 为大管家 --- sm 作用&#xff1a;为…

【Python学习笔记】第二十六节 Python PyMySQL

一、什么是 PyMySQL&#xff1f;PyMySQL 是在 Python3.x 版本中用于连接 MySQL 服务器的一个库。可以用它来连接Python和MySQL。如果你追求速度&#xff0c;这是一个很好的选择&#xff0c;因为它比mysql-connector-python快。PyMySQL 遵循 Python 数据库 API v2.0 规范&#x…

【存储】存储特性

存储特性精简配置技术&#xff08;SmartThin&#xff09;SmartThin主要功能容量虚拟化存储空间写时分配&#xff1a;Capacity-on-Write读写重定向&#xff1a;Direct-on-Time应用场景及配置流程存储分层技术&#xff08;SmartTier&#xff09;存储分层工作原理关键技术容量初始…