laravel详细学习

laravel详细学习

[TOC]

apache配置

1
2
3
4
5
6
7
8
9
10
11
12
13
# Virtual Hosts

#
<VirtualHost *:80>
ServerName localhost #这句是自己随意取的域名
ServerAlias localhost #服务器别名
DocumentRoot "${INSTALL_DIR}/www" #网站根目录INSTALL_DIR即为安装目录
<Directory "${INSTALL_DIR}/www/">
Options +Indexes +Includes +FollowSymLinks +MultiViews #根目录显示目录结构
AllowOverride All # 重写规则的定义.适用于所有文件的访问.
Require local
</Directory>
</VirtualHost>

介绍

laravel是基于mvc模式的php框架,m——模型层,v——视图层,c——控制器层;以下为laravel框架的目录文件,框出来的文件目录将在后续中用到

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|– app 包含Controller、Model、路由等在内的应用目录,大部分业务将在该目录下进行
|  |– Console 命令行程序目录
|  |  |– Commands 包含了用于命令行执行的类,可在该目录下自定义类
|  |  |– Kernel.php 命令调用内核文件,包含commands变量(命令清单,自定义的命令需加入到这里)和schedule方法(用于任务调度,即定时任务)
|  |– Events 事件目录
|  |– Exceptions 包含了自定义错误和异常处理类
|  |– Http HTTP传输层相关的类目录
|  |  |– Controllers 控制器目录
|  |  |– Middleware 中间件目录
|  |  |– Requests 请求类目录
|  |  |– Kernel.php 包含http中间件和路由中间件的内核文件
|  |  |– routes.php 强大的路由
|  |– Jobs 该目录下包含队列的任务类
|  |– Listeners 监听器目录
|  |– Providers 服务提供者目录
|  |– User.php 自带的模型实例,我们新建的Model默认也存储在该目录
|– bootstrap 框架启动载入目录
|  |– app.php 创建框架应用实例
|  |– autoload.php 自动加载
|  |– cache 存放框架启动缓存,web服务器需要有该目录的写入权限
|– config 各种配置文件的目录
|  |– app.php 系统级配置文件
|  |– auth.php 用户身份认证配置文件,指定好table和model就可以很方便地用身份认证功能了
|  |– broadcasting.php 事件广播配置文件
|  |– cache.php 缓存配置文件
|  |– compile.php 编译额外文件和类需要的配置文件,一般用户很少用到
|  |– database.php 数据库配置文件
|  |– filesystems.php 文件系统配置文件,这里可以配置云存储参数
|  |– mail.php 电子邮件配置文件
|  |– queue.php 消息队列配置文件
|  |– services.php 可存放第三方服务的配置信息
|  |– session.php 配置session的存储方式、生命周期等信息
|  |– view.php 模板文件配置文件,包含模板目录和编译目录等
|– database 数据库相关目录
|  |– factories 5.1以上版本的新特性,工厂类目录,也是用于数据填充
|  |  |– ModelFactory.php 在该文件可定义不同Model所需填充的数据类型
|  |– migrations 存储数据库迁移文件
|  |– seeds 存放数据填充类的目录
|     |– DatabaseSeeder.php 执行php artisan db:seed命令将会调用该类的run方法。该方法可调用执行该目录下其他Seeder类,也可调用factories方法生成ModelFactory里定义的数据模型
|– public 网站入口,应当将ip或域名指向该目录而不是根目录。可供外部访问的css、js和图片等资源皆放置于此
|  |– index.php 入口文件
|  |– .htaccess Apache服务器用该文件重写URL
|  |– web.config IIS服务器用该文件重写URL
|– resources 资源文件目录
|  |– assets 可存放包含LESS、SASS、CoffeeScript在内的原始资源文件
|  |– lang 本地化文件目录
|  |– views 视图文件就放在这啦
|– storage 存储目录。web服务器需要有该目录及所有子目录的写入权限
|  |– app 可用于存储应用程序所需的一些文件
|  |– framework 该目录下包括缓存、sessions和编译后的视图文件
|  |– logs 日志目录
|– tests 测试目录
|– vendor 该目录下包含Laravel源代码和第三方依赖包
|– .env 环境配置文件。config目录下的配置文件会使用该文件里面的参数,不同生产环境使用不同的.env文件即可。
|– artisan 强大的命令行接口,你可以在app/Console/Commands下编写自定义命令
|– composer.json 存放依赖关系的文件
|– composer.lock 锁文件,存放安装时依赖包的真实版本
|– gulpfile.js gulp(一种前端构建工具)配置文件
|– package.json gulp配置文件
|– phpspec.yml phpspec(一种PHP测试框架)配置文件
|– phpunit.xml phpunit(一种PHP测试框架)配置文件
|– server.php PHP内置的Web服务器将把这个文件作为入口。以public/index.php为入口的可以忽略掉该文件

app是应用的核心代码文件目录,以后的代码基本都在这里完成;app/Http/Controller目录是应用的控制器文件;routes.php是框架的路由文件,负责路由分配和映射;Http下的类文件,比如上面目录中的User.php、Menu.php文件是应用的模型文件;config目录是所有应用的配置文件目录;public是框架的入口文件及静态资源文件目录;resources/views则是应用的视图文件目录。

关于路由

在 Laravel 应用中,定义路由有两个入口,一个是 routes/web.php,用于处理终端用户通过 Web 浏览器直接访问的请求,另一个是 routes/api.php

get请求和post请求

1
2
3
4
5
6
7
8
9
10
11
<?php
Route::get(/test1,function (){
return 'hello';
});
Route::post(/test1,function (){
return 'hello';
});
//两种访问方式页面返回hello

Route::get('/', 'WelcomeController@index'); 访问/时调用App\Http\Controllers\WelcomeController 控制器的 index 方法

关于多路由访问有match和any方式

1
2
3
4
5
6
7
<?php
Route::match(['get','post'], 'multi', function(){
return 'multi post or get';
});
Route::any('multi', function(){
return 'multi get or post';
});

路由参数

如果你定义的路由需要传递参数,只需要在路由路径中进行标识并将其传递到闭包函数即可:

1
2
3
Route::get('user/{id}', function ($id) {
return "用户ID: " . $id;
});

这样,当你访问 http://blog.test/user/1000 的时候,就可以在浏览器看到 用户ID: 1000 字符串。

此外,你还可以定义可选的路由参数,只需要在参数后面加个 ? 标识符即可,同时你还可以为可选参数指定默认值:

1
2
3
Route::get('user/{id?}', function ($id = 1) {
return "用户ID: " . $id;
});

这样,如果不传递任何参数访问 http://blog.test/user,则会使用默认值 1 作为用户 ID。更深入的用法还可以用正则匹配

路由分组

所谓路由分组,其实就是通过 Route::group 将几个路由聚合到一起,然后给它们应用对应的共享特征:

1
2
3
4
Route::group([], function () { 
Route::get('hello', function () { return 'Hello'; });
Route::get('world', function () { return 'World'; });
});

路由别名

 给路由通过[‘as’ => ‘alias’]数组使用别名后,可通过route(‘别名’)生成url,请看代码理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

//路由别名

Route::get('student/info',['as' => 'studentInfo' ,function(){
//通过route('studentInfo')生成完成url后返回
return route('studentInfo');
}]);


访问url:http://127.0.0.1/laravel/public/student/info
页面返回:http://127.0.0.1/laravel/public/student/info
注:别名的好处在于,以后在控制器中使用route('别名')的方式生成url后,即便修改了路由的名字,也不用再修改控制器程序,因为通过别名程序能自动生成修改后的url

具体还有路由路径前缀, 子域名路由,子命名空间,路由命名前缀。可以看看laravel官方手册

中间件

简介

中间件为过滤进入应用的 HTTP 请求提供了一套便利的机制。例如,Laravel 内置了一个中间件来验证用户是否经过认证(如登录),如果用户没有经过认证,中间件会将用户重定向到登录页面,而如果用户已经经过认证,中间件就会允许请求继续往前进入下一步操作。

当然,除了认证之外,中间件还可以被用来处理很多其它任务。比如:CORS 中间件可以用于为离开站点的响应添加合适的头(跨域);日志中间件可以记录所有进入站点的请求,从而方便我们构建系统日志系统。

Laravel 框架自带了一些中间件,包括认证、CSRF 保护中间件等等。所有的中间件都位于 app/Http/Middleware 目录下。

定义中间件

这个命令会在 app/Http/Middleware 目录下创建一个新的中间件类 CheckAge,在这个中间件中,我们只允许提供的 age 大于 200 的请求才能访问应用该中间件的路由,否则,我们会将用户重定向到 / URI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

namespace App\Http\Middleware;

use Closure;

class CheckAge
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->age <= 200) {
return redirect('/');
}

return $next($request);
}
}

正如你所看到的,如果请求参数中的 age 小于等于 200,中间件会返回一个 HTTP 重定向给客户端;否则,请求会被传递下去。将请求往下传递可以通过调用回调函数 $next 并传入当前 $request

理解中间件的最好方式就是将中间件看做 HTTP 请求到达目标动作之前必须经过的“层”,每一层都会检查请求并且可以完全拒绝它。所有的中间都是在服务容器中解析,所以你可以在中间件的构造函数中类型提示任何依赖。

具体还有请求之前/之后的中间件,注册中间件,中间件参数等。

CSRF防护

简单地说,csrf就是攻击者伪装用户来攻击授信网站。

在laravel中防护csrf用的时csrf令牌,其实就是用token或者cookie来验证请求输入的 token 值和 Session 中存储的 token 是否一致,如果没有传递该字段或者传递过来的字段值和 Session 中存储的数值不一致,则会抛出异常。

控制器入门

可以通过artisan命令快速创建一个控制器

1
php artisan make:controller TakeController

该命令会在 app/Http/Controllers 目录下创建一个新的名为 TaskController.php 的文件,默认生成的控制器代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ShowController extends Controller
{
//该方法用于将 user 变量渲染到 user/profile 视图中。然后在 user 目录下新建 profile.blade.php 文件
public function show(User $user)
{
return view('user.profile', ['user' => $user]);
}
}

单一动作控制器

__invoke 方法:

例如可在上述类中定义

1
2
3
4
public function __invoke($id)
{
return view('user.profile', ['user' => User::findOrFail($id)]);
}

当你为这个单动作控制器注册路由的时候,不需要指定方法:

1
Route::get('user/{user}', \App\Http\Controllers\ShowController::class);

控制器中间件

Route::get('profile', [UserController::class, 'show'])->middleware('auth');

不过,在控制器的构造函数中设置中间件更方便,你可以使用基类提供的 middleware 方法轻松分配中间件给该控制器的动作,你甚至可以限制中间件只应用到该控制器类的指定方法:

1
2
3
4
5
6
7
class UserController extends Controller{
public function __construct(){
$this->middleware('auth'); // auth 中间件会应用到所有方法
$this->middleware('log')->only('index'); // log 中间件只会应用到 index 方法
$this->middleware('subscribed')->except('store'); // subscribed 中间件会应用到 store 之外的所有方法
}
}

资源控制器

命令创建

php artisan make:controller PostController --resource

该 Artisan 命令将会生成一个控制器文件 app/Http/Controllers/PostController.php,这个控制器包含了每一个资源操作对应的方法:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PostController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}

/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}

/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
}

/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
}

/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}

/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}

/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}

然后创建路由

1
Route::resource('post',app/Http/Controllers/PostController.php::class);

在这里插入图片描述

获取用户输入

1
2
Route::get('task/create', 'TaskController@create');
Route::post('task', 'TaskController@store');

我们可以在TaskController.php中创建create方法满足我们的功能

1
2
3
4
5
6
7
8
public function store(Request $request)
{
$task = new Task();
$task->title = $request->input('title');
$task->description = $request->input('description');
$task->save();
return redirect('task'); // 重定向到 GET task 路由
}

后面还可以用门面来获取input.

请求

访问请求实例

在控制器中获取当前 HTTP 请求实例,需要在构造函数或方法中对 Illuminate\Http\Request 类进行依赖注入,这样当前请求实例会被服务容器自动注入:

在这里插入图片描述

依赖注入 & 路由参数

如果还期望在控制器方法中获取路由参数,只需要将路由参数置于其它依赖之后即可,例如,如果你的路由定义如下:

1
2
3
use App\Http\Controllers\UserController;

Route::put('user/{id}', [UserController::class, 'update']);

仍然可以对 Illuminate\Http\Request 进行依赖注入并通过如下方式定义控制器方法来访问路由参数 id

1
2
3
4
public function update(Request $request, $id)
{
//
}

还可以在路由闭包中注入 Illuminate\Http\Request,在执行闭包函数的时候服务容器会自动注入输入请求:

1
2
3
4
5
use Illuminate\Http\Request;

Route::get('/', function (Request $request) {
//
});

获取请求路径

1
2
3
4
$path = $request->path(); //如果请求URL是 http://blog.test/user/1,则 path 方法将会返回 user/1
if($request->is('user/*')){
//如果请求URL是 http://blog.test/user/1,该方法会返回 true。
}

获取请求 URL

获取请求方法

获取请求输入

文件上传

上述在文档中都比较简单易懂直接看即可

响应

这块主要就是Response 对象,其他没有什么特别的

返回一个完整的 Response 实例允许你自定义响应的 HTTP 状态码和头信息。Response 实例继承自 Symfony\Component\HttpFoundation\Response 基类,该类提供了一系列方法用于创建 HTTP 响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Route::get('cookie/response', function () {
return response('Hello World', 200)
->header('Content-Type', 'text/plain'); //添加响应头
});

return response($content) //使用 withHeaders 方法来指定头信息数组添加到响应:
->withHeaders([
'Content-Type' => $type,
'X-Header-One' => 'Header Value',
'X-Header-Two' => 'Header Value',
]);

return response($content) //添加 Cookie 到响应
->header('Content-Type', $type)
->cookie('name', 'value', $minutes);

利用辅助函数redirect重定向
Route::get('dashboard', function () {
return redirect('home/dashboard');
});

还有一些响应,视图响应,文件响应等

关于视图

这个比较没什么特别的东西,跳过。

关于核心类Kernel类

vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php
是laravel处理网络请求的最核心类,在app容器准备好了之后,就会调用本类,之后所有的处理都在此类中。

具体参考:https://www.136.la/php/show-9877.html

https://www.136.la/php/show-6124.html

https://www.136.la/php/show-16856.html

https://blog.csdn.net/w13707470416/article/details/84979186?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-1&spm=1001.2101.3001.4242

https://blog.csdn.net/weixin_33916256/article/details/89063048?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&spm=1001.2101.3001.4242

            https://blog.csdn.net/imbibi/article/details/78542258?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-4&spm=1001.2101.3001.4242

启动过程

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
index.php
bootstrap/autoload.php --> 自动加载
bootstrap/app.php --> 初始化服务容器(注册基础的服务提供者(事件、日志、路由)、注册核心类别名)
bootstrap/app.php --> 注册共享的Kernel和异常处理器
Foundation\\Http\\Kernel.php --> 处理请求和响应
index.php --> 将响应信息发送到浏览器
index.php --> 处理继承自TerminableMiddleware接口的中间件(Session)并结束应用生命周期

其中处理请求和响应包括:
解析Illuminate\\Contracts\\Http\\Kernel,实例化App\\Http\\Kernel
a. 实例化Kernel : 构造函数:设置$app/$router,初始化$router中middleware数值
b. handle处理请求:
加载路由中间件、加载环境变量、加载配置文件、加载异常处理机制、注册门面、注册服务提供者、启动服务提供者、管道模式注入中间件
c.将响应信息发送到浏览器
注册request实例到容器 ($app[\'request\']->Illuminate\\Http\\Request) -- $request是经过Symfony封装的请求对象
清空之前容器中的request实例
调用bootstrap方法,启动一系列启动类的bootstrap方法:
Illuminate\\Foundation\\Bootstrap\\DetectEnvironment 环境配置($app[‘env’])
Illuminate\\Foundation\\Bootstrap\\LoadConfiguration 基本配置($app[‘config’])
Illuminate\\Foundation\\Bootstrap\\ConfigureLogging 日志文件($app[‘log’])
Illuminate\\Foundation\\Bootstrap\\HandleExceptions 错误&异常处理
Illuminate\\Foundation\\Bootstrap\\RegisterFacades 清除已解析的Facade并重新启动,注册config文件中alias定义的所有Facade类到容器
Illuminate\\Foundation\\Bootstrap\\RegisterProviders 注册config中providers定义的所有Providers类到容器
Illuminate\\Foundation\\Bootstrap\\BootProviders 调用所有已注册Providers的boot方法
通过Pipeline发送请求,经过中间件,再由路由转发,最终返回响应


1.自动加载
包括全局函数的加载、顶级命名空间映射、PSR0、PSR4标准的实现

2.初始化服务容器
注册容器本身
将基本的绑定注册到容器中,包括容器自身、容器实例名称app
实例化
app, Illuminate\\Container\\Container
关键代码:
protected function registerBaseBindings() {
static::setInstance($this);
$this->instance(\'app\', $this);
$this->instance(Container::class, $this);
}
注册基础服务提供者
向容器分别注册了Key为以下值得实例
events
log
router、url、redirect、Illuminate\\Contracts\\Routing\\ResponseFactory
关键代码:
protected function registerBaseServiceProviders() {
$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new RoutingServiceProvider($this));
}
注册容器别名(注册共享的Kernel)
在调用此方法之前,我们想取得一个容器实例的做法是 App::make(\'app\');
现在我们可以使用三种方法来取得一个容器实例app
App::make(\'Illuminate\\Foundation\\Application\')
App::make(\'Illuminate\\Contracts\\Container\\Container\')
App::make(\'Illuminate\\Contracts\\Foundation\\Application\')
关键代码:
public function registerCoreContainerAliases(){
...
}

3. 注册共享的Kernel和异常处理器
关键代码:
$app->singleton(
Illuminate\\Contracts\\Http\\Kernel::class,
App\\Http\\Kernel::class
);

$app->singleton(
Illuminate\\Contracts\\Console\\Kernel::class,
App\\Console\\Kernel::class
);

$app->singleton(
Illuminate\\Contracts\\Debug\\ExceptionHandler::class,
App\\Exceptions\\Handler::class
);

4. 处理请求和响应
实例化App\\Http\\Kernel
构造函数:设置$app/$router,初始化$router中middleware数值
关键代码:
public function __construct(Application $app, Router $router)
{
$this->app = $app;
$this->router = $router;

$router->middlewarePriority = $this->middlewarePriority;

foreach ($this->middlewareGroups as $key => $middleware) {
$router->middlewareGroup($key, $middleware);
}

foreach ($this->routeMiddleware as $key => $middleware) {
$router->aliasMiddleware($key, $middleware);
}
}

5. handle处理请求
a. 注册request实例到容器 ($app[\'request\']->Illuminate\\Http\\Request) -- $request是经过Symfony封装的请求对象
b. 清空之前容器中的request实例
c. 调用bootstrap方法,启动一系列启动类的bootstrap方法
d. 通过Pipeline发送请求,经过中间件,再由路由转发,最终返回响应

关键代码:
protected function sendRequestThroughRouter($request)
{
$this->app->instance(\'request\', $request);

Facade::clearResolvedInstance(\'request\');

$this->bootstrap();

return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}


6. bootstrap方法
a.检测环境变量文件是否正常
b.取得配置文件,即把/config/下的所有配置文件读取到容器(app()->make(\'config\')可以查看所有配置信息)
c.注册异常: set_error_handler,set_exception_handler, register_shutdown_function
d.把/config/app.php里面的aliases项利用PHP库函数class_alias创建别名,从此,我们可以使用App::make(\'app\')方式取得实例
e.把/config/app.php里面的providers项,注册到容器
f.运行容器中注册的所有的ServiceProvider中得boot方法

关键代码:
protected $bootstrappers = [
\\Illuminate\\Foundation\\Bootstrap\\LoadEnvironmentVariables::class,
\\Illuminate\\Foundation\\Bootstrap\\LoadConfiguration::class,
\\Illuminate\\Foundation\\Bootstrap\\HandleExceptions::class,
\\Illuminate\\Foundation\\Bootstrap\\RegisterFacades::class,
\\Illuminate\\Foundation\\Bootstrap\\RegisterProviders::class,
\\Illuminate\\Foundation\\Bootstrap\\BootProviders::class,
];

7. 将响应信息发送到浏览器
关键代码:
$response->send();


9. 处理继承自TerminableMiddleware
关键代码:
$kernel->terminate($request, $response);


10. Laravel路由
$this->dispatchToRouter()
--> $this->router->dispatch($request)
--> $this->dispatchToRoute($request); -- /Illuminate/Routing/Router.php
--> $response = $this->runRouteWithinStack($route, $request);

//干货来了
protected function runRouteWithinStack(Route $route, Request $request)
{
// 取得routes.php里面的Middleware节点
$middleware = $this->gatherRouteMiddlewares($route);
//这个有点眼熟
return (new Pipeline($this->container))
->send($request)
->through($middleware) //执行上述的中间件
->then(function($request) use ($route)
{
//不容易啊,终于到Controller类了
return $this->prepareResponse(
$request,
$route->run($request); //run控制器
);
});
}

事件


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!