web實踐小項目<一>:簡單日程管理系統(涉及html/css,javascript,python,sql,日期處理)


暑假自學了些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(2014,6,35))和日期負數(如Date(2014,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對象時傳入的參數是對應的,比如你想創建2014年8月3日,那么傳入2014,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, Safari
      xmlhttp=new XMLHttpRequest();
  }
else
  {// code for IE6, IE5
      xmlhttp=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, Safari
  xmlhttp=new XMLHttpRequest();
  }
else
  {// code for IE6, IE5
  xmlhttp=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@qq.com

 

import http.server
from http.server import HTTPServer
from http.server import CGIHTTPRequestHandler
def 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 os
import sqlite3
conn=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)''')
conn.commit()
cursor.close()
conn.close()

sql的語法我自己還不太熟練,有興趣的可以百度。

建表時主要考慮了后續的可擴展使用度以及編程或排序的方便

里面那個totalday列是發現根據year,month,date不好用between子句來獲得想要的內容,比如我想提取2014-7-27~2014-8-30這段時間內的日程安排信息,但是你用jobs.day between 27 and 30時明顯就不對了。所以把所有日期轉換成一個整數如20141128直接比較大小看起來才是正道(同樣的,希望有人分享更正規的做法)。但是要注意月份和日期可能只有一位數,轉整數時需要給其補0再連接轉換成整數。這體現在后續的數據傳送的格式上。

獲取有日程安排日信息的腳本

#!/usr/bin/python3.4
import os
import sqlite3
from datetime import *
import time
query=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.4
import os
import sqlite3
query=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.4
import cgi
import sqlite3
form=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:#update
        cursor.execute('''
    update jobs set jobslist = ? where jobs.totalday =?
    ''',(jobslist,day))
    elif method == 0:#insert
        cursor.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:#delete
        cursor.execute('''
    delete from jobs where jobs.totalday=?
    ''',(day,))
    conn.commit()
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.4
import os
import sqlite3
from datetime import *
import time
query=os.environ['QUERY_STRING'].split('&')
begin=query[0].split('-')

beday=int(begin[0]+begin[1]+begin[2])
delta=date(2014,6,15)-date(2014,6,1)
nowdate=date(int(begin[0]),int(begin[1]),int(begin[2]))
enddate=nowdate+delta
endtmpyear=str(enddate.year)
endtmpmon=enddate.month
endtmpday=enddate.day
if 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(2014,6,15)-date(2014,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等。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM