个人博客搭建全记录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 选项中的所有内容均为非必填的。至少填写 title 和 date 的值。

配置选项 默认值 描述
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 的值尽量保证每篇文章是唯一的,因为本主题中 Gitalk 和 Gitment 识别 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
---