暑假自学了些html/css,javascript和python,苦于学完无处练手几乎过目即忘...最后在同学的建议下做了个简单日程管理系统。借第一版完成之际,希望能将实践期间犯过的错误和获得的新知进行整理,希望能给其他初学者提供参考,也希望有大神在浏览我粗糙的开发过程中能指出一些意见或建议。
(阅读以下内容需要有一定的html/css,javascript,python和sql基础,and谢谢阅读!)
注:实践中的环境为ubuntu 14.04操作系统,python3.4(2.7实测也可行),firefox30.0
一、简单日程系统简介
先上一张界面的清爽截图(请原谅理工男的布局和配色审美...)
各个分区的功能应该比较明显,左下的文本域用于显示和修改被选中日期当天的日程安排。日历中对于今天的日期突出字体颜色显示,对当天有日程安排的日期突出背景色显示,对月历中非本月的部分进行虚化显示。同时每个月份的日历是动态生成的,所以上述系统可以显示任意年份月份的日历。
同时鼠标在日历上移动时有跟随格背景色突出功能
选取某一日期时跟随格颜色跳变产生按钮视觉效果,同时下方的修改按钮解锁。
在文本域中输入日程后点击修改,会同步更新服务器端的数据库,页面中的日历和右侧的“最近14天内日程”提醒框。同时通过javascript的ajax实现页面的局部更新而不必产生页面刷新跳转。
修改时会自动判断是创建一条新的日程安排存档(以一天为单位)还是删除(如果文本框为空)抑或是更新。
二、开发过程:
从界面布局开始思考不知道这科学不,特别最后那个14天内日程提醒还是因为最后发现右边太空了的产物= =||
然后功能上的初步设想就是实现一个hold住任意年份月份的日历日程系统,同时提供对日程的增、删、修改功能(恩,就是一个日程系统的基本功能),其它具体的视觉效果什么的都是边编码边想到补充的(不知道正确的开发方式和这差别多大求指教)。
前端
首先搞定了前端的大部分代码(包括html/css和javascript的部分),这里只列举一些个人觉得有点意义的要点和处理思路,包括一些错误 > <
1.日历的动态生成:
A.页面中的日历(使用<table>)
<table id="calendar"><tr class="weekday"><td class="head">星期日</td><td class="head">星期一</td><td class="head">星期二</td><td class="head">星期三</td><td class="head">星期四</td><td class="head">星期五</td><td class="head">星期六</td></tr><tr class="day"><td id="begin">1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td></tr><tr class="day"><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td></tr><tr class="day"><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td></tr><tr class="day"><td >1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td></tr><tr class="day"><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td></tr><tr class="day"><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td id="end">7</td></tr></table>
代码上没什么特别的,特别<td>标签里的那些1234...可以无视加载时肯定要改(那时只是为了先配合css样式看看显示效果)但是会注意到月历的第一天和最后一天都给了个id,这样方便后面给服务器端提供始末日期数据用以获取当前月历中的有日程安排日信息。其它的class属性为了方便css样式表。
顺便温习下通过id获取元素的js语法:document.getElementById("idname") 返回对元素的引用。
和通过标签名获取元素列表的语法:document.getElementsByTagName("tagname") 返回元素列表。注意这个Element后有s..被坑了几次..
一开始打算每个月份只显示最少星期(即有可能5个星期)的日历,结果发现这会增加一些工作量(比如需要不断删减创建新的表格行)于是看了眼操作系统中的日历发现人家直接6个星期生活乐无忧= =。
于是固定下来每个月显示42天后,由于初始页面要显示今天所在月的月历,所以直接js里一个new Date()获得今天日期对象,然后算法上是通过求得今天所在月份的第一天的星期数,再倒推回去求本月历第一天的日期,然后一个for循环为日历里的每个格赋值(修改其innerHTML改变显示的文字)并将value属性赋值为日期信息(格式xxxx-xx-xx)方便后面向服务器端获取日程安排内容时的后端脚本处理。
B.日期计算
然后就是略坑的js中的日期运算,没有直接加个数字n就返回n天后的日期对象这种好事...于是只能每次在循环里
new Date(firstday.getFullYear(),firstday.getMonth(),firstday.getDate()+i)一个一个弄,不过还好,js里的Date对象支持日期超出(如new Date(,6,35))和日期负数(如Date(,6,-2))会自动往下个月和前个月转换。注:getDate()获得日期,getDay()获得星期。
但是...注意Date对象是这样的,w3school中的资料显示月份是0~11,星期0~6,于是一开始我以为星期中的0代表星期一,以此类推。结果后面测试发现1代表星期一,于是我以为w3school错了应该是1~7,最后debug许久发现原来星期日就是0 = =...这个故事告诉我们基础知识一定要搞清楚...
另外,虽然月份是0~11,如果你直接拿Date去print的话会发现它是正常的1~12,只是在getMonth()方法时是0~11,而这个与你创建一个新的Date对象时传入的参数是对应的,比如你想创建8月3日,那么传入,7,3。
顺便说一下,firefox浏览器右键->查看元素后可以选取调试器用来调试js代码,而且支持加断点查看变量值!
这一部分的核心代码如下:
var firstday=new Date();firstday.setFullYear(nowyear,nowmonth,1);//设置某一天要用setFullYear设置 直接传数字 月份参数比实际少一(如传入6) 但显示出来和实际相符(如显示7)var weekday=firstday.getDay();if( weekday!=0) {//weekday 0-6 0是周日 month 0~11 date 1~31 setDate超时会自动加到下个月负数会自动减到前个月...firstday.setDate(firstday.getDate()-weekday);}var table=document.getElementsByTagName("td");for(i=0;i<42;i++){var date=new Date(firstday.getFullYear(),firstday.getMonth(),firstday.getDate()+i);table[7+i].innerHTML=date.getDate();tmpmon=((date.getMonth()+1)>=10)?(date.getMonth()+1):('0'+(date.getMonth()+1));tmpday=(date.getDate()>=10)?(date.getDate()):('0'+date.getDate());table[7+i].value=date.getFullYear()+'-'+tmpmon+'-'+tmpday;if(date.getFullYear()==nowdate.getFullYear()&&date.getMonth()==nowdate.getMonth()&&date.getDate()==nowdate.getDate()){table[7+i].className="today";rec14beg=table[7+i].value;}else {table[7+i].className="none";table[7+i].style.backgroundColor="#FFFFFF";}if(date.getMonth()==nowmonth)//控制月历中不同部分的透明度table[7+i].style.opacity=1;else {table[7+i].style.opacity=0.5;}
C.动态生成
使用3个js全局变量(全局变量在<script>标签的内容中定义,且不能包含在任何函数内)用于记录今天的日期,当前显示年份,当前显示月份,每次点击前/后一月/年时修改他们的值再把日历中的42个格子重新刷新,同时每次这样的刷新都会像服务器端发送始末日期信息以获取当前显示月历中的有日程安排日信息用以突出背景色。
2.一些布局或显示技巧
A.使用padding属性来控制文字的位置
某些时候单纯的控制对齐方式不足以满足对显示位置的要求,这时可以使用padding属性进行调整。
使用方式是直接使用css样式表,或者是直接在标签里使用如<p style="padding-right:20px">
style="css样式语法"是通行的一种定义css样式的方法,如果懒得每次都在内/外联css样式表里写选择器而某个样式又不需要大规模应用而且你不太记得通过属性怎么写(我标签属性目前只试过<p align:"xxx">写对...)那么直接用这种语法写会方便得多(应该大多数html编辑器都支持对css样式表语法提供提示(如bluefish))。
B.当你不希望两个文字元素隔行而你又需要能通过id访问某个元素时使用<span>标签
比如界面里右上角那个时钟就是用下面的html语句:
<p align="right" style="padding-right:20px">现在是:<span id="clock"></span></p>
C.利用某侧空间的技巧——float样式
页面里的“14天内日程”提醒框是为了占据右边竖直方向的空间,如果直接插进去就算改了align属性还是会成为布局中间的间隔物,为了让它满足布局效果,样式表中float:right就行了,然后太靠右的话再靠padding-right属性来调。至于外面那圈虚线,是outline属性的结果,有关样式表条目:
.tips{float: right;width:50%;outline:#44AE9D dashed thin; margin-right:1%; }//使用百分比能应对更多的浏览器页面大小情况
D.使用<pre>来显示需要保留空格和换行的字符串
和服务器端交互时拿到的字符串通常希望保留空格和换行,但是如果直接赋值给一个<p>标签会自动把这些过滤掉,而<pre>就可以保留。
3.ajax技术
w3school上的教程缺少对POST方式的说明,我的实现中GET和POST两种都用过,其中POST方法适合于数据传输量大的情况(上限2M而GET据说某些浏览器只有1K,因此上传修改后的日程内容时使用了这一方法)。
GET方式比较普通:
var xmlhttp;if (window.XMLHttpRequest){// code for IE7+, Firefox, Chrome, Opera, Safarixmlhttp=new XMLHttpRequest();}else{// code for IE6, IE5xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");}str="cgi-bin/getschedule.cgi?"+date;//创建get的url,其中包含QUERY_STRING内容 后端脚本直接解析它即可xmlhttp.open("GET",str,true);xmlhttp.send();xmlhttp.onreadystatechange=function () {//完成时的处理函数if (xmlhttp.readyState==4&&xmlhttp.status==200) {document.getElementById("schedule").value=xmlhttp.responseText;//responseText存放了服务器脚本返回的文本内容}}
POST方式,下面的代码是用于向服务器发起对数据库的增、删、改请求时的函数:
function updatedb() {var xmlhttp;if (window.XMLHttpRequest){// code for IE7+, Firefox, Chrome, Opera, Safarixmlhttp=new XMLHttpRequest();}else{// code for IE6, IE5xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");}if (tdchosen.className.indexOf("job")<0&& document.getElementById("schedule" ).value==0){return;}senddata='date='+document.getElementById("nowchoice").value+'&&schedule='+document.getElementById("schedule" ).value+'&&method=';//与GET类似用&&隔开各个键=值对if (tdchosen.className.indexOf("job")<0&&document.getElementById("schedule" ).value!=0) {tdchosen.className+="job";tdchosen.style.backgroundColor="#EBD44D";senddata+='0';//增加}else if (tdchosen.className.indexOf("job")>=0&&document.getElementById("schedule" ).value!=0) {senddata+='1';//修改}else if (tdchosen.className.indexOf("job")>=0&&document.getElementById("schedule" ).value==0) {tdchosen.className="none";tdchosen.style.backgroundColor="#FFFFFF";senddata+='2';//删除}xmlhttp.open("POST","cgi-bin/updatedb.cgi",true);//url里不需要附上QUERY_STRING 需要发送的数据在后面的send函数里发送xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");//该语句必须,用于告知服务器端脚本传送的数据按键=值对格式 xmlhttp.send( senddata); xmlhttp.onreadystatechange=function (){if (xmlhttp.readyState==4&&xmlhttp.status==200) {document.getElementById("feedback").innerHTML=xmlhttp.responseText;}}}
4.应避免的一个错误和一个需要注意的地方
A.我们在修改页面中的文本元素的显示内容时习惯修改innerHTML属性,但是对<textarea>这种用户编辑其显示内容会跟着修改其value属性的元素,在后台使用js时应对value属性做修改。(开发期间曾经因为修改innerHTML而出现左下方文本域某些情况下不显示的bug)
B.js的ajax(异步方法)获得的字符串是utf-8格式的,为了显示不出现乱码,需要注意服务器端传回的字符串的编码格式是否同为utf-8。
后端
1.服务器和文件系统
用了python最简单的一个支持cgi的简单服务器类HTTPServer(初学者的本质暴露无疑)然后用CGIHTTPRequestHandler作为处理程序类的基类。(理论上cgi通常指通过表单<form>标签提交数据并返回要求页面的动态页面生成方式,它会使得页面产生刷新跳转,而ajax是局部刷新页面无需跳转,但在我的实现中,只要控制脚本返回的数据的表头即可用这样的方式实现ajax。当然,在这里跪求正规的ajax服务器实现方式,欢迎评论留言交流或邮箱449339387@)
import http.serverfrom http.server import HTTPServerfrom http.server import CGIHTTPRequestHandlerdef run(server_class=HTTPServer, handler_class=CGIHTTPRequestHandler):server_address=('',8001)httpd=server_class(server_address, handler_class)httpd.serve_forever()if __name__ == '__main__':run()
然后就直接运行这个脚本它会一直运行到你用ctrl-C,期间可以在终端看到运行过程中信息包括后台python脚本的错误信息
eg.
文件系统方面,注意使用上述cgi服务器时,假设服务器脚本在顶层目录,则所有的url相对寻址以该脚本所在位置为起始点。要求所有脚本文件必须在cgi-bin这个文件夹下(否则会是获取这个源文件的内容而不是执行后的返回结果),同时你需要chmod u+x 脚本文件名以赋予它可被执行的权限。(脚本文件名后缀.cgi或.py结尾均可)
另外,注意数据库文件与服务器脚本的目录关系,如果在同一层目录,后台脚本中使用的相对路径在被服务器调用时是以服务器所在位置为起点的。但这为脚本的单独测试造成了不便,因为脚本文件是在cgi-bin目录下不和服务器同一层级,如果只有一个数据库文件,需要自行修改访问路径,或者你自己拿个数据库文件副本放cgi-bin下专门测试用(我自己开发过程中就拿了个副本测试但是后来忘了两个数据库文件内容已经不一样了于是对一些现象一直以为是bug结果发现是目录对应关系没理解透..)
2.其它后端脚本
数据库使用的是sqlite3,原因是它是装python自带的,然后也是个关系型数据库,对sql的语法支持够用,恩,等哪天再研究mysql...
一开始需要创建数据库中的表和索引(index,用于后续select时可以根据该索引所对应的表项排序)
import osimport sqlite3conn=sqlite3.connect('jobs_database')cursor=conn.cursor()cursor.execute('''create table jobs(userid varchar,year integer,month integer,date integer,totalday integer,jobslist varchar)''')cursor.execute('''create index userid on jobs(userid)''')cursor.execute('''create index years on jobs(year)''')cursor.execute('''create index months on jobs(month)''')cursor.execute('''create index dates on jobs(date)''')cursor.execute('''create index totaldays on jobs(totalday)''')mit()cursor.close()conn.close()
sql的语法我自己还不太熟练,有兴趣的可以百度。
建表时主要考虑了后续的可扩展使用度以及编程或排序的方便
里面那个totalday列是发现根据year,month,date不好用between子句来获得想要的内容,比如我想提取-7-27~-8-30这段时间内的日程安排信息,但是你用jobs.day between 27 and 30时明显就不对了。所以把所有日期转换成一个整数如1128直接比较大小看起来才是正道(同样的,希望有人分享更正规的做法)。但是要注意月份和日期可能只有一位数,转整数时需要给其补0再连接转换成整数。这体现在后续的数据传送的格式上。
获取有日程安排日信息的脚本
#!/usr/bin/python3.4import osimport sqlite3from datetime import *import timequery=os.environ['QUERY_STRING'].split('&')begin=query[0].split('-')end=query[1].split('-')beday=int(begin[0]+begin[1]+begin[2])endday=int(end[0]+end[1]+end[2])conn=sqlite3.connect("jobs_database")cursor=conn.cursor()cursor.execute('''select jobs.year,jobs.month,jobs.date,jobs.jobslist from jobs where jobs.totalday between ? and ? order by jobs.totalday asc''',(beday,endday))result=cursor.fetchall()cursor.close()conn.close()bedate=date(int(begin[0]),int(begin[1]),int(begin[2]))response=str(result.__len__())+'&'for item in result:response+=str((date(item[0],item[1],item[2])-bedate)).split()[0]+'&'print('Content-type: text/plain\n')print(response)
页面传回来的数据是一对当前月历始末日期,格式“xxxx-xx-xx&xxxx-xx-xx”,由于页面已经处理好了补零的问题,后台脚本就可以简单地连接转成整数,查询。
这里返回数据时要注意两点:
1.print('Content-type: text/plain\n')语句是必须的,它告诉浏览器送回来一个字符串而不是页面(text/html)
2.是要传送回一堆xxxx-xx-xx格式的日期呢还是别的呢?
考虑到传送回日期的话还要不断去遍历表格中的每个格子判断日期是否对上来进行背景色变化,复杂度就上去了。(当然,可以本地js写个映射表,但月历一改表就得重新维护吃力不讨好)于是这里选择后台累一点直接返回对应格子的索引下标,同时第一个数字是总共当前月历中有日程安排日的总数,方便后面js里写for循环的方便和准确。于是返回的格式是"总数&索引1&索引2&...&",有&这个间隔符后面js处理就方便了。而且相比于传送日期这样的网络通信量更少却提供了所需的全部信息。(突然发现这有点协议的意思了:))
另外,补充一句,实测后发现运行后台脚本时使用的python解释器的版本是根据开头的#!/usr/bin/python3.4来确定的,ubuntu系统中2.x和3.x版本的python都有装,两个版本在socket编程等地方有一定差异,因此某些情况下需要注意这一句使用了哪个版本(上面语句如果为#!/usr/bin/python在ubuntu中对应使用的时python2.x)
获取被选中日的日程内容:
通过js代码中的预先判断决定是否需要发起该获取请求(对于标记为没有日程安排的格子明显不需要),减少网络通信量(网络带宽永远是珍稀资源,虽然放在这个小项目里形式上的意义更大=。=不过实测中使用ajax确实是有一些肉眼可见的延迟的)。
#!/usr/bin/python3.4import osimport sqlite3query=os.environ['QUERY_STRING'].split('-')day=int(query[0]+query[1]+query[2])conn=sqlite3.connect('jobs_database')cursor=conn.cursor()cursor.execute('''select jobs.jobslist from jobs where jobs.totalday = ?''',(day,))result=cursor.fetchall()cursor.close()conn.close()response=""if result.__len__()!=0:response=result[0][0]print('Content-type: text/plain\n')print(response)
然后是最大头的增删改操作:
#!/usr/bin/python3.4import cgiimport sqlite3form=cgi.FieldStorage()date=form.getfirst('date').split('-')day=int(date[0]+date[1]+date[2])jobslist=form.getfirst('schedule')method=int(form.getfirst('method'))conn=sqlite3.connect('jobs_database')cursor=conn.cursor()try:if method == 1:#updatecursor.execute('''update jobs set jobslist = ? where jobs.totalday =?''',(jobslist,day))elif method == 0:#insertcursor.execute('''insert into jobs(userid,year,month,date,jobslist,totalday)values(?,?,?,?,?,?)''', ('author',int(date[0]),int(date[1]),int(date[2]),jobslist,day))elif method == 2:#deletecursor.execute('''delete from jobs where jobs.totalday=?''',(day,))mit()except:print('Content-type: text/plain\n')print('更新失败,请刷新页面后重试!')else:print('Content-type: text/plain\n')print('操作成功')finally:cursor.close()conn.close()
专门写异常处理但是只在这个脚本里写只是为了回顾写这个小项目的初衷:把自学的内容尽可能用一遍。所有有了上面的try except finally。注意包括else时异常处理的执行顺序哦!
最后是获取“14天内日程”提示框内容:
#!/usr/bin/python3.4import osimport sqlite3from datetime import *import timequery=os.environ['QUERY_STRING'].split('&')begin=query[0].split('-')beday=int(begin[0]+begin[1]+begin[2])delta=date(,6,15)-date(,6,1)nowdate=date(int(begin[0]),int(begin[1]),int(begin[2]))enddate=nowdate+deltaendtmpyear=str(enddate.year)endtmpmon=enddate.monthendtmpday=enddate.dayif endtmpmon < 10:endtmpmon='0'+str(endtmpmon)if endtmpday <10:endtmpday='0'+str(endtmpday)endday=int(endtmpyear+str(endtmpmon)+str(endtmpday))conn=sqlite3.connect("jobs_database")cursor=conn.cursor()cursor.execute('''select jobs.year,jobs.month,jobs.date,jobs.jobslist from jobs where jobs.totalday between ? and ? order by jobs.totalday asc''',(beday,endday))result=cursor.fetchall()cursor.close()conn.close()print('Content-type: text/plain\n')response=""for item in result:print('<strong>距今'+str((date(item[0],item[1],item[2])-nowdate)).split()[0]+'天:\n'+item[3]+'</strong><br/>')
由于想试试python的日期处理所以这次的14天后是后端自己算的,注意python中两个date对象做差得到的不是int而是datetime.timedelta类的对象,又不能直接+整数n得到n天后了,只好用一个tricky的方法delta=date(,6,15)-date(,6,1)造一个14天的delta再给它往今天的date对象上加(再次跪求正规做法!!)。
然后要获取距离多少天这又是个问题,用dir查了下好像没啥转成int的方法,网上介绍用获取距某个默认时间的秒数再数学运算求的方法感觉太麻烦了= =还好datetime.timedelta类对象str后得到的字符串表示是“xxx days xx:xx:xx”,所以又是tricky的方法拿到字符串后split然后拿0号下标就是要的天数字符串!(再再次跪求正规做法!!)
三、Future work
项目的全部源码由于还没有全部补好注释待我弄完再发上来(或者我终于良心发现去研究github怎么用然后传到github回这里贴链接)
最后说下这个标题所示的不能免俗的话题:
1.虽然为了能把所学的内容尽可能用上的初衷用了服务器脚本和数据库,不过好像一点网络的效用都没发挥(恩,maybe局域网)。从网络角度考虑应该实现多用户使用,这也是为什么数据库的表里面预留了userid这一项。后续可以加个登陆页面做个表单填账号密码数据库里加个密码表做验证。
2.然后就说到了安全性,第一版的这个项目安全性几乎没有= =,待哪日学了信息安全的课程再说。
3.多用户后对数据库的多线程访问问题也出现了,这块还完全不清楚,需要择日专门学数据库。
4.服务器的问题也希望能弄得更清楚,包括怎么正确支持ajax等。