# 引言
cdn.jsdelivr.net
知名的免费 cdn 服务提供商,在去年年底由于大陆域名备案的问题,国内 cdn
解析异常时常无法连接到服务器
尤其是重度依赖 jsDelivr
的 Shoka
主题,在这种情况下由于资源无法加载完成导致网页卡顿,显示效果异常等一系列问题
于是决定探索资源本地化方案去除对 jsDelivr
的依赖
截止到目前国内的 cdn.jsdelivr.net
的 DNS
污染貌似已经解除了,不过资源本地化还是有很多好处的,记录下过程权当自己的备忘了。
# 资源本地化
# jsDelivr 依赖热点
Shoka
主题用到了不少第三方组件,全部基于jsDelivr
加载,资源依赖配置在了 [主题配置文件]{.blue}_config.shoka.yml
的vendors
内。- 依赖的组件又再次依赖
jsDelivr
加载依赖valine: gh/amehime/MiniValine@4.2.2-beta10/dist/MiniValine.min.js
再次依赖
# 依赖资源下载
本地化的一个非常重要的部分就是,将远程资源下载到本地。好在依赖项不是很多哈,可以慢慢手动操作一波,就是慢点 🤣~~
需要注意的是,为了最小化修改,其实应该在本地享有和链接一样的目录结构,这样可以只修改加载代码而不用修改配置
我们在博客主目录 source
下新建 cdn.jsdelivr.net
文件夹,后续下载都放在这个目录下 (通过 Hexo
发布后的文件地址访问将会是 站点网址\cdn.jsdelivr.net\相对路径
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/
)
- 个别依赖项整包下载
个别依赖项不只是简单的需要 *.min.js
文件就够了,还需要额外依赖很多资源文件,所以对于这种我们采取手动下载整个组件源码放置到本地
目前发现有如下组件 (若有遗漏,欢迎指正!)
katex
下载方式:
访问页面 https://www.jsdelivr.com/package/
+ npm/katex
(若需下载其他组件替换这里即可) 即可前往下载页
选择对应版本下载即可,下载完成后将对应的文件解压,放置在 步骤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 |
这儿略微麻烦一点, jsDelivr
有 combine
的功能,可以做到一次请求多个 目标文件 和批成一个文件,上面的组件以空格为分割分别分成了两个地方加载,我们需要进行如下步骤
- 合批请求保存两批
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
主要观察主题文件夹内相关文件,我们需要修改如下几处
- 在主题配置文件
_config.shoka.yml
内新增配置
# 本地资源文件夹 | |
resource: '/cdn.jsdelivr.net/' | |
# 现有 vendors 配置新增 | |
vendors: | |
# 是否使用本地化资源 | |
locally: true |
- 脚本配置项修改
修改主题文件 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, | |
// ... | |
} |
- 主题自定义辅助函数
_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 ''; | |
}); | |
// ... | |
}); |
- 主题工具脚本修改
文件 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.yml
中 minify
部分修改如下
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
做版本管理,那么可以采取如下操作,这儿只是描述了大概思路,具体的可以灵活变通
- 本地资源文件夹单独作为一个资源库,使用
Git
管理 - 使用
Git Action
, 在资源文件库主分支上传时,同步到COS
存储 _config.shoka.yml
配置resource
为CDN
域名MiniValine.min.js
资源路径替换为CDN
相对路径访问链接- 本地博客文件仓库,使用
Git submodule
引入资源文件夹 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
三连即可看到效果,后续如果要增加其他资源可以直接放置在本地资源文件夹