学习 Javascript 之变量

变量的声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 声明变量 */
var x = 3.14;
var y;
var z = x + y;
var a = 1, b = 2, c = true;
var foo = "Hello" + "World";
var bar = 2 > 1 ? 2 != 1 : 1 > 2 ;
/* 声明常量 */
const PI = 3.1415;
/* ES6 - 声明变量 */
let a = 10;
let [a, b, c] = [1, 2, 3];

变量的声明作用域

  • var
    • 在全局作用域内有效
  • let
    • 只在声明所在的块级作用域内有效
  • const
    • 只在声明所在的块级作用域内有效

变量的声明陷阱

  • 变量提升
    • 变量无法在 let 命令声明之前使用,否则报错
    • 变量可以在 var 命令声明之前使用,值为 undefined
  • 暂时性死区
    • 在代码块内,使用 let 命令声明变量之前,该变量都是不可用的
  • 重复声明
    • var 命令允许在相同作用域内,重复声明一个变量
    • let 命令不允许在相同作用域内,重复声明一个变量

解构赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 数组的解构赋值 */
let [foo, [[bar], baz]] = [1, [[2], 3]];
/* 对象的解构赋值 */
let { foo, bar } = { foo: "aaa", bar: "bbb" };
/* 字符串的解构赋值 */
const [a, b, c, d, e] = 'hello';
/* 函数的解构赋值 */
function add([x, y]){
return x + y;
}
add([1, 2]); // 3

变量的解构赋值用途如下:

  • 交换变量的值
  • 从函数返回多个值
  • 函数参数的定义
  • 提取 JSON 数据
  • 函数参数的默认值
  • 遍历 Map 结构
  • 输入模块的指定方法

Head First Design Patterns

如何定义模式

模式是某种情景下,针对某问题的某种解决方案。

  • 情景
    • 应用某个模式的情况,应该是会不断出现的情况
  • 问题
    • 想在某个情景下达到的目标,但也可以是某情景下的约束
  • 解决方案
    • 一个通用的设计,用来解决约束、达到目标

帮助记忆上述概念的方法

如果你发现自己处于某个情境下,面对着所欲达到的目标被一群约束影响着的问题,然而,你能够应用某个设计,克服这些约束并达到该目标,将你领向某个解决方案。

比如

  • 问题:我要如何准时上班
  • 情景:我的钥匙锁在车里了 | 搭地铁肯定要迟早了
  • 解决方案:打破窗户、进入车内、启动引擎,开车上班 | 打开手机、呼叫滴滴、上出租车,打车上班。

更多定义的细节:

  • 意图
  • 动机
  • 对比
  • 适用性
  • 举一反三

如何更加详细去定义和学习一个设计模式

  • 名称:用于学习与分享时共享词汇,概括性描述一个设计模式的作用
  • 分类:用于归纳设计模式的用途
  • 动机:给出了问题以及如何解决这个问题的具体场景
  • 结构:提供了图示,显示出参与此模式的类之间的关系
  • 参与者:描述在此设计中所涉及到的类和对象在模式中的责任和角色
  • 结果:描述采用此模式之后产生的效果:好与不好
  • 协作:告诉我们参与者如何在此模式中合作
  • 实现:提供了你在实现该模式时需要使用的技巧,以及应该小心面对的问题
  • 代码:提供实现的代码,对如何实现有所帮助
  • 已知应用:用来描述已经在真实系统中发现的模式例子
  • 相关模式:描述了此模式和其他模式之间的关系

「Tips」Yii2 前端请求的参数字段映射数据模型的属性

使用 Yii 2 构建 RESTFul API 项目时,需要实现一个功能。这个功能可以将 HTTP 请求的参数和后端数据模型相映射,让前端的请求参数命名和后端数据的模型属性不需要保持一致,并在指定场景中实现数据验证。实现代码如下:

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
<?php
/*
* 前端传递 JSON 数据进来,格式是{"name":"admin","pwd":"123456"}
* 需要对数据在指定场景下验证,并对应到模型的 username 属性和 password_hash 属性
*/
// 模型代码
class SignInForm
{
public $name;
public $pwd;
public function rules()
{
$rules = [
[
[
'name',
'pwd',
],
'required',
'on' => 'signIn',
],
[
['name'],
'default',
'value' => $this->username,
'on' => 'signIn',
],
[
['pwd'],
'default',
'value' => $this->password_hash,
'on' => 'signIn',
],
];
return $rules;
}
}
// 控制器代码
<?php
use Yii;
use workerbee\api\controllers\BaseController;
class UserController extends BaseController
{
public function actionSignIn()
{
$data['SignInForm'] = Yii::$app->request->post();
$signInModel = new SignInForm(['scenario' => 'signIn']);
if ($signInModel->load($data) && $signInModel->validate()) {
$user = SignInForm::findByUsername($signInModel->username);
$validator = Yii::$app->getSecurity()->validatePassword($signInModel->pwd, $user->password_hash);
if ($user !== null && $validator) {
$user->access_token = $signInModel->getNewAccessToken();
if ($user->save()) {
return $user;
}
throw new ServerErrorHttpException('登陆失败.');
}
throw new BadRequestHttpException('账号名或密码错误.');
}
throw new UnprocessableEntityHttpException('数据验证失败.');
}
}

「Tips」.htaccess 配置 HTTP Header

使用 Yii 2 做 RESTFul API 项目时,发现在前端发起一个请求时,会出现以下错误:

1
Request header field access-token is not allowed by Access-Control-Allow-Headers in preflight response.

排查问题进行了很久,最后才发现在 Yii 2 的 web 目录下,有个文件 .htaccess,是可以配置 Apache 服务器接受 HTTP 请求的头部信息的,在特定选项加入 access-token 即刻不再报错,因为本次项目使用 HTTP Header 传递 access-token 的,没有加入 access-token 就意味 Apache 服务器不接受头部带有 access-token 的 HTTP 请求 。文件配置代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Header always set Access-Control-Allow-Origin "*"
Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE, PUT"
Header always set Access-Control-Max-Age "1000"
Header always set Access-Control-Allow-Headers "x-requested-with, Content-Type, origin, authorization, accept, client-security-token, access-token"
RewriteEngine on
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php

「Tips」PHP 7.1 命名空间的新特性和使用方法

新特性和使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
// Pre PHP 7 code
use some\namespace\ClassA;
use some\namespace\ClassB;
use some\namespace\ClassC as C;
use function some\namespace\fn_a;
use function some\namespace\fn_b;
use function some\namespace\fn_c;
use const some\namespace\ConstA;
use const some\namespace\ConstB;
use const some\namespace\ConstC;
// PHP 7+ code
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};

Swagger-PHP 基础使用

Swagger-PHP 的基本语法

参考文档

常用属性

红色为必写的字段,蓝色为封装的字段。

  • @SWG\Swagger
    - swagger
    - info: @SWG\Info
    - host - 接口域名
    - basePath - 接口前缀路径
    - schemes: [“http”, “https”, “ws”, “wss”]
    - consumes - 请求 Mime Types
    - produces - 响应 Mime Types
    - paths: @SWG\Path
  • @SWG\Info
    • title - 文档标题
    • description - 文档描述
    • termsOfService - 所属团队
    • contact: @SWG\Contact - 联系方式
  • @SWG\Contact
    • url - 联系链接
    • name - 联系名称
    • email - 联系邮箱
  • @SWG\License
    • name - 开源许可证
    • url - 许可证地址
  • @SWG\Path
    • get: @SWG\Get - HTTP Get 请求
    • put: @SWG\Put - HTTP Put 请求
    • post: @SWG\Post - HTTP Post 请求
    • delete: @SWG\Delete - HTTP Delete 请求
    • options: @SWG\Options - HTTP Options 请求
  • @SWG\GET
    • tags - 请求分类
    • summary - 请求简介
    • description - 请求描述
    • operationId - 请求编号,要求唯一
    • consumes - 请求 Mime Types
    • produces - 响应 Mime Types
    • parameters: @SWG\Parameter - 请求参数
    • responses: @SWG\Response - 请求响应
    • schemes: [“http”,”https”,”ws”,”wss”] - 请求协议
    • deprecated - 是否弃用
  • @SWG\Parameter
    • name - 请求参数名称
    • in: [“query”,”header”,”path”,”formData”,”body”] - 请求参数存放方式
    • description - 请求参数描述
    • required - 是否要求
    • schema - 当 inbody 时可以使用,用于描述参数
    • type: [“string”, “number”, “integer”, “boolean”, “array”, “file”] - 请求参数类型
    • format: [“int32”, “int64”, “float”, “double”, “byte”, “date”, “date-time”] - 请求参数格式
    • allowEmptyValue - 是否允许空值
    • items - 当 typearray,itemsrequired,描述参数数组
    • collectionFormat
    • default - 请求参数默认值
  • @SWG\Response
    - default
    - response object
    - description - 响应描述
    - schema: @SWG\Schema
    - headers: @SWG\Header - 响应头部
    - example: @SWG\Example - 响应数据例子
    - reference object
    - $ref
    - HTTP Status Code
    - response object
    - description
    - schema: @SWG\Schema
    - headers: @SWG\Header
    - example: @SWG\Example
    - reference object
    - $ref
  • @SWG\Definition
    - definition
    - required
    - @SWG\Property
  • @SWG\Property
    • property - 模型成员属性
    • type: [“string”, “number”, “integer”, “boolean”, “array”, “file”] - 模型参数类型
    • format: [“int32”, “int64”, “float”, “double”, “byte”, “date”, “date-time”] - 模型参数格式
  • @SWG\Header
    • header - 头部名称
    • type - 头部数值类型
    • description - 头部简介

模块文件上使用

打开文件 api/modules/v1/module.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
/**
* v1 module definition class
*
* @SWG\Swagger(
* swagger="2.0",
* @SWG\Info(
* title="安乐窝商品库 API(标题)",
* description="安乐窝商品库 API(描述)",
* termsOfService="安乐窝开发团队",
* version="1.0.0(版本号)",
* @SWG\Contact(
* email="2794408425@qq.com(邮件)",
* name="安乐窝(联系称呼)",
* url="http://api.app/v1/swagger-ui/dist/index.html"
* ),
* @SWG\License(
* name="MIT",
* url="http://github.com/gruntjs/grunt/blob/master/LICENSE-MIT"
* ),
* ),
* host="api.app/",
* basePath="v1/",
* schemes={"http"},
* produces={"application/json"},
* consumes={"application/json"},
* @SWG\Definition(
* definition="ErrorModel",
* required={"code", "message"},
* @SWG\Property(
* property="code",
* type="integer",
* format="int32"
* ),
* @SWG\Property(
* property="message",
* type="string"
* )
* ),
* )
*/

上述代码生成文档时的效果如下图:

控制器文件上使用

打开文件 api/modules/v1/controllers/MenuController.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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/**
* @SWG\Get(
* path="/menu",
* tags={"菜单接口"},
* description="获取菜单列表",
* operationId="menu/index",
* @SWG\Parameter(
* name="access-token",
* in="query",
* description="Access Token",
* required=true,
* type="string",
* default="HelloWorld"
* ),
* @SWG\Response(
* response=200,
* description="Success Response",
* @SWG\Header(header="x-pagination-per-page", type="string", description="Per Page"),
* @SWG\Header(header="x-pagination-page-count", type="string", description="Page Count"),
* @SWG\Header(header="x-pagination-total-count", type="string", description="Total Count"),
* @SWG\Header(header="x-pagination-current-page", type="string", description="Current Page"),
* @SWG\Schema(
* @SWG\Property(
* property="items",
* type="array",
* @SWG\Items(
* type="object",
* @SWG\Property(property="id", type="integer", example=1),
* @SWG\Property(property="name", type="string", example="权限管理"),
* @SWG\Property(property="route", type="string", example="/admin/permission/index")
* )
* ),
* @SWG\Property(
* property="_links",
* type="object",
* @SWG\Property(
* property="self",
* type="object",
* @SWG\Property(
* property="href", type="string", example="http://api.app/v1/menu?access-token=HelloWorld&page=1"
* )
* ),
* ),
* @SWG\Property(
* property="_meta",
* type="object",
* @SWG\Property(property="totalCount", type="integer", example=20),
* @SWG\Property(property="pageCount", type="integer", example=1),
* @SWG\Property(property="currentPage", type="integer", example=1),
* @SWG\Property(property="perPage", type="integer", example=20),
* )
* )
* ),
* @SWG\Response(
* response="404",
* description="HTTP 404 Error",
* @SWG\Schema(ref="#/definitions/ErrorModel")
* ),
* @SWG\Response(
* response="500",
* description="HTTP 500 Error",
* @SWG\Schema(ref="#/definitions/ErrorModel")
* )
* )
*/
/**
* @SWG\Post(
* path="/menu",
* tags={"菜单接口"},
* operationId="menu/create",
* description="新增菜单",
* @SWG\Parameter(
* name="body",
* in="body",
* description="新增菜单所需的参数",
* required=true,
* @SWG\Schema(ref="#/definitions/MenuModel")
* ),
* @SWG\Parameter(
* in="query",
* name="access-token",
* default="HelloWorld",
* description="Access-Token",
* required=true,
* type="string",
* format="string"
* ),
* @SWG\Response(
* response=201,
* description="pet response",
* @SWG\Schema(
* type="object",
* @SWG\Property(property="id", type="integer", example=1),
* @SWG\Property(property="name", type="string", example="歪理兔"),
* @SWG\Property(property="route", type="string", example="/admin/very/too"),
* )
* ),
* @SWG\Response(
* response="default",
* description="unexpected error",
* @SWG\Schema(ref="#/definitions/ErrorModel")
* )
* )
*/

上述代码生成文档时的效果如下图:

模型文件上使用

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
/**
* @SWG\Definition(
* definition="MenuModel",
* required={"id", "name", "route"},
* @SWG\Property(
* property="id",
* type="integer",
* format="int32"
* ),
* @SWG\Property(
* property="name",
* type="string"
* ),
* @SWG\Property(
* property="route",
* type="string"
* ),
* @SWG\Property(
* property="parent",
* type="integer"
* ),
* @SWG\Property(
* property="order",
* type="integer"
* ),
* @SWG\Property(
* property="data",
* type="string"
* )
* )
*/

使用 Yii2 构建 RESTFul API

安装 Yii2 高级模板

1
2
composer global require "fxp/composer-asset-plugin:^1.2.0"
composer create-project --prefer-dist yiisoft/yii2-app-advanced Api

初始化项目

1
2
cd Api
sudo php init

打开文件 common/config/main-local.php,配置数据库信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
return [
'components' => [
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=api',
'username' => 'root',
'password' => '123456',
'charset' => 'utf8',
],
'mailer' => [
'class' => 'yii\swiftmailer\Mailer',
'viewPath' => '@common/mail',
// send all mails to a file by default. You have to set
// 'useFileTransport' to false and configure a transport
// for the mailer to send real emails.
'useFileTransport' => true,
],
],
];

执行迁移文件,生成数据表

1
php yii migrate

新建模板 api,执行操作如下:

  • 复制目录 frontendapi
  • 复制目录 environments/dev/frontendenvironments/dev/api
  • 复制目录 environments/prod/frontendenvironments/prod/api
  • 修改目录 api 里面文件的命名空间 namespace
  • 打开文件 common/config/bootstrap.php 并加入代码 Yii::setAlias('api', dirname(dirname(__DIR__)) . '/api');

清理模板 api,删除文件如下:

  • api/assets
  • api/views
  • api/controllers
  • api/web/assets
  • api/web/css

配置 Apache 虚拟主机

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
<VirtualHost *:80>
ServerName api.frontend.app
DocumentRoot "/Users/LuisEdware/Code/Api/frontend/web/"
<Directory "/Users/LuisEdware/Code/Api/frontend/web/">
# use mod_rewrite for pretty URL support
RewriteEngine on
# If a directory or a file exists, use the request directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Otherwise forward the request to index.php
RewriteRule . index.php
# use index.php as index file
DirectoryIndex index.php
# ...other settings...
# Apache 2.4
Require all granted
## Apache 2.2
# Order allow,deny
# Allow from all
</Directory>
</VirtualHost>
<VirtualHost *:80>
ServerName api.backend.app
DocumentRoot "/Users/LuisEdware/Code/Api/backend/web/"
<Directory "/Users/LuisEdware/Code/Api/backend/web/">
# use mod_rewrite for pretty URL support
RewriteEngine on
# If a directory or a file exists, use the request directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Otherwise forward the request to index.php
RewriteRule . index.php
# use index.php as index file
DirectoryIndex index.php
# ...other settings...
# Apache 2.4
Require all granted
## Apache 2.2
# Order allow,deny
# Allow from all
</Directory>
</VirtualHost>
<VirtualHost *:80>
ServerName api.app
DocumentRoot "/Users/LuisEdware/Code/Api/api/web/"
<Directory "/Users/LuisEdware/Code/Api/api/web/">
# use mod_rewrite for pretty URL support
RewriteEngine on
# If a directory or a file exists, use the request directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Otherwise forward the request to index.php
RewriteRule . index.php
# use index.php as index file
DirectoryIndex index.php
# ...other settings...
# Apache 2.4
Require all granted
## Apache 2.2
# Order allow,deny
# Allow from all
</Directory>
</VirtualHost>

搭建基本的 RESTFul 架构

创建控制器

在目录 api/controllers 下创建控制器 UserController,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
namespace api\modules\v1\controllers;
use yii\rest\ActiveController;
class OrderController extends ActiveController
{
// 声明控制器所采用的数据模型
public $modelClass = 'api\modules\v1\models\Order';
// 序列化输出
public $serializer = [
'class' => 'yii\rest\Serializer',
'collectionEnvelope' => 'items',
];
}

配置路由规则

打开文件 api/config/main-local.php,在数组 $config['components'] 中新增代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
'components' => [
// 接受 JSON 格式的数据
'request' => [
'cookieValidationKey' => 'sTGCwRd3spEa33BW2HMjuuwZnsJO6RMM',
'parsers' => [
'application/json' => 'yii\web\JsonParser',
],
],
// 配置路由规则
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
[
'class' => 'yii\rest\UrlRule',
'controller' => 'user',
'pluralize' => false,
],
],
],
],

创建数据模型

在目录 api\models 新增数据模型,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
namespace api\models;
use yii\db\ActiveRecord;
class User extends ActiveRecord
{
public static function tableName()
{
return "user";
}
public function fields()
{
// 删除敏感字段
$fields = parent::fields();
unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']);
return $fields;
}
}

测试接口地址

使用 HTTP GET 的方式去访问 http://api.app/user,得到 JSON 数据如下:

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
{
"items": [
{
"id": 1,
"username": "okirlin",
"email": "brady.renner@rutherford.com",
"role": 10,
"status": 10,
"created_at": 1391885313,
"updated_at": 1391885313
},
{
"id": 2,
"username": "troy.becker",
"email": "nicolas.dianna@hotmail.com",
"role": 10,
"status": 0,
"created_at": 1391885313,
"updated_at": 1391885313
},
{
"id": 3,
"username": "miles",
"email": "506510463@qq.com",
"role": 10,
"status": 0,
"created_at": 1231231233,
"updated_at": 1231231233
}
],
"_links": {
"self": {
"href": "http://api.app/user?page=1"
}
},
"_meta": {
"totalCount": 8,
"pageCount": 1,
"currentPage": 1,
"perPage": 20
}
}

版本控制

实现方式

有两种方案可以实现 API 版本控制:

  • 在 URL 中带上请求的版本号,比如:http://example.com/v1/users
  • 把版本号放在HTTP请求头中,通常通过 Accept 头,像这样:Accept: application/json; version=v1

Yii2 两种方案都支持,官方推荐,使用模块来实现版本化,简化代码维护和管理,在 URL 上带上一个大版本号(比如: v1, v2),在每个大版本中,使用 Accept HTTP 请求头来判定小版本号并编写针对各小版本的条件语句。

创建模块

新建目录 api/modules/v1,目录下新建文件 Module.php,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
namespace api\modules\v1;
/**
* v1 module definition class
*/
class Module extends \yii\base\Module
{
/**
* @inheritdoc
*/
public $controllerNamespace = 'api\modules\v1\controllers';
/**
* @inheritdoc
*/
public function init()
{
parent::init();
}
}

创建控制器

新建目录 api/modules/v1/controllers,在该目录下新增控制器 OrderController.php,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
namespace api\modules\v1\controllers;
use yii\rest\ActiveController;
class OrderController extends ActiveController
{
// 声明控制器所采用的数据模型
public $modelClass = 'api\modules\v1\models\Order';
// 序列化输出
public $serializer = [
'class' => 'yii\rest\Serializer',
'collectionEnvelope' => 'items',
];
}

创建模型

新建目录 api/modules/v1/models,在该目录下新增数据模型 Order.php,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
namespace api\modules\v1\models;
use yii\db\ActiveRecord;
class Order extends ActiveRecord
{
public static function tableName()
{
return "z_order";
}
// 返回指定的字段
public function fields()
{
return [
'id',
// 重命名字段
'orderNumber' => 'orderno'
];
}
}

配置路由与模块

打开文件 api/config/main-local.php,在数组 $config[] 新增模块版本,在数组 $config['components']['urlManager']['rules'] 新增路由规则,代码如下:

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
<?php
$config = [
'modules' => [
'v1' => [
'class' => 'api\modules\v1\Module',
],
],
'components' => [
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => 'sTGCwRd3spEa33BW2HMjuuwZnsJO6RMM',
'parsers' => [
'application/json' => 'yii\web\JsonParser',
],
],
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
[
'class' => 'yii\rest\UrlRule',
'controller' => 'user',
'pluralize' => false,
],
[
'class' => 'yii\rest\UrlRule',
'controller' => 'v1/order',
'pluralize' => false,
],
],
],
],
];

测试接口地址

使用 HTTP GET 的方式去访问 http://api.app/v1/order,得到 JSON 数据如下:

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
{
"orders": [
{
"id": 57,
"orderNumber": "201310140001"
},
{
"id": 68,
"orderNumber": "201310150001"
},
{
"id": 69,
"orderNumber": "201310150002"
},
{
"id": 70,
"orderNumber": "201310150003"
},
{
"id": 71,
"orderNumber": "201310170001"
},
{
"id": 72,
"orderNumber": "201310170002"
},
{
"id": 73,
"orderNumber": "201310170003"
},
{
"id": 74,
"orderNumber": "201310170004"
},
]
}

API 认证

Yii 2 有三种认证用户的验证方式

  • HttpBasicAuth::className():是通过弹窗填用户名密码,默认只需要在用户名栏填入 Access-Token 返回接口数据。
  • HttpBearerAuth::className():是通过请求 Headers 带上 Authorization: Bearer Access-Token 参数返回接口数据。
  • QueryParamAuth::className():是通过 URL 带上 ?access-token=Access-Token 参数返回接口数据。

采用 QueryParamAuth 方式进行验证,因为 RESTFul API 是无状态的,不能通过 sessioncookie 来保持状态,在模块文件 Module.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
<?php
namespace api\modules\v1;
use Yii;
/**
* v1 module definition class
*/
class Module extends \yii\base\Module
{
/**
* @inheritdoc
*/
public $controllerNamespace = 'api\modules\v1\controllers';
/**
* @inheritdoc
*/
public function init()
{
parent::init();
// 禁止使用 Session
Yii::$app->user->enableSession = false;
}
// v1 模块的请求都需要认证
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => QueryParamAuth::className(),
];
return $behaviors;
}
}

使用 HTTP GET 的方式去访问 http://api.app/v1/order?access-token=HelloWorld,得到 JSON 数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"orders": [
{
"id": 57,
"orderNumber": "201310140001"
},
{
"id": 68,
"orderNumber": "201310150001"
},
{
"id": 69,
"orderNumber": "201310150002"
},
{
"id": 70,
"orderNumber": "201310150003"
},
]
}

使用 Swagger 生成 API 可调试文档

配置 Swagger-UI

执行以下命令,将 Swagger-UI 克隆到 api/web/v1/ 文件夹下:

1
2
3
4
cd api/web
mkdir v1
cd v1
git clone https://github.com/swagger-api/swagger-ui.git

执行以下命令,在目录 api/web/v1 下生成 Swagger 文档配置文件:

1
2
mkdir -p swagger-document/
vim swagger-document/swagger.json

打开文件 api/web/v1/swagger-ui/dist/index.html,修改生成文档的配置文件地址:

1
2
3
4
5
if (url && url.length > 1) {
url = decodeURIComponent(url[1]);
} else {
url = "http://api.app/v1/swagger-document/swagger.json";
}

Swagger-PHP 的基本语法

参考文档

红色为必写的字段,蓝色为封装的字段。

  • @SWG\Swagger
    - swagger
    - info: @SWG\Info
    - host - 接口域名
    - basePath - 接口前缀路径
    - schemes: [“http”, “https”, “ws”, “wss”]
    - consumes - 请求 Mime Types
    - produces - 响应 Mime Types
    - paths: @SWG\Path
  • @SWG\Info
    • title - 文档标题
    • description - 文档描述
    • termsOfService - 所属团队
    • contact: @SWG\Contact - 联系方式
  • @SWG\Contact
    • url - 联系链接
    • name - 联系名称
    • email - 联系邮箱
  • @SWG\License
    • name - 开源许可证
    • url - 许可证地址
  • @SWG\Path
    • get: @SWG\Get - HTTP Get 请求
    • put: @SWG\Put - HTTP Put 请求
    • post: @SWG\Post - HTTP Post 请求
    • delete: @SWG\Delete - HTTP Delete 请求
    • options: @SWG\Options - HTTP Options 请求
  • @SWG\GET
    • tags - 请求分类
    • summary - 请求简介
    • description - 请求描述
    • operationId - 请求编号,要求唯一
    • consumes - 请求 Mime Types
    • produces - 响应 Mime Types
    • parameters: @SWG\Parameter - 请求参数
    • responses: @SWG\Response - 请求响应
    • schemes: [“http”,”https”,”ws”,”wss”] - 请求协议
    • deprecated - 是否弃用
  • @SWG\Parameter
    • name - 请求参数名称
    • in: [“query”,”header”,”path”,”formData”,”body”] - 请求参数存放方式
    • description - 请求参数描述
    • required - 是否要求
    • schema - 当 inbody 时可以使用,用于描述参数
    • type: [“string”, “number”, “integer”, “boolean”, “array”, “file”] - 请求参数类型
    • format: [“int32”, “int64”, “float”, “double”, “byte”, “date”, “date-time”] - 请求参数格式
    • allowEmptyValue - 是否允许空值
    • items - 当 typearray,itemsrequired,描述参数数组
    • collectionFormat
    • default - 请求参数默认值
  • @SWG\Response
    - default
    - response object
    - description - 响应描述
    - schema: @SWG\Schema
    - headers: @SWG\Header - 响应头部
    - example: @SWG\Example - 响应数据例子
    - reference object
    - $ref
    - HTTP Status Code
    - response object
    - description
    - schema: @SWG\Schema
    - headers: @SWG\Header
    - example: @SWG\Example
    - reference object
    - $ref
  • @SWG\Definition
    - definition
    - required
    - @SWG\Property
  • @SWG\Property
    • property - 模型成员属性
    • type: [“string”, “number”, “integer”, “boolean”, “array”, “file”] - 模型参数类型
    • format: [“int32”, “int64”, “float”, “double”, “byte”, “date”, “date-time”] - 模型参数格式
  • @SWG\Header
    • header - 头部名称
    • type - 头部数值类型
    • description - 头部简介

配置 Swagger-PHP

使用 Composer 安装 Swagger-PHP,在项目根目录中执行命令如下:

1
composer require zircote/swagger-php

api\controllers 新增文档控制器 DocumentController.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
<?php
namespace api\controllers;
use Yii;
use Swagger;
use yii\base\Exception;
use yii\web\Controller;
class DocumentController extends Controller
{
// 根据版本号生成不同版本的 API 文档
public function actionBuild($version)
{
$apiDir = Yii::getAlias('@api');
$swagger = Swagger\scan($apiDir);
$jsonFile = $apiDir . "/web/{$version}/swagger-document/swagger.json";
if (!file_exists($jsonFile)) {
throw new Exception("Swagger.json 文件不存在");
}
$isWrite = file_put_contents($jsonFile, $swagger);
if ($isWrite == true) {
$this->redirect("/{$version}/swagger-ui/dist/index.html");
} else {
throw new Exception("Swagger.json 文件写入失败");
}
}
}

新增路由如下:

1
2
3
4
5
6
<?php
[
'class' => 'yii\web\UrlRule',
'pattern' => 'document/build/<version:\w+>',
'route' => 'document/build',
],

模块文件 app/modules/Module.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
/**
* v1 module definition class
*
* @SWG\Swagger(
* swagger="2.0",
* @SWG\Info(
* title="安乐窝商品库 API(标题)",
* description="安乐窝商品库 API(描述)",
* termsOfService="安乐窝开发团队",
* version="1.0.0(版本号)",
* @SWG\Contact(
* email="2794408425@qq.com(邮件)",
* name="安乐窝(联系称呼)",
* url="http://api.app/v1/swagger-ui/dist/index.html"
* ),
* @SWG\License(
* name="MIT",
* url="http://github.com/gruntjs/grunt/blob/master/LICENSE-MIT"
* ),
* ),
* host="api.app/",
* basePath="v1/",
* schemes={"http"},
* produces={"application/json"},
* consumes={"application/json"},
* @SWG\Definition(
* definition="ErrorModel",
* required={"code", "message"},
* @SWG\Property(
* property="code",
* type="integer",
* format="int32"
* ),
* @SWG\Property(
* property="message",
* type="string"
* )
* ),
* )
*/

Yii 2 的 RESTFul APT 会自动为控制器提供六个动作如:indexviewcreateupdatedeleteoptions,给控制器 app/v1/controlelr/OrderController.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
/**
* @SWG\Get(
* tags={"order"},
* path="/order",
* summary="列出所有的订单",
* operationId="index",
* description="列出所有的订单",
* @SWG\Parameter(
* name="access-token",
* in="query",
* description="Access Token for RESTFul API",
* required=true,
* type="string",
* format="string"
* ),
* @SWG\Response(
* response=200,
* description="Successful Operation",
* ),
* @SWG\Response(
* response="404",
* description="Not Found Operation",
* @SWG\Schema(
* ref="#/definitions/Error"
* )
* )
* )
*/

打开控制器 app/modules/v1/controller/UserController.php,继承 actions() 方法,修改 user/create 的场景,并添加 user/create 文档生成注释代码,如下:

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
<?php
namespace api\modules\v1\controllers;
use yii\rest\ActiveController;
class UserController extends ActiveController
{
// 声明控制器所采用的数据模型
public $modelClass = 'api\models\User';
// 序列化输出
public $serializer = [
'class' => 'yii\rest\Serializer',
'collectionEnvelope' => 'items',
];
// 修改 user/create 的 rules 场景
public function actions()
{
$actions = parent::actions();
$actions['create']['scenario'] = 'createScenario';
return $actions;
}
}
/**
* @SWG\Post(
* path="/user",
* tags={"user"},
* summary="创建一名用户",
* description="This can only be done by the logged in user.",
* operationId="create",
* @SWG\Parameter(
* in="path",
* name="username",
* default="LuisEdware",
* description="Created user object",
* required=true,
* type="string",
* format="string"
* ),
* @SWG\Parameter(
* in="path",
* name="auth_key",
* default="iwTNae9t34OmnK6l4vT4IeaTk-YWI2Rv",
* description="Created user object",
* required=true,
* type="string",
* format="string"
* ),
* @SWG\Parameter(
* in="path",
* name="password_hash",
* default="$2y$13$CXT0Rkle1EMJ/c1l5bylL.EylfmQ39O5JlHJVFpNn618OUS1HwaIi",
* description="Created user object",
* required=true,
* type="string",
* format="string"
* ),
* @SWG\Parameter(
* in="path",
* name="password_reset_token",
* default="t5GU9NwpuGYSfb7FEZMAxqtuz2PkEvv_1487320567",
* description="Created user object",
* required=true,
* type="string",
* format="string"
* ),
* @SWG\Parameter(
* in="path",
* name="email",
* default="2794408425@qq.com",
* description="Created user object",
* required=true,
* type="string",
* format="string"
* ),
* @SWG\Parameter(
* in="path",
* name="role",
* default="1",
* description="Created user object",
* required=true,
* type="integer",
* format="integer"
* ),
* @SWG\Parameter(
* in="path",
* name="status",
* default="1",
* description="Created user object",
* required=true,
* type="integer",
* format="integer"
* ),
* @SWG\Parameter(
* in="path",
* name="created_at",
* default="1391885313",
* description="Created user object",
* required=true,
* type="integer",
* format="integer"
* ),
* @SWG\Parameter(
* in="path",
* name="updated_at",
* default="1391885313",
* description="Created user object",
* required=true,
* type="integer",
* format="integer"
* ),
* @SWG\Parameter(
* in="path",
* name="access_token",
* default="HelloWorld",
* description="Created user object",
* required=true,
* type="string",
* format="string"
* ),
* @SWG\Parameter(
* in="query",
* name="access-token",
* default="HelloWorld",
* description="Created user object",
* required=true,
* type="string",
* format="string"
* ),
* @SWG\Response(response="default", description="successful operation")
* )
*/

打开文件 app/modules/v1/models/User.php,新增代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
public function rules()
{
return [
[
[
'username',
'auth_key',
'password_hash',
'password_reset_token',
'email',
'role',
'status',
'created_at',
'updated_at',
'access_token',
],
'required',
'on' => ['createScenario'],
]
];
}

ap1/modules/v1 下新增控制器 MenuController.php,新增模型 Menu.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
<?php
namespace api\modules\v1\controllers;
use yii\rest\ActiveController;
class MenuController extends ActiveController
{
// 声明控制器所采用的数据模型
public $modelClass = 'api\modules\v1\models\Menu';
// 序列化输出
public $serializer = [
'class' => 'yii\rest\Serializer',
'collectionEnvelope' => 'items',
];
public function actions()
{
$actions = parent::actions();
$actions['create']['scenario'] = 'createScenario';
return $actions;
}
}
/**
* @SWG\Post(
* path="/menu",
* tags={"menu"},
* operationId="createMenu",
* description="创建菜单",
* consumes={"application/json"},
* produces={"application/json"},
* @SWG\Parameter(
* name="body",
* in="body",
* description="menu to add to the store",
* required=true,
* @SWG\Schema(ref="#/definitions/menu"),
* ),
* @SWG\Parameter(
* in="query",
* name="access-token",
* default="HelloWorld",
* description="Created user object",
* required=true,
* type="string",
* format="string"
* ),
* @SWG\Response(
* response=200,
* description="pet response",
* @SWG\Schema(ref="#/definitions/menu")
* ),
* @SWG\Response(
* response="default",
* description="unexpected error",
* @SWG\Schema(ref="#/definitions/ErrorModel")
* )
* )
*/

模型

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
<?php
namespace api\modules\v1\models;
use yii\db\ActiveRecord;
class Menu extends ActiveRecord
{
public static function tableName()
{
return "menu";
}
// 返回指定的字段
public function fields()
{
return [
'id',
'name',
'route',
];
}
public function rules()
{
return [
[
[
'name',
'parent',
'route',
'order',
'data',
],
'required',
'on' => ['createScenario'],
],
];
}
}

在浏览器中打开 URL: http://api.app/document/build/v1 生成文档。

API 授权

现在已经解决了 RESTFUL API 的脚手架、版本化、API 验证和 API 文档生成的问题,接下来我们来完成 API 授权功能。

首先我们需要建立授权数据,而所有授权数据相关的任务如下:

  • 定义角色和权限;
  • 建立角色和权限的关系;
  • 定义规则;
  • 将规则与角色和权限作关联;
  • 指派角色给用户。

我们先配置所需的组件,我们使用 Yii2 自带 RBAC 组件进行权限控制,打开配置文件 api/config/main.php,在数组 components 新增代码如下:

1
2
3
'authManager' => [
'class' => 'yii\rbac\DbManager',
],

然后在根目录执行命令,在数据库创建好相关的数据表,执行命令如下:

1
php yii migrate --migrationPath=@yii/rbac/migrations

在项目根目录 console/controllers 文件夹新建控制器 RbacController.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
<?php
namespace console\controllers;
use Yii;
use yii\console\Controller;
class RbacController extends Controller
{
public function actionInit()
{
$authManager = Yii::$app->authManager;
// 添加 "createMenu" 权限
$createMenu = $authManager->createPermission('menu/create');
$createMenu->description = 'Create a Menu';
$authManager->add($createPost);
// 添加 "updateMenu" 权限
$updateMenu = $authManager->createPermission('menu/update');
$updateMenu->description = 'Update Menu';
$authManager->add($updateMenu);
// 添加 "author" 角色并赋予 "menu/create" 权限
$author = $authManager->createRole('author');
$authManager->add($author);
$authManager->addChild($author, $createPost);
// 添加 "admin" 角色并赋予 "menu/update" 和 "author" 权限
$admin = $authManager->createRole('admin');
$authManager->add($admin);
$authManager->addChild($admin, $updatePost);
$authManager->addChild($admin, $author);
// 为用户指派角色。其中 1 和 2 是由 IdentityInterface::getId() 返回的id (译者注:user表的id)
$authManager->assign($admin, 1);
$authManager->assign($author, 2);
}
}

然后在根目录中执行命令 php yii rbac/init,填充 RBAC 的测试数据,并在文件 v1/Module.php 中更改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
// v1 模块的请求都需要认证
public function behaviors()
{
$behaviors = parent::behaviors();
// API 认证
$behaviors['authenticator'] = [
'class' => QueryParamAuth::className(),
];
// API 授权
Yii::$app->user->setIdentity(User::findIdentityByAccessToken(Yii::$app->request->get('access-token')));
$operation = Yii::$app->controller->id . '/' . Yii::$app->controller->action->id;
if (!Yii::$app->user->can($operation)) {
echo '没有' . $operation . '的操作权限';
}
}

编写测试

如果是 RESTFul API 的架构,我会选择编写 API 测试和单元测试。首先先从 API 测试开始。首先在 api 中创建 API 测试配置文件,执行命令如下:

1
codecept generate:suite api

进入文件夹api/tests,打开文件 api.suite.yml,编写配置如下:

1
2
3
4
5
6
7
8
9
class_name: ApiTester
modules:
enabled: [PhpBrowser, REST]
config:
PhpBrowser:
url: http://api.app
REST:
url: http://api.app
depends: PhpBrowser

在文件夹 api 生成 API 测试文件,执行命令如下:

1
2
codecept generate:cept api/v1 SearchMenu
codecept generate:cept api/v1 CreateMenu

打开文件 api/tests/api/v1/SearchMenuCept.php,编写代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
use api\tests\ApiTester;
use Codeception\Util\HttpCode;
$I = new ApiTester($scenario);
$I->wantTo('search menus');
$I->haveHttpHeader('Content-Type', 'application/json');
$I->sendGet('/v1/menu', ['access-token' => "HelloWorld"]);
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseIsJson();
$I->seeResponseMatchesJsonType([
'items' => 'array',
'_links' => 'array',
'_meta' => [
'totalCount' => 'integer',
'pageCount' => 'integer',
'currentPage' => 'integer',
'perPage' => 'integer',
],
]);

打开文件 api/tests/api/v1/CreateMenuCept.php,编写代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
use api\tests\ApiTester;
$I = new ApiTester($scenario);
$I->wantTo('create menu');
$I->haveHttpHeader('Content-Type', 'application/json');
$I->sendPOST('/v1/menu?access-token=HelloWorld', [
'name' => '歪理兔',
'parent' => '1',
'route' => '/go/to/very-to',
'order' => 0,
'data' => 'hello',
]);
$I->seeResponseCodeIs(201);
$I->seeResponseIsJson();
$I->seeResponseMatchesJsonType([
'id' => 'integer',
'name' => 'string',
'route' => 'string',
]);

接着构建测试环境,并运行所编写的 API 测试,执行命令如下:

1
2
codecept build
codecept run

错误处理

Yii 2 的 REST 框架的 HTTP 状态代码如下:

  • 200: OK。一切正常。
  • 201: 响应 POST 请求时成功创建一个资源。Location header 包含的URL指向新创建的资源。
  • 204: 该请求被成功处理,响应不包含正文内容 (类似 DELETE 请求)。
  • 304: 资源没有被修改。可以使用缓存的版本。
  • 400: 错误的请求。可能通过用户方面的多种原因引起的,例如在请求体内有无效的JSON 数据,无效的操作参数,等等。
  • 401: 验证失败。
  • 403: 已经经过身份验证的用户不允许访问指定的 API 末端。
  • 404: 所请求的资源不存在。
  • 405: 不被允许的方法。 请检查 Allow header 允许的HTTP方法。
  • 415: 不支持的媒体类型。 所请求的内容类型或版本号是无效的。
  • 422: 数据验证失败 (例如,响应一个 POST 请求)。 请检查响应体内详细的错误消息。
  • 429: 请求过多。 由于限速请求被拒绝。
  • 500: 内部服务器错误。 这可能是由于内部程序错误引起的。

对应的 class 使用如下:

  • BadRequestHttpException - 400
  • UnauthorizedHttpException - 401
  • ForbiddenHttpException - 403
  • NotFoundHttpException - 404
  • MethodNotAllowedHttpException - 405
  • UnsupportedMediaTypeHttpException - 415
  • UnprocessableEntityHttpException - 422
  • TooManyRequestsHttpException - 429
  • ServerErrorHttpException - 500

「复习」使用阿里云从零开始部署 Laravel

准备

更新 Yum

1
yum update

更新 Yum 源

1
2
3
rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-7.rpm
rpm -Uvh https://mirror.webtatic.com/yum/el7/webtatic-release.rpm
rpm -Uvh http://repo.mysql.com/mysql-community-release-el7-5.noarch.rpm

安装 Git

1
yum install -y git

安装 Htop

1
yum install -y htop

安装 Vim

1
yum install -y vim

Nginx

安装 Nginx 代码仓库

1
yum install -y epel-release

安装 Nginx

1
yum install -y nginx

启动 Nginx

1
systemctl start nginx

开机启动 Nginx

1
systemctl enable nginx

Nginx 配置文件

  • /usr/share/nginx/html 默认的代码目录
  • /etc/nginx/conf.d/default.conf 默认的配置文件

MariaDB

安装 MariaDB

1
yum install -y mariadb-server mariadb

启动 MariaDB

1
systemctl start mariadb

初始化 MariaDB

1
mysql_secure_installation

开机启动 MariaDB

1
systemctl enable mariadb

PHP

安装 PHP

1
yum install -y php71w php71w-mysql php71w-xml php71w-soap php71w-xmlrpc php71w-mbstring php71w-json php71w-gd php71w-mcrypt php71w-fpm php71w-pdo php71w-pecl-redis

启动 PHP-FPM

1
systemctl start php-fpm

开机启动

1
systemctl enable php-fpm

配置 PHP 关联 Nginx,编辑 Nginx 配置文件如下:

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
server{
listen 80;
server_name cowcat.cc;
# note that these lines are originally from the "location /" block
root /usr/share/nginx/html;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}

/usr/share/nginx/html 新增文件 index.php,编辑代码如下:

1
2
<?php
phpinfo();

输入域名,检查 PHP 是否与 Nginx 关联起来

Redis

安装 Redis

1
2
3
wget http://dl.fedoraproject.org/pub/epel/7/x86_64/e/epel-release-7-9.noarch.rpm
rpm -ivh epel-release-7-9.noarch.rpm
yum install -y redis

启动 Redis

1
systemctl start redis

开机启动 Redis

1
systemctl enable redis

Beanstalkd

安装 Beanstalkd

1
2
3
4
5
wget http://cbs.centos.org/kojifiles/packages/beanstalkd/1.9/3.el7/x86_64/beanstalkd-1.9-3.el7.x86_64.rpm
wget http://cbs.centos.org/kojifiles/packages/beanstalkd/1.9/3.el7/x86_64/beanstalkd-debuginfo-1.9-3.el7.x86_64.rpm
rpm -ivh beanstalkd-1.9-3.el7.x86_64.rpm
rpm -ivh beanstalkd-debuginfo-1.9-3.el7.x86_64.rpm
yum install -y beanstalkd

启动 Beanstalkd

1
systemctl start beanstalkd

开机启动

1
systemctl enable beanstalkd

Composer

安装 Composer

1
2
3
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
composer -v

全局设置国内镜像

1
composer config -g repo.packagist composer https://packagist.phpcomposer.com

Node.js

安装 Node.js

1
2
yum install -y gcc-c++ make
yum install -y nodejs

安装 webpack、gulp、bower

1
2
3
npm install bower -g
npm install gulp -g
npm install webpack -g

Gogs

设置子域名

1
hostnamectl set-hostname git.cowcat.cc

禁用 SELinux 相关选项

1
2
sed -i /etc/selinux/config -r -e 's/^SELINUX=.*/SELINUX=disabled/g'
systemctl reboot

创建 gogs 数据库

1
2
3
CREATE DATABASE IF NOT EXISTS gogs CHARACTER SET utf8 COLLATE utf8_general_ci;
use gogs;
SET default_storage_engine=INNODB;

安装 Go

1
yum install -y go

安装 Gogs

1
2
3
4
5
6
7
8
9
10
// 下载二进制包
wget http://7d9nal.com2.z0.glb.qiniucdn.com/0.10rc/linux_amd64.zip
// 解压
unzip linux_amd64.zip
cd gogs/
// 后台运行
nohup ./gogs web &
tail -f nohup.out

访问 URL 进行安装,安装 URL 是主机 IP 地址加上 3000 端口。

以守护进程的形式运行 Gogs:https://github.com/gogits/gogs/blob/master/scripts/init/centos/gogs

Walle

进入 MySQL,新增数据库如下:

1
create database walle default character set utf8 COLLATE utf8_general_ci;

安装依赖

1
yum install ansible

代码检出

1
2
mkdir -p /data/www/ # 新建目录
git clone https://github.com/meolu/walle-web.git # 代码检出

连接数据库

1
2
3
4
5
6
7
8
9
cd walle-web/
vim config/local.php
// 修改 PHP 数组
'db' => [
'dsn' => 'mysql:host=127.0.0.1;dbname=walle', # 新建数据库walle
'username' => 'username', # 连接的用户名
'password' => 'password', # 连接的密码
],

安装 vendor

1
2
cd /data/www/walle-web
composer install --prefer-dist --no-dev --optimize-autoloader -vvvv

初始化项目

1
2
cd /data/www/walle-web
./yii walle/setup

配置 Nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server {
listen 80;
server_name walle.compony.com; # 改你的host
root /the/dir/of/walle-web/web; # 根目录为web
index index.php;
# 建议放内网
# allow 192.168.0.0/24;
# deny all;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
try_files $uri = 404;
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}

然后访问你配置好的域名或服务器地址,管理员默认账号密码是 admin/admin,开发者默认账号密码是 demo/demo

有些错误 walle 捕捉不到,默认操作日志在 /tmp/walle/ 下,具体可在 config/local.php 里 log.dir 配置路径,tail 着日志,部署看日志。

如果发现 /tmp/walle 下没有日志,很有可能是因为 CentOs 7 yum 安装的 php-fpm 默认 /tmp 目录不可写:/usr/lib/systemd/system/php-fpm.service 中的 PrivateTmp=true 禁止了向tmp目录写日志,解决方法如下:

1
2
3
4
5
vi /usr/lib/systemd/system/php-fpm.service
PrivateTmp=false
systemctl daemon-reload
systemctl reload php-fpm

Supervisor

安装 Supervisor

1
2
3
4
5
6
// supervisor 是 python 编写
yum install python-setuptools
// 安装 supervisor
easy_install supervisor
// 运行命令生成一个默认的配置文件
echo_supervisord_conf > /etc/supervisord.conf

配置 Supervisor,打开刚才生成的配置文件 vim /etc/supervisord.conf,把 include 块的注释打开,并修改如下:

1
2
[include]
files = /etc/supervisor/*.conf

使用新的配置文件启动 Supervisor

1
supervisord -c /etc/supervisord.conf

查看 Supervisor 是否正常启动

1
ps aux | grep supervisord

Vsftpd

安装 Vsftpd

1
yum install -y vsftpd

开机启动

1
systemctl enable vsftpd

修改配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
vim /etc/vsftpd/vsftpd.conf
# 编辑选项
anonymous_enable=NO
local_enable=YES
chroot_local_user=YES
chroot_list_enable=YES
chroot_list_file=/etc/vsftpd/chroot_list
# 配置文件最后添加
allow_writeable_chroot=YES

新增用户

1
2
useradd -d /data/www/ftp/ -g ftp -s /sbin/nologin ftp.cowcat.cc
passwd ftp.cowcat.cc

Alias 常用进程命令

  • Nginx
    • alias nginx.start=’systemctl start nginx’
    • alias nginx.restart=’systemctl restart nginx’
    • alias nginx.stop=’systemctl stop nginx’
    • alias nginx.status=’systemctl status nginx’
  • Mysql
    • alias mysql.start=’systemctl start mysql’
    • alias mysql.restart=’systemctl restart mysql’
    • alias mysql.stop=’systemctl stop mysql’
    • alias mysql.status=’systemctl status mysql’
  • PHP-FPM
    • alias php-fpm.start=’systemctl start php-fpm’
    • alias php-fpm.restart=’systemctl restart php-fpm’
    • alias php-fpm.stop=’systemctl stop php-fpm’
    • alias php-fpm.status=’systemctl status php-fpm’
  • Redis
    • alias redis.start=’systemctl start redis’
    • alias redis.restart=’systemctl restart redis’
    • alias redis.stop=’systemctl stop redis’
    • alias redis.status=’systemctl status redis’
  • Beanstalkd
    • alias beanstalkd.start=’systemctl start beanstalkd’
    • alias beanstalkd.restart=’systemctl restart beanstalkd’
    • alias beanstalkd.stop=’systemctl stop beanstalkd’
    • alias beanstalkd.status=’systemctl status beanstalkd’
  • Gogs
    • alias gogs.start=’/etc/rc.d/init.d/gogs start’
    • alias gogs.stop=’/etc/rc.d/init.d/gogs stop’
    • alias gogs.restart=’/etc/rc.d/init.d/gogs restart’
    • alias gogs.status=’/etc/rc.d/init.d/gogs status’

学习 UML(二)

类图

类图(class diagram)只是 UML 的一部分,但它们可能是最常用的,因为类图在描述面向对象关系时非常有用。

类、抽象类、接口

类是类图的主要部分。类用带有类名的方框来描述,如下图所示:

抽象类通常使用斜体的类名,或者增加 {abstract} 到类名下来表示,如下图所示:

接口的定义方式和类相同,但接口必须增加 <<interface>> 到类名上来表示,如下图所示:

属性

属性用于描述一个类的属性,属性直接列在类名下面的格子中。属性使用符号来表示类属性的可见性,其中 + 对应 public- 对应 private# 对应 protected,如下图所示:

操作

操作用于描述类方法,操作表示类方法可见性的办法与属性一致。类方法有参数,则包含在括号之中;类方法有返回类型,则用冒号来描述,如下图所示:

继承

UML 一般用「泛化」来描述继承关系。这个关系用从子类到父类的一条线来标识,线的顶端有一个空心闭合箭头,如下图所示:

实现

UML 用「实现」来描述接口和实现接口的类之间的关系。这个关系用从实现接口的类到接口的一条虚线来标识,线的顶端有一个空心闭合箭头,如下图所示:

关联

当一个类的属性保存了对另一个类的一个实例或多个实例的引用时,就产生了关联。类之间的关联有单向关联、双向关联和多个引用关联,如下图所示:

聚合和组合

聚合和组合都描述了一个类长期持有其他类的一个或多个实例的情况。通过聚合或组合,被引用的对象实例成为引用对象的一部分。

聚合的情况下,被包含对象是容器的一个核心部分,但是它们也可以同时被其他对象所包含。聚合关系用一条空心菱形开头的线来说明。

组合则是一个更强的关系。在组合中,被包含对象只能被它的容器所引用。当容器被删除时,它也应该被删除。组合关系可以用类聚合关系的方式描述,但是菱形必须是实心的。


描述使用

一个对象使用另一个对象的关系在 UML 中被描述为一个依赖关系。一个被使用的类可以作为类方法的参数传递或者作为方法调用的结果得到。

使用注解

类图可以捕捉到系统的结构,但类图并不能解释类处理任务的过程,可以通过使用注解来补充说明。注解由一个折角的方框组成。它通常包含伪代码片段。注解使类图变得易于理解。