Flask开发入门

First Post:

Last Update:

介绍

对于springboot+vue、asp.net还是其他一些技术,我们实现网页都需要较长的学习时间,因此,为了快速开发,满足需求,我参考Python的Flask编写了这篇文章。

我使用的是flask+mysql的web网页集成开发。

参考文献如下:

Flask官方文档

Flask 入门教程

既然介绍说完了,那就开始吧。

准备工作

在学习Flask之前,你需要具备Python和Html的基本知识,如果没有这方面的知识,可以先去学习一下再回到本文章

Python的本地环境建议3.9.0以上版本,对于IDE工具,你可以使用PyCharm、VsCode、VisualStudio

安装Flask

1
pip install flask

安装成功后就可以正式写代码了

一、你的第一个Flask应用

初始化项目

我使用的是PyCharm,当然你也可以选择其他IDE工具

新建一个项目,然后创建一个基于本地python解释器的虚拟环境(防止污染本地环境)

配置完成的结构

编写代码

在main.py中编写以下代码,这样你就成功拥有了第一个Flask应用

1
2
3
4
5
6
7
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
return 'Welcome to My Watchlist!'

运行截图

代码分析

如上面所见,我们成功初始化了一个flask项目,那么我们都做了什么呢?

1
2
from flask import Flask
app = Flask(__name__)

我们导入了flask库,实例化了这个类

1
2
3
@app.route('/')
def hello():
return 'Welcome !!!'

我们创建了一个函数,在这个函数前面,我们使用了@app.route('/')装饰器,它将我们导向了对应的url 我们可以在这里做一些数据的处理,然后你就可以启动服务后访问到它 对于函数名,我们可以自由定义

此处函数的return值可以被html解析成html,也可以是其他数据。

装饰器的一些使用原则

关于@app.route(‘/‘)URL规则

1.route内部的url都可以运行在部署的端口上,其编写规则是以斜线 /作为开头

1
2
3
@app.route('/login')
def hello():
return 'Welcome !!!'

2.对于一个函数,可以附加多个装饰器@app.route(‘/‘)。

1
2
3
4
5
@app.route('/')
@app.route('/login')
@app.route('/admin')
def login():
return open(html_path+"login.html", "r", encoding="utf-8").read()

3.对于@app.route(‘/‘),我们还可以定义变量。

1
2
3
@app.route('/user/<username>')
def user_page(username):
return '欢迎你' + username

运行截图

用户输入的数据会包含恶意代码,所以不能直接作为响应返回,需要使用 MarkupSafe(Flask 的依赖之一)提供的 escape() 函数对 name 变量进行转义处理,比如把 < 转换成 <。这样在返回响应时浏览器就不会把它们当做代码执行。

1
2
3
4
5
from markupsafe import escape

@app.route('/user/<username>')
def user_page(username):
return f'User: {escape(username)}'

运行截图

视图函数的一些技巧

我们可以通过url查看使用哪个函数

作为代表某个路由的端点(endpoint),同时用来生成视图函数对应的 URL。

对于程序内的 URL,Flask 提供了一个 url_for 函数来生成 URL,它接受的第一个参数就是端点值,默认为视图函数的名称

1
2
3
4
5
6
7
8
9
10
11
12
from flask import url_for
@app.route('/test')
def test_url_for():
# 下面是一些调用示例(请访问 http://localhost:5000/test 后在命令行窗口查看输出的 URL):
print(url_for('login')) # 生成 login 视图函数对应的 URL,将会输出:/admin
# 注意下面两个调用是如何生成包含 URL 变量的 URL 的
print(url_for('user_page', username='greyli')) # 输出:/user/greyli
print(url_for('user_page', username='peter')) # 输出:/user/peter
print(url_for('test_url_for')) # 输出:/test
# 下面这个调用传入了多余的关键字参数,它们会被作为查询字符串附加到 URL 后面。
print(url_for('test_url_for', num=2)) # 输出:/test?num=2
return 'Test page'

运行截图

举例

我导入一个html文件,然后返回这个html页面

main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os.path

from flask import Flask

app = Flask(__name__)
html_path = os.path.dirname(__file__) + "\\views\\"


@app.route('/')
def login():
return open(html_path+"login.html", "r", encoding="utf-8").read()


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

login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
<!DOCTYPE html>
<html lang="en">
<!-- https://codepen.io/danielkvist/pen/LYNVyPL -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
:root {
/* COLORS */
--white: #e9e9e9;
--gray: #333;
--blue: #0367a6;
--lightblue: #008997;

/* RADII */
--button-radius: 0.7rem;

/* SIZES */
--max-width: 758px;
--max-height: 420px;

font-size: 16px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}

body {
align-items: center;
/* 决定背景图像的位置是在视口内固定,或者随着包含它的区块滚动。 */
/* https://developer.mozilla.org/zh-CN/docs/Web/CSS/background-attachment */
background: var(--white) url("https://res.cloudinary.com/dbhnlktrv/image/upload/v1599997626/background_oeuhe7.jpg") no-repeat fixed center;
background-size: cover;
display: grid;
height: 100vh;
place-items: center;
}

.form__title {
font-weight: 300;
margin: 0 0 1.25rem;
}

.link {
color: var(--gray);
font-size: 0.9rem;
margin: 1.5rem 0;
text-decoration: none;
}

.container {
background-color: var(--white);
border-radius: var(--button-radius);
box-shadow: 0 0.9rem 1.7rem rgba(0, 0, 0, 0.25),
0 0.7rem 0.7rem rgba(0, 0, 0, 0.22);
height: var(--max-height);
max-width: var(--max-width);
overflow: hidden;
position: relative;
width: 100%;
}

.container__form {
height: 100%;
position: absolute;
top: 0;
transition: all 0.6s ease-in-out;
}

.container--signin {
left: 0;
width: 50%;
z-index: 2;
}

.container.right-panel-active .container--signin {
transform: translateX(100%);
}

.container--signup {
left: 0;
opacity: 0;
width: 50%;
z-index: 1;
}

.container.right-panel-active .container--signup {
animation: show 0.6s;
opacity: 1;
transform: translateX(100%);
z-index: 5;
}

.container__overlay {
height: 100%;
left: 50%;
overflow: hidden;
position: absolute;
top: 0;
transition: transform 0.6s ease-in-out;
width: 50%;
z-index: 100;
}

.container.right-panel-active .container__overlay {
transform: translateX(-100%);
}

.overlay {
background: var(--lightblue) url("https://cdn.pixabay.com/photo/2018/08/14/13/23/ocean-3605547_1280.jpg") no-repeat fixed center;
background-size: cover;
height: 100%;
left: -100%;
position: relative;
transform: translateX(0);
transition: transform 0.6s ease-in-out;
width: 200%;
}

.container.right-panel-active .overlay {
transform: translateX(50%);
}

.overlay__panel {
align-items: center;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
position: absolute;
text-align: center;
top: 0;
transform: translateX(0);
transition: transform 0.6s ease-in-out;
width: 50%;
}

.overlay--left {
transform: translateX(-20%);
}

.container.right-panel-active .overlay--left {
transform: translateX(0);
}

.overlay--right {
right: 0;
transform: translateX(0);
}

.container.right-panel-active .overlay--right {
transform: translateX(20%);
}

.btn {
background-color: var(--blue);
background-image: linear-gradient(90deg, var(--blue) 0%, var(--lightblue) 74%);
border-radius: 20px;
border: 1px solid var(--blue);
color: var(--white);
cursor: pointer;
font-size: 0.8rem;
font-weight: bold;
letter-spacing: 0.1rem;
padding: 0.9rem 4rem;
text-transform: uppercase;
transition: transform 80ms ease-in;
}

.form>.btn {
margin-top: 1.5rem;
}

.btn:active {
transform: scale(0.95);
}

.btn:focus {
outline: none;
}

.form {
background-color: var(--white);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 0 3rem;
height: 100%;
text-align: center;
}

.input {
background-color: #fff;
border: none;
padding: 0.9rem 0.9rem;
margin: 0.5rem 0;
width: 100%;
}

@keyframes show {

0%,
49.99% {
opacity: 0;
z-index: 1;
}

50%,
100% {
opacity: 1;
z-index: 5;
}
}
</style>
</head>

<body>
<div class="container right-panel-active">
<!-- Sign Up -->
<div class="container__form container--signup">
<form action="#" class="form" id="form1">
<h2 class="form__title">Sign Up</h2>
<label>
<input type="text" placeholder="User" class="input" />
</label>
<label>
<input type="email" placeholder="Email" class="input" />
</label>
<label>
<input type="password" placeholder="Password" class="input" />
</label>
<button class="btn">Sign Up</button>
</form>
</div>

<!-- Sign In -->
<div class="container__form container--signin">
<form action="#" class="form" id="form2">
<h2 class="form__title">Sign In</h2>
<label>
<input type="email" placeholder="Email" class="input" />
</label>
<label>
<input type="password" placeholder="Password" class="input" />
</label>
<a href="#" class="link">Forgot your password?</a>
<button class="btn">Sign In</button>
</form>
</div>

<!-- Overlay -->
<div class="container__overlay">
<div class="overlay">
<div class="overlay__panel overlay--left">
<button class="btn" id="signIn">Sign In</button>
</div>
<div class="overlay__panel overlay--right">
<button class="btn" id="signUp">Sign Up</button>
</div>
</div>
</div>
</div>

<script>
const signInBtn = document.getElementById("signIn");
const signUpBtn = document.getElementById("signUp");
const fistForm = document.getElementById("form1");
const secondForm = document.getElementById("form2");
const container = document.querySelector(".container");

signInBtn.addEventListener("click", () => {
container.classList.remove("right-panel-active");
});

signUpBtn.addEventListener("click", () => {
container.classList.add("right-panel-active");
});

fistForm.addEventListener("submit", (e) => e.preventDefault());
secondForm.addEventListener("submit", (e) => e.preventDefault());

</script>
</body>

</html>

运行截图

初始化的登录页面

二、HTML模板

在一般的 Web 程序里,访问一个地址通常会返回一个包含各类信息的 HTML 页面。

因为我们的程序是动态的,页面中的某些信息需要根据不同的情况来进行调整,比如对登录和未登录用户显示不同的信息,所以页面需要在用户访问时根据程序逻辑动态生成。

我们把包含变量和运算逻辑的 HTML 或其他格式的文本叫做模板,执行这些变量替换和逻辑计算工作的过程被称为渲染,这个工作我们使用模板渲染引擎——Jinja2 来完成。

通常来讲,会新建一个templates目录来存放模板,static目录存放静态文件。

模板基本语法

1
2
3
4
5
6
<h1>{{ username }}的个人主页</h1>
{% if bio %}
<p>{{ bio }}</p> {# 这里的缩进只是为了可读性,不是必须的 #}
{% else %}
<p>自我介绍为空。</p>
{% endif %} {# 大部分 Jinja 语句都需要声明关闭 #}

Jinja2 的语法和 Python 大致相同,你在后面会陆续接触到一些常见的用法。在模板里,你需要添加特定的定界符将 Jinja2 语句和变量标记出来,下面是三种常用的定界符:

  • {{ ... }} 用来标记变量。
  • {% ... %} 用来标记语句,比如 if 语句,for 语句等。
  • `` 用来写注释。

编写主页模板

我们先在 templates 目录下创建一个 index.html 文件,作为主页模板。主页需要显示电影条目列表和个人信息,代码如下所示:

templates/index.html:主页模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ name }}'s Watchlist</title>
</head>
<body>
<h2>{{ name }}'s Watchlist</h2>
{# 使用 length 过滤器获取 movies 变量的长度 #}
<p>{{ movies|length }} Titles</p>
<ul>
{% for movie in movies %} {# 迭代 movies 变量 #}
<li>{{ movie.title }} - {{ movie.year }}</li> {# 等同于 movie['title'] #}
{% endfor %} {# 使用 endfor 标签结束 for 语句 #}
</ul>
<footer>
<small>&copy; 2018 <a href="http://helloflask.com/book/3">HelloFlask</a></small>
</footer>
</body>
</html>

为了方便对变量进行处理,Jinja2 提供了一些过滤器,语法形式如下:

1
{{ 变量|过滤器 }}

左侧是变量,右侧是过滤器名。比如,上面的模板里使用 length 过滤器来获取 movies 的长度,类似 Python 里的 len() 函数。

提示 访问 https://jinja.palletsprojects.com/en/3.0.x/templates/#builtin-filters 查看所有可用的过滤器。

准备虚拟数据

为了模拟页面渲染,我们需要先创建一些虚拟数据,用来填充页面内容:

app.py:定义虚拟数据

1
2
3
4
5
6
7
8
9
10
11
12
13
name = 'Grey Li'
movies = [
{'title': 'My Neighbor Totoro', 'year': '1988'},
{'title': 'Dead Poets Society', 'year': '1989'},
{'title': 'A Perfect World', 'year': '1993'},
{'title': 'Leon', 'year': '1994'},
{'title': 'Mahjong', 'year': '1996'},
{'title': 'Swallowtail Butterfly', 'year': '1996'},
{'title': 'King of Comedy', 'year': '1999'},
{'title': 'Devils on the Doorstep', 'year': '1999'},
{'title': 'WALL-E', 'year': '2008'},
{'title': 'The Pork of Music', 'year': '2012'},
]

渲染主页模板

使用 render_template() 函数可以把模板渲染出来,必须传入的参数为模板文件名(相对于 templates 根目录的文件路径),这里即 'index.html'。为了让模板正确渲染,我们还要把模板内部使用的变量通过关键字参数传入这个函数,如下所示:

app.py:返回渲染好的模板作为响应

1
2
3
4
5
6
7
from flask import Flask, render_template

# ...

@app.route('/')
def index():
return render_template('index.html', name=name, movies=movies)

为了更好的表示这个视图函数的作用,我们把原来的函数名 login 改为 index,意思是“索引”,即主页。

在传入 render_template() 函数的关键字参数中,左边的 movies 是模板中使用的变量名称,右边的 movies 则是该变量指向的实际对象。这里传入模板的 name 是字符串,movies 是列表,但能够在模板里使用的不只这两种 Python 数据结构,你也可以传入元组、字典、函数等。

render_template() 函数在调用时会识别并执行 index.html 里所有的 Jinja2 语句,返回渲染好的模板内容。在返回的页面中,变量会被替换为实际的值(包括定界符),语句(及定界符)则会在执行后被移除(注释也会一并移除)。

运行截图

三、静态文件

静态文件(static files)和我们的模板概念相反,指的是内容不需要动态生成的文件。比如图片、CSS 文件和 JavaScript 脚本等。

在 Flask 中,我们需要创建一个 static 文件夹来保存静态文件,它应该和程序模块、templates 文件夹在同一目录层级。

生成静态文件 URL

在 HTML 文件里,引入这些静态文件需要给出资源所在的 URL。为了更加灵活,这些文件的 URL 可以通过 Flask 提供的 url_for() 函数来生成。

举例:

1
<img src="{{ url_for('static', filename='foo.jpg') }}">

在 Python 脚本里,url_for() 函数需要从 flask 包中导入,而在模板中则可以直接使用,因为 Flask 把一些常用的函数和对象添加到了模板上下文(环境)里。

添加 Favicon

Favicon(favourite icon) 是显示在标签页和书签栏的网站头像。你需要准备一个 ICO、PNG 或 GIF 格式的图片,大小一般为 16×16、32×32、48×48 或 64×64 像素。把这个图片放到 static 目录下,然后像下面这样在 HTML 模板里引入它:

templates/index.html:引入 Favicon

1
2
3
4
<head>
...
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
</head>

保存后刷新页面,即可在浏览器标签页上看到这个图片。

添加图片

为了让页面不那么单调,我们来添加两个图片:一个是显示在页面标题旁边的头像,另一个是显示在页面底部的龙猫动图。我们在 static 目录下面创建一个子文件夹 images,把这两个图片都放到这个文件夹里:

1
2
$ cd static
$ mkdir images

创建子文件夹并不是必须的,这里只是为了更好的组织同类文件。同样的,如果你有多个 CSS 文件,也可以创建一个 css 文件夹来组织他们。下面我们在页面模板中添加这两个图片,注意填写正确的文件路径:

templates/index.html:添加图片

1
2
3
4
5
6
<h2>
<img alt="Avatar" src="{{ url_for('static', filename='images/avatar.png') }}">
{{ name }}'s Watchlist
</h2>
...
<img alt="Walking Totoro" src="{{ url_for('static', filename='images/totoro.gif') }}">

提示 这两张图片你可以自己替换为任意的图片(注意更新文件名),也可以在示例程序的 GitHub 仓库下载。

添加 CSS

虽然添加了图片,但页面还是非常简陋,因为我们还没有添加 CSS 定义。下面在 static 目录下创建一个 CSS 文件 style.css,内容如下:

static/style.css:定义页面样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/* 页面整体 */
body {
margin: auto;
max-width: 580px;
font-size: 14px;
font-family: Helvetica, Arial, sans-serif;
}

/* 页脚 */
footer {
color: #888;
margin-top: 15px;
text-align: center;
padding: 10px;
}

/* 头像 */
.avatar {
width: 40px;
}

/* 电影列表 */
.movie-list {
list-style-type: none;
padding: 0;
margin-bottom: 10px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
}

.movie-list li {
padding: 12px 24px;
border-bottom: 1px solid #ddd;
}

.movie-list li:last-child {
border-bottom:none;
}

.movie-list li:hover {
background-color: #f8f9fa;
}

/* 龙猫图片 */
.totoro {
display: block;
margin: 0 auto;
height: 100px;
}

接着在页面的 <head> 标签内引入这个 CSS 文件:

templates/index.html:引入 CSS 文件

1
2
3
4
<head>
...
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css">
</head>

提示 当你把 CSS 写到单独的文件后,浏览器获取到这个文件后会对其进行缓存(其他静态文件同理,比如 JavaScript 文件)。Flask 从 2.0 版本开始支持自动重载静态文件的变化,如果你使用的仍然是旧版本的 Flask,那么每当你对 CSS 文件的内容进行更新后,都需要使用下面的快捷键清除缓存:

  • Google Chrome(Mac):Command + Shift + R
  • Google Chrome(Windows & Linux):Ctrl + F5
  • Firefox(Mac):Command + Shift + R
  • Firefox(Windows & Linux):Ctrl + F5
  • Safari:Command + Option + R

最后要为对应的元素设置 class 属性值,以便和对应的 CSS 定义关联起来:

templates/index.html:添加 class 属性

1
2
3
4
5
6
7
8
9
<h2>
<img alt="Avatar" class="avatar" src="{{ url_for('static', filename='images/avatar.png') }}">
{{ name }}'s Watchlist
</h2>
...
<ul class="movie-list">
...
</ul>
<img alt="Walking Totoro" class="totoro" src="{{ url_for('static', filename='images/totoro.gif') }}">

运行截图

totoro avatar

四、数据库

使用 SQLAlchemy 操作数据库

为了简化数据库操作,我们将使用 SQLAlchemy——一个 Python 数据库工具(ORM,即对象关系映射)。借助 SQLAlchemy,你可以通过定义 Python 类来表示数据库里的一张表(类属性表示表中的字段 / 列),通过对这个类进行各种操作来代替写 SQL 语句。这个类我们称之为模型类,类中的属性我们将称之为字段

Flask 有大量的第三方扩展,这些扩展可以简化和第三方库的集成工作。我们下面将使用一个叫做 Flask-SQLAlchemy 的扩展来集成 SQLAlchemy。

但是我的开发更喜欢用MySQL数据库或者SqlServer,这里不对这个数据库进行过多介绍。

使用MySQL数据库

在python中,可以使用pymysql库进行数据库操作

1
2
# 安装pymysql
pip install pymysql

在mysql数据库中新建

image-20240716235643515

编写一个mysql配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import pymysql
from pymysql import cursors
class DbConfig:
def __init__(self):
self.conn = pymysql.connect(
host='127.0.0.1', # 主机名(或IP地址)
port=3306, # 端口号,默认为3306
user='root', # 用户名
password='123456', # 密码
charset='utf8mb4', # 设置字符编码
autocommit=True, # 自动提交更改
database='flask_study', # 数据库名称
cursorclass=cursors.DictCursor # 转换成字典格式输出
)
self.cursor = self.conn.cursor()

def findUser(self):
self.cursor.execute('select * from user where id=1')
users = self.cursor.fetchall()
return users

def findMovies(self):
self.cursor.execute('select * from movies')
movies = self.cursor.fetchall()
return movies

def __del__(self):
self.cursor.close()
self.conn.close()

在使用数据的时候可以像下面这样写

1
2
name = DbConfig().findUser()[0]['username']
movies = DbConfig().findMovies()

完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import pymysql
from flask import Flask, render_template
from pymysql import cursors


class DbConfig:
def __init__(self):
self.conn = pymysql.connect(
host='127.0.0.1', # 主机名(或IP地址)
port=3306, # 端口号,默认为3306
user='root', # 用户名
password='123456', # 密码
charset='utf8mb4', # 设置字符编码
autocommit=True, # 自动提交更改
database='flask_study', # 数据库名称
cursorclass=cursors.DictCursor # 转换成字典格式输出
)
self.cursor = self.conn.cursor()

def findUser(self):
self.cursor.execute('select * from user where id=1')
users = self.cursor.fetchall()
return users

def findMovies(self):
self.cursor.execute('select * from movies')
movies = self.cursor.fetchall()
return movies

def __del__(self):
self.cursor.close()
self.conn.close()


app = Flask(__name__)


@app.route('/')
def index():
name = DbConfig().findUser()[0]['username']
movies = DbConfig().findMovies()
return render_template('index.html', name=name, movies=movies)


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

运行截图

大体上就是这个样子了

这种开发满足了基本需求,开发周期短,但是想要好看的页面,可以使用json数据做一个前后端分离或者整合的项目。