哔哔闪念[2.0] | 新增微信发位置、发视频、发链接功能
2025.01.03 更新
- 增强: 完成云函数项目重构,代码逻辑更清晰、高效。由 Cursor 提供技术支持
9.27 更新
优化 bbtalk.js 方法,解决 pjax 兼容问题
6.28 更新:Safari 无法播放 mp4 问题已破案
下文提到哔哔闪念发送视频后,存储在我的cdn.guole.fun
上,结果发现 Safari 无法播放。一顿操作猛如虎,各种排查最终确认问题如下,且已根本解决。
原因:
罪魁祸首:workbox
,我早前通过正则给 cdn.guole.fun
域名加了 workbox 缓存,结果发现如果不特殊配置,workbox 缓存的视频无法正常响应 Safari 的 range 请求。甚至 Chrome 也偶现无法播放,不光 Safari。我原来是这样用的,简单粗暴:
1 | workbox.routing.registerRoute( |
解决方案:
只需改为如下即可。压根不用 workbox 缓存 JSON 、mp4 资源了(workbox缓存音频后,也会影响其正常响应)。Chrome 官方给了说明,该日再研究折腾:提供缓存的音频和视频:
1 | workbox.routing.registerRoute( |
6.26 再更新:新增说说转储 JSON 功能
如题,现在云函数可以在你新增 / 删除 / 修改 / 插入 / 追加说说内容后,自动拉取变动数据生成 JSON 文件,每个 JSON 文件的条数 = 环境变量设置的PageSize
值。也就是说,前端读 JSON 即可,不用直接请求 LeanCloud 了,提升哔哔闪念的加载速度!(JSON 自动上传腾讯云 COS ,配合 CDN 加持,老上头了)
新的资源 URL 是下面这样,json/bb-json
这部分是你自己在环境变量中设置的上传路径 Tcb_JsonPath
值:
1 | https://cdn.guole.fun/json/bb-json/bbtalk_page${page}.json |
生成的 JSON 暂时明明格式为:”bbtalk_page=” + 页数 + “.json” 。前端读取时,只用递增这个页面即可。首页轮播哔哔,固定读取 bbtalk_page1.json
就完事儿了!配合上述功能,哔哔闪念独立页面的前端 JS 方法,调整 fetch
请求地址为如下类型即可(v1.0.1 以上版本):
1 | fetch(`https://cdn.guole.fun/json/bb-json/bbtalk_page${page}.json`) |
首页轮播哔哔,其中的 jsonUrl
变量值调整为如下这种写死即可:
1 | jsonUrl = 'https://cdn.guole.fun/json/bb-json/bbtalk_page1.json'; |
更新:解决 Safari 无法播放视频问题
- 原因:Safari 处理
video
标签时,会先发一个获取视频 1 字节信息的请求,拿视频封面之类的。只有用户点击播放时,才会再发一个请求,获取后面的完整内容。这里需要 cos 正常响应range
请求。但是我原本使用了本站的cdn.guole.fun
来获取媒体资源,但这个 CDN 加速域名配置的加速类型是“ CDN 网页小文件”,似乎会覆盖导致存储桶本身自动给 .mp4 文件设置的Content-Type: video/mp4
头部信息不生效,Chrome 中正常,但 iOS Safari 中就不行(iOS 里的 Chrome / 微信内嵌浏览器都使用 Safari 内核……) - 解决:测试直接放存储桶的资源链接,是可以正常播放的。但是我这边重新设置
cdn.guole.fun
的加速类型为“音视频点播”时报错,设置不成功。我就曲线解决了,重新创建了个media.guole.fun
加速域名(与cdn.guole.fun
都关联同一个存储桶……),专门用来读取多媒体资源,cdn.guole.fun
先保留用作它用,后续考虑要不要删除重新创建为“音视频点播”(2023.6.26 已删除重建解决)。 - 创建好相关域名后,拆分成:顶级域名、二级域名、子域,在下面环境变量中配置既可。
综上所述,如果你准备新建一个存储桶用来放说说多媒体资源,一定记得选加速类型为“音视频点播”。不建议直接暴露使用存储桶的原始域名(没有 CDN 加速)。同时,存储桶和加速域名设置好防盗链,免得被盗刷……
原文
分享下本站说说的发展历程。
早前,使用黑石哔哔、木木的 bber 、小康的 ispeak 等,后来腾讯云开发收费了,就陆续没法用了。
目前本站哔哔存储在 LeanCloud
(其他 MongoDB 啥的应该也没问题),/bb/
和首页轮播是我写了个 Vercel
云函数接口,加了个缓存功能,同时避免 LeanCloud
的访问秘钥在前端被泄露。欢迎扫描关注,先行体验效果再决定要不要折腾。
这个端午竟然没出去玩!!就把之前用木木的 bber 复刻了出来,同时新增了发视频、发位置、发链接卡片等功能。微信其实还支持个语音消息,由于是 amr
格式音频,浏览器没法播放,服务端复用 FFmpeg
等插件转换格式,又一直环境不对……心态崩了,干脆摆烂……谁搞定了,欢迎 PR 或下面留言通知我啊!
注:本文包含的微信发说说功能,部署在腾讯云 Serverless 里,其他 Vercel 、Railway 应该也没问题,我没折腾。之前域名备案,一定要有服务器之类的 IP ,就买了三年 Serverless 套餐,个人使用不超量的话也很便宜,所以 也不能闲着便宜了腾讯不是……
效果演示
支持特性
- 包含
- 部署在云函数上,无需服务器
- 使用微信随时随地发布闪念瞬间(memos 很香,但是得有机器……)
- 在原来 @木木木 的
bber-weixin
基础上升级而来,新增支持:发位置、发链接卡片、发视频功能; - 发图片:直接拍照片或发本地图片给公众号既可(存在腾讯云 cos ,原有的去不图床逻辑没删,但我也没验证);
- 发视频:录视频、或本地视频直接发给公众号既可(存在腾讯云 cos,支持 .mp4 / .flv / .ts ,其他格式受腾讯 CDN 支持范围影响,不支持 Safari 播放);
- 发位置:需要在 /bb/ 页面引入资源,详见使用指南;(其他页面嵌入地图,见另一个插件:hexo-tag-map)
- 发链接卡片:直接分享卡片到公众号;
发语音:本来想借用 FFmpeg 在服务端转换语音音频 amr 为 mp3,前端再调 aplayer 等播放,但是夭折了……云函数里一直搞不定 FFmpeg 的环境……
部署到 LeanCloud
这部分很简单,到 LeanCloud 注册个 国际版
账号,然后创建一个应用,比如就叫 bb
,然后新建 2 个集合 content
、UserBindingStatus
。
content
集合差不多有这几个字段:字段名 | 类型 | 说明 |
---|---|---|
objectId | String | 唯一标识符,LeanCloud 自动创建,无视它; |
ACL | ACL | 读写权限,LeanCloud 自动创建,无视它 |
MsgType | String | 微信消息类型,前端用来处理一些逻辑 |
content | String | 说说内容 |
from | String | 说说来源,自定义 |
other | String | 一些特殊消息,比如位置消息,保存脚本,前端插入后渲染出地图 |
createdAt | Date | 创建日期时间,LeanCloud 自动创建,无视它;若导入数据,得修改这个字段,ISO 格式的时间戳,比如 2023-06-24T07:20:55.650Z |
updatedAt | Date | 更新日期时间,LeanCloud 自动创建,无视它 |
UserBindingStatus
集合不用管,字段云函数会自动创建。(其实 content
集合应该也不用手动创建字段……只是我没试过)
获取 LeanCloud 凭证
- 上述 content 和 UserBindingStatus 集合需要在同一个应用里,这样 appid 才一样;
- 在 LeanCloud 应用凭证 界面(设置——应用凭证),找到
AppID
/AppKey
/MasterKey
信息,记录下来下文使用。
获取腾讯云 API 访问密钥
- 注册并认证腾讯云开发者;
- 在腾讯云 访问管理 界面新建 1 个密钥。(建议创建一个子用户,可以更小颗粒度限制权限范围,更安全);
- 如果使用子用户(访问方式选择“编程访问”),在关联策略中,搜索
cos
,选择QcloudCOSDataFullControl 对象存储(COS)数据读、写、删除、列出的访问权限
这一项既可;(为啥要删除权限,因为你发指令删除说说时,如果包含了视频图片等资源,是同步删了存储桶文件的)
4. 记录下
SecretId
/ SecretKey
;获取微信公众号凭证
- 注册一个微信公众号,个人只能注册订阅号 微信公众平台;
- 在公众平台 —— 设置与开发 —— 基本设置界面,记录下
AppID
/AppSecret
; - 在服务器配置中,选择兼容模式,自定义
Token
,随机生成EncodingAESKey
都记录下来; - 不要关闭这个页面,等下回来配置服务器地址;
创建云函数
- 访问腾讯云云函数平台 腾讯云 Serverless;
- 创建一个云函数,选择
从头开始
,函数类型:事件函数
;函数名称:自定义;地域:中国香港
(要访问 LeanCloud,香港可能好一点);运行环境:Nodejs 16.13
;
3. 其他配置中,启用
日志投递
。默认格式;启用固定公网出口 IP
;触发器选择 自定义创建
,触发方式选择 API 网关触发
,勾选集成响应
,其他用默认的不用改。4. 创建一个
src
目录,函数代码拷贝我 Github 仓库:BBtalk-Serverless,总共是 6 个文件,都放在 src
目录下保存。不要在线安装依赖,部署太慢了,往下看;5. 本地创建个空目录,
git clone
或手动下载解压缩都可以,下载我的代码包Github 仓库:BBtalk-Serverless,然后 cd src
后,npm install
安装依赖;6. 进入
src
下的 node_modules
目录,全选压缩为 .zip 压缩包;(一定要进入node_modules
目录全选压缩)7. 在腾讯云云函数页面,选择
层
,新建层
,名称随意,上传刚才压缩的 zip 包;8. 进入函数服务 —— 刚才创建的云函数 ——
层管理
,绑定刚才创建的 层
;9. 回到
函数配置
,右上角编辑
,创建以下环境变量(Tcb_Bucket / Tcb_Region / Tcb_ImagePath / Tcb_MediaPath / Tcb_JsonPath 请自行手动创建):key | value |
---|---|
Binding_Key | 绑定密钥,自定义一条字符串 |
LeanCloud_ID | 上面记录的 LeanCloud AppID |
LeanCloud_KEY | 上面记录的 LeanCloud AppKey |
LeanCloud_MasterKey | 上面记录的 LeanCloud MasterKey |
Tcb_SecretId | 上面记录的腾讯云 API 访问秘钥 SecretId |
Tcb_SecretKey | 上面记录的腾讯云 API 访问秘钥 SecretKey |
WeChat_Token | 上面记录的微信公众平台自定义的 Token |
WeChat_appId | 上面记录的微信公众平台自定义的 AppID |
WeChat_appSecret | 上面记录的微信公众平台自定义的 AppSecret |
WeChat_encodingAesKey | 上面记录的微信公众平台自定义的 EncodingAESKey |
Upload_Media_Method | 图片 / 视频的上传方式,可选值:cos 或qubu ,要使用发视频功能必须配置为cos ;qubu 我没测试… |
Tcb_Bucket | 腾讯云存储桶名称(如“file-1258755354”)。若 Upload_Media_Method 为 cos ,此项必填 |
Tcb_Region | 腾讯云存储桶地域(如“ap-guangzhou”)。若 Upload_Media_Method 为 cos ,此项必填 |
Tcb_ImagePath | 腾讯云存储桶图片路径(如“/img/bb-img/”,即图片上传你要放在什么路径)。若 Upload_Media_Method 为 cos ,此项必填 |
Tcb_MediaPath | 腾讯云存储桶媒体路径(如“/media/bb-media/”,即媒体上传你要放在什么路径)。若 Upload_Media_Method 为 cos ,此项必填 |
Tcb_JsonPath | 腾讯云存储桶 JSON 转储路径(如“/json/bb-json/”,即媒体上传你要放在什么路径)。若 Upload_Media_Method 为 cos ,此项必填 |
TopDomain | 顶级域名(如 media.guole.fun 中的 “fun”)。若 Upload_Media_Method 为 cos ,此项必填 |
SecondLevelDomain | 二级域名(如 media.guole.fun 中的 “guole”)。若 Upload_Media_Method 为 cos ,此项必填 |
SubDomain | 子域(如 media.guole.fun 中的 “media”)。若 Upload_Media_Method 为 cos ,此项必填 |
PageSize | 从 LeanCloud 获取数据转 Json 的每页条数。若 Upload_Media_Method 为 cos ,此项必填 |
- 提交保存环境变量后,回到函数配置界面,点击
部署
; - 在云函数
触发管理
中,记录下访问路径
;
完成微信公众号配置
在刚才停留的公众号配置界面,服务器地址填写上面记录的云函数访问路径
,然后提交,应该就成功了。
IP 白名单:一定要配置这个,不然发图、视频时,取不到微信 Access token 。ip 就是上面云函数的固定出口 IP 地址。
index.main_handler
,浏览器访问云函数访问路径
,显示403
错误应该就没问题;Hexo 创建说说页面
在 Blog 根目录,执行hexo new page bb
新建一个页面,bb
是说说页面路径,可以自定义;接着:
在 /bb/
目录下的 index.md
文件中,添加以下内容:
1 | --- |
再去 /source/css/
目录下创建一个 bbtalk.css
文件(也可以命名其他的或者放在其他地方,上面 md 文件里记得同步修改),内容如下:
1 | [data-theme=light] { |
引入的 timeago.js 内容如下,放到 source/js/
目录下(其他位置也可,记得修改 md 中地址):
1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).timeago={})}(this,function(e){"use strict";var r=["second","minute","hour","day","week","month","year"];var a=["秒","分钟","小时","天","周","个月","年"];function t(e,t){n[e]=t}function i(e){return n[e]||n.en_US}var n={},f=[60,60,24,7,365/7/12,12];function o(e){return e instanceof Date?e:!isNaN(e)||/^\d+$/.test(e)?new Date(parseInt(e)):(e=(e||"").trim().replace(/\.\d+/,"").replace(/-/,"/").replace(/-/,"/").replace(/(\d)T(\d)/,"$1 $2").replace(/Z/," UTC").replace(/([+-]\d\d):?(\d\d)/," $1$2"),new Date(e))}function d(e,t){for(var n=e<0?1:0,r=e=Math.abs(e),a=0;e>=f[a]&&a<f.length;a++)e/=f[a];return(0===(a*=2)?9:1)<(e=Math.floor(e))&&(a+=1),t(e,a,r)[n].replace("%s",e.toString())}function l(e,t){return((t?o(t):new Date)-o(e))/1e3}var s="timeago-id";function h(e){return parseInt(e.getAttribute(s))}var p={},v=function(e){clearTimeout(e),delete p[e]};function m(e,t,n,r){v(h(e));var a=r.relativeDate,i=r.minInterval,o=l(t,a);e.innerText=d(o,n);var u,c=setTimeout(function(){m(e,t,n,r)},Math.min(1e3*Math.max(function(e){for(var t=1,n=0,r=Math.abs(e);e>=f[n]&&n<f.length;n++)e/=f[n],t*=f[n];return r=(r%=t)?t-r:t,Math.ceil(r)}(o),i||1),2147483647));p[c]=0,u=c,e.setAttribute(s,u)}t("en_US",function(e,t){if(0===t)return["just now","right now"];var n=r[Math.floor(t/2)];return 1<e&&(n+="s"),[e+" "+n+" ago","in "+e+" "+n]}),t("zh_CN",function(e,t){if(0===t)return["刚刚","片刻后"];var n=a[~~(t/2)];return[e+" "+n+"前",e+" "+n+"后"]}),e.cancel=function(e){e?v(h(e)):Object.keys(p).forEach(v)},e.format=function(e,t,n){return d(l(e,n&&n.relativeDate),i(t))},e.register=t,e.render=function(e,t,n){var r=e.length?e:[e];return r.forEach(function(e){m(e,e.getAttribute("datetime"),i(t),n||{})}),r},Object.defineProperty(e,"__esModule",{value:!0})}); |
最后,md 文件引用的 BBtalk.js
代码如下,放到 source/js/
目录下(其他位置也可,记得修改 md 中地址):
1 | var bbtalk = { |
首页轮播说说
自定义或放到其他能全局引入的 js 文件里都可以,代码如下:
1 | //- Guo Le's Blog |
在theme/Butterfly/layout/index.pug
中加入以下内容,icon 我用了阿里的,你可以自己换。
1 | extends includes/layout.pug |
css 样式随便找个地方丢进去,全局引用即可。有不对的,在本站再扒一下,太久远了 很难翻。
1 | /* 首页 bber 轮播 */ |
大功告成
到这里,应该七七八八了。自己关注上面的微信号,发送以下指令绑定。Binding_Key
是云函数配置的环境变量。
1 | /b bb,${Binding_Key} |
假如我的Binding_Key
环境变量中配置为123456
(实际使用时,尽量设置复杂点啊……比如 32 位随机字符串),那么绑定的命令就是:/b bb,123456
云函数代码中,我用了一些正则,如果换成你自己的,自行检查更新下正则内容……我大部分都是匹配 。上传图片 / 视频的逻辑,已抽取成环境变量,可自由配置(正则也做了环境变量导入)。如果你没有添加 自定义域名,那云函数cdn.guole.fun
这个域名,处理一些逻辑的index.js
里不要加serverURL
:
1 | AV.init({ |
哔哔秘笈
1 | 「哔哔秘笈」 |
接下来,发条说说试试看吧。也欢迎关注我的,试试效果。
- 感谢鼓励 🙏