豪翔天下

Change My World by Program

0%

CSRF(跨站请求伪造)

通过手段欺骗用户访问第三方页面,通过用户在第三方页面执行常规的操作来进行欺骗,这些操作包含了用户已登陆网站的一些请求,当用户点击该请求时,请求会发往原站,同时由于用户已经登录,所以原站是能验证用户的,通常会判断为合法请求。防御:

1.表单增加hash值,就像laravel和django自带的表单csrf验证一样,而这个hash值和用户是对应起来的,第三方是无法同时拿到这两个东西的

2.验证码

最近团队由于项目需求的增多,需要单独设计一个权限系统出来,参考了网上很多的权限/访问控制系统以及一些设计原则比如RBAC(Role-Based Access Control),这里,记录一下自己对权限系统的一种构想。需要注意的是,RBAC中也有分组的概念,但是它的分组仅仅是为了将相同权限的人集中在一起。我的构想虽然没有分组的概念,但是也符合RBAC原则的,只是因为我做的更彻底,不仅将角色与权限直接关联,更是将用户与权限直接关联,产生了一定的冗余,更适用于人数少但是权限多的情况。

实体表

应用表

字段名 字段注释
id app_id

角色表

字段名 字段注释
id role_id
app_id 应用ID
name 名称(英文)
beta 备注

权限表

字段名 字段注释
id privilege_id
app_id 应用ID,不提供表示通用的权限
name 名称(英文),例如projects表示项目相关的权限,这里如果想要有更复杂的层级关系,可以用点号进行分割,但不建议增大权限的层次
beta 备注

操作表

记录一些通用的操作,例如增删该查等。当然,这里的操作表可以省略,而直接将操作以父子关系放在权限表中。

字段名 字段注释
id operator_id
app_id 不提供表示通用的操作
name 名称(英文),例如: add,delete,edit,query等,或者页面上index,create,admin等
beta 备注

关联表

用户与权限关联表:此表与角色权限对应表之间是有一定冗余的,但是考虑到个性化需求以及第三方APP的需要,所以在可以忍受的范围内

字段名 字段注释
id
user_id 用户ID
role_id 角色ID: 为0表示该权限是独立分配的,独立分配的优先
privilege_id 权限ID
operator_id 操作ID
disabled boolean

角色与权限对应表

可以将用户与权限的关联表和角色与权限的关联表合并,使用target来标识

字段名 字段注释
id
role_id 角色ID
privilege_id 权限ID
operator_id 操作ID

用户与角色对应表

字段名 字段注释
id
role_id 角色ID
user_id 用户ID

常用操作

添加权限: 直接添加,并设置好其
给单独用户添加权限:保存在角色与权限表中,这样用户的实际权限就应该是用户绑定角色拥有的权限与用户单独分配的权限之和。
给单独用户删除权限:如果是单独添加并且绑定角色所没有的,直接删除关联关系;如果是绑定角色所关联的权限,那么将关联表中的权限标识为禁用;如果是单独添加并且绑定角色也有的,同样标识为禁用。所以最终,用户的实际权限是用户绑定角色拥有的权限与用户单独分配的权限之和减去禁用的权限,当然由于有冗余,直接在关联表中读取没禁用的权限即可。

删除用户角色: 直接根据role_id进行disabled,添加了就不直接删除
删除用户权限:直接根据role_id进行disabled,也不直接删除
其他平台登录:只需要在登录的时候获取该用户在该平台的所有的额权限即可,连分组都不需要,因为权限都是在权限系统中进行统一登记注册的,所以其他平台不用单独存储权限列表,而只需要获取获取当前用户的权限,放到缓存里面即可,例如redis,可以直接存储为一个列表user:privileges []

gulp

前端的自动化构建工具,能够自动化处理一些常见的任务:

  • 搭建web服务器
  • 修改文件自动刷新浏览器
  • 预处理Sass、Less
  • 优化资源:CSS、Javascript、图片等

安装方式

全局安装:npm install gulp -g
当前项目安装: npm install --save-dev gulp
安装后,会在当前目录生成一个node_modules目录,然后执行npm init初始化当前项目,根据提示输入一些项目的基本信息,然后会生成一个package.json文件

目录结构

app/        # 开发目录,存放源文件
    css
    fonts
    images
    js
dist/        # 存放生产环境下的内容
gulpfile.js
node_modules/
package.json

gulpfile.js文件

var gulp = require('gulp'),    # 去node_modules下导入相应的包
    livereload = require('gulp-livereload'),
    del = require('del'),
    notify = require('gulp-notify'),
    browserify = require('gulp-browserify'),
    jshint = require('gulp-jshint');

gulp.task('scripts', function(){
//这里可以不用把所有的.js合并成一个,而是可以按需合并,比如每个文件都需要用到的就合并成一个,其他单独的则单独合并,不许return,写两个gulp.src即可,比如gulp.src('app/js/base.js').pipe......和gulp.src('app/js/new.js').pipe...
    .pipe(browserify({
        insertGlobals: true,
        debug: !gulp.env.production
    }))
    .pipe(concat('base.js'))        // 这是把上面所有的js文件合并为一个文件
    .pipe(gulp.dest(DEST+'/js'))
    .pipe(rename({suffix: '.min'}))
    .pipe(uglify())
    .pipe(gulp.dest(DEST+'/js'))
    .pipe(notify({
        message: 'Scripts task complete'
    }));
    
gulp.task('clean', function(cb){    # 定义任务
    del(['dist', cb]);
});

gulp.task('default', ['clean'], function(){
    gulp.start('scripts');
});

gulp.task('watch', function(){
    gulp.watch('app/js/index.js', ['scripts']);    # 见识文件,当监控的文件变化时执行相应的任务
    livereload.listen();
    gulp.watch(['dist/**']).on('change', livereload.changed);
});

在命令行执行gulp task_name就可以运行该任务了

其他插件

  • gulp-jshint: js代码校验
  • gulp-concat: js代码合并
  • gulp-notify: 更改提醒
  • gulp-uglify: js代码压缩
  • gulp-livereload: 自动刷新页面
  • del: 清除文件

这本书在最近一年里面特别火,以前好多从来不看书的朋友都在朋友圈里发这本书的照片,仿佛大家一下子爱上了文字。我当然不会因为人家看我就看的,只是每次一看到这本书的名字,就不由得想起日本的情感电视剧《深夜食堂》,不知道为什么,就感觉他们可能会有好多相似的地方,应该会和《深夜食堂》一样,在很平淡的叙述中体会其中的韵味,引人入胜。于是最后,我还是看了。它和《深夜食堂》有相似的,也有不同的。《深夜食堂》是把精彩的故事写平淡,而《解忧杂货店》则是把本应该跌宕起伏的故事写得平淡,然后一步一步将错综复杂的故事串起来,功力同样深厚,毕竟是东野圭吾的作品,不得不叹服。

浪矢杂货店,一个连接过去与未来的杂货店,连接了一个老头与三个坏孩子一起帮助别人排忧解难的故事。通过几个矛盾人物与他们之间的书信交流来推动故事发展,更精妙的是几个矛盾人物之间又刚好有一丝丝的联系,联系虽然不深,但是却足以让人看到其中环环相扣的逻辑,被作者的思维所震撼。

我一直都像很多国人一样,哪怕自己过得再堕落,哪怕自己不相信大道理也要自以为是地告诫别人,偶尔,也会很认真地回应别人的玩笑,因为“我们也要尽可能帮助他人,你没有无视他开玩笑写的问题,而是认真回答,所以才回一直牢记在心里”。我希望自己能力所能及地帮助他人,特别是刚入编程领域的小学弟们,我很希望他们来问我问题,哪怕最简单的,哪怕最幼稚的,我也要认认真真地回答。遗憾的是,我并不是什么大神,所以找我咨询的学弟微乎其微呀。

人生,重要的是选择。作为一个挣扎于选择困难症不能自拔的天秤座的我来说,其实,很多时候在犹豫不决的时候,决定其实早就在自己心中,只是,要么因为各种客观原因,要么就是自己主观上的回避。最常见的莫过于购物的时候,说简单点就是没钱,所以才会犹豫不决,这个倒很好解决,多挣钱就行。又比如人生中的抉择,月兔想参加奥运会,但是怕选不上所以找了一个照顾男友的理由;克朗想成为歌手,然后又以照顾父亲为理由放弃追逐梦想。追逐梦想,能够轻松实现自己想要的,谁又不想呢。只是现实很多时候过于残酷,我们不知道继续坚持会不会得到自己期待的结果,只是从现在的情形看来,结果肯定不会太好,这个时候如果来了一个很合理的,连自己都能说服的理由,又何尝不想放弃呢,放弃多好,努力不一定成功,但不努力,一定很轻松。似乎,只要不是自己的原因,这样的理由就非常容易被接受,也不至于被身边的人嘲笑。但是,终究不过是自欺欺人。明明知道自己想要的,但还是选择逃避。不要轻易被自己的努力感动,真的,你做的努力相比于真正成功了的人,是微乎其微的。

常用快捷键

Mac自带截图功能

  • Command + Shift + 4 普通截图
  • Command + Shift + 4,然后按空格,对指定窗口截图
  • Command + Shift + 3 全屏截图

finder显示隐藏文件

Command + Shift + .

Mac瘦身

  • 最好的工具mac-cleanup-sh

  • ~/Library/Application Support/Code/User/workspaceStorage: VS Code的工作区文件夹唉,但是所有的扩展都会重建这个文件夹,把年代久远的删除了

Shell配置使用

Mac使用Iterm2的Profile功能实现类似ssh标签/xshell登录的功能

Preferences中不仅可以设置默认Profile的窗口样式等,还是通过新建不同的Profile来实现自动登录。例如:

这样如果想要进入某个服务器,只需要在iterm2中点击顶部菜单Profiles->aliyun即可直接进入服务器。对于复杂的输入密码的场景,可以参考Linux 手册expect进行配置

mac shell使用rz、sz直接上传或者下载服务器文件

  • 需要注意的是在使用except登录服务器的情况下,使用lrzsz不会起作用

首先使用brew install lrzsz安装命令行工具

然后保存iterm2-send-zmodem.sh 和iterm2-recv-zmodem.sh两个脚本/usr/local/bin目录下

打开iterm2Perferences->Profiles->Advanced->Triggers->Edit,添加如下trigger

1
2
\*\*B0100			Run Silent Coprocess	/usr/local/bin/iterm2-send-zmodem.sh
\*\*B00000000000000 Run Silent Coprocess /usr/local/bin/iterm2-recv-zmodem.sh

Homebrew配置使用

  • brew如果加cask参数表示下载的是.dmg/.pkg文件,不需要install等操作
1
export ALL_PROXY=socks5://127.0.0.1:1080	# homebrew走ss代理

查看每个CPU的负载/GPU负载

活动监视器->窗口->CPU使用率/CPU历史记录/GPU历史记录

修改终端欢迎字符

1
vim /private/etc/motd	# 直接输入即可

系统管理命令

1
2
3
4
5
6
7
8
# lsof
lsof # 查看所有进程打开的文件
lsof -i # 查看有网络连接的应用程序列表
sudo lsof -i -P | grep LISTEN | grep :8080 # 查看指定端口的占用情况

dscacheutil -q group # 查看所有用户和组

sudo shutdown -h 2306132200 # 定时关机,后面是年月日时分秒

更换文件图标

http://www.cnblogs.com/wormday/archive/2011/05/06/2038703.html

与Android联动

  • brew cask install android-file-transfer可以管理小米手机上的文件

自制iPhone铃声

iTunes 簡單自製 iPhone 鈴聲不求人

Mackup配置备份

1
2
3
4
5
6
7
8
9
10
brew install mackup

vim ~/.mackup.cfg进行配置
[storage]
engine = file_system # 表示用文件系统进行存储
path = /Users/haofly/OneDrive # 指定路径

mackup backup # 备份命令
mackup restore # 数据恢复
mackup uninstall# 将配置文件拷回原来的系统目录

ios safari移动端真机调试

https://channaly.medium.com/how-debug-cordova-based-application-with-chrome-dev-tool-43e095a735b4

  • 可用于调试Cordova/Inoic/Phonegap等hybrid项目
  1. ios移动端配置: Settings -> Safari -> Advanced , 打开JavaScriptWeb Inspector
  2. mac上safari浏览器配置: Preferences -> Advanced -> Show Develop menu in menu bar
  3. 当连接上设备后就可以点击Safari->Develop->iPhone选择你的设备即可

android通过蓝牙向Mac/macbook传输文件失败

  • 需要在电脑左上角apple logo -> System Preferences -> Sharing -> Bluetooth Sharing打开并设置读写权限

iPad或者iPhone投屏到Mac

  1. iPad数据线连接Mac
  2. Mac上打开QuickTime Player,然后选择File -> New Movie Recording,在中间录像按钮边上点开的下拉菜单选择你的设备

安装python2或者python3

不要期待brew,最好直接从官网下载对应版本的dmg文件安装,都可以直接安装的,如果要使用多个不同的版本,也可以使用pyenv:

1
2
3
4
5
brew install pyenv
brew install pyenv-virtualenv
pyenv install --list
pyenv install 3.9.10 # 安装指定版本
pyenv virtualenv 3.9.10 venv # 创建venv环境

Mac OS降级

  • 降级最简单的是直接从timemachine恢复,但是大多数情况下我们并没有timemachine,那么就需要U盘了

  • 首先需要从苹果官方下载老版本的系统

  • 然后执行以下命令制作系统盘,执行完成后会提示Done.,如果多次尝试仍然报错,那么可能需要换个更大的U盘,或者换一个U盘。(我最开始用的16G的USB2.0,后来换成32G的USB3.0就可以了)

    1
    2
    # 其中Catalina.app是下载的系统的名称,MyVolume是U盘名称
    sudo /Applications/Install\ macOS\ Catalina.app/Contents/Resources/createinstallmedia --volume /Volumes/MyVolume
  • 开机时按住Option键,然后选择U盘进入,然后用磁盘工具格式化磁盘,重装系统即可

TroubleShooting

  • 磁盘空间爆了,重启后spotlight一直显示正在索引: 原因可能是误删了索引的文件(索引文件确实有哦几个G),修复需要执行以下几个命令:

    1
    2
    3
    sudo mdutil -i off /
    sudo mdutil -E /
    sudo mdutil -i on /
  • Library not loaded: /usr/local/opt/readline/lib/libreadline.6.2.dylib Referenced from: /usr/local/bin/gawk Reason: image not found: 执行下面这个命令更新所有brew安装的包可以修复

    1
    brew upgrade
  • 明明安装了xcode命令行工具却还是提示找不到,可以用这个命令重装一下:

    1
    2
    3
    xcode-select --print-path	# 一般会打印/Library/Developer/CommandLineTools
    sudo rm -r -f /Library/Developer/CommandLineTools
    xcode-select --install # 重新安装
  • Macos使用ssh登陆linux服务器无法显示中文,需要设置终端的字符集:

    1
    2
    3
    # vim ~/.zshrc,在底部输入如下内容,然后保存重启终端
    export LC_ALL=en_US.UTF-8
    export LANG=en_US.UTF-8
  • autoreconf: command not found: brew install autoconf && brew install automake

  • **xcrun: error: invalid active developer path, missing xcrun **: 重装xcode工具: xcode-select --install

  • macOS Big Sur无法使用VPN: 系统限制没办法,得修改VPN服务器的配置,参考苹果的官方说明

  • telnet: command not found: brew install telnet

  • Safari不能审查元素,没有审查元素按钮:得手动打开开发者菜单: Preferences -> Advanced -> Show Develop menu in menu bar新版本是Show features for web developer

  • MacOs安装指定的java版本: https://www.azul.com/downloads/?package=jdk

  • zsh: bad CPU type in executable: 执行softwareupdate --install-rosetta

即使我写了这样的文章,我依然坚信设计模式只是某些垃圾语言用来规避其语言本身弊端的规范而已,不然,就只能写出糟糕的代码了。

创建型

Factory Method(工厂方法模式/工厂模式)

工厂模式是一种类,提供了创建对象的某些方法,可以直接使用工厂类创建对象,而不是直接使用new。

优点:如果要改变所创建对象的类型,只需要修改该工厂即可。比如有个类需要读取用户数据来创建,原本是读取的数据库,现在要从文本读取,就得把那个类及其所有依赖都更改一遍。

php示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
interface IPost{
function getName();
}

class Post implements IPost{
public function __construct($id){}

public static function Load($id){
return new Post($id);
}

public static function Create(){
return new Post(null);
}

public function getName(){
return "haofly";
}
}

$post = Post::Load(1);
echo $post->getName();
?>

AbstractFactory(抽象工厂模式)

抽象工厂模式提供了一种方式,可以将一组具有同一主题的单独的工厂封装起来。客户端程序不需要知道(或关心)它从这些内部的工厂方法中获得对象的具体类型,因为客户端程序仅使用这些对象的通用接口。抽象工厂模式将一组对象的实现细节与他们的一般使用分离开来。

php示例,代码来自维基百科

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?php
abstract class AbstractFactory {
abstract public function CreateButton();
abstract public function CreateBorder();
}

class MacFactory extends AbstractFactory{
public function CreateButton()
{

return new MacButton();
}
public function CreateBorder()
{
return new MacBorder();
}
}
class WinFactory extends AbstractFactory{
public function CreateButton()
{
return new WinButton();
}
public function CreateBorder()
{
return new WinBorder();
}
}
class Button{}
class Border{}

class MacButton extends Button{
function __construct()
{
echo 'MacButton is created' . "\n";
}
}
class MacBorder extends Border{
function __construct()
{
echo 'MacBorder is created' . "\n";
}
}


class WinButton extends Button{
function __construct()
{
echo 'WinButton is created' . "\n";
}
}
class WinBorder extends Border{
function __construct()
{
echo 'WinBorder is created' . "\n";
}
}
?>
# 客户端使用
<?
$type = 'Mac'; //value by user.
if(!in_array($type, array('Win','Mac')))
die('Type Error');
$factoryClass = $type.'Factory';
$factory=new $factoryClass;
$factory->CreateButton();
$factory->CreateBorder();
?>

Prototype(原型模式)

通过复制一个已经存在的实例来返回新的实例,而不是新建实例。

优点: 多用于创建复杂的或者耗时的实例,这种情况,复制一个已经存在的实例使程序运行更高效,活着创建值相等,只是命名不一样的同类数据。

Singleton(单例模式)

php示例,代码来自IBM

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
require_once("DB.php");

class DatabaseConnectiono{
private $_handle = null;

private function __construct(){
$dsn = 'mysql://root:password@localhost/photos';
$this->_handle = & DB::Connect($dsn, array());
}

public static function get(){
static $db = null;
if($db == null)
$db = new DatabaseConnectioni();
return $db;
}

public function handle(){
return $this->_handle;
}
}

print("Handle=".DatabaseConnection::get()->handle()."\n");
print("Handle=".DatabaseConnection::get()->handle()."\n");

结构型

Adapter Class/Object(适配器/转换器)

介绍: 把一个类的接口变换成客户端所期待的另一种接口,Adapter模式使原本因接口不匹配或不兼容而无法在一起工作的两个类能够在一起工作。外部请求方式一样,内部实现方式不一样。

应用场景:

  1. 想使用一个已经存在的类,但是它的接口并不完全符合需求
  2. 适用于第三方库的API会发生改变而选择不直接把第三方API给用户使用的情况下,在前面封装一层。

php实例,代码来自真实的归宿

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?php  
/**
* 目标角色
*/
interface Target {
/**
* 源类的方法:这个方法将来有可能继续改进
*/
public function hello();

/**
* 目标点
*/
public function world();
}

/**
* 源角色:修改后的方法
*/
class Adapter {
/**
* 源类含有的方法
*/
public function world() {
echo ' world <br />';
}

/**
* 将hello方法改为了greet方法
*/
public function greet() {
echo ' Greet ';
}
}

/**
* 类适配器角色
*/
class Adapter implements Target {
private $_adaptee;

public function __construct(Adaptee $adaptee) {
$this->_adaptee = $adaptee;
}

/**
* 源类中没有hello方法,在此补充
*/
public function hello() {
$this->_adaptee->greet();
}
}
/**
* 客户端程序
*/
class Client {
public static function main() {
$adaptee = new Adaptee();
$adapter = new Adapter($adaptee);
$adapter->hello(); // 无论内部怎么变,外部都用hello来引用
$adapter->world();
}
}

Client::main();
?>

Bridge(桥接模式)

把事务对象和其具体行为、具体特征分离开来,使它们可以各自独立的变化。

python实例,代码来自维基百科

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
"""圆形、三角形归于抽象的形状,而画圆、画三角形归于抽象的画图"""

# Implementor
class DrawingAPI:
def drawCircle(x, y, radius):
pass

# ConcreteImplementor 1/2
class DrawingAPI1(DrawingAPI):
def drawCircle(self, x, y, radius):
print "API1.circle at %f:%f radius %f" % (x, y, radius)

# ConcreteImplementor 2/2
class DrawingAPI2(DrawingAPI):
def drawCircle(self, x, y, radius):
print "API2.circle at %f:%f radius %f" % (x, y, radius)

# Abstraction
class Shape:
# low-level
def draw(self):
pass

# high-level
def resizeByPercentage(self, pct):
pass

# Refined Abstraction
class CircleShape(Shape):
def __init__(self, x, y, radius, drawingAPI):
self.__x = x
self.__y = y
self.__radius = radius
self.__drawingAPI = drawingAPI

# low-level i.e. Implementation specific
def draw(self):
self.__drawingAPI.drawCircle(self.__x, self.__y, self.__radius)

# high-level i.e. Abstraction specific
def resizeByPercentage(self, pct):
self.__radius *= pct

def main():
shapes = [
CircleShape(1, 2, 3, DrawingAPI1()),
CircleShape(5, 7, 11, DrawingAPI2())
]

for shape in shapes:
shape.resizeByPercentage(2.5)
shape.draw()

if __name__ == "__main__":
main()

Composite(组合模式)

Decorator(装饰模式)

Facade(外观)

Flywight(享元)

Proxy(代理)

行为型

Interpreter(解释器)

Template Method(模板方法)

Observer(观察者模式)

优点: 被观察对象不用具体了解观察者具体实现,而是由观察者去实现。被观察者忽略了依赖它的对象,它要关注在事件发生时触发该事件并发送消息给观察者即可。这个和依赖注入有点相近的地方。

php示例,代码来自IBM

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
<?php
/**
* 测试代码创建 UserList,并将 UserListLogger 观察者添加到其中。然后添加一个消费者,并将这一更改通知 UserListLogger
*/

# 定义要通过怎样的方法才能成为观察者
interface IObserver{
function onChanged($sender, $args);
}
# 定义可以被观察的对象
interface IObservable{
function addObserver($observer);
}
# 实现IOBbservable,以便将本身注册为可观察
class UserList implements IObservable{
private $_observers = array();

public function addCustomer($name){
foreach($this->_observers as $obs){
$obs->onChanged($this, $name); # 通知观察者
}
}

public function addObserver($observer){
$this->_observers[] = $observer;
}
}

class UserListLogger implements IOBserver{
public function onChanged($sender, $args){
echo("'$args' added to user list\n");
}
}

$ul = new UserList();
$ul->addObserver(new UserListLogger());
$ul->addCustomer("Jack");

Command(命令)

Chain of Respopnsibility(责任链)

包含了一些命令对象和一系列的处理对象,每一个处理对象决定它能处理哪些命令对象,它也知道如何将它不能处理的命令对象传递给该链中的下一个处理对象。

php示例,代码来自维基百科

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?php
abstract class Logger {
const ERR = 3;
const NOTICE = 5;
const DEBUG = 7;

protected $mask;
protected $next; // The next element in the chain of responsibility

public function setNext(Logger $l) {
$this->next = $l;
return $this;
}

abstract public function message($msg, $priority);
}

class DebugLogger extends Logger {
public function __construct($mask) {
$this->mask = $mask;
}

public function message($msg, $priority) {
if ($priority <= $this->mask) {
echo "Writing to debug output: {$msg}\n";
}

if (false == is_null($this->next)) {
$this->next->message($msg, $priority);
}
}
}

class EmailLogger extends Logger {
public function __construct($mask) {
$this->mask = $mask;
}

public function message($msg, $priority) {
if ($priority <= $this->mask) {
echo "Sending via email: {$msg}\n";
}

if (false == is_null($this->next)) {
$this->next->message($msg, $priority);
}
}
}

class StderrLogger extends Logger {
public function __construct($mask) {
$this->mask = $mask;
}

public function message($msg, $priority) {
if ($priority <= $this->mask) {
echo "Writing to stderr: {$msg}\n";
}

if (false == is_null($this->next)) {
$this->next->message($msg, $priority);
}
}
}

class ChainOfResponsibilityExample {
public function __construct() {
// build the chain of responsibility
$l = new DebugLogger(Logger::DEBUG);
$e = new EmailLogger(Logger::NOTICE);
$s = new StderrLogger(Logger::ERR);

$e->setNext($s);
$l->setNext($e);

$l->message("Entering function y.", Logger::DEBUG); // handled by DebugLogger
$l->message("Step1 completed.", Logger::NOTICE); // handled by DebugLogger and EmailLogger
$l->message("An error has occurred.", Logger::ERR); // handled by all three Loggers
}
}

new ChainOfResponsibilityExample();
?>

Iterator(迭代器)

Mediator(中介者)

Memento(备忘录)

State(状态)

Strategy(策略模式)

指对象有某种行为,但是在不同的场景中,该行为有不同的实现算法。

抽象工厂与策略模式不同的地方在于,工厂是创建型模式,关注的是对象的创建,而策略是行为型模式,关注的是对行为的封装,对于工厂来说无论返回的对象内部是怎样的,只要是我想要的对象就行,而对于策略模式来说返回的对象即使不同也会有相同的方法/行为。

php示例,代码来自维基百科

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php
class StrategyExample {
public function __construct() {
$context = new Context(new ConcreteStrategyA());
$context->execute();

$context = new Context(new ConcreteStrategyB());
$context->execute();

$context = new Context(new ConcreteStrategyC());
$context->execute();
}
}

interface IStrategy {
public function execute();
}

class ConcreteStrategyA implements IStrategy {
public function execute() {
echo "Called ConcreteStrategyA execute method\n";
}
}

class ConcreteStrategyB implements IStrategy {
public function execute() {
echo "Called ConcreteStrategyB execute method\n";
}
}

class ConcreteStrategyC implements IStrategy {
public function execute() {
echo "Called ConcreteStrategyC execute method\n";
}
}

class Context {
var $strategy;

public function __construct(IStrategy $strategy) {
$this->strategy = $strategy;
}

public function execute() {
$this->strategy->execute();
}
}

new StrategyExample;
?>

Visitor(放问者)

TroubleShooting

  • 工厂模式和策略模式的区别
    如大多数网上的解释一样,工厂模式更注重对象的创建,策略模式更注重行为的不同。根据我的理解工厂模式更适用于创建不同的对象,这些对象拥有不同的方法。而策略模式则更多是针对有相同方法的对象。例如网上的文章经常举的例子一样,数据库的操作,其实每个数据库的操作都不一样,所以这里更适用于工厂模式。但是如果有后端一样的场景,那么策略模式就更方便了。例如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # 工厂模式
    DBFactory::create(Redis::class)->record()->add();

    # 策略模式(不同的公有云相同的create方法)
    abstract class CloudContext
    {
    protected $cloud;
    public function __construct(Cloud $cloud) {
    $this->cloud = $cloud;
    }
    }
    class HostContext extends CLoudContext
    {
    public function create(
    $this->cloud->create();
    );
    }
    $host new HostContext(CloudFactory::create(AliyunCloud::class));
    $host->create();

安装hexo

1
2
3
curl --silent --location https://rpm.nodesource.com/setup_6.x | bash -
yum install nodejs -y
npm install -g hexo-cli
添加RSS

通过npm install -g hexo-generator-feed安装
然后修改hexo/_config.yml添加如下代码:

1
2
plugins:
- hexo-generator-feed
添加sitemap

通过npm install -g hexo-generator-sitemap安装
然后修改hexo/_config.yml添加如下代码:

1
2
plugins:
- hexo-generator-sitemap

全局设置

文章设置

文章的抬头可以指定如下信息:

1
2
3
4
title: "文章标题"
date: 2016-08-07 8:52:39 # 文章发布时间
updated: 2016-09-02 15:50:00 # 文章修改时间
categories: tools # 文章分类

TroubleShooting

  • nginx 403:可能是因为没有在public生成index.html文件导致,检查是否执行过npm install安装完所有的模块,以及themes目录下是否存在该主题的文件

  • no content页面只有框架没有内容: 可能是历史的css文件遗留所致,此时可以清空public目录下的文件再重新生成

  • unknown block tag: load,出现类似的错误,极大可能是文章中出现了特定的没有转义的标签,例如这里就是很有可能就存在一个类似这种标签,这种标签只能使用代码块(三个单引号)而不能使用两个单引号来引用:

    1
    {% load	# 这样子开头
  • Error: Cannot find module ‘babel-runtime/regenerator’: 原因是没有安装这个包,可以执行npm install babel-runtime --save

每次看到selenium都觉得很牛,但是苦于文档(包括英文)太少,我到今天才真正完整地安装使用了一把。我不喜欢来一个项目就在自己电脑上搭一个运行环境,而是喜欢在docker或者虚拟机里进行操作,问题是docker或者虚拟机里并没有任何的可视化的浏览器,而Selenium又依赖于这些浏览器驱动,我是最讨厌安装驱动的,因为驱动这个东西电脑不同差距特别大,总是会出现各种问题。而在服务器上如何安装selenium或者splinter,这个过程在网上基本是找不到的,所以这里记录下自己的安装方法。

注:这里之所以要使用splinter,而不只使用selenium是因为splinterselenium之上又封装了一层,使得接口更为简单。另外,即使是用的splinter,也可以通过browser.driver直接获取到selenium的驱动的。

Linux install Splinter(Selenium)

首先,需要安装必要的python包pip3 install splinter selenium xvfbwrapper需要注意的是,splinter只有在使用浏览器的时候才需要安装selenium,如果仅仅是在flask或者django中进行测试是不需要的。

安装chromedriver

ChromeDriver首页-WebDriver for Chrome,下载对应操作系统的最新的chromedriver

1
2
3
4
5
wget http://chromedriver.storage.googleapis.com/2.23/chromedriver_linux64.zip
unzip chromedriver_linux64.zip
mv chromedriver /usr/bin # 添加到PATH即可

chromedriver # 运行命令进行测试,没抛错则表示正确了
Docker版本Selenium

https://github.com/SeleniumHQ/docker-selenium

handless browser

https://zhuanlan.zhihu.com/p/26810049?utm_medium=social&utm_source=qq

Linux Server(Raspberry Pi)安装浏览器

上面的方式是直接打开浏览器的方式,但是在Server上面没有界面,也就没有浏览器,这种情况就得安装单独的真对server的浏览器了。最先我想使用ChromeDriver,但是无论怎么折腾也安装不上,于是就用了Firefox,发现一篇很好的教程。它这个版本被称作Selenium headless firefox。安装步骤如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 添加repository,并安装firefox
sudo add-apt-repository ppa:mozillateam/firefox-stable
sudo apt-get update
sudo apt-get install firefox

# 安装Xvfb: 是用来虚拟X服务程序,实现X11显示的协议
sudo apt-get install xvfb
sudo Xvfb :10 -ac # 10表示编号

# 设置环境变量
export DISPLAY=:10

# 就可以使用了
firefox

开始Splinter(Selenium)

无桌面环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from splinter import Browser
from xvfbwrapper import Xvfb
from selenium.webdriver.chrome.options import Options

# 由于是在server上运行chrome,所以必须用一些模拟器
vdisplay = Xvfb()
vdisplay.start()

# 这些设置都是必要的
chrome_options = Options()
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-setuid-sandbox")
chrome_options.add_argument("--ignore-certificate-errors") // 忽略网页的证书错误

# 这里才是正式的使用了
browser = Browser('chrome', options=chrome_options, executable_path='/root/bin/chromedriver')
browser.visit('https://haofly.net')
print(browser.title)

browser.quit()
vdisplay.stop()

桌面环境

如果直接在本地有桌面环境的情况下进行测试那么,直接这样子:

1
2
3
4
5
6
7
8
from splinter import Browser
from selenium.webdriver.chrome.options import Options

chrome_options = Options()
browser = Browser('chrome', executable_path='/Users/haofly/share/chromedriver', user_agent='User-Agent设置', options=chrome_options)
browser.driver.set_window_size(1500, 900) # 设置浏览器的size
browser.visit('https://wiki.haofly.net')
print(browser.title)

Options选项

通过chrome_options.add_argument('')可以设置非常多的浏览器的参数

1
2
3
disable-infobars	// 禁用网页上部的提示栏,比如2.28的webdriver开始会提示你Chrome正受到自动测试软件的控制,这个特性应该是chrome为了安全给加的

option.add_argument('--user-data-dir=/path') # 指定个人资料路径,指向自己的浏览器,这样,chromedriver的配置就能和自己的一样了

获取所有网络请求

很多时候访问一个页面,在该页面可能会同时访问其他的资源,例如js,css,甚至其他一些关键信息。这时候就要求我们能够获取中间的所有的请求,但是selenium是不带这个功能的,只能使用一些代理,例如:browsermob-proxy。其不需要安装,只需要release下载bin包,然后pip install browsermob-proxy,然后在使用的时候指定路径即可。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from browsermobproxy import Server
server = Server("~/browsermob-proxy-2.1.4/bin/browsermob-proxy")
server.start()
proxy = server.create_proxy()

chrome_options = Options()
chrome_options.add_argument('--proxy-server={host}:{port}'.format(host='localhost', port=proxy.port))
browser = Browser('chrome', executable_path='~/share/chromedriver2.28', options=chrome_options)
browser.driver.set_window_size(1500, 900) # 设置浏览器的size
browser.driver.set_page_load_timeout(1) # 设置页面加载超时时间,防止visit一直卡住

proxy.new_har()
browser.visit('https://haofly.net')
print(proxy.har) # 以json的形式打印出中间所有的网络请求

浏览器操作

1
2
3
4
5
6
7
8
9
10
browser.windows	# 所有打开了的窗口
browser.windows[0] # 第一个窗口
browser.windows.current # 当前窗口
browser.windows.current = browser.windows[2] # 切换窗口
browser.window[0].close() # 关闭窗口
browser.window[0].close_others() # 关闭其他窗口
browser.window[0].is_current # 序号为零的窗口是否是当前的窗口
browser.window[0].next # 下一个窗口
browser.window[0].prev # 上一个窗口
browser.quit() # 退出浏览器

页面操作

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
browser.visit(url)	# 访问URL
browser.reload() # 重新加载当前页
browser.back() # 回退
browser.forward() # 向前

browser.find_by_tag('h1').mouse_over() # 鼠标移动到某个元素上
browser.find_by_tag('h1').mouse_out() # 鼠标移开
browser.find_by_tag('h1').click() # 鼠标点击事件
browser.find_ty_tag('h1').double_click()# 鼠标双击事件
browser.find_by_tag('h1').right_click() # 鼠标右键点击

draggable = browser.find_by_tag('h1') # 鼠标拖曳事件
target = browser.find_by_css('.container')
draggable.drag_and_drop(target)

# 点击链接
browser.click_link_by_href('http://www.the_site.com/my_link')
browser.click_link_by_partial_href('my_link')
browser.click_link_by_text("submit")
browser.click_link_by_partial_text('part of link text')
browser.click_link_by_id('link_id')

# 点击按钮
browser.find_by_name('send').first.click()
browser.find_link_by_text('my link').first.click()

# 表单填写
browser.fill('query', 'my name') # 直接通过name属性的元素输入
browser.find_by_tag('input').first.fill('my name')
browser.attach_file('file', '/path/to/file/somefile.jpg')
browser.choose('some-radio', 'radio-value')
browser.check('check-name') # checkbox
browser.choose('name', 'value') # radio
browser.uncheck('some-check')
browser.select('uf', 'rj')

# 元素基本操作方法
ele.click() # 点击元素
ele.fill('value') # 输入字符
ele.has_class('name') # 是否包含某个类
ele.mouse_out()
ele.mouse_over()
ele.screenshot()
ele.select('option', slowly=False) # 下拉选择option
ele.text # 元素内部的文字内容
ele.type('value', slowly=False)
ele.uncheck()
ele.value # 跟.text差不多
ele.clear() # 清空元素内容

# selenium发送按键
browser.driver.find_element_by_id('title').send_keys(Keys.BACKSPACE)

数据获取

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
browser.title	# 获取网页title
browser.html # 获取网页内容
browser.url # 获取网页url
browser.find_by_css('h1').first.value # 获取元素值

browser.is_text_present('', wait_time=None) # html里面是否存在某个字符串

# 查找css元素
browser.find_by_css('h1')
browser.find_by_xpath('//h1')
browser.find_by_tag('h1')
browser.find_by_name('name')
browser.find_by_text('Hello World!')[1]
browser.find_by_id('firstheader').last
browser.find_by_value('query').first
browser.driver.find_element_by_name('A').find_element_by_xpath('..') # 利用selenium原生来实现寻找parent父级元素

# 寻找网页链接
browser.find_link_by_text()
browser.find_link_by_partial_text()
browser.find_link_by_href()
browser.find_link_by_partial_href()

# 可以连续用的
browser.find_by_tag('div').first.find_by_name('name')

# 获取Alert/Prompt
alert = browser.get_alert(wait_time=None) # 获取alert,官方文档说获取不到返回为None,但是实际会报超时错误

TroubleShooting

  • Chrome driver crashes when opens a new tab

    原因可能是服务器内存太低了,需要加大虚拟内存

  • selenium.common.exceptions.WebDriverException: Message: session not created exception,将webdriver更新到最新版基本上能解决问题

  • session not created: This version of ChromeDriver only supports Chrome version 77: 这是因为下载的chromedriver版本和你当前系统已经安装的chrome版本不一致造成的,需要对其中某一个进行升级或降级

  • Unable to connect host: 如果浏览器能正常访问,那么可能是chromedriver打开网页有问题,导致splinter未去访问目标网页,并不是网络连接问题

  • browsermob-proxy 地址已在使用中: 可以指定代理服务器的端口:

    1
    2
    3
    4
    5
    from browsermobproxy import Server
    dict={'port':8090}
    server = Server(path="./browsermob-proxy",options=dict)
    server.start()
    proxy = server.create_proxy()
相关文章

Getting Started with Headless Chrome

Setting up a Digital Ocean server for Selenium, Chrome, and Python

PHP的单元测试工具比Python好选择,基本上就是PHPUnit了。

断言

1
$this->setExpectedException(Exception class);	// 断言将会出现某个exception	

Mockery mock对象

Mockery默认包含在PHPUnit中,根据我的经验,学会了mock才算真正学会了单元测试,无论是Python还是PHP都是如此。使用它也解决了我之前以为依赖注入对测试来说极大地加大了复杂度的错误思想。使用方法如下:

1
2
3
4
5
6
7
8
# 首先在一个测试类中
$this->object = Mockery:mock('class');

# 模拟方法的返回值
$this->object->shouldReceive('getValue')->andReturn('abc');
$this->object->getValue() # 这样就会返回abc
$this->object->shouldReceive('getAttribute')->with('value')->andReturn('def');
$this->value # 这样会返回属性的值

Xdebug的使用

既然用上了单元测试,那为何不继续深入使用,添加代码覆盖率的测试,以前我认为覆盖率只是用来给我们玩玩儿的,当我真正开始写了后才知道,真的太有用了。首先,他让我了解到一个真相,那就是我自己写的代码,很多地方,我自己都不清楚,有些异常我根本不知道在什么情况下会抛,有好多重复的地方代码根本不应该存在,有好多逻辑没考虑完整。要查看代码覆盖率还需要安装Xdebug。

安装方式:

1
2
3
4
sudo apt-get install php-pear php5-dev libcurl3-openssl-dev -y
pecl install xdebug
# 然后分别在/etc/php5/cli/php.ini和/etc/php5/apache2/php.ini 里面分别添加
zend_extension="/usr/local/php/modules/xdebug.so"

使用:

phpunit testfile.php --coverage-html foldername即可生成html文件

TroubleShooting

  • Call to a member funciton make() on null错误

    只需要添加父类构造方法即可:

    1
    2
    3
    function __construct(){
    parent::setUp();
    }

前几天,一位朋友突然问我道,为什么我的博客最近更新的都是有关读后感的,而少了以前那么多的技术干货。很欣慰,居然有朋友一直在看我的站点,虽然并没有看到它想看的,但是他所要的技术干货我现在其实也有,只是并没有放在主站,而是放在豪翔天下的wiki这个二级域名下。至于为什么,我想是因为厌倦了很多博客的内容样式,厌倦了很多教程的复制粘贴,才有了把它们一分为二的想法。

看了一下百度统计和谷歌Analytics,博客从13年至今,统计在内的PV总共有20000+,单日最高PV居然有1300+,但是平日里也就10左右。虽然我有用统计工具,但是几乎不会去关注这些统计结果,一直以来我写博客都是为了自己在写,即使是技术干货,也是为了方便自己以后查阅。但是最近又有了一些新的方法,我认为,我现在可以宣传自己了。以前的我很自卑,觉得自己写的东西,那肯定都是别人写过的,而且自己写的东西更是没有多少技术深度的东西,但是事实好像并不是这样。我最骄傲的地方就是与身边的不一样,我每天都在浏览着很多科技博客、技术新闻,以为自己一直是站在技术前沿的人,也意识到自己学的东西范围非常广,自己的深度也不够,眼前有很多路,却不知道该往哪里走。这就像一个瓶子,明明上面就是瓶颈了,但就是怎么做也钻不出去,甚至在往下掉。
到新公司也快半年了,新同事们很好,也很牛逼,感觉大家简直就像游侠一样,不想去北上广争夺,只想待在重庆这个小江湖,说实话,他们的技能水平,在北上广绝对也能称得上高手。这半年里,因为有人在旁边指导,在代码质量方面成长得挺快,至少现在写代码的时候能明显感觉到自己以前写的代码是有多么的bad smell。在给别人开发的时候也感觉自己的代码量真的太少了。以前都是自己开发自己的东西,觉得有时候有点错误没什么,但是给别人开发的过程中,一点儿不满意人家就会指出来,而且让人伤心的是,这里面起码有百分之五十确实是自己代码质量的问题。路漫漫其修远兮,编程之路,我还有很长的路要走。

前几天翻了以前的博客,我去,居然没有今年的年初计划,不禁思考,我这半年是怎么浑浑噩噩过来的,接下来的半年又该怎样浑浑噩噩地过呢。浑浑噩噩这个词我不喜欢,你说我懒我会接受,我懒得开心,但是说我浑浑噩噩,说我不思进取,那肯定是不行的。在这里,补一下下半年的目标:

  1. 熟练日语五十音(说了很多次要学日语,结果到现在连五十音都还不认识,要想装逼,至少要会说吧)
  2. 结交三个以上IT界朋友(年龄相仿+忘年)
  3. 博客日访问量10+(我承认,现在的博客日访问量还有零的)
  4. 年底公司评级达到P4及以上(这就要求在公司好好工作啦)
  5. 给父母做全身检查(哪怕再缺钱也得做)
  6. SegmentFault声望达到1000以上