Flask 微型 Python 框架

Flask是一个轻量级的基于Python的web框架。

一.Flask框架的诞生:

Flask诞生于2010年, Armin Ronacher的一个愚人节玩笑。不过现在已经是一个用python语言基于Werkzeug工具箱编写的轻量级web开发框架,它主要面向需求简单,项目周期短的小应用。

二.安装

通过pip3安装Flask即可:

$ sudo pip3 install Flask

进入python交互模式看下Flask的介绍和版本:

$ python3

>>> import flask
>>> print(flask.__doc__)

flask
~~~~~
A microframework based on Werkzeug. It's extensively documented
and follows best practice patterns.
:copyright: © 2010 by the Pallets team.
:license: BSD, see LICENSE for more details.
>>> print(flask.__version__)
1.0.2

3.从 Hello World 开始

本节主要内容:使用Flask写一个显示”Hello World!”的web程序,如何配置、调试Flask。

1 Hello World

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

static和templates目录是默认配置,其中static用来存放静态资源,例如图片、js、css文件等。templates存放模板文件。
我们的网站逻辑基本在server.py文件中,当然,也可以给这个文件起其他的名字。

在server.py中加入以下内容:

from flask import Flask
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    return 'Hello World!'
 
if __name__ == '__main__':
    app.run()

运行server.py:

$ python3 server.py 
 * Running on http://127.0.0.1:5000/

打开浏览器访问http://127.0.0.1:5000/,浏览页面上将出现Hello World!。
终端里会显示下面的信息:

127.0.0.1 - - [16/May/2014 10:29:08] "GET / HTTP/1.1" 200 -

变量app是一个Flask实例,通过下面的方式:

@app.route('/')
def hello_world():
return 'Hello World!'

当客户端访问/时,将响应hello_world()函数返回的内容。注意,这不是返回Hello World!这么简单,Hello World!只是HTTP响应报文的实体部分,状态码等信息既可以由Flask自动处理,也可以通过编程来制定。

2 修改Flask的配置

app = Flask(__name__)

上面的代码中,python内置变量__name__的值是字符串__main__ 。Flask类将这个参数作为程序名称。当然这个是可以自定义的,比如app = Flask(“my-app”)。
Flask默认使用static目录存放静态资源,templates目录存放模板,这是可以通过设置参数更改的:

app = Flask("my-app", static_folder="path1", template_folder="path2")

更多参数请参考__doc__:

from flask import Flask
print(Flask.__doc__)

3 调试模式

上面的server.py中以app.run()方式运行,这种方式下,如果服务器端出现错误是不会在客户端显示的。但是在开发环境中,显示错误信息是很有必要的,要显示错误信息,应该以下面的方式运行Flask:

app.run(debug=True)

将debug设置为True的另一个好处是,程序启动后,会自动检测源码是否发生变化,若有变化则自动重启程序。这可以帮我们省下很多时间。

4 绑定IP和端口

默认情况下,Flask绑定IP为127.0.0.1,端口为5000。我们也可以通过下面的方式自定义:

app.run(host='0.0.0.0', port=80, debug=True)

0.0.0.0代表电脑所有的IP。80是HTTP网站服务的默认端口。什么是默认?比如,我们访问网站http://www.example.com,其实是访问的http://www.example.com:80,只不过:80可以省略不写。

由于绑定了80端口,需要使用root权限运行server.py。也就是:

$ sudo python3 server.py

5 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-001

 

4.获取 URL 参数

URL参数是出现在url中的键值对,例如http://127.0.0.1:5000/?disp=3中的url参数是{'disp':3}

1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 列出所有的url参数

在server.py中添加以下内容:

from flask import Flask, request
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return request.args.__str__()
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

在浏览器中访问http://127.0.0.1:5000/?user=Flask&time&p=7&p=8,将显示:

ImmutableMultiDict([('user', 'Flask'), ('time', ''), ('p', '7'), ('p', '8')])

较新的浏览器也支持直接在url中输入中文(最新的火狐浏览器内部会帮忙将中文转换成符合URL规范的数据),在浏览器中访问http://127.0.0.1:5000/?info=这是爱,,将显示:

ImmutableMultiDict([('info', '这是爱,')])

浏览器传给我们的Flask服务的数据长什么样子呢?可以通过request.full_path和request.path来看一下:

from flask import Flask, request
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    print(request.path)
    print(request.full_path)
    return request.args.__str__()
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

浏览器访问http://127.0.0.1:5000/?info=这是爱,,运行server.py的终端会输出:

/
/?info=%E8%BF%99%E6%98%AF%E7%88%B1%EF%BC%8C

3 获取某个指定的参数

例如,要获取键info对应的值,如下修改server.py:

from flask import Flask, request
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    return request.args.get('info')
 
if __name__ == '__main__':
    app.run(port=5000)

运行server.py,在浏览器中访问http://127.0.0.1:5000/?info=hello,浏览器将显示:

hello

不过,当我们访问http://127.0.0.1:5000/时候却出现了500错误,浏览器显示:

如果开启了Debug模式,会显示:

为什么为这样?

这是因为没有在URL参数中找到info。所以request.args.get(‘info’)返回Python内置的None,而Flask不允许返回None。

解决方法很简单,我们先判断下它是不是None:

from flask import Flask, request
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    r = request.args.get('info')
    if r==None:
        # do something
        return ''
    return r
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

另外一个方法是,设置默认值,也就是取不到数据时用这个值:

from flask import Flask, request
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    r = request.args.get('info', 'hi')
    return r
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

函数request.args.get的第二个参数用来设置默认值。此时在浏览器访问http://127.0.0.1:5000/,将显示:

hi

4 如何处理多值

还记得上面有一次请求是这样的吗? http://127.0.0.1:5000/?user=Flask&time&p=7&p=8,仔细看下,p有两个值。

如果我们的代码是:

from flask import Flask, request
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    r = request.args.get('p')
    return r
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

在浏览器中请求时,我们只会看到7。如果我们需要把p的所有值都获取到,该怎么办?

不用get,用getlist:

from flask import Flask, request
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    r = request.args.getlist('p')  # 返回一个list
    return str(r)
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

浏览器输入 http://127.0.0.1:5000/?user=Flask&time&p=7&p=8,我们会看到[‘7’, ‘8’]。

5 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-002

五.获取POST方法传送的数据

作为一种HTTP请求方法,POST用于向指定的资源提交要被处理的数据。我们在某网站注册用户、写文章等时候,需要将数据传递到网站服务器中。并不适合将数据放到URL参数中,密码放到URL参数中容易被看到,文章数据又太多,浏览器不一定支持太长长度的URL。这时,一般使用POST方法。

本文使用python的requests库模拟浏览器。
安装方法:

$ sudo pip3 install requests

1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

<h3″>2 查看POST数据内容

以用户注册为例子,我们需要向服务器/register传送用户名name和密码password。如下编写HelloWorld/server.py。

from flask import Flask, request
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/register', methods=['POST'])
def register():
    print(request.headers)
    print(request.stream.read())
    return 'welcome'
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

`@app.route(‘/register’, methods=[‘POST’])是指url/register只接受POST方法。可以根据需要修改methods`参数,例如如果想要让它同时支持GET和POST,这样写:

@app.route('/register', methods=['GET', 'POST']) 

浏览器模拟工具client.py内容如下:

import requests

user_info = {'name': 'letian', 'password': '123'}
r = requests.post("http://127.0.0.1:5000/register", data=user_info)

print(r.text)

运行HelloWorld/server.py,然后运行client.py。client.py将输出:

welcome

而HelloWorld/server.py在终端中输出以下调试信息(通过print输出):

Host: 127.0.0.1:5000
User-Agent: python-requests/2.19.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 24
Content-Type: application/x-www-form-urlencoded
 
 
b'name=letian&password=123'

前6行是client.py生成的HTTP请求头,由print(request.headers)输出。

请求体的数据,我们通过print(request.stream.read())输出,结果是:

b'name=letian&password=123'

3 解析POST数据

上面,我们看到post的数据内容是:

b'name=letian&password=123'

我们要想办法把我们要的name、password提取出来,怎么做呢?自己写?不用,Flask已经内置了解析器request.form。

我们将服务代码改成:

from flask import Flask, request
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/register', methods=['POST'])
def register():
    print(request.headers)
    # print(request.stream.read()) # 不要用,否则下面的form取不到数据
    print(request.form)
    print(request.form['name'])
    print(request.form.get('name'))
    print(request.form.getlist('name'))
    print(request.form.get('nickname', default='little apple'))
    return 'welcome'
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

执行client.py请求数据,服务器代码会在终端输出:

Host: 127.0.0.1:5000
User-Agent: python-requests/2.19.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 24
Content-Type: application/x-www-form-urlencoded
 
 
ImmutableMultiDict([('name', 'letian'), ('password', '123')])
letian
letian
['letian']
little apple

request.form会自动解析数据。

request.form[‘name’]和request.form.get(‘name’)都可以获取name对应的值。对于request.form.get()可以为参数default指定值以作为默认值。所以:

print(request.form.get('nickname', default='little apple'))

输出的是默认值

little apple

如果name有多个值,可以使用request.form.getlist(‘name’),该方法将返回一个列表。我们将client.py改一下:

import requests
 
user_info = {'name': ['letian', 'letian2'], 'password': '123'}
r = requests.post("http://127.0.0.1:5000/register", data=user_info)
 
print(r.text)

此时运行client.py,print(request.form.getlist(‘name’))将输出:

[u'letian', u'letian2']

4 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-003

六. 处理和响应JSON数据

使用 HTTP POST 方法传到网站服务器的数据格式可以有很多种,比如「5. 获取POST方法传送的数据」讲到的name=letian&password=123这种用过&符号分割的key-value键值对格式。我们也可以用JSON格式、XML格式。相比XML的重量、规范繁琐,JSON显得非常小巧和易用。

1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 处理JSON格式的请求数据

如果POST的数据是JSON格式,request.json会自动将json数据转换成Python类型(字典或者列表)。

编写server.py:

from flask import Flask, request
 
app = Flask("my-app")
 
 
@app.route('/')
def hello_world():
    return 'Hello World!'
 
 
@app.route('/add', methods=['POST'])
def add():
    print(request.headers)
    print(type(request.json))
    print(request.json)
    result = request.json['a'] + request.json['b']
    return str(result)
 
 
if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000, debug=True)

编写client.py模拟浏览器请求:

import requests
 
json_data = {'a': 1, 'b': 2}
 
r = requests.post("http://127.0.0.1:5000/add", json=json_data)
 
print(r.text)

运行server.py,然后运行client.py,client.py 会在终端输出:

3

server.py 会在终端输出:

Host: 127.0.0.1:5000
User-Agent: python-requests/2.19.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 16
Content-Type: application/json
 
 
<class 'dict'>
{'a': 1, 'b': 2}

注意,请求头中Content-Type的值是application/json。

3 响应JSON-方案1

响应JSON时,除了要把响应体改成JSON格式,响应头的Content-Type也要设置为application/json。
编写server2.py:

from flask import Flask, request, Response
import json
 
app = Flask("my-app")
 
 
@app.route('/')
def hello_world():
    return 'Hello World!'
 
 
@app.route('/add', methods=['POST'])
def add():
    result = {'sum': request.json['a'] + request.json['b']}
    return Response(json.dumps(result),  mimetype='application/json')
 
 
if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000, debug=True)

修改后运行。

编写client2.py:

import requests
 
json_data = {'a': 1, 'b': 2}
 
r = requests.post("http://127.0.0.1:5000/add", json=json_data)
 
print(r.headers)
print(r.text)

运行client.py,将显示:

{'Content-Type': 'application/json', 'Content-Length': '10', 'Server': 'Werkzeug/0.14.1 Python/3.6.4', 'Date': 'Sat, 07 Jul 2018 05:23:00 GMT'}
{"sum": 3}

上面第一段内容是服务器的响应头,第二段内容是响应体,也就是服务器返回的JSON格式数据。

另外,如果需要服务器的HTTP响应头具有更好的可定制性,比如自定义Server,可以如下修改add()函数:

@app.route('/add', methods=['POST'])
def add():
    result = {'sum': request.json['a'] + request.json['b']}
    resp = Response(json.dumps(result),  mimetype='application/json')
    resp.headers.add('Server', 'python flask')
    return resp

client2.py运行后会输出:

{'Content-Type': 'application/json', 'Content-Length': '10', 'Server': 'python flask', 'Date': 'Sat, 07 Jul 2018 05:26:40 GMT'}
{"sum": 3}

4 响应JSON-方案2

使用 jsonify 工具函数即可。

from flask import Flask, request, jsonify
 
app = Flask("my-app")
 
 
@app.route('/')
def hello_world():
    return 'Hello World!'
 
 
@app.route('/add', methods=['POST'])
def add():
    result = {'sum': request.json['a'] + request.json['b']}
    return jsonify(result)
 
 
if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000, debug=True)

5 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-004

七. 上传文件

上传文件,一般也是用POST方法。

7.1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 上传文件

这一部分的代码参考自How to upload a file to the server in Flask。

我们以上传图片为例:
假设将上传的图片只允许’png’、’jpg’、’jpeg’、’gif’这四种格式,通过url/upload使用POST上传,上传的图片存放在服务器端的static/uploads目录下。

首先在项目HelloWorld中创建目录static/uploads:

mkdir HelloWorld/static/uploads

werkzeug库可以判断文件名是否安全,例如防止文件名是../../../a.png,安装这个库:

$ sudo pip3 install werkzeug

server.py代码:

from flask import Flask, request
 
from werkzeug.utils import secure_filename
import os
 
app = Flask(__name__)
 
# 文件上传目录
app.config['UPLOAD_FOLDER'] = 'static/uploads/'
# 支持的文件格式
app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif'}  # 集合类型
 
 
# 判断文件名是否是我们支持的格式
def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1] in app.config['ALLOWED_EXTENSIONS']
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/upload', methods=['POST'])
def upload():
    upload_file = request.files['image']
    if upload_file and allowed_file(upload_file.filename):
        filename = secure_filename(upload_file.filename)
        # 将文件保存到 static/uploads 目录,文件名同上传时使用的文件名
        upload_file.save(os.path.join(app.root_path, app.config['UPLOAD_FOLDER'], filename))
        return 'info is '+request.form.get('info', '')+'. success'
    else:
        return 'failed'
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

app.config中的config是字典的子类,可以用来设置自有的配置信息,也可以设置自己的配置信息。函数allowed_file(filename)用来判断filename是否有后缀以及后缀是否在app.config[‘ALLOWED_EXTENSIONS’]中。

客户端上传的图片必须以image01标识。upload_file是上传文件对应的对象。app.root_path获取server.py所在目录在文件系统中的绝对路径。upload_file.save(path)用来将upload_file保存在服务器的文件系统中,参数最好是绝对路径,否则会报错(网上很多代码都是使用相对路径,但是笔者在使用相对路径时总是报错,说找不到路径)。函数os.path.join()用来将使用合适的路径分隔符将路径组合起来。

好了,定制客户端client.py:

import requests
 
file_data = {'image': open('Lenna.jpg', 'rb')}
 
user_info = {'info': 'Lenna'}
 
r = requests.post("http://127.0.0.1:5000/upload", data=user_info, files=file_data)
 
print(r.text)

运行client.py,当前目录下的Lenna.jpg将上传到服务器。

然后,我们可以在static/uploads中看到文件Lenna.jpg。

要控制上产文件的大小,可以设置请求实体的大小,例如:

app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 #16MB

不过,在处理上传文件时候,需要使用try:…except:…。

如果要获取上传文件的内容可以:

file_content = request.files['image'].stream.read()

3 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-005

八. Restful URL

简单来说,Restful URL可以看做是对 URL 参数的替代。

8.1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 编写代码

编辑server.py:

from flask import Flask
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/user/')
def user(username):
    print(username)
    print(type(username))
    return 'hello ' + username
 
 
@app.route('/user//friends')
def user_friends(username):
    print(username)
    print(type(username))
    return 'hello ' + username
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

运行HelloWorld/server.py。使用浏览器访问http://127.0.0.1:5000/user/letian,HelloWorld/server.py将输出:

letian
<class 'str'>

而访问http://127.0.0.1:5000/user/letian/,响应为404 Not Found。

浏览器访问http://127.0.0.1:5000/user/letian/friends,可以看到:

Hello letian. They are your friends.

HelloWorld/server.py输出:

letian
<class 'str'>

3 转换类型

由上面的示例可以看出,使用 Restful URL 得到的变量默认为str对象。如果我们需要通过分页显示查询结果,那么需要在url中有数字来指定页数。按照上面方法,可以在获取str类型页数变量后,将其转换为int类型。不过,还有更方便的方法,就是用flask内置的转换机制,即在route中指定该如何转换。

新的服务器代码:

from flask import Flask
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/page/')
def page(num):
    print(num)
    print(type(num))
    return 'hello world'
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

`@app.route(‘/page/int:num‘)`会将num变量自动转换成int类型。

运行上面的程序,在浏览器中访问http://127.0.0.1:5000/page/1,HelloWorld/server.py将输出如下内容:

1
<class 'int'>

如果访问的是http://127.0.0.1:5000/page/asd,我们会得到404响应。

在官方资料中,说是有3个默认的转换器:

int     accepts integers
float     like int but for floating point values
path     like the default but also accepts slashes

看起来够用了。

4 一个有趣的用法

如下编写服务器代码:

from flask import Flask
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/page/-')
def page(num1, num2):
    print(num1)
    print(num2)
    return 'hello world'
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

在浏览器中访问http://127.0.0.1:5000/page/11-22,HelloWorld/server.py会输出:

11
22

5 编写转换器

自定义的转换器是一个继承werkzeug.routing.BaseConverter的类,修改to_python和to_url方法即可。to_python方法用于将url中的变量转换后供被`@app.route包装的函数使用,to_url方法用于flask.url_for`中的参数转换。

下面是一个示例,将HelloWorld/server.py修改如下:

from flask import Flask, url_for
 
from werkzeug.routing import BaseConverter
 
 
class MyIntConverter(BaseConverter):
 
    def __init__(self, url_map):
        super(MyIntConverter, self).__init__(url_map)
 
    def to_python(self, value):
        return int(value)
 
    def to_url(self, value):
        return value * 2
 
 
app = Flask(__name__)
app.url_map.converters['my_int'] = MyIntConverter
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/page/')
def page(num):
    print(num)
    print(url_for('page', num=123))   # page 对应的是 page函数 ,num 对应对应`/page/`中的num,必须是str
    return 'hello world'
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

浏览器访问http://127.0.0.1:5000/page/123后,HelloWorld/server.py的输出信息是:

123
/page/123123

6 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-006

7 值得读

理解RESTful架构

9. 使用url_for生成链接

工具函数url_for可以让你以软编码的形式生成url,提供开发效率。

9.1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 编写代码

编辑HelloWorld/server.py:

from flask import Flask, url_for
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    pass
 
@app.route('/user/')
def user(name):
    pass
 
@app.route('/page/')
def page(num):
    pass
 
@app.route('/test')
def test():
    print(url_for('hello_world'))
    print(url_for('user', name='letian'))
    print(url_for('page', num=1, q='hadoop mapreduce 10%3'))
    print(url_for('static', filename='uploads/01.jpg'))
    return 'Hello'
 
if __name__ == '__main__':
    app.run(debug=True)

运行HelloWorld/server.py。然后在浏览器中访问http://127.0.0.1:5000/test,HelloWorld/server.py将输出以下信息:

/
/user/letian
/page/1?q=hadoop+mapreduce+10%253
/static/uploads/01.jpg

3 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-007

十. 使用redirect重定向网址

redirect函数用于重定向,实现机制很简单,就是向客户端(浏览器)发送一个重定向的HTTP报文,浏览器会去访问报文中指定的url。

10.1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 编写代码

使用redirect时,给它一个字符串类型的参数就行了。
编辑HelloWorld/server.py:

from flask import Flask, url_for, redirect
 
app = Flask(__name__)
 
@app.route('/')
def hello_world():
    return 'hello world'
 
@app.route('/test1')
def test1():
    print('this is test1')
    return redirect(url_for('test2'))
 
@app.route('/test2')
def test2():
    print('this is test2')
    return 'this is test2'
 
if __name__ == '__main__':
    app.run(debug=True)

运行HelloWorld/server.py,在浏览器中访问http://127.0.0.1:5000/test1,浏览器的url会变成http://127.0.0.1:5000/test2,并显示:

this is test2

3 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-008

十一. 使用Jinja2模板引擎

模板引擎负责MVC中的V(view,视图)这一部分。Flask默认使用Jinja2模板引擎。

Flask与模板相关的函数有:

  • flask.render_template(template_name_or_list, **context)
    Renders a template from the template folder with the given context.
  • flask.render_template_string(source, **context)
    Renders a template from the given template source string with the given context.
  • flask.get_template_attribute(template_name, attribute)
    Loads a macro (or variable) a template exports. This can be used to invoke a macro from within Python code.

这其中常用的就是前两个函数。

这个实例中使用了模板继承、if判断、for循环。

11.1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 创建并编辑HelloWorld/templates/default.html

内容如下:

    

 

    {% block body %}{% endblock %}
 
 
```
 
可以看到,在``标签中使用了if判断,如果给模板传递了`page_title`变量,显示之,否则,不显示。
 
``标签中定义了一个名为`body`的block,用来被其他模板文件继承。
 
### 11.3 创建并编辑HelloWorld/templates/user_info.html
内容如下:
 
```
{% extends "default.html" %}
 
{% block body %}
    {% for key in user_info %}
 
        {{ key }}: {{ user_info[key] }} 
 
 
    {% endfor %}
{% endblock %}

变量user_info应该是一个字典,for循环用来循环输出键值对。

4 编辑HelloWorld/server.py

内容如下:

from flask import Flask, render_template
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/user')
def user():
    user_info = {
        'name': 'letian',
        'email': '[email protected]',
        'age':0,
        'github': 'https://github.com/letiantian'
    }
    return render_template('user_info.html', page_title='letian\'s info', user_info=user_info)
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

render_template()函数的第一个参数指定模板文件,后面的参数是要传递的数据。

5 运行与测试

运行HelloWorld/server.py:

$ python3 HelloWorld/server.py

在浏览器中访问http://127.0.0.1:5000/user,效果图如下:

查看网页源码:

        name: letian 
        email: [email protected] 
        age: 0 
        github: https://github.com/letiantian 

6 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-009

十二. 自定义404等错误的响应

要处理HTTP错误,可以使用flask.abort函数。

1 示例1:简单入门

建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

代码

编辑HelloWorld/server.py:

from flask import Flask, render_template_string, abort
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/user')
def user():
    abort(401)  # Unauthorized 未授权
    print('Unauthorized, 请先登录')
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

效果

运行HelloWorld/server.py,浏览器访问http://127.0.0.1:5000/user,效果如下:

要注意的是,HelloWorld/server.py中abort(401)后的print并没有执行。

2 示例2:自定义错误页面

代码
将服务器代码改为:

from flask import Flask, render_template_string, abort
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/user')
def user():
    abort(401)  # Unauthorized


@app.errorhandler(401)
def page_unauthorized(error):
    return render_template_string('<h1> Unauthorized </h1><h2>{{ error_info }}</h2>', error_info=error), 401
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

page_unauthorized函数返回的是一个元组,401 代表HTTP 响应状态码。如果省略401,则响应状态码会变成默认的 200。

效果

运行HelloWorld/server.py,浏览器访问http://127.0.0.1:5000/user,效果如下:

 

3 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-010

十三. 用户会话

session用来记录用户的登录状态,一般基于cookie实现。

下面是一个简单的示例。

1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 编辑HelloWorld/server.py

内容如下:

from flask import Flask, render_template_string, \
    session, request, redirect, url_for
 
app = Flask(__name__)
 
app.secret_key = 'F12Zr47j\3yX R~X@H!jLwf/T'
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/login')
def login():
    page = '''
        <form action="{{ url_for('do_login') }}" method="post">
        <p>name: <input type="text" name="user_name" /></p>
        <input type="submit" value="Submit" />
    </form>
    '''
return render_template_string(page)

@app.route('/do_login', methods=['POST'])
def do_login():
    name = request.form.get('user_name')
    session['user_name'] = name
    return 'success'
 
 
@app.route('/show')
def show():
    return session['user_name']
 
 
@app.route('/logout')
def logout():
    session.pop('user_name', None)
    return redirect(url_for('login'))
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

3 代码的含义

app.secret_key用于给session加密。

/login中将向用户展示一个表单,要求输入一个名字,submit后将数据以post的方式传递给/do_login/do_login将名字存放在session中。

如果用户成功登录,访问/show时会显示用户的名字。此时,打开firebug等调试工具,选择session面板,会看到有一个cookie的名称为session

/logout用于登出,通过将session中的user_name字段pop即可。Flask中的session基于字典类型实现,调用pop方法时会返回pop的键对应的值;如果要pop的键并不存在,那么返回值是pop()的第二个参数。

另外,使用redirect()重定向时,一定要在前面加上return

4 效果

进入http://127.0.0.1:5000/login,输入name,点击submit:

进入http://127.0.0.1:5000/show查看session中存储的name:

5 设置sessin的有效时间

下面这段代码来自Is there an easy way to make sessions timeout in flask?

from datetime import timedelta
from flask import session, app
 
session.permanent = True
app.permanent_session_lifetime = timedelta(minutes=5)

这段代码将session的有效时间设置为5分钟。

6 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-011

十四. 使用Cookie

Cookie是存储在客户端的记录访问者状态的数据。具体原理,请见 http://zh.wikipedia.org/wiki/Cookie 。 常用的用于记录用户登录状态的session大多是基于cookie实现的。
cookie可以借助flask.Response来实现。下面是一个示例。

1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 代码

修改HelloWorld/server.py:

from flask import Flask, request, Response, make_response
import time
 
app = Flask(__name__)
 
 
@app.route('/')
def hello_world():
    return 'hello world'
 
 
@app.route('/add')
def login():
    res = Response('add cookies')
    res.set_cookie(key='name', value='letian', expires=time.time()+6*60)
    return res
 
 
@app.route('/show')
def show():
    return request.cookies.__str__()
 
 
@app.route('/del')
def del_cookie():
    res = Response('delete cookies')
    res.set_cookie('name', '', expires=0)
    return res
 
 
if __name__ == '__main__':
    app.run(port=5000, debug=True)

由上可以看到,可以使用Response.set_cookie添加和删除cookie。expires参数用来设置cookie有效时间,它的值可以是datetime对象或者unix时间戳,笔者使用的是unix时间戳。

res.set_cookie(key='name', value='letian', expires=time.time()+6*60)

上面的expire参数的值表示cookie在从现在开始的6分钟内都是有效的。

要删除cookie,将expire参数的值设为0即可:

res.set_cookie('name', '', expires=0)

set_cookie()函数的原型如下:

set_cookie(key, value=’’, max_age=None, expires=None, path=’/‘, domain=None, secure=None, httponly=False)

Sets a cookie. The parameters are the same as in the cookie Morsel object in the Python standard library but it accepts unicode data, too.
Parameters:

     key – the key (name) of the cookie to be set.
value – the value of the cookie.
max_age – should be a number of seconds, or None (default) if the cookie should last only as long as the client’s browser session.

     expires – should be a datetime object or UNIX timestamp.

     domain – if you want to set a cross-domain cookie. For example, domain=”.example.com” will set a cookie that is readable by the domain www.example.com, foo.example.com etc. Otherwise, a cookie will only be readable by the domain that set it.

     path – limits the cookie to a given path, per default it will span the whole domain.

3 运行与测试

运行HelloWorld/server.py:

$ python3 HelloWorld/server.py

使用浏览器打开http://127.0.0.1:5000/add,浏览器界面会显示

add cookies

下面查看一下cookie,如果使用firefox浏览器,可以用firebug插件查看。打开firebug,选择Cookies选项,刷新页面,可以看到名为name的cookie,其值为letian。

在“网络”选项中,可以查看响应头中类似下面内容的设置cookie的HTTP「指令」:

Set-Cookie: name=letian; Expires=Sun, 29-Jun-2014 05:16:27 GMT; Path=/

在cookie有效期间,使用浏览器访问http://127.0.0.1:5000/show,可以看到:

{'name': 'letian'}

4 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-012

十五. 闪存系统 flashing system

Flask的闪存系统(flashing system)用于向用户提供反馈信息,这些反馈信息一般是对用户上一次操作的反馈。反馈信息是存储在服务器端的,当服务器向客户端返回反馈信息后,这些反馈信息会被服务器端删除。

下面是一个示例。

1 建立Flask项目

按照以下命令建立Flask项目HelloWorld:

mkdir HelloWorld
mkdir HelloWorld/static
mkdir HelloWorld/templates
touch HelloWorld/server.py

2 编写HelloWorld/server.py

内容如下:

from flask import Flask, flash, get_flashed_messages
import time
 
app = Flask(__name__)
app.secret_key = 'some_secret'
 
 
@app.route('/')
def index():
    return 'hi'
 
 
@app.route('/gen')
def gen():
    info = 'access at '+ time.time().__str__()
    flash(info)
    return info
 
 
@app.route('/show1')
def show1():
    return get_flashed_messages().__str__()
 
 
@app.route('/show2')
def show2():
    return get_flashed_messages().__str__()
 
 
if __name__ == "__main__":
    app.run(port=5000, debug=True)

3 效果

运行服务器:

$ python3 HelloWorld/server.py

打开浏览器,访问http://127.0.0.1:5000/gen,浏览器界面显示(注意,时间戳是动态生成的,每次都会不一样,除非并行访问):

access at 1404020982.83

查看浏览器的cookie,可以看到session,其对应的内容是:

.eJyrVopPy0kszkgtVrKKrlZSKIFQSUpWSknhYVXJRm55UYG2tkq1OlDRyHC_rKgIvypPdzcDTxdXA1-XwHLfLEdTfxfPUn8XX6DKWCAEAJKBGq8.BpE6dg.F1VURZa7VqU9bvbC4XIBO9-3Y4Y

再一次访问http://127.0.0.1:5000/gen,浏览器界面显示:

access at 1404021130.32

cookie中session发生了变化,新的内容是:

.eJyrVopPy0kszkgtVrKKrlZSKIFQSUpWSknhYVXJRm55UYG2tkq1OlDRyHC_rKgIvypPdzcDTxdXA1-XwHLfLEdTfxfPUn8XX6DKWLBaMg1yrfCtciz1rfIEGxRbCwAhGjC5.BpE7Cg.Cb_B_k2otqczhknGnpNjQ5u4dqw

然后使用浏览器访问http://127.0.0.1:5000/show1,浏览器界面显示:

['access at 1404020982.83', 'access at 1404021130.32']

这个列表中的内容也就是上面的两次访问http://127.0.0.1:5000/gen得到的内容。此时,cookie中已经没有session了。

如果使用浏览器访问http://127.0.0.1:5000/show1或者http://127.0.0.1:5000/show2,只会得到:

[]

4 高级用法

flash系统也支持对flash的内容进行分类。修改HelloWorld/server.py内容:

from flask import Flask, flash, get_flashed_messages
import time
 
app = Flask(__name__)
app.secret_key = 'some_secret'
 
 
@app.route('/')
def index():
    return 'hi'
 
 
@app.route('/gen')
def gen():
    info = 'access at '+ time.time().__str__()
    flash('show1 '+info, category='show1')
    flash('show2 '+info, category='show2')
    return info
 
 
@app.route('/show1')
def show1():
    return get_flashed_messages(category_filter='show1').__str__()
 
@app.route('/show2')
def show2():
    return get_flashed_messages(category_filter='show2').__str__()
 
 
if __name__ == "__main__":
    app.run(port=5000, debug=True)

某一时刻,浏览器访问http://127.0.0.1:5000/gen,浏览器界面显示:

access at 1404022326.39

不过,由上面的代码可以知道,此时生成了两个flash信息,但分类(category)不同。

使用浏览器访问http://127.0.0.1:5000/show1,得到如下内容:

['1 access at 1404022326.39']

而继续访问http://127.0.0.1:5000/show2,得到的内容为空:

[]

5 在模板文件中获取flash的内容

在Flask中,get_flashed_messages()默认已经集成到Jinja2模板引擎中,易用性很强。下面是来自官方的一个示例:

6 本节源码

https://github.com/letiantian/Learn-Flask/tree/master/flask-demo-013

gin+gorm+router 快速搭建 crud restful API 接口

gin mysql_gin+gorm+router 快速搭建 crud restful API 接口

下载扩展

go get github.com/go-sql-driver/mysql 
go get github.com/jinzhu/gorm
go get github.com/gin-gonic/gin

建表语句

CREATE TABLE `users` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
`password` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

结构

├──api
│ ├── apis 
│ │ └── user.go 
│ ├── database 
│ │ └── mysql.go 
│ ├── models 
│ │ └── user.go 
│ └── router 
│ └── router.go
└──main.go

apis/apis/user.go

package apis

import (
    "github.com/gin-gonic/gin"
    model "api/models"
    "net/http"
    "strconv"
)

//列表数据
func Users(c *gin.Context) {
    var user model.User
    user.Username = c.Request.FormValue("username")
    user.Password = c.Request.FormValue("password")
    result, err := user.Users()

    if err != nil {
        c.JSON(http.StatusOK, gin.H{
            "code":    -1,
            "message": "抱歉未找到相关信息",
        })
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "code": 1,
        "data":   result,
    })
}

//添加数据
func Store(c *gin.Context) {
    var user model.User
    user.Username = c.Request.FormValue("username")
    user.Password = c.Request.FormValue("password")
    id, err := user.Insert()

    if err != nil {
        c.JSON(http.StatusOK, gin.H{
            "code":    -1,
            "message": "添加失败",
        })
        return
    }
    c.JSON(http.StatusOK, gin.H{
        "code":  1,
        "message": "添加成功",
        "data":    id,
    })
}

//修改数据
func Update(c *gin.Context) {
    var user model.User
    id, err := strconv.ParseInt(c.Param("id"), 10, 64)
    user.Password = c.Request.FormValue("password")
    result, err := user.Update(id)
    if err != nil || result.ID == 0 {
        c.JSON(http.StatusOK, gin.H{
            "code":    -1,
            "message": "修改失败",
        })
        return
    }
    c.JSON(http.StatusOK, gin.H{
        "code":  1,
        "message": "修改成功",
    })
}

//删除数据
func Destroy(c *gin.Context) {
    var user model.User
    id, err := strconv.ParseInt(c.Param("id"), 10, 64)
    result, err := user.Destroy(id)
    if err != nil || result.ID == 0 {
        c.JSON(http.StatusOK, gin.H{
            "code":    -1,
            "message": "删除失败",
        })
        return
    }
    c.JSON(http.StatusOK, gin.H{
        "code":  1,
        "message": "删除成功",
    })
}

database/mysql.go

package database

import (
    _ "github.com/go-sql-driver/mysql" //加载mysql
    "github.com/jinzhu/gorm"
    "fmt"
)

var Eloquent *gorm.DB

func init() {
    var err error
    Eloquent, err = gorm.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test?charset=utf8&parseTime=True&loc=Local&timeout=10ms")

    if err != nil {
        fmt.Printf("mysql connect error %v", err)
    }

    if Eloquent.Error != nil {
        fmt.Printf("database error %v", Eloquent.Error)
    }
}

models/user.go

    package models

    import (
        orm "api/database"
    )

    type User struct {
        ID       int64  `json:"id"`       // 列名为 `id`
        Username string `json:"username"` // 列名为 `username`
        Password string `json:"password"` // 列名为 `password`
    }

    var Users []User

    //添加
    func (user User) Insert() (id int64, err error) {

        //添加数据
        result := orm.Eloquent.Create(&user)
        id =user.ID
        if result.Error != nil {
            err = result.Error
            return
        }
        return
    }

    //列表
    func (user *User) Users() (users []User, err error) {
        if err = orm.Eloquent.Find(&users).Error; err != nil {
            return
        }
        return
    }

    //修改
    func (user *User) Update(id int64) (updateUser User, err error) {

        if err = orm.Eloquent.Select([]string{"id", "username"}).First(&updateUser, id).Error; err != nil {
            return
        }

        //参数1:是要修改的数据
        //参数2:是修改的数据
        if err = orm.Eloquent.Model(&updateUser).Updates(&user).Error; err != nil {
            return
        }
        return
    }

    //删除数据
    func (user *User) Destroy(id int64) (Result User, err error) {

        if err = orm.Eloquent.Select([]string{"id"}).First(&user, id).Error; err != nil {
            return
        }

        if err = orm.Eloquent.Delete(&user).Error; err != nil {
            return
        }
        Result = *user
        return
    }

router/router.go

package router

import (
    "github.com/gin-gonic/gin"
    . "api/apis"
)

func InitRouter() *gin.Engine {
    router := gin.Default()

    router.GET("/users", Users)

    router.POST("/user", Store)

    router.PUT("/user/:id", Update)

    router.DELETE("/user/:id", Destroy)

    return router
}

main.go

    package main

    import (
        _ "api/database"
        "api/router"
        orm "api/database"
    )

    func main() {
        defer orm.Eloquent.Close()
        router := router.InitRouter()
        router.Run(":8000")
    }

执行 go run main.go

访问地址

    • POST localhost:8006/user 添加
    • GET localhost:8006/users 列表
    • DELETE localhost:8006/user/id 删除
    • PUT localhost:8006/user/id 修改

将Vue项目打包为Windows应用(.exe)

背景

朋友是做商品零售,每月都需要将销售数据汇总至年度销售表格中,在这个过程中存在很多重复性的工作,无奈中。在一次聊天中,我了解到他的需求,就用 Vue 做了一个页面,可以实现 Excel 转成 JSON 进行操作,最后再将 JSON 转成 Excel ,虽然后来了解到用 Python应该会更高效,待日后来研究!

不过咱好歹有个图形界面,用户体验好!(自我安慰一波~)

接下来问题便来了,朋友完全不懂编程,每次都准备开发环境也挺麻烦,便想着能不能做成可执行文件.exe,直接双击安装,生成快捷方式,直接就能用,人性化点赞!

1.首先从electron官网克隆一个demo

选择一个你想存放项目的盘。(可以不用新建文件夹,看个人)直接运行cmd;

注意这里的最好是npm的依赖包

npm与cnpm的区别

  • 说到npm与cnpm的区别,可能大家都知道,但大家容易忽视的一点,是cnpm装的各种node_module,这种方式下所有的包都是扁平化的安装。
  • 一下子node_modules展开后有非常多的文件。导致了在打包的过程中非常慢。但是如果改用npm来安装node_modules的话,所有的包都是树状结构的,层级变深。
  • 由于这个不同,对一些项目比较大的应用,很容易出现打包过程慢且node内存溢出的问题
  • 所以建议大家在打包前,讲使用cnpm安装的依赖包删除,替换成npm安装的依赖包。
git clone https://github.com/electron/electron-quick-start
cd electron-quick-start
cnpm install //npm,cnpm 都可以,cnpm速度较快.
npm start

项目跑起来以后, 就会出现electron的桌面页面,找到clone下来项目的入口文件main.js 和package.json.接下来修改路径和配置。

//----main.js----
function createWindow () {

// and load the index.html of the app.
mainWindow.loadURL(`file://${__dirname}/../dist/index.html`) //修改这里

2. 接下来,在已创建好的vue-cli项目中

安装electron依赖,运行如下命令:

npm install electron --save-dev
npm install electron-packager --save-dev

现在将clone项目中的main.js拷到刚刚新建的项目中的build文件夹下,并重命名为electron.js  , 并更改config/index.js中生产模式下(build)的assetsPublicPth

 build: {
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',

assetsPublicPath: './', //这里改为./

3. 在新建的项目的package.json文件中增加一条指令

如下:

 "scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"unit": "jest --config test/unit/jest.conf.js --coverage",
"e2e": "node test/e2e/runner.js",
"test": "npm run unit && npm run e2e",
"build": "node build/build.js",
"electron_dev": "npm run build && electron build/electron.js" //增加的指令

接着执行:

npm run build //生成dist目录(包含静态资源文件) 
npm run electron_dev //启动electron

现在,生成桌面应用基本成功实现了,还剩下最后一步:打包!

首先,复制build目录下的electron.js到dist目录中,注意很关键的一步是复制过来之后,要调整一下loadURL路径的格式,

像这样:

function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({

})

// and load the index.html of the app.
mainWindow.loadURL(`${__dirname}/index.html`) //--修改的--

接着,复制clone例子的package.json到新建项目的dist目录中。在项目的package.json中(注意不是dist下的package.json)为之前下载好的electron-packager,增加一条启动命令。

 "build": "node build/build.js",
"electron_dev": "npm run build && electron build/electron.js",
"electron_build": "electron-packager ./dist/ --platform=win32 --arch=ia32 --icon=./src/assets/yizhu.ico --overwrite" //--新增的命令--

接着,如果你要替换应用图标的话,就在项目中的scr文件夹下的assets目录下,放入你要设置的exe文件的图标,为.ico格式。

这里指的注意的是,你的ico图标是什么名称,上一条的electron_build里面的路径最后就要改成你图标的名称,像这里的yizhu.ico一样,yizhu.ico就是我自己图标的名称。(这点很重要!)

这里我要强调一点, 有同学到这里运行报错, 很有可能是你的图片路径没改过来. 还有一点就是你把自己的图片强行修改为ico格式了,这点是不允许的. 一定要是原生的ico格式的图标. 且看我最下面截图的ico的图标是怎样的. 这里我附上一个转为ico格式的链接. 操作简单.

最后,运行

npm run build //刷新静态资源文件
npm run electron_build //启动

这个时候已经生成了aps-win32-ia32文件夹,找到里面的helloworld.exe文件即可运行。当然,我这里没有给文件重命名,你们可以自行命名。

到这里,exe文件已经最终完成。

 

go 之 gorm

1、简介

ORM

Object-Relationl Mapping, 它的作用是映射数据库和对象之间的关系,方便我们在实现数据库操作的时候不用去写复杂的 sql 语句,把对数据库的操作上升到对于对象的操作。

特性

  • 全功能 ORM
  • 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
  • Create,Save,Update,Delete,Find 中钩子方法
  • 支持 Preload、Joins 的预加载
  • 事务,嵌套事务,Save Point,Rollback To Saved Point
  • Context、预编译模式、DryRun 模式
  • 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
  • SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询
  • 复合主键,索引,约束
  • Auto Migration
  • 自定义 Logger
  • 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
  • 每个特性都经过了测试的重重考验
  • 开发者友好

2.简单例子

gorm

gorm 就是基于 Go 语言实现的 ORM 库。

类似于 Java 生态里大家听到过的 Mybatis、Hibernate、SpringData 等。

Github

https://github.com/jinzhu/gorm

官方文档

https://gorm.io/

 

安装

go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite

快速入门

package main

import (
  "gorm.io/gorm"
  "gorm.io/driver/sqlite"
)

type Product struct {
  gorm.Model
  Code  string
  Price uint
}

func main() {
  db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
  if err != nil {
    panic("failed to connect database")
  }

  // 迁移 schema
  db.AutoMigrate(&Product{})

  // Create
  db.Create(&Product{Code: "D42", Price: 100})

  // Read
  var product Product
  db.First(&product, 1) // 根据整型主键查找
  db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录

  // Update - 将 product 的 price 更新为 200
  db.Model(&product).Update("Price", 200)
  // Update - 更新多个字段
  db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段
  db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

  // Delete - 删除 product
  db.Delete(&product, 1)
}

BuildAdmin开源后台介绍

使用流行技术栈快速创建商业级后台管理系统-BuildAdmin是基于TP6、Vue3.x、Typescript、Vite、Pinia、Element plus等开源后台系统的CRUD功能介绍。系统自带WEB终端、同时提供Web和Server端、内置全局数据回收站和字段级数据修改保护、自动注册路由、无限子级权限管理等,无需授权即可免费商用,希望能帮助大家实现快速开发。设计数据表,界面立成。一行命令即可直接生成数据表的增删改查代码,大气实用的表格、多达22种表单组件支持、拖拽排序、受权限控制的编辑和删除、支持关联表等等,可为您节省大量开发时间。BuildAdmin内置了一个WEB终端以实现一些理想中的功能,比如:虽然是基于vue3的系统,但你在安装本系统时,并不需要手动执行npm install和npm build命令。且后续本终端将为您提供更多方便、快捷的服务。

🚀 CRUD代码生成

设计数据表,界面立成。一行命令即可直接生成数据表的增删改查代码,大气实用的表格、多达22种表单组件支持、拖拽排序、受权限控制的编辑和删除、支持关联表等等,可为您节省大量开发时间。[ 视频介绍 | 使用文档 ]

💥 内置WEB终端

我们内置了WEB终端以实现一些理想中的功能,比如:虽然是基于vue3的系统,但你在安装本系统时,并不需要手动执行npm installnpm build命令。且后续本终端将为您提供更多方便、快捷的服务。[ 视频介绍 | 使用文档 ]

🔀 前后端分离

web文件夹内包含:干净(不含后端代码)、完整(所有前端代码文件均在此内) 的前端代码文件,对前端开发者友好,且我们正在开发package.json自动维护以及积分制的模板与案例市场功能,作为纯前端开发者,您可以将BAdmin当做学习与资源的社群,本系统可为您准备好案例和模板等所需要的环境,而您只需专注于学习或工作,不需要会任何后端代码! [ 邀您:和我们一起 ]

👍 流行且稳定的技术栈

除了基于TP6前后端分离架构外,我们的Vue3使用了Setup、状态管理使用了Pinia、并使用了TypeScript、Vite、Element plus等可以为你的知识面添砖加瓦的技术栈。[ 相关技术学习文档 ]

✨ 高颜值

提供三种布局模式,其中默认布局使用无边框设计风格,它并没有强行填满屏幕的每一个缝然后使用边框线进行分隔,所有的功能版块,都像是悬浮在屏幕上的,同时又将屏幕空间及其合理的利用了 [ 查看在线演示 ]

🔐 权限验证

可视化的权限管理,然后根据权限动态的注册路由、菜单、页面、按钮(权限节点)、支持无限父子级权限分组、前后端搭配鉴权,自由分派页面和按钮权限 [ 使用文档 ]

Python如何安装cv2模块

通常由俩种方式,一种使用pip,无需选择opencv版本;一种手动选择opencv版本,使用whl文件下载

一. pip自动下载

pip install opencv-python

大多数的情况下,是可以的安装成功CV2,可是有时,这个指令安装的pip会出现CV2版本与python安装的版本,不匹配导致,你安装的opencv不成功,由于国外的网路问题,可以加入 -i 指定国内源

清华:https://pypi.tuna.tsinghua.edu.cn/simple

阿里云:https://mirrors.aliyun.com/pypi/simple/

中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple/

华中理工大学:http://pypi.hustunique.com/

山东理工大学:http://pypi.sdutlinux.org/

二. 下载whl文件,手动选择opencv版本

1.先更新pip

python -m pip install --upgrade pip

2.从清华源中选择对应的opencv版本,中下载其他版本的库,因为根据安装的python 版本下载相应的镜像文件
比如 python 3.5.4 64bit
我下载的是

opencv_python-3.1.0.0-cp35-cp35m-win_amd64.whl

3.下载完成后,在whl文件对应的目录下,cmd 到whl对应文件目录下,执行

pip install opencv_python-3.1.0.0-cp35-cp35m-win_amd64.whl

结语

针对python cv2安装遇到的安装失败问题,提出多种方法,进行实验,本文的方法参考网页的方法,进行汇总。

You-get安装方法及使用教程

you-get爬虫,依赖于Python3.10,可以爬取网页无法下载的视频文件,具体步骤如下:
1,下载Python3.10无脑下一步安装
python语言生态非常繁荣,有大量使用python开发的工具。you-get 只是其中一个。关注我后续会介绍更多好用的 python 工具。
2,新建一个空白文件夹,清空地址栏输入cmd后回车打开“命令指示符”
3,输入以下字符下载you-get模块

pip install you-get

显示 Successfully installed you-get 字样即表示 you-get 安装成功。

这里解释一下,上述命令行 -i 后面的值代表使用清华大学的软件源,下载安装速度会比较快。
4,打开浏览器,复制视频所在地址链接
5,输入“you-get http://视频链接”

you-get https://www.bilibili.com/video/BV1st4y1D771?spm_id_from=333.851.b_62696c695f7265706f72745f64616e6365.7

记住删除地址后面.recommand后缀

另外如果是批量下载一个视频列表,可以在上述命令后加上 ” –playlist”, 例如:

you-get https://www.bilibili.com/video/BV1st4y1D771?spm_id_from=333.851.b_62696c695f7265706f72745f64616e6365.7 --playlist

 

6,下载的问题,you-get 帮你解决了,you-get下载的视频一般挺大,需要一个外置存储设备。我用的是闪迪 500 GB的移动固态硬盘,感觉比移动机械硬盘小巧很多,速度快,而且是 type-c,插我的 Macbook Pro 简直完美,不用转接线。

Protocol Buffers 短暂介绍

protobuf简介

protocolbuffer(以下简称PB)是google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml 进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。

protocol buffer初步使用

下面是一个简单的使用的例子:
首先需要定义一个.proto文件,其中需要定义你希望操作的对象的结构。

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
 
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
 
  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }
 
  repeated PhoneNumber phone = 4;
}

保存为person.proto文件,之后下载protoc编译工具,并解压,使用PB将proto文件生成java类。

protoc.exe --java_out=. person.proto

在指定的java_out目录下就可以生成java对应的类,这里生成了PersonOuterClass.java。将文件放入项目,并引入PB的jar包。

compile group: 'com.google.protobuf', name: 'protobuf-java', version: '3.1.0'

接下来就可以使用PB进行对象的操作了。

public static void main(String[] args) throws Exception{
        //创建对象
        PersonOuterClass.Person p= PersonOuterClass.Person.newBuilder().setName("my_name").setId(2).build();
        System.out.print(p.toString());
        //序列化对象
        FileOutputStream fos=new FileOutputStream("D://person");
        p.writeTo(fos);
        fos.close();
        //反序列化对象
        FileInputStream fis=new FileInputStream("D://person");
        PersonOuterClass.Person pread=PersonOuterClass.Person.parseFrom(fis);
        System.out.println(pread.toString());
        fis.close();
 }

与java原生的serializable接口进行比较

public class JavaPerson implements Serializable{
       public String name;
       public Integer id;
       public String email;
        
       public enum PhoneType{
           MOBILE,HOME,WORK
       }
       public class PhoneNumber{
           public String number;
           public PhoneType type;
       }
       List phone;
        
       public static void main(String[] args) throws Exception{
           JavaPerson jp=new JavaPerson();
           jp.name="my_name";
           jp.id=2;
 
           FileOutputStream fileOut = new FileOutputStream("d://person.ser");
           ObjectOutputStream outStream = new ObjectOutputStream(fileOut);
           outStream.writeObject(jp);
           outStream.close();
           fileOut.close();
 
 
           FileInputStream fileIn =new FileInputStream("d://person.ser");
           ObjectInputStream in = new ObjectInputStream(fileIn);
           jp = (JavaPerson) in.readObject();
           in.close();
           fileIn.close();
       }
   }

运行比较结果:

PB较java默认形式代码更简洁。
循环运行10000次序列化和反序列化后PB比java快约30%。
生成的文件PB有11字节而java有215字节。
PB提供额外的字段校验支持。

SwiftAdmin 极速开发框架

开发环境:Windows服务器版 VScode Apache MySQL5.7 PHP7 – PHP8

基于ThinkPHP Layui 完美契合,在开发上采用最精简最高效的做法去完成业务系统的需求,是一款优秀的中后台极速开发解决方案。

开发初衷


  1. SAPHP框架的开发,主要是为了减少在自己开发过程中的频繁造轮子,并且SAPHP框架主张简单就是高效的原则,所以最简单的东西才是效率最高的,可能你的应用场景很复杂,但是SAPHP足够应付!
  2. 在最开始接触互联网的时候,都是用一些开源的CMS系统制作自己的网站,后期因为扩展和二次开发的问题,导致觉得很多东西并不是那么简单易用,比如后台的很多JS代码封装的不是很好,而且界面可操作性很差,所以自己开发这款框架封装了很多常用的特性,足以满足日常后台的开发需要,在使用的过程中你会发现,SAPHP框架里面用的最多的是属性而不是对象,一是为了在书写HTML标签的时候方便。二是为了和layui本身区分开!这样让你更容易在这个上面进行扩展!
  3. 系统默认从基础控制器继承了增删改查操作。但这种方式并不适合大多数硬性的应用场景和逻辑需求,你可能在后期需要摈弃大多数利用了一键CURD的方法进行重载函数,虽然SAPHP框架里面也有,但框架的设计初衷是为了在易用性和操作性上折中找一个方案来做,当前基于第一个版本的SAPHP框架在这方面的表现还不是特别好。但随着应用场景检验和优化,本框架会逐步的进行完善和提高性能!
  4. 在市面上目前的开源极速开发框架的学习成本略高,想搞一个学习成本极低,但性能不低的框架(CMS系统)!
  5. 想着开发一款底层设计配置和应用分开的系统,这样对于很多小白用户不会在项目已经上线运行中的时候,误操作系统的配置导致数据丢失,错乱的问题。比如有些字段需要手动在数据库进行修改。

侧重点


  • SAPHP的架构和开发更倾向于内容管理系统[CMS]的方向,当然你也可以当中API系统使用
  • 系统默认的缓存机制为redis缓存,所以请确保安装redis扩展和服务器[摒弃操蛋的file缓存吧]
  • 如果你只是需要一个极简的API管理系统,那么建议你删除不需要的模块和菜单项!
  • 会侧重于SEO优化、客户管理、流量管理、蜘蛛池、区块链以及采集方面的应用!!!
  • 坚持偏向于社区版开源的方向,主要由社区共同的爱好者免费开发维护插件!!!
  • SAPHP已经上线插件市场🔧,适用于中小型企业采购付费商业版插件的使用!!
  • 本框架特别适合个人开发者和小型创业公司,找一款真正适合自己的框架不容易,所以先来试试SAPHP吧!

框架优势


  • 代码量最少最精简、逻辑简单清晰
  • 参考官方文档,只需会PHP JS 开箱即用
  • 界面基于ant design设计 [可操作性强]
  • 控制器与栏目管理双鉴权,满足日常大部分需求
  • 前端JavaScript鉴权,后端AUTH类鉴权,减少请求
  • 封装常用组件和快捷属性,小白即可快速二次开发
  • 支持全文索引XS/ElasticSearch轻松支持PB级数据
  • 通用型thinkPHP插件开发架构,可轻松迁移其他插件
  • 代码安全质量高,修复大部分低危、高危代码漏洞
  • 高占比AJAX数据调用,响应速度可媲美前后端分离

集成功能


  • [√] API模块 支持token鉴权,支持细分规则
  • [√] CMS模块 系统内容CMS模块,搭配模板,开箱即用
  • [√] 用户管理 用户是系统操作者,该功能主要完成系统用户配置。
  • [√] 公司管理 设置公司常用信息,前端标签调用
  • [√] 部门管理 配置系统组织机构(部门、小组),树结构展现支持数据权限。
  • [√] 岗位管理 配置系统用户所属担任职务。
  • [√] 菜单管理 配置系统菜单,操作权限,按钮、栏目等权限标识等。
  • [√] 角色管理 角色菜单权限分配、设置角色按机构进行数据范围权限划分。
  • [√] 插件管理 可开发定制属于自己的插件,可安装升级社区插件!!!
  • [√] 导航管理 支持导航定制,小分类导航配置适合SEO
  • [√] 内容管理 系统默认模型数据已完成后端数据录入,可快速二次开发!!!!
  • [√] 广告管理 运营必选功能,获取广告代码自动校验过期时间
  • [√] 数据字典 对系统中经常使用的一些较为固定的数据进行维护,并使用自定义标签交互
  • [√] 操作日志 用户后台操作日志,全局异常、SQL注入等记录
  • [√] TAG过滤 支持违规词、敏感词配置
  • [√] 短信平台 支持阿里云、腾讯云短信发送
  • [√] 附件上传 支持FTP、阿里云、腾讯云OSS附件上传
  • [√] 全文检索 支持XunSearch、ElasticSearch集群PB级全文检索
  • [ ] 服务监控 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
  • [ ] 定时任务 在线(添加、修改、删除)任务调度包含执行结果日志。
  • [ ] 代码生成 前后端代码的生成(php、html、layui、sql)支持一键CRUD 。

Golang框架选型比较: goframe, beego, iris和gin

由于工作需要,这些年来也接触了不少的开发框架,Golang的开发框架比较多,不过基本都是Web”框架”为主。这里稍微打了个引号,因为大部分”框架”从设计和功能定位上来讲,充其量都只能算是一个组件,需要项目使用的话得自己四处再去找找其他的组件,或者自己造轮子。如果用于Web开发,这些”框架”的Web开发能力均已完备,无太大差别,且均是自标准库net/http.Server的二次封装。由于框架众多,这里笔者只选择了几个曾做过技术选型评估、较为熟悉,且目前比较流行和典型的Golang”框架”,从适用于业务项目开发框架的角度,做一个简单的横向比较,以便大家在项目框架选型时做个参考。

评估指标

指标
说明
指标
说明
基本介绍 来源各自官网。
模块化设计 是否支持模块化插拔设计、模块之间低耦合设计,是否可以独立使用其中某部分组件。
模块完善度 框架提供的功能模块是否丰富。模块能否能覆盖日常普遍的开发需求。
使用易用性 易用性不仅仅是值框架好不好用,更多是团队能否在低成本下快速接入,长期来看能否低成本维护。
文档完善性 参考官网提供的介绍资料,包括但不限于:文档、视频、示例、案例资料。同时,本地中文文档支持也是参考项。
工程化完备 是否能够快速接入项目开发,是否提供项目接入规范、设计模式、开发工具链,文档是否完善、源码是否易读、是否便于长期维护。
开发模式 框架适用的开发模式,或者官方推荐的开发模式。
工程规范 项目接入时的开发规范,如目录规范、设计规范、编码规范、命名规范等。
社区活跃 官方与社区沟通是否便捷,问题是否能够快速解答,BUG是否能够快速响应处理。
开发工具链 项目开发时使用到的CLI开发工具,如初始化项目、交叉编译、代码生成、swagger、热编译能力等等。
Web: 性能测试 来源第三方评测 https://github.com/the-benchmarker/web-frameworks 。
Web: 路由冲突处理 存在路由注册冲突时有无良好的解决方案,在业务项目开发中比较常见。
Web: 域名支持 Web路由是否支持域名绑定,甚至多域名的绑定。
Web: 文件服务 Web服务是否提供静态资源的访问能力。
Web: 优雅重启/关闭 Web服务在重启时不会影响请求执行,关闭时会等待正在执行的请求处理完,新请求不再接入。
ORM 框架是否自带ORM组件,ORM组件是业务项目的核心组件。无论是自研还是通过第三方组件引入。
Session 框架是否提供会话管理组件,无论是通用型Session组件,还是仅针对于Web服务的Session组件。
I18N支持 国际化组件支持(常用但非核心组件)。
配置管理 配置管理也是框架需要完备的核心组件能力。
日志组件 日志组件也是框架需要完备的核心组件能力。
数据校验 数据校验也是框架需要完备的核心组件能力。
缓存管理 缓存管理也是框架需要完备的核心组件能力。无论是内存还是Redis,无论是自研还是通过第三方组件引入。
资源打包 支持将依赖的文件资源例如静态资源、配置文件等固定文件编译到可执行文件中。框架组件自动支持资源检索。
链路跟踪 框架是否具备分布式链路跟踪能力,分布式跟踪在微服务架构中是必不可少的能力。
测试框架 框架是否支持单元测试接入,提供单元测试接入规范。无论是使用标准库还是第三方测试框架。
突出优点 比较明显的几点优点。
突出缺点 比较明显的几点缺点。

横向比较

  • 以下部分对比参数涉及评分的部分,满分总共按照10分为标准。
  • 如果标记为”-“的部分,表示不支持或者需要引入第三方插件支持。
  • 以下特性如果官网提供文档则直接提供文档地址,找不到文档但是笔者知道有就会简单标注。
  • 各个”框架”功能特性实现不同,在文档、功能、易用性上存在较大差异,各位朋友可自行查阅链接。

 

GoFrame
Beego
Iris
Gin
比较版本 v1.15.2 v1.12.3 v12.0.2 v1.6.3
项目类型 开源(国内) 开源(国内) 开源(海外) 开源(海外)
开源协议 MIT Apache-2 BSD-3-Clause MIT
框架类型 模块化框架 Web框架 Web”框架” Web”框架”
基本介绍 工程完备、简单易用,模块化、高质量、高性能、企业级开发框架。 最简单易用的企业级Go应用开发框架。 目前发展最快的Go Web框架。提供完整的MVC功能并且面向未来。 一个Go语言写的HTTP Web框架。它提供了Martini风格的API并有更好的性能。
项目地址 github.com/gogf/gf github.com/beego/beego github.com/kataras/iris github.com/gin-gonic/gin
官网地址 goframe.org beego.me iris-go.com gin-gonic.com
模块化设计
模块完善度 10 6 4 2
使用易用性 9 9 9 10
文档完善度 10 8 6 4
工程化完备 10 8 5 1
社区活跃 9 10 9 10
开发模式 模块引入三层架构、MVC MVC MVC
工程规范 分层设计对象设计 项目结构
开发工具链 gf工具链 bee工具链
Web: 性能测试 8 8 8 9
Web: HTTPS HTTPS & TLS 支持 CustomHttpConfiguration 支持
Web: HTTP2 支持 支持
Web: WebSocket WebSocket服务
Web: 分组路由 路由注册-分组路由 Namespace GroupingRoutes
Web: 路由冲突处理
Web: 域名支持 域名绑定
Web: 文件服务 静态文件服务 静态文件处理 ServingStaticFiles
Web: 多端口/实例 多端口监听多实例监听 RunMultipleServiceUsingIris
Web: 优雅重启/关闭 平滑重启特性 热升级 GracefulShutdownOrRestart GracefulRestartOrStop
ORM ORM文档 ORM文档
Session Session Session
I18N支持 I18N I18N Localization
模板引擎 模板引擎 View设计 TemplateRendering HtmlRendering
配置管理 配置管理 参数配置 CustomHttpConfig
日志组件 日志组件 Logging
数据校验 数据校验 表单数据验证 CustomValidators
缓存管理 缓存管理 Cache
资源打包 资源管理 bee工具bale命令
链路跟踪 链路跟踪
测试框架 单元测试 Testing Testing
突出优点 goframe主要以工程化和企业级方向为主,特别是模块化设计和工程化设计思想非常棒。针对业务项目而言,提供了开发规范、项目规范、命名规范、设计模式、开发工具链、丰富的模块、高质量代码和文档,社区活跃。作者也是资深的PHP开发者,PHP转Go的小伙伴会倍感亲切。 beego开源的比较早,最早的一款功能比较全面的Golang开发框架,一直在Golang领域有着比较大的影响力,作者谢大多年组织着国内影响力比较大GopherCN活动。beego有着比较丰富的开发模块、开箱即用,提供了基于MVC设计模式的项目结构、开发工具链,主要定位为Web开发,当然也可以用于非Web项目开发。 iris主要侧重于Web开发,提供了Web开发的一系列功能组件,基于MVC开发模式。iris这一年发展比较快,从一个Web Server的组件,也慢慢朝着beego的设计方向努力。 gin专注于轻量级的Web Server,比较简单,易于理解,路由和中间件设计不错,可以看做替代标准库net/http.Server的路由加强版web server。献给爱造轮子的朋友们。
突出缺点 开源时间较晚,推广过于佛系,目前主要面向国内用户,未推广海外。 起步较早,自谢大创业后,近几年发展较慢。非模块化设计,对第三方重量级模块依赖较多。 号称性能最强,结果平平。非模块化设计。最近两年开始朝beego方向发展,但整体框架能力还不完备,需要加油。 功能简单易用,既是优点,也是缺点。

经验分享

不同的需求场景,存在不同的选择。选择适合的工具,解决适合的问题。

开源不存在孰好孰坏之分,开源作者能够本着开源精神给大家分享技术成果用以学习和使用,这本身就是一件非常不易并且值得称道的事情。

最后,笔者在这里跟大家分享一下自己所在团队的情况,以及在Golang技术栈转型过程中所走的弯路,希望能在框架选型这一环节,能给大家作一定参考。

团队最初痛点

团队转型Golang技术栈的一些背景。主要几点:

  1. 团队后端最初的主要技术栈为PHP,由于业务发展需要,进行微服务改造。第一版微服务采用了PHP+JsonRpc的通信方式。
  2. 随着项目增多,公司也组件了自己的DevOps团队,底层部署转向了Docker+Kubernetes容器架构,并且引入了Golang技术栈。
  3. 由于一些痛点,通过一段时间对PHPGolang的比较,团队决定快速转型Golang技术栈,主要痛点如下:
    1. PHP项目在业务复杂后、项目中后期的开发和维护成本整体偏高。主要原因还是其过高的灵活性,非结构化的变量设计,参差不齐的开发人员素质。
    2. 上云容器化部署后,PHPDevOps效率太低。复杂的Composer版本管理,超大的Docker镜像大小,都影响着DevOps的效率。相比较而言,Golang效率极其高效。
    3. JsonRpc通信协议设计下,接口的扩展性和灵活性很高,但服务之间很难快速确定接口的输入与输出定义,只能根据文档和示例进行对接和维护。由于代码和文档分离,大部分场景下接口文档维护往往滞后于接口变化。随着服务的不断增加,非结构化的通信协议管理使得服务接口的开发和维护成本进一步提高。
    4. JsonRpc的通信协议本质基于HTTP1.x+Json,执行效率过低,算不上真正的微服务通信协议,很难对接上主流的服务治理框架。相比较基于HTTP2.xgRPC协议有着成熟微服务开发框架和服务治理解决方案。
    5. 业务梳理的考量,PHPGolang技术栈的迁移,其实也是一次技术重构的契机,在技术重构的过程中也重新梳理业务系统设计,偿还技术债务。

进一步的痛点

Golang确实足够简单,相比较其他的解释类开发语言,没有过多的语法糖和语言特性,因此团队上手很快,并快速完成了一部分业务系统的技术重构。但随之而来的是更加严重的痛点。主要几点:

  1. 轮子过多:Golang实在太简单了,以至于我们的团队成员爆发了压抑许久的闷骚劲,充分发挥”造后不管”的造轮精神,开发/封装了许多大大小小的轮子。这些轮子均能满足最基本的功能,例如:日志、配置、缓存等等。但轮子并不是实现一个基础功能的半成品就了事,需要保证功能性、稳定性、扩展性和可维护性,要能结合更多生产实践验证,更需要能够长期维护、持续进行迭代改进。否则,就是一堆大小不一的成人玩具。造轮一时爽,维护火葬场。直到现在,我们还在为分散在100多个Golang项目中的数十个成人玩具做大统一的事情痛苦不已。当然,这个问题也跟组织架构和团队管理也有很大关系。
  2. 不成体系:
    1. 我们坚信一个package只做一件事情,并且特地使用单仓库包的形式进行包管理,相当于每个package都是独立维护的git仓库。其实单仓库包package设计并不存在必要性,反而独立的单仓库包提高了组件和框架的维护成本。
    2. 这种单仓库包设计难以形成技术体系,在团队技术管理上,难以形成统一的技术框架。单仓包显得很孤立,而一个技术体系的建立除了需要制定规范和标准,更需要技术框架来准确落地。一个成体系的、统一的技术框架,至少涉及到数十个基础技术组件,不可能完全孤立设计。每一个package的基础功能实现都很简单,但是如何能够统一组织在一起却不是一件简单的事情,这需要团队的技术管理者需要有一定的技术底蕴、格局和前瞻性,而不是和普通开发者那样眼界只能局限于package本身。
    3. 这种孤立的单仓库包设计,对于业务项目的规范化约束不强,每一个组件都可以独立替换,也至于痛点1的问题越发严重(连日志组件都好几套,虽然都满足基本的日志规范设计)。最终,我们最初引以为傲的单仓库包设计,最终变成了一堆散沙。例如,就连需要增加标准化的链路跟踪功能,由于单仓库包过于散乱和不统一,使得推进改进成本极其高昂。
    4. 除了使得技术体系难以建立,技术规范难以准确落地之外,每个组件的易用性也设计得较差。举个简单例子,我们的日志组件、缓存组件、数据库组件、HTTP/gRPC Server组件都需要对接配置管理功能,单仓包需要保证低耦合设计,因此开发者在使用的时候需要先手动读取配置、并转换为目标配置对象、并注入到对应的组件初始化方法中,随后才能将该对象运用到业务逻辑中,若干个业务项目均是重复此步骤。其实goframe在这块的易用性设计就挺不错,每个包当然是独立设计的,在统一的技术框架体系下,再独立提供一个耦合的单例模块将常用的对象进行单例化封装,自动实现配置读取、配置对象转换、配置对象注入及组件对象初始化,开发者仅需要调用一个单例方法即可。而这个常用单例模块,成为了我们技术框架体系的一部分,极大地提高了业务项目的开发和维护效率。
  3. 版本不一致:在业务项目不断增多之后,轮子版本不一致性也越来越明显。什么是版本不一致?举个例子。我们有个轮子叫做httpClient,总共发布了10来个版本;我们总共有100多个Golang项目,几乎每个版本都在使用。我们提交了一个bug fix,却难以让所有项目都能更新。对其他的轮子也是类似的情况,况且我们也有数十个各种轮子,被各个项目独立使用中。

经过反思总结,总结了以下几点:

  1. 团队需要一个统一的技术框架,而不是东拼西凑的一堆单仓库包。
  2. 我们只需要维护一个框架的版本,而不是维护数十个单仓库包的版本。
  3. 框架的组件必须模块化、低耦合设计,保证内部组件也可以独立引用。
  4. 核心组件严格禁止单仓库包设计,并且必须由框架统一维护。

最终的抉择

走过这么多弯路之后,我们决心建立一套成体系的Golang开发框架。除了要求团队能够快速学习,维护成本低,并且我们最主要的诉求,是核心组件不能是半成品,框架必须是上过大规模生产验证的,稳定和成熟的。随着,我们重新对行业中流行的技术框架做了技术评估,包括上面说的那些框架。原本的初衷是想将内部的各个轮子统一做一个成体系的框架,在开源项目中找一些有价值的参考。

后来找到了goframe,仔细评估和学习了框架设计,发现框架设计思想和我们的经验总结如出一则!

这里不得不提一件尴尬的事情。其实最开始转Golang之前(2019年中旬)也做过一些调研,那时goframe版本还不高,并且我们负责评估的团队成员有一种先入为主的思想,看到模块和文档这么多,感觉应该挺复杂,性能应该不高,于是没怎么看就PASS。后来选择一些看起来简单的开源轮子自己做了些二次封装。

这次经过一段时间的仔细调研和源码学习,得出一个结论,goframe框架的框架架构、模块化和工程化设计思想非常棒,执行效率很高,模块不仅丰富,而且质量之高,令人惊叹至极!相比较我们之前写的那些半成品轮子,简直就是小巫见大巫。团队踩了一年多的坑,才发现团队确实需要一个统一的技术框架而不是一堆不成体系的轮子,其实人家早已给了一条明光大道,并且一直在前面默默努力。

经过团队内部的调研和讨论,我们决定使用goframe逐步重构我们的业务项目。由于goframe是模块化设计的,因此我们也可以对一些模块做必要的替换。重构过程比较顺利,基础技术框架的重构并不会对业务逻辑造成什么影响,反而通过goframe的工程化思想和很棒的开发工具链,在统一技术框架后,极大地提高了项目的开发和维护效率,使得团队可以专心于业务开发,部门也陆续有了更多的产出。目前我们已经有大部门业务项目专向了goframe。平台每日流量千万级别。

最后,感谢开源作者们的默默贡献!我们也在努力推动团队秉着来源社区,回馈社区的思想,未来也会更多参与社区贡献。