分类目录归档:计算机与编程

我的第一个Chrome扩展:GhostPage(网页X光)

没有接触过Chrome扩展的时候,觉得它一定很复杂。crx的后缀也让我猜不出是用什么语言编写的,现在才发现不过就是HTML+CSS+JavaScript……

这个扩展本身很简单,实际只有一行代码。Google提供了一个非常好的入门指导,过一遍基本上就可以编写简单的扩展了。这个扩展作用也就是通过去掉页面样式来检查页面结构(参考这篇文章),可以用来解决下面的问题:

  • 元素超出边线,挤出的空白导致多余的平行滚动
  • 元素不对齐

使用这个扩展配合Chrome的检查工具可以快速的定位到出问题的元素。

Chrome商店的地址:
https://chrome.google.com/webstore/detail/ghostpage/hegpcollkgldlimbhkimijhhhoaicipp

crx文件下载(备用):https://chrome-extension-downloader.com/77577d1b5c39e945e597a9981f6e0ee6/hegpcollkgldlimbhkimijhhhoaicipp/GhostPage_v1.0.crx

附注:虽然扩展编写很简单,但为了交5美元的开发者认证费用却花了我一天时间。尝试了下面几种方式均失败了:

  • 全球付
  • Google Play礼品卡
  • 其他几个虚拟信用卡服务

最后花了两倍多的价格在淘宝上买了一个虚拟信用卡,第二天中午扣款成功了。

Flask-Dropzone:为你的Flask程序添加文件上传功能

这篇文章属于“Flask常用扩展介绍系列”,这个系列的文章目录索引可以在《Flask常用扩展介绍系列文章索引》看到。
 

某天在Stack Overflow上看到一个关于Dropzone.js的问题,就去研究了一下,写了一个回答,顺便写了一个小demo。我发现,如果有一个集成Dropzone.js到Flask,并且简化设置步骤的扩展,肯定要比其他上传方式简单的多——于是就有了Flask-Dropzone

Flask-Dropzone在模板中提供了一些方法来帮助你创建上传区域,引入相关资源。你只需要添加一些配置就可以实现上传类型的过滤,文件大小限制,上传后跳转等功能。当然,你还要自己编写视图函数来处理和保存文件,并进行服务器端的二次验证。

《Flask Web开发实战》中的第3个示例程序(图片社交程序Albumy)使用了这个扩展。

用法说明

安装

$ pip install flask-dropzone

初始化

和其他扩展类似,你可以通过实例化Dropzone类,并传入程序实例app进行初始化:

from flask_dropzone import Dropzone

app = Flask(__name__)
dropzone = Dropzone(app)

在使用工厂函数创建程序实例时,你也可以使用init_app()方法:

from flask_dropzone import Dropzone

dropzone = Dropzone()

def create_app():
    app = Flask(__name__)
    dropzone.init_app(app)
    return app

引入Dropzone.js资源

你需要自己手动编写引入Dropzone.js的CSS和JavaScript资源的语句。在开发时,或对于玩具项目,你可以可以使用Flask-Dropzone提供的两个快捷方法:

<head>
    ...
    {{ dropzone.load_css() }}
</head>
<body>
    ...
    {{ dropzone.load_js() }}
</body>

创建并美化上传区域

如果你不需要对上传区域的样式有太多控制,那么你只需要在想要渲染上传区域的地方使用dropzone.create()方法:

{{ dropzone.create(action='处理上传文件的路由URL') }}

记得把action的值更改成你要处理文件上传的的URL。你可以使用style()方法为上传区域添加简单的自定义样式:

<head>
...  <!-- 在引入Dropzone.js的CSS文件后调用style()方法 -->
{{ dropzone.style('border: 2px dashed #0087F7; margin: 10%') }}
</head>

上传区域的截图示例如下所示:

Flask-Dropzone上传截图

Flask-Dropzone上传截图

在服务器端处理并保存上传文件

当文件被拖拽到上传区域,或是点击上传区域选择上传文件后,这些文件会以AJAX请求的形式发送到你在dropzone.create()方法中使用action参数传入的URL。我们需要在服务器端创建对应的视图函数来处理这些请求,下面是一个最基本的示例:

import os

from flask import Flask, request
from flask_dropzone import Dropzone

app = Flask(__name__)

dropzone = Dropzone(app)

@app.route('/uploads', methods=['GET', 'POST'])
def upload():

    if request.method == 'POST':  # 如果请求类型为POST,说明是文件上传请求
        f = request.files.get('file')  # 获取文件对象
        f.save(os.path.join('the/path/to/save', f.filename))  # 保存文件

    return 'upload template'  # 渲染上传页面

 

上传完成后重定向

这里需要注意的是,因为Dropzone.js通过AJAX请求提交文件,所以你没法在保存文件后将页面重定向。对于这个问题,你可以使用配置变量DROPZONE_REDIRECT_VIEW设置上传完成后跳转到的目标端点,或是添加一个按钮让用户自己点击进行跳转。

服务器端验证

尽管Dropzone.js可以在前端对用户提交的文件进行验证,但为了安全考虑,我们仍然需要在服务器端进行二次验证。在服务器端验证时,如果验证出错,我们不能像往常那样使用flash()函数“闪现”错误消息,因为AJAX请求接受到响应后并不会重载页面,所以不会显示通过flash()函数发送的消息。正确的做法是返回400错误响应,使用错误消息作为响应的主体。下面是一个简单的进行服务器端验证并返回错误消息得示例:

@app.route('/', methods=['POST', 'GET'])
def upload():
    if request.method == 'POST':
        f = request.files.get('file')
        if f.filename.split('.')[1] != 'png':
            return 'PNG only!', 400  # return the error message, with a proper 4XX code
        f.save(os.path.join('the/path/to/save', f.filename))
    return render_template('index.html')

在上面的代码中,我们验证图片是不是png格式,如果不是就返回一个错误提示,在服务器端会在图片下面看到我们返回的错误消息:

blank

 

 

完整的配置列表

Flask-Dropzone提供了丰富的配置变量,你可以使用它们对Dropzone.js进行各类配置。

Name Default Value Info
DROPZONE_SERVE_LOCAL   False       使用Flask-Dropzone提供的load_css()和load_js()方法时,这个变量设置是否使用内置的本地资源,默认从CND加载
DROPZONE_MAX_FILE_SIZE 3 允许的文件最大值,单位为MB
DROPZONE_INPUT_NAME file 上传字段的name值
DROPZONE_ALLOWED_FILE_CUSTOM False 是否使用自定义文件类型允许规则,具体见后面
DROPZONE_ALLOWED_FILE_TYPE 'default' 允许的文件类型,具体见后面
DROPZONE_MAX_FILES ‘null’ 一次可以上传的文件数量最大值
DROPZONE_DEFAULT_MESSAGE “Drop files here to upload” 上传区域显示的提示文字
DROPZONE_INVALID_FILE_TYPE “You can’t upload files of this type.” 文件类型错误的错误消息
DROPZONE_FILE_TOO_BIG “File is too big {{filesize}}. Max filesize: {{maxFilesize}}MiB.” 文件太大时显示的错误消息
DROPZONE_SERVER_ERROR “Server error: {{statusCode}}” 服务器错误的错误消息
DROPZONE_BROWSER_UNSUPPORTED “Your browser does not support drag’n’drop file uploads.” 浏览器不支持的错误消息
DROPZONE_MAX_FILE_EXCEED “Your can’t upload any more files.” 超过最大文件数量限制的错误消息
DROPZONE_UPLOAD_MULTIPLE False 是否在单个请求中发送多个文件,默认一个请求发送一个文件
DROPZONE_PARALLEL_UPLOADS 2 当DROPZONE_UPLOAD_MULTIPLE设为True时,设置单个请求包含的文件数量
DROPZONE_REDIRECT_VIEW None 上传完成后重定向的模板端点
DROPZONE_ENABLE_CSRF False 是否开启CSRF保护

这些配置的用法你可以参考Flask-Drozone的文档或是示例程序了解,这里我们仅简单介绍一下对文件类型进行过滤的设置方法。

设置文件类型过滤

Flask-Dropzone内置了一些文件类型(通过MIME定义),可选的值和对应的文件类型如下所示:

  • default:默认值,运行所有类型
  • image:图片
  • audio:音频
  • video:视频
  • text:文本
  • app:程序

你需要为DROPZONE_ALLOWED_FILE_TYPE设置对应的值,比如下面设置仅允许上传图片:

app.config['DROPZONE_ALLOWED_FILE_TYPE'] = 'image'

如果你想要自己定义允许的文件类型列表,那么你需要将DROPZONE_ALLOWED_FILE_CUSTOM设置True,然后传入一个包含允许的文件后缀名列表组成的字符串给DROPZONE_ALLOWED_FILE_TYPE变量,使用逗号分隔多个后缀名,比如:

app.config['DROPZONE_ALLOWED_FILE_CUSTOM'] = True
app.config['DROPZONE_ALLOWED_FILE_TYPE'] = 'image/*, .pdf, .txt'

对于平行上传、CSRF保护等内容的具体实现方法你可以参考文档了解。不过,这个项目目前还没有创建完善的文档,暂时只是写到README里,如果你发现了英文语法或拼写错误,欢迎指正,同时也欢迎为项目贡献代码。

示例程序

Flask-Dropzone的Git仓库中的examples目录下包含4个示例程序,分别演示了基本用法、CSRF保护、平行上传和上传完成后跳转四个功能。

源码可以在这里找到:https://github.com/greyli/flask-dropzone/tree/master/examples

另外,helloflask仓库里在demos/form目录下的示例程序也包含一个Flask-Dropzone使用示例。

相关链接

 

编程名词辨析(5×10’)

在和很多同学的交流中,发现有些编程词语被误用了。今天我们来辨析一下。

我简化了大部分解释,如果需要了解更多,请点链接。如果发现有错,请指正。

 

包(package)和模块(module)

:一个包含__init__.py文件的文件夹就是一个包,这个文件夹里可以放多个Python模块。比如:

my_package
    - __init__.py
    - hello.py

模块:一个Python文件(.py)就是一个模块。

What’s the difference between a Python module and a Python package?

 

插件(plugin)和扩展(extension)

在Flask里,插件和扩展没有具体的区别,都指代增强Flask功能的Python包,但通常称作扩展。

 

参数(parameter)和参数(argument)

参数(形参):函数所期望你传入的参数(即name

def say_hello(name):
    print 'hello,' + name

参数(实参):传给函数的实际的参数(即‘kitty’

say_hello('kitty')

What’s the difference between an argument and a parameter?

函数(function)和方法(method)

函数:为了便于进行管理和重用而包装起来的代码块。

def meow():
    print 'meow meow meow'

方法:在类中定义的函数是方法。

class Cat(object):
    
    def meow(self):
        print 'meow meow meow'

Difference between functions and methods in Python

 

表达式(expression)和语句(statement)

表达式:能被打印和赋值的是表达式。比如:

1 + 1
[x for x in y]

语句:任何一行(或多行)Python代码就是语句(包括表达式),比如:

print 42
name = 'john'

What is the difference between an expression and a statement in Python?

 

用Flask从零实现豆瓣相册

这是我在知乎专栏 – Hello, Flask!上连载的一个教程,这篇作为目录会持续更新。

被新年绑架,一个月忙忙碌碌,现在终于有时间继续写专栏。本来想先写其他的实践,比如便签本、个人博客,但这些都和《Flask Web开发》的Flasky差不多,没什么好写的。想着干脆先把这个相册实现了。

为什么是豆瓣相册

当然,最主要的原因是豆瓣相册比较简单,容易实现。而且用一个东西用久了,总想改造一下。

页面逻辑和豆瓣相册相同:用户相册主页——相册页——图片页——大图

主要的功能点:

  1. 相册与图片
  2. 添加喜欢
  3. 图片上传
  4. 批量编辑
  5. 拖拽排序
  6. 图片浏览
  7. 图片处理
  8. 隐私设置
  9. 自定义头像
  10. 相册增强插件

 

系列目录

  1. 相册与图片

 

相关项目

上面的功能实现在这个项目里都可以看到,后面的文章也会基于这个项目。因为这也是个实验场,有很多代码和实现方法需要重新考量。如果发现问题,欢迎提issue和贡献代码~

github.com/greyli/fanxiangce

部分截图

user page

用户主页

相册页面

相册页面

批量编辑页面

批量编辑页面

photo page

图片浏览页

做个新的相册网站

等这个系列结束,你就可以自己做个相册网站,找一些厉害的插件改造它,再起个好名字。然后到豆瓣里发篇日记,就叫《我做了个更好的豆瓣相册》。当然,那时它可能叫西红柿相册,或是袋鼠相册……

 

图片墙生成器

用Flask重写了图片墙生成器。

Github项目:https://github.com/greyli/image-wall
图片墙Demo:http://fanxiangce.com/demo1(旧Demo)

 

主要变化

和之前的Python版本相比的变动:

  • 实现了上传功能
  • 优化了CSS和JS代码
  • 简化了页面元素,去掉了没用的赞助页面
  • 把HTML分离出Python,代码结构更清晰

 

实现步骤

1、用户上传图片;

2、生成用户文件夹,重命名图片;

@app.route('/', methods=['GET', 'POST'])
def index():
    # 为每一个用户创建文件夹
    username = hashlib.md5('demo' + str(time.time())).hexdigest()[:7]
    if request.method == 'POST' and 'photo' in request.files:
        amount = len(request.files.getlist('photo'))
    if amount in range(10, 101):  # 控制图片数量在10~100之间
        for num, img in enumerate(request.files.getlist('photo')):
            filename = username + str(num)  # 使用用户名加序号命名图片
            photos.save(img, name=username + '/' + filename + '.')
        return redirect(url_for('image_wall', username=username))
    return render_template('index.html')

3、根据图片数量分配相应的行列图片数量,并为每一张图片生成三维坐标;

4、渲染模板,传入这些坐标;

{% for image in images %}
<div class="step center-text" data-x="{{ image[1] }}" data-y="{{ image[2] }}" data-z="{{ image[3] }}">
    <img class="image" src="{{ url_for('static', filename='photos/') }}{{ image[0] }}">
</div>
{% endfor %}

 

 

Google+社群——Hello, Flask!

为了方便讨论和交流,创建了一个Google+社群。现在可以在社群里分享信息,创建问题和讨论。欢迎加入!

关于提问

专栏关注人数增多以后,提问也越来越多。但是有些问题我完全不知道问的是什么……学会提问很重要!

遇到问题后应该有这么一个解决流程:

检查代码是否有语法错误——查看相应的源码和文档——Google搜索——如果到了这一步还没法解决,再到论坛上发帖(建议到StackOverflow上提问)。

问题应该尽量包括下面的内容:

  1. 期望效果
  2. 实际效果
  3. 你的操作步骤和尝试过的解决办法
  4. 相关的代码和错误输出
  5. 操作系统和语言、库等的版本

最后还要注意排版,内容尽量简洁,措辞礼貌一些。

因为Google+社群里不好放代码,可以在StackOverflow或知乎上提问,粘贴链接过来,或是在Github上创建Gist

 

Google+社群

你可以在这里分享关于Flask、Python以及Web开发的一切信息,说点儿想说的,不用拘束。

plus.google.com/u/0/com

Flask文件上传系列教程

文章目录

1、Flask文件上传(一):原生实现

    • 上传配置
    • 安全问题

2、Flask文件上传(二):使用扩展实现

    • 使用Flask-Uploads简化上传过程
    • 大型项目里Flask-Uploads的配置

3、Flask文件上传(三):完整实现

    • 使用Flask-WTF创建上传表单
    • 分离模板文件

4、Flask文件上传(四):文件管理与多文件上传

  • 文件管理
  • 文件名处理
  • 中文文件名问题
  • 多文件上传

5、Flask文件上传(五):拖拽上传和进度条

    • 使用插件集成进度条等功能
    • 使用Pillow生成图像缩略图

 

项目Demo

1、简单的文件管理系统:helloflask/cloud-drive

    • 文件上传
    • 文件删除
    • 图片预览

2、完善的文件管理系统:greyli/flask-file-uploader

    • 拖拽上传
    • 进度条
    • 文件管理
    • Bootstrap样式
    • 图片预览
    • 文件信息

 

 

JavaScript计算器

用JavaScript做了一个计算器,大部分时间都花在完善样式和交互上了,现在还想着再给它添加一个主题。我发现,我喜欢把一个东西从丑变美的过程,好看比好用更吸引我。这不是一个好习惯……calculator
Demo:https://greyli.github.io/calculator/
源码:https://github.com/greyli/calculator

 

优化交互、美化样式

在这个计算器里,用到了一些处理技巧,可以让它看起来更真实和漂亮。

计算器边缘阴影

这里使用box-shadow的inset方法,也就是把阴影内嵌到元素里,让计算器看起来是有厚度的:

box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), inset -1px -6px 12px 0.1px #89847e;

参考内容:https://css-tricks.com/snippets/css/css-box-shadow/

按钮按下效果

其实是设置按钮的box-shadow,按下时把box-shadow设为none,同时按钮向下移动

button {
  box-shadow: 1px 2px #666;
}

button:active {
  box-shadow: none;
  transform: translateY(4px);
}

按钮上的字不可选择

双击按钮或是拖动按钮选择会出现蓝色背景色,设置user-select去掉这个特性

.un-selectable {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

去掉按钮被选中后的蓝色边线

button:focus {outline:0;}  /* 设为none效果相同,或加上 !important */

 

换域名小记

起名字

起名字是个简单但让人头疼的事情。

你想要一个独特的名字,但又不想透露太多的个人信息。既不能太土,又不能太长,不能太难读,也不能太难记,而且最要命的是,如果你想在所有社交网站都有一个相同的id,这太难了——好名字全被注册完了。

连续换了两个域名后,我想这次终于遇到对我来说完美的域名了——greyli.com

 

换域名

更改完DNS设置后,在数据库里用新域名替换掉旧域名。

和上次同时换主机和域名不同,这次只需要在数据库里改动一些内容就完成了:

wp_options SET option_value = replace(option_value, 'www.mydomain.com','www.newdomain.com');
UPDATE wp_posts SET post_content = replace(post_content, 'www.mydomain.com','www.newdomain.com');
UPDATE wp_comments SET comment_content = replace(comment_content, 'www.mydomain.com', 'www.newdomain.com');
UPDATE wp_comments SET comment_author_url = replace(comment_author_url, 'www.mydomain.com', 'www.newdomain.com');
 
 
文章里有些地址是完整的,所以可能还需要加上下面这条语句:
UPDATE wp_posts SET post_content = replace(post_content, 'http://www.olddomain.com','http://www.newdomain.com');
 
 

Flask项目配置(Configuration)

在Flask项目中,我们会用到很多配置(Config)。比如说设置秘钥,设置数据库地址,像下面这样:

...
app.config['SECRET_KEY'] = 'some strange words'

Flask的配置对象(config)是一个字典的子类(subclass),所以你可以把配置用键值对的方式存储进去。这是一个通用的处理接口,Flask内置的配置,扩展提供的配置,你自己的配置,都集中在一处。

为什么需要使用自己的配置?
假设你在做一个博客,有十个视图函数都定义了每页显示的文章数。当你写好以后,发现每页的文章太多,想把这个值改小一点,这时你要找到这十个视图函数,分别修改这个值,很蠢吧?使用配置你就使用一行控制所有的变量:

app.config['POST_PER_PAGE'] = 12

在十个视图函数里使用配置变量代替固定值:

post_per_page = app.config['POST_PER_PAGE']

其实不就是设置了一个变量嘛……

你有两种方式来设置配置:

  1. 直接写出配置的值,像上面那样。
  2. 对于不适合写在程序里的配置,比如密码等,需要把配置写入系统环境变量,然后使用os模块提供的方法获取:
set MAIL_USERNAME=me@greyli.com  # windows
export MAIL_USERNAME=me@greyli.com  # *unix

获取变量并写入:

import os

from flask import Flask

app = Flask(__name__)
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')

如果你使用虚拟环境,设置环境变量时注意要激活虚拟环境,同时不要给变量值加引号。

set MAIL_USERNAME=me@greyli.com  # 结果是'me@greyli.com'
set MAIL_USERNAME='me@greyli.com'  # 结果是"'me@greyli.com'"

 

你有三种方式来处理配置。

直接写入主脚本

当你的程序很小的时候,可以直接把配置写在主脚本里:

from flask import Flask

app = Flask(__name__)
app.config['SECRET_KEY'] = 'some secret words'
app.config['DEBUG'] = True
app.config['ITEMS_PER_PAGE'] = 10

使用字典的update方法可以简化代码:

from flask import Flask

app = Flask(__name__)
app.config.update(
    DEBUG=True,
    SECRET_KEY='some secret words',
    ITEMS_PER_PAGE=10
)

 

单独的配置文件

程序逐渐变大时,配置也逐渐增多,写在主脚本里太占地方,不够优雅(这时你应该已经把表单,路由,数据库模型等等分成独立的文件了。关于大型项目结构,后续会总结)。我们可以创建一个单独的配置文件。和上面同样的配置,现在可以改写为:

config.py

SECRET_KEY = 'some secret words'
DEBUG = True
ITEMS_PER_PAGE = 10

在创建程序实例后导入配置:

import config

...
app = Flask(__name__)
app.config.from_object(config)
...

 

创建不同的配置类

大型项目需要多个配置组合,比如开发时的配置,测试的配置,部署的配置……这样我们需要在配置文件里创建不同的配置类,然后在创建程序实例时引入相应的配置类。

最佳实践是创建一个存储通用配置的基类,然后为不同的使用使用场景创建新的继承基类的配置类:

config.py(这里为开发和测试创建了不同的数据库)

import os
basedir = os.path.abspath(os.path.dirname(__file__))


class Config:  # 基类
    SECRET_KEY = 'some secret words'
    ITEMS_PER_PAGE = 10


class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')


class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')
    WTF_CSRF_ENABLED = False


config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,

    'default': DevelopmentConfig
}

 

通过from_object()方法导入配置:

from config import config  # 导入存储配置的字典

...
app = Flask(__name__)
app.config.from_object(config['development'])  # 获取相应的配置类
...

 

关于大型项目结构,扩展的初始化,使用程序工厂函数创建程序实例等内容见后续。

 

相关链接

 

– – – – –

更多关于Flask和Web开发的优质原创内容,欢迎关注Hello, Flask! – 知乎专栏

Flask实践:待办事项(ToDo-List)

这一次我用Flask实现了一个待办事项(To-Do List)应用。这篇文章主要介绍这个应用的大致实现过程和一些让交互更简洁的处理技巧。这个App用了这篇文章里提到的Materialize框架。

程序界面

程序界面

Demo体验:http://task5.herokuapp.com/

简化模板

不论是从用户体验的角度,还是从减少工作量,提高加载速度等方面考虑,页面和跳转应该尽可能的少一些,尤其是这样的工具类应用。这可以通过优化模板实现,也可以通过JavaScript实现。

举两个例子。

一、编辑条目

我找到了另一个使用Flask实现的To-Do List应用,做个参照。在这个应用里,加上基模板,他一共用了4个页面。

Demo:http://flask-todoapp.herokuapp.com/

编辑一个条目的流程是这样的:

  1. 点击编辑按钮,页面跳转到另一个模板
  2. 渲染出表单,编辑内容
  3. 提交后跳转回主页面。

当你点击新建或是编辑条目/分类时,会跳转到另一个页面:

跳转到新页面编辑条目

跳转到新页面编辑条目

这其实只要一个页面就够了。

借助jQuery,我们可以这样实现:

  1. 渲染主页面的条目时,为每一个条目附加一个编辑表单,使用CSS隐藏它(display: none)
  2. 点击编辑按钮时,隐藏条目,显示表单(使用jQuery的hide和show函数)
  3. 表单提交后跳转到原页面
编辑条目

编辑条目

这样一来,就只需要一个页面了。在续篇里,使用AJAX技术在后台传送数据,体验会更好。

二、删除条目

想象一下,你在豆瓣收藏了很多喜欢的条目,你想清理一下,在页面最底端你删除了一个条目,如果这时页面在删除后跳转,就回到页面顶端了,可你还想删除在底端的另一些条目。因为删除不用传递数据,我们可以用这个小技巧解决:

在页面上,点击删除按钮后,使用jQuery的slideUp()函数隐藏条目,在后台仍然指定相应的视图函数,但是这个函数不跳转到任何页面(只是在数据库里删除条目),只返回下面的204状态码(代表无内容)。

return (''), 204

这样删除条目的体验会更加流畅和自然。

 

数据库框架Flask-SQLAlchemy

因为程序很小,我这次仍然把配置选项放在单个文件里。这次使用了数据库框架Flask-SQLAlchemy,数据库使用SQLite。和其他扩展一样,安装后初始化并配置数据库:

from flask_sqlalchemy import SQLAlchemy

basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SECRET_KEY'] = 'a secret string'
app.config['SQLALCHEMY_DATABASE_URI'] = \
   'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

db = SQLAlchemy(app)

解释一下这里的配置选项:

  • SQLALCHEMY_DATABASE_URI:这里要注意拼写,不要把URI写成URL。前一个是通用资源表示号(Universal Resource Identifier),后者是统一资源定位符(Uniform Resource Locator)。
  • SQLALCHEMY_COMMIT_ON_TEARDOWN:按字面理解就是,在每次请求结束后自动提交数据库会话,也就是db.session.commit()。
    • 在2.0版本里,这个配置选项被认为有害,从文档里删掉了,在这个issue里讨论了这个问题。如果不想手动提交数据库会话,你可以继续使用它。
    • 但是有时候,你需要手动提交,比如在这个应用里,有一个创建新的分类的函数,创建后使用redirect函数跳转到这个新建的分类(需要分类的id作为参数),如果不提交数据库会话,就没法获得新建分类的id。
@app.route('/new-category', methods=['GET', 'POST'])
def new_category():
    name = request.form.get('name')
    category = Category(name=name)
    db.session.add(category)
    db.session.commit()  # commit后使下面的category.id获得实际值
return redirect(url_for('category', id=category.id))
  • SQLALCHEMY_TRACK_MODIFICATIONS:启动项目后会出现一个警告,我不太清楚这个配置的作用(跟踪对象的修改?)。将它设为True可以抑制这个警告。

 

上述配置会在你程序根目录下生成一个data.sqlite文件。而最后实例SQLAlchemy类的db对象表示程序的数据库,你在视图函数里使用它(db)来操作数据库。

 

一对多关系

在这个应用里,用到了一对多关系(条目和分类)。这是条目和分类的模型(Model):

class Item(db.Model):
    __tablename__ = 'items'
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.Text)
    category_id = db.Column(db.Integer, db.ForeignKey('categories.id'))  # step2


class Category(db.Model):
    __tablename__ = 'categories'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64))
    items = db.relationship('Item', backref='category')  # step1

实现一个一对多关系需要两步(这里父模型指分类Category,子模型指条目Item):

  1. 在父模型里relationship函数定义一个引向子模型的对象(items)
  2. 在子模型里定义一个外键(category_id),并添加一个到父模型的反向引用(backref)

在存储一个条目到数据库时,同时也要指定一个分类,这里的分类需要是一个对象(Object),而不是id:

body = request.form.get('item')  # 条目的内容
id = request.form.get('category')  # 分类的id
category = Category.query.get_or_404(id)  # 分类的对象
item = Item(body=body, category=category)
db.session.add(item)

这次还用到了另一个扩展——Flask-Script,用来增强命令行功能。暂时没有太深入的应用,有机会再作总结。

 

具体实现

程序比较简单,具体实现没太多需要介绍的。和之前不同的是,这次我使用jQuery对表单进行简单的验证。感兴趣的话,可以去体验一下demo,或是看看源码。为了方便体验(偷懒),没有添加用户系统,你可以随便玩,但条目和分类的数量做了一些限制。

目前功能比较简单,后续会加上拖拽排序,设置日期,设置优先级等功能(考虑加上像wunderlist那样的音效……)。

安装和下载见介绍页:https://helloflask.github.io/todo/

在续篇里,我会介绍使用jQuery(AJAX)把它改造成一个单页应用(SPA,Single-page application)。

 

相关链接

– – – – –

更多关于Flask和Web开发的优质原创内容,欢迎关注Hello, Flask! – 知乎专栏