├── .gitignore ├── LICENSE ├── README.md ├── book ├── book.zip ├── book │ ├── SUMMARY │ │ └── index.html │ ├── chapter001 │ │ └── index.html │ ├── chapter002 │ │ └── index.html │ ├── chapter003 │ │ └── index.html │ ├── chapter004 │ │ └── index.html │ ├── chapter005 │ │ └── index.html │ ├── chapter006 │ │ └── index.html │ ├── chapter007 │ │ └── index.html │ ├── chapter008 │ │ └── index.html │ ├── chapter009 │ │ └── index.html │ ├── chapter010 │ │ └── index.html │ ├── chapter011 │ │ └── index.html │ ├── chapter012 │ │ └── index.html │ ├── chapter013 │ │ └── index.html │ ├── css │ │ ├── highlight.css │ │ ├── theme.css │ │ └── theme_extra.css │ ├── fonts │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ └── fontawesome-webfont.woff │ ├── img │ │ └── favicon.ico │ ├── imgs │ │ ├── chapter002-001.png │ │ ├── chapter003-001.png │ │ ├── chapter003-002.png │ │ ├── chapter003-003.png │ │ ├── chapter003-004.png │ │ ├── image0001.png │ │ ├── image0002.png │ │ ├── twtf_chapter010_001.png │ │ ├── twtf_chapter010_002.png │ │ ├── twtf_chapter010_003.png │ │ ├── twtf_chapter010_004.png │ │ ├── twtf_chapter012_001.png │ │ └── twtf_chapter012_002.png │ ├── index.html │ ├── js │ │ ├── highlight.pack.js │ │ ├── jquery-2.1.1.min.js │ │ ├── modernizr-2.8.3.min.js │ │ └── theme.js │ ├── mkdocs │ │ ├── js │ │ │ ├── lunr.min.js │ │ │ ├── mustache.min.js │ │ │ ├── require.js │ │ │ ├── search-results-template.mustache │ │ │ ├── search.js │ │ │ └── text.js │ │ └── search_index.json │ ├── part001 │ │ └── index.html │ ├── part002 │ │ └── index.html │ ├── part003 │ │ └── index.html │ ├── search.html │ └── sitemap.xml ├── docs │ ├── SUMMARY.md │ ├── chapter001.md │ ├── chapter002.md │ ├── chapter003.md │ ├── chapter004.md │ ├── chapter005.md │ ├── chapter006.md │ ├── chapter007.md │ ├── chapter008.md │ ├── chapter009.md │ ├── chapter010.md │ ├── chapter011.md │ ├── chapter012.md │ ├── chapter013.md │ ├── imgs │ │ ├── chapter002-001.png │ │ ├── chapter003-001.png │ │ ├── chapter003-002.png │ │ ├── chapter003-003.png │ │ ├── chapter003-004.png │ │ ├── image0001.png │ │ ├── image0002.png │ │ ├── twtf_chapter010_001.png │ │ ├── twtf_chapter010_002.png │ │ ├── twtf_chapter010_003.png │ │ ├── twtf_chapter010_004.png │ │ ├── twtf_chapter012_001.png │ │ └── twtf_chapter012_002.png │ ├── index.md │ ├── introduct.md │ ├── part001.md │ ├── part002.md │ └── part003.md └── mkdocs.yml ├── code ├── README.md ├── application │ ├── __init__.py │ ├── controllers │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── todo.py │ │ └── user.py │ ├── extensions.py │ └── models │ │ ├── __init__.py │ │ ├── todo.py │ │ └── user.py ├── commands.py ├── config │ ├── __init__.py │ ├── default.py │ ├── development.py │ ├── development_sample.py │ ├── production.py │ ├── production_sample.py │ └── testing.py ├── deploy │ ├── flask_env.sh │ ├── gunicorn.conf │ ├── nginx.conf │ └── supervisor.conf ├── manage.py ├── pylintrc ├── requirements.txt ├── tests │ ├── __init__.py │ └── todo.py └── wsgi.py ├── cover.png └── introduct.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *.swp 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | venv 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | # PyBuilder 61 | target/ 62 | 63 | #Ipython Notebook 64 | .ipynb_checkpoints 65 | _book/* 66 | 67 | # ide 68 | .idea 69 | 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The Way To Flask 2 | ======= 3 | 4 | ### 本文目标 5 | 6 | 通过讲解 Flask 以及它的扩展们,介绍通用用法以及使用过程中的问题和坑,帮助读者使用 Python 编程语言快速得开发健壮的 Web(API)服务端程序。本书在编写之初以及编写过程中始终坚持以下几条原则: 7 | 8 | - 让 Python 初学者/会其他语言但没用过 Python 的人能快速入手 9 | - 循序渐进得让读者感受 Flask 的简便与强大 10 | - 以生动有趣的语言讲述 Flask 从入门到着迷 11 | 12 | ## Flask 简介 13 | 14 | Flask 是一个使用 Python 编写的轻量级 Web 应用框架,核心的思想就是自身尽可能提供少的东西,作为一个微框架,将更多的内容以插件的形式提供,因此,衍生出了一系列以 Flask 为核心的 插件。 15 | 16 | 截止至 2019 年 10 月 24 日,在 [Github](https://github.com/pallets/flask) 上已有 **47100+** 个星,**13000+** 个 Fork 以及 **2300+** 个 Watch。 17 | 18 | 通过使用 pip 包管理工具统计,Flask 的扩展已经达到 **3,900+**,涵盖大部分日常工作使用到的内容。 19 | 20 | ### 声明 21 | 22 | 本教程由 [Liu Liqiang](https://liqiang.io) 编写,使用 [GNU FDL v1.3 Licence](http://www.gnu.org/licenses/fdl-1.3.html) 发布,如有转载、商业使用等用途,请在 Licence 的约束下进行,本人保留一切权利。 23 | 24 | ### 联系我 25 | 26 | 如果对本书提到的知识点有不解或者觉得有误,可以根据以下联系方式与我联系,同时,欢迎大家一起编撰修改本书,让更多的人能够喜爱 Flask。 27 | 28 | - 主页:[https://liqiang.io](https://liqiang.io) 29 | - 邮箱:liqianglau@outlook.com 30 | - HomePage: [https://liqiang.io/book/the-way-to-flask/](https://liqiang.io/book/the-way-to-flask/) 31 | - GitHub: https://github.com/liuliqiang/the-way-to-flask.git 32 | 33 | ### 更新记录 34 | 35 | ## Version 1.4 36 | 37 | - data: 2019-10-24 38 | - desc: 39 | - 在这个特殊的程序员日子,我更新了一下代码,并且重新部署了一下在线版本 40 | 41 | ## Version 1.3 42 | - begin: 2017-05-01 43 | - end: 2017-05-01 44 | - desc: 转移到 Mkdocs 45 | 46 | ## Version 1.2 47 | - begin: 2017-03-01 48 | - end: 2017-03-01 49 | - desc:修改一些文档的错误 50 | 51 | ## Version 1.1 52 | - date: 2016-6-11 53 | - desc: 在 Pycon2016 上观看了《Flask at Scale》的讲解,对 Flask 的项目有了更多的一些理解,发现了 V1.0 的内容已经符合可维护性的要求,在这个版本中新加入优化性能的部分。 54 | 55 | ## Version 1.0 56 | 57 | - date: 2016-6-2 58 | - desc: 终于在一个多月的时间里完成了第一版,期间发生了很多事情,但是,还是坚持下来了,完成了第一版的《The Way To Flask》,虽然个人觉得还有很大的改进空间,但至少是有这么粗糙的一版,后面有什么问题,可以根据大家的建议进行改进。 59 | -------------------------------------------------------------------------------- /book/book.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book.zip -------------------------------------------------------------------------------- /book/book/SUMMARY/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 目录 - The Way to Flask 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 150 | 151 |
152 | 153 | 154 | 158 | 159 | 160 |
161 |
162 |
163 |
    164 |
  • Docs »
  • 165 | 166 | 167 | 168 |
  • 目录
  • 169 |
  • 170 | 171 |
  • 172 |
173 |
174 |
175 |
176 | 206 |
207 | 226 | 227 |
228 |
229 | 230 |
231 | 232 |
233 | 234 |
235 | 236 | 237 | 238 | 239 | Next » 240 | 241 | 242 |
243 | 244 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /book/book/chapter001/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 本书概述 - The Way to Flask 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 158 | 159 |
160 | 161 | 162 | 166 | 167 | 168 |
169 |
170 |
171 |
    172 |
  • Docs »
  • 173 | 174 | 175 | 176 |
  • 本书概述
  • 177 |
  • 178 | 179 |
  • 180 |
181 |
182 |
183 |
184 |
185 | 186 |

本书概述

187 |

在 Python 中有很多优秀的 Web 开发框架,例如 Django、Flask和 Tornado等等。

188 |

每种框架都有其自身的独特之处,

189 |
    190 |
  • Django 自己集成了丰富的功能,将数据库模块、模板以及后台管理等模块都集成在自身内部,和框架一起打包发布;
  • 191 |
  • 而 Flask 则以最简原则,自身框架只附带很简单的路由、模板功能,而提供了简单的扩展接口,从而将其他的功能都以扩展的形式提供,从而产生了大量的强大的各种扩展,Flask也因此以扩展丰富而受欢迎;
  • 192 |
  • Tornado 则与 Django 和 Flask 走不同的道路,Tornado 的主打功能是异步请求处理,适用于 IO 操作繁多的应用。
  • 193 |
194 |

这个系列文章的主要介绍对象就是 Flask 以及它的插件们,因此对于其他框架也就在上面简约得一言带过,有兴趣的同学可以自行查找资料学习。

195 |

本书的文章顺序主要按照以下的骨架进行介绍:

196 |
    197 |
  • 第一部分讲解 Flask 的基础功能
  • 198 |
  • 第二部分讲解 Flask 的几个重要插件以及注意点
  • 199 |
  • 第三部分将以前面介绍的内容综合起来实践一个 Todo 系统
  • 200 |
201 |

为了让同学们在阅读的时候同时实践可以产生和我讲解出现一样的效果,下面我有必要罗列一下本书中使用到的数据库、Python库的版本等信息。

202 |

数据库

203 |
MongoDB:
204 |     version:3.2.6
205 |     ip:localhost
206 |     port:27017
207 | Redis:
208 |     version:3.0.5
209 |     ip:localhost
210 |     port:6379
211 | 
212 |

Python 依赖库

213 |
Flask==0.10.1
214 | flask-mongoengine==0.7.5
215 | Flask-Login==0.3.2
216 | Flask-Admin==1.4.0
217 | Flask-Redis==0.1.0
218 | Flask-WTF==0.12
219 | 
220 | 221 |
222 |
223 | 244 | 245 |
246 |
247 | 248 |
249 | 250 |
251 | 252 |
253 | 254 | 255 | 256 | « Previous 257 | 258 | 259 | Next » 260 | 261 | 262 |
263 | 264 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /book/book/chapter002/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 简单的 Flask 应用 - The Way to Flask 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 158 | 159 |
160 | 161 | 162 | 166 | 167 | 168 |
169 |
170 |
171 |
    172 |
  • Docs »
  • 173 | 174 | 175 | 176 |
  • 简单的 Flask 应用
  • 177 |
  • 178 | 179 |
  • 180 |
181 |
182 |
183 |
184 |
185 | 186 |

简单的 Flask 应用

187 |

作为本书的第一个示例,也可能是你接触的第一个 Flask 应用,我还是以程序届常规的 Hello World 为例来编写一个非常简单的例子。

188 |

这个例子的功能就是你在浏览器中输入URL:

189 |
http://localhost:5000
190 | 
191 |

然后,你就可以在浏览器中看到:

192 |
Hello World!
193 | 
194 |

Simple Flask App

195 |

首先,我们先来看一个简短的代码

196 |
#!/usr/bin/env python
197 | # encoding: utf-8
198 | from flask import Flask
199 | 
200 | app = Flask(__name__)
201 | 
202 | 
203 | @app.route('/')
204 | def index():
205 |     return "Hello World!"
206 | 
207 | app.run()
208 | 
209 |

将这段代码保存为 app.py,然后再使用 python 运行这个文件:

210 |
python app.py
211 | 
212 |

回车之后,你将会看到类似以下的输出:

213 |
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
214 | 
215 |

如果有其它错误,你可以仔细看看是什么问题,代码和我上面是否一致,还有一个很重要的点就是,你是否已经安装了 Flask ,如果没有的话,那么安装一下:

216 |
pip install Flask==0.10.1
217 | 
218 |

安装完成后,继续使用 Python 运行上述文件:

219 |
python app.py
220 | 
221 |

然后在浏览器上打开以下URL:

222 |
http://localhost:5000
223 | 
224 |

你将会看到这个界面:

225 |

chapter002-001.png

226 |

那就说明你的第一个 Flask 应用已经运行成功了。

227 |

简析第一个应用

228 |

对于你运行成功的第一个程序很简单,我们就做一些简单的分析,让你有一个简单的了解。

229 |

前两行的编码说明就不说了,这是 Python 的特性,而不是 Flask 特有的,如果读者有不懂的话,建议查看 Python 的文件编码说明。

230 |

然后继续看代码,我们的所有代码只有一个 import,就是 Flask,这是 Flask 这个框架的核心,我们把它认为是服务器就可以了,目前不需要多关注:

231 |
from flask import Flask
232 | 
233 |

然后接下去看,解析来一句是初始化了一个 Flask 变量,那么我们就可以认为是创建了一个服务器;需要注意的是这里传递了一个参数 name,我们知道在 Python 中 name 这个变量是表示模块的名称,这个参数对于 Flask 很重要,因为 Flask 会依赖于它去判断从哪里找模板、静态文件

234 |
app = Flask(__name__)
235 | 
236 |

接下来三句目前来说可能有点超出我们的讨论范围,但是我们这里稍微讲解一下好了,这三句中关键是第一句和第三句。

237 |
@app.route('/')
238 | def index():
239 |     return "Hello World!"
240 | 
241 |

第一句中关键的是 '/' 这个参数,这个参数的作用是说下面的这个函数对应于我们在浏览器中输入的地址:

242 |
http://localhost:5000 + 后面的参数
243 | 
244 |

这样说,大家可能不太明白,假设换成:

245 |
@app.route('/hello')
246 | def hello():
247 |     return "hello world"
248 | 
249 |

的话,那么也就表示,我们在浏览器中访问:

250 |
http://localhost:5000/hello
251 | 
252 |

那么 Flask 就会调用到 hello 这个函数。

253 |

那第三句的意思大家可能会比较容易理解了,没错,return 的内容就是我们在浏览器中看到的内容了。我们的代码中 return 的是 "Hello World!",那么我们在浏览器中看到的就是 Hello World!

254 |

到目前为止,我们 import 了服务器(import Flask),创建了服务器(Flask(name)),是时候将服务器运行起来了,是的,最后一句

255 |
app.run()
256 | 
257 |

就是表示将服务器运行起来,接受浏览器的访问。

258 |

那么整个过程就是这样的,当我们在浏览器中输入

259 |
http://localhost:5000
260 | 
261 |

的时候,其实浏览器默默得在我们的 URL 后面加入了一个 /,真实访问的就是

262 |
http://localhost:5000/
263 | 
264 |

其实也就是对应着我们的

265 |
app.route('/')
266 | 
267 |

函数了,这个函数

268 |
return "Hello World!"
269 | 
270 |

所以我们在浏览器中就看到了:

271 |
Hello World!
272 | 
273 | 274 |
275 |
276 | 297 | 298 |
299 |
300 | 301 |
302 | 303 |
304 | 305 |
306 | 307 | 308 | 309 | « Previous 310 | 311 | 312 | Next » 313 | 314 | 315 |
316 | 317 | 318 | 319 | 320 | -------------------------------------------------------------------------------- /book/book/chapter008/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 配置管理 - The Way to Flask 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 162 | 163 |
164 | 165 | 166 | 170 | 171 | 172 |
173 |
174 |
175 |
    176 |
  • Docs »
  • 177 | 178 | 179 | 180 |
  • 配置管理
  • 181 |
  • 182 | 183 |
  • 184 |
185 |
186 |
187 |
188 |
189 | 190 |

建立目录管理配置

191 |

在前面一章《更好得维护代码》 中,我们将项目按照功能作用划分到不同的目录中,这样使得我们的项目结构更加清晰和规整了。但是,因为上一章节的内容比较多,如果作为初学者来说,肯定是有好多有疑问的地方,从本章开始都会进行介绍,让大家对 Flask 的使用更加得得心应手。

192 |

本章主要介绍的是 Flask 中的配置管理,从前面章节《更好得维护代码》 里,可以发现,配置目录 config 下包含多个配置文件,为什么要包含这么多文件,而我们要如何处理这些配置文件,都是本章的讲解内容。

193 |
├── config
194 | │   ├── __init__.py
195 | │   ├── default.py
196 | │   ├── development.py
197 | │   ├── development_sample.py
198 | │   ├── production.py
199 | │   ├── production_sample.py
200 | │   └── testing.py
201 | 
202 |

环境分类

203 |

有一个重要的概念是需要我们关注的,那就是每个配置文件都是与环境相关的,也就是说,就是因为有多个环境,所以才会出现多个配置。如果不太理解这句话的意思的话,我们看一下 config 目录下的文件名,其实可以分为几类:

204 |
    205 |
  • development: 开发环境,一般为本地开发环境使用
  • 206 |
  • production:生产环境,一般为线上部署运行环境使用
  • 207 |
  • testing: 测试环境,一般用于各种测试使用
  • 208 |
209 |

如我们所看到的一样,我们在平时的工作中会有各种环境,我们在本地开发调试的时候应该有个本地环境,当我们转测试之后后会有个测试环境,测试完成之后放到线上之后会有个线上正式环境,而每个环境很难保持配置完全一致,所谓的不一致是指例如数据库信息、程序运行的模型等,例如我们本地的开发环境数据库地址是:

210 |
MongoDB:
211 | version:3.2.6
212 | ip:localhost
213 | port:27017
214 | 
215 |

而在生产环境却是:

216 |
MongoDB:
217 |     version:3.2.6
218 |     ip:192.168.59.104
219 |     port:27017
220 | 
221 |

所以为了方便开发、测试和部署,我们就会设置多份配置文件,这样就可以快速得在不同环境中运行。如果你有其他的情况,可以随时添加配置文件,完全没问题。

222 |

加载配置

223 |

那这么多份配置文件,我如何让程序制定加载哪份配置文件呢?这里的奥妙就在 config/__init__.py 文件中。我们打开这个文件看看:

224 |
# coding: UTF-8
225 | import os
226 | 
227 | 
228 | def load_config(mode=os.environ.get('MODE')):
229 |     """Load config."""
230 |     try:
231 |         if mode == 'PRODUCTION':
232 |             from .production import ProductionConfig
233 |             return ProductionConfig
234 |         elif mode == 'TESTING':
235 |             from .testing import TestingConfig
236 |             return TestingConfig
237 |         else:
238 |             from .development import DevelopmentConfig
239 |             return DevelopmentConfig
240 |     except ImportError:
241 |         from .default import Config
242 |         return Config
243 | 
244 |

在 config/init.py 文件中,我定义了一个 load_config 函数,这个函数接受一个 mode 参数,表示是获取什么环境的配置,如果不传这个参数的话,那默认使用的就是系统环境变量中的 MODE 环境变量,然后就根据指定的环境返回指定的配置文件。

245 |

如果没有指定的配置文件的话,那么就只能返回默认环境变量了。同样的,如果你需要新增自定义的环境配置文件,那么只需要简单得修改这个函数,并且指定加载你自定义的配置文件即可。

246 |

使用配置

247 |

加载配置这一问题解决之后,接下来就是在我们的 Flask 应用中使用这些配置了,既然都 load 好了配置,那么使用也就问题不大了,这里是一个示例:

248 |
"""Create Flask app."""
249 | config = load_config(mode)
250 | 
251 | app = Flask(__name__)
252 | app.config.from_object(config)
253 | 
254 |

这里首先将配置 load 出来,然后使用 Flask 对象的 config.from_object 设置配置。就这么简单。

255 |

总结

256 |

本章对 Flask 中如何配置多环境的配置文件进行了说明和介绍,然后分析了如何加载不同配置文件的原理,最后,给出了一个如何在实际应用中使用配置的示例。

257 | 258 |
259 |
260 | 281 | 282 |
283 |
284 | 285 |
286 | 287 |
288 | 289 |
290 | 291 | 292 | 293 | « Previous 294 | 295 | 296 | Next » 297 | 298 | 299 |
300 | 301 | 302 | 303 | 304 | -------------------------------------------------------------------------------- /book/book/chapter010/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 管理数据库数据 - The Way to Flask 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 164 | 165 |
166 | 167 | 168 | 172 | 173 | 174 |
175 |
176 |
177 |
    178 |
  • Docs »
  • 179 | 180 | 181 | 182 |
  • 管理数据库数据
  • 183 |
  • 184 | 185 |
  • 186 |
187 |
188 |
189 |
190 |
191 | 192 |

使用 Flask-Admin 管理数据库数据

193 |

我们回过头来看看我们到目前为止的 REST API,发现好像现在都不知道有多少条 User 记录了,甚至于连获取所有 User 记录的 API 都没提供,更别说随便查看用户的记录了。面对这个困境, Flask 的扩展是否还能给我们更多的惊喜呢?答案肯定还是可以的。这一章节,我将带读者认识一个 Flask 中的管理扩展 —— Flask-Admin。

194 |

使用 Flask-Admin,我们可以方便快捷得管理我们的 Model 数据,让我们能够省去一大堆开发管理系统的时间,而更多得将精力放到梳理业务逻辑之上,下面就开始讲解一下如何使用 Flask-Admin。

195 |

安装 Flask-Admin

196 |

没有什么特殊的,还是直接使用 pip 安装:

197 |
pip install Flask-Admin==1.4.0
198 | 
199 |

就直接安装上了 Flask-Admin 扩展,然后等待后续使用

200 |

初始化 Flsak-Admin

201 |

和其他常见扩展一般,Flask-Admin 还是需要和我们的 app 服务器绑定,所以还是老套路,但是,因为我们规范化了我们的目录结构,所以这里我们需要注意的是,创建 Flask-Admin 对象要放在 application/extensions.py 文件中,所以在我们的 application/extensions.py 中已经写入以下语句:

202 |
from flask.ext.admin import Admin
203 | admin = Admin()
204 | 
205 |

接下来就是要和我们的 Flask 服务器进行绑定了,还是老套路,不过还是因为规范化的原因,我们的绑定需要放到 application/__init__.py 中执行,那就需要在 application/__init__.py 文件中的 register_extensions 函数中添加以下语句:

206 |
from application.extensions import admin
207 | 
208 | admin.init_app(app)
209 | 
210 |

然后就算完成了,接下来,我们运行服务器试试,此时我们运行服务器是使用以下语句了:

211 |
python manage.py runserver
212 | 
213 |

初见 Flask-Admin

214 |

当我们服务器跑起来之后,我们要想看到管理界面,只需要在浏览器中输入URL:

215 |
http://localhost:5000/admin
216 | 
217 |

你就能看到最简单的管理后台了,但是!!这里面什么都没有,就像这样:

218 |

twtf_chapter010_001.png

219 |

看来第一印象不是很好,那么,我们要怎样才能看到东西?其实也不复杂,既然没有数据,那我们就将我们的数据 Model 加进去,怎么加,下面给出一个简单的例子,同样还是在 application/init.py 中:

220 |
from flask_admin.contrib.mongoengine import ModelView
221 | 
222 | from application.models import User, Role
223 | 
224 | def register_extensions(app):
225 |     admin.init_app(app)
226 |     admin.add_view(ModelView(User))
227 |     admin.add_view(ModelView(Role))
228 | 
229 |

这样就可以了,还是那样,重启一下我们的服务器再访问:

230 |
http://localhost:5000/admin
231 | 
232 |

这个时候,我们会发现有两个 Model 了:

233 |

twtf_chapter010_002.png

234 |

操作 Flask-Admin

235 |

在后台中我们可以看到一些选项,例如List、Create、With select,然后这些选项下面是一个表格,也许你会发现这个表格是空的,那是因为你的数据库中没有数据,所以是空的很自然,那么我们要怎么添加数据呢?试试点一下 “Create” 看下:

236 |

twtf_chapter010_003.png

237 |

看到的是这个,我们填充完各个字段之后,点提交就能看到表格中有数据了。

238 |

twtf_chapter010_004.png

239 |

总结

240 |

本章很简约得介绍了 Flask 中的管理扩展 Flask-Admin,并且演示了如何添加管理我们的 Model 数据,并且简单得介绍了一下支持的操作,但是这些都只是皮毛,如果读者有兴趣的话,可以阅读我的文章《Flask-Admin》了解更多知识,也可以查看 Flask-Admin 的官方文档学习关于 Flask-Admin 的内容。

241 | 242 |
243 |
244 | 265 | 266 |
267 |
268 | 269 |
270 | 271 |
272 | 273 |
274 | 275 | 276 | 277 | « Previous 278 | 279 | 280 | Next » 281 | 282 | 283 |
284 | 285 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /book/book/chapter012/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 编写 TODO 应用【part2】 - The Way to Flask 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 162 | 163 |
164 | 165 | 166 | 170 | 171 | 172 |
173 |
174 |
175 |
    176 |
  • Docs »
  • 177 | 178 | 179 | 180 |
  • 编写 TODO 应用【part2】
  • 181 |
  • 182 | 183 |
  • 184 |
185 |
186 |
187 |
188 |
189 | 190 |

编写 TODO 应用【part002】

191 |

设置配置

192 |

配置的话,我们全放在 config 目录下,并且按环境划分,因为只使用到开发环境,所以就只设置了开发环境的:

193 |

config/init.py

194 |
    # coding: UTF-8
195 |     import os
196 | 
197 | 
198 |     def load_config(mode=os.environ.get('MODE')):
199 |         """Load config."""
200 |         try:
201 |             if mode == 'PRODUCTION':
202 |                 from .production import ProductionConfig
203 |                 return ProductionConfig
204 |             elif mode == 'TESTING':
205 |                 from .testing import TestingConfig
206 |                 return TestingConfig
207 |             else:
208 |                 from .development import DevelopmentConfig
209 |                 return DevelopmentConfig
210 |         except ImportError:
211 |             from .default import Config
212 |             return Config
213 | 
214 | config/development.py
215 | 
216 |     # coding: utf-8
217 |     import os
218 | 
219 | 
220 |     class DevelopmentConfig(object):
221 |         """Base config class."""
222 |         # Flask app config
223 |         DEBUG = False
224 |         TESTING = False
225 |         SECRET_KEY = "sample_key"
226 | 
227 |         # Root path of project
228 |         PROJECT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
229 | 
230 |         # Site domain
231 |         SITE_TITLE = "twtf"
232 |         SITE_DOMAIN = "http://localhost:8080"
233 | 
234 |         # MongoEngine config
235 |         MONGODB_SETTINGS = {
236 |             'db': 'the_way_to_flask',
237 |             'host': '192.168.59.103',
238 |             'port': 27017
239 |         }
240 | 
241 | ## 配置运行脚本
242 | 
243 | 到此,我们的应用代码算是写完了,然后就是运行服务器了,还是使用 Flask-Script,所以我们需要配置 manage.py,内容为;
244 | 
245 | manage.py
246 | 
247 | #!/usr/bin/env python
248 | # encoding: utf-8
249 | from flask_script import Manager
250 | from flask_script.commands import ShowUrls
251 | 
252 | from application import create_app
253 | 
254 | manager = Manager(create_app)
255 | manager.add_option('-c', '--config', dest='mode', required=False)
256 | 
257 | manager.add_command("showurls", ShowUrls())
258 | 
259 | if __name__ == "__main__":
260 |     manager.run()
261 | 
262 |

运行服务器

263 |
pyhton manage.py -c development runserver
264 | 
265 |

当你看到以下语句的时候说明你的服务器运行成功了:

266 |
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
267 | 
268 |

添加用户

269 |

因为现在数据库中是没有用户的,所以我们需要手动添加一个用户先,在管理后台可以添加:

270 |
http://localhost:5000/admin/user/
271 | 
272 |

273 |

274 |

测试功能:

275 |

登录:

276 |
POST /auth/login HTTP/1.1
277 | Host: localhost:5000
278 | 
279 | {"username": "zhangsan",
280 |  "password": "password"}
281 | 
282 |

响应应该是:

283 |
{
284 |   "email": "zhangsan@gmail.com",
285 |   "name": "zhangsan",
286 |   "role": "ADMIN"
287 | }
288 | 
289 | 290 |
291 |
292 | 313 | 314 |
315 |
316 | 317 |
318 | 319 |
320 | 321 |
322 | 323 | 324 | 325 | « Previous 326 | 327 | 328 | Next » 329 | 330 | 331 |
332 | 333 | 334 | 335 | 336 | -------------------------------------------------------------------------------- /book/book/chapter013/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 使用 Gunicorn 和 Nginx 部署项目 - The Way to Flask 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 164 | 165 |
166 | 167 | 168 | 172 | 173 | 174 |
175 |
176 |
177 |
    178 |
  • Docs »
  • 179 | 180 | 181 | 182 |
  • 使用 Gunicorn 和 Nginx 部署项目
  • 183 |
  • 184 | 185 |
  • 186 |
187 |
188 |
189 |
190 |
191 | 192 |

使用 Gunicorn 和 Nginx 部署 Flask 项目

193 |

在实际的生产环境中,我们很少是直接使用命令:

194 |
python app.py
195 | 
196 |

运行 Flask 应用提供服务的,正常都会集成 WSGI Web服务器提供服务,而在众多的 WSGI Web 服务器中,比较常用的主要有两种,分别是 Gunicorn 和 UWSGI,同时,我们也会使用 Nginx 作为反向代理进行部署应用。

197 |

本文因为需要安装 Nginx,所以文章内的命令和使用的系统相关,但是这样的命令不多,本文使用的 Ubuntu 16.04,因此包管理软件是 apt,如果使用的 RedHat 系列的话,那完全可以用 yum 代替。其他系列的系统可以查找相关文档寻找代替管理工具。

198 |

安装组件

199 |
sudo apt-get update
200 | sudo apt-get install python-pip python-dev nginx
201 | 
202 | pip install gunicorn 
203 | pip install flask
204 | 
205 |

这里前两句是更新一下软件源,并且保证我们的 pip 和 python 依赖库已经安装上了,同时,别忘了安装反向代理 Nginx。后面两句就是安装我们必备的 Gunicorn 和 Flask Python 库了。

206 |

下载代码

207 |

因为在我们的前文中已经写了一个代码了,所以这里就继续使用这段代码,使用方式是:

208 |
git clone git@github.com:luke0922/the-way-to-flask.git
209 | cd the-way-to-flask/code
210 | pip install -r requirements.txt
211 | python manage.py runserver
212 | 
213 |

此时,我们的服务器应该是已经运行起来了,但是,默认 Ubuntu 是开启了防火墙屏蔽所有端口访问的,所以我们可能需要打开防火墙端口,在 Ubuntu 16.04 中可以这样做:

214 |
sudo ufw allow 5000
215 | 
216 |

现在,应该可以访问我们的应用了,在命令行上我们敲一下这个命令,访问以下 WEB 服务:

217 |
http://localhost:5000
218 | 
219 |

一切正常的话,

220 |

创建 WSGI 切入点

221 |
vim wsgi.py
222 | 
223 |

里面内容填:

224 |
from myproject import app
225 | 
226 | if __name__ == "__main__":
227 |     app.run()
228 | 
229 |

然后使用这个命令运行代码:

230 |
gunicorn --bind 0.0.0.0:5000 wsgi:app
231 | 
232 |

依旧访问这个地址看看:

233 |
http://localhost:5000
234 | 
235 |

常见 systemd Unit File

236 |
vim /etc/systemd/system/app.service
237 | 
238 |

里面的内容写:

239 |
[Unit]
240 | Description=Gunicorn instance to serve myproject
241 | After=network.target
242 | 
243 | [Service]
244 | User=www
245 | Group=www
246 | WorkingDirectory=/home/www/myproject
247 | Environment="PATH=/home/www/myproject/myprojectenv/bin"
248 | ExecStart=/home/www/myproject/myprojectenv/bin/gunicorn --workers 3 --bind unix:myproject.sock -m 007 wsgi:app
249 | 
250 | [Install]
251 | WantedBy=multi-user.target
252 | 
253 |

保存退出,然后尝试一下命令:

254 |
sudo systemctl start app
255 | sudo systemctl enable app
256 | 
257 |

配置 Nginx

258 |

配置Nginx

259 |
sudo nano /etc/nginx/sites-available/myproject
260 | 
261 |

里面写:

262 |
server {
263 |     listen 80;
264 |     server_name server_domain_or_IP;
265 | 
266 |      location / {
267 |         include proxy_params;
268 |         proxy_pass http://unix:/home/sammy/myproject/myproject.sock;
269 |     }
270 | }
271 | 
272 |

保存之后,用 nginx 自带工具验证一遍

273 |
nginx -t
274 | 
275 |

如果ok的话然后让 nginx 重新加载配置

276 |
nginx -s reload
277 | 
278 |

关闭服务器端口:

279 |
    280 |
  • sudo ufw delete allow 5000
  • 281 |
  • sudo ufw allow 'Nginx Full'
  • 282 |
283 |

此时访问服务器试试:

284 |
http://192.168.59.103
285 | 
286 | 287 |
288 |
289 | 308 | 309 |
310 |
311 | 312 |
313 | 314 |
315 | 316 |
317 | 318 | 319 | 320 | « Previous 321 | 322 | 323 | 324 |
325 | 326 | 327 | 328 | 329 | -------------------------------------------------------------------------------- /book/book/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* 2 | This is the GitHub theme for highlight.js 3 | 4 | github.com style (c) Vasily Polovnyov 5 | 6 | */ 7 | 8 | .hljs { 9 | display: block; 10 | overflow-x: auto; 11 | color: #333; 12 | -webkit-text-size-adjust: none; 13 | } 14 | 15 | .hljs-comment, 16 | .diff .hljs-header, 17 | .hljs-javadoc { 18 | color: #998; 19 | font-style: italic; 20 | } 21 | 22 | .hljs-keyword, 23 | .css .rule .hljs-keyword, 24 | .hljs-winutils, 25 | .nginx .hljs-title, 26 | .hljs-subst, 27 | .hljs-request, 28 | .hljs-status { 29 | color: #333; 30 | font-weight: bold; 31 | } 32 | 33 | .hljs-number, 34 | .hljs-hexcolor, 35 | .ruby .hljs-constant { 36 | color: #008080; 37 | } 38 | 39 | .hljs-string, 40 | .hljs-tag .hljs-value, 41 | .hljs-phpdoc, 42 | .hljs-dartdoc, 43 | .tex .hljs-formula { 44 | color: #d14; 45 | } 46 | 47 | .hljs-title, 48 | .hljs-id, 49 | .scss .hljs-preprocessor { 50 | color: #900; 51 | font-weight: bold; 52 | } 53 | 54 | .hljs-list .hljs-keyword, 55 | .hljs-subst { 56 | font-weight: normal; 57 | } 58 | 59 | .hljs-class .hljs-title, 60 | .hljs-type, 61 | .vhdl .hljs-literal, 62 | .tex .hljs-command { 63 | color: #458; 64 | font-weight: bold; 65 | } 66 | 67 | .hljs-tag, 68 | .hljs-tag .hljs-title, 69 | .hljs-rule .hljs-property, 70 | .django .hljs-tag .hljs-keyword { 71 | color: #000080; 72 | font-weight: normal; 73 | } 74 | 75 | .hljs-attribute, 76 | .hljs-variable, 77 | .lisp .hljs-body, 78 | .hljs-name { 79 | color: #008080; 80 | } 81 | 82 | .hljs-regexp { 83 | color: #009926; 84 | } 85 | 86 | .hljs-symbol, 87 | .ruby .hljs-symbol .hljs-string, 88 | .lisp .hljs-keyword, 89 | .clojure .hljs-keyword, 90 | .scheme .hljs-keyword, 91 | .tex .hljs-special, 92 | .hljs-prompt { 93 | color: #990073; 94 | } 95 | 96 | .hljs-built_in { 97 | color: #0086b3; 98 | } 99 | 100 | .hljs-preprocessor, 101 | .hljs-pragma, 102 | .hljs-pi, 103 | .hljs-doctype, 104 | .hljs-shebang, 105 | .hljs-cdata { 106 | color: #999; 107 | font-weight: bold; 108 | } 109 | 110 | .hljs-deletion { 111 | background: #fdd; 112 | } 113 | 114 | .hljs-addition { 115 | background: #dfd; 116 | } 117 | 118 | .diff .hljs-change { 119 | background: #0086b3; 120 | } 121 | 122 | .hljs-chunk { 123 | color: #aaa; 124 | } 125 | -------------------------------------------------------------------------------- /book/book/css/theme_extra.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Sphinx doesn't have support for section dividers like we do in 3 | * MkDocs, this styles the section titles in the nav 4 | * 5 | * https://github.com/mkdocs/mkdocs/issues/175 6 | */ 7 | .wy-menu-vertical span { 8 | line-height: 18px; 9 | padding: 0.4045em 1.618em; 10 | display: block; 11 | position: relative; 12 | font-size: 90%; 13 | color: #838383; 14 | } 15 | 16 | .wy-menu-vertical .subnav a { 17 | padding: 0.4045em 2.427em; 18 | } 19 | 20 | /* 21 | * Long navigations run off the bottom of the screen as the nav 22 | * area doesn't scroll. 23 | * 24 | * https://github.com/mkdocs/mkdocs/pull/202 25 | * 26 | * Builds upon pull 202 https://github.com/mkdocs/mkdocs/pull/202 27 | * to make toc scrollbar end before navigations buttons to not be overlapping. 28 | */ 29 | .wy-nav-side { 30 | height: calc(100% - 45px); 31 | overflow-y: auto; 32 | min-height: 0; 33 | } 34 | 35 | .rst-versions{ 36 | border-top: 0; 37 | height: 45px; 38 | } 39 | 40 | @media screen and (max-width: 768px) { 41 | .wy-nav-side { 42 | height: 100%; 43 | } 44 | } 45 | 46 | /* 47 | * readthedocs theme hides nav items when the window height is 48 | * too small to contain them. 49 | * 50 | * https://github.com/mkdocs/mkdocs/issues/#348 51 | */ 52 | .wy-menu-vertical ul { 53 | margin-bottom: 2em; 54 | } 55 | 56 | /* 57 | * Wrap inline code samples otherwise they shoot of the side and 58 | * can't be read at all. 59 | * 60 | * https://github.com/mkdocs/mkdocs/issues/313 61 | * https://github.com/mkdocs/mkdocs/issues/233 62 | * https://github.com/mkdocs/mkdocs/issues/834 63 | */ 64 | code { 65 | white-space: pre-wrap; 66 | word-wrap: break-word; 67 | padding: 2px 5px; 68 | } 69 | 70 | /** 71 | * Make code blocks display as blocks and give them the appropriate 72 | * font size and padding. 73 | * 74 | * https://github.com/mkdocs/mkdocs/issues/855 75 | * https://github.com/mkdocs/mkdocs/issues/834 76 | * https://github.com/mkdocs/mkdocs/issues/233 77 | */ 78 | pre code { 79 | white-space: pre; 80 | word-wrap: normal; 81 | display: block; 82 | padding: 12px; 83 | font-size: 12px; 84 | } 85 | 86 | /* 87 | * Fix link colors when the link text is inline code. 88 | * 89 | * https://github.com/mkdocs/mkdocs/issues/718 90 | */ 91 | a code { 92 | color: #2980B9; 93 | } 94 | a:hover code { 95 | color: #3091d1; 96 | } 97 | a:visited code { 98 | color: #9B59B6; 99 | } 100 | 101 | /* 102 | * The CSS classes from highlight.js seem to clash with the 103 | * ReadTheDocs theme causing some code to be incorrectly made 104 | * bold and italic. 105 | * 106 | * https://github.com/mkdocs/mkdocs/issues/411 107 | */ 108 | pre .cs, pre .c { 109 | font-weight: inherit; 110 | font-style: inherit; 111 | } 112 | 113 | /* 114 | * Fix some issues with the theme and non-highlighted code 115 | * samples. Without and highlighting styles attached the 116 | * formatting is broken. 117 | * 118 | * https://github.com/mkdocs/mkdocs/issues/319 119 | */ 120 | .no-highlight { 121 | display: block; 122 | padding: 0.5em; 123 | color: #333; 124 | } 125 | 126 | 127 | /* 128 | * Additions specific to the search functionality provided by MkDocs 129 | */ 130 | 131 | .search-results article { 132 | margin-top: 23px; 133 | border-top: 1px solid #E1E4E5; 134 | padding-top: 24px; 135 | } 136 | 137 | .search-results article:first-child { 138 | border-top: none; 139 | } 140 | 141 | form .search-query { 142 | width: 100%; 143 | border-radius: 50px; 144 | padding: 6px 12px; /* csslint allow: box-model */ 145 | border-color: #D1D4D5; 146 | } 147 | 148 | .wy-menu-vertical li ul { 149 | display: inherit; 150 | } 151 | 152 | .wy-menu-vertical li ul.subnav ul.subnav{ 153 | padding-left: 1em; 154 | } 155 | 156 | .wy-menu-vertical .subnav li.current > a { 157 | padding-left: 2.42em; 158 | } 159 | .wy-menu-vertical .subnav li.current > ul li a { 160 | padding-left: 3.23em; 161 | } 162 | 163 | /* 164 | * Improve inline code blocks within admonitions. 165 | * 166 | * https://github.com/mkdocs/mkdocs/issues/656 167 | */ 168 | .admonition code { 169 | color: #404040; 170 | border: 1px solid #c7c9cb; 171 | border: 1px solid rgba(0, 0, 0, 0.2); 172 | background: #f8fbfd; 173 | background: rgba(255, 255, 255, 0.7); 174 | } 175 | 176 | /* 177 | * Account for wide tables which go off the side. 178 | * Override borders to avoid wierdness on narrow tables. 179 | * 180 | * https://github.com/mkdocs/mkdocs/issues/834 181 | * https://github.com/mkdocs/mkdocs/pull/1034 182 | */ 183 | .rst-content .section .docutils { 184 | width: 100%; 185 | overflow: auto; 186 | display: block; 187 | border: none; 188 | } 189 | 190 | td, th { 191 | border: 1px solid #e1e4e5 !important; /* csslint allow: important */ 192 | border-collapse: collapse; 193 | } 194 | -------------------------------------------------------------------------------- /book/book/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /book/book/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /book/book/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /book/book/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/img/favicon.ico -------------------------------------------------------------------------------- /book/book/imgs/chapter002-001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/imgs/chapter002-001.png -------------------------------------------------------------------------------- /book/book/imgs/chapter003-001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/imgs/chapter003-001.png -------------------------------------------------------------------------------- /book/book/imgs/chapter003-002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/imgs/chapter003-002.png -------------------------------------------------------------------------------- /book/book/imgs/chapter003-003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/imgs/chapter003-003.png -------------------------------------------------------------------------------- /book/book/imgs/chapter003-004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/imgs/chapter003-004.png -------------------------------------------------------------------------------- /book/book/imgs/image0001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/imgs/image0001.png -------------------------------------------------------------------------------- /book/book/imgs/image0002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/imgs/image0002.png -------------------------------------------------------------------------------- /book/book/imgs/twtf_chapter010_001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/imgs/twtf_chapter010_001.png -------------------------------------------------------------------------------- /book/book/imgs/twtf_chapter010_002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/imgs/twtf_chapter010_002.png -------------------------------------------------------------------------------- /book/book/imgs/twtf_chapter010_003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/imgs/twtf_chapter010_003.png -------------------------------------------------------------------------------- /book/book/imgs/twtf_chapter010_004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/imgs/twtf_chapter010_004.png -------------------------------------------------------------------------------- /book/book/imgs/twtf_chapter012_001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/imgs/twtf_chapter012_001.png -------------------------------------------------------------------------------- /book/book/imgs/twtf_chapter012_002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/book/imgs/twtf_chapter012_002.png -------------------------------------------------------------------------------- /book/book/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 前言 - The Way to Flask 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 166 | 167 |
168 | 169 | 170 | 174 | 175 | 176 |
177 |
178 |
179 |
    180 |
  • Docs »
  • 181 | 182 | 183 | 184 |
  • 前言
  • 185 |
  • 186 | 187 |
  • 188 |
189 |
190 |
191 |
192 |
193 | 194 |

The Way To Flask

195 |

本文目标

196 |

通过讲解 Flask 以及它的扩展们,介绍通用用法以及使用过程中的问题和坑,帮助读者使用 Python 编程语言快速得开发健壮的 Web(API)服务端程序。本书在编写之初以及编写过程中始终坚持以下几条原则:

197 |
    198 |
  • 让 Python 初学者/会其他语言但没用过 Python 的人能快速入手
  • 199 |
  • 循序渐进得让读者感受 Flask 的简便与强大
  • 200 |
  • 以生动有趣的语言讲述 Flask 从入门到着迷
  • 201 |
202 |

Flask 简介

203 |

Flask 是一个使用 Python 编写的轻量级 Web 应用框架,核心的思想就是自身尽可能提供少的东西,作为一个微框架,将更多的内容以插件的形式提供,因此,衍生出了一系列以 Flask 为核心的 插件。截止至2016年06月02日,在 Github 上已有 20730 个星,6426 个 Fork 以及 1511 个 Watch。

204 |

通过使用 pip 包管理工具统计,Flask 的扩展已经达到 800+,涵盖大部分日常工作使用到的内容。

205 |

声明

206 |

本文由 Yetship 编写,使用 GNU FDL v1.3 Licence 发布,如有转载、商业使用等用途,请在 Licence 的约束下进行,本人保留一切权利。

207 |

联系我

208 |

如果对本书提到的知识点有不解或者觉得有误,可以根据以下联系方式与我联系,同时,欢迎大家一起编撰修改本书,让更多的人能够喜爱 Flask。

209 |
    210 |
  • 主页:https://liuliqiang.info
  • 211 |
  • 邮箱:liqianglau@outlook.com
  • 212 |
  • Gitbook: https://luke0922.gitbooks.io/the-way-to-flask/content/
  • 213 |
  • GitHub: https://github.com/luke0922/the-way-to-flask.git
  • 214 |
215 |

更新记录

216 |

Version 1.0

217 |
    218 |
  • date: 2016-6-2
  • 219 |
  • desc: 终于在一个多月的时间里完成了第一版,期间发生了很多事情,但是,还是坚持下来了,完成了第一版的《The Way To Flask》,虽然个人觉得还有很大的改进空间,但至少是有这么粗糙的一版,后面有什么问题,可以根据大家的建议进行改进。
  • 220 |
221 |

Version 1.1

222 |
    223 |
  • date: 2016-6-11
  • 224 |
  • desc: 在 Pycon2016 上观看了《Flask at Scale》的讲解,对 Flask 的项目有了更多的一些理解,发现了 V1.0 的内容已经符合可维护性的要求,在这个版本中新加入优化性能的部分。
  • 225 |
226 |

Version 1.2

227 |
    228 |
  • begin: 2017-03-01
  • 229 |
  • end: 2017-03-01
  • 230 |
  • desc:修改一些文档的错误
  • 231 |
232 |

Version1.3

233 |
    234 |
  • begin: 2017-05-01
  • 235 |
  • end: 2017-05-01
  • 236 |
  • desc:使用 mkdocs 重构文档
  • 237 |
238 | 239 |
240 |
241 |
242 | 243 | 251 | 252 | 253 |
254 | 255 |
256 | 257 | 258 |
259 | 260 | Built with MkDocs using a theme provided by Read the Docs. 261 |
262 | 263 |
264 |
265 | 266 |
267 | 268 |
269 | 270 |
271 | 272 | 273 | 274 | « Previous 275 | 276 | 277 | Next » 278 | 279 | 280 |
281 | 282 | 283 | 284 | 285 | 286 | 290 | -------------------------------------------------------------------------------- /book/book/js/theme.js: -------------------------------------------------------------------------------- 1 | $( document ).ready(function() { 2 | // Shift nav in mobile when clicking the menu. 3 | $(document).on('click', "[data-toggle='wy-nav-top']", function() { 4 | $("[data-toggle='wy-nav-shift']").toggleClass("shift"); 5 | $("[data-toggle='rst-versions']").toggleClass("shift"); 6 | }); 7 | 8 | // Close menu when you click a link. 9 | $(document).on('click', ".wy-menu-vertical .current ul li a", function() { 10 | $("[data-toggle='wy-nav-shift']").removeClass("shift"); 11 | $("[data-toggle='rst-versions']").toggleClass("shift"); 12 | }); 13 | 14 | $(document).on('click', "[data-toggle='rst-current-version']", function() { 15 | $("[data-toggle='rst-versions']").toggleClass("shift-up"); 16 | }); 17 | 18 | // Make tables responsive 19 | $("table.docutils:not(.field-list)").wrap("
"); 20 | 21 | hljs.initHighlightingOnLoad(); 22 | 23 | $('table').addClass('docutils'); 24 | }); 25 | 26 | window.SphinxRtdTheme = (function (jquery) { 27 | var stickyNav = (function () { 28 | var navBar, 29 | win, 30 | stickyNavCssClass = 'stickynav', 31 | applyStickNav = function () { 32 | if (navBar.height() <= win.height()) { 33 | navBar.addClass(stickyNavCssClass); 34 | } else { 35 | navBar.removeClass(stickyNavCssClass); 36 | } 37 | }, 38 | enable = function () { 39 | applyStickNav(); 40 | win.on('resize', applyStickNav); 41 | }, 42 | init = function () { 43 | navBar = jquery('nav.wy-nav-side:first'); 44 | win = jquery(window); 45 | }; 46 | jquery(init); 47 | return { 48 | enable : enable 49 | }; 50 | }()); 51 | return { 52 | StickyNav : stickyNav 53 | }; 54 | }($)); 55 | 56 | // The code below is a copy of @seanmadsen code posted Jan 10, 2017 on issue 803. 57 | // https://github.com/mkdocs/mkdocs/issues/803 58 | // This just incorporates the auto scroll into the theme itself without 59 | // the need for additional custom.js file. 60 | // 61 | $(function() { 62 | $.fn.isFullyWithinViewport = function(){ 63 | var viewport = {}; 64 | viewport.top = $(window).scrollTop(); 65 | viewport.bottom = viewport.top + $(window).height(); 66 | var bounds = {}; 67 | bounds.top = this.offset().top; 68 | bounds.bottom = bounds.top + this.outerHeight(); 69 | return ( ! ( 70 | (bounds.top <= viewport.top) || 71 | (bounds.bottom >= viewport.bottom) 72 | ) ); 73 | }; 74 | if( $('li.toctree-l1.current').length && !$('li.toctree-l1.current').isFullyWithinViewport() ) { 75 | $('.wy-nav-side') 76 | .scrollTop( 77 | $('li.toctree-l1.current').offset().top - 78 | $('.wy-nav-side').offset().top - 79 | 60 80 | ); 81 | } 82 | }); 83 | -------------------------------------------------------------------------------- /book/book/mkdocs/js/mustache.min.js: -------------------------------------------------------------------------------- 1 | (function(global,factory){if(typeof exports==="object"&&exports){factory(exports)}else if(typeof define==="function"&&define.amd){define(["exports"],factory)}else{factory(global.Mustache={})}})(this,function(mustache){var Object_toString=Object.prototype.toString;var isArray=Array.isArray||function(object){return Object_toString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}var RegExp_test=RegExp.prototype.test;function testRegExp(re,string){return RegExp_test.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};function escapeHtml(string){return String(string).replace(/[&<>"'\/]/g,function(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tags){if(typeof tags==="string")tags=tags.split(spaceRe,2);if(!isArray(tags)||tags.length!==2)throw new Error("Invalid tags: "+tags);openingTagRe=new RegExp(escapeRegExp(tags[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tags[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tags[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function(){return this.tail===""};Scanner.prototype.scan=function(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function(view){return new Context(view,this)};Context.prototype.lookup=function(name){var cache=this.cache;var value;if(name in cache){value=cache[name]}else{var context=this,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){value=context.view;names=name.split(".");index=0;while(value!=null&&index")value=this._renderPartial(token,context,partials,originalTemplate);else if(symbol==="&")value=this._unescapedValue(token,context);else if(symbol==="name")value=this._escapedValue(token,context);else if(symbol==="text")value=this._rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype._renderSection=function(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j 2 |

{{title}}

3 |

{{summary}}

4 | 5 | -------------------------------------------------------------------------------- /book/book/mkdocs/js/search.js: -------------------------------------------------------------------------------- 1 | require([ 2 | base_url + '/mkdocs/js/mustache.min.js', 3 | base_url + '/mkdocs/js/lunr.min.js', 4 | 'text!search-results-template.mustache', 5 | 'text!../search_index.json', 6 | ], function (Mustache, lunr, results_template, data) { 7 | "use strict"; 8 | 9 | function getSearchTerm() 10 | { 11 | var sPageURL = window.location.search.substring(1); 12 | var sURLVariables = sPageURL.split('&'); 13 | for (var i = 0; i < sURLVariables.length; i++) 14 | { 15 | var sParameterName = sURLVariables[i].split('='); 16 | if (sParameterName[0] == 'q') 17 | { 18 | return decodeURIComponent(sParameterName[1].replace(/\+/g, '%20')); 19 | } 20 | } 21 | } 22 | 23 | var index = lunr(function () { 24 | this.field('title', {boost: 10}); 25 | this.field('text'); 26 | this.ref('location'); 27 | }); 28 | 29 | data = JSON.parse(data); 30 | var documents = {}; 31 | 32 | for (var i=0; i < data.docs.length; i++){ 33 | var doc = data.docs[i]; 34 | doc.location = base_url + doc.location; 35 | index.add(doc); 36 | documents[doc.location] = doc; 37 | } 38 | 39 | var search = function(){ 40 | 41 | var query = document.getElementById('mkdocs-search-query').value; 42 | var search_results = document.getElementById("mkdocs-search-results"); 43 | while (search_results.firstChild) { 44 | search_results.removeChild(search_results.firstChild); 45 | } 46 | 47 | if(query === ''){ 48 | return; 49 | } 50 | 51 | var results = index.search(query); 52 | 53 | if (results.length > 0){ 54 | for (var i=0; i < results.length; i++){ 55 | var result = results[i]; 56 | doc = documents[result.ref]; 57 | doc.base_url = base_url; 58 | doc.summary = doc.text.substring(0, 200); 59 | var html = Mustache.to_html(results_template, doc); 60 | search_results.insertAdjacentHTML('beforeend', html); 61 | } 62 | } else { 63 | search_results.insertAdjacentHTML('beforeend', "

No results found

"); 64 | } 65 | 66 | if(jQuery){ 67 | /* 68 | * We currently only automatically hide bootstrap models. This 69 | * requires jQuery to work. 70 | */ 71 | jQuery('#mkdocs_search_modal a').click(function(){ 72 | jQuery('#mkdocs_search_modal').modal('hide'); 73 | }); 74 | } 75 | 76 | }; 77 | 78 | var search_input = document.getElementById('mkdocs-search-query'); 79 | 80 | var term = getSearchTerm(); 81 | if (term){ 82 | search_input.value = term; 83 | search(); 84 | } 85 | 86 | search_input.addEventListener("keyup", search); 87 | 88 | }); 89 | -------------------------------------------------------------------------------- /book/book/part001/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 第一部分 - The Way to Flask 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 150 | 151 |
152 | 153 | 154 | 158 | 159 | 160 |
161 |
162 |
163 |
    164 |
  • Docs »
  • 165 | 166 | 167 | 168 |
  • 第一部分
  • 169 |
  • 170 | 171 |
  • 172 |
173 |
174 |
175 |
176 |
177 | 178 |

第一部分

179 |

Flask 快速入门

180 | 185 | 186 |
187 |
188 |
189 | 190 | 198 | 199 | 200 |
201 | 202 |
203 | 204 | 205 |
206 | 207 | Built with MkDocs using a theme provided by Read the Docs. 208 |
209 | 210 |
211 |
212 | 213 |
214 | 215 |
216 | 217 |
218 | 219 | 220 | 221 | « Previous 222 | 223 | 224 | Next » 225 | 226 | 227 |
228 | 229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /book/book/part002/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 第二部分 - The Way to Flask 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 150 | 151 |
152 | 153 | 154 | 158 | 159 | 160 |
161 |
162 |
163 |
    164 |
  • Docs »
  • 165 | 166 | 167 | 168 |
  • 第二部分
  • 169 |
  • 170 | 171 |
  • 172 |
173 |
174 |
175 |
176 |
177 | 178 |

第二部分

179 |

Flask 插件使用指南

180 | 187 | 188 |
189 |
190 |
191 | 192 | 200 | 201 | 202 |
203 | 204 |
205 | 206 | 207 |
208 | 209 | Built with MkDocs using a theme provided by Read the Docs. 210 |
211 | 212 |
213 |
214 | 215 |
216 | 217 |
218 | 219 |
220 | 221 | 222 | 223 | « Previous 224 | 225 | 226 | Next » 227 | 228 | 229 |
230 | 231 | 232 | 233 | 234 | -------------------------------------------------------------------------------- /book/book/part003/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 第三部分 - The Way to Flask 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 150 | 151 |
152 | 153 | 154 | 158 | 159 | 160 |
161 |
162 |
163 |
    164 |
  • Docs »
  • 165 | 166 | 167 | 168 |
  • 第三部分
  • 169 |
  • 170 | 171 |
  • 172 |
173 |
174 |
175 |
176 |
177 | 178 |

第三部分

179 |

Flask 项目实战

180 | 186 | 187 |
188 |
189 |
190 | 191 | 199 | 200 | 201 |
202 | 203 |
204 | 205 | 206 |
207 | 208 | Built with MkDocs using a theme provided by Read the Docs. 209 |
210 | 211 |
212 |
213 | 214 |
215 | 216 |
217 | 218 |
219 | 220 | 221 | 222 | « Previous 223 | 224 | 225 | Next » 226 | 227 | 228 |
229 | 230 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /book/book/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | The Way to Flask 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 140 | 141 |
142 | 143 | 144 | 148 | 149 | 150 |
151 |
152 |
153 |
    154 |
  • Docs »
  • 155 | 156 | 157 |
  • 158 | 159 |
  • 160 |
161 |
162 |
163 |
164 |
165 | 166 | 167 |

Search Results

168 | 169 | 173 | 174 |
175 | Searching... 176 |
177 | 178 | 179 |
180 |
181 |
182 | 183 | 184 |
185 | 186 |
187 | 188 | 189 |
190 | 191 | Built with MkDocs using a theme provided by Read the Docs. 192 |
193 | 194 |
195 |
196 | 197 |
198 | 199 |
200 | 201 |
202 | 203 | 204 | 205 | 206 | 207 |
208 | 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /book/book/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | /SUMMARY/ 7 | 2017-05-31 8 | daily 9 | 10 | 11 | 12 | 13 | 14 | / 15 | 2017-05-31 16 | daily 17 | 18 | 19 | 20 | 21 | 22 | /part001/ 23 | 2017-05-31 24 | daily 25 | 26 | 27 | 28 | 29 | 30 | /chapter001/ 31 | 2017-05-31 32 | daily 33 | 34 | 35 | 36 | 37 | 38 | /chapter002/ 39 | 2017-05-31 40 | daily 41 | 42 | 43 | 44 | 45 | 46 | /chapter003/ 47 | 2017-05-31 48 | daily 49 | 50 | 51 | 52 | 53 | 54 | /part002/ 55 | 2017-05-31 56 | daily 57 | 58 | 59 | 60 | 61 | 62 | /chapter004/ 63 | 2017-05-31 64 | daily 65 | 66 | 67 | 68 | 69 | 70 | /chapter005/ 71 | 2017-05-31 72 | daily 73 | 74 | 75 | 76 | 77 | 78 | /chapter006/ 79 | 2017-05-31 80 | daily 81 | 82 | 83 | 84 | 85 | 86 | /chapter007/ 87 | 2017-05-31 88 | daily 89 | 90 | 91 | 92 | 93 | 94 | /chapter008/ 95 | 2017-05-31 96 | daily 97 | 98 | 99 | 100 | 101 | 102 | /chapter009/ 103 | 2017-05-31 104 | daily 105 | 106 | 107 | 108 | 109 | 110 | /chapter010/ 111 | 2017-05-31 112 | daily 113 | 114 | 115 | 116 | 117 | 118 | /part003/ 119 | 2017-05-31 120 | daily 121 | 122 | 123 | 124 | 125 | 126 | /chapter011/ 127 | 2017-05-31 128 | daily 129 | 130 | 131 | 132 | 133 | 134 | /chapter012/ 135 | 2017-05-31 136 | daily 137 | 138 | 139 | 140 | 141 | 142 | /chapter013/ 143 | 2017-05-31 144 | daily 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /book/docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [前言](index.md) 4 | * [第一部分](part001.md) 5 | * [本书概述](chapter001.md) 6 | * [简单的 Flask 应用](chapter002.md) 7 | * [简单的 REST 服务](chapter003.md) 8 | * [第二部分](part002.md) 9 | * [使用 Flask-MongoEngine 集成数据库](chapter004.md) 10 | * [使用 Flask-Login 注册登录](chapter005.md) 11 | * [自建装饰器实现权限控制](chapter006.md) 12 | * [规范结构维护代码](chapter007.md) 13 | * [建立目录管理配置](chapter008.md) 14 | * [使用 Flask-Script 启动应用](chapter009.md) 15 | * [使用 Flask-Admin 管理数据库数据](chapter010.md) 16 | * [第三部分](part003.md) 17 | * [编写 TODO 应用【part001】](chapter011.md) 18 | * [编写 TODO 应用【part002】](chapter012.md) 19 | * [使用 Gunicorn 和 Nginx 部署项目](chapter013.md) 20 | 21 | 22 | -------------------------------------------------------------------------------- /book/docs/chapter001.md: -------------------------------------------------------------------------------- 1 | # 本书概述 2 | 3 | 在 Python 中有很多优秀的 Web 开发框架,例如 Django、Flask和 Tornado等等。 4 | 5 | 每种框架都有其自身的独特之处, 6 | 7 | - Django 自己集成了丰富的功能,将数据库模块、模板以及后台管理等模块都集成在自身内部,和框架一起打包发布; 8 | - 而 Flask 则以最简原则,自身框架只附带很简单的路由、模板功能,而提供了简单的扩展接口,从而将其他的功能都以扩展的形式提供,从而产生了大量的强大的各种扩展,Flask也因此以扩展丰富而受欢迎; 9 | - Tornado 则与 Django 和 Flask 走不同的道路,Tornado 的主打功能是异步请求处理,适用于 IO 操作繁多的应用。 10 | 11 | 这个系列文章的主要介绍对象就是 Flask 以及它的插件们,因此对于其他框架也就在上面简约得一言带过,有兴趣的同学可以自行查找资料学习。 12 | 13 | 本书的文章顺序主要按照以下的骨架进行介绍: 14 | 15 | - 第一部分讲解 Flask 的基础功能 16 | - 第二部分讲解 Flask 的几个重要插件以及注意点 17 | - 第三部分将以前面介绍的内容综合起来实践一个 Todo 系统 18 | 19 | 为了让同学们在阅读的时候同时实践可以产生和我讲解出现一样的效果,下面我有必要罗列一下本书中使用到的数据库、Python库的版本等信息。 20 | 21 | ### 数据库 22 | 23 | MongoDB: 24 | version:3.2.6 25 | ip:localhost 26 | port:27017 27 | Redis: 28 | version:3.0.5 29 | ip:localhost 30 | port:6379 31 | 32 | ### Python 依赖库 33 | 34 | Flask==0.10.1 35 | flask-mongoengine==0.7.5 36 | Flask-Login==0.3.2 37 | Flask-Admin==1.4.0 38 | Flask-Redis==0.1.0 39 | Flask-WTF==0.12 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /book/docs/chapter002.md: -------------------------------------------------------------------------------- 1 | # 简单的 Flask 应用 2 | 3 | 作为本书的第一个示例,也可能是你接触的第一个 Flask 应用,我还是以程序届常规的 Hello World 为例来编写一个非常简单的例子。 4 | 5 | 这个例子的功能就是你在浏览器中输入URL: 6 | 7 | http://localhost:5000 8 | 9 | 然后,你就可以在浏览器中看到: 10 | 11 | Hello World! 12 | 13 | ## Simple Flask App 14 | 15 | 首先,我们先来看一个简短的代码 16 | 17 | #!/usr/bin/env python 18 | # encoding: utf-8 19 | from flask import Flask 20 | 21 | app = Flask(__name__) 22 | 23 | 24 | @app.route('/') 25 | def index(): 26 | return "Hello World!" 27 | 28 | app.run() 29 | 30 | 将这段代码保存为 `app.py`,然后再使用 python 运行这个文件: 31 | 32 | python app.py 33 | 34 | 回车之后,你将会看到类似以下的输出: 35 | 36 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 37 | 38 | 如果有其它错误,你可以仔细看看是什么问题,代码和我上面是否一致,还有一个很重要的点就是,你是否已经安装了 Flask ,如果没有的话,那么安装一下: 39 | 40 | pip install Flask==0.10.1 41 | 安装完成后,继续使用 Python 运行上述文件: 42 | 43 | python app.py 44 | 45 | 然后在浏览器上打开以下URL: 46 | 47 | http://localhost:5000 48 | 49 | 你将会看到这个界面: 50 | 51 | ![chapter002-001.png](./imgs/chapter002-001.png) 52 | 53 | 54 | 那就说明你的第一个 Flask 应用已经运行成功了。 55 | 56 | 57 | ## 简析第一个应用 58 | 59 | 对于你运行成功的第一个程序很简单,我们就做一些简单的分析,让你有一个简单的了解。 60 | 61 | 前两行的编码说明就不说了,这是 Python 的特性,而不是 Flask 特有的,如果读者有不懂的话,建议查看 Python 的文件编码说明。 62 | 63 | 然后继续看代码,我们的所有代码只有一个 `import`,就是 `Flask`,这是 Flask 这个框架的核心,我们把它认为是服务器就可以了,目前不需要多关注: 64 | 65 | from flask import Flask 66 | 67 | 然后接下去看,解析来一句是初始化了一个 Flask 变量,那么我们就可以认为是创建了一个服务器;需要注意的是这里传递了一个参数 __name__,我们知道在 Python 中 __name__ 这个变量是表示模块的名称,**这个参数对于 Flask 很重要,因为 Flask 会依赖于它去判断从哪里找模板、静态文件**。 68 | 69 | app = Flask(__name__) 70 | 71 | 接下来三句目前来说可能有点超出我们的讨论范围,但是我们这里稍微讲解一下好了,这三句中关键是第一句和第三句。 72 | 73 | @app.route('/') 74 | def index(): 75 | return "Hello World!" 76 | 77 | 第一句中关键的是 `'/'` 这个参数,这个参数的作用是说下面的这个函数对应于我们在浏览器中输入的地址: 78 | 79 | http://localhost:5000 + 后面的参数 80 | 81 | 这样说,大家可能不太明白,假设换成: 82 | 83 | @app.route('/hello') 84 | def hello(): 85 | return "hello world" 86 | 87 | 的话,那么也就表示,我们在浏览器中访问: 88 | 89 | http://localhost:5000/hello 90 | 91 | 那么 Flask 就会调用到 hello 这个函数。 92 | 93 | 那第三句的意思大家可能会比较容易理解了,没错,return 的内容就是我们在浏览器中看到的内容了。我们的代码中 return 的是 "Hello World!",那么我们在浏览器中看到的就是 Hello World! 94 | 95 | 到目前为止,我们 import 了服务器(**import Flask**),创建了服务器(***Flask(__name__)***),是时候将服务器运行起来了,是的,最后一句 96 | 97 | app.run() 98 | 99 | 就是表示将服务器运行起来,接受浏览器的访问。 100 | 101 | 那么整个过程就是这样的,当我们在浏览器中输入 102 | 103 | http://localhost:5000 104 | 105 | 的时候,其实浏览器默默得在我们的 URL 后面加入了一个 `/`,真实访问的就是 106 | 107 | http://localhost:5000/ 108 | 109 | 其实也就是对应着我们的 110 | 111 | app.route('/') 112 | 113 | 函数了,这个函数 114 | 115 | return "Hello World!" 116 | 117 | 所以我们在浏览器中就看到了: 118 | 119 | Hello World! 120 | -------------------------------------------------------------------------------- /book/docs/chapter003.md: -------------------------------------------------------------------------------- 1 | # 简单的 REST 服务 2 | 3 | 随着移动设备的不断发展,移动端的需求日益增大,对于大多数公司来说,可能用户量已超越 PC 端。而随着移动端发展,伴随而来的是对于客户端和服务器的交互越来越轻量化,相对 “笨重” 的 HTML 页面逐渐被移动端抛弃(但是 H5 的出现,这一情况有所转变),而此时 REST 服务模式被越来越多人接受。 4 | 5 | 通俗来说,REST 服务最少都需要提供查询功能,丰富一下的则会提供增删改查功能,其中还可能包含批量的操作。但是,本章因为是介绍如何使用 Flask 编写一个 REST 服务器的示例,所以本章要介绍的功能是: 6 | 7 | - 使用 PUT、DELETE、POST和GET 进行数据增删改查 8 | - 返回 json 结构的数据 9 | 10 | ## 修改第一个程序 11 | 12 | 我们回忆一下第一个程序,他的功能就是我们在浏览器中输入 URL 13 | 14 | http://localhost:5000 15 | 16 | 时,返回一个字符串 “Hello World!”,于是我们就想,我们能不能将这个字符串换成 json 序列?这样不就等于我们实现了 REST 的查询 API 了? 17 | 18 | 于是,我们可能第一冲动就会这么实现: 19 | 20 | #!/usr/bin/env python 21 | # encoding: utf-8 22 | import json 23 | from flask import Flask 24 | 25 | app = Flask(__name__) 26 | 27 | 28 | @app.route('/') 29 | def index(): 30 | return json.dumps({'name': 'tyrael', 31 | 'email': 'liqianglau@outlook.com'}) 32 | 33 | app.run() 34 | 35 | 其实我们就是修改了返回的字符串,将它修改成 JSON 的字符串,然后我们在浏览器上打开 36 | 37 | http://localhost:5000 38 | 39 | 看到的是: 40 | 41 | ![](./imgs/chapter003-001.png) 42 | 43 | 哇!!好像是实现了我们想要的功能,返回了 JSON 字符串,但是我们打开 Chrome(我使用的是 Chrome,Safari 和 Firefox 同样有类似的工具)的调试工具(Windows下按: Ctrl + Alt + I,Mac 下按:Cmd + Shift + I),我们可以看到其实这个返回的数据类型居然是 html 类型: 44 | 45 | ![](./imgs/chapter003-002.png) 46 | 47 | 你可能会奇怪这会有什么影响,这个影响大多数情况下应该不大,但是对于某些移动端的库,可能会根据这个响应头来处理数据,这个时候就悲剧了。 48 | 49 | 50 | ## 返回json 51 | 52 | 处理这个情况我们不能简单得想把这个响应头设置成 json 格式,这样修补 bug 是会导致其他 bug 的,譬如其他我们不知道的地方还有类似的坑。 53 | 54 | 更好的解决方案是使用 Flask 的 jsonify 函数,我这里使用这个函数修改一下代码: 55 | 56 | #!/usr/bin/env python 57 | # encoding: utf-8 58 | import json 59 | from flask import Flask, jsonify 60 | 61 | app = Flask(__name__) 62 | 63 | 64 | @app.route('/') 65 | def index(): 66 | return jsonify({'name': 'tyrael', 67 | 'email': 'liqianglau@outlook.com'}) 68 | 69 | app.run() 70 | 71 | 这里做了两处修改,分别是: 72 | 73 | from flask import ...., jsonify 74 | ... ... 75 | return jsonify({'name': 'tyrael', 76 | 'email': 'liqianglau@outlook.com'}) 77 | 78 | 此时,我们再保存代码,运行代码,并且访问看看: 79 | 80 | ![](./imgs/chapter003-003.png) 81 | 82 | 我们发现代码居然排好了版式,然后再看看响应头: 83 | 84 | ![](./imgs/chapter003-004.png) 85 | 86 | 响应头也变成了 `application/json` 了。 87 | 88 | 好了,那么我们这里达到了第一个目的了,返回 json 数据。但是,我们的另外一个目的—使用 DEL,PUT和 POST 方法怎么处理? 89 | 90 | 91 | ## 请求方法 92 | 93 | 我们知道常用的 HTTP 请求方法有 6 种,分别是 94 | 95 | - GET 96 | - POST 97 | - PUT 98 | - DELETE 99 | - PATCH 100 | - HEAD 101 | 102 | 那么我们刚刚的代码只能默认得处理 GET 的情况(浏览器默认使用GET),那么其他情况怎么处理? 103 | 104 | 这时我们回到我们的代码中,既然我们的 **URL** 是通过 105 | 106 | app.route('...') 107 | 108 | 来拼接的,那么,请求方法是不是也可以在这里指定? 109 | 110 | 事实上就是这样的,请求方法通过一个叫做 methods 的参数指定,例如下面分别对应 POST、DELETE、PUT 方法。 111 | 112 | @app.route('/', methods=['POST']) 113 | @app.route('/', methods=['DELETE']) 114 | @app.route('/', methods=['PUT']) 115 | 116 | 还有一个问题就是我们因为要做数据的增删改查,所以需要考虑数据的保存,因为数据库的操作在本章又是超出范围的讨论,所以这里我们简单得以文件作为保存数据的媒介。进行数据操作,那么我们的代码可以这么写: 117 | 118 | #!/usr/bin/env python 119 | # encoding: utf-8 120 | import json 121 | from flask import Flask, request, jsonify 122 | 123 | app = Flask(__name__) 124 | 125 | 126 | @app.route('/', methods=['GET']) 127 | def query_records(): 128 | name = request.args.get('name') 129 | print name 130 | with open('/tmp/data.txt', 'r') as f: 131 | data = f.read() 132 | records = json.loads(data) 133 | for record in records: 134 | if record['name'] == name: 135 | return jsonify(record) 136 | return jsonify({'error': 'data not found'}) 137 | 138 | 139 | @app.route('/', methods=['PUT']) 140 | def create_record(): 141 | record = json.loads(request.data) 142 | with open('/tmp/data.txt', 'r') as f: 143 | data = f.read() 144 | 145 | if not data: 146 | records = [record] 147 | else: 148 | records = json.loads(data) 149 | records.append(record) 150 | 151 | with open('/tmp/data.txt', 'w') as f: 152 | f.write(json.dumps(records, indent=2)) 153 | return jsonify(record) 154 | 155 | 156 | @app.route('/', methods=['POST']) 157 | def update_record(): 158 | record = json.loads(request.data) 159 | new_records = [] 160 | with open('/tmp/data.txt', 'r') as f: 161 | data = f.read() 162 | records = json.loads(data) 163 | 164 | for r in records: 165 | if r['name'] == record['name']: 166 | r['email'] = record['email'] 167 | new_records.append(r) 168 | 169 | with open('/tmp/data.txt', 'w') as f: 170 | f.write(json.dumps(new_records, indent=2)) 171 | return jsonify(record) 172 | 173 | 174 | @app.route('/', methods=['DELETE']) 175 | def delte_record(): 176 | record = json.loads(request.data) 177 | new_records = [] 178 | with open('/tmp/data.txt', 'r') as f: 179 | data = f.read() 180 | records = json.loads(data) 181 | for r in records: 182 | if r['name'] == record['name']: 183 | continue 184 | new_records.append(r) 185 | 186 | with open('/tmp/data.txt', 'w') as f: 187 | f.write(json.dumps(new_records, indent=2)) 188 | 189 | return jsonify(record) 190 | 191 | app.run(debug=True) 192 | 193 | 这段代码虽然很长,但是代码都比较容易懂,而且都是比较简单的文件操作。 194 | 195 | 这段代码我们需要关注的点有以下几点: 196 | 197 | - 如何设置请求方法 198 | 199 | @app.route('/', methods=['GET']) 200 | @app.route('/', methods=['PUT']) 201 | @app.route('/', methods=['POST']) 202 | @app.route('/', methods=['DELETE']) 203 | 204 | - 如何获取数据 205 | 206 | 在 Flask 中有一个 request 变量,这是一个请求上下文的变量,然后里面包含多个属性是可以用来获取请求的参数的,例如我们这里用到了两种方式: 207 | 208 | 1. request.args.get('name') 209 | 210 | `request.args` 这个属性用于表示 GET 请求在 URL 上附带的参数 211 | 212 | 2. json.loads(request.data) 213 | 214 | `request.data` 这个属性用于表示 POST 等请求的请求体中的数据 215 | 216 | 我们目前对 request 变量就做这么多介绍吧,毕竟我们本章的目标是让大家了解如何处理 GET、POST、PUT 等不同的请求方式如何处理。 217 | 218 | -------------------------------------------------------------------------------- /book/docs/chapter004.md: -------------------------------------------------------------------------------- 1 | # 使用 Flask-MongoEngine 集成数据库 2 | 3 | 在前面一章 [简单的 REST 服务](./chapter003.md) 中,我们的数据都是保存在文件中的,我们可以发现,这样很是繁琐,每个请求中都需要进行读取文件,写出文件的操作,虽然显然我们可以对文件操作进行一个封装,但是,毕竟是文件存储,数据稍微多一点查询等操作必然时间变长。 4 | 5 | 面对这样的一个问题,这里引入了对数据库的依赖,在我们的 [本书概述](./chapter001.md) 中,我介绍了数据库的版本信息,本章使用的是 MongoDB,具体的版本还有数据库地址信息为: 6 | 7 | version:3.2.6 8 | ip:localhost 9 | port:27017 10 | 11 | ## 创建数据模型 12 | 13 | 既然我们想使用数据库来保存数据,我们可以使用原生的 pymongo 来操作 MongoDB,但是,我们这里为了更进一步得简化我们的操作,所以我们需要创建数据模型。 14 | 15 | 数据模型主要的功能是用于说明我们的数据包含哪些字段,每个字段分别是什么类型,有什么属性(唯一的,还是固定几个值中的一个)等等。这样可以帮助我们在操作数据的时候可以时刻很清晰得知道我们的数据的信息,即使我们不看数据库中的数据。 16 | 17 | 这里我们要介绍的操作 MongoDB 的 Flask 扩展是 Flask-MongoEngine,这个扩展是 MongoEngine 在 Flask 上的扩展,也就是说,我们完全可以独立使用 MongoEngine 而不依赖于 Flask,但依不依赖相差不多,我个人觉得最大的区别在于配置如何处置,所以这里使用依赖 Flask 的扩展。 18 | 19 | 要在 Flask 中使用 MongoEngine,首先我们需要先在 Flask 中配置 MongoDB 的信息,然后再使用我们的服务器初始化 MongoEngine,这样我们就将数据库和服务器建立了联系,这个在代码中可以这样来表示: 20 | 21 | app.config['MONGODB_SETTINGS'] = { 22 | 'db': 'the_way_to_flask', 23 | 'host': 'localhost', 24 | 'port': 27017 25 | } 26 | 27 | db = MongoEngine() 28 | db.init_app(app) 29 | 30 | 建立联系之后,我们就可以使用 MongoEngine 创建数据模型了。 31 | 32 | 我们这里还是继承上一章中的数据模型,也就是只有两个字段,分别是 name 和 email: 33 | 34 | class User(db.Document): 35 | name = db.StringField() 36 | email = db.StringField() 37 | 38 | 这样,我们的数据模型创建好了,整段完整的代码是: 39 | 40 | #!/usr/bin/env python 41 | # encoding: utf-8 42 | from flask import Flask 43 | from flask_mongoengine import MongoEngine 44 | 45 | 46 | app = Flask(__name__) 47 | app.config['MONGODB_SETTINGS'] = { 48 | 'db': 'the_way_to_flask', 49 | 'host': 'localhost', 50 | 'port': 27017 51 | } 52 | 53 | db = MongoEngine() 54 | db.init_app(app) 55 | 56 | 57 | class User(db.Document): 58 | name = db.StringField() 59 | email = db.StringField() 60 | 61 | 62 | if __name__ == "__main__": 63 | app.run(debug=True) 64 | 65 | ## 操作数据 66 | 67 | 现在我们已经有数据模型(Model)和数据库关联起来了,那光有关联没用啊,我们没办法操作啊。接下来的内容就是讲解如何通过 Model 对数据库中的数据进行增删改查。 68 | 69 | 70 | ### 查询 71 | MongoEngine 的增删改查非常简单,例如查询,我们可以使用: 72 | 73 | User.objects(name="zhangsan").first() 74 | 75 | 这个语句就将数据库中名字为 zhangsan 的用户查询出来了。我们来分析一下这个语句是怎么查询的。 76 | 77 | 首先是 `User.objects`,这里的 `User` 我们已经知道了是我们的 Model,那既然 User 都已经是 Model 了为什么还要 objects 呢? 78 | 79 | 就是因为 User 是 Model,因为 Model 本身只代表数据结构,那和我们查询有什么关系呢?所以这里引入了一个 objects 属性,表示一个查询集,这个集合默认就表示 User 表中的所有数据,所以我们后面的 `name=“zhangsan”` 就有点好理解了,其实就是从 User 表中的所有数据中过滤出 name 的值为 zhangsan 的记录,别忘了,过滤出来的数据是一个集合,而不是一个 User 对象,所以我们后面还加了一个 `first` 获取这个集合的第一个元素。 80 | 81 | 这样,我们就查询到了一个 User 对象。 82 | 83 | ### 新增 84 | 85 | 增加新记录就更简单了,例如我想插入一个 **name** 为 `lisi`,**email** 为 `lisi@gmail.com` 的用户,那么我们可以这样写: 86 | 87 | User(name='lisi', email='lisi@gmail.com').save() 88 | 89 | 就这么简单,首先,我们想创建了一个 User 对象,然后调用 save 方法就可以了。 90 | 91 | ### 删除 92 | 93 | 考虑一下如果我们要删除一个记录,我们是不是需要先找到这个需要删除的记录?在 MongoEngine 中就是这样的,如果我们要删除一个记录,我们想找到它,使用的是查询: 94 | 95 | user = User.objects(name="zhangsan").first() 96 | 97 | 找到之后,很简单,只需调用 delete 方法即可: 98 | 99 | user.delete() 100 | 101 | 这样,我们就将 zhangsan 这个用户删除掉了。 102 | 103 | ### 更新 104 | 105 | 和删除一样,如果我们需要更新一条记录,那么我们也先需要找到他,假设我们需要更新 lisi 的邮箱为: lisi@outlook.com,那么我们可以这么写: 106 | 107 | user = User.objects(name="zhangsan").first() 108 | user.update(email="lisi@outlook.com") 109 | 110 | 第一句还是查询啦,第二句这里使用了 update 方法,直接将需要修改的属性以及改变后的值作为参数传入,即可完成更新操作。 111 | 112 | ### 完整代码 113 | 114 | 这样,我们就知道了如何利用模型进行增删改查,那么我们就将这个知识都应用到我们的 REST 服务中,改写后的代码如下: 115 | 116 | #!/usr/bin/env python 117 | # encoding: utf-8 118 | import json 119 | from flask import Flask, request, jsonify 120 | from flask_mongoengine import MongoEngine 121 | 122 | 123 | app = Flask(__name__) 124 | app.config['MONGODB_SETTINGS'] = { 125 | 'db': 'the_way_to_flask', 126 | 'host': 'localhost', 127 | 'port': 27017 128 | } 129 | 130 | db = MongoEngine() 131 | db.init_app(app) 132 | 133 | 134 | class User(db.Document): 135 | name = db.StringField() 136 | email = db.StringField() 137 | 138 | def to_json(self): 139 | return {"name": self.name, 140 | "email": self.email} 141 | 142 | 143 | @app.route('/', methods=['GET']) 144 | def query_records(): 145 | name = request.args.get('name') 146 | user = User.objects(name=name).first() 147 | 148 | if not user: 149 | return jsonify({'error': 'data not found'}) 150 | else: 151 | return jsonify(user.to_json()) 152 | 153 | 154 | @app.route('/', methods=['PUT']) 155 | def create_record(): 156 | record = json.loads(request.data) 157 | user = User(name=record['name'], 158 | email=record['email']) 159 | user.save() 160 | return jsonify(user.to_json()) 161 | 162 | 163 | @app.route('/', methods=['POST']) 164 | def update_record(): 165 | record = json.loads(request.data) 166 | user = User.objects(name=record['name']).first() 167 | if not user: 168 | return jsonify({'error': 'data not found'}) 169 | else: 170 | user.update(email=record['email']) 171 | return jsonify(user.to_json()) 172 | 173 | 174 | @app.route('/', methods=['DELETE']) 175 | def delte_record(): 176 | record = json.loads(request.data) 177 | user = User.objects(name=record['name']).first() 178 | if not user: 179 | return jsonify({'error': 'data not found'}) 180 | else: 181 | user.delete() 182 | return jsonify(user.to_json()) 183 | 184 | 185 | if __name__ == "__main__": 186 | app.run(debug=True) 187 | 188 | CRUD 使用的基本上都是我们介绍的方法,大家可以自己尝试得编写一些。 -------------------------------------------------------------------------------- /book/docs/chapter005.md: -------------------------------------------------------------------------------- 1 | # 使用 Flask-Login 注册登录 2 | 3 | 在我们的前几章中,围绕着要讲解的内容持续得再丰富一个 REST 服务。但是,截止到目前为止,我们这个 REST 服务都是没有权限控制的,也就是说,如果将这个 REST 服务发布到外网上去,那么将可以被任何人操作,增删改查都不是问题。 4 | 5 | 作为我们的重要服务(真的很重要:-D),我们怎么能让别人随便操作我们的数据呢,所以这一章就讲解一下如何使用 Flask 的又一扩展 **Flask-Login** 来进行访问控制。 6 | 7 | ## 安装 Flask-Login 8 | 9 | 根据在 《[本书概述](chapter001.md)》中列举的那样,我们使用的 **Flask-Login **的版本是 10 | 11 | Flask-Login==0.3.2 12 | 13 | 所以安装的话直接使用 pip 安装即可: 14 | 15 | pip install Flask-Login==0.3.2 16 | 17 | ## 初始化 Flask-Login 18 | 19 | 和我们在上一章使用 Flask-MongoEngine 一样,使用 Flask-Login 还是依赖于 Flask,所以我们还是需要和 app 这样服务器绑定起来,所以我们一开始还是需要这样和服务器绑定的: 20 | 21 | from flask.ext.login import LoginManager 22 | login_manager = LoginManager() 23 | login_manager.init_app(app) 24 | 25 | 这样就将 Flask-Login 和服务器绑定起来了。但是,这好像没有什么作用啊,我们要怎么登陆呢?Flask-Login 怎么才知道登录的 URL 的是哪个?怎么验证我们的账号密码?怎么才能知道登陆的用户是谁?这些都是关键的问题啊。 26 | 27 | ## 设置 Flask-Login 28 | 29 | 对于前面提到的问题,我们一一解决,解决完之后我们的 Flask-Login 就差不多算是会使用了。 30 | 31 | 首先是登陆的 URL 是什么?这个在 Flask-Login 中是没有默认的登陆 URL 的,所以需要我们指定: 32 | 33 | from flask.ext.login import login_user 34 | 35 | login_manager.login_view = 'login' 36 | 37 | @app.route('/login', methods=['POST']) 38 | def login(): 39 | info = json.loads(request.data) 40 | username = info.get('username', 'guest') 41 | password = info.get('password', '') 42 | 43 | user = User.objects(name=username, 44 | password=password).first() 45 | if user: 46 | login_user(user) 47 | return jsonify(user.to_json()) 48 | else: 49 | return jsonify({"status": 401, 50 | "reason": "Username or Password Error"}) 51 | 52 | 这里其实就做了两件事: 53 | 54 | 1. 指定了 login_view 为 'login' 55 | 2. 编写的登陆的代码逻辑 56 | 57 | 那我们来看第一点,指定 login_view,也就是告诉 Flask 我们的处理的登陆的 URL 是哪个。这里我们发现是 'login',那么 Flask 是怎么根据 login 找到我们的登陆逻辑所在的位置的呢?这里除了 'login' 我们还能填写其他的字符串吗? 58 | 59 | 这里先给出答案,是不能的,也就是说,在我们这段代码中,必须指定为 'login',这里的 'login' 的意思就是在当前文件找到 60 | 61 | def login(self, xxx) 62 | 63 | 这个函数,然后它就是我们处理登陆逻辑代码所在的地方。 64 | 65 | 假如说我们处理登陆逻辑的代码没有放在这个文件,而是放在了其他文件,例如 auth.py 里面的 login 函数里面,那么我们就需要指定为: 66 | 67 | login_view = 'auth.login' 68 | 69 | ### 登陆逻辑 70 | 71 | 还是看回上一段代码,我们发现这是一个普通的 Flask 处理请求的函数,说普通在于: 72 | 73 | - 从客户端的请求中获得参数,和之前的 CRUD 一样 74 | - 无论是登陆成功还是失败都返回 json 串给客户端 75 | 76 | 那么凭什么这段代码就能胜任登陆用户的职责呢?问题的关键就在于 77 | 78 | login_user(user) 79 | 80 | 这一句,仅仅是通过这简单的一句,就将当前用户的状态设置成已登录。这里不做过深入的讲解,只需要知道当这个函数被调用之后,用户的状态就是登陆状态了。 81 | 82 | 那现在问题是,下次有请求过来,我们怎么知道是不是有用户登陆了,怎么知道是哪个用户?这时我们就会发现我们的 Model 还不够完善,需要完善一下 Model。具体应该这样完善一下: 83 | 84 | class User(db.Document): 85 | name = db.StringField() 86 | password = db.StringField() 87 | email = db.StringField() 88 | 89 | def to_json(self): 90 | return {"name": self.name, 91 | "email": self.email} 92 | 93 | def is_authenticated(self): 94 | return True 95 | 96 | def is_active(self): 97 | return True 98 | 99 | def is_anonymous(self): 100 | return False 101 | 102 | def get_id(self): 103 | return str(self.id) 104 | 105 | 我们可以看到,这里增加了两个方法,分别是: 106 | 107 | - is_authenticated:当前用户是否被授权,因为我们登陆了就可以操作,所以默认都是被授权的 108 | - is_anonymous: 用于判断当前用户是否是匿名用户,很明显,如果这个用户登陆了,就必须不是 109 | - is_active: 用于判断当前用户是否已经激活,已经激活的用户才能登陆 110 | - get_id: 获取改用户的唯一标示 111 | 112 | 这里,我们仅仅可以通过 is_authenticated 来判断用户时候有权限操作我们的 API,但是,我们还不能知道当前的登陆用户是谁,所以我们还需要告诉 Flask-Login 如何通过一个 id 获取到用户的方法: 113 | 114 | @login_manager.user_loader 115 | def load_user(user_id): 116 | return User.objects(id=user_id).first() 117 | 118 | 通过指定 user_loader,我们就可以查询到当前的登陆用户是谁了。这样我们就将登陆、判断用户是否登陆都完善起来了。 119 | 120 | ## 登陆可见 121 | 122 | 既然都登陆了,我们就需要控制登陆的权限了,我们设置增加、删除和修改的 REST API 为登陆才能使用,唯有查询的 API 才能随便可见。 123 | 124 | 控制登陆可用的方法比较简单,只需要加一个 login_required 的装饰器即可。我们还是以之前那些章节的 REST DEMO 为例进行改写: 125 | 126 | from flask.ext.login import login_required 127 | 128 | @app.route('/', methods=['PUT']) 129 | @login_required 130 | def create_record(): 131 | ...... 132 | 133 | @app.route('/', methods=['POST']) 134 | @login_required 135 | def update_record(): 136 | ...... 137 | 138 | @app.route('/', methods=['DELETE']) 139 | @login_required 140 | def delte_record(): 141 | ...... 142 | 143 | 这样我们就限制了增加、修改和删除操作必须登陆用户才能操作,而我们也能记录是哪个用户做的操作了。 144 | 145 | ## 用户信息 146 | 147 | 既然服务器提供了登陆的支持,那么肯定少不了退出登陆的支持;同时,作为客户端,可能关注的是想知道到底有没有登陆? 148 | 149 | 对于退出登陆,很简单,都根本不需要使用到 User 的这个 Model 了。代码如下: 150 | 151 | from flask.ext.login import logout_user 152 | 153 | @app.route('/logout', methods=['POST']) 154 | def logout(): 155 | logout_user() 156 | return jsonify(**{'result': 200, 157 | 'data': {'message': 'logout success'}}) 158 | 159 | 这里就调用了一个 `logout_user` 的方法就退出了登陆。 160 | 161 | 然而即使退出了登陆客户端也不知道,除非尝试请求一下新增、修改或者删除的操作,发现无法操作了,这时就知道了我已经退出登陆了,这样明显不合理!所以,这里再增加一个获取当前登陆用户信息的接口: 162 | 163 | from flask.ext.login import current_user 164 | 165 | @app.route('/user_info', methods=['POST']) 166 | def user_info(): 167 | if current_user.is_authenticated: 168 | resp = {"result": 200, 169 | "data": current_user.to_json()} 170 | else: 171 | resp = {"result": 401, 172 | "data": {"message": "user no login"}} 173 | return jsonify(**resp) 174 | 175 | 这里一个重要的点就是第一句,这里有一个成员叫做 `current_user`,这个变量表示的是当前请求的登陆用户,如果登陆了,那么它就是我们设置的 Model User 的对象,根据我们的 Model 定义, is_authenticated 一直为 True,表示登陆了;如果没有登陆,那么它就是默认的匿名用户 AnonymousUserMixin 的对象,is_authenticated 就为 False,就表示没有登陆。 176 | 177 | 如果登陆的话,那么 current_user 就是 User 的对象了,那么 to_json 方法就可以返回当前登陆用户的用户信息了,这样的话,我们就可以编写获取用户信息的 API 了。 178 | 179 | 本章的完整代码为: 180 | 181 | #!/usr/bin/env python 182 | # encoding: utf-8 183 | import json 184 | from flask import Flask, request, jsonify 185 | from flask.ext.login import (current_user, LoginManager, 186 | login_user, logout_user, 187 | login_required) 188 | from flask_mongoengine import MongoEngine 189 | 190 | 191 | app = Flask(__name__) 192 | app.config['MONGODB_SETTINGS'] = { 193 | 'db': 'the_way_to_flask', 194 | 'host': 'localhost', 195 | 'port': 27017 196 | } 197 | app.secret_key = 'youdontknowme' 198 | 199 | db = MongoEngine() 200 | login_manager = LoginManager() 201 | db.init_app(app) 202 | login_manager.init_app(app) 203 | 204 | 205 | login_manager.login_view = 'login' 206 | 207 | 208 | @login_manager.user_loader 209 | def load_user(user_id): 210 | return User.objects(id=user_id).first() 211 | 212 | 213 | @app.route('/login', methods=['POST']) 214 | def login(): 215 | info = json.loads(request.data) 216 | username = info.get('username', 'guest') 217 | password = info.get('password', '') 218 | 219 | user = User.objects(name=username, 220 | password=password).first() 221 | if user: 222 | login_user(user) 223 | return jsonify(user.to_json()) 224 | else: 225 | return jsonify({"status": 401, 226 | "reason": "Username or Password Error"}) 227 | 228 | 229 | @app.route('/logout', methods=['POST']) 230 | def logout(): 231 | logout_user() 232 | return jsonify(**{'result': 200, 233 | 'data': {'message': 'logout success'}}) 234 | 235 | 236 | @app.route('/user_info', methods=['POST']) 237 | def user_info(): 238 | if current_user.is_authenticated: 239 | resp = {"result": 200, 240 | "data": current_user.to_json()} 241 | else: 242 | resp = {"result": 401, 243 | "data": {"message": "user no login"}} 244 | return jsonify(**resp) 245 | 246 | 247 | class User(db.Document): 248 | name = db.StringField() 249 | password = db.StringField() 250 | email = db.StringField() 251 | 252 | def to_json(self): 253 | return {"name": self.name, 254 | "email": self.email} 255 | 256 | def is_authenticated(self): 257 | return True 258 | 259 | def is_active(self): 260 | return True 261 | 262 | def is_anonymous(self): 263 | return False 264 | 265 | def get_id(self): 266 | return str(self.id) 267 | 268 | 269 | @app.route('/', methods=['GET']) 270 | def query_records(): 271 | name = request.args.get('name') 272 | user = User.objects(name=name).first() 273 | 274 | if not user: 275 | return jsonify({'error': 'data not found'}) 276 | else: 277 | return jsonify(user.to_json()) 278 | 279 | 280 | @app.route('/', methods=['PUT']) 281 | @login_required 282 | def create_record(): 283 | record = json.loads(request.data) 284 | user = User(name=record['name'], 285 | password=record['password'], 286 | email=record['email']) 287 | user.save() 288 | return jsonify(user.to_json()) 289 | 290 | 291 | @app.route('/', methods=['POST']) 292 | @login_required 293 | def update_record(): 294 | record = json.loads(request.data) 295 | user = User.objects(name=record['name']).first() 296 | if not user: 297 | return jsonify({'error': 'data not found'}) 298 | else: 299 | user.update(email=record['email'], 300 | password=record['password']) 301 | return jsonify(user.to_json()) 302 | 303 | 304 | @app.route('/', methods=['DELETE']) 305 | @login_required 306 | def delte_record(): 307 | record = json.loads(request.data) 308 | user = User.objects(name=record['name']).first() 309 | if not user: 310 | return jsonify({'error': 'data not found'}) 311 | else: 312 | user.delete() 313 | return jsonify(user.to_json()) 314 | 315 | 316 | if __name__ == "__main__": 317 | app.run(port=8080, debug=True) -------------------------------------------------------------------------------- /book/docs/chapter006.md: -------------------------------------------------------------------------------- 1 | # 自建装饰器实现权限控制 2 | 3 | 在上一章 [《登陆注册》](chapter005.md)中,我们为 REST 的 API 设置了新增、更新和删除的操作需要登陆才能完成。细想一下,这样未免太过草率,因为对于一个系统来说,用户肯定是分为不同的级别的,例如普通的用户也就只能查查数据,然后一些用户还能多一个增加数据的权限,再高级一点的还能修改数据,最高级的就是增删改查都能。 4 | 5 | 对于这些更加丰富的需求,我们目前的登陆可用明显还不能满足需求,因此,按常规本章应该会引入一个新的扩展,而 Flask 确实是有一款叫做 `Flask-Principal`,的扩展可以满足我们的需求,通过这个扩展,我们希望能够达到更细粒度得控制用户的权限。但是,我**嫌弃这个扩展太累赘**了,所以本章不准备使用这个扩展,而是自己编写一个权限控制的扩展进行权限的控制。 6 | 7 | ## 权限控制设计 8 | 9 | 我们这里的权限控制采用 [RBAC](https://en.wikipedia.org/wiki/Role-based_access_control) 的方式,首先,我们会创建一个 Role 的 Model,然后给每个 User 分配一个 Role,这样的话,我们就可以限制某个操作需要某种 Role 才能执行,这样的话就实现了更细粒度的权限控制。 10 | 11 | 这里还有个实现细节需要先说明一下,我们的 Role 的权限是以二进制位来表示的,每一个二进制位表示一种权限: 12 | 13 | - 第一位表示可以读取记录 14 | - 第二位表示可以新建记录 15 | - 第三位表示可以更新记录 16 | - 第四位表示可以删除记录 17 | 18 | 这样的话,如果一个用户只能读取记录,那么他对应的 Role 的权限应该是 **0000 0001b** ,换算成十六进制的话就是: 0x01 19 | 20 | 如果一个用户所有操作都可以执行,那么它的权限应该对应于 **0000 1111b**,换算成十六进制的话就是:0x0f 21 | 22 | 那么,假如我们要判断一个用户时候可以进行新建操作,那么应该怎么实现这个逻辑?我这里的实现机制是如果是只有新建操作,那么对应的权限就是:**0000 0010b**,那如果我要判断一个用户时候有新建的权限,那么我只需要对这个**用户的权限**和这个操作所**需要的权限**进行 **and 操作**,如果得到的结果等于需要的权限的话,那么就表示该用户拥有权限,可能说得有点复杂,上一个简单的例子 23 | 24 | 用户 A 的权限: 0000 0001b 只有读取记录的权限 25 | 用户 B 的权限: 0000 1111b 拥有所有权限 26 | 新建记录需要权限: 0000 0010b 需要新建权限 27 | 用户A是否可以新建: 0000 0001b and 0000 0010b = 0000 0000b != 新建权限,所以不能新建 28 | 用户B时候可以新建: 0000 1111b and 0000 0010b = 0000 0010b == 新建权限,所以可以新建 29 | 30 | 大概就是这样一个场景,大家可以自己动手演练演练,看下是否可行。 31 | 32 | ## 创建 Role Model 33 | 34 | 之前已经在 [《集成数据库》](chapter004.md) 章节中讲解过了如何创建 Model,所以这里直接根据之前的经验创建 Role Model,然后再往 User 中加上一个 Role 字段。 35 | 36 | class Permission: 37 | READ = 0x01 38 | CREATE = 0x02 39 | UPDATE = 0x04 40 | DELETE = 0x08 41 | DEFAULT = READ 42 | 43 | class Role(db.Document): 44 | name = db.StringField() 45 | permission = db.IntField() 46 | 47 | class User(db.Document): 48 | name = db.StringField() 49 | password = db.StringField() 50 | email = db.StringField() 51 | role = db.ReferenceField('Role', default=DEFAULT_ROLE) 52 | 53 | 这里就简单得创建了一个 Role 的 Model,而 Role 只有一个名称,用于标示这个角色,另外一个就是该角色拥有的权限了。然后就是在 User 中添加了一个 ReferenceField,这个在 MongoEngine 里面就表示是外引用的意思,我们可以直接通过这个成员变量访问到用户的 Role 的 permission。 54 | 55 | 同时,为了保持代码的可维护性,我们将 permission 都写在一个类中,还设置了一个默认的权限,默认为 READ。 56 | 57 | 因为我们现在的数据库中还没有 Role 相关的记录,所以我们需要在启动应用的时候进行插入数据,所以我做了这样的一个操作: 58 | 59 | # init roles 60 | if Role.objects.count() <= 0: 61 | READ_ROLE = Role('READER', Permission.READ) 62 | CREATE_ROLE = Role('CREATER', Permission.CREATE) 63 | UPDATE_ROLE = Role('UPDATER', Permission.UPDATE) 64 | DELETE_ROLE = Role('DELETER', Permission.DELETE) 65 | DEFAULT_ROLE = Role('DEFAULT', Permission.DEFAULT) 66 | 67 | READ_ROLE.save() 68 | CREATE_ROLE.save() 69 | UPDATE_ROLE.save() 70 | DELETE_ROLE.save() 71 | DEFAULT_ROLE.save() 72 | else: 73 | READ_ROLE = Role.objects(permission=Permission.READ).first() 74 | CREATE_ROLE = Role.objects(permission=Permission.CREATE).first() 75 | UPDATE_ROLE = Role.objects(permission=Permission.UPDATE).first() 76 | DELETE_ROLE = Role.objects(permission=Permission.DELETE).first() 77 | DEFAULT_ROLE = Role.objects(permission=Permission.DEFAULT).first() 78 | 79 | 虽然这段代码有不严谨的地方,但是作为讲解的话无关大雅,通过这段代码,我们可以保证在下面的代码中我们有五种 Role 的对象,分别对应着增删改查,还有一个默认的角色,他为读取权限。同时,我们也应该修改一下我们的 API,让他能够增加用户的默认权限。 80 | 81 | @app.route('/', methods=['POST']) 82 | @login_required 83 | def create_record(): 84 | record = json.loads(request.data) 85 | user = User(name=record['name'], 86 | password=record['password'], 87 | email=record['email'], 88 | role=DEFAULT_ROLE) 89 | user.save() 90 | return jsonify(user.to_json()) 91 | 92 | 这段代码只增加了一行,就是: 93 | 94 | role=DEFAULT_ROLE 95 | 96 | ## 权限控制 97 | 98 | 好,到这里算是完成了一半了,我们的角色已经算是有了,然后就是怎么进行权限控制了,我希望权限控制代码能够竟可能得简单,最好是能用装饰器实现,对于一些默认权限就能访问的,我希望不用加权限控制的代码就好了。没有不能实现的需求,只是实现得好坏而已,所以,既然我们都能描述出需求,那么就能够写出满足需求的代码。 99 | 100 | 首先,我们是需要编写一个权限控制的[装饰器](https://liuliqiang.info/python-decorator-description/)的,我们希望这个[装饰器](https://liuliqiang.info/python-decorator-description/)可以很方便得进行权限控制,最好是可以这样: 101 | 102 | @creater_required() 103 | def create_model(): 104 | ... ... 105 | 106 | 或者这样也可以接受: 107 | 108 | @permission_required(CREATE_PERMISSION): 109 | def create_model(): 110 | ... ... 111 | 112 | 那么,就先写一个较为简单的版本试试先: 113 | 114 | def permission_required(permission): 115 | def decorator(func): 116 | @wraps(func) 117 | def decorated_function(*args, **kwargs): 118 | if not current_user.is_authenticated: 119 | abort(401) 120 | user_permission = current_user.role.permission 121 | if user_permission & permission == permission: 122 | return func(*args, **kwargs) 123 | else: 124 | abort(403) 125 | return decorated_function 126 | return decorator 127 | 128 | 这一版本我们可以简单得看这几句关键的代码: 129 | 130 | if not current_user.is_authenticated: 131 | abort(401) 132 | user_permission = current_user.role.permission 133 | if user_permission & permission == permission: 134 | return func(*args, **kwargs) 135 | else: 136 | abort(403) 137 | 138 | 首先用户没有登陆肯定是没有权限的了,所以返回 401 未授权错误,如果用户没有权限(权限设计中的描述),那么就返回 403 禁止访问。 139 | 140 | 接着我们就在我们的 REST API 中尝试一下这个权限,这里相对新增用户进行尝试: 141 | 142 | @app.route('/', methods=['POST']) 143 | @permission_required(Permission.CREATE) 144 | def create_record(): 145 | record = json.loads(request.data) 146 | user = User(name=record['name'], 147 | password=record['password'], 148 | email=record['email'], 149 | role=DEFAULT_ROLE) 150 | user.save() 151 | return jsonify(user.to_json() 152 | 153 | 这里只将 `@login_required` 的装饰器换成了 154 | 155 | @permission_required(Permission.CREATE) 156 | 157 | 然后我们尝试一下新建记录: 158 | 159 | POST http://localhost:8080 160 | 161 | { 162 | "email": "liqianglau@outlook.com", 163 | "name": "tyrael", 164 | "password": "password" 165 | } 166 | 167 | 然后发现响应是: 168 | 169 | ![Image-2016-05-26-020312001.png](https://ooo.0o0.ooo/2016/05/25/5745ea1c6bd61.png) 170 | 171 | 说明我们的权限控制生效啦。 172 | 173 | 174 | -------------------------------------------------------------------------------- /book/docs/chapter007.md: -------------------------------------------------------------------------------- 1 | # 规范结构维护代码 2 | 3 | 到本章为止,我们的 DEMO 程序功能已经日益强大,增删改查,用户登录,权限控制,数据库操作,功能已经有点复杂了,然后看看代码,发现也已经差不多 200 行了。这时,我们不禁要想,难道我们要在这一个 `app.py` 文件中继续编写下去吗?感觉每次添加新的功能好像都是在头(添加引用)在尾(添加逻辑)添加代码,难道这是正确的做法吗? 4 | 5 | 很显然,作为有洁癖的工程师,肯定不能容忍所有代码都这么一团塞在一个文件里面的,所以我们至少会想到拆分代码然后放到几个模块里面,例如什么 model.py, controller.py 啊之类的。但是,作为有深度洁癖的人,觉得把一大堆文件放在一个目录里面也是件糟心的事,所以,这里我要介绍一下我比较推荐的代码目录结构。 6 | 7 | ## Flask 代码目录结构 8 | 9 | 虽然目录结构见仁见智,个人有个人的看法和习惯,但总的来说,经过很多人的实践和总结,还是有很多共同的意见和想法的,而我在查看他人的目录结构结合自身在工作中的使用经验,总结了一个个人认为比较恰当的目录结构供参考,而本书也是以这个目录结构为架构进行下去的。 10 | 11 | 我推荐的目录结构: 12 | 13 | . 14 | ├── README.md 15 | ├── application 16 | │   ├── __init__.py 17 | │   ├── controllers 18 | │   │   └── __init__.py 19 | │   ├── forms 20 | │   │   └── __init__.py 21 | │   ├── models 22 | │   │   └── __init__.py 23 | │   ├── services 24 | │   │   └── __init__.py 25 | │   ├── static 26 | │   │   └── __init__.py 27 | │   ├── templates 28 | │   │   └── __init__.py 29 | │   └── utils 30 | │   └── __init__.py 31 | ├── config 32 | │   ├── __init__.py 33 | │   ├── default.py 34 | │   ├── development.py 35 | │   ├── development_sample.py 36 | │   ├── production.py 37 | │   ├── production_sample.py 38 | │   └── testing.py 39 | ├── deploy 40 | │   ├── flask_env.sh 41 | │   ├── gunicorn.conf 42 | │   ├── nginx.conf 43 | │   └── supervisor.conf 44 | ├── manage.py 45 | ├── pylintrc 46 | ├── requirements.txt 47 | ├── tests 48 | │   └── __init__.py 49 | └── wsgi.py 50 | 51 | 这里稍作介绍,首先是第一级目录的话,主要分为两类,一类是目录,另一类是运行相关的文件;其中目录有: 52 | 53 | - application:项目所有逻辑代码都放这 54 | - config:项目的配置文件,按不同环境各占一份 55 | - deploy:部署相关的文件,后续将使用到 56 | - tests:单元测试代码所在的目录 57 | 58 | 文件的话分别有: 59 | 60 | - manage.py:Flask-Script 运行文件,后面介绍 61 | - pylintrc:静态分析代码使用的 pylint 标准 62 | - requirements.txt:项目依赖库的列表 63 | - wsgi.py:wsgi 运行的文件 64 | 65 | ## 规范代码到指定目录 66 | 67 | 既然我们已经规定好了目录结构,是时候将我们的意面分到各个盘子里了。首先从文件开始,因为我们还没有介绍 Flask-Script,静态检查和 wsgi,所以就忽略这些文件,那么就剩下 requirements.txt 文件了。这个文件的内容都在我们的 《[本书概述](chapter001.md)》中列举了,直接放进去就好了。 68 | 69 | Flask==0.10.1 70 | flask-mongoengine==0.7.5 71 | Flask-Login==0.3.2 72 | Flask-Admin==1.4.0 73 | Flask-Redis==0.1.0 74 | Flask-WTF==0.12 75 | 76 | 然后是时候解耦代码了,我们没有表单,暂时没有 services,没有静态文件也没有页面模板,所以可以这样合并: 77 | 78 | - 将 route 代码放到 application/controllers 中 79 | - 将 model 代码放到 application/models 中 80 | - 将初始化绑定 app 的代码放到 application/__init__.py 中 81 | - 将 数据库等配置放到 config/development.py 中 82 | 83 | 最后就是编写 manager.py 文件了。这里概要得列举几个重要的文件,更多的文件大家可以从 github 上 clone 代码出来阅读。 84 | 85 | ## 合并后的文件 86 | 87 | manager.py 88 | 89 | # coding: utf-8 90 | from flask.ext.script import Manager 91 | from application import create_app 92 | 93 | # Used by app debug & livereload 94 | PORT = 8080 95 | 96 | app = create_app() 97 | manager = Manager(app) 98 | 99 | 100 | @manager.command 101 | def run(): 102 | """Run app.""" 103 | app.run(port=PORT) 104 | 105 | 106 | if __name__ == "__main__": 107 | manager.run() 108 | 109 | application/__init__.py 110 | 111 | # coding: utf-8 112 | import sys 113 | import os 114 | 115 | # Insert project root path to sys.path 116 | project_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 117 | if project_path not in sys.path: 118 | sys.path.insert(0, project_path) 119 | 120 | import logging 121 | from flask import Flask 122 | from flask_wtf.csrf import CsrfProtect 123 | from config import load_config 124 | from application.extensions import db, login_manager 125 | from application.models import User 126 | from application.controllers import user_bp 127 | 128 | # convert python's encoding to utf8 129 | try: 130 | reload(sys) 131 | sys.setdefaultencoding('utf8') 132 | except (AttributeError, NameError): 133 | pass 134 | 135 | 136 | def create_app(): 137 | """Create Flask app.""" 138 | config = load_config() 139 | print config 140 | 141 | app = Flask(__name__) 142 | app.config.from_object(config) 143 | 144 | if not hasattr(app, 'production'): 145 | app.production = not app.debug and not app.testing 146 | 147 | # CSRF protect 148 | CsrfProtect(app) 149 | 150 | if app.debug or app.testing: 151 | # Log errors to stderr in production mode 152 | app.logger.addHandler(logging.StreamHandler()) 153 | app.logger.setLevel(logging.ERROR) 154 | 155 | # Register components 156 | register_extensions(app) 157 | register_blueprint(app) 158 | 159 | return app 160 | 161 | 162 | def register_extensions(app): 163 | """Register models.""" 164 | db.init_app(app) 165 | login_manager.init_app(app) 166 | 167 | login_manager.login_view = 'login' 168 | 169 | @login_manager.user_loader 170 | def load_user(user_id): 171 | return User.objects(id=user_id).first() 172 | 173 | 174 | def register_blueprint(app): 175 | app.register_blueprint(user_bp) 176 | 177 | application/controllers/__init__.py 178 | 179 | #!/usr/bin/env python 180 | # encoding: utf-8 181 | import json 182 | 183 | from flask import Blueprint, request, jsonify 184 | from flask.ext.login import current_user, login_user, logout_user 185 | 186 | from application.models import User 187 | 188 | 189 | user_bp = Blueprint('user', __name__, url_prefix='') 190 | 191 | 192 | @user_bp.route('/login', methods=['POST']) 193 | def login(): 194 | info = json.loads(request.data) 195 | username = info.get('username', 'guest') 196 | password = info.get('password', '') 197 | 198 | user = User.objects(name=username, 199 | password=password).first() 200 | if user: 201 | login_user(user) 202 | return jsonify(user.to_json()) 203 | else: 204 | return jsonify({"status": 401, 205 | "reason": "Username or Password Error"}) 206 | 207 | 208 | @user_bp.route('/logout', methods=['POST']) 209 | def logout(): 210 | logout_user() 211 | return jsonify(**{'result': 200, 212 | 'data': {'message': 'logout success'}}) 213 | 214 | @user_bp.route('/user_info', methods=['POST']) 215 | def user_info(): 216 | if current_user.is_authenticated: 217 | resp = {"result": 200, 218 | "data": current_user.to_json()} 219 | else: 220 | resp = {"result": 401, 221 | "data": {"message": "user no login"}} 222 | return jsonify(**resp) 223 | 224 | config/development.py 225 | 226 | # coding: utf-8 227 | import os 228 | 229 | 230 | class DevelopmentConfig(object): 231 | """Base config class.""" 232 | # Flask app config 233 | DEBUG = False 234 | TESTING = False 235 | SECRET_KEY = "sample_key" 236 | 237 | # Root path of project 238 | PROJECT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 239 | 240 | # Site domain 241 | SITE_TITLE = "twtf" 242 | SITE_DOMAIN = "http://localhost:8080" 243 | 244 | # MongoEngine config 245 | MONGODB_SETTINGS = { 246 | 'db': 'the_way_to_flask', 247 | 'host': 'localhost', 248 | 'port': 27017 249 | } 250 | 251 | -------------------------------------------------------------------------------- /book/docs/chapter008.md: -------------------------------------------------------------------------------- 1 | # 建立目录管理配置 2 | 3 | 在前面一章[《更好得维护代码》](chapter007.md) 中,我们将项目按照功能作用划分到不同的目录中,这样使得我们的项目结构更加清晰和规整了。但是,因为上一章节的内容比较多,如果作为初学者来说,肯定是有好多有疑问的地方,从本章开始都会进行介绍,让大家对 Flask 的使用更加得得心应手。 4 | 5 | 本章主要介绍的是 Flask 中的配置管理,从前面章节[《更好得维护代码》](chapter007.md) 里,可以发现,配置目录 **config** 下包含多个配置文件,为什么要包含这么多文件,而我们要如何处理这些配置文件,都是本章的讲解内容。 6 | 7 | ├── config 8 | │   ├── __init__.py 9 | │   ├── default.py 10 | │   ├── development.py 11 | │   ├── development_sample.py 12 | │   ├── production.py 13 | │   ├── production_sample.py 14 | │   └── testing.py 15 | 16 | ## 环境分类 17 | 18 | 有一个重要的概念是需要我们关注的,那就是**每个配置文件都是与环境相关的**,也就是说,就是因为有多个环境,所以才会出现多个配置。如果不太理解这句话的意思的话,我们看一下 config 目录下的文件名,其实可以分为几类: 19 | 20 | - development: 开发环境,一般为本地开发环境使用 21 | - production:生产环境,一般为线上部署运行环境使用 22 | - testing: 测试环境,一般用于各种测试使用 23 | 24 | 如我们所看到的一样,我们在平时的工作中会有各种环境,我们在本地开发调试的时候应该有个本地环境,当我们转测试之后后会有个测试环境,测试完成之后放到线上之后会有个线上正式环境,而每个环境很难保持配置完全一致,所谓的不一致是指例如数据库信息、程序运行的模型等,例如我们本地的开发环境数据库地址是: 25 | 26 | MongoDB: 27 | version:3.2.6 28 | ip:localhost 29 | port:27017 30 | 31 | 而在生产环境却是: 32 | 33 | MongoDB: 34 | version:3.2.6 35 | ip:192.168.59.104 36 | port:27017 37 | 38 | 所以为了方便开发、测试和部署,我们就会设置多份配置文件,这样就可以快速得在不同环境中运行。如果你有其他的情况,可以随时添加配置文件,完全没问题。 39 | 40 | ## 加载配置 41 | 42 | 那这么多份配置文件,我如何让程序制定加载哪份配置文件呢?这里的奥妙就在 **`config/__init__.py`** 文件中。我们打开这个文件看看: 43 | 44 | # coding: UTF-8 45 | import os 46 | 47 | 48 | def load_config(mode=os.environ.get('MODE')): 49 | """Load config.""" 50 | try: 51 | if mode == 'PRODUCTION': 52 | from .production import ProductionConfig 53 | return ProductionConfig 54 | elif mode == 'TESTING': 55 | from .testing import TestingConfig 56 | return TestingConfig 57 | else: 58 | from .development import DevelopmentConfig 59 | return DevelopmentConfig 60 | except ImportError: 61 | from .default import Config 62 | return Config 63 | 64 | 在 config/__init__.py 文件中,我定义了一个 load_config 函数,这个函数接受一个 `mode` 参数,表示是获取什么环境的配置,如果不传这个参数的话,那默认使用的就是系统环境变量中的 `MODE` 环境变量,然后就根据指定的环境返回指定的配置文件。 65 | 66 | 如果没有指定的配置文件的话,那么就只能返回默认环境变量了。同样的,如果你需要新增自定义的环境配置文件,那么只需要简单得修改这个函数,并且指定加载你自定义的配置文件即可。 67 | 68 | ## 使用配置 69 | 70 | 加载配置这一问题解决之后,接下来就是在我们的 Flask 应用中使用这些配置了,既然都 load 好了配置,那么使用也就问题不大了,这里是一个示例: 71 | 72 | """Create Flask app.""" 73 | config = load_config(mode) 74 | 75 | app = Flask(__name__) 76 | app.config.from_object(config) 77 | 78 | 这里首先将配置 load 出来,然后使用 Flask 对象的 config.from_object 设置配置。就这么简单。 79 | 80 | ## 总结 81 | 82 | 本章对 Flask 中如何配置多环境的配置文件进行了说明和介绍,然后分析了如何加载不同配置文件的原理,最后,给出了一个如何在实际应用中使用配置的示例。 83 | 84 | 85 | -------------------------------------------------------------------------------- /book/docs/chapter009.md: -------------------------------------------------------------------------------- 1 | # 使用 Flask-Script 启动应用 2 | 3 | 看到这章的内容也许你会有疑惑,启动应用?不是很简单吗?我直接使用 4 | 5 | python app.py 6 | 7 | 不就将应用跑起来了吗,而且我还能看到访问的日志呢。是的,没错,直接运行代码是可以将我们编写的 Web 应用跑起来,而且还能很好得查看运行信息,但是,假设你想更换配置呢?例如,你有 development1.py 和 development2.py 两个配置文件,一开始你使用 development1.py 运行,然后你想换成 development2.py 这个配置文件,那么你需要怎么做?根据我们在[《配置管理》](chapter008.md)中介绍的那样,你有两个选择,分别是: 8 | 9 | - 修改系统变量 MODE 10 | - 修改代码,直接指定 create_app('development1') => create_app('development2') 11 | 12 | 看上去都不是很方便,因为这至少涉及到两个动作,第一个是修改模式,第二个是启动应用。那么这个时候,假如我们在运行应用的时候能够指定需要使用的配置文件的话,那不是方便多了,例如: 13 | 14 | python app.py development1 15 | 16 | 这样的话,好像就好多了,确实,这样确实满足了我们的需求,但是,这仅仅满足了一个需求,那万一我们还想看我们的应用对外暴露的 API 有哪些呢?我还想使用我们应用的 python shell 呢?这些都是比较难实现的。然而,作为一个提供丰富扩展的框架,Flask 的贡献者们也已经为我们想到了,并且给我们提供了一个扩展 Flask-Script,可以让我们从这些繁琐的事情中解放出来。 17 | 18 | 可能机智的你已经发现了,在我们的[《更好得维护代码》](chapter007.md)中已经根目录里面多了一个 manage.py 的文件,是的,这个文件就是为使用 Flask-Script 而创建的,而我们启动应用也将使用这个文件。下面就来介绍一下 Flask-Script 的一些知识。 19 | 20 | ## 安装 Flask-Script 21 | 22 | 依旧还是老套路,直接使用 pip 安装既可。 23 | 24 | pip install Flask-Script 25 | 26 | ## 小试身手 27 | 28 | 和我们之前使用过的 Flask 扩展不一样,Flask-Script 不需要获取我们 app 的配置信息,所以就不用使用 init_app 这样的初始化操作了,但是,毕竟 app 是我们的 Flask 服务器,所以还是需要使用到它,所以我们一开始的启动脚本可以这样写: 29 | 30 | # coding: utf-8 31 | from flask_script import Manager 32 | from application import create_app 33 | 34 | app = create_app('development') 35 | manager = Manager(app) 36 | 37 | 38 | if __name__ == "__main__": 39 | manager.run() 40 | 41 | 我们这就做了两个操作,分别是: 42 | 43 | manager = Manager(app) 44 | manager.run() 45 | 46 | 很奇怪的是,和我们最开始的运行的相比,好像更复杂了,因为我们最初的版本直接这样跑就可以了: 47 | 48 | app.run() 49 | 50 | 那为什么要多一个 manager 呢?因为manager 可以做更多的操作,例如指定运行参数,查看所有 API 等。 51 | 52 | ## 指定运行参数 53 | 54 | 如果我们想指定配置文件,Flask-Script 提供了一个 -c 的参数,然后可以这样做: 55 | 56 | #!/usr/bin/env python 57 | # encoding: utf-8 58 | from flask_script import Manager 59 | from application import create_app 60 | 61 | manager = Manager(create_app) 62 | manager.add_option('-c', '--config', dest='mode', required=False) 63 | 64 | 65 | if __name__ == "__main__": 66 | manager.run() 67 | 68 | 其实这里做的改变就是不直接创建 app,而是将创建方法直接传给 Manager,然后多了重要的一行,那就是: 69 | 70 | manager.add_option('-c', '--config', dest='mode', required=False) 71 | 72 | 通过名字可以发现这是一个添加选项的语句,默认就是给我们的 create_app 添加参数选项,然后我们看一下它的参数分别有哪些: 73 | 74 | - -c : 运行参数的简写 75 | - --config : 运行参数的全写 76 | - dest : 传递给 create_app 的参数名字,因为我们是 create_app(mode) ,所以这里是 'mode' 77 | - required : 是否是必须的,这里因为有默认的 mode ,所以不需要必选。 78 | 79 | 就这样我们就可以给 create_app 传递参数了,那怎么传,这样子: 80 | 81 | python manage.py -c development # 开发环境运行 82 | python manage.py -c testing # 测试环境运行 83 | 84 | 就是这么简单,我们就可以在执行得时候指定要运行的环境了。 85 | 86 | ## 查看所有暴露的 API 87 | 88 | 很多时候,因为我们的项目是多人开发,所以我们经常会不知道我们的代码暴露了哪些 API。事实上,这在多人协作的项目中是非常常见的问题,例如我经历过的两个企业,其中一家是世界500强的 IT 企业,都存在这个问题,而他们的解决方法就是使用一份 Excel 管理暴露的接口,而这些接口都由各个模块的负责人自己填写,当然,后面我开发了一个统一管理系统对 API 进行管理,但毕竟在大的企业中,很难协调好所有的部门和产品,所以还是很难有全局的视角。 89 | 90 | 但是,在 Flask 中,我们就不需要担心有这种问题了,因为我们的 Flask-Script 还是提供了一个命令可以快速得帮助我们列举出我们的公开接口,使用上也很简单,直接这样写即可: 91 | 92 | from flask_script.commands import ShowUrls 93 | 94 | manager.add_command("showurls", ShowUrls()) 95 | 96 | 然后,我们在控制台上敲以下命令: 97 | 98 | python manage.py showurls 99 | 100 | 你很惊喜得会发现这些输出: 101 | 102 | Rule Endpoint 103 | --------------------------------------- 104 | /login user.login 105 | /logout user.logout 106 | /static/ static 107 | /user_info user.user_info 108 | 109 | 它将我们所有的公开接口都打印出来了,但是,可能你也发现了,不完善的地方就是这里只给出了 URI,并没有给出请求方法,例如 GET、POST 等。这是有待提高的地方。 110 | 111 | ## 总结 112 | 113 | 本章介绍了 Flask-Script 这一扩展的使用,并且介绍了两个用法,分别是使用指定参数启动应用以及查看所有暴露出来的 API URI,但是也许你会有一些新的想法,但是 Flask-Script 并没有提供给你,没关系,Flask 作为一个对扩展友好的框架,你有任何想法都可以通过扩展来实现,更多的详情读者可以参考[Flask-Script官方文档](https://flask-script.readthedocs.io/en/latest/)来实现。 114 | 115 | -------------------------------------------------------------------------------- /book/docs/chapter010.md: -------------------------------------------------------------------------------- 1 | # 使用 Flask-Admin 管理数据库数据 2 | 3 | 我们回过头来看看我们到目前为止的 REST API,发现好像现在都不知道有多少条 User 记录了,甚至于连获取所有 User 记录的 API 都没提供,更别说随便查看用户的记录了。面对这个困境, Flask 的扩展是否还能给我们更多的惊喜呢?答案肯定还是可以的。这一章节,我将带读者认识一个 Flask 中的管理扩展 —— Flask-Admin。 4 | 5 | 使用 Flask-Admin,我们可以方便快捷得管理我们的 Model 数据,让我们能够省去一大堆开发管理系统的时间,而更多得将精力放到梳理业务逻辑之上,下面就开始讲解一下如何使用 Flask-Admin。 6 | 7 | ## 安装 Flask-Admin 8 | 9 | 没有什么特殊的,还是直接使用 pip 安装: 10 | 11 | pip install Flask-Admin==1.4.0 12 | 13 | 就直接安装上了 Flask-Admin 扩展,然后等待后续使用 14 | 15 | ## 初始化 Flsak-Admin 16 | 17 | 和其他常见扩展一般,Flask-Admin 还是需要和我们的 app 服务器绑定,所以还是老套路,但是,因为我们规范化了我们的目录结构,所以这里我们需要注意的是,创建 Flask-Admin 对象要放在 application/extensions.py 文件中,所以在我们的 application/extensions.py 中已经写入以下语句: 18 | 19 | from flask.ext.admin import Admin 20 | admin = Admin() 21 | 22 | 接下来就是要和我们的 Flask 服务器进行绑定了,还是老套路,不过还是因为规范化的原因,我们的绑定需要放到 **application/__init__.py** 中执行,那就需要在 **application/__init__.py** 文件中的 register_extensions 函数中添加以下语句: 23 | 24 | from application.extensions import admin 25 | 26 | admin.init_app(app) 27 | 28 | 然后就算完成了,接下来,我们运行服务器试试,此时我们运行服务器是使用以下语句了: 29 | 30 | python manage.py runserver 31 | 32 | ## 初见 Flask-Admin 33 | 34 | 当我们服务器跑起来之后,我们要想看到管理界面,只需要在浏览器中输入URL: 35 | 36 | http://localhost:5000/admin 37 | 38 | 你就能看到最简单的管理后台了,但是!!这里面什么都没有,就像这样: 39 | 40 | ![twtf_chapter010_001.png](https://ooo.0o0.ooo/2016/05/29/574ab443a9841.png) 41 | 42 | 看来第一印象不是很好,那么,我们要怎样才能看到东西?其实也不复杂,既然没有数据,那我们就将我们的数据 Model 加进去,怎么加,下面给出一个简单的例子,同样还是在 application/__init__.py 中: 43 | 44 | from flask_admin.contrib.mongoengine import ModelView 45 | 46 | from application.models import User, Role 47 | 48 | def register_extensions(app): 49 | admin.init_app(app) 50 | admin.add_view(ModelView(User)) 51 | admin.add_view(ModelView(Role)) 52 | 53 | 这样就可以了,还是那样,重启一下我们的服务器再访问: 54 | 55 | http://localhost:5000/admin 56 | 57 | 这个时候,我们会发现有两个 Model 了: 58 | 59 | ![twtf_chapter010_002.png](https://ooo.0o0.ooo/2016/05/29/574ab739927e1.png) 60 | 61 | ## 操作 Flask-Admin 62 | 63 | 在后台中我们可以看到一些选项,例如List、Create、With select,然后这些选项下面是一个表格,也许你会发现这个表格是空的,那是因为你的数据库中没有数据,所以是空的很自然,那么我们要怎么添加数据呢?试试点一下 “Create” 看下: 64 | 65 | ![twtf_chapter010_003.png](https://ooo.0o0.ooo/2016/05/29/574ab85fce70a.png) 66 | 67 | 看到的是这个,我们填充完各个字段之后,点提交就能看到表格中有数据了。 68 | 69 | ![twtf_chapter010_004.png](https://ooo.0o0.ooo/2016/05/29/574ab8c1b38c9.png) 70 | 71 | ## 总结 72 | 73 | 本章很简约得介绍了 Flask 中的管理扩展 Flask-Admin,并且演示了如何添加管理我们的 Model 数据,并且简单得介绍了一下支持的操作,但是这些都只是皮毛,如果读者有兴趣的话,可以阅读我的文章[《Flask-Admin》](https://liuliqiang.info/flask-admin-turtorise/)了解更多知识,也可以查看 Flask-Admin 的[官方文档](https://flask-admin.readthedocs.io/en/latest/)学习关于 Flask-Admin 的内容。 74 | 75 | -------------------------------------------------------------------------------- /book/docs/chapter012.md: -------------------------------------------------------------------------------- 1 | # 编写 TODO 应用【part002】 2 | 3 | ## 设置配置 4 | 5 | 配置的话,我们全放在 config 目录下,并且按环境划分,因为只使用到开发环境,所以就只设置了开发环境的: 6 | 7 | config/__init__.py 8 | 9 | # coding: UTF-8 10 | import os 11 | 12 | 13 | def load_config(mode=os.environ.get('MODE')): 14 | """Load config.""" 15 | try: 16 | if mode == 'PRODUCTION': 17 | from .production import ProductionConfig 18 | return ProductionConfig 19 | elif mode == 'TESTING': 20 | from .testing import TestingConfig 21 | return TestingConfig 22 | else: 23 | from .development import DevelopmentConfig 24 | return DevelopmentConfig 25 | except ImportError: 26 | from .default import Config 27 | return Config 28 | 29 | config/development.py 30 | 31 | # coding: utf-8 32 | import os 33 | 34 | 35 | class DevelopmentConfig(object): 36 | """Base config class.""" 37 | # Flask app config 38 | DEBUG = False 39 | TESTING = False 40 | SECRET_KEY = "sample_key" 41 | 42 | # Root path of project 43 | PROJECT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 44 | 45 | # Site domain 46 | SITE_TITLE = "twtf" 47 | SITE_DOMAIN = "http://localhost:8080" 48 | 49 | # MongoEngine config 50 | MONGODB_SETTINGS = { 51 | 'db': 'the_way_to_flask', 52 | 'host': '192.168.59.103', 53 | 'port': 27017 54 | } 55 | 56 | ## 配置运行脚本 57 | 58 | 到此,我们的应用代码算是写完了,然后就是运行服务器了,还是使用 Flask-Script,所以我们需要配置 manage.py,内容为; 59 | 60 | manage.py 61 | 62 | #!/usr/bin/env python 63 | # encoding: utf-8 64 | from flask_script import Manager 65 | from flask_script.commands import ShowUrls 66 | 67 | from application import create_app 68 | 69 | manager = Manager(create_app) 70 | manager.add_option('-c', '--config', dest='mode', required=False) 71 | 72 | manager.add_command("showurls", ShowUrls()) 73 | 74 | if __name__ == "__main__": 75 | manager.run() 76 | 77 | 78 | ## 运行服务器 79 | 80 | pyhton manage.py -c development runserver 81 | 82 | 当你看到以下语句的时候说明你的服务器运行成功了: 83 | 84 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 85 | 86 | ## 添加用户 87 | 88 | 因为现在数据库中是没有用户的,所以我们需要手动添加一个用户先,在管理后台可以添加: 89 | 90 | http://localhost:5000/admin/user/ 91 | 92 | ![](imgs/twtf_chapter012_001.png) 93 | ![](imgs/twtf_chapter012_002.png) 94 | 95 | ## 测试功能: 96 | 97 | 登录: 98 | 99 | POST /auth/login HTTP/1.1 100 | Host: localhost:5000 101 | 102 | {"username": "zhangsan", 103 | "password": "password"} 104 | 105 | 响应应该是: 106 | 107 | { 108 | "email": "zhangsan@gmail.com", 109 | "name": "zhangsan", 110 | "role": "ADMIN" 111 | } 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /book/docs/chapter013.md: -------------------------------------------------------------------------------- 1 | # 使用 Gunicorn 和 Nginx 部署 Flask 项目 2 | 3 | 在实际的生产环境中,我们很少是直接使用命令: 4 | 5 | python app.py 6 | 7 | 运行 Flask 应用提供服务的,正常都会集成 WSGI Web服务器提供服务,而在众多的 WSGI Web 服务器中,比较常用的主要有两种,分别是 Gunicorn 和 UWSGI,同时,我们也会使用 Nginx 作为反向代理进行部署应用。 8 | 9 | 本文因为需要安装 Nginx,所以文章内的命令和使用的系统相关,但是这样的命令不多,本文使用的 **Ubuntu 16.04**,因此包管理软件是 **apt**,如果使用的 RedHat 系列的话,那完全可以用 **yum** 代替。其他系列的系统可以查找相关文档寻找代替管理工具。 10 | 11 | ## 安装组件 12 | 13 | sudo apt-get update 14 | sudo apt-get install python-pip python-dev nginx 15 | 16 | pip install gunicorn 17 | pip install flask 18 | 19 | 这里前两句是更新一下软件源,并且保证我们的 pip 和 python 依赖库已经安装上了,同时,别忘了安装反向代理 Nginx。后面两句就是安装我们必备的 Gunicorn 和 Flask Python 库了。 20 | 21 | ## 下载代码 22 | 23 | 因为在我们的前文中已经写了一个代码了,所以这里就继续使用这段代码,使用方式是: 24 | 25 | git clone git@github.com:luke0922/the-way-to-flask.git 26 | cd the-way-to-flask/code 27 | pip install -r requirements.txt 28 | python manage.py runserver 29 | 30 | 此时,我们的服务器应该是已经运行起来了,但是,默认 Ubuntu 是开启了防火墙屏蔽所有端口访问的,所以我们可能需要打开防火墙端口,在 Ubuntu 16.04 中可以这样做: 31 | 32 | sudo ufw allow 5000 33 | 34 | 现在,应该可以访问我们的应用了,在命令行上我们敲一下这个命令,访问以下 WEB 服务: 35 | 36 | http://localhost:5000 37 | 38 | 一切正常的话, 39 | 40 | ## 创建 WSGI 切入点 41 | 42 | 43 | vim wsgi.py 44 | 45 | 里面内容填: 46 | 47 | from myproject import app 48 | 49 | if __name__ == "__main__": 50 | app.run() 51 | 52 | 然后使用这个命令运行代码: 53 | 54 | gunicorn --bind 0.0.0.0:5000 wsgi:app 55 | 56 | 依旧访问这个地址看看: 57 | 58 | http://localhost:5000 59 | 60 | ## 常见 systemd Unit File 61 | 62 | vim /etc/systemd/system/app.service 63 | 64 | 里面的内容写: 65 | 66 | [Unit] 67 | Description=Gunicorn instance to serve myproject 68 | After=network.target 69 | 70 | [Service] 71 | User=www 72 | Group=www 73 | WorkingDirectory=/home/www/myproject 74 | Environment="PATH=/home/www/myproject/myprojectenv/bin" 75 | ExecStart=/home/www/myproject/myprojectenv/bin/gunicorn --workers 3 --bind unix:myproject.sock -m 007 wsgi:app 76 | 77 | [Install] 78 | WantedBy=multi-user.target 79 | 80 | 保存退出,然后尝试一下命令: 81 | 82 | sudo systemctl start app 83 | sudo systemctl enable app 84 | 85 | ## 配置 Nginx 86 | 87 | 配置Nginx 88 | 89 | sudo nano /etc/nginx/sites-available/myproject 90 | 91 | 里面写: 92 | 93 | server { 94 | listen 80; 95 | server_name server_domain_or_IP; 96 | 97 | location / { 98 | include proxy_params; 99 | proxy_pass http://unix:/home/sammy/myproject/myproject.sock; 100 | } 101 | } 102 | 103 | 保存之后,用 nginx 自带工具验证一遍 104 | 105 | nginx -t 106 | 107 | 如果ok的话然后让 nginx 重新加载配置 108 | 109 | nginx -s reload 110 | 111 | 关闭服务器端口: 112 | 113 | - sudo ufw delete allow 5000 114 | - sudo ufw allow 'Nginx Full' 115 | 116 | 此时访问服务器试试: 117 | 118 | http://192.168.59.103 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /book/docs/imgs/chapter002-001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/docs/imgs/chapter002-001.png -------------------------------------------------------------------------------- /book/docs/imgs/chapter003-001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/docs/imgs/chapter003-001.png -------------------------------------------------------------------------------- /book/docs/imgs/chapter003-002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/docs/imgs/chapter003-002.png -------------------------------------------------------------------------------- /book/docs/imgs/chapter003-003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/docs/imgs/chapter003-003.png -------------------------------------------------------------------------------- /book/docs/imgs/chapter003-004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/docs/imgs/chapter003-004.png -------------------------------------------------------------------------------- /book/docs/imgs/image0001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/docs/imgs/image0001.png -------------------------------------------------------------------------------- /book/docs/imgs/image0002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/docs/imgs/image0002.png -------------------------------------------------------------------------------- /book/docs/imgs/twtf_chapter010_001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/docs/imgs/twtf_chapter010_001.png -------------------------------------------------------------------------------- /book/docs/imgs/twtf_chapter010_002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/docs/imgs/twtf_chapter010_002.png -------------------------------------------------------------------------------- /book/docs/imgs/twtf_chapter010_003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/docs/imgs/twtf_chapter010_003.png -------------------------------------------------------------------------------- /book/docs/imgs/twtf_chapter010_004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/docs/imgs/twtf_chapter010_004.png -------------------------------------------------------------------------------- /book/docs/imgs/twtf_chapter012_001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/docs/imgs/twtf_chapter012_001.png -------------------------------------------------------------------------------- /book/docs/imgs/twtf_chapter012_002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/book/docs/imgs/twtf_chapter012_002.png -------------------------------------------------------------------------------- /book/docs/index.md: -------------------------------------------------------------------------------- 1 | The Way To Flask 2 | ======= 3 | 4 | ### 本文目标 5 | 6 | 通过讲解 Flask 以及它的扩展们,介绍通用用法以及使用过程中的问题和坑,帮助读者使用 Python 编程语言快速得开发健壮的 Web(API)服务端程序。本书在编写之初以及编写过程中始终坚持以下几条原则: 7 | 8 | - 让 Python 初学者/会其他语言但没用过 Python 的人能快速入手 9 | - 循序渐进得让读者感受 Flask 的简便与强大 10 | - 以生动有趣的语言讲述 Flask 从入门到着迷 11 | 12 | ## Flask 简介 13 | 14 | Flask 是一个使用 Python 编写的轻量级 Web 应用框架,核心的思想就是自身尽可能提供少的东西,作为一个微框架,将更多的内容以插件的形式提供,因此,衍生出了一系列以 Flask 为核心的 插件。截止至2016年06月02日,在 [Github](https://github.com/pallets/flask) 上已有 **20730** 个星,**6426** 个 Fork 以及 **1511** 个 Watch。 15 | 16 | 通过使用 pip 包管理工具统计,Flask 的扩展已经达到 **800+**,涵盖大部分日常工作使用到的内容。 17 | 18 | ### 声明 19 | 20 | 本文由 [Yetship](https://liuliqiang.info) 编写,使用 [GNU FDL v1.3 Licence](http://www.gnu.org/licenses/fdl-1.3.html) 发布,如有转载、商业使用等用途,请在 Licence 的约束下进行,本人保留一切权利。 21 | 22 | ### 联系我 23 | 24 | 如果对本书提到的知识点有不解或者觉得有误,可以根据以下联系方式与我联系,同时,欢迎大家一起编撰修改本书,让更多的人能够喜爱 Flask。 25 | 26 | - 主页:https://liuliqiang.info 27 | - 邮箱:liqianglau@outlook.com 28 | - Gitbook: https://luke0922.gitbooks.io/the-way-to-flask/content/ 29 | - GitHub: https://github.com/luke0922/the-way-to-flask.git 30 | 31 | ### 更新记录 32 | 33 | ## Version 1.0 34 | 35 | - date: 2016-6-2 36 | - desc: 终于在一个多月的时间里完成了第一版,期间发生了很多事情,但是,还是坚持下来了,完成了第一版的《The Way To Flask》,虽然个人觉得还有很大的改进空间,但至少是有这么粗糙的一版,后面有什么问题,可以根据大家的建议进行改进。 37 | 38 | ## Version 1.1 39 | - date: 2016-6-11 40 | - desc: 在 Pycon2016 上观看了《Flask at Scale》的讲解,对 Flask 的项目有了更多的一些理解,发现了 V1.0 的内容已经符合可维护性的要求,在这个版本中新加入优化性能的部分。 41 | 42 | ## Version 1.2 43 | - begin: 2017-03-01 44 | - end: 2017-03-01 45 | - desc:修改一些文档的错误 46 | 47 | ## Version1.3 48 | - begin: 2017-05-01 49 | - end: 2017-05-01 50 | - desc:使用 mkdocs 重构文档 51 | 52 | -------------------------------------------------------------------------------- /book/docs/introduct.md: -------------------------------------------------------------------------------- 1 | The Way To Flask 2 | ======= 3 | 4 | ### 本文目标 5 | 6 | 通过讲解 Flask 以及它的扩展们,介绍通用用法以及使用过程中的问题和坑,帮助读者使用 Python 编程语言快速得开发健壮的 Web(API)服务端程序。本书在编写之初以及编写过程中始终坚持以下几条原则: 7 | 8 | - 让 Python 初学者/会其他语言但没用过 Python 的人能快速入手 9 | - 循序渐进得让读者感受 Flask 的简便与强大 10 | - 以生动有趣的语言讲述 Flask 从入门到着迷 11 | 12 | ## Flask 简介 13 | 14 | Flask 是一个使用 Python 编写的轻量级 Web 应用框架,核心的思想就是自身尽可能提供少的东西,作为一个微框架,将更多的内容以插件的形式提供,因此,衍生出了一系列以 Flask 为核心的 插件。截止至2016年06月02日,在 [Github](https://github.com/pallets/flask) 上已有 **20730** 个星,**6426** 个 Fork 以及 **1511** 个 Watch。 15 | 16 | 通过使用 pip 包管理工具统计,Flask 的扩展已经达到 **800+**,涵盖大部分日常工作使用到的内容。 17 | 18 | ### 声明 19 | 20 | 本文由 [Yetship](https://liuliqiang.info) 编写,使用 [GNU FDL v1.3 Licence](http://www.gnu.org/licenses/fdl-1.3.html) 发布,如有转载、商业使用等用途,请在 Licence 的约束下进行,本人保留一切权利。 21 | 22 | ### 联系我 23 | 24 | 如果对本书提到的知识点有不解或者觉得有误,可以根据以下联系方式与我联系,同时,欢迎大家一起编撰修改本书,让更多的人能够喜爱 Flask。 25 | 26 | - 主页:https://liuliqiang.info 27 | - 邮箱:liqianglau@outlook.com 28 | - Gitbook: https://luke0922.gitbooks.io/the-way-to-flask/content/ 29 | - GitHub: https://github.com/luke0922/the-way-to-flask.git 30 | 31 | ### 更新记录 32 | 33 | ## Version 1.0 34 | 35 | - date: 2016-6-2 36 | - desc: 终于在一个多月的时间里完成了第一版,期间发生了很多事情,但是,还是坚持下来了,完成了第一版的《The Way To Flask》,虽然个人觉得还有很大的改进空间,但至少是有这么粗糙的一版,后面有什么问题,可以根据大家的建议进行改进。 37 | 38 | ## Version 1.1 39 | - date: 2016-6-11 40 | - desc: 在 Pycon2016 上观看了《Flask at Scale》的讲解,对 Flask 的项目有了更多的一些理解,发现了 V1.0 的内容已经符合可维护性的要求,在这个版本中新加入优化性能的部分。 41 | 42 | ## Version 1.2 43 | - begin: 2017-03-01 44 | - end: 45 | - desc:修改一些文档的错误 46 | 47 | -------------------------------------------------------------------------------- /book/docs/part001.md: -------------------------------------------------------------------------------- 1 | # 第一部分 2 | 3 | Flask 快速入门 4 | 5 | * [本书概述](chapter001.md) 6 | * [简单的 Flask 应用](chapter002.md) 7 | * [简单的 REST 服务](chapter003.md) 8 | 9 | -------------------------------------------------------------------------------- /book/docs/part002.md: -------------------------------------------------------------------------------- 1 | # 第二部分 2 | 3 | Flask 插件使用指南 4 | 5 | * [集成数据库](chapter004.md) 6 | * [注册登录](chapter005.md) 7 | * [权限控制](chapter006.md) 8 | * [更好得维护代码](chapter007.md) 9 | * [配置管理](chapter008.md) 10 | 11 | -------------------------------------------------------------------------------- /book/docs/part003.md: -------------------------------------------------------------------------------- 1 | # 第三部分 2 | 3 | Flask 项目实战 4 | 5 | * [TODO](chapter009.md) 6 | * [TODO](chapter010.md) 7 | * [TODO](chapter011.md) 8 | * [TODO](chapter012.md) 9 | 10 | -------------------------------------------------------------------------------- /book/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: The Way to Flask 2 | pages: 3 | - 目录: SUMMARY.md 4 | - 前言: index.md 5 | - 第一部分: part001.md 6 | - 本书概述: chapter001.md 7 | - 简单的 Flask 应用: chapter002.md 8 | - 简单的 REST 服务: chapter003.md 9 | - 第二部分: part002.md 10 | - 集成数据库: chapter004.md 11 | - 注册登录: chapter005.md 12 | - 权限控制: chapter006.md 13 | - 更好得维护代码: chapter007.md 14 | - 配置管理: chapter008.md 15 | - 启动应用: chapter009.md 16 | - 管理数据库数据: chapter010.md 17 | - 第三部分: part003.md 18 | - 编写 TODO 应用【part1】: chapter011.md 19 | - 编写 TODO 应用【part2】: chapter012.md 20 | - 使用 Gunicorn 和 Nginx 部署项目: chapter013.md 21 | theme: readthedocs 22 | -------------------------------------------------------------------------------- /code/README.md: -------------------------------------------------------------------------------- 1 | twtf 2 | ======= 3 | 4 | Introduction to twtf. -------------------------------------------------------------------------------- /code/application/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | import os 4 | import sys 5 | import logging 6 | import logging.handlers 7 | from datetime import datetime 8 | 9 | from flask import Flask, current_app 10 | from flask_admin.contrib.mongoengine import ModelView 11 | 12 | from config import load_config 13 | from application.extensions import db, login_manager, admin, jwt 14 | from application.models import User, Role 15 | from application.controllers import all_bp 16 | 17 | # convert python's encoding to utf8 18 | try: 19 | reload(sys) 20 | sys.setdefaultencoding('utf8') 21 | except (AttributeError, NameError): 22 | pass 23 | 24 | 25 | def create_app(mode): 26 | """Create Flask app.""" 27 | config = load_config(mode) 28 | 29 | app = Flask(__name__) 30 | app.config.from_object(config) 31 | 32 | if not hasattr(app, 'production'): 33 | app.production = not app.debug and not app.testing 34 | 35 | # Register components 36 | configure_logging(app) 37 | register_extensions(app) 38 | register_blueprint(app) 39 | 40 | return app 41 | 42 | 43 | def register_extensions(app): 44 | """Register models.""" 45 | db.init_app(app) 46 | login_manager.init_app(app) 47 | 48 | # flask-admin configs 49 | admin.init_app(app) 50 | admin.add_view(ModelView(User)) 51 | admin.add_view(ModelView(Role)) 52 | 53 | login_manager.login_view = 'auth.login' 54 | 55 | @login_manager.user_loader 56 | def load_user(user_id): 57 | return User.objects(id=user_id).first() 58 | 59 | # jwt config 60 | def jwt_authenticate(username, password): 61 | logging.info("username:{}\npassword:{}\n".format(username, password)) 62 | user = User.objects(name=username, password=password).first() 63 | return user 64 | 65 | def jwt_identity(payload): 66 | logging.info("payload:{}".format(payload)) 67 | user_id = payload['identity'] 68 | return User.objects(id=user_id).first() 69 | 70 | def make_payload(identity): 71 | iat = datetime.utcnow() 72 | exp = iat + current_app.config.get('JWT_EXPIRATION_DELTA') 73 | nbf = iat + current_app.config.get('JWT_NOT_BEFORE_DELTA') 74 | identity = str(identity.id) 75 | return {'exp': exp, 'iat': iat, 'nbf': nbf, 'identity': identity} 76 | 77 | jwt.authentication_handler(jwt_authenticate) 78 | jwt.identity_handler(jwt_identity) 79 | jwt.jwt_payload_handler(make_payload) 80 | 81 | jwt.init_app(app) 82 | 83 | 84 | def register_blueprint(app): 85 | for bp in all_bp: 86 | app.register_blueprint(bp) 87 | 88 | 89 | def configure_logging(app): 90 | logging.basicConfig() 91 | if app.config.get('TESTING'): 92 | app.logger.setLevel(logging.CRITICAL) 93 | return 94 | elif app.config.get('DEBUG'): 95 | app.logger.setLevel(logging.DEBUG) 96 | return 97 | 98 | app.logger.setLevel(logging.INFO) 99 | 100 | info_log = os.path.join("running-info.log") 101 | info_file_handler = logging.handlers.RotatingFileHandler( 102 | info_log, maxBytes=104857600, backupCount=10) 103 | info_file_handler.setLevel(logging.DEBUG) 104 | info_file_handler.setFormatter(logging.Formatter( 105 | '%(asctime)s %(levelname)s: %(message)s ' 106 | '[in %(pathname)s:%(lineno)d]') 107 | ) 108 | app.logger.addHandler(info_file_handler) 109 | -------------------------------------------------------------------------------- /code/application/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | import auth 4 | import user 5 | import todo 6 | 7 | all_bp = [ 8 | auth.auth_bp, 9 | user.user_bp, 10 | todo.todo_bp 11 | ] 12 | -------------------------------------------------------------------------------- /code/application/controllers/auth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | import json 4 | 5 | from flask import Blueprint, request, jsonify 6 | from flask_login import login_user, logout_user 7 | 8 | import application.models as Models 9 | 10 | 11 | auth_bp = Blueprint('auth', __name__, url_prefix='/auth') 12 | 13 | 14 | @auth_bp.route('/login', methods=['POST']) 15 | def login(): 16 | print "rdata: {}".format(request.data) 17 | info = json.loads(request.data) 18 | username = info.get('username', 'guest') 19 | password = info.get('password', '') 20 | 21 | user = Models.User.objects(name=username, 22 | password=password).first() 23 | if user: 24 | login_user(user) 25 | return jsonify(user.to_json()) 26 | else: 27 | return jsonify({"status": 401, 28 | "reason": "Username or Password Error"}) 29 | 30 | 31 | @auth_bp.route('/logout', methods=['POST']) 32 | def logout(): 33 | logout_user() 34 | return jsonify(**{'result': 200, 35 | 'data': {'message': 'logout success'}}) 36 | -------------------------------------------------------------------------------- /code/application/controllers/todo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | import json 4 | from datetime import datetime 5 | 6 | from flask import Blueprint, request, jsonify 7 | from flask_jwt import jwt_required, current_identity as current_user 8 | 9 | import application.models as Models 10 | 11 | 12 | todo_bp = Blueprint('todos', __name__, url_prefix='/todo') 13 | 14 | 15 | @todo_bp.route('/item', methods=['POST']) 16 | @jwt_required() 17 | def create_todo_item(): 18 | data = json.loads(request.data) 19 | content = data.get('content') 20 | note = data.get('note', None) 21 | priority = data.get('priority', 0) 22 | 23 | if not content: 24 | return jsonify({ 25 | 'data': {}, 26 | 'msg': 'no content', 27 | 'code': 1001, 28 | 'extra': {}}) 29 | 30 | item = Models.Item(content=content, created_date=datetime.now(), 31 | completed=False, created_by=current_user.id, 32 | notes=[note] if note else [], 33 | priority=priority) 34 | item.save() 35 | return jsonify({ 36 | 'data': item.to_json(), 37 | 'msg': 'create item success', 38 | 'code': 1000, 39 | 'extra': {} 40 | }) 41 | 42 | 43 | @todo_bp.route('/item', methods=['DELETE']) 44 | @jwt_required() 45 | def delete_todo_item(): 46 | data = json.loads(request.data) 47 | id = data.get('id') 48 | 49 | if not id: 50 | return jsonify({ 51 | 'data': {}, 52 | 'msg': 'no id', 53 | 'code': 2001, 54 | 'extra': {}}) 55 | 56 | item = Models.Item.objects(id=id, created_by=str(current_user.id)).first() 57 | item.delete() 58 | return jsonify({ 59 | 'data': item.to_json(), 60 | 'msg': 'delete item success', 61 | 'code': 2000, 62 | 'extra': {} 63 | }) 64 | 65 | 66 | @todo_bp.route('/item', methods=['PUT']) 67 | @jwt_required() 68 | def update_todo_item(): 69 | data = json.loads(request.data) 70 | id = data.get('id') 71 | type = data.get('type') 72 | 73 | if type == "update_content": 74 | content = data.get('content') 75 | Models.Item.objects(id=id, created_by=str(current_user.id)).first().update(content=content) 76 | elif type == "insert_notes": 77 | note = data.get('note') 78 | Models.Item.objects(id=id, created_by=str(current_user.id)).first().update(push__notes=note) 79 | elif type == "done": 80 | Models.Item.objects(id=id, created_by=str(current_user.id)).first().update( 81 | completed=True, completed_date=datetime.now()) 82 | return jsonify({ 83 | 'data': {'oper': type, 84 | 'id': id}, 85 | 'msg': 'oper done', 86 | 'code': 3000, 87 | 'extra': {} 88 | }) 89 | 90 | 91 | @todo_bp.route('/item', methods=['GET']) 92 | @jwt_required() 93 | def get_todo_item(): 94 | query_string = request.args.get('q') 95 | data = json.loads(query_string) 96 | id = data.get('id') 97 | 98 | item = Models.Item.objects(id=id, created_by=str(current_user.id)).first() 99 | return jsonify({ 100 | 'data': item.to_json(), 101 | 'msg': 'query item success', 102 | 'code': 4000, 103 | 'extra': {} 104 | }) 105 | 106 | 107 | @todo_bp.route('/items', methods=['GET']) 108 | @jwt_required() 109 | def get_todo_items(): 110 | data = json.loads(request.args.get('q')) 111 | page = data.get('page', 1) 112 | page_size = data.get('page_size', 10) 113 | 114 | begin = (page - 1) * page_size 115 | end = begin + page_size 116 | items = Models.Item.objects(created_by=str(current_user.id))[begin: end] 117 | rsts = [] 118 | for item in items: 119 | rsts.append(item.to_json()) 120 | 121 | return jsonify({ 122 | 'data': rsts, 123 | 'msg': 'query items success', 124 | 'code': 5000, 125 | 'extra': {} 126 | }) 127 | -------------------------------------------------------------------------------- /code/application/controllers/user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from flask import Blueprint, jsonify 4 | from flask_jwt import jwt_required, current_identity 5 | 6 | 7 | user_bp = Blueprint('users', __name__, url_prefix='') 8 | 9 | 10 | @user_bp.route('/user_info', methods=['POST']) 11 | @jwt_required() 12 | def user_info(): 13 | resp = {"result": 200, 14 | "data": current_identity.to_json()} 15 | return jsonify(**resp) 16 | -------------------------------------------------------------------------------- /code/application/extensions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from flask_jwt import JWT 4 | from flask_admin import Admin 5 | from flask_login import LoginManager 6 | from flask_mongoengine import MongoEngine 7 | 8 | 9 | db = MongoEngine() 10 | login_manager = LoginManager() 11 | admin = Admin() 12 | jwt = JWT() 13 | -------------------------------------------------------------------------------- /code/application/models/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from user import * 4 | from todo import * 5 | 6 | 7 | def all(): 8 | result = [] 9 | models = [user, todo] 10 | 11 | for m in models: 12 | result += m.__all__ 13 | 14 | return result 15 | 16 | __all__ = all() 17 | -------------------------------------------------------------------------------- /code/application/models/todo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from application.extensions import db 4 | 5 | __all__ = ['Item'] 6 | 7 | 8 | class Item(db.Document): 9 | content = db.StringField(required=True) 10 | created_date = db.DateTimeField() 11 | completed = db.BooleanField(default=False) 12 | completed_date = db.DateTimeField() 13 | created_by = db.ReferenceField('User', required=True) 14 | notes = db.ListField(db.StringField()) 15 | priority = db.IntField() 16 | 17 | def __repr__(self): 18 | return "".format(str(self.id), 19 | self.content) 20 | 21 | def to_json(self): 22 | return { 23 | 'id': str(self.id), 24 | 'content': self.content, 25 | 'completed': self.completed, 26 | 'completed_at': self.completed_date.strftime("%Y-%m-%d %H:%M:%S") if self.completed else "", 27 | 'created_by': self.created_by.name, 28 | 'notes': self.notes, 29 | 'priority': self.priority 30 | } 31 | -------------------------------------------------------------------------------- /code/application/models/user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from application.extensions import db 4 | 5 | __all__ = ['Role', 'User'] 6 | 7 | 8 | class Permission: 9 | READ = 0x01 10 | CREATE = 0x02 11 | UPDATE = 0x04 12 | DELETE = 0x08 13 | DEFAULT = READ 14 | 15 | 16 | class Role(db.Document): 17 | name = db.StringField() 18 | permission = db.IntField() 19 | 20 | def __repr__(self): 21 | return "{}-{}".format(self.name, self.permission) 22 | 23 | def __str__(self): 24 | return self.__repr__() 25 | 26 | def __unicode__(self): 27 | return self.__repr__() 28 | 29 | 30 | class User(db.Document): 31 | name = db.StringField() 32 | password = db.StringField() 33 | email = db.StringField() 34 | role = db.ReferenceField('Role') 35 | 36 | @property 37 | def id(self): 38 | return str(self._id) 39 | 40 | def to_json(self): 41 | return {"name": self.name, 42 | "email": self.email, 43 | "role": self.role.name} 44 | 45 | def is_authenticated(self): 46 | return True 47 | 48 | def is_active(self): 49 | return True 50 | 51 | def is_anonymous(self): 52 | return False 53 | 54 | def get_id(self): 55 | return str(self.id) 56 | -------------------------------------------------------------------------------- /code/commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | 4 | import gevent.wsgi 5 | import gevent.monkey 6 | from werkzeug.contrib import profiler 7 | from flask_script import Command 8 | 9 | 10 | class ProfileServer(Command): 11 | """ 12 | Run the server with profiling tools 13 | """ 14 | 15 | def __init__(self, host='localhost', port=9000, **options): 16 | self.port = port 17 | self.host = host 18 | self.server_options = options 19 | 20 | def __call__(self, app, **kwargs): 21 | f = open('profiler.log', 'w') 22 | stream = profiler.MergeStream(sys.stdout, f) 23 | 24 | app.config['PROFILE'] = True 25 | app.wsgi_app = profiler.ProfilerMiddleware(app.wsgi_app, stream, 26 | restrictions=[30]) 27 | app.run(debug=True) 28 | 29 | 30 | class GEventServer(Command): 31 | """ 32 | Run the server with gevent 33 | """ 34 | 35 | def __init__(self, host='127.0.0.1', port=5000, **options): 36 | self.port = port 37 | self.host = host 38 | self.server_options = options 39 | 40 | def __call__(self, app, **kwargs): 41 | gevent.monkey.patch_all() 42 | 43 | ws = gevent.wsgi.WSGIServer(listener=(self.host, self.port), 44 | application=app) 45 | print "* Running on http://{}:{}/ (Press CTRL+C to quit)".format(self.host, self.port) 46 | ws.serve_forever() 47 | -------------------------------------------------------------------------------- /code/config/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: UTF-8 2 | import os 3 | 4 | 5 | def load_config(mode=os.environ.get('MODE')): 6 | """Load config.""" 7 | try: 8 | if mode == 'PRODUCTION': 9 | from .production import ProductionConfig 10 | return ProductionConfig 11 | elif mode == 'TESTING': 12 | from .testing import TestingConfig 13 | return TestingConfig 14 | else: 15 | from .development import DevelopmentConfig 16 | return DevelopmentConfig 17 | except ImportError: 18 | from .default import Config 19 | return Config 20 | -------------------------------------------------------------------------------- /code/config/default.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import os 3 | 4 | 5 | class Config(object): 6 | """Base config class.""" 7 | # Flask app config 8 | DEBUG = False 9 | TESTING = False 10 | SECRET_KEY = "sample_key" 11 | JWT_AUTH_USERNAME_KEY = "username" 12 | JWT_AUTH_PASSWORD_KEY = "password" 13 | 14 | # Root path of project 15 | PROJECT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 16 | 17 | # Site domain 18 | SITE_TITLE = "twtf" 19 | SITE_DOMAIN = "http://localhost:8080" 20 | 21 | # MongoEngine config 22 | MONGODB_SETTINGS = { 23 | 'db': 'the_way_to_flask', 24 | 'host': 'localhost', 25 | 'port': 27017 26 | } 27 | -------------------------------------------------------------------------------- /code/config/development.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import os 3 | 4 | from .default import Config 5 | 6 | 7 | class DevelopmentConfig(Config): 8 | """Base config class.""" 9 | # Flask app config 10 | DEBUG = False 11 | TESTING = False 12 | SECRET_KEY = "sample_key" 13 | 14 | # Root path of project 15 | PROJECT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 16 | 17 | # Site domain 18 | SITE_TITLE = "twtf" 19 | SITE_DOMAIN = "http://localhost:8080" 20 | 21 | # MongoEngine config 22 | MONGODB_SETTINGS = { 23 | 'db': 'the_way_to_flask', 24 | 'host': '192.168.59.103', 25 | 'port': 27017 26 | } 27 | -------------------------------------------------------------------------------- /code/config/development_sample.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from .default import Config 3 | 4 | 5 | class DevelopmentConfig(Config): 6 | # App config 7 | DEBUG = True 8 | 9 | # SQLAlchemy config 10 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root:@localhost/twtf" 11 | -------------------------------------------------------------------------------- /code/config/production.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from .default import Config 3 | 4 | 5 | class ProductionConfig(Config): 6 | # App config 7 | SECRET_KEY = "\xb5\xb3}#\xb7A\xcac\x9d0\xb6\x0f\x80z\x97\x00\x1e\xc0\xb8+\xe9)\xf0}" 8 | PERMANENT_SESSION_LIFETIME = 3600 * 24 * 7 9 | SESSION_COOKIE_NAME = 'twtf_session' 10 | 11 | # Site domain 12 | SITE_DOMAIN = "http://www.twtf.com" 13 | 14 | # Db config 15 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root:password@localhost/twtf" 16 | 17 | # Sentry 18 | SENTRY_DSN = '' 19 | -------------------------------------------------------------------------------- /code/config/production_sample.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from .default import Config 3 | 4 | 5 | class ProductionConfig(Config): 6 | # App config 7 | SECRET_KEY = "\xb5\xb3}#\xb7A\xcac\x9d0\xb6\x0f\x80z\x97\x00\x1e\xc0\xb8+\xe9)\xf0}" 8 | PERMANENT_SESSION_LIFETIME = 3600 * 24 * 7 9 | SESSION_COOKIE_NAME = 'twtf_session' 10 | 11 | # Site domain 12 | SITE_DOMAIN = "http://www.twtf.com" 13 | 14 | # Db config 15 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root:password@localhost/twtf" 16 | 17 | # Sentry 18 | SENTRY_DSN = '' 19 | -------------------------------------------------------------------------------- /code/config/testing.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import os 3 | 4 | from .default import Config 5 | 6 | 7 | class TestingConfig(Config): 8 | # Flask app config 9 | DEBUG = False 10 | TESTING = True 11 | SECRET_KEY = "sample_key" 12 | 13 | # Root path of project 14 | PROJECT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 15 | 16 | # Site domain 17 | SITE_TITLE = "twtf" 18 | SITE_DOMAIN = "http://localhost:8080" 19 | 20 | # MongoEngine config 21 | MONGODB_SETTINGS = { 22 | 'db': 'the_way_to_flask_test', 23 | 'host': '192.168.59.103', 24 | 'port': 27017 25 | } 26 | -------------------------------------------------------------------------------- /code/deploy/flask_env.sh: -------------------------------------------------------------------------------- 1 | export MODE=PRODUCTION 2 | -------------------------------------------------------------------------------- /code/deploy/gunicorn.conf: -------------------------------------------------------------------------------- 1 | workers = 2 2 | bind = '127.0.0.1:8888' 3 | proc_name = 'twtf' 4 | pidfile = '/tmp/twtf.pid' -------------------------------------------------------------------------------- /code/deploy/nginx.conf: -------------------------------------------------------------------------------- 1 | # Website 2 | server { 3 | listen 80; 4 | server_name www.twtf.com; 5 | root /var/www/twtf; 6 | 7 | location / { 8 | proxy_pass http://127.0.0.1:8888/; 9 | proxy_redirect off; 10 | proxy_set_header Host $host; 11 | proxy_set_header X-Real-IP $remote_addr; 12 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 13 | } 14 | 15 | location /static { 16 | root /var/www/twtf/output; 17 | expires 15d; 18 | } 19 | 20 | location /pkg { 21 | root /var/www/twtf/output; 22 | expires 15d; 23 | } 24 | 25 | location /pages { 26 | root /var/www/twtf/output; 27 | expires 15d; 28 | } 29 | 30 | location /uploads { 31 | root /var/www/twtf/; 32 | expires 15d; 33 | } 34 | } 35 | 36 | # 301 redirect 37 | server { 38 | listen 80; 39 | server_name twtf.com; 40 | return 301 http://www.twtf.com$request_uri; 41 | } -------------------------------------------------------------------------------- /code/deploy/supervisor.conf: -------------------------------------------------------------------------------- 1 | [program:twtf] 2 | command=/var/www/twtf/venv/bin/gunicorn -c deploy/gunicorn.conf wsgi:app 3 | directory=/var/www/twtf 4 | user=root 5 | autostart=true 6 | autorestart=true 7 | environment = MODE="PRODUCTION" -------------------------------------------------------------------------------- /code/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from flask_script import Manager 4 | from flask_script.commands import ShowUrls 5 | 6 | from commands import GEventServer, ProfileServer 7 | from application import create_app 8 | 9 | manager = Manager(create_app) 10 | manager.add_option('-c', '--config', dest='mode', required=False) 11 | 12 | manager.add_command("showurls", ShowUrls()) 13 | manager.add_command("gevent", GEventServer()) 14 | manager.add_command("profile", ProfileServer()) 15 | 16 | 17 | if __name__ == "__main__": 18 | manager.run() 19 | -------------------------------------------------------------------------------- /code/requirements.txt: -------------------------------------------------------------------------------- 1 | Click==7.0 2 | enum34==1.1.6 3 | Flask==1.1.1 4 | Flask-Admin==1.5.4 5 | Flask-JWT==0.3.2 6 | Flask-Login==0.3.2 7 | flask-mongoengine==0.7.5 8 | Flask-Redis==0.1.0 9 | Flask-Script==2.0.5 10 | Flask-WTF==0.12 11 | gevent==1.1.1 12 | greenlet==0.4.15 13 | itsdangerous==1.1.0 14 | Jinja2==2.10.3 15 | MarkupSafe==1.1.1 16 | mongoengine==0.18.2 17 | PyJWT==1.4.2 18 | pymongo==3.9.0 19 | redis==3.3.11 20 | six==1.12.0 21 | Werkzeug==0.16.0 22 | WTForms==2.2.1 23 | -------------------------------------------------------------------------------- /code/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/code/tests/__init__.py -------------------------------------------------------------------------------- /code/tests/todo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | from unittest import TestCase 4 | 5 | from application import create_app 6 | from application.extensions import db 7 | 8 | 9 | class TodoTest(TestCase): 10 | def setUp(self): 11 | self.app = create_app('testing') 12 | self.ctx = self.app.app_context() 13 | self.ctx.push() 14 | self.client = self.app.test_client() 15 | db.connection.drop_database('the_way_to_flask_test') 16 | 17 | def tearDown(self): 18 | self.ctx.pop() 19 | -------------------------------------------------------------------------------- /code/wsgi.py: -------------------------------------------------------------------------------- 1 | from application import create_app 2 | 3 | app = create_app() 4 | -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliqiang/the-way-to-flask/09bda3b38c3da49cfdbe44acf8b77a791af39e38/cover.png -------------------------------------------------------------------------------- /introduct.md: -------------------------------------------------------------------------------- 1 | The Way To Flask 2 | ======= 3 | 4 | ###本文目标 5 | 6 | 通过讲解 Flask 以及它的扩展们,介绍通用用法以及使用过程中的问题和坑,帮助读者快速得开发健壮的 Web(API)服务端程序 7 | 8 | ###声明 9 | 10 | 本文由 [Yetship](https://liuliqiang.info) 编写,使用 [GNU FDL v1.3 Licence](http://www.gnu.org/licenses/fdl-1.3.html) 发布,如有转载、商业使用等用途,请在 Licence 的约束下进行,本人保留一切权利。 11 | 12 | ###联系我 13 | 14 | - 主页:https://liuliqiang.info 15 | - 邮箱:liqianglau@outlook.com 16 | - Gitbook: https://luke0922.gitbooks.io/the-way-to-flask/content/ 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------