低成本的静态网站部署

2021 06 17, Thu

之前提到了Gatsby这类生成静态网站的工具,我现在的博客也用了Gatsby来生成页面。但是对有的同学来说,没有地方去部署自己的博客、项目网站,是一个略微麻烦的问题。稍微列举几种我能想到的方法,仅供参考。

现有的托管网站

Pages类服务

Github提供了一个服务叫Github Pages,是一个自动构建并且部署静态网站的服务,非常多的同学用Github Pages搭建了自己的第一个技术博客。但是Github Pages的可访问性比较离谱,受限于出口带宽速度也比较慢,所以墙内也有很多类似的服务,比如说Gitee PagesCoding静态网站服务Gitlab PagesBitbucket的静态托管(没有名字的服务)……

但是就我用过的托管站点,都有自己的局限性,比如说构建过程只支持特定的博客工具。属于比较简单,没有成本,但是局限性比较大的部署途径。其中大部分站点也都不是专门的静态托管站,所以在访问速度或者某些方面多多少少有一些短板。

此外,在 写这篇 post 的时间点 ( 2021-6-16 ) ,Gitee Pages处于降级的状态,不是完全不能用,但是受政策限制,现在更新博客都需要工作人员介入,速度非常慢。这也是我写这篇post的原因,希望大家可以尽快解决问题,同时希望大家能意识到Gitee这类代码托管并不是专门的静态页面托管站,不要过于依赖这类服务。

网站托管服务 / 虚拟主机

到现在为止还有非常多的php站点,是通过虚拟主机类服务部署的。这类服务大都是为了Wordpress之类的站点服务,不过部署静态页面也是足够的。虽然说我们的静态网站用不到,但是免费版服务可能会附带一个容量比较小的数据库。

比较有名的有比如说Hostinger啊这些,很多云服务商也会提供类似的服务,比如说腾讯云的静态网站托管。不过我觉得没有什么必要……毕竟我们可以用下面这种方法。

对象存储

我自己用的方法是把构建好的静态资源放到对象存储中,然后配置CDN来通过域名访问网站。这种站点唯一的缺点是无法在服务器端执行脚本,但是托管博客啊、文档啊、html5小游戏啊,绰绰有余了。一般都会用同一家的对象存储加上CDN。

国外的这种服务我知道的有Google Cloud的Cloud Storage搭配LBAWS。这两家都有免费的额度。对于Google Cloud来说,有一些LB的区域国内也是可以访问的。

国内类似的服务我知道的就有七牛云(免费的存储和HTTP流量,HTTPS需要付少量费用)、阿里云腾讯云华为云等。

这类托管的特点就是,便宜,可以自动化,不能执行服务器端脚本,无法利用数据库。但是对于很多网站来说够用了。

使用对象存储搭建网站

用对象存储配置网站其实也很简单:

  • 准备工作:只做一次
    • (国内必备)域名备案
    • 创建对象存储的Bucket,并配置为公开读私有写
    • (根据服务商可能可选)添加CDN域名,并配置CDN源站为刚才创建的对象存储
      • 解析域名到CDN服务
    • (可选)在CDN上配置HTTPS证书
  • 部署、更新网站
    • 构建网站
    • 上传文件到对象存储
    • (更新网站后)刷新CDN缓存

以七牛为例,部署网站

准备工作

创建bucket

首先我们创建一个对象存储的bucket,这个bucket名字自己知道就行,地区尽量离自己近一些,访问控制选择公开(也就是公开读私有写)。

创建bucket

添加CDN域名

然后七牛要求CDN必须用我们自己的域名,它给的默认的域名只能用来初期测试用,所以我们添加一个CDN域名

添加域名

由于是测试站点,我就没有用https,需要的话七牛可以免费申请,只是流量要钱,但是大部分站点真的用不了多少流量。

添加好了以后会给我一个域名解析的CNAME,只要配置到域名上就行。

CNAME

解析域名

我的域名在腾讯云。是已经备案过的。直接解析即可

CNAME

每个人购买域名的服务商不一样,具体的可能需要参考自己服务商的流程。

部署网站

构建网站

我的网站是Gatsby构建的。所以用yarn build就可以把网站构建好,放在public目录中。

guochao ~/Workspace/demo.jeffthecoder.xyz (git)-[master] % yarn build                 
yarn run v1.22.1
$ gatsby build
success open and validate gatsby-configs - 0.087s
warn Plugin gatsby-plugin-draft is not compatible with your gatsby version 3.2.1 - It requires gatsby@^2.0.0
warn Plugin gatsby-plugin-draft is not compatible with your gatsby version 3.2.1 - It requires gatsby@^2.0.0
success load plugins - 1.367s
success onPreInit - 0.036s
success initialize cache - 0.006s
success copy gatsby files - 0.101s
......
success onPostBuild - 0.090s
info Done building in 30.670324559 sec

✨  Done in 31.07s.
yarn build  77.69s user 6.59s system 269% cpu 31.263 total

如果你用的是vuepress之类的工具,或者是其他的静态站生成器,那就用对应的工具生成站点就行。

上传到对象存储

七牛提供了命令行工具,qshell,可以上传文件到对象存储。命令行工具需要的key已经通过官方的方法配置好了。

我习惯用json配置的qupload:

$ guochao ~/Workspace/demo.jeffthecoder.xyz (git)-[master] % cat .qiniu-sync.json
{
  "src_dir": "public",
  "bucket": "demo-jeffthecoder",
  "overwrite": true,
  "check_exists": true,
  "check_hash": true,
  "rescan_local": true
}
$ guochao ~/Workspace/demo.jeffthecoder.xyz (git)-[master] % qshell qupload .qiniu-sync.json

其他的对象存储应该也有自己的SDK或者工具,用正常的方法上传下载就行。

注意大部分对象存储是没有目录概念的,有的工具会把目录创建成文件,导致无法正常访问站点。

更新CDN缓存

类似的也可以用qshell:

$ guochao ~/Workspace/demo.jeffthecoder.xyz (git)-[master] % cat .qiniu-refresh-dirs.txt
http://demo.jeffthecoder.xyz/
$ guochao ~/Workspace/demo.jeffthecoder.xyz (git)-[master] % qshell cdnrefresh --dirs -i .qiniu-refresh-dirs.txt

也有网页版:

CNAME

稍等片刻,等待操作记录中,操作完成后,就可以访问到你新的页面了。

其他的服务商操作类似。比如说腾讯云:

CNAME

高端局:懒人玩法 -> CI/CD & Git hooks

这样部署和Github Pages比起来看起来笨重了很多,需要自己操作很多,尤其是更新博客的这里,Pages只要推一次仓库就会自动构建,而上面的步骤很多都要我们自己来做。

那……我是个懒人,我选择躺平……肯定不是啦。Github Pages是一种非常简单的服务,我们完全可以自己做一个,而且不费多少事情。

Github Pages做了什么

GitHub Pages实际上是Github的CI/CD结合静态托管构建的一个服务,整个流程非常简单:

  • 判断是什么站点
  • 构建
  • 部署静态文件
  • 刷新CDN

看上去是不是很像上面的流程?我们完全可以自己利用各种Git服务的CICD搭建一个类似的

利用Gitee Go自动部署静态博客

我们在这里需要稍微写一点脚本。逻辑就是遍历目录,然后逐个上传文件。

安装依赖

我自己用了一点Gatsby的插件,这些插件在npm安装时很可能会拉源码编译安装,所以我需要写一个脚本安装一些构建用的库。这些步骤你们可以自己确认需要不需要。

set -x

handle_centos() {
    yum install -y epel-release https://rpms.remirepo.net/enterprise/remi-release-7.rpm
    yum-config-manager --enable remi
    yum install -y ImageMagick-devel libpng-devel make gcc-c++ file nasm vips-devel
}

handle_ubuntu() {
    apt update
    apt install -y libvips-dev libvips-tools libpng-dev file nasm
}

handle_alpine() {
    apk update && apk add vips-dev libpng-dev file nasm
}

if [ -r /etc/os-release ]; then
    source /etc/os-release
    case "${ID}" in
        centos)
            handle_centos
            ;;
        ubuntu)
            handle_ubuntu
            ;;
        alpine)
            handle_alpine
            ;;
        *)
            echo "detected unsupported system: ${ID}"
            exit -1
            ;;
    esac
else
    if command apt &> /dev/null; then
        handle_ubuntu
    elif command apk &> /dev/null; then
        handle_alpine
    elif command yum &> /dev/null; then
        handle_centos
    else
        echo "unknown platform"
        exit -1
    fi
fi

上传用的脚本

如果是基于javascript的项目,可以npm或者yarn安装qiniu这个SDK包。

const qiniu = require("qiniu");
const fs = require("fs");
const path = require("path")

var mac = new qiniu.auth.digest.Mac(process.env.ACCESSKEY, process.env.SECRETKEY);



var config = new qiniu.conf.Config();
var formUploader = new qiniu.form_up.FormUploader(config);

var cdnManager = new qiniu.cdn.CdnManager(mac);

let files = [];
let root = path.resolve(process.env.WWWROOT);
const walkIntoDir = (curdir) => {
    var entries = fs.readdirSync(curdir);
    entries.forEach((entry) => {
        let fullpath = path.join(curdir, entry);
        var stat = fs.statSync(fullpath);
        if (stat.isDirectory()) {
            walkIntoDir(fullpath);
        } else if (stat.isFile()) {
            let key = path.relative(root, fullpath);
            files.push({
                fullpath,
                key,
            });
        }
    });
};
walkIntoDir(root);

let uploadAsync = (key, fullpath) => {
    return new Promise((resolve, reject) => {
        var options = {
            scope: `${process.env.BUCKET}:${key}`,
        };
        var putPolicy = new qiniu.rs.PutPolicy(options);
        var uploadToken = putPolicy.uploadToken(mac);
        var putExtra = new qiniu.form_up.PutExtra();
        // 文件上传
        formUploader.putFile(uploadToken, key, fullpath, putExtra, function (respErr, respBody, respInfo) {
            if (respErr) {
                throw respErr;
            }

            if (respInfo.statusCode == 200) {
                resolve(`${key} => ${fullpath}: OK`);
            } else {
                reject(`${key} => ${fullpath}: Err(${respInfo.statusCode}): ${JSON.stringify(respBody)}`);
            }
        });
    });
};

let retries = {};
(async () => {
    while (true) {
        let file = files.pop();
        if (!file) {
            return;
        }
        let { key, fullpath } = file;
        try {
            console.log(await uploadAsync(key, fullpath));
        } catch (e) {
            console.log(e);
            const already_retried = retries[key] || 0;
            console.log(`this is the ${already_retried + 1} time`);
            if (already_retried >= 3) {
                continue;
            }
            console.log(`will retry later`);
            files.unshift(file);
        }
    }
})().then(() => {
    cdnManager.refreshDirs(process.env.REFRESHDIR.split(","), (...args) => {
        console.log(...args);
    });
})

在package中的scripts中加上deploy

{
  // ...
  "scripts": {
    "develop": "gatsby develop",
    "start": "gatsby develop",
    "build": "gatsby build",
    "serve": "gatsby serve",
    "clean": "gatsby clean",
    "lint": "prettier --write src/**/* ./*.json ./*.js",
    "postinstall": "husky install",

    "deploy": "node scripts/deploy.js"
  },
  // ...
}

脚本里面用了环境变量,可以用命令行直接测试一下。

七牛的ACCESSKEY和SECRETKEY需要从自己的页面获取到

测试一下。Unix下面直接运行

$ guochao ~/Workspace/demo.jeffthecoder.xyz (git)-[master] % env ACCESSKEY=你账户的ACCESSKEY SECRETKEY=你账户的SECRETKEY BUCKET=你的bucket名字 WWWROOT=构建出来的文件在什么目录里面 REFRESHDIR="https://blog.jeffthecoder.xyz/" node scripts/deploy.js

文件多的话需要你自己改一下脚本,并行上传。

Gitee Go的Workflow

在.workflow中加入一个yaml文件用作gitee go的流(我现学现卖复制的)

注意其中,我设置了大量的nodejs的源,同学使用的时候可以自己复制需要的就行。我就是举个例子顺便偷个懒

# ========================================================
# npm 构建参考流水线样例
# 功能:输出当前 npm 构建环境的环境信息
# ========================================================
name: gitee-go-deploy-demo                # 定义一个唯一 ID 标识为 gitee-go-npm-example,名称为 “npm-流水线示例” 的流水线
displayName: 'npm-构建并部署demo'
triggers:                                 # 流水线触发器配置
  push:                                   # 设置 master 分支 在产生代码 push 时精确触发(PRECISE)构建
    - matchType: PRECISE
      branch: master
commitMessage: ''                         # 通过匹配当前提交的 CommitMessage 决定是否执行流水线
stages:                                   # 构建阶段配置
  - stage:                                # 定义一个 ID 标识为 npm-build-stage ,名为 “npm Stage” 的阶段
      name: npm-build-stage
      displayName: 'Deploy'
      failFast: false                     # 允许快速失败,即当 Stage 中有任务失败时,直接结束整个 Stage
      steps:                              # 构建步骤配置
        - step: npmbuild@1                # 采用 npm 编译环境
          name: npm-build                 # 定义一个 ID 标识为 npm-build ,名为 “npm Step” 的阶段
          displayName: 'npm run deploy'
          inputs:                         # 构建输入参数设定
            nodeVersion: 14.15             # 指定 node 环境版本为 14.15
            goals: “bash scripts/install-deps.sh && npm install cnpm -g --registry=https://registry.npm.taobao.org &&  cnpm install && npm run build && npm run deploy"

Gitee服务器在境内,构建需要用到各种工具的镜像,我们用cnpm简单替代一下。

环境变量

因为部署的时候需要从Gitee访问七牛,在代码中硬编码密钥是不好的选择,所以我用了环境变量来配置这些。

Gitee环境变量

只要ACCESSKEY和SECRETKEY是密钥就行了,其他的无所谓。

更新你的网站

接下来,我们只要更新你的仓库就可以正常更新博客啦!

顺便一提,这篇博客就是用这样的方法更新的 :)

不过Gitee Go他们这个流水线更新有点慢啊。我都放了一个小时了还在“正在申请构建资源”。什么时候构建完了我什么时候再更新博客吧~

博客已经成功更新了,大部分的问题是服务器在国内,nodejs的npm不好使,换cnpm马上就好了。但是有一个任务卡在一个地方一直没有进展,80小时过去了还没有停止。如果自己遇到了记得提前和Gitee工作人员说,可以重新送一些时间的,毕竟是beta,正常的。

用了npm这类工具,从国外拉包的同学,注意一个问题:换源。博客里面的配置和脚本中我已经换过源了,晚上很久没有进度,一有进度失败就是因为node拉不下来二进制,从源码编译又缺依赖的库。

Gitee需要改进一点是,现在的情况中,一个Step不结束,输出无法显示到网页中。任务进程没有退出的时候,也应该检查stdout的管道有没有内容,有就直接记到日志里面。不要最后再输出,这样很多问题无法看到。

另一种方法:Git Hook

有的时候我们不想要Git服务去做这些事情(因为说实话我配置Gitee Go配置的有点懵,构建过程要安装很多东西就会因为网络啊依赖啊我自己的失误啊失败很多次),本地桌面环境会更好配置一些。那么有没有可能在本地自动化这些事情呢?有!那就是Git Hooks,Git的钩子脚本。我在写这篇博客之前一直都是用

所谓钩子就是在特定时机自动被调用的东西。在本地的Git仓库中(也就是通过init不带–bare和clone这两种方法创建的仓库),.git/hook中都可以放各种各样的脚本,这些脚本会在commit之前、commit之后、push之前、receive/fetch/pull之前等等时机自动执行。

那么我们要做到同样的事情,可以用和上面一模一样的脚本,在git目录中配置post-commit脚本内容为npm run build && npm run deploy,为脚本设置+x权限,然后当我们commit完成后,就会自动执行这个命令了。

如果你的脚本没有执行,需要检查这么几件事情:

  • 你的脚本是不是叫做post-commit
    • 有没有加额外的拓展名,比如说post-commit.sh
  • 如果在Unix下面,post-commit文件有没有+x的可执行文件权限,没有的话chmod +x post-commit
  • git config -l检查一下有没有core.hookspath,有的话就是被某个程序改过或者你自己改过,需要把hook放到你自己的目录下
    • 比如说像是我用了husky,那么我就需要用husky加hook:npx husky add .husky/post-commit 'npm run build && npm run deploy'