(十)注册和登录

  1. 这一节流程还是比较简单的,只是涉及到大量的JavaScript编程。且廖大的github上也有不少错误,改bug花了不少时间。
    先放新建立的文件,templates下面的register.html,对base页面进行扩展:
{% extends '__base__.html' %}

{% block title %}注册{% endblock %}

{% block beforehead %}

{% endblock %}

{% block content %}
    

欢迎注册!

{% endblock %}

templates下面的signin.html,没有对blog页面进行扩展:




    
    登录 - Awesome Python Webapp
    
    
    
    
    
    
    
    



    


handlers.py中加入了注册、认证和登陆的页面处理函数,_COOKIE_KEY通过读取config.py获得:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import re, time, json, logging, hashlib, base64, asyncio
from aiohttp import web
from coroweb import get, post
from apis import APIError, APIValueError, APIResourceNotFoundError
from models import User, Comment, Blog, next_id
from config import configs

COOKIE_NAME = 'awesession'
_COOKIE_KEY = configs.session.secret    # ?

def user2cookie(user, max_age):
    expires = str(int(time.time() + max_age))
    s = '%s-%s-%s-%s' % (user.id, user.passwd, expires, _COOKIE_KEY)
    # "用户id"+"过期时间"+SHA1("用户id"+"用户密码"+"过期时间"+"SecretKey")
    L = [user.id, expires, hashlib.sha1(s.encode('utf-8')).hexdigest()]
    return '-'.join(L)

async def cookie2user(cookie_str):
    if not cookie_str:
        return None
    try:
        L = cookie_str.split('-')
        if len(L) != 3:
            return None
        uid, expires, sha1 = L
        if int(expires) < time.time():
            return None
        user = await User.find(uid)
        if user is None:
            return None
        s = '%s-%s-%s-%s' % (uid, user.passwd, expires, _COOKIE_KEY)
        if sha1 != hashlib.sha1(s.encode('utf-8')).hexdigest():
            logging.info('Invalid sha1.')
            return None
        user.passwd = '******'
        return user
    except Exception as e:
        logging.exception(e)
        return None

# 参数aiohttp.web.request实例,包含了所有浏览器发送过来的HTTP协议里面的信息
@get('/')
def index(request):
    summary = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
    blogs = [
        Blog(id='1', name='Test Blog', summary=summary, created_at=time.time()-120),
        Blog(id='2', name='Write Sth', summary=summary, created_at=time.time()-3600),
        Blog(id='3', name='Learn Swift', summary=summary, created_at=time.time()-7200)
    ]
    return {'__template__': 'blogs.html', 'blogs': blogs, '__user__': request.__user__}

@get('/register')
def register():
    return {'__template__': 'register.html'}

@get('/signin')
def signin():
    return {'__template__': 'signin.html'}

@get('/signout')
def signout(request):
    referer = request.headers.get('Referer')
    # 状态码302
    r = web.HTTPFound(referer or '/')
    r.set_cookie(COOKIE_NAME, '-delete-', max_age=0, httponly=True)
    logging.info('User signed out.')
    return r

# +表示一或多次, {m,n}表示至少m次至多n次(因为可能是.cn或.com)
_RE_EMAIL = re.compile(r'^[a-z0-9\.\-\_]+\@[a-z0-9\-\_]+(\.[a-z0-9\-\_]+){1,4}$')
_RE_SHA1 = re.compile(r'^[0-9a-f]{40}$')

@post('/api/users')
async def api_register_user(*, email, name, passwd):
    # 用户名为空或全为空格
    if not name or not name.strip():    # 没有用户名的字符限制?
        raise APIValueError('name')
    if not email or not _RE_EMAIL.match(email):
        raise APIValueError('email')
    if not passwd or not _RE_SHA1.match(passwd):
        raise APIValueError('passwd')
    users = await User.findAll('email=?', [email])
    if len(users) > 0:
        raise APIError('register:failed', 'email', 'Email is already in use.')
    uid = next_id()
    sha1_passwd = '%s:%s' % (uid, passwd)
    encrypt_passwd = hashlib.sha1(sha1_passwd.encode('utf-8')).hexdigest()
    # image使用Gravatar全球通用头像
    image = 'http://www.gravatar.com/avatar/%s?d=mm&s=120'
    user = User(id=uid,name=name.strip(),email=email,passwd=encrypt_passwd,image=image % hashlib.md5(email.encode('utf-8')).hexdigest())
    await user.save()
    r = web.Response()
    r.set_cookie(COOKIE_NAME, user2cookie(user, 86400), max_age=86400, httponly=True)
    user.passwd = '******'
    r.content_type = 'application/json'
    r.body = json.dumps(user, ensure_ascii=False).encode('utf-8')
    return r

@post('/api/authenticate')
async def authenticate(*, email, passwd):
    if not email:
        raise APIValueError('email', 'Invalid email.')
    if not passwd:
        raise APIValueError('passwd', 'Invalid password.')
    users = await User.findAll('email=?', [email])
    if len(users) == 0:
        raise APIValueError('email', 'Email not exist.')
    user = users[0]
    sha1 = hashlib.sha1()
    sha1.update(user.id.encode('utf-8'))
    sha1.update(b':')
    sha1.update(passwd.encode('utf-8'))
    if user.passwd != sha1.hexdigest():
        raise APIValueError('passwd', 'Invalid password.')
    r = web.Response()
    # set_cookie(name, value, max_age, httponly(设为True在浏览器的document对象中就看不到cookie))
    r.set_cookie(COOKIE_NAME, user2cookie(user, 86400), max_age=86400, httponly=True)
    user.passwd = '******'
    r.content_type = 'application/json'
    # json.dumps(obj, ensure_asci), 把python对象转换成json格式的str
    # 如果ensure_ascii为false, 则结果可能包含非ASCII字符, 并且返回值可能是一个unicode实例
    r.body = json.dumps(user, ensure_ascii=False).encode('utf-8')
    return r

app.py中加入了用于认证的中间件,数据库相关参数通过读取config.py获得:

# -*- coding: utf-8 -*-
import logging; logging.basicConfig(level=logging.INFO)
import asyncio, os, json, time
from datetime import datetime
from aiohttp import web
import orm
from jinja2 import Environment, FileSystemLoader
from coroweb import add_routes, add_static
from config import configs
from handlers import cookie2user, COOKIE_NAME

def init_jinja2(app, **kw):
    logging.info('Init jinja2...')
    options = dict(
        # 字典的get()方法返回指定键的值, 如果值不在字典中返回默认值
        autoescape = kw.get('autoescape', True),    # 自动转义
        block_start_string = kw.get('block_start_string', '{%'),
        block_end_string = kw.get('block_end_string', '%}'),
        variable_start_string = kw.get('variable_start_string', '{{'),
        variable_end_string = kw.get('variable_end_string', '}}'),
        auto_reload = kw.get('auto_reload', True)    # 自动重新加载模板
    )
    path = kw.get('path', None)
    if path is None:
        # __file__获取当前执行脚本
        path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')
    logging.info('Set jinja2 template path: %s' % path)
    # Environment(loader=PackageLoader('path'), 其他高级参数...)
    # 创建一个默认设定下的模板环境和一个在path目录下寻找模板的加载器
    env = Environment(loader=FileSystemLoader(path), **options)
    filters = kw.get('filters', None)
    if filters is not None:
        for name, f in filters.items():
            env.filters[name] = f
    app['__templating__'] = env

# middlewar把通用的功能从每个URL处理函数中拿出来, 集中放到一个地方
# 接受一个app实例, 一个handler(URL处理函数, 如index), 并返回一个新的handler
async def logger_factory(app, handler):
    async def logger(request):
        logging.info('Request: %s %s' % (request.method, request.path))
        return (await handler(request))
    return logger

async def auth_factory(app, handler):
    # 伴随每一个请求, aiohttp内部创建一个Request对象request
    # request = web_reqrep.Request(
    #     app, message, payload, self.transport, self.reader,
    #     self.writer, secure_proxy_ssl_header=self._secure_proxy_ssl_header)
    async def auth(request):
        logging.info('Check user: %s %s' % (request.method, request.path))
        request.__user__ = None
        # cookies是一个dict
        cookie_str = request.cookies.get(COOKIE_NAME)
        if cookie_str:
            # 解密cookie
            user = await cookie2user(cookie_str)
            if user:
                logging.info('Set current user: %s' % user.email)
                request.__user__ = user
        if request.path.startswith('/manage/') and (request.__user__ is None or not request.__user__.admin):    # /manage/?
            return web.HTTPFound('/signin')
        return (await handler(request))
    return auth

async def data_factory(app, handler):
    async def parse_data(request):
        if request.method == 'POST':
            if request.content_type.startswith('application/json'):
                request.__data__ = await request.json()
                logging.info('Request json: %s' % str(request.__data__))
            elif request.content_type.startswith('application/x-www-form-urlencoded'):
                request.__data__ = await request.post()
                logging.info('Request form: %s' % str(request.__data__))
        return (await handler(request))
    return parse_data

# 转化得到response对象的middleware
async def response_factory(app, handler):
    async def response(request):
        logging.info('Response handler...')
        r = await handler(request)
        # web.StreamResponse是HTTP响应处理的基类
        # 包含用于设置HTTP响应头,Cookie,响应状态码,写入HTTP响应BODY等的方法
        if isinstance(r, web.StreamResponse):
            return r
        if isinstance(r, bytes):
            # 转换为web.Response对象
            resp = web.Response(body=r)
            # .*( 二进制流,不知道下载文件类型)
            resp.content_type = 'application/octet-stream'
            return resp
        if isinstance(r, str):
            if r.startswith('redirect:'):
                return web.HTTPFound(r[9:])
            resp = web.Response(body=r.encode('utf-8'))
            # .html
            resp.content_type = 'text/html;charset=utf-8'
            return resp
        if isinstance(r, dict):
            template = r.get('__template__')
            if template is None:
                resp = web.Response(body=json.dumps(r, ensure_ascii=False, default=lambda o: o.__dict__).encode('utf-8'))
                # 序列化后的JSON字符串
                resp.content_type = 'application/json;charset=utf-8'
                return resp
            else:
                r['__user__'] = request.__user__
                # 调用get_template()方法环境中加载模板,调用render()方法用若干变量来渲染它
                resp = web.Response(body=app['__templating__'].get_template(template).render(**r).encode('utf-8'))
                resp.content_type = 'text/html;charset=utf-8'
                return resp
        # HTTP状态码和响应头部
        if isinstance(r, int) and r >= 100 and r < 600:
            return web.Response(r)
        if isinstance(r, tuple) and len(r) == 2:
            t, m = r
            if isinstance(t, int) and t >= 100 and t < 600:
                return web.Response(t, str(m))
        resp = web.Response(body=str(r).encode('utf-8'))
        # .txt
        resp.content_type = 'text/plain;charset=utf-8'
        return resp
    return response

def datetime_filter(t):
    # time.time()返回当前时间的时间戳(1970纪元后经过的浮点秒数)
    delta = int(time.time() - t)
    if delta < 60:
        return u'1分钟前'
    if delta < 3600:
        return u'%s分钟前' % (delta // 60)
    if delta < 86400:
        return u'%s小时前' % (delta // 3600)
    if delta < 604800:
        return u'%s天前' % (delta // 86400)
    # 把timestamp转换为datetime
    dt = datetime.fromtimestamp(t)
    return u'%s年%s月%s日' % (dt.year, dt.month, dt.day)

# 协程,不能直接运行,需要把协程加入到事件循环(loop), 由后者在适当的时候调用
async def init(loop):
    await orm.create_pool(loop=loop, **configs.db)
    # 创建Web服务器,即aiohttp.web.Application类的实例,作用是处理URL、HTTP协议
    # 添加middleware时自动变成倒序
    app = web.Application(loop=loop, middlewares=[
        logger_factory, auth_factory, response_factory
        ])
    init_jinja2(app, filters=dict(datetime=datetime_filter))    # 没有参数t?
    # import handlers.py
    add_routes(app, 'handlers')
    add_static(app)
    # 用协程创建TCP服务(这里写的是我的虚拟机地址,为了本机也能访问)
    srv = await loop.create_server(app.make_handler(), '192.168.179.140', 7000, reuse_address=True, reuse_port=True)
    logging.info('Server started at http://192.168.179.140:7000...') 
    return srv

# 创建一个事件循环
loop = asyncio.get_event_loop()
# 将协程注册到事件循环,并启动事件循环
loop.run_until_complete(init(loop))
# run_forever会一直运行,直到stop在协程中被调用
loop.run_forever()

static/js下加入awesome.js:

if (! window.console) {
    window.console = {
        log: function() {},
        info: function() {},
        error: function () {},
        warn: function () {},
        debug: function () {}
    };
}

// patch for string.trim():

if (! String.prototype.trim) {
    String.prototype.trim = function() {
        return this.replace(/^\s+|\s+$/g, '');
    };
}

if (! Number.prototype.toDateTime) {
    var replaces = {
        'yyyy': function(dt) {
            return dt.getFullYear().toString();
        },
        'yy': function(dt) {
            return (dt.getFullYear() % 100).toString();
        },
        'MM': function(dt) {
            var m = dt.getMonth() + 1;
            return m<10 ? '0'+m: m.toString();
        },
        'M': function(dt) {
            var m = dt.getMonth() + 1;
            return m.toString();
        },
        'dd': function(dt) {
            var d = dt.getDate();
            return d<10? '0'+d: d.toString();
        },
        'd': function(dt) {
            var d = dt.getDate();
            return d.toString();
        },
        'hh': function(dt) {
            var h = dt.getHours();
            return h<10? '0'+h: h.toString();
        },
        'h': function(dt) {
            return dt.getHours().toString();
        },
        'mm': function(dt) {
            var m = dt.getMinutes();
            return m<10? '0'+m: m.toString();
        },
        'm': function(dt) {
            return dt.getMinutes().toString();
        },
        'ss': function(dt) {
            var s = dt.getSeconds();
            return s<10? '0'+s: s.toString();
        },
        's': function(dt) {
            return dt.getSeconds().toString();
        },
        'a': function(dt) {
            var h = dt.getHours();
            return h<12? 'AM': 'PM';
        }
    };
    var token = /([a-zA-Z]+)/;
    Number.prototype.toDateTime = function(format) {
        var fmt = format || 'yyyy-MM-dd hh:mm:ss'
        var dt = new Date(this * 1000);
        var arr = fmt.split(token);
        for (var i=0; i/g, '>');
}

// 将查询字符串解析为对象
function parseQueryString() {
    var
        // location.search获取从问号开始的URL(即查询部分)
        q = location.search,
        r = {},
        i, pos, s, qs;
    // charAt()方法返回指定位置的字符
    if (q && q.charAt(0)==='?') {
        qs = q.substring(1).split('&');
        for (i=0; i 604800000) {
        var that = new Date(timestamp);
        var
            y = that.getFullYear(),
            m = that.getMonth() + 1,
            d = that.getDate(),
            hh = that.getHours(),
            mm = that.getMinutes();
        s = y===today.getFullYear()? '': y+'年';
        s = s + m + '月' + d + '日' + hh + ':' +(mm<10? '0': '') + mm;
    }
    // 1~6天
    else if (t >= 86400000) {
        s = Math.floor(t / 86400000) + '天前';
    }
    // 1~23小时
    else if (t >= 3600000) {
        s = Math.floor(t / 3600000) + '小时前';
    }
    else if (t >= 60000) {
        s = Math.floor(t / 60000) + '分钟前';
    }
    return s;
}

// $(function() {});是$(document).ready(function(){ })的简写
// 里面的代码是在页面元素都加载完才执行的
$(function() {
    $('.x-smartdate').each(function() {
        // $(this)是一个jQuery对象, text()设置元素的文本内容
        // attr()函数用于设置或返回当前jQuery对象所匹配的元素节点的属性值
        $(this).removeClass('x-smartdate').text(toSmartDate($(this).attr('date')));
    });
});

// 还没看懂...
function Template(tpl) {
    var
        fn,
        match,
        // 替换字符
        code = ['var r=[];\nvar _html = function (str) { return str.replace(/&/g, \'&\').replace(/"/g, \'"\').replace(/\'/g, \''\').replace(//g, \'>\'); };'],
        re = /\{\s*([a-zA-Z\.\_0-9()]+)(\s*\|\s*safe)?\s*\}/m,    // 两个捕获组, 这是什么字符串?
        // 定义了一个addLine函数, 接受的参数是text
        addLine = function (text) {
            code.push('r.push(\''+text.replace(/\'/g, '\\\'').replace(/\n/g, '\\n').replace(/\r/g, '\\r')+'\');');
        };
    // exec()方法用于检索字符串中的正则表达式的匹配, 返回一个数组
    // 如["abc", index: 1, input: "2abcdkfabco98787abc9"]
    while (match = re.exec(tpl)) {
        if (match.index > 0) {
            addLine(tpl.slice(0, match.index));
        }
        // 第二组的匹配结果
        if (match[2]) {
            code.push('r.push(String(this.'+match[1]+'));');
        }
        else {
            code.push('r.push(_html(String(this.'+match[1]+')));');
        }
        tpl = tpl.substring(match.index+match[0].length);
    }
    addLine(tpl);
    code.push('return r.join(\'\');');
    fn = new Function(code.join('\n'));
    this.render = function (model) {
        return fn.apply(model);    //对model?
    };
}

// 扩展jQuery.form
$(function () {
    console.log('Extends $form...');
    // jQuery.fn.extend(object)为jQuery类添加成员函数(插件)
    $.fn.extend({
        // 大多数的异步方法都接受一个callback函数, 该函数接受一个Error对象传入作为第一个参数
        // 如果第一个参数不是null而是一个Error实例, 则说明发生了错误, 应该进行处理
        showFormError: function (err) {
            // 返回this.each便于继续链式操作
            return this.each(function () {
                var
                    $form = $(this),
                    // find()方法获得当前元素集合中每个元素的后代, 通过选择器、jQuery对象或元素来筛选
                    // uk-alert-danger是红色提示框
                    $alert = $form && $form.find('.uk-alert-danger'),
                    fieldName = err && err.data;
                // is()根据选择器、元素或jQuery对象来检测匹配元素集合, 如果这些元素中至少有一个元素匹配给定的参数, 则返回true
                if (! $form.is('form')) {
                    console.error('Cannot call showFormError() on non-form object.');
                    return;
                }
                $form.find('input').removeClass('uk-form-danger');
                $form.find('select').removeClass('uk-form-danger');
                $form.find('textarea').removeClass('uk-form-danger');
                if ($alert.length === 0) {
                    console.warn('Cannot find .uk-alert-danger element.');
                    return;
                }
                if (err) {
                    $alert.text(err.message? err.message: (err.error? err.error: err)).removeClass('uk-hidden').show();
                    // offset()方法获得匹配元素在当前窗口的偏移, 有top和left两个属性
                    // scrollTop()方法返回匹配元素的滚动条的垂直位置
                    if (($alert.offset().top - 60) < $(window).scrollTop()) {
                        $('html,body').animate({ scrollTop: $alert.offset().top - 60});
                    }
                    if (fieldName) {
                        // jQuery按属性查找, 注册页里没有name属性?
                        $form.find('[name='+fieldName+']').addClass('uk-form-danger');
                    }
                }
                else {
                    $alert.addClass('uk-hidden').hide();
                    $form.find('.uk-form-danger').removeClass('uk-form-danger');
                }
            });
        },
        // 用在下面postJSON, 发送数据时
        showFormLoading: function (isLoading) {
            return this.each(function () {
                var
                    $form = $(this),
                    $submit = $form && $form.find('button[type=submit]'),
                    $buttons = $form && $form.find('button'),
                    $i = $submit && $submit.find('i'), 
                    iconClass = $i && $i.attr('class');
                if (! $form.is('form')) {
                    console.error('Cannot call showFormLoading() on non-form object.');
                    return;
                }
                if (!iconClass || iconClass.indexOf('uk-icon') < 0) {
                    console.warn('Icon  not found.');    //改了一下
                    return;
                }
                if (isLoading) {
                    // attr('disabled','disabled')将页面中某个元素置为不可编辑或触发状态
                    $buttons.attr('disabled', 'disabled');
                    $i && $i.addClass('uk-icon-spinner').addClass('uk-icon-spin');
                }
                else {
                    $buttons.removeAttr('disabled');
                    $i && $i.removeClass('uk-icon-spinner').removeClass('uk-icon-spin');
                }
            });
        },
        postJSON: function (url, data, callback) {
            if (arguments.length===2) {
                callback = data;
                data = {};
            }
            return this.each(function () {
                var $form = $(this);
                $form.showFormError();    //这里的作用?
                $form.showFormLoading(true);
                _httpJSON('POST', url, data, function (err, r) {
                    if (err) {
                        $form.showFormError(err);
                        $form.showFormLoading(false);
                    }
                    callback && callback(err, r);
                });
            });
        }
    });
});

// ajax提交表单
function _httpJSON(method, url, data, callback) {
    var opt = {
        type: method,
        dataType: 'json'
    };
    if (method==='GET') {
        opt.url = url + '?' + data;
    }
    if (method==='POST') {
        opt.url = url;
        // JSON.stringify()用于将JavaScript值转换为JSON字符串
        opt.data = JSON.stringify(data || {});
        opt.contentType = 'application/json';
    }
    // ajax()方法通过HTTP请求加载远程数据
    // jQuery.ajax返回的是jqXHR对象, 它是浏览器原生XMLHttpRequest对象的一个超集
    // 并实现了Promise接口, 使它拥有了Promise的所有属性、方法和行为
    // jqXHR.done(function(data,textStatus,jqXHR){}), 一种可供选择的请求成功时调用的回调选项构造函数
    $.ajax(opt).done(function (r) {
        if (r && r.error) {
            return callback(r);
        }
        return callback(null, r);
    // jqXHR.fail(function(jqXHR,textStatus,errorThrown){}), 一种可供选择的请求失败时调用的回调选项构造函数
    }).fail(function (jqXHR, textStatus) {
        return callback({'error': 'http_bad_response', 'data': ''+jqXHR.status, 'message': '网络好像出问题了(HTTP'+jqXHR.status+')'});
    });
}

function getJSON(url, data, callback) {
    if (arguments.length===2) {
        callback = data;
        data = {};
    }
    if (typeof (data)==='object') {
        var arr = [];
        $.each(data, function (k, v) {
            // encodeURIComponent()函数可把字符串作为URI组件进行编码
            // 该方法不会对ASCII字母和数字进行编码, 也不会对这些ASCII标点符号进行编码:- _ . ! ~ * ' ( )
            // 其他字符(如;/?:@&=+$,#这些用于分隔URI组件的标点符号), 都是由一个或多个十六进制的转义序列替换的
            arr.push(k+'='+encodeURIComponent(v));
        });
        data = arr.join('&');
    }
     _httpJSON('GET', url, data, callback);
}

function postJSON(url, data, callback) {
    if (arguments.length===2) {
        callback = data;
        data = {};
    }
    _httpJSON('POST', url, data, callback);
}

if (typeof(Vue)!=='undefined') {    // Vue在哪里出现?
    // 全局过滤器Vue.filter(名字,function (val))
    Vue.filter('datetime', function (value) {
        var d = value;
        if (typeof(value)==='number') {
            d = new Date(value);
        }
        return d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate()+' '+d.getHours()+':'+d.getMinutes();
    });
    // Vue.component(在html文件里使用的tag, template)
    Vue.component('pagination', {
        template: '
    ' + // v-if条件渲染指令, 根据其后表达式的bool值判断是否渲染该元素 '
  • ' + // 定义on—click函数, 动作是gotoPage(page_index-1) // 用#定义锚点, 浏览器读取这个URL后, 会自动将id=0的位置滚动至可视区域 '
  • ' + // v-text操作元素中的纯文本 '
  • ' + '
  • ' + '
  • ' + '
' }); } // 重定向 function redirect(url) { var hash_pos = url.indexOf('#'), query_pos = url.indexOf('?'), hash = ''; if (hash_pos >= 0) { hash = url.substring(hash_pos); url = url.substring(0, hash_pos); } url = url + (query_pos>=0? '&': '?') + 't=' + new Date().getTime() + hash; console.log('Redirect to: ' + url); location.assign(url); } function _bindSubmit($form) { // 将函数绑定到submit事件 $form.submit(function (event) { event.preventDefault(); showFormError($form, null); var fn_error = $form.attr('fn-error'), // 三个属性在哪? fn_success = $form.attr('fn-success'), fn_data = $form.attr('fn-data'), // serialize()方法通过序列化表单值, 创建URL编码文本字符串, 如a=1&b=2&c=3 data = fn_data? window[fn_data]($form): $form.serialize(); // window[fn_data]($form)什么意思? var $submit = $form.find('button[type=submit]'), $i = $submit.find('i'), iconClass = $i.attr('class'); if (!iconClass || iconClass.indexOf('uk-icon') < 0) { $i = undefined; } $submit.attr('disabled', 'disabled'); $i && $i.addClass('uk-icon-spinner').addClass('uk-icon-spin'); postJSON($form.attr('action-url'), data, function (err, result) { $i && $i.removeClass('uk-icon-spinner').removeClass('uk-icon-spin'); if (err) { console.log('postJSON failed: ' + JSON.stringify(err)); $submit.removeAttr('disabled'); fn_error? fn_error(): showFormError($form, err); // fn_error()? } else { var r=fn_success? window[fn_success](result): false; // window[fn_success](result)什么意思? if (r===false) { $submit.removeAttr('disabled'); } } }); }); $form.find('button[type=submit]').removeAttr('disabled'); } $(function () { $('form').each(function () { var $form = $(this); if ($form.attr('action-url')) { _bindSubmit($form); } }); }); $(function() { if (location.pathname==='/' || location.pathname.indexOf('/blog')===0) { $('li[data-url=blogs]').addClass('uk-active'); } }); function _display_error($obj, err) { // 选择器选取每个当前是可见的元素, 除以下几种情况之外的元素即是可见元素: // 设置为 display:none // type="hidden" 的表单元素 // Width 和 height 设置为 0 // 隐藏的父元素(同时隐藏所有子元素) if ($obj.is(':visible')) { $obj.hide(); } var msg = err.message || String(err); var L = ['
']; L.push('

Error: '); L.push(msg); L.push('

Code: '); L.push(err.error || '500'); L.push('

'); // slideDown()以滑动方式显示隐藏的

元素 $obj.html(L.join('')).slideDown(); } function error(err) { _display_error($('#error'), err); } function fatal(err) { _display_error($('#loading'), err); }

static/css下加入awesome.css:

 /* 鼠标移动到链接上时和按下去时的状态 */
 /* 文字修饰为无, 一般用于设置清除超链接的默认下划线 */
a:hover, a:active {
    text-decoration: none;
}

#vm {
    display: none;
}
#loading {
    margin-bottom: 15px;
}
#error {
    display: none;
    margin-bottom: 15px;
}
  1. 测试效果:






你可能感兴趣的:((十)注册和登录)