前言:有句老话,叫做「现在都 8102 年了,你怎么还 XXX?」。随着前端工具的越来越完善和好用,现在前端能做的东西,实在太多了。而现在主流的 Flask 教程,都是基于以往的服务端模板渲染的架构。这在 2018 年,未免有些过时和笨拙。我曾看过一个用 Flask 写的 Todo 项目,每个交互都要向服务端发送 AJAX, 甚至连动态添加 DOM 元素都交由服务端渲染好再用 jQuery 添加。本系列文章,亦将由一个 Todo App 入手,实践前后端分离的架构,进而初窥全栈开发的门径。诚然,在前后端分离的系统中,Python 作为后端并不是一个最优的选择(出门右转 Golang)。但一则我热爱 Python 和 Flask,二则别的我也不太会,所以我假定阅读本文的作者,已经看过Flask 的官方文档,或Miguel Grinberg 的 Flask Mega 教程。那么现在开始。

本文项目地址: https://github.com/frostming/flask-vue-todo

前后端分离的思路

有人要问,我为什么要前后端分离?这个说起就话长了,网上也能搜索到一些解答,不过可简要概括为以下两点:

  • 前端越来越重,很多页面交互,交由前端来实现会更加方便。我一直秉承:让专业的人做专业的事。这样事情会做得更漂亮。
  • 前后端脱耦,可以分别交给两个人(团队)去做,且不会互相牵制。

那么哪些事是前端该做哪些是后端该做的呢?凡是涉及页面逻辑的部分,都是前端的工作,包括路由,渲染,页面事件等等。而只有在需要服务端的数据时,才给后端发请求。这样能大大节省网络带宽,减少网络延时的影响,一切交互都在本地,享受飞一般的感觉。特别是 Todo App,你肯定不想每加一项,勾选一个完成都要 busy 一阵吧,哪怕就是 10ms 也是无法忍受,所以 Todo App 非常适合用前后端分离来实现。当然,Todo App 也是各种前端框架的常见例子了,所以不太了解前端的各位 Pythonista 们,照着教程来一遍就差不多了,Flask 的后端仅仅需要完成两个功能:

  • 将内容持久化到服务器数据库
  • 加入用户验证系统

建立 Vue 应用

我选用 Vue.js 作为前端框架,当然用 React.js 也是可以的,它们都有强大的工具链,但 Vue.js 的好处是它是中国人开发的,几乎所有官方库文档都有中文版哦,方便学习嘛,而且个人感觉 Vue.js 用起来也确实更爽一点。

目录结构

与传统的 Flask app 不同,前后端分离架构推荐静态文件(html, css, js 们)和 Python 文件分开存放。目录结构如下:

flask-vue-todo
├─frontend    # 存放前端文件
├─backend    # 存放python文件

安装依赖

首先我们需要安装一键建立 Vue 项目的命令行工具vue-cli,安装方法(本文使用 Yarn 管理前端依赖,npm 大同小异):

yarn global add vue-cli

按照上述结构建立好项目之后,进入frontend目录,执行:

vue init webpack-simple

在一通眼花缭乱的进度条之后项目就建好了,执行yarn run dev看看效果吧。

编写 Todo App

这一部分我不做重点介绍。此应用主要有以下逻辑:

  • 输入内容按下回车时在 Todo 列表中加上一项
  • 点 Todo 项前的 checkbox 将其标为完成
  • 点 Todo 项的红叉将其删除
  • 通过 All, Undone, Completed 过滤显示的 Todo 项

我使用了Vuex来管理应用的状态。注意把 Ajax 请求部分单独抽离到一个文件中方便管理,这时你可以先让它永远返回成功即可。为了符合之后即将使用的 axios 的 API,可以这样写请求:

// api/index.js
const mockTodos = [
  { id: 1, text: "Item 1", done: false },
  { id: 2, text: "Item 2", done: true },
]

const mockRequest = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      Math.random() < 0.85 ? resolve(mockTodos) : reject(new Error("Get Todo list error!"))
    }, 100)
  })
}

const api = {
  getTodos() {
    return mockRequest("/todos")
  },
}

当然,我在应用中做了很多美化的工作让应用显得高大上,符合 Vue.js 的 UI。

再次执行yarn run dev(若已执行则不必,它会自动热重载),你会看到编写完成的效果。yarn run build来编译已经写好的源文件。

编写 Flask 部分

好了,现在切换到backend目录,后端的应用预备作为一个 API server 来使用,为方便与前端交互,输入输出均采用 JSON 格式,Flask 中可用flask.jsonify将结果转换成 JSON 的响应。告别看文档啃 Stackoverflow 爬坑,一切都是熟悉的味道,写起 Flask 来那还不上下翻飞?所有 API 请求都给它放到一个蓝图里,包含以下接口:

  • 获取所有 Todo 项,包括它们的完成状态
  • 更新 Todo 项
  • 删除 Todo 项
  • 新建 Todo 项

这根本就是数据库的增删查改嘛,用上flask-sqlalchemy简直不要太方便。其实这么简单的操作无需用 SQL,用一个 NonSQL 数据库会更好,但为了部署 Heroku,它提供免费的 PostgreSQL 数据库。主路由就简单了,只剩一个index了,因为页面路由都交给前端了嘛,这时我们的 App 就成了一个「单页应用」(SPA)了。

@app.route('/')
def index():
    return render_template('index.html')

且慢,因为我们改换了目录结构,你必须告诉 Flask 静态文件和 html 文件的正确位置,编译好的静态文件在frontend/dist中,index.htmlfrontend中:

FRONTEND_FOLDER = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'frontend')

def create_app():
    app = Flask(
        __name__, static_folder=os.path.join(FRONTEND_FOLDER, 'dist'),
        template_folder=FRONTEND_FOLDER
    )
    ...

对了,不要记得所有错误也都以 JSON 格式返回:

from werkzeug.exceptions import HTTPException

@app.errorhandler(HTTPException)
def handle_http_error(exc):
    return jsonify({'status': 'error', 'description': exc.description}), exc.code

好了,现在可以把前端部分中之前伪造的请求换成真的了,我就用的 Vue.js 推荐的axios,需要初始化一下,把所有请求变成 JSON 请求:

import axios from "axios"

const api = axios.create({
  headers: {
    "Content-Type": "application/json",
  },
})

赶紧运行FLASK_ENV=development flask run吧,然后你就能从http://localhost:5000看到效果了。

关于前端开发服务器和后端开发服务器

可能有的同学已经注意到了,前端和后端都有一个开发服务器,但默认端口号不同,一个是 8080,一个是 5000。其中 8080 的开发服务器是调试前端页面用的,它仅仅包含静态文件,这时后端 API 是不可用状态的。但它有很多方便调试的功能,比如详尽的错误信息和热重载,编写前端时,用这个就够了,但 API 请求需要弄成假的。

而 5000 端口的服务器是 Flask 提供的,启用了FLASK_ENV=development可以打开 Flask 的DEBUG模式。它也能访问主页,但那是前端已经编译好的,不支持热重载哦。当然,Flask 支持 Python 文件热重载,现在知道专业的人干专业的事的道理了吧。区别总结如下:

localhost:8080 localhost:5000
能访问页面?
能访问 API?
热重载 HTML/CSS/Javascript Python
更新静态文件 刷新生效 yarn run build,再强制刷新

还有,这两个服务器,都不能在生产环境使用哦。那么,能否同时获取这两个服务器的好处呢?当然是可以了,同时启动两个服务器,然后把 Flask 启动的那个 5000 服务器单纯作为 API 服务器,从 8080 端口访问页面。这时,API 请求的 URL 就与当前地址不同了,需要显式配置请求 URL 到 5000 端口:

...
const api = axios.create({
  baseURL: 'http://localhost:5000',
  headers: {
    'Content-Type': 'application/json'
  }
})

好,到现在为止,我们已经成功运行了一个可以持久化到服务器数据库的 Todo App,下篇文章我们将会加入更多功能,使得 App 更加像样。