写在前面
前文已经介绍过,笔者使用 webhooks 的方式更新博客,本地 push 仓库,Gitee 会通知云服务器,云服务器运行脚本自动 pull 仓库打包,但是这个方法遇到各种各样的问题需要手动解决,实在不够优雅,为了能一键更(tou)新(lan),找到了一个目前为止还算简洁的方法。
懒惰是人类第一生产力
一、遇到的问题
1.git 部署
在云服务器需要安装 git,手动配置密钥,不够轻量化,
2.新项目部署
要部署新项目,在服务器还得手动 clone 一遍,仓库和服务器也要再手动设置设置一遍 webhooks
3.错误不可见
笔者遇到过本地 push 新代码,项目站始终没有更新,手动查看日志和 build 产物,发现 pull 的仓库不完整,检查发现主题是项目的子仓库,需要合并到主仓库。
如果遇到其他错误依然要手动到服务器检查,项目构建带来诸多不便
二、新的方案
因为目前 webhooks 方案出现的问题都是在云端构建的过程中,所以直接在本地构建,再上传构建产物到服务器,这里就要使用 ssh2-sftp-client 这个包了,可以直接使用 js 脚本 ssh 链接服务器
1.安装包
pnpm i ssh2-sftp-client2.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 中的一个 |
注意:
- 如果
img属性不填写的话,文章特色图会根据文章标题的hashcode的值取余,然后选取主题中对应的特色图片,从而达到让所有文章的特色图各有特色。date的值尽量保证每篇文章是唯一的,因为本主题中Gitalk和Gitment识别id是通过date的值来作为唯一标识的。- 如果要对文章设置阅读验证密码的功能,不仅要在 Front-matter 中设置采用了 SHA256 加密的 password 的值,还需要在主题的
_config.yml中激活了配置。有些在线的 SHA256 加密的地址,可供你使用:开源中国在线工具、chahuo、站长工具。- 您可以在文章 md 文件的 front-matter 中指定 reprintPolicy 来给单个文章配置转载规则
最全示例
---title: typora-vue-theme主题介绍date: 2018-09-07 09:25:00author: 赵奇img: /source/images/xxx.jpgtop: truehide: falsecover: truecoverImg: /images/1.jpgpassword: 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92toc: falsemathjax: falsesummary: 这是你自定义的文章摘要内容,如果这个属性有值,文章卡片摘要就显示这段文字,否则程序会自动截取文章的部分内容作为摘要categories: Markdowntags: - Typora - Markdown---



.png)

