type
Post
status
Published
date
Jan 20, 2022
slug
osar7h
summary
本文介绍了yuque-hexo插件语雀图片防盗链的解决方案,包括具体的实现流程和常见问题的解决方案。同时,提供了对于其他云服务的实现思路。
category
技术分享
tags
Hexo
创建时间
Apr 7, 2023 07:15 PM
更新时间
Apr 10, 2023 05:51 AM
password
icon
Task List
引言
在使用yuque-hexo同步文章到博客后,由于语雀的图片由有防盗链的限制,会导致部署后,博客网站显示图片异常。 处理办法有两种:
- 在语雀上使用图片的时候,避开直接复制图片到语雀。先将图片上传到自己的图床后,直接使用
markdown
的图片语法:
插入图片到适当位置,例如:

- 为了不破坏语雀编辑器的体验,我修改了
yuque-hexo
的源代码,发布了yuqe-hexo-with-cdn
插件。适配了将语雀中的图片上传到腾讯云 COS 图床后,将原有的语雀图片链接替换掉。
yuqe-hexo-with-cdn 插件
使用文档说明:yuqe-hexo-with-cdn
目前x-code已经将我的方案合并到主分支了,可以直接使用yuque-hexo进行配置
改造思路
原 yuque-hexo 生成.md 文章简易流程


yuqe-hexo-with-cdn 改造思路
整体思路主要是在生成
yuque.json
之前进行语雀图片的替换

具体实现流程


具体实现
// mock数据格式作为参考 const article = { title: '博客标题', body: '图片1: 图片2:' } // 获取语雀的图片链接的正则表达式,返回匹配到的多条记录 const imageUrlRegExp = /!\[(.*?)]\((.*?)\)/mg; async function img2Cos(article) { // 1. 从文章中获取语雀的图片URL列表 const matchYuqueImgUrlList = article.body.match(imageUrlRegExp); // matchYuqueImgUrlList: ["", ""] // 如果没有匹配到,说明该文章不存在图片 if (!matchYuqueImgUrlList) return article; // 循环列表进行处理 const promiseList = matchYuqueImgUrlList.map(async matchYuqueImgUrl => { // 获取真正的图片url const yuqueImgUrl = getImgUrl(matchYuqueImgUrl); // yuqueImgUrl: https://dfas.qqc/test1.png#a=1 // 2.将图片转成buffer文件 const imgBuffer = await img2Buffer(yuqueImgUrl); // 如果解析错误,说明图片有问题,直接跳过后续步骤 if (!imgBuffer) { return { originalUrl: matchYuqueImgUrl, yuqueRealImgUrl: yuqueImgUrl, url: yuqueImgUrl, }; } // 3. 根据buffer文件生成唯一的hash文件名 const fileName = await getFileName(imgBuffer, yuqueImgUrl); // fileName: abcdefg-tudnamdana.png try { // 4. 检查图床中是否存在该文件 let url = await hasObject(fileName); // 存在:url: 腾讯云图床链接 // 不存在 url: '' // 5. 如果图床已经存在,直接替换 // 如果图床不存在,则先上传到图床,再将原本的语雀url进行替换 if (!url) { url = await uploadImg(imgBuffer, fileName); // url: 腾讯云图床链接 } return { // 原始的语雀图片:originalUrl:  originalUrl: matchYuqueImgUrl, // 真正的语雀图片:yuqueRealImgUrl: https://dfas.qqc/test1.png yuqueRealImgUrl: yuqueImgUrl, // 图床中的图片:url: 腾讯云图床链接 url, }; } catch (e) { out.error(`访问COS出错,请检查配置: ${e}`); process.exit(-1); } }) // 得到图片对象数组 const urlList = await Promise.all(promiseList); // [ // { // originalUrl: "", // yuqueRealImgUrl: "https://dfas.qqc/test1.png", // url: "腾讯云图床链接1" // }, // { // originalUrl: "", // yuqueRealImgUrl: "https://dfas.qqc/test2.png", // url: "腾讯云图床链接2" // } // ] // 6. 将语雀图片链接进行替换 urlList.forEach(function(url) { if (url) { article.body = article.body.replace(url.originalUrl, ``); out.info(`replace ${url.yuqueRealImgUrl} to ${url.url}`); } }); // 7. 返回新的文章对象 return article; // article: { // title: '博客标题', // body: '图片1: 图片2:' // } } // 工具类 // 从markdown格式的url中获取url function getImgUrl(markdownImgUrl) { const _temp = markdownImgUrl.replace(/\!\[(.*?)]\(/, ''); const _temp_index = _temp.indexOf(')'); // 得到真正的语雀的url return _temp.substring(0, _temp_index) .split('#')[0]; } // 将图片转成buffer // 这里用到了superagent库进行转换 async function img2Buffer(yuqueImgUrl) { return await new Promise(async function(resolve) { try { await superagent .get(yuqueImgUrl) .buffer(true) .parse(res => { const buffer = []; res.on('data', chunk => { buffer.push(chunk); }); res.on('end', () => { const data = Buffer.concat(buffer); resolve(data); }); }); } catch (e) { // 非法图片返回null out.warn(`invalid img: ${yuqueImgUrl}`); resolve(null); } }); } // 根据文件内容获取唯一文件名称 // 这里用到了开源的七牛云的算法,详情:https://juejin.cn/post/6844903775660933133 async function getFileName(imgBuffer, yuqueImgUrl) { return new Promise(resolve => { getEtag(imgBuffer, hash => { const imgName = hash; // 获取文件名后缀 const imgSuffix = yuqueImgUrl.substring(yuqueImgUrl.lastIndexOf('.')); // 拼接文件名 const fileName = `${imgName}${imgSuffix}`; // 返回文件名 resolve(fileName); }); }); } // 检查COS是否已经存在图片,存在则返回url async function hasObject(fileName) { // prefixKey: blog-image if (!bucket.length || !region.length) { out.error('请检查COS配置'); process.exit(-1); } return new Promise(resolve => { cos.headObject({ Bucket: bucket, // 存储桶名字(必须) Region: region, // 存储桶所在地域,必须字段 Key: `${prefixKey}/${fileName}`, // 文件名 必须 }, function(err, data) { if (data) { // 拼接腾讯云图床的图片URL const url = `https://${bucket}.cos.${region}.myqcloud.com/${prefixKey}/${fileName}`; resolve(url); } else { // 不存在返回空 resolve(''); } }); }); } // 上传图片到COS async function uploadImg(imgBuffer, fileName) { return new Promise((resolve, reject) => { cos.putObject({ Bucket: bucket, // 存储桶名字(必须) Region: region, // 存储桶所在地域,必须字段 Key: `${prefixKey}/${fileName}`, // 文件名 必须 StorageClass: 'STANDARD', // 上传模式(标准模式) Body: imgBuffer, // 上传文件对象 }, function(err, data) { if (data) { // 返回图片链接 resolve(`https://${data.Location}`); } if (err) { reject(err); } }); }); }
常见问题
语雀的流程图/文本绘图等不适配
语雀的流程图/文本绘图等无法生成 markdown 展示,所以我的做法是,在语雀编辑器书写的时候,先编写流程图,写好了再截图,作为图片放在流程图的前面。这样生成的 md 文件就只有图片被解析出来了。

特殊情况下需要使用 markdown 语法的图片链接示例
因为该插件会将匹配到的所有 markdown 语法的图片都上传到图床(包括代码块中的示例),所以在书写语雀文章时,非特殊情况不要使用该语法。或者在书写的时候,将链接非法化即可,例如:


插件在处理的过程中会检测出来非法链接,就不会上传该图片了

关于
如果你不想用腾讯云 COS 图床,你也可以按照这个思路,将 COS 相关的配置进行改造,上传到你自己的图床!