提交 2847d4f0 编写于 作者: J jackfrued

更新了文档目录结构

上级 5f9b9a57
## 预备知识
### 并发编程
所谓并发编程就是让程序中有多个部分能够并发或同时执行,并发编程带来的好处不言而喻,其中最为关键的两点是提升了执行效率和改善了用户体验。下面简单阐述一下Python中实现并发编程的三种方式:
1. 多线程:Python中通过`threading`模块的`Thread`类并辅以`Lock``Condition``Event``Semaphore``Barrier`等类来支持多线程编程。Python解释器通过GIL(全局解释器锁)来防止多个线程同时执行本地字节码,这个锁对于CPython(Python解释器的官方实现)是必须的,因为CPython的内存管理并不是线程安全的。因为GIL的存在,Python的多线程并不能利用CPU的多核特性。
2. 多进程:使用多进程可以有效的解决GIL的问题,Python中的`multiprocessing`模块提供了`Process`类来实现多进程,其他的辅助类跟`threading`模块中的类类似,由于进程间的内存是相互隔离的(操作系统对进程的保护),进程间通信(共享数据)必须使用管道、套接字等方式,这一点从编程的角度来讲是比较麻烦的,为此,Python的`multiprocessing`模块提供了一个名为`Queue`的类,它基于管道和锁机制提供了多个进程共享的队列。
```Python
"""
用下面的命令运行程序并查看执行时间,例如:
time python3 example06.py
real 0m20.657s
user 1m17.749s
sys 0m0.158s
使用多进程后实际执行时间为20.657秒,而用户时间1分17.749秒约为实际执行时间的4倍
这就证明我们的程序通过多进程使用了CPU的多核特性,而且这台计算机配置了4核的CPU
"""
import concurrent.futures
import math
PRIMES = [
1116281,
1297337,
104395303,
472882027,
533000389,
817504243,
982451653,
112272535095293,
112582705942171,
112272535095293,
115280095190773,
115797848077099,
1099726899285419
] * 5
def is_prime(num):
"""判断素数"""
assert num > 0
for i in range(2, int(math.sqrt(num)) + 1):
if num % i == 0:
return False
return num != 1
def main():
"""主函数"""
with concurrent.futures.ProcessPoolExecutor() as executor:
for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
print('%d is prime: %s' % (number, prime))
if __name__ == '__main__':
main()
```
3. 异步编程(异步I/O):所谓异步编程是通过调度程序从任务队列中挑选任务,调度程序以交叉的形式执行这些任务,我们并不能保证任务将以某种顺序去执行,因为执行顺序取决于队列中的一项任务是否愿意将CPU处理时间让位给另一项任务。异步编程通常通过多任务协作处理的方式来实现,由于执行时间和顺序的不确定,因此需要通过钩子函数(回调函数)或者`Future`对象来获取任务执行的结果。目前我们使用的Python 3通过`asyncio`模块以及`await``async`关键字(Python 3.5中引入,Python 3.7中正式成为关键字)提供了对异步I/O的支持。
```Python
import asyncio
async def fetch(host):
"""从指定的站点抓取信息(协程函数)"""
print(f'Start fetching {host}\n')
# 跟服务器建立连接
reader, writer = await asyncio.open_connection(host, 80)
# 构造请求行和请求头
writer.write(b'GET / HTTP/1.1\r\n')
writer.write(f'Host: {host}\r\n'.encode())
writer.write(b'\r\n')
# 清空缓存区(发送请求)
await writer.drain()
# 接收服务器的响应(读取响应行和响应头)
line = await reader.readline()
while line != b'\r\n':
print(line.decode().rstrip())
line = await reader.readline()
print('\n')
writer.close()
def main():
"""主函数"""
urls = ('www.sohu.com', 'www.douban.com', 'www.163.com')
# 获取系统默认的事件循环
loop = asyncio.get_event_loop()
# 用生成式语法构造一个包含多个协程对象的列表
tasks = [fetch(url) for url in urls]
# 通过asyncio模块的wait函数将协程列表包装成Task(Future子类)并等待其执行完成
# 通过事件循环的run_until_complete方法运行任务直到Future完成并返回它的结果
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
if __name__ == '__main__':
main()
```
> 说明:目前大多数网站都要求基于HTTPS通信,因此上面例子中的网络请求不一定能收到正常的响应,也就是说响应状态码不一定是200,有可能是3xx或者4xx。当然我们这里的重点不在于获得网站响应的内容,而是帮助大家理解`asyncio`模块以及`async`和`await`两个关键字的使用。
我们对三种方式的使用场景做一个简单的总结。
以下情况需要使用多线程:
1. 程序需要维护许多共享的状态(尤其是可变状态),Python中的列表、字典、集合都是线程安全的,所以使用线程而不是进程维护共享状态的代价相对较小。
2. 程序会花费大量时间在I/O操作上,没有太多并行计算的需求且不需占用太多的内存。
以下情况需要使用多进程:
1. 程序执行计算密集型任务(如:字节码操作、数据处理、科学计算)。
2. 程序的输入可以并行的分成块,并且可以将运算结果合并。
3. 程序在内存使用方面没有任何限制且不强依赖于I/O操作(如:读写文件、套接字等)。
最后,如果程序不需要真正的并发性或并行性,而是更多的依赖于异步处理和回调时,异步I/O就是一种很好的选择。另一方面,当程序中有大量的等待与休眠时,也应该考虑使用异步I/O。
> 扩展:关于进程,还需要做一些补充说明。首先,为了控制进程的执行,操作系统内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程使之继续执行,这种行为被称为进程切换(也叫调度)。进程切换是比较耗费资源的操作,因为在进行切换时首先要保存当前进程的上下文(内核再次唤醒该进程时所需要的状态,包括:程序计数器、状态寄存器、数据栈等),然后还要恢复准备执行的进程的上下文。正在执行的进程由于期待的某些事件未发生,如请求系统资源失败、等待某个操作完成、新数据尚未到达等原因会主动由运行状态变为阻塞状态,当进程进入阻塞状态,是不占用CPU资源的。这些知识对于理解到底选择哪种方式进行并发编程也是很重要的。
### I/O模式和事件驱动
对于一次I/O操作(以读操作为例),数据会先被拷贝到操作系统内核的缓冲区中,然后从操作系统内核的缓冲区拷贝到应用程序的缓冲区(这种方式称为标准I/O或缓存I/O,大多数文件系统的默认I/O都是这种方式),最后交给进程。所以说,当一个读操作发生时(写操作与之类似),它会经历两个阶段:(1)等待数据准备就绪;(2)将数据从内核拷贝到进程中。
由于存在这两个阶段,因此产生了以下几种I/O模式:
1. 阻塞 I/O(blocking I/O):进程发起读操作,如果内核数据尚未就绪,进程会阻塞等待数据直到内核数据就绪并拷贝到进程的内存中。
2. 非阻塞 I/O(non-blocking I/O):进程发起读操作,如果内核数据尚未就绪,进程不阻塞而是收到内核返回的错误信息,进程收到错误信息可以再次发起读操作,一旦内核数据准备就绪,就立即将数据拷贝到了用户内存中,然后返回。
3. 多路I/O复用( I/O multiplexing):监听多个I/O对象,当I/O对象有变化(数据就绪)的时候就通知用户进程。多路I/O复用的优势并不在于单个I/O操作能处理得更快,而是在于能处理更多的I/O操作。
4. 异步 I/O(asynchronous I/O):进程发起读操作后就可以去做别的事情了,内核收到异步读操作后会立即返回,所以用户进程不阻塞,当内核数据准备就绪时,内核发送一个信号给用户进程,告诉它读操作完成了。
通常,我们编写一个处理用户请求的服务器程序时,有以下三种方式可供选择:
1. 每收到一个请求,创建一个新的进程,来处理该请求;
2. 每收到一个请求,创建一个新的线程,来处理该请求;
3. 每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求
第1种方式实现比较简单,但由于创建进程开销比较大,会导致服务器性能比较差;第2种方式,由于要涉及到线程的同步,有可能会面临竞争、死锁等问题;第3种方式,就是所谓事件驱动的方式,它利用了多路I/O复用和异步I/O的优点,虽然代码逻辑比前面两种都复杂,但能达到最好的性能,这也是目前大多数网络服务器采用的方式。
## Tornado入门
### Tornado概述
Python的Web框架种类繁多(比Python语言的关键字还要多),但在众多优秀的Web框架中,Tornado框架最适合用来开发需要处理长连接和应对高并发的Web应用。Tornado框架在设计之初就考虑到性能问题,通过对非阻塞I/O和epoll(Linux 2.5.44内核引入的一种多路I/O复用方式,旨在实现高性能网络服务,在BSD和macOS中是kqueue)的运用,Tornado可以处理大量的并发连接,更轻松的应对C10K(万级并发)问题,是非常理想的实时通信Web框架。
> 扩展:基于线程的Web服务器产品(如:Apache)会维护一个线程池来处理用户请求,当用户请求到达时就为该请求分配一个线程,如果线程池中没有空闲线程了,那么可以通过创建新的线程来应付新的请求,但前提是系统尚有空闲的内存空间,显然这种方式很容易将服务器的空闲内存耗尽(大多数Linux发行版本中,默认的线程栈大小为8M)。想象一下,如果我们要开发一个社交类应用,这类应用中,通常需要显示实时更新的消息、对象状态的变化和各种类型的通知,那也就意味着客户端需要保持请求连接来接收服务器的各种响应,在这种情况下,服务器上的工作线程很容易被耗尽,这也就意味着新的请求很有可能无法得到响应。
Tornado框架源于FriendFeed网站,在FriendFeed网站被Facebook收购之后得以开源,正式发布的日期是2009年9月10日。Tornado能让你能够快速开发高速的Web应用,如果你想编写一个可扩展的社交应用、实时分析引擎,或RESTful API,那么Tornado框架就是很好的选择。Tornado其实不仅仅是一个Web开发的框架,它还是一个高性能的事件驱动网络访问引擎,内置了高性能的HTTP服务器和客户端(支持同步和异步请求),同时还对WebSocket提供了完美的支持。
了解和学习Tornado最好的资料就是它的官方文档,在[tornadoweb.org](http://www.tornadoweb.org)上面有很多不错的例子,你也可以在Github上找到Tornado的源代码和历史版本。
### 5分钟上手Tornado
1. 创建并激活虚拟环境。
```Shell
mkdir hello-tornado
cd hello-tornado
python3 -m venv venv
source venv/bin/activate
```
2. 安装Tornado。
```Shell
pip install tornado
```
3. 编写Web应用。
```Python
"""
example01.py
"""
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write('<h1>Hello, world!</h1>')
def main():
app = tornado.web.Application(handlers=[(r'/', MainHandler), ])
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
if __name__ == '__main__':
main()
```
4. 运行并访问应用。
```Shell
python example01.py
```
![](./res/run-hello-world-app.png)
在上面的例子中,代码example01.py通过定义一个继承自`RequestHandler`的类(`MainHandler`)来处理用户请求,当请求到达时,Tornado会实例化这个类(创建`MainHandler`对象),并调用与HTTP请求方法(GET、POST等)对应的方法,显然上面的`MainHandler`只能处理GET请求,在收到GET请求时,它会将一段HTML的内容写入到HTTP响应中。`main`函数的第1行代码创建了Tornado框架中`Application`类的实例,它代表了我们的Web应用,而创建该实例最为重要的参数就是`handlers`,该参数告知`Application`对象,当收到一个请求时应该通过哪个类的对象来处理这个请求。在上面的例子中,当通过HTTP的GET请求访问站点根路径时,就会调用`MainHandler``get`方法。 `main`函数的第2行代码通过`Application`对象的`listen`方法指定了监听HTTP请求的端口。`main`函数的第3行代码用于获取Tornado框架的`IOLoop`实例并启动它,该实例代表一个条件触发的I/O循环,用于持续的接收来自于客户端的请求。
> 扩展:在Python 3中,`IOLoop`实例的本质就是`asyncio`的事件循环,该事件循环在非Windows系统中就是`SelectorEventLoop`对象,它基于`selectors`模块(高级I/O复用模块),会使用当前操作系统最高效的I/O复用选择器,例如在Linux环境下它使用`EpollSelector`,而在macOS和BSD环境下它使用的是`KqueueSelector`;在Python 2中,`IOLoop`直接使用`select`模块(低级I/O复用模块)的`epoll`或`kqueue`函数,如果这两种方式都不可用,则调用`select`函数实现多路I/O复用。当然,如果要支持高并发,你的系统最好能够支持epoll或者kqueue这两种多路I/O复用方式中的一种。
如果希望通过命令行参数来指定Web应用的监听端口,可以对上面的代码稍作修改。
```Python
"""
example01.py
"""
import tornado.ioloop
import tornado.web
from tornado.options import define, options, parse_command_line
# 定义默认端口
define('port', default=8000, type=int)
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write('<h1>Hello, world!</h1>')
def main():
# python example01.py --port=8000
parse_command_line()
app = tornado.web.Application(handlers=[(r'/', MainHandler), ])
app.listen(options.port)
tornado.ioloop.IOLoop.current().start()
if __name__ == '__main__':
main()
```
在启动Web应用时,如果没有指定端口,将使用`define`函数中设置的默认端口8000,如果要指定端口,可以使用下面的方式来启动Web应用。
```Shell
python example01.py --port=8000
```
### 路由解析
上面我们曾经提到过创建`Application`实例时需要指定`handlers`参数,这个参数非常重要,它应该是一个元组的列表,元组中的第一个元素是正则表达式,它用于匹配用户请求的资源路径;第二个元素是`RequestHandler`的子类。在刚才的例子中,我们只在`handlers`列表中放置了一个元组,事实上我们可以放置多个元组来匹配不同的请求(资源路径),而且可以使用正则表达式的捕获组来获取匹配的内容并将其作为参数传入到`get``post`这些方法中。
```Python
"""
example02.py
"""
import os
import random
import tornado.ioloop
import tornado.web
from tornado.options import define, options, parse_command_line
# 定义默认端口
define('port', default=8000, type=int)
class SayingHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def get(self):
sayings = [
'世上没有绝望的处境,只有对处境绝望的人',
'人生的道路在态度的岔口一分为二,从此通向成功或失败',
'所谓措手不及,不是说没有时间准备,而是有时间的时候没有准备',
'那些你认为不靠谱的人生里,充满你没有勇气做的事',
'在自己喜欢的时间里,按照自己喜欢的方式,去做自己喜欢做的事,这便是自由',
'有些人不属于自己,但是遇见了也弥足珍贵'
]
# 渲染index.html模板页
self.render('index.html', message=random.choice(sayings))
class WeatherHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def get(self, city):
# Tornado框架会自动处理百分号编码的问题
weathers = {
'北京': {'temperature': '-4~4', 'pollution': '195 中度污染'},
'成都': {'temperature': '3~9', 'pollution': '53 良'},
'深圳': {'temperature': '20~25', 'pollution': '25 优'},
'广州': {'temperature': '18~23', 'pollution': '56 良'},
'上海': {'temperature': '6~8', 'pollution': '65 良'}
}
if city in weathers:
self.render('weather.html', city=city, weather=weathers[city])
else:
self.render('index.html', message=f'没有{city}的天气信息')
class ErrorHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def get(self):
# 重定向到指定的路径
self.redirect('/saying')
def main():
"""主函数"""
parse_command_line()
app = tornado.web.Application(
# handlers是按列表中的顺序依次进行匹配的
handlers=[
(r'/saying/?', SayingHandler),
(r'/weather/([^/]{2,})/?', WeatherHandler),
(r'/.+', ErrorHandler),
],
# 通过template_path参数设置模板页的路径
template_path=os.path.join(os.path.dirname(__file__), 'templates')
)
app.listen(options.port)
tornado.ioloop.IOLoop.current().start()
if __name__ == '__main__':
main()
```
模板页index.html。
```HTML
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tornado基础</title>
</head>
<body>
<h1>{{message}}</h1>
</body>
</html>
```
模板页weather.html。
```HTML
<!-- weather.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tornado基础</title>
</head>
<body>
<h1>{{city}}</h1>
<hr>
<h2>温度:{{weather['temperature']}}摄氏度</h2>
<h2>污染指数:{{weather['pollution']}}</h2>
</body>
</html>
```
Tornado的模板语法与其他的Web框架中使用的模板语法并没有什么实质性的区别,而且目前的Web应用开发更倡导使用前端渲染的方式来减轻服务器的负担,所以这里我们并不对模板语法和后端渲染进行深入的讲解。
### 请求处理器
通过上面的代码可以看出,`RequestHandler`是处理用户请求的核心类,通过重写`get``post``put``delete`等方法可以处理不同类型的HTTP请求,除了这些方法之外,`RequestHandler`还实现了很多重要的方法,下面是部分方法的列表:
1. `get_argument` / `get_arguments` / `get_body_argument` / `get_body_arguments` / `get_query_arugment` / `get_query_arguments`:获取请求参数。
2. `set_status` / `send_error` / `set_header` / `add_header` / `clear_header` / `clear`:操作状态码和响应头。
3. `write` / `flush` / `finish` / `write_error`:和输出相关的方法。
4. `render` / `render_string`:渲染模板。
5. `redirect`:请求重定向。
6. `get_cookie` / `set_cookie` / `get_secure_cookie` / `set_secure_cookie` / `create_signed_value` / `clear_cookie` / `clear_all_cookies`:操作Cookie。
我们用上面讲到的这些方法来完成下面的需求,访问页面时,如果Cookie中没有读取到用户信息则要求用户填写个人信息,如果从Cookie中读取到用户信息则直接显示用户信息。
```Python
"""
example03.py
"""
import os
import re
import tornado.ioloop
import tornado.web
from tornado.options import define, options, parse_command_line
# 定义默认端口
define('port', default=8000, type=int)
users = {}
class User(object):
"""用户"""
def __init__(self, nickname, gender, birthday):
self.nickname = nickname
self.gender = gender
self.birthday = birthday
class MainHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def get(self):
# 从Cookie中读取用户昵称
nickname = self.get_cookie('nickname')
if nickname in users:
self.render('userinfo.html', user=users[nickname])
else:
self.render('userform.html', hint='请填写个人信息')
class UserHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def post(self):
# 从表单参数中读取用户昵称、性别和生日信息
nickname = self.get_body_argument('nickname').strip()
gender = self.get_body_argument('gender')
birthday = self.get_body_argument('birthday')
# 检查用户昵称是否有效
if not re.fullmatch(r'\w{6,20}', nickname):
self.render('userform.html', hint='请输入有效的昵称')
elif nickname in users:
self.render('userform.html', hint='昵称已经被使用过')
else:
users[nickname] = User(nickname, gender, birthday)
# 将用户昵称写入Cookie并设置有效期为7天
self.set_cookie('nickname', nickname, expires_days=7)
self.render('userinfo.html', user=users[nickname])
def main():
"""主函数"""
parse_command_line()
app = tornado.web.Application(
handlers=[
(r'/', MainHandler), (r'/register', UserHandler)
],
template_path=os.path.join(os.path.dirname(__file__), 'templates')
)
app.listen(options.port)
tornado.ioloop.IOLoop.current().start()
if __name__ == '__main__':
main()
```
模板页userform.html。
```HTML
<!-- userform.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tornado基础</title>
<style>
.em { color: red; }
</style>
</head>
<body>
<h1>填写用户信息</h1>
<hr>
<p class="em">{{hint}}</p>
<form action="/register" method="post">
<p>
<label>昵称:</label>
<input type="text" name="nickname">
(字母数字下划线,6-20个字符)
</p>
<p>
<label>性别:</label>
<input type="radio" name="gender" value="男" checked>男
<input type="radio" name="gender" value="女">女
</p>
<p>
<label>生日:</label>
<input type="date" name="birthday" value="1990-01-01">
</p>
<p>
<input type="submit" value="确定">
</p>
</form>
</body>
</html>
```
模板页userinfo.html。
```HTML
<!-- userinfo.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tornado基础</title>
</head>
<body>
<h1>用户信息</h1>
<hr>
<h2>昵称:{{user.nickname}}</h2>
<h2>性别:{{user.gender}}</h2>
<h2>出生日期:{{user.birthday}}</h2>
</body>
</html>
```
## Tornado中的异步化
在前面的例子中,我们并没有对`RequestHandler`中的`get``post`方法进行异步处理,这就意味着,一旦在`get``post`方法中出现了耗时间的操作,不仅仅是当前请求被阻塞,按照Tornado框架的工作模式,其他的请求也会被阻塞,所以我们需要对耗时间的操作进行异步化处理。
在Tornado稍早一些的版本中,可以用装饰器实现请求方法的异步化或协程化来解决这个问题。
-`RequestHandler`的请求处理函数添加`@tornado.web.asynchronous`装饰器,如下所示:
```Python
class AsyncReqHandler(RequestHandler):
@tornado.web.asynchronous
def get(self):
http = httpclient.AsyncHTTPClient()
http.fetch("http://example.com/", self._on_download)
def _on_download(self, response):
do_something_with_response(response)
self.render("template.html")
```
-`RequestHandler`的请求处理函数添加`@tornado.gen.coroutine`装饰器,如下所示:
```Python
class GenAsyncHandler(RequestHandler):
@tornado.gen.coroutine
def get(self):
http_client = AsyncHTTPClient()
response = yield http_client.fetch("http://example.com")
do_something_with_response(response)
self.render("template.html")
```
- 使用`@return_future`装饰器,如下所示:
```Python
@return_future
def future_func(arg1, arg2, callback):
# Do stuff (possibly asynchronous)
callback(result)
async def caller():
await future_func(arg1, arg2)
```
在Tornado 5.x版本中,这几个装饰器都被标记为**deprcated**(过时),我们可以通过Python 3.5中引入的`async``await`(在Python 3.7中已经成为正式的关键字)来达到同样的效果。当然,要实现异步化还得靠其他的支持异步操作的三方库来支持,如果请求处理函数中用到了不支持异步操作的三方库,就需要靠自己写包装类来支持异步化。
下面的代码演示了在读写数据库时如何实现请求处理的异步化。我们用到的数据库建表语句如下所示:
```SQL
create database hrs default charset utf8;
use hrs;
/* 创建部门表 */
create table tb_dept
(
dno int not null comment '部门编号',
dname varchar(10) not null comment '部门名称',
dloc varchar(20) not null comment '部门所在地',
primary key (dno)
);
insert into tb_dept values
(10, '会计部', '北京'),
(20, '研发部', '成都'),
(30, '销售部', '重庆'),
(40, '运维部', '深圳');
```
我们通过下面的代码实现了查询和新增部门两个操作。
```Python
import json
import aiomysql
import tornado
import tornado.web
from tornado.ioloop import IOLoop
from tornado.options import define, parse_command_line, options
define('port', default=8000, type=int)
async def connect_mysql():
return await aiomysql.connect(
host='120.77.222.217',
port=3306,
db='hrs',
user='root',
password='123456',
)
class HomeHandler(tornado.web.RequestHandler):
async def get(self, no):
async with self.settings['mysql'].cursor(aiomysql.DictCursor) as cursor:
await cursor.execute("select * from tb_dept where dno=%s", (no, ))
if cursor.rowcount == 0:
self.finish(json.dumps({
'code': 20001,
'mesg': f'没有编号为{no}的部门'
}))
return
row = await cursor.fetchone()
self.finish(json.dumps(row))
async def post(self, *args, **kwargs):
no = self.get_argument('no')
name = self.get_argument('name')
loc = self.get_argument('loc')
conn = self.settings['mysql']
try:
async with conn.cursor() as cursor:
await cursor.execute('insert into tb_dept values (%s, %s, %s)',
(no, name, loc))
await conn.commit()
except aiomysql.MySQLError:
self.finish(json.dumps({
'code': 20002,
'mesg': '添加部门失败请确认部门信息'
}))
else:
self.set_status(201)
self.finish()
def make_app(config):
return tornado.web.Application(
handlers=[(r'/api/depts/(.*)', HomeHandler), ],
**config
)
def main():
parse_command_line()
app = make_app({
'debug': True,
'mysql': IOLoop.current().run_sync(connect_mysql)
})
app.listen(options.port)
IOLoop.current().start()
if __name__ == '__main__':
main()
```
上面的代码中,我们用到了`aiomysql`这个三方库,它基于`pymysql`封装,实现了对MySQL操作的异步化。操作Redis可以使用`aioredis`,访问MongoDB可以使用`motor`,这些都是支持异步操作的三方库。
\ No newline at end of file
## WebSocket的应用
Tornado的异步特性使其非常适合处理高并发的业务,同时也适合那些需要在客户端和服务器之间维持长连接的业务。传统的基于HTTP协议的Web应用,服务器和客户端(浏览器)的通信只能由客户端发起,这种单向请求注定了如果服务器有连续的状态变化,客户端(浏览器)是很难得知的。事实上,今天的很多Web应用都需要服务器主动向客户端(浏览器)发送数据,我们将这种通信方式称之为“推送”。过去很长一段时间,程序员都是用定时轮询(Polling)或长轮询(Long Polling)等方式来实现“推送”,但是这些都不是真正意义上的“推送”,而且浪费资源且效率低下。在HTML5时代,可以通过一种名为WebSocket的技术在服务器和客户端(浏览器)之间维持传输数据的长连接,这种方式可以实现真正的“推送”服务。
### WebSocket简介
WebSocket 协议在2008年诞生,2011年成为国际标准([RFC 6455](https://tools.ietf.org/html/rfc6455)),现在的浏览器都能够支持它,它可以实现浏览器和服务器之间的全双工通信。我们之前学习或了解过Python的Socket编程,通过Socket编程,可以基于TCP或UDP进行数据传输;而WebSocket与之类似,只不过它是基于HTTP来实现通信握手,使用TCP来进行数据传输。WebSocket的出现打破了HTTP请求和响应只能一对一通信的模式,也改变了服务器只能被动接受客户端请求的状况。目前有很多Web应用是需要服务器主动向客户端发送信息的,例如股票信息的网站可能需要向浏览器发送股票涨停通知,社交网站可能需要向用户发送好友上线提醒或聊天信息。
![](./res/websocket.png)
WebSocket的特点如下所示:
1. 建立在TCP协议之上,服务器端的实现比较容易。
2. 与HTTP协议有着良好的兼容性,默认端口是80(WS)和443(WSS),通信握手阶段采用HTTP协议,能通过各种 HTTP 代理服务器(不容易被防火墙阻拦)。
3. 数据格式比较轻量,性能开销小,通信高效。
4. 可以发送文本,也可以发送二进制数据。
5. 没有同源策略的限制,客户端(浏览器)可以与任意服务器通信。
![](./res/ws_wss.png)
### WebSocket服务器端编程
Tornado框架中有一个`tornado.websocket.WebSocketHandler`类专门用于处理来自WebSocket的请求,通过继承该类并重写`open``on_message``on_close` 等方法来处理WebSocket通信,下面我们对`WebSocketHandler`的核心方法做一个简单的介绍。
1. `open(*args, **kwargs)`方法:建立新的WebSocket连接后,Tornado框架会调用该方法,该方法的参数与`RequestHandler``get`方法的参数类似,这也就意味着在`open`方法中可以执行获取请求参数、读取Cookie信息这样的操作。
2. `on_message(message)`方法:建立WebSocket之后,当收到来自客户端的消息时,Tornado框架会调用该方法,这样就可以对收到的消息进行对应的处理,必须重写这个方法。
3. `on_close()`方法:当WebSocket被关闭时,Tornado框架会调用该方法,在该方法中可以通过`close_code``close_reason`了解关闭的原因。
4. `write_message(message, binary=False)`方法:将指定的消息通过WebSocket发送给客户端,可以传递utf-8字符序列或者字节序列,如果message是一个字典,将会执行JSON序列化。正常情况下,该方法会返回一个`Future`对象;如果WebSocket被关闭了,将引发`WebSocketClosedError`
5. `set_nodelay(value)`方法:默认情况下,因为TCP的Nagle算法会导致短小的消息被延迟发送,在考虑到交互性的情况下就要通过将该方法的参数设置为`True`来避免延迟。
6. `close(code=None, reason=None)`方法:主动关闭WebSocket,可以指定状态码(详见[RFC 6455 7.4.1节](https://tools.ietf.org/html/rfc6455#section-7.4.1))和原因。
### WebSocket客户端编程
1. 创建WebSocket对象。
```JavaScript
var webSocket = new WebSocket('ws://localhost:8000/ws');
```
>说明:webSocket对象的readyState属性表示该对象当前状态,取值为CONNECTING-正在连接,OPEN-连接成功可以通信,CLOSING-正在关闭,CLOSED-已经关闭。
2. 编写回调函数。
```JavaScript
webSocket.onopen = function(evt) { webSocket.send('...'); };
webSocket.onmessage = function(evt) { console.log(evt.data); };
webSocket.onclose = function(evt) {};
webSocket.onerror = function(evt) {};
```
> 说明:如果要绑定多个事件回调函数,可以用addEventListener方法。另外,通过事件对象的data属性获得的数据可能是字符串,也有可能是二进制数据,可以通过webSocket对象的binaryType属性(blob、arraybuffer)或者通过typeof、instanceof运算符检查类型进行判定。
### 项目:Web聊天室
```Python
"""
handlers.py - 用户登录和聊天的处理器
"""
import tornado.web
import tornado.websocket
nicknames = set()
connections = {}
class LoginHandler(tornado.web.RequestHandler):
def get(self):
self.render('login.html', hint='')
def post(self):
nickname = self.get_argument('nickname')
if nickname in nicknames:
self.render('login.html', hint='昵称已被使用,请更换昵称')
self.set_secure_cookie('nickname', nickname)
self.render('chat.html')
class ChatHandler(tornado.websocket.WebSocketHandler):
def open(self):
nickname = self.get_secure_cookie('nickname').decode()
nicknames.add(nickname)
for conn in connections.values():
conn.write_message(f'~~~{nickname}进入了聊天室~~~')
connections[nickname] = self
def on_message(self, message):
nickname = self.get_secure_cookie('nickname').decode()
for conn in connections.values():
if conn is not self:
conn.write_message(f'{nickname}说:{message}')
def on_close(self):
nickname = self.get_secure_cookie('nickname').decode()
del connections[nickname]
nicknames.remove(nickname)
for conn in connections.values():
conn.write_message(f'~~~{nickname}离开了聊天室~~~')
```
```Python
"""
run_chat_server.py - 聊天服务器
"""
import os
import tornado.web
import tornado.ioloop
from handlers import LoginHandler, ChatHandler
if __name__ == '__main__':
app = tornado.web.Application(
handlers=[(r'/login', LoginHandler), (r'/chat', ChatHandler)],
template_path=os.path.join(os.path.dirname(__file__), 'templates'),
static_path=os.path.join(os.path.dirname(__file__), 'static'),
cookie_secret='MWM2MzEyOWFlOWRiOWM2MGMzZThhYTk0ZDNlMDA0OTU=',
)
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
```
```HTML
<!-- login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tornado聊天室</title>
<style>
.hint { color: red; font-size: 0.8em; }
</style>
</head>
<body>
<div>
<div id="container">
<h1>进入聊天室</h1>
<hr>
<p class="hint">{{hint}}</p>
<form method="post" action="/login">
<label>昵称:</label>
<input type="text" placeholder="请输入你的昵称" name="nickname">
<button type="submit">登录</button>
</form>
</div>
</div>
</body>
</html>
```
```HTML
<!-- chat.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tornado聊天室</title>
</head>
<body>
<h1>聊天室</h1>
<hr>
<div>
<textarea id="contents" rows="20" cols="120" readonly></textarea>
</div>
<div class="send">
<input type="text" id="content" size="50">
<input type="button" id="send" value="发送">
</div>
<p>
<a id="quit" href="javascript:void(0);">退出聊天室</a>
</p>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
$(function() {
// 将内容追加到指定的文本区
function appendContent($ta, message) {
var contents = $ta.val();
contents += '\n' + message;
$ta.val(contents);
$ta[0].scrollTop = $ta[0].scrollHeight;
}
// 通过WebSocket发送消息
function sendMessage() {
message = $('#content').val().trim();
if (message.length > 0) {
ws.send(message);
appendContent($('#contents'), '我说:' + message);
$('#content').val('');
}
}
// 创建WebSocket对象
var ws= new WebSocket('ws://localhost:8888/chat');
// 连接建立后执行的回调函数
ws.onopen = function(evt) {
$('#contents').val('~~~欢迎您进入聊天室~~~');
};
// 收到消息后执行的回调函数
ws.onmessage = function(evt) {
appendContent($('#contents'), evt.data);
};
// 为发送按钮绑定点击事件回调函数
$('#send').on('click', sendMessage);
// 为文本框绑定按下回车事件回调函数
$('#content').on('keypress', function(evt) {
keycode = evt.keyCode || evt.which;
if (keycode == 13) {
sendMessage();
}
});
// 为退出聊天室超链接绑定点击事件回调函数
$('#quit').on('click', function(evt) {
ws.close();
location.href = '/login';
});
});
</script>
</body>
</html>
```
"""
handlers.py - 用户登录和聊天的处理器
"""
import tornado.web
import tornado.websocket
nicknames = set()
connections = {}
class LoginHandler(tornado.web.RequestHandler):
def get(self):
self.render('login.html', hint='')
def post(self):
nickname = self.get_argument('nickname')
if nickname in nicknames:
self.render('login.html', hint='昵称已被使用,请更换昵称')
self.set_secure_cookie('nickname', nickname)
self.render('chat.html')
class ChatHandler(tornado.websocket.WebSocketHandler):
def open(self):
nickname = self.get_secure_cookie('nickname').decode()
nicknames.add(nickname)
for conn in connections.values():
conn.write_message(f'~~~{nickname}进入了聊天室~~~')
connections[nickname] = self
def on_message(self, message):
nickname = self.get_secure_cookie('nickname').decode()
for conn in connections.values():
if conn is not self:
conn.write_message(f'{nickname}说:{message}')
def on_close(self):
nickname = self.get_secure_cookie('nickname').decode()
del connections[nickname]
nicknames.remove(nickname)
for conn in connections.values():
conn.write_message(f'~~~{nickname}离开了聊天室~~~')
"""
chat_server.py - 聊天服务器
"""
import os
import tornado.web
import tornado.ioloop
from chat_handlers import LoginHandler, ChatHandler
def main():
app = tornado.web.Application(
handlers=[(r'/login', LoginHandler), (r'/chat', ChatHandler)],
template_path=os.path.join(os.path.dirname(__file__), 'templates'),
static_path=os.path.join(os.path.dirname(__file__), 'static'),
cookie_secret='MWM2MzEyOWFlOWRiOWM2MGMzZThhYTk0ZDNlMDA0OTU=',
)
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
if __name__ == '__main__':
main()
"""
example01.py - 五分钟上手Tornado
"""
import tornado.ioloop
import tornado.web
from tornado.options import define, options, parse_command_line
# 定义默认端口
define('port', default=8000, type=int)
class MainHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def get(self):
# 向客户端(浏览器)写入内容
self.write('<h1>Hello, world!</h1>')
def main():
"""主函数"""
# 解析命令行参数,例如:
# python example01.py --port 8888
parse_command_line()
# 创建了Tornado框架中Application类的实例并指定handlers参数
# Application实例代表了我们的Web应用,handlers代表了路由解析
app = tornado.web.Application(handlers=[(r'/', MainHandler), ])
# 指定了监听HTTP请求的TCP端口(默认8000,也可以通过命令行参数指定)
app.listen(options.port)
# 获取Tornado框架的IOLoop实例并启动它(默认启动asyncio的事件循环)
tornado.ioloop.IOLoop.current().start()
if __name__ == '__main__':
main()
"""
example02.py - 路由解析
"""
import os
import random
import tornado.ioloop
import tornado.web
from tornado.options import define, options, parse_command_line
# 定义默认端口
define('port', default=8000, type=int)
class SayingHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def get(self):
sayings = [
'世上没有绝望的处境,只有对处境绝望的人',
'人生的道路在态度的岔口一分为二,从此通向成功或失败',
'所谓措手不及,不是说没有时间准备,而是有时间的时候没有准备',
'那些你认为不靠谱的人生里,充满你没有勇气做的事',
'在自己喜欢的时间里,按照自己喜欢的方式,去做自己喜欢做的事,这便是自由',
'有些人不属于自己,但是遇见了也弥足珍贵'
]
# 渲染index.html模板页
self.render('index.html', message=random.choice(sayings))
class WeatherHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def get(self, city):
# Tornado框架会自动处理百分号编码的问题
weathers = {
'北京': {'temperature': '-4~4', 'pollution': '195 中度污染'},
'成都': {'temperature': '3~9', 'pollution': '53 良'},
'深圳': {'temperature': '20~25', 'pollution': '25 优'},
'广州': {'temperature': '18~23', 'pollution': '56 良'},
'上海': {'temperature': '6~8', 'pollution': '65 良'}
}
if city in weathers:
self.render('weather.html', city=city, weather=weathers[city])
else:
self.render('index.html', message=f'没有{city}的天气信息')
class ErrorHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def get(self):
# 重定向到指定的路径
self.redirect('/saying')
def main():
"""主函数"""
parse_command_line()
app = tornado.web.Application(
# handlers是按列表中的顺序依次进行匹配的
handlers=[
(r'/saying/?', SayingHandler),
(r'/weather/([^/]{2,})/?', WeatherHandler),
(r'/.+', ErrorHandler),
],
# 通过template_path参数设置模板页的路径
template_path=os.path.join(os.path.dirname(__file__), 'templates')
)
app.listen(options.port)
tornado.ioloop.IOLoop.current().start()
if __name__ == '__main__':
main()
"""
example03.py - RequestHandler解析
"""
import os
import re
import tornado.ioloop
import tornado.web
from tornado.options import define, options, parse_command_line
# 定义默认端口
define('port', default=8000, type=int)
users = {}
class User(object):
"""用户"""
def __init__(self, nickname, gender, birthday):
self.nickname = nickname
self.gender = gender
self.birthday = birthday
class MainHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def get(self):
# 从Cookie中读取用户昵称
nickname = self.get_cookie('nickname')
if nickname in users:
self.render('userinfo.html', user=users[nickname])
else:
self.render('userform.html', hint='请填写个人信息')
class UserHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def post(self):
# 从表单参数中读取用户昵称、性别和生日信息
nickname = self.get_body_argument('nickname').strip()
gender = self.get_body_argument('gender')
birthday = self.get_body_argument('birthday')
# 检查用户昵称是否有效
if not re.fullmatch(r'\w{6,20}', nickname):
self.render('userform.html', hint='请输入有效的昵称')
elif nickname in users:
self.render('userform.html', hint='昵称已经被使用过')
else:
users[nickname] = User(nickname, gender, birthday)
# 将用户昵称写入Cookie并设置有效期为7天
self.set_cookie('nickname', nickname, expires_days=7)
self.render('userinfo.html', user=users[nickname])
def main():
"""主函数"""
parse_command_line()
app = tornado.web.Application(
handlers=[
(r'/', MainHandler),
(r'/register', UserHandler),
],
template_path=os.path.join(os.path.dirname(__file__), 'templates'),
)
app.listen(options.port)
tornado.ioloop.IOLoop.current().start()
if __name__ == '__main__':
main()
"""
example04.py - 同步请求的例子
"""
import json
import os
import requests
import tornado.gen
import tornado.ioloop
import tornado.web
import tornado.websocket
import tornado.httpclient
from tornado.options import define, options, parse_command_line
define('port', default=8888, type=int)
# 请求天行数据提供的API数据接口
REQ_URL = 'http://api.tianapi.com/guonei/'
# 在天行数据网站注册后可以获得API_KEY
API_KEY = 'your_personal_api_key'
class MainHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def get(self):
resp = requests.get(f'{REQ_URL}?key={API_KEY}')
newslist = json.loads(resp.text)['newslist']
self.render('news.html', newslist=newslist)
def main():
"""主函数"""
parse_command_line()
app = tornado.web.Application(
handlers=[(r'/', MainHandler), ],
template_path=os.path.join(os.path.dirname(__file__), 'templates'),
)
app.listen(options.port)
tornado.ioloop.IOLoop.current().start()
if __name__ == '__main__':
main()
"""
example05.py - 异步请求的例子
"""
import aiohttp
import json
import os
import tornado.gen
import tornado.ioloop
import tornado.web
import tornado.websocket
import tornado.httpclient
from tornado.options import define, options, parse_command_line
define('port', default=8888, type=int)
# 请求天行数据提供的API数据接口
REQ_URL = 'http://api.tianapi.com/guonei/'
# 在天行数据网站注册后可以获得API_KEY
API_KEY = 'your_personal_api_key'
class MainHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
async def get(self):
async with aiohttp.ClientSession() as session:
resp = await session.get(f'{REQ_URL}?key={API_KEY}')
json_str = await resp.text()
print(json_str)
newslist = json.loads(json_str)['newslist']
self.render('news.html', newslist=newslist)
def main():
"""主函数"""
parse_command_line()
app = tornado.web.Application(
handlers=[(r'/', MainHandler), ],
template_path=os.path.join(os.path.dirname(__file__), 'templates'),
)
app.listen(options.port)
tornado.ioloop.IOLoop.current().start()
if __name__ == '__main__':
main()
"""
example06.py - 异步操作MySQL
"""
import json
import aiomysql
import tornado
import tornado.web
from tornado.ioloop import IOLoop
from tornado.options import define, parse_command_line, options
define('port', default=8888, type=int)
async def connect_mysql():
return await aiomysql.connect(
host='1.2.3.4',
port=3306,
db='hrs',
charset='utf8',
use_unicode=True,
user='yourname',
password='yourpass',
)
class HomeHandler(tornado.web.RequestHandler):
async def get(self, no):
async with self.settings['mysql'].cursor(aiomysql.DictCursor) as cursor:
await cursor.execute("select * from tb_dept where dno=%s", (no, ))
if cursor.rowcount == 0:
self.finish(json.dumps({
'code': 20001,
'mesg': f'没有编号为{no}的部门'
}))
return
row = await cursor.fetchone()
self.finish(json.dumps(row))
async def post(self, *args, **kwargs):
no = self.get_argument('no')
name = self.get_argument('name')
loc = self.get_argument('loc')
conn = self.settings['mysql']
try:
async with conn.cursor() as cursor:
await cursor.execute('insert into tb_dept values (%s, %s, %s)',
(no, name, loc))
await conn.commit()
except aiomysql.MySQLError:
self.finish(json.dumps({
'code': 20002,
'mesg': '添加部门失败请确认部门信息'
}))
else:
self.set_status(201)
self.finish()
def make_app(config):
return tornado.web.Application(
handlers=[(r'/api/depts/(.*)', HomeHandler), ],
**config
)
def main():
parse_command_line()
app = make_app({
'debug': True,
'mysql': IOLoop.current().run_sync(connect_mysql)
})
app.listen(options.port)
IOLoop.current().start()
if __name__ == '__main__':
main()
"""
example07.py - 将非异步的三方库封装为异步调用
"""
import asyncio
import concurrent
import json
import tornado
import tornado.web
import pymysql
from pymysql import connect
from pymysql.cursors import DictCursor
from tornado.ioloop import IOLoop
from tornado.options import define, parse_command_line, options
from tornado.platform.asyncio import AnyThreadEventLoopPolicy
define('port', default=8888, type=int)
def get_mysql_connection():
return connect(
host='1.2.3.4',
port=3306,
db='hrs',
charset='utf8',
use_unicode=True,
user='yourname',
password='yourpass',
)
class HomeHandler(tornado.web.RequestHandler):
executor = concurrent.futures.ThreadPoolExecutor(max_workers=10)
async def get(self, no):
return await self._get(no)
@tornado.concurrent.run_on_executor
def _get(self, no):
con = get_mysql_connection()
try:
with con.cursor(DictCursor) as cursor:
cursor.execute("select * from tb_dept where dno=%s", (no, ))
if cursor.rowcount == 0:
self.finish(json.dumps({
'code': 20001,
'mesg': f'没有编号为{no}的部门'
}))
return
row = cursor.fetchone()
self.finish(json.dumps(row))
finally:
con.close()
async def post(self, *args, **kwargs):
return await self._post(*args, **kwargs)
@tornado.concurrent.run_on_executor
def _post(self, *args, **kwargs):
no = self.get_argument('no')
name = self.get_argument('name')
loc = self.get_argument('loc')
conn = get_mysql_connection()
try:
with conn.cursor() as cursor:
cursor.execute('insert into tb_dept values (%s, %s, %s)',
(no, name, loc))
conn.commit()
except pymysql.MySQLError:
self.finish(json.dumps({
'code': 20002,
'mesg': '添加部门失败请确认部门信息'
}))
else:
self.set_status(201)
self.finish()
def main():
asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
parse_command_line()
app = tornado.web.Application(
handlers=[(r'/api/depts/(.*)', HomeHandler), ]
)
app.listen(options.port)
IOLoop.current().start()
if __name__ == '__main__':
main()
import asyncio
import re
import aiohttp
PATTERN = re.compile(r'\<title\>(?P<title>.*)\<\/title\>')
async def show_title(url):
async with aiohttp.ClientSession() as session:
resp = await session.get(url, ssl=False)
html = await resp.text()
print(PATTERN.search(html).group('title'))
def main():
urls = ('https://www.python.org/',
'https://git-scm.com/',
'https://www.jd.com/',
'https://www.taobao.com/',
'https://www.douban.com/')
# asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
# 获取事件循环()
loop = asyncio.get_event_loop()
tasks = [show_title(url) for url in urls]
loop.run_until_complete(asyncio.wait(tasks))
if __name__ == '__main__':
main()
import asyncio
async def fetch(host):
"""从指定的站点抓取信息(协程函数)"""
print(f'Start fetching {host}\n')
# 跟服务器建立连接
reader, writer = await asyncio.open_connection(host, 80)
# 构造请求行和请求头
writer.write(b'GET / HTTP/1.1\r\n')
writer.write(f'Host: {host}\r\n'.encode())
writer.write(b'\r\n')
# 清空缓存区(发送请求)
await writer.drain()
# 接收服务器的响应(读取响应行和响应头)
line = await reader.readline()
while line != b'\r\n':
print(line.decode().rstrip())
line = await reader.readline()
print('\n')
writer.close()
def main():
"""主函数"""
urls = ('www.sohu.com', 'www.douban.com', 'www.163.com')
# 获取系统默认的事件循环
loop = asyncio.get_event_loop()
# 用生成式语法构造一个包含多个协程对象的列表
tasks = [fetch(url) for url in urls]
# 通过asyncio模块的wait函数将协程列表包装成Task(Future子类)并等待其执行完成
# 通过事件循环的run_until_complete方法运行任务直到Future完成并返回它的结果
futures = asyncio.wait(tasks)
print(futures, type(futures))
loop.run_until_complete(futures)
loop.close()
if __name__ == '__main__':
main()
"""
协程(coroutine)- 可以在需要时进行切换的相互协作的子程序
"""
import asyncio
from example_of_multiprocess import is_prime
def num_generator(m, n):
"""指定范围的数字生成器"""
for num in range(m, n + 1):
print(f'generate number: {num}')
yield num
async def prime_filter(m, n):
"""素数过滤器"""
primes = []
for i in num_generator(m, n):
if is_prime(i):
print('Prime =>', i)
primes.append(i)
await asyncio.sleep(0.001)
return tuple(primes)
async def square_mapper(m, n):
"""平方映射器"""
squares = []
for i in num_generator(m, n):
print('Square =>', i * i)
squares.append(i * i)
await asyncio.sleep(0.001)
return squares
def main():
"""主函数"""
loop = asyncio.get_event_loop()
start, end = 1, 100
futures = asyncio.gather(prime_filter(start, end), square_mapper(start, end))
futures.add_done_callback(lambda x: print(x.result()))
loop.run_until_complete(futures)
loop.close()
if __name__ == '__main__':
main()
"""
用下面的命令运行程序并查看执行时间,例如:
time python3 example05.py
real 0m20.657s
user 1m17.749s
sys 0m0.158s
使用多进程后实际执行时间为20.657秒,而用户时间1分17.749秒约为实际执行时间的4倍
这就证明我们的程序通过多进程使用了CPU的多核特性,而且这台计算机配置了4核的CPU
"""
import concurrent.futures
import math
PRIMES = [
1116281,
1297337,
104395303,
472882027,
533000389,
817504243,
982451653,
112272535095293,
112582705942171,
112272535095293,
115280095190773,
115797848077099,
1099726899285419
] * 5
def is_prime(num):
"""判断素数"""
assert num > 0
if num % 2 == 0:
return False
for i in range(3, int(math.sqrt(num)) + 1, 2):
if num % i == 0:
return False
return num != 1
def main():
"""主函数"""
with concurrent.futures.ProcessPoolExecutor() as executor:
for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
print('%d is prime: %s' % (number, prime))
if __name__ == '__main__':
main()
aiohttp==3.5.4
aiomysql==0.0.20
asn1crypto==0.24.0
async-timeout==3.0.1
attrs==19.1.0
certifi==2019.3.9
cffi==1.12.2
chardet==3.0.4
cryptography==2.6.1
idna==2.8
multidict==4.5.2
pycparser==2.19
PyMySQL==0.9.2
requests==2.21.0
six==1.12.0
tornado==5.1.1
urllib3==1.24.1
yarl==1.3.0
<!-- chat.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tornado聊天室</title>
</head>
<body>
<h1>聊天室</h1>
<hr>
<div>
<textarea id="contents" rows="20" cols="120" readonly></textarea>
</div>
<div class="send">
<input type="text" id="content" size="50">
<input type="button" id="send" value="发送">
</div>
<p>
<a id="quit" href="javascript:void(0);">退出聊天室</a>
</p>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
$(function() {
// 将内容追加到指定的文本区
function appendContent($ta, message) {
var contents = $ta.val();
contents += '\n' + message;
$ta.val(contents);
$ta[0].scrollTop = $ta[0].scrollHeight;
}
// 通过WebSocket发送消息
function sendMessage() {
message = $('#content').val().trim();
if (message.length > 0) {
ws.send(message);
appendContent($('#contents'), '我说:' + message);
$('#content').val('');
}
}
// 创建WebSocket对象
var ws= new WebSocket('ws://localhost:8888/chat');
// 连接建立后执行的回调函数
ws.onopen = function(evt) {
$('#contents').val('~~~欢迎您进入聊天室~~~');
};
// 收到消息后执行的回调函数
ws.onmessage = function(evt) {
appendContent($('#contents'), evt.data);
};
// 为发送按钮绑定点击事件回调函数
$('#send').on('click', sendMessage);
// 为文本框绑定按下回车事件回调函数
$('#content').on('keypress', function(evt) {
keycode = evt.keyCode || evt.which;
if (keycode == 13) {
sendMessage();
}
});
// 为退出聊天室超链接绑定点击事件回调函数
$('#quit').on('click', function(evt) {
ws.close();
location.href = '/login';
});
});
</script>
</body>
</html>
<!-- login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tornado聊天室</title>
<style>
.hint { color: red; font-size: 0.8em; }
</style>
</head>
<body>
<div>
<div id="container">
<h1>进入聊天室</h1>
<hr>
<p class="hint">{{hint}}</p>
<form method="post" action="/login">
<label>昵称:</label>
<input type="text" placeholder="请输入你的昵称" name="nickname">
<button type="submit">登录</button>
</form>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>新闻列表</title>
</head>
<body>
<h1>新闻列表</h1>
<hr>
{% for news in newslist %}
<div>
<img src="{{news['picUrl']}}" alt="">
<p>{{news['title']}}</p>
</div>
{% end %}
</body>
</html>
\ No newline at end of file
/**
* admin.css
*/
/*
fixed-layout 固定头部和边栏布局
*/
html,
body {
height: 100%;
overflow: hidden;
}
ul {
margin-top: 0;
}
.admin-icon-yellow {
color: #ffbe40;
}
.admin-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1500;
font-size: 1.4rem;
margin-bottom: 0;
}
.admin-header-list a:hover :after {
content: none;
}
.admin-main {
position: relative;
height: 100%;
padding-top: 51px;
background: #f3f3f3;
}
.admin-menu {
position: fixed;
z-index: 10;
bottom: 30px;
right: 20px;
}
.admin-sidebar {
width: 260px;
min-height: 100%;
float: left;
border-right: 1px solid #cecece;
}
.admin-sidebar.am-active {
z-index: 1600;
}
.admin-sidebar-list {
margin-bottom: 0;
}
.admin-sidebar-list li a {
color: #5c5c5c;
padding-left: 24px;
}
.admin-sidebar-list li:first-child {
border-top: none;
}
.admin-sidebar-sub {
margin-top: 0;
margin-bottom: 0;
box-shadow: 0 16px 8px -15px #e2e2e2 inset;
background: #ececec;
padding-left: 24px;
}
.admin-sidebar-sub li:first-child {
border-top: 1px solid #dedede;
}
.admin-sidebar-panel {
margin: 10px;
}
.admin-content {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
background: #fff;
}
.admin-content,
.admin-sidebar {
height: 100%;
overflow-x: hidden;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
}
.admin-content-body {
-webkit-box-flex: 1;
-webkit-flex: 1 0 auto;
-ms-flex: 1 0 auto;
flex: 1 0 auto;
}
.admin-content-footer {
font-size: 85%;
color: #777;
}
.admin-content-list {
border: 1px solid #e9ecf1;
margin-top: 0;
}
.admin-content-list li {
border: 1px solid #e9ecf1;
border-width: 0 1px;
margin-left: -1px;
}
.admin-content-list li:first-child {
border-left: none;
}
.admin-content-list li:last-child {
border-right: none;
}
.admin-content-table a {
color: #535353;
}
.admin-content-file {
margin-bottom: 0;
color: #666;
}
.admin-content-file p {
margin: 0 0 5px 0;
font-size: 1.4rem;
}
.admin-content-file li {
padding: 10px 0;
}
.admin-content-file li:first-child {
border-top: none;
}
.admin-content-file li:last-child {
border-bottom: none;
}
.admin-content-file li .am-progress {
margin-bottom: 4px;
}
.admin-content-file li .am-progress-bar {
line-height: 14px;
}
.admin-content-task {
margin-bottom: 0;
}
.admin-content-task li {
padding: 5px 0;
border-color: #eee;
}
.admin-content-task li:first-child {
border-top: none;
}
.admin-content-task li:last-child {
border-bottom: none;
}
.admin-task-meta {
font-size: 1.2rem;
color: #999;
}
.admin-task-bd {
font-size: 1.4rem;
margin-bottom: 5px;
}
.admin-content-comment {
margin-bottom: 0;
}
.admin-content-comment .am-comment-bd {
font-size: 1.4rem;
}
.admin-content-pagination {
margin-bottom: 0;
}
.admin-content-pagination li a {
padding: 4px 8px;
}
@media only screen and (min-width: 641px) {
.admin-sidebar {
display: block;
position: static;
background: none;
}
.admin-offcanvas-bar {
position: static;
width: auto;
background: none;
-webkit-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
overflow-y: visible;
min-height: 100%;
}
.admin-offcanvas-bar:after {
content: none;
}
}
@media only screen and (max-width: 640px) {
.admin-sidebar {
width: inherit;
}
.admin-offcanvas-bar {
background: #f3f3f3;
}
.admin-offcanvas-bar:after {
background: #BABABA;
}
.admin-sidebar-list a:hover, .admin-sidebar-list a:active{
-webkit-transition: background-color .3s ease;
-moz-transition: background-color .3s ease;
-ms-transition: background-color .3s ease;
-o-transition: background-color .3s ease;
transition: background-color .3s ease;
background: #E4E4E4;
}
.admin-content-list li {
padding: 10px;
border-width: 1px 0;
margin-top: -1px;
}
.admin-content-list li:first-child {
border-top: none;
}
.admin-content-list li:last-child {
border-bottom: none;
}
.admin-form-text {
text-align: left !important;
}
}
/*
* user.html css
*/
.user-info {
margin-bottom: 15px;
}
.user-info .am-progress {
margin-bottom: 4px;
}
.user-info p {
margin: 5px;
}
.user-info-order {
font-size: 1.4rem;
}
/*
* errorLog.html css
*/
.error-log .am-pre-scrollable {
max-height: 40rem;
}
/*
* table.html css
*/
.table-main {
font-size: 1.4rem;
padding: .5rem;
}
.table-main button {
background: #fff;
}
.table-check {
width: 30px;
}
.table-id {
width: 50px;
}
@media only screen and (max-width: 640px) {
.table-select {
margin-top: 10px;
margin-left: 5px;
}
}
/*
gallery.html css
*/
.gallery-list li {
padding: 10px;
}
.gallery-list a {
color: #666;
}
.gallery-list a:hover {
color: #3bb4f2;
}
.gallery-title {
margin-top: 6px;
font-size: 1.4rem;
}
.gallery-desc {
font-size: 1.2rem;
margin-top: 4px;
}
/*
404.html css
*/
.page-404 {
background: #fff;
border: none;
width: 200px;
margin: 0 auto;
}
.am-datatable-hd{margin-bottom:10px}.am-datatable-hd label{font-weight:400}.am-datatable-filter{text-align:right}.am-datatable-filter input{margin-left:.5em}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_desc_disabled{cursor:pointer;position:relative}table.dataTable thead .sorting:after{position:absolute;top:50%;margin-top:-12px;right:8px;display:block;font-weight:400}table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after{position:absolute;top:50%;margin-top:-12px;right:8px;display:block;opacity:.5;font-weight:400}table.dataTable thead .sorting:after{opacity:.2;content:"\f0dc"}table.dataTable thead .sorting_asc:after{content:"\f15d"}table.dataTable thead .sorting_desc:after{content:"\f15e"}div.DTFC_LeftBodyWrapper table.dataTable thead .sorting:after,div.DTFC_LeftBodyWrapper table.dataTable thead .sorting_asc:after,div.DTFC_LeftBodyWrapper table.dataTable thead .sorting_desc:after,div.DTFC_RightBodyWrapper table.dataTable thead .sorting:after,div.DTFC_RightBodyWrapper table.dataTable thead .sorting_asc:after,div.DTFC_RightBodyWrapper table.dataTable thead .sorting_desc:after,div.dataTables_scrollBody table.dataTable thead .sorting:after,div.dataTables_scrollBody table.dataTable thead .sorting_asc:after,div.dataTables_scrollBody table.dataTable thead .sorting_desc:after{display:none}table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{color:#eee}table.dataTable thead>tr>th{padding-right:30px}table.dataTable th:active{outline:none}table.dataTable.table-condensed thead>tr>th{padding-right:20px}table.dataTable.table-condensed thead .sorting:after,table.dataTable.table-condensed thead .sorting_asc:after,table.dataTable.table-condensed thead .sorting_desc:after{top:6px;right:6px}div.dataTables_scrollHead table{margin-bottom:0!important;border-bottom-left-radius:0;border-bottom-right-radius:0}div.DTFC_LeftHeadWrapper table thead tr:last-child td:first-child,div.DTFC_LeftHeadWrapper table thead tr:last-child th:first-child,div.DTFC_RightHeadWrapper table thead tr:last-child td:first-child,div.DTFC_RightHeadWrapper table thead tr:last-child th:first-child,div.dataTables_scrollHead table thead tr:last-child td:first-child,div.dataTables_scrollHead table thead tr:last-child th:first-child{border-bottom-left-radius:0!important;border-bottom-right-radius:0!important}div.dataTables_scrollBody table{border-top:none;margin-top:0!important;margin-bottom:0!important}div.DTFC_LeftBodyWrapper tbody tr:first-child td,div.DTFC_LeftBodyWrapper tbody tr:first-child th,div.DTFC_RightBodyWrapper tbody tr:first-child td,div.DTFC_RightBodyWrapper tbody tr:first-child th,div.dataTables_scrollBody tbody tr:first-child td,div.dataTables_scrollBody tbody tr:first-child th{border-top:none}div.dataTables_scrollFoot table{margin-top:0!important;border-top:none}table.table-bordered.dataTable{border-collapse:separate!important}table.table-bordered thead td,table.table-bordered thead th{border-left-width:0;border-top-width:0}table.table-bordered tbody td,table.table-bordered tbody th,table.table-bordered tfoot td,table.table-bordered tfoot th{border-left-width:0;border-bottom-width:0}table.table-bordered td:last-child,table.table-bordered th:last-child{border-right-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}.table.dataTable tbody tr.active td,.table.dataTable tbody tr.active th{background-color:#08c;color:#fff}.table.dataTable tbody tr.active:hover td,.table.dataTable tbody tr.active:hover th{background-color:#0075b0!important}.table.dataTable tbody tr.active td>a,.table.dataTable tbody tr.active th>a{color:#fff}.table-striped.dataTable tbody tr.active:nth-child(odd) td,.table-striped.dataTable tbody tr.active:nth-child(odd) th{background-color:#017ebc}table.DTTT_selectable tbody tr{cursor:pointer}div.DTTT .btn:hover{text-decoration:none!important}ul.DTTT_dropdown.dropdown-menu{z-index:2003}ul.DTTT_dropdown.dropdown-menu a{color:#333!important}ul.DTTT_dropdown.dropdown-menu li{position:relative}ul.DTTT_dropdown.dropdown-menu li:hover a{background-color:#08c;color:#fff!important}div.DTTT_collection_background{z-index:2002}div.DTTT_print_info,div.dataTables_processing{top:50%;left:50%;text-align:center;background-color:#fff}div.DTTT_print_info{color:#333;padding:10px 30px;opacity:.95;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0,0,0,.5);box-shadow:0 3px 7px rgba(0,0,0,.5);position:fixed;width:400px;height:150px;margin-left:-200px;margin-top:-75px}div.DTTT_print_info h6{font-weight:400;font-size:28px;line-height:28px;margin:1em}div.DTTT_print_info p{font-size:14px;line-height:20px}div.dataTables_processing{position:absolute;width:100%;height:60px;margin-left:-50%;margin-top:-25px;padding-top:20px;padding-bottom:20px;font-size:1.2em;background:-webkit-gradient(linear,left top,right top,color-stop(0%,rgba(255,255,255,0)),color-stop(25%,rgba(255,255,255,.9)),color-stop(75%,rgba(255,255,255,.9)),color-stop(100%,rgba(255,255,255,0)));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0%,rgba(255,255,255,.9) 25%,rgba(255,255,255,.9) 75%,rgba(255,255,255,0) 100%);background:-webkit-gradient(linear,left top,right top,from(rgba(255,255,255,0)),color-stop(25%,rgba(255,255,255,.9)),color-stop(75%,rgba(255,255,255,.9)),to(rgba(255,255,255,0)));background:linear-gradient(to right,rgba(255,255,255,0) 0%,rgba(255,255,255,.9) 25%,rgba(255,255,255,.9) 75%,rgba(255,255,255,0) 100%)}div.DTFC_LeftHeadWrapper table{background-color:#fff}div.DTFC_LeftFootWrapper table{background-color:#fff;margin-bottom:0}div.DTFC_RightHeadWrapper table{background-color:#fff}div.DTFC_RightFootWrapper table,table.DTFC_Cloned tr.even{background-color:#fff;margin-bottom:0}div.DTFC_LeftHeadWrapper table,div.DTFC_RightHeadWrapper table{border-bottom:none!important;margin-bottom:0!important;border-top-right-radius:0!important;border-bottom-left-radius:0!important;border-bottom-right-radius:0!important}div.DTFC_LeftBodyWrapper table,div.DTFC_RightBodyWrapper table{border-top:none;margin:0!important}div.DTFC_LeftFootWrapper table,div.DTFC_RightFootWrapper table{border-top:none;margin-top:0!important}div.FixedHeader_Cloned table{margin:0!important}.am-datatable-pager{margin-top:0;margin-bottom:0}.am-datatable-info{padding-top:6px;color:#555;font-size:1.4rem}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child:before{top:8px;left:4px;height:16px;width:16px;display:block;position:absolute;color:#fff;border:2px solid #fff;border-radius:16px;text-align:center;line-height:14px;-webkit-box-shadow:0 0 3px #444;box-shadow:0 0 3px #444;-webkit-box-sizing:content-box;box-sizing:content-box;content:'+';background-color:#31b131}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child.dataTables_empty:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child.dataTables_empty:before{display:none}table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th:first-child:before{content:'-';background-color:#d33333}table.dataTable.dtr-inline.collapsed>tbody>tr.child td:before{display:none}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child{padding-left:27px}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child:before{top:5px;left:4px;height:14px;width:14px;border-radius:14px;line-height:12px}table.dataTable.dtr-column>tbody>tr>td.control,table.dataTable.dtr-column>tbody>tr>th.control{position:relative;cursor:pointer}table.dataTable.dtr-column>tbody>tr>td.control:before,table.dataTable.dtr-column>tbody>tr>th.control:before{top:50%;left:50%;height:16px;width:16px;margin-top:-10px;margin-left:-10px;display:block;position:absolute;color:#fff;border:2px solid #fff;border-radius:16px;text-align:center;line-height:14px;-webkit-box-shadow:0 0 3px #666;box-shadow:0 0 3px #666;-webkit-box-sizing:content-box;box-sizing:content-box;content:'+';background-color:#5eb95e}table.dataTable.dtr-column>tbody>tr.parent td.control:before,table.dataTable.dtr-column>tbody>tr.parent th.control:before{content:'-';background-color:#dd514c}table.dataTable>tbody>tr.child{padding:.5em 1em}table.dataTable>tbody>tr.child:hover{background:0 0!important}table.dataTable>tbody>tr.child ul{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable>tbody>tr.child ul li{border-bottom:1px solid #efefef;padding:.5em 0}table.dataTable>tbody>tr.child ul li:first-child{padding-top:0}table.dataTable>tbody>tr.child ul li:last-child{border-bottom:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:700}
\ No newline at end of file
因为 它太大了无法显示 source diff 。你可以改为 查看blob
/*!
* FullCalendar v0.0.0 Stylesheet
* Docs & License: http://fullcalendar.io/
* (c) 2016 Adam Shaw
*/.fc-icon,body .fc{font-size:1em}.fc-button-group,.fc-icon{display:inline-block}.fc-bg,.fc-row .fc-bgevent-skeleton,.fc-row .fc-highlight-skeleton{bottom:0}.fc-icon,.fc-unselectable{-khtml-user-select:none;-webkit-touch-callout:none}.fc{direction:ltr;text-align:left}.fc-rtl{text-align:right}.fc th,.fc-basic-view td.fc-week-number,.fc-icon,.fc-toolbar{text-align:center}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-list-view,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#ddd}.fc-unthemed .fc-popover{background-color:#fff}.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-popover .fc-header{background:#eee}.fc-unthemed .fc-popover .fc-header .fc-close{color:#666}.fc-unthemed .fc-today{background:#fcf8e3}.fc-highlight{background:#bce8f1;opacity:.3}.fc-bgevent{background:#8fdf82;opacity:.3}.fc-nonbusiness{background:#d7d7d7}.fc-icon{height:1em;line-height:1em;overflow:hidden;font-family:"Courier New",Courier,monospace;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.fc-icon:after{position:relative}.fc-icon-left-single-arrow:after{content:"\02039";font-weight:700;font-size:200%;top:-7%}.fc-icon-right-single-arrow:after{content:"\0203A";font-weight:700;font-size:200%;top:-7%}.fc-icon-left-double-arrow:after{content:"\000AB";font-size:160%;top:-7%}.fc-icon-right-double-arrow:after{content:"\000BB";font-size:160%;top:-7%}.fc-icon-left-triangle:after{content:"\25C4";font-size:125%;top:3%}.fc-icon-right-triangle:after{content:"\25BA";font-size:125%;top:3%}.fc-icon-down-triangle:after{content:"\25BC";font-size:125%;top:2%}.fc-icon-x:after{content:"\000D7";font-size:200%;top:6%}.fc button{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;height:2.1em;padding:0 .6em;font-size:1em;white-space:nowrap;cursor:pointer}.fc button::-moz-focus-inner{margin:0;padding:0}.fc-state-default{border:1px solid;background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);color:#333;text-shadow:0 1px 1px rgba(255,255,255,.75);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05)}.fc-state-default.fc-corner-left{border-top-left-radius:4px;border-bottom-left-radius:4px}.fc-state-default.fc-corner-right{border-top-right-radius:4px;border-bottom-right-radius:4px}.fc button .fc-icon{position:relative;top:-.05em;margin:0 .2em;vertical-align:middle}.fc-state-active,.fc-state-disabled,.fc-state-down,.fc-state-hover{color:#333;background-color:#e6e6e6}.fc-state-hover{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.fc-state-active,.fc-state-down{background-color:#ccc;background-image:none;box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.fc-state-disabled{cursor:default;background-image:none;opacity:.65;box-shadow:none}.fc-event.fc-draggable,.fc-event[href],.fc-popover .fc-header .fc-close,a[data-goto]{cursor:pointer}.fc .fc-button-group>*{float:left;margin:0 0 0 -1px}.fc .fc-button-group>:first-child{margin-left:0}.fc-popover{position:absolute;box-shadow:0 2px 6px rgba(0,0,0,.15)}.fc-popover .fc-header{padding:2px 4px}.fc-popover .fc-header .fc-title{margin:0 2px}.fc-ltr .fc-popover .fc-header .fc-title,.fc-rtl .fc-popover .fc-header .fc-close{float:left}.fc-ltr .fc-popover .fc-header .fc-close,.fc-rtl .fc-popover .fc-header .fc-title{float:right}.fc-unthemed .fc-popover{border-width:1px;border-style:solid}.fc-unthemed .fc-popover .fc-header .fc-close{font-size:.9em;margin-top:2px}.fc-popover>.ui-widget-header+.ui-widget-content{border-top:0}.fc-divider{border-style:solid;border-width:1px}hr.fc-divider{height:0;margin:0;padding:0 0 2px;border-width:1px 0}.fc-bg table,.fc-row .fc-bgevent-skeleton table,.fc-row .fc-highlight-skeleton table{height:100%}.fc-clear{clear:both}.fc-bg,.fc-bgevent-skeleton,.fc-helper-skeleton,.fc-highlight-skeleton{position:absolute;top:0;left:0;right:0}.fc table{width:100%;box-sizing:border-box;table-layout:fixed;border-collapse:collapse;border-spacing:0;font-size:1em}.fc td,.fc th{border-style:solid;border-width:1px;padding:0;vertical-align:top}.fc td.fc-today{border-style:double}a[data-goto]:hover{text-decoration:underline}.fc .fc-row{border-style:solid;border-width:0}.fc-row table{border-left:0 hidden transparent;border-right:0 hidden transparent;border-bottom:0 hidden transparent}.fc-row:first-child table{border-top:0 hidden transparent}.fc-row{position:relative}.fc-row .fc-bg{z-index:1}.fc-row .fc-bgevent-skeleton td,.fc-row .fc-highlight-skeleton td{border-color:transparent}.fc-row .fc-bgevent-skeleton{z-index:2}.fc-row .fc-highlight-skeleton{z-index:3}.fc-row .fc-content-skeleton{position:relative;z-index:4;padding-bottom:2px}.fc-row .fc-helper-skeleton{z-index:5}.fc-row .fc-content-skeleton td,.fc-row .fc-helper-skeleton td{background:0 0;border-color:transparent;border-bottom:0}.fc-row .fc-content-skeleton tbody td,.fc-row .fc-helper-skeleton tbody td{border-top:0}.fc-scroller{-webkit-overflow-scrolling:touch}.fc-row.fc-rigid,.fc-time-grid-event{overflow:hidden}.fc-scroller>.fc-day-grid,.fc-scroller>.fc-time-grid{position:relative;width:100%}.fc-event{position:relative;display:block;font-size:.85em;line-height:1.3;border-radius:3px;border:1px solid #3a87ad;font-weight:400}.fc-event,.fc-event-dot{background-color:#3a87ad}.fc-event,.fc-event:hover,.ui-widget .fc-event{color:#fff;text-decoration:none}.fc-not-allowed,.fc-not-allowed .fc-event{cursor:not-allowed}.fc-event .fc-bg{z-index:1;background:#fff;opacity:.25}.fc-event .fc-content{position:relative;z-index:2}.fc-event .fc-resizer{position:absolute;z-index:4;display:none}.fc-event.fc-allow-mouse-resize .fc-resizer,.fc-event.fc-selected .fc-resizer{display:block}.fc-event.fc-selected .fc-resizer:before{content:"";position:absolute;z-index:9999;top:50%;left:50%;width:40px;height:40px;margin-left:-20px;margin-top:-20px}.fc-event.fc-selected{z-index:9999!important;box-shadow:0 2px 5px rgba(0,0,0,.2)}.fc-event.fc-selected.fc-dragging{box-shadow:0 2px 7px rgba(0,0,0,.3)}.fc-h-event.fc-selected:before{content:"";position:absolute;z-index:3;top:-10px;bottom:-10px;left:0;right:0}.fc-ltr .fc-h-event.fc-not-start,.fc-rtl .fc-h-event.fc-not-end{margin-left:0;border-left-width:0;padding-left:1px;border-top-left-radius:0;border-bottom-left-radius:0}.fc-ltr .fc-h-event.fc-not-end,.fc-rtl .fc-h-event.fc-not-start{margin-right:0;border-right-width:0;padding-right:1px;border-top-right-radius:0;border-bottom-right-radius:0}.fc-ltr .fc-h-event .fc-start-resizer,.fc-rtl .fc-h-event .fc-end-resizer{cursor:w-resize;left:-1px}.fc-ltr .fc-h-event .fc-end-resizer,.fc-rtl .fc-h-event .fc-start-resizer{cursor:e-resize;right:-1px}.fc-h-event.fc-allow-mouse-resize .fc-resizer{width:7px;top:-1px;bottom:-1px}.fc-h-event.fc-selected .fc-resizer{border-radius:4px;border-width:1px;width:6px;height:6px;border-style:solid;border-color:inherit;background:#fff;top:50%;margin-top:-4px}.fc-ltr .fc-h-event.fc-selected .fc-start-resizer,.fc-rtl .fc-h-event.fc-selected .fc-end-resizer{margin-left:-4px}.fc-ltr .fc-h-event.fc-selected .fc-end-resizer,.fc-rtl .fc-h-event.fc-selected .fc-start-resizer{margin-right:-4px}.fc-day-grid-event{margin:1px 2px 0;padding:0 1px}tr:first-child>td>.fc-day-grid-event{margin-top:2px}.fc-day-grid-event.fc-selected:after{content:"";position:absolute;z-index:1;top:-1px;right:-1px;bottom:-1px;left:-1px;background:#000;opacity:.25}.fc-day-grid-event .fc-content{white-space:nowrap;overflow:hidden}.fc-day-grid-event .fc-time{font-weight:700}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer{margin-left:-2px}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer{margin-right:-2px}a.fc-more{margin:1px 3px;font-size:.85em;cursor:pointer;text-decoration:none}a.fc-more:hover{text-decoration:underline}.fc-limited{display:none}.fc-day-grid .fc-row{z-index:1}.fc-more-popover{z-index:2;width:220px}.fc-more-popover .fc-event-container{padding:10px}.fc-now-indicator{position:absolute;border:0 solid red}.fc-unselectable{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}.fc-toolbar{margin-bottom:1em}.fc-toolbar .fc-left{float:left}.fc-toolbar .fc-right{float:right}.fc-toolbar .fc-center{display:inline-block}.fc .fc-toolbar>*>*{float:left;margin-left:.75em}.fc .fc-toolbar>*>:first-child{margin-left:0}.fc-toolbar h2{margin:0}.fc-toolbar button{position:relative}.fc-toolbar .fc-state-hover,.fc-toolbar .ui-state-hover{z-index:2}.fc-toolbar .fc-state-down{z-index:3}.fc-toolbar .fc-state-active,.fc-toolbar .ui-state-active{z-index:4}.fc-toolbar button:focus{z-index:5}.fc-view-container *,.fc-view-container :after,.fc-view-container :before{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.fc-view,.fc-view>table{position:relative;z-index:1}.fc-basicDay-view .fc-content-skeleton,.fc-basicWeek-view .fc-content-skeleton{padding-bottom:1em}.fc-basic-view .fc-body .fc-row{min-height:4em}.fc-row.fc-rigid .fc-content-skeleton{position:absolute;top:0;left:0;right:0}.fc-day-top.fc-other-month{opacity:.3}.fc-basic-view .fc-day-number,.fc-basic-view .fc-week-number{padding:2px}.fc-basic-view th.fc-day-number,.fc-basic-view th.fc-week-number{padding:0 2px}.fc-ltr .fc-basic-view .fc-day-top .fc-day-number{float:right}.fc-rtl .fc-basic-view .fc-day-top .fc-day-number{float:left}.fc-ltr .fc-basic-view .fc-day-top .fc-week-number{float:left;border-radius:0 0 3px}.fc-rtl .fc-basic-view .fc-day-top .fc-week-number{float:right;border-radius:0 0 0 3px}.fc-basic-view .fc-day-top .fc-week-number{min-width:1.5em;text-align:center;background-color:#f2f2f2;color:grey}.fc-basic-view td.fc-week-number>*{display:inline-block;min-width:1.25em}.fc-agenda-view .fc-day-grid{position:relative;z-index:2}.fc-agenda-view .fc-day-grid .fc-row{min-height:3em}.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton{padding-bottom:1em}.fc .fc-axis{vertical-align:middle;padding:0 4px;white-space:nowrap}.fc-ltr .fc-axis{text-align:right}.fc-rtl .fc-axis{text-align:left}.ui-widget td.fc-axis{font-weight:400}.fc-time-grid,.fc-time-grid-container{position:relative;z-index:1}.fc-time-grid{min-height:100%}.fc-time-grid table{border:0 hidden transparent}.fc-time-grid>.fc-bg{z-index:1}.fc-time-grid .fc-slats,.fc-time-grid>hr{position:relative;z-index:2}.fc-time-grid .fc-content-col{position:relative}.fc-time-grid .fc-content-skeleton{position:absolute;z-index:3;top:0;left:0;right:0}.fc-time-grid .fc-business-container{position:relative;z-index:1}.fc-time-grid .fc-bgevent-container{position:relative;z-index:2}.fc-time-grid .fc-highlight-container{z-index:3;position:relative}.fc-time-grid .fc-event-container{position:relative;z-index:4}.fc-time-grid .fc-now-indicator-line{z-index:5}.fc-time-grid .fc-helper-container{position:relative;z-index:6}.fc-time-grid .fc-slats td{height:1.5em;border-bottom:0}.fc-time-grid .fc-slats .fc-minor td{border-top-style:dotted}.fc-time-grid .fc-slats .ui-widget-content{background:0 0}.fc-time-grid .fc-highlight{position:absolute;left:0;right:0}.fc-ltr .fc-time-grid .fc-event-container{margin:0 2.5% 0 2px}.fc-rtl .fc-time-grid .fc-event-container{margin:0 2px 0 2.5%}.fc-time-grid .fc-bgevent,.fc-time-grid .fc-event{position:absolute;z-index:1}.fc-time-grid .fc-bgevent{left:0;right:0}.fc-v-event.fc-not-start{border-top-width:0;padding-top:1px;border-top-left-radius:0;border-top-right-radius:0}.fc-v-event.fc-not-end{border-bottom-width:0;padding-bottom:1px;border-bottom-left-radius:0;border-bottom-right-radius:0}.fc-time-grid-event.fc-selected{overflow:visible}.fc-time-grid-event.fc-selected .fc-bg{display:none}.fc-time-grid-event .fc-content{overflow:hidden}.fc-time-grid-event .fc-time,.fc-time-grid-event .fc-title{padding:0 1px}.fc-time-grid-event .fc-time{font-size:.85em;white-space:nowrap}.fc-time-grid-event.fc-short .fc-content{white-space:nowrap}.fc-time-grid-event.fc-short .fc-time,.fc-time-grid-event.fc-short .fc-title{display:inline-block;vertical-align:top}.fc-time-grid-event.fc-short .fc-time span{display:none}.fc-time-grid-event.fc-short .fc-time:before{content:attr(data-start)}.fc-time-grid-event.fc-short .fc-time:after{content:"\000A0-\000A0"}.fc-time-grid-event.fc-short .fc-title{font-size:.85em;padding:0}.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer{left:0;right:0;bottom:0;height:8px;overflow:hidden;line-height:8px;font-size:11px;font-family:monospace;text-align:center;cursor:s-resize}.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after{content:"="}.fc-time-grid-event.fc-selected .fc-resizer{border-radius:5px;border-width:1px;width:8px;height:8px;border-style:solid;border-color:inherit;background:#fff;left:50%;margin-left:-5px;bottom:-5px}.fc-time-grid .fc-now-indicator-line{border-top-width:1px;left:0;right:0}.fc-time-grid .fc-now-indicator-arrow{margin-top:-5px}.fc-ltr .fc-time-grid .fc-now-indicator-arrow{left:0;border-width:5px 0 5px 6px;border-top-color:transparent;border-bottom-color:transparent}.fc-rtl .fc-time-grid .fc-now-indicator-arrow{right:0;border-width:5px 6px 5px 0;border-top-color:transparent;border-bottom-color:transparent}.fc-event-dot{display:inline-block;width:10px;height:10px;border-radius:5px}.fc-rtl .fc-list-view{direction:rtl}.fc-list-view{border-width:1px;border-style:solid}.fc .fc-list-table{table-layout:auto}.fc-list-table td{border-width:1px 0 0;padding:8px 14px}.fc-list-table tr:first-child td{border-top-width:0}.fc-list-heading{border-bottom-width:1px}.fc-list-heading td{font-weight:700}.fc-ltr .fc-list-heading-main{float:left}.fc-ltr .fc-list-heading-alt,.fc-rtl .fc-list-heading-main{float:right}.fc-rtl .fc-list-heading-alt{float:left}.fc-list-item.fc-has-url{cursor:pointer}.fc-list-item:hover td{background-color:#f5f5f5}.fc-list-item-marker,.fc-list-item-time{white-space:nowrap;width:1px}.fc-ltr .fc-list-item-marker{padding-right:0}.fc-rtl .fc-list-item-marker{padding-left:0}.fc-list-item-title a{text-decoration:none;color:inherit}.fc-list-item-title a[href]:hover{text-decoration:underline}.fc-list-empty-wrap2{position:absolute;top:0;left:0;right:0;bottom:0}.fc-list-empty-wrap1{width:100%;height:100%;display:table}.fc-list-empty{display:table-cell;vertical-align:middle;text-align:center}.fc-unthemed .fc-list-empty{background-color:#eee}
\ No newline at end of file
/*!
* FullCalendar v0.0.0 Print Stylesheet
* Docs & License: http://fullcalendar.io/
* (c) 2016 Adam Shaw
*/
/*
* Include this stylesheet on your page to get a more printer-friendly calendar.
* When including this stylesheet, use the media='print' attribute of the <link> tag.
* Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css.
*/
.fc {
max-width: 100% !important;
}
/* Global Event Restyling
--------------------------------------------------------------------------------------------------*/
.fc-event {
background: #fff !important;
color: #000 !important;
page-break-inside: avoid;
}
.fc-event .fc-resizer {
display: none;
}
/* Table & Day-Row Restyling
--------------------------------------------------------------------------------------------------*/
.fc th,
.fc td,
.fc hr,
.fc thead,
.fc tbody,
.fc-row {
border-color: #ccc !important;
background: #fff !important;
}
/* kill the overlaid, absolutely-positioned components */
/* common... */
.fc-bg,
.fc-bgevent-skeleton,
.fc-highlight-skeleton,
.fc-helper-skeleton,
/* for timegrid. within cells within table skeletons... */
.fc-bgevent-container,
.fc-business-container,
.fc-highlight-container,
.fc-helper-container {
display: none;
}
/* don't force a min-height on rows (for DayGrid) */
.fc tbody .fc-row {
height: auto !important; /* undo height that JS set in distributeHeight */
min-height: 0 !important; /* undo the min-height from each view's specific stylesheet */
}
.fc tbody .fc-row .fc-content-skeleton {
position: static; /* undo .fc-rigid */
padding-bottom: 0 !important; /* use a more border-friendly method for this... */
}
.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td { /* only works in newer browsers */
padding-bottom: 1em; /* ...gives space within the skeleton. also ensures min height in a way */
}
.fc tbody .fc-row .fc-content-skeleton table {
/* provides a min-height for the row, but only effective for IE, which exaggerates this value,
making it look more like 3em. for other browers, it will already be this tall */
height: 1em;
}
/* Undo month-view event limiting. Display all events and hide the "more" links
--------------------------------------------------------------------------------------------------*/
.fc-more-cell,
.fc-more {
display: none !important;
}
.fc tr.fc-limited {
display: table-row !important;
}
.fc td.fc-limited {
display: table-cell !important;
}
.fc-popover {
display: none; /* never display the "more.." popover in print mode */
}
/* TimeGrid Restyling
--------------------------------------------------------------------------------------------------*/
/* undo the min-height 100% trick used to fill the container's height */
.fc-time-grid {
min-height: 0 !important;
}
/* don't display the side axis at all ("all-day" and time cells) */
.fc-agenda-view .fc-axis {
display: none;
}
/* don't display the horizontal lines */
.fc-slats,
.fc-time-grid hr { /* this hr is used when height is underused and needs to be filled */
display: none !important; /* important overrides inline declaration */
}
/* let the container that holds the events be naturally positioned and create real height */
.fc-time-grid .fc-content-skeleton {
position: static;
}
/* in case there are no events, we still want some height */
.fc-time-grid .fc-content-skeleton table {
height: 4em;
}
/* kill the horizontal spacing made by the event container. event margins will be done below */
.fc-time-grid .fc-event-container {
margin: 0 !important;
}
/* TimeGrid *Event* Restyling
--------------------------------------------------------------------------------------------------*/
/* naturally position events, vertically stacking them */
.fc-time-grid .fc-event {
position: static !important;
margin: 3px 2px !important;
}
/* for events that continue to a future day, give the bottom border back */
.fc-time-grid .fc-event.fc-not-end {
border-bottom-width: 1px !important;
}
/* indicate the event continues via "..." text */
.fc-time-grid .fc-event.fc-not-end:after {
content: "...";
}
/* for events that are continuations from previous days, give the top border back */
.fc-time-grid .fc-event.fc-not-start {
border-top-width: 1px !important;
}
/* indicate the event is a continuation via "..." text */
.fc-time-grid .fc-event.fc-not-start:before {
content: "...";
}
/* time */
/* undo a previous declaration and let the time text span to a second line */
.fc-time-grid .fc-event .fc-time {
white-space: normal !important;
}
/* hide the the time that is normally displayed... */
.fc-time-grid .fc-event .fc-time span {
display: none;
}
/* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */
.fc-time-grid .fc-event .fc-time:after {
content: attr(data-full);
}
/* Vertical Scroller & Containers
--------------------------------------------------------------------------------------------------*/
/* kill the scrollbars and allow natural height */
.fc-scroller,
.fc-day-grid-container, /* these divs might be assigned height, which we need to cleared */
.fc-time-grid-container { /* */
overflow: visible !important;
height: auto !important;
}
/* kill the horizontal border/padding used to compensate for scrollbars */
.fc-row {
border: 0 !important;
margin: 0 !important;
}
/* Button Controls
--------------------------------------------------------------------------------------------------*/
.fc-button-group,
.fc button {
display: none; /* don't display any button-related controls */
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Amaze UI Admin index Examples</title>
<meta name="description" content="这是一个 index 页面">
<meta name="keywords" content="index">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="renderer" content="webkit">
<meta http-equiv="Cache-Control" content="no-siteapp"/>
<link rel="icon" type="image/png" href="../i/favicon.png">
<link rel="apple-touch-icon-precomposed" href="../i/app-icon72x72@2x.png">
<meta name="apple-mobile-web-app-title" content="Amaze UI"/>
<script src="../js/echarts.min.js"></script>
<link rel="stylesheet" href="../css/amazeui.min.css"/>
<link rel="stylesheet" href="../css/amazeui.datatables.min.css"/>
<link rel="stylesheet" href="../css/app.css">
<script src="../js/jquery.min.js"></script>
</head>
<body data-type="widgets">
<script src="../js/theme.js"></script>
<div class="am-g tpl-g">
<!-- 头部 -->
<header>
<!-- logo -->
<div class="am-fl tpl-header-logo">
<a href="javascript:;"><img src="../img/logo.png" alt=""></a>
</div>
<!-- 右侧内容 -->
<div class="tpl-header-fluid">
<!-- 侧边切换 -->
<div class="am-fl tpl-header-switch-button am-icon-list">
<span>
</span>
</div>
<!-- 搜索 -->
<div class="am-fl tpl-header-search">
<form class="tpl-header-search-form" action="javascript:;">
<button class="tpl-header-search-btn am-icon-search"></button>
<input class="tpl-header-search-box" type="text" placeholder="搜索内容...">
</form>
</div>
<!-- 其它功能-->
<div class="am-fr tpl-header-navbar">
<ul>
<!-- 欢迎语 -->
<li class="am-text-sm tpl-header-navbar-welcome">
<a href="javascript:;">欢迎你, <span>Amaze UI</span> </a>
</li>
<!-- 新邮件 -->
<li class="am-dropdown tpl-dropdown" data-am-dropdown>
<a href="javascript:;" class="am-dropdown-toggle tpl-dropdown-toggle" data-am-dropdown-toggle>
<i class="am-icon-envelope"></i>
<span class="am-badge am-badge-success am-round item-feed-badge">4</span>
</a>
<!-- 弹出列表 -->
<ul class="am-dropdown-content tpl-dropdown-content">
<li class="tpl-dropdown-menu-messages">
<a href="javascript:;" class="tpl-dropdown-menu-messages-item am-cf">
<div class="menu-messages-ico">
<img src="../img/user04.png" alt="">
</div>
<div class="menu-messages-time">
3小时前
</div>
<div class="menu-messages-content">
<div class="menu-messages-content-title">
<i class="am-icon-circle-o am-text-success"></i>
<span>夕风色</span>
</div>
<div class="am-text-truncate"> Amaze UI 的诞生,依托于 GitHub 及其他技术社区上一些优秀的资源;Amaze UI
的成长,则离不开用户的支持。
</div>
<div class="menu-messages-content-time">2016-09-21 下午 16:40</div>
</div>
</a>
</li>
<li class="tpl-dropdown-menu-messages">
<a href="javascript:;" class="tpl-dropdown-menu-messages-item am-cf">
<div class="menu-messages-ico">
<img src="../img/user02.png" alt="">
</div>
<div class="menu-messages-time">
5天前
</div>
<div class="menu-messages-content">
<div class="menu-messages-content-title">
<i class="am-icon-circle-o am-text-warning"></i>
<span>禁言小张</span>
</div>
<div class="am-text-truncate"> 为了能最准确的传达所描述的问题, 建议你在反馈时附上演示,方便我们理解。</div>
<div class="menu-messages-content-time">2016-09-16 上午 09:23</div>
</div>
</a>
</li>
<li class="tpl-dropdown-menu-messages">
<a href="javascript:;" class="tpl-dropdown-menu-messages-item am-cf">
<i class="am-icon-circle-o"></i> 进入列表…
</a>
</li>
</ul>
</li>
<!-- 新提示 -->
<li class="am-dropdown" data-am-dropdown>
<a href="javascript:;" class="am-dropdown-toggle" data-am-dropdown-toggle>
<i class="am-icon-bell"></i>
<span class="am-badge am-badge-warning am-round item-feed-badge">5</span>
</a>
<!-- 弹出列表 -->
<ul class="am-dropdown-content tpl-dropdown-content">
<li class="tpl-dropdown-menu-notifications">
<a href="javascript:;" class="tpl-dropdown-menu-notifications-item am-cf">
<div class="tpl-dropdown-menu-notifications-title">
<i class="am-icon-line-chart"></i>
<span> 有6笔新的销售订单</span>
</div>
<div class="tpl-dropdown-menu-notifications-time">
12分钟前
</div>
</a>
</li>
<li class="tpl-dropdown-menu-notifications">
<a href="javascript:;" class="tpl-dropdown-menu-notifications-item am-cf">
<div class="tpl-dropdown-menu-notifications-title">
<i class="am-icon-star"></i>
<span> 有3个来自人事部的消息</span>
</div>
<div class="tpl-dropdown-menu-notifications-time">
30分钟前
</div>
</a>
</li>
<li class="tpl-dropdown-menu-notifications">
<a href="javascript:;" class="tpl-dropdown-menu-notifications-item am-cf">
<div class="tpl-dropdown-menu-notifications-title">
<i class="am-icon-folder-o"></i>
<span> 上午开会记录存档</span>
</div>
<div class="tpl-dropdown-menu-notifications-time">
1天前
</div>
</a>
</li>
<li class="tpl-dropdown-menu-notifications">
<a href="javascript:;" class="tpl-dropdown-menu-notifications-item am-cf">
<i class="am-icon-bell"></i> 进入列表…
</a>
</li>
</ul>
</li>
<!-- 退出 -->
<li class="am-text-sm">
<a href="javascript:;">
<span class="am-icon-sign-out"></span> 退出
</a>
</li>
</ul>
</div>
</div>
</header>
<!-- 风格切换 -->
<div class="tpl-skiner">
<div class="tpl-skiner-toggle am-icon-cog">
</div>
<div class="tpl-skiner-content">
<div class="tpl-skiner-content-title">
选择主题
</div>
<div class="tpl-skiner-content-bar">
<span class="skiner-color skiner-white" data-color="theme-white"></span>
<span class="skiner-color skiner-black" data-color="theme-black"></span>
</div>
</div>
</div>
<!-- 侧边导航栏 -->
<div class="left-sidebar">
<!-- 用户信息 -->
<div class="tpl-sidebar-user-panel">
<div class="tpl-user-panel-slide-toggleable">
<div class="tpl-user-panel-profile-picture">
<img src="../img/user04.png" alt="">
</div>
<span class="user-panel-logged-in-text">
<i class="am-icon-circle-o am-text-success tpl-user-panel-status-icon"></i>
禁言小张
</span>
<a href="javascript:;" class="tpl-user-panel-action-link"> <span class="am-icon-pencil"></span> 账号设置</a>
</div>
</div>
<!-- 菜单 -->
<ul class="sidebar-nav">
<li class="sidebar-nav-heading">Components <span class="sidebar-nav-heading-info"> 附加组件</span></li>
<li class="sidebar-nav-link">
<a href="/">
<i class="am-icon-home sidebar-nav-link-logo"></i> 首页
</a>
</li>
<li class="sidebar-nav-link">
<a href="../html/tables.html">
<i class="am-icon-table sidebar-nav-link-logo"></i> 表格
</a>
</li>
<li class="sidebar-nav-link">
<a href="../html/calendar.html">
<i class="am-icon-calendar sidebar-nav-link-logo"></i> 日历
</a>
</li>
<li class="sidebar-nav-link">
<a href="../html/form.html">
<i class="am-icon-wpforms sidebar-nav-link-logo"></i> 表单
</a>
</li>
<li class="sidebar-nav-link">
<a href="../html/chart.html">
<i class="am-icon-bar-chart sidebar-nav-link-logo"></i> 图表
</a>
</li>
<li class="sidebar-nav-heading">Page<span class="sidebar-nav-heading-info"> 常用页面</span></li>
<li class="sidebar-nav-link">
<a href="javascript:;" class="sidebar-nav-sub-title">
<i class="am-icon-table sidebar-nav-link-logo"></i> 数据列表
<span class="am-icon-chevron-down am-fr am-margin-right-sm sidebar-nav-sub-ico"></span>
</a>
<ul class="sidebar-nav sidebar-nav-sub">
<li class="sidebar-nav-link">
<a href="../html/table-list.html">
<span class="am-icon-angle-right sidebar-nav-link-logo"></span> 文字列表
</a>
</li>
<li class="sidebar-nav-link">
<a href="../html/table-list-img.html">
<span class="am-icon-angle-right sidebar-nav-link-logo"></span> 图文列表
</a>
</li>
</ul>
</li>
<li class="sidebar-nav-link">
<a href="/signup">
<i class="am-icon-clone sidebar-nav-link-logo"></i> 注册
<span class="am-badge am-badge-secondary sidebar-nav-link-logo-ico am-round am-fr am-margin-right-sm">6</span>
</a>
</li>
<li class="sidebar-nav-link">
<a href="/login">
<i class="am-icon-key sidebar-nav-link-logo"></i> 登录
</a>
</li>
<li class="sidebar-nav-link">
<a href="/page404" class="active">
<i class="am-icon-tv sidebar-nav-link-logo"></i> 404错误
</a>
</li>
</ul>
</div>
<!-- 内容区域 -->
<div class="tpl-content-wrapper">
<div class="row-content am-cf">
<div class="widget am-cf">
<div class="widget-body">
<div class="tpl-page-state">
<div class="tpl-page-state-title am-text-center tpl-error-title">404</div>
<div class="tpl-error-title-info">Page Not Found</div>
<div class="tpl-page-state-content tpl-error-content">
<p>对不起,没有找到您所需要的页面,可能是URL不确定,或者页面已被移除。</p>
<a href="/" class="am-btn am-btn-secondary am-radius tpl-error-btn">Back Home</a></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="../js/amazeui.min.js"></script>
<script src="../js/amazeui.datatables.min.js"></script>
<script src="../js/dataTables.responsive.min.js"></script>
<script src="../js/app.js"></script>
</body>
</html>
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册