普通视图

发现新文章,点击刷新页面。
昨天以前ChenYFanのBlog

SpeedUp!使用黑科技为你的网站提速

作者 CYF
2022年6月7日 10:28

实战,利用黑科技ServiceWorker提速你的网站。

本文所标记的内容,大多是直接复制粘贴即可实现的。但依然会存在这和您的服务存在冲突这一情况。请阅读上一篇基础文章欲善其事,必利其器 - 论如何善用ServiceWorker进行合理的修改。

我们简单的列一下表,这是我们加速清单:

前端竞速CDN

最传统的网页加载,其静态资源一旦独立是直接放在同域服务器下的。

不过后来,为了减少主服务器开销,我们通常将静态资分离至其他面向用户加载比较快的节点,以减少主服务器开销,提升网页加载速度。

现在,各种公益cdn服务蓬勃生长,其节点通常是全球部署,并且对各个国家/地区做了优化,使用公益cdn,他的质量和可用性是远远大于自己部署的。

在一个网页加载中,主网页只提供一个html(你所看的网页其大小约15kb),而流量开销巨头是静态资源(只论js,本页大约800kb)。我们对一个网站的加速,第一步就应当从静态加速做起。

对于加速存储的选择,通常市面上有三种主流加速cdnjs/npm/gh。而在这其中,对于个人而言,我还是推崇npm,这在接下来对于竞速节点的选择就多了起来。

对于加速节点的选择,从mainland角度来讲,首先你要遵循一个普世结论:不要用跨国节点。本身China国度就有个奇葩规定,没有ICP备案许可就不得在mainland开展网站服务。这使得大部分本应该分散在边缘末端的流量全压到了国际出口的汇聚层。你再引导他们去海外拉资源,凑过去挤热闹,其稳定性和速率完全无法保证,这在生产环境使用无异于自杀。这里提一嘴jsdelivr,备案掉了,节点迁出中国了,那你面向国内的网站早就可以切了。使用fastly,联通和电信晚上ntt几乎堵到妈都不认识,能连上就是奇迹。除非你真的不在意加速,也不在意资源能不能正常加载,在面向mainland的生产环境里还坚持jsd就是愚不可及的行为。

当然,在这里我还是推崇黑科技ServiceWorker,它可以利用Promise.any,并发数个请求至不同的cdn节点,提升前端资源加载。而借助于强大的js引擎,其在本地处理的效率奇高,性能折损几乎不计。

在这之前,我们定义一个lfetch,它的作用是对urls这个数组里的所有网址发起并发请求,并且在任意一个节点返回正常值时打断其余请求,避免流量浪费。

const lfetch = async (urls, url) => {    let controller = new AbortController(); //针对此次请求新建一个AbortController,用于打断并发的其余请求    const PauseProgress = async (res) => {        //这个函数的作用时阻塞响应,直到主体被完整下载,避免被提前打断        return new Response(await (res).arrayBuffer(), { status: res.status, headers: res.headers });    };    if (!Promise.any) { //Polyfill,避免Promise.any不存在,无需关注        Promise.any = function (promises) {            return new Promise((resolve, reject) => {                promises = Array.isArray(promises) ? promises : []                let len = promises.length                let errs = []                if (len === 0) return reject(new AggregateError('All promises were rejected'))                promises.forEach((promise) => {                    promise.then(value => {                        resolve(value)                    }, err => {                        len--                        errs.push(err)                        if (len === 0) {                            reject(new AggregateError(errs))                        }                    })                })            })        }    }    return Promise.any(urls.map(urls => {//并发请求        return new Promise((resolve, reject) => {            fetch(urls, {                signal: controller.signal//设置打断点            })                .then(PauseProgress)//阻塞当前响应直到下载完成                .then(res => {                    if (res.status == 200) {                        controller.abort()//打断其余响应(同时也打断了自己的,但本身自己已下载完成,打断无效)                        resolve(res)//返回                    } else {                        reject(res)                    }                })        })    }))}

在这其中,有一个难点:fetch返回的时状态而非内容。
即当fetchPromise resolve时,它是已经获得了响应,但此时内容还没下载,提前打断会导致无法返回。这是PauseProgress作用。

之后我们考虑静态资源加速。这时候用npm的好处就体现出来了,不仅镜像多,其格式也还算固定。

注意,此方法是以流量换速度的方式进行的,虽然在任何一个节点返回正确内容后会打断其余请求,但依然会造成不可避免的流量消耗(+~20%)。如果你面向的是手机流量用户,请三思而后行!

此代码与freecdn-js核心功能相似,但实现方法并不同,并且完全支持动态网页。

例如jquery,你可以这样加速:

原始地址:https://cdn.jsdelivr.net/npm/jquery@3.6.0各类镜像:https://fastly.jsdelivr.net/npm/jquery@3.6.0https://gcore.jsdelivr.net/npm/jquery@3.6.0https://unpkg.com/jquery@3.6.0https://unpkg.zhimg.com/jquery@3.6.0 #回源有问题https://unpkg.com/jquery@3.6.0https://npm.elemecdn.com/jquery@3.6.0https://npm.sourcegcdn.com/jquery@3.6.0 #滥用封仓库https://cdn1.tianli0.top/jquery@3.6.0 #滥用封仓库

我们可以简单搓一个sw小脚本完成前端加速:

ServiceWorker完整代码:
我已经很详细的看了上面的阐述,并且我不会直接照搬
const CACHE_NAME = 'ICDNCache';let cachelist = [];self.addEventListener('install', async function (installEvent) {    self.skipWaiting();    installEvent.waitUntil(        caches.open(CACHE_NAME)            .then(function (cache) {                console.log('Opened cache');                return cache.addAll(cachelist);            })    );});self.addEventListener('fetch', async event => {    try {        event.respondWith(handle(event.request))    } catch (msg) {        event.respondWith(handleerr(event.request, msg))    }});const handleerr = async (req, msg) => {    return new Response(`<h1>CDN分流器遇到了致命错误</h1>    <b>${msg}</b>`, { headers: { "content-type": "text/html; charset=utf-8" } })}let cdn = {//镜像列表    "gh": {        jsdelivr: {            "url": "https://cdn.jsdelivr.net/gh"        },        jsdelivr_fastly: {            "url": "https://fastly.jsdelivr.net/gh"        },        jsdelivr_gcore: {            "url": "https://gcore.jsdelivr.net/gh"        }    },    "combine": {        jsdelivr: {            "url": "https://cdn.jsdelivr.net/combine"        },        jsdelivr_fastly: {            "url": "https://fastly.jsdelivr.net/combine"        },        jsdelivr_gcore: {            "url": "https://gcore.jsdelivr.net/combine"        }    },    "npm": {        eleme: {            "url": "https://npm.elemecdn.com"        },        jsdelivr: {            "url": "https://cdn.jsdelivr.net/npm"        },        zhimg: {            "url": "https://unpkg.zhimg.com"        },        unpkg: {            "url": "https://unpkg.com"        },        bdstatic: {            "url": "https://code.bdstatic.com/npm"        },        tianli: {            "url": "https://cdn1.tianli0.top/npm"        },        sourcegcdn: {            "url": "https://npm.sourcegcdn.com/npm"        }    }}//主控函数const handle = async function (req) {    const urlStr = req.url    const domain = (urlStr.split('/'))[2]    let urls = []    for (let i in cdn) {        for (let j in cdn[i]) {            if (domain == cdn[i][j].url.split('https://')[1].split('/')[0] && urlStr.match(cdn[i][j].url)) {                urls = []                for (let k in cdn[i]) {                    urls.push(urlStr.replace(cdn[i][j].url, cdn[i][k].url))                }                if (urlStr.indexOf('@latest/') > -1) {                    return lfetch(urls, urlStr)                } else {                    return caches.match(req).then(function (resp) {                        return resp || lfetch(urls, urlStr).then(function (res) {                            return caches.open(CACHE_NAME).then(function (cache) {                                cache.put(req, res.clone());                                return res;                            });                        });                    })                }            }        }    }    return fetch(req)}const lfetch = async (urls, url) => {    let controller = new AbortController();    const PauseProgress = async (res) => {        return new Response(await (res).arrayBuffer(), { status: res.status, headers: res.headers });    };    if (!Promise.any) {        Promise.any = function (promises) {            return new Promise((resolve, reject) => {                promises = Array.isArray(promises) ? promises : []                let len = promises.length                let errs = []                if (len === 0) return reject(new AggregateError('All promises were rejected'))                promises.forEach((promise) => {                    promise.then(value => {                        resolve(value)                    }, err => {                        len--                        errs.push(err)                        if (len === 0) {                            reject(new AggregateError(errs))                        }                    })                })            })        }    }    return Promise.any(urls.map(urls => {        return new Promise((resolve, reject) => {            fetch(urls, {                signal: controller.signal            })                .then(PauseProgress)                .then(res => {                    if (res.status == 200) {                        controller.abort();                        resolve(res)                    } else {                        reject(res)                    }                })        })    }))}

全站NPM静态化

此法chen独创,是一种比较野路子的手法,但加速效果显著。
通常建议用于Hexo等静态博客,WordPress等需要做好伪静态,并且要配置好动态接口。

hexo作为静态博客有什么好处,那当然是纯静态啦。生成的静态文件随便搬到一个web服务器都能用。

自然就有了接下的骚操作,用npm托管博客,然后在请求的时候用sw劫持并引流到npm镜像,效果就如同本博客所示,加载速度(抛开首屏不谈)接近于闪开。

首先我博客的CI是用GithubAction的,在部署的过程中顺便把html上传到npm是最简单不过的事情,只要在生成代码块后面再加一块:

- uses: JS-DevTools/npm-publish@v1  with:    token: ${{ secrets.NPM }}

配置NPM环境变量,之后需要更新的时候叠一个新版本就行。

然后接下来我们就要解决ServiceWorker获取问题。我们先设立一个监听器:

self.addEventListener('fetch', async event => {    event.respondWith(handle(event.request))});const handle = async(req)=>{    const urlStr = req.url    const urlObj = new URL(urlStr);    const urlPath = urlObj.pathname;    const domain = urlObj.hostname;    //从这里开始}

首先我们要判断一下这个域名是不是博客主域名,不然瞎拦截到其他地方可不好:

if(domain === "blog.cyfan.top"){//这里写你需要拦截的域名    //从这里开始处理}

我们还要记得给url进行提前处理,剥离出路径,去除参数。在这其中尤其要注意的是默认路由的处理。

通常情况下我们访问一个网址,web服务器会在后面添加.html后缀。对于一个默认路径则是index.html

还要注意的是剥离#,不然又会获取失败。

定义一个fullpath函数,用于预处理和剥离路径:

const fullpath = (path) => {    path = path.split('?')[0].split('#')[0]    if (path.match(/\/$/)) {        path += 'index'    }    if (!path.match(/\.[a-zA-Z]+$/)) {        path += '.html'    }    return path}

结果类似于:

> fullpath('/')'/index.html'> fullpath('/p/1')'/p/1.html'> fullpath('/p/1?q=1234')'/p/1.html'> fullpath('/p/1.html#QWERT')'/p/1.html'

然后再定义一个镜像并发函数,用于生成待获取的网址:

const generate_blog_urls = (packagename, blogversion, path) => {    const npmmirror = [        `https://unpkg.com/${packagename}@${blogversion}/public`,        `https://npm.elemecdn.com/${packagename}@${blogversion}/public`,        `https://cdn.jsdelivr.net/npm/${packagename}@${blogversion}/public`,        `https://npm.sourcegcdn.com/npm/${packagename}@${blogversion}/public`,        `https://cdn1.tianli0.top/npm/${packagename}@${blogversion}/public`    ]    for (var i in npmmirror) {        npmmirror[i] += path    }    return npmmirror}

接下来我们将其填入主路由中:

if(domain === "blog.cyfan.top"){//这里写你需要拦截的域名    return lfetch(generate_blog_urls('chenyfan-blog','1.1.4',fullpath(urlPath)))}

然而谨记,npm返回的文件格式通常是text而非html,所以我们还要进一步处理header。处理也简单,连环then下去就行:

if(domain === "blog.cyfan.top"){    return lfetch(generate_blog_urls('chenyfan-blog','1.1.4',fullpath(urlPath)))    .then(res=>res.arrayBuffer())//arrayBuffer最科学也是最快的返回    .then(buffer=>new Response(buffer,{headers:{"Content-Type":"text/html;charset=utf-8"}}))//重新定义header}

那么接下来,除了首屏以外,你的网站相当于托管在全球(包括中国)各个主流cdn服务器中,提速效果无与伦比.如果你将原网站托管在cf,那么你将获得一个打不死,国内加载速度超快(首屏除外)的网站。

解放双手 - npm版本自定义更新

有人会问了,我为什么不能直接用latest来获取最新版本,还要手动指定?

简单地说,在生产环境中用@latest指定最新版本是一个很不合理、且非常愚蠢的操作。你永远都不知道对面的缓存什么时候清除,获取到的可能是一年前的版本。

为了节约成本,避免回源,cdn服务商通常会缓存资源,尤其是那些静态资源。以jsd为例,其latest缓存为7天,不过可以手动刷新。unpkg cf边缘2星期,eleme已经将近6个月没更新了。至于那些自建的。你根本搞不懂他什么时候更新,使用latest会导致随机获取到版本,接近无法正常使用。

而指定版本的,cdn通常会永久缓存。毕竟版本定死了东西是不会变的,而请求指定版本的cdn也可以或多或少提升访问速度,因为文件时永久缓存的,HIT的热度比较高。

sw端

利用npm registry获取最新版本,其官方endpoint如下:

https://registry.npmjs.org/chenyfan-blog/latest

其version字段即最新版本。

npm registry的镜像也不少,以腾讯/阿里为例:

https://registry.npmmirror.com/chenyfan/latest #阿里,可手动同步https://mirrors.cloud.tencent.com/npm/chenyfan/latest #腾讯,每日凌晨同步

获取最新版本也不难:

const mirror = [        `https://registry.npmmirror.com/chenyfan-blog/latest`,        `https://registry.npmjs.org/chenyfan-blog/latest`,        `https://mirrors.cloud.tencent.com/npm/chenyfan-blog/latest`]const get_newest_version = async (mirror) => {    return lfetch(mirror, mirror[0])        .then(res => res.json())        .then(res.version)}

在这里还有一个坑点:ServiceWorker的全局变量会在所有页面关闭后销毁。下次启动时会优先响应handle,其定义变量要在handle响应后才会执行。因此,对于最新版本的存储,不能直接定义变量,需要持久化,这里另辟蹊径,采用CacheStorage存储:

self.db = { //全局定义db,只要read和write,看不懂可以略过    read: (key, config) => {        if (!config) { config = { type: "text" } }        return new Promise((resolve, reject) => {            caches.open(CACHE_NAME).then(cache => {                cache.match(new Request(`https://LOCALCACHE/${encodeURIComponent(key)}`)).then(function (res) {                    if (!res) resolve(null)                    res.text().then(text => resolve(text))                }).catch(() => {                    resolve(null)                })            })        })    },    write: (key, value) => {        return new Promise((resolve, reject) => {            caches.open(CACHE_NAME).then(function (cache) {                cache.put(new Request(`https://LOCALCACHE/${encodeURIComponent(key)}`), new Response(value));                resolve()            }).catch(() => {                reject()            })        })    }}const set_newest_version = async (mirror) => { //改为最新版本写入数据库    return lfetch(mirror, mirror[0])        .then(res => res.json()) //JSON Parse        .then(async res => {            await db.write('blog_version', res.version) //写入            return;        })}setInterval(async() => {    await set_newest_version(mirror) //定时更新,一分钟一次}, 60*1000);setTimeout(async() => {     await set_newest_version(mirror)//打开五秒后更新,避免堵塞},5000)

再将上面的生成urls从:

generate_blog_urls('chenyfan-blog','1.1.4',fullpath(urlPath))

改为

//若第一次没有,则使用初始版本1.1.4,或者你也可以改成latest(不推荐)generate_blog_urls('chenyfan-blog',await db.read('blog_version') || '1.1.4',fullpath(urlPath))

此后用户加载时,会尽可能的获取最新版本,无需手动更新。

ci端

有了前端自动更新,我们在上传包的时候还要手动更新package.json中的version字段。其实这里也可以直接交给ci处理。

然而需要注意,npm version patch 虽然能更新z位数版本,但其更新不会上传到仓库。换句话说,这只能上传一次。因此这个地方我干脆建议用随机数,反正通过api获得的latest都是最新的上传。

案例代码这里不展示,实际上这一步做起来也不难。

调剂响应

缓存 - 互联网上一伟大发明

虽然我也不清楚有一小部分人对那么丁点CacheStorage缓存占用那么敏感,但至少,缓存对网站加载速度的提升有极大的帮助。

通常,有大量的资源是访问一个网站重复需要的,对于这些东西,浏览器会自带MemoryCache或DiskCache,但这一类缓存不是持久的,在下一次打开网站的时候缓存不一定生效。而CacheStorage是浏览器自带的缓存桶(Key/Value),这种桶是持久存储,长期有效,而sw是能控制这桶。

而不同的网域资源所应当采取的缓存策略也是不一样的。对于确定版本的静态资源,直接终生缓存;对于有时限的静态资源,应当不缓存或只缓存一小段时间。

CacheStorage不是sw专属的,你可以在前端和sw中同时控制它,这是几分样例代码:

const CACHE_NAME = 'cache-v1'; //定义缓存桶名称caches.open(CACHE_NAME).then(async function (cache) {    const KEYNAME = new Request('https://example.com/1.xxx') //定义KEY,[Object Request]    await cache.put(KEYNAME, new Response('Hello World')); //自定义填入    await cache.match(KEYNAME).then(async function (response) {        console.log(await response.text()); //匹配并输出    })    await cache.put(KEYNAME, await fetch('https://example.com/2.xxx').then(res=>res.arrayBuffer())); //使用fetch填入缓存    await cache.matchAll().then(function (responses) { //列出所有(也可以根据内容列出指定的项        for (let response of responses) {            console.log(response.url);        }    })    await cache.delete(KEYNAME) //删除指定项});

SetTimeout - 毫秒级调控响应

对于同一个网页,你需要合理的对他执行决策树,这是目前我的博客[网页]采取的决策树:

js里有两个对时间控制的古老函数:SetTimeoutSetInterval.在这里我采用SetTimeout,同时并行执行任务.

这里采用代码片段比较容易解释:

if (请求的网址是博客) {    //这里一定要用Promise,这样在之后settimeout就不需要回调,直接Resolve    return new Promise(function (resolve, reject) {        setTimeout(async () => {            if (存在缓存) {                setTimeout(() => {                    resolve(获取当前页面缓存(请求))                }, 200);//200ms表示下面的拉取失败后直接返回缓存,但下面的拉取不会被踢出,更新会在后台执行,会填入缓存,但也不会返回                setTimeout(() => {                    resolve(                        拉取最新版本的网页(请求)                            .then(填入缓存并返回内容)//返回缓存                    )                }, 0);//表示立刻执行,优先级最高            } else {                setTimeout(() => {                    resolve(                        拉取最新版本的网页(请求)                            .then(填入缓存并返回内容)//返回缓存                    )                }, 0);//表示立刻执行,优先级最高                setTimeout(() => {                    resolve(返回错误提示())                }, 5000)//5000ms后如果没有返回最新内容就直接返回错误提示,如果成功了此次返回是无效的            }        }, 0)//这里需要一个大settimeout包裹以便于踢出主线程,否则无法并行处理    })}

优化首屏

window.stop - 与死神擦肩而过

事实上,ServiceWorker最显著的缺点是要在第一次加载网页安装后,刷新一次才能激活。即访客第一次访问是不受sw控制的。换句话说,访客首屏不受加速,其加载速度与你的服务器有直接联系 [虽然安装完成后就起飞了,但安装前就是屑]

而本人未成年,浙江的规定又是未成年不得备案,在加上之后备案主题切换异常的麻烦。我的决定是至少高考前都不会备案。那这后果就很直接,我并不能使用国内的cdn节点。再加上我又没这个经济实力用香港CN2或拉iplc专线,对首屏服务器这一块优化其实能做的很少。

更加令人抓狂的是,在首屏加载时,静态资源会被直接获取,并且不缓存,而sw激活后又会强制第二次获取,拉跨速度。

不过我们可以尽可能弱化这一劣势。我们可以用js打断所有的请求,以确保首屏只加载一个html和一个sw.js,其余资源都不会加载,降低首屏加载延迟。

我们尽可能将打断代码提到<head> 以确保不被其他资源堵塞,对于hexo来说打开主题的head.ejs,在<head>标签靠前的位置中加入:

(async () => {//使用匿名函数确保body已载入    /*    ChenBlogHelper_Set 存储在LocalStorage中,用于指示sw安装状态    0 或不存在 未安装    1 已打断    2 已安装    3 已激活,并且已缓存必要的文件(此处未写出,无需理会)    */    const $ = document.querySelector.bind(document);//语法糖    if ('serviceWorker' in navigator) { //如果支持sw        if (Number(window.localStorage.getItem('ChenBlogHelper_Set')) < 1) {            window.localStorage.setItem('ChenBlogHelper_Set', 1)            window.stop()            document.write('Wait')        }        navigator.serviceWorker.register(`/sw.js?time=${ranN(1, 88888888888888888888)}`)//随机数,强制更新            .then(async () => {                if (Number(window.localStorage.getItem('ChenBlogHelper_Set')) < 2) {                    setTimeout(() => {                        window.localStorage.setItem('ChenBlogHelper_Set', 2)                        //window.location.search = `?time=${ranN(1, 88888888888888888888)}` //已弃用,在等待500ms安装成功后直接刷新没有问题                        window.location.reload()//刷新,以载入sw                    }, 500)//安装后等待500ms使其激活                }            })            .catch(err => console.error(`ChenBlogHelperError:${err}`))    }})()

当然了,这回导致白屏500ms,如果你觉得不好看,也可以和我一样载入一个等待界面,将document.write()改为:

document.body.innerHTML = await (await fetch('https://npm.elemecdn.com/chenyfan-blog@1.0.13/public/notice.html')).text()

即可

主服务器优化 - 绝望中的最后一根稻草

实际上我一直以为,主服务器的优化在整个网站中是最必要的,也是最不重要的。必要的原因是它关系到最初的加载速度,也是访客中最主要的一环;不重要的是相对于静态资源的记载、页面的渲染和脚本的编译,首屏的加载对整体效果太小了。

尤其是在sw托管后,之后的所有访问,除了sw的更新(以及所有镜像源全炸后),基本与主服务器脱离了关系,正常访问与其没有关系。加速源站实则没有必要。

当然,你说有用吗,那肯定还有点用处,至少首次加载不至于卡人心态。我的要求很简单,能在600ms内完成首次html下载的基本就没问题了。
最优的无非是香港CN2,不过我们没这个能力。退而求其次,普通的香港服务器我们也能接受。不过依旧是白嫖至上的念头,我才用了Vercel。Vercel也是可以自选节点(主要是亚马逊和谷歌)的,我稍加测试【主要注重可连接性,其次是延迟。丢包和速度作为建站(尤其只有一个10kb的网页)最次】,决定使用一下策略:

电信 104.199.217.228 【台北 谷歌Cloud】 70ms (绕新加坡 但是是质量最好的)联通 18.178.194.147 【日本 亚马逊】 45ms (4837和aws在tyo互联)移动 18.162.37.140 【香港 谷歌Cloud】 60ms (移动去香港总是最好的选择)

在这里,不推荐其他地区的理由是:

电信:基本上除了台北,去GoogleCloud的都是走日本ntt × ; 亚马逊这一块有大部分绕美,去日本延迟异常的大。
联通:去香港的绕美了,去日本这一条存在这直接的互联点,负载比较小,延迟也不错。
移动:没什么好说的,移动互联除了去亚太的都是垃圾。

后记

一张思维导图总结全文,你学废了吗?

2022新春小记

作者 CYF
2022年1月10日 21:57

这是一篇随迟但到的新春贺词(

首先呢,旧年总结是不可能做的,这辈子都不可能做的。我发这篇文章的首要目的就是证明我还活着,毕竟上一篇文章到现在已经快半年了(实际上确实有半年)

2021年是牛年,这一年过得很平淡,但却伴随着不少热点事件,刨去政治的,防沉迷、反诈、jsd掉备等等。这些热点我会在以后(maybe)一一热炒冷饭。

新文仍在赶工,可以戳我围观新文(还没写完)

博客已经开启了自己写的ChenBlogHelper,在第一次进入博客的时候会自动安装并刷新激活。此helper能够在前端绕过备案、优选cdn以及统计。

博客现在在中国大陆有一个广州节点承载,希望这能够给你们带来更好的访问体验。

总之,新的一年,祝大家顺心顺意,愉快地享受新年的每一天!

为什么是APP而不是网页

作者 CYF
2021年7月28日 13:24

一个简单的功能,完全可以在浏览器内实现,凭什么国内某些软件这么希望你去下载,去使用他们的app?

就在不久前,我是真的体会到了什么叫流氓厂商。点名批评一下百度,我苹果手机Safari随便在百度上搜索点什么,还没把营销号、广告和垃圾信息从眼中剔除,突然间,AppStore界面平移到我眼前,一个叫百度的软件可怜巴巴的望着我。于是我点击左上角返回键,然后继续搜索…

这不是一件在国内很常见的事情吗,然后我继续浏览,点击一个百度百科网页链接,又是还没开看,appstore显示了出来,这次是百度百科app。

好,没事,我平复了一下心情,整理了一下被打乱的思绪,继续浏览着百科,滑到页面底部,加载新的内容时,一个弹窗显示出来:使用百度百科APP,获取更好的浏览体验!

关闭,继续浏览。

点击百科内部的内链,尝试跳转到另一个百科界面,突然,浏览器一片空白,我又被引导向appstore。

很抱歉,我直接关闭了百度,使用谷歌和维基百科继续查询资料。这一次,谷歌虽然也在下方提示【在IOS上尝试使用谷歌桌面版,获取更好的体验】,但至始至终没有把我强制跳到appstore。维基百科就更不用说了,连使用app都没有提示。

退出了浏览器,我不禁陷入了沉思。我还记得不久前拿到朋友的新鸿蒙手机,划开屏幕一看,第一个界面全是百度系列:百度、百度搜索、百度智能浏览器、百度贴吧、百度知道…一个个功能冗余的百度app赫然显示在我的眼前,当我一脸震惊地看向朋友,他耸耸肩:点进去就自己下载的,不安装就没办法看了。

看着自己苹果手机中的两个一个浏览器:Safari和Alook,我停止了思考,当一个大厂天天为自己的免费网盘带宽叫屈,下载一个3M的电子书被限成一副狗样时,你还能相信他有这么大的带宽给用户推自己的动辄100MB的APP?这能算是本末倒置吗?

从此,我在手机上再也没有用过百度系。必应和谷歌,DogeDoge和DuckDuckGo成为了我的搜索主力。

为什么是APP

隐私

app对隐私的疯狂到了什么地步?我也就不贴知乎链接了,就贴一个今天cctv的内容吧:

https://tv.cctv.com/live/cctv13/index.shtml?spm=C28340.P1dzdfA9CsHZ.E1oxZyG629bH.79&stime=1627446780&etime=1627448400&type=lbacks

在安卓环境【尤其是国内某些套壳系统】下,app的权限不算小,有些时候可以在没有提醒的情况下把你的浏览器记录翻个遍。

ios其实相对安卓来说,至少系统能主动提醒用户是否给予其访问权利。

这一点我也十分佩服MIUI,能在这种隐私岁随意获取风气下站住来守住用户的底线,无论其目的如何,这一点已经赢得了我的好感【虽然我不用安卓】

对于软件商来说,用户的数据是一大笔财富。比如知道所有人的喜好、购买能力等,这些信息掌握得越细致,越能挖掘到更多的商机。

暂且不说百度,就连TIM和QQ也会主动扫描用户Chrome浏览记录我靠那我的nhentai浏览记录怎么办

互唤醒【For安卓】

为了实现广告营销,部分软件实际上要向用户主动推送广告信息。

尤其是安卓,由于谷歌市场退出中国大陆,国内安卓生态其实很乱,一个简单的消息推送,也能难倒一群开发者。

为什么消息推送变成了一个难题?其实我们想象中的消息推送与实际上的方式有很大差距:

想象中:用户手机<==主动推送==微信服务器
实际上:用户手机<==被动推送==>苹果|安卓消息推送服务器<==主动推送==微信服务器

苹果还好说,18年以前经常会出现微信无法推送的情况,但自从大陆线路优化以及云上贵州的迁移,其推送服务逐渐变得正常。然而谷歌早已退出中国市场,其内置的推送服务器已经不可链接,请问这些app这么办?

答:常驻系统后台。

但是常驻系统后台成为一个Zombine进程也不可避免会被杀掉,请问这又能怎么办?

答:相互唤醒。

当用户打开一个app,此app会在后台激活另一群app,然后如果当前app被杀了,被激活的app又会激活那个被杀的app。

这样就很好理解了虽然只有百度app才会推送广告,但他依旧会引导你去下载百度浏览器—避免被杀掉啊。

为什么不是浏览器应用

隐私

在这个隐私即金钱的时代,对于国内厂商来说,首先一个遗憾的事情是,浏览器是很难获取到用户的隐私信息。不是说功能限制,而是浏览器其核心就是沙盒化。在没有用户同意和外接接口、插件的前提下,你不可能直接用js获取到用户手机/电脑上的文件。

而且最致命的是,如果网页应用敢在后台偷偷上传用户隐私,控制台一开就会使其暴露无遗,相对比APP的黑盒操作,那简直是天差地别。

功能限制

js功能其实很强大,但有些底层和协议上的限制不能做就是不能做,你不可能用js空手写一个SMTP发送邮件,你也不可能直接用SSH协议链接服务器【WebSSH需要在服务器主动安装服务端】

其次,一些十分耗资源和计算力的服务不可能在浏览器上实现,比如腾讯不可能把王者荣耀搬到浏览器上,你也不可能在浏览器里跑机器学习。

网络限制

如果使用浏览器,其每一次打开服务网店都要重新下载上面的js、css和图片资源,这一瞬间爆发对服务器压力其实不小。

而使用app,他可以事先在后台下载好广告图片,其样式和功能无需重新下载,并且很多资源可以缓存在本地,即使短暂离线也能推送。

这一点,PWA技术完全可以胜任。PWA通过在浏览器内ServiceWorker拦截和缓存内容实现离线浏览。但目前来讲PWA技术在国内不温不火【很明显,触碰到了某些企业的利益】,所以还是以应用程序为主。

但是,你这样剩下来的流量费还是比不过强制更新来的多啊

为什么国外没有出现类似的情况

监管缺失

海外,安卓应用最官方的商店只有一家:GooglePlay,虽然不像AppStore那种不上架连安装都不给的程度,但也是一种象征。没有上架谷歌商店的应用基本都会被判定为盗版或者危险。而且谷歌play对广告监管很严。如果一个应用敢像百度般,疯狂推送广告和自唤醒,可能连安全审查都过不去。

在国内,连老大都管不了,宛若袁世凯暴毙天下军阀混战,其乱象不言而喻。

隐私意识缺乏

李彦宏有句名言:中国人更愿意用隐私交换便捷性。

虽然此话一出被无数网友嘲讽,但也不得不承认这确实如此。甚至有些时候自己也是被迫的。没有多少人会上网的时候开无数个虚拟机中继代理AdGuard,相反,有更多人为了PDD的几分钱蔬菜而抢破头。一句话:国人都喜欢薅羊毛,但最终都会成为韭菜被割。

相反,在国外,人们对于隐私十分看重,哪怕GoogleAdsense都被罚了好几次,还不用说Tor之类的隐私保护软件。

使用观念的不同

我个人的习惯是,完成一件事情,用什么东西都越轻越好,不是有必要就不下客户端。比如在电脑微信接收消息,你可以选择下载微信客户端完成传输,也可以用网页微信。相较于前者,后者用完就关,不留痕迹,速度也快。

然而国人的习惯大多是:先下载下来,万一以后有用呢。

当我看到电视上的手机广告,大多8H16G运存128G内存起步,盯着手里这台国产只装了QQ到2021年还能打Minecraft的iPhone6s【实际配置2GB运存A9处理器】,不禁留下了悔恨的泪水:幸好没买安卓。

安卓手机即使内存再大,其底层核心还是虚拟化,加上国内的恶劣的生态,如果你不留神多下点软件,其流畅度甚至比不过6年前的6s。

而手机卡,大多数人的第一个想法是:换一台手机。而不是:我删掉点软件,只保留QQ和微信。

尤其是,在国内的内循环已经完成的前提下,更多人选择了买爱国手机,装爱国软件。实际上,留一条隐私底线其实也没有什么。但偏偏有人喜欢把自己隐私送给别人。

后言

实际上,绝大多数软件从C/S架构向B/S架构的转换是不可避免的。但是国内的生态似乎在阻碍着这一发展。

或许有人会问,隐私再保护有什么用。那我只能说,如果你的隐私在黑市只能卖1毛钱一条,那隐私保护的好的人或许能卖5块钱一条。真正的危害其实不在于精准推送,而更怕有人会拿去做违法事情,暴力你,诈骗你。

致敬袁老,精神永在

作者 CYF
2021年5月22日 16:49

“共和国勋章”获得者、中国工程院院士、国家杂交水稻工程技术研究中心主任、湖南省政协原副主席袁隆平,因多器官功能衰竭,于2021年5月22日13时07分在长沙逝世,享年91岁。

感谢袁老,让我们端的起饭碗,让我们00后的孩子至少吃得饱。一路走好。

我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=3uzi7w7znlsa

看毛片(KMP)算法小记

作者 CYF
2021年4月7日 19:22

一直摸鱼的CYF突然安静了下来,因为他想学学一个别人都会的算法。

我菜就是菜,只能学别人早就会的算法了

先贴维基链接【讲的比CSDN清楚,自己访问】

https://zh.wikipedia.org/wiki/KMP%E7%AE%97%E6%B3%95

长串匹配短串,基本上DP打一遍就冲了。

DP本质简单的很:

匹配串:ABCDPOPABQO待匹配串:ABQ

开始匹配

ABCDPOPABQO↑ABQ↑ABCDPOPABQO ↑ABQ ↑ ABCDPOPABQO  ↑ABQ  ↑  ABCDPOPABQO ↑ ABQ ↑...

显而易见,这种复杂度极高【O(m*n)】,当然,冲个入门级别的绝对没问题

然而类似基因匹配这种数量级恶心心的东西,DP绝对是不够的。

或者说一个恶心的数据

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAB

这样如果直接硬DP,那绝对TLE。

于是,我们选择一个简单的算法,看毛片 KMP算法,他可以很好的提升我们匹配的效率

KMP

首先名字很有意思,之所以叫做KMP,是因为这个算法是由Knuth、Morris、Pratt三个提出来的,取了这三个人的名字的头一个字母。

跳过已匹配字符

扯远了,将匹配方式简单讲一下

ABCDPOPABQO↑ABQ↑匹配,指针右移【以下未特殊表明均指针】ABCDPOPABQO ↑ABQ ↑ 匹配,右移ABCDPOPABQO  ↑ABQ  ↑

这时候我们撞上了不匹配的情景,怎么办?右移一位?

不,我们发现原串里面的C在待匹配串里面根本没出现,并且原串[0]~[2]均不匹配,所以我们选择把匹配串整个向右移动三位

ABCDPOPABQO   ↑   ABQ   ↑   不匹配,字符串右移一位ABCDPOPABQO↑    ABQ    ↑   省略数步ABCDPOPABQO       ↑       ABQ       ↑   匹配,右移ABCDPOPABQO        ↑       ABQ        ↑匹配,右移ABCDPOPABQO         ↑       ABQ         ↑匹配,右移这时候待匹配串已匹配完毕,记录并将整个串移动到末尾ABCDPOPABQO          ↑          ABQ          ↑  待匹配串已超过原串长度,结束

部分匹配

上面的例子可能没有讲到重点,接下来换个例子,部分匹配才是KMP的核心

ABCEABCABCDABDABCDABD

OK我们开始匹配

ABCDABCDABDABCDABD↑ABCDABD↑ABCDABCDABDABCDABD ↑ABCDABD ↑ ABCDABCDABDABCDABD  ↑ABCDABD  ↑ABCDABCDABDABCDABD   ↑ABCDABD   ↑   ABCDABCDABDABCDABD    ↑ABCDABD    ↑ABCDABCDABDABCDABD     ↑ABCDABD     ↑ABCDABCDABDABCDABD      ↑ABCDABD      ↑

这个时候我们发现了原串[6]与比较串不符合,这时候怎么办?直接跳到后面去?

ABCDABCDABDABCDABD       ↑       ABCDABD       ↑

好家伙,你会直接丢掉[4]~[10],而这正是我们要匹配的

所以我们定个小规矩:

移动位数 = 已匹配的字符数 - 对应的部分匹配值

换句话说,就是移动到下一个重复片段的地方

所以这个时候我们应该移动4位而不是6位

ABCDABCDABDABCDABD    ABCDABD  ...ABCDABCDABDABCDABD    ABCDABD

这个时候我们已经匹配到了一串,那么接下来怎么移?还是直接移动6位?

不,我们还是要用自己定下的规矩,移动4位

ABCDABCDABDABCDABD          ↑        ABCDABD          ↑

此时,我们才能将其整个向右移动2位

ABCDABCDABDABCDABD           ABCDABD   ...ABCDABCDABDABCDABD           ABCDABD

OK我们将其匹配完毕,整个ABCDABCDABDABCDABD包含两处ABCDABD

部分匹配表

这是一张神奇的表格

首先我们搞清楚前缀和后缀是什么

SHITERS

- 前缀Q['S','SH','SHI','SHIT','SHITE','SHITER']- 后缀H['HITERS','ITERS','TERS','ERS','RS','S']

“部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度

那么SHITERS的部分匹配值是多少呢?相当于Q和H里面有几个元素是共存的?

答,一个

所以,其S部分匹配值是1,而其他均为0

所以这张表有什么用?和部分匹配表有什么关系?

回到之前的,我们会发现有些时候往后匹配时有时候后缀和前缀会相同,那么匹配值向右移动就是其部分匹配值。

尝试上手

板子题P3375

匹配这样子就是有手就行,而所谓的border其实就是部分匹配值比较扯淡的是第一次看的时候就是没搞懂border

扯皮点,直接搞getfail

void getfail(int plen){    border[0]=0;     for(int i=1;i<plen;i++){        int j=border[i];        while(j&&p[i]!=p[j]) j=border[j];if(p[i]==p[j])border[i+1]=j+1;    }}

然后夹带上主代码直接冲了

#include<bits/stdc++.h>#define M 1000000using namespace std;//脚手架char c[M],p[M];int border[M];//...getfail丢着int main(){    scanf("%s",c);scanf("%s",p);    int clen=strlen(c);int plen=strlen(p);    getfail(plen);    int j=0;    for(int i=0;i<clen;i++){        while(j&&p[j]!=c[i]) j=border[j];        if(p[j]==c[i]) j++;        if(j==plen) printf("%d\n",i-plen+2);    }    for(int i=1;i<=plen;i++) printf("%d ",border[i]);    return 0;}

Euserv正确打开优化方式

作者 CYF
2021年3月14日 11:12

Euserv,盛名远扬【老白嫖怪了】,但是如何合理打开它,却是一个难题。这篇文章就是简单讲讲合理使用其免费的纯IPv6小鸡

首先是bench测试普通小鸡

---------------------------------------------------------------------- CPU Model             : AMD Phenom(tm) II X6 1055T Processor CPU Cores             : 1 CPU Frequency         : 3101.198 MHz CPU Cache             : 512 KB Total Disk            : 9.8 GB (3.4 GB Used) Total Mem             : 976 MB (334 MB Used) Total Swap            : 976 MB (0 MB Used) System uptime         : 76 days, 21 hour 6 min Load average          : 16.17, 19.45, 20.53 OS                    : CentOS Linux release 7.9.2009 (Core) Arch                  : x86_64 (64 Bit) Kernel                : 4.20.8-1.el7.elrepo.x86_64 TCP CC                : cubic Virtualization        : LXC Organization          : AS29432 TREX Regional Exchanges Oy Region                : Pirkanmaa---------------------------------------------------------------------- I/O Speed(1st run)    : 19.3 MB/s I/O Speed(2nd run)    : 28.5 MB/s I/O Speed(3rd run)    : 34.0 MB/s Average I/O speed     : 27.3 MB/s---------------------------------------------------------------------- Node Name        Upload Speed      Download Speed      Latency      Speedtest.net    288.12 Mbps       350.72 Mbps         46.84 ms

这是一张简单优化后的小鸡

---------------------------------------------------------------------- CPU Model             : Intel(R) Xeon(R) CPU E3-1270 v3 @ 3.50GHz CPU Cores             : 1 CPU Frequency         : 3740.322 MHz CPU Cache             : 8192 KB Total Disk            : 9.8 GB (0.9 GB Used) Total Mem             : 976 MB (60 MB Used) Total Swap            : 976 MB (0 MB Used) System uptime         : 0 days, 0 hour 36 min Load average          : 5.06, 6.01, 7.01 OS                    : Debian GNU/Linux 10 Arch                  : x86_64 (64 Bit) Kernel                : 4.20.8-1.el7.elrepo.x86_64 TCP CC                : cubic Virtualization        : LXC Organization          : AS13335 Cloudflare, Inc. Location              : Frankfurt am Main / DE Region                : Hesse---------------------------------------------------------------------- I/O Speed(1st run)    : 71.8 MB/s I/O Speed(2nd run)    : 58.1 MB/s I/O Speed(3rd run)    : 57.3 MB/s Average I/O speed     : 62.4 MB/s---------------------------------------------------------------------- Node Name        Upload Speed      Download Speed      Latency      Speedtest.net    203.45 Mbps       105.67 Mbps         9.28 ms      Beijing    CU    71.94 Mbps        112.58 Mbps         271.97 ms    Shanghai   CU    89.92 Mbps        111.97 Mbps         245.60 ms    Guangzhou  CT    0.18 Mbps         131.79 Mbps         234.00 ms    Guangzhou  CU    103.38 Mbps       118.68 Mbps         284.22 ms    Shenzhen   CU    82.26 Mbps        115.67 Mbps         268.41 ms    Hongkong   CN    81.69 Mbps        141.33 Mbps         274.20 ms    Singapore  SG    99.51 Mbps        108.96 Mbps         330.77 ms    Tokyo      JP    104.81 Mbps       85.70 Mbps          246.56 ms   ----------------------------------------------------------------------

安装 - Debian10

这里务必要安装Debian系统,不然后面可能会有点小问题

可能要很长一段时间,完毕后ServerData记录ipv6地址和密码备用

链接SSH

由于次小鸡用的是纯ipv6,鉴于国内ipv6的部署情况您很有可能连接不上,请选择以下六种方式链接

  • CloudFlareSpectrum + UcloudGlobalSSH
  • 嘿哟终端
  • ZeroTier虚拟局域网
  • 挂ipv6代理
  • 使用另一台已安装宝塔的Euserv小鸡,用宝塔自带的终端ssh中继到另一台服务器
  • 使用ipv4+ipv6双栈vps,用ssh链接

篇幅所限,只讲第一种

CloudFlareSpectrum + UcloudGlobalSSH

这个方案比较推荐,就是需要白嫖一个CloudFlarePro

UcloudGlobalSSH只能支持ipv4,所以你需要一个CloudFlareSpectrum中继

CloudFlareSpectrum每月5GB流量,仅SSH链接完全足够

这一步之后需要注意解析你的专属ip,CloudFlareSpectrum使用的ip不是供用的。

Windows命令提示符使用

nslookup abc.cyfan.top.cdn.cloudflare.net

或者使用我的DNS over HTTPS

https://api.cyfan.top/ohhhdns?name=abc.cyfan.top.cdn.cloudflare.net&host=true

请自己更改abc.cyfan.top这个域名。

解析的ip形如172.65.124.0,但这个ip并不好,三网都很差,所以用UcloudGlobalSSH

UcloudGlobalSSH拥有免费版一天1GB,完全足矣

https://console.ucloud.cn/upathx/globalssh

新建一个隧道

将之前解析的ip写入,区域建议香港,点击确定,生成专属域名

然后链接,我这里用的是XShell,其实客户端自己看喜好

域名就是ucloud的专属域名,端口是UC分配给你的而不是22,922是UC给我的端口,密码是Euserv的密码,用户直接用root

然后就直接链接

使用此方式链接方式如下

你 - 中国 <=40ms=> Ucloud - 中国香港 <=10ms=> CloudFlareSpectrum - AnyCast <=Argo 横跨北半球,150ms=> Euserv - 德国

也就是说链接直连可以与美国vps媲美

DNS设置 -DNS64

Euserv只有一个ipv6地址,没有ipv4网卡,所以只能链接纯ipv6网站

使用DNS64可以强制把域名解析到ipv6地址,并且原来只有ipv4的也能通过算法解析到ipv6

由于你要安装Warp,为了下载来自外网的软件,需要使用DNS64

nano /etc/resolv.conf

大概是这样的

search blue.kundencontroller.deoptions rotatenameserver 2a02:180:6:5::1cnameserver 2a02:180:6:5::1enameserver 2a02:180:6:5::1dnameserver 2a02:180:6:5::4

删除最后4行nameserver,添加

nameserver 2001:67c:2b0::4nameserver 2001:67c:2b0::6

Warp安装 - 上IPv4地址

Debian安装时若意外退出则需要相当麻烦解除锁定,所以建立一个稳定的隧道是必须的

首先安装一些必要依赖

apt updateapt install curl sudo lsb-release -y

添加 back­ports 源,并安装wireguard

echo "deb http://deb.debian.org/debian $(lsb_release -sc)-backports main" | sudo tee /etc/apt/sources.list.d/backports.listsudo apt updatesudo apt install net-tools iproute2 openresolv dnsutils -ysudo apt install wireguard-tools --no-install-recommends

然后因为是LXC虚拟内核,无奈之下只能使用go语言编译的内核

curl -fsSL git.io/wireguard-go.sh | sudo bash

安装wgcf【第三方注册器】,注册并生成配置

curl -fsSL git.io/wgcf.sh | sudo bashwgcf registerwgcf generate

修改配置

nano wgcf-profile.conf

内容差不多这样:

[Interface]PrivateKey = xxxAddress = 172.16.0.2/32Address = fd01:5ca1:ab1e:89f5:9dfa:759c:9348:13e6/128DNS = 1.1.1.1MTU = 1280[Peer]PublicKey = xxxAllowedIPs = 0.0.0.0/0AllowedIPs = ::/0Endpoint = engage.cloudflareclient.com:2408

几个关键配置

  • 第五行DNS改成
DNS = 2606:4700:4700::1111
  • 删除第十行,否则Warp会托管ipv6

    AllowedIPs = ::/0
  • 修改第十一行为

Endpoint = [2606:4700:d0::a29f:c001]:2408

输入以下命令

sudo cp wgcf-profile.conf /etc/wireguard/wgcf.confsudo systemctl start wg-quick@wgcfsudo systemctl enable wg-quick@wgcfecho 'precedence  ::ffff:0:0/96   100' | sudo tee -a /etc/gai.conf

然后ping一下baidu:

root@srv10866:~# ping baidu.comPING baidu.com (220.181.38.148) 56(84) bytes of data.64 bytes from 220.181.38.148 (220.181.38.148): icmp_seq=1 ttl=50 time=452 ms64 bytes from 220.181.38.148 (220.181.38.148): icmp_seq=2 ttl=50 time=300 ms64 bytes from 220.181.38.148 (220.181.38.148): icmp_seq=3 ttl=50 time=302 ms...^C--- baidu.com ping statistics ---26 packets transmitted, 23 received, 11.5385% packet loss, time 121msrtt min/avg/max/mdev = 297.390/307.370/451.503/30.906 ms

简单安装besttrace

wget https://cdn.ipip.net/17mon/besttrace4linux.zipapt-get install zipunzip besttrace4linux.zipchmod +x besttrace

路由到CF网络都是一步到位

root@srv10866:~# ./besttrace -q 1 1.0.0.1traceroute to 1.0.0.1 (1.0.0.1), 30 hops max, 32 byte packets 1  one.one.one.one (1.0.0.1)  9.61 ms  AS13335  CLOUDFLARE.COM, apnic.netroot@srv10866:~# ./besttrace -q 1 cloudflare.comtraceroute to cloudflare.com (104.16.133.229), 30 hops max, 32 byte packets 1  104.16.133.229  13.62 ms  AS13335  CLOUDFLARE.COM, cloudflare.com

路由到谷歌,那就是穿透一层内网直接出去

root@srv10866:~# ./besttrace -q 1 google.comtraceroute to google.com (142.250.186.46), 30 hops max, 32 byte packets 1  172.16.0.1  9.45 ms  *  LAN Address 2  162.158.82.1  18.34 ms  AS13335  Germany, Hesse, Frankfurt, cloudflare.com 3  162.158.84.5  10.08 ms  AS13335  Germany, Hesse, Frankfurt, cloudflare.com 4  108.170.251.129  28.06 ms  AS15169  United States, google.com 5  172.253.71.89  11.58 ms  AS15169  United States, google.com 6  fra24s04-in-f14.1e100.net (142.250.186.46)  12.48 ms  AS15169  Germany, Hesse, Frankfurt, google.com

路由到百度就很有意思,从HKG出去进入移动CMI北上北京

去年10月移动在欧洲和cf做的对等互联,因为遵循路由链最短原则,电信和联通在欧洲没有比这个更短的,到联通是cf-gtt-联通,到电信是cf-level3-电信,到移动是cf-移动,所以会走移动
金句From – CLAM

root@srv10866:~# ./besttrace -q 1 baidu.comtraceroute to baidu.com (39.156.69.79), 30 hops max, 32 byte packets 1  172.16.0.1  9.50 ms  *  LAN Address 2  162.158.82.1  10.37 ms  AS13335  Germany, Hesse, Frankfurt, cloudflare.com 3  223.119.65.37  20.01 ms  AS58453  China, Hong Kong, ChinaMobile 4  * 5  221.183.46.250  257.40 ms  AS9808  China, ChinaMobile 6  221.176.27.253  200.99 ms  AS9808  China, ChinaMobile 7  111.24.2.241  202.99 ms  AS9808  China, ChinaMobile 8  * 9  39.156.27.1  203.69 ms  AS9808  China, Beijing, ChinaMobile10  *11  *12  *13  *14  39.156.69.79  259.35 ms  AS9808  China, Beijing, ChinaMobile

Speedtest

curl -fsSL git.io/speedtest-cli.sh | sudo bashspeedtest

返回

   Speedtest by Ookla     Server: RETN - Frankfurt (id = 31120)        ISP: Cloudflare Warp    Latency:     9.88 ms   (0.11 ms jitter)   Download:   112.57 Mbps (data used: 101.0 MB)                                    Upload:   127.19 Mbps (data used: 115.5 MB)                               Packet Loss:     0.0% Result URL: https://www.speedtest.net/result/c/556afff6-e56a-4d53-844b-fe88b562ceb8

速度不快,但是起码上ipv4了

失败的光速叛逃!CloudFlarePage初体验

作者 CYF
2021年2月20日 09:24

众所周知,CloudFlare曾开放CloudFlarePage内测资格申请,如今我获得了资格,免费享受边缘部署岂不美哉!可万万没想到,简单的迁移过程会出现如此问题

据我所知,使用CloudFlare做hexo无非以下两种:

  • GithubPage+CloudFlareCDN
  • CloudFlareWorkerKV+ClouFlareWorkerSite

CloudFlare早先时候支持WorkerSite,当时KV照实没有免费,我也不想为了100ms的回源耗时而花费金钱。不过后来KV在一定额度上免费了,打开WorkerSite的文档,第一步wrangler直接把我劝退。

笑话,国内使用wrangler,那还不如CloudFlare+GithubPage。

苏卡卡大佬写的一篇文章讲述了自己部署WorkerSite的经历,ChrAlpha’s Blog也曾提到过迁移的过程。不过我懒,我觉得100ms回源不算什么,赔个Worker还是有点亏。

2020年11月份,偶然得知CFPage正在公开招聘Pagebeta计划,抱着试试看的心理,我简单写了些就交了上去。凭借着对CloudFlare发布新产品小心翼翼的态度,我揣摩估计很难申请到。果不其然,年都过了,连封邮件都没通知我。

2021年2月20日,我先日常翻了遍邮件,0。正准备继续开发HexoPlusPlus,登陆CloudFlare,却发现右边多了个新玩意:

不愧是我,我一眼就看出来我的CFPage申请到了。

迁移

由于我的博客源码在Github上,而CFPage只能从Github上获取源码。
我面向中国大陆CDN有一部分用的也是CloudFlare,通过BNXB第三方接入。
因为之前用的就是GithubAction的集成部署,所以package.json已经配置完成了。

那么我看起来确实是最佳的接入选择。

非常操蛋的是,我无法删除已经添加的CloudFlarePage域名,所以我没有办法重新演示我如何安装,下面的截图是删除时的错误:

首先,进去,点击创建项目选择博客的github存储库,获得GithubAccess权限后跳转到这:

勾选需要接入的项目

  • 构建设置 - 框架预设

由于之前吃过Vercel的亏,不想选Hexo,但是抱着试试看的心态,还是选择了内置的hexo。

选择之后,后面的构建命令和构建输出也直接填好了,保存并部署?

因为本博客使用了neat而不是gulp插件压缩html管他呢反正丢CI,所以构建时间会比较长。

修改CNAME记录和TXT记录,分别去BNXB修改CNMAE和DNSPOD【NS所在处】修改TXT记录

就好了?就好了。

这是我最快的迁移速度

NetWork选项卡里出x-server: Cloudflare Pages说明迁移完毕。

使用体验

简洁,加载速度快,这是我第一个感受。

用GithubAction老是卡在同一个界面不会动,要手动刷新一下才能出来,而且加载的东西贼多,我这台破笔记本有点卡。

CloudFlarePage则轻巧的多,并且部署状况很快就能体现出来。

无缝切换,这是我的第二个体验。

以前换CI换CDN的时候,非得断掉先然后才能切换回来,现在两步走之后,直接在dns平台修改无缝迁移,体验良好,总耗时不超过10分钟。

鉴于CloudFlare在国内的连接情况,电信这一条线我还是切回vercel【也是0回源】,其他的走CloudFlarePage

平台还是beta,这是第三个感受。

我删不掉已经添加的域名,这是我最纳闷的一点。

然后我不想让cfpage检测我的gh-pages分支,因为GithubAction还有存在的必要,但是CFpage每每还是检测pages分支,然后扔出部署失败,提示错误。

这两个问题我已经丢论坛里了,目前还未回复

至于快多少,因为没有免费版本Argo做对比,也没有大量数据做对比,目前无法得出结论。

真的要说快多少,因为大部分静态资源切jsd上,所有经过cf的只有一个单html,至少我在国内大环境下,我还没体会出100ms能快多少(´இ皿இ`)

另外CloudFlarePage用的证书很奇怪,可能是为了CNAME兼容性,不用自己自家的证书,反而用Let’s Encrypt,鉴于OSCP在国内阻断,ios用户可能会出现首次访问白屏,这一点我有点担心。

最好的一点莫过于完全贴合HexoPlusPlus了,以后我就有充分的理由宣传HPP了

额度

  • 每月构建次数:500次

【用了HPP,构建次数再多也不够500/mo,这一点我还是放心的,再说我常年咕咕咕】

  • 自定义域名:10个 【我也没这么多域名】

  • 文件:2w个【绝对用不完】

  • 总大小:25MB【图片啥的都扔图床,其实也就5MB】

  • 带宽:无限制【!!!】

然后我切回来了

高高兴兴的搞完了CFPage部署,水了这篇文章去吃饭。吃完饭后回来一看谷歌统计,好家伙404的怎么这么多。

还好之前的GithubPage没有删掉,去bnxb赶紧切了回来。

完了我也经历了和Sukka大佬一样的问题

问题很容易定位,所有的404来自cfpage而非vercel。

首先是Vercel,开代理的情况下国外访问均正确解析至vercel,可以在x-vercel头里看出来

地址是https://blog.cyfan.top/p/52382e42.html,相应代码是200

关闭代理,将自动选择CloudFlareCDN+GithubPage,可以从x-github-request-id看出

地址是https://blog.cyfan.top/p/52382e42.html,相应代码是200

然后是有问题的CFPage,可以在x-server头里看出来

地址是https://blog-9una.pages.dev/p/52382e42.html,相应代码是308跳转,跳向https://blog-9una.pages.dev/p/52382e42CFPage会把末尾.html抹掉

抹掉就罢了,结果在vercel这边又出问题

但是最奇葩的是,githubpage是允许不带html裸访

这就是整个经过,CFPage必须抹掉后缀,GithubPage保持无所谓,Vercel必须不能抹掉

解决办法

先从自己入手,CFPage和vercel不能共留,干掉vercel?好主意,毕竟国内访问CF并无大碍,但是有个大问题梗在面前,评论怎么办,之前的收录怎么办,首页链接不一致怎么办?

我又不想抛弃CFpage,于是试图和Sukka大佬针对中文解码一样来个拯救计划,结果发现,CFPage不开源……

后来仔细翻了一遍文档才发现,这样是有意为之,故意删掉后缀名。

的,切回Github+CFCDN,这一早上的折腾白费了( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃

PanList:ServerLess百度网盘列表直链程序

作者 CYF
2021年1月6日 21:44

警告

本篇文章在panlist发布后两天内写成,panlist尚处于高速迭代状态,本篇教程随时会失效!

百度网盘2021年最新不限速下载方式是怎么回事呢?百度网盘相信大家都很熟悉,但是百度网盘2021年最新不限速下载方式是怎么回事呢,下面就让小编带大家一起了解吧。
百度网盘2021年最新不限速下载方式,其实就是PandownloadCloudFlareWorkers版本,大家可能会很惊讶百度网盘怎么会2021年最新不限速下载方式呢?但事实就是这样,小编也感到非常惊讶。
这就是关于百度网盘2021年最新不限速下载方式的事情了,大家有什么想法呢,欢迎在评论区告诉小编一起讨论哦!

之前写过GoogleDriveOneDrive 的CloudFlare列表程序,这两个有个共同点,那就是国内都无法很好的下载,所以用CloudFlareWorkers作为中间件转发流量。

比较讽刺的是,CloudFlareWorkers用的都是国外节点,但以上两个运行于此的目录列表程序支持的下载速度却远远大于国内有节点的百度网盘,不说是移动BGP走香港有多好,至少我在『电信』网络环境下的下载速度都远远大于百度网盘。很显然,百度网盘作为垄断国内网盘市场的资本,既然已经度过了烧钱的时间,那么现在自然是能剩多少省多少,这就是『吃相难看』。

两年前,当微博上的人们开始质问突然『诈尸』的百度网盘微博账号,网友们才纷纷意识到自己的百度网盘被恶意限速了,紧接着,破解百度网盘限速的方式层出不穷。只是后来,这些方式一点一点被打压下去,最终戛然而止。

最近这几天因为在写HexoPlusPlus: A ServerLess Hexo Dashboard 正在努力学习CloudFlareWorkers。由于评论模块需要数据结构有关知识,可惜这一块知识大都都被一些所谓白嫖CloudFlareWorkers的教程所淹没:

"Google上CloudFlareWorkers搜索结果"

在查询有关资料的时候,Github上一颗冉冉新星引起了我的注意,PanList ,根据其Commit显示,这个仓库仅仅是两天前刚刚开辟的,但是本文写是就有61Star和21Fork 【当然我也有Star】,可见其热门程度。不过简单的翻看了一下我就明白其为何如此热门,第一,它是专门对付令人头疼的百度网盘,第二,它是构建于CloudFlareWorkers。

起步

电脑登录百度网盘:

"百度网盘"

按下F12进入开发者模式,选择Application模块,点击Cookies:

"Cookies"

在这里,我们需要获取两个Cookie:BDUSSSTOKEN

获取

GithubJSDelivr
原版Github下载地址原版JSDelivr下载地址

修改前面四行代码,分别为

const BDUSS = ''const STOKEN = ''const USERNAME = ''const PASSWORD = ''

将获取到的Cookie复制到第一行和第二行。第三、四行是登录后台所需的账号密码。

AnyWay,你也可以将这些写入到后台变量

新建一个Worker,将处理好的代码直接复制进去:

复制代码

保存并部署,确定。

绑定域名

绑定域名

路由:{待绑定的域名}/* [后面*的原理是覆盖该路径下所有文件]
Worker:你新建的Worker名字

评测

登陆界面:

login

打开后:

挺讽刺的是,去除了百度网盘原始界面乱七八糟的东西,CloudFlareWorkers的国外服务器打开速度都比百度网盘快

Chrome直接下载速度

传递Cookie后IDM下载速度:

注:单线程下载速率最终取决于百度网盘的限制,也就是说无论如何你也无法逃避百度网盘对源头的遏制,就在我下完Soul这部将近2GB的电影后,我的账号就被百度拉入黑名单,现在即使用IDM也只有80kb/s 但是还是比原来的客户端快

尾声

本质上panlist无非就是通过baidu没有公开的API通过Cookie鉴权来获取文件列表和下载,虽然依旧没有突破单线程下载限速,但至少实现了直链下载和多线程下载的功能,并且部署在CloudFlareWorkers上的方式确实大开眼界,无服务器应用的范围又扩展了一步,我十分看好这个项目。

另外,本人也在努力开发一款同样基于CloudFlareWorkers的评论系统,以下是截图:

最后,来一句姗姗来迟的新年祝福:

CFWorker-ODIndex中文文档&使用教程

作者 CYF
2020年8月14日 09:51

写在最前面
自从这篇博文发布后,OneIndex迎来了一次重大更新,去除了FireBase依赖,转用KV存储刷新令牌。本片教程已过时,具体会在春节前后更新最新版本。

OneDrive,相信大家都不陌生,微软家的网盘,虽说有类似于OneManager一样的目录列表程序,但是直连海外总是会遭受严重的丢包,得到极其糟糕的体验。这时候CLoudFlare作为不那么好使的CDN就排上用场了,项目onedrive-cf-index 解决了这个问题,但是据我观察,这个仓库作者应该是个中国人,但是readme文档可不是中文,这对一些使用者造成了一定的困扰,而且文档相当言简意赅,这一篇文章相当于文档的中文翻译和使用教程.

使用前言
部署此项目较为麻烦,请确保你满足以下俩个及以上想法后,才继续往下读:

  1. 我非常需要搭建于CloudFlare的OneDrive目录列表吗?
  2. 我非常需要这个版本的目录列表吗?
  3. 我有这个耐心吗?
    如果没有,那么我们建议你可以使用另一个功能和UI稍差的cf目录列表OneDrive-Index-Cloudflare-Worker 这个版本有不是很详尽的中文文档,并且较为简单,或者试试一步生成的GDIndex,可以参考我之前写的GDIndex部署参考

简介

新功能

  • 新设计: spencer.css。
  • 根据文件类型呈现的文件图标。
  • 使用“Font Awesome icons” 图标代替材料设计图标(以获得更好的设计一致性)。
  • 使用github-markdown-css进行README.md渲染
  • 添加Cookie以更好地进行目录导航。
  • 支持文件预览:
  • 图片:.png,.jpg,.gif
  • 纯文本:.txt
  • 文档:.md,.mdown,.markdown
  • 代码:.js,.py,.c,.json。
  • PDF:延迟加载,加载进度和内置的PDF查看器
  • 音乐/音频: .mp3,.aac,.wav,.oga
  • 视频: .mp4,.flv,.webm,.m3u8

  • 代码语法以GitHub样式突出显示。(使用PrismJS。)
  • 图像预览支持中型缩放效果。
  • 使用Google Firebase实时数据库缓存和刷新令牌。(对于那些负担不起Cloudflare Workers KV存储的人。 😢)
  • 在Turbolinks®的帮助下进行延迟加载。(从folder转到时有些问题file preview,但不会降低用户体验。)

在这表面下:

  • 一直以来都是CSS动画。
  • 使用wrangler和webpack打包源代码。
  • 转换所有CDN以使用jsDelivr加载。
  • 没有外部JS脚本,所有脚本都已通过webpack加载!(除了某些库。)


所有其他功能

请参阅:新功能| OneDrive-Index-Cloudflare-Worker

开始

导入此项目

请注意!
请不要Fork此项目,
在这个过程中你将会获取到数串token,fork的仓库无法转为私有仓库,这将会对你的数据造成威胁.

点击右上角的 + 号选择Import项目

原先仓库地址填写https://github.com/spencerwooo/onedrive-cf-index,新仓库名称随意,仓库类型必须改为Private私人,最后导入

导入成功,进入repo:

获取Token

进入https://heymind.github.io/tools/microsoft-graph-api-auth 开始准备你所需要的东西:

进入https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade,注册一个新app【使用微软账号】

几番跳转后,来到这样的界面:

注意,我们强烈建议用英文界面,中文界面可能存在翻译错误产生误导

点击OneIndex,进入应用详情

点击 Redirect URIs 修改跳转链接:

将此处改为 https://heymind.github.io/tools/microsoft-graph-api-auth

设置API:

至少选择 offline_access, Files.Read, Files.Read.All .

返回,获取 Application (client) ID

在这里填上复制的client id,并点击AUTHORIZE验证

你会发现跳转一段时间后又回到了拉取界面,不过多了一个提示框,上面写着已经获取到Code

接着我们获取Secrets,点击 Certificates & secrets,进入该选项卡:

新增一个:

名字随意,期限为永久:

复制生成的密钥,请注意此处密钥仅出现一次,以后就不会再出现了

将获得密钥粘贴进第五步:

点击GetToken,将跳转至如下界面:

此处若失败,请重试第四步

将获得字段的refresh_token写入,重新制得token:

取得token:

妥善保管,接下来我们会用到。

OneDriveToken完毕,接下来进入FireBase教程:

作为数据库存储,原本采用的是cloudFlareKV存储,但考虑到大部分用户没有这个钱去买KV,于是采用了第三方存储。

进入https://firebase.google.com/

开始创建项目:

项目名字随意,下一步

一直下一步,稍后将进入管理界面

选择Database,将其设置为发布模式,禁用写入权限,在数据一栏中添加key,名字为value,值为 https://项目名.firebaseio.com/auth.json ,如我的:
https://oneindex-chenyfan.firebaseio.com/auth.json

点击右上角的小齿轮-项目设置-服务账号-旧版凭据-数据库账号-显示密钥,复制密钥。

FireBase教学完毕。

配置CloudFlare

登录你的CloudFlare,点击右上角,账户设置:

选择API令牌选项卡,生成新的API:

选择自定义令牌:

配置至少如下,可以选择增加

点击获取API令牌

接着还要获取账户ID和区域ID

进入任意一域名,右侧拦里头会有俩ID,复制:

到这里,你获取了以下所有密钥:

  • Azure-refresh_token
  • Azure-client_id
  • Azure-client_secret
  • GoogleFireBase-firebase_url
  • GoogleFireBase-firebase_token
  • CloudFlare-APIToken
  • CloudFlare-ZoneID
  • CloudFlare-AccountID

八个Token,切勿丢失 累死我了

配置OneDrive

用微软账户登录OneDrive,新建文件夹,名字为“Public”

本地wrangler部署

由于此篇教程具体将GithubAction实现无服务器部署,本块内容一笔带过,请直接看到GithubAction部署一块

本地安装依赖环境

yarn global add @cloudflare/wrangleryarn install

用wrangler登录CloudFlare

wrangler config

wrangler.toml 修改

name: The draft worker's name, your worker will be published at <name>.<worker_subdomain>.workers.dev.account_id: Your Cloudflare Account ID.zone_id: Your Cloudflare Zone ID.

src/config/default.js 修改:

client_id: Your client_id from above.base: Your base path from above.firebase_url: Your firebase_url from above.

添加环境变量:

wrangler secret put REFRESH_TOKENwrangler secret put CLIENT_SECRETwrangler secret put FIREBASE_TOKEN

部署命令:

wrangler publish

GithubAction部署

GithubAction部署

进入你导入的仓库,点击 wrangler.toml

  • name ==> 默认生成为<#name>.<#User>.workers.dev,随意
  • account_id ==> CloudFlare-AccountID
  • zone_id ==> CloudFlare-ZoneID

进入/src/config/default.js

修改样式

src/render/htmlWrapper.js : 修改标题

src/folderView.js : 修改介绍

设置密钥

密钥名:CF_API_TOKEN

内容: ==> CloudFlare-APIToken

激活Action:

设置CloudFlare变量

进入新创建的Worker:

  • CLIENT_SECRET ==> Azure-client_secret
  • FIREBASE_TOKEN ==> GoogleFireBase-firebase_token
  • REFRESH_TOKEN ==> Azure-refresh_token

部署,发布,绑定域名【可选】:

成品:

Demo:https://onedrive.cyfan.top

后言

其实默认才5GB,还不如GoogleDrive香,但是据说白嫖E5开发者还不错。

OK就到这了,还不懂的欢迎评论区,或者去官方github地址发issues。

Live2D:为你的hexo博客添加萌萌哒看板娘

作者 CYF
2019年8月15日 14:09

前言:

19号我就要上学了,以后可能会突然失踪一段时间,9月1号正式开学后,我会恢复到2周一更新的频率。在这里对所有关注我博客的人说声抱歉。

我寥寥无几的评论区里突然有人问我右下角的看板娘是怎么做到的:

wpaladins •*********@163.com •2019-08-13 13:33:44
你网站的各种东西确实很炫,只是打开你的博客之后我垃圾mba的风扇就一直转个不停😂我想知道左下角的妹子怎么搞?😁

这让我感动了一分钟….

本来我就有写这一篇文章的感觉,只是没有这个打算,既然您问了,那我就写吧。

live2D for hexo 作者:原作大大的博客

材料:

简单开始:

安装Live2D:

请先cd到你的hexo安装目录下!

如果你安装过低版本的Live2D,那先运行:npm uninstall hexo-helper-live2d
安装: npm uninstall hexo-helper-live2d

注意
中国用户由于显而易见的原因,速度超级慢,具体解决方案前往我早年写的文章 查看

安装完成之后,就会在博客的根目录package.json文件中存在依赖,同时出现node_moduels文件夹

选择模型:

原生模型:

  • Epsilon2.1
  • Gantzert_Felixander
  • haru
  • miku
  • ni-j
  • nico
  • nietzche
  • nipsilon
  • nito
  • shizuku
  • tsumiki
  • wanko
  • z16
  • hibiki
  • koharu
  • haruto
  • Unitychan
  • tororo
  • hijiki

额外模型:
可以前往summerscar大佬的Github查看。

模型预览:
进入https://summerscar.me/live2dDemo/,在modelName:键入模型名即可。

安装模型:

原生模型: 在hexo文件夹根下键入npm install --save live2d-widget-模型名即可。
额外模型: 把文件夹下载到node_moduels下即可。

配置:

打开站点配置文件,在末尾填上:

live2d:  model:    scale: 1    hHeadPos: 0.5    vHeadPos: 0.618    use: live2d-widget-model-tororo // 下载的动画模型名称  display:    superSample: 2    width: 120    height: 200    position: left // 模型在网页显示位置,left为左,right为右    hOffset: 20    vOffset: 50  mobile:    show: true  // 移动设备是否显示,true则显示,false则不显示    scale: 0.5  react:    opacityDefault: 0.7    opacityOnHover: 0.2

接着hexo server看看吧!

进阶:

你看到别人的的看板娘会说话,能拍照,还能打游戏?
那你也能!

警告:
这会严重拖慢你的网站加载速度,并且功能比较鸡肋,并不建议添加.如果你的博客建立在Github Pages上,添加会造成加载困难以致无法加载!

由于我没有使用进阶版,本文以下部分转载来自https://blog.csdn.net/qq_39610915/article/details/90679768

使用大神定制作品
https://github.com/stevenjoezhang/live2d-widget

按照教程
https://blog.pangao.vip/Hexo博客NexT主题美化之新增看板娘(能说话、能换装)/
设定了一番
但网页中并没有出现看板娘
如果你和我一样是纯新手,而且完全按照上述教程进行,那么恭喜你,看板娘一定不会出现!
这是为什么呢?
首先我们去查看这个项目的说明

依赖 Dependencies

本插件需要jQuery和font-awesome支持,请确保它们已在页面中加载,例如在<head>中加入:
jQuery and font-awesome is required for this plugin. You can add this to <head>:

<script src="https://npm.elemecdn.com/jquery/dist/jquery.min.js"></script><link rel="stylesheet" href="https://npm.elemecdn.com/font-awesome/css/font-awesome.min.css">

否则无法正常显示。(如果你的网页已经加载了jQuery,就不要重复加载了)

而教程中并没有指出这一点,因此缺少依赖的博客,一定不会加载出看板娘。
其次是autoload.js的路径设置问题。

正确的步骤如下

第一步

下载大神项目,解压到本地博客目录的themes/next/source下,修改autoload.js文件,将其中

const live2d_path = "https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget/";

改为

const live2d_path = "/live2d/";

autoload.js中的注释的绝对地址指的是,将资源打包放到hexo/theme/next/source中后,以hexo/theme/next/source为根目录(/)的绝对路径,一般我们下载下来的是master分支,那么修改路径就是/live2d-widget-master/xxx.jsautoload.js的最后一个函数initWidget(“/live2d-widget-master/“, “https://api.fghrsh.net/live2d”); 中的第二个参数不要变,是一个后台api。我选择将文件夹重命名为“live2d”,因此按照如上方式修改。

第二步

/themes/next/layout/_layout.swing中,<body>标签中新增如下内容,########为你自己的github账号:

<script src="https://########.github.io/live2d/autoload.js"></script>

<head>标签中新增如下内容:一定一定要加上依赖!!!!!

<script src="https://npm.elemecdn.com/jquery/dist/jquery.min.js"></script><link rel="stylesheet" href="https://npm.elemecdn.com/font-awesome/css/font-awesome.min.css"/>

第三步

在主题配置文件_config.yml 中,新增如下内容:

live2d:  enable: true

想修改看板娘大小、位置、格式、文本内容等,可查看并修改 waifu-tips.jswaifu-tips.jsonwaifu.css

最终效果如图:

后端API地址:
https://github.com/fghrsh/live2d_api

特别提醒!!!!!!
根据原作者反映,2018年十月,使用人数过多曾让该api暂时停止提供服务,更稳妥的方法是自建api。


❌
❌