# 引言

cdn.jsdelivr.net 知名的免费 cdn 服务提供商,在去年年底由于大陆域名备案的问题,国内 cdn 解析异常时常无法连接到服务器

尤其是重度依赖 jsDelivrShoka 主题,在这种情况下由于资源无法加载完成导致网页卡顿,显示效果异常等一系列问题

于是决定探索资源本地化方案去除对 jsDelivr 的依赖

截止到目前国内的 cdn.jsdelivr.netDNS 污染貌似已经解除了,不过资源本地化还是有很多好处的,记录下过程权当自己的备忘了。

# 资源本地化

# jsDelivr 依赖热点

  1. Shoka 主题用到了不少第三方组件,全部基于 jsDelivr 加载,资源依赖配置在了 主题配置文件 _config.shoka.ymlvendors 内。
  2. 依赖的组件又再次依赖 jsDelivr 加载依赖
    • valine: gh/amehime/MiniValine@4.2.2-beta10/dist/MiniValine.min.js 再次依赖

# 依赖资源下载

本地化的一个非常重要的部分就是,将远程资源下载到本地。好在依赖项不是很多哈,可以慢慢手动操作一波,就是慢点 🤣~~

需要注意的是,为了最小化修改,其实应该在本地享有和链接一样的目录结构,这样可以只修改加载代码而不用修改配置

我们在博客主目录 source 下新建 cdn.jsdelivr.net 文件夹,后续下载都放在这个目录下 (通过 Hexo 发布后的文件地址访问将会是 站点网址\cdn.jsdelivr.net\相对路径

  1. vendors 依赖资源下载

很多组件基本就是加载最后压缩过的单文件 *.min.js ,所以直接 py 脚本一波走,把 vendors 内的配置全部下载到本地,同时根据链接在本地创建相对的目录结构

import urllib.request
import requests
import re
import os
def get_file(url):
    """
    递归下载网站的文件
    :param url:
    :return:
    """
    if isFile(url):
        print(url)
        try:
            download(url)
        except:
            pass
    else:
        urls = get_url(url)
        for u in urls:
            get_file(u)
def isFile(url):
    """
    判断一个链接是否是文件
    :param url:
    :return:
    """
    if url.endswith('/'):
        return False
    else:
        return True
def download(url):
    """
    :param url:文件链接
    :return: 下载文件,自动创建目录
    """
    full_name = url.split('//')[-1]
    filename = full_name.split('/')[-1]
    dirname = "/".join(full_name.split('/')[:-1])
    if os.path.exists(dirname):
        pass
    else:
        os.makedirs(dirname, exist_ok=True)
    urllib.request.urlretrieve(url, full_name)
def get_url(base_url):
    """
    :param base_url:给定一个网址
    :return: 获取给定网址中的所有链接
    """
    text = ''
    try:
        text = requests.get(base_url).text
    except Exception as e:
        print("error - > ", base_url, e)
        pass
    reg = '<a href="(.*)">.*</a>'
    urls = [base_url + url for url in re.findall(reg, text) if url != '../']
    return urls
if __name__ == '__main__':
    with open('list.txt', 'r') as f:
        lines = f.readlines()
        url_list = []
        for line in lines:
            get_file(line.strip('\n'))

将需要下载的链接放在和 py 脚本同级的 list.txt 内 ( vendors 内部链接加上 https://cdn.jsdelivr.net/ )

jsDelivrResDownload

  1. 个别依赖项整包下载

个别依赖项不只是简单的需要 *.min.js 文件就够了,还需要额外依赖很多资源文件,所以对于这种我们采取手动下载整个组件源码放置到本地

目前发现有如下组件 (若有遗漏,欢迎指正!)

  • katex

下载方式:

访问页面 https://www.jsdelivr.com/package/ + npm/katex (若需下载其他组件替换这里即可) 即可前往下载页

选择对应版本下载即可,下载完成后将对应的文件解压,放置在 步骤1 的本地文件夹内即可

  1. 依赖项依赖下载

个别组件还套娃依赖其他组件,这一部分也需要处理🤣🤣🤣

目前发现的有如下组件 (若有遗漏,欢迎指正!)

  • valine

找到我们下载的 MiniValine.min.js 文件打开,全局搜索 jsdelivr ,可以发现如下依赖项

第三方组件:

https://cdn.jsdelivr.net/npm/xss@1.0.11/dist/xss.min.js
https://cdn.jsdelivr.net/npm/ua-parser-js@1.0.2/dist/ua-parser.min.js
https://cdn.jsdelivr.net/npm/mathjax@3.2.1/es5/tex-svg.js
https://cdn.jsdelivr.net/npm/marked@1.2.0/lib/marked.min.js
https://cdn.jsdelivr.net/npm/blueimp-md5@2.18.0/js/md5.min.js
https://cdn.jsdelivr.net/npm/leancloud-storage@4.12.2/dist/av-min.js

这儿略微麻烦一点, jsDelivrcombine 的功能,可以做到一次请求多个 目标文件 和批成一个文件,上面的组件以空格为分割分别分成了两个地方加载,我们需要进行如下步骤

  • 合批请求保存两批 js 文件
// 另存为 /cdn.jsdelivr.net/combine/valine/parse.js
https://cdn.jsdelivr.net/combine/npm/xss@1.0.11/dist/xss.min.js,npm/ua-parser-js@1.0.2/dist/ua-parser.min.js,npm/mathjax@3.2.1/es5/tex-svg.js,npm/marked@1.2.0/lib/marked.min.js
// 另存为 /cdn.jsdelivr.net/combine/valine/parse.min.js
https://cdn.jsdelivr.net/combine/npm/blueimp-md5@2.18.0/js/md5.min.js,npm/leancloud-storage@4.12.2/dist/av-min.js
  • 更改对应依赖的加载代码,主要是替换其中的合批下载链接为自己本地资源路径链接
(t=[],window.autosize||t.push("npm/autosize@4.0.2/dist/autosize.min.js"),window.filterXSS||t.push("npm/xss@1.0.8/dist/xss.min.js"),e.config.closeUA||window.UAParser||t.push("npm/ua-parser-js@0.7.22/src/ua-parser.min.js"),!e.math&&void 0!==e.config.math||"undefined"!=typeof MathJax||t.push("npm/mathjax@3/es5/tex-svg.js"),!e.md&&void 0!==e.config.md||window.marked||t.push("npm/marked@1.2.0/lib/marked.min.js"),(0,i.default)("https://cdn.jsdelivr.net/combine/",(function(){e.initBody(),window.MV.scriptEle=!0}),1==window.MV.scriptEle||0==t.length))
// 替换为
(t=[],window.autosize||t.push("npm/autosize@4.0.2/dist/autosize.min.js"),window.filterXSS||t.push("npm/xss@1.0.8/dist/xss.min.js"),e.config.closeUA||window.UAParser||t.push("npm/ua-parser-js@0.7.22/src/ua-parser.min.js"),!e.math&&void 0!==e.config.math||"undefined"!=typeof MathJax||t.push("npm/mathjax@3/es5/tex-svg.js"),!e.md&&void 0!==e.config.md||window.marked||t.push("npm/marked@1.2.0/lib/marked.min.js"),(0,i.default)("/cdn.jsdelivr.net/combine/valine/parse.js"+t.join(","),(function(){e.initBody(),window.MV.scriptEle=!0}),1==window.MV.scriptEle||0==t.length))
(e.config.NoRecordIP||(void 0===window.MV.ip?(0,a.default)(e):e.C.ip=window.MV.ip),t=[],window.md5||t.push("npm/blueimp-md5@2.18.0/js/md5.min.js"),window.AV||t.push("npm/leancloud-storage@4/dist/av-min.js"),(0,i.default)("https://cdn.jsdelivr.net/combine/"+t.join(","),(function(){e.initCheck(),window.MV.scriptInit=!0}),1==window.MV.scriptInit||0==t.length))
// 替换为
(e.config.NoRecordIP||(void 0===window.MV.ip?(0,a.default)(e):e.C.ip=window.MV.ip),t=[],window.md5||t.push("npm/blueimp-md5@2.18.0/js/md5.min.js"),window.AV||t.push("npm/leancloud-storage@4/dist/av-min.js"),(0,i.default)("/cdn.jsdelivr.net/combine/valine/parse.min.js",(function(){e.initCheck(),window.MV.scriptInit=!0}),1==window.MV.scriptInit||0==t.length))

表情包:

https://cdn.jsdelivr.net/npm/alus@latest
https://cdn.jsdelivr.net/gh/MiniValine/qq@master
https://cdn.jsdelivr.net/gh/MiniValine/Bilibilis@master
https://cdn.jsdelivr.net/gh/MiniValine/tieba@master
https://cdn.jsdelivr.net/gh/MiniValine/twemoji@master
https://cdn.jsdelivr.net/gh/MiniValine/weibo@master

表情包,直接采取 个别依赖项整包下载 中的方式整包进行下载放置在本地的资源文件夹

完成后,需要进行一个操作,替换 MiniValine.min.js 文件中的下载链接 https://cdn.jsdelivr.net/ 替换为 /cdn.jsdelivr.net/

# 资源加载代码修改

全局搜索 jsdelivr 主要观察主题文件夹内相关文件,我们需要修改如下几处

  1. 在主题配置文件 _config.shoka.yml 内新增配置
# 本地资源文件夹
resource: '/cdn.jsdelivr.net/'
# 现有 vendors 配置新增
vendors:
    # 是否使用本地化资源
    locally: true
  1. 脚本配置项修改

修改主题文件 themes/shoka/scripts/generaters/script.js ,将配置文件新增的配置在代码内初始化,供后续加载使用

// 大概 12 行左右
var siteConfig = {
    version: env['version'],
    hostname: config.url,
    root: config.root,
    statics: theme.statics,
    // 新增
    resource: theme.resource,
    vendors_locally: theme.vendors.locally,
    // ...
}
  1. 主题自定义辅助函数 _vendor_js

文件 themes/shoka/scripts/helpers/asset.js 找到辅助函数新增如下配置 (大约在 45 行左右)

hexo.extend.helper.register('_vendor_js', () => {
    const config = hexo.theme.config.vendors.js;
    if (!config) return '';
    // 新增
    if (hexo.theme.config.vendors.locally) {
        return htmlTag('script', { src: hexo.theme.config.resource + 'combine/js/base.js' }, '');
    }
    // Get a font list from config
    let vendorJs = ['pace', 'pjax', 'fetch', 'anime', 'algolia', 'instantsearch', 'lazyload', 'quicklink'].map(item => {
        if (config[item]) {
            return config[item];
        }
        return '';
    });
    // ...
});
  1. 主题工具脚本修改

文件 themes/shoka/source/js/_app/utils.js ,函数 assetUrl 直接替换为如下代码即可

const assetUrl = function (asset, type) {
    var str = CONFIG[asset][type];
    if (str.indexOf("http") > -1)
        return str;
    if (str.indexOf("combine") > -1) {
        if (CONFIG.vendors_locally) {
            return CONFIG.resource + "combine/" + asset + "/" + type + "." + asset;
        }
        return "//cdn.jsdelivr.net/" + str;
    } else if (str.indexOf("npm") > -1 || str.indexOf("gh") > -1) {
        if (CONFIG.vendors_locally) {
            return CONFIG.resource + str;
        }
        return "//cdn.jsdelivr.net/" + str;
    }
    return statics + str;
}

# 周边配置修改

# 代码压缩白名单

本地资源文件夹是放置在了 source/cdn.jsdelivr.net/ ,如果启用了代码压缩,我们需要将本地资源文件夹加入 白名单

Hexo 配置文件 _config.ymlminify 部分修改如下

minify:
    html:
        enable: true
        exclude: # 排除 hexo-feed 用到的模板文件
            - '**/json.ejs'
            - '**/atom.ejs'
            - '**/rss.ejs'
            - '**/cdn.jsdelivr.net/**/*.*'
    css:
        enable: true
        exclude:
            - '**/*.min.css'
            - '**/cdn.jsdelivr.net/**/*.*'
    js:
        enable: true
        mangle:
            toplevel: true
        output:
        compress:
        exclude:
            - '**/*.min.js'
            - '**/custom/*.js'
            - '**/cdn.jsdelivr.net/**/*.*'

- '**/cdn.jsdelivr.net/**/*.*' 为新增部分,排除该目录所有资源压缩

# 自建 CDN (可选)

到现在的步骤,其实 jsDelivr 本地化已经完成,但是它引入了几个新的弊端

  • 本地资源文件夹 其实很少更改,现在每次 Hexo 推送都会把它推送一次,极大的增加了推送时间。
  • 如果使用了 云服务厂商 对象存储(COS) 来做静态站点,特别的还挂载了 CDN 回源,那一般对象存储都会配置云函数,在发生文件变更时刷新 CDN 缓存,在推送刷新时,会浪费刷新次数 (土豪有钱请随意),使得真正需要刷新的文件可能会刷新失败。

针对这种情况,如果本地博客文件基于 GitHub 做版本管理,那么可以采取如下操作,这儿只是描述了大概思路,具体的可以灵活变通

  1. 本地资源文件夹单独作为一个资源库,使用 Git 管理
  2. 使用 Git Action , 在资源文件库主分支上传时,同步到 COS 存储
  3. _config.shoka.yml 配置 resourceCDN 域名
  4. MiniValine.min.js 资源路径替换为 CDN 相对路径访问链接
  5. 本地博客文件仓库,使用 Git submodule 引入资源文件夹
  6. hexo 三连前,使用 rm -rf 命令删除 public 文件夹下资源文件夹,避免推送

这个可以在 package.json 配置一个命令, scripts 内新增,后续使用 npm run upload 替代 hexo 三连

"scripts": {
    "upload": "hexo clean && hexo g && rm -rf ./public/cdn.jsdelivr.net/ && hexo d",
},

# 结语

至此 jsDelivr 本地化完成,博客依赖项加载再也不用看 jsDelivr 的心情了, Hexo 三连即可看到效果,后续如果要增加其他资源可以直接放置在本地资源文件夹

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

yx.zhang 微信支付

微信支付

yx.zhang 支付宝

支付宝