Python Web开发之——构建基于Flask框架的web后端项目

声明:这篇文章主要面向python/Flask/web后端初级开发者,文章主要讲解了如何搭建一个基于Flask的纯web后台项目,以及相关的知识原理。不涉及部署相关操作。由于接触web开发不久,难免会有疏漏的地方,请读者指正。

<br />

下面是文章中会涉及到的内容:
  • HTTP请求发送到后端与响应的过程
  • Why Flask & 概览
  • Flask的常用工具和项目配置
  • pycharm
  • HTTP requester
  • redis
  • 从第一个路由注册接口开始
    • 使用装饰器处理接口必填字段
    • Flask上下文获取request参数
    • 错误处理
    • 数据库连接池
    • 循环引用的那些坑
    • 封装加密方法
    • ORM与Model
    • celery多线程、异步任务
    • 设置response模板
    • 完整的注册接口
    • 使用Blueprint进行模块划分
  • 总结与展望

前言

前些日子转了python后台开发=。= 说实话现在对后端更有兴趣。我很享受这种不管做什么都是在学习新的知识的感觉,像新生儿一样对这个世界充满好奇。

这篇文章总结了我最近一段时间的学习成果:使用Flask框架搭建一个可扩展的中小型web service,并在其中加上一些原理的阐述或者链接。在本文中以实际的用户模块为例。之所以写这篇文章是因为自己在入门的时候遇到了很多坑,文档或者个人博客并不能满足我的需要,不是很基础(毫无架构可言,而且大多是不实用的博客项目)就是特别复杂。在此感谢我的同学/同事evolsnow,在开发学习过程中给了我很大的帮助。也希望这篇文章能帮到想入门python/Flask的同学。

HTTP请求发送到后端与响应过程

在进行项目搭建之前,我们先大致回顾一下一个HTTP请求是如何发送至后端并且响应的。

  1. 通讯双方进行连接
    首先通讯双方遵从HTTP协议。当我们输入这样一个请求:http://www.test.com/api/info ,首先请求端会进行DNS解析,把http://www.test.com 变成一个ip地址,如果url里不包含端口号,则会使用该协议的默认端口号。通过ip地址和端口,三次握手建立一个tcp连接。
  2. 请求:连接成功建立后,开始向web服务器发送HTTP请求。Flask通过wsgi协议传递请求。
  3. 响应:接到请求后,交给相应路由处理,根据地址转发给相应的控制器(函数)处理。后端大部分工作就是写这些处理过程。处理完成后将最终的response返回给用户。这其中我们拿到的request与response都是由python的wsgi工具包werkzeug提供的。
  4. 关闭连接:通讯双方均可关闭socket结束tcp/ip会话。

关于wsgi:wsgi协议将处理请求的组件按照功能及调用关系分成了三种:

  • server
  • middleware
  • application。

其中,server可以调用middleware和application,middleware可以调用application。
符合WSGI的框架对于一次HTTP请求的完整处理过程为:

  • server读取解析请求,生成environ和start_response,然后调用middleware;
  • middleware完成自己的处理部分后,可以继续调用下一个middleware或application,形成一个完整的请求链;
  • application位于请求链的最后一级,其作用就是生成最终的响应。
如需更深入了解该过程,可以查看WSGIWerkzeug

<br />

一、Why Flask & 概览

我负责项目web后端的用户模块,对并发量要求不高。考虑到Flask小巧简单易上手,同时具有强大的扩展能力,使其功能可以不弱于django、Tornado等框架,我最终选择了Flask。下面是Flask最简单的一个示例,这篇文章要做的就是将其充实、扩展、拆分,使代码具有良好的可扩展性和可读性。

from flask import Flask
app = Flask(__name__)

@app.route('api/test')
def hello():
    return 'Hello World!'

if __name__ == '__main__':
app.run()

本文假设你已有基础的python语法知识。装饰器是一种代码运行期间动态增加功能的方式,本质上是一个返回函数的高阶函数,也可以简单的将其理解为一个函数的包装函数。上述代码中的route方法是一个装饰器,这个装饰器的作用就是将地址(api/test)与方法名hello联系起来,当HTTP请求的url为(api/test)时候将调用hello方法进行处理。也就是建立了url与处理函数的映射。深入了解可以查看这篇文章

看起来不复杂,那就让我们继续吧!先从环境工具配置开始:

常用工具

1.IDE

jetbrains家的pycharm,自带终端(虚拟环境、安装插件、启动redis等操作)、Python Console运行python、Version Control版本控制、Even Log打印。

pycharm.png

2.请求工具

火狐浏览器插件—HTTP requester。可以自定义请求方式 request methods、请求内容request body、请求头 request header等,当你写好一个接口时,可以非常方便得进行测试。


httprequester.png

项目配置

1.虚拟环境

可以按官方文档使用终端进行配置,也可以在pycharm的偏好设置里进行设置,这里我们使用python3.5的解释器。安装后执行命令进入虚拟环境,其中yourvenv为你指定创建的虚拟环境目录

$ source yourvenv/bin/activate

2.使用pip进行包管理

pip是python的包管理工具,如果你是通过homebrew安装python则会自动安装pip。其他情况参考stackoverflow。安装好pip之后,在虚拟环境中通过

$ pip install flask(库名)

安装Flask以及其他三方库。

3.配置redis

Reids是现在最流行的的非关系型数据库(key-value)。其数据缓存在内存中,因此效率很高。多用于存储临时的、高度动态的数据。在用户模块中我们将会对验证码进行redis存储(短时间内进行写入读取操作,并且在缓存一定时间后删除)。本文中将会已用户注册时生成的邀请码为例,进行redis存取操作。
从Redis官网下载安装包并按文档安装后,终端执行

$ redis-server

启动redis,在项目中pip安装即可调用其API。

4.项目相关约定

  • 项目采用前后端分离的方式,只进行数据交互,不使用python的jinja2模板去渲染页面给web前端
  • web前端、iOS、Android与后端数据交互的格式均为json
  • 前端的请求头默认带terminal(前端类型)、version(版本号),后端返回的数据中包含“code”、“msg”参数,code=0表示请求处理成功,当code!=0时,msg为错误信息
  • 本文中的后端开发环境为macOS系统,使用python3.5版本

二、从第一个路由注册接口开始

在web开发中,路由(route)是指根据url分配到对应的处理程序。
在用户模块中,注册接口无疑是最基础的。我们先来分析注册过程:

  • 前端(包括网页端、iOS、Android)传过来的参数中必须包含用户名与密码,考虑到产品有邀请人机制,因此可能会传邀请码过来。因此,我们首先要做的是,判断该接口是否传了所必须的参数包括手机号和密码,其次判断邀请码是否存在并做相应处理
  • 判断请求中是否有邀请码这一参数,若有则通过邀请码从数据库中查找邀请人。此处邀请码及邀请人存在redis数据库中(此处用redis只是教程需要,存入mysql即可)。若没有,则向用户返回错误码及信息
  • 将密码加密,存入数据库并返回一个用户id(默认自增,用来创建邀请码),若失败则返回相应错误
  • 若用户在请求参数中包含“idfa”参数,则更新用户来源渠道,该数据用于推广运营
  • 上述过程处理完毕后,根据用户id生成token并返回给用户
from flask import Flask
app = Flask(__name__)

@app.route('user/register methods=['POST']')
def user_register():
    #判断是否含有phone、password参数,若没有则返回400错误
    #判断是否有invitationCode参数,若有则从redis数据库中获取邀请人,若获取不到则返回错误,参考REST
    #获取phone参数,加密密码并将用户信息存入mysql数据,若成功则返回用户id,失败返回错误
    #根据用户id生成邀请码,并存入redis数据库中
    #判断是否有idfa参数,若有则在后台线程在mysql中修改用户来源(非必须同步操作,放后台即可)
    #生成token
    return 注册接口返回token(包含默认的code、msg)

if __name__ == '__main__':
app.run()

1. 使用装饰器处理接口必填字段

考虑到post请求都有检测必填参数并返回信息的需求,因此我们可以写一个装饰器来处理,避免每个接口都写一大段相同的判断代码。装饰器是一个返回函数的高阶函数,可以在函数执行之前执行其他操作。在这里我们可以写一个接受多个参数(即一个请求的所有必填参数)的装饰器,如下:

def require(*required_args):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            for arg in required_args:
                if arg not in request.json:
                    return flask.jsonify(code=400, msg='参数不正确') 
           return func(*args, **kw) 
       return wrapper
    return decorator

我们只需要在请求方法之前添加装饰器即可:

@app.route('user/register', methods=['POST'])
@require('phone','password')
def customer_register():
    xxxx
    return xxx

当请求中包含了phone和password参数时,不进行任何操作,返回原函数即user_register(),当有一个参数缺失时,直接返回错误信息400。注意,此处的400并不是http请求的statusCode状态码,而是后端与前端的一种约定,只是返回数据的其中一个参数。

其中flask.jsonfy()方法可以将key-value参数转换为json格式。jsonify()与dump()的区别在此

为了让项目清晰易懂,我们将此类为请求处理方法准备的装饰器单独放在一个文件中。在项目下创建一个python包文件夹(python package)命名为handler,创建hd_base.py文件,并将装饰器函数写在该文件中。当需要时,只需要引入该方法即可:

from handler.hd_base import required

看到这段代码,大家可能会好奇,我是怎么拿到请求中的参数呢? 程序怎么知道代码中的request就是我们现在正请求的request呢,这就涉及到了Flask的上下文。

2.Flask上下文获取request参数

一般来讲,想要拿到一个请求的内容,如header、body等,需要将这个请求当做参数传给我们定义的user_register()函数。但是考虑到我们可能会调各种各样的东西,为了避免大量可有可无的参数把函数搞的一团糟,Flask使用了上下文(context)临时把某些对象变成了全局可访问。Flask上下文分为应用上下文(AppContext)和请求上下文(RequestContext),可以简单地理解为一个应用运行过程中/一次请求中的所有数据。在上面的装饰器代码中,我们用到了request.json.get()来获取请求内容,其中request对象就是全局可访问的。你又有疑问,在多线程服务器中,多个线程同时处理不同客户端发送的不同请求时,每个线程拿到的request能一样吗?在Flask中,同一时刻一个线程只处理一个请求,使用上下文让特定的变量在一个线程中全局可访问,不会干扰其他线程。详见这篇文章
要使用上下文中的request全局可访问对象,需要引入:

from flask import request

若参数以json形式传输,则可通过request.json.get('phone')获取某参数key的值,请求头可以通过request.json.get('version')获取,表单则可通过rquest.form获取。

3.错误处理

上述代码中,装饰器对参数不满足的情况返回了包含错误信息的json数据。那么如何直接抛错,返回statusCode状态码错误呢,Flask为我们提供了abort方法与errohandler装饰器。将上述装饰器代码稍作修改即可:

def require(*required_args):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            for arg in required_args:
                if arg not in request.json:
                    return flask.abort(400)
           return func(*args, **kw) 
       return wrapper
    return decorator

@app.errorhandler(400)
def not_found(error): 
     return make_response(flask.jsonify({'error': '参数不正确'}), 400)

当请求不包含必填参数时,请求的发送端就会收到请求失败的信息,http状态码为400(请求成功的状态码为200,更多查看http状态码)。

4.数据库连接池

项目使用了MySQL数据库和Redis数据库。根据不同情况选择使用。

MySQL

pip安装flask-sqlalchemy。sqlalchemy是实现ORM的库,将会在下文进行介绍。按照官方文档进行数据库连接(此处为远程数据库连接):

from flask import Flask
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://dbname:MrVg+X1ZwS4RiCh9@120.25.102.84:3306/db1'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)

当要使用数据库时,调用db即可,下文将会结合ORM使用。

Redis

pip 安装Redis,查阅文档,进行连接(此处连接到本地Redis数据库):

import redis
_redis_cache = redis.Redis(connection_pool=redis.ConnectionPool(host='127.0.0.1', port=6379, db=1))
_redis_db = redis.StrictRedis(host='127.0.0.1', port=6379, db=2)

我在这里使用两种方式连接了Redis数据库,其作用是相同的。其中_redis_cache用于缓存数据(如用户验证码),_redis_db用做数据库(消耗内存,但是速度快),按需使用。为了方便使用,我们将redis常用的存取方法进行封装:

class RedisDB:
    def __init__(self, conn=_redis_db):
        self.conn = conn
    def set(self, key, value, expire=None):
        self.conn.set(key, value, expire)
    def hget(self, name, key):
        ret = self.conn.hget(name, key) 
        if ret: 
           ret = ret.decode('utf-8')
        return ret
    def hset(self, name, key, value):
        self.conn.hset(name, key, value)

class RedisCache(RedisDB):
    def __init__(self):
        super().__init__(_redis_cache)

我们在项目中创建一个python pacage命名为common,创建文件redisdb.py,将封装的redis相关类与方法写在其中。
当需要使用Redis时,实例化后进行操作:

rdb = RedisDB()
rdb.set('a',123)     #(key,value)
a = rdb.get('a')
print(a)             #输出123

连接好数据库,我们已经可以完成注册接口的一部分代码了:

from handler.hd_base import require
from common import redisdb
from flask import Flask
app = Flask(__name__)

rdb = redisdb.RedisDB()

@app.route('user/register', methods=['POST'])
@require('phone','password')
def customer_register():
    #邀请码相关处理
    inviter = None
    if request.json.get('invitationCode'):
    inviter = rdb.hget('invitationCode',request.json['invitationCode'].upper())
    if not inviter:
        return flask.jsonify(error=400,msg='邀请码无效')
    #其他处理

if __name__ == '__main__':
    app.run()

从项目文件目录的角度来讲,你可能发觉,所有的请求都写在这个我们创建项目时的第一个文件中,有些不太合适。因此我们可以将这个文件拆分,将实例化应用、接口方法、应用启动三个部分分离。

app/__init__.py中:

from flask import Flask
app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://dbname:MrVg+X1ZwS4RiCh9@120.25.102.84:3306/db1'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)

app/run.py中:

from app import app
if __name__ == '__main__':
    app.run()

handler/hd_user.py中:

@app.route('user/register', methods=['POST'])
@require('phone','password')
def customer_register():
     xxxxxx
     return xxxxx

创建python包(python package)为app、handler,将实例化应用代码写入自动生成的app包下的init.py中,当需要用到app时,引入即可。同理,在app包下创建run.py,将应用启动代码写入。在handler下创建hd_user.py,写入路由方法。

5.循环引用的那些坑

在你为了项目可读性将代码进行分离时,可能会遇到一个奇怪的错误:cannot import xxxx,无法引入某个模块、方法或变量。如果你确认自己没有犯一些低级错误,那么可能就是产生了循环引用。这也是项目初期我遇到的浪费时间最多的坑。

其实问题出在模块导入的顺序上。 比如:在A文件头执行到语句 from B import XXX,程序马上就会转到B文件中去,从头到尾顺序寻找B文件中的XXX函数,而A文件就暂停执行,直到把XXX函数复制到内存中。但在这个过程中,如果B文件头中又导入了A文件中的函数,由于XXX函数还没有被复制,A文件就会因为暂停执行而无法导入,就会出现上面的错误。你可以选择尝试修改import顺序消除循环引用,但解决这个问题最好的方法就是重新梳理逻辑,将重复使用的东西单独提取出来的。当然,上面代码中,将app单独放在一个文件里定义,就是为了避免循环引用,每个文件需要app对象时,单独import它即可。

6.封装加密方法

对密码进行md5加盐加密是一种常见的安全手段。MD5加密算法是不可逆的,如果需要验证密码是否正确,需要对待验证的密码进行同样的MD5加密,然后和数据库中存放的加密后的结果进行对比。但是MD5加密依然不够安全,因此我们在此基础上采用加盐加密
在common包中新建security.py文件,封装加密方法:

#md5加盐加密
def _hashed_with_salt(info, salt):        
    m = hashlib.md5()
    m.update(info.encode('utf-8'))
    m.update(salt)
    return m.hexdigest()

#对登录密码进行加密
def hashed_login_pwd(pwd):      
    return _hashed_with_salt(pwd, const.login_pwd_salt)

之所以将加盐加密方法单独拿出来,是因为项目后期可能会对其他信息进行加密,比如交易密码。到时复用加密方法,修改参数即可,可以保证代码简洁。通过如下代码即可对用户登录密码进行加密:

from common import security
password = security.hashed_login_pwd(request.json['password'])

7.ORM与Model

ORM全称Object Relation Mapping,即对象关系映射。用于实现面向对象编程语言里不同类型系统的数据之间的转换。简单来讲,就是你不再需要写sql语句对数据库进行CRUD操作,只需建立每张表对应的模型Model类,调用相应方法即可达到相同的效果。
以用户表为例,我们新建Model包,创建user.py,添加user模型:

from app import db
from common import utils
from model import userconst

class User(db.Model):
    __tablename__ = 'user'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80),unique=False)
    phone = db.Column(db.String(80), unique=True)
    password = db.Column(db.String(80),unique=False)
    source = db.Column(db.Integer,unique=False)
    terminal = db.Column(db.Integer,unique=False)
    invited_from = db.Column(db.String(80),unique=False)

    #重写该方法,方便输出user信息
    def __repr__(self):
        user = ''
        user += 'name: %s\n' % (self.name)
        user += 'phone: %s\n' % (self.phone)
        user += 'password: %s\n' % (self.password)
        ········
        return user

def create_user(phone,password,invited_from,terminal):
    user = User(phone=phone,password=password,source=userconst.SOURCE_DEFAULT,
                  terminal=terminal,invited_from=invited_from)
    db.session.add(user)
    try:
        db.session.commit()
    except BaseException:
        return 0
    else:
        return user.id    

其中userconst.py 定义了一些user相关的常量。

我们定义了User类,并定义了一个创建用户的方法。若数据库添加用户成功,则返回默认自增id,若有任何异常错误,返回0,代表创建用户失败,即注册失败。

本例我们使用ORM代替了sql语句"INSERT INTO user VALUES (phone, name,....)"。
现在,让我们将注册路由的方法更进一步。在user_register方法中添加如下代码:


·········
def customer_register():
    #邀请码相关操作,已省略
    ········
    phone = request.json['phone']
    #对密码进行MD5加盐加密
    password = security.hashed_login_pwd(request.json['password'])
    #写入数据库并处理返回
    uid = create_user(phone,password,inviter,request.headers['terminal'])
    if uid == 0:
        return error_resp(500,'注册失败,请核对信息后重新输入')
    #其他处理
········


了解了ORM后,我们可以试着从数据库中获取手机号为“18012341234”的用户的姓名并修改:

user = db.session().query(User).filter_by(phone='18012341234').first()
print(user.name)     #输出姓名
user.name = '张三'
db.session.add(user)
db.session.commit()    #提交修改

可以看到,简单、精确、易用是ORM的特点,但是ORM映射会消耗内存,当数据变得复杂且庞大时,使用ORM会带来不小的性能损耗,我们要根据实际情况进行选择。

8.celery多线程、异步任务

Celery是一个异步任务队列,一个基于分布式消息传递的作业队列。它支持使用任务队列的方式在分布的机器、进程、线程上执行任务调度。总的想法就是你的应用程序可能需要执行任何消耗资源的任务都可以交给任务队列,让你的应用程序自由和快速地响应客户端请求。

在handler中新建tasks.py,配置celery,添加相关方法:

from celery import Celery

def make_celery(app):
    celery = Celery(app.import_name, broker=app.config['redis://localhost'])
    celery.conf.update(app.config)
    TaskBase = celery.Task
    class ContextTask(TaskBase):
        abstract = True
        def __call__(self, *args, **kwargs):
            with app.app_context():
                return TaskBase.__call__(self, *args, **kwargs)
    celery.Task = ContextTask
    return celery

celery = make_celery(app)

当我们需要实现后台任务时,只需要在方法前添加celery的装饰器。将下列代码写入tasks.py中:

@celery.task
def update_user_source(ifa, phone):
    records = db.session().query(Advertisement).filter_by(ifa=ifa).all()
    if len(records) == 0:
        return
    selected = sorted(records, key=lambda x: x.create_time, reverse=True)[0]
    source = Advertisement.AD_SOURCE_MAP[selected.ad_service]
    user = db.session().query(Customer).filter_by(phone=phone).first()
    user.source = source
    db.session.add(user)
    db.session.commit()

在终端中启动celery worker

$ celery -A tasks worker

在注册接口中,我们可以在用户数据插入成功之后,后台更改用户的source来源:

def customer_register():
    ········
    idfa = request.headers.get('idfa')
    if idfa:
        update_user_source.delay(idfa,phone)
        rdb.hset(const.user_idfa,uid,idfa)
    ········

其中const.py在common包下,存放字符串常量。
delay() 方法是强大的 apply_async() 调用的快捷方式。这样相当于使用 apply_async():

update_user_source.apply_async(args=[idfa,phone])

当使用 apply_async(),你可以给 Celery 后台任务如何执行的更详细的说明。一个有用的选项就是要求任务在未来的某一时刻执行。例如,这个调用将安排任务运行在大约一分钟后:

update_user_source.apply_async(args=[idfa,phone], countdown=60)

9.设置response模板

按照我们本项目的约定,接口返回的数据中带code和msg字段,当code为0说明请求成功,msg为空;当code不为0时,msg为错误信息。在handler中创建template.py:

import flask

_base_dic = {
    'code':0,
}

def error_resp(code,msg):
    return flask.jsonify(error=code,msg=msg)

def register_teml(token):
    return dict({
        'token':token,
    },**_base_dic)

设置注册接口的response:

def user_register()
    #所有处理
    ············
    return flask.jsonify(**template.register_teml(token=security.generate_token(uid)))

10.完整的注册接口

至此,注册接口我们就完成了。完整的路由方法如下:

@app.route('user/register', methods=['POST'])
@require('phone','password')
def customer_register():
    #邀请码相关处理
    inviter = None
    if request.json.get('invitationCode'):
    inviter = rdb.hget('invitationCode',request.json['invitationCode'].upper())
    if not inviter:
        return flask.jsonify(error=400,msg='邀请码无效')
    phone = request.json['phone']
    #对密码进行MD5加盐加密
    password = security.hashed_login_pwd(request.json['password'])
    #写入数据库并处理返回
    uid = create_user(phone,password,inviter,request.headers['terminal'])
    if uid == 0:
        return error_resp(500,'注册失败,请核对信息后重新输入')
    #用户来源处理
    idfa = request.headers.get('idfa')
    if idfa:
        update_user_source.delay(idfa,phone)
        rdb.hset(const.user_idfa,uid,idfa)
     return flask.jsonify(**template.register_teml(token=security.generate_token(uid)))

现在,一切看起来都很顺利。但是当用户模块的接口添加了登陆、验证码相关、重置密码、获取用户信息等接口后,hd_user会变得很大,这时再添加其他模块的接口时,文件就会变得难以维护。而且当你尝试着新建一个文件写其他模块的接口时,会产生循环引用、代码重复、耦合性太强等等问题。这还仅仅是纯后端,不涉及用模板渲染html页面。这时我们应该就想到将项目模块化,在Flask中Blueprint很好的帮我们解决了这个问题。

10.使用Blueprint进行模块划分

Blueprint通常译作蓝图或蓝本。蓝本允许你将不同路由分开,提供一些规范标准,并且附带了很多好处:让程序更加松耦合,更加灵活,增加复用性,提高查错效率,降低出错概率。如果你是纯萌新,没有大中型项目经验,对蓝图可能会理解困难。我在知乎找到一篇回答,从『小白』和『专业』两个方面解释了蓝图是什么,大家可以去看看:如何理解Flask中的蓝本?

Blueprint使用也非常简单。从避免循环导入、减少代码耦合的角度看,我们在app包下创建bpurls.py,注册蓝图:

from flask import Blueprint

userBP = Blueprint('user', __name__,url_prefix='/user')
productBP = Blueprint('product', __name__,url_prefix='/product')

每个蓝图代表一个模块,可以设置前缀。在hd_user中导入userBP,此时对方法进行修改:

#@app.route('user/register', methods=['POST'])
@userBP.route('register', methods=['POST'])
def customer_register():
      ·······

同理,当你写product模块相关接口时,创建hd_product.py,导入productBP即可。
蓝本在使用模板的项目中用处更加突出,有兴趣的同学可以查阅相关文档

总结及展望

这篇文章我最近一段时间的项目实践总结。虽然内容很简单,但是由于自己知识漏洞较多,还是学到了不少新知识。在写文章的时候查阅了很多资料,眼界也更加开阔。虽说iOS还未精通,python web也才入门,但是我对其他技术也充满好奇。下一步可能会在下班空闲时间深入已有知识体系的同时,学习下数据相关的技术。总之,stay hungry ,stay fool !

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容

  • 22年12月更新:个人网站关停,如果仍旧对旧教程有兴趣参考 Github 的markdown内容[https://...
    tangyefei阅读 35,170评论 22 257
  • # Python 资源大全中文版 我想很多程序员应该记得 GitHub 上有一个 Awesome - XXX 系列...
    aimaile阅读 26,454评论 6 428
  • GitHub 上有一个 Awesome - XXX 系列的资源整理,资源非常丰富,涉及面非常广。awesome-p...
    若与阅读 18,629评论 4 418
  • 环境管理管理Python版本和环境的工具。p–非常简单的交互式python版本管理工具。pyenv–简单的Pyth...
    MrHamster阅读 3,788评论 1 61
  • 对视的那一刹那 我读懂了自己为爱的付出 却读不懂你 每一次轻声的问候 那掩饰的随意中 都蕴积了我千个日夜的乞盼 看...
    从心活过阅读 319评论 3 9