- 这一节流程还是比较简单的,只是涉及到大量的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;
}
-
测试效果: