基础篇
本文使用python2 + windows10 的环境进行开发
(一)web.py的安装
如果你使用的python2, 直接运行下面的命令即可(版本为: 0.39)
pip install web.py
关于包的名称 web.py 竟然带了.py这个后缀一开始也让我小小的疑惑了一下,不过各位安装的时候还是完整填写名称的好
对于习惯用python3的朋友,请使用下面的命令安装web.py
pip install web.py==0.40-dev1
(二)第一个web.py程序
一个很宏大的目标,往往是从一个小的demo开始的,所以我先来学习web.py最基础的Hello World程序。
import web urls = ( '/', 'index' ) class index: def GET(self): return 'Hello, World!' if __name__ == "__main__": app = web.application(urls, globals()) app.run()
保存上面的代码到脚本文件 demo.py, 然后运行
python demo.py
之后访问 http://0.0.0.0:8080
即可查看到我们的代码运行效果了
(三)web.py如何与数据库交互?
学习制作网站,了解web框架是如何与数据库交互的至关重要,至少需要学会使用他;web.py的官网文档中给出了两种主流数据库的交互参考,这里以postgresql为例,进行学习。
- 首先,你需要在自己的电脑上安装配置postgresql,而我直接在Linux环境下运行下面的命令安装(树莓派 + Raspbian)
sudo apt-get install postgresql
具体配置方式可以自行百度,网上有非常多的教程。
2. 其次,安装Python和Postgresql交互的中间件,这里采用官网给出的一种中间模块:psycopg2. 安装方式如下:
pip install psycopg2
3. 然后,使用数据库操作工具,创建一个名为 webpy的数据库,再创建表。表的创建SQL如下:
CREATE TABLE todo ( id serial primary key, title text, created timestamp default now(), done boolean default 'f' );
4. 然后使用模板,在脚本根目录下创建template 文件夹存放 HTML文档,我在里面创建了一个main.html文件,然后将下面的代码插入了进去:
$def with (todos) <ul> $for todo in todos: <li id="t$todo.id">$todo.title</li> </ul>
这串HTML代码中,以 $符号起的行即为Python代码,这里的意思是如果todos存在, 就循环遍历todos,然后生成li标签,而这就是模板。若直接在Python代码中插入HTML不够美观也不符合软件设计原则,而在HTML中插入Python代码则相对来说更加美观和整洁,故如此处理。
5. 最后,数据库简单操作的完整代码如下:
# encoding: utf-8 import web urls = ( '/', 'index' ) render = web.template.render('template/') db = web.database(dbn='postgres', user="postgres", pw="123456", db="webpy", host="192.168.3.20", port=5432) class index: def GET(self): todos = db.select('todo') # 查询 return render.main(todos) if __name__ == "__main__": app = web.application(urls, globals()) app.run()
额外增加一个写入数据库的例子:
# encoding: utf-8 import web urls = ( '/', 'index', '/add', 'add' ) render = web.template.render('template/') db = web.database(dbn='postgres', user="postgres", pw="123456", db="webpy", host="192.168.3.20", port=5432) class index: def GET(self): todos = db.select('todo') # 查询 return render.main(todos) class add: def POST(self): i = web.input() # input函数可以访问form提交的任何数据 n = db.insert("todo", title=i.title) raise web.seeother('/') # 重定向到index页面 if __name__ == "__main__": app = web.application(urls, globals()) app.run()
也就是在前一个demo的基础上,增加了一个add类和一个URL链接导航 /add 用来接收从前端传过来的参数。前端模板如下:
$def with (todos) <ul> $for todo in todos: <li id="t$todo.id">$todo.title</li> </ul> <form method="post" action="add"> <p><input type="text" name="title" /><input type="submit" value="Add"></p> </form>
四、web.py如何返回Json数据
def POST(): web.header('Content-Type', 'application/json') return json.dumps({"json": "yes"})
实战篇
(一)需求分析
在写代码之前,充分分析需求是避免重复劳动的重要步骤,由于我没有学过软件工程及设计模式,这里我想到什么写什么,设计模式在这篇文章写完后我就去学习。
现在可以基本理清思路。
- 博客需要展示,因此有一个列表界面和一个完整显示文章的界面
- 博客发布页面
- 用户管理界面,包括对博客的增删改查
- 管理登录界面
(二)数据库设计
按照我的理解,一般的内容型网站的编写,好的数据库逻辑关系,会大大影响代码操作的骚的程度。我没那么骚,就简单弄个,下面是创建表的SQL
CREATE TABLE blog ( id serial primary key, title text, contents text, created timestamp default now(), updated timestamp default now(), author INTEGER ); CREATE TABLE blog_users ( id serial primary key, username VARCHAR(255), pwd VARCHAR(255) )
手动插入用户
INSERT INTO blog_users (username, pwd) VALUES ('admin', 'admin');
(三)URL设计
其实我一开始是想学习一下Restful API的设计规则的,然而我真的很懒,随便设计一下吧,反正需求挺简单的
/ index /index index /?p=1 分页 /?c=1 blog内容详情 /login 管理界面登录 /reset 退出登录 /post 发布、更新、删除链接 /admin 管理界面
代码如下
urls = ( '/', 'index', '/index', 'index', '/login', 'login', '/reset', 'reset', '/post', 'post', '/admin', 'admin' )
(四)实现
1. 登录(Login)功能实现
首先,既然是登录,就必须要考虑cookie的处理,不过还好,web.py已经提供了session模块
session = web.session.Session(app, web.session.DiskStore('sessions'), initializer={'login': 0})
第一个参数: app即我们初始化的应用。
第二个参数: DiskStore函数,用于在本地存储session文件;相对的还有DBStore函数,可在数据库存储session数据,我记得以前实习的公司就是用memcache在云端存储的session,以达到多服务器共享的session的目的。
第三个参数:initializer 是一个字典类型的参数,用来存放session中可能用到的数据,比如这里的login,就可以用来判断用户是否已经登录。比较抽象,但一读代码就能理解他究竟做了些什么。
登录的判断逻辑是我从官网的cookbook里面抄的,通过login为1或0来判断用户的登录状态,然后实现Login代码,代码如下
def logged(): if session.login == 1: return True else: return False class login: def GET(self): if logged(): raise web.seeother("/") else: return render.login() def POST(self): username, password = web.input().username, web.input().password indent = db.select('blog_users', where='username=$username', vars=locals())[0] web.header('Content-Type', 'application/json') try: if password == indent.pwd: session.login = 1 return json.dumps({'login':1, 'status': 'success'}) else: session.login =0 return json.dumps({'login':0, 'status': 'failed'}) except: session.login = 0 return json.dumps({'login':0, 'status': 'failed'})
此代码仅用于学习,千万不要弄到生产环境咯,不然会有非常大的安全隐患,后面我还会专门探讨一下web安全的问题,最近刚好在作CTF题。
其次,登录的模板。我做了个非常简单的模板,途中发现web.py的文件里面没办法直接用jquery的语法,关键字冲突了,只能尝试用静态文件的办法去处理了,具体代码如下
Login.html
<form method="post" action="login"> <input type="text" name="username" placeholder="username"> <input type="password" name="password" placeholder="password"> <!-- <input type="submit" name="login"></p> --> <input type="button" name="login" class="login" value="Login"> </form> <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"> </script> <script type="text/javascript" src="http://127.0.0.1/static/login.js"></script>
login.js
$(".login").on("click", function(event){ var data = {}; $("input[name!=login]").each(function(i){ data[$(this).attr('name')] = $(this).val(); }) $.post('/login', data, function(payload){ console.log(payload); if (payload.login == 1){ alert("登录成功"); window.location.href = '/'; }else{ alert("登录失败,请重新登录"); } }) })
由于不能直接在模板中写jquery代码,所以我配置了一下nginx服务器,提供静态文件访问,就成功绕过了直接在模板中写代码的尴尬。配置如下
location /static { root K:/Git/web-demo; }
至此,Login的基础功能基本完成。
2. 管理功能(admin)实现
先修改login页面的跳转,让登录用户跳转到admin页面
class login: def GET(self): if logged(): raise web.seeother("/admin") else: return "%s" % render.login()
然后开始编写admin这个类
class admin: def GET(self): if logged(): entries = db.select("blog") return render.admin(entries) else: raise web.seeother("/login")
我让admin这个类,主要用来展示博客文章,并添加可以编辑的按钮;对于没有登录的用户则直接跳转到登录页面,下面是admin的模板
$def with (entries) <ul> $for article in entries: <li id="$article.id"> <h4><a href="/edit?p=$article.id">$article.title</a> <span class="btn" id="delete" onclick="dArticle($article.id)">删除</span></h4> <div>$article.created</div> </li> </ul> <div class="add" id="add" onclick="add()">添加博客</div> <script type="text/javascript"> function add(){ window.location.href = "/edit"; } function dArticle(dataid){ var url = "/delete"; var postStr = "p=" + dataid; var ajax = null; if (window.XMLHttpRequest){ ajax = new XMLHttpRequest(); } else if (window.ActiveXObject()){ ajax = new ActiveXObject("Microsoft.XMLHTTP"); } else { return ; } ajax.open("POST", url, true); ajax.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); ajax.send(postStr); ajax.onreadystatechange = function(){ if (ajax.readyState == 4 && ajax.status == 200){ location.reload(); // 刷新当前页面 } } } </script>
通过js发起post请求的方法是我刚从 《白帽子说web安全》里面学来的,所以这里我偷懒直接写了js,没有新增文件去使用jquery的代码写post请求。多读书还是有好处的
如果你仔细看了的话,你可能会发现我在这儿使用了一个 edit和一个delete类, 这个类主要是为了处理博客的增删改添加得类,于是我得重新注册一遍url
'/edit', 'edit', '/delete', 'delete'
3、添加、更新文章功能
edit这个类有两个功能,其一是添加和更新博客,其二是返回编辑的前端页面,下面是我的代码:
class edit: def GET(self): if logged(): i = web.input(p=None) if i.p: print(i) entries = db.select("blog", i, where="id=$p")[0] return render.edit(entries) else: return render.edit(entries=None) else: raise web.seeother("/login") def POST(self): if logged(): i = web.input(p=None, title=None, content=None) web.header('Content-Type', 'application/json') if not i.p: title = i.title if i.title != None else " " contents = i.contents if i.contents != None else " " db.insert("blog", title=title, contents=contents, author=1); return json.dumps({'success':1, "message": "add blog success"}) elif i.p: title = i.title if i.title != None else " " contents = i.contents if i.contents != None else " " db.update("blog", title=title, where="id=" + i.p, contents=contents, author=1, updated="now()"); return json.dumps({'success':1, "message": "add blog success"}) else: return "Please Login."
可以看到,POST函数处理post请求,通过logged() 判断登录情况,通过web.input()获取请求参数,然后分类处理添加文章和更新文章;在处理GET时,通过判断有没有标识 文章的 id存在,来安排编辑的前端页面,对照着前端页面看更容易理解。下面是我的前端代码
$def with (entries) <!DOCTYPE html> <html> <head> <title> 博客编辑 - demo</title> </head> <body> <div id="add"> <h2>博客编辑</h2> <div class="bar"> <div class="move-back btn" id="move-back">返回</div> <div class="save btn" id="save"> 保存 </div> </div> <div class="title"> $if entries: <input type="text" name="title" value="$entries.title"> $else: <input type="text" name="title" /> </div> <div class="content"> <textarea id="content" rows="3" cols="20" value=""></textarea> </div> $if entries: <div style="display: none;" id="blog-id" data-id="$entries.id"></div> </div> </body> </html> <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script> <script type="text/javascript" src="http://127.0.0.1/static/edit.js"></script> <script type="text/javascript"> $if entries: addContent("$entries.contents"); </script>
果然前端代码都是超级多的,后面我就直接贴Github得链接了。不过这儿,edit.js我还是贴出来
document.getElementById("move-back").addEventListener("click", function(){ window.location.href = "/admin"; }) $("#save").on("click", function(event){ var title = $("input[name=title]").val(); var content = $("#content").val(); var blog_id = $("#blog-id"); var data = {}; if (blog_id != null){ data.p = blog_id.attr('data-id'); } data.title = title; data.contents = content; $.post("/edit", data, function(payload){ if (payload.success == 1){ window.location.href = "/admin"; } else { alert("添加失败,请检查网络"); } }) }) function addContent(data){ $("#content").val(data); }
4、 博客删除功能
删除我又单独写了一个类,哈哈哈哈,感觉写得好乱
delete类的代码
class delete: def POST(self): if logged(): web.header('Content-Type', 'application/json') i = web.input(p=None) if i.p: db.delete("blog", where="id="+i.p) return json.dumps({"success": 1,"message": "删除成功"}) return json.dumps({"success": 0, "message": "删除失败"})
和edit类的代码基本一致
到此为止,只剩下一个首页还没写好了。
5、首页实现
首页无非是直接显示内容,当然可能需要做个分页,不着急,我慢慢写。
首页的前端后端代码很简单,查询数据库,拿出数据即可
class index: def GET(self): entries = db.select('blog') # 查询 return render.index(entries)
前端代码直接放在li列表里面吧
<div class="content"> <ul> $if entries: $for arti in entries: <li> <div><a href="/?p=$arti.id">$arti.title</a></div> </li> </ul> </div>
遍历所有文章,生成列表。
然后就是,文章详情页面了,我给他取名叫single
class index: def GET(self): i = web.input(p=None) if not i.p: entries = db.select('blog') # 查询 return render.index(entries) elif i.p: entries = db.select("blog", where="id=" + i.p)[0] return render.single(entries)
修改index页面,让带参数p的请求转到详情页面
然后,详情页面改成这样
<div class="main-content"> $if entries: <div class="title" id="title">$entries.title</div> <div class="datetime" id="datetime">$entries.created</div> <div class="data"> $entries.contents </div> </div>
OK,大功告成。这个demo基本就完成了。
四、优化工作
优化工作再慢慢写吧
完整项目代码: Github
# 2019年7月6日19点47分
经过两天的努力,终于把这个demo的admin基本写完了。然后就是慢慢写博客了,最近在浏览别人的博客的时候,忽然发现有非常多的优秀的博客,当然大多是一些技术博客,毕竟我学识有限,所知不多,但是这给了我一个很好的启发,有没有办法更好的整理这些博客的内容呢?要不要开发一个整理的网站试试?
然后挑选一些大佬的博客放在上面,并归类整理一些近期的文章出来。
废话到这儿,继续写博客
暂无评论