从零开始搭建自己的Swoole框架(二)项目的规划和设计草案

项目的规划和设计草案

前言

在第(一)章中我们已经实现了 hellow world!

但是现在不用急着撸代码,

而是要把我们设计这款框架的想法捋顺。

期望效果

按照重要性给各项指标进行打分,其中星星越多代表重要性越高。

开发舒适度:★★★★★

这是最关键的一条!

使用了我们的框架,

开发者撸代码会变成愉♂悦的过程!

我给它起个口号:

享受令人愉♂悦的开发过程。 — by FireRabbit-Engine 火兔引擎

但是怎么个舒适法得有一个定义,不然就太宽泛了。

  • 能够帮助开发者快速排查错误以及调试的能力
  • 在框架层面自动帮用户解决掉麻烦问题,比如 SQL 注入、用户权限、跨域问题、表单校验等等
  • 指令式开发,开发者可以在控制台输入指令自动生成对应的文件,比如输入 make:model 就会生成一个数据库查询模型

高性能:★★★★★

我们既然选择了 swoole 扩展,

自然要体现出它应该具有的高性能特点,

高性能的指标是响应速度,期望效果是 1-30 ms。

(以接口的响应速度为判定指标)

它要非常的快!不快怎么装 X 呢!

高并发:★★★★★

同样是 swoole 的特色,

我们的框架也应该支持强大的并发连接。

框架完成后我们会用 ab 测试来查看高并发情况下的性能。

扩展性:★★★★★

人没梦想跟咸鱼有什么区别!

万一框架火了呢!?

我们这套框架应该是能让开发者 DIY 的。

框架的扩展性要非常的强,

各个模块之间尽可能的解耦。

安全性:★★★★★

手撸框架要十分注意的地方!

市面的框架基本上已经把安全隐患在框架层面解决了,

以至于我们完全忽视了本应该注意的漏洞。

就比如 SQL 注入,你用的框架封装好的 ORM 自动帮你处理了。

但是我们自己从零开始撸框架就不一样了,

我们要让这个框架使用起来非常安全。

代码规范:★★★

为了后期可维护,代码规范也是十分必要的,

毕竟老夫也不是什么大神,只能说在认知的范围内尽量吧……

如果后期觉得设计不合理,会推翻重做。

小白化:★★★

让一个没学过 swoole 的人也能上手。

(其实我自己就是现学现卖)

我们再给它起个口号:

有手就能撸。 — by FireRabbit-Engine 火兔引擎

“会增删改查吗?”

“明天可以来上班了。” —— 根据本人亲身经历改编。

半自动化:★★★

谁说做开发就一定要手撸代码的?

要我说的话,

开发的最高境界是“无码”

其实是我开发游戏的时候得到的灵感,

比如关卡的设计,每个场景都要单独写一个吗?

那如果是几千个关卡的游戏呢?

只要一个配置文件就能搞定!

而我们只需要写一个关卡解释器,

将配置的参数实例化为游戏的场景。

所以我有一个预感,以后的开发者不需要写太多的代码,

只要配置文件就可以解决大部分的问题。

在开发这个框架的时候,我会把这个思想融入到设计层面。

亲,我们这个框架高清无码。— by FireRabbit-Engine 火兔引擎

模块设计

框架是由模块构成的,

我们要把每个模块都拆分成独立的,以后还能拆分成单独的包。

入口文件

swoole 已经是单一入口了,

所以直接用就行了。

配置文件

有一个专门用来读取配置文件的模块,

除了框架的配置还支持用户自定义配置。

不同的模块有不同的配置文件,全部单独区分:

1
2
3
4
5
# 文件目录 config/
database.php // 数据库配置
cache.php // 缓存配置
sms.php // 短信配置
…… // 诸如此类

自动加载

直接使用 composer 实现自动加载即可。

遵循 psr-4 加载规范。

路由模块

需要一个路由解析器来处理 swoole 收到的请求路径,

将请求分发到控制器和对应的方法,还有处理路径上的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 用户浏览一篇博客文章
http://127.0.0.1/article/1

# swoole 收到 nginx 转发的请求是这样的
/article/1

# 这个时候我们就需要定义一个解释器解析路由
/article/1 是一个字符串,要将字符串按照某种规则匹配

# 最终解析的结果为:
/article/1 => 控制器是 article,方法是 show,参数是 1

# 实例化控制器
$name = 'ArticleController'; // 解析后得到的控制器名字
$controller = new $name;

# 执行对应的方法
$method = 'show';
$controller->$show;

以上就是路由解析器的大概原理。

真正要实现是很复杂的,因为路由是各种各样奇奇怪怪的:

1
2
3
4
5
6
7
8
9
10
11
# 文章详情
http://127.0.0.1/article/1

# 编辑文章
http://127.0.0.1/article/1/edit

# 很长很长,看得出来哪些是路径参数吗?
http://127.0.0.1/article/classify_1/list/12345/show

# 注意!下面这种是 query 参数,要获取这种参数很简单
http://127.0.0.1/article?id=1

如果我们的网站不打算使用路径参数,

那就简单得多了,但是用 query 参数很不美观。

比如 TP 框架的这种路径:

1
2
3
4
5
# c 代表控制器,a 代表方法,id 是查询参数
http://127.0.0.1?c=article&a=show&id=1

# 美化后的路由(这才是我们想要的)
http://127.0.0.1/article/1

请求和响应

这个就是面试的时候经常考的问题了,

就不再详细介绍了,

简单地说就是用户访问我们的网站,

我们要给它输出什么样的结果。

  • 请求要经过过滤器(中间件、数据/权限验证)
  • 响应返回的格式(content-type)

表单验证

对用户提交的表单字段验证。

如果每个表单都要这样:

1
2
3
4
5
6
7
8
# 用户注册
if(isset($_GET['name'] && strlen($_GET['name']) < 4 || strlen($_GET['name']) > 10) {
echo '用户名不能为空,且长度为4~10';
}

# 下面还有重名判断、邮箱、手机号码、密码长度……
# 接着,用户修改密码,找回密码,更新账户资料
# 以上,请再来一遍……

一个框架没有表单处理模块,你离升仙也就不远了。

中间件

通俗的将就是请求拦截器。

表单验证也是拦截请求,

但是在拦截的层面上不一样,

表单验证一般是在控制器里面拦截的,

而中间件是在还没进入到控制器的时候就开始拦截。

比如一个抢购活动在晚上 9 点开始:

1
2
3
4
5
6
7
8
$startTime = date('Y-m-d 21:00:00')
$currentTime = date('Y-m-d H:i:s');

if($currentTime >= $startTime ) {
# 显示活动页
} else {
# 显示活动还没开始
}

你可以在控制器里面写上这样的代码来判断活动是否开始,

但是这种硬编码绝对会让你原地爆炸,

即使你自己觉得没啥,你同事绝对会想在月黑风高的时候把你弄死……

我们为什么不这样做呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 定义一个“规则”,把它命名为:2021_12_12_active
规则 2021_12_12_active :
如果当前时间大于等于 2021-12-12 21:00:00 则返回 true,否则返回 false

# 然后再修改路由解析器:
用户访问:http://127.0.0.1/goods/1
路由解析器解析出:控制器:goods,方法:show,参数:1

它正准备实例化对象呢!
这个时候,中间件站了出来,说:路由老哥,等一哈!

路由老哥愣住了,停下来听中间件想说什么。
中间件不慌不忙的解释道:这个路径被加上了“规则”,要验证之后才可以。

而这个规则叫做:2021_12_12_active
根据《小学生手册》里规定:
第 2021_12_12_active 条:
好孩子在 2021-12-12 这一天必须在晚上九点以后上传睡觉。
在九点之前睡觉的都不是好孩子!不能让它们通过!
这件事交给我来处理,你可以退下了。

于是路由解释器就把执行权限交给了中间件。
中间件就会查询时间是否符合要求,
如果符合要求就让请求正常进行下去,
不符合要求就返回异常的结果。

中间件就是一套“规则”,你可能会问那跟表单验证有什么不同?

其实表单验证就是中间件的子集,除了表单验证之外,还有权限验证。

比如你可以规定一个请求不带 token 就不让它通过。

表单验证和权限验证虽然都属于中间件,

但我们还是会单独拆分出来,

因为他们在细节还是有些不同的,

而且我们还可以通过封装实现更加简单的操作。

表单验证和权限验证均可以在控制器里面处理,如果是在控制器层面处理的话就不叫做中间件的,为了实现复用性,我们会把表单验证跟用户验证以中间件的形式实现

日志

不论是本地调试还是线上查询问题所在,

日志都是非常重要的,日志系统可以支持数据库和文件。

我们暂时只支持文件日志。

常量

需要一个专门的类来保存框架使用的各种常量。

比如返回的错误码之类的,redis 的键等等。

如果不统一管理常量的话,后期会变得非常麻烦。

缓存

将缓存的调用封装起来,

我们自定义一个 Cache 类,

外部只要调用暴露出来的接口即可。

而 Cache 内部则可以支持不同的缓存数据库,

包括 Redis 集群的调用等等。

目前仅考虑:Redis

数据库(Model)

同缓存系统,

数据库模型直接暴露一个供外部调用的接口,

然后通过配置文件来自定义包括集群甚至是数据库类型等等配置。

数据库类型目前仅考虑:MySQL

模型文件主要是定义字段以及处理一些关联关系。

不将复杂的代码放到这里。

比较复杂的数据库逻辑可以再定义一个 Action 层来处理。

模板引擎(View)

为了支持搜索引擎的 SEO,

我们不采用前后端分离的设计,

当然也不能直接写 phtml(即 php 和 html 混合的代码)

第一次学 PHP 的时候,我们都接触过这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# index.php

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1><?php echo 'hello world';></h1>
</body>
</html>

phtml 文件把 php 的逻辑代码与 html 代码混合在一起,

如果是循环或者增加条件判断,简直不忍直视……

而使用模板引擎的情况下,我们只要传入参数,

然后模板渲染结果就可以了,

模板引擎当然也可以改成可配置的。

目前仅考虑:blade

控制器(Controller)

在我设计的这个框架里,

控制器不是用来处理业务逻辑的,

而是验证+分发请求+返回响应。

不过,验证可以完全交给中间件来处理,

这样控制器的代码就可以更加简洁。

期望效果:

1
2
3
4
5
6
7
8
public function index() {
// 获取参数
$params = $this->getParams();
// 将参数传递给 service,处理查询逻辑
$items = ArticleService::getInstance()->getList($params);
// 返回视图响应,并将参数传给视图渲染页面
$this->view('home.index', ['items' => $items]);
}

所有控制器的方法内的代码不超过 10 行。

逻辑处理(Service)

处理逻辑代码的地方,本质是一个类。

队列

按照原生开发的话,应该会使用 redis + 死循环来实现队列,

但是考虑到 swoole 自带了任务处理机制,

等我学习之后再具体考虑怎么实现。

单元测试

没有。

总结

大致就想到这么多,后期如果有需要再进行补充。

文章作者: 火烧兔子
文章链接: http://huotuyouxi.com/2021/02/06/my-swoole-framework-2/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 火兔游戏工作室