普通视图

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

CloudFlare CDNJS:免费加速你的前端资源

2025年4月28日 14:18

我们前面介绍了 CloudFlare 的 R2 服务:Cloudflare R2 对象存储白嫖指南:10G存储+免流量费,打造免费图床,其实 CloudFlare 还有另外一个我们使用比较多的服务,就是 CDNJS 服务。

什么是 CDNJS?

CDNJS 是 Cloudflare 维护的一个免费开源 CDN 服务,专门托管热门的前端库(如 jQuery、React、Vue、Font Awesome 等),开发者只需通过简单的链接引用,就能让全球用户快速加载这些资源,无需自己部署服务器。

它有如下的核心优势:

✅ ​全球加速:依托 Cloudflare 的全球 CDN 网络,资源加载更快。
✅ ​自动同步:与 npm/GitHub 同步,确保使用最新版本。
​完全免费:零成本使用,无需担心带宽费用。
✅ ​广泛兼容:支持传统 JS、ES Modules、WASM 等多种格式。

CDNJS 的发展历程

CDNJS 最初是由 ​Thomas Davis​(前端开发者,当时就职于 Twitter)于 ​2011 年 创建,并联合开源社区共同维护,初期只是托管几十个热门库。

当初发起这个库的最大原因就是为了解决开发者直接托管第三方库(如 jQuery)的痛点:

  • 带宽成本高:小团队或个人开发者自建 CDN 成本高昂。
  • 更新延迟:手动下载和部署库版本效率低下。
  • 全球化需求:当时已有的 CDN(如 Google Hosted Libraries)覆盖节点有限。

2014 年接手 CDNJS 的服务器和网络基础设施,并提供了企业级基础设施和全球 CDN 节点支持。

2016 年引入自动化 npm/GitHub 同步,大幅提升效率。

2020 年开始支持现代 Web 技术(如 WASM、ES Modules)。

为什么需要 CDNJS?

自托管 vs. CDNJS

对比项自托管CDNJS
加载速度依赖自身服务器带宽全球 CDN 加速
维护成本需手动更新版本自动同步最新版
缓存命中率仅限自身用户全球共享缓存,命中率更高
可用性服务器宕机=资源不可用高可用性,99.99% SLA

适用场景

  • 个人博客、小型网站(节省带宽成本)
  • 企业级应用(提升全球访问速度)
  • WordPress 等 CMS(优化前端性能)

如何使用 CDNJS

在 HTML 中直接引用 CDNJS 链接即可:

<!-- 加载 jQuery -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>

<!-- 加载 Font Awesome CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />

如要增强安全性,可以使用 ​SRI(Subresource Integrity)​ 防止资源被篡改:

<script 
  src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"
  integrity="sha256-kmHvs0M+1Qz9wuZOJ8E6OE2bM4S4n2GeXJ6X5+5ow="
  crossorigin="anonymous"></script>

由于 CloudFlare 在国内没有加速节点,国内访问可能会有些慢,所以推荐一些国内的 cdnjs 的镜像服务:

服务商特点延迟(国内/国外)
CloudFlare
https://cdnjs.cloudflare.com
✅ 全球加速
⚠️ 国内速度较慢
528 ms / 4 ms
又拍云 CDN
https://s4.zstatic.net
✅ 镜像自 Cloudflare CDNJS
✅ 库齐全,国内速度快
30 ms / 8 ms
字节跳动 CDN
https://cdn.bytedance.com
✅ 速度快企业级稳定性
⚠️ 但库较少,仅 JS 无 CSS 库
27 ms / 216 ms
360 前端静态资源库
https://lib.baomitu.com
⚠️ 更新不太及时
⚠️ 库较少
13 ms / 3 ms
SM.MS
https://cdnjs.loli.net/ajax/libs
✅ 镜像自 Cloudflare CDNJS
⚠️ 个人开发者
160 ms / 8 ms
7ED
https://use.sevencdn.com
✅ 镜像自 Cloudflare CDNJS
⚠️ 个人开发者
33 ms / 180 ms

如果你使用的是 WordPress 博客,那么 #WPJAM Basic# 插件也提供了一个前端公共库,让你一键切换,在「WPJAM」-「优化设置」-「增强优化」:

目前已经内置了下面这几家:

cdnjs.cloudflare.com
s4.zstatic.net
cdnjs.snrat.com
lib.baomitu.com
cdnjs.loli.net
use.sevencdn.com

根据上面的推荐,国内最佳可能是 s4.zstatic.net,具体还是需要你自己切换和体验。

CDNJS 总结

CDNJS 是前端开发者的利器,能显著提升资源加载速度并降低维护成本。

  • 全球用户 → 直接使用 cdnjs.cloudflare.com
  • 国内优化 → 切换至 s4.zstatic.net
  • WordPress → 通过 WPJAM Basic 插件一键配置

PHP 8 重大变更:is_callable() 不再支持类名+非静态方法检查,强制返回 false!

2025年4月18日 00:10

最近在 PHP 8 环境下,发现 #WPJAM Basic# 的一个严重问题:就是后台文章和分类列表的一些操作无效了,点击保存按钮没有任何反应。经过深入调试,发现问题根源在于 PHP 8 对is_callable()函数的行为进行了重大调整。

技术细节

在 PHP 8 之前,is_callable() 函数在检查类名与非静态方法的组合时会返回 true,即使这种调用方式在实际执行时可能会导致问题。PHP 8 对此进行了更严格的检查,以提前发现潜在的错误调用方式,因此 is_callable() 在检查一个类名与非静态方法时将返回失败(应当检查一个类的实例)。

具体看下面这个例子就大概能够明白了:

class Test{
    public function method1() { }
    public static function method2() { }
}

// PHP 8 之前
var_dump(is_callable(['Test', 'method1']));	// bool(true)
var_dump(is_callable(['Test', 'method2']));	// bool(true)

// PHP 8 之后
var_dump(is_callable(['Test', 'method1']));	// bool(false)
var_dump(is_callable(['Test', 'method2']));	// bool(true)
var_dump(is_callable([new Test, 'method1']));	// bool(true)

变更原因

PHP 8 的这一变更是为了更严格地执行面向对象编程规范:

  1. 静态与非静态方法调用的区分:非静态方法需要类的实例才能调用,而静态方法可以直接通过类名调用。
  2. 提前错误检测:在 PHP 7 及以下版本,虽然 is_callable() 会返回 true,但实际以静态方式调用非静态方法时会产生警告。PHP 8 通过 is_callable() 提前返回 false 来更早发现问题。
  3. 代码质量提升:强制开发者明确方法调用的上下文,避免模糊的调用方式。

解决方案

既然知道这个原因,那就是代码要根据上下文来写了。所以如果要检查某个对象的非静态方法是否可调用,应该使用一个对象实例而不是类名。

此外,其实很多时候我们只需要检查类的方法是否存在,而不关心其可调用性(例如访问控制),那么可以简单使用 method_exists() 进行判断就好了:

if (method_exists('Test', 'method1')) {
    // Do something
}

原来 explode 还有第三个参数,竟然这么好用!

2025年4月2日 18:25

explode 是 PHP 一个常见的字符串处理函数,主要用于将字符串按照指定的分隔符拆分成数组。比如下面的代码就是通过英文逗号将字符串转换为数组。

$str	= "apple,banana,orange";
$fruits	= explode(",", $str);

// 结果:
// Array
// (
//     [0] => apple
//     [1] => banana
//     [2] => orange
// )

我相信大部分人(主要是我)都是这么用这个函数,但是绝大部分都不知道,explode 这个 PHP 函数还有第三个参数:$limit,这是一个可选参数,限制返回数组的最大元素数量。

我标题里面说这么好用,好用在哪里呢?🧐

🙋‍♀️🌰 我有个字符串,比如 args=type=select&name=gravatar,这个字符串其实是两部分,第一部分是 args 是 key,第二部分(type=select&name=gravatar),它其实是个 query_string,我的目的到时候通过 WordPress 函数 wp_parse_args 函数是将其解析成数组的。

如果直接使用 explode 分割字符串拆成数组的话:

$str	= "args=type=select&name=gravatar";
$arr	= explode("=", $str);

// 结果:
// Array
// (
//     [0] => args
//     [1] => type
//     [2] => select&name
//     [3] => gravatar
// )

这是我又要使用 implode 函数将除了第一个元素之外,其他元素拼成字符串。

有点烦躁。😒

这是第三个参数就派上用场了,设置为 2,看看结果

$str	= "args=type=select&name=gravatar";
$arr	= explode("=", $str, 2);

// 结果:
// Array
// (
//     [0] => args
//     [1] => type=select&name=gravatar
// )

是不是就是我需要的结果!🥳

最后我们在看看第三个参数 $limit 的用法:

  1. 如果设置了 limit 参数并且是正数,则返回的数组包含最多 limit 个元素,而最后那个元素将包含 str 的剩余部分
  2. 如果 limit 参数是负数,则返回除了最后的 -limit 个元素外的所有元素。
  3. 如果 limit 是 0,则会被当做 1。

好了,以后不要傻傻自己使用 implode 函数拼贴回去了,记得 explode  还有第三个参数的,而且非常好用。

前端真麻烦,概念真多,今天碰到了函数节流(Throttle)和函数防抖(Debounce)

2025年3月28日 15:06

前面我在 WordPress 后台集成了标签选择器(tag-input)和多选选择器(mu-select),其中「Tag-input」有个功能是:按退格键(Backspace)不仅可以删除输入的文字,也可以删除前面的标签(为了防止误删,需要快速按两次)。

「为了防止误删,需要快速按两次」这个是怎么实现的,我搜索了一圈和问了 DeepSeek,原来有个牛逼的技术叫做 Debounce,中文翻译成函数防抖,和它一起出现的还有个概念叫做 Throttle,中文翻译是函数节流。

于是各种搜索和 DeepSeek 问,把这两个概念理解的七七七八八,简单整理一篇文章,防止以后快速能用上。先来 DeepSeek 最这个的一句话总结:

函数节流(Throttle)和函数防抖(Debounce)是控制函数执行频率的两种常用技术,它们的核心目标都是优化高频事件触发时的性能,但实现方式和适用场景有所不同。

函数节流(Throttle)​

函数节流的核心是固定时间间隔内只执行一次函数,即使事件被频繁触发,函数也会按照设定的时间间隔规律性执行。

实现原理

  • 记录上一次执行的时间戳,每次触发时判断当前时间与上次执行的时间差。
  • 如果时间差大于等于设定的间隔,则执行函数并更新上次执行时间;否则忽略本次触发。

应用场景

  • 高频事件需定期响应:如滚动(scroll)、窗口调整(resize)、鼠标移动(mousemove)。
  • 防止重复操作:如游戏中的射击按钮(限制点击频率)、表单提交按钮(避免重复提交)。

代码示例

这里仅做演示,一般 LodashUnderScore 已经带了 throttle 函数。

function throttle(func, wait){
	let lastTime	= 0;
	return function(...args){
		const now	= Date.now();
		if(now - lastTime >= wait){
			func.apply(this, args);
			lastTime	= now;
		}
	};
}

函数防抖(Debounce)​

函数防抖的核心是等待事件停止触发后,延迟执行函数。如果事件在等待期间被再次触发,则重新计时。

实现原理

  • 每次触发时清除之前的定时器,并重新设置一个定时器。
  • 只有在事件停止触发超过设定时间后,函数才会执行。

应用场景

  • 输入结束后执行:如搜索框联想建议(用户停止输入后发送请求)。
  • 避免重复触发:如窗口调整结束后的布局计算(resize结束)、表单提交按钮防抖(防止多次提交)。

代码示例

这里仅做演示,一般 LodashUnderScore 已经带了 debounce 函数。

function debounce(func, wait, immediate=false){
	let timeout;
	return function(...args){
		const context	= this;
		const later		= ()=> {
			timeout	= null;
			if(!immediate){
				func.apply(context, args);
			}
		};
		const callNow	= immediate && !timeout;
		clearTimeout(timeout);
		timeout	= setTimeout(later, wait);
		if(callNow){
			func.apply(context, args);
		}
	};
}

核心区别

特性节流(Throttle)​防抖(Debounce)​
执行时机固定间隔执行一次事件停止触发后延迟执行
适用场景需定期响应的连续事件(如滚动、拖拽)只需最终结果的场景(如输入验证、搜索)
高频触发结果规律性执行(如每 100ms 一次)仅执行最后一次触发

如何选择?

  • 需要即时反馈:用节流(如拖拽、滚动时更新元素位置),结合 leadingtrailing 选项
  • 只需最终结果:用防抖(如搜索建议、输入验证),结合 immediate 选项。

通过合理使用这两种技术,可以有效优化性能并提升用户体验。

从 Division by zero 到 set_error_handler

2025年3月20日 17:16

最近做了个简单的表格应用,其中有个功能是支持公式运算,如下图,毛利率这列是其他两列相除算出来的:

Division by zero

做完,客户还没用一天,就碰到问题了,因为有时候公式中的被除数是 0,这时候 log 中一堆 「Division by zero」的 Warning,影响查看其他的 log 的查看。

刚开始我想到的办法是在填充公式变量的时候,判断一下被除数是零就抛出「除零错误」,不去真正计算就好了,确实可以解决问题。嘻嘻,我还是挺聪明的。😎

但是很快又碰到新的问题了,因为有这样的公式:$a / ($b + $c) ,然后 ($b + $c) 的结果为 0。😂

如果继续按照原来的做法,我要根据运算的优先级,来判断被除数是 0,如果公式一复杂,那么什么时候是个头啊。🤦🤦‍♂️🤦‍♀️🤕

Warning 还是异常

先搜索了一圈 Division by zero,然后在 PHP 文档,看到 PHP 就已经有一个预定义的异常 DivisionByZeroError

但是为啥我的代码能够执行成功,只是在 log 中出现一堆 「Division by zero」的 Warning,那就继续看文档:

在 PHP 7 中使用算术运算符 / 不会抛出异常,而在 PHP 8 中会抛出异常,可惜客户的系统还在使用 PHP 7.4。

直接问 DeepSeek 我该怎么办?它给了我四个方法:

1. 手动检查并抛出异常

我这个又不是简单的被除数,真的是。🙄

2. 使用 set_error_handler 捕获警告

貌似这个可以。😘

3. 封装除法操作类

我是算术公式,这个不合适。🤪

4. 升级 PHP 8

我能升级就不会问你了。🤬

看来使用方案二了!

使用 set_error_handler 捕获警告

就是通过 set_error_handler 函数将警告转换成异常抛出。

set_error_handler(function($no, $str){
	if(str_contains($str , 'Division by zero')){
		throw new DivisionByZeroError($str); 
	}

	throw new ErrorException($str , $no);

	return true;
});

如果警告信息的字符串里面有 Division by zero,就抛出 DivisionByZeroError 异常,其他情况抛出 ErrorException

然后原来的执行表达式运算的代码改成 try - catch 结构:

try{
	// 原来执行运算表达式代码
}catch(DivisionByZeroError $e){
	return $if_error ?? '!除零错误';
}catch(throwable $e){
	return $if_error ?? '!计算错误';
}

完美解决!🍾🥂🎆🎇🎊

还没开心 5 分钟,很快就来了但是。😞😮‍💨

如果这样自定义错误处理程序,那么之后的代码的警告都被他接管了,然后直接抛出异常。这 🙄🫤

继续查 PHP 文档,问 DeepSeek,原来 set_error_handler 有返回值的,如果之前有定义的错误处理程序,则返回之前定义的错误处理程序,如果没有定义或者是内置的错误处理程序,则返回 null。

这样的话,那就好处理,给 try - catch 结构加上 finally,最终的代码如下:

$handler	= set_error_handler(function($no, $str){
	if(str_contains($str , 'Division by zero')){
		throw new DivisionByZeroError($str); 
	}

	throw new ErrorException($str , $no);

	return true;
});

try{
	// 原来执行运算表达式代码
}catch(DivisionByZeroError $e){
	return $if_error ?? '!除零错误';
}catch(throwable $e){
	return $if_error ?? '!计算错误';
}finally{
	if($handler){ // 之前有定义的错误处理程序
		set_error_handler($handler);
	}else{ // 恢复到内置的错误处理程序
		restore_error_handler();
	}
}

搞定,收工,记录一下!📝 🐂🍺

PHP 8.4 正式版发布,一文快速预览新功能

2024年11月23日 23:50

PHP 8.4 在 11 月 21 号就发布了,它包含了许多新功能,对于 WordPress 用户来说,了解 PHP 新版的功能也是一个简单必要的工作,并且  8.4 版是 PHP 在 2024 年的年度的一个主要的版本更新,所以我们就简单快速过一遍 PHP 8.4 的新功能。😄

属性挂钩

对象属性现在可以其 get 和 set 操作中关联相关的附加逻辑。根据用法,这可能会也可能不会使属性变为虚拟属性,即该属性根本没有实际的存储值。

<?php
class Person
{
    // “虚拟”属性,可能无法明确设置。
    public string $fullName {
        get => $this->firstName . ' ' . $this->lastName;
    }

    // 所有的写入操作都会经过这个挂钩,结果就是写入的内容。
    // 读取访问正常。
    public string $firstName {
        set => ucfirst(strtolower($value));
    }

    // 所有的写入操作都会经过这个挂钩,它必须写入支持值本身。
    // 读取访问正常。
    public string $lastName {
        set {
            if (strlen($value) < 2) {
                throw new \InvalidArgumentException('Too short');
            }
            $this->lastName = $value;
        }
    }
}

$p = new Person();

$p->firstName = 'peter';
print $p->firstName; // 打印“Peter”
$p->lastName = 'Peterson';
print $p->fullName; // 打印“Peter Peterson”

不对称属性可见性

现在可以将对象属性的 set 可见性和 get 可见性分开控制。

<?php
class Example
{
    // 第一个可见性修饰符控制 get 可见性,第二个修饰符控制 set 可见性。
    // The get-visibility must not be narrower than set-visibility.
    public protected(set) string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }
}

惰性对象

现在可以创建对象,将初始化延迟到访问时。库和框架可以利用这些惰性对象来延迟获取初始化所需的数据或依赖项。

<?php
class Example
{
    public function __construct(private int $data)
    {
    }

    // ...
}

$initializer = static function (Example $ghost): void {
    // 获取数据或者依赖项
    $data = ...;
    // 初始化
    $ghost->__construct($data);
};

$reflector = new ReflectionClass(Example::class);
$object = $reflector->newLazyGhost($initializer);

#[\Deprecated] 属性

新的 #[\Deprecated] 属性使 PHP 的现有弃用机制可用于用户定义的函数、方法和类常量。

class PhpVersion
{
    #[\Deprecated(
        message: "use PhpVersion::getVersion() instead",
        since: "8.4",
    )]
    public function getPhpVersion(): string
    {
        return $this->getVersion();
    }

    public function getVersion(): string
    {
        return '8.4';
    }
}

$phpVersion = new PhpVersion();
// Deprecated: Method PhpVersion::getPhpVersion() is deprecated since 8.4, use PhpVersion::getVersion() instead
echo $phpVersion->getPhpVersion();

新的 ext-dom 功能和 HTML5 支持

新的 DOM API 包括符合标准的支持,用于解析 HTML5 文档,修复了 DOM 功能行为中的几个长期存在的规范性错误,并添加了几个函数,使处理文档更加方便。

新的 DOM API 可以在 Dom 命名空间中使用。使用新的 DOM API 可以使用 Dom\HTMLDocument 和 Dom\XMLDocument 类创建文档。

$dom = Dom\HTMLDocument::createFromString(
    <<<HTML
        <main>
            <article>PHP 8.4 is a feature-rich release!</article>
            <article class="featured">PHP 8.4 adds new DOM classes that are spec-compliant, keeping the old ones for compatibility.</article>
        </main>
        HTML,
    LIBXML_NOERROR,
);

$node = $dom->querySelector('main > article:last-child');
var_dump($node->classList->contains("featured")); // bool(true)

BCMath 的对象 API 

新的 BcMath\Number 对象使在处理任意精度数字时可以使用面向对象的方式和标准的数学运算符。

这些对象是不可变的,并实现了 Stringable 接口,因此可以在字符串上下文中使用,如 echo $num

use BcMath\Number;

$num1 = new Number('0.12345');
$num2 = new Number('2');
$result = $num1 + $num2;

echo $result; // '2.12345'
var_dump($num1 > $num2); // false

新的 array_*() 函数

新增函数 array_find()array_find_key()array_any() 和 array_all()

$animal = array_find(
    ['dog', 'cat', 'cow', 'duck', 'goose'],
    static fn (string $value): bool => str_starts_with($value, 'c'),
);

var_dump($animal); // string(3) "cat"

PDO 驱动程序特定子类

新的 Pdo\DblibPdo\FirebirdPdo\MySqlPdo\OdbcPdo\Pgsql 和 Pdo\Sqlite 的子类可用。

$connection = PDO::connect(
    'sqlite:foo.db',
    $username,
    $password,
); // object(Pdo\Sqlite)

$connection->createFunction(
    'prepend_php',
    static fn ($string) => "PHP {$string}",
); // Does not exist on a mismatching driver.

$connection->query('SELECT prepend_php(version) FROM php');

new MyClass()->method() 不需要括号

现在可以在不使用括号包裹 new 表达式的情况下访问新实例化对象的属性和方法。

$connection = PDO::connect(
    'sqlite:foo.db',
    $username,
    $password,
); // object(Pdo\Sqlite)

$connection->createFunction(
    'prepend_php',
    static fn ($string) => "PHP {$string}",
); // Does not exist on a mismatching driver.

$connection->query('SELECT prepend_php(version) FROM php');

新的类、接口和函数

新的 延迟对象。
基于 IR 框架的新 JIT 实现。
新增 request_parse_body() 函数。
新增 bcceil()、bcdivmod()、bcfloor() 和 bcround() 函数。
新增 RoundingMode 枚举用于 round(),包括 4 个新的舍入模式 TowardsZero、AwayFromZero、NegativeInfinity 和 PositiveInfinity。
新增 DateTime::createFromTimestamp()、DateTime::getMicrosecond()、DateTime::setMicrosecond()、DateTimeImmutable::createFromTimestamp()、DateTimeImmutable::getMicrosecond() 和 DateTimeImmutable::setMicrosecond() 方法。
新增 mb_trim()、mb_ltrim()、mb_rtrim()、mb_ucfirst() 和 mb_lcfirst() 函数。
新增 pcntl_getcpu()、pcntl_getcpuaffinity()、pcntl_getqos_class()、pcntl_setns() 和 pcntl_waitid() 函数。
新增 ReflectionClassConstant::isDeprecated()、ReflectionGenerator::isClosed() 和 ReflectionProperty::isDynamic() 方法。
新增 http_get_last_response_headers()、http_clear_last_response_headers() 和 fpow() 函数。
新增 XMLReader::fromStream()、XMLReader::fromUri()、XMLReader::fromString()、XMLWriter::toStream()、XMLWriter::toUri() 和 XMLWriter::toMemory() 方法。
新增 grapheme_str_split() 函数。

弃用和向后不兼容

IMAP、OCI8、PDO_OCI 和 pspell 扩展已从 PHP 中分离并移至 PECL。
隐式可空参数类型现已弃用。
使用 _ 作为类名现已弃用。
将零的负数次幂现已弃用。
向 round() 传递无效模式将抛出 ValueError。
来自扩展 date、intl、pdo、reflection、spl、sqlite、xmlreader 的类常量现在是有类型的。
GMP 类现已是 final 类。
已删除 MYSQLI_SET_CHARSET_DIR、MYSQLI_STMT_ATTR_PREFETCH_ROWS、MYSQLI_CURSOR_TYPE_FOR_UPDATE、MYSQLI_CURSOR_TYPE_SCROLLABLE 和 MYSQLI_TYPE_INTERVAL 常量。
已弃用 mysqli_ping()、mysqli_kill()、mysqli_refresh() 函数,mysqli::ping()、mysqli::kill()、mysqli::refresh() 方法,以及 MYSQLI_REFRESH_* 常量。
stream_bucket_make_writeable() 和 stream_bucket_new() 现在返回 StreamBucket 实例而不是 stdClass。
exit() 行为变更。
E_STRICT 常量已弃用。

所以你打算升级吗?我打算先把 WPJAM 所有插件的最低版本的要求从 7.4 先升级到 8.0 了,不过现在我已经开始修复一些插件在 PHP 8 版本的兼容问题了。

❌
❌