Tornado项目结构
之前答应过群里几个同学要晒下我们的Tornado项目结构,后来就忘了。。。今天晒出来。
无论是Tornado项目还是Django的项目,大体结构都是一样的。最外层是工程结构,包含了配置、文档、打包等信息,当然还有源码。
项目结构大体都是这样:
代码语言:javascript复制project
- conf/
- docs/
- src/
- package1/
- __init__.py
- models.py
- manager.py
- server.py
- setup.py
- MANIFEST.in
对于项目来说,只考虑两个问题,第一:本地开发是否方便;第二:线上部署是否方便。
开发方便
Django的./manage.py runserver
的方式对于本地开发调试就很方便,所以对于Tornado项目来说,也需要有一个类似的机制可以方便的在开发环境中启动项目。
部署方便
因为我们是采用标准的PyPi包分发的方式部署的项目,所有项目文件最终都会落到site-packages
中,所以包目录的规划就是个问题。
比如像Django那样,把所有的App作为独立的包分散到site-packages
中,还是把源码目录"src"作为独立的包放到site-packages
中。
两种不同的方式,在启动时也有所差别,因为包的路径是不一样的。这里不讨论哪种方式更合理,我们只说实际的使用情况。
所以部署方便的点在于,我把包放到site-packages
中后是否能方便的启动项目。这意味着包的结构需要兼容本地启动和线上启动。
本地和线上的差别
所以就扯到另外一个问题,本地启动项目时,你当前脚本所在的目录就是默认的包根目录,也就是在sys.path
中会加入当前文件所在目录,也就是上面结构中的project/src
。你要引用package1下的模块,直接from package1.models import xxxx
即可。
而在线上的情况是,源码目录是作为独立包放在site-packages
中的。你要引用的话需要from src.package1.models import xxx
。
这种本地和线上不同引用的问题在Django中是没有的,除非你调整了Django的结构。
问题解决
包的依赖路径问题,基本上都可以通过sys.path.insert(<path>)
来解决。
两种解决的方式,一个是改线上的sys.path
,一个是改本地的。线上的改动只需要在项目加载时把src目录先insert
到sys.path中,作为一个新的根路径。
另外一个就是改本地的启动命令,把src所在的目录加到sys.path
中。
从个人的习惯来说,能调整本地逻辑的就不去修改部署环境的逻辑,让其按照默认的方式处理是最稳当的方式。毕竟线上挂了影响很大。
所以我会在本地启动项目是做上面的处理,同时所有包的引用触发是同包的情况下,否则都需要从src开始,也就是:from src.package1.models import xx
。
最终目录结构
代码语言:javascript复制project
├── MANIFEST.in
├── README.md
├── conf
│ └── supervisord.conf
├── fabfile
│ ├── __init__.py
├── requirements.txt
├── setup.cfg
├── setup.py
└── project_src
├── __init__.py
├── handlers
│ ├── __init__.py
│ ├── base.py
│ ├── index.py
├── server.py
├── settings
│ ├── __init__.py
│ ├── base.py
│ └── develop.py
├── templates
│ └── base.html
├── url.py
└── models
├── __init__.py
├── model1.py
└── model2.py
其中一server.py的示例代码如下:
代码语言:javascript复制#!/usr/bin/env python
import os
import sys
from logging.config import dictConfig
from importlib import import_module
import click
import tornado.ioloop
import tornado.web
from raven.contrib.tornado import AsyncSentryClient
def make_app(debug=None):
from project_src.url import urls
assert isinstance(urls, (list, tuple)), 'urls must be list or tuple'
return tornado.web.Application(urls, debug=debug)
def init_log():
dictConfig(tornado.settings.LOGGING)
@click.command()
@click.option('--port', default=9090)
@click.option('--profile', default='develop')
def serve(port, profile):
settings = import_module(f'project_src.settings.{profile}')
tornado.settings = settings
init_log()
app = make_app(settings.DEBUG)
app.listen(port)
app.sentry_client = AsyncSentryClient(
'<sentry>'
)
sys.stdout.write(f"Start server at:http://0.0.0.0:{port} nProfile: {profile}n")
tornado.ioloop.IOLoop.current().start()
if __name__ == "__main__":
current_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, current_path)
serve()
最后
其实无论哪种方式,只要能够保证大家保有共识就可以,比如the5fire不去修改线上的加载路径,目的是避免有人去按照常识去修改包目录之后产生不可预知的后果。