批处理读取指定文件名称到
版本特性
======php.8.2=============
PHP8.2 解决了 PHP 类型系统的几个缺点和限制,允许 PHP 应用采用更好的类型安全。包括添加了 true 类型,允许 null 和 false 作为独立的类型使用,并支持 DNF 类型(泛型解析)。 PHP8.2 支持分离范式类型,现在可以进行组合联合类型和交际类型,这可以定义声明精确而富有表现力的参数、返回值和属性。 php8.2 之前 class Foo { public function bar(mixed $entity) { if ((($entity instanceof A) && ($entity instanceof B)) || ($entity === null)) { return $entity; } throw new Exception('Invalid entity'); }} 现在 class Foo { public function bar((A&B)|null $entity) { return $entity; }} 支持 true 和 false 作为独立的类型,如果 bool 始终相同的时候可以用它来声明。 function alwaysReturnsFalse(): false {}function alwaysReturnsNull(): null {}function alwaysReturnsTrue(): true {} 其中 null 的类型在之前的版本中就可以在联合类型声明中使用,现在可以独立使用了。 只读类 PHP8.1 增加了一个 readonly 的属性声明。一个 readonly 的属性只能设置一次,并且 PHP 会阻止任何作用域内的修改。 PHP8.2 对 readonly 声明进行更进一步的使用,可以将类声明为 readonly。当一个类被声明为 readonly,它的所有属性都会自动声明 readonly。此外,这个类不能使用动态属性,以确保所有的属性都是有定义的。 readonly class User { public string $username; public string $uid;} 所有的属性都会自动声明城 readonly。 新的随机数扩展 在 PHP 的历史发展中,它支持各种各样的随机数生成器,他们有不同程度的性能和不同的用例,并且适合安全应用程序。PHP8.2 更进一步,将所有与随机数相关的功能重构为一个名为 random 的扩展。新的扩展不会破坏任何现有的接口使用,因此现有的 rand,mt_rand 函数将继续工作,不需要任何更改。它还提供了一个对象接口,用可插拔的体系生成随机数,因此很容易模拟随机数生成器并提供新的随机数生成器,从而市 PHP 应用程序安全且易于测试。 trait 常量 在 PHP8.2 中,可以在 trait 中声明常量。trait 不能直接访问,但当类使用 trait 时,这些常量就变成了类的常量。 trait Foo{ public const CONSTANT = 1;}class Bar{ use Foo;}var_dump(Bar::CONSTANT); // 1var_dump(Foo::CONSTANT); // Error 敏感参数支持 PHP8.2 新增了一个内置参数属性命名:#[\SensitiveParameter]。能够使 PHP 在堆栈跟踪和错误消息中隐藏掉实际值。 我们经常会在参数或属性中定义密码、秘钥或其他敏感信息。当 PHP 发生错误时,这些值会被记录下来。显示到屏幕上或者记录到日志中。这样人们就能通过这些方式得到敏感数据。 比如下面的例子: function passwordHash(#[\SensitiveParameter] string $password) { debug_print_backtrace(); } passwordHash('hunter2'); 打印的内容如下: array(1) {[0]=> array(4) { ["file"]=> string(38) "..."["line"]=> int(9)["function"]=> string(3) "foo" ["args"]=> array(1) { // [0]=> string(38) "hunter2" 这一行不会被打印出来 [0]=> object(SensitiveParameterValue)#1 (0) {}} }} hunter2 不会被打印出来。 新的函数和类 解析 INI 数量值:ini_parse_quantity 将 PHP ini 值识别成字节。 ini_parse_quantity('256M'); // 268435456 curl 维持活动:curl_upkeep 在 PHP8.2 中,curl 扩展会触发底层 curl 库来运行必要任务,以保持 curl 连接存活。最常见的用法就是定期调用 curl_upkeep 来实现 http 持久连接(keep-alive)。 检索密码长度:openssl_cipher_key_length 在 PHP8.2 OpenSSL 中,有一个名为 openssl_cipher_key_length 的函数,能够接受任何支持的密码所需的秘钥长度,在之前需要硬编码才能实现: openssl_cipher_key_length("CHACHA20-POLY1305"); // 32openssl_cipher_key_length("AES-128-GCM"); // 16openssl_cipher_key_length("AES-256-GCM"); // 32 重置记录的峰值内存使用量:memory_reset_peak_usage 这对于多次调用或迭代调用时很有用。 PHP8.2 中的弃用 PHP8.2 也带来了相当一部分的弃用。当语法、函数和特性被弃用时,PHP 会发起一个弃用通知,该通知不应该中断 PHP 程序,但会被记录到错误日志中。 注意:PHP8.0 以后,PHP 的默认错误报告行为是 E_ALL 已弃用动态属性 PHP8.2 中最值得注意的弃用之一就是弃用动态属性。如果一个类属性没有声明就被调用或赋值,就会退出程序。 class User { public int $uid;}$user = new User();$user->name = 'Foo'; 这个可能会影响到很多的的 PHP 遗留程序,推荐的修复方法是在类型中声明属性。 对此也有例外用法,比如 stdClass 和它的子类将正常使用,__get 和__set 魔术方法将正常使用,或者声明 #AllowDynamicProperties。
===========php8.1=========
=======================Php8.1============ 让我们首先介绍PHP 8.1中的所有新特性。这是一个相当多的清单。 注意:随着PHP 8.1发布日期的临近,此列表可能会增加或缩小。我们将致力于使其保持最新状态。 纯交集类型 枚举 永不返回类型 Fibers 新的只读属性 定义最终类常量 新的fsync()和fdatasync()函数 新的array_is_list()函数 新的Sodium XChaCha20函数 新的IntlDatePatternGenerator类 支持AVIF图像格式 新的$_FILES:目录上传的full_path键 对字符串键控数组的数组解包支持 显式八进制数字表示法 MurmurHash3和xxHash哈希算法支持 DNS-over-HTTPS (DoH) 支持 使用CURLStringFile从字符串上传文件 新的MYSQLI_REFRESH_REPLICA常量 使用继承缓存提高性能 一流的可调用语法 纯交集类型 PHP 8.1添加了对交集类型的支持。它类似于PHP 8.0 中引入的联合类型,但它们的预期用途恰恰相反。 为了更好地理解它的用法,让我们回顾一下PHP中类型声明的工作原理。 本质上,您可以向函数参数、返回值和类属性添加类型声明。这种分配称为类型提示,并确保值在调用时是正确的类型。否则,它会立即抛出一个TypeError。反过来,这可以帮助您更好地调试代码。 但是,声明单一类型有其局限性。联合类型通过允许您声明具有多种类型的值来帮助您克服这个问题,并且输入必须至少满足声明的类型之一。 另一方面,RFC将交集类型描述为: “交集类型”需要一个值来满足多个类型约束而不是单个约束。 …纯交集类型使用语法 T1&T2&… 指定,可用于当前接受类型的所有位置… 请注意使用&(AND) 运算符来声明交集类型。相反,我们使用|(OR) 运算符来声明联合类型。 在交集类型中使用大多数标准类型将导致永远无法满足的类型(例如整数和字符串)。因此,交集类型只能包括类类型(即接口和类名)。 以下是如何使用交集类型的示例代码: 复制 class A { private Traversable&Countable $countableIterator; public function setIterator(Traversable&Countable $countableIterator): void { $this->countableIterator = $countableIterator; } public function getIterator(): Traversable&Countable { return $this->countableIterator; } } 在上面的代码中,我们将变量countableIterator定义为两种类型的交集:Traversable和Countable。在这种情况下,声明的两种类型是接口。 交集类型也符合已用于类型检查和继承的标准PHP变化规则。但是还有两个关于交集类型如何与子类型交互的额外规则。您可以在其 RFC 中阅读有关交集类型差异规则的更多信息。 在某些编程语言中,您可以在同一个声明中组合联合类型和交集类型。但是PHP 8.1禁止它。因此,它的实现被称为“纯”交集类型。但是,RFC 确实提到它“留作未来范围”。 枚举 PHP 8.1终于添加了对枚举(也称为枚举或枚举类型)的支持。它们是用户定义的数据类型,由一组可能的值组成。 编程语言中最常见的枚举示例是布尔类型,具有true和false两个可能的值。它是如此普遍,以至于它融入了许多现代编程语言。 根据RFC,PHP 中的枚举首先将被限制为“单元枚举”: 此RFC的范围仅限于“单元枚举”,即枚举本身就是一个值,而不是简单的原始常量的花哨语法,并且不包括附加的相关信息。此功能极大地扩展了对数据建模、自定义类型定义和 monad 样式行为的支持。枚举启用了“使无效状态不可表示”的建模技术,这会导致更健壮的代码,而无需进行详尽的测试。 为了达到这个阶段,PHP团队研究了许多已经支持枚举的语言。他们的调查发现,您可以将枚举分为三类:花式常量、花式对象和完整代数数据类型 (ADT)。这是一个有趣的阅读! PHP 实现了“Fancy Objects”枚举,并计划在未来将其扩展到完整的ADT。它在概念和语义上都模仿了Swift、Rust和Kotlin中的枚举类型,尽管它没有直接模仿它们中的任何一个。 RFC使用一副牌中著名的西装类比来解释它是如何工作的: 复制 enum Suit { case Hearts; case Diamonds; case Clubs; case Spades; } 在这里,Suit枚举定义了四个可能的值:Hearts、Diamonds、Clubs和Spades。您可以直接访问使用语法这些值:Suit::Hearts,Suit::Diamonds,Suit::Clubs,和Suit::Spades。 这种用法可能看起来很熟悉,因为枚举是建立在类和对象之上的。它们的行为相似并且具有几乎完全相同的要求。枚举与类、接口和特征共享相同的命名空间。 上面提到的枚举称为Pure Enums。 如果您想为任何情况提供标量等效值,您还可以定义支持枚举。但是,支持的枚举只能有一种类型,int或者string(永远不会)。 复制 enum Suit: string { case Hearts = 'H'; case Diamonds = 'D'; case Clubs = 'C'; case Spades = 'S'; } 此外,支持枚举的所有不同情况都必须具有唯一值。你永远不能混合纯枚举和支持枚举。 RFC进一步深入探讨了枚举方法、静态方法、常量、常量表达式等等。涵盖所有这些超出了本文的范围。您可以参考文档以熟悉它的所有优点。 never 返回类型 PHP 8.1添加了一个名为never. 在 alwaysthrow或exit. 根据它的RFC,URL重定向函数总是exit(显式或隐式)是其使用的一个很好的例子: 复制 function redirect(string $uri): never { header('Location: ' . $uri); exit(); } function redirectToLoginPage(): never { redirect('/login'); } 一个never-declared函数应满足三个条件: 它不应该有return明确定义的语句。 它不应该有return隐式定义的语句(例如if-else语句)。 它必须以exit语句(显式或隐式)结束其执行。 上面的URL重定向示例显示了never返回类型的显式和隐式用法。 在never返回类型共享与许多相似之处void返回类型。它们都确保函数或方法不返回值。但是,它的不同之处在于执行更严格的规则。例如,void-declared 函数仍然可以return没有显式值,但您不能对never-declared 函数执行相同的操作。 根据经验,void当您希望 PHP 在函数调用后继续执行时,请继续执行。never当你想要相反的时候就去吧。 此外,never定义为“bottom”类型。因此,任何声明的类方法never都“never”不能将其返回类型更改为其他类型。但是,您可以使用void-declared 方法扩展never-declared 方法。 info: 原始RFC将never返回类型列为noreturn,这是两个PHP静态分析工具(即Psalm和PHPStan)已经支持的返回类型。由于这是由Psalm和PHPStan的作者自己提出的,因此他们保留了其术语。但是,由于命名约定,PHP团队对noreturn vs never进行了民意调查,never最终成为永远的赢家。因此,对于PHP 8.1+版本,始终使用never代替noreturn. Fibers 从历史上看,PHP代码几乎一直是同步代码。代码执行暂停,直到返回结果,即使是 I/O 操作。您可以想象为什么这个过程可能会使代码执行速度变慢。 有多种第三方解决方案可以克服这一障碍,允许开发人员异步编写PHP代码,尤其是并发 I/O 操作。一些流行的示例包括amphp、ReactPHP和Guzzle。 但是,在PHP中没有处理此类实例的标准方法。此外,在同一个调用堆栈中处理同步和异步代码会导致其他问题。 Fibers是PHP通过虚拟线程(或绿色线程)处理并行性的方式。它试图通过允许 PHP 函数中断而不影响整个调用堆栈来消除同步和异步代码之间的差异。 以下是RFC的承诺: 向PHP添加对Fibers的支持。 引入一个新的Fiber类和对应的反射类ReflectionFiber。 添加异常类FiberError和FiberExit来表示错误。 Fibers允许现有接口(PSR-7、Doctrine ORM等)的透明非阻塞I/O实现。那是因为占位符(promise)对象被消除了。相反,函数可以声明I/O结果类型,而不是无法指定解析类型的占位符对象,因为PHP不支持泛型。 您可以使用Fibers开发全栈、可中断的PHP函数,然后您可以使用这些函数在PHP中实现协作多任务处理。当Fiber暂停整个执行堆栈时,您可以放心,因为它不会损害您的其余代码。 图表说明了使用Fibers的PHP代码执行流程。 为了说明Fibers的用法,它的RFC使用了这个简单的例子: 复制 $fiber = new Fiber(function (): void { $value = Fiber::suspend('fiber'); echo "Value used to resume fiber: ", $value, "\n"; }); $value = $fiber->start(); echo "Value from fiber suspending: ", $value, "\n"; $fiber->resume('test'); 你在上面的代码中创建了一个“fiber”,并立即用字符串挂起它fiber。该echo声明用作fiber恢复的视觉提示。 您可以通过调用$fiber->start()检索此字符串值。 然后,使用字符串“test”恢复fiber,该字符串是对Fiber::suspend()的调用返回的。完整代码执行会产生如下输出: 复制 Value from fiber suspending: fiber Value used to resume fiber: test 这是PHP Fibers工作的准系统教科书示例。这是执行七个异步GET请求的另一个Fibers示例。 尽管如此,大多数PHP开发人员永远不会直接处理Fibers。RFC甚至提出了同样的建议: Fibers是大多数用户不会直接使用的高级功能。此功能主要针对库和框架作者,以提供事件循环和异步编程 API。Fibers允许在任何时候将异步代码执行无缝集成到同步代码中,而无需修改应用程序调用堆栈或添加样板代码。 Fiber API不应直接在应用程序级代码中使用。Fibers提供了一个基本的、低级别的流控制API来创建更高级别的抽象,然后在应用程序代码中使用这些抽象。 考虑到它的性能优势,您可以期待PHP库和框架能够利用这一新特性。看看他们如何在他们的生态系统中实施Fibers会很有趣。 新的readonly属性 PHP 8.1添加了对readonly属性的支持。它们只能从它们被声明的作用域初始化一次。初始化后,您永远无法修改它们的值。这样做会引发错误异常。 RFC写道: 一个只读属性只能使用一次,并且只能从已申报范围的初始化。对该属性的任何其他分配或修改都将导致Error异常。 这是一个如何使用它的示例: 复制 class Test { public readonly string $kinsta; public function __construct(string $kinsta) { // Legal initialization. $this->kinsta = $kinsta; } } 一旦初始化,就再也回不去了。将此功能融入PHP会大大减少通常用于启用此函数的样板代码。 readonly属性在类内部和外部都提供了强大的不变性保证。中间运行什么代码并不重要。调用readonly属性将始终返回相同的值。 但是,readonly在特定用例中使用该属性可能并不理想。例如,您只能将它们与类型化属性一起使用,因为没有类型的声明是隐式的null,不能是readonly。 此外,设置readonly属性不会使对象不可变。该readonly属性将保存相同的对象,但该对象本身可以更改。 此属性的另一个小问题是您无法克隆它。已经有针对此特定用例的解决方法。如有必要,请查看它。 定义final类常量 从PHP 8.0开始,您可以使用其子类覆盖类常量。这是由于在PHP中实现继承的方式。 以下是如何覆盖先前声明的常量值的示例: 复制 class Moo { public const M = "moo"; } class Meow extends Moo { public const M = "meow"; } 现在,如果奶牛想要更严格地控制猫的行为(至少在常量方面),他们可以使用PHP 8.1的新final修饰符来实现。 一旦你声明了一个常量为 final,就意味着。 复制 class Moo { final public const M = "moo"; } class Meow extends Moo { public const M = "meow"; } // Fatal error: Meow::M cannot override final constant Moo::M 您可以在最后的类常量PHP RFC中阅读更多关于它的内容。 新的fsync()和fdatasync()函数 PHP 8.1添加了两个名为fsync()和fdatasync()新文件系统函数。对于那些习惯于Linux同名函数的人来说,它们似乎很熟悉。那是因为它们是相关的,只是为PHP实现的。 事实上,这一补充早该进行了。PHP是少数仍未实现fsync() 和 fdatasync() 的主要编程语言之一——也就是说,直到PHP 8.1。 fsync()函数类似于PHP现有的fflush()函数,但在一个方面有很大不同。而fflush()将应用程序的内部缓冲区刷新到操作系统,fsync()则更进一步并确保将内部缓冲区刷新到物理存储。这确保了完整且持久的写入,以便您即使在应用程序或系统崩溃后也可以检索数据。 这是一个如何使用它的示例。 复制 $doc = 'kinsta.txt'; $kin = fopen($doc, 'ki'); fwrite($kin, 'doc info'); fwrite($kin, "\r\n"); fwrite($kin, 'more info'); fsync($kin); fclose($kin); fsync()在末尾添加调用可确保保存在 PHP 或操作系统内部缓冲区中的任何数据都被写入存储。在此之前,所有其他代码执行都将被阻止。 它的相关函数是fdatasync()。使用它来同步数据,但不一定是元数据。对于元数据不是必需的数据,此函数调用会使写入过程更快一点。 但是,您应该注意PHP 8.1在Windows下尚不完全支持fdatasync()。它只是作为fsync()的别名。在POSIX上,fdatasync()已正确实施。 新的array_is_list()函数 PHP数组可以保存整数和字符串键。这意味着您可以将它们用于多种用途,包括列表、哈希表、字典、集合、堆栈、队列等等。您甚至可以在数组中包含数组,从而创建多维数组。 您可以有效地检查特定条目是否为数组,但检查它是否有任何缺失的数组偏移量、乱序键等就不是那么容易了。简而言之,您无法快速验证数组是否为列表。 所述array_is_list()函数检查是否一个数组的键是按顺序从0开始,并与没有间隙。如果满足所有条件,它将返回true。默认情况下,它还返回true空数组。 以下是在同时满足true和false条件的情况下使用它的几个示例: 复制 // true array_is_list() examples array_is_list([]); // true array_is_list([1, 2, 3]); // true array_is_list(['cats', 2, 3]); // true array_is_list(['cats', 'dogs']); // true array_is_list([0 => 'cats', 'dogs']); // true array_is_list([0 => 'cats', 1 => 'dogs']); // true // false array_is_list() examples array_is_list([1 => 'cats', 'dogs']); // as first key isn't 0 array_is_list([1 => 'cats', 0 => 'dogs']); // keys are out of order array_is_list([0 => 'cats', 'bark' => 'dogs']); // non-integer keys array_is_list([0 => 'cats', 2 => 'dogs']); // gap in between keys 带有乱序键的PHP数组列表是潜在的错误来源。在继续执行代码之前,使用此函数强制严格遵守列表要求是对PHP的一个很好的补充。 新的Sodium XChaCha20函数 Sodium是一个现代的、易于使用的加密库,用于加密、解密、密码散列、签名等。该PECL libsodium包增加了对钠的包装,使PHP开发人员可以使用它。 即使是Facebook、Discord、Malwarebytes和Valve等领先的科技公司也使用libsodium来通过快速安全的连接来保护他们的用户。 libsodium支持XChaCha20加密算法对数据进行加密和解密,特别是对流加密。同样,PECL libsodium扩展已经支持XChaCha20,但仅支持Poly1305消息验证代码。 许多PHP应用程序直接使用XChaCha20进行流加密。为了让事情变得更简单,从PHP 8.1开始,您将拥有三个新函数,无需身份验证即可使用XChaCha20加密或解密数据。这种模式称为“分离模式”。 新推出的XChaCha20函数是: sodium_crypto_stream_xchacha20_keygen: 返回一个安全的随机密钥,用于sodium_crypto_stream_xchacha20。 sodium_crypto_stream_xchacha20:将密钥和随机数扩展为伪随机字节的密钥流。 sodium_crypto_stream_xchacha20_xor:使用随机数和密钥(无身份验证)加密消息。 此外,在全局命名空间中定义了两个新的PHP常量: SODIUM_CRYPTO_STREAM_XCHACHA20_KEYBYTES(分配32) SODIUM_CRYPTO_STREAM_XCHACHA20_NONCEBYTES(分配24) 不过请谨慎使用。由于没有身份验证,解密操作容易受到常见的密文攻击。 您可以在GitHub页面上阅读有关其用法和要求的更多信息。 新的IntlDatePatternGenerator类 PHP的底层ICU库支持创建本地化的日期和时间格式,但不能完全自定义。 例如,如果您想在PHP 8.0之前创建特定于语言环境的数据和时间格式,您可以使用预定义的IntlDateFormatter常量以6种方式完成: IntlDateFormatter::LONG:更长,例如November 10, 2017 or 11:22:33pm IntlDateFormatter::MEDIUM:短一点,比如November 10, 2017 IntlDateFormatter::SHORT:只是数字,比如10/11/17 or 11:22pm 其中每一个也有自己的RELATIVE_变体,将日期格式设置在当前日期之前或之后的有限范围内。在 PHP 中,值是yesterday, today和tomorrow。 假设您想使用年份的长版本和月份的短版本,例如10/11/2017。但在PHP 8.0,你不能这样做。 在PHP 8.1+中,您可以使用新的IntlDatePatternGenerator类指定日期、月份和时间使用的格式。您可以将这些组件的确切顺序留给格式化程序。 您应该注意到,虽然这个类中只有Date一词,但它与ICU的DateTimePatternGenerator一致。这意味着您还可以使用它来创建灵活的时间格式。为了简化命名,PHP团队选择使用较短的IntlDatePatternGenerator术语。 这是直接来自其RFC的示例: 复制 $skeleton = "YYYYMMdd"; $today = \DateTimeImmutable::createFromFormat('Y-m-d', '2021-04-24'); $dtpg = new \IntlDatePatternGenerator("de_DE"); $pattern = $dtpg->getBestPattern($skeleton); echo "de: ", \IntlDateFormatter::formatObject($today, $pattern, "de_DE"), "\n"; $dtpg = new \IntlDatePatternGenerator("en_US"); $pattern = $dtpg->getBestPattern($skeleton), "\n"; echo "en: ", \IntlDateFormatter::formatObject($today, $pattern, "en_US"), "\n"; /* de: 24.04.2021 en: 04/24/2021 */ 在上面的代码中,skeleton变量定义了要使用的特定日期或时间格式。但是,格式化程序处理最终结果的顺序。 支持AVIF图像格式 AVIF或AV1图像文件格式,是一种基于AV1视频编码格式的相对较新的免版税图像格式。除了提供更高的压缩率(因此文件更小)之外,它还支持多种特性,例如透明度、HDR等。 AVIF格式最近才标准化(2021年6月8日)。这为Chrome 85+和Firefox 86+等浏览器增加了对AVIF图像的支持铺平了道路。 PHP 8.1的图像处理和GD扩展增加了对AVIF图像的支持。 但是,要包含此特性,您需要编译具有AVIF支持的GD扩展。您可以通过运行以下命令来执行此操作。 对于Debian/Ubuntu: 复制 apt install libavif-dev 对于Fedora/RHEL: 复制 dnf install libavif-devel 这将安装所有最新的依赖项。接下来,您可以通过--with-avif使用./configure脚本运行标志来编译AVIF支持。 复制 ./buildconf --force ./configure --enable-gd --with-avif 如果您要从头开始一个新环境,您还可以在此处启用其他PHP扩展。 安装后,您可以通过在PHP终端中运行以下命令来测试是否启用了AVIF支持: 复制 php -i | grep AVIF 如果您已正确安装AVIF,您将看到以下结果: 复制 AVIF Support => enabled 您还可以使用gd_info()调用来检索GD功能列表,包括是否启用了AVIF支持功能。 这个更新的PHP 8.1 GD扩展还添加了两个用于处理AVIF图像的新函数:imagecreatefromavif和imageavif. 它们的工作方式与JPEG和PNG对应物类似。 imagecreatefromavif函数从给定的AVIF图像返回一个GdImage实例。然后,您可以使用此实例来编辑或转换图像。 另一个imageavif函数输出AVIF图像文件。例如,您可以使用它将JPEG转换为AVIF: 复制 $image = imagecreatefromjpeg('image.jpeg'); imageavif($image, 'image.avif'); 您可以在其GitHub页面上阅读有关此新特征的更多信息。 新的目录上传$_FILES: full_path键 PHP维护了大量预定义变量来跟踪各种事物。其中之一是$_FILES 变量保存通过HTTP POST方法上传的项目的关联数组。 大多数现代浏览器都支持使用HTML文件上传字段上传整个目录。甚至PHP<8.1也支持此功能,但有一个很大的警告。您无法上传具有确切目录结构或相对路径的文件夹,因为PHP没有将此信息传递给$_FILES数组。 这与另外一个名为新的关键的变化在PHP 8.1full_path的$_FILES阵列。使用这些新数据,您可以在服务器上存储相对路径或复制确切的目录结构。 您可以通过$FILES使用var_dump($_FILES);命令输出数组来测试此信息。 但是,如果您正在使用此功能,请谨慎操作。确保您防范标准文件上传攻击。 对字符串键控数组的数组解包支持 PHP 7.4添加了对使用数组展开运算符 ( … )进行数组解包的支持。它可以作为使用array_merge()函数的更快替代方法。但是,此特性仅限于数字键数组,因为在合并具有重复键的数组时,解包字符串键数组会导致冲突。 但是,PHP 8添加了对命名参数的支持,消除了这个限制。因此,数组解包现在也支持使用相同语法的字符串键数组: 复制 $array = [...$array1, ...$array2]; 这个RFC示例说明了如何在PHP 8.1中处理合并具有重复字符串键的数组: 复制 $array1 = ["a" => 1]; $array2 = ["a" => 2]; $array = ["a" => 0, ...$array1, ...$array2]; var_dump($array); // ["a" => 2] 在这里,字符串键“a”在通过数组解包合并之前出现了三次。但只有它属于$array2的最后一个值获胜。 显式八进制数字表示法 PHP 支持各种数字系统,包括十进制 (base-10)、二进制 (base-2)、八进制 (base-8) 和十六进制 (base-16)。十进制数字系统是默认值。 如果你想使用任何其他数字系统,那么你必须在每个数字前加上一个标准前缀: 十六进制: 0x前缀。(例如 17 = 0x11) 二进制: 0b前缀。(例如 3 = 0b11) 八进制: 0前缀。(例如 9 = 011) 您可以看到八进制数字系统的前缀与其他系统的前缀有何不同。为了标准化这个问题,许多编程语言增加了对显式八进制数字符号的支持:0o或0O。 从PHP 8.1开始,您可以在八进制数字系统中将上述示例(即以10为基数的数字9)编写为0o11或0O11。 复制 0o16 === 14; // true 0o123 === 83; // true 0O16 === 14; // true 0O123 === 83; // true 016 === 0o16; // true 016 === 0O16; // true 此外,这个新特性也适用于PHP 7.4中引入的下划线数字文字分隔符。 在其RFC中阅读有关此新PHP 8.1特征的更多信息。 MurmurHash3和xxHash哈希算法支持 PHP 8.1添加了对MurmurHash3和xxHash散列算法的支持。它们不是为加密用途而设计的,但它们仍然提供令人印象深刻的输出随机性、分散性和唯一性。 这些新的散列算法比大多数PHP现有的散列算法都快。事实上,其中一些散列算法的变体比RAM吞吐量更快。 由于PHP 8.1还增加了对声明特定于算法的$options参数的支持,您可以对这些新算法执行相同的操作。这个新参数的默认值为[]。因此,它不会影响我们现有的任何哈希函数。 您可以在他们的GitHub页面上阅读有关这些PHP 8.1新功能的更多信息:MurmurHash3、xxHash、Algorithm-specific $options. DNS-over-HTTPS (DoH) 支持 DNS-over-HTTPS (DoH) 是一种通过HTTPS协议进行DNS解析的协议。DoH使用HTTPS加密客户端和DNS解析器之间的数据,通过防止中间人攻击来提高用户隐私和安全性。 从PHP 8.1开始,您可以使用Curl扩展来指定DoH服务器。它需要使用libcurl 7.62+版本编译PHP。对于大多数流行的操作系统(包括Linux发行版)来说,这不是问题,因为它们通常包含Curl 7.68+。 您可以通过指定CURLOPT_DOH_URL选项来配置DoH服务器URL 。 复制 $doh = curl_init('https://www.wbolt.com'); curl_setopt($doh, CURLOPT_DOH_URL, 'https://dns.google/dns-query'); curl_exec($doh); 在上面的示例中,我们使用了Google的公共DNS服务器。另外,请注意https://在所有使用的URL中的使用。确保完美地配置它,因为在Curl中没有可以回退的默认DNS服务器。 您还可以从Curl文档中包含的公共DoH服务器列表中进行选择。 此外,Curl文档的CURLOPT_DOH_URL 参考解释了如何彻底使用其各种参数。 使用CURLStringFile从字符串上载文件 PHP Curl扩展支持带有文件上传的HTTP(S)请求。它使用CURLFile类来实现这一点,该类接受URI或文件路径、mime类型和最终文件名。 但是,使用CURLFile类,您只能接受文件路径或URI,而不能接受文件本身的内容。如果您已经将文件上传到内存中(例如处理过的图像、XML文档、PDF),您必须使用data://Base64编码的URI。 但是libcurl已经支持一种更简单的方式来接受文件的内容。新的CURLStringFile类增加了对此的支持。 您可以阅读其GitHub页面以了解有关如何在PHP 8.1中实现的更多信息。 新的MYSQLI_REFRESH_REPLICA常数 PHP 8.1的mysqli扩展添加了一个名为MYSQLI_REFRESH_REPLICA. 它相当于现有的MYSQLI_REFRESH_SLAVE常数。 这个变化在MySQL 8.0.23中受到欢迎,以解决技术词汇中的种族不敏感问题(最常见的例子包括“奴隶”和“主人”)。 您应该注意到旧的常量没有被删除或弃用。开发人员和应用程序可以继续使用它。对于希望抛弃此类术语的开发人员和公司而言,此新常量只是一种选择。 使用继承缓存提高性能 继承缓存(Inheritance Cache )是opcache的新增功能,可消除PHP类继承开销。 PHP类由opcache单独编译和缓存。但是,它们已经在运行时针对每个请求进行了链接。这个过程可能涉及几个兼容性检查和从父类和特征借用方法/属性/常量。 因此,即使每个请求的结果都相同,这仍需要相当长的时间来执行。 继承缓存链接所有唯一的依赖类(父类、接口、特征、属性类型、方法)并将结果存储在opcache共享内存中。由于这种情况现在只发生一次,因此继承需要较少的指令。 此外,它消除了对不可变类的限制,例如未解析的常量、类型化属性和协变类型检查。因此,存储在opcache中的所有类都是不可变的,进一步减少了所需的指令数量。 总而言之,它有望带来显着的性能优势。该补丁的作者Dimitry Stogov发现它在基础Symfony “Hello, World!” 上有8%的改进。程序。我们迫不及待地想在我们的以下PHP基准测试中测试它。 First-Class可调用语法 PHP 8.1添加了First-Class可调用语法来取代使用字符串和数组的现有编码。除了创建更干净的Closure 之外,静态分析工具也可以访问这种新语法并尊重声明的范围。 以下是一些取自RFC的示例: 复制 $fn = Closure::fromCallable('strlen'); $fn = strlen(...); $fn = Closure::fromCallable([$this, 'method']); $fn = $this->method(...) $fn = Closure::fromCallable([Foo::class, 'method']); $fn = Foo::method(...); 这里,所有的表达式对都是等价的。三点 ( … ) 语法类似于参数解包语法 ( ...$args)。除了这里,参数尚未填写。 PHP 8.1中的变化 PHP 8.1 还包括对其现有语法和功能的更改。让我们来讨论它们: PHP Interactive Shell需要readline扩展 MySQLi默认错误模式设置为异常 CSV写入函数的可定制行尾 新version_compare运算符限制 HTML编码和解码函数现在使用ENT_QUOTES | ENT_SUBSTITUTE 关于非法紧凑函数调用的警告 新的从资源到类对象的迁移 PHP Interactive Shell需要readline扩展 PHP的readline扩展支持交互式shell功能,例如导航、自动完成、编辑等。虽然它与PHP捆绑在一起,但默认情况下并未启用。 您可以使用PHP CLI的-a命令行选项访问PHP交互式shell: 复制 php -a Interactive shell php > php > echo "Hello"; Hello php > function test() { php { echo "Hello"; php { } php > test(); Hello 在PHP 8.1之前,即使没有启用readline扩展,您也可以使用PHP CLI打开交互式shell 。正如预期的那样,shell的交互功能不起作用,使该-a选项变得毫无意义。 在PHP 8.1 CLI中,如果您没有启用readline扩展,交互式shell会退出并显示错误消息。 复制 php -a Interactive shell (-a) requires the readline extension. MySQLi默认错误模式设置为异常 在PHP 8.1之前,MySQLi默认为静默错误。这种行为通常会导致代码不遵循严格的错误/异常处理。开发人员必须实现自己的显式错误处理功能。 PHP 8.1通过将MySQLi的默认错误报告模式设置为抛出异常来更改此行为。 复制 Fatal error: Uncaught mysqli_sql_exception: Connection refused in ...:... 由于这是一个重大更改,对于PHP<8.1版本,您应该mysqli_report在建立第一个MySQLi连接之前使用该函数显式设置错误处理模式。或者,您可以通过实例化一个mysqli_driver实例来选择错误报告值来执行相同的操作。 RFC遵循PHP 8.0中引入的类似更改。 CSV写入函数的可定制行尾 在PHP 8.1之前,PHP的内置CSV写入函数fputcsv和SplFileObject::fputcsv被硬编码为\n 在每行末尾添加(或换行符)。 PHP8.1为这些函数添加了对名为eol的新参数的支持。您可以使用它来传递可配置的行尾字符。默认情况下,它仍然使用\n 字符。因此,您可以继续在现有代码中使用它。 标准字符转义规则适用于使用行尾字符。如果您想使用\r、\n或\r\n作为EOL字符,您必须将它们括在双引号中。 这是跟踪此新更改的GitHub页面。 新的 version_compare 运算符限制 PHP的version_compare()函数比较两个版本号字符串。此函数接受一个可选的第三个参数,operator用于测试特定关系。 尽管文档中没有明确说明,但在PHP 8.1之前,您可以将此参数设置为部分值(例如g, l, n)而不会出现错误。 PHP 8.1对version_compare()函数的operator参数添加了更严格的限制来克服这种情况。您现在可以使用的唯一运算符是: ==、=和eq !=、<>和ne >和gt >=和ge <和lt <=和le 没有更多的部分运算符值。 HTML编码和解码函数改用 ENT_QUOTES | ENT_SUBSTITUTE HTML实体是字符的文本表示,否则会被解释为HTML。想想诸如<和>用于定义HTML 标签的字符(例如<a>、<h3>、<script>)。 HTML实体<是& lt;(小于符号)和>是& gt;(大于符号)。 注意:删除“&”和“amp”之间的空格。 您可以在HTML文档中安全地使用这些HTML实体,而无需触发浏览器的渲染引擎。 例如,& lt;script& gt;将在浏览器中显示为<script>,而不是被解释为HTML标记。 之前PHP 8.1,则用htmlspecialchars()和 htmlentities() 函数转换如 “, <, >和& 符号为各自的HTML实体。但默认情况下,他们没有将单引号字符 (') 转换为其HTML实体。此外,如果文本中存在格式错误的 UTF-8,它们将返回一个空字符串。 在PHP 8.1中,这些HTML编码和解码函数(及其相关函数)也会默认将单引号字符转换为它们的HTML实体。 如果给定的文本包含无效字符,函数将用Unicode替换字符 (�) 替换它们,而不是返回空字符串。PHP 8.1通过默认将这些函数的签名更改为ENT_QUOTES | ENT_SUBSTITUTE而不是ENT_COMPAT来实现这一点。 大多数框架已经ENT_QUOTES用作默认标志值。因此,由于此更改,您不会看到太大差异。然而,新ENT_SUBSTITUTE标志并没有被广泛使用。PHP 8.1将导致无效的UTF-8字符被替换为 � 字符而不是返回空字符串。 关于非法紧凑函数调用的警告 PHP 的compact()函数超级好用。您可以使用它来创建一个数组,其中包含使用名称和值的变量。 例如,考虑以下代码: 复制 $animal = 'Cat'; $sound = 'Meow'; $region = 'Istanbul'; compact('animal', 'sound', 'region'); // ['animal' => "Cat", 'sound' => "Meow", 'region' => "Istanbul"] 紧凑型函数的文档指出,这将只接受字符串参数或字符串值数组值。但是,在PHP 7.3之前,任何未设置的字符串都将被悄悄跳过。 PHP7.3修改了compact()函数,以便在使用未定义变量时发出通知。PHP 8.1更进一步,并发出警告。 您可以阅读其GitHub 页面以了解此更改是如何发生的。 新的从资源到类对象迁移 PHP的长期目标之一是从资源转向标准类对象。 由于历史原因,资源对象在PHP应用程序中被广泛使用。因此,资源向类对象的迁移需要尽可能减少破坏性。PHP 8.1迁移了五个这样的资源: 将file_info资源迁移到finfo对象 PHP的finfo 类为函数提供了一个面向对象的接口fileinfo。但是,使用finfo函数返回resource具有file_info类型的对象而不是finfo类本身的实例。 PHP 8.1修复了这个异常。 迁移到IMAP\Connection类对象的IMAP资源 根据资源到对象的迁移目标,当PHP最终修改类的实现细节时,新的IMAP\Connection类将潜在的破坏性更改降至最低。 这个新类也被声明final,所以你不被允许extend。 在其GitHub页面上阅读有关其实现的更多信息。 FTP连接资源现在为FTP\Connection类对象 在PHP<8.1中,如果您使用或函数ftp_connect()或者ftp_ssl_connect()创建FTP连接,您将返回一个ftp类型的资源对象。 PHP 8.1添加了新的FTP\Connection类来纠正这个问题。和IMAP\Connection类一样,它也被声明final为防止它被扩展。 在其GitHub页面上阅读有关其实现的更多信息。 字体标识符迁移到GdFont类对象 PHP的GD扩展提供了imageloadfont() 函数来加载用户定义的位图并返回其字体标识符资源 ID(一个整数)。 在PHP 8.1中,此函数将改为返回GdFont类实例。此外,为了使迁移轻松自如,以前从imageloadfont()接受资源ID的所有函数现在都将采用新的GdFont类对象。 在其GitHub页面上阅读有关此迁移的更多信息。 LDAP资源迁移到对象 LDAP或轻量级目录访问协议,用于访问“目录服务器”。就像硬盘目录结构一样,它是一个独特的数据库,以树状结构保存数据。 PHP包含一个LDAP扩展,它接受或返回PHP 8.1之前的资源对象。但是,它们现在都已无缝迁移到新的类实例。已经过渡的资源类型有: ldap link资源到\LDAP\Connection类对象 ldap result资源到\LDAP\Result类对象 ldap result entry资源到\LDAP\ResultEntry类对象 浏览其GitHub页面以更好地了解此迁移。 Pspell资源现在为类对象 PHP的Pspell扩展允许您检查拼写和单词建议。 PHP<8.1使用pspell和pspell config带有整数标识符的资源对象类型。这两个资源对象现在替换为PSpell\Dictionary和PSpell\Config类对象。 与之前的迁移一样,之前接受或返回资源对象标识符的所有Pspell函数都将采用新的类对象实例。 有关更多信息,请参阅其GitHub页面。 PHP 8.1中的弃用 PHP 8.1弃用了许多以前的功能。以下列表简要概述了PHP 8.1弃用的功能: 不能将null传递给不可为Null的内部函数参数 受限制的$GLOBALS使用 内部函数的返回类型声明 不推荐使用可序列化接口 不兼容的float到int转换已弃用 mysqli::get_client_info方法和mysqli_get_client_info($param) 已弃用 不推荐使用所有mhash*() 函数(散列扩展) filter.default和filter.default_options INI设置已弃用 在false上弃用autovivification 不推荐使用mysqli_driver->driver_version属性 不能将null 传递给不可为Null的内部函数参数 从PHP 8.0开始,它的内部函数null即使对于不可为null的参数也默默地接受值。这不适用于用户定义的函数——它们只接受null可为空的参数。 例如,考虑这种用法: 复制 var_dump(str_contains("foobar", null)); // bool(true) 在这里,null值被静默转换为空字符串。因此,结果返回true。 该RFC旨在通过在PHP 8.1中抛出弃用警告来同步内部函数的行为。 复制 var_dump(str_contains("foobar", null)); // Deprecated: Passing null to argument of type string is deprecated 在下一个主要的 PHP 版本(即 PHP >=9.0)中,弃用将成为 TypeError,使内部函数的行为与用户定义的函数保持一致。 限制$GLOBALS使用 PHP的$GLOBALS变量提供对其内部符号表的直接引用。支持此功能很复杂,并且会影响阵列操作性能。另外,它很少被使用。 根据RFC,$GLOBALS不再允许间接修改。此更改向后不兼容。 这种变化的影响相对较低: 在前2k个composer包中,我发现了23个使用 $GLOBALS而不直接取消引用它的案例。根据粗略的检查,只有两种情况没有以只读方式使用$GLOBALS。 但是,只读用法$GLOBALS继续照常工作。不再支持的是$GLOBALS整体写入。因此,您可能会遇到轻微的性能提升,尤其是在使用普通PHP数组时。 内部函数的返回类型声明 PHP 8.0允许开发人员为大多数内部函数和方法声明参数和返回类型。这要归功于各种RFC,例如内部函数的一致类型错误、联合类型2.0和混合类型v2。 但是,在很多情况下可能会丢失类型信息。其中一些包括具有资源型,out pass-by-ref 参数,非final方法返回类型,函数或不按一般规则解析参数的方法。您可以在其RFC中阅读确切的详细信息。 此RFC仅解决非最终方法的返回类型的问题。然而,PHP团队并没有立即完全淘汰它,而是提供了一个渐进的迁移路径,以使用相关的方法返回类型更新您的代码库。 非最终的内部方法返回类型(如果可能)在PHP 8.1中暂时声明,它们将在PHP 9.0中强制执行。这意味着在PHP 8.x版本中,当内部方法以返回类型不兼容的方式被覆盖时,在继承检查期间会引发“弃用”通知,而PHP 9.0会使这些成为致命错误。 如果您在更新到PHP 8.1后看到此弃用通知,请确保更新您的方法的返回类型。 不推荐使用可序列化接口 PHP 7.4通过两个新的魔术方法引入了自定义对象序列化机制:__serialize()和__unserialize(). 这些新方法旨在最终替换损坏的Serializable接口。 该RFC提议通过制定最终删除Serializable的计划来最终确定该决定。 在PHP 8.1中,如果你在没有实现和方法的情况下实现了Serializable接口,PHP将抛出“Deprecated”警告。 在PHP8.1中,如果在没有实现__serialize()和__unserialize()方法的情况下实现Serializable接口,PHP将抛出“Deprecated”警告。 复制 Deprecated: The Serializable interface is deprecated. Implement __serialize() and __unserialize() instead (or in addition, if support for old PHP versions is necessary) in ... on line ... 如果你支持PHP <7.4和PHP >=7.4,你应该实现Serializable接口和新的魔法方法。在PHP >=7.4版本上,魔术方法将优先。 不推荐使用不兼容的float 到int 转换 PHP是一种动态类型语言。因此,在很多情况下自然会发生类型强制。大多数这些强制是无害的,而且超级方便。 但是,当浮点数转换为整数时,通常会涉及数据丢失。例如,当浮点数3.14转换为整数3时,它会丢失其小数值。 当浮点数超出平台整数范围或浮点数字符串转换为整数时,也会发生同样的情况。 PHP 8.1纠正了这种行为,并使其动态类型强制更符合大多数现代编程语言。目标是使这种强制可预测和直观。 在PHP 8.1中,当不兼容的float被隐式强制转换为int时,您将看到弃用通知。但是什么构成了整数兼容的浮点数?RFC对此做出了回答: 如果具有以下特征,则称浮点数与整数兼容: 是一个数字(即不是 NaN 或无穷大) 在PHP整数范围内(取决于平台) 没有小数部分 此弃用通知将在下一个主要PHP版本(即PHP 9.0)中升级为TypeError。 mysqli::get_client_info和mysqli_get_client_info($param)方法已过时 MySQL客户端API定义了两个常量:client_info(字符串)和client_version(整数)。MySQL Native Driver (MySQLnd) 是官方PHP源代码的一部分,并将这些常量与PHP版本挂钩。在libmysql中,它们代表编译时的客户端库版本。 在PHP8.1之前,mysqli以4种方式公开这些常量:mysqli_driver属性、mysqli 属性、mysqli_get_client_info()函数和mysqli::get_client_info方法。但是,对于client_version没有方法。 MySQLnd 以两种方式向PHP公开这些常量:常量和函数调用。为了统一使用这两个选项的mysqli访问方法,PHP 8.1弃用了其他两个选项: get_client_infomysqli类中的方法。相反,您可以只使用该mysqli_get_client_info()函数。 mysqli_get_client_info()带参数的函数。不带任何参数调用函数以避免弃用通知。 在其GitHub 页面上阅读有关此弃用的更多信息。 不推荐使用所有mhash*()函数(散列扩展) PHP5.3将mhash*()函数集成到ext/hash中,作为ext/mhash的兼容层。后来,PHP7.0删除了ext/mhash。 与hash_*()函数不同,mhash*()函数并不总是可用。您必须在配置PHP时单独启用它们。 在PHP 7.4中,散列扩展与PHP捆绑在一起,使其成为PHP的默认扩展。但是,--enable-mhash出于兼容性原因,它仍然支持启用该选项。 PHP团队决定在PHP 8.1中弃用mhash*()函数,并在PHP 9.0中完全删除它们。不推荐使用的函数是mhash(),mhash_keygen_s2k(),mhash_count(),mhash_get_block_size()和mhash_get_hash_name()。您可以使用标准ext/hash功能代替它们。 filter.default和filter.default_optionsINI设置已过时 PHP的filter.defaultINI设置允许您将过滤器适用于所有的PHP超全局-即GPCR的数据($_GET,$_POST,$_COOKIE,$_REQUEST,和$_SERVER)。 例如,您可以设置filter.default=magic_quotes或filter.default=add_slashes(基于PHP版本)来复活PHP有争议且不安全的魔术引号功能(在PHP 5.4中删除)。 filter.default的INI设置提供了额外的功能,允许使用更多的过滤器,使情况变得更糟。例如,它的另一个选项-filter.default=special_chars -只为HTML启用魔法引号。人们对这些设置的了解要少得多。 如果filter.default 设置为除unsafe_raw(默认值)以外的任何值,PHP8.1将抛出一个弃用警告。您不会看到ilter.default_options的单独弃用通知,但PHP9.0将删除这两个INI设置。 作为替代方案,您可以开始使用filter_var()函数。它使用指定的过滤器过滤变量。 弃用autovivification的false PHP允许自动激活(从假值自动创建数组)。如果变量未定义,此功能非常有用。 尽管如此,当值为false或null时自动创建数组并不理想。 此RFC不允许从错误值自动激活。但是,请注意,仍然允许来自未定义变量和null的自动激活。 在PHP 8.1中,附加到false类型的变量将发出弃用通知: 复制 Deprecated: Automatic conversion of false to array is deprecated in PHP 9.0同样会抛出致命错误,这与其他标量类型相同。 mysqli_driver->driver_version属性已弃用 MySQLi 扩展的mysqli_driver->driver_version属性已经13年没有更新了。尽管此后对驱动程序进行了许多更改,但它仍然返回旧的驱动程序版本值,使该属性变得毫无意义。 在PHP 8.1中,mysqli_driver->driver_version属性已弃用。 其他小改动 PHP 8.1中有更多的弃用。将它们全部列出在这里将是一项令人筋疲力尽的工作。我们建议您直接查看RFC以了解这些较小的弃用情况。 PHP的GitHub页面还包括一个PHP 8.1升级说明指南。它列出了在升级到PHP 8.1之前您应该考虑的所有重大更改。 小结 PHP 8.1离我们不远了。并且它已经承诺将其前身提升一倍,这是不小的壮举。 我们认为最令人兴奋的PHP 8.1特性是枚举、纤维、纯交集类型及其许多性能改进。此外,我们迫不及待地要让PHP 8.1步入正轨,并对各种PHP框架和CMS进行基准测试。
===========php8===========
===================================Php8================================================ 1.联合类型 PHP7 class Number { /** @var int|float */ private $number; /** * @param float|int $number */ public function __construct($number) { $this->number = $number; } } new Number('NaN'); // Ok PHP8 class Number { public function __construct( private int|float $number ) {} } new Number('NaN'); // TypeError 2.mixed类型 mixed本身是以下类型之一: array bool callable int float null object resource string 注意,mixed也可以用作参数或属性类型,而不仅仅是返回类型。 另外由于mixed已经包含null,因此不允许将其设置为nullable。以下内容将触发错误 3.JIT作为PHP底层编译引擎,对于PHP8的性能贡献是非常之大,不过对于常规WEB应用来说,优势不明显,但仍然是非常的高大上特性,是PHP8的扛鼎之作 4.最实用的特性:构造器属性提升、Nullsafe运算符、str_contains()、 str_starts_with()、 str_ends_with() 5.构造器属性提升 代替如下代码: class Money { public Currency $currency; public int $amount; public function __construct( Currency $currency, int $amount, ) { $this->currency = $currency; $this->amount = $amount; } } 你可以这样做: class Money { public function __construct( public Currency $currency, public int $amount, ) {} } 6.nullsafe运算符 现在可以用新的 nullsafe 运算符链式调用,而不需要条件检查 null。 如果链条中的一个元素失败了,整个链条会中止并认定为 Null。 $country =null; if ($session !== null) { $user = $session->user; if ($user !== null) { $address = $user->getAddress(); if ($address !== null) { $country = $address->country; } } } 简化为一行代码 $country = $session?->user?->getAddress()?->country; 7.确实是有点酷 str_contains()、str_starts_with()和str_ends_with()函数 有些人可能会说它早就该有了,但我们终于不必再依赖strpos() 来知道字符串是否包含另一个字符串了。 代替如下: if (strpos('string with lots of words', 'words') !== false) { /* … */ } 你可以这样做 if (str_contains('string with lots of words', 'words')) { /* … */ } 感觉大多数场景应该是不需要使用strpos了吧,外两个早就应该有了,str_starts_with()和str_ends_with()这两个函数现在能省事不少。 str_starts_with('haystack', 'hay'); // true str_ends_with('haystack', 'stack'); // true 8.最具潜力的特性:注解、Match表达式、WeakMap 现在可以用原生的PHP语法来使用结构化的元数据,而不需要再依赖PHPDoc解析,性能也随之提升。之前定义注解路由可能需要使用: class PostsController { /** * @Route("/api/posts/{id}", methods={"GET"}) */ public function get($id) { /* ... */ } } 现在你可以直接用PHP的注解语法来定义,并通过反射直接获取 class PostsController { #[Route("/api/posts/{id}", methods: ["GET"])] public function get($id) { /* ... */ } } 9.Match表达式 你可以称它为switch表达式的大哥:match可以返回值,不需要break语句,可以组合条件,使用严格的类型比较,并且不执行任何类型的强制。 如下所示: $result = match($input) { 0 => "hello", '1', '2', '3' => "world", }; 10.WeakMap WeakMap保留对对象的引用,这些引用不会阻止这些对象被垃圾回收。 特别是在 ORM 的情况下,它可以管理请求中的数百个,如果不是数千个实体;weak maps可以提供更好、更资源友好的处理这些对象的方法。 下面是weak maps的示例: class Foo { private WeakMap $cache; public function getSomethingWithCaching(object $obj): object { return $this->cache[$obj] ??= $this->computeSomethingExpensive($obj); } } 11其它特性 php7 0 == 'foobar' // 返回true php8 0 == 'foobar' // 返回false 可以在对象上使用::class $foo = new Foo(); var_dump($foo::class); 现在可以对对象使用::class,它的工作方式与 get_class() 相同。 12.traits 中的抽象方法改进 Traits 可以指定抽象方法,这些方法必须由使用它们的类实现。在PHP8,必须保持一致的方法定义,包括参数类型和返回类型。
==========php7.4==========
====================================7.4========================================= 1、属性值类型声明 <?php class User { public int $id; public string $name; } 2、箭头函数 <?php $factor = 10; $nums = array_map(fn($n) => $n * $factor, [1, 2, 3, 4]); // $nums = array(10, 20, 30, 40); 3、空值连写赋值运算符 <?php $array['key'] ??= computeDefault(); // is roughly equivalent to if (!isset($array['key'])) { $array['key'] = computeDefault(); } 4、数组内展开 <?php $parts = ['apple', 'pear']; $fruits = ['banana', 'orange', ...$parts, 'watermelon']; // ['banana', 'orange', 'apple', 'pear', 'watermelon']; 5、数值型字面量分隔符 <?php 6.674_083e-11; // float 299_792_458; // decimal 0xCAFE_F00D; // hexadecimal 0b0101_1111; // binary 6、弱引用 引用对象使其不被销毁 7、__toString()方法允许跑出异常
=======php7.2==========
====================================php7.2====================================== 1、Nullable类型 参数以及返回值的类型通过在类型前加上一个问号使之允许为空 <?php function testReturn(): ?string { return 'elePHPant'; } var_dump(testReturn()); function testReturn(): ?string { return null; } var_dump(testReturn()); function test(?string $name) { var_dump($name); } test('elePHPant'); test(null); test(); 2、Void 函数 方法要么干脆省去 return 语句,要么使用一个空的 return 语句。 对于 void 函数来说,NULL 不是一个合法的返回值。 <?php function swap(&$left, &$right) : void { if ($left === $right) { return; } $tmp = $left; $left = $right; $right = $tmp; } $a = 1; $b = 2; var_dump(swap($a, $b), $a, $b); 3、对称性数组解构 短数组语法([])现在作为list()语法的一个备选项,可以用于将数组的值赋给一些变量(包括在foreach中) <?php $data = [ [1, 'Tom'], [2, 'Fred'], ]; // list() style list($id1, $name1) = $data[0]; // [] style [$id1, $name1] = $data[0]; // list() style foreach ($data as list($id, $name)) { // logic here with $id and $name } // [] style foreach ($data as [$id, $name]) { // logic here with $id and $name } 4、类常量可见性 <?php class ConstDemo { const PUBLIC_CONST_A = 1; public const PUBLIC_CONST_B = 2; protected const PROTECTED_CONST = 3; private const PRIVATE_CONST = 4; } 5、iterable 伪类 <?php function iterator(iterable $iter) { foreach ($iter as $val) { // } } 6、多异常捕获处理 <?php try { // some code } catch (FirstException | SecondException $e) { // handle first and second exceptions } 7、list()现在支持键名 这意味着它可以将任意类型的数组 都赋值给一些变量 <?php $data = [ ["id" => 1, "name" => 'Tom'], ["id" => 2, "name" => 'Fred'], ]; // list() style list("id" => $id1, "name" => $name1) = $data[0]; // [] style ["id" => $id1, "name" => $name1] = $data[0]; // list() style foreach ($data as list("id" => $id, "name" => $name)) { // logic here with $id and $name } // [] style foreach ($data as ["id" => $id, "name" => $name]) { // logic here with $id and $name } 8、支持为负的字符串偏移量 一个负数的偏移量会被理解为一个从字符串结尾开始的偏移量。 <?php var_dump("abcdef"[-2]); var_dump(strpos("aabbcc", "b", -3)); 7.2 1、新的object类型 这种新的类型 object, 引进了可用于逆变(contravariant)参数输入和协变(covariant)返回任何对象类型。 <?php function test(object $obj) : object { return new SplQueue(); } test(new StdClass()); 2、通过名称加载扩展 使用 dl() 函数进行启用 3、允许重写抽象方法(Abstract method) <?php abstract class A { abstract function test(string $s); } abstract class B extends A { // overridden - still maintaining contravariance for parameters and covariance for return abstract function test($s) : int; } 4、扩展了参数类型 重写方法和接口实现的参数类型现在可以省略了。不过这仍然是符合LSP,因为现在这种参数类型是逆变的。 <?php interface A { public function Test(array $input); } class B implements A { public function Test($input){} // type omitted for $input } 5、允许分组命名空间的尾部逗号 <?php use Foo\Bar\{ Foo, Bar, Baz, }; 6、更灵活的Heredoc 与 Nowdoc 语法 7、数组的解构支持引用赋值 [&$a, [$b, &$c]] = $d 8、Instanceof 运算符支持字面量 判断结果总是 FALSE 9、允许函数和方法被调用时参数最后的空逗号收尾 10、多字节字符串的相关函数 mb_convert_case(): mb_strtolower()) mb_strtoupper()) <?php mb_ereg('(?<word>\w+)', '国', $matches); // => [0 => "国", 1 => "国", "word" => "国"]; <?php mb_ereg_replace('\s*(?<word>\w+)\s*', "_\k<word>_\k'word'_", ' foo '); // => "_foo_foo_"
========php7.0==========
=========================================Php7============================================== 参数类型声明 标量类型声明 有两种模式: 强制 (默认) 和 严格模式。 现在可以使用下列类型参数(无论用强制模式还是严格模式): 字符串(string), 整数 (int), 浮点数 (float), 以及布尔值 (bool)。 它们扩充了PHP5中引入的其他类型:类名,接口,数组和 回调类型。 <?php // Coercive mod function sumOfInts(int ...$ints) { return array_sum($ints); } var_dump(sumOfInts(2, '3', 4.1)); 2、返回值类型声明 返回类型声明指明了函数返回值的类型。可用的类型与参数声明中可用的类型相同。 <?php function arraysSum(array ...$arrays): array { return array_map(function(array $array): int { return array_sum($array); }, $arrays); } print_r(arraysSum([1,2,3], [4,5,6], [7,8,9])); 3、null合并运算符 如果变量存在且值不为NULL, 它就会返回自身的值,否则返回它的第二个操作数。 <?php // Fetches the value of $_GET['user'] and returns 'nobody' // if it does not exist. $username = $_GET['user'] ?? 'nobody'; // This is equivalent to: $username = isset($_GET['user']) ? $_GET['user'] : 'nobody'; // Coalesces can be chained: this will return the first // defined value out of $_GET['user'], $_POST['user'], and // 'nobody'. $username = $_GET['user'] ?? $_POST['user'] ?? 'nobody'; ?> 4、太空船操作符(组合比较符) 用于比较两个表达式。当$a小于、等于或大于$b时它分别返回-1、0或1 <?php // 整数 echo 1 <=> 1; // 0 echo 1 <=> 2; // -1 echo 2 <=> 1; // 1 // 浮点数 echo 1.5 <=> 1.5; // 0 echo 1.5 <=> 2.5; // -1 echo 2.5 <=> 1.5; // 1 // 字符串 echo "a" <=> "a"; // 0 echo "a" <=> "b"; // -1 echo "b" <=> "a"; // 1 5、通过 define() 定义常量数组 在 PHP5.6 中仅能通过 const 定义。 <?php define('ANIMALS', [ 'dog' 'cat', 'bird' ]); echo ANIMALS[1]; // 输出 "cat" 6、匿名类 <?php interface Logger { public function log(string $msg); } class Application { private $logger; public function getLogger(): Logger { return $this->logger; } public function setLogger(Logger $logger) { $this->logger = $logger } } $app = new Application; $app->setLogger(new class implements Logger { public function log(string $msg) { echo $msg; } }); var_dump($app->getLogger()); 7、Unicode codepoint 转译语法 接受一个以16进制形式的 Unicode codepoint,并打印出一个双引号或heredoc包围的 UTF-8 编码格式的字符串 echo "\u{aa}"; echo "\u{0000aa}"; echo "\u{9999}"; 8、Closure::call() 简短干练的暂时绑定一个方法到对象上闭包并调用它。 <?php class A {private $x = 1;} // PHP 7 之前版本的代码 $getXCB = function() {return $this->x;}; $getX = $getXCB->bindTo(new A, 'A'); // 中间层闭包 echo $getX(); // PHP 7+ 及更高版本的代码 $getX = function() {return $this->x;}; echo $getX->call(new A); 9、为unserialize()提供过滤 通过白名单的方式来防止潜在的代码注入。 <?php // 将所有的对象都转换为 __PHP_Incomplete_Class 对象 $data = unserialize($foo, ["allowed_classes" => false]); // 将除 MyClass 和 MyClass2 之外的所有对象都转换为 __PHP_Incomplete_Class 对象 $data = unserialize($foo, ["allowed_classes" => ["MyClass", "MyClass2"]); // 默认情况下所有的类都是可接受的,等同于省略第二个参数 $data = unserialize($foo, ["allowed_classes" => true]); 10、断言 assert()现在是一个语言结构,它允许第一个参数是一个表达式,而不仅仅是一个待计算的 string或一个待测试的boolean。 <?php ini_set('assert.exception', 1); class CustomError extends AssertionError {} assert(false, new CustomError('Some error message')); 11、Group use declarations 从同一 namespace 导入的类、函数和常量现在可以通过单个 use 语句 一次性导入了。 // PHP 7+ 及更高版本的代码 use some\namespace\{ClassA, ClassB, ClassC as C}; use function some\namespace\{fn_a, fn_b, fn_c}; use const some\namespace\{ConstA, ConstB, ConstC}; 12、生成器可以返回表达式 允许在生成器函数中通过使用 return 语法来返回一个表达式 (但是不允许返回引用值), 可以通过调用 Generator::getReturn() 方法来获取生成器的返回值, 但是这个方法只能在生成器完成产生工作以后调用一次。 <?php $gen = (function() { yield 1; yield 2; return 3; })(); foreach ($gen as $val) { echo $val, PHP_EOL; } echo $gen->getReturn(), PHP_EOL; 13、生成器委托 只需在最外层生成器中使用 yield from, 就可以把一个生成器自动委派给其他的生成器, Traversable 对象或者 array。 <?php function gen() { yield 1; yield 2; yield from gen2(); } function gen2() { yield 3; yield 4; } foreach (gen() as $val) { echo $val, PHP_EOL; } 14、整数除法函数 intdiv() 进行 整数的除法运算。 <?php var_dump(intdiv(10, 3)); 15、可以使用 list() 函数来展开实现了 ArrayAccess 接口的对象
==============PHP5.3==
1.支持命名空间(namespace) 2.通过static关键字,实现方法的延迟静态绑定 3.支持goto语句 4. 支持闭包匿名函数或Lambda函数array_map()或array_walk()等函数的回调函数 5. 两个魔术方法__callStatic()和__invoke() 6. 新增Nowdoc语法结构 7. const 关键字 8.三元省 expr1 ?: expr3 9. 静态成员或静态方法支持 10.支持try…catch
=========PHP5.4========
1新增traits Traits提供了一种灵活的代码重用机制 2. 数组【】支持写法 3.支持函数方法数组化访问 5.加入webserver 6上传进度支持 $_SESSION[“upload_progress_name”] 7json_encode 可中文输出 8二进制直接量 9函数的参数类型 10. 废除了register_globals、 magic_quotes、 allow_call_time_pass_reference以及安全模式等等。 11. 早期 在早期版本中,你可以在函数调用时,在参数前添加&修饰符来指明参数变量按引用传递,你只需要在函数声明时指定按引用传递即可
==========Php5.5=======
新增Generator生成器yield 2. try…catch 新增了finally 3. foreach中支持嵌套list()结构 4..empty() 支持传入表达式,而不仅是一个变量 5. 非变量array或string也能支持下标访问 <?php echo array(1, 2, 3)[0]; echo [1, 2, 3][0]; echo "foobar"[0]; // 输出:11f 6. 新增密码哈希API password_hash password_verify 7. 新增 boolval() 函数 8. 新增 array_column() 函数二维数组中指定的列
======Php5.6====
1增强了const 常量 以前只能使用固定的值,现在允许常量计算,允许使用包含数字、字符串字面值和常量的表达式结果定义const常量,常量的值也可以为一个数组 2支持使用 … 运算符定义变长参数函数 实现变长 3 ** 进行幂运算 printf(2 ** 3); // 8 $a **= 3; 4命名空间 use 操作符支持函数和常量的导入 对应的结构为 use function 和 use const。 5. 解包功能 6. 上传超过2G的大文件 7. php://input 可以被复用 在 PHP 5.6 之前 php://input 打开的数据流只能读取一次; 数据流不支持 seek 操作 现在依赖于 SAPI 的实现,请求体数据被保存的时候, 它可以打开另一个 php://input 数据流并重新读取。 通常情况下,这种情况只是针对 POST 请求,而不是其他请求方式
分类: PHP
7种 实现web实时消息推送的方案(转)
什么是消息推送(push)
推送的场景比较多,比如有人关注我的公众号,这时我就会收到一条推送消息,以此来吸引我点击打开应用。
消息推送(push
)通常是指网站的运营工作等人员,通过某种工具对用户当前网页或移动设备APP进行的主动消息推送。
消息推送一般又分为web端消息推送
和移动端消息推送
。
上边的这种属于移动端消息推送,web端消息推送常见的诸如站内信、未读邮件数量、监控报警数量等,应用的也非常广泛。
在具体实现之前,咱们再来分析一下前边的需求,其实功能很简单,只要触发某个事件(主动分享了资源或者后台主动推送消息),web页面的通知小红点就会实时的+1
就可以了。
通常在服务端会有若干张消息推送表,用来记录用户触发不同事件所推送不同类型的消息,前端主动查询(拉)或者被动接收(推)用户所有未读的消息数。
消息推送无非是推(push
)和拉(pull
)两种形式,下边我们逐个了解下。
短轮询
轮询(polling
)应该是实现消息推送方案中最简单的一种,这里我们暂且将轮询分为短轮询
和长轮询
。
短轮询很好理解,指定的时间间隔,由浏览器向服务器发出HTTP
请求,服务器实时返回未读消息数据给客户端,浏览器再做渲染显示。
一个简单的JS定时器就可以搞定,每秒钟请求一次未读消息数接口,返回的数据展示即可。
setInterval(() => {
// 方法请求
messageCount().then((res) => {
if (res.code === 200) {
this.messageCount = res.data
}
})
}, 1000);
效果还是可以的,短轮询实现固然简单,缺点也是显而易见,由于推送数据并不会频繁变更,无论后端此时是否有新的消息产生,客户端都会进行请求,势必会对服务端造成很大压力,浪费带宽和服务器资源。
长轮询
长轮询是对上边短轮询的一种改进版本,在尽可能减少对服务器资源浪费的同时,保证消息的相对实时性。长轮询在中间件中应用的很广泛,比如Nacos
和apollo
配置中心,消息队列kafka
、RocketMQ
中都有用到长轮询。
Nacos配置中心交互模型是push还是pull?一文中我详细介绍过Nacos
长轮询的实现原理,感兴趣的小伙伴可以瞅瞅。
这次我使用apollo
配置中心实现长轮询的方式,应用了一个类DeferredResult
,它是在servelet3.0
后经过Spring封装提供的一种异步请求机制,直意就是延迟结果。
DeferredResult
可以允许容器线程快速释放占用的资源,不阻塞请求线程,以此接受更多的请求提升系统的吞吐量,然后启动异步工作线程处理真正的业务逻辑,处理完成调用DeferredResult.setResult(200)
提交响应结果。
下边我们用长轮询来实现消息推送。
因为一个ID可能会被多个长轮询请求监听,所以我采用了guava
包提供的Multimap
结构存放长轮询,一个key可以对应多个value。一旦监听到key发生变化,对应的所有长轮询都会响应。前端得到非请求超时的状态码,知晓数据变更,主动查询未读消息数接口,更新页面数据。
@Controller
@RequestMapping("/polling")
public class PollingController {
// 存放监听某个Id的长轮询集合
// 线程同步结构
public static Multimap<String, DeferredResult<String>> watchRequests = Multimaps.synchronizedMultimap(HashMultimap.create());
/**
* 公众号:程序员小富
* 设置监听
*/
@GetMapping(path = "watch/{id}")
@ResponseBody
public DeferredResult<String> watch(@PathVariable String id) {
// 延迟对象设置超时时间
DeferredResult<String> deferredResult = new DeferredResult<>(TIME_OUT);
// 异步请求完成时移除 key,防止内存溢出
deferredResult.onCompletion(() -> {
watchRequests.remove(id, deferredResult);
});
// 注册长轮询请求
watchRequests.put(id, deferredResult);
return deferredResult;
}
/**
* 公众号:程序员小富
* 变更数据
*/
@GetMapping(path = "publish/{id}")
@ResponseBody
public String publish(@PathVariable String id) {
// 数据变更 取出监听ID的所有长轮询请求,并一一响应处理
if (watchRequests.containsKey(id)) {
Collection<DeferredResult<String>> deferredResults = watchRequests.get(id);
for (DeferredResult<String> deferredResult : deferredResults) {
deferredResult.setResult("我更新了" + new Date());
}
}
return "success";
}
当请求超过设置的超时时间,会抛出AsyncRequestTimeoutException
异常,这里直接用@ControllerAdvice
全局捕获统一返回即可,前端获取约定好的状态码后再次发起长轮询请求,如此往复调用。
@ControllerAdvice
public class AsyncRequestTimeoutHandler {
@ResponseStatus(HttpStatus.NOT_MODIFIED)
@ResponseBody
@ExceptionHandler(AsyncRequestTimeoutException.class)
public String asyncRequestTimeoutHandler(AsyncRequestTimeoutException e) {
System.out.println("异步请求超时");
return "304";
}
}
我们来测试一下,首先页面发起长轮询请求/polling/watch/10086
监听消息更变,请求被挂起,不变更数据直至超时,再次发起了长轮询请求;紧接着手动变更数据/polling/publish/10086
,长轮询得到响应,前端处理业务逻辑完成后再次发起请求,如此循环往复。
长轮询相比于短轮询在性能上提升了很多,但依然会产生较多的请求,这是它的一点不完美的地方。
iframe流
iframe流就是在页面中插入一个隐藏的<iframe>
标签,通过在src
中请求消息数量API接口,由此在服务端和客户端之间创建一条长连接,服务端持续向iframe
传输数据。
传输的数据通常是
HTML
、或是内嵌的javascript
脚本,来达到实时更新页面的效果。
这种方式实现简单,前端只要一个<iframe>
标签搞定了
<iframe src="/iframe/message" style="display:none"></iframe>
服务端直接组装html、js脚本数据向response
写入就行了
@Controller
@RequestMapping("/iframe")
public class IframeController {
@GetMapping(path = "message")
public void message(HttpServletResponse response) throws IOException, InterruptedException {
while (true) {
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-cache,no-store");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().print(" <script type=\"text/javascript\">\n" +
"parent.document.getElementById('clock').innerHTML = \"" + count.get() + "\";" +
"parent.document.getElementById('count').innerHTML = \"" + count.get() + "\";" +
"</script>");
}
}
}
但我个人不推荐,因为它在浏览器上会显示请求未加载完,图标会不停旋转,简直是强迫症杀手。
SSE (我的方式)
很多人可能不知道,服务端向客户端推送消息,其实除了可以用WebSocket
这种耳熟能详的机制外,还有一种服务器发送事件(Server-sent events
),简称SSE
。
SSE
它是基于HTTP
协议的,我们知道一般意义上的HTTP协议是无法做到服务端主动向客户端推送消息的,但SSE是个例外,它变换了一种思路。
SSE在服务器和客户端之间打开一个单向通道,服务端响应的不再是一次性的数据包而是text/event-stream
类型的数据流信息,在有数据变更时从服务器流式传输到客户端。
整体的实现思路有点类似于在线视频播放,视频流会连续不断的推送到浏览器,你也可以理解成,客户端在完成一次用时很长(网络不畅)的下载。
SSE
与WebSocket
作用相似,都可以建立服务端与浏览器之间的通信,实现服务端向客户端推送消息,但还是有些许不同:
- SSE 是基于HTTP协议的,它们不需要特殊的协议或服务器实现即可工作;
WebSocket
需单独服务器来处理协议。 - SSE 单向通信,只能由服务端向客户端单向通信;webSocket全双工通信,即通信的双方可以同时发送和接受信息。
- SSE 实现简单开发成本低,无需引入其他组件;WebSocket传输数据需做二次解析,开发门槛高一些。
- SSE 默认支持断线重连;WebSocket则需要自己实现。
- SSE 只能传送文本消息,二进制数据需要经过编码后传送;WebSocket默认支持传送二进制数据。
SSE 与 WebSocket 该如何选择?
技术并没有好坏之分,只有哪个更合适
SSE好像一直不被大家所熟知,一部分原因是出现了WebSockets,这个提供了更丰富的协议来执行双向、全双工通信。对于游戏、即时通信以及需要双向近乎实时更新的场景,拥有双向通道更具吸引力。
但是,在某些情况下,不需要从客户端发送数据。而你只需要一些服务器操作的更新。比如:站内信、未读消息数、状态更新、股票行情、监控数量等场景,SEE
不管是从实现的难易和成本上都更加有优势。此外,SSE 具有WebSockets
在设计上缺乏的多种功能,例如:自动重新连接
、事件ID
和发送任意事件
的能力。
前端只需进行一次HTTP请求,带上唯一ID,打开事件流,监听服务端推送的事件就可以了
<script>
let source = null;
let userId = 7777
if (window.EventSource) {
// 建立连接
source = new EventSource('http://localhost:7777/sse/sub/'+userId);
setMessageInnerHTML("连接用户=" + userId);
/**
* 连接一旦建立,就会触发open事件
* 另一种写法:source.onopen = function (event) {}
*/
source.addEventListener('open', function (e) {
setMessageInnerHTML("建立连接。。。");
}, false);
/**
* 客户端收到服务器发来的数据
* 另一种写法:source.onmessage = function (event) {}
*/
source.addEventListener('message', function (e) {
setMessageInnerHTML(e.data);
});
} else {
setMessageInnerHTML("你的浏览器不支持SSE");
}
</script>
服务端的实现更简单,创建一个SseEmitter
对象放入sseEmitterMap
进行管理
private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
/**
* 创建连接
*
* @date: 2022/7/12 14:51
* @auther: 公众号:程序员小富
*/
public static SseEmitter connect(String userId) {
try {
// 设置超时时间,0表示不过期。默认30秒
SseEmitter sseEmitter = new SseEmitter(0L);
// 注册回调
sseEmitter.onCompletion(completionCallBack(userId));
sseEmitter.onError(errorCallBack(userId));
sseEmitter.onTimeout(timeoutCallBack(userId));
sseEmitterMap.put(userId, sseEmitter);
count.getAndIncrement();
return sseEmitter;
} catch (Exception e) {
log.info("创建新的sse连接异常,当前用户:{}", userId);
}
return null;
}
/**
* 给指定用户发送消息
*
* @date: 2022/7/12 14:51
* @auther: 公众号:程序员小富
*/
public static void sendMessage(String userId, String message) {
if (sseEmitterMap.containsKey(userId)) {
try {
sseEmitterMap.get(userId).send(message);
} catch (IOException e) {
log.error("用户[{}]推送异常:{}", userId, e.getMessage());
removeUser(userId);
}
}
}
我们模拟服务端推送消息,看下客户端收到了消息,和我们预期的效果一致。
注意: SSE不支持IE
浏览器,对其他主流浏览器兼容性做的还不错。
MQTT
什么是 MQTT协议?
MQTT
全称(Message Queue Telemetry Transport):一种基于发布/订阅(publish
/subscribe
)模式的轻量级
通讯协议,通过订阅相应的主题来获取消息,是物联网(Internet of Thing
)中的一个标准传输协议。
该协议将消息的发布者(publisher
)与订阅者(subscriber
)进行分离,因此可以在不可靠的网络环境中,为远程连接的设备提供可靠的消息服务,使用方式与传统的MQ有点类似。
TCP
协议位于传输层,MQTT
协议位于应用层,MQTT
协议构建于TCP/IP
协议上,也就是说只要支持TCP/IP
协议栈的地方,都可以使用MQTT
协议。
为什么要用 MQTT协议?
MQTT
协议为什么在物联网(IOT)中如此受偏爱?而不是其它协议,比如我们更为熟悉的 HTTP
协议呢?
- 首先
HTTP
协议它是一种同步协议,客户端请求后需要等待服务器的响应。而在物联网(IOT)环境中,设备会很受制于环境的影响,比如带宽低、网络延迟高、网络通信不稳定等,显然异步消息协议更为适合IOT
应用程序。 HTTP
是单向的,如果要获取消息客户端必须发起连接,而在物联网(IOT)应用程序中,设备或传感器往往都是客户端,这意味着它们无法被动地接收来自网络的命令。- 通常需要将一条命令或者消息,发送到网络上的所有设备上。
HTTP
要实现这样的功能不但很困难,而且成本极高。
具体的MQTT协议介绍和实践,这里我就不再赘述了,大家可以参考我之前的两篇文章,里边写的也都很详细了。
MQTT协议的介绍
我也没想到 springboot + rabbitmq 做智能家居,会这么简单
MQTT实现消息推送
未读消息(小红点),前端 与 RabbitMQ 实时消息推送实践,贼简单~
Websocket
websocket
应该是大家都比较熟悉的一种实现消息推送的方式,上边我们在讲SSE的时候也和websocket进行过比较。
WebSocket是一种在TCP
连接上进行全双工通信的协议,建立客户端和服务器之间的通信渠道。浏览器和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
springboot整合websocket,先引入websocket
相关的工具包,和SSE相比额外的开发成本。
<!-- 引入websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
服务端使用@ServerEndpoint
注解标注当前类为一个websocket服务器,客户端可以通过ws://localhost:7777/webSocket/10086
来连接到WebSocket服务器端。
@Component
@Slf4j
@ServerEndpoint("/websocket/{userId}")
public class WebSocketServer {
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private static final CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>();
// 用来存在线连接数
private static final Map<String, Session> sessionPool = new HashMap<String, Session>();
/**
* 公众号:程序员小富
* 链接成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") String userId) {
try {
this.session = session;
webSockets.add(this);
sessionPool.put(userId, session);
log.info("websocket消息: 有新的连接,总数为:" + webSockets.size());
} catch (Exception e) {
}
}
/**
* 公众号:程序员小富
* 收到客户端消息后调用的方法
*/
@OnMessage
public void onMessage(String message) {
log.info("websocket消息: 收到客户端消息:" + message);
}
/**
* 公众号:程序员小富
* 此为单点消息
*/
public void sendOneMessage(String userId, String message) {
Session session = sessionPool.get(userId);
if (session != null && session.isOpen()) {
try {
log.info("websocket消: 单点消息:" + message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
前端初始化打开WebSocket连接,并监听连接状态,接收服务端数据或向服务端发送数据。
<script>
var ws = new WebSocket('ws://localhost:7777/webSocket/10086');
// 获取连接状态
console.log('ws连接状态:' + ws.readyState);
//监听是否连接成功
ws.onopen = function () {
console.log('ws连接状态:' + ws.readyState);
//连接成功则发送一个数据
ws.send('test1');
}
// 接听服务器发回的信息并处理展示
ws.onmessage = function (data) {
console.log('接收到来自服务器的消息:');
console.log(data);
//完成通信后关闭WebSocket连接
ws.close();
}
// 监听连接关闭事件
ws.onclose = function () {
// 监听整个过程中websocket的状态
console.log('ws连接状态:' + ws.readyState);
}
// 监听并处理error事件
ws.onerror = function (error) {
console.log(error);
}
function sendMessage() {
var content = $("#message").val();
$.ajax({
url: '/socket/publish?userId=10086&message=' + content,
type: 'GET',
data: { "id": "7777", "content": content },
success: function (data) {
console.log(data)
}
})
}
</script>
页面初始化建立websocket连接,之后就可以进行双向通信了,效果还不错
自定义推送
上边我们给我出了6种方案的原理和代码实现,但在实际业务开发过程中,不能盲目的直接拿过来用,还是要结合自身系统业务的特点和实际场景来选择合适的方案。
推送最直接的方式就是使用第三推送平台,毕竟钱能解决的需求都不是问题,无需复杂的开发运维,直接可以使用,省时、省力、省心,像goEasy、极光推送都是很不错的三方服务商。
一般大型公司都有自研的消息推送平台,像我们本次实现的web站内信只是平台上的一个触点而已,短信、邮件、微信公众号、小程序凡是可以触达到用户的渠道都可以接入进来。
消息推送系统内部是相当复杂的,诸如消息内容的维护审核、圈定推送人群、触达过滤拦截(推送的规则频次、时段、数量、黑白名单、关键词等等)、推送失败补偿非常多的模块,技术上涉及到大数据量、高并发的场景也很多。所以我们今天的实现方式在这个庞大的系统面前只是小打小闹。
Github地址
文中所提到的案例我都一一的做了实现,整理放在了Github
上,觉得有用就 Star 一下吧!
传送门:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-realtime-data
redislog
<?php require("redis.php"); $l = $redis->get("userid"); $count = $redis->lLen("uid");//总数 添加中脸链表关键词uid $page_size = 3;//页大小 $page_num = (!empty($_GET['page'])) ? $_GET['page'] : 1;//当前页 $page_count = ceil($count/$page_size);//页总数 // add.php 形成链表 //用 lrange查看分页 lrange uid 0 2 第一页 lrange uid 3 5 第二页 lrange uid 6 8 $uid_lstart = ($page_num -1 ) * $page_size; $uid_lend = $uid_lstart + ($page_size -1); $ids = $redis->lRange("uid",$uid_lstart,$uid_lend); //1.这款的数组=============$ids改写为sort排序================================= $sort_dispaly_hash_key = 'user'; $sort_dispaly_f = array('uid','username','age'); $sort_pai = 'desc'; $sort_by = 'user:*->uid'; $sort_farr = array(); foreach ($sort_dispaly_f as $index => $item) { $sort_farr[] = $sort_dispaly_hash_key.':*->'.$item; } $sort_farrl = count($sort_dispaly_f); $sort=array( 'BY'=>$sort_by, 'SORT'=>$sort_pai, 'limit'=>array($uid_lstart,$page_size), 'GET'=>$sort_farr ); $rs_sort= $redis->sort('uid',$sort); $rs_sort_new = []; if(!empty($rs_sort)){ foreach ($rs_sort as $index => $item) { if($index % $sort_farrl == 0){ $insert_f = array(); foreach ($sort_dispaly_f as $kk => $sort_dispaly_f_v) { $index_k = ($kk == 0) ? $index : $index+$kk; $insert_f[$sort_dispaly_f_v] = $rs_sort[$index_k]; } $rs_sort_new[] = $insert_f; } } } /** 无分页的列表 for($i=1;$i<=$l ;$i++){ $data[] = $redis->hgetall("user:".$i); } //$data = array_filter($data); **/ /** * 有分页的列表 * list.php?page=1 * list.php?page=2 * list.php?page=3 */ //print_r($ids); //测试没问题 foreach ($ids as $v){ $data[] = $redis->hgetall("user:".$v); } $data = $rs_sort_new; ?> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> <?php $login_uname = ""; if(!empty($_COOKIE['token'])){ $login_uid = $redis->get("token:".$_COOKIE['token']); $login_uname = $redis->hGet("user:".$login_uid,"username"); echo "<a href=\"add.php\">添加</a>"; echo "欢迎您,{$login_uname} <a href=\"logout.php\" >退出</a>"; }else{ echo "<a href=\"login.php\" >登录</a> | "; echo " <a href=\"reg.php\" >注册</a>"; } ?> <br />============================================<br /> <table border="1"> <tr> <th>id</th> <th>姓名</th> <!--<th>密码</th>--> <th>年龄</th> <th>操作</th> </tr> <?php foreach ($data as $index => $v) { ?> <tr> <td><?php echo $v['uid'];?></td> <td><?php echo $v['username'];?></td> <!--<td><?php echo $v['password'];?></td>--> <td><?php echo $v['age'];?></td> <td> <?php if(!empty($_COOKIE['token'])) { ?> <a href="del.php?id=<?php echo $v['uid'];?>">删除</a> <a href="mod.php?id=<?php echo $v['uid'];?>">编辑</a> <?php if($login_uid !== $v['uid'] ){ ?> <a href="addfans.php?id=<?php echo $v['uid']?>&login_uid=<?php echo $login_uid ?>" />加关注</a> <?php } ?> <?php } ?> </td> </tr> <?php } ?> <tr> <td colspan="4"> <a href="?page=<?php echo (($page_num - 1) <= 1 ) ? 1 : ($page_num - 1) ?>">上一页</a> <a href="?page=<?php echo (($page_num + 1) >= $page_count ) ? $page_count : ($page_num + 1) ?>">下一页</a> <a href="?page=1">首页</a> <a href="?page=<?php echo $page_count ?>">尾页</a> 当前 <?php echo $page_num ?> 页 总共 <?php echo $page_count ?>页 总共 <?php echo $count ?> 个用户 </td> </tr> </talbe> <br /> <table border="1"> <tr> <td colspan="4">我关注了谁(<?php echo $redis->sCard("user:".$login_uid.":guanzhu"); ?>)</td> </tr> <?php $rs_guanzhu = $redis->sMembers("user:".$login_uid.":guanzhu"); foreach ( $rs_guanzhu as $index => $item) { $row = $redis->hGetAll("user:".$item) ?> <tr> <td><?php echo $row['uid'] ?> </td> <td><?php echo $row['username'] ?></td> <td><?php echo $row['age'] ?></td> <td> <?php $rs_gongtong = $redis->sInter("user:".$login_uid.":guanzhu","user:".$item.":guanzhu"); if (!empty($rs_gongtong)){ echo "共同关注了:"; foreach ($rs_gongtong as $fff=> $gv){ $row_gv = $redis->hGetAll("user:".$gv); echo $row_gv['username'].','; } } ?> </td> </tr> <?php } ?> </table> <table border="1"> <tr> <td colspan="4">我的粉丝,谁关注我(<?php echo $redis->sCard("user:".$login_uid.":fensi"); ?>)</td> </tr> <?php $rs_fensi = $redis->sMembers("user:".$login_uid.":fensi"); foreach ( $rs_fensi as $index => $item) { $row = $redis->hGetAll("user:".$item) ?> <tr> <td><?php echo $row['uid'] ?> </td> <td><?php echo $row['username'] ?></td> <td><?php echo $row['age'] ?></td> <td> <?php //互粉是好友 $login_is_u_g = $redis->sIsMember("user:".$item.":guanzhu",$login_uid); $u_is_login_g = $redis->sIsMember("user:".$login_uid.":guanzhu",$item); if ($login_is_u_g && $u_is_login_g) { echo "是好友"; } ?> </td> </tr> <?php } ?> </table> <table border="1"> <tr> <td colspan="3">是好友的-互为关注 sInter fensi guanzhu </td> </tr> <?php $rs_fensi = $redis->sInter("user:".$login_uid.":fensi","user:".$login_uid.":guanzhu"); foreach ( $rs_fensi as $index => $item) { $row = $redis->hGetAll("user:".$item) ?> <tr> <td><?php echo $row['uid'] ?> </td> <td><?php echo $row['username'] ?></td> <td><?php echo $row['age'] ?></td> </tr> <?php } ?> </table> <br /> redis key的个数:<?php echo $redis_db_count = $redis->dbSize(); ?><br /> redis 信息:<?php $redis_info= $redis->info(); ?><br /> redis版本:<?php echo $redis_info['redis_version'];?><br /> <div style="display: none"> <pre> <?php print_r($redis_info); ?> </pre> </div> ========================redis源码包git信息==================<br /> redis_git_sha1:<?php echo $redis_info['redis_git_sha1'];?><br /> redis_git_dirty:<?php echo $redis_info['redis_git_dirty'];?><br /> redis_build_id:<?php echo $redis_info['redis_build_id'];?><br /> redis_mode:<?php $redis_mode_arr = ["standalone"=>"独立","cluster"=>"集群"]; echo $redis_mode_arr[$redis_info['redis_mode']]; ?><br />内存碎片率 当前连接数:<?php echo $redis_info['connected_clients'];?><br /> 已使用内存: <?php echo $redis_info['used_memory'];?>|<?php echo $redis_info['used_memory_human'];?><br /> Redis占用的物理内存: <?php echo $redis_info['used_memory_rss'];?>|<?php echo $redis_info['used_memory_rss_human'];?><br /> Redis的内存使用峰值: <?php echo $redis_info['used_memory_peak'];?>|<?php echo $redis_info['used_memory_peak_human'];?><br /> 所在服务器系统总内存: <?php echo $redis_info['total_system_memory'];?>|<?php echo $redis_info['total_system_memory_human'];?><br /> lua使用的内存: <?php echo $redis_info['used_memory_lua'];?>|<?php echo $redis_info['used_memory_lua_human'];?><br /> 最大使用内存限制: <?php echo $redis_info['maxmemory'];?>|<?php echo $redis_info['maxmemory_human'];?><span style="color:#cccccc;font-size: 11px">(最大使用内存限制,超过时,会使用LRU或LFU策略删除key,该值尽量设置的小于系统内存[maxmemory])</span><br /> 内存碎片率: <?php echo $redis_info['mem_fragmentation_ratio'];?><span style="color:#cccccc;font-size: 11px">(used_memory_rss / used_memory内存碎片率,过高对性能的影响)</span><br /> 内存分配器: <?php echo $redis_info['mem_allocator'];?><br /> =========================Persistence RDB和AOF相关信息================<br /> loading:<?php echo $redis_info['loading']; ?> 是否有加载转储文件的标识,0=无,1=有,内存耗尽可能会有<br /> 最后一次保存时间戳:<?php echo $redis_info['rdb_last_save_time']; ?> | <?php echo date("Y-m-d H:i:s",$redis_info['rdb_last_save_time']) ; ?><br /> =========================数据库中的key总量,过期key总量,平均过期时长(毫秒)=================== <br /> <?php for ($i=0;$i<=16;$i++){ if(!empty($redis_info['db'.$i])){ echo 'db'.$i.': ======'.$redis_info['db'.$i].'<br />'; } } ?> =====================================================================<br /> <pre> <?php $redis_key = $redis->keys("*"); //window 是这样 liunx 返回其他 $rs_type_arr = [ '不存在','字符串','集合','列表','有序集合','哈希表']; $redis_key_new = []; if(!empty($redis_key)){ foreach ($redis_key as $item) { //echo $item.'=>' .$rs_type_arr[$redis->type($item)]."<br />"; $redis_key_new[$rs_type_arr[$redis->type($item)]][] = $item; } } if (!empty($redis_key_new)){ $hx_arr = $str_arr = $ls_arr= $yxjh_arr = $wxjh_arr = []; foreach ($redis_key_new as $index=> $item) { if ($index == '哈希表'){ $hx_arr = $item; } if ($index == '字符串'){ $str_arr = $item ; } if ($index == '列表'){ $ls_arr = $item ; } if ($index == '有序集合'){ $yxjh_arr = $item ; } if ($index == '集合'){ $wxjh_arr = $item ; } } } if(!empty($str_arr)) { $str_arr_new = []; foreach ($str_arr as $index => $item) { $str_arr_new[$item] = $redis->Get($item); } echo "=======================================string=============================<br />"; print_r($str_arr_new); } if(!empty($ls_arr)) { $ls_arr_new = []; foreach ($ls_arr as $index => $item) { $ls_arr_new[$item] = $redis->lrange($item, 0 , -1); } echo "======================================list==================================<br />"; print_r($ls_arr_new); } if(!empty($hx_arr)){ $hx_arr_new = []; foreach ($hx_arr as $index => $item) { $hx_arr_new[$item] = $redis->hGetAll($item); } echo "======================================hash===================================<br />"; print_r($hx_arr_new); } if(!empty($wxjh_arr)){ $wxjh_arr_new = []; foreach ($wxjh_arr as $index => $item) { $wxjh_arr_new[$item] = $redis->sMembers($item); } echo "======================================set集合===================================<br />"; print_r($wxjh_arr_new); } ?> </pre>
php扩展 通用 – compser安装
cd yac-2.3.1
/usr/local/php/bin/phpize
./configure –with-php-config=/usr/local/php/bin/php-config
make && make install
ls /usr/local/php/lib/php/extensions/no-debug-non-zts-20190902/
composer -v
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
composer config -l -g
phpstudy thinkphp6
window10 下 phpstudy2018
nginx 模式下 配置
是双杠
root “G:\\myapp\\tp\\public”;
完整
server {
listen 80;
server_name myapp.cc ;
root “G:\\myapp\\tp\\public”;
index index.php index.html index.htm;
location / {
#autoindex on;
if (!-e $request_filename) {
rewrite ^/(.*)$ /index.php/$1 last;
break;
}
}
location ~ \.php(.*)$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_split_path_info ^((?U).+\.php)(/?.+)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
include fastcgi_params;
}
}
compser phpstudy
查看composer版本 1.查询位置在compser安装目录下执行
E:phpstudy_proExtensionsphpphp7.1.9ntsphp.exe composer.phar -V 显示版本号则为正常
2.composer默认是国外的镜像,如果想使用国内镜像 https://packagist.phpcomposer.com 还需要配置一下E:phpstudy_proExtensionsphpphp7.1.9ntsphp.exe composer.phar config -g repo.packagist composer httpp://packagist.phpcomposer.com
https://packagist.phpcomposer.com
快速设置windows 国内镜像
在windows 黑窗口 操作 > composer config -g repo.packagist composer https://packagist.phpcomposer.com
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
查看全局配置:
composer config -l -g
安装laravel v6.18.3版本 composer create-project laravel/laravel laravel6.8 v6.18.3
===================================================
出现的错误:
[InvalidArgumentException]
Could not find package topthink/think with stability stable.
解决方案:
删除之前的镜像:composer config -g –unset repos.packagist
mockjs使用
1.安装
npm install mockjs –save
npm install axios –save
安装axios是为了能模拟后台接口。
2.建立目录结构如下:
3.在main.js引入mockjs
4. mock.js
import Mock from ‘mockjs’ // 引入mockjs
const Random = Mock.Random // Mock.Random 是一个工具类,用于生成各种随机数据
let data = [] // 用于接受生成数据的数组
let size = [
‘300×250’, ‘250×250’, ‘240×400’, ‘336×280’,
‘180×150’, ‘720×300’, ‘468×60’, ‘234×60′,
’88×31’, ‘120×90’, ‘120×60’, ‘120×240’,
‘125×125’, ‘728×90’, ‘160×600’, ‘120×600’,
‘300×600’
] // 定义随机值
for(let i = 0; i < 10; i ++) { // 可自定义生成的个数
let template = {
‘Boolean’: Random.boolean, // 可以生成基本数据类型
‘Natural’: Random.natural(1, 10), // 生成1到100之间自然数
‘Integer’: Random.integer(1, 100), // 生成1到100之间的整数
‘Float’: Random.float(0, 100, 0, 5), // 生成0到100之间的浮点数,小数点后尾数为0到5位
‘Character’: Random.character(), // 生成随机字符串,可加参数定义规则
‘String’: Random.string(2, 10), // 生成2到10个字符之间的字符串
‘Range’: Random.range(0, 10, 2), // 生成一个随机数组
‘Date’: Random.date(), // 生成一个随机日期,可加参数定义日期格式
‘Image’: Random.image(Random.size, ‘#02adea’, ‘Hello’), // Random.size表示将从size数据中任选一个数据
‘Color’: Random.color(), // 生成一个颜色随机值
‘Paragraph’:Random.paragraph(2, 5), //生成2至5个句子的文本
‘Name’: Random.name(), // 生成姓名
‘Url’: Random.url(), // 生成web地址
‘Address’: Random.province() // 生成地址
}
data.push(template)
}
Mock.mock(‘/data/index’, ‘post’, data) // 根据数据模板生成模拟数据
5.api.js
import axios from ‘axios’
axios.defaults.headers.post[‘Content-Type’] = ‘application/x-www-form-urlencoded’
// 请求拦截器
axios.interceptors.request.use(function(config) {
return config;
}, function(error) {
return Promise.reject(error);
})
// 响应拦截器
axios.interceptors.response.use(function(response) {
return response;
}, function(error) {
return Promise.reject(error);
})
// 封装axios的post请求
export function fetch(url, params) {
return new Promise((resolve, reject) => {
axios.post(url, params)
.then(response => {
resolve(response.data);
})
.catch((error) => {
reject(error);
})
})
}
export default {
mockdata(url, params) {
return fetch(url, params);
}
}
6.mock.vue
<template>
<div>
</div>
</template>
<script>
import api from ‘./../axios/api.js’
export default {
name: ‘Mock’,
data () {
return {
dataShow: []
}
},
created () {
this.getdata()
},
methods: {
getdata: function() {
api.mockdata(‘/data/index’)
.then(res => {
console.log(res);
this.dataShow = res.data;
})
}
}
}
</script>
<!– Add “scoped” attribute to limit CSS to this component only –>
<style scoped>
</style>
7.控制台打印出的数据
以上是使用mockjs生成的模拟数据,基本可以满足平时开发的需要,不过mockjs还有其他的用法未能一一列出来,建议各位朋友们去mock官网的文档了解更多,地址:http://mockjs.com/ 。
————————————————
前端构建工具gulpjs的使用介绍及技巧
gulpjs是一个前端构建工具,与gruntjs相比,gulpjs无需写一大堆繁杂的配置参数,API也非常简单,学习起来很容易,而且gulpjs使用的是nodejs中stream来读取和操作数据,其速度更快。如果你还没有使用过前端构建工具,或者觉得gruntjs太难用的话,那就尝试一下gulp吧。
本文导航:
1、gulp的安装
首先确保你已经正确安装了nodejs环境。然后以全局方式安装gulp:
npm install -g gulp
全局安装gulp后,还需要在每个要使用gulp的项目中都单独安装一次。把目录切换到你的项目文件夹中,然后在命令行中执行:
npm install gulp
如果想在安装的时候把gulp写进项目package.json文件的依赖中,则可以加上–save-dev:
npm install --save-dev gulp
这样就完成了gulp的安装。至于为什么在全局安装gulp后,还需要在项目中本地安装一次,有兴趣的可以看下stackoverflow上有人做出的回答:why-do-we-need-to-install-gulp-globally-and-locally、what-is-the-point-of-double-install-in-gulp。大体就是为了版本的灵活性,但如果没理解那也不必太去纠结这个问题,只需要知道通常我们是要这样做就行了。
2、开始使用gulp
2.1 建立gulpfile.js文件
就像gruntjs需要一个Gruntfile.js
文件一样,gulp也需要一个文件作为它的主文件,在gulp中这个文件叫做gulpfile.js
。新建一个文件名为gulpfile.js
的文件,然后放到你的项目目录中。之后要做的事情就是在gulpfile.js
文件中定义我们的任务了。下面是一个最简单的gulpfile.js
文件内容示例,它定义了一个默认的任务。
var gulp = require('gulp');
gulp.task('default',function(){
console.log('hello world');
});
此时我们的目录结构是这样子的:
├── gulpfile.js
├── node_modules
│ └── gulp
└── package.json
2.2 运行gulp任务
要运行gulp任务,只需切换到存放gulpfile.js
文件的目录(windows平台请使用cmd或者Power Shell等工具),然后在命令行中执行gulp
命令就行了,gulp
后面可以加上要执行的任务名,例如gulp task1
,如果没有指定任务名,则会执行任务名为default
的默认任务。
3、gulp的API介绍
使用gulp,仅需知道4个API即可:gulp.task()
,gulp.src()
,gulp.dest()
,gulp.watch()
,所以很容易就能掌握,但有几个地方需理解透彻才行,我会在下面一一说明。为了避免出现理解偏差,建议先看一遍官方文档。
3.1 gulp.src()
在介绍这个API之前我们首先来说一下Grunt.js和Gulp.js工作方式的一个区别。Grunt主要是以文件为媒介来运行它的工作流的,比如在Grunt中执行完一项任务后,会把结果写入到一个临时文件中,然后可以在这个临时文件内容的基础上执行其它任务,执行完成后又把结果写入到临时文件中,然后又以这个为基础继续执行其它任务…就这样反复下去。而在Gulp中,使用的是Nodejs中的stream(流),首先获取到需要的stream,然后可以通过stream的pipe()
方法把流导入到你想要的地方,比如Gulp的插件中,经过插件处理后的流又可以继续导入到其他插件中,当然也可以把流写入到文件中。所以Gulp是以stream为媒介的,它不需要频繁的生成临时文件,这也是Gulp的速度比Grunt快的一个原因。再回到正题上来,gulp.src()
方法正是用来获取流的,但要注意这个流里的内容不是原始的文件流,而是一个虚拟文件对象流(Vinyl files),这个虚拟文件对象中存储着原始文件的路径、文件名、内容等信息,这个我们暂时不用去深入理解,你只需简单的理解可以用这个方法来读取你需要操作的文件就行了。其语法为:
gulp.src(globs[, options])
globs参数是文件匹配模式(类似正则表达式),用来匹配文件路径(包括文件名),当然这里也可以直接指定某个具体的文件路径。当有多个匹配模式时,该参数可以为一个数组。
options为可选参数。通常情况下我们不需要用到。
下面我们重点说说Gulp用到的glob的匹配规则以及一些文件匹配技巧。
Gulp内部使用了node-glob模块来实现其文件匹配功能。我们可以使用下面这些特殊的字符来匹配我们想要的文件:
*
匹配文件路径中的0个或多个字符,但不会匹配路径分隔符,除非路径分隔符出现在末尾**
匹配路径中的0个或多个目录及其子目录,需要单独出现,即它左右不能有其他东西了。如果出现在末尾,也能匹配文件。?
匹配文件路径中的一个字符(不会匹配路径分隔符)[...]
匹配方括号中出现的字符中的任意一个,当方括号中第一个字符为^
或!
时,则表示不匹配方括号中出现的其他字符中的任意一个,类似js正则表达式中的用法!(pattern|pattern|pattern)
匹配任何与括号中给定的任一模式都不匹配的?(pattern|pattern|pattern)
匹配括号中给定的任一模式0次或1次,类似于js正则中的(pattern|pattern|pattern)?+(pattern|pattern|pattern)
匹配括号中给定的任一模式至少1次,类似于js正则中的(pattern|pattern|pattern)+*(pattern|pattern|pattern)
匹配括号中给定的任一模式0次或多次,类似于js正则中的(pattern|pattern|pattern)*@(pattern|pattern|pattern)
匹配括号中给定的任一模式1次,类似于js正则中的(pattern|pattern|pattern)
下面以一系列例子来加深理解
*
能匹配a.js
,x.y
,abc
,abc/
,但不能匹配a/b.js
*.*
能匹配a.js
,style.css
,a.b
,x.y
*/*/*.js
能匹配a/b/c.js
,x/y/z.js
,不能匹配a/b.js
,a/b/c/d.js
**
能匹配abc
,a/b.js
,a/b/c.js
,x/y/z
,x/y/z/a.b
,能用来匹配所有的目录和文件**/*.js
能匹配foo.js
,a/foo.js
,a/b/foo.js
,a/b/c/foo.js
a/**/z
能匹配a/z
,a/b/z
,a/b/c/z
,a/d/g/h/j/k/z
a/**b/z
能匹配a/b/z
,a/sb/z
,但不能匹配a/x/sb/z
,因为只有单**
单独出现才能匹配多级目录?.js
能匹配a.js
,b.js
,c.js
a??
能匹配a.b
,abc
,但不能匹配ab/
,因为它不会匹配路径分隔符[xyz].js
只能匹配x.js
,y.js
,z.js
,不会匹配xy.js
,xyz.js
等,整个中括号只代表一个字符[^xyz].js
能匹配a.js
,b.js
,c.js
等,不能匹配x.js
,y.js
,z.js
当有多种匹配模式时可以使用数组
//使用数组的方式来匹配多种文件
gulp.src(['js/*.js','css/*.css','*.html'])
使用数组的方式还有一个好处就是可以很方便的使用排除模式,在数组中的单个匹配模式前加上!
即是排除模式,它会在匹配的结果中排除这个匹配,要注意一点的是不能在数组中的第一个元素中使用排除模式
gulp.src([*.js,'!b*.js']) //匹配所有js文件,但排除掉以b开头的js文件
gulp.src(['!b*.js',*.js]) //不会排除任何文件,因为排除模式不能出现在数组的第一个元素中
此外,还可以使用展开模式。展开模式以花括号作为定界符,根据它里面的内容,会展开为多个模式,最后匹配的结果为所有展开的模式相加起来得到的结果。展开的例子如下:
a{b,c}d
会展开为abd
,acd
a{b,}c
会展开为abc
,ac
a{0..3}d
会展开为a0d
,a1d
,a2d
,a3d
a{b,c{d,e}f}g
会展开为abg
,acdfg
,acefg
a{b,c}d{e,f}g
会展开为abdeg
,acdeg
,abdeg
,abdfg
3.2 gulp.dest()
gulp.dest()方法是用来写文件的,其语法为:
gulp.dest(path[,options])
path为写入文件的路径
options为一个可选的参数对象,通常我们不需要用到
要想使用好gulp.dest()
这个方法,就要理解给它传入的路径参数与最终生成的文件的关系。
gulp的使用流程一般是这样子的:首先通过gulp.src()
方法获取到我们想要处理的文件流,然后把文件流通过pipe方法导入到gulp的插件中,最后把经过插件处理后的流再通过pipe方法导入到gulp.dest()
中,gulp.dest()
方法则把流中的内容写入到文件中,这里首先需要弄清楚的一点是,我们给gulp.dest()
传入的路径参数,只能用来指定要生成的文件的目录,而不能指定生成文件的文件名,它生成文件的文件名使用的是导入到它的文件流自身的文件名,所以生成的文件名是由导入到它的文件流决定的,即使我们给它传入一个带有文件名的路径参数,然后它也会把这个文件名当做是目录名,例如:
var gulp = require('gulp');
gulp.src('script/jquery.js')
.pipe(gulp.dest('dist/foo.js'));
//最终生成的文件路径为 dist/foo.js/jquery.js,而不是dist/foo.js
要想改变文件名,可以使用插件gulp-rename
下面说说生成的文件路径与我们给gulp.dest()
方法传入的路径参数之间的关系。
gulp.dest(path)
生成的文件路径是我们传入的path参数后面再加上gulp.src()
中有通配符开始出现的那部分路径。例如:
var gulp = reruire('gulp');
//有通配符开始出现的那部分路径为 **/*.js
gulp.src('script/**/*.js')
.pipe(gulp.dest('dist')); //最后生成的文件路径为 dist/**/*.js
//如果 **/*.js 匹配到的文件为 jquery/jquery.js ,则生成的文件路径为 dist/jquery/jquery.js
再举更多一点的例子
gulp.src('script/avalon/avalon.js') //没有通配符出现的情况
.pipe(gulp.dest('dist')); //最后生成的文件路径为 dist/avalon.js
//有通配符开始出现的那部分路径为 **/underscore.js
gulp.src('script/**/underscore.js')
//假设匹配到的文件为script/util/underscore.js
.pipe(gulp.dest('dist')); //则最后生成的文件路径为 dist/util/underscore.js
gulp.src('script/*') //有通配符出现的那部分路径为 *
//假设匹配到的文件为script/zepto.js
.pipe(gulp.dest('dist')); //则最后生成的文件路径为 dist/zepto.js
通过指定gulp.src()
方法配置参数中的base
属性,我们可以更灵活的来改变gulp.dest()
生成的文件路径。
当我们没有在gulp.src()
方法中配置base
属性时,base
的默认值为通配符开始出现之前那部分路径,例如:
gulp.src('app/src/**/*.css') //此时base的值为 app/src
上面我们说的gulp.dest()
所生成的文件路径的规则,其实也可以理解成,用我们给gulp.dest()
传入的路径替换掉gulp.src()
中的base
路径,最终得到生成文件的路径。
gulp.src('app/src/**/*.css') //此时base的值为app/src,也就是说它的base路径为app/src
//设该模式匹配到了文件 app/src/css/normal.css
.pipe(gulp.dest('dist')) //用dist替换掉base路径,最终得到 dist/css/normal.css
所以改变base路径后,gulp.dest()
生成的文件路径也会改变
gulp.src(script/lib/*.js) //没有配置base参数,此时默认的base路径为script/lib
//假设匹配到的文件为script/lib/jquery.js
.pipe(gulp.dest('build')) //生成的文件路径为 build/jquery.js
gulp.src(script/lib/*.js, {base:'script'}) //配置了base参数,此时base路径为script
//假设匹配到的文件为script/lib/jquery.js
.pipe(gulp.dest('build')) //此时生成的文件路径为 build/lib/jquery.js
用gulp.dest()
把文件流写入文件后,文件流仍然可以继续使用。
3.3 gulp.task()
gulp.task
方法用来定义任务,内部使用的是Orchestrator,其语法为:
gulp.task(name[, deps], fn)
name 为任务名
deps 是当前定义的任务需要依赖的其他任务,为一个数组。当前定义的任务会在所有依赖的任务执行完毕后才开始执行。如果没有依赖,则可省略这个参数
fn 为任务函数,我们把任务要执行的代码都写在里面。该参数也是可选的。
gulp.task('mytask', ['array', 'of', 'task', 'names'], function() { //定义一个有依赖的任务
// Do something
});
gulp.task()
这个API没什么好讲的,但需要知道执行多个任务时怎么来控制任务执行的顺序。
gulp中执行多个任务,可以通过任务依赖来实现。例如我想要执行one
,two
,three
这三个任务,那我们就可以定义一个空的任务,然后把那三个任务当做这个空的任务的依赖就行了:
//只要执行default任务,就相当于把one,two,three这三个任务执行了
gulp.task('default',['one','two','three']);
如果任务相互之间没有依赖,任务会按你书写的顺序来执行,如果有依赖的话则会先执行依赖的任务。
但是如果某个任务所依赖的任务是异步的,就要注意了,gulp并不会等待那个所依赖的异步任务完成,而是会接着执行后续的任务。例如:
gulp.task('one',function(){
//one是一个异步执行的任务
setTimeout(function(){
console.log('one is done')
},5000);
});
//two任务虽然依赖于one任务,但并不会等到one任务中的异步操作完成后再执行
gulp.task('two',['one'],function(){
console.log('two is done');
});
上面的例子中我们执行two任务时,会先执行one任务,但不会去等待one任务中的异步操作完成后再执行two任务,而是紧接着执行two任务。所以two任务会在one任务中的异步操作完成之前就执行了。
那如果我们想等待异步任务中的异步操作完成后再执行后续的任务,该怎么做呢?
有三种方法可以实现:
第一:在异步操作完成后执行一个回调函数来通知gulp这个异步任务已经完成,这个回调函数就是任务函数的第一个参数。
gulp.task('one',function(cb){ //cb为任务函数提供的回调,用来通知任务已经完成
//one是一个异步执行的任务
setTimeout(function(){
console.log('one is done');
cb(); //执行回调,表示这个异步任务已经完成
},5000);
});
//这时two任务会在one任务中的异步操作完成后再执行
gulp.task('two',['one'],function(){
console.log('two is done');
});
第二:定义任务时返回一个流对象。适用于任务就是操作gulp.src获取到的流的情况。
gulp.task('one',function(cb){
var stream = gulp.src('client/**/*.js')
.pipe(dosomething()) //dosomething()中有某些异步操作
.pipe(gulp.dest('build'));
return stream;
});
gulp.task('two',['one'],function(){
console.log('two is done');
});
第三:返回一个promise对象,例如
var Q = require('q'); //一个著名的异步处理的库 https://github.com/kriskowal/q
gulp.task('one',function(cb){
var deferred = Q.defer();
// 做一些异步操作
setTimeout(function() {
deferred.resolve();
}, 5000);
return deferred.promise;
});
gulp.task('two',['one'],function(){
console.log('two is done');
});
gulp.task()
就这些了,主要是要知道当依赖是异步任务时的处理。
3.4 gulp.watch()
gulp.watch()
用来监视文件的变化,当文件发生变化后,我们可以利用它来执行相应的任务,例如文件压缩等。其语法为
gulp.watch(glob[, opts], tasks)
glob 为要监视的文件匹配模式,规则和用法与gulp.src()
方法中的glob
相同。
opts 为一个可选的配置对象,通常不需要用到
tasks 为文件变化后要执行的任务,为一个数组
gulp.task('uglify',function(){
//do something
});
gulp.task('reload',function(){
//do something
});
gulp.watch('js/**/*.js', ['uglify','reload']);
gulp.watch()
还有另外一种使用方式:
gulp.watch(glob[, opts, cb])
glob和opts参数与第一种用法相同
cb参数为一个函数。每当监视的文件发生变化时,就会调用这个函数,并且会给它传入一个对象,该对象包含了文件变化的一些信息,type
属性为变化的类型,可以是added
,changed
,deleted
;path
属性为发生变化的文件的路径
gulp.watch('js/**/*.js', function(event){
console.log(event.type); //变化类型 added为新增,deleted为删除,changed为改变
console.log(event.path); //变化的文件的路径
});
4、一些常用的gulp插件
gulp的插件数量虽然没有grunt那么多,但也可以说是应有尽有了,下面列举一些常用的插件。
4.1 自动加载插件
使用gulp-load-plugins
安装:npm install --save-dev gulp-load-plugins
要使用gulp的插件,首先得用require
来把插件加载进来,如果我们要使用的插件非常多,那我们的gulpfile.js
文件开头可能就会是这个样子的:
var gulp = require('gulp'),
//一些gulp插件,abcd这些命名只是用来举个例子
a = require('gulp-a'),
b = require('gulp-b'),
c = require('gulp-c'),
d = require('gulp-d'),
e = require('gulp-e'),
f = require('gulp-f'),
g = require('gulp-g'),
//更多的插件...
z = require('gulp-z');
虽然这没什么问题,但会使我们的gulpfile.js
文件变得很冗长,看上去不那么舒服。gulp-load-plugins
插件正是用来解决这个问题。
gulp-load-plugins
这个插件能自动帮你加载package.json
文件里的gulp插件。例如假设你的package.json
文件里的依赖是这样的:
{
"devDependencies": {
"gulp": "~3.6.0",
"gulp-rename": "~1.2.0",
"gulp-ruby-sass": "~0.4.3",
"gulp-load-plugins": "~0.5.1"
}
}
然后我们可以在gulpfile.js
中使用gulp-load-plugins
来帮我们加载插件:
var gulp = require('gulp');
//加载gulp-load-plugins插件,并马上运行它
var plugins = require('gulp-load-plugins')();
然后我们要使用gulp-rename和gulp-ruby-sass这两个插件的时候,就可以使用plugins.rename
和plugins.rubySass
来代替了,也就是原始插件名去掉gulp-
前缀,之后再转换为驼峰命名。
实质上gulp-load-plugins
是为我们做了如下的转换
plugins.rename = require('gulp-rename');
plugins.rubySass = require('gulp-ruby-sass');
gulp-load-plugins
并不会一开始就加载所有package.json
里的gulp插件,而是在我们需要用到某个插件的时候,才去加载那个插件。
最后要提醒的一点是,因为gulp-load-plugins
是通过你的package.json
文件来加载插件的,所以必须要保证你需要自动加载的插件已经写入到了package.json
文件里,并且这些插件都是已经安装好了的。
4.2 重命名
使用gulp-rename
安装:npm install --save-dev gulp-rename
用来重命名文件流中的文件。用gulp.dest()
方法写入文件时,文件名使用的是文件流中的文件名,如果要想改变文件名,那可以在之前用gulp-rename
插件来改变文件流中的文件名。
var gulp = require('gulp'),
rename = require('gulp-rename'),
uglify = require("gulp-uglify");
gulp.task('rename', function () {
gulp.src('js/jquery.js')
.pipe(uglify()) //压缩
.pipe(rename('jquery.min.js')) //会将jquery.js重命名为jquery.min.js
.pipe(gulp.dest('js'));
//关于gulp-rename的更多强大的用法请参考https://www.npmjs.com/package/gulp-rename
});
4.3 js文件压缩
使用gulp-uglify
安装:npm install --save-dev gulp-uglify
用来压缩js文件,使用的是uglify引擎
var gulp = require('gulp'),
uglify = require("gulp-uglify");
gulp.task('minify-js', function () {
gulp.src('js/*.js') // 要压缩的js文件
.pipe(uglify()) //使用uglify进行压缩,更多配置请参考:
.pipe(gulp.dest('dist/js')); //压缩后的路径
});
4.4 css文件压缩
使用gulp-minify-css
安装:npm install --save-dev gulp-minify-css
要压缩css文件时可以使用该插件
var gulp = require('gulp'),
minifyCss = require("gulp-minify-css");
gulp.task('minify-css', function () {
gulp.src('css/*.css') // 要压缩的css文件
.pipe(minifyCss()) //压缩css
.pipe(gulp.dest('dist/css'));
});
4.5 html文件压缩
使用gulp-minify-html
安装:npm install --save-dev gulp-minify-html
用来压缩html文件
var gulp = require('gulp'),
minifyHtml = require("gulp-minify-html");
gulp.task('minify-html', function () {
gulp.src('html/*.html') // 要压缩的html文件
.pipe(minifyHtml()) //压缩
.pipe(gulp.dest('dist/html'));
});
4.6 js代码检查
使用gulp-jshint
安装:npm install --save-dev gulp-jshint
用来检查js代码
var gulp = require('gulp'),
jshint = require("gulp-jshint");
gulp.task('jsLint', function () {
gulp.src('js/*.js')
.pipe(jshint())
.pipe(jshint.reporter()); // 输出检查结果
});
4.7 文件合并
使用gulp-concat
安装:npm install --save-dev gulp-concat
用来把多个文件合并为一个文件,我们可以用它来合并js或css文件等,这样就能减少页面的http请求数了
var gulp = require('gulp'),
concat = require("gulp-concat");
gulp.task('concat', function () {
gulp.src('js/*.js') //要合并的文件
.pipe(concat('all.js')) // 合并匹配到的js文件并命名为 "all.js"
.pipe(gulp.dest('dist/js'));
});
4.8 less和sass的编译
less使用gulp-less,安装:npm install --save-dev gulp-less
var gulp = require('gulp'),
less = require("gulp-less");
gulp.task('compile-less', function () {
gulp.src('less/*.less')
.pipe(less())
.pipe(gulp.dest('dist/css'));
});
sass使用gulp-sass,安装:npm install --save-dev gulp-sass
var gulp = require('gulp'),
sass = require("gulp-sass");
gulp.task('compile-sass', function () {
gulp.src('sass/*.sass')
.pipe(sass())
.pipe(gulp.dest('dist/css'));
});
4.9 图片压缩
可以使用gulp-imagemin插件来压缩jpg、png、gif等图片。
安装:npm install --save-dev gulp-imagemin
var gulp = require('gulp');
var imagemin = require('gulp-imagemin');
var pngquant = require('imagemin-pngquant'); //png图片压缩插件
gulp.task('default', function () {
return gulp.src('src/images/*')
.pipe(imagemin({
progressive: true,
use: [pngquant()] //使用pngquant来压缩png图片
}))
.pipe(gulp.dest('dist'));
});
gulp-imagemin的使用比较复杂一点,而且它本身也有很多插件,建议去它的项目主页看看文档
4.10 自动刷新
使用gulp-livereload插件,安装:npm install --save-dev gulp-livereload
。
当代码变化时,它可以帮我们自动刷新页面
该插件最好配合谷歌浏览器来使用,且要安装livereload chrome extension扩展插件,不能下载的请自行翻墙。
var gulp = require('gulp'),
less = require('gulp-less'),
livereload = require('gulp-livereload');
gulp.task('less', function() {
gulp.src('less/*.less')
.pipe(less())
.pipe(gulp.dest('css'))
.pipe(livereload());
});
gulp.task('watch', function() {
livereload.listen(); //要在这里调用listen()方法
gulp.watch('less/*.less', ['less']);
});
如对gulp还有什么不明白之处,或者本文有什么遗漏或错误,欢迎一起交流和探讨~
js数组对象过滤:filter,find,some,every
1、filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。
原数组不变
不会对空数组进行检测
let arr1 = [1,2,3,4]
let arr2 = arr1.filter(item=>item===1)
console.log(arr1, 'arr1') // [1,2,3,4] arr1
console.log(arr2, 'arr2') // [1] arr2
let arr3 = [{
id:1,
name:'aa',
desc: 'aaaa'
},{
id:2,
name:'bb'
},{
id:3,
name: 'aa'
}]
let arr4 = arr3.filter(item=>item.name === 'aa')
console.log(arr4, 'arr4') // [{id:1,name:'aa', desc:'aaaa'},{id:3,name:'aa'}] arr4
2、find() 对于空数组,函数是不会执行的。
不会改变原数组
返回符合测试条件的第一个数组元素值
let arr5 = [1,2,1,3,4,5]
let arr6 = arr5.find(item=>item===1)
console.log(arr6, 'arr6') // 1 arr6
let arr7 = arr3.find(item=>item.name === 'aa')
console.log(arr7, 'arr7') // {id:1,name:'aa',desc:'aaaa'} arr7
3、some 用于检测数组中的元素是否满足指定条件
会依次执行数组的每个元素-如果有一个元素满足条件(即只要有条件满足即可相当于或),则表达式返回true , 剩余的元素不会再执行检测,如果没有满足条件的元素,则返回false
let someArr1 = [1,2,3,4]
let someArr2 = someArr1.some(item=>item === 1)
console.log(someArr2, 'someArr1') // true someArr1
let someArr4 = [{
id:1,
name:'bb'
},{
id:4,
name:'cc'
},{
id:1,
name:'dd'
}]
let someArr3 = someArr4.find(info=>{
return arr3.some(item=>item.id === info.id)
})
console.log(someArr3) // {id:1,name:'bb'}
4、every() 方法用于检测数组所有元素是否都符合指定条件(通过函数提供)
every() 方法使用指定函数检测数组中的所有元素-如果数组中检测到有一个元素不满足,则整个表达式返回 false ,且剩余的元素不会再进行检测,如果所有元素都满足条件,则返回 true
let everyArr = [1,2,3,4]
let everyArr2 = everyArr.every(item=>item===1)
console.log(everyArr2, 'everyArr2') //false "everyArr2"
es6数组方法find()、findIndex()与filter()的总结
lfind()
该方法主要应用于查找第一个符合条件的数组元素。它的参数是一个回调函数。在回调函数中可以写你要查找元素的条件,当条件成立为true时,返回该元素。如果没有符合条件的元素,返回值为undefined。
以下代码在myArr数组中查找元素值大于4的元素,找到后立即返回。返回的结果为查找到的元素:
const myArr=[1,2,3,4,5,6];
var v=myArr.find(value=>value>4);
console.log(v);// 5
没有符合元素,返回undefined:
const myArr=[1,2,3,4,5,6];
var v=myArr.find(value=>value>40);
console.log(v);// undefined
回调函数有三个参数。value:当前的数组元素。index:当前索引值。arr:被查找的数组。
查找索引值为4的元素:
const myArr=[1,2,3,4,5,6];
var v=myArr.find((value,index,arr)=>{
return index==4
});
console.log(v);// 5
findIndex()
findIndex()与find()的使用方法相同,只是当条件为true时findIndex()返回的是索引值,而find()返回的是元素。如果没有符合条件元素时findIndex()返回的是-1,而find()返回的是undefined。findIndex()当中的回调函数也是接收三个参数,与find()相同。
const bookArr=[
{
id:1,
bookName:"三国演义"
},
{
id:2,
bookName:"水浒传"
},
{
id:3,
bookName:"红楼梦"
},
{
id:4,
bookName:"西游记"
}
];
var i=bookArr.findIndex((value)=>value.id==4);
console.log(i);// 3
var i2=bookArr.findIndex((value)=>value.id==100);
console.log(i2);// -1
filter()
filter()与find()使用方法也相同。同样都接收三个参数。不同的地方在于返回值。filter()返回的是数组,数组内是所有满足条件的元素,而find()只返回第一个满足条件的元素。如果条件不满足,filter()返回的是一个空数组,而find()返回的是undefined
var userArr = [
{ id:1,userName:"laozhang"},
{ id:2,userName:"laowang" },
{ id:3,userName:"laoliu" },
]
console.log(userArr.filter(item=>item.id>1));
//[ { id: 2, userName: 'laowang' },{ id: 3, userName: 'laoliu' } ]
数组去重:
var myArr = [1,3,4,5,6,3,7,4];
console.log(myArr.filter((value,index,arr)=>arr.indexOf(value)===index));
//[ 1, 3, 4, 5, 6, 7 ]
js中filter用法总结
基本用法
let arr = [1, 3, 5, 8] let arrFilter = arr.filter(ele => ele > 4) console.log(arrFilter) // [5, 8]
let arrObj = [{ name: 'aa', age: 13 }, { name: 'bb', age: 23 }, { name: 'cc', age: 18 }, { name: 'dd', age: 11 }, { name: 'ee', age: 28 }] let arrObjFilter = arrObj.filter(ele => ele.age > 18) console.log(arrObjFilter) // [{name: 'bb', age: 23}, {name: 'ee', age: 28}]
进阶用法
- 数组去重(有点过时)
let arr = [1, 2, 3, 2, 3, 4] let arrFilter = arr.filter((ele, index, arr) => { return arr.indexOf(ele) === index }) console.log(arrFIlter)
let arr = [1, 2, 3, 2, 3, 4] let arrFilter = [...new Set(arr)] console.log(arrFilter)
目前比较常用的方法是使用ES6的set完成,上面
- 数组中的空字符去除
let arr = ['1', '2', '3', '', null, undefined, ' ', '4'] let arrFilter = arr.filter((ele, index, arr) => { return ele && ele.trim() }) console.log(arrFIlter)
- 高级用法
结合map使用可以先过滤出符合条件的对象然后去除某些不需要的字段,比如:
// 需求: 年龄大于18的姓名 let arrObj = [{ name: 'aa', age: 13 }, { name: 'bb', age: 23 }, { name: 'cc', age: 18 }, { name: 'dd', age: 11 }, { name: 'ee', age: 28 }] let arrObjFilter = arrObj.filter(ele => ele.age > 18).map(ele => { return ele.name }) console.log(arrObjFilter) // ['bb', 'ee']
https://juejin.cn/post/6844903551886426119
【移动端】理解viewport:
在响应式设计或移动Web开发当中经常见到的一句代码:
<meta name="viewport" content="width=device-width, initial-scale=1">
一、viewport的概念
Viewprot是用户网页的可视区域。
在默认情况下,一般来讲,移动设备上的viewport都是要大于浏览器可视区域的,这是因为考虑到移动设备的分辨率相对于桌面电脑来说都比较小,所以为了能在移动设备上正常显示那些传统的为桌面浏览器设计的网站,移动设备上的浏览器都会把自己默认的viewport设为980px或1024px(也可能是其它值,这个是由设备自己决定的),但带来的后果就是浏览器会出现横向滚动条,因为浏览器可视区域的宽度是比这个默认的viewport的宽度要小的。下图列出了一些设备上浏览器的默认viewport的宽度。
二、设备的物理像素device pixels与CSS pixels
在pc端中,css中的1px等于物理上的1px;
在移动端中,css中的1px并不等于物理上的1px,因为手机屏幕的分辨率已经越来越高,高像素但是屏幕尺寸却没有发生太大变化,那就意味着一个物理像素点实际上塞入了好几个像素。
在移动端浏览器中以及某些桌面浏览器中,window对象有一个devicePixelRatio属性,它的官方的定义为:设备物理像素和设备独立像素的比例,也就是 devicePixelRatio = 物理像素 / 独立像素。css中的px就可以看做是设备的独立像素,所以通过devicePixelRatio,我们可以知道该设备上一个css像素代表多少个物理像素。例如,在Retina屏的iphone上,devicePixelRatio的值为2,也就是说1个css像素相当于2个物理像素。但是要注意的是,devicePixelRatio在不同的浏览器中还存在些许的兼容性问题,所以我们现在还并不能完全信赖这个东西。
还有一个因素也会引起css中px的变化,那就是用户缩放。例如,当用户把页面放大一倍,那么css中1px所代表的物理像素也会增加一倍;反之把页面缩小一倍,css中1px所代表的物理像素也会减少一倍。
所以在做移动端开发时,为了使移动端的页面在不同的手机上同样的大小来显示,我们可以将页面的宽度固定,然后获取设备的宽度,可以得到我们之前设定的宽度与设备宽度的比例,再使用HTML5新增的viewport来对页面进行缩放,并固定不允许用户再重新缩放。
三、PPK的关于三个viewport的理论
1.layout viewport:是网页的所有内容的宽度,layout viewport 的宽度是大于浏览器可视区域的宽度的。layout viewport的宽度可以通过 document.documentElement.clientWidth 来获取。
2.visual viewport:代表浏览器可视区域的大小;visual viewport的宽度可以通过window.innerWidth 来获取,但在Android 2, Oprea mini 和 UC 8中无法正确获取。
3.ideal viewport:完美适配移动设备的viewport;所谓的完美适配指的是,首先不需要用户缩放和横向滚动条就能正常的查看网站的所有内容;第二,显示的文字的大小是合适,比如一段14px大小的文字,不会因为在一个高密度像素的屏幕里显示得太小而无法看清,理想的情况是这段14px的文字无论是在何种密度屏幕,何种分辨率下,显示出来的大小都是差不多的。
四、利用meta标签对viewport进行控制
我们在开发移动设备的网站时,最常见的的一个动作就是把下面这个东西复制到我们的head标签中:
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
该meta标签的作用是:
1.让当前viewport的宽度等于设备的宽度(width=device-width, initial-scale=1.0);
2.不允许用户手动缩放(user-scalable=0)。
meta viewport 标签首先是由苹果公司在其safari浏览器中引入的,目的就是解决移动设备的viewport问题。后来安卓以及各大浏览器厂商也都纷纷效仿,引入对meta viewport的支持:
width | 设置layout viewport 的宽度,为一个正整数,或字符串”width-device” |
initial-scale | 设置页面的初始缩放值,为一个数字,可以带小数 |
minimum-scale | 允许用户的最小缩放值,为一个数字,可以带小数 |
maximum-scale | 允许用户的最大缩放值,为一个数字,可以带小数 |
height | 设置layout viewport 的高度,这个属性对我们并不重要,很少使用 |
user-scalable | 是否允许用户进行缩放,值为”no”或”yes”, no 代表不允许,yes代表允许 |
五、把当前的viewport宽度设置为 ideal viewport 的宽度
要得到ideal viewport就必须把默认的layout viewport的宽度设为移动设备的屏幕宽度。因为meta viewport中的width能控制layout viewport的宽度,所以我们只需要把width设为width-device这个特殊的值就行了。
<meta name="viewport" content="width=device-width, initial-scale=1">
要把当前的viewport宽度设为ideal viewport的宽度,既可以设置 width=device-width,也可以设置 initial-scale=1,但这两者各有一个小缺陷,就是iphone、ipad以及IE 会横竖屏不分,通通以竖屏的ideal viewport宽度为准。所以,最完美的写法应该是,两者都写上去,这样就 initial-scale=1 解决了 iphone、ipad的毛病,width=device-width则解决了IE的毛病。
六、下面我们来看看移动端布局具体的代码:
<head> <title>Test Page</title> <script> var deviceWidth = parseInt(window.screen.width); //获取当前设备的屏幕宽度 var deviceScale = deviceWidth / 640; //得到当前设备屏幕与640之间的比例,之后我们就可以将网页宽度固定为640px var ua = navigator.userAgent; //获取当前设备类型(安卓或苹果) if (/Android (\d+.\d+)/.test(ua)) { var version = parseFloat(RegExp.$1); if (version > 2.3) { document.write(‘<meta name=”viewport” content=”width=640,initial-scale=’ + deviceScale + ‘, minimum-scale = ‘ + deviceScale + ‘, maximum-scale = ‘ + deviceScale + ‘, target-densitydpi=device-dpi”>’); } else { document.write(‘<meta name=”viewport” content=”width=640,initial-scale=0.75,maximum-scale=0.75,minimum-scale=0.75,target-densitydpi=device-dpi” />’); } } else { document.write(‘<meta name=”viewport” content=”width=640, user-scalable=no”>’); }
window.innerHeight属性,window.innerWidth属性
这两个属性返回网页的CSS布局占据的浏览器窗口的高度和宽度,单位为像素。很显然,当用户放大网页的时候(比如将网页从100%的大小放大为200%),这两个属性会变小。注意,这两个属性值包括滚动条的高度和宽度。
screen对象包含了显示设备的信息。
screen.height:显示设备的高度,单位为像素。
screen.width:显示设备的宽度,单位为像素。
以上两个属性,除非调整显示设备的分辨率,否则看作是常量,不会发生变化。
laravel 设置定时任务(任务调度)
创建定时任务
crontab -e #添加代码 * * * * * /usr/bin/php7.0 /var/www/html/laravel/artisan schedule:run >> /dev/null 2>&1 注意:/usr/bin/php7.0为你的php位置 ,* * * * *分别代表 分 时 日 月 周 (定时任务的时间) /var/www/html/laravel/为你的项目位置
查看定时任务
crontab -l
定义调度
在App\Console\Commands下创建Test.php
<?php namespace App\Console\Commands; use Illuminate\Console\Command; use Illuminate\Foundation\Inspiring; use Log; class Test extends Command { protected $name = 'test';//命令名称 protected $description = '测试'; // 命令描述,没什么用 /** * Execute the console command. * * @return mixed */ public function handle() { log::info('test'); // 功能代码写到这里 } }
编辑 app/Console/Kernel.php
文件,将新生成的类进行注册:
<?php namespace App\Console; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; class Kernel extends ConsoleKernel { /** * The Artisan commands provided by your application. * * @var array */ protected $commands = [ \App\Console\Commands\Test::class, ]; /** * Define the application's command schedule. * * @param \Illuminate\Console\Scheduling\Schedule $schedule * @return void */ protected function schedule(Schedule $schedule) { $schedule->command('test')//Test.php中的name ->everyFiveMinutes();//每五分钟执行一次 } }
PS:如果有多个定时任务,只需要参照test.php再次生成一个,Kernel.php中的$commands数组中再添加新加的类,schedule中$schedule->command(‘新name’)->everyFiveMinutes();即可
常用:
->cron('* * * * *'); 在自定义Cron调度上运行任务 ->everyMinute(); 每分钟运行一次任务 ->everyFiveMinutes(); 每五分钟运行一次任务 ->everyTenMinutes(); 每十分钟运行一次任务 ->everyThirtyMinutes(); 每三十分钟运行一次任务 ->hourly(); 每小时运行一次任务 ->daily(); 每天凌晨零点运行任务 ->dailyAt('13:00'); 每天13:00运行任务 ->twiceDaily(1, 13); 每天1:00 & 13:00运行任务 ->weekly(); 每周运行一次任务 ->monthly(); 每月运行一次任务 ->monthlyOn(4, '15:00'); 每月4号15:00运行一次任务 ->quarterly(); 每个季度运行一次 ->yearly(); 每年运行一次 ->timezone('America/New_York'); 设置时区
K面试
https://blog.csdn.net/weixin_47821281/article/details/115672118?utm_medium=distribute.pc_feed_category.none-task-blog-hot-3.nonecase&dist_request_id=&depth_1-utm_source=distribute.pc_feed_category.none-task-blog-hot-3.nonecase&spm=1000.2115.3001.4128
移动端touch、click、tap的区别
一、click 与tap比较
click与tap都会出发点击事件,但是在手机web端,click会有200-300ms延迟,所以一般用tap(轻击)代替click作为点击事件。singleTap 和 doubleTap分别代表单击和双击。
二、使用tap会出现点透事件(事件穿透)
很多用过Zepto(移动端开发的库)都说使用tap会出现点透事件。
1、什么是tap事件穿透
执行完上层绑定的tap事件后,下层如果绑定着click事件或者本身就存在点击事件(a/input)也会默认触发,这就是点透事件,
2、出现点透事件的原因
首先要知道tap事件是通过监听绑定document上的touch事件来模拟的,并且tap 事件是冒泡到document上才出发的;
touchstart:在这个dom上用手触摸就能开始
click:在这个dom上用手触摸,且手指未曾移动,且在这个dom上手指离开屏幕,且触摸和离开时间很短(有的浏览器可能不检测时间间隔,照样可以出发click),才开始触发。
也就是说在移动端的事件触发从早到晚排序:touchstart touchstop click。所以click的触发是有延时的,大约300ms,所以click触发的原则是,触发当前有click的元素,且该元素面朝用户的最前端,才会触发click。
3、解决办法
(1)github上有一个叫做fastclick的库https://github.com/ftlabs/fastclick
在dom ready时初始化在body上,如:
$(function(){ newFastClick(document.body); })
然后给需要“无延迟点击”的元素绑定click事件即可。实战开发中,click响应速度比tap还要快一点。
(2)为元素绑定touchend事件,并在内部加上e.preventDefault();
$A.on('touchend',function(e){//而touchend是原生的事件,在dom本身上就会被捕获触发 $demo.hide() e.preventDefault(); })
监听touchend事件,阻止A元素的touchend的默认行为,从而阻止click事件的产生.
三、touch事件
主要有
touchstart事件:当手机触摸屏幕时触发,即使已经有一个手指放在屏幕上也会触发。
touchmove事件:当手指在屏幕上华东法人时候连续地触发。在这个事件发生期间,调用preventDefault()事件可以阻止滚动。
touchend事件:当手指从屏幕上离开的时候触发。
touchcancel事件:当系统停止跟踪触摸的触发。关于这个实际的确切触发时间。
每个触摸事件都包括了三个触摸列表:
1.touches:当前位于屏幕上的所有手指的一个列表。
2.targetTouches:位于当前DOM元素上的手指的一个列表。
3.changedTouches:涉及当前事件的手指的一个列表。
例如,在一个touchend事件中,这就会是移开的手指。
这些列表由包含了触摸信息的对象组成:
1.identifier:一个数值,唯一标识触摸会话(touchsession)中的当前手指。
2.target:DOM元素,是动作所针对的目标。
3.客户/页面/屏幕坐标:动作在屏幕上发生的位置。
4.半径坐标和rotationAngle:画出大约相当于手指形状的椭圆形。
================================================================
问题1:移动端click事件会延迟响应
Solution: 移动端使用zeptojs中的tap事件来替换click
问题2: tap事件有点透的问题存在
点透:点击上一层按钮的事件,会触发下一层的点击事件或者链接
Solution: 因为tap事件的实现原理就是通过touch事件。因此,我们可以通过touch事件的结合来模拟点击事件。
Element.on('touchstart', function() {})
.on('touchmove', function() {})
.on('touchend', function() {})
上述方式类似于mousedown + mousemove + mouseup的结合使用。当然可以直接使用touchend,但是touchend 的问题在与如果从其他区域滑动到绑定事件的元素上,然后离开,也会触发touchend,因此这里存在用户体验很奇怪的问题,所以要根据需求结合使用。
问题3: 有的时候,一个页面需要在移动端和pc端同时使用,但是不能同时绑定click与touchend,因为这样会在移动端触发两次。
Solution: 通过判断是否存在于移动端,分别绑定相应的事件
// 点击之后执行什么内容
function foo(event){};
// 判断浏览器是否是移动端
function isMobile() {
return navigator.userAgent.match(/(iphone|ipad|ipod|ios|android|mobile|blackberry|iemobile|mqqbrowser|juc|fennec|wosbrowser|browserng|Webos|symbian|windows phone)/i);
}
//封装一个tap事件
function tap(ele, callback) {
var tag = 0;
if (isMobile()) {
$(ele).on('tuochstart', function(event) {
event.preventDefault();
tag = 0;
// 如果移动了,则不算tap,这一点和click处理有一些区别
}).on('touchmove', function() {
tag = 0;
}).on('touchend', function(event) {
if (tag == 0) {
callback(event);
}
})
} else {
$(ele).on('click', function(event) {
callback(event);
})
}
}
js中hasOwnProperty方法和in运算符区别
hasOwnPropert方法
hasOwnPropert()方法返回值是一个布尔值,指示对象自身属性中是否具有指定的属性,因此这个方法会忽略掉那些从原型链上继承到的属性。
看下面的例子:
Object.prototype.foo = ‘animal’;
let obj = {
name: ‘xiaoming’,
age: ’12’
}
console.log(obj.hasOwnProperty(‘foo’)) // false
console.log(obj.hasOwnProperty(‘name’)) // true
可以看到,如果在函数原型上定义一个变量foo,hasOwnProperty方法会直接忽略掉。
in运算符
在MDN中是这么定义它的 :
如果指定的属性在指定的对象或其原型链中,则in 运算符返回true。
还是用上面的例子来演示:
console.log(‘foo’ in obj) // true
1
可以看到in运算符会检查它或者其原型链是否包含具有指定名称的属性。
在对象遍历中的应用
in运算符在对一个js对象进行遍历的时候会经常涉及到,比方说我如果想遍历上面obj这个对象的说有属性,一般会这么做:
for(let key in obj) {
console.log(key);
}
// name
// age
// foo
这么对对象进行遍历有的同学可能已经看出了问题,没错,我们通常希望遍历的是某个对象的自身属性,而不要去查找它的原型链上定义的属性。如何做呢?
一般我们会利用hasOwnProperty方法这样做:
for(let key in obj) {
if(obj.hasOwnProperty(key)) {
console.log(key);
}
}
// name
// age
上面的用法确实可以解决对象遍历过程中遇到的问题,但不是最优解,还可以这么解决。
for(let key of Object.keys(obj)) {
console.log(key);
}
// name
// age
Object.keys()方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用 for…in 循环遍历该对象时返回的顺序一致 。
js中如何判断一个变量是数组还是对象
在javascript中当使用typeof方法来判断[]和{}时,返回值都是’object’,那么如何来判断一个变量是数组还是对象呢?这里记录两种方法:
利用toString()方法
Object.prototype.toString.call({}) // [object Object]
Object.prototype.toString.call([]) // [object Array]
利用Array.isArray()方法
Array.isArray([]) // true
Array.isArray({}) // false
利用第一种方法去判断兼容性更好一些,假如不存在Array.isArray()方法可以这么创建该方法:
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === ‘[object Array]’;
};
}
es6箭头函数中return的用法,只有一行语句,可以省略大括号,并且省略return关键
- 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用
return
关键字返回 -
const foo = (a, b) => { a+b; } foo(1, 2) // undefined const foo1 = (a, b) => { return a+b; } foo1(1, 2) // 3
如果箭头函数只有一行语句,可以省略大括号,并且省略
return
关键字。 -
const foo = (a, b) => a+b // 等价于const foo = (a, b) => { return a+b } foo(1, 2) // 3
这里的
foo = (a, b) => a+b
相当于foo = (a, b) => { return a+b }
- 上面的用法可以用来简化回调函数,看下面的例子:
-
// 正常函数写法 [1,2,3].map(function (x) { return x * x; }); // 箭头函数写法 [1,2,3].map(x => x * x);
es6之扩展运算符 三个点(…)
let bar = { a: 1, b: 2 }; let baz = { ...bar }; // { a: 1, b: 2 }
上述方法实际上等价于:
let bar = { a: 1, b: 2 }; let baz = Object.assign({}, bar); // { a: 1, b: 2 }
内部的同名属性会被覆盖掉
let bar = {a: 1, b: 2}; let baz = {...bar, ...{a:2, b: 4}}; // {a: 2, b: 4}
let obj1 = { a: 1, b: 2}; let obj2 = { ...obj1, b: '2-edited'}; console.log(obj1); // {a: 1, b: 2} console.log(obj2); // {a: 1, b: "2-edited"}
扩展运算符同样可以运用在对数组的操作中。
- 可以将数组转换为参数序列
-
function add(x, y) { return x + y; } const numbers = [4, 38]; add(...numbers) // 42
- 可以复制数组
-
const arr1 = [1, 2]; const arr2 = [...arr1];
- 扩展运算符可以与解构赋值结合起来,用于生成数组
const [first, ...rest] = [1, 2, 3, 4, 5]; first // 1 rest // [2, 3, 4, 5]
字符串转为真正的数组
[...'hello'] // [ "h", "e", "l", "l", "o" ]
些数据结构转为数组 等价
es5
中的Array.prototype.slice.call(arguments)
写法 -
// arguments对象 function foo() { const args = [...arguments]; }
- 扩展运算符可以与解构赋值结合起来,用于生成数组