用php做一个九行菱形,PHP MVC框架【Myphp】的编写

1、什么是MVC

MVC(Model-View-Controller)是软件工程的一种软件架构模式。

在MVC模式设计下,软件系统被分来三个模块:模型(Model)、视图(VIew)、控制器(Controller)。

PHP下的MVC模式又称为Web MVC,自上世纪70年代进化而来。

使用MVC模式的目的是:实现一种动态的程序设计,便于后续对程序的修改和拓展,且使得程序的某一部分的重复利用成为可能。

MVC各模块的职能:

模型Model:管理大部分的业务逻辑和所有的数据库逻辑。模型抽象简化了连接和操作数据库的操作。

控制器Controller:负责响应用户请求、准备数据,决定如何展示数据。

视图View:负责数据渲染,通过HTML方式呈现给用户。

3d211080f7496e2696f332845c9b1446.gif

一个典型的Web MVC 处理流程:

Controller接受到用户发来的请求;

Controller调用Model完成对状态的读写操作;

Controller把数据传递给View;

View渲染出HTML页面并展示给用户。

2、为什么要自己开发MVC框架

为了做以MVC模式开发的各类CMS的代码审计。

3、准备工作

3.1 开发环境准备

建站软件:phpstudy2018

IDE:phpstorm2018.1

php版本:5.4.45-nts

Apache&MySQL

3.2 目录准备

我给该Web MVC框架取名为:MyPhp

该项目目录为:MyPhpFrame1

整个项目的目录结构如下:

MyPhpFrame1 web框架部署根目录

├─application      应用目录

│ ├─controllers    控制器目录

│ ├─models      模块目录

│ └─views       视图目录

├─config        配置文件目录

├─myphp       框架核心目录

├─runtime 运行临时目录

├─static静态文件目录

├─.htaccess Apache目录配置

└─index.php 入口文件

MyPhpFrame1位于Apache站点根目录之下。通过访问 http://localhost/MyPhpFrame1 ,可以访问到该项目。

3.3 重定向

?3.2展示的目录中.htaccess文件,是Apache服务器的目录级别的分布式配置文件,可以针对特定目录改变Apache配置。

.htaccess 可以帮我们实现:重写URL、网页301重定向、自定义404错误页面、改变文件扩展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能。

Apache服务器通过启用AllowOverride All实现对应目录下的配置可重写。

本框架下.htaccess文件内容为:

?

#打开Rerite功能

RewriteEngine On

# 如果请求的是真实存在的文件f或目录d,直接访问

RewriteCond %{REQUEST_FILENAME} !-f

RewriteCond %{REQUEST_FILENAME} !-d

#重定向所有请求到index.php?url=原路径

RewriteRule ^(.*)$ index.php?url=$1 [PT,L]

#[PT] passthrough,使得RewriteRule的结果重写加入到URL的匹配中

#[L] last,使得mod_rewrite 停止处理规则集

这里使用该.htaccess的原因是:

1、 静态文件可以直接访问,比如css文件、js文件都可以直接访问。

(如果是非index.php的php文件,可以访问,不过由于框架特性,类之间需要extends,可能会报错。如果是目录,也可以访问,如果apache开启了目录列表,则可以看到index of目录,否则返回403。)

2、 程序有了单一的入口,就是index.php。

? 当请求地址不是真实存在的文件或目录时,请求就会传给index.php。

例如,访问地址:http://localhost/MyPhpFrame1/item/index,文件系统中并不存在这样的文件或目录。则Apache会把重写这个地址为:http://localhost/MyPhpFrame1/index.php?url=item/index。这样在php中用$_GET['url']就可以拿到 item/index了。

3.4 代码规范

代码规范如下:

MySQL的表名:使用小写字母与下划线(_)命名,如:item、bus_info

Model模块名:使用大驼峰法(首字母大写),并在名称后加上Model,如:ItemModel、BusModel

Controller控制器名:使用大驼峰法(首字母大写),并在名称后加上Controller,如:ItemController、BusController

Action方法名:使用小驼峰法(首字母小写),如:index、selectAll

View视图 部署结构为:控制器名/行为名,如:item/index.php、item/manage.php

使用代码规范的目的:使得程序能更好地相互调用。

4、PHP MVC核心框架

4.1 入口文件

index.php为整个项目的入口文件,位于项目根目录/下。

文件内容为:

//初始化常量

define('APP_PATH',__DIR__.'/');//网站根目录

define('CONFIG_PATH',APP_PATH.'config/');//网站配置目录

define('APP_DEBUG',false);//开启调试模式

define('APP_URL','http://localhost/MyPhpFrame1/');//网站URL

define('RUNTIME_PATH',APP_PATH.'runtime/');//网站临时目录//加载配置文件

require CONFIG_PATH.'/config.php';//加载框架核心文件

require APP_PATH.'myphp/MyPhp.php';//实例化框架类,并执行run()方法

$myphp=newMyphp();$myphp->run();

?

可以看到,上面的php代码并没有使用php结束符 ?>。

纯php代码中php结束符是可选的,提倡不写php结束符。如果这个是一个被别人require的php文件,没有这个结束符,可以避免多余输出(也就是?>之后的任何数据,包括空格、换行符等)导致header, setcookie, session_start函数执行的失败(这几个函数执行前,不允许展示任何数据)。

4.2 配置文件

config.php是项目的配置文件。位于config/目录下。

config.php的作用是:定义数据库连接参数,配置默认控制器名和默认动作名。

config.php文件内容为:

define('DB_NAME','myphpdb');define('DB_USER','root');define('DB_PASSWORD','root');define('DB_HOST','localhost');//默认控制器名和默认方法名

define('DEFAULT_CONTROLLER','Item');define('DEFAULT_ACTION','index');

4.3 框架核心类

MyPhp.php是MyPhp框架的核心类文件。位于myphp/目录下。

?在入口文件中,对框架类做了两步操作:实例化、调用run()方法。

run()方法调用了框架类自身方法,完成以下操作:

类自动重载

环境设置

清理转义字符

移除全局变量

处理路由

MyPhp.php文件内容为:

* MyPhp核心框架类*/

//初始化常量

defined('APP_PATH') or define('APP_PATH',__DIR__.'\');defined('APP_URL')or define('APP_URL','http://localhost/MyPhpFrame1');defined('APP_DEBUG') or define('APP_DEBUG',false);defined('CONFIG_PATH') or define('CONFIG_PATH',APP_PATH.'config\');defined('RUNTIME_PATH') or define('RUNTIME_PATH',APP_PATH.'runtime/');defined('DEFAULT_CONTROLLER') or define('DEFAULT_CONTROLLER','Item');defined('DEFAULT_ACTION') or define('DEFAULT_ACTION','index');classMyPhp

{/**

*运行程序*/

functionrun()

{

spl_autoload_register(array($this,'loadClass'));//spl_autoload_register — 注册给定的函数作为 __autoload 的实现

//__autoload — 尝试加载未定义的类。当我们实例化一个未定义的类时,就会触发此函数

$this->setReporting();$this->removeMagicQuotes();$this->unregisterGlobals();$this->Route();

}/**

*路由处理

*abc.com/controllerName/actionName/queryString

* eg:

* 访问url:localhost/item/show/name/1

* 进入到route方法后,分割url,获得:

* $controller:item

* action:show

* QueryString:array(name,1)

* 然后,实例化一个新控制器:itemController,并调用itemController->show()方法*/

functionRoute()

{$controllerName=DEFAULT_CONTROLLER;$actionName=DEFAULT_ACTION;if(!empty($_GET['url']))

{$url=$_GET['url'];//http://localhost/$urlArray=explode('/',$url);//explode 把字符串打散为数组

//获取控制器名

$controllerName=ucfirst($urlArray[0]); //ucfirst 首字母转换为大写

//获取动作名

array_shift($urlArray);//array_shift 删除数组中的第一个元素,并返回被删除元素的值

$actionName=empty($urlArray[0])?$actionName:$urlArray[0];//获取URL参数

array_shift($urlArray);$queryString=empty($urlArray[0])?array():$urlArray;

}//url数据为空时

$queryString=empty($queryString)?array():$queryString;//判断控制器、方法 是否存在

$controller=$controllerName.'Controller';if(!class_exists($controller))//class_exists — 检查类是否已定义

{exit($controller.'控制器不存在');

}elseif(!method_exists($controller,$actionName))

{exit($actionName.'方法不存在');

}//实例化控制器,因为控制器对象里面

//还会用到控制器名和操作名,所以实

//例化的时候把他们俩的名称也传入。查看Controller基类就明白。

$dispatch=new $controller($controllerName,$actionName);//$dispatch保存控制器实例化后的对象,我们就可以调用它的方法,也可以向方法中传入参数

//call_user_func_array 调用回调函数,并把一个数组参数作为回调函数的参数

//以下等同于:$dispatch->$action($queryString)

call_user_func_array(array($dispatch,$actionName),$queryString);

}/** 设置开发环境

**/

functionsetReporting()

{if(APP_DEBUG===true)

{error_reporting(E_ALL); //报告所有错误

ini_set('display_errors','On');//ini_set 设置指定配置选项的值。这个选项会在脚本运行时保持新的值,并在脚本结束时恢复。

}else{error_reporting(E_ALL);ini_set('display_errors','Off');ini_set('log_errors','On');ini_set('error_log',RUNTIME_PATH.'logs/error.log');

}

}/** 删除多余的反斜杠*/

function stripSlashesDeep($value)

{$value=is_array($value)?array_map('stripSlashesDeep',$value):stripslashes($value);//递归调用

// stripslashes — 返回一个去除转义反斜线后的字符串(' 转换为 ' 等等)。双反斜线(\)被转换为单个反斜线()

//array_map — 为数组的每个元素应用回调函数

return $value;

}/** 检测转义后的字符并清除反斜杠*/

functionremoveMagicQuotes()

{//get_magic_quotes_gpc 获得php配置magic_quotes_gpc的bool值

//如果开启magic_quotes_gpc,则对GET、POST、COOKIE 数据自动运行addslashes()

//addslashes 在预定义字符之前添加反斜杠。预定义字符:单引号',双引号",反斜杠,NULL

//magic_quotes_gpc特性已自 PHP 5.3.0 起废弃并将自 PHP 5.4.0 起移除。所以在5.4版本以后php配置文件是找不到魔术引号的配置信息的

//PHP 5.4之后,get_magic_quotes_gpc统一返回false

if(get_magic_quotes_gpc())

{$_GET=$this->stripSlashesDeep($_GET);$_POST=$this->stripSlashesDeep($_POST);$_COOKIE=$this->stripSlashesDeep($_COOKIE);$_SESSION=$this->stripSlashesDeep($_SESSION);

}

}/** 检测自定义全局变量(register globals)并移除,模拟register_globals=Off*/

functionunregisterGlobals()

{/** register_globals的意思就是注册为全局变量,5.4之后已被弃用。当register_globals=On时,

* 局部变量的在脚本的全局域也可用(eg:$_GET['a']也将以$a的形式存在)

* 这样写是不好的实现,会影响代码中的其他变量*/

if(ini_get('register_globals'))

{$array=array('_SESSION','_POST','_GET','_COOKIE','_REQUEST','_SERVER','_ENV','_FILES');foreach ($array as $value){echo $value;foreach($GLOBALS[$value]as $key=>$var)//处理每个内置数组中每个键值对

{if($var===$GLOBALS[$key]){//如果变量值等于全局变量中对应同名的变量值

unset($GLOBALS[$key]);//销毁对应的全局变量

}

}

}

}

}/** 自动加载控制器和模型类*/

static function loadClass($class)

{

//echo '执行loadClass('.$class.')
';$frameworks=__DIR__ . '\'.$class.'.class.php';$controllers=APP_PATH.'application\controllers\'.$class.'.php';$models=APP_PATH.'application\models\'.$class.'.php';

//echo $frameworks.'
';

//echo $controllers.'
';

//echo $models.'
';if(file_exists($frameworks)){//加载核心框架类

//echo '开始加载 框架核心类:'.$frameworks.'
';include $frameworks;

//echo '成功加载 框架核心类:'.$frameworks.'
';

}elseif (file_exists($controllers))

{

//echo '开始加载 应用控制器类:'.$controllers.'
';//加载应用控制器类else

include $controllers;

//echo '成功加载 应用控制器类:'.$controllers.'
';

}elseif (file_exists($models))

{

//echo '开始加载 应用模型类:'.$models.'
';//加载应用模型类

include $models;

//echo '成功加载 应用模型类:'.$models.'
';

}else{//加载失败代码

exit('加载核心类文件失败!');

}

//echo 'loadClass('.$class.')结束
';

}

}

讲解2个方法:loadClass()、route()

localClass()作用是:加载未定义的类时,导入对应的类文件。

首先构造对应类的可能的文件路径:如果对应类是核心框架类,则类文件路径应该为$frameworks;如果对应类是应用控制器类,则类文件路径应该为$controllers;如果对应类是应用模型类,则类文件路径应该为$models。

接着,对每个可能存在类文件路径,进行file_exists判定,存在则include。

则无本框架下任意类都可以完成自动加载。

?route()作用是:通过url,解析出控制器名、方法名和url参数,然后实例化对应的控制器,执行对应的方法,并传入对应的url参数。

假设浏览器访问的URL为:yourhost.com/controllerName/actionName/queryString

首先,Apache会根据.htaccess重写URL,重写后的URL为:yourhost.com/index.php?url=controllerName/actionName/queryString

route()从全局变量$_GET['url']中获得字符串 controllerName/actionName/queryString

然后,route()会将字符串转换为数组,通过对数组的操作获得3部分:controllerName、actionName、queryString。

最后,route()会实例化对应控制器,并调用对应方法。

例如,URL链接为:yourhost.com/item/manage/6,经过route()处理后:???

$controllerName为:Item

$actionName为:manage

$urlArray为:array(6)

?处理完成后,route()会实例化控制器ItemController,并调用它的manage(array(6))

4.4 控制器Controller基类

接下来,就是在myphp框架中创建MVC基类,包括控制器、模型、视图三个基类。

在myphp/目录下,新建一个控制器基类,文件名为Controller.class.php,主要功能就是对整个程序进行调度,文件内容为:

* 控制器基类*/

classController

{protected $_controller; //控制器名

protected $_action; //动作名

protected $_view; //视图对象

//构造函数:初始化属性,并实例化对应视图模型

function __construct($controller,$action)

{$this->_controller=$controller;$this->_action=$action;$this->_view=new View($controller,$action);

}//分配变量

//Controller 类用assign()方法实现把变量保存到View对象中。

//这样,应用Controller调用父类Controller的 $this->render()后,视图文件就可以显示这些变量。

function assign($name,$value)

{$this->_view->assign($name,$value);

}//渲染视图

functionrender()

{//TODO: Implement __destruct() method.

$this->_view->render();

}

}

Controller类通过 assign()方法 实现了变量从Controller对象到VIew对象的传递(VIew类的assign就是将数据保存到自己数组中)。

这样,Controller类在调用$this->render()后,视图对象就可以渲染展示这些变量了。

4.5 模型Model基类

在myphp/目录下,新建一个模型基类,文件名为Model.class.php,文件内容为:

* 模型基类*/

class Model extendsSql

{protected $_model;protected $_table;function__construct()

{//连接数据库

$this->connect(DB_HOST,DB_USER,DB_PASSWORD,DB_NAME);//获取模型类名称

$this->_model=get_class($this);$this->_model=rtrim($this->_model,'Model');//rtrim 从字符串右侧移指定字符

//模型类名称与数据库中的表名一致

$this->_table=strtolower($this->_model);

}function__destruct()

{//TODO: Implement __destruct() method.

}

}

可以看到,model基类继承了Sql类。

因为数据操作比较复杂,所以我为这部分操作单独创建了一个Sql类。

在myphp/目录下,新建一个Sql类,文件名为Sql.class.php,文件内容为:

* 数据库操作类*/

classSql

{protected $_dbHandle;protected $_result;//连接数据库

public function connect($host,$user,$pass,$dbname)

{try{$dsn=sprintf("mysql:host=%s;dbname=%s;charset=utf8",$host,$dbname);//sprintf 把百分号(%)符号替换成一个作为参数进行传递的变量:

$options=array(PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC);//PDO::FETCH_ASSOC:返回一个索引为结果集列名的数组

$this->_dbHandle=new PDO($dsn,$user,$pass,$options);

}catch(PDOException $e)

{exit('错误:'.$e->getMessage());

}

}//查询所有数据

public functionselectAll()

{$sql=sprintf("select * from `%s`",$this->_table);$sth=$this->_dbHandle->prepare($sql);$sth->execute();return $sth->fetchAll();

}//根据条件(id)查询

public function select($id)

{$sql=sprintf("select * from `%s` where `id`='%s'",$this->_table,$id);$sth=$this->_dbHandle->prepare($sql);$sth->execute();return $sth->fetch();

}//根据条件(id)删除

public function delete($id)

{$sql=sprintf("delete from `%s` where `id`='%s'",$this->_table,$id);$sth=$this->_dbHandle->prepare();$sth->execute();return $sth->rowCount();

}//自定义sql查询语句,返回影响的行数

public function query($sql)

{$sth=$this->_dbHandle->prepare($sql);$sth->execute($sql);return $sth->rowCount();

}//新增数据

public function add($data)

{$sql=sprintf("insert into `%s` %s",$this->_table,$this->formatInsert($data));return $this->query($sql);

}//修改数据

public function update($id,$data)

{$sql=sprintf("update `%s` set %s where `id`='%s'",$this->_table,$this->formatUpdate($data),$id);return $this->query($sql);

}//将数组转换为insert语句中的数据格式

/*$array=array("id"=>1,"name"=>"jack","age"=>19);

formatInsert($array)返回字符串:

(`id`,`name`,`age`) values ('1','jack','19')*/

private function formatInsert($data)

{$fields=array();$values=array();foreach($data as $key=>$value)

{$fields[]=sprintf("`%s`",$key);$values[]=sprintf("'%s'",$value);

}$filed=implode(',',$fields);//implode 把数组元素组合为字符串:

$value=implode(',',$values);return sprintf("(%s) values (%s)",$filed,$value);

}//将数组转换为update语句中的数据格式

/*$array=array("name"=>"jack","age"=>19);

formatUpdate($array)返回字符串:

`name`='1',`jack`='19'*/

private function formatUpdate($data)

{$fields=array();foreach ($data as $key=>$value)

{$fields[]=sprintf("`%s`='%s'",$key,$value);

}return implode(',',$fields);

}

}

4.6 视图View基类

在myphp/目录下,新建一个视图基类,文件名为View.class.php,文件内容为:

* 视图基类*/

classView

{protected $variables=array();protected $_controller;protected $_action;function __construct($controller,$action)

{$this->_controller=$controller;$this->_action=$action;

}//导入变量

function assign($name,$value)

{$this->variables[$name]=$value;

}//渲染显示

functionrender()

{extract($this->variables);//extract - 用来将一个数组分解成多个变量直接使用。

$defaultHeader=APP_PATH.'application/views/header.php';$defaultFooter=APP_PATH.'application/views/footer.php';$controllerHeader=APP_PATH.'application/views/'.$this->_controller.'/header.php';$controllerFooter=APP_PATH.'application/views/'.$this->_controller.'/footer.php';//页头文件

if(file_exists($controllerHeader))

{include ($controllerHeader);

}else{include ($defaultHeader);

}//页内容文件

include (APP_PATH.'application/views/'.$this->_controller.'/'.$this->_action.'.php');//页脚文件

if(file_exists($controllerFooter))

{include ($controllerFooter);

}else{include ($defaultFooter);

}

}

}

至此,核心的PHP MVC框架核心就搭建完成了。

下面,我要编写基于框架的应用代码来测试这个框架的功能。

5、基于框架的应用

5.1 部署数据库

在SQL中新建一个数据库 myphpdb,增加一个item表,并插入表中2个记录,SQL命令如下:

CREATE DATABASE `myphpdb` DEFAULT CHARACTER SETutf8 COLLATE utf8_general_ci;USE`myphpdb`;CREATE TABLE`item`(

`id`int(11) NOT NULLauto_increment,

`item_name`varchar(255) NOT NULL,PRIMARY KEY(`id`)

)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;INSERT INTO `item` VALUES(1,'Hello World.');INSERT INTO `item` VALUES(2,'Let's go!');

5.2 部署模型

在application/models/目录下,创建一个ItemModel.php文件,主要功能是增加了检索数据的业务逻辑,文件内容为:

* 用户Model*/class ItemModel extends Model

{/**

* 自定义当前模型操作的数据库表名称

* 如果不指定,则默认为类名称的小写字符串,

* 此处为item 表

**/

public $_table='item';/**

* 搜索功能,以为sql父类中,并没有现成的like搜索

* 所以需要自己写sql语句,对数据库的操作应该都放

* 在Model中,然后提供给Controller直接调用*/

public functionsearch($keyword)

{

$sql=sprintf("select * from `%s` where `item_name` like '%%%s%%'",$this->_table,$keyword);

$sth=$this->_dbHandle->prepare($sql);

$sth->execute();return $sth->fetchAll();

}}

因为 Item 模型继承了 Model基类,所以它拥有 Model 基类的所有功能。

5.3 部署控制器

在application/controllers/目录下,创建一个ItemController.php文件,主要功能是准备数据、调用对应的视图,文件内容为:

* Item控制器类*/class ItemController extends Controller

{//首页文件,测试myphp框架自定义的sql查询public function index()

{

$keyword=isset($_GET['keyword'])?$_GET['keyword']:'';if($keyword)

{

$items=(new ItemModel())->search($keyword);

}else{

$items=(new ItemModel())->selectAll();

}//传入视图数据

$this->assign('title','全部条目');

$this->assign('keyword',$keyword);

$this->assign('items',$items);//渲染试图

$this->render();

}//添加记录,测试myphp框架的sql查询-create

public function add()

{

$data['item_name']=$_POST['value'];

$count=(new ItemModel)->add($data);

$this->assign('title','添加成功');

$this->assign('count',$count);//渲染试图

$this->render();

}//操作管理public function manage($id=null)

{

$item=array();

$postUrl=APP_URL.'/item/add';if($id)

{

$item=(new ItemModel)->select($id);

$postUrl=APP_URL.'/item/update';

}

$this->assign('title','管理条目');

$this->assign('item',$item);

$this->assign('postUrl',$postUrl);//渲染试图

$this->render();

}//更新记录,测试框架的sql查询-update

public function update()

{

$data=array('id'=>$_POST['id'],'item_name'=>$_POST['value']);

$count=(new ItemModel)->update($data['id'],$data);

$this->assign('title','修改成功');

$this->assign('count',$count);//渲染试图

$this->render();

}//删除记录,测试框架的sql查询-delete

public function delete($id=null)

{

$count=(new ItemModel)->delete($id);

$this->assign('title','删除成功');

$this->assign('count',$count);//渲染试图

$this->render();

}

}

5.3 部署视图

在 application/views/目录下新建 header.php 和 footer.php 两个页头页脚模板文件,文件内容为:

header.php 内容:

<?php echo $title;?>

footer.php 内容:

页头文件使用了main.css文件,内容:

html,body{margin:0;padding:10px;font-size:20px;

}input{color:black;font-family:Georgia, times;font-size:24px;font-weight:normal;line-height:1.2em;

}a{color:blue;font-family:Georgia,times;font-size:20px;font-weight:normal;line-height:1.2em;text-decoration:none;

}a:hover{text-decoration:underline;

}h1{color:#000000;font-size:41px;letter-spacing:-2px;line-height:1em;font-family:helvetica,Arial,sans-serif;border-bottom:1px dotted #cccccc;

}td{padding:1px 30px 1px 0;

}

现在,在application/view/item/目录下,创建以下几个视图文件。

index.php,作用是展示数据库中item表的所有记录、检索记录、删除记录,文件内容为:

"name="keyword">

item/manage">新建

ID内容操作
<?php echo $item['id'];?><?php echo $item['item_name'];?>

item/manage/<?php echo $item['id']; ?>">编辑

item/delete/<?php echo $item['id']; ?>">删除

manage.php,作用是编辑记录,文件内容为:

"method="POST">

">

">

item/index">返回

add.php,作用是提示 已添加记录,文件内容为:

item/index">成功添加<?php echo $count;?>条记录,点击返回

update.php,作用是提示 已修改记录,文件内容为:

item/index">成功修改<?php echo $count;?>项,点击返回

delete.php,作用是提示 已删除记录,文件内容为:

item/index">成功删除<?php echo $count;?>项,点击返回

至此,所有的应用代码已经编写完成。

6、访问应用

在浏览器中访问 http://localhost/MyPhpFrame1/? ,成功!

3e8ab7c79d85fe357d9645ff33034399.png

严重参考:

https://www.awaimai.com/128.html

https://www.cnblogs.com/Steven-shi/p/5914175.html

感谢他们的分享!!

?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值