个人博客搭建全记录03-更优雅的更新和部署

写在前面

前文已经介绍过,笔者使用 webhooks 的方式更新博客,本地 push 仓库,Gitee 会通知云服务器,云服务器运行脚本自动 pull 仓库打包,但是这个方法遇到各种各样的问题需要手动解决,实在不够优雅,为了能一键更(tou)新(lan),找到了一个目前为止还算简洁的方法。

懒惰是人类第一生产力

一、遇到的问题

1.git 部署

在云服务器需要安装 git,手动配置密钥,不够轻量化,

2.新项目部署

要部署新项目,在服务器还得手动 clone 一遍,仓库和服务器也要再手动设置设置一遍 webhooks

3.错误不可见

笔者遇到过本地 push 新代码,项目站始终没有更新,手动查看日志和 build 产物,发现 pull 的仓库不完整,检查发现主题是项目的子仓库,需要合并到主仓库。

如果遇到其他错误依然要手动到服务器检查,项目构建带来诸多不便

二、新的方案

因为目前 webhooks 方案出现的问题都是在云端构建的过程中,所以直接在本地构建,再上传构建产物到服务器,这里就要使用 ssh2-sftp-client 这个包了,可以直接使用 js 脚本 ssh 链接服务器

GitHub

NPM

1.安装包

pnpm i ssh2-sftp-client

2.ssh 配置

在根目录新建deploy.config.jsssh 配置文件

export default {
  host: 'xxx.xxx.xxx.xxx', // 服务器公网地址
  port: 22, // 默认SSH端口
  username: 'root', // 服务器用户名
  // password: 'xxx', // 服务器密码
  privateKey: fs.readFileSync('/path/to/key'), // 使用密钥

  // 本地打包后的目录
  localPath: './dist',

  // 服务器上的部署目录 (请确保该目录已存在)
  remotePath: '/www/wwwroot/my_blog',
}

为了安全起见,我的服务器关闭了密码登录,所以使用密钥登录

3.编写脚本

在项目根目录新建scripts文件夹,该文件夹下新建deploy.js

引入配置文件和包

const path = require('path')
const Client = require('ssh2-sftp-client')
const config = require('../deploy.config.js')

实例化

const sftp = new Client()

// 获取 Hexo 生成的静态文件目录(默认 public)
const localBuildDir = path.resolve(__dirname, '../public')

上传的函数

async function main() {
  try {
    console.log('⏳ 正在连接服务器...')
    await sftp.connect(config)
    console.log('✅ 服务器连接成功')

    console.log(`🧹 清理远程目录: ${config.remotePath}`)
    const remoteExists = await sftp.exists(config.remotePath)
    if (remoteExists) {
      // 递归删除远程目录后再重新创建,确保干净上传
      await sftp.rmdir(config.remotePath, true)
    }
    await sftp.mkdir(config.remotePath, true)

    console.log(`📂 准备上传: ${localBuildDir} -> ${config.remotePath}`)

    // 开始上传目录
    sftp.on('upload', (info) => {
      console.log(`Uploading: ${info.source}`)
    })

    // uploadDir 会自动覆盖同名文件
    await sftp.uploadDir(localBuildDir, config.remotePath)

    console.log('🎉 发布成功!')
  } catch (err) {
    console.error('❌ 发布失败:', err.message)
  } finally {
    sftp.end()
  }
}

注意:

在链接服务器之后先清除原有的目录,避免奇奇怪怪的问题

完整代码

const path = require('path')
const Client = require('ssh2-sftp-client')
const config = require('../deploy.config.js')

const sftp = new Client()

// 获取 Hexo 生成的静态文件目录(默认 public)
const localBuildDir = path.resolve(__dirname, '../public')

async function main() {
  try {
    console.log('⏳ 正在连接服务器...')
    await sftp.connect(config)
    console.log('✅ 服务器连接成功')

    console.log(`🧹 清理远程目录: ${config.remotePath}`)
    const remoteExists = await sftp.exists(config.remotePath)
    if (remoteExists) {
      // 递归删除远程目录后再重新创建,确保干净上传
      await sftp.rmdir(config.remotePath, true)
    }
    await sftp.mkdir(config.remotePath, true)

    console.log(`📂 准备上传: ${localBuildDir} -> ${config.remotePath}`)

    // 开始上传目录
    sftp.on('upload', (info) => {
      console.log(`Uploading: ${info.source}`)
    })

    // uploadDir 会自动覆盖同名文件
    await sftp.uploadDir(localBuildDir, config.remotePath)

    console.log('🎉 发布成功!')
  } catch (err) {
    console.error('❌ 发布失败:', err.message)
  } finally {
    sftp.end()
  }
}

main()

package.json 下新增

"deploy:sftp": "hexo clean && hexo generate && node scripts/deploy.js",
"fastcommit": "git add . && git commit -m 'update' && git pull --rebase && git push origin master",
"fastdeploy": "pnpm fastcommit && pnpm deploy:sftp"

这样直接运行就可以实现一键上传 build 产物和提交远程仓库了

注意:

如果项目要提交远程仓库,记得把 deploy.config.js 忽略

4.测试

pnpm fastdeploy

三、其他杂项

文章模板修改

为了新建文章方便,可以修改 Hexo 文章模板,位置在/scaffolds/post.md

Front-matter 选项中的所有内容均为非必填的。至少填写 titledate 的值。

配置选项 默认值 描述
title Markdown 的文件标题 文章标题,强烈建议填写此选项
date 文件创建时的日期时间 发布时间,强烈建议填写此选项,且最好保证全局唯一
author _config.yml 中的 author 文章作者
img featureImages 中的某个值 文章特征图,推荐使用图床(腾讯云、七牛云、又拍云等)来做图片的路径.如: http://xxx.com/xxx.jpg
top true 推荐文章(文章是否置顶),如果 top 值为 true,则会作为首页推荐文章
hide false 隐藏文章,如果hide值为true,则文章不会在首页显示
cover false v1.0.2版本新增,表示该文章是否需要加入到首页轮播封面中
coverImg v1.0.2版本新增,表示该文章在首页轮播封面需要显示的图片路径,如果没有,则默认使用文章的特色图片
password 文章阅读密码,如果要对文章设置阅读验证密码的话,就可以设置 password 的值,该值必须是用 SHA256 加密后的密码,防止被他人识破。前提是在主题的 config.yml 中激活了 verifyPassword 选项
toc true 是否开启 TOC,可以针对某篇文章单独关闭 TOC 的功能。前提是在主题的 config.yml 中激活了 toc 选项
mathjax false 是否开启数学公式支持 ,本文章是否开启 mathjax,且需要在主题的 _config.yml 文件中也需要开启才行
summary 文章摘要,自定义的文章摘要内容,如果这个属性有值,文章卡片摘要就显示这段文字,否则程序会自动截取文章的部分内容作为摘要
categories 文章分类,本主题的分类表示宏观上大的分类,只建议一篇文章一个分类
tags 文章标签,一篇文章可以多个标签
keywords 文章标题 文章关键字,SEO 时需要
reprintPolicy cc_by 文章转载规则, 可以是 cc_by, cc_by_nd, cc_by_sa, cc_by_nc, cc_by_nc_nd, cc_by_nc_sa, cc0, noreprint 或 pay 中的一个

注意:

  1. 如果 img 属性不填写的话,文章特色图会根据文章标题的 hashcode 的值取余,然后选取主题中对应的特色图片,从而达到让所有文章的特色图各有特色
  2. date 的值尽量保证每篇文章是唯一的,因为本主题中 GitalkGitment 识别 id 是通过 date 的值来作为唯一标识的。
  3. 如果要对文章设置阅读验证密码的功能,不仅要在 Front-matter 中设置采用了 SHA256 加密的 password 的值,还需要在主题的 _config.yml 中激活了配置。有些在线的 SHA256 加密的地址,可供你使用:开源中国在线工具chahuo站长工具
  4. 您可以在文章 md 文件的 front-matter 中指定 reprintPolicy 来给单个文章配置转载规则

最全示例

---
title: typora-vue-theme主题介绍
date: 2018-09-07 09:25:00
author: 赵奇
img: /source/images/xxx.jpg
top: true
hide: false
cover: true
coverImg: /images/1.jpg
password: 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92
toc: false
mathjax: false
summary: 这是你自定义的文章摘要内容,如果这个属性有值,文章卡片摘要就显示这段文字,否则程序会自动截取文章的部分内容作为摘要
categories: Markdown
tags:
  - Typora
  - Markdown
---