e攻城狮

  • 首页

  • 随笔

  • 分类

  • 瞎折腾

  • 搜索

layuiAdmin 专业版(单页面)开发者文档

发表于 2020-03-12 分类于 前端开发

在线演示 正版授权

layuiAdmin pro (单页版)是完全基于 layui 架构而成的后台管理模板系统,可以更轻松地实现前后端分离,它是 mvc 的简化版,全面接管 视图 和 页面路由,并可自主完成数据渲染,服务端通常只负责数据接口,而前端只需专注视图和事件交互,所有的页面动作都是在一个宿主页面中完成,因此这赋予了 layuiAdmin 单页面应用开发的能力。

题外

  • 该文档适用于 layuiAdmin 专业版(单页面),阅读之前请务必确认是否与你使用的版本对应。
  • 熟练掌握 layuiAdmin 的前提是熟练掌握 layui,因此除了本篇文档, layui 的文档 也是必不可少的存在。

快速上手

部署

  • 解压文件后,将 layuiAdmin 完整放置在任意目录
  • 通过本地 web 服务器去访问 ./start/index.html 即可运行 Demo

由于 layuiAdmin 可采用前后端分离开发模式,因此你无需将其放置在你的服务端 MVC 框架中,你只需要给 layuiAdmin 主入口页面(我们也称之为:宿主页面)进行访问解析,它即可全权完成自身路由的跳转和视图的呈现,而数据层则完全通过服务端提供的异步接口来完成。

目录说明

  • src/: layuiAdmin 源代码,通常用于开发环境(如本地),推荐你在本地开发时,将 ./start/index.html 中的 layui.css 和 layui.js 的引入路径由 dist 改为 src 目录。
    • src/controller/:存放 JS 业务模块,即对视图进行事件等交互性处理
    • src/lib/:layuiAdmin 的核心模块,一般不推荐修改
    • src/style/:存放样式,其中 admin.css是核心样式
    • src/views/:存放视图文件。其中 layout.html 是整个框架结构的承载,一般不推荐做大量改动。
    • src/config.js:layuiAdmin 的全局配置文件,可随意修改。
    • src/index.js:layuiAdmin 的入口模块,一般不推荐修改
  • dist/: 通过 gulp 将 layuiAdmin src 目录的源代码进行构建后生成的目录(即:将 JS 和 CSS 文件进行了压缩等处理),通常用于线上环境。关于 gulp 的使用,下文也有介绍。
  • start/: 存放 layuiAdmin 的入口页面、模拟接口数据、layui

宿主页面

你所看到的 start/index.html 是我们提供好的宿主页面,它是整个单页面的承载,所有的界面都是在这一个页面中完成跳转和渲染的。事实上,宿主页面可以放在任何地方,但是要注意修改里面的 <link> <script> 的 src 和 layui.config 中 base 的路径。

全局配置

当你已经顺利在本地预览了 layuiAdmin 后,你一定迫不及待关注更深层的结构。打开 src 目录,你将看到 config.js,里面存储着所有的默认配置。你可以按照实际需求选择性修改,下面是 layuiAdmin 默认提供的配置:

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
layui.define(['laytpl', 'layer', 'element', 'util'], function(exports){
exports('setter', {
container: 'LAY_app' //容器ID
,base: layui.cache.base //记录layuiAdmin文件夹所在路径
,views: layui.cache.base + 'views/' //视图所在目录
,entry: 'index' //默认视图文件名
,engine: '.html' //视图文件后缀名
,pageTabs: false //是否开启页面选项卡功能。单页版不推荐开启

,name: 'layuiAdmin Pro'
,tableName: 'layuiAdmin' //本地存储表名
,MOD_NAME: 'admin' //模块事件名

,debug: true //是否开启调试模式。如开启,接口异常时会抛出异常 URL 等信息

,interceptor: false //是否开启未登入拦截

//自定义请求字段
,request: {
tokenName: 'access_token' //自动携带 token 的字段名。可设置 false 不携带。
}

//自定义响应字段
,response: {
statusName: 'code' //数据状态的字段名称
,statusCode: {
ok: 0 //数据状态一切正常的状态码
,logout: 1001 //登录状态失效的状态码
}
,msgName: 'msg' //状态信息的字段名称
,dataName: 'data' //数据详情的字段名称
}

//独立页面路由,可随意添加(无需写参数)
,indPage: [
'/user/login' //登入页
,'/user/reg' //注册页
,'/user/forget' //找回密码
,'/template/tips/test' //独立页的一个测试 demo
]

//扩展的第三方模块
,extend: [
'echarts', //echarts 核心包
'echartsTheme' //echarts 主题
]

//主题配置
,theme: {
//配色方案,如果用户未设置主题,第一个将作为默认
color: [{
main: '#20222A' //主题色
,selected: '#009688' //选中色
,logo: '' //logo区域背景色
,header: '' //头部区域背景色
,alias: 'default' //默认别名
}] //为了减少篇幅,更多主题此处不做列举,可直接参考 config.js

//初始的颜色索引,对应上面的配色方案数组索引
//如果本地已经有主题色记录,则以本地记录为优先,除非清除 localStorage(步骤:F12呼出调试工具→Aplication→Local Storage→选中页面地址→layuiAdmin→再点上面的X)
// 1.0 正式版开始新增
,initColorIndex: 0
}
});
});

侧边菜单

  • 在 start/json/menu.js 文件中,我们放置了默认的侧边菜单数据,你可以去随意改动它。
  • 如果你需要动态加载菜单,你需要将 views/layout.html 中的对应地址改成你的真实接口地址
    侧边菜单最多可支持到三级。无论你采用静态的菜单还是动态的,菜单的数据格式都必须是一段合法的 JSON,且必须符合以下规范:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"code": 0, //状态码,key 名可以通过 config.js 去重新配置
"msg":"", //提示信息
"data": [{ //菜单数据,key名可以通过 config.js 去重新配置
"name": "component", //一级菜单名称(与视图的文件夹名称和路由路径对应)
"title": "组件", //一级菜单标题
"icon": "layui-icon-component", //一级菜单图标样式
"jump": '' ,//自定义一级菜单路由地址,默认按照 name 解析。一旦设置,将优先按照 jump 设定的路由跳转
"spread": true ,//是否默认展子菜单(1.0.0-beta9 新增)
"list": [{ //二级菜单
"name": "grid", //二级菜单名称(与视图的文件夹名称和路由路径对应)
"title": "栅格", //二级菜单标题
"jump": '', //自定义二级菜单路由地址
"spread": true, //是否默认展子菜单(1.0.0-beta9 新增)
"list": [{ //三级菜单
"name": "list", //三级菜单名(与视图中最终的文件名和路由对应),如:component/grid/list
"title": "等比例列表排列" //三级菜单标题
},{
"name": "mobile",
"title": "按移动端排列"
}
}]
}

TIPS:实际运用时,切勿出现上述中的注释,否则将不是合法的 JSON ,会出现解析错误。

需要注意的是以下几点:

  1. 当任意级菜单有子菜单,点击该菜单都只是收缩和展开操作,而并不会跳转,只有没有子菜单的菜单才被允许跳转。
  2. 菜单的路由地址默认是按照菜单层级的 name 来设定的。
    我们假设一级菜单的 name 是:a,二级菜单的是:b,三级菜单的 name 是 c,那么:
    • 三级菜单最终的路由地址就是:/a/b/c
    • 如果二级菜单没有三级菜单,那么二级菜单就是最终路由,地址就是:/a/b/
    • 如果一级菜单没有二级菜单,那么一级菜单就是最终路由,地址就是:/a/
  3. 但如果你设置了 参数 jump,那么就会优先读取 jump 设定的路由地址,如:”jump”: “/user/set”

路由

layuiAdmin 的路由是采用 location.hash 的机制,即路由地址是放在 ./#/ 后面,并通过 layui 自带的方法: layui.router() 来进行解析。每一个路由都对应一个真实存在的视图文件,且路由地址和视图文件的路径是一致的(相对 views 目录)。因此,你不再需要通过配置服务端的路由去访问一个页面,也无需在 layuiAdmin 内部代码中去定义路由,而是直接通过 layuiAdmin 的前端路由去访问,即可匹配相应目录的视图,从而呈现出页面结果。

路由规则

1
./#/path1/path2/path3/key1=value1/key2=value2…

一个实际的示例:

1
2
./#/user/set
./#/user/set/uid=123/type=1#xxx(下面将以这个为例继续讲解)

当你需要对路由结构进行解析时,你只需要通过 layui 内置的方法 layui.router() 即可完成。如上面的路由解析出来的结果是:

1
2
3
4
5
6
{
path: ['user','set']
,search: {uid: 123, type: 1}
,href: 'user/set/uid=123/type=1'
,hash: 'xxx'
}

可以看到,不同的结构会自动归纳到相应的参数中,其中:

  • path:存储的是路由的目录结构
  • search:存储的是路由的参数部分
  • href:存储的是 layuiAdmin 的完整路由地址
  • hash:存储的是 layuiAdmin 自身的锚记,跟系统自带的 location.hash 有点类似

通过 layui.router() 得到路由对象后,你就可以对页面进行个性化操作、异步参数传值等等。如:

1
2
3
4
5
6
7
8
//在 JS 中获取路由参数
var router = layui.router();
admin.req({
url: 'xxx'
,data: {
uid: router.search.uid
}
});
1
2
3
4
5
6
7
8
9
<!--  在动态模板中获取路由参数 -->
<script type="text/html" template lay-url="./xxx/?uid={{ layui.router().search.uid }}">
…
</script>

<!-- 或 -->
<script type="text/html" template lay-url="./xxx/" lay-data="{uid:'{{ layui.router().search.uid }}'}">
…
</script>

路由跳转

通过上文的路由规则,你已经大致清楚了 layuiAdmin 路由的基本原理和解析方法。那么如何完成路由的跳转呢?

  1. 在视图文件的 HTML 代码中,通过对任意元素设定 lay-href="/user/set/uid=123/type=1" ,好处是:任意元素都可以触发跳转。缺点是:只能在浏览器当前选项卡完成跳转(注意:不是 layuiAdmin 的选项卡)
  2. 直接对 a 标签设定 href,如: <a href="#/user/set">text</a> 。好处是:你可以通过设定 target="_blank" 来打开一个浏览器新选项卡。缺点是:只能设置 a 标签,且前面必须加 /#/
  3. 在 JS 代码中,还可通过 location.hash = '/user/set'; 来跳转。前面无需加 #,它会自动追加。

路由结尾

在路由结尾部分出现的 / 与不出现,是两个完全不同的路由。比如下面这个:

  1. user/set
    读取的视图文件是:.views/user/set.html
  2. user/set/
    读取的视图文件是:./views/user/set/index.html (TIPS:这里的 index.html 即是目录下的默认主视图,下文会有讲解)

因此一定要注意结尾处的 /,避免视图读取错误。

视图

这或许是你应用 layuiAdmin 时的主要焦点,在开发过程中,你的大部分精力都可能会聚焦在这里。它取代了服务端 MVC 架构中的 view 层,使得应用开发变得更具扩展性。因此如果你采用 layuiAdmin 的 SPA(单页应用)模式,请务必要抛弃服务端渲染视图的思想,让页面的控制权限重新回归到前端吧!

views 目录存放的正是视图文件,你可以在该目录添加任意的新目录和新文件,通过对应的路由即可访问。

注意:如果是单页面模式,视图文件通常是一段 HTML 碎片,而不能是一个完整的 html 代码结构。

视图与路由的关系

每一个视图文件,都对应一个路由。其中 index.html 是默认文件(你也可以通过 config.js 去重新定义)。视图文件的所在目录决定了路由的访问地址,如:

视图路径 对应的路由地址
./views/user/index.html /user/
./views/user.html /user
./views/user/set/index.html /user/set/
./views/user/set.html /user/set
./views/user/set/base.html /user/set/base

通过上述的表格列举的对应关系,可以总结出:

  • 当视图文件是 index.html,那么路由地址就是它的上级目录(相对 _views_),以 / 结尾
  • 当视图文件不是 index.html,那么路由地址就是它的上级目录+视图文件名,不以 / 结尾

值得注意的是:路由路径并非最多只能三级,它可以无限极。但对应的视图也必须存放在相应的层级目录下

视图中加载 JS 模块

在视图文件中,除了写 HTML,也可以写 JavaScript 代码。如:

1
2
3
4
5
6
7
8
9
<div id=“LAY-demo-hello”>Hello layuiAdmin</div>
<script>
layui.use('admin', function(){
var $ = layui.jquery;
admin.popup({
content: $('#LAY-demo-hello').html()
});
});
</script>

如果该视图对应的 JS 代码量太大,我们更推荐你在 controller 目录下新增一个业务模块,并在视图中直接 layui.use 去加载该模块。下面以控制台主页 index.html 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div>html区域<div>

<script>
//加载 controller 目录下的对应模块
/*
小贴士:
这里 console 模块对应 的 console.js 并不会重复加载,
然而该页面的视图可能会重新插入到容器,那如何保证脚本能重新控制视图呢?有两种方式:
1): 借助 layui.factory 方法获取 console 模块的工厂(回调函数)给 layui.use
2): 直接在 layui.use 方法的回调中书写业务代码,即:
layui.use('console', function(){
//同 console.js 中的 layui.define 回调中的代码
});

这里我们采用的是方式1。其它很多视图中采用的其实都是方式2,因为更简单些,也减少了一个请求数。

*/
layui.use('console', layui.factory('console'));
</script>

当视图被渲染后,layui.factory 返回的函数也会被执行,从而保证在不重复加载 JS 模块文件的前提下,保证脚本能重复执行。

动态模板

layuiAdmin 的视图是一个“动静结合”的载体,除了常规的静态模板,你当然还可以在视图中存放动态模板,因此它可谓是焦点中的焦点。

定义模板

在视图文件中,通过下述规则定义模板:

1
2
3
<script type="text/html" template>
<!-- 动态模板碎片 -->
</script>

下面是一个简单的例子:

1
2
3
4
<script type="text/html" template>
当前 layuiAdmin 的版本是:{{ layui.admin.v }}
路由地址:{{ layui.router().href }}
</script>

在不对动态模板设定数据接口地址的情况下,它能读取到全局对象。但更多时候,一个动态模板应该是对应一个接口地址,如下所示:

1
2
3
4
5
6
7
8
<script type="text/html" template lay-url="接口地址">
我叫:{{ d.data.username }}
{{# if(d.data.sex === '男'){ }}
公的
{{# } else { }}
母的
{{# } }}
</script>

模板中的 d 对应的是你接口返回的 json 转化后的一维对象,如:

1
2
3
4
5
6
7
{
"code": 0,
"data": {
"username": "贤心",
"sex": "男"
}
}

那么,上述动态模板最终输出的结果就是:

1
2
我叫:贤心
公的

模板基础属性

动态模板支持以下基础属性

  • lay-url
    用于绑定模板的数据接口地址,支持动态模板解析,如:
1
2
3
<script type="text/html" template lay-url="https://api.xxx.com?id={{ layui.router().search.id }}">
<!-- 动态模板碎片 -->
</script>
  • lay-data
    用于定义接口请求的参数,其值是一个 JavaScript object 对象,同样支持动态模板解析,如:
1
2
3
<script type="text/html" template lay-url="" lay-data="{id: '{{ layui.router().search.id }}', type: 1}">
<!-- 动态模板碎片 -->
</script>
  • lay-headers
    用户定义接口请求的 Request Headers 参数,用法与 lay-data 的完全类似,支持动态模板解析。

  • lay-done
    接口请求完毕并完成视图渲染的回调脚本,里面支持写任意的 JavaScript 语句。事实上它是一个封闭的函数作用域,通过给 Function 实例返回的函数传递一个参数 d,用于得到接口返回的数据:

1
2
3
<script type="text/html" template lay-url="" lay-done="console.log(d);">
<!-- 动态模板碎片 -->
</script>

很多时候,你在动态模板中可能会放入一些类似于 layui 的 form 元素,而有些控件需要执行 form.render() 才会显示,这时,你可以对 lay-done 赋值一个全局函数,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script type="text/html" template lay-url="" lay-done="layui.data.done(d);">
<div class="layui-form" lay-filter="LAY-filter-demo-form">
<input type="checkbox" title="复选框">
</div>
</script>

<!-- 注意:别看眼花了,下面可不是动态模板,而是 JS 脚本区域 -->
<script>
layui.data.done = function(d){
layui.use(['form'], function(){
var form = layui.form;
form.render(null, 'LAY-filter-demo-form'); //渲染该模板下的动态表单
});
};
</script>

TIPS:

  • 如果模板渲染完毕需要处理过多的交互,我们强烈推荐你采用上述的方式定义一个全局函数赋值给 lay-done,会极大地减少维护成本。
  • 无需担心该全局函数的冲突问题,该函数是一次性的。其它页面即便声明了一个同样的函数,也只是用于新的视图,丝毫不会对之前的视图造成任何影响。
  • layui.data.done 中的 done 可以随意命名,但需与 lay-done 的赋值对应上。

模板语法

动态模板基于 layui 的 laytpl 模块,详细语法可见: 传送门

登录与接口鉴权

由于 layuiAdmin 接管了视图层,所以不必避免可能会与服务端分开部署,这时你有必要了解一下 layuiAdmin 默认提供的:从 登录 到 接口鉴权 ,再到 注销 的整个流程。

登录拦截器

进入登入页面登入成功后,会在 localStorage 的本地表中写入一个字段。如: access_token (名称可以在 config.js 自定义)。拦截器判断没有 access_token 时,则会跳转到登入页。尽管可以通过伪造一个假的 access_token 绕过视图层的拦截,但在请求接口时,会自动带上 access_token,服务端应再次做一层校验。

流程

  1. 打开 config.js ,将 interceptor 参数设置为 true(该参数为 1.0.0-beta6 开始新增)。那么,当其未检查到 access_token 值时,会强制跳转到登录页面,以获取 access_token。
  2. 打开登录对应的视图文件 views/user/login.html,在代码最下面,你将看到一段已经写好的代码,你需要的是将接口地址改为服务端的真实接口,并返回 access_token 值。
  3. layuiAdmin 会将服务端返回的 access_token 值进行本地存储,这时你会发现 layuiAdmin 不再强制跳转到登录页面。并在后面每次请求服务端接口时,都会自动在参数和 Request Headers 中带上 access_token,以便服务端进行鉴权。
  4. 若鉴权成功,顺利返回数据;若鉴权失败,服务端的 code 应返回 1001(可在 config.js 自定义) , layuiAdmin 将会自动清空本地无效 token 并跳转到登入页。
  5. 退出登录:重新打开 controller/common.js,搜索 logout,配上注销接口即可。

如果是在其它场景请求的接口(如:table.render()),那么你需要获取本地存储的 token 复制给接口参数,如:

1
2
3
4
5
6
7
table.render({
elem: '#xxxx'
,url: 'url'
,where: {
access_token: layui.data('layuiAdmin').access_token
}
})

事实上,layuiAdmin 的所有 Ajax 请求都是采用 admin.req(options),它会自动传递 access_token,因此推荐你在 JS 执行 Ajax 请求时直接使用它。其中参数 options 和 $.ajax(options) 的参数完全一样。

接口鉴权

我们推荐服务端遵循 JWT(JSON Web Token) 标准进行鉴权。对 JWT 不甚了解的同学,可以去搜索一些相关资料,会极大地增加应用的可扩展性。当然,你也可以直接采用传统的 cookie / session 机制。

基础方法

  • config 模块

你可以在任何地方通过 layui.setter 得到 config.js 中的配置信息

  • admin 模块

var admin = layui.admin;

  • admin.req(options)
    Ajax 请求,用法同 $.ajax(options),只是该方法会进行错误处理和 token 的自动传递

  • admin.screen()
    获取屏幕类型,根据当前屏幕大小,返回 0 - 3 的值
    0: 低于768px的屏幕
    1:768px到992px之间的屏幕
    2:992px到1200px之间的屏幕
    3:高于1200px的屏幕

  • admin.exit()
    清除本地 token,并跳转到登入页

  • admin.sideFlexible(status)
    侧边伸缩。status 为 null:收缩;status为 “spread”:展开

  • admin.on(eventName, callback)
    事件监听,下文会有讲解

  • admin.popup(options)
    弹出一个 layuiAdmin 主题风格的 layer 层,参数 options 跟 layer.open(options) 完全相同

  • admin.popupRight(options)
    在屏幕右侧呼出一个面板层。options 同上。

1
2
3
4
5
6
7
admin.popupRight({
id: 'LAY-popup-right-new1' //定义唯一ID,防止重复弹出
,success: function(){
//将 views 目录下的某视图文件内容渲染给该面板
layui.view(this.id).render('视图文件所在路径');
}
});
  • admin.resize(callback)
    窗口 resize 事件处理,我们推荐你使用该方法取代 jQuery 的 resize 事件,以避免多页面标签下可能存在的冲突。

  • admin.events

    • admin.events.refresh()
      刷新当前右侧区域

    • admin.events.closeThisTabs()
      关闭当前标签页

    • admin.events.closeOtherTabs()
      关闭其它标签页

    • admin.events.closeAllTabs()
      关闭全部标签页

  • view 模块

var view = layui.view;

  • view(id)
    获取指定容器,并返回一些视图渲染的方法,如:
1
2
3
4
5
6
7
8
9
//渲染视图,viewPath 即为视图路径
view('#id').render(viewPath).then(function(){
//视图文件请求完毕,视图内容渲染前的回调
}).done(function(){
//视图文件请求完毕和内容渲染完毕的回调
});

//直接向容器插入 html,tpl 为 模板字符;data 是传入的数据。该方法会自动完成动态模板解析
view('#id').send(tpl, data);

另外,render 方法支持动态传参,以用于视图内容接受。如:

1
2
3
4
5
6
7
8
admin.popup({
id: 'LAY-popup-test1'
,success: function(){
view(this.id).render('视图文件所在路径', {
id: 123 //这里的 id 值你可以在一些事件中动态获取(如 table 模块的编辑)
});
}
})

那么,在视图文件中,你可以在动态模板中通过 \{\{ d.params.xxx \}\} 得到传入的参数,如:

1
2
3
4
5
6
7
<script type="text/html" template lay-url="http://api.com?id={{ d.params.id }}">
配置了接口的动态模板,且接口动态获取了 render 传入的参数:{{ d.params.id }}
</script>

<script type="text/html" template>
也可以直接获取:<input type="hidden" name="id" value="{{ d.params.id }}">
</script>

而如果是在 JS 语句中去获取模板传递过来的变量,可以借助动态模板的 lay-done 属性去实现,如:

1
2
3
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">

</script>

然后在 JS 语句中通过执行动态模板 lay-done 中对应的方法得到对应的参数值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
//定义一个 lay-done 对应的全局方法,以供动态模板执行
layui.data.sendParams = function(params){
console.log(params.id) //得到传递过来的 id 参数(或其他参数)值

//通过得到的参数值,做一些你想做的事
//…

//若需用到 layui 组件,layui.use 需写在该全局方法里面,如:
layui.use(['table'], function(){
var table = layui.table;
table.render({
elem: ''
,url: 'url?id='+ params.id
});
});
};
</script>

注意:上述实现需保证 layuiAdmin 为 1.2.0+`

总之,驾驭好 view().render().done(callback) 对您的项目开发至关重要。

ID唯一性

如果你开启了标签页功能,请务必注意 ID 的冲突,尤其是在你自己绑定事件的情况。ID 的命令可以遵循以下规则来规避冲突:

1
LAY-路由-任意名

以_消息中心_页面为例,假设它的路由为:/app/message/,那么 ID 应该命名为:

1
<button class="layui-btn" id="LAY-app-message-del">删除</button>

实用组件

Hover 提示层

通过对元素设置 lay-tips="提示内容" 来开启一个 hover 提示,如:

1
<i class="layui-icon layui-icon-tips" lay-tips="要支持的噢" lay-offset="5"></i>

其中 lay-offset 用于定于水平偏移距离(单位px),以调整箭头让其对准元素

事件监听

  • hash
    监听路由地址改变
1
2
3
4
// 下述中的 xxx 可随意定义,不可与已经定义的 hash 事件同名,否则会覆盖上一事件
admin.on('hash(xxx)', function(router){
console.log(router); //得到路由信息
});
  • side
    监听侧边伸缩
1
2
3
4
// 下述中的 xxx 可随意定义,不可与已经定义的 side 事件同名,否则会覆盖上一事件
admin.on('side(xxx)', function(obj){
console.log(obj.status); //得到伸缩状态:spread 为展开状态,其它值为收缩状态
});

兼容性

layuiAdmin 使用到了 layui 的栅格系统,而栅格则是基于浏览器的媒体查询。ie8、9不支持。 所以要在宿主页面(如 start/index.html )加上下面这段保证兼容:

1
2
3
4
5
<!-- 让IE8/9支持媒体查询,从而兼容栅格 -->
<!--[if lt IE 9]>
<script src="https://cdn.staticfile.org/html5shiv/r29/html5.min.js"></script>
<script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->

缓存问题

由于单页面版本的视图文件和静态资源模块都是动态加载的,所以可能存在浏览器的本地缓存问题,事实上我们也考虑到这个,因此,为了避免改动后的文件未及时生效,你只需在入口页面(默认为start/index.html)中,找到 layui.config,修改其 version 的值即可。

我们推荐你分场景来更新缓存:

  • 场景一:如果项目是在本地开发。你可以设置 version 为动态毫秒数,如:
1
version: new Date().getTime() //这样你每次刷新页面,都会更新一次缓存
  • 场景二:如果项目是在线上运行。建议你手工更新 version,如:
1
version: '1.0.0' //每次发布项目时,跟着改动下该属性值即可更新静态资源的缓存

升级事项

从官网更新资源包后,除了 src 和 dist 目录需要注意一下,其它目录和文件均可覆盖,下面以 src 为例(dist 由于是 src 构建后生成的目录,所以本质是和 src 一样的)

src 目录下可以直接覆盖的有:

  • src/lib/
  • src/style/
  • src/index.js

需要灵活调配的有:

  • src/controller/
  • src/views/
  • src/config.js
    如果没有改动默认配置,事实上 config.js 也可以覆盖升级

开发过程中,建议同时运行两个 layuiAdmin 。一个是已经实际运用的,一个是 layuiAdmin 本身的 Demo。以便从 Demo 中获取参考和提取示例。

源码构建

当你在 src 目录完成开发后,你可通过 gulp 对 src 源码进行自动化构建,以生成用于线上环境的 dist 目录。
其中,gulpfile.js 是 layuiAdmin 写好的任务脚本,package.json 是任务配置文件,你只需按照以下步骤:

  • step1:确保你的电脑已经安装好了 Node.js,如果未安装,可去官网下载安装传送门
  • step2: 命令行安装 gulp:npm install gulp -g
  • step3:切换到 layuiAdmin 项目根目录(即 gulpfile.js 所在目录),命令行安装任务所依赖的包:npm install

安装完成后,后续只需直接执行命令:gulp 即可完成 src 到 dist 目录的构建

关于版权

layuiAdmin 受国家计算机软件著作权保护,未经官网正规渠道授权擅自公开产品源文件、以及直接对产品二次出售的,我们将追究相应的法律责任。

获取官方正版授权: 传送门

正则表达式

发表于 2020-02-18

元字符

下表包含了元字符的完整列表以及它们在正则表达式上下文中的行为:

字符 描述
\ 将下一个字符标记为一个特殊字符、或一个原义字符、或一个 向后引用、或一个八进制转义符。例如,’n’ 匹配字符 “n”。’\n’ 匹配一个换行符。序列 ‘\‘ 匹配 “" 而 “(“ 则匹配 “(“。
^ 匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 ‘\n’ 或 ‘\r’ 之后的位置。
$ 匹配输入字符串的结束位置。如果设置了RegExp 对象的 Multiline 属性,$ 也匹配 ‘\n’ 或 ‘\r’ 之前的位置。
* 匹配前面的子表达式零次或多次。例如,zo* 能匹配 “z” 以及 “zoo”。* 等价于{0,}。
+ 匹配前面的子表达式一次或多次。例如,’zo+’ 能匹配 “zo” 以及 “zoo”,但不能匹配 “z”。+ 等价于 {1,}。
? 匹配前面的子表达式零次或一次。例如,”do(es)?” 可以匹配 “do” 或 “does” 。? 等价于 {0,1}。
{n} n 是一个非负整数。匹配确定的 n 次。例如,’o{2}’ 不能匹配 “Bob” 中的 ‘o’,但是能匹配 “food” 中的两个 o。
{n,} n 是一个非负整数。至少匹配n 次。例如,’o{2,}’ 不能匹配 “Bob” 中的 ‘o’,但能匹配 “foooood” 中的所有 o。’o{1,}’ 等价于 ‘o+’。’o{0,}’ 则等价于 ‘o*’。
{n,m} m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,”o{1,3}” 将匹配 “fooooood” 中的前三个 o。’o{0,1}’ 等价于 ‘o?’。请注意在逗号和两个数之间不能有空格。
? 当该字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串 “oooo”,’o+?’ 将匹配单个 “o”,而 ‘o+’ 将匹配所有 ‘o’。
. 匹配除换行符(\n、\r)之外的任何单个字符。要匹配包括 ‘\n’ 在内的任何字符,请使用像”(.
(pattern) 匹配 pattern 并获取这一匹配。所获取的匹配可以从产生的 Matches 集合得到,在VBScript 中使用 SubMatches 集合,在JScript 中则使用 $0…$9 属性。要匹配圆括号字符,请使用 ‘(‘ 或 ‘)‘。
(?:pattern) 匹配 pattern 但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用 “或” 字符 (
(?=pattern) 正向肯定预查(look ahead positive assert),在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,”Windows(?=95
(?!pattern) 正向否定预查(negative assert),在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如”Windows(?!95
(?<=pattern) 反向(look behind)肯定预查,与正向肯定预查类似,只是方向相反。例如,”(?<=95
(?<!pattern) 反向否定预查,与正向否定预查类似,只是方向相反。例如”(?<!95
x|y 匹配 x 或 y。例如,’z
[xyz] 字符集合。匹配所包含的任意一个字符。例如, ‘[abc]’ 可以匹配 “plain” 中的 ‘a’。
[^xyz] 负值字符集合。匹配未包含的任意字符。例如, ‘[^abc]’ 可以匹配 “plain” 中的’p’、’l’、’i’、’n’。
[a-z] 字符范围。匹配指定范围内的任意字符。例如,’[a-z]’ 可以匹配 ‘a’ 到 ‘z’ 范围内的任意小写字母字符。
[^a-z] 负值字符范围。匹配任何不在指定范围内的任意字符。例如,’[^a-z]’ 可以匹配任何不在 ‘a’ 到 ‘z’ 范围内的任意字符。
\b 匹配一个单词边界,也就是指单词和空格间的位置。例如, ‘er\b’ 可以匹配”never” 中的 ‘er’,但不能匹配 “verb” 中的 ‘er’。
\B 匹配非单词边界。’er\B’ 能匹配 “verb” 中的 ‘er’,但不能匹配 “never” 中的 ‘er’。
\cx 匹配由 x 指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 ‘c’ 字符。
\d 匹配一个数字字符。等价于 [0-9]。
\D 匹配一个非数字字符。等价于 [^0-9]。
\f 匹配一个换页符。等价于 \x0c 和 \cL。
\n 匹配一个换行符。等价于 \x0a 和 \cJ。
\r 匹配一个回车符。等价于 \x0d 和 \cM。
\s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。
\S 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
\t 匹配一个制表符。等价于 \x09 和 \cI。
\v 匹配一个垂直制表符。等价于 \x0b 和 \cK。
\w 匹配字母、数字、下划线。等价于’[A-Za-z0-9_]’。
\W 匹配非字母、数字、下划线。等价于 ‘[^A-Za-z0-9_]’。
\xn 匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如,’\x41’ 匹配 “A”。’\x041’ 则等价于 ‘\x04’ & “1”。正则表达式中可以使用 ASCII 编码。
\num 匹配 num,其中 num 是一个正整数。对所获取的匹配的引用。例如,’(.)\1’ 匹配两个连续的相同字符。
\n 标识一个八进制转义值或一个向后引用。如果 \n 之前至少 n 个获取的子表达式,则 n 为向后引用。否则,如果 n 为八进制数字 (0-7),则 n 为一个八进制转义值。
\nm 标识一个八进制转义值或一个向后引用。如果 \nm 之前至少有 nm 个获得子表达式,则 nm 为向后引用。如果 \nm 之前至少有 n 个获取,则 n 为一个后跟文字 m 的向后引用。如果前面的条件都不满足,若 n 和 m 均为八进制数字 (0-7),则 \nm 将匹配八进制转义值 nm。
\nml 如果 n 为八进制数字 (0-3),且 m 和 l 均为八进制数字 (0-7),则匹配八进制转义值 nml。
\un 匹配 n,其中 n 是一个用四个十六进制数字表示的 Unicode 字符。例如, \u00A9 匹配版权符号 (?)。

常用正则

汉字

1
/[\u4e00-\u9fa5]/

手机

1
/^1[3-9]\d{9}$/

移动

1
/^1(3[4-9]\|47\|5[012789]\|78\|8[23478])\d{8}$/

联通

1
/^1(3[0-2]\|5[56]\|8[56]\|76)\d{8}$/

电信

1
/^1(33\|53\|8[019]\|77)\d{8}$/

邮箱

1
/\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/

火车车次

1
/^[GCDZTSPKXLY1-9]\d{1,4}$/

手机机身码(IMEI)

1
/^\d{15,17}$/

必须带端口号的网址(或ip)

1
/^((ht|f)tps?:\/\/)?[\w-]+(\.[\w-]+)+:\d{1,5}\/?$/

网址(URL)

1
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/

统一社会信用代码

1
/^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/

统一社会信用代码(宽松匹配)(15位/18位/20位数字/字母)

1
/^(([0-9A-Za-z]{15})|([0-9A-Za-z]{18})|([0-9A-Za-z]{20}))$/

迅雷链接

1
/^thunderx?:\/\/[a-zA-Z\d]+=$/

ed2k链接(宽松匹配)

1
/^ed2k:\/\/\|file\|.+\|\/$/

磁力链接(宽松匹配)

1
/^magnet:\?xt=urn:btih:[0-9a-fA-F]{40,}.*$/

子网掩码(不包含 0.0.0.0)

1
/^(254|252|248|240|224|192|128)\.0\.0\.0|255\.(254|252|248|240|224|192|128|0)\.0\.0|255\.255\.(254|252|248|240|224|192|128|0)\.0|255\.255\.255\.(255|254|252|248|240|224|192|128|0)$/

linux”隐藏文件”路径

1
/^\/(?:[^/]+\/)*\.[^/]*/

linux文件夹路径

1
/^\/(?:[^/]+\/)*$/

linux文件路径

1
/^\/(?:[^/]+\/)*[^/]+$/

window”文件夹”路径

1
/^[a-zA-Z]:\\(?:\w+\\?)*$/

window下”文件”路径

1
/^[a-zA-Z]:\\(?:\w+\\)*\w+\.\w+$/

股票代码(A股)

1
/^(s[hz]|S[HZ])(000[\d]{3}|002[\d]{3}|300[\d]{3}|600[\d]{3}|60[\d]{4})$/

大于等于0, 小于等于150, 支持小数位出现5, 如145.5, 用于判断考卷分数

1
/^150$|^(?:\d|[1-9]\d|1[0-4]\d)(?:\.5)?$/

html注释

1
/<!--[\s\S]*?-->/g

md5格式(32位)

1
/^[a-fA-F0-9]{32}$/

GUID/UUID

1
/^[a-f\d]{4}(?:[a-f\d]{4}-){4}[a-f\d]{12}$/i

版本号(version)格式必须为X.Y.Z

1
/^\d+(?:\.\d+){2}$/

视频(video)链接地址(视频格式可按需增删)

1
/^https?:\/\/(.+\/)+.+(\.(swf|avi|flv|mpg|rm|mov|wav|asf|3gp|mkv|rmvb|mp4))$/i

图片(image)链接地址(图片格式可按需增删)

1
/^https?:\/\/(.+\/)+.+(\.(gif|png|jpg|jpeg|webp|svg|psd|bmp|tif))$/i

24小时制时间(HH:mm:ss)

1
/^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/

12小时制时间(hh:mm:ss)

1
/^(?:1[0-2]|0?[1-9]):[0-5]\d:[0-5]\d$/

base64格式

1
/^\s*data:(?:[a-z]+\/[a-z0-9-+.]+(?:;[a-z-]+=[a-z0-9-]+)?)?(?:;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*?)\s*$/i

数字/货币金额(支持负数、千分位分隔符)

1
/^-?\d+(,\d{3})*(\.\d{1,2})?$/

数字/货币金额 (只支持正数、不支持校验千分位分隔符)

1
/(?:^[1-9]([0-9]+)?(?:\.[0-9]{1,2})?$)|(?:^(?:0)$)|(?:^[0-9]\.[0-9](?:[0-9])?$)/

银行卡号(10到30位, 覆盖对公/私账户, 参考微信支付)

1
/^[1-9]\d{9,29}$/

中文姓名

1
/^(?:[\u4e00-\u9fa5·]{2,16})$/

英文姓名

1
/(^[a-zA-Z][a-zA-Z\s]{0,20}[a-zA-Z]$)/

车牌号(新能源)

1
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z](?:((\d{5}[A-HJK])|([A-HJK][A-HJ-NP-Z0-9][0-9]{4}))|[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳])$/

车牌号(非新能源)

1
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]$/

车牌号(新能源+非新能源)

1
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/

手机号(mobile phone)中国(严谨), 根据工信部2019年最新公布的手机号段

1
/^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[189]))\d{8}$/

手机号(mobile phone)中国(宽松), 只要是13,14,15,16,17,18,19开头即可

1
/^(?:(?:\+|00)86)?1[3-9]\d{9}$/

手机号(mobile phone)中国(最宽松), 只要是1开头即可, 如果你的手机号是用来接收短信, 优先建议选择这一条

1
/^(?:(?:\+|00)86)?1\d{10}$/

日期(宽松)

1
/^\d{1,4}(-)(1[0-2]|0?[1-9])\1(0?[1-9]|[1-2]\d|30|31)$/

日期(严谨, 支持闰年判断)

1
/^(([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})-(((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[1][0-9]|2[0-8]))))|((([0-9]{2})(0[48]|[2468][048]|[13579][26])|((0[48]|[2468][048]|[3579][26])00))-02-29)$/

中国省

1
/^浙江|上海|北京|天津|重庆|黑龙江|吉林|辽宁|内蒙古|河北|新疆|甘肃|青海|陕西|宁夏|河南|山东|山西|安徽|湖北|湖南|江苏|四川|贵州|云南|广西|西藏|江西|广东|福建|台湾|海南|香港|澳门$/

可以被moment转化成功的时间 YYYYMMDD HH:mm:ss

1
/^\d{4}([/:-\S])(1[0-2]|0?[1-9])\1(0?[1-9]|[1-2]\d|30|31) (?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/

email(邮箱)

1
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/

座机(tel phone)电话(国内),如: 0341-86091234

1
/^(?:(?:\d{3}-)?\d{8}|^(?:\d{4}-)?\d{7,8})(?:-\d+)?$/

身份证号(1代,15位数字)

1
/^[1-9]\d{7}(?:0\d|10|11|12)(?:0[1-9]|[1-2][\d]|30|31)\d{3}$/

身份证号(2代,18位数字),最后一位是校验位,可能为数字或字符X

1
/^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/

身份证号, 支持1/2代(15位/18位数字)

1
/^\d{6}((((((19|20)\d{2})(0[13-9]|1[012])(0[1-9]|[12]\d|30))|(((19|20)\d{2})(0[13578]|1[02])31)|((19|20)\d{2})02(0[1-9]|1\d|2[0-8])|((((19|20)([13579][26]|[2468][048]|0[48]))|(2000))0229))\d{3})|((((\d{2})(0[13-9]|1[012])(0[1-9]|[12]\d|30))|((\d{2})(0[13578]|1[02])31)|((\d{2})02(0[1-9]|1\d|2[0-8]))|(([13579][26]|[2468][048]|0[048])0229))\d{2}))(\d|X|x)$/

护照(包含香港、澳门)

1
/(^[EeKkGgDdSsPpHh]\d{8}$)|(^(([Ee][a-fA-F])|([DdSsPp][Ee])|([Kk][Jj])|([Mm][Aa])|(1[45]))\d{7}$)/

帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线组合

1
/^[a-zA-Z]\w{4,15}$/

中文/汉字

1
/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])+$/

小数

1
/^\d+\.\d+$/

只包含数字

1
/^\d+$/

html标签(宽松匹配)

1
/<(\w+)[^>]*>(.*?<\/\1>)?/

匹配中文汉字和中文标点

1
/[\u4e00-\u9fa5|\u3002|\uff1f|\uff01|\uff0c|\u3001|\uff1b|\uff1a|\u201c|\u201d|\u2018|\u2019|\uff08|\uff09|\u300a|\u300b|\u3008|\u3009|\u3010|\u3011|\u300e|\u300f|\u300c|\u300d|\ufe43|\ufe44|\u3014|\u3015|\u2026|\u2014|\uff5e|\ufe4f|\uffe5]/

qq号格式正确

1
/^[1-9][0-9]{4,10}$/

数字和字母组成

1
/^[A-Za-z0-9]+$/

英文字母

1
/^[a-zA-Z]+$/

小写英文字母组成

1
/^[a-z]+$/

大写英文字母

1
/^[A-Z]+$/

密码强度校验,最少6位,包括至少1个大写字母,1个小写字母,1个数字,1个特殊字符

1
/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/

用户名校验,4到16位(字母,数字,下划线,减号)

1
/^[a-zA-Z0-9_-]{4,16}$/

ip-v4[:端口]

1
/^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])(?::(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$/

ip-v6[:端口]

1
/(^(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$)|(^\[(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))\](?::(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$)/i

16进制颜色

1
/^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/

微信号(wx),6至20位,以字母开头,字母,数字,减号,下划线

1
/^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/

邮政编码(中国)

1
/^(0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[1-7]|6[1-7]|7[0-5]|8[013-6])\d{4}$/

中文和数字

1
/^((?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])|(\d))+$/

不能包含字母

1
/^[^A-Za-z]*$/

java包名

1
/^([a-zA-Z_]\w*)+([.][a-zA-Z_]\w*)+$/

mac地址

1
/^((([a-f0-9]{2}:){5})|(([a-f0-9]{2}-){5}))[a-f0-9]{2}$/i

匹配连续重复的字符

1
/(.)\1+/

数字和英文字母组成,并且同时含有数字和英文字母

1
/^(?=.*[a-zA-Z])(?=.*\d).+$/

香港身份证

1
/^[a-zA-Z]\d{6}\([\dA]\)$/

澳门身份证

1
/^[1|5|7]\d{6}\(\d\)$/

台湾身份证

1
/^[a-zA-Z][0-9]{9}$/

大写字母,小写字母,数字,特殊符号 @#$%^&*~()-+=` 中任意3项密码

1
/^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\W_!@#$%^&*`~()-+=]+$)(?![a-z0-9]+$)(?![a-z\W_!@#$%^&*`~()-+=]+$)(?![0-9\W_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9\W_!@#$%^&*`~()-+=]/

ASCII码表中的全部的特殊字符

1
/[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/

正整数,不包含0

1
/^\+?[1-9]\d*$/

负整数,不包含0

1
/^-[1-9]\d*$/

整数

1
/^-?[1-9]\d*$/

浮点数

1
/^(-?[1-9]\d*\.\d+|-?0\.\d*[1-9]\d*|0\.0+)$/

浮点数(严格)

1
/^(-?[1-9]\d*\.\d+|-?0\.\d*[1-9])$/

email(支持中文邮箱)

1
/^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/

域名(非网址, 不包含协议)

1
/^([0-9a-zA-Z-]{1,}\.)+([a-zA-Z]{2,})$/

NGINX常用指令

发表于 2020-02-15 分类于 服务器运维

最近经常用到nginx,所以想着系统的看一下常用的指令

HttpCore指令集

server_name

含义:设置虚拟服务器的名字,第一个名字将成为主服务器名称;服务器名称可以使用*代替名称的第一部分或者最后一部分;也可以使用正则表达式进行捕获,目前不常用

示例

1
2
3
4
5
6
7
8
9
10
11
server {
server_name example.com *.example.com www.example.*;
}
# 使用正则
server {
server_name ~^(www\.)?(.+)$;

location / {
root /sites/$2;
}
}

location

含义:根据URI设置配置,可以使用合法的字符串或者正则表达式

语法:location [=||*|^~] /uri/ { … }

上下文:server

匹配符 含义
= 精确匹配
~ 正则,大小写敏感
~* 正则,大小写不敏感
^~ 前缀匹配

示例

1
2
3
4
5
6
7
8
9
10
11
12
location = /ceshi1/ {
proxy\_pass http://127.0.0.1:7001/xxxx;
# /ceshi1 -> /xxxx
# /ceshi1/a -> 404 Not Found
# /ceshi1a -> 404 Not Found
}
location ^~ /ceshi2/ {
proxy\_pass http://127.0.0.1:7001/xxxx;
# /ceshi2 -> /xxxx
# /ceshi2/a -> /xxxxa
# /ceshi2a -> 404 Not Found
}

常用变量

变量名 含义
$arg_PARAMETER GET请求的参数,PARAMETER为参数名
args/query_string GET请求的query_string,比如/test?xxxx=ceshi1&yyyy=ceshi2,则args_xxxx 为 ceshi1
$content_type 请求头Content-Type
$cookie_COOKIE 名称为COOKIE的cookie
$host 按照以下优先顺序:来自请求行的主机名,来自 Host 请求头字段的主机名,或与请求匹配的服务器名
$https 如果连接以 SSL 模式运行,则为 on,否则为空字符串
$is_args 如果请求行有参数则为 ?,否则为空字符串
$http_HEADER 获取请求头字段,注意要将中划线给为下划线,示例为http_referer
$remote_addr 客户端地址
$remote_port 客户端端口
$request_method 请求方法
$request_uri 完整的原始请求URI(带参数)
$scheme 请求模式,http或https
$status 响应状态
document_uri 请求的url path,可能和初始不同,比如使用rewrite

HttpUpstream指令集

该模块用于定义可被proxy_pass等指令应用的服务器组

upstream

含义:定义一组服务器,服务器可以监听不同端口。

示例

1
2
3
4
5
6
7
8
9
10
11
12
upstream backend {
server backend1.example.com weight=5;
server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
server unix:/tmp/backend3;

server backup1.example.com backup;
}
server {
location / {
proxy_pass http://backend;
}
}

默认情况下,使用加权轮询均衡算法在服务器间分配请求。在上面的示例中,每 7 个请求将按如下方式分发:5 个请求转到 backend1.example.com,另外 2 个请求分别转发给第二个和第三个服务器。如果在与服务器通信期间发生错误,请求将被传递到下一个服务器,依此类推,直到尝试完所有正常运行的服务器。如果无法从这些服务器中获得成功响应,则客户端将接收到与最后一个服务器的通信结果。

server

含义:定义服务器地址和其他参数

HttpRewrite指令集

rewrite

含义:根据正则表达式修改URL或者修改字符串,注意,重写表达式只对相对路径有效,如果你想匹配主机名,应该使用if语句

使用位置:server或者location或者if

语法:rewrite regex replacement flag

关于flag

值 含义
last 停止当前请求,并根据该次修改重新发起请求,然后走一遍这个配置文件
break 替换后继续往下执行指令(完成rewrite指令集)
redirect 临时重定向,返回302
permanent 永久重定向,返回301

关于参数

如果不想让匹配的内容的参数附在重定向的后面,则在最后加一个问号,如下

1
rewrite  ^/users/(.*)$  /show?user=$1?  last;

工程示例

1
rewrite ./test/index.html break; # 转发所有的请求给index.html页面,并完成所有的rewrite指令集

break

含义:完成所有的rewrite指令集

使用位置:server,location,if

if

含义:逻辑判断

使用位置:server,location

语法:if(condition){}

比较符

符号 含义
= 相等
!= 不等
~* 正则匹配,大小写不敏感
~ 正则匹配,大小写敏感
!~* 不符合,大小写不敏感
!~ 不符合,大小写敏感
-f and !-f 判断文件存在或者不存在
-d and !-d 检测一个目录是否存在
-e and !-e 检测是否存在一个文件,一个目录或者一个符号链接
-x and !-x 检测一个文件是否可执行

示例

1
2
3
4
set $xsrfToken "";
if ($http_cookie ~* "XSRF-TOKEN=(.+?)(?=;|$)"){
set $xsrfToken $1; # 设置变量xsrfToken为cookie中匹配到的值
}

set

含义:设置变量的值

使用位置(上下文):server,location,if

示例:如上

return

含义:返回一个状态值给客户端

使用位置(上下文):server,location,if

示例:

1
2
3
if ($invalid_referer) { # 检测到Referers不合法,则禁止访问,返回403
return 403;
}

HttpProxy指令集

proxy_pass

含义:设置代理服务器的协议、地址以及映射位置的可选URL,协议可以指定http或https,可以将地址指定为域名或IP地址,以及一个可选端口号;如果域名解析为多个地址,则所有这些地址将以轮询方式使用;可以将地址指定为upstream

使用位置: http -> server -> location

语法:proxy_pass URL

转发时,URI的传递方式如下

  • 如果proxy_pass指定了URI,则转发时会将location匹配的规则部分替换为URI部分
1
2
3
location /name/ {
proxy_pass http://127.0.0.1/remote/; # http://a.com/name/test -> http://127.0.0.1/remote/test
}
  • 如果proxy_pass没有指定URI,则请求URL将会以location匹配为准,示例如下
1
2
3
location /xxxx {
proxy_pass http://127.0.0.1; # 将http://a.com/xxxx/test -> http://127.0.0.1/xxxx/test
}

例外情况,无法确定怎么替换URI

  • location使用正则匹配,proxy_pass不使用URI
  • 使用rewrite,将忽略proxy_pass中的URI

proxy_set_header

含义:设置请求头内容

语法:proxy_set_header header value

使用位置:http,server,location

1
proxy_set_header X-XSRF-TOKEN $xsrfToken;

proxy_connect_timeout

含义:定义与代理服务器建立连接的超时时间,注意,次超时时间通常不会超过75s

1
proxy_connect_timeout 60;

proxy_send_timeout

含义:设置将请求传输到代理服务器的超时时间。超时时间仅作用于两个连续的写操作之间,而不是整个请求的传输过程。如果代理服务器在该时间内未收到任何内容,则关闭连接。

1
proxy_send_timeout 60;

proxy_read_timeout

含义:定义从代理服务器读取响应的超时时间。该超时时间仅针对两个连续的读操作之间设置,而不是整个响应的传输过程。如果代理服务器在该时间内未传输任何内容,则关闭连接。

1
proxy_read_timeout 60;

HttpHeaders指令集

add_header

含义:增加响应头内容

使用位置: http -> server -> location

语法: add_header name value;

1
add_header Cache-Control 'no-store'; # 设置所有内容不会被缓存

expires

含义:增加响应头Expires字段,便于确定缓存时间

使用位置:http -> server -> location

语法:expires [time|epoch|max|off]

值 含义
time 数量
epoch 1 January, 1970, 00:00:01 GMT
max Cache-Control值为10年,Expires为31 December 2037 23:59:59 GMT
off 【默认值】不使用时间
1
expires off; #不使用过期时间

注意1:优先级 强缓存优先级 > 对比缓存优先级 ;对于强缓存优先级,pragma > Cache-Control > Expires;对于对比缓存优先级,ETag > Last-Modified

注意2:Cache-Control是http1.1的头字段,Expires是http1.0的头字段,建议两者都写

注意3:Cache-Control默认值为private,其他细节不赘述

细节参考:https://www.imooc.com/article/22841

HttpReferer指令集

valid_referers

含义:判断请求头Referers的正确性,结果会赋值给$invalid_referer

使用位置:http -> server -> location

语法:valid_referers [none|blocked|server_names]

值 含义
none 无Referer,一般直接刷新会是如此
blocked 有,但是被删除,这些值不以http://或者https://开头
server_names 写一个匹配的路径规则,要带域名,会从scheme后面开始匹配,端口也会忽略,写正则以~开头
1
2
3
4
5
6
7
location /views {
valid_referers *.com/chart/; #判断Referer是否匹配给出的路径,匹配则$invalid_referer为false,否则为true
if ($invalid_referer) {
return 403;
}
}
# 如果Referer为a.com/chart/1则符合规则,如果为a.com/chart则不符合,如果没有referer也不符合

附录

location/proxy_pass/rewrite对比使用总结

将location和proxy_pass的所有匹配情况测试结果放在下面,对应关系为【访问path -> 转发成的path】

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
location /test1 {
proxy_pass http://127.0.0.1:7001/;
# /test1 -> /
# /test1/a -> //a
# /test1a -> /a
}
location /test2 {
proxy_pass http://127.0.0.1:7001/xxxx;
# /test2 -> /xxxx
# /test2/a -> /xxxx/a
# /test2a -> /xxxxa
}
location /test3 {
proxy_pass http://127.0.0.1:7001/xxxx/;
# /test3 -> /xxxx//
# /test3/a -> /xxxx//a
# /test3a -> /xxxx/a
}
location /test4 {
proxy_pass http://127.0.0.1:7001;
# /test4 -> /test4
# /test4/a -> /test4/a
# /test4a -> /xxxx4a
}
location /test5/ {
proxy_pass http://127.0.0.1:7001/;
# /test5 -> /
# /test5/a -> /a
# /test5a -> 404 Not Found
}
location /test6/ {
proxy_pass http://127.0.0.1:7001/xxxx;
# /test6 -> /xxxx
# /test6/a -> /xxxxa
# /test6a -> 404 Not Found
}
location /test7/ {
proxy_pass http://127.0.0.1:7001/xxxx/;
# /test7 -> /xxxx/
# /test7/a -> /xxxx/a
# /test7a -> 404 Not Found
}
location /test8/ {
proxy_pass http://127.0.0.1:7001;
# /test8 -> /test8/
# /test8/a -> /test8/a
# /test8a -> 404 Not Found
}
location /test9/ {
rewrite . /b break;
proxy_pass http://127.0.0.1:7001/xxxx;
# /test9 -> /b
# /test9/a -> /b
# /test9a -> 404 Not Found
}
location ~* /test10(.*)/ {
#proxy_pass http://127.0.0.1:7001/xxxx;
# nginx无法执行通过,会报错,不能带URI
}

location = /ceshi1/ {
proxy_pass http://127.0.0.1:7001/xxxx;
# /ceshi1 -> /xxxx
# /ceshi1/a -> 404 Not Found
# /ceshi1a -> 404 Not Found
}
location ^~ /ceshi2/ {
proxy_pass http://127.0.0.1:7001/xxxx;
# /ceshi2 -> /xxxx
# /ceshi2/a -> /xxxxa
# /ceshi2a -> 404 Not Found
}

PHP发布自己的composer包

发表于 2020-01-13 分类于 PHP

1. 准备工作

注册 Github 账号 : https://github.com/
注册 Packagist 账号 : https://packagist.org/ (可以使用github账户登录)

2. 添加git仓库

2.1 创建 Github 仓库

并按要求填写信息

2.2 克隆到本地

1
git clone https://github.com/gouyuwang/lumen.git

2.3 初始化composer.json

1
composer init

接下来全部回车 (如果有依赖,则按需求搜索依赖包)

或者手动创建composer.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
{
"name": "gouyuwang/lumen",
"description": "lumen helper",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "gouyuwang",
"email": "529156563@qq.com"
}
],
# 如果你的数据需要加上PHP版本,并加入了src目录 需要追加下面配置
"require": {
"php": ">=7.0",
"php-curl-class/php-curl-class": "^7.2"
},
"require-dev": {
"composer/composer": "^1.2",
"friendsofphp/php-cs-fixer": "~2",
"phpunit/phpunit": "^4.8.35 || ^5.7",
"php-curl-class/php-curl-class": "^7.2"
},
"autoload": {
"psr-4": {
"gouyuwang\\lumen\\": "src/"
}
}
}

2.4 初始化 .gitignore

文件内容

1
/vendor/

2.5 业务逻辑

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

namespace gouyuwang\lumen;

class Response
{
public static function echo ( $text)
{
header("Content-type:text/html");
echo $text;
exit;
}
}

注意:PHP文件书写几个要点

  • 文中出现的不管是系统还是依赖的class 都必须开头使用 use 引入
  • 命名空间是你的项目名称对应的目录 [git-username]/[文件目录]
  • 代码放入src 类名与文件名最好相同

2.6 文件目录结构

3. 上传至git

如果有sourcetree 可以忽略下面的提交命令

3.1 提交

1
2
3
4
5
git init
git add .
git commit -m "first commit"
git remote add origin gouyuwang@github.com:gouyuwang/lumen.git
git push origin master

3.2 打tag

为什么要打tag?

tag相当于你的项目到了一个新的阶段,不再是开发版,否则使用

1
composer require gouyuwang/lumen @dev #其中 `@dev` 必不可少

查看最新提交的版本号

1
git log --oneline --decorate --graph

打tag

1
2
3
git tag v1.1 61d974a  // 在某个commit 上打tag

git push origin test_tag // 提交到线上库

删除 tag

1
2
3
git tag -d v1.1 // 删除某个commit 上的tag

git push origin :refs/tags/v1.1 // 提交到线上库

4. 提交packageist

访问并登陆:https://packagist.org

点击右上角的 submit , 输入你的git地址 : https://github.com/gouyuwang/lumen.git

点击 check, 完成后点击 submit 即提交完成

5. 检出依赖包

1
composer require gouyuwang/lumen

如果报错:

1
2
[InvalidArgumentException]
Could not find a version of package gouyuwang/lumen matching your minimum-stability (stable). Require it with an explicit version constraint allowing its desired stability.

可能是你的tag没打成功,或者打成功后,并没有同步到国外服务器,请耐心等待

替代方案:

1
composer require gouyuwang/lumen @dev

现在你可以在项目中使用你写的依赖库了

Linux禁止ping以及开启ping的方法

发表于 2020-01-10

Linux默认是允许Ping响应的,系统是否允许Ping由内核参数、防火墙2个因素决定的,需要2个因素同时允许才能允许Ping,2个因素有任意一个禁Ping就无法Ping。

具体的配置方法如下:

一、内核参数设置

icmp_echo_ignore_all : 0表示允许,1表示禁止

e攻城狮

  • 临时允许PING操作的命令
1
> echo 0 >/proc/sys/net/ipv4/icmp_echo_ignore_all
  • 永久允许PING配置方法。

/etc/sysctl.conf 中增加一行

1
net.ipv4.icmp_echo_ignore_all=0

如果已经有net.ipv4.icmp_echo_ignore_all这一行了,直接修改值即可。

禁止Ping的方法类似,只需要设置icmp_echo_ignore_all=1即可

修改完成后执行sysctl -p使新配置生效。

二、防火墙设置

(注:此处的方法的前提是内核配置是默认值,也就是没有禁止Ping)

这里以iptables防火墙为例,其他防火墙操作方法可参考防火墙的官方文档。

  • 允许PING设置
1
2
3
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT

iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT

或者也可以临时停止防火墙操作的。

1
service iptables stop
  • 禁止PING设置
1
iptables -A INPUT -p icmp --icmp-type 8 -s 0/0 -j DROP

Lumen项目整合

发表于 2019-12-12

一、安装

通过 Lumen 安装器

首先,使用 Composer 下载 Lumen 安装包:

1
composer global require "laravel/lumen-installer"

请确保你已将 ~/.composer/vendor/bin 路径添加到环境变量 PATH 中,只有这样系统才能找到 lumen 的可执行文件。如何让安装Composer?

一旦安装完成,使用 lumen new 将会在您指定的目录中创建一个新的Lumen 项目。例如: lumen new blog 命令将会创建一个名字叫 blog 的目录 ,此目录里面存放着新安装的 Lumen 和代码依赖。这个方法的安装速度比通过 Composer 安装要快很多:

1
lumen new blog

此方法的缺点也很明显,无法安装指定的lumen版本.需要安装指定版本的Lumen可参考下面的方法.

通过 Composer Create-Project 命令安装

你也可以在你电脑的终端输入 create-project 命令来安装 Lumen :

1
composer create-project  laravel/lumen blog  --prefer-dist "5.2.*"

二、项目目录

在lumen 基础上添加了 Models 目录

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
/
|-- app 项目应用目录
| |-- Console 自定义的 Artisan 命令
| |-- |-- Kernel.php 注册自定义的 Artisan 命令以及定义调度任务
| |-- Events 存放事件类
| |-- Extends 存放自定义扩展,例如Excel导出等
| |-- Exceptions 异常处理
| | |-- Handler.php 处理应用抛出的异常
| |-- helpers.php 自定义函数方法
| |-- Http 主程序
| | |-- Middleware 中间件
| | |-- Controllers 控制器
| | | |-- Admin 后台目录
| | | |-- V1 V1版本
| | | |-- Logic 业务逻辑
| | | |-- Api 前台目录
| | | |-- V1 V1版本
| | | |-- Logic 业务逻辑
| | | |-- Pub 公共目录
| | | |-- V1 V1版本
| | |-- Logic 业务逻辑
| | |-- Controller.php 版本父级控制层
| | |-- Logic.php 版本父级逻辑层
| |-- Jobs 队列任务
| |-- Listeners 事件监听器
| |-- Providers 服务提供
| |-- Models 数据模型目录
|-- bootstrap 自动加载配置
| |-- app.php 启动、自动加载配置
|-- config 所有应用配置文件
| |-- auth.php auth授权配置
| |-- cors.php cors跨域配置
| |-- database.php database配置
|-- database 数据迁移、填充文件
|-- public Web站点根目录
|-- resources 视图、原生资源文件
| |-- lang 语言目录
|-- routes 路由配置文件
| |-- admin 后台管理路由配置
| |-- admin.php
| |-- api 前台路由配置
| |-- api.php
| |-- pub 公共路由配置
| |-- pub.php
|-- storage 缓存、框架生成文件
| |-- app 存放应用生成的文件
| |-- framework 框架生成的文件或缓存
| |-- logs 日志文件
|-- tests 自动化测试
|-- vendor Composer依赖文件
|-- .env 配置文件
|-- .env.example 配置文件备份
|-- composer.json
\

三、 数据模型

3.1 命名规范

数据模型相关的命名规范:

  • 数据模型类名 必须 为「单数」, 如:App\Models\Photo
  • 类文件名 必须 为「单数」,如:app/Models/Photo.php
  • 数据库表名字 必须 为「复数」,多个单词情况下使用「Snake Case」 如:photos, my_photos
  • 数据库表迁移名字 必须 为「复数」,如:2014_08_08_234417_create_photos_table.php
  • 数据填充文件名 必须 为「复数」,如:PhotosTableSeeder.php
  • 数据库字段名 必须 为「Snake Case」,如:view_count, is_vip
  • 数据库表主键 必须 为「id」
  • 数据库表外键 必须 为「resource_id」,如:user_id, post_id
  • 数据模型变量 必须 为「resource_id」,如:$user_id, $post_id

3.1 Mysql模型

在app目录下创建数据模型 Models

在数据模型开头修改命名空间

1
2
3
4
5
6
7
8
# 在app根目录的数据模型 命名空间为:
namespace App;

#修改为命名空间为
namespace App\Models;

#使用 Models下的User模型
use App\Models\User;

其他目录的添加同上,只是在命名空间上做修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
namespace App\Models;

use App\Extend\TraitDbEloquent;
use Illuminate\Database\Eloquent\Model;

class BaseModel extends Model
{
use TraitDbEloquent;

// default db
protected $connection = 'mysql';

public function __construct()
{
parent::__construct();
}
}

3.2 Mongo模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace App\Mongo;

use App\Extend\TraitDbEloquent;
use Jenssegers\Mongodb\Eloquent\Model as Eloquent;

class BaseModel extends Eloquent
{
use TraitDbEloquent;

// primary key
protected $primaryKey = "_id";

// primary key type
protected $keyType = "string";

// default db
protected $connection = 'mongodb';

public function __construct()
{
parent::__construct();
}
}

其中 TraitDbEloquent 封装了数据库CURD操作。

四、统一输出

在helpers.php中统一处理输出模块,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 统一输出
* @param int $code
* @param string $msg
* @param array $data
* @return array
*/
function format_return($code, $msg = null, $data = null)
{
$res = [
'code' => $code,
'msg' => is_null($msg) ? trans("reponse.$code") : $msg
];
if (!is_null($data)) {
$res['data'] = $data;
}
return $res;
}

其中trans定义了状态信息,放在 ./resources/lang/文件,方便支持多语言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// zh-CN/reponse.php 中文简体
<?php

return [
2000 => '请求成功',
4000 => '请求失败',
];

//en/reponse.php 英文
<?php

return [
2000 => 'ok',
4000 => 'fail',
];

五、 JWT配置

5.1 安装

通过composer安装jwt-auth

1
composer require tymon/jwt-auth "1.*"

注意:jwt-auth 0.5.* 版本未对lumen做封装

5.2 修改自动加载配置 文件 bootstrap/app.php

  • 去掉 $app->withFacades() $app->withEloquent() 的注释
  • 添加 jwt 接口
1
2
3
4
5
6
7
8

//使用 Facades 静态类
$app->withFacades(true,[
'Tymon\JWTAuth\Facades\JWTAuth' => 'JWTAuth',
'Tymon\JWTAuth\Facades\JWTFactory' => 'JWTFactory'
]);

$app->withEloquent(); //使用 Eloquent ORM
  • 去掉 auth 中间件 注释
1
2
3
4
//auth 中间件
$app->routeMiddleware([
'auth' => App\Http\Middleware\Authenticate::class,
]);
  • 去掉appServiceProvider的注释,并且在 AppServiceProvider 中注册 LumenServiceProvider
1
2
3
$app->register(App\Providers\AppServiceProvider::class);
$app->register(App\Providers\AuthServiceProvider::class);
$app->register(\Tymon\JWTAuth\Providers\LumenServiceProvider::class);

5.3 jwt配置

  • 获取 auth 配置文件
    在 lumen 根目录下 创建 config 文件夹 (laravel 框架 自动加载 config 文件夹的内容),并将 vendor/laravel/lumen-framework/config 中的 auth.php 文件复制到刚刚创建的config文件夹中。修改 auth.php 文件,将 api 认证指定为 jwt,并绑定users 数据模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//原
'guards' => [
'api' => ['driver' => 'api'],
],

//修改为:
'guards' => [
'api' => ['driver' => 'jwt'],
'provider' => 'users'
],


//指定数据模型
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => \App\Models\User::class,
],
],
  • JWT 协议需要用到 secret,所以需要生成一个 secret,在根目录下 执行命令
1
2
3
php artisan jwt:secret
//执行成功返回如下
jwt-auth secret [Y] set successfully.

执行成功后,会把生成的secret写入 .env 文件中
并配置jwt token 的三个时间

1
2
3
4
5
6
7
JWT_SECRET=xxxxxxxxxx
//有效时间 单位:分钟
JWT_TTL = 60
//刷新时间 单位:分钟 默认 14天
JWT_REFRESH_TTL = 20160
//宽限时间 单位:秒
JWT_BLACKLIST_GRACE_PERIOD = 60

作者更喜欢使用这个JWT扩展类 https://github.com/firebase/php-jwt

六、异步事件处理

在很多场景中我们需要某些特定函数后台运行,例如 访问量+1 、发送短信通知、日志存储等,同步的做法是在业务逻辑执行完成以后,在执行文件末尾添加 相关的访问量统计等方法。 这对程序的效率是由很大的影响的,采用异步处理可以有效处理此类业务场景。
lumen提供了 events 事件处理机制。以日志监控为例,我们需要对用户请求、程序响应记录在日志服务器上面,这一块我们只需要在Middleware中拦截入口流量和出口流量,记录即可。代码如下:

  • 注册EventServiceProvider
1
2
// Event
$app->register(App\Providers\EventServiceProvider::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
    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
    <?php
    namespace App\Http\Middleware;

    use App\Events\MonitorEvent;
    use Closure;


    class Monitor
    {
    /**
    * Handle an incoming request.
    *
    * @param \Illuminate\Http\Request $request
    * @param \Closure $next
    * @param string|null $guard
    *
    * @return mixed
    */
    public function handle($request, Closure $next, $guard = null)
    {
    // 开始计时
    $start = microtime(true);
    // 执行业务
    $response = $next($request);
    // 结束计时
    $end = microtime(true);

    // 流量监控
    if (ENV('OPEN_TRAFFIC_MONITOR', true)) {
    $params = [
    'ip' => $request->getClientIp(),
    'host' => $request->getHost(),
    'route' => $request->path(),
    'method' => $request->method(),
    'header' => $request->header(),
    'params' => limit_var_size($request->all(), 1024),
    'response' => limit_var_size($response->getOriginalContent(), 1024),
    'require_time' => date('Y-m-d H:i:s', $start),
    'response_time' => date('Y-m-d H:i:s', $end),
    'ttl' => $end - $start,
    'status' => $response->getStatusCode(),
    ];
    // 触发事件
    event(new MonitorEvent($params));
    }

    return $response;
    }
    }
  • 事件接收器

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
<?php

namespace App\Events;

use Illuminate\Queue\SerializesModels;

abstract class Event
{
use SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public $data;

public function __construct($data = [])
{
$this->data = $data;
}

public function getData()
{
return $this->data;
}
}
  • 事件监听处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace App\Listeners;

use App\Events\Event;
use App\Http\Controllers\Pub\V1\Logic\MonitorLogic;

class MonitorListener extends Listener
{

/**
* Handle the event.
*
* @param $event
* @return mixed
*/
public function handle(Event $event)
{
return (new MonitorLogic())->run($event->getData());
}
}
  • 注册事件处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php  
namespace App\Providers;

use Laravel\Lumen\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'App\Events\MonitorEvent' => [
'App\Listeners\MonitorListener',
],
];
}

最重要的一步, .env配置中 QUEUE_DRIVER=sync 改为 QUEUE_DRIVER=redis,即把同步队列设置为异步队列。

配置完成,启动队列: php artisan queue:listen

GET和POST的区别

发表于 2019-12-09 分类于 PHP

GET 和 POST 是 HTTP 请求的两种基本方法,要说它们的区别,接触过 WEB 开发的人都能说出一二。
最直观的区别就是 GET 把参数包含在 URL 中,POST 通过 request body 传递参数。
你可能自己写过无数个 GET 和 POST 请求,或者已经看过很多权威网站总结出的他们的区别,你非常清楚知道什么时候该用什么。
当你在面试中被问到这个问题,你的内心充满了自信和喜悦。

你轻轻松松的给出了一个 “标准答案”:

  • GET在浏览器回退时是无害的,而POST会再次提交请求。
  • GET产生的URL地址可以被Bookmark,而POST不可以。
  • GET请求会被浏览器主动cache,而POST不会,除非手动设置。
  • GET请求只能进行url编码,而POST支持多种编码方式。
  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  • GET请求在URL中传送的参数是有长度限制的,而POST么有。
  • 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
  • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
  • GET参数通过URL传递,POST放在Request body中。

“很遗憾,这不是我们要的回答!”

如果我告诉你 GET 和 POST 本质上没有区别你信吗?
让我们扒下 GET 和 POST 的外衣,坦诚相见吧!

GET 和 POST 是什么?HTTP 协议中的两种发送请求的方法。

HTTP 是什么?HTTP 是基于 TCP/IP 的关于数据如何在万维网中如何通信的协议。

HTTP 的底层是 TCP/IP。所以 GET 和 POST 的底层也是 TCP/IP,也就是说,GET/POST 都是 TCP 链接。GET 和 POST 能做的事情是一样一样的。你要给 GET 加上 request body,给 POST 带上 url 参数,技术上是完全行的通的。

“标准答案” 里的那些区别是怎么回事?

在我大万维网世界中,TCP 就像汽车,我们用 TCP 来运输数据,它很可靠,从来不会发生丢件少件的现象。但是如果路上跑的全是看起来一模一样的汽车,那这个世界看起来是一团混乱,送急件的汽车可能被前面满载货物的汽车拦堵在路上,整个交通系统一定会瘫痪。为了避免这种情况发生,交通规则 HTTP 诞生了。HTTP 给汽车运输设定了好几个服务类别,有 GET, POST, PUT, DELETE 等等,HTTP 规定,当执行 GET 请求的时候,要给汽车贴上 GET 的标签(设置 method 为 GET),而且要求把传送的数据放在车顶上(url 中)以方便记录。如果是 POST 请求,就要在车上贴上 POST 的标签,并把货物放在车厢里。当然,你也可以在 GET 的时候往车厢内偷偷藏点货物,但是这是很不光彩;也可以在 POST 的时候在车顶上也放一些数据,让人觉得傻乎乎的。HTTP 只是个行为准则,而 TCP 才是 GET 和 POST 怎么实现的基本。

但是,我们只看到 HTTP 对 GET 和 POST 参数的传送渠道(url 还是 requrest body)提出了要求。“标准答案” 里关于参数大小的限制又是从哪来的呢?

在我大万维网世界中,还有另一个重要的角色:运输公司。不同的浏览器(发起 http 请求)和服务器(接受 http 请求)就是不同的运输公司。 虽然理论上,你可以在车顶上无限的堆货物(url 中无限加参数)。但是运输公司可不傻,装货和卸货也是有很大成本的,他们会限制单次运输量来控制风险,数据量太大对浏览器和服务器都是很大负担。业界不成文的规定是,(大多数)浏览器通常都会限制 url 长度在 2K 个字节,而(大多数)服务器最多处理 64K 大小的 url。超过的部分,恕不处理。如果你用 GET 服务,在 request body 偷偷藏了数据,不同服务器的处理方式也是不同的,有些服务器会帮你卸货,读出数据,有些服务器直接忽略,所以,虽然 GET 可以带 request body,也不能保证一定能被接收到哦。

好了,现在你知道,GET 和 POST 本质上就是 TCP 链接,并无差别。但是由于 HTTP 的规定和浏览器 / 服务器的限制,导致他们在应用过程中体现出一些不同。

你以为本文就这么结束了?

我们的大 BOSS 还等着出场呢。。。

这位 BOSS 有多神秘?当你试图在网上找 “GET 和 POST 的区别” 的时候,那些你会看到的搜索结果里,从没有提到他。他究竟是什么呢。。。

GET 和 POST 还有一个重大区别,简单的说:

GET产生一个TCP数据包;POST产生两个TCP数据包。
长的说:

对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

也就是说,GET只需要汽车跑一趟就把货送到了,而POST得跑两趟,第一趟,先去和服务器打个招呼“嗨,我等下要送一批货来,你们打开门迎接我”,然后再回头把货送过去。

因为POST需要两步,时间上消耗的要多一点,看起来GET比POST更有效。因此Yahoo团队有推荐用GET替换POST来优化网站性能。但这是一个坑!跳入需谨慎。为什么?

  1. GET与POST都有自己的语义,不能随便混用。

  2. 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。

  3. 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。

现在,当面试官再问你 “GET 与 POST 的区别” 的时候,你的内心是不是这样的?

php如何实现“多继承”

发表于 2019-12-04 分类于 PHP

Trait

自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait。

Trait 是为类似 PHP 的单继承语言而准备的一种代码复用机制。Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用 method。Trait 和 Class 组合的语义定义了一种减少复杂性的方式,避免传统多继承和 Mixin 类相关典型问题。

Trait 和 Class 相似,但仅仅旨在用细粒度和一致的方式来组合功能。 无法通过 trait 自身来实例化。它为传统继承增加了水平特性的组合;也就是说,应用的几个 Class 之间不需要继承。

Example #1 Trait 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
trait ezcReflectionReturnInfo {
function getReturnType() { /*1*/ }
function getReturnDescription() { /*2*/ }
}

class ezcReflectionMethod extends ReflectionMethod {
use ezcReflectionReturnInfo;
/* ... */
}

class ezcReflectionFunction extends ReflectionFunction {
use ezcReflectionReturnInfo;
/* ... */
}
?>

优先级

从基类继承的成员会被 trait 插入的成员所覆盖。优先顺序是来自当前类的成员覆盖了 trait 的方法,而 trait 则覆盖了被继承的方法。

Example #2 优先顺序示例

从基类继承的成员被插入的 SayWorld Trait 中的 MyHelloWorld 方法所覆盖。其行为 MyHelloWorld 类中定义的方法一致。优先顺序是当前类中的方法会覆盖 trait 方法,而 trait 方法又覆盖了基类中的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Base {
public function sayHello() {
echo 'Hello ';
}
}

trait SayWorld {
public function sayHello() {
parent::sayHello();
echo 'World!';
}
}

class MyHelloWorld extends Base {
use SayWorld;
}

$o = new MyHelloWorld();
$o->sayHello();
?>

以上例程会输出:

Hello World!

Example #3 另一个优先级顺序的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
trait HelloWorld {
public function sayHello() {
echo 'Hello World!';
}
}

class TheWorldIsNotEnough {
use HelloWorld;
public function sayHello() {
echo 'Hello Universe!';
}
}

$o = new TheWorldIsNotEnough();
$o->sayHello();
?>

以上例程会输出:

Hello Universe!

多个 trait

通过逗号分隔,在 use 声明列出多个 trait,可以都插入到一个类中。

Example #4 多个 trait 的用法

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
<?php
trait Hello {
public function sayHello() {
echo 'Hello ';
}
}

trait World {
public function sayWorld() {
echo 'World';
}
}

class MyHelloWorld {
use Hello, World;
public function sayExclamationMark() {
echo '!';
}
}

$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
$o->sayExclamationMark();
?>

以上例程会输出:

Hello World!

冲突的解决

如果两个 trait 都插入了一个同名的方法,如果没有明确解决冲突将会产生一个致命错误。

为了解决多个 trait 在同一个类中的命名冲突,需要使用 insteadof 操作符来明确指定使用冲突方法中的哪一个。

以上方式仅允许排除掉其它方法,as 操作符可以 为某个方法引入别名。 注意,as 操作符不会对方法进行重命名,也不会影响其方法。

Example #5 冲突的解决

在本例中 Talker 使用了 trait A 和 B。由于 A 和 B 有冲突的方法,其定义了使用 trait B 中的 smallTalk 以及 trait A 中的 bigTalk。

Aliased_Talker 使用了 as 操作符来定义了 talk 来作为 B 的 bigTalk 的别名。

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
trait A {
public function smallTalk() {
echo 'a';
}
public function bigTalk() {
echo 'A';
}
}

trait B {
public function smallTalk() {
echo 'b';
}
public function bigTalk() {
echo 'B';
}
}

class Talker {
use A, B {
B::smallTalk insteadof A;
A::bigTalk insteadof B;
}
}

class Aliased_Talker {
use A, B {
B::smallTalk insteadof A;
A::bigTalk insteadof B;
B::bigTalk as talk;
}
}
?>
1
2
3
Note:

在 PHP 7.0 之前,在类里定义和 trait 同名的属性,哪怕是完全兼容的也会抛出 E_STRICT(完全兼容的意思:具有相同的访问可见性、初始默认值)。

修改方法的访问控制

使用 as 语法还可以用来调整方法的访问控制。

Example #6 修改方法的访问控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
trait HelloWorld {
public function sayHello() {
echo 'Hello World!';
}
}

// 修改 sayHello 的访问控制
class MyClass1 {
use HelloWorld { sayHello as protected; }
}

// 给方法一个改变了访问控制的别名
// 原版 sayHello 的访问控制则没有发生变化
class MyClass2 {
use HelloWorld { sayHello as private myPrivateHello; }
}
?>

从 trait 来组成 trait

正如 class 能够使用 trait 一样,其它 trait 也能够使用 trait。在 trait 定义时通过使用一个或多个 trait,能够组合其它 trait 中的部分或全部成员。

Example #7 从 trait 来组成 trait

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
<?php
trait Hello {
public function sayHello() {
echo 'Hello ';
}
}

trait World {
public function sayWorld() {
echo 'World!';
}
}

trait HelloWorld {
use Hello, World;
}

class MyHelloWorld {
use HelloWorld;
}

$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
?>

以上例程会输出:

Hello World!

Trait 的抽象成员

为了对使用的类施加强制要求,trait 支持抽象方法的使用。

Example #8 表示通过抽象方法来进行强制要求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
trait Hello {
public function sayHelloWorld() {
echo 'Hello'.$this->getWorld();
}
abstract public function getWorld();
}

class MyHelloWorld {
private $world;
use Hello;
public function getWorld() {
return $this->world;
}
public function setWorld($val) {
$this->world = $val;
}
}
?>

Trait 的静态成员

Traits 可以被静态成员静态方法定义。

Example #9 静态变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
trait Counter {
public function inc() {
static $c = 0;
$c = $c + 1;
echo "$c\n";
}
}

class C1 {
use Counter;
}

class C2 {
use Counter;
}

$o = new C1(); $o->inc(); // echo 1
$p = new C2(); $p->inc(); // echo 1
?>

Example #10 静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
trait StaticExample {
public static function doSomething() {
return 'Doing something';
}
}

class Example {
use StaticExample;
}

Example::doSomething();
?>

属性

Trait 同样可以定义属性。

Example #11 定义属性

1
2
3
4
5
6
7
8
9
10
11
12
<?php
trait PropertiesTrait {
public $x = 1;
}

class PropertiesExample {
use PropertiesTrait;
}

$example = new PropertiesExample;
$example->x;
?>

Trait 定义了一个属性后,类就不能定义同样名称的属性,否则会产生 fatal error。 有种情况例外:属性是兼容的(同样的访问可见度、初始默认值)。 在 PHP 7.0 之前,属性是兼容的,则会有 E_STRICT 的提醒。

Example #12 解决冲突

1
2
3
4
5
6
7
8
9
10
11
12
<?php
trait PropertiesTrait {
public $same = true;
public $different = false;
}

class PropertiesExample {
use PropertiesTrait;
public $same = true; // PHP 7.0.0 后没问题,之前版本是 E_STRICT 提醒
public $different = true; // 致命错误
}
?>

mysql中的用户变量

发表于 2019-11-11 分类于 数据库

前言

LeetCode上有一道SQL练习题感觉很有必要备注一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
【中等】180.编写一个 SQL 查询,查找所有至少连续出现三次的数字。

+----+-----+
| Id | Num |
+----+-----+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 2 |
| 5 | 1 |
| 6 | 2 |
| 7 | 2 |
+----+-----+
例如,给定上面的 Logs 表, 1 是唯一连续出现至少三次的数字。

+-----------------+
| ConsecutiveNums |
+-----------------+
| 1 |
+-----------------+

题解:

  • ID是连续的:
1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT *
FROM
Logs l1,
Logs l2,
Logs l3
WHERE
l1.Id = l2.Id - 1
AND l2.Id = l3.Id - 1
AND l1.Num = l2.Num
AND l2.Num = l3.Num
/*
利用了ID连续,前后ID相差1的特性来确定,缺点也很明显, 数据必须保证前后ID是连续的且不断裂的
*/
  • ID连续性不确定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SELECT DISTINCT
Num AS ConsecutiveNums
FROM
(
SELECT
Num,
CASE
WHEN @prev = Num THEN
@count := @count + 1
WHEN (@prev := Num) IS NOT NULL THEN
@count := 1
END AS CNT
FROM
LOGS,
(
SELECT
@prev := NULL ,@count := NULL
) AS t
) AS temp
WHERE
temp.CNT >= 3
/*
利用用户变量实现对连续出现的值进行计数,与自关联或自连接相比,这种方法的效率更高,不受Logs表中的Id是否连续的限制,而且可以任意设定某个值连续出现的次数。
*/

MySQL的变量分类

MySQL变量一共分为两大类:用户自定义变量和系统变量。如下:

  • 用户自定义变量
    • 局部变量
    • 会话变量
  • 系统变量
    • 会话变量
    • 全局变量

局部变量

局部变量一般用于SQL的语句块中,比如存储过程中的begin和end语句块。其作用域仅限于该语句块内。生命周期也仅限于该存储过程的调用期间。

1
2
3
4
5
6
7
8
9
10
11
drop procedure if exists _procedure_test;
create procedure _procedure_test
(
in a int,
in b int
)
begin
declare c int default 0;
set c = a + b;
select c as c;
end;

上述存储过程中定义的变量c就是局部变量。

会话变量

会话变量即为服务器为每个客户端连接维护的变量。在客户端连接时,使用相应全局变量的当前值对客户端的回话变量进行初始化。设置会话变量不需要特殊权限,但客户端只能更改自己的会话变量。其作用域与生命周期均限于当前客户端连接。

会话变量的赋值:

1
2
3
mysql> set session var_name = value;
mysql> set @@session.var_name = value;
mysql> set var_name = value;

会话变量的查询:

1
2
3
mysql> select @@var_name;
mysql> select @@session.var_name;
mysql> show session variables like "%var%";

全局变量

全局变量影响服务器整体操作。当服务器启动时,它将所有全局变量初始化为默认值。这些默认值可以在选项文件中或在命令行中指定的选项进行更改。要想更改全局变量,必须具有SUPER权限。全局变量作用于server的整个生命周期,但是不能跨重启。即重启后所有设置的全局变量均失效。要想让全局变量重启后继续生效,需要更改相应的配置文件。

全局变量的设置:

1
2
mysql> set global var_name = value; //注意:此处的global不能省略。根据手册,set命令设置变量时若不指定GLOBAL、SESSION或者LOCAL,默认使用SESSION
mysql> set @@global.var_name = value; //同上

全局变量的查询:

1
2
mysql> select @@global.var_name;
mysql> show global variables like "%var%";

自定义变量

你可以利用SQL语句将值存储在用户自定义变量中,然后再利用另一条SQL语句来查询用户自定义变量。这样以来,可以再不同的SQL间传递值。

用户自定义变量的声明方法形如:@var_name,其中变量名称由字母、数字、“.”、“_”和“$”组成。当然,在以字符串或者标识符引用时也可以包含其他字符(例如:@’my-var’,@”my-var”,或者@my-var)。

用户自定义变量是会话级别的变量。其变量的作用域仅限于声明其的客户端链接。当这个客户端断开时,其所有的会话变量将会被释放。

用户自定义变量是不区分大小写的。

使用SET语句来声明用户自定义变量:

1
mysql> SET @var_name = expr[, @var_name = expr] ...

在使用SET设置变量时,可以使用 = 或者 := 操作符进行赋值。

当然,除了SET语句还有其他赋值的方式。比如下面这个例子,但是赋值操作符只能使用 := , 因为 = 操作符将会被认为是比较操作符。

1
2
3
4
5
6
7
mysql> SET @t1 = 1, @t2 = 2, @t3 := 4;
mysql> SELECT @t1, @t2, @t3, @t4 := @t1 + @t2 + @t3;
+------+------+------+--------------------+
| @t1 | @t2 | @t3 | @t4 := @t1+@t2+@t3 |
+------+------+------+--------------------+
| 1 | 2 | 4 | 7 |
+------+------+------+--------------------+

用户变量的类型仅限于:整形、浮点型、二进制与非二进制串和NULL。在赋值浮点数时,系统不会保留精度。其他类型的值将会被转成相应的上述类型。比如:一个包含时间或者空间数据类型(temporal or spatial data type)的值将会转换成一个二进制串。

如果用户自定义变量的值以结果集形式返回,系统会将其转换成字符串形式。

如果查询一个没有初始化的变量,将会以字符串类型返回NULL。

不要在同一个非SET语句中同时赋值并使用同一个用户自定义变量

用户自定义变量可以用于很多上下文中。但是目前并不包括那些显式使用常量的表达式中,比如SELECT中的LIMIT子句,或者LOAD DATA中的IGNORE N LINES的字句中。

通常来说,除了在SET语句中,不要再同一个SQL语句中同时赋值并使用同一个用户自定义变量。举个变量自增的例子,下面的是没问题的:

1
mysql> SET @a = @a + 1;

对于其他语句,比如SELECT,也许会得到期望的效果,但这真心不靠谱。比如下面的语句,也许你自然地会认为MySQL会先执行@a的值,然后再进行赋值操作:

1
mysql> SELECT @a, @a:=@a+1, ...;

然而,用户自定义变量表达式的计算顺序还没有定义呢。

除此之外,还有另一个问题。变量的默认返回类型由语句开始时的类型决定的,正如下面的例子:

1
2
mysql> SET @a='test';
mysql> SELECT @a,(@a:=20) FROM tbl_name;

上述的SELECT语句中,MySQL会报告给客户端第一列的字段类型为字符串,同时将所有对@a变量的使用均转换为字符串处理,尽管在SELECT语句中将@a变量设置为数字类型。在SELECT语句执行后,@a变量才会在下一个语句中识别为数字类型。

为了避免上述问题的发生,要么不在同一个语句中同时赋值并使用变量,要么在使用之前,将变量设置为0,0.0,或者’’,以确定它的数据类型。

变量的值是在SQL发送到客户端后才计算的

在SELECT语句中,在每一个select表达式被发送给客户端后,才会进行计算。这就意味着,在形如HAVING,GROUP BY和ORDER BY只句中有使用在当前select表达式定义的变量的情况下,该语句将不会得到如期的效果。

1
mysql> SELECT (@aa:=id) AS a, (@aa+3) AS b FROM tbl_name HAVING b=5;

上述在HAVING只句中使用了在当前的select列表中定义的别名b,其使用了变量@aa。这条语句并不会得到如期的效果:@aa变量为上一次SQL语句执行的结果集中的ID值,并非当前的。

例 : 在MySQL中实现Rank高级排名函数

除了上面题目中的示例,这里给出另外一个常用的示例:

MySQL中没有Rank排名函数,当我们需要查询排名时,我们可以利用自定义变量来达到Rank函数一样的高级排名效果。

首先我们先创建一个我们需要进行高级排名查询的players表,

pid name age
1 Samual 25
2 Vino 20
3 John 20
4 Andy 22
5 Brian 21
6 Dew 24
7 Kris 25
8 William 26
9 George 23
10 Peter 19
11 Tom 20
12 Andre 20

实现Rank普通排名函数

在这里,我们希望获得一个排名字段的列,以及age的升序排列。所以我们的查询语句将是:

1
2
3
SELECT pid, name, age, @curRank := @curRank + 1 AS rank
FROM players p, (SELECT @curRank := 0) q
ORDER BY age
PID NAME AGE RANK
10 Peter 19 1
12 Andre 20 2
2 Vino 20 3
3 John 20 4
11 Tom 20 5
5 Brian 21 6
4 Andy 22 7
9 George 23 8
6 Dew 24 9
7 Kris 25 10
1 Samual 25 11
8 William 26 12

要在mysql中声明一个变量,你必须在变量名之前使用@符号。FROM子句中的(@curRank := 0)部分允许我们进行变量初始化,而不需要单独的SET命令。当然,也可以使用SET,但它会处理两个查询:

1
2
3
4
SET @curRank := 0;
SELECT pid, name, age, @curRank := @curRank + 1 AS rank
FROM players
ORDER BY age

实现Rank查询以降序排列

首要按age的降序排列,其次按name进行排列,只需修改查询语句加上ORDER BY和 DESC以及列名即可。

1
2
3
SELECT pid, name, age, @curRank := @curRank + 1 AS rank
FROM players p, (SELECT @curRank := 0) q
ORDER BY age DESC, name
PID NAME AGE RANK
8 William 26 1
7 Kris 25 2
1 Samual 25 3
6 Dew 24 4
9 George 23 5
4 Andy 22 6
5 Brian 21 7
12 Andre 20 8
3 John 20 9
11 Tom 20 10
2 Vino 20 11
10 Peter 19 12

实现Rank普通并列排名函数

现在,如果我们希望为并列数据的行赋予相同的排名,则意味着那些在排名比较列中具有相同值的行应在MySQL中计算排名时保持相同的排名(例如在我们的例子中的age)。为此,我们使用了一个额外的变量。

1
2
3
4
5
6
7
SELECT pid, name, age,
CASE
WHEN @prevRank = age THEN @curRank
WHEN @prevRank := age THEN @curRank := @curRank + 1
END AS rank
FROM players p, (SELECT @curRank :=0, @prevRank := NULL) r
ORDER BY age
PID NAME AGE RANK
10 Peter 19 1
12 Andre 20 2
2 Vino 20 2
3 John 20 2
11 Tom 20 2
5 Brian 21 3
4 Andy 22 4
9 George 23 5
6 Dew 24 6
7 Kris 25 7
1 Samual 25 7
8 William 26 8

如上所示,具有相同数据和排行的两行或多行,它们都会获得相同的排名。玩家Andre, Vino, John 和Tom都有相同的age,所以他们排名并列第二。下一个最高age的玩家(Brian)排名第3。这个查询相当于MSSQL和ORACLE 中的DENSE_RANK()函数。

实现Rank高级并列排名函数

当使用RANK()函数时,如果两个或以上的行排名并列,则相同的行都会有相同的排名,但是实际排名中存在有关系的差距。

1
2
3
4
5
6
7
SELECT pid, name, age, rank FROM
(SELECT pid, name, age,
@curRank := IF(@prevRank = age, @curRank, @incRank) AS rank,
@incRank := @incRank + 1,
@prevRank := age
FROM players p, (SELECT @curRank :=0, @prevRank := NULL, @incRank := 1) r
ORDER BY age ) s

这是一个查询中的子查询。我们使用三个变量(@incRank,@prevRank,@curRank)来计算关系的情况下,在查询结果中我们已经补全了因为并列而导致的排名空位。我们已经封闭子查询到查询。这个查询相当于MSSQL和ORACLE中的RANK()函数。

PID NAME AGE RANK
10 Peter 19 1
12 Andre 20 2
2 Vino 20 2
3 John 20 2
11 Tom 20 2
5 Brian 21 6
4 Andy 22 7
9 George 23 8
6 Dew 24 9
7 Kris 25 10
1 Samual 25 10
8 William 26 12

在这里我们可以看到,Andre,Vino,John和Tom都有相同的age,所以他们排名并列第二。下一个最高年龄的球员(Brian)排名第6,而不是第3,因为有4个人并列排名在第2。

好的,通过本文加深对sql的运用,也希望你能从中获取收获。

参考链接:

深入MySQL用户自定义变量 胡小旭
在MySQL中实现Rank高级排名函数 风澈vio

redis持久化

发表于 2019-11-06 分类于 数据库

前言

Redis是一种高级key-value数据库。它跟memcached类似,不过数据可以持久化,而且支持的数据类型很丰富。有字符串,链表,集 合和有序集合。支持在服务器端计算集合的并,交和补集(difference)等,还支持多种排序功能。所以Redis也可以被看成是一个数据结构服务 器。
Redis的所有数据都是保存在内存中,然后不定期的通过异步方式保存到磁盘上(这称为“半持久化模式”);也可以把每一次数据变化都写入到一个append only file(aof)里面(这称为“全持久化模式”)。

由于Redis的数据都存放在内存中,如果没有配置持久化,redis重启后数据就全丢失了,于是需要开启redis的持久化功能,将数据保存到磁 盘上,当redis重启后,可以从磁盘中恢复数据。redis提供两种方式进行持久化,一种是RDB持久化(原理是将Reids在内存中的数据库记录定时 dump到磁盘上的RDB持久化),另外一种是AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)。那么这两种持久化方式有什么区别呢,改如何选择呢?网上看了大多数都是介绍这两 种方式怎么配置,怎么使用,就是没有介绍二者的区别,在什么应用场景下使用。

二者的区别

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

RDB持久化

AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

AOF持久化

二者优缺点

RDB

  • 优势

1). 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数 据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。

2). 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。

3). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。

4). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。

  • 劣势

1). 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。

2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

AOF

  • 优势

1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其 效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变 化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。

2). 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操 作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据 一致性的问题。

3). 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创 建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。

4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。

  • 劣势

1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。

二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。

常用配置

  • RDB持久化配置

Redis会将数据集的快照dump到dump.rdb文件中。此外,我们也可以通过配置文件来修改Redis服务器dump快照的频率,在打开6379.conf文件之后,我们搜索save,可以看到下面的配置信息:

1
2
3
4
5
save 900 1             # 在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。

save 300 10 # 在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。

save 60 10000 # 在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。
  • AOF持久化配置

在Redis的配置文件中存在三种同步方式,它们分别是:

1
2
3
4
5
appendfsync always      # 每次有数据修改发生时都会写入AOF文件。

appendfsync everysec # 每秒钟同步一次,该策略为AOF的缺省策略。

appendfsync no # 从不同步。高效但是数据不会被持久化。
1…345…16
Mr.Gou

Mr.Gou

155 日志
11 分类
38 标签
RSS
GitHub E-Mail Weibo
Links
  • 阮一峰的网络日志
  • 离别歌 - 代码审计漏洞挖掘pythonc++
  • 一只猿 - 前端攻城尸
  • 雨了个雨's blog
  • nMask's Blog - 风陵渡口
  • 区块链技术导航
© 2023 蜀ICP备2022014529号