正则表达式笔记

极客时间教程 php正则文档 转义序列(反斜线)符号表例如\n等

PHP正则语法

/foo bar/mode

分隔符可以是任意非字母数字、非反斜线、非空白字符。 静默忽略合法分隔符之前的空白字符。一般用/分割

方括号外的元字符

元字符描述
\一般用于转义字符
^断言目标的开始位置(或在多行模式下是行首)
$断言目标的结束位置(或在多行模式下是行尾)
.匹配除换行符外的任何字符(默认)
[开始字符类定义
]结束字符类定义
|开始一个可选分支
(子组的开始标记
)子组的结束标记
?作为量词,表示 0 次或 1 次匹配。位于量词后面用于改变量词的贪婪特性。 (查阅量词)
*量词,0 次或多次匹配
+量词,1 次或多次匹配
{自定义量词开始标记
}自定义量词结束标记
方括号外

方括号内的元字符(字符类

元字符描述
\转义字符
^仅在作为第一个字符(方括号内)时,表明字符类取反
标记字符范围
方括号内

模式修复符号

 模式修饰符中的空格,换行符会被忽略,其他字符会导致错误。

i (PCRE_CASELESS) 字母会进行大小写不敏感匹配。

m (PCRE_MULTILINE) 多行匹配,如果目标字符串 中没有 “\n” 字符,或者模式中没有出现 ^ 或 $,设置这个修饰符不产生任何影响。

s (PCRE_DOTALL)如果设置了这个修饰符,模式中的点号元字符匹配所有字符,包含换行符。如果没有这个 修饰符,点号不匹配换行符。这个修饰符等同于 perl 中的/s修饰符。 一个取反字符类比如 [^a] 总是匹配换行符,而不依赖于这个修饰符的设置。

x (PCRE_EXTENDED)如果设置了这个修饰符,模式中的没有经过转义的或不在字符类中的空白数据字符总会被忽略, 并且位于一个未转义的字符类外部的#字符和下一个换行符之间的字符也被忽略。 这个修饰符 等同于 perl 中的 /x 修饰符,使被编译模式中可以包含注释。 注意:这仅用于数据字符。 空白字符 还是不能在模式的特殊字符序列中出现,比如序列 (?( 引入了一个条件子组(译注: 这种语法定义的 特殊字符序列中如果出现空白字符会导致编译错误。 比如(?(就会导致错误)。

A (PCRE_ANCHORED)如果设置了这个修饰符,模式被强制为”锚定”模式,也就是说约束匹配使其仅从 目标字符串的开始位置搜索。这个效果同样可以使用适当的模式构造出来,并且 这也是 perl 种实现这种模式的唯一途径。

D (PCRE_DOLLAR_ENDONLY)如果这个修饰符被设置,模式中的元字符美元符号仅仅匹配目标字符串的末尾。如果这个修饰符 没有设置,当字符串以一个换行符结尾时, 美元符号还会匹配该换行符(但不会匹配之前的任何换行符)。 如果设置了修饰符m,这个修饰符被忽略.

S当一个模式需要多次使用的时候,为了得到匹配速度的提升,值得花费一些时间 对其进行一些额外的分析。如果设置了这个修饰符,这个额外的分析就会执行。当前, 这种对一个模式的分析仅仅适用于非锚定模式的匹配(即没有单独的固定开始字符)。

U (PCRE_UNGREEDY)这个修饰符逆转了量词的”贪婪”模式。 使量词默认为非贪婪的,通过量词后紧跟? 的方式可以使其成为贪婪的。这和 perl 是不兼容的。 它同样可以使用 模式内修饰符设置 (?U)进行设置, 或者在量词后以问号标记其非贪婪(比如.*?)。

注意:

在非贪婪模式,通常不能匹配超过 pcre.backtrack_limit 的字符。

X (PCRE_EXTRA)这个修饰符打开了 PCRE 与 perl 不兼容的附件功能。模式中的任意反斜线后就 ingen 一个 没有特殊含义的字符都会导致一个错误,以此保留这些字符以保证向后兼容性。 默认情况下,在 perl 中,反斜线紧跟一个没有特殊含义的字符被认为是该字符的原文。 当前没有其他特性由这个修饰符控制。

J (PCRE_INFO_JCHANGED)内部选项设置(?J)修改本地的PCRE_DUPNAMES选项。允许子组重名, (译注:只能通过内部选项设置,外部的 /J 设置会产生错误。) 自 PHP 7.2.0 起,也能支持 J 修饰符。

u (PCRE_UTF8)此修正符打开一个与 Perl 不兼容的附加功能。 模式和目标字符串都被认为是 UTF-8 的。 无效的目标字符串会导致 preg_* 函数什么都匹配不到; 无效的模式字符串会导致 E_WARNING 级别的错误。 5 字节和 6 字节的 UTF-8 字符序列以无效字符序列对待。

正则表达式中的常用术语

1) grep

最初是 ED 编辑器中的一条命令,用来显示文件中特定的内容。后来成为一个独立的工具 grep。

2) egrep

grep 虽然不断地更新升级,但仍然无法跟上技术的脚步。为此,贝尔实验室写出了 egrep,意为“扩展的 grep”。这大大增强了正则表达式的能力。

3) POSIX(Portable Operating System Interface of UNIX)

可移植操作系统接口。在 grep 发展的同时,其他一些开发人员也根据自己的喜好开发出了具有独特风格的版本。但问题也随之而来,有的程序支持某个元字符,而有的程序则不支持。因此,就有了POSIX。POSIX 是一系列标准,确保了操作系统之间的移植性。不过 POSIX 和 SQL 一样,没有成为最终的标准而只能作为一个参考。

4) Perl(Practical Extraction and Reporting Language)

实际抽取与汇报语言。1987 年,Larry Wall 发布了 Perl。在随后的 7 年时间里,从 Perl1 到现在的 Perl5,最终成为了 POSIX 之后的另一个标准。

5) PCRE

Perl 的成功,让其他的开发人员在某种程度上要兼容”Perl”,包括 C/C++、Java、Python 等都有自己的正则表达式。1997 年,Philip Hazel 开发了 PCRE 库,这是兼容 Perl 正则表达式的一套正则引擎,其他开发人员可以将 PCRE 整合到自己的语言中,为用户提供丰富的正则功能。许多软件都使用 PCRE,PHP 正是其中的一员。

PHP 有两套函数库支持的正则表达式处理操作:

  • 一套是由 PCRE(Perl Compatible Regular Expression)库提供、与 Perl 语言兼容的正则表达式函数,以preg_xxx为前缀函数
  • 另一套是 POSIX(Portable Operating System Interface)扩展语法正则表达式函数,以ereg_xxxx(mb扩展)为前缀的函数

Javascript中正则表达式是对象 JS正则表达式指南

区别

js正则:var patrn=/^[0-9]{1,20}$/;

php正则:$pattern='/(\d)(\d)/';

java正则:String pattern = “(\\D*)(\\d+)(.*)”;

java没有分解符,java中的转义字符是\\

JS和PHP和JAVA的正则表达式的区别规则大致一样,注意细微处的差别

常用正则表达式

php判断一个字符是否是汉字

参考

PHP正则表达式,看这一篇就够了

JS和PHP和JAVA的正则表达式的区别

数据迁移使用规范

目的,为了使命名更具有语义化和可读性,方便维护

以laravel框架为例,按实际操作行为划分

  • 添加 删除 修改(主要参考迁移功能,提供的api方法) 对应名称create drop modify
  • 2种以上的操作行为,都算修改
  • 给单一操作增加后缀,字段 column,索引 index,视图view, 存储过程 ,函数
  • 命名重复情况增加版本号 v1.0.0 …

操作表

表字段,标注注释,表索引

创建表

create_xxxx_table.php

删除表

drop_xxxx_table.php

修改表

包含添加删除字段或索引或其他 ,2种以上行为

modify_xxxx_table.php

操作字段

字段名称,字段数据类型,字段注释

添加字段

create_xxxx_table_column.php

删除字段

drop_xxxx_table_column.php

修改字段

包含添加和删除和修改字段,2种以上行为

modify_xxxx_table_column.php

操作索引

操作普通索引,全文索引,空间索引等;

索引优化是业务开发中,修改频率很高的行为,所以需要单独列出来,有很多场景需要,单独的变更索引,而不修改字段

添加索引

create_xxxx_table_index.php

删除索引

drop_xxxx_table_index.php

修改索引

包含添加 删除 修改 和自定义 2种以上行为时

modify_xxxx_table_index.php

单一操作行为的规则,参考上文以此类推。

多次重复行为时增加语义化版本后缀

需要改进

比如第一次业务变更 增加了字段 create_users_table_column.php

第二次业务变更,又要对users表增加字段,则命名为 create_users_table_column_v1.0.0.php

laravel8.x以上 可以使用压缩迁移

// 转储当前数据库架构并删除所有现有迁移。。。
php artisan schema:dump --prune

Guzzle使用经验总结

中文文档 英文文档

调用接口

主要功能,很多sdk都是使用该类库开发

写爬虫抓取页面

Laravel 下使用 Guzzle 编写多线程爬虫实战

项目中应用案例

java的古籍PC网站,该项目无人维护,无法提供书籍数据的接口。分析页面结构和接口使用guzzle库爬取书籍数据,完成数据对接。

在所用请求中共享cookie功能 文档

//创建客户端
$this->client = new Client([
    'base_uri' => $this->config['base_uri'],
    'timeout'  => 20.0,
    'cookies' => true, //共享cookie会话
);
//登录
protected function login()
{
    $response = $this->client->request('POST', 'XXX', [
            'headers' => [
                'Accept' => 'application/json'
            ],
            'form_params' => [
                'loginName' => $this->config['login_name'],
                'loginPassword' => $this->config['login_password']
            ]
        ]);

        $json = json_decode($response->getBody(), true);

        if (isset($json['operateMsg']) && $json['operateMsg'] !== '登录成功!') {
            throw new GujiException('原古籍系统账号故障');
        }
}

//请求接口数据
protected function request(string $pathUrl, array $param)
{
        $this->login(); //首先登录获取Cookies
        $response = $this->client->request('POST', $pathUrl, [
            'headers' => [
                'Accept' => 'application/json'
            ],
            'form_params' => $param
        ]);

        $contents = $response->getBody()->getContents();
        $json = json_decode($contents, true);

        if (json_last_error() === JSON_ERROR_NONE) {
            return $json;
        } elseif (json_last_error() == 10) {
            //解决json_decode错误Single unpaired UTF-16 surrogate in unicode escape
            $contents = \preg_replace('/(?<!\\\)\\\u[a-f0-9]{4}/iu', '', $contents);
            $json = \json_decode($contents, true);

            if (json_last_error() !== JSON_ERROR_NONE) {
                $json = $this->customJsonDecode($contents);
            }

            return $json;
        }
        {
            throw new GujiException("请求古籍系统接口失败");
        }
}

//抓取页面数据
protected function capture(string $pathUrl, array $param = [])
{
        $this->login(); //首先登录获取Cookies
        $response = $this->client->request('GET', $pathUrl, $param);

        if ($response->getStatusCode() == 200) {
            //获取页面内容
            return $response->getBody()->getContents();
        } else {
            throw new GujiException("古籍系统故障");
        }
}

跟随重定向

https://docs.guzzlephp.org/en/stable/faq.html#how-can-i-track-redirected-requests

https://docs.guzzlephp.org/en/stable/request-options.html#allow-redirects

调用非知名第三方支付系统,前后端分离架构,前端重定向到接口,接口调用第三方支付接口,成功后跟随响应到成功页面

use Illuminate\Support\Facades\Http;
use Psr\Http\Message\UriInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

//Http::withOptions laravel对guzzle的封装,详情看文档
$response = Http::withOptions([
            'allow_redirects' => [
                'max'             => 1,
                'on_redirect'     => function (
                    RequestInterface $request,
                    ResponseInterface $response,
                    UriInterface $uri
                ) use ($data) {

                    //自动跟随重定向响应
                    header('Location:' . $uri);
                },
            ]
        ])->asForm()->post($this->config['base_uri'] . '/multipay/h5.do', $data);

如何编写高质量代码

极客时间课程导图

代码质量的评价词

灵活性(flexibility)、可扩展性(extensibility)、可维护性(maintainability)、可读性(readability)、可理解性(understandability)、易修改性(changeability)、可复用(reusability)、可测试性(testability)、模块化(modularity)、高内聚低耦合(high cohesion loose coupling)、高效(high effciency)、高性能(high performance)、安全性(security)、兼容性(compatibility)、易用性(usability)、整洁(clean)、清晰(clarity)、简单(simple)、直接(straightforward)、少即是多(less code is more)、文档详尽(well-documented)、分层清晰(well-layered)、正确性(correctness、bug free)、健壮性(robustness)、鲁棒性(robustness)、可用性(reliability)、可伸缩性(scalability)、稳定性(stability)、优雅(elegant)、好(good)、坏(bad)

面向对象

面向对象和面向过程是编程范式(方法),不局限于编程语言,即使语言不是面向对象的语言。面向过程语言的特点是不支持丰富的面向对象编程特性(继承,多台,封装)。

面向过程风格的代码被组织成了一组方法集合及其数据结构(struct User),方法和数据结构的定义是分开的

面向对象风格的代码被组织成一组类,方法和数据结构被绑定一起,定义在类中。

面向对象编程语言更适用于大规模复杂应用。二进制指令,汇编语言,面向过程语言,是一种计算机思维方式,面向对象是一种人类思维方式,需要对业务建模,将现实的世界的事物,映射为类或对象,将开发者聚焦业务本身,而不是思考如何和机器打交道。

面向过程风格代码转换面向对象风格,规律总结

函数等于类方法,复用的变量等于类型属性

并不是代码封装成类,就是面向对象编程

滥用geter,setter,违反了封装特性,举例:将购物车列表list可以随意修改,不应该将业务逻辑暴露给上层代码,要封装到方法中。让上层无法随意修改list

封装、抽象、继承、多态分别可以解决哪些编程问题

封装:通过访问控制语法提供数据访问保护,隐藏内部数据,外部仅能通过类有限的结果访问修改内部数据。调用者无需关心业务的细节,调用就可以了。

抽象:基于interfaceabstract语法,更好的实现封装的方法,隐藏实现细节,是封装的具体实现。

继承:获取父类的方法和属性,复用代码,使用时要避免过度继承和高度耦合

多态:基于,父类可以引用子类,支持继承语法,子类可以重写(override)父类中的方法的语法机制实现多态特性。可以基于interface类实现,提高代码扩展性和复用性

面向对象语言的的设计流程,类分析->类设计->实现

思考如何给业务建模,如何将需求翻译为类,如何给类之间建立交互关系,而完成这些工作完全不需要考虑错综复杂的处理流程。

有了类的设计之后,然后再像搭积木一样,按照处理流程,将类组装起来形成整个程序。这种开发模式、思考问题的方式,能让我们在应对复杂程序开发的时候,思路更加清晰

面向对象编程比面向过程编程,更加容易应对大规模复杂程序的开发。但像 Unix、Linux 这些复杂的系统,也都是基于 C 语言这种面向过程的编程语言开发的,你怎么看待这个现象?

取决于需求是业务复杂度,还是技术复杂度

1.面向过程的编程语言不代表不能实现面向对象思想 2.操作系统的复杂相对于业务系统的快速开发迭代是另一个维度复杂,基础系统更看重性能和稳定,而业务系统看重的是维护,复用,拓展。3.操作系统是要频繁跟硬件打交道的,“低级”的语言更快更简洁

接口和抽象类,基于接口而非实现编程

组合优于继承

贫血模型和充血模型

设计模式

随着编程语言的演进,一些设计模式(比如 Singleton)也随之过时,甚至成了反模式,一些则被内置在编程语言中(比如 Iterator),另外还有一些新的模式诞生(比如 Monostate)。

反面模式 明显出现但又低效或是有待优化的设计模式,是用来解决问题的带有共同性的不良方法

PHP设计模式全集 github源码

单例模式 在应用程序调用的时候,只能获得一个对象实例

场景:数据库连接 日志 在应用中锁定文件 线程池(threadpool)、缓存(cache)、对话框、处理偏好设置和注册表(register)对象

laravel中服务容器注册单例类 simps-swoole使用单例trait来实现单例

管道设计范式

Laravel 中的 Pipeline (管道) — 管道设计范式

在 PHP 中管道( Pipeline (管道) ) 能帮我们做什么?

Laravel Database——数据库服务的启动与连接

Laravel用到的设计模式有哪些?

超全的设计模式简介(45 种)

UML统一建模语言

30分钟学会uml类图 uml类图符合说明图

泛化、实现、关联、聚合、组合、依赖

laravel request dd 结果

uml类图符号的作用,以laravel框架的request类 dd($request)结果为示例,“+”表示 public ,“-”表示 private, “#”表示 protected ,不带符号表示 default

其它

库与框架的区别

参考教程

极客时间教程 设计模式之美 软件设计之美 软件工程之美

图说设计模式

Composer使用经验总结

切换国内镜像 composer 加速扩展包—— hirak/prestissimo 中文文档

–ignore-platform-reqs:  忽略 phphhvmlib-* 和 ext-* 要求并强制安装,就算本地环境不完全要求。平台配置选项可见 platform 文档

举例说明,windows平台的php不支持pcntl扩展,安装laravel-horizon时需要用的该扩展,可以通过指令强制安装

–with-all-dependencies:(-W)

 添加所有白名单中的依赖到白名单,包括那些根依赖,

允许升级、降级和删除目前锁定在特定版本的软件包。

composer require packagename:*

不确定当前项目可以使用扩展的哪个版本的时,可以使用require xxx:* 来计算是否有任何版本可以安装

laravel5.5安装horizon示例

依赖冲突

比如package A 和 B依赖相同的package C,但是版本不同,需要通过github 或packagelist 的composer.json 找到A和B依赖的最低版本C然后卸载C 然后安装适合版本的A和B,这样会自动安装最适合版本C

laravel 如何覆盖composer的 vendor类文件? 适用于需要修改vendor代码的场景

项目和框架运行缓慢

基于composer构建的项目出现过,加载运行缓慢的问题。碰到过旧laravel项目运行php artisan -v 时,命令显示非常慢。运行php artisan server 访问页面响应也很慢。

具体原因未验证,可能是安装依赖时,和运行项目时的php版本不一致,使用低版本安装了依赖,切换了高版本的php。composer安装项目后,会在vendor/bin目录下生成命令文件和缓存之类的,

解决方法,将vendor目录完全删除,然后重新运行composer install ,项目在升级或切换composer版本和php版本之后保险起见需要重新安装依赖

引入本地包

laravel-devstart 本地包的名称,path指定目录 要用基于当前项目路的相对路径,适用于开发扩展包

//设置路径,本质是创建了一个系统软连接
composer config repositories.laravel-devstart path ../../package/laravel-devstart
//引入本地包的master分支
composer require yangliuan/laravel-devstart:dev-master

扩展包开发

安装 Package Builder 包结构创建工具
composer global require overtrue/package-builder 
安装项目需要的依赖

修改composer.json

编写异常

编写单元测试

测试扩展包

发布版本到github

发布第一个版本

自动化测试

使用 GitHub Actions 做自动化测试

使用 StyleCI 自动修复代码格式

参考教程

LX2 PHP 扩展包实战教程 – 从入门到发布

PHP回顾之创建自己的扩展包 概述

扩展包版本对应

Laravel旧版本和扩展包对应版本,方便维护旧项目时参考

Laravel版本扩展包版本
5.5“laravel/horizon”: “2.1”
5.5“barryvdh/laravel-ide-helper”: “2.4.1”
5.5“doctrine/dbal”: “^2.10”
5.5“laravel/passport”: “~4.0”
5.5“laravel/tinker”: “~1.0”
5.5“propaganistas/laravel-phone”: “^4.2”
5.5“barryvdh/laravel-debugbar”: “3.4”,
5.5“filp/whoops”: “~2.0”
5.5“friendsofphp/php-cs-fixer”: “^2.14” 配置文件为.php_cs.dist

PHP 开发工具扩展包 使用笔记

前置知识

PHP代码风格指南

PSR 是 PHP Standard Recommendations (PHP 推荐标准)的简写,由 PHP FIG 组织制定的 PHP 规范,是 PHP 开发的实践标准。中文文档 例如composer 遵循psr4标准

很多php框架和组件都遵循psr规范,因此要编写遵循行业规范的代码

PHP-CS-Fixer

推荐在项目中安装配置,因为不同的项目php版本和规范不同,依赖的php-cs-fixer的版本也不相同

安装

composer require --dev friendsofphp/php-cs-fixer

项目根目录创建 .php-cs-fixer.php 配置文件(旧版本为.php_cs) 并且将它纳入git版本管理,保证该项目的所有开发人员都使用相同的规范

将缓存文件.php-cs-fixer.cache 添加到.gitignore文件中 (旧版本为.php_cs.cache)

规则和配置文件

可用内置规则地址 Laravel框架规则集地址

配置文件生成工具

点击问号,查看配置工具使用说明

通过规则风格筛选 比如php版本,psr12等

选中需要的规则

点击导出按钮生成规则配置文件

如何使用

手动使用方式 文档

#指定规则运行
$ php php-cs-fixer.phar fix /path/to/project --rules=@PSR12

#框架项目中指定配置文件手动修复,以laravel为例,在项目根目录下执行
$ vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php
#执行结果如下
Loaded config default from ".php-cs-fixer.php".
   1) app/Http/Controllers/Admin/CategoryController.php
   2) app/Http/Controllers/Admin/HotKeywordsController.php
  ... 此处省略中间部分输出信息
 205) routes/api.php
 206) tests/Feature/CommonApiTest.php
Fixed all files in 2.135 seconds, 20.000 MB memory used

php-cs-fixer vscode插件 github

扩展>>安装 ,个人配置项

{
   "php-cs-fixer.executablePath": "${extensionPath}/php-cs-fixer.phar",
   "php-cs-fixer.onsave": true,
   "[php]": {
        "editor.defaultFormatter": "junstyle.php-cs-fixer"
   },
}

PHPStan

付费替代品vscode 插件 PHP Intelephense 支持代码自动提示,跳转追踪和语法检测 99RMB永久授权

laravel版本 Larastan github 也是基于phpstan开发的

Laravel-ide-helper

安装

composer require --dev barryvdh/laravel-ide-helper

如果需要为模型生成字段,需要安装如下扩展包,参考文章是laravel5.5版本 跟laravel-ide-helper版本有关

laravel-ide-helper”: “^2.10″版本测试会自动安装doctrine/dbal 扩展包

composer require doctrine/dbal

生成配置文件 config目录下会生成ide-helper.php

php artisan vendor:publish --provider="Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider" --tag=config

为Facades生成文档提示,会在项目根目录下生成_ide_helper.php,可以根据团队需要决定是否将该文件加入.gitignore

php artisan ide-helper:generate

为模型生成注释,要在创建完成模型文件和数据迁移文件运行之后,若数据库字段有注释,则模型字段也会有注释

#不加命令参数给所有模型生成注释
php artisan ide-helper:models
#包含指定模型
php artisan ide-helper:models "App\Models\ExcelDemo"
#忽略指定模型
php artisan ide-helper:models --ignore="App\Models\BaseModel"
#会提示是否覆盖原模型,不覆盖会在根目录下生成_ide_helper_models.php文件,推荐no选项
 Do you want to overwrite the existing model files? Choose no to write to _ide_helper_models.php instead (yes/no) [no]:
 > 

使用 Travis-CI 做自动化测试

使用 StyleCI 自动修复代码格式

编辑器相关插件如下

Linux文件系统 FAQ

rm cannot remove xxx Read-only file system 无法删除文件

df dir 查看无法删除的目录所在的文件系统

df /snap/remmina/
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/nvme0n1p2 479152840 195515620 259227828 43% /

fsck -y /   #执行修复文件系统 的挂载根目录 或者 有问题的文件所在的挂载目录;

shutdown -r now

再删除ubuntu系统中的snap应用文件时上述方法没有解决

使用df -h 查看 发现remmina中的 /snap/remmina/4978 的挂载点是/dev/loop12

sudo umount /dev/loop12 去掉挂载后,再删除就可以了 ,原因是磁盘的挂载目录无法直接被删除

laravel广播使用笔记,基于laravel-websocket扩展包

阅读文档

Laravel 广播系统文档

laravel-websockets文档

安装需要的用的依赖

//websocket服务端
composer require beyondcode/laravel-websockets

//发布数据迁移文件
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="migrations"

//发布配置文件
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="config"

//安装队列管理面板 或者 使用php artisan queue:work也可以
composer require laravel/horizon
php artisan horizon:install

//执行迁移
php artisan migrate

//前端依赖
npm install laravel-echo pusher-js

修改配置

.env 配置

//广播驱动设置为pusher
BROADCAST_DRIVER=pusher

//队列驱动改为redis
QUEUE_CONNECTION=redis

//使用laravel-websockets做后端,pusher配置随便填写
PUSHER_APP_ID=yangliuan
PUSHER_APP_KEY=yangliuan
PUSHER_APP_SECRET=yangliuan
//注意一定要注释这行,否则laravel-websockets不生效
#PUSHER_APP_CLUSTER=mt1

//websocket端口号
LARAVEL_WEBSOCKETS_PORT=6001

config/app.php 配置

取消如下Provider的注释

/*
 * Application Service Providers...
 */
...
// App\Providers\BroadcastServiceProvider::class,

config/broadcasting.php 配置

按如下修改

'connections' => [

        'pusher' => [
            'driver' => 'pusher',
            'key' => env('PUSHER_APP_KEY'),
            'secret' => env('PUSHER_APP_SECRET'),
            'app_id' => env('PUSHER_APP_ID'),
            'options' => [
                'cluster' => env('PUSHER_APP_CLUSTER'),
                 //本地开发关闭安全连接
                'useTLS' => false,
                //本地host配置
                'host' => '127.0.0.1',
                //端口
                'port' => env('LARAVEL_WEBSOCKETS_PORT', 6001),
                //协议
                'scheme' => 'http',
            ],
        ],

config/websockets.php

'apps' => [
        [
            'id' => env('PUSHER_APP_ID'),
            'name' => env('APP_NAME'),
            'key' => env('PUSHER_APP_KEY'),
            'secret' => env('PUSHER_APP_SECRET'),
            'path' => env('PUSHER_APP_PATH'),
            'capacity' => null,
            //是否开启客户端发送消息
            'enable_client_messages' => false,
            //是否开启统计
            'enable_statistics' => true,
        ],
    ],

测试案例场景

使用laravel excel 队列导出文件后,自动提示并下载,使用公共频道 demo地址

后端代码

注册频道

route/channels.php 文件,可以自定义频道名称

Broadcast::channel('excel', function () {
    return true;
});

创建ExcelExportCompletedEvent事件

php artisan make:event ExcelExportCompletedEvent

<?php
use Illuminate\Broadcasting\Channel;//公共频道
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel; //存在频道可以加入和离开,需要授权
use Illuminate\Broadcasting\PrivateChannel; //私有频道,需要授权
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

//注意事件一定要实现ShouldBroadcast接口
class ExcelExportCompletedEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    //public属性会自动转换为广播数据
    //自定义广播数据  文档 https://learnku.com/docs/laravel/9.x/broadcasting/12223#b2f5d1
   
    public $excel_path;

    public $disk;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(string $excel_path, string $disk)
    {    
        //文件路径
        $this->excel_path = $excel_path;
        //磁盘
        $this->disk = $disk;
    }

    /**
     * 监听频道
    *  监听自己的定义频道名称
     * 
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new Channel('excel');
    }
}

创建导出文件 ExcelDemoPictureQueryExport 细节略过详情看 laravel excel文档

创建通知队列,用于触发时间

php artisan make:job ExcelNotifyJob

class ExcelNotifyJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $attach;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(array $attach)
    {
        $this->attach = $attach;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        //发送广播通知
        ExcelExportCompletedEvent::dispatch($this->attach['file_name'], $this->attach['disk']);
    }
}

控制器代码

/**
 * 字段导出图片 使用队列 并接受广播通知
 *
 * @param Request $request
 * @return void
 */
public function queueImages(Request $request)
{
  $file_name = 'excel-demo-'.date('YmdHis').\mt_rand(100000, 999999).'.xlsx';
  $disk = 'public';
  Excel::queue(new ExcelDemoPictureQueryExport(), $file_name, $disk)
  //导出成功后,使用任务链调用excel通知job
  //DOC:https://learnku.com/docs/laravel/8.5/queues/10395#dispatching-jobs
  //DOC:https://docs.laravel-excel.com/3.1/exports/queued.html#appending-jobs
  ->chain([
      new ExcelNotifyJob(compact('file_name', 'disk'))
  ]);

  return response()->json();
}

//下载文件方法
public function store(Request $request)
{
    $request->validate([
       'storage_path' => 'bail|required|string',
       'disk' => 'bail|nullable|string',
    ]);
    $realPath = Storage::disk($request->input('disk') ?? 'public')
       ->path($request->input('storage_path'));

    return response()->download($realPath)->deleteFileAfterSend();
}

前端代码

使用 Laravel Jetstream Inertia.js  构建

封装laravel-echo.js

import Echo from 'laravel-echo';

window.pusher = require('pusher-js');

const echo = new Echo({
    broadcaster: 'pusher',
    key: 'yangliuan',
    wsHost: window.location.hostname,
    wsPort: 6001,
    forceTLS: false,
    enabledTransports: ['ws', 'wss'],
})

export { echo }

Excel.vue

<template>
 <app-layout title="Dashboard">
  ...
  <a href="#" @click="queueImagesClick">
  <div class="mt-3 flex items-center text-sm font-semibold text-indigo-700">
  <div>队列导出图片并用广播接受通知</div>
  <div class="ml-1 text-indigo-500">
  <svg viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
 </div>
 </div>
 </a>
 </app-layout>
</template>

<script>
 ...
 //导入laravel echo 
 import { echo } from '@/laravel-echo'

 export default defineComponent({
    components: {
       AppLayout,
       JetApplicationLogo,
       Link
    },
    created() {
      //监听公共频道excel,响应下载excel文件
       echo.channel('excel')
           .listen('ExcelExportCompletedEvent', (e) => {
               alert('ExcelExportCompletedEvent')
               this.downloadExcel(e.excel_path,e.disk)
               console.log(e);
           })
    },
    methods: {
       //下载方法
       downloadExcel(excel_path,disk) {
          const download_url =  '/api/files/download?storage_path=' + excel_path + '&disk=' + disk
          window.open(download_url)
       },
      //点击时间调用队列导航图片接口
       queueImagesClick() {
          axios.post('/api/excel/export/queue-images').then( response => {
             console.log(response)
          })
       }
  }
})
</script>
//开启websocket服务
php artisan websockets:serve

//开启horizon队列
php artisan horizon

演示

授权频道案例

nginx代理websocket

laravel websocket文档

websocket单独使用一个域名不和http接口共享域名,配置如下

server {
    listen 80;
    server_name websocket.exhibition.demo;

    location / {
        proxy_pass             http://127.0.0.1:6001;
        proxy_read_timeout     60;
        proxy_connect_timeout  60;
        proxy_redirect         off;

        # Allow the use of websockets
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

第二种方式websocket和http接口公用一个域名

map $http_upgrade $type {
  default "";
  websocket "ws";
}

server {
    listen 80;
    server_name api2.exhibition.demo;
    root /home/yangliuan/Code/Php/business-logic/laravel-exhibition/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    index index.php;
    charset utf-8;

    location = /favicon.ico {
        access_log off; log_not_found off;
    }

    location = /robots.txt {
        access_log off; log_not_found off;
    }
   
   //http
    location /  {
        try_files $uri $uri/ /index.php?$query_string;
    }
 
   //websocket
    location @ws  {
        proxy_pass             http://127.0.0.1:6001;
        proxy_set_header Host  $host;
        proxy_read_timeout     60;
        proxy_connect_timeout  60;
        proxy_redirect         off;

        # Allow the use of websockets
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    location ~ [^/]\.php(/|$) {
        fastcgi_pass unix:/dev/shm/php-cgi.sock;
        fastcgi_index index.php;
        include fastcgi.conf;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

参考

websocket实现手机扫码登陆