Nginx 从入门到实战

一、系统环境

  • 系统版本:Centos 7.0 以上版本,64 位。
  • 环境调试
    • 四个确认
      • 确认系统网络:ping www.baidu.com
      • 确认 yum 可用:yum list | grep gcc
      • 确认关闭 iptables 规则:iptables -F && iptables -t nat -F
      • 确认停用 selinux:setenforce 0
    • 两项安装
      • yum -y install gcc gcc-c++ autoconf pcre pcre-devel make automake
      • yum -y install wget httpd-tools vim
      • yum -y install net-tools
    • 一次初始化
      • cd /opt
      • mkdir app download logs work backup
    • SSH 登录
      • 编辑 SSH 配置文件:vi /etc/ssh/sshd_config
        • Port 22
        • ListenAddress 0.0.0.0
        • ListenAddress ::
        • PermitRootLogin yes
        • PasswordAuthentication yes
      • 启用 SSH 服务:service sshd restart
      • 设置 SSH 开机启动:systemctl enable sshd.service
      • 虚拟机设置端口转发规则:https://www.jianshu.com/p/120ee4a6be9e

二、Nginx 的中间件架构

Nginx 简述

Nginx 是一个开源、高性能、可靠的 HTTP 中间件、代理服务器。

Nginx 优势

  • IO 多路复用 epoll。
  • 功能模块少、代码模块化。
  • CPU 亲和(affinity)
  • sendfile

什么是 IO 多路复用

多个描述符的 IO 操作都能在同一个线程内并发交替地顺序完成,这就叫 IO 多路复用。

什么是 epoll

IO 多路复用的实现方式。

什么是 CPU 亲和(affinity)

是一种把 CPU 核心和 Nginx 工作进程绑定凡事,把每个 worker 进程固定在一个 CPU 上执行,减少切换 CPU 的 Cache Miss,获得更好的性能。

什么是 sendfile

Nginx 安装

1). 新增 yum 源

1
vim /etc/yum.repos.d/nginx.repo

2). 编辑 yum 源

1
2
3
4
5
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/7/$basearch/
gpgcheck=0
enabled=1

3). 测试 yum 源是否可用

1
yum list | grep nginx

4). 安装 nginx

1
yum -y install nginx

Nginx 基础使用

1). 安装目录讲解

1
2
# 显示程序安装目录
rpm -ql nginx

路径 类型 作用
/etc/logrotate.d/nginx 配置文件 Nginx 日志轮转,用于 logrotate 服务的日志切割
/etc/nginx 目录 Nginx 目录
/etc/nginx/nginx.conf 配置文件 Nginx 主配置文件
/etc/nginx/conf.d 配置文件 Nginx 主配置文件
/etc/nginx/conf.d/default.conf 配置文件 Nginx 主配置文件
/etc/nginx/fastcgi-params 配置文件 cgi 配置文件
/etc/nginx/uwsgi-params 配置文件 cgi 配置文件
/etc/nginx/scgi-params 配置文件 cgi 配置文件
/etc/nginx/mime.types 配置文件 设置 HTTP 协议的 Content-Type 与扩展名对应关系
/etc/lib/systemd/system/nginx-debug.service 配置文件 用于配置出系统守护进程管理器管理方式
/etc/lib/systemd/system/nginx.service 配置文件 用于配置出系统守护进程管理器管理方式
/etc/sysconfig/nginx 配置文件 用于配置出系统守护进程管理器管理方式
/etc/sysconfig/nginx-debug 配置文件 用于配置出系统守护进程管理器管理方式
/etc/lib64/nginx/modules 目录 Nginx 模块目录
/etc/nginx/modules 目录 Nginx 模块目录
/usr/sbin/nginx 命令 Nginx 服务的启动管理的终端命令
/usr/sbin/nginx-debug 命令 Nginx 服务的启动管理的终端命令
/usr/share/doc/nginx-1.12.0 目录 Nginx 的手册和帮助文件
/var/cache/nginx 目录 Nginx 的缓存目录
/var/log/nginx 目录 Nginx 的日志目录

2). Nginx 编译参数

1
2
# 显示编译参数
nginx -V

编译选项 作用
–prefix=/etc/nginx 设定 Nginx 安装目录
–sbin-path=/usr/sbin/nginx 设定 Nginx 可执行文件安装路径
–modules-path=/usr/lib64/nginx/modules 设定 Nginx 模块目录
–conf-path=/etc/nginx/nginx.conf 设定 Nginx 配置文件路径
–error-log-path=/var/log/nginx/error.log 设定 Nginx 错误日志文件路径
–http-log-path=/var/log/nginx/access.log 设定 Nginx HTTP 请求日志文件路径
–pid-path=/var/run/nginx.pid 设定 nginx.pid 的路径
–lock-path=/var/run/nginx.lock 设定 nginx.lock 文件的路径
–http-client-body-temp-path=/var/cache/nginx 指定 HTTP 客户端请求缓存文件存放目录的路径
–http-proxy-temp-path=/var/cache/nginx/proxy_temp 指定 HTTP 反向代理缓存文件存放目录的路径
–http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp 指定 HTTP FastCGI缓存文件存放目录的路径
–user=nginx 设定 Nginx 进程启动的用户
–group-nginx 设定 Nginx 进程启动的用户组

3). Nginx 日志类型

  • error_log:存储 Nginx 错误日志
  • access_log:存储 HTTP 请求相关日志

内置的日志级别有:

  • 第一级别日志
    • stderr
    • emerg
    • alert
    • crit
    • error
    • warn
    • notice
    • info
    • debug
  • 第二级别日志
    • debug_core
    • debug_alloc
    • debug_mutex
    • debug_event
    • debug_http
    • debug_mail
    • debug_mysql
1
2
3
4
5
6
7
8
# 定义错误日志
error_log path/logs/error.log info
# 自定义日志格式
log_format customLog "$remote_addr^A$remote_user^A$time_local^A$request_method^A$uri^A$args^A$server_protocol"
"^A$status^A$body_bytes_sent^A$http_referer"
"^A$http_user_agent";
access_log path/logs/access.log customLog

4). Nginx 变量

  • HTTP 请求变量
  • 内置变量
  • 自定义变量

5). Nginx 模块

  • Nginx 官方模块
  • Nginx 第三方模块

5-1). http_stub_status_module 模块

这个模块的作用是展示 Nginx 当前处理连接的状态。

模块的配置语法:

  • Syntax: stub_status;
  • Default:
  • Context: server, location
1
2
3
location /nginx-http-stub-status-module {
stub_status;
}
  • Active connections:Nginx 当前活跃的连接数
  • server accepts handled requests
    • 第一个参数:Nginx 处理握手总次数
    • 第二个参数:Nginx 处理连接总次数
    • 第三个参数:Nginx 处理请求总次数
  • Reading
    • 第一个参数:表示正在读取的连接数量
    • 第二个参数:表示正在写入的连接数量
    • 第三个参数:表示正在等待的连接数量

5-2). http_random_index_module 模块

这个模块的作用是从指定目录中随机选择一个文件作为主页。

模块的配置语法:

  • Syntax: random_index on | off;
  • Default: random_index off;
  • Context: server, location
1
2
3
4
location /nginx-http-random-index-module {
alias /opt/app/HttpRandomIndexModule/;
random_index on;
}

5-3). http_sub_module 模块

这个模块的作用是用于 HTTP 内容替换。

模块的配置语法:

  • Syntax: sub_filter (string) (replacement);
  • Default:
  • Context: http, server, location
  • 设置字符串替换;string 是被替换的字符串,replacement 是新的字符串。

  • Syntax: sub_filter_last_modified on | off;

  • Default: sub_filter_last_modified off;
  • Context: http, server, location
  • 判断是否存在更新,存在更新则返回新的资源,否则返回缓存。

  • Syntax: sub_filter_once on | off;

  • Default: sub_filter_once on;
  • Context: http, server, location
  • 设置字符串替换是多次替换或只是替换一次。
1
2
3
4
5
6
location /nginx-http-sub-module {
alias /opt/app/HttpSubModule/;
index index.html;
sub_filter '<h1>Hello World</h1>' '<h5>Luis Edware</h5>';
sub_filter_once off;
}

5-4). limit_conn_module 模块

这个模块的作用是根据定义的键来限制每个键值的 TCP 连接数。

  • Syntax: limit_conn_zone key zone=name:size;
  • Default: none
  • Context: http

该指令描述会话状态存储区域。键的状态中保存了当前连接数,键的值可以是特定变量的任何非空值。key 定义键,zone=name 定义区域名称,size 定义各个键共享内存空间大小。如果内存空间被耗尽,服务器将会对后续所有的请求返回 503 错误。

  • Syntax: limit_conn zone number;
  • Default: none
  • Context: http, server, location

该指令给每个键值指定最大同时连接数,当超过这个数字时返回 503 错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
limit_conn_zone $binary_remote_addr zone=conn_zone:1m;
server{
...
location /nginx-limit-conn-module {
alias /opt/app/LimitConnModule/;
index index.html;
limit_conn conn_zone 1;
}
...
}

5-5). limit_req_module 模块

这个模块的作用是根据定义的键值来限制请求处理的频率。限制的方法如同漏斗,每秒固定处理请求数,推迟过多请求。

  • Syntax: limit_req_zone $variable zone=name:size rate=rate;
  • Default: none
  • Context: http

设置一块共享内存限制域用来保存键值的状态参数。速度可以设置为每秒处理请求数和每分钟处理请求数,其值必须是整数,所以如果你需要指定每秒处理少于1个的请求,2秒处理一个请求,可以使用 “30r/m”

  • Syntax: limit_req zone=name [burst=number] [nodelay];
  • Default: none
  • Context: http, server, location

设置对应的共享内存限制域和允许被处理的最大请求数阈值。 如果请求的频率超过了限制域配置的值,请求处理会被延迟,所以所有的请求都是以定义的频率被处理的。 超过频率限制的请求会被延迟,直到被延迟的请求数超过了定义的阈值,这时,这个请求会被终止,并返回503 (Service Temporarily Unavailable) 错误。这个阈值的默认值为0

1
2
3
4
5
6
7
8
limit_req_zone $binary_remote_addr zone=req_zone:1m rate=1r/s;
location /nginx-limit-req-module {
alias /opt/app/LimitReqModule/;
index index.html;
# nodelay 参数表示不希望超过的请求被延迟,burst 参数表示请求阈值。
limit_req zone=req_zone burst=5 nodelay;
}

5-6). http_access_module 模块

这个模块的作用是提供对于特定 host 的客户端的访问控制。对于使用了代理服务器的客户端的请求识别率不高。

  • Syntax: allow address | CIDR | unix: | all;
  • Default: none
  • Context: http, server, location, limit_except
1
2
3
4
5
location /nginx-http-access-module {
alias /opt/app/HttpAccessModule/;
index index.html;
allow 192.168.1.103;
}
  • Syntax: deny address | CIDR | unix: | all;
  • Default: none
  • Context: http, server, location, limit_except
1
2
3
4
5
location /nginx-http-access-module {
alias /opt/app/HttpAccessModule/;
index index.html;
deny all;
}

5-7). http_auth_basic_module 模块

这个模块的作用是让用户输入正确的账号和密码才允许访问 Web 资源。使用 htpasswd 生成账号密码文件。

  • Syntax: auth_basic string | off;
  • Default: auth_basic off;
  • Context: http, server, location, limit_except
  • Syntax: auth_basic_user_file file;
  • Default: none
  • Context: http, server, location, limit_except
1
2
3
4
5
6
location /nginx-http-auth-basic-module {
alias /opt/app/HttpAuthBasicModule/;
auth_basic "Hello World! Please input your account and password!";
auth_basic_user_file /opt/app/HttpAuthBasicModule/AccountPassword;
index index.html;
}

http_auth_basic_module 局限性

  • 用户信息依赖文件方式
  • 操作管理机械,效率低下

解决方案

  • Nginx 结合 Lua 实现高效验证
  • Nginx 结合 LDAP,利用 nginx-auth-ldap 模块

Nginx 进阶学习

一、静态资源 Web 服务

二、代理服务

三、负载均衡调度器 SLB

四、动态缓存

Laravel 项目开发常用扩展包

Laravel IDE Helper

barryvdh/laravel-ide-helper 扩展包可以通过执行命令生成 IDE Helper 文件,从而使用户的 IDE (PHPStorm, Sublme) 能够实现自动完成、代码跟踪和代码智能提示等功能,提高开发者的工作效率。

安装与使用

1). 使用 Composer 进行安装

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

2). 在文件 app/Providers/AppServiceProvider.php 中注册该扩展包

1
2
3
4
5
6
7
8
9
10
11
...
public function register()
{
// 生成环境不需要注册该服务
if ($this->app->environment() !== 'production') {
$this->app->register(\Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class);
}
}
...

3). 执行以下命令生成 IDE Helper 文件

1
php artisan ide-helper:generate

4). 也可以在文件 composer.json 进行配置,在安装或更新扩展包时自动生成 IDE Helper 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
"scripts": {
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"php artisan ide-helper:generate",
"php artisan ide-helper:meta",
"@php artisan package:discover"
]
},
...

5). 也可以生成模型字段智能提示的代码注释,安装以下扩展包并执行命令生成代码注释

1
2
3
4
5
6
7
8
9
10
composer require doctrine/dbal --dev
// 生成 app\User.php 模型代码注释
php artisan ide-helper:models User
// 生成 app\Models\Foto.php 模型代码注释
php artisan ide-helper:models "App\Models\Foto"
// 生成目录 Models 下所有模型的代码注释
php artisan ide-helper:models --dir="Models"

Laravel Debugbar

barryvdh/laravel-debugbar 扩展包可以将 Laravel 项目的各项开发信息呈现给开发者,可以让开发者快速定位问题,提高 Debug 效率。

安装与使用

1). 使用 Composer 进行安装

1
composer require barryvdh/laravel-debugbar --dev

2). 确保文件 .envAPP_DEBUG 值为 true

3). 在文件 app/Providers/AppServiceProvider.php 中注册该扩展包

1
2
3
4
5
6
7
8
9
10
...
public function register()
{
if (env('APP_DEBUG')) {
$this->app->register(\Barryvdh\Debugbar\ServiceProvider::class);
}
}
...

4). 执行以下命令完成配置

1
php artisan vendor:publish --provider="Barryvdh\Debugbar\ServiceProvider"

5). 常用方法,需要在配置文件 config/app 的数组 aliases 添加 'Debugbar' => Barryvdh\Debugbar\Facade::class,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 抛出信息
Debugbar::info($object);
Debugbar::error('Error!');
Debugbar::warning('Watch out…');
Debugbar::addMessage('Another message', 'mylabel');
// 计算耗时
Debugbar::startMeasure('render','Time for rendering');
Debugbar::stopMeasure('render');
Debugbar::addMeasure('now', LARAVEL_START, microtime(true));
Debugbar::measure('My long operation', function() {
// Do something…
});
// 抛出异常
try {
throw new Exception('foobar');
} catch (Exception $e) {
Debugbar::addThrowable($e);
}
// 启用关闭 Debugbar
Debugbar::enable();
Debugbar::disable();

多语言

overtrue/laravel-lang 是基于 caouecs/Laravel-lang、包含 52 种语言的扩展包。

安装与使用

1). 使用 Composer 进行安装

1
composer require "overtrue/laravel-lang:~3.0"

2). 安装完毕之后,将文件 config/app.php 的代码进行替换:

1
2
3
4
5
// 原来代码
Illuminate\Translation\TranslationServiceProvider::class,
// 替换代码
Overtrue\LaravelLang\TranslationServiceProvider::class,

3). 然后修改文件 config/app.php 的配置项并执行命令

1
2
3
4
5
6
7
8
...
'locale' => 'zh-CN',
...
// 需要执行的命令
php artisan lang:publish zh-CN

4). 需要额外添加语言项,请在 resources/lang/zh-CN 下建立文件即可,例如新增文件 resources/lang/zh-CN/example.php,代码如下:

1
2
3
4
5
6
<?php
return [
'hello' => '你好',
'world' => ':hello世界',
];

然后在可以使用函数 trans() 返回自定义语言项:

1
2
echo trans('example.hello');
echo trans('example.world', ['hello' => '这个棒棒的']);

验证码

mews/captcha 是一个 Laravel 5 验证码扩展包。

安装与使用

1). 使用 Composer 进行安装

1
composer require mews/captcha

2). 在文件 config/app.php 加入代码如下:

1
2
3
4
5
6
7
8
'providers' => [
// ...
Mews\Captcha\CaptchaServiceProvider::class,
],
'aliases' => [
// ...
'Captcha' => Mews\Captcha\Facades\Captcha::class,
]

3). 然后执行命令如下:

1
php artisan vendor:publish

4). 常用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 生成图片
captcha();
Captcha::create();
// 生成 URL
captcha_src();
Captcha::src();
// 生成 HTML
captcha_img();
Captcha::img();
// 使用不同配置
captcha_img('flat');
Captcha::img('inverse');

例子

以命令 php artisan make:auth 生成的代码为例,在文件 login.blade.php 添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="form-group {{ $errors->has('captcha') ? ' has-error' : '' }}">
<label for="captcha" class="col-md-4 control-label">验证码</label>
<div class="col-md-6">
<input id="captcha" class="form-control" name="captcha" >
<img class="thumbnail captcha" src="{{ captcha_src('flat') }}" onclick="this.src='/captcha/flat?'+Math.random()" title="点击图片重新获取验证码" style="margin-bottom: 0;margin-top: 10px;cursor: pointer;">
@if ($errors->has('captcha'))
<span class="help-block">
<strong>{{ $errors->first('captcha') }}</strong>
</span>
@endif
</div>
</div>

然后在验证用户登录的代码块添加表单验证规则和验证错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function rules()
{
return [
...
'captcha' => 'required|captcha',
];
}
public function messages(){
return [
...
'captcha.required' => '验证码不能为空',
'captcha.captcha' => '请输入正确的验证码',
];
}

用户切换

viacreative/sudo-su 是一个允许开发人员切换为其他用户登录的扩展包,在测试用户权限时可以大大提高开发效率。

安装与使用

1). 使用 Composer 进行安装

1
composer require viacreative/sudo-su --dev

2). 在文件 app/Providers/AppServiceProvider.php 中注册扩展包的服务提供者

1
2
3
4
5
6
7
8
9
10
...
public function register()
{
if (config('app.debug')) {
$this->app->register(\VIACreative\SudoSu\ServiceProvider::class);
}
}
...

3). 然后在视图文件中添加代码如下:

1
2
3
@if (config('app.debug'))
@include('sudosu::user-selector')
@endif

4). 发布扩展包的相关资源到项目中

1
php artisan vendor:publish

5). 修改配置文件 config/sudosu.php

1
2
3
4
5
// 允许使用扩展包的项目环境
'allowed_tlds' => ['dev', 'local'],
// 用户对应的模型
'user_model' => App\Models\User::class

学习 ES6

ES5 和 ES6 区别

ES5 和 ES6 分别实现函数默认参数

1
2
3
4
5
6
7
8
9
10
11
12
// ES5
function test(param)
{
param = param || "default param";
...
}
// ES6
function test(param = "default param")
{
...
}

ES5 和 ES6 分别实现字符串模板

1
2
3
4
5
6
7
// ES5 必须使用第三方库
var compiled = _.template("hello: <%= name %>");
compiled({name: "world"});
// ES6
var name = "world";
var string = `hello ${name}`;

ES6 特性

let 和 const

let 声明的变量只在声明的块作用域中有效

1
2
3
4
5
6
7
8
function test(){
for (let i = 1; i < 3; i++) {
console.log(i);
}
console.log(i);
}
test();

let 声明的变量不能被 let 重复声明

1
2
3
4
5
6
function test(){
let a = 1;
let a = 2;
}
test();

const 声明的变量在声明之后无法修改其赋值

1
2
3
4
5
6
7
function test(){
const PI = 3.1415926535;
PI = 8;
console.log(PI);
}
test();

const 声明的变量必须在声明时候赋值

1
2
3
4
5
6
7
function test(){
const PI;
PI = 3.1415926535;
console.log(PI);
}
test();

解构赋值

数组解构赋值

example1

1
2
3
let a, b, c;
[a, b, c] = [1, 2, 3];
console.log(a, b, c);

example2

1
2
3
let a, b, c;
[a, b, ...c] = [1, 2, 3, 4, 5];
console.log(a, b, c);

example3

1
2
3
let a, b, c, d;
[a, b, c = 3, d] = [1, 2];
console.log(a, b, c, d);

对象解构赋值
1
2
3
let a, b;
({a,b} = {a:1, b:2});
console.log(a, b);
使用场景

交换赋值

1
2
3
4
5
6
7
8
9
10
11
12
// ES5
var a = 6, b = 9, tmp;
tmp = a ;
a = b;
b = tmp;
console.log(a, b);
// ES6
let a = 6;
let b = 9;
[a, b] = [b, a];
console.log(a, b);

获取函数返回值 1

1
2
3
4
5
6
7
function test(){
return [1, 2];
}
let a, b;
[a, b] = test();
console.log(a, b);

获取函数返回值 2

1
2
3
4
5
6
7
function test(){
return [1, 2, 3, 4, 5];
}
let a, b, c;
[a,,,b] = f();
console.log(a, b);

获取函数返回值 3

1
2
3
4
5
6
7
function test(){
return [1, 2, 3, 4, 5];
}
let a, b, c;
[a,,...b] = test();
console.log(a,b);

获取对象返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
let data = {
title: 'abc',
test: [
{
title: 'efg',
description: 'Hello World'
}
]
};
let {title: esTitle, test: [{title: tsTitle, description: helloWorld}]} = data;
console.log(esTitle, tsTitle, helloWorld);
}

字符转扩展

Unicode 表示法
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
{
console.log('a', `\u0061`);
console.log('s', `\u20BB7`);
console.log('s', `\u{20BB7}`);
}
{
// 每两个字节相当于一个长度,一个字符等于两个字节,一个汉字等于两个字符
let s = '𠮷';
console.log('length', s.length);
// ES5
console.log('0', s.charAt(0));
console.log('1', s.charAt(1));
console.log('at0', s.charCodeAt(0));
console.log('at1', s.charCodeAt(1))
// ES6
let w = '𠮷A';
console.log('length', w.length);
console.log('code0', w.codePointAt(0));
console.log('code0', w.codePointAt(0).toString(16));
console.log('code1', w.codePointAt(1));
console.log('code2', w.codePointAt(2));
}
{
// ES5
console.log(String.fromCharCode("0x20bb7"));
// ES6
console.log(String.fromCodePoint("0x20bb7"));
}
遍历接口
1
2
3
4
5
6
7
8
9
10
11
12
{
// ES5
var string = '\u{20bb7}abc';
for(let i=0; i < string.length; i++){
console.log('es5', string[i]);
}
// ES6
for(let code of string){
console.log('es6', code);
}
}
新增方法
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
{
// 判断字符串是否包含 "r" 字符
let string = "string";
console.log("inculdes", string.includes("r"));
// 判断字符串起始字符串
console.log("start", string.startsWith("str"));
// 判断字符串截止字符串
console.log("start", string.endsWith("ng"));
// 重复字符串
let a = "abc";
console.log(a.repeat(2));
// 字符串模板
let name = "Luis Edware";
let info = "Hello World";
let temp = `I am ${name}, ${info}`;
console.log(temp);
// 标签模板
let user = {
name: "luis",
info: "hello world"
};
function test(s, v1, v2){
console.log(s, v1, v2);
}
test`i am ${user.name},${user.info}`;
// 字符不转义
console.log(String.raw`Hi\n${1+2}`);
console.log(`Hi\n${1+2}`);
}

数值扩展

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
{
// 按十进制格式输出二进制
console.log(0B111110111111);
// 按十进制格式输出八进制
console.log(0O767);
// 判断数值是否为无穷大
console.log('15', Number.isFinite(15));
// 判断数值是否为 NaN
console.log('NaN', Number.isNaN(NaN))
// 判断数值是否为整型
console.log('Integer 25', Number.isInteger(25));
console.log('Integer 25.0', Number.isInteger(25.0));
console.log('String 25', Number.isInteger("25"));
// 判断数值是否在安全数值范围内
console.log(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
console.log('10', Number.isSafeInteger(10));
console.log('90071992547409919', Number.isSafeInteger(90071992547409919));
// 只取整数部分
console.log(4.1, Math.trunc(4.1));
console.log(4.9, Math.trunc(4.9));
// 返回数字符号
console.log(-5, Math.sign(-5));
console.log(1, Math.sign(1));
console.log('string', Math.sign('string'));
console.log(0, Math.sign(0));
// 返回立方根
console.log('-1', Math.cbrt(-1));
console.log('8', Math.cbrt(8));
}

数组扩展

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
{
// 创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型
let arr = Array.of(1, "Hello", 2, "World");
console.log(arr);
let empty = Array.of();
console.log(empty);
// 遍历 DOM
let p = document.querySelectorAll('p');
let pArray = Array.from(p);
pArray.forEach(function(item){
console.log(item.textContent);
});
// 遍历数组
let numberArr = Array.from([1, 3, 5], function (item) {
return item*2;
});
console.log(numberArr);
// 遍历数组的索引
for (let index of ['1', 'c', 'ks'].keys()) {
console.log('key', index);
}
// 遍历数组的索引和值
for (let [index, value] of ['1', 'c', 'ks'].entries()) {
console.log('key', index);
console.log('value', value);
}
// 浅复制数组的一部分到同一数组中的另一个位置,并返回它,而不修改其大小
console.log([1, 2, 3, 4, 5].copyWithin(0, 3, 4));
// 方法返回数组中满足提供的测试函数的第一个元素的值
console.log([1, 2, 3, 4, 5, 6].find(function(item){return item > 3;}));
console.log([1, 2, 3, 4, 5, 6].findIndex(function(item){return item > 3;}));
console.log([1, 2, NaN].includes(1));
}

函数扩展

参数默认值
1
2
3
4
5
6
7
8
{
function test(x, y = 'world'){
console.log('默认值', x, y);
}
test('Hello');
test('Hello', 'Luis Edware')
}
rest 参数
1
2
3
4
5
6
7
8
9
{
function test(...arg){
for (let v of arg) {
console.log('rest', v);
}
}
test(1, 2, 3, 4, 'a');
}
扩展运算符
1
2
3
4
5
{
// 把数组转换成离散的值
console.log(...[1, 2, 4]);
console.log('a',...[1, 2, 4]);
}
箭头函数
1
2
3
4
5
6
7
8
9
{
// arrow 函数名、v 函数参数、=> 函数返回值
let arrow = v => v*2;
console.log('arrow', arrow(3));
// 当函数没有参数时,使用括号代替
let arrow2 = () => 5;
console.log('arrow2', arrow2());
}
尾调用
1
2
3
4
5
6
7
8
9
10
{
function tail(x){
console.log('tail', x);
}
function fx(x){
return tail(x);
}
fx(123);
}

对象扩展

简洁表示法
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
{
// ES5
var o = 1;
var k = 2;
var es5 = {
o: o,
k: k
}
// ES6
let es6 = {
o,
k
}
console.log(es5, es6);
// ES5
var es5Method = {
hello: function(){
console.log('hello');
}
}
// ES6
let es6Method = {
hello(){
console.log('world');
}
}
console.log(es5Method, es6Method);
}
属性表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
// 属性表达式
let a = 'b';
let es5Object = {
a: 'c',
b: 'c'
};
function test(){
return 'c';
}
let es6Object = {
[a]: 'c',
[test()]: 'd'
};
console.log(es5Object, es6Object);
}
扩展运算符
1
2
3
4
5
6
7
8
9
{
let {a, b, ...c} = {
a: 'test',
b: 'kill',
c: 'dddd',
d: 'ffff'
}
console.log(a, b, c)
}
Object 新增方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
// Object is 和全等判断的功能没有区别
console.log('字符串', Object.is('abc', 'abc'), 'abc' === 'abc');
// 两个数组引用的是两个不同的内存地址
console.log('数组', Object.is([], []), [] === []);
// 拷贝
console.log('拷贝', Object.assign({a: 'a'}, {b: 'b'}));
// 循环
let test = {k: 123, o: 456};
for (let [key, value] of Object.entries(test)) {
console.log([key, value]);
}
}

Symbol

Symbol 的概念

Symbol 是一种基本数据类型

Symbol 的作用

Symbol() 函数会返回 Symbol 值,每个值都是唯一的。一个 symbol 值能作为对象属性的标识符。

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
{
let a1 = Symbol();
let a2 = Symbol();
console.log(a1 === a2);
let a3 = Symbol('foo');
let a4 = Symbol('foo');
console.log(a3 === a4);
}
{
let a1 = Symbol.for('abc');
let obj = {
[a1]: '123',
'abc': 345,
'c': 456
}
console.log(obj);
// 无法遍历 Symbol 类型
for (let [key, value] of Object.entries(obj)) {
console.log(key, value);
}
// 只能遍历 Symbol 类型
Object.getOwnPropertySymbols(obj).forEach(function(item){
console.log(obj[item]);
})
// 全部遍历
Reflect.ownKeys(obj).forEach(function(item){
console.log('ownkeys', item, obj[item]);
})
}

数据结构

Set

Set 对象是值的集合,你可以按照插入的顺序迭代它的元素。 Set中的元素只会出现一次,即 Set 中的元素是唯一的。

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
{
let list = new Set();
list.add(1);
list.add(3);
list.add(5);
list.add(7);
console.log('size', list.size); // size 4
console.log('length', list.length); // length undefined
}
{
let arr = [1, 2, 3, 4, 5, 1, 2];
let list = new Set(arr);
// 重复的数组元素会被过滤掉
console.log(list); // Set { 1, 2, 3, 4, 5 }
}
{
let arr = ['add', 'delete', 'clear', 'has'];
let list = new Set(arr);
console.log('has', list.has('has'));
console.log('delete', list.delete('add'), list);
list.clear()
console.log('clear', list);
}
{
// Set 遍历
let arr = ['add', 'delete', 'clear', 'has'];
let list = new Set(arr);
for (let key of list.keys()) {
console.log('key', key);
}
for (let value of list.values()) {
console.log('value', value);
}
for (let item of list) {
console.log('item', item);
}
for (let [key, value] of list.entries()) {
console.log('entries', key, value);
}
list.forEach(function(item){
console.log(item);
});
}

WeakSet

WeakSet 和 Set 的区别:WeakSet 只能存储对象类型的数据。WeakSet 存储对象数据时是基于弱引用,不会检测对象数据是否被垃圾回收。WeakSet 类型没有 clear() 方法,WeakSet 不能遍历。

Map

new Map([iterable]),Iterable 可以是一个数组或者其他 iterable 对象,其元素或为键值对,或为两个元素的数组。 每个键值对都会添加到新的 Map。null 会被当做 undefined。

1
2
3
4
5
6
7
8
9
10
11
{
let map = new Map();
let arr = ['123'];
map.set(arr, 456);
console.log(map, map.get(arr));
}
{
let map = new Map([['a',123],['b',456]]);
console.log('map args', map);
console.log('size', map.size)
}
WeakMap
  • 只能存储对象类型数据
  • 没有 clear() 方法
  • 不能遍历
Map 与 Array 的对比
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
{
// 增删查改对比
let map = new Map();
let arr = [];
// 增
map.set('t', 1);
arr.push({t: 1});
console.info('map-array', map, arr);
// 查
let map_exist = map.has('t');
let arr_exist = arr.find(item => item.t);
console.info('map-array', map_exist, arr_exist);
// 改
map.set('t', 2);
arr.forEach(item => item.t ? item.t = 2 : '');
console.info('map-array', map, arr);
// 删
map.delete('t');
let index = arr.findIndex(item => item.t);
arr.splice(index, 1);
console.info('map-array', map, arr);
}
Set 与 Array 的对比
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
{
// 增删查改对比
let set = new Set();
let arr = [];
// 增
obj = {t:1};
set.add(obj);
arr.push(obj);
console.log('set-array', set, arr);
// 查
let setExist = set.has(obj);
let arrExist = arr.find(item => item.t);
console.log('set-array', setExist, arrExist);
// 改
set.forEach(item => item.t ? item.t = 2 : '');
arr.forEach(item => item.t ? item.t = 2 : '');
console.log('set-array', set, arr);
// 删
set.forEach(item => item.t ? set.delete(obj) : '');
let index = arr.findIndex(item => item.t);
arr.splice(index, 1);
console.info('set-array', set, arr);
}
Map、Set 与 Object 对比
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
{
// map、set、object 对比
let item = {t:1};
let map = new Map();
let set = new Set();
let obj = {};
// 增
map.set('t', 1);
set.add(item);
obj['t'] = 1;
console.log('map-set-obj', map, set, obj);
// 查
console.info({
mapExist: map.has('t'),
setExist: set.has(item),
objExist: 't' in obj
});
// 改
map.set('t', 2);
item.t = 2;
obj['t'] = 2;
console.info('map-set-obj-modify', map, set, obj);
// 删除
map.delete('t');
set.delete(item);
delete obj['t'];
console.info('map-set-obj-delete', map, set, obj);
}

以后使用数据结构时,优先考虑 Set 和 Map 数据结构,然后才是 Array 和 Object 数据结构

Proxy 和 Reflect

Proxy 概念、适用场景
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
{
// 原始对象
let obj = {
time: '2017-03-11',
name: 'net',
_r: 123
}
// 通过 Proxy 创建一个代理对象 monitor
let monitor = new Proxy(obj, {
// 拦截对象属性读取
get(target, key){
if (key === 'time') {
return target[key].replace('2017', '2888');
}
},
// 拦截对象设置属性
set(target, key, value){
if (key === 'name') {
return target[key] = value;
}else{
return target[key];
}
},
// 拦截 key in Object 操作
has(target, key){
if (key === 'name') {
return target[key]
}else{
return false;
}
},
// 拦截删除操作
deleteProperty(target, key){
if (key.indexOf('_') > -1) {
delete target[key];
return true;
}else{
return target[key];
}
},
// 拦截 keys
ownKeys(target){
return Object.keys(target).filter(item => item != 'time');
}
});
// 用户通过访问 monitor 来操作 obj
console.log(monitor.time);
console.log(monitor.name = "Hello World");
console.log(monitor._r = "_r", obj);
console.log('time' in monitor, 'name' in monitor);
console.log(delete monitor['_r']);
console.log(delete monitor['name'], monitor);
console.log(Object.keys(monitor));
}
Reflect 概念、适用场景
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
{
let obj = {
time: '2017-03-11',
name: 'net',
_r: 123
}
console.log("Reflect get", Reflect.get(obj, 'time'));
Reflect.set(obj, 'name', 'MuKeWang');
console.log(obj);
console.log(Reflect.has(obj, 'name'));
}
{
// 代理验证
function validator(target, validators){
return new Proxy(target, {
_validators: validators,
set(target, key, value, proxy){
if (target.hasOwnProperty(key)) {
let va = this._validators[key];
console.log(va);
if (!!va(value)) {
return Reflect.set(target, key, value, proxy)
}else{
throw Error(`不能设置 ${key}${value}`);
}
}else {
throw Error(`${key} 不存在`);
}
}
});
}
const personValidators = {
name(val){
return typeof val === 'string';
},
age(val){
return typeof val === 'number' && val > 18;
}
}
class Person{
constructor(name, age){
this.name = name;
this.age = age;
return validator(this, personValidators);
}
}
const person = new Person('LuisEdware', 12);
console.log(person);
console.log(person.age = 48);
}

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
{
// 基础定义
class Parent{
constructor(name = "LuisEdware"){
this.name = name;
}
}
let parent = new Parent('HelloWorld');
console.log(parent);
// 继承
class Child extends Parent{
constructor(name){
// super 一定要放在构造函数的第一行
super(name);
this.type = 'child';
}
}
let child = new Child();
console.log(child);
}
{
// Getter Setter
class Parent{
constructor(name = 'Luis'){
this.name = name;
}
get firstName(){
return "Luis";
}
get fullName(){
return this.name + 'Edware';
}
set firstName(value){
return this.name = value;
}
}
parent = new Parent();
console.log(parent.firstName = "I Love You,");
console.log(parent.fullName);
}
{
class Parent{
constructor(name = 'Luis'){
this.name = name;
}
// 静态方法
static tell(){
console.log('Do you like what do you see?');
}
}
// 静态属性
Parent.type = "static Propertory";
Parent.tell();
console.log('静态属性', Parent.type);
}

Promise

Promise 对象用于一个异步操作的最终完成(或失败)及其结果值的表示。(简单点说就是处理异步请求。

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
{
// 基础实现模拟 Ajax 请求
let ajax = function(callback){
console.log('执行1');
setTimeout(function(){
callback && callback.call();
}, 2000);
}
ajax(function(){
console.log('TimeOut 2');
});
}
{
// Promise 实现模拟 Ajax 请求
let ajax = function(){
console.log('执行2');
return new Promise(function(resolve, reject){
setTimeout(function () {
resolve();
}, 4000)
})
}
ajax().then(function(){
console.log("TimeOut 4");
},function(){
});
}
{
// 串行 Promise
let ajax = function(){
console.log('执行3');
return new Promise(function(resolve, reject){
setTimeout(function () {
resolve();
}, 1000)
})
}
ajax().then(function(){
console.log("执行 3-1");
return new Promise(function(resolve, reject){
setTimeout(function () {
resolve();
}, 1000)
});
}).then(function(){
console.log("执行 3-2");
});
}
{
// Promise 捕获错误
let ajax = function(num){
console.log('执行4');
return new Promise(function(resolve, reject){
if (num > 5) {
resolve();
}else{
throw new Error("出现错误");
}
});
}
ajax(6).then(function(){
console.log('log', 6);
}).catch(function(error){
console.log('catch', error);
});
ajax(3).then(function(){
console.log('log', 3);
}).catch(function(error){
console.log('catch', error);
});
}
{
// 所有图片加载完毕之后再添加到页面
function loadImg(src){
return new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = src;
img.onload = function(){
resolve(img);
}
img.onerror = function(){
reject(err);
}
})
}
function showImgs(imgs){
imgs.forEach(function(img){
document.body.appendChild(img);
})
}
Promise.all([
loadImg('https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1514996206326&di=167c5eb9be5db7f9dd33714cfd7bb271&imgtype=0&src=http%3A%2F%2Fimg.shouyoutan.com%2FUploads-s%2Fnews%2F2017-08-03%2F5982c65433748.jpg'),
loadImg('https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1514996240993&di=8f5bbd56e43773a3bfe6f7c28c88d219&imgtype=jpg&src=http%3A%2F%2Fimg4.imgtn.bdimg.com%2Fit%2Fu%3D1635656209%2C4021449300%26fm%3D214%26gp%3D0.jpg'),
loadImg('https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1514996213584&di=96bc51499c4786b86a4f158111dd8014&imgtype=0&src=http%3A%2F%2Fxs.dmzj.com%2Fimg%2F127%2F19%2F4409de1973e9ee2f2ddec617dcfebb6d.jpg')
]).then(showImgs);
}
{
// 有一个图片加载完毕之后就添加到页面,并停止加载
function loadImg(src){
return new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = src;
img.onload = function(){
resolve(img);
}
img.onerror = function(){
reject(err);
}
})
}
function showImgs(img){
let p = document.createElement('p');
p.appendChild(img);
document.body.appendChild(p);
}
Promise.race([
loadImg('https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1514996206326&di=167c5eb9be5db7f9dd33714cfd7bb271&imgtype=0&src=http%3A%2F%2Fimg.shouyoutan.com%2FUploads-s%2Fnews%2F2017-08-03%2F5982c65433748.jpg'),
loadImg('https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1514996240993&di=8f5bbd56e43773a3bfe6f7c28c88d219&imgtype=jpg&src=http%3A%2F%2Fimg4.imgtn.bdimg.com%2Fit%2Fu%3D1635656209%2C4021449300%26fm%3D214%26gp%3D0.jpg'),
loadImg('https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1514996213584&di=96bc51499c4786b86a4f158111dd8014&imgtype=0&src=http%3A%2F%2Fxs.dmzj.com%2Fimg%2F127%2F19%2F4409de1973e9ee2f2ddec617dcfebb6d.jpg')
]).then(showImgs);
}

模块化

  • export 语句用于在创建 JavaScript 模块时,从模块中导出函数、对象或原始值,以便其他程序可以通过 import 语句使用它们。
  • import 语句用于导入由另一个模块导出的绑定。

export

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
export let a = 123;
export function test(){
console.log('test');
}
export class Hello{
test(){
console.log('class');
}
}
// 上述代码等同于如下代码
let a = 123;
let test = function(){
console.log('test');
}
class Hello{
test(){
console.log('class');
}
}
export default {
a,
test,
Hello
}

import

1
2
3
4
5
import {a, test, Hello} from 'file.js';
import * as all from 'file.js';
// 使用 default 可以自命名导入变量名称
import Luis from 'file'.js

PHP 秒杀系统

  • 秒杀系统简介
  • 系统环境搭建
  • 系统设计
  • 前端后台功能开发
  • 单机优化、分布式方案、防外挂机器

秒杀系统简介

秒杀系统特点

  • 人多商品少
  • 时间短流量高
  • 外挂机器

秒杀系统技术分析

  • 瞬时高并发的处理能力
  • 多层次的分布式处理能力
  • 人机交互与对抗

系统环境搭建

技术选型分析:

  • Linux + Nginx + PHP + MySQL + Redis
  • CDN,智能 DNS
  • 分布式缓存,全国多节点
  • 多线路接入
  • 负载均衡 LVS
  • 大型 Web 集群

系统设计

基本功能和流程

  • 后台:活动管理、商品管理、订单管理、日志管理
  • 前台:商品展示、秒杀、购物车、我的订单

秒杀 -> 问答验证 -> 库存验证 -> 生成订单 -> 成功

  • 问答验证
    • 用户登录验证
    • 用户黑名单验证
    • 用户参数验证
  • 库存验证
    • 单件商品库存验证
    • 多个商品库存验证
    • 商品赠品库存验证
  • 订单验证
    • 商品明细验证
    • 订单信息验证

数据库设计

  • 活动信息表 activities
    • id - 活动 ID
    • title - 活动名称
    • start_time - 活动开始时间
    • end_time - 活动结束时间
    • status - 活动状态 - 0 待上线,1 已上线,2 已下线
    • creator_ip - 创建人 IP
    • created_at - 创建时间
    • updated_at - 更新时间
  • 商品信息表 goods
    • id - 商品 ID
    • activity_id - 活动 ID
    • title - 商品名称
    • description - 商品描述
    • icon - 商品小图
    • normal_price - 原价
    • discount_price - 秒杀价
    • total_num - 总数量
    • user_num - 单个用户限购数量
    • left_num - 剩余可购买数量
    • status - 商品状态 - 0 待上架,1 已上架,2 已下架
    • creator_ip - 创建人 IP
    • created_at - 创建时间
    • updated_at - 更新时间
  • 问答信息表 questions
    • id
    • activity_id
    • title
    • question 1
    • answer 1
    • question 10
    • answer 10
    • status - 问答状态 - 0 正常,1 删除
    • creator_ip - 创建人 IP
    • created_at - 创建时间
    • updated_at - 更新时间
  • 订单信息表
    • id - 订单 ID
    • activity_id - 活动 ID - 添加普通索引
    • goods_id - 商品 ID - 添加普通索引
    • userid - 用户 ID - 添加普通索引 user_name - 用户名称
    • total_num - 购买商品数量
    • goods_num - 购买商品种类数量
    • total_price - 订单总金额
    • discount_price - 订单优惠后金额
    • confirmed_at - 确认订单时间
    • paid_at - 订单支付时间
    • overed_at - 订单过期时间
    • canceled_at - 订单取消时间
    • goods_info - 订单商品详情
    • status - 订单状态 - 1 待支付,2 已支付,3 已过期,4 管理员已确认,5 已取消,6 已删除,7 已发货,8 已收货,9 已完成
    • creator_ip - 创建人 IP
    • created_at - 创建时间
    • updated_at - 更新时间

秒杀功能开发

  • 商品展示页面静态化
  • 商品状态控制
  • 商品秒杀业务逻辑
    • 验证用户是否登陆
    • 验证提交参数是否正确合法
    • 验证用户是否已经购买
    • 验证问答信息是否正确
    • 验证活动信息,商品信息是否正常
    • 验证用户购买的商品数量是否在限制的范围内
    • 验证商品是否还有剩余数量
    • 扣除商品剩余数量
    • 创建订单
    • 返回提示信息

单商品秒杀

单商品特点

  • 简单,没有更多选择
  • 独立,没有关联关系
  • 容易,验证逻辑更少

万次秒杀

  • 请求少,并发,实现了基本功能就可以满足
  • 不需要太多考虑优化方面
  • 单机,MySQL 数据库就可以支持

百万次秒杀

  • 请求量和并发量较大
  • Web 服务器集群
  • 引入 Redis 缓存

组合商品秒杀

组合商品特点

  • 支持多商品的选择
  • 多个商品的库存、限购数量
  • 验证和处理的逻辑更多

优化单机性能

  • 减少页面大小,启用 gzip 压缩
  • 减少资源请求数量,合并和压缩 CSS,JavaScript
  • 设置浏览器缓存,利用 CDN 加速,Nginx 配置文件缓存
  • 数据库索引
  • 减少数据规模,数据清理

用户行为分析

  • 用户访问页面的路径
  • 页面停留时间、点击位置和时间
  • 用户基本信息、用户 IP、浏览器信息等

PHP 面试考察点

PHP 招聘要求总结

硬性要求

  • 熟悉常用的算法和数据结构
  • 熟悉(精通) PHP
  • 熟悉 PHP 框架:Laravel、Yii
  • 熟悉 HTML 5 + CSS 3
  • 熟悉 CSS 框架:Bootstrap
  • 熟悉(精通) JavaScript
  • 熟悉 JavaScrip 框架:Vue.js、React.js
  • 熟悉 设计模式、熟悉 MVC 模式
  • 熟悉 HTTP、HTTP 2、TCP、UDP 等标准协议
  • 熟悉 Apache、Nginx 等 Web 服务器的配置、调优及问题定位分析
  • 熟悉 MySQL,熟悉 SQL 调优和数据结构设计
  • 熟悉 NoSQL 数据库:Redis、MongoDB
  • 熟悉 Linux,了解 Linux 常用命令及工具
  • 熟悉 Web 安全相关内容并有相关实践经验
  • 熟悉前端性能优化
  • 有高性能数据库设计的开发经验
  • 有高并发、高访问量的开发经验

加分项

  • 熟悉 PHP7、ES6、TypeScript
  • 熟悉 NodeJS
  • Elasticsearch

PHP 面试考察点

  • PHP 框架考察点
  • JavaScript、jQuery、AJAX 基础知识考察点
  • Linux 基础知识考察点
  • MySQL 数据库考察点
    • MySQL 基础知识
    • MySQL 高性能索引创建
    • MySQL SQL 语句编写和优化
    • MySQL 的高可扩展和高可用及安全性
  • 程序设计考察点
  • 数据结构、常见算法考察点
    • 常见数据结构特征
    • 算法的工作原理
    • 时间复杂度
    • 空间复杂度
    • 其他逻辑算法
    • PHP 内置函数实现
  • 高并发解决方案考察点
    • 如何理解高并发
      • PV
      • UV
      • QPS
    • QPS 阶段性优化
    • 优化案例
      • 防盗链
      • 减少 HTTP 请求
      • 浏览器缓存
      • CDN
      • 数据库缓存(Memcache/NoSQL)
      • MySQL 的读写分离
      • 分区以及分库分表
      • LVS 负载均衡
  • 面前准备与面试注意事项

JavaScript、jQuery、AJAX

JavaScript、jQuery

  • 回顾真题:下列不属于 JavaScript 语法关键/保留字的是(var、$、function、while)
  • 考点分析:JavaScript 基本语法
  • 知识延伸:
    • JavaScript 内置对象
    • JavaScript HTML DOM 对象
    • jQuery 基础知识

实践真题

1
2
3
4
/**
* JavaScript 中为 id 是 test 的元素设置样式为 good
* 要求使用 jQuery 事件写在页面元素加载完成之后,动态绑定 click 事件到 btnOK 元素
*/

AJAX

  • 回顾真题:AJAX 技术利用了什么协议?简述 AJAX 的工作机制
    • 概念:Asynchronous JavaScript and XML
    • 作用:通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新
  • 考点分析:AJAX 基本工作原理
    • XMLHttpRequest 对象是 AJAX 的基础
      • XMLHttpRequest 对象请求方法:open(method,url,async)、send(string)
      • XMLHttpRequest 对象响应
        • 属性:responseText、responseXML,readyState
        • 事件:onreadystatechange
  • 知识延伸:
    • 原生 JavaScript AJAX 操作
    • jQuery 的 AJAX 操作
      • $(ele).load();
      • $.ajax()
      • $.get()
      • $.post()
      • $.getJSON()
      • $.getScript()

实践真题

1
2
3
// 写出原生 JavaScript AJAX 操作 TODO
// 写出 jQuery 处理 AJAX 的方法 TODO

数据结构、常见算法考察点

常见算法

  • 回顾真题:请写出常见的排序算法,并用 PHP 实现冒泡排序,将数组按照从小到大的方式进行排序。
  • 考官考点:
    • 冒泡排序原理和实现
  • 知识延伸:
    • 算法的概念
    • 时间复杂度和空间复杂度的概念
    • 常见排序算法
    • 常见查找算法
  • 解题方法:此类考点非常重要也比较复杂,需要考生充分理解各种排序算法和查找算法的原理以及实现方式,另外还需要理解时间复杂度和空间复杂度的计算方式和概念,此类考察点毋庸置疑是考察考生的逻辑思维能力,因此需要大家仔细研究各种算法的实现方式。
  • 一网打尽:

    • 请简述时间复杂度和空间复杂度概念
    • 对无序数组排序,最优的时间复杂度是什么,用 PHP 或者 JavaScript 写出一个实际的例子,该算法的空间复杂度是什么?
    • 一个有序数组中,查询特定 item 是否存在的最优算法是什么?时间复杂度是什么?
  • 算法就是任何良定义的计算过程,该过程取某个值或值的集合作为输入并产生某个值或值的集合作为输出。

  • 一个问题可以有多种算法,每种算法都有不同的效率。
  • 一个算法具有五个特征:有穷性、确切性、输入项、输出项、可行性。
时间负责度和空间复杂度的概念

算法评定:算法评定的目的在于选择合适算法和改进算法。一个算法的评价主要从时间复杂度和空间复杂度来考虑。

时间复杂度:执行算法所需要的计算工作量。一般来说,计算机算法是问题规模 n 的函数 f(n),算法的时间复杂度也因此记做 T(n) = O(f(n))。问题的规模 n 越大,算法执行的时间的增长率与 f(n) 的增长率正相关,称作渐进时间复杂度。

时间复杂度计算方式:O(n^2)、O(1)、O(n)?,第一,得出算法的计算次数公式。例如 1+2+3+...+n,相当于 for($i = 1; $i <= $n; $++){$sum += $i;},这个算法的计算次数为 n 次,那么这个算法的时间复杂度是 O(n)。第二,用常数 1 来取代所有时间当中的所有加法常数,例如上述算法,不管 $i 等于多少,都只计算 $i + 3 次,那么这个算法的时间复杂度不能为 O(3),而是 O(1)。第三,在修改后的运行次数函数中,只保留最高阶项,比如最终计算次数是 n^2+n+1,那么时间复杂度为:O(n^2)。第四,如果最高阶存在且不是1,则去除与这个项相乘的常数,比如最终计算次数是 2n^+3n+1,那么时间复杂度为:O(n)

时间复杂度举例

常数阶:O(1)

1
2
3
4
5
6
function test($n){
echo $n;
echo $n;
echo $n;
}
test();

线性阶:O(n)

1
2
3
```
平方阶/立方阶:O(n^2)/O(n^3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
对数阶:O(log2n)
```php
<?php
$n = 20;
while($n >= 1){
$n = $n/2;
}
/**
* n
* n/2
* n/2/2
* n/2/2/...
*
* n/(2^m) = 1
* 2^m = n
* m = log2n 或 m = logn
*/
  • 常见时间复杂度:
    • 常数阶
    • 线性阶
    • 平方阶
    • 立方阶
    • 对数阶
    • nlog2n 阶
    • 指数阶
  • 效率从高到底
    • O(1)
    • O(log2n)
    • O(n)
    • O(n^2)
    • O(n^3)
    • O(2^n)
    • O(n!)
    • O(n^n)

空间复杂度:算法需要消耗的内存空间,记作 S(n)=O(f(n))。包括程序代码所占用的空间,输入数据所占用的空间和辅助变量所占用的空间这三个方面。计算和表示方法与时间复杂度类似,一般用复杂度的渐进性来表示。

排序算法

  • 冒泡排序
  • 直接插入排序
  • 希尔排序
  • 选择排序
  • 快速排序
  • 堆排序
  • 归并排序

冒牌排序

原理:两两相邻的数进行比较,如果反序就交换,否则不交换。时间复杂度:最坏 O(n^2),平均 O(n^2);空间复杂度:O(1)。

1
<?php

直接插入排序

原理:每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。时间复杂度:最坏 O(n^2) 平均 O(n^2);空间复杂度:O(1)。

1
<?php

希尔排序

原理:把待排序的数据根据增量分成几个子序列,对子序列进行插入排序,直到增量为 1,直接进行插入排序;增量的排序,一般是数据的长度的一半,再变为原来增量的一半,直到增量为 1。时间复杂度:最坏 O(n^2),平均 O(nlog2n);空间复杂度:O(1)。

1
<?php

选择排序

原理:每次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。时间复杂度:最坏 O(n^2),平均 O(n^2)。空间复杂度:O(1)。

1
<?php

快速排序

原理:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都要比另外一部分的所有数据都要小,然后再按照此方法对这两部分数据分别进行快速排序,整个排序过程可以递归完成。时间复杂度:最差 O(n^2),平均 O(nlog2n);空间复杂度是 最差 O(n),平均 O(log2n)

1
<?php

堆排序

原理:把待排序的元素按照大小在二叉树位置上排列,排序好的元素要满足:父节点的元素要大于等于子节点;这个过程叫做堆化过程,如果根节点存放的是最大的数,则叫做大根堆,如果是最小,就叫小跟堆,可以把根节点拿出来,然后再堆化,循环到最后一个节点。时间复杂度:最差 O(nlog2n),平均 O(nlog2n)。空间复杂度:O(1)

1
<?php

归并排序

原理:将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个有序的子序列,再把有序的子序列合并为整体有序序列。时间复杂度:最差 O(nlog2n),平均 O(nlog2n)。 空间复杂度是:O(n)。

1
<?php

查找算法:

  • 二份查找
  • 顺序查找

二分查找

原理:从数组的中间元素开始,如果中间元素正好是要查找的元素,搜索结束。如果某一个特定元素大于或者小于中间元素,则在数组大于或者小于中间元素的那一半中查找,而且跟开始一样从中间开始比较,如果某一步骤数组为空,代表找不到。时间复杂度:最差 O(log2n),平均 O(log2n)。空间复杂度:迭代 O(1),递归 O(log2n)

1
<?php

顺序查找

原理:按一定的顺序检查数据中每一个元素,直到找到所需要寻找的特定值为止。时间复杂度:最差 O(n),平均 O(n);空间复杂度:O(1)。

1
<?php

总结:二分查找算法的时间复杂度最差是 O(log2n),顺序查找的时间复杂度是 O(n),所以二份查找算法更快,但是递归情况下,二分查找算法更加消耗内容,时间复杂度为 O(log2n)。

Web API 的设计与开发

什么是 Web API

本文中的 Web API 是指「使用 HTTP 协议通过网络调用的 API」,简而言之,Web API 就是一个 Web 系统,通过访问 URI 可以与服务器完成信息交互,或者获得存放在服务器的数据信息等,这样调用者通过程序进行访问后即可机械地使用这些数据。

设计优美的 Web API 的重要性

  • 设计优美的 Web API 易于使用
    • 提升开发效率
    • 减少开发周期
  • 设计优美的 Web API 便于更改
    • 避免更改之后无法使用
    • 尽量不影响正在使用的用户
  • 设计优美的 Web API 健壮性好
    • 安全可靠,难以破解
    • 考虑 API 特有安全问题
  • 设计优美的 Web API 不怕公之于众
    • 不怕受到质疑
    • 拥有技术影响

如何设计优美的 Web API

  1. 首先需要决定的是将什么样的信息,通过 API 公开。
  2. 其次考虑作为访问目标的端点。
  3. 然后考虑交互方式与合适的响应数据格式。
  4. 最后还需要考虑安全性以及访问控制等相关内容。

两个重要的设计原则,如下所示:

  • 设计规范明确的内容必须遵守相关规范。
  • 没有设计规范的内容必须遵守相关事实标准。

端点的设计与请求的形式

本文中的端点是指用于访问 API 的 URI。一般而言,因为 API 将各种不同的功能进行了封装,所以会拥有多个不同的端点。
(URI:统一资源标志符,一个用于标识某一互联网资源名称的字符串)

API 端点基本原则

优秀的 URI 设计,有一个非常重要的原则:容易记忆,URI 包含的功能一目了然。可以把这一条原则细化成多个小原则,如下所示:

HTTP 方法和 API 端点

HTTP 方法是进行 HTTP 访问时指定的操作,URI 和 HTTP 方法之间的关系可以认为操作对象和操作方法的关系。
如果把 URI 当作 API 的「操作对象 = 资源」,那么 HTTP 方法则表示「进行怎样的操作」。通过用不同的方法访问一个端点,不但可以获取信息,还能修改信息,删除信息。

HTTP 常用方法如下:

  • GET:获取信息
  • POST:创建信息
  • PUT:更新覆盖信息
  • PATCH:更新部分信息
  • DELETE:删除信息

HTML 文档 Form 元素仅仅支持 GET 和 POST 方法,想要用其他 HTTP 方法,实现方式有两种,都是基于 POST 方法进行实现:

  • 通过 _method 参数来实现
  • 通过 X-HTTP-Method-Override HTTP Header 实现

HTTP 方法和API 端点的设计

目前成型的 API 设计模式如下:

获取单个数据

规范:GET http://api.example.com/v1/users/:id

各大公司的实现方式如下所示:

  • Twitter:/statuses/retweets/123456.json
  • LinkedIn:/companies/123456
  • Foursquare:/venues/123456

获取数据集合

规范:GET http://api.example.com/v1/users

各大公司的实现方式如下所示:

  • Twitter:/statuses/mentions_timeline.json
  • YouTube:/activities
  • LinkedIn:/companies
  • Foursquare:/venuegroups/list
  • Disqus:/blacklists/list.json

端点设计注意事项

  • 使用名词的复数形式
    • 复数形式表示资源的集合
    • 复数形式与数据库表名一致更加恰当
    • 复数形式表示名词等于资源,HTTP 方法表示动词等于操作,这相当于用最简洁的方式描述对资源进行那些操作
  • 注意所用的单词
    • 所选用的单词要符合 API 的语义性
  • 不使用空格及需要编码的字符
    • 不够一目了然
    • 可能无法访问资源
  • 使用连接符来连接多个单词
    • 方法
      • 蛇形法:http:/api.example.com/v1/users/1/profile_image
      • 驼峰法:http:/api.example.com/v1/users/1/profileImage
      • 脊柱法:http:/api.example.com/v1/users/1/profile-image
      • 点分法:http:/api.example.com/v1/users/1/profile.image
    • 事实标准
      • Twitter:/statuses/user_timeline
      • YouTube:/guideCategories
      • Facebook:/me/books.quotes
      • LinkedIn:/v1/people-search
      • Bit.ly:/v3/user/popular_earned_by_clicks
      • Disqus:/api/3.0/applications/listUsage.json
    • 推荐使用连字符;原因是 Google 推荐使用连字符,使用连字符对 SEO 友好;其次 URI 里的主机名(域名)允许使用连字符而禁止使用下划线,且不区分大小写。而且点子符具有特殊含义。
      因此,为了使用和主机名一致的规则来统一 URI 命名,用连字符连接多个单词最适合不过。

搜索与查询参数的设计

获取数据量和获取位置的查询参数。这两个参数就是俗称的「分页」参数。各大公司的示例如下:

在线服务名称 获取数据量 获取相对位置 获取绝对位置
Twitter count cursor max_id
YouTube maxResults pageToken publishedBefore / publishedAfter
Flickr per_page page max_upload_date
LinkedIn count start
Instagram max_id
Last.fm limit page
eBay paginationInput.entriesPerPage paginationInput.pageNumber
del.icio.us count / results start
bit.ly limit offset
Tumblr limit offset since_id
Disqus limit offset
Github per_page page
Pocket limit offset
Etsy limit offset

从中可以看出一般在线服务会用 limit、count 和 per_page 来表示获取的数据量,而使用 page、offset 和 cursor 来表示获取数据的位置。

使用相对位置存在的问题

  • 在 MySQL 等 RDB 中,当使用 offset 或 limit 来获取指定的数据位置时,随着数据量的增加,响应速度会不断下降。
  • 如果数据更新的频率很高,会导致当前获取的数据出现一定的偏差。

用于过滤的参数。在 API 里设置过滤条件,以此来实现搜索用户的功能。各个在线服务实现如下:

查询参数和路径的使用区别

在设计 URI 时,必须决定是把客户端指定的特定参数放在查询参数里还是路径里,决策的依据有以下两点:

  • 是否是表示唯一资源所需的信息
  • 是否可以省略

首先第一点提到了资源是否唯一,这主要基于「URI 表示资源」这一根本思想。像用户 ID 能够表示资源的唯一性,将用户 ID 放在路径中就比较合适。然后是否可以省略,比如罗列、搜索时用到的 offset、limit 或 page 参数,如果忽略,很多情况下都会启用默认值
而不会出错,所以放在查询参数里更为合适。

登录与 OAuth 2.0

OAuth 一般用于面向第三方大范围公开的 API 中的认证工作。

OAuth 2.0 的认证流程(Grant Type)有:

  • Authorization Code
  • Implicit
  • Resource Owner Password Credentials
  • Client Credentials

OAuth 的端点示例

  • RFC 6749: /token
  • Twitter: /oauth2/token
  • Dropbox: /oauth2/authorize
  • Fackbook: /oauth/access_token
  • google: /o/oauth2/token
  • github: /login/oauth/access_token
  • instagram: /oauth/authorize
  • tumblr: /oauth/access_token

当正确的信息送达服务器端后,服务器端便会返回如下 JSON 格式的响应信息

1
2
3
4
5
6
{
"access_token": "令牌",
"token_type": "bearer",
"expires_in": 过期时间,
"refresh_token": "刷新令牌凭证"
}

token_type 的 bearer 是 RFC 6750 定义的 OAuth 2.0 所用的 token 类型。根据 RFC 6750 的定义,
客户端有 3 种方式将 bearer token 信息发送给服务器。

第一种,将 token 信息添加到请求信息的首部时,客户端要用到 Authorization 首部,并按如下方式指定 token 的内容

1
2
3
GET /v1/users HTTP / 1.1
Host: api.example.com
Authorization: Bearer lkj123hjkasd879asdiuoqwe7a

第二种,将 token 信息添加到请求体时,则需要将请求消息里的 Content-Type 设定为 application/x-www-form-urlencoded,
并用 access_token 来命名消息体里的参数,然后附加上 token 信息

1
2
3
4
5
POST /v1/users HTTP / 1.1
Host: api.example.com
Content-type: application/x-www-form-urlencoded
access_token=lkj123hjkasd879asdiuoqwe7a

第三种,以查询参数的形式添加 token 参数时,可以在名为 access_token 的查询参数后指定 token 信息发送给服务器。

1
2
GET /v1/users?access_token=lkj123hjkasd879asdiuoqwe7a
Host: server.example.com

自身信息的别名

在线服务 用于表示自身信息的关键字 示例
Instagram self /users/self/media/liked
Etsy __SELF__ /users/__SELF__/favorites/listings/12345?method=DELETE
LikedIn ~ /people/~
Reddit me /me
Tumblr user /user/info
Google Calendar me /users/me/calendarList
Xing me /users/me

通过这样设计端点,开发时需要输出哪个用户的信息就必须从认证信息中获取,这就必然会导致自身信息的处理和获取其他用户信息的处理要分开进行。
可以更容易地防止误将其他用户的个人信息对外公开的 BUG 发生。

主机名和端点的共有部分

完整的端点是类似于 https://api.example.com/v1/users 这样的 HTTP 的 URI 信息。https://api.example.com/v1/ 是 API 的共有部分,
对这部分内容的设计也有必要进行一番考量。

在线服务 端点的共有部分
Twitter api.twitter.com/1.1
Foursquare api.foursquare.com/v2
Tumblr api.tumblr.com/v2
Github api.github.com
LinkedIn api.linkedin.com/v1

通过版本信息来管理 API

  • 在 URI 中嵌入版本编号
  • 在查询字符串里加入版本信息
  • 使用媒体类型来指定版本信息

响应数据的设计

响应数据的格式

关于这点不用谈论,选择 JSON 作为默认数据格式即可。

响应数据的指定方式

如果客户端需要支持其他的响应数据格式,有三种方法可以向服务端传达这一信息

推荐使用在请求首部指定媒体类型的方法

使用 JSONP

数据内部结构的思考方法

  • 让用户来选择响应的内容
  • 封装是否必要
  • 数据是否应该扁平化
  • 序列与格式

响应数据的设计

1.出错信息的表示

1.1通过状态码来表示出错信息

在返回出错信息之前,首先必须选择合适的状态码。

  • 1 字头:消息
  • 2 字头:成功
  • 3 字头:重定向
  • 4 字头:客户端原因引起的错误
  • 5 字头:服务器端原因引起的错误

1.2向客户端返回详细的出错信息

返回出错信息的方法有两种:一种是将详细信息放入 HTTP 响应消息首部,另一种则是通过响应消息体返回。

1
2
3
4
5
6
7
8
9
10
11
12
X-MYNAME-ERROR-CODE: 2017
X-MYNAME-ERROR-MESSAGE: Hello world
X-MYNAME-ERROR-INFO: "..."
{
"error": {
"code": 2013,
"message": "Bad authentication token",
"info": "..."
}
}

1.3 发生错误时防止返回 HTML

某些 API 在发生错误时会将 HTML 信息写入信息体,但虽说发生了错误,但客户端依然在访问 API,所以仍然期待服务器返回 JSON 或 XML 等数据格式。

1.4 维护与状态码

当停止 API 来进行维护工作时,不仅仅要使用 503 状态码来告知用户当前服务已经停止,还要使用 Retry-After 这一 HTTP 首部来告知用户维护结束的时间。

最大程度地利用 HTTP 协议规范

正确使用状态码 TODO

使用 HTTP 缓存

HTTP 缓存机制分为两类,过期模型和验证模型。过期模型是指预先决定响应数据的保存期限,当到达期限后就会再次访问服务器来重新获取所需的数据;
而验证模型则会轮询当前保存的缓存数据是否为最新数据,并只在服务器端进行数据更新时,才重新获取新的数据。

在 HTTP 协议中,缓存处于可用的状态时成为 fresh 状态,而处于不可用的状态时则成为 stale 状态

过期模型可以通过在服务器的响应消息里包含何时过期的信息来实现。HTTP 1.1 中定义了两种实现方法:一个方法是用 Cache-Control 响应消息首部,
另一个方法是用 Expires 响应消息首部,分别如下所示:

1
2
Expires: Fri, 01 Jan 2016 00:00:00 GMT
Cache-Control: max-age=3600

当 Expires 和 Cache-Control 同时使用时,Cache-Control 优先。另外 HTTP 时间格式必须遵循 RFC 1123 的规范,而且只能使用 GMT 作为时区。

验证模型采用了询问服务器的方式来判断当前所保存的缓存是否有效。和到期之前不会发生网络访问的过期模型不同,验证模型会在检查缓存的过程中会不时地去访问网络。
在执行验证模型时,需要应用程序支持附带条件的请求。要进行附带条件的请求,就必须向服务器传达「客户端当前保存的信息的状态」,为此需要用到最后更新日期或
实体标签作为指标。最后更新时间表示当前数据最后一次更新的日期;而实体标签则是表示某个特定资源版本的标识符。最后更新日期和实体标签会被分别填充在
「Last-Modified」 和 「ETag」 响应信息首部返回给客户端。

HTTP 1.1 还存在「启发式过期」,当服务端没有给出明确的过期时间时,客户端可以决定大约需要将缓存数据保存多久。这时客户端就要根据服务器端的更新频率、
具体状况等信息,自行决定缓存的过期时间。

不希望实施缓存的情况,可以使用「Cache-Control」首部实现,或者在「Expires」使用过去的日期或不正确的日期也能到达到同样的效果。

1
2
3
4
// 先用验证模型确认返回的资源是否发生了变化,然后根据令牌来确认是否更新缓存
Cache-Control: no-cache
// 直接禁止浏览器以及中间缓存存储任何版本的返回响应。每次请求都要求返回完整的响应
Cache-Control: no-store

媒体类型的指定

HTTP 协议中必须指定媒体类型来描述请求信息和响应信息里所承载的数据形式。媒体类型简而言之就是数据格式。

  • 使用 Content-Type 指定请求或响应的媒体类型
  • 使用 Accept 指定请求或响应的媒体类型
  • 使用 x- 的媒体类型
  • 使用自定义的媒体类型
    • 无前缀
    • vnd. 前缀
    • prs. 前缀
    • x. 前缀

同源策略和跨域资源共享

通过 XHTTPRequest 对不同的域进行访问将无法获取响应数据,这一原则成为同源策略(Same Origin Policy)。同源策源主要是出于安全方面的考虑,
它只允许从相同的源来读取数据,并通过 URI 里的 schema(http,https),主机(api.example.com),端口号的组合来判断是否同源。

上述 URI 皆不同源。

CORS(Cross-Origin Resource Sharing)跨域资源访问可以解决同源策略带来的问题。实施 CORS 时,客户端需要使用 Origin 请求首部。然后服务端会检查其中的源
是否能够允许被访问。并使用 Allow-Origin 响应首部来返回允许访问的源。

CORS 在特定场景下会先行查询请求是否能被接收。使用 OPTION 方法发送请求。然后服务端会响应这样的请求,并返回三个首部

1
2
3
4
Access-Control-Allow-Origin: 允许源清单
Access-Control-Allow-Methods: 允许请求方法清单
Access-Control-Allow-Headers: 允许请求头部清单
Access-Control-Allow-Max-Age: 允许事先请求的信息在缓存中保存的时间

定义私有的 HTTP 首部

如果将 HTTP 首部作为存放元信息的场所,当需要发送无法找到合适首部的元数据时,可以自定义私有的 HTTP 首部,如下所示:

1
X-AppName-PixelRatio: 2.0

开发牢固的 Web API

Web API 安全问题:
  • 非法获取服务器端和客户端之间的信息
    • 数据分组嗅探(PacketSniffing)
    • 会话劫持(SessionJacking)
  • 利用服务器端的安全漏洞非法获取和篡改信息
    • 中间人攻击(Man-In-The-Middle-Attack, MITM 攻击)
    • 篡改参数
    • 请求再次发送
  • 预设通过浏览器访问的 API 中的问题
    • XSS(Cross Site Scripting 跨站脚本攻击)
    • CSRF(Cross Site Request Forgery 跨站请求伪造)
    • JSON 劫持
与安全相关的 HTTP 首部
  • X-Content-Type-Options
  • X-XSS-Protection
  • X-Frame-Options
  • Content-Security-Policy
  • Strict-Transport-Security
  • Public-Key-Pins
  • Set-Cookie
应对大规模访问的对策
  • 限制每个用户的访问
    • 用什么样的机制来识别用户
    • 如何确定限速的数值
    • 以什么单位来设置限速的数值
    • 在什么时候重置限速的数值
  • 应对超出上限值的情况
  • 向用户告知限速的信息
    • X-RateLimit-Limit: 单位时间的访问上限
    • X-RateLimit-Reset: 访问次数重置的时间
    • X-RateLimit-Remaining: 剩余的访问次数

日常开发常用命令

Yarn

1
2
3
4
5
6
7
8
// 安装依赖时不读取或生成 yarn.lock 锁文件
yarn install --no-bin-links
// 使用 cross-env 解决跨平台 NODE_ENV 问题
yarn add cross-env
// 设置 yarn 源
yarn config set registry https://registry.npm.taobao.org

npm 热更新

1
2
3
4
5
// 监控所有相关的资源文件以便进行更改。Webpack 会在检测到文件更改时自动重新编译资源:
npm run watch-poll
// 强制安装所有模块,不管该模块之前是否安装过
npm install -f

修改时区和默认语言

1
2
3
4
5
6
7
// config/app.php 文件
return [
...
'timezone' => 'Asia/Shanghai',
...
'locale' => 'zh-CN',
];

Migration

1
2
3
php artisan make:migration create_users_table --create=users
php artisan make:migration add_votes_to_users_table --table=users
php artisan make:migration seed_users_table_data

快速实现用户模块

1
2
php artisan make:auth
php artisan migrate

使用第三方扩展包 overtrue/laravel-lang 实现多语言方案

1
composer require "overtrue/laravel-lang:~3.0"

通过 redirect()->intended() 实现页面重定向到上一次请求尝试访问的页面上

1
redirect()->intended(route('home'));

通过 makeVisible() 临时显示模型里指定的隐藏属性 $hidden

1
$usersArray = $users->makeVisible(['password', 'remember_token'])->toArray();

数据批量填充

1
2
3
4
5
6
7
8
9
10
<?php
$userIds = ['1','2','3'];
$faker = app(Faker\Generator::class);
$statuses = factory(Status::class)->times(100)->make()->each(function ($status) use ($faker, $userIds) {
$status->user_id = $faker->randomElement($userIds);
});
Status::insert($statuses->toArray());

Carbon 设置中文

1
2
3
4
5
6
7
8
9
public function boot()
{
\Carbon\Carbon::setLocale('zh');
}
...
// 显示友好的时间差
$model->updated_at->diffForHumans();

Auth::routes(); 等同代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
// Authentication Routes...
Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
Route::post('login', 'Auth\LoginController@login');
Route::post('logout', 'Auth\LoginController@logout')->name('logout');
// Registration Routes...
Route::get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
Route::post('register', 'Auth\RegisterController@register');
// Password Reset Routes...
Route::get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
Route::post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
Route::get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
Route::post('password/reset', 'Auth\ResetPasswordController@reset');

form 元素完整声明

1
2
3
4
5
6
<form action="{{ route('users.update', $user->id) }}"
method="POST"
accept-charset="UTF-8"
enctype="multipart/form-data"
>
</form>

Laravel Rules 图片宽高验证规则

1
'avatar' => 'mimes:jpeg,bmp,png,gif|dimensions:min_width=200,min_height=200',

使用第三方扩展包 Intervention/image 实现图片裁剪功能

1
2
3
composer require intervention/image
php artisan vendor:publish --provider="Intervention\Image\ImageServiceProviderLaravel5"

使用数据迁移来实现生成数据填充

使用第三方扩展包 summerblue/generator 快速生成项目代码

1
composer require 'summerblue/generator:~0.5' --dev

使用第三方扩展包 hieu-le/active 来判断路由参数

1
composer require "hieu-le/active:~3.5"

使用 Request::url() 获取当前请求的 URL

使用 HTMLPurifier for Laravel 5 进行 XSS (跨站点脚本攻击)防御

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
composer require "mews/purifier:~2.0"
php artisan vendor:publish --provider="Mews\Purifier\PurifierServiceProvider"
// 打开文件 config/purifier.php 配置 purifier 信息
<?php
return [
'encoding' => 'UTF-8',
'finalize' => true,
'cachePath' => storage_path('app/purifier'),
'cacheFileMode' => 0755,
'settings' => [
'user_topic_body' => [
'HTML.Doctype' => 'XHTML 1.0 Transitional',
'HTML.Allowed' => 'div,b,strong,i,em,a[href|title],ul,ol,ol[start],li,p[style],br,span[style],img[width|height|alt|src],*[style|class],pre,hr,code,h2,h3,h4,h5,h6,blockquote,del,table,thead,tbody,tr,th,td',
'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,margin,width,height,font-family,text-decoration,padding-left,color,background-color,text-align',
'AutoFormat.AutoParagraph' => true,
'AutoFormat.RemoveEmpty' => true,
],
],
];
// 配合辅助函数 clean 使用
$topic->body = clean($topic->body, 'user_topic_body');

使用 Guzzle 实现 HTTP 请求

1
composer require "guzzlehttp/guzzle:~6.3"

使用 PinYin 实现汉字转拼音

1
composer require "overtrue/pinyin:~3.0"

配置 Redis 队列

1
2
3
4
composer require "predis/predis:~1.0"
// .env 配置队列驱动
QUEUE_DRIVER=redis

失败任务:有时候队列中的任务会失败。Laravel 内置了一个方便的方式来指定任务重试的最大次数。当任务超出这个重试次数后,它就会被插入到 failed_jobs 数据表里面。我们可以使用 queue:failed-table 命令来创建 failed_jobs 表的迁移文件:

1
2
3
4
// 生成迁移文件
php artisan queue:failed-table
// 执行迁移文件
php artisan migrate

生成任务类

1
php artisan make:job QueenClass

  • Illuminate\Contracts\Queue\ShouldQueue 接口,表示将任务添加到队列,而不是同步执行
  • 引入 SerializesModels Trait,Eloquent 模型会被优雅的序列化和反序列化
  • handle 方法会在队列任务执行时被调用

Horizon 队列监控

1
2
composer require "laravel/horizon:~1.0"
php artisan vendor:publish --provider="Laravel\Horizon\HorizonServiceProvider"

使用 Supervisor 进程工具进行管理,配置和使用请参照 文档 进行配置;
每一次部署代码时,需 artisan horizon:terminate 然后再 artisan horizon 重新加载代码。

使用 @includeWhen() 判断是否加载文件

1
2
3
4
@includeWhen($boolean, 'view.name', ['some' => 'data'])
#### Laravel 消息通知系统
数据通知频道会在一张数据表里存储所有通知信息。我们后面会通过查询这张表的内容在应用界面上展示通知。

php artisan notifications:table
php artisan migrate
// 在 users 表里新增 notification_count 字段,用来跟踪用户有多少未读通知
php artisan make:migration add_notification_count_to_users_table –table=users

1
2
#### 使用 spatie/larave-permission 来实现权限管理

composer require “spatie/laravel-permission:~2.7”
php artisan vendor:publish –provider=”Spatie\Permission\PermissionServiceProvider” –tag=”migrations”
php artisan migrate
php artisan vendor:publish –provider=”Spatie\Permission\PermissionServiceProvider” –tag=”config”

1
2
#### 使用 summerblue/administrator:~1.1 快速生成管理后台

// 安装组件
composer require “summerblue/administrator:~1.1”

// 发布资源文件:config/administrator.php 配置文件,public/packages/summerblue/administrator 前端资源文件
php artisan vendor:publish –provider=”Frozennode\Administrator\AdministratorServiceProvider”

// 创建配置文件目录
mkdir config/administrator config/administrator/settings

1
2
#### Trait 方法重写

use Notifiable {
notify as protected laravelNotify;
}

1
2
3
#### 编写命令,提供给计划任务调用
使用调度器时,我们需要修改系统的 Cron 计划任务配置信息,运行以下命令:

export EDITOR=vi && crontab -e

1
复制下面这一行:

          • php /home/vagrant/Code/larabbs/artisan schedule:run >> /dev/null 2>&1
            ```

刻意练习

有目的的练习的四个特点

  • 有目的的练习具有定义明确的特定目标
    • 定义明确的具体目标,可以有效地用于引导你的练习
  • 有目的的练习是专注的
    • 要想取得进步,必须完全把注意力集中在你的任务上
  • 有目的的练习包含反馈
    • 无论你在努力做什么事情,都需要反馈来准确辨别你在哪些方面还有不足,以及怎么会存在这些不足
  • 有目的的练习需要走出舒适区
    • 对于任何类型的练习,这是一条基本的真理:如果你从来不迫使自己走出舒适区,便永远无法进步

遇到瓶颈怎么办

  • 试着做不同的事情,而非更难的事情
  • 并非达到极限,而是动机不足

大脑的适应能力

  • 一个人遇到的挑战越大,大脑的变化就越大,学习也就越高效
  • 但是过分逼迫自己可能导致倦怠。因此,处在舒适区之外却离得不太远的挑战,能使大脑改变最为迅速
  • 大脑就像肌肉,越练越大

挑战体内平衡和发展那种潜力的最佳方式是什么?

大师比新手强在哪里

  • 有意义的记忆更高效
  • 了解事物运作的规律
  • 既见树木,又见森林

在刻意练习之中,我们的大脑究竟是什么发生了变化?

他们经过年复一年的练习,已经改变了大脑中的神经回路,以创建高度专业化的心理表征,这些心理表征反过来使得令人难以置信的记忆、规律的识别、问题的解决等成为可能,也使得他们能够培养和发展各种高级的能力,以便在特定的专业领域中表现卓越。

心理表征

理解这些心理表征是什么,以及它们怎样运行,最好的方式是为心理表征的概念创建良好的心理表征。

  • 心理表征有助于找出规律
    • 预测未来
    • 无意识决策
  • 心理表征有助于解释信息
  • 心理表征有助于组织信息
  • 心理表征有助于制定计划
  • 心理表征有助于高效学习

黄金标准

刻意练习与一般练习的区别十分关键:首先,需要一个已经得到合理发展的行业或领域;其次,需要一位能够布置训练作业的导师

合理发展的行业或领域有几个共同的特点:

  1. 对于绩效的测量,总是存在客观的方面
  2. 往往具有足够的竞争性,以至于从业人员有强烈的动机来训练和提高
  3. 通常都是已经形成规模的,相关的技能已得到数十年甚至数世纪的培养
  4. 这些行业或领域中,有一些从业人员还担任导师和教练,随着时间的推移,他们已经发展出日渐复杂的一整套训练方法,使得该行业或领域的技能水平稳定提高。

刻意练习的特点