先说,我是python的新手,所以多有谬误,请各位大神放过。
这篇文章只是自己折腾的研究的经验结果,分享出来,献丑了。
一直以来一直寻找一种比较好的方式来组织flask的程序,无论是blueprint、application factories还是底层的werkzeug的DispatcherMiddleware都试过,一直都有这样那样的问题。
实际来说,比如你的网站,突然需要加入一个模块,这个模块的耦合度不是很高,有独立的数据库,然后可能用过一段时间后就卸下来了。这样其实对于flask来说,很多人说用blueprint就好了。其实不然,最简单的,blueprint的耦合度其实不低,单纯的“有独立数据库”这条,你用sqlalchemy就很难实现,当然有人说有SQLALCHEMY_BINDS,但都不是我想要的。因为这种模块,很可能只是一个新人来了之后随便急急忙忙开发的,代码质量不需要去保证很多,因为是临时性的。
好了,不多说了,这篇文章,其实就是为了展现一种标题上描述的,适用于大型架构,同时不失灵活性、各种模块可以方便拔插的flask应用的组织结构,这种方式很类似于django的app,各个模块在不需要耦合的时候可以独立建立project单独开发,之后再合并。
下面来step by step:
1、下载附件中的my_basic_flask.rar,那是一个我用pycharm写的工程,是组成架构的app。(旁边那个显示不出来的图,右键另存后,改后缀名为rar即可解压)
解释一下那个project的各个部分。
A、根目录
结构很明显:根目录是run.py和alembic的配置文件。
run.py其实没什么用,是在pycharm里运行和测试项目用的。
# run.py from basic_app import create_app if __name__ == '__main__': create_app().run()
run.py和alembic.ini都不用改。
B、alembic文件夹
然后到alembic文件夹。用过alembic的都知道,其实它是用alembic init命令生成的,但是我做了修改。基本上里面最重要的就是env.py这个文件了。其实你也可以都不用修改,它就能正常运行。好吧一句话,不要动这个文件夹,让它就那样吧!
另外切记,version文件夹不要删,不然因为权限问题,alembic没办法生成文件夹,就一堆fail了。
C、basic_app文件夹
重点来了。
看看这个文件夹的结构。
2个文件夹admin和auth是2个blueprint,后面再说。先说根目录。
config.py就是放一些设置了
#config.py import os parent_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) DEBUG = True SECRET_KEY = "dev" SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(parent_path, 'app.db')
注意那个parent_path,其实是为了和alembic能一样,所以不要改吧。
__init__.py,这个是应用工厂的基础,所有magic的重点!我贴出来吧。
# __init__.py from flask import Flask from flask.ext.sqlalchemy import SQLAlchemy from . import config db = SQLAlchemy() def load_models(): from .auth import models from .admin import models load_models() def init_extensions(app): db.init_app(app) def init_views(app): from . import auth from . import admin app.register_blueprint(auth.bp, url_prefix="/auth") app.register_blueprint(admin.bp, url_prefix='/admin') def create_app(config=config): app = Flask(__name__) app.config.from_object(config) init_extensions(app) init_views(app) return app
几个注意点。
1、是包名的引用,都是用的相对引用,因为这样才能在之后放到werkzeug.wsgi.DispatcherMiddleware没有问题。
2、是load_models,用这种方法把models加载入内存,才能让alembic正确的识别出数据库对象。
3、是用create_app的应用工厂方法,这个好处很多,包括alembic、middleware和unittest。结构简单明了。
D、admin文件夹
解释一个blueprint,admin包。
static和templates不用说了。
看看__init__.py
from flask import Blueprint bp = Blueprint("admin", __name__, template_folder="templates") from . import views
在这里定义Blueprint,并引入views
# views.py from . import bp from .models import Admin @bp.route('/') def hello(): return 'my basic flask structure is here! Admin' @bp.route('/<username>') def show_user(username='batman'): u = None try: u = Admin.query.filter_by(username=username).first_or_404() except(): pass finally: if u is not None: return 'hello, Admin {} from DB!'.format(u.username) else: return 'hello, Admin {} from param!'.format(username)
这里注意相对引用包。
这里还演示了一个数据库查询。
最后看看models.py
# models.py from .. import db class Admin(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False, unique=True, index=True) password = db.Column(db.String(100), nullable=False)
还是注意相对引用。
E、说说一个难点,alembic的用法
打开cmd,看看我的操作记录。
注意,是引用FlaskEnv这个虚拟Env下的alembic.exe,在my_basic_flask文件夹(当前工程的文件夹)下运行命令行。
D:\>cd D:\PythonProjects\my_basic_flask D:\PythonProjects\my_basic_flask>D:\PythonProjects\FlaskEnv\Scripts\alembic.exe init alembic D:\PythonProjects\my_basic_flask>D:\PythonProjects\FlaskEnv\Scripts\alembic.exe revision --autogenerate -m "create model tables" INFO [alembic.migration] Context impl SQLiteImpl. INFO [alembic.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'group' INFO [alembic.autogenerate.compare] Detected added index 'ix_group_name' on '[' name']' INFO [alembic.autogenerate.compare] Detected added table 'admin' INFO [alembic.autogenerate.compare] Detected added index 'ix_admin_name' on '[' name']' INFO [alembic.autogenerate.compare] Detected added table 'user' INFO [alembic.autogenerate.compare] Detected added index 'ix_user_username' on '['username']' INFO [alembic.autogenerate.compare] Detected added table 'user_group' Generating D:\PythonProjects\my_basic_flask\alembic\versions\cb328c966ae_create_ model_tables.py ... done
做到这步,对于sqlite数据库来说,要去做一个小动作。
进入
D:\PythonProjects\my_basic_flask\alembic\versions\cb328c966ae_create_model_tables.py
删除这几段。
with op.batch_alter_table('group', schema=None) as batch_op: batch_op.create_index(batch_op.f('ix_group_name'), ['name'], unique=True) with op.batch_alter_table('admin', schema=None) as batch_op: batch_op.create_index(batch_op.f('ix_admin_name'), ['name'], unique=True) with op.batch_alter_table('user', schema=None) as batch_op: batch_op.create_index(batch_op.f('ix_user_username'), ['username'], unique=True)
最后继续运行命令,就OK了
D:\PythonProjects\my_basic_flask>D:\PythonProjects\FlaskEnv\Scripts\alembic.exe upgrade head INFO [alembic.migration] Context impl SQLiteImpl. INFO [alembic.migration] Will assume non-transactional DDL. INFO [alembic.migration] Running upgrade -> cb328c966ae, create model tables
F、如何在middleware整合这些app
在生成了数据库后,你的目录结构应该是这样的
ok,把这个项目拷出来,放到一个文件夹下,叫app1吧,并新建一个空的__init__.py,放在根目录下,使这个文件夹成为一个包。run.py可以删了。
现在的目录结构应该是这样了。
好了。这个就是一个基础的开发好的可拔插的包了。下面看看怎么做middleware的分发。
建立一个文件夹all_apps,把app1放进去。
然后放在apache里设定的文件夹,还记得吗?不记得的话,翻一下之前的文章吧!
我的文件夹叫 TestInApache。
然后是2个重要的文件,其实在之前我的说middleware的文章里有过描述。还是放出来吧。这两个文件是在TestInApache文件夹下的,和all_apps同级。
先是run_server.wsgi
#run_server.wsgi activate_this = 'D:/PythonProjects/FlaskEnv/Scripts/activate_this.py' execfile(activate_this, dict(__file__=activate_this)) import site import sys # Remember original sys.path. prev_sys_path = list(sys.path) # Add site-packages directory. site.addsitedir('D:/PythonProjects/FlaskEnv/Lib/site-packages') # Reorder sys.path so new directories at the front. new_sys_path = [] for item in list(sys.path): if item not in prev_sys_path: new_sys_path.append(item) sys.path.remove(item) sys.path[:0] = new_sys_path sys.path.insert(0, 'D:/PythonProjects/TestInApache') sys.path.insert(0, 'D:/PythonProjects/TestInApache/all_apps') from app import app as application
和之前的文章提过的有一行不同,就在这里:
sys.path.insert(0, 'D:/PythonProjects/TestInApache/all_apps')
然后是app.py
#app.py # -*- coding: utf-8 -*- import sys sys.path.insert(0, 'D:/PythonProjects/TestInApache/all_apps') from werkzeug.serving import run_simple from werkzeug.wsgi import DispatcherMiddleware from all_apps.app1 import basic_app from all_apps.index import app as app_index app = DispatcherMiddleware(app_index, { '/app1': basic_app.create_app() })
基本上都是固定的。
如果要引入新的app,就在里面仿照下面的,引入对应的包,然后在DispatcherMiddleware注册上去,就OK了。
from all_apps.app1 import basic_app from all_apps.index import app as app_index app = DispatcherMiddleware(app_index, { '/app1': basic_app.create_app() })
这里我做了个很简单的Index包,来示例一下。
先放一下最终的目录结构
index包的内容其实可以不放的,很简单,但是放上来吧。
#__init__.py from flask import Flask from . import views app = Flask(__name__) app.register_blueprint(views.bp)
#views.py from flask import Blueprint bp = Blueprint("app1", __name__, url_prefix='') @bp.route('/') def index(): return 'simple app!'
基本上可以结束了。最后。
G、说说访问的Url问题
这个可不是个小问题,会迷惑很多人。
我在本机搭建了apache的测试环境,对于这个项目,我做了个example.com的解析来测试。
先看直接访问example.com
明显,是直接由middleware分发到了index包内。
还记得我们做的basic_app包内吗?url应该是什么呢?
应该是http://example.com/admin 吗?
好吧,想起DispatcherMiddleware的设定没?
app = DispatcherMiddleware(app_index, { '/app1': basic_app.create_app() })
试试 http://example.com/app1/admin ,出来了。
然后在试试传入个值,访问 http://example.com/app1/admin/admin_name
好了,基本上没什么说的了。
最后放上整个文件夹,同样是改后缀rar即可。
Fin。