基于Python3.7和Flask的HTML5开发入门

2019-02-10 21:01:35


本文将会讲解基于Python3.7+Flask+AJAX+Vue.js+Semantic UI的HTML5开发入门。

如果你能认真看并且看懂这篇文章并且对这些东西感兴趣并且能够深入进去钻研,那么你应该可以在三小时内搓出来一个类似于https://yutong.site/sam 这种的东西。

本文假设读者会Python3和一些简单的Web开发相关的知识。

本文将会以开发一个简单的后缀自动机绘制程序为例讲解主要内容。

(原谅我语文太烂几乎不会任何生动的描述...所以只能这样了..)

正文

成品效果

https://yutong.site/sam

(洛谷的图床有一定的压缩)

前言

Python

想必很多人都知道Python可以拿来做高精度。

但Python的用途远不止于$O(n^{log_23})$的复杂度计算高精度乘法。

这篇教程也选择了Python3作为后端语言来开发Web App。

Flask

Flask 是一个Python的历史悠久的Web开发框架,在各种项目中得到了广泛的应用。

(其实Python的Web框架还有Django等一大堆,不过Flask是比较轻量的了..)

HTML5

HTML5是HTML最新的修订版本,由万维网联盟(W3C)于2014年10月完成标准制定。目标是取代1999年所制定的HTML 4.01和XHTML 1.0标准,以期能在互联网应用迅速发展的时候,使网络标准达到匹配当代的网络需求。广义论及HTML5时,实际指的是包括HTML、CSS和JavaScript在内的一套技术组合。它希望能够减少网页浏览器对于需要插件的丰富性网络应用服务(Plug-in-Based Rich Internet Application,RIA),例如:Adobe Flash、Microsoft Silverlight与Oracle JavaFX的需求,并且提供更多能有效加强网络应用的标准集。

HTML5添加了许多新的语法特征,其中包括<video>、<audio>和<canvas>元素,同时集成了SVG内容。这些元素是为了更容易的在网页中添加和处理多媒体和图片内容而添加的。其它新的元素如<section>、<article>、<header>和<nav>则是为了丰富文档的数据内容。新的属性的添加也是为了同样的目的。同时也有一些属性和元素被移除掉了。一些元素,像<a>、<cite>和<menu>被修改,重新定义或标准化了。同时APIs和DOM已经成为HTML5中的基础部分了。HTML5还定义了处理非法文档的具体细节,使得所有浏览器和客户端程序能够一致地处理语法错误。

——维基百科

简而言之,一个较于十几年前的HTML4提供了更多更强大功能的新的HTML标准。现在主流浏览器均已支持。

AJAX

一句话描述AJAX:在不刷新页面的情况下,向服务端发送HTTP请求并获取结果。

本文将使用jQuery的AJAX部分。

Vue.js

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

如果你想在深入学习 Vue 之前对它有更多了解,我们制作了一个视频,带您了解其核心概念和一个示例工程。

如果你已经是有经验的前端开发者,想知道 Vue 与其它库/框架有哪些区别,请查看对比其它框架。

——https://cn.vuejs.org/v2/guide/

用通俗易懂的语言描述,Vue可以做到像开发Native UI程序一样方便的操作Web程序,同时可以做到将Web程序中的元素很方便的与数据进行绑定。

Vue官方有完整的中文文档。

开发前的准备

项目文件夹架构

SAMDrawer
--static\(用来存放静态文件,比如图片,JS..可以直接通过/static路由访问)
----jqueryXXXXX.js
----vueXXXXX.js
--templates\(用来存放web页面文件)
----base.html(所有Web页面的公共父级模板)
----main.html(主页)
--.gitignore
--ds_drawer.py(接受字符串,返回绘制得到的图片文件)
--LICENSE(开源协议,本程序为MIT协议)
--main.py(主程序)
--README.md(git仓库介绍)

第一行代码

使用Flask首先需要导入相关模块。

Flask相关的大部分东西都在模块flask里。

import flask

app = flask.Flask("SAM Drawer")

@app.route("/")
def home()->str:
    return "qwq"

def main()->None:
    app.run(port=8080)

if __name__ == "__main__":
    main()

运行这个程序,如果没有异常应该会得到类似于下面的信息。

PS G:\SAMDrawer> py qwq.py
 * Serving Flask app "SAM Drawer" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:8080/ (Press CTRL+C to quit)

然后打开浏览器输入http://127.0.0.1:8080 ,如果看到了qwq就说明这个应用运行成功了。

Ctrl+C 结束程序。

flask.Flask 的构造函数第一个参数为应用名,其他参数见Flask官方文档

app.route 为一个函数装饰器,使用这个装饰器修饰的函数自动进入路由。

程序中出现的为最朴素的路由,然而Flask的路由还可以带参数,此处暂时不讲解。

什么是路由?简单的说,这个装饰器的第一个参数,就是你在浏览器中键入的路径。

例如对于http://127.0.0.1:8080/qwqqwq,会匹配到路由/qwqqwq ,如果找不到相应的路由,Flask就会返回一个404页面。

对于一个路由处理函数,如果直接返回str 类型的数据,那么这些数据就会被直接发给客户端,对于其他情况(例如使用模板或者自定义的Response),下面会有说明。

app.run()是用于调试情况下的启动Flask服务器的函数。

实际运营环境中最好不要使用,而应该通过uWSGIFlask程序部署至nginx或者apache等服务端上。

if __name__ == "__main__" 是因为,处于模块全局位置的代码会在每次模块被import的时候执行,但如果这个模块作为整个Python程序的入口点,那么当前情况下的__name__ 就会是__main__,而在其他情况下则会是模块的名字。

开始写HTML吧

Flask使用Jinja作为模板引擎。

为什么需要模板?

当服务端想要通过HTML向客户端传输一些东西的时候,一个常见的选择就是直接把东西写在HTML内。

对于PHP之类的语言,可以直接在HTML文件内嵌入语句来实现。

而对于Flask,则可以通过在HTML内放置一些占位符,然后通过模板渲染引擎解析这些占位符并替换成对应的数据。

templates 文件夹内新建main.html ,并在其中输入以下代码。

<html>

<head>
    <title>
        当前时间
    </title>
</head>

<body>
    服务端时间为:{{SERVER_TIME}}
</body>

</html>

然后把/的路由函数替换为以下内容。

@app.route("/", methods=["GET"])
def home()->str:
    return flask.render_template("main.html", SERVER_TIME=time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))

methods参数为一个list,指定这个路由可以被访问的方式,常见的有GETPOST,由于本文重点不在HTTP协议,故不会详细说明。

简而言之,直接在浏览器地址栏输入地址打开网页的方式一般都是GET

flask.render_template函数执行渲染模板,并且返回一个Response,其第一个参数为模板的文件名,这个文件所处的目录可以通过Flask 类的构造函数中的template_folder参数指定(默认为templates )

之后为一个可变参数表,表示各种预定义的标识符。

模板中的{{SERVER_TIME}} 表示这里将会被render_template函数传入的SERVER_TIME 参数所替换。

更复杂的HTML5

下面将会使用UI库Semantic UI。

请预先将vue.jsjquery-3.3.1.jssemantic.jssemantic.css放到/static目录里。

Semantic UI是一个强大且开源的HTML5界面库,官方有完整详细的帮助文档(https://semantic-ui.com/introduction/getting-started.html )。

由于篇幅原因不会对Semantic UI进行详细介绍(否则至少还能再写十篇日报),只会介绍其中一些常用且关键的东西。

Semantic UI 主要通过元素所属的class来区分不同的控件。

<html>

<head>
    <script src="/static/jquery-3.3.1.js"></script>
    <script src="/static/vue.js"></script>
    <script src="/static/semantic.js"></script>
    <link rel="stylesheet" href="/static/semantic.css">
    <title>
        SAM Drawer
    </title>
</head>
<body style="background-color: rgb(236, 233, 233)">
    <div class="ui main container" style="margin-top:70px;margin-bottom:70px">
        <script>
            var vue;
            $(this).ready(function () {
                vue = new Vue({
                    delimiters: ['{[', ']}'],
                    el: "#main",
                    data: {
                        error: false,
                        error_text: "",
                        str: "",
                        showing: false,
                        loading: false,
                        svg_content: ""
                    },
                    methods: {
                        submit: function () {
                            $("svg-content").css({ "height": "100px" });
                            this.error = false
                            this.showing = true;
                            this.loading = true;
                            $.ajax({
                                url: "/draw",
                                method: "POST",
                                async: true,
                                data: {
                                    string: vue.str
                                },
                                success: function (result) {
                                    result = JSON.parse(result)
                                    console.log(result);
                                    vue.loading = false;
                                    if (result.code != 0) {
                                        vue.error = true;
                                        vue.error_text = result.message;
                                        return;
                                    }
                                    vue.svg_content = result.data;
                                }
                            });
                        }
                    }
                });
            });
        </script>
        <div style="top: 10%">
            <div class="ui left aligned container" style="width: 100%" id="main">
                <div class="ui header">
                    <h1>SAM Drawer</h1>
                </div>
                <div class="ui error message" v-if="error">
                    <div class="ui header">
                        错误
                    </div>
                    <p>{[error_text]}</p>
                </div>
                <div class="ui segment stacked">
                    <div class="ui inverted active dimmer" v-if="loading">
                        <div class="ui text loader">
                            正在加载..
                        </div>
                    </div>
                    <div class="ui form" id="main-form">
                        <div class="field">
                            <label>请输入字符串:</label>
                            <input type="text" id="string" v-model="str" v-on:keyup.enter="submit">
                        </div>
                        <p>如果要绘制广义SAM,请使用|分隔字符串</p>
                        <div class="ui submit button" id="submit-button" v-on:click="submit">提交</div>
                    </div>
                </div>
                <div v-if="showing" class="ui segment stacked">
                    <div class="ui inverted active dimmer" v-if="loading">
                        <div class="ui text loader">
                            正在加载..
                        </div>
                    </div>
                    <div id="svg-content" class="ui center aligned container" style="min-height: 100px"
                        v-html="svg_content">

                    </div>
                </div>
            </div>
        </div>
    </div>
</body>

</html>
```

由于大部分情况下Semantic UI并不依赖于元素名来区分控件,故我的代码中大部分元素均为```<div>``` 

```ui``` 大部分的Semantic UI的空间均有这个class.

```container``` Semantic UI中的容器,容器可以自动调整大小。正常情况下,其他所有的Semantic UI元素均应该直接或间接放在容器内。

```main container``` 表示这是主容器。

```plain
A container is an element designed to contain page elements to a reasonable maximum width based on the size of a user's screen. This is useful to couple with other UI elements like grid or menu to restrict their width to a reasonable size for display.
—— https://semantic-ui.com/elements/container.html
```

```ui left aligned container``` 表示这是个左对齐容器,即其所有子元素默认情况下为左对齐。

```ui header``` 表示这是一个Semantic UI的header(https://semantic-ui.com/elements/header.html ).

```ui segment stacked``` 是一个我比较喜欢的东西,会创建一个边框有堆叠效果(类似于一堆纸叠在一起)的线框(https://semantic-ui.com/elements/segment.html )。

```ui inverted active dimmer ```为一个激活且具有反色效果的Dimmer(原谅我词穷不知道该如何翻译),这个Dimmer一会会被用在加载时的等待动画(https://semantic-ui.com/modules/dimmer.html )。 

```ui text loader ```是一个带文本的加载器,具体效果就是一个转着的圆圈,下面会带一行字(https://semantic-ui.com/elements/loader.html )。

```ui form ```是Semantic UI的表单组件(https://semantic-ui.com/collections/form.html )。我使用这个仅仅用于布局,访问服务端仍然是AJAX。 

```field``` 是表单中的一个域。默认情况下每个域占一行。

### jQuery部分

```$(obj)``` 表示选择相应的对象,```obj``` 可以是一个具体的对象或者选择器。

选择器为一个字符串,形如```.ui``` (选择所有class带ui的元素),```#qwq``` 选择所有id为qwq的元素,```owo``` ,选择所有名称为```owo``` 的元素。

``` $(obj).ready(func) ``` 指定当```obj``` 被加载完成后应该调用的函数。

全局空间里,```this ```表示整个页面的对象。

```$(obj).css({K:V})``` 更改元素的CSS。

```$.ajax(obj)``` 发送AJAX请求。```obj``` 为一个对象,描述请求的具体内容。

通常包括以下内容

```javascript
{
     url: "/draw", //请求的目标URL
     method: "POST",//请求的方式
     async: true,//是否异步
     data: {//请求体
     },
     success: function (result) {//请求成功时的回调函数,result为服务端的回复,字符串
     }
}

Vue.js部分

vue = new Vue(obj) 初始化一个新的Vue 对象,使用obj 来描述对象的一些属性。

obj 通常形如:

{
    el:"#qwq",//指定这个Vue对象的根元素,这个元素的所有直接或间接子元素均会被纳入这个对象之中。
    data:{

    },
        //这个对象所能使用的数据。
        //Vue对象中的一个元素可以与里面的数据绑定,并且可以随着数据的修改而做出响应式的变化。
        //例如可以通过v-if将一个对象是否渲染绑定到data中的一个Boolean变量上。
        //还可以通过v-model将一个文本框的内容绑定到一个字符串变量上。
    ,methods:{

    }//这个Vue对象会使用到的函数,例如可以通过v-on来指定当一个按钮被点击时调用的函数。
}

<div class="ui error message" v-if="error"> 是一个错误提示区域,当error 为true时,这个区域才会显示。

{[error_text]}是具体的错误文本。为了避免与Jinja的模板标识符冲突,特地换成了{[]}

<div class="ui inverted active dimmer" v-if="loading">loading为true的时候,这个loader才会显示。

<input type="text" id="string" v-model="str" v-on:keyup.enter="submit"> 这个文本框的内容与 str绑定,当文本框的内容被修改时,str的内容也会发生相应的变化。

同时指定了当回车键按下时调用submit函数。

<div class="ui submit button" id="submit-button" v-on:click="submit">提交</div>定义了一个Semantic UI中的表单提交按钮,同时这个按钮被点击的时候会调用submit函数。

<div id="svg-content" class="ui center aligned container" style="min-height: 100px"v-html="svg_content"> 声明了一个div,同时将这个div的HTML内容绑定到svg_content变量(因为服务端发回来的渲染结果是svg格式的)。

submit函数。这个函数用来提交给服务器一个字符串,并获取渲染结果。

this.error = false; //隐藏掉错误显示
this.showing = true;//显示图像
this.loading = true;//显示加载动画

服务端成功发送请求后:

result = JSON.parse(result)//解析JSON。服务端发回的结果是JSON。
console.log(result);//输出调试信息
vue.loading = false;//隐藏加载动画。
if (result.code != 0) {//服务端发回的JSON中有一个属性为code,0表示成功,非0表示失败、
    vue.error = true;//显示错误
    vue.error_text = result.message;//设置错误信息
    return;
}
vue.svg_content = result.data;//正常情况下,设定svg内容。

后端部分

ds_drawer.py

SAM绘制器。需要Graphviz

from graphviz import Digraph
from io import BytesIO

import copy
import pdb
import tempfile
import os
import collections

class SAMNode:
    link = None
    chds = None
    max_len = 0
    right_size = None
    vtx_id = 0
    node_list = []
    accept = False
    to = None
    chd_on_tree = None

    def __str__(self):
        ret = "SAMNode{{ID:{},max_len={},rightsize={},".format(
            self.vtx_id, self.max_len, self.right_size)
        ret += "link={},".format(None if self.link is None else self.link.vtx_id)
        for k, v in self.chds.items():
            ret += "{}->{},".format(k, v.vtx_id)
        return ret+"}}"

    def __repr__(self):
        return self.__str__()

    def __init__(self, node_list, strid, max_len=0):
        self.max_len = max_len
        self.node_list = node_list
        self.node_list.append(self)
        self.vtx_id = len(node_list)
        self.chds = dict()
        self.right_size = collections.OrderedDict()
        self.right_size[strid] = 1
        self.to = list()
        self.chd_on_tree = list()

    def clone(self):
        cloned = SAMNode(self.node_list, self.max_len)
        cloned.link = self.link
        cloned.chds = copy.copy(self.chds)
        cloned.right_size = collections.OrderedDict()
        cloned.accept = False
        return cloned

def append(char: str, last: SAMNode, root: SAMNode, str_id: int)->SAMNode:
    new = SAMNode(root.node_list, str_id, last.max_len+1)
    curr = last
    new.accept = True
    while curr and (char not in curr.chds):
        curr.chds[char] = new
        curr = curr.link
    if curr is None:
        new.link = root
    elif curr.chds[char].max_len == curr.max_len+1:
        new.link = curr.chds[char]
    else:
        oldNode: SAMNode = curr.chds[char]
        newNode: SAMNode = oldNode.clone()
        new.link = oldNode.link = newNode
        newNode.max_len = curr.max_len+1
        while curr is not None and curr.chds[char] == oldNode:
            curr.chds[char] = newNode
            curr = curr.link
    return new

def DFS(node: SAMNode):
    for chd in node.chd_on_tree:
        DFS(chd)
        for k, v in chd.right_size.items():
            if k not in node.right_size:
                node.right_size[k] = v
            else:
                node.right_size[k] += v

def generate_graph(string, format):
    nodes = []
    root = SAMNode(nodes, 0)
    root.right_size = collections.OrderedDict()
    strs = []
    if "|" in string:
        strs = string.split("|")
    else:
        strs.append(string)
    print(strs)
    for idx, curr in enumerate(strs):
        last = root
        for x in curr:
            last = append(x, last, root, idx+1)
    dot = Digraph("SAM")
    nodes.sort(key=lambda x: x.max_len, reverse=True)
    for x in nodes:
        for i in range(1, 1+len(strs)):
            if i not in x.right_size:
                x.right_size[i] = 0
        if x.link is not None:
            x.link.chd_on_tree.append(x)
    DFS(root)
    for x in nodes:
        sorted(x.right_size)
    nodes.sort(key=lambda x: x.vtx_id)
    for node in nodes:
        label = "{}\nMax={}".format(node.vtx_id, node.max_len)
        if len(node.right_size):
            sorted(node.right_size)
            for k, v in node.right_size.items():
                label += "\nsize{}={}".format(k, v)
        dot.node(str(node.vtx_id), label)
        if node.link:
            dot.edge(str(node.vtx_id), str(node.link.vtx_id), color="red")
        for k, v in node.chds.items():
            dot.edge(str(node.vtx_id), str(v.vtx_id), k)
    tmpdir = tempfile.mkdtemp()
    target = os.path.join(tmpdir, "qwq")
    dot.render(filename=target, format=format)
    return target+"."+format

main.py

from flask import Flask, render_template, request, make_response
from json import JSONEncoder
from io import BytesIO
import ds_drawer
import mimetypes
app = Flask("SAMDrawer")

# 主页
@app.route("/", methods=["GET"])
def root():
    return render_template("main.html")

# 绘制
@app.route("/draw", methods=["POST", "GET"])
def draw():
    # request.form是一个dict-like object,表示客户端提交的数据。
    if "string" not in request.form:
        # 直接返回文本即可。
        return encode_json({
            "code": -1,
            "message": "Please give a string."
        })
    string = request.form["string"]
    if len(string) > 50:
        return encode_json({
            "code": -1,
            "message": "Too long.."
        })
    svg_data = ""
    target = ds_drawer.generate_graph(string, "svg")
    with open(target, "r") as file:
        svg_data = file.read()
    # 一切正常时直接发回结果。
    return encode_json({
        "code": 0,
        "data": svg_data
    })

# 编码obj到JSON。
def encode_json(obj):
    return JSONEncoder().encode(obj)

if __name__ == "__main__":
    app.run(host="127.0.0.1", port="90")

此时运行main.py,如果一切正常那么就可以通过http://127.0.0.1:90访问了。