豪翔天下

Change My World by Program

0%

想在家里做NAS、DNS等私有云服务,但是无奈家里淘汰下来的电脑已无力承担如此重任。没办法了,就只能试试树莓派。不试不知道,一试吓一跳,完全就是一手掌大小的电脑,听说desktop版本还能使用word等软件,虽然只有1GB内存,但是200多块(淘宝店)就能买到这个东西,那是非常值了。当然,作为一个技术爱好者,别人是完全无法体会这种快乐的。要是其功耗再低点或者能采用其它的供电方式(比如无线供电、电池供电),感觉完全能颠覆智能市场。

系统安装

制作启动镜像

镜像下载:https://www.raspberrypi.org/downloads/,我下载的是RASPBIAN分支,因为其是官方提供且基于Debian,和Ubuntu操作一样.官方镜像raspbian的各个版本地址以及release_notes参见: downloads,更新系统以及重装系统前,一定要先看看release_notes,比如从2016年11月那一版本开始,默认的ssh就没有开启了。。。对于没有显示器的我来说,只能重装。
Mac环境

1
2
3
4
5
6
df  # 查看当前已经挂载的卷,一般sd卡在最后,Filesystem是/dev/disk2s1,Mounted on /Volumes/No Name,可以在Finder里面将sd卡的名字改为Pi(我那个默认是No Name)
diskutil unmount /dev/disk2s1 #将sd卡卸载
>> Volume Pi on disk2s1 unmounted
diskutil list # 查看是否有sd卡设备
dd bs=4m if=pi.img of=/dev/rdisk2 #将镜像文件pi.img写入sd卡,需要注意这条命令使用的是rdisk2,这是原始字符设备
diskutil unmountDisk /dev/disk2 # 再卸载sd卡,此时可以拔出来插入树莓派的sd卡槽了

启动操作系统

收到货的那天,发现其有一个DC接口,还以为是通过DC接口供电,出门走了一圈都没发现有卖这货的,于是回家,自习已看,发现可以用Android的电源为期供电的,那接口名字忘了。和网上建议的一样,我采用的是5V 2A的供电设备(其实是直接插到小米插线板上的)

然后,我又发现,我家里没多的网线,那怎么办,我装的不是desktop版本,没有网线就不能SSH进去。不过还好,它支持HDMI,于是我把它功过HDMI连接上了家里40英寸的电视,(HDMI高清显示,真他妈爽)就像这样,还通过USB插了外置键盘。

默认是通电自动启动的,所以插上电就会进入系统了,默认用户名pi,默认密码是raspberry。

配置

初始化配置

通过sudo raspi-config来运行设置工具:

  • 第一项将sd卡的剩余空间全部用来使用

  • 然后修改Internationalisaton Options里面的时区及默认字符编码zh_CN GB2312/zh_CN.UTF-8 UTF-8

  • 接着修改源,这个国度没办法的事

    1
    2
    3
    4
    5
    6
    # sudo nano /etc/apt/sources.list.d/raspi.list,修改如下
    deb http://mirrors.ustc.edu.cn/archive.raspberrypi.org/debian/ jessie main

    # sudo nano /etc/apt/sources.list,修改为如下:
    deb http://mirrors.ustc.edu.cn/raspbian/raspbian/ jessie main non-free contrib
    deb-src http://mirrors.ustc.edu.cn/raspbian/raspbian/ jessie main non-free contrib
  • 最后,安装必要的软件
    1
    2
    3
    sudo apt-get update && sudo apt-get upgrade -y
    sudo apt-get install vim tree ttf-wqy-microhei git
    sudo rpi-update # 如果想要升级固件,可以这样升级,如果提示命令找不到可以先install rpi-update

WIFI设置

1
2
3
4
5
6
7
8
9
10
11
# 当然,我不可能一直用电视作显示器吧,这时候我买的无线设备就有用场了,直接通过USB插到树莓派上,然后设置wifi  
$ ifconfig # 可以看到wlan0,表示已经识别无线网卡
$ sudo vim /etc/network/interfaces添加或修改关于wlan0的配置
auto wlan0
allow-hotplug wlan0
iface wlan0 inet dhcp
wpa-ssid WIFI名称
wpa-psk WIFI密码

# 然后通过如下命令重启网卡
sudo ifdown wlan0 && sudo ifup wlan0

ownCloud私有云搭建(现在不大推荐,内存占用太高)

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
作为私有云方案,我选择的ownCloud,而不是Samba,因为Samba功能仅仅算是ftp的共享,而不是一个私有云方案,当然ownCloud也有为人诟病的地方,比如内存占用高(树莓派2上占用100多MB),另一个是因为它本身是基于Apache的,树莓派内存总共就1G,我可不想既有Apache又有Nginx,所以直接用的是Nginx+php5-fpm的方案,不过这样子,配置过程就有点麻烦了。  
# 首先,安装基本服务
sudo apt-get install php5-common php5-cli php5-fpm
sudo apt-get install nginx
sudo apt-get install mysql-server mysql-client

# 配置MySQL,ownCloud需要提前创建用户、数据库和分配权限
> create database 库名 character set utf8 collate utf8_general_ci;
> grant ALL on 库名.* 用户名@localhost identified by "密码" # 注意,ownCloud是不允许root用户的,因为权限太多

# 配置文件权限
chmod 775 -R owncloud/ # 不要分配777,分配了也不能用
chown -R www-data:www-data owncloud/

# 配置php5-fpm
$ printenv PATH 获取系统环境变量
vim /etc/php5/fpm/pool.d/www.conf,将下面几行前面的注释去掉
;env[HOSTNAME] = $HOSTNAME
;env[PATH] = /usr/local/bin:/usr/bin:/bin # 这里还要修改为刚才获取到的环境变量
;env[TMP] = /tmp
;env[TMPDIR] = /tmp
;env[TEMP] = /tmp

# 配置nginx,按照官网的教程配置Nginx conf:https://doc.owncloud.org/server/7.0/admin_manual/installation/nginx_configuration.html

对于官网的配置,我做了如下几项修改:
location ~ .php(?:$|/)$这里面修改为:
location ~ ^(.+?.php)(/.*)?$ \{
  fastcgi_split_path_info ^(.+.php)(/.+)$;
  include fastcgi_params;
  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  fastcgi_param PATH_INFO $fastcgi_path_info;
  fastcgi_pass unix:/var/run/php5-fpm.sock;
  fastcgi_index index.php;
  include fastcgi_params;
  fastcgi_param PHP_VALUE "post_max_size=10G \n upload_max_filesize=10G"; # 上传默认居然为513MB,这里可以修改大,不然在owncloud无法调整到更大
}
检查配置文件是否正确用# nginx -t nginx.conf

TeamViewer For Raspberry安装

deb包下载地址,然后执行如下命令进行安装和配置:

1
2
3
4
sudo dpkg -i ....deb	# 安装该deb包
sudo apt-get -f install # 安装依赖
sudo dpkg -i ....deb # 重新安装,如果出现错误,可以参见TroubleShooting
sudo setup # 进行配置,输入teamveiwer的用户名和密码即可在外部网络进行访问了,会自动出现在你的Teamviewr联系人中

TroubleShooting

  • apt出现错误xdg-desktop-menu: No writable system menu directory found.
    执行命令sudo mkdir /usr/share/desktop-directories/

  • nodejs各个版本当前的维护情况(10.x已经不再维护,12.x在2022年4月30日停止维护,14.x在2023年4月30日停止维护,16.x在2024年4月30日停止维护)。个人觉得当前应该使用的版本是MAINTENANCE LTS START的,ACTIVE LTS START应该没有MAINTENANCE LTS START的稳定,所以现在直到2022-10-18都应使用14.x

安装

需要注意的是,关于npm的所有命令,最好都不要用root用户执行,否则会出现各种不可预料甚至连官方文档都说不清的问题

稳定版:

1
2
3
4
5
6
7
8
9
10
11
12
13
# centos用下面命令安装指定版本nodejs
sudo curl --silent --location https://rpm.nodesource.com/setup_18.x | sudo bash -
sudo yum install -y nodejs

# ubuntu用下面命令安装指定版本nodejs
sudo curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
# docker里面没有sudo就直接
curl -sL https://deb.nodesource.com/setup_18.x | sudo bash -
apt-get install -y nodejs

# 添加淘宝镜像,既然用的阿里云,那淘宝的镜像也就不介意了
npm install -g nrm && nrm use cnpm # 这样可以防止npm和cnpm混用导致的各种not found的错误
npm install -g cnpm --registry=https://registry.npm.taobao.org

安装package.json 直接npm install后面不加package.json的名字

package.json文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"scripts": { // 指定了运行脚本命令的npm命令行缩写
"start": "node index.js",
"test": "",
"deploy": "next build & next export & copyfiles _redirects out/" // 可以使用copyfiles工具来复制文件,npm install copyfiles --save-dev
},
"bin": { // 用于指定各个内部命令对应的可执行文件的位置
"someTool": "./bin/someTool.js" // 当然这也可以直接在scripts里面写成./node_modules/bin/someTool.js
},
"engines": { // 指定了运行环境
"node": ">=0.10.3 <0.12",
"npm": "~1.0.20"
},
"dependencies": { // 指定项目运行所依赖的模块
"aaa": "~1.2.2", // 波浪号,这里表示>1.2.2(1.2.x)且<1.3.x
"bbb": "^1.2.2", // 插入号,这里表示>1.2.2(1.x.x)且<2.x.x
"ccc": "latest", // 安装最新版本
},
"devDependencies": { // 指定项目开发所依赖的模块

}
}

常用语法

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
// 声明一个类
class Rectangle {
height = 0; // 公有字段声明
static displayName = "Point"; // 静态变量

// 类的构造函数
constructor(height, width) {
this.height = height;
this.width = width;
}

// Getter方法
get area() {
return this.calcArea()
}

// Method方法
calcArea() {
return this.height * this.width;
}

// 静态方法
static distance(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.hypot(dx, dy);
}
}

nodejs原生http请求

  • 无需安装任何package
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const https = require('https')

const req = https.request('https://haofly.net', res => {
res.on('data', (chunk) => (body += chunk.toString()));
res.on('error', () => reject(res))
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode <= 299) {
resolve({statusCode: res.statusCode, headers: res.headers, body: body});
} else {
reject('Request failed. status: ' + res.statusCode + ', body: ' + body);
}
})
})
req.on('error', () => reject)
req.write(body, 'binary')
req.end()

命令行

1
process.argv	// 从命令行接收参数

ECSMAScript/es6概念

  • exportimportes6之后才支持的

  • es的重要版本

    1
    2
    3
    4
    5
    6
    7
    ES6 ES2015 # 2015年6月发布, 之后每年6月出一个新版本
    ES7 ES2016
    ES8 ES2017
    ES9 ES2018
    ES10 ES2019
    ES11 ES2020
    ES12 ES2021
  • 一些比较使用的新的语法

    1
    2
    3
    // 解构赋值
    const [a, b, c] = [value1, value2, value3]
    const {name, age} = obj

常用命令

  • 报名前面带”@”符号的,表示是属于某个组织,又组织上传到镜像源里面的

Nvm

  • 可以通过node -v > .nvmrc将当前node版本限制在文件中,之后在当前目录只需要执行nvm use即可自动选择对应的版本

可以通过nvm来同时使用多个node版本,mac上可以直接brew install nvm进行安装(其他平台直接官网安装方法),安装完成后根据提示添加shrc文件,常用命令如下:

1
2
3
4
5
nvm ls-remote	# 查看所有可用的node版本
nvm install xxx # 下载需要的版本
nvm use xxx # 使用指定的版本
nvm alias default xxx # 设置默认的node版本
nvm uninstall v7.10.1

npm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
npm init		# 将当前目录设置为一个npm库,自动生成package.json文件,如果没有package.json文件可以用这个方法生成,它也会自动把node_module下的已安装包加进来的
npm install 包名 --save # 安装包,并且更新到package.json中去
npm install 包名 --save-dev # 安装包,并且更新到package.json的开发依赖中区
npm install 包名@3.1.0 --save # 安装指定版本的包
npm install git+https://github.com/haoflynet/example.git # 从Github仓库安装模块
npm install git://github.com/haoflynet/example.git
npm list --depth=0 # 列出已安装模块
npm list -g --depth=0 # 列出全局安装的包
npm list --depth=0 2> /dev/null # 忽略标准错误输出(npm ERR!这种错误将被忽略
npm view 包名 versions # 列出指定包的所有版本
npm update # 升级当前目录下的所有模块
npm update 包名 # 更新指定包
npm install npm -g # 升级npm
npm install -g n && n stable # 升级node.js到最新稳定版
升级node.js
npm install --verbose # 显示debug日志

npm config delete name # 删除某个配置

# 代理设置
npm config set proxy=http://127.0.0.1:1080 && npm config set proxy=https://127.0.0.1:1080

Yarn

  • yarn1.10x开始会在yarn.lock中增加integrity字段,用于验证包的有效性
  • yarn真的笔npm快,而且每次lock文件的变动要少一些
1
2
3
4
yarn add 包名	# 安装包
yarn add -D 包名 # 安装dev依赖
npm install yarn@latest -g # 升级yarn
yarn dev -p 8000 # yarn能直接将参数传递给scripts,npm不行

TypeScript

  • 给js引入type,使开发更加严谨

  • 引入步骤:

    1. npm install --save-dev typescript @types/node
    2. 初始化./node_modules/.bin/tsc --init
    3. 最后使用tsc命令进行编译,将它放入package.jsonscripts里面即可
  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    "compilerOptions": {
    "target": "es5", // 生成的代码的语言版本
    "skipLibCheck": true, // 跳过类型声明文件的类型检查
    "allowSyntheticDefaultImports": true, // 运行import x from 'y'这种操作,即使模块没有显示指定default的到处
    "strict": true, // 开启严格模式
    },
    "include": ["src"], // 搜索ts文件的路径
    }
  • 函数定义

    1
    2
    // 可变参数
    function test(field1: string, ...fields: string)
  • 常用类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    interface MyType {
    [key: string]: string; // mapping类型
    }

    type MyType = {
    username: string;
    pass: string;
    }

    type Type2 = keyof MyType; // Type2会被解析为'username'|'pass'

    enum MyEnum { // 注意定义枚举的时候要把key和value都写出来,如果写成enum MyEnum {VALUE1, VALUE2}这样可能会导致后面无法拿来匹配值,无论字符串还是枚举都匹配不上
    VALUE1 = 'VALUE1', VALUE2 = 'VALUE2', VALUE3 = 'VALUE3'
    }
  • 自定义类型:

    1
    2
    3
    4
    5
    interface MyType {
    name: string;
    children: MyType2[]; // 定义数组
    [index: number]: { id: number; label: string; key: any };
    }
  • 常见错误

    • Object is of type ‘unknown’ typescript generics: 如果程序无法判断准确的类型,那么我们需要强制声明一下类型,例如(error as Error).message
    • Property ‘classList’ does not exist on type ‘Never’: 对于react的ref需要这样定义: const myEl = useRef<HTMLDivElement>(null);
    • window undefined: 尝试生命一个window对象
      1
      2
      3
      4
      export interface CustomWindow extends Window {
      customAttribute: any;
      }
      declare let window: CustomWindow;

使用Forever管理NodeJs应用(生产环境最好用pm2)

  • 直接使用sudo npm install forever -g进行安装

forever常用命令

1
2
3
4
5
6
7
8
9
10
11
12
forever list	# 查看当前所有管理的服务
forever stopall # 停止所有服务
forever stop 服务ID # 停止指定服务
forever restartall # 重启所有服务
forever logs -f 服务ID # 查看某个服务的日志

# 下面这些命令一般用于非config文件启动方式
forever server.js # 直接启动进程
forever start server.js # 以daemon方式启动进程
forever start -l /var/log/forever.log -a server.js # 指定日志文件
forever start -o /var/log/forever/out.log -e /var/log/forever/err.log -a server.js # 分别指定日志和错误日志文件,-a表示追加
forever start -w server.js # 监听文件夹下所有文件的改动并自动重启

常用包推荐

  • adm-zip: 制作zip包工具,很多lambda函数都需要将仓库打包成zip文件,这个库就很有用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const AdmZip = require('adm-zip');

    const zip = new AdmZip();

    zip.addLocalFolder('../repo', './', (path) => {
    if (path.includes('node_modules') || // 忽略特定的文件夹
    path.includes('build/') ||
    path.includes('dist/') ||
    path.includes('.zip') ||
    path.includes('logs/')
    ) {
    return false;
    }
    return true;
    });
    zip.writeZip('./repo.zip');
  • bcrypt: 非常推荐的安全的密码/密码hash库,不用自己维护盐值,它是把计算次数和盐值都放到hash值里面去了

  • dotenv: 支持.env文件读取环境变量

    1
    2
    3
    4
    5
    6
    // 默认读取项目根目录的.env文件,也可以自定义.env文件的路径
    import { config } from 'dotenv';

    config({
    path: '../.env',
    });
  • human-numbers:转换数字的大小K、M、B、T,不过它其实就一个方法,都可以不用它这个包

  • node-csv: 读写CSV文件的库,它由cdv-generate,csv-parse,csv-transform,csv-stringify几个部分组成,一个一次性安装,也可以单独安装

  • object-sizeof: 获取变量所占内存的大小,有时候非常需要这样的东西

  • randomstring: 生成随机字符串

  • uuid: uuid首选version 4,每秒生成10亿个,大约需要85年才会重复

  • yup: 非常简单且易于集成的认证库

TroubleShooting

  • Permission Denied问题,使用npm命令总是会出现这个问题,解决方法最简单的是把npm目录的拥有者修改为当前用户的名字 sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share}

  • 安装包时报错Unexpected end of JSON input while parsing near ‘ : ‘ 尝试先执行npm cache clean --force,然后再安装

  • gyp: No Xcode or CLT version detected!: 需要先安装xcode命令工具: xcode-select --install

  • npm install结果被系统killed掉了: 一般是内存不足,可以使用增加swap的方法,参考Linux 手册

  • ReferenceError: describe is not defined NodeJs: 应该是mocha这个测试库报的错,安装它即可: npm install mocha -g

  • wasm code commit Allocation failed - process out of memory: 在Apple m1(apple silicon)上npm编译失败,可以尝试将node升级到v15.3.0及以上

  • a promise was created in a handler but was not returned from it: 通常是bluebird报错,函数没有正确地返回,遇到这个情况一个是验证回掉函数then是否有正确的返回,如果没有,那么可以添加一个return null语句,需要注意的是,如果then回掉里面只有一个语句,例如.then(res => res + 'abc'),这样不用单独写return,但如果里面的语句不只一句就得加了

  • Node Sass does not yet support your current environment: Windows 64-bit with Unsupported runtime (88): npm rebuild node-sass

  • Error: spawn ../node_modules/optipng-bin/vendor/optipng ENOENT: 尝试执行npm rebuild

  • this._settlePromiseFromHandler is not a function: 尝试删除node_module目录并重新安装

  • gulp: command not found: npm install gulp-cli -g

  • SyntaxError: Unexpected token export: 尝试使用module.exports = XXX来到处模块或方法

  • Unsupported platform for fsevents@1.4.9: wanted {“os”:”darwin”,”arch”:”any”} (current: {“os”:”win32”,”arch”:”x64”}: 原因是在m1的mac上面安装了该包并上传了自己的package-lock.json,得清理一下缓存才行了:

    1
    2
    3
    4
    rm -rf node_modules package-lock.json
    npm cache clean --force
    npm cache verify
    npm install --verbose
  • Uncaught Error: ENOENT: no such file or directory, uv_cwd: 检查一下当前目录是否还存在文件,node_modules这些目录是否还在

  • error TS2694: Namespace ‘NodeJS’ has no exported member ‘TypedArray’.: 尝试yarn add --dev @types/node

  • Cannot invoke an object which is possibly ‘undefined’ 通常是在调用一个可能为undefined的对象的方法的时候出现,需要对方法也是用问号表达式: props.obj?.click?.()

  • npm ERR! integrity checksum failed when using sha1: wanted … but got …: 尝试执行

    1
    2
    npm cache clean -force
    npm cache verify
  • 从Github私有仓库安装: 需要在github生成token,然后放入.npmrc中:

    1
    2
    @optionsai:registry=https://npm.pkg.github.com/
    //npm.pkg.github.com/:_authToken=这里就是token
  • nodejs如何退出进程

    1
    process.exit(
  • /usr/lib/libcurl.dylib (No such file or directory): 在mac上安装失败,可以尝试

    1
    2
    brew install curl-openssl
    export PATH="/opt/homebrew/opt/curl/bin:$PATH" >> ~/.zshrc
扩展阅读

N-club: 使用Koa + MongoDB + Redis搭建的论坛系统

不容错谷哦的Node.js项目架构n

Redis

  • 统计:比如行为指标、点击量统计、访问量统计、排行榜、最新或最高的N个数据等

  • 缓存:会话缓存,页面缓存,全局变量缓存 队列:队列服务

  • 过期:需要设置过期时间的数据

MongoDB

  • 文本:日志、文章等

MariaDB

  • 数据引擎丰富,包含以下的存储引擎:
    • Aria(增强版的MyISAM)
    • XtraDB(增强版的InnoDB)
    • FederatedX
    • OQGRAPH
    • SphinxSE
    • IBMDB2I
    • TokuDB
    • Cassandra
    • CONNECT
    • SEQUENCE
    • Spider
    • PBXT
  • Group commit for the binary log组提交技术,能够将多个并发提交的事物加入一个队列,对这个队列里的事务,利用一次I/O合并提交,解决写日志频繁刷磁盘的问题
  • 基于表的多线程并行复制技术
  • 线程池thread pool技术
  • 时间精确到微妙级别
  • 增加多源复制和基于表的并行复制
  • 发展速度远远超过MySQL

MySQL

Percona

  • 相比于MySQL,它仅仅是针对InnoDB引擎做了性能上的改善,称为XtraDB

背景: 天朝大局域网,网上都说可以打电话叫客服切换到公网IP,但是电信、移动宽带,无论打客服还是安装师傅,居然从上到下都不知道公网IP是什么,他们以为我要公网IP是要独立宽带,公网IP和共享宽带明明是两个概念好不好,你可以封80端口,但是其它什么端口至少给我留一个总行吧,客服没用,就只能自己动手了。

阅读全文 »

经过长达5个月的艰辛历程(实际的编程时间少之又少),终于自己打造出了一个静态博客。虽然没有wordpress那么方便,虽然没有预期中那么漂亮,虽然还有很多功
能还没有完善,但是这是我第一次用自己的技术为自己做了一个“会用”的东西。

开博两年多了,从Github的octopress到wordpress再到自己搭建静态博客,写了总共一百多篇文章,由于本次改版采用的是大重构,所以我是一篇文章
一篇文章的迁移过来的,只是格式有些还没改动,不过大体能成型了。目前博客有我很喜欢的几大特点:

1.静态化,直接使用nginx实现html文件的静态访问

2.ajax异步提交评论,评论不会立即出发更新html的接口,而是后台审核后才能更新

3.SEO自己做,这个还有很多学习的地方

4.每篇文章对应一张大图,其实是自己爱上摄影后才选择的这个主题,因为这样不仅让我在每篇文章编写时用心,还能用心拍照片(当然,实在没照片的时候就使用的Pixe
bay的免费可商用的图片)

5.采用我最喜欢的Python语言进行编写(Djanago框架)

总之,这几个月很少更新博客,原因是公司的事情太多,自己没有合理利用好时间,所以导致这种这个局面。其实,最近几个月的实习,自己还是有很多感悟的,以后会陆续写成
文章发表出来的。也不知道有没有人看,反正,开心就好咯。

基本框架

项目常用项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flask import Flask, request
app = Flask(__name__)

@app.route('/', methods=['GET', 'POST']) # 定义仅接收哪些类型的请求
def hello():
if request.method == 'POST':
return "POST"
return "Hello World!"

# 带参数的路由
@app.route('/<username>')
def func(username)
@app.route('/post/<int:post_id>')

if __name__ == "__main__":
app.run(debug=True, host='0.0.0.0', port='5000')

但是,业界普遍认为,tornadoflask同时使用能够同时发挥两个框架的有点,前者用于异步处理高并发请求,后者便于编写,这时候torando作为一个httpserver对外提供http访问,flasktornado更加简单易用,只是因为flask在生产环境是需要WSGI server的,所以Tornado是非常适合的,至少比Apache作为server好,而前面的nginx也只是作为负载。Flask's build-in server is not suitable for production as it doesn't scale well and by default serves only one request at a time——Deployment Options

请求和响应

request

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
# 获取表单提交数据, 仅用于表单
request.form.get('begin', '')

# 获取POST数据
request.json
request.json.get('username')

# 获取GET参数
request.args.get('q', '')

# 处理get请求
data = request.get_json(silent=False)
request.query_string # 获取字符串形式的query string

# 获取header头
request.headers.get('Auth-Token')

# 获取用户真实IP
if request.headers.getlist('X-Forwarded-For'):
ip = request.headers.getlist('X-Forwarded-For')[0]
else:
ip = request.remote_addr

# 获取并保存上传的文件
obj = request.files.get('field1')
print(obj.filename)
obj.save('/path/filename.jpg')

resposne

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
# 返回指定状态码
return make_response(jsonify({'a': 'b'}), 201)
return jsonify({'a': 'b'}), 201 # 也可以简单点这样做

# 返回JSON数据
from flask import jsonify
return jsonify(username=g.user.username, email='asd')
return jsonify({'username': g.user.username})

# 允许CORS,可以使用flask-cors库,也可以全局设置如下这个
@app.after_request
def after_request(response):
response.headers.add('Access-Control-Allow-Origin', '*')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
return response

## 如果仅针对单独的接口,可以添加装饰器,当然,该接口必须允许OPTIONS请求,才能直接返回请求
def allow_cross_domain(fun):
@wraps(fun)
def wrapper_fun(*args, **kwargs):
rst = make_response(fun(*args, **kwargs))
rst.headers['Access-Control-Allow-Origin'] = '*'
rst.headers['Access-Control-Allow-Methods'] = 'PUT,GET,POST,DELETE,PATCH'
allow_headers = "Referer,Accept,Origin,User-Agent"
rst.headers['Access-Control-Allow-Headers'] = allow_headers
return rst
return wrapper_fun

中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
@app.before_request
def before(request):
pass

@app.after_request
def after(response):
return response

# 全局错误处理
@app.errorhandler(Exception)
def handle_exception(e: Exception):
traceback.print_exc()
return jsonify({'error': str(e)}), 500

数据库

使用flask-sqlalchemy操作数据库,具体文档可以参考SQLAlchemy手册flask-sqlalchemy帮我们做了很多我们其实不用关心的操作,例如自动新建和关闭session,但是需要注意的是,flask-sqlalchemy默认会在每次使用session的时候开启一个事务,每次请求完成自动结束事务,所以千万不要用它来运行长任务,否则事务一直不关闭,会导致表级锁,无法对表进行操作

常用扩展

flask-jwt-extended

  • jwt扩展
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
token = create_access_token(identity={'id': 1, 'role': 'admin'})

@app.route(...)
@jwt_required() # 必须在路由上加上这个装饰器
def test():
user = get_jwt_identity() # 直接获取identity的数据

# 顺便可以写个验证中间件
def admin_only(func):
@wraps(func)
def wrapper(*args, **kwargs):
user = get_jwt_identity()
if user.get('role') != 'admin':
return jsonify({'error': 'Admin only operation'}), 401
return func(*args, **kwargs)
return wrapper

flask-bcrypt

  • bcrypt扩展,加密密码用

flask-socketio

  • 需要注意的是必须安装eventlet依赖才能使用websocket协议,否则是通过http协议来实现的,还是会没几秒发送轮训请求
1
socketio = SocketIO(app, cors_allowed_origins='*')	# 允许跨域cors

TroubleShooting

  • AssertionError: View function mapping is overwriting an existing endpoint function: main: 原因可能是在给控制器函数添加装饰器的时候没有重新定义其名称,导致每个使用该装饰器的控制器的签名都一样了,可以这样设置控制器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def route_decorator(func):
    def wrapper():
    try:
    return func()
    except Exception as e:
    print(str(e))
    return {"message": str(e), "success": False, "code": 500}
    wrapper.__name__ = func.__name__ # 需要将签名设置为和以前的函数签名一致
    return wrapper

安装方法

支持python3mysql drivermysqlclientpymysql,不推荐只支持2的MySQLdb

1
2
3
4
5
6
7
8
9
10
11
# ubuntu
sudo apt-get install python3-dev gcc libmysqlclient-dev
pip install mysqlclient

# CentOS
sudo yum install pytho36-devel mysql-devel # python36-devel指定python版本
sudo yum install mariadb-devel MariaDB-shared # 如果不安装会出现cannot find -lmariadb错误
pip install mysqlclient

# Mac m1/Apple Silicon
export ARCHFLAGS="-arch x86_64"
阅读全文 »

官网说: “高度包容、快速而极简的Node.js Web框架”,我认为Express最大的优点是可用于API开发,而不是web开发,首先,它的路由定义简单,其次,nodejs天生的异步特性使得其性能极佳。

安装与启动

1
2
3
4
5
6
npm install express-generator -g   # 安装应用程序生成器
express myapp # 生成一个名为myapp的工程目录
cd myapp && npm install # 安装依赖项
DEBUG=myapp:* npm start # MacOS或Linux上启动

DEBUG=express:* node app.js # 打开调试模式

然后在浏览器访问http://localhost:3000/即可访问应用程序了。

最简单的例子(这个例子基本不能处理任何其他的请求,除非用上面的生成器来生成,就会带了一些解析请求生成响应的功能):

1
2
3
4
5
6
7
8
9
10
11
12
13
var express = require('express');
var app = express();

// 如果不加下面这个,那么req.body是undefined的
app.use(express.json());

app.get('/', function (req, res) {
res.send('Hello World!');
});

app.listen(3000, function () {
console.log('Example app listening on port 3000!');
});

请求与响应

请求

1
2
3
4
5
6
7
8
9
10
// 获取请求参数,比如访问的事http://192.168.1.1:6004/code?code=xxxxx
req.query.name // 获取get参数
req.params.id // 获取路由中用冒号定义的参数
req.body.name // 获取POST参数

req.url // 例如:/?code=xxxxx
req.originalUrl // 例如:/code?code=xxxxx
req.baseUrl // /code
req.path // 为什么是/,这里的path应该是指除去路由部分,比如app.use('/code')除去这部分
req.get('host') // 192.168.1.1:6004

响应

1
2
3
res.redirect(301, 'http://google.com')	// 301响应
res.status(200).json({}) // JSON响应
res.send('<html></html>') // 直接响应HTML内容

路由

路由结构定义为:app.METHOD(PATH, HANDLER),例如

1
2
3
4
5
app.get('/', function(req, res){
res.send('Hello World!');
})

app.post('/*', function(req, res){}); // 使用通配符的路由参数

中间件

中间件函数能够访问请求对象(req)、相应对象(res)以及应用程序的请求/相应循环中的下一个中间件函数。

app.use([path], function)用于加载处理http请求的中间件(middleware),请求会以此被use的顺序处理。

服务器

  1. 安装服务(pypi)已经没怎么维护了,这里直接从github拉取源码

    1
    2
    3
    yum install git -y
    git clone -b master https://github.com/shadowsocks/shadowsocks.git
    cd shadowsocks && python3.6 setup.py install

    安装完成后使用ssserver -p 443 -k password -m aes-256-gcm(完整daemon命令ssserver -p 443 -k password -m aes-256-gcm --log-file /var/log/ssserver -d start)进行测试(不再推荐其他协议),从客户端发起连接,发现能科学上网了。

  2. 设置开机启动

    mkdir /etc/shadowsocks

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    vim /etc/shadowsocks/config.json
    # 在config.json中复制入以下配置:
    {
    "server":"0.0.0.0",
    "server_port":8388,
    "local_address": "127.0.0.1",
    "local_port":1080,
    "password":"yourpassword",
    "timeout":300,
    "method":"aes-256-gcm"
    }

    其中端口和密码可按需进行修改

  3. 启动服务``ssserver -c /etc/shadowsocks/config.json`

新机器一键安装脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yum update && yum groupinstall -y 'development tools'
yum install -y zlib-dev openssl-devel sqlite-devel bzip2-devel xz-libs git wget

wget https://www.python.org/ftp/python/3.6.0/Python-3.6.0.tar.xz
xz -d Python-3.6.0.tar.xz
tar -xvf Python-3.6.0.tar
cd Python-3.6.0
./configure && make && sudo make altinstall
cd

git clone https://github.com/shadowsocks/shadowsocks.git
cd shadowsocks && git checkout -b master origin/master
python3.6 setup.py install

ssserver -p 443 -k zuguowansui -m aes-256-gcm --log-file /var/log/ssserver -d start

客户端

  1. 安装必要的软件

    1
    2
    3
    4
    yum install -y epel-release
    yum install -y python
    yum install python-pip privoxy # privoxy用于将ss转换为http代理,代理端口默认为8118
    pip install shadowsocks
  2. 修改相应的配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #vim /etc/shadowsocks.json
    {
    "server": "250.250.250.250",
    "server_port": "2333",
    "local_address": "0.0.0.0",
    "local_port": 1086,
    "password": "",
    "method": "rc4-md5"
    }
  3. 启动服务

    1
    2
    nohup sslocal -c /etc/shadowsocks.json /dev/null 2>&1 &	# 后台执行
    echo " nohup sslocal -c /etc/shadowsocks.json /dev/null 2>&1 &" >> /etc/rc.local # 开机自动启动

Socks5代理转换为HTTP代理

  • 需要注意的是,export的时候应该是小写http_proxy,大写在某些系统里面不起作用

使用的软件叫做privoxy

1
2
3
4
5
sudo apt-get install privoxy
# sudo vim /etc/privoxy/config,将ss代理的配置设置进去,另外可以在该配置文件里面修改日志级别,可以打印更详细的日志
forward-socks5 / 127.0.0.1:1086 .
# 然后重启,sudo /etc/init.d/privoxy restart即可
export http_proxy=127.0.0.1:8118 # 默认代理端口是8118

TroubleShooting

pytest

Python的测试类,调研了一下nose和pytest,虽然nose的使用量确实比pytest多一点,但是活跃度并不高,从15年后就没发布新版本了,而pytest的github还在一直刷。所以选择了pytest来学习python的测试。

常用命令

1
2
3
pytest test.py		# 测试脚本
pytest -x # 当第一次出现失败的时候就停止测试
pytest --maxfail=2 # 设置最大失败次数

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TestName:	# 测试类必须以Test开头
def setup_class(self):
"""测试类开始的时候执行,相当于__init__"""
def teardown_class(self):
"""测试类结束的时候执行,相当于__del__"""
def setup_method(self):
"""每个测试方法开始的时候执行"""
def teardown_method(self):
"""每个测试方法结束的时候执行"""
def test_one(self): # 测试方法必须以test_开头
assert 1 is 2

with pytest.raises(MyException) as e: # 断言下面的语句会抛出指定的错误
x = 1/1

mock对象

mock确实是用来代替我们的测试对象。表面上看,用了mock那还干嘛要测试,其实,不明白的原因是mock的应用场景并不是单纯地代替测试对象,而是代替的那些在当前测试用例并不十分必要,而且该测试对象耗时或者不稳定,和我们真正要测试的代码逻辑无关,那么久可以使用mock来模拟该对象的返回值,实现我们真正想要的测试用例。比如我们现在是要测试函数b,但是函数b却依赖函数a,我们在这里并不关心a,只需要拿到它的返回值而已,所以可以用mock来模拟,以防止a函数本身可能发生的不稳定、耗时等现象。
另外,mock还可以用来保存几个测试用例的全局变量(要说的话,几个测试用例之间是不能有依赖的,只是我现在的场景是需要登录一个网站然后测试里面的功能,只能用mock来保存cookies)pytest测试类是不能更改类变量的。比如:

1
2
3
4
5
6
7
8
9
class TestClass:
var1 = '123'
mock = mock.Mock()

def testLogin(self):
r = requests.post(url, data={})
self.mock.cookies.return_value = r.cookies.items()
def testFun(self):
r = request.get(url, cookies=self.mock.cookies())
扩展阅读