用户登录 之 实施

准备数据

既然要『登录』,必然是已经有用户存在了,所以,我们准备几个账户的数据,以供后续使用 username & password 实现登录。
为了方便,在 FirstWeb 中对应网站目录下,能找到现成的 users.json,大抵的数据格式如下:

[
    {
        "username": "hepo",
        "password": "123456",
        "from": "Earth"
    },
    {
        "username": "hepochen",
        "password": "12345678",
        "from": "China"
    }
]

关于 JSON

我们准备的数据文件是一个 .json 后缀的文件,也就是 JSON 文件,而JSON 是很常见的数据格式。
在示例中,是 [{}, {}, .etc] 这样的结构,也就是一个 列表,里面的每个元素是数据对象{} 在 Python 中是 字典 类型的数据,而在 Javascript 中则是 对象,本质上自有区别,一般使用,倒也相似。

JSON 是前端层面非常普及的格式,以至于会让很多人忽略一个问题:JSON 格式,首先是字符串,而不是特定意义上的数据格式。毕竟,只有 字符串 才是各个不同程序语言都能识别的。
JSON 的数据要转化后,才能在具体的某个程序语言中被作为数据对象使用。JSON 能对应常见的数据类型,比如字符、整数、浮点数、列表、数据对象(字典),但是不同语言中特定的类型则无法对应,比如 日期类型,一般都是另外压码、解码。

JSON 虽然是开放式的,但有比较严的格式要求,初学者在使用它的时候,除了中英文引号错误外,普遍会遇到另一个问题就是 多了一个逗号。我们从另外角度思考,它最初的设计立意于跨越不同程序语言的数据交互,那么严苛一些的格式要求,倒不算过分。
比如下面这一行,在 Python 或者 Javascript 中都是可以执行的,而在 JSON 格式中却是错误的,因为多了一个 , 而导致格式出错:

{ "a": 2, }

(新手)如何避免 JSON 这种格式错误? 除了 Atom 这些代码编辑器内部的警告之外,还可以搜索引擎搜 JSON 校验 即可,会有不少在线的工具辅助……

衍生的思考

用户进行登录的时候,一般都是用 username & password,现在更加常见的有 手机号码 + (一定有限期内的)验证码
本质而言,多个字段(比如 username+password) 最终会复合为某个特殊的值,可以认为是一个新的(虚拟)字段(虽然它并不存在),就像组合成了一把 钥匙。这个 钥匙 的概念可以产生不少推演,比如 邮箱地址+邮箱里收到的验证码、手机号码(或邮箱地址)+人脸识别、手机号码(或邮箱地址)+手势识别(利用摄像头)。甚至,单独某张照片,也可以作为一把钥匙

在涉及到 登录 的时候,(传统)常见的处置方式中,具体涉及到的环节还有: 注册、登录、忘记密码、用户名不匹配、密码不匹配、重置密码 等环节;看似简单、基础的,其实自己画一个流程图,会发现,并不容易,比如容易多次离开当前页面的场景,是设计、引导的噩梦。
一把钥匙 这种逻辑去看待问题,你或许在未来会发现『传统』的方式都是有巨大的改进空间,但付出的代价或许是增加一定的用户认知(就是给用户添多了一点麻烦)。而另外一方面,原本的登录、注册环节其实对于天然未使用过互联网的人类来说,其实并不容易理解,我们绝大多数人已经习以为常,不过是被长期教育的结果。

对了,顺便一提,有人在探究 登录登陆 哪个更正确,你觉得呢?
或者说,这个重要吗?
有人觉得重要,那便重要;有人觉得不重要,那便不重要。唯独要避免的其实是: 在这可是可非的小细节中,不要因为别人觉得重要,而自己觉得重要;而不要因为别人觉得不重要,而就自己觉得不重要了。


用 Jade 构建 HTML 页面

在上一篇《准备》的内容中,已介绍过构建一个 FORM(表单) 的基本 HTML 结构。但『基本』的,未必是『常见』的,基于这个考虑,我们又另外呈现了融合了 Javascript 的情况,具体源码,参考如下:

h2 使用 A 元素 代替 INPUT (submit)
        form#form_two(action="/", method="POST")
            div.form_field.username
                label Username
                input(type="text", name="username", placeholder="hepo")
            div.form_field.password
                label Password
                input(type="password", name="password", placeholder="123456")
            div.form_field.submit
                a#raw_submit_button(href="raw_javascript_submit()") 原生 Javascript 提交
                a#submit_button(href="submit_by_jquery()") jQuery 提交
                a#ajax_submit_button(href="submit_by_ajax()") Ajax 机制

几个 Button 使用 A 元素 (更容易进行样式上的定义) 进行了替换,上面点击的链接实际上是一个 Javascript 的函数,各个函数对应的 Javascript 源码参考如下:

var raw_javascript_submit = function(){
    // FORM 类型的 DOM 元素,原生的 Javascript 而言,有 submit 这个函数可以调用
    document.getElementById('form_two').submit();
}

var submit_by_jquery = function(){
    $('#form_two').submit()
}

var submit_by_ajax = function(){
    var action_url = '/check_login';
    var data_for_post = $('#form_two').serialize() + '&type=ajax';
    $.post(action_url, data_for_post, function(response){
        alert("提交成功!");
    })
}

构建『 Debug 呈现』区域

『 Debug 呈现』区域,也是 Debug 常用的一种手段,这样,可以帮助我们临时地获得一些数据、呈现出来,从而帮助判断。
此次『呈现』区域的两个地方,主要是(客户端过来的)数据以及 Cookies:
一、数据要从客户端提交到服务端,一开始,你可能并不知道这个 数据 长什么样子,所以,这个要显示出来。
二、Cookies 在前面篇章中也有说过,它能保留当前页面的某些状态,比如 已登录 的状态。

对应的源码参考:

h2 Cookies
p 当前的Cookies:
pre= request.cookies.json
+response.set_cookie('hello', 'cookie')
        
if request.method == 'POST'
    h2 来自浏览器的数据交互:
    p
        span Method:
        b= request.method
    p
        span Data:
        pre= request.values.to_dict().json

注: 一般来说,HTML 内的源码,多一个空行、几个空格,对于 <dom></dom> 这种封闭的标签而言,并无大的影响;而上面源码中出现的 PRE 这个标签是比较特殊的,它会原始地呈现内部的(文本)内容。


进行登录的校验

HTML 的页面有了、Javascript 的脚本对应了、在后端也能输出客户端过来的数据,那么,接下来就很自然要去校验登录username & password 正确性了。

具体的源码,请自己在 FirstWeb 中查看,下面仅呈现了主要判断逻辑的代码:

matched_user = {}
req_vs = request.values
for user in users
    if user.username==req_vs.username and user.password==req_vs.password
        +matched_user.update(user)

在上面这段代码中,我们是将所有的 users 进行遍历,如果匹配,则获得 matched_user
但在一个真正的产品中,就很恐怖了,是坚决不允许这样遍历的,因为性能太低了,如果有一百万的用户(用户量还算小了),就可能要进行一百万次的遍历!那么,要如何避免呢?
我们在讲『动态页面』的时候,说到过数据库的索引,这里要命中索引去处理了。
知道使用数据库去解决问题,是一回事,真正怎么解决,又是另外一回事。如果数据库的索引建立得不正确,或者索引创建正确,命中时却指定了错误的索引,都会导致数据库内的遍历(数据库中的概念是 Scan),性能极低,甚至可能因为内存不够、崩溃,让整个服务器都挂了。

在本课程最开始时候,我们讲过变量空间的一些基本概念,特别是自带空间的可变变量,虽然刚入门阶段,这个概念略微麻烦,但确也非常基础。
编程,真正核心的能力是解决问题的能力,而不在于对某些术语、名词产生记忆;很多时候,我们更要灵活应变
比如上面获取 matched_user,一般的逻辑应该是先给一个变量,匹配了再赋值给它,如下:

matched_user = None
req_vs = request.values
for user in users
    if user.username==req_vs.username and user.password==req_vs.password
        matched_user = user

但实际情况,无法正常运行。归根结底,这部分 Jade 的代码,先转为 HTML 类型的模板语言,并最终转为(编译) Python 代码,在 Python 代码中,有点黑盒的状态 (其实可以进一步获得编译后的 Python 源码,但没有大的必要)。基本可以判断,多个代码片段之间,因为解析的原因,事实上产生了不同的 空间,相对本地的变量就无法跨越空间被调用了。所以,我们使用了 matched_user = {} 作为横跨不同空间的变量,在不同空间内,对其进行操作,而变量的 id 并不会改变,也就是说变量仍然是原先的地址上。

说到 灵活应变,上面的源码中出现了 req_vs = request.values 这个临时变量,其实它并无实际意义,不过是最终导出为 PDF 或打印成稿的时候,如果不缩写,会导致不必要的代码换行的现象,或许会让读者阅读时,多了一些晦涩。
类似的情况还有,有时候,也只是我们考虑到要增强代码的可读性,仅此而已。
至于有无意义,自行判断,并不算什么重要的事情。

另外,由于 Demo 的原因,所以我们单独对 账户登录结果 进行呈现;真正在某个产品中的场景,不会出现这种情况,一般会是跳转到用户已登录后的某个页面或者主页。

check_login.jade


由 jQuery & Ajax 机制 提交 中,我们会发现,其实 HTML 中有没有 FORM(表单) 结构都没有关系,使用 Javascript 从 FORM 中提取了数据,然后再 Ajax 提交到服务端。
所谓的 Ajax ,可以简单理解为通过 Javascript 提交数据,服务端处理数据并返回,有必要的话,再由 Javascript 修改当前页面的 DOM 元素作为反馈;从而实现在不刷新页面的前提下,更新页面局部内容。

在 Demo 中,简单地往 /check_login 这个 URL 上提交数据,而这个 URL 由 check_login.jade 来负责处理。
check_login.jade 在 FirstWeb 中可以看到源码,只是简单的『伪造』了一下,并未真正校验登录。
作为进一步的扩展,你可以尝试补全 check_login.jade 的后端逻辑,以及补全 submit_by_ajax 这个 Javascript 脚本中的函数逻辑。