一、简介

freecdn 是一个纯前端的 CDN 解决方案,用于降低网站流量成本,同时提高网站稳定性、安全性,并且无需修改现有的业务逻辑。

互联网上有很多免费的公共库 CDN,例如 cdnjsjsdelivrunpkg,但哪个最稳定,始终没有明确的答案。

有些国外的 CDN 虽然有大公司支持,但在国内无法确保网络稳定性;有些国内的 CDN 虽然网络稳定,但无法确保未来是否仍在维护。

这些 CDN 一旦出现问题,轻则网站打开变慢,重则功能损坏,经济损失甚至超过节省的费用。因此,不少人认为免费的才是最贵的,最终选择自己购买 CDN。

任何一个东西,既要有超低成本,又要有超高稳定性,显然是很难做到的。但是,既然成本很低,那不妨多准备几个,组成可容错的冗余系统,这样整体稳定性就变高了。

假设单个免费 CDN 的稳定性只有 90%,但事先准备 10 个,这时整体稳定性可达 99.99999999%

如何实现这样的冗余系统?这就是 freecdn 要做的事!

FreeCDN 开源地址:https://github.com/EtherDream/freecdn

1.1 实现原理

freecdn 的原理并不复杂,其核心使用了 HTML5 中一个重要的 API —— Service Worker。它是一种浏览器后台服务,能拦截当前站点产生的 HTTP 请求,并能控制返回结果,相当于给网站加了一层反向代理。有了这个黑科技,我们可以把传统 CDN 的功能搬到前端,例如负载均衡、故障切换等,通过 JS 灵活处理各种请求。

同时,网站开发者提供一个清单,记录用到的公共库以及备用 URL。

当 Service Worker 加载公共库出现问题时,可以不返回错误给上层页面,而是继续加载备用 URL,直到获得正确结果才返回。
multi-cdns

因此,只要有一个备用节点正常,资源就不会加载失败。稳定性大幅提高。

甚至,你可以选择 更激进的加载策略 —— 同时加载多个备用 URL,哪个先完成就用哪个,实现带宽换时间的效果!

对于常见的公共库,你无需自己收集备用 URL,通过工具可自动生成清单。

二、接入FreeCDN流程

2.1 引入前端脚本

  1. 安装 freecdn 命令行工具

    npm install freecdn -g
    
  2. 创建前端脚本,将在项目中生成 freecdn-loader.min.jsfreecdn-internal 目录,需要将生成的这些文件放在网站的根目录下

    freecdn js --make
    
  3. 在网页的 head 中引用。为了让用户访问任意界面都可以安装 FreeCDN ,应该在每个界面都引入,且位置越靠前越好。

    <head>
      <script src="/freecdn-loader.min.js"></script>
      ...
    

至此,博客中已经成功接入了 FreeCDN 的相关前端脚本,但是这样还不够,还需生成一个指定哪些文件有哪些访问路径的 freecdn-manifest.txt 清单文件,生成并将这个文件放在网站根目录就可以使用 FreeCDN 了。

2.2 最简方式配置

  1. 生成清单文件

    freecdn find --save
    

    得到清单文件 freecdn-manifest.txt,内容大致如下:

    /assets/chrome.png
    	https://ajax.cdnjs.com/ajax/libs/browser-logos/27.0.0/chrome/chrome_512x512.png
    	https://cdnjs.cloudflare.com/ajax/libs/browser-logos/27.0.0/chrome/chrome_512x512.png
    	hash=qRgUkISo5k/bgIWNHGfLsC8WmasnE7jYdCZvthIFLno=
    
    /assets/edge.png
    	https://ajax.cdnjs.com/ajax/libs/browser-logos/62.1.4/edge/edge_512x512.png
    	https://cdnjs.cloudflare.com/ajax/libs/browser-logos/62.1.4/edge/edge_512x512.png
    	hash=e/QvJoMnfQFLXXrHe6ZC7v8IVc6cNuL7MqTY4h/L4ZQ=
    
    ...
    

    格式很简单,每个原始文件对应多个备用文件,以及相应的参数。

生成清单文件后,将该文件一同放在网站根目录,这时访问即可看到网站已经被成功代理,效果图如下:

ServiceWorker安装状态

ServiceWorker代理了请求

2.3 加速自己的脚本文件

生成清单文件后可以看到大部分文件都不会出现在清单文件中,这是源于有些文件是我们自己编写的,在开源库中没有对应的 CDN 可以访问,还有就是 FreeCDN 的公共 CDN 库不是很齐全,一些不是很知名的开源包也会出现没找到 CDN 的情况。

这种情况可以将我们的脚本上传到 Github 因为GitHub 提供了 raw.githubusercontent.com 站点,可通过 HTTP 访问仓库中的文件,此外,还有一些第三方 CDN 也可以加速 GitHub 文件,例如 jsdelivr

由于 CDN 缓存时间很长,因此每次更新必须设置 tag,例如 0.0.1、0.0.2 递增,从而可使用不同的 URL 避开缓存。

但是自己上传的脚本文件是没办法在 FreeCDN 的公共 CDN 库中找到的,所以我们需要创建一个记录这些文件的 hash 和 url 的文件,然后将它导入到 FreeCDN 提供的本地数据库。

本文提供两种生成 hash 和 url 列表文件的方法:

通过 sh 脚本生成 hash-url.txt 文件

#!/bin/bash
USER=zjcqoo
REPO=test
VER=0.0.1

files=$(find * -type f ! -path "freecdn-*" ! -name ".*")
list=""

for file in $files; do
  hash=$(openssl dgst -sha256 -binary $file | openssl base64 -A)
  list="$list
$hash https://raw.githubusercontent.com/$USER/$REPO/$VER/$file
$hash https://cdn.jsdelivr.net/gh/$USER/$REPO@$VER/$file"
done

echo "$list" > hash-url.txt

通过 gulp 生成 hash-url.txt 文件

const fs = require("fs");
const crypto=require('crypto');
const version = '0.0.1';

task("freecdn", (done) => {
    if (version == null) {
        console.log(`[FreeCDN] No '--version' parameters are specified`)
        done();
        return;
    }
    const tempFileName = 'hash-url.txt'

    const readFile = (dir, ignoreFiles) => {
        let files = fs.readdirSync(dir, "utf-8");
        files.forEach((file, i) => {
            let filePath = dir + "/" + file
            let states = fs.statSync(filePath);
            if (ignoreFiles.length !== 0 && ignoreFiles.includes(filePath)) {
                return;
            }
            if (states.isDirectory()) {
                readFile(filePath, ignoreFiles)
            } else {
                let hash = crypto.createHash('SHA256').update(fs. readFileSync(filePath)).digest('base64')
                fs.appendFileSync(tempFileName, `${hash} https://raw.githubusercontent.com/nineya/halo-theme-dream/${version}/${filePath}\n`)
                fs.appendFileSync(tempFileName, `${hash} https://cdn.jsdelivr.net/gh/nineya/halo-theme-dream@${version}/${filePath}\n`)
            }
        })
    }

    readFile("./", ["./freecdn-internal", "./freecdn-loader.min.js"])
    
    done();
});

通以上步骤我们得到了一个 hash 和 url 的列表文件 hash-url.txt,文件内容大致如下:

XTZpbfuMkxViYSW2q370udBiH6h2xPPsbA9GLxfBfBg= https://raw.githubusercontent.com/nineya/halo-theme-dream/0.0.1/assets/video/test.mp4
XTZpbfuMkxViYSW2q370udBiH6h2xPPsbA9GLxfBfBg= https://cdn.jsdelivr.net/gh/nineya/halo-theme-dream@0.0.1/assets/video/test.mp4
L4HFRlSKt3ggTXAKWFWe5OQCyXsn+1ZWBIGdGE2g/MQ= https://raw.githubusercontent.com/nineya/halo-theme-dream/0.0.1/assets/css/main.css
L4HFRlSKt3ggTXAKWFWe5OQCyXsn+1ZWBIGdGE2g/MQ= https://cdn.jsdelivr.net/gh/nineya/halo-theme-dream@0.0.1/assets/css/main.css

将文件导入到 FreeCDN 的数据库

freecdn db --import < hash-url.txt

查看本地数据库,确定已经有了文件映射的内容

freecdn db --list

更多关于数据库内容维护的命令可查看帮助

freecdn db --help

确定 hash-url.txt 文件成功导入到数据库后,创建清单文件:

freecdn find --save

这时候再看清单文件已经有了我们自己写的脚本文件的相关映射了。

hash-url.txt 文件仅用于导入脚本文件到本地数据库,导入完成后即可删除。

2.4 加速外部资源

以上的方式加速的全部都是本地的资源文件,但是我们网站中可能引用了很多外部 CDN 的资源,我们也希望这些资源的访问能够被加速,或者说能够在网站失效时自动切换站点。

我们需要手动创建一个 urls.txt 文件,将这些外部资源的路径记录在这个文件中,并在创建清单文件时指定。这样,在创建清单文件时将会去访问这些文件,然后获取这些文件的 sha256 摘要,一同写入到清单文件中。

访问远程的文件也导致了创建清单文件的速度降低,还是建议将这些远程文件都下载到本地。

urls.txt 文件中一行一个资源路径,内容模板如下:

https://ajax.cdnjs.com/ajax/libs/jquery/3.2.1/jquery.js
https://ajax.cdnjs.com/ajax/libs/twitter-bootstrap/4.6.0/css/bootstrap.min.css
https://ajax.cdnjs.com/ajax/libs/twitter-bootstrap/4.6.0/js/bootstrap.min.js

准备好 urls.txt 文件后,创建清单文件,通过 --with-urls 参数指定 urls.txt 文件

freecdn find --save --with-urls urls.txt

urls.txt 文件仅用于生成清单文件,不需要将文件加入到站点根目录下。

2.5 其他

除了以上介绍的功能, FreeCDN 还具有 并行加载文件 等功能,本文不再介绍,可前往 FreeCDN 中查看相关的教程。

https://github.com/EtherDream/freecdn