【前端】HTML+CSS+JavaScript:前端入門,利用ajax實現表格的自動更新

一、概述

寫這個網頁的最初動機是我的一門課需要圖形界面實現,時間有點緊,去學習QT之類的已經來不及了,同時我又對前端很感興趣,因此起了用HTML來寫一個網頁作爲圖形界面的想法。

後端代碼參見該鏈接。主要實現一個記分牌流水線算法。前端主要需要實現的功能爲輸入指令流,將指令流傳遞到後端,後端經過處理會生成三張表。每經過一個週期(默認爲1s),表中內容更新一次。後端會將這三張表的內容傳遞迴前端,前端要保證實時更新這三張表。另外,還要實現週期長度的自定義功能。

具體實現效果參見該網址

源代碼在這裏

二、分析

1、文件結構

前端顯示網頁有三個html文件,分別爲base,entry和results;有一個css文件,hf,爲什麼是hf呢?我原來學HeadFirst的時候,寫過一個簡單的CSS,就拿來用了。那個base是最基礎的網頁佈局,後面兩個都是繼承自它。後端有兩個文件,其中一個的主要內容爲流水線算法的實現,scoreboard,另一個使用Flask實現通信,vsearch4web。entry對應的網址爲/和/entry,result對應的網址爲/search4。

2、流程設計

我的思路是寫兩個網頁,第一個網頁中,輸入指令流,然後按鍵進入第二個網頁,第二個網頁顯示動態的三個表格。具體效果如下:

點擊Do it後跳轉到第二個網頁。

在該網頁進行動態刷新。流程圖如下:

因此我們要實現的功能有以下幾個:

①、如何實現輸入網址,顯示entry網頁?

②、如何向網頁輸入指令流?

③、如何實現從entry跳轉到results?

④、如何實現將指令流傳遞到服務器?

⑤、如何顯示這三張表?

⑥、如何動態的刷新這三張表?

⑦、如何自定義刷新頻率?

接下來我們將一個一個解決這些問題。

三、前端代碼實現

1、如何實現輸入網址,顯示entry網頁?

這個問題很好解決。Python的Flask包可以極容易的解決這個問題,具體實現參見我的該文章

2、如何向網頁輸入指令流?

這個問題很好解決:使用HTML的input標籤,即可輸入一行文本;使用textarea標籤,即可輸入多行文本。由於我們的指令流有多行,因此使用後者。如下:

<div style="text-align:center;">
<textarea class="boxes" name="instruction_stream" >Input</textarea>

這個div標籤可以將html分成不同部分,於是就可以將它們分別賦爲不同的屬性。裏面的textarea就是我們的輸入了。都是很基礎的html語法。

3、如何實現從entry跳轉到search4?

這一步需要前後端的交互。在entry中,有如下代碼:

<form method='POST' action='/search4'>
<!--
<table>
<p>Use this form to submit a instruction stream:</p>
<tr><td>Instruction_Stream</td><td><input name='instruction_stream' type='TEXT' width='60'></td></tr>
</table>
-->
<div style="text-align:center;">
<textarea class="boxes" name="instruction_stream" >Input</textarea>
</div>
<p style="text-align:center;color:#00FF00">When you're ready, click this button:</p>
<p style="text-align:center"><input value='Do it!' type='SUBMIT'></p>
</form>

一眼就能看見我們在上面的textarea。在這個問題中,這不是重點。重點是第一行:method爲POST,action爲/search4。

這兩個是什麼意思呢?

很遺憾由於我沒有學過HTML,因此無法用專業的語言說明這個,我只能這樣描述:它會有一個動作,就是向/search4這個url發送一個POST請求。在後端代碼中有一段是對應的,因此可以跳轉到/search4這個網址。

接下來看倒數第二行。input的type表示這將會向服務器發送(submit)一條信息(我直觀上自己的理解是這樣的)。在vsearch4web,也就是服務器上面實現通信的文件中,有如下代碼:

@app.route('/search4',methods=['POST'])
def do_search() -> 'html':
    session['Cycle']=session['Cycle']+1
    instructions=request.form['instruction_stream']
    instruction=instructions.split('\n')
    session['instructions']=instructions
    session['instruction']=instruction
    for i in range(0, len(instruction)):
        instruction[i]=instruction[i].replace('\r', '')
    ins_Table,_func,_reg=scoreboard.goto_cycle(session['Cycle'],instruction)
    return render_template('results.html',
                           the_title='Here are your results',
                           Instruction_Stream=instructions,
                           insTable=ins_Table,
                           func=_func,
                           reg=_reg,)

看到和第一行對應的代碼了麼?可以這麼理解:服務器監聽/search4這個url,一旦接收到類型爲POST的請求,就執行do_search這個函數。該函數將會返回一個網頁,名爲results.html,新的網頁對應的url就叫/search4,於是就實現了跳轉。

也就是說,客戶端向服務器的不同url發送請求,服務器就執行該url下面對應的函數,從而返回客戶端需要的結果。這一點很重要。

4、如何實現將指令流傳遞到服務器?

在3中,我們已經說明了,客戶端會向服務器發送一條POST請求,這請求的內容是什麼呢?肯定會是指令流。爲什麼瀏覽器知道我要把輸入的指令流傳過去呢?

你看HTML中的第一行,有一個form標籤,form裏面的內容是一個html表單,瀏覽器會將表單中輸入的內容裝在請求中傳遞給服務器。

那服務器如何取出這個指令流呢?我們看通信文件,那個request.form就是我們從請求中取出的數據。這可以看成是一個字典,其鍵爲'instruction_stream',再看HTML文件,textarea是不是有個標籤,name,內容就是這個鍵名呢?

這樣就圓回來了。實際上的具體過程還需要去讀一讀html的書才能知道原理,對於我這種練手的來說,知道如何做就可以了。

5、如何顯示這三張表?

這涉及到html如何顯示錶格數據。代碼如下:

<p style="text-align:center;color:#00FF00">Instruction Status:</p>
<table border="1" id="T_ins">
  <tr>
    <th>Instruction</th>
    <th>Target</th>
    <th>J</th>
    <th>K</th>
    <th>Issue</th>
    <th>Read Operand</th>
    <th>Execution Complet</th>
    <th>Write Result</th>
  </tr>
  <!--<div class="insTable">{{ insTable }}</div>-->
    {% for item in insTable %}
        <tr>
            <td id="instruction">{{ item["instruction"] }}</td>
            <td id="target">{{ item["target"] }}</td>
            <td id="j">{{ item["j"] }}</td>
            <td id="k">{{ item["k"] }}</td>
            <td id="issue">{{ item["issue"] }}</td>
            <td id="readOperand">{{ item["readOperand"] }}</td>
            <td id="exeComplet">{{ item["exeComplet"] }}</td>
            <td id="writeResult">{{ item["writeResult"] }}</td>
        </tr>
    {% endfor %}
</table>

第一行的標籤p用來顯示錶名,之後的標籤table表示這是提個表格。tr用來指定表格的行,th用來指定表格的列;td用來指定表格中的元素。那這一堆大括號幹嘛的啊?大括號是與後端使用的Flask對應的。看上面後端代碼的最後一行:

    return render_template('results.html',
                           the_title='Here are your results',
                           Instruction_Stream=instructions,
                           insTable=ins_Table,
                           func=_func,
                           reg=_reg,)

返回值的第一個是html文件,相當於骨架,之後的變量就相當於是肉,在骨架中填上肉,才能組成一個人(不過這聽起來怎麼這麼瘮得慌)。這些肉是什麼呢?右邊是常數或者是函數中的變量,左邊就對應html中大括號對應的字符。比如說insTable,就在大括號中的for item in insTable,也就將這些大括號中的鍵替換成了返回的鍵值。從而實現了表格的顯示。

由於表格是動態的,因此選擇循環按行生成表格。

6、如何動態的刷新這三張表?

終於講到這篇文章的核心問題了。從上面我們可以知道,服務器返回一個html和一堆變量,瀏覽器就能給我們組成一個網頁,其中有這三張表,那麼,我要想刷新這三張表,一次又一次的請求服務器不就可以了?

原則上是這樣,但是太蛋疼了。比如我我要看100週期的表格變化,那麼就要刷新100次整個網頁,對用戶來說觀感很不友好,而且服務器每次都要傳回相同的html文件。很是浪費。有沒有一種方法,只更新變量,不更新html呢?

如果有這種方法,那麼就可以從服務器取得每個週期的結果,然後將html中的值替換一下不就好了,這多簡單。有麼?

當然有,這方法叫做ajax,即“Asynchronous Javascript And XML”。我想說一點,之前爬網易雲音樂時這個xhr搞得我很痛苦,xhr就屬於這個ajax,現在我自己也需要用這個技術了。

如何使用呢?直接上代碼吧:

<script type="text/javascript">
var func;
var insTable;
var res;
var a=1000;
function callcycle(){
$.ajax({
    url:'/cycle',
    type:'POST',
	data:JSON.stringify({'username':'js','psw':'123456789'}),
    dataType: 'json',
    success:function(res){
	console.log(res);
	},
    error:function (res) {
        console.log(0);
    }
})}
var set1=setInterval(callcycle,a);
</script>

在這段js代碼中,我們定義了一個函數,callcycle,這個函數的函數體就是一段ajax,其主要的參數有如下幾個:url,就是ajax要發送請求的地址,type表明該請求的類型爲POST,data爲該請求中要附帶的信息,我其實不需要任何信息,這就是舉個例子。dataType爲信息類型,在服務器端要按該類型解碼。success表示如果發送請求成功,會得到返回結果,我們這裏得到的就是res,然後輸出res;如果發送請求失敗,那麼就輸出0。注意這裏的輸出是按f12後控制檯顯示的輸出,調試用的。

接下來使用setInterval函數調用上面的callcycle函數。setInterval函數是定時調用函數,每過a毫秒就會執行一次callcycle函數。於是就實現了每過相同時間訪問一次url,得到一次返回值。

接下來的問題就是:如何從返回值更新html呢?代碼如下:

$.ajax({
    url:'/cycle',
    type:'POST',
	data:JSON.stringify({'username':'js','psw':'123456789'}),
    dataType: 'json',
    success:function(res){
    insTable=res['ins_Table'];
    func=res['func'];
	reg=res['reg'];
	cyclenum=res['Cycle'];
	//console.log('ins');
	//console.log(insTable);	
	var tb = document.getElementById('T_ins');    // table 的 id
	var rows = tb.rows;                           // 獲取表格所有行
	//console.log(rows[0]);
	//console.log(rows[0][0]);
	//console.log(rows.length);
	for(var i = 1; i<rows.length; i++ ){
		//console.log(i);
		rows[i].cells["instruction"].innerText=insTable[i-1]["instruction"];
		rows[i].cells["target"].innerText=insTable[i-1]["target"];
		rows[i].cells["j"].innerText=insTable[i-1]["j"];
		rows[i].cells["k"].innerText=insTable[i-1]["k"];
		rows[i].cells["issue"].innerText=insTable[i-1]["issue"];
		rows[i].cells["readOperand"].innerText=insTable[i-1]["readOperand"];
		rows[i].cells["exeComplet"].innerText=insTable[i-1]["exeComplet"];
		rows[i].cells["writeResult"].innerText=insTable[i-1]["writeResult"];
		
	}
	var tc = document.getElementById('T_fun');    // table 的 id
	var rowsf = tc.rows;                           // 獲取表格所有行
	for(var i = 1; i<rowsf.length; i++ ){
		rowsf[i].cells["busy"].innerText=func[i-1]["busy"];
		rowsf[i].cells["Op"].innerText=func[i-1]["Op"];
		rowsf[i].cells["Fi"].innerText=func[i-1]["Fi"];
		rowsf[i].cells["Fj"].innerText=func[i-1]["Fj"];
		rowsf[i].cells["Fk"].innerText=func[i-1]["Fk"];
		rowsf[i].cells["Qj"].innerText=func[i-1]["Qj"];
		rowsf[i].cells["Qk"].innerText=func[i-1]["Qk"];
		rowsf[i].cells["Rj"].innerText=func[i-1]["Rj"];
		rowsf[i].cells["Rk"].innerText=func[i-1]["Rk"];
	}
	document.getElementById("F0").innerText = reg["F0"];
	document.getElementById("F1").innerText = reg["F1"];
	document.getElementById("F2").innerText = reg["F2"];
	document.getElementById("F3").innerText = reg["F3"];
	document.getElementById("F4").innerText = reg["F4"];
	document.getElementById("F5").innerText = reg["F5"];
	document.getElementById("F6").innerText = reg["F6"];
	document.getElementById("F7").innerText = reg["F7"];
	document.getElementById("F8").innerText = reg["F8"];
	document.getElementById("F9").innerText = reg["F9"];
	document.getElementById("F10").innerText = reg["F10"];
	document.getElementById("F11").innerText = reg["F11"];
	document.getElementById("cyclenum").innerText = cyclenum;
	//$(".insTable").html(res['ins_Table']);
	//$(".func").html(res['func']);
	//$(".reg").html(res['reg']);
	//console.log(res);
	},
    error:function (res) {
        console.log(0);
        console.log(1);
    }
})}

主要看success裏面的代碼:首先我們從服務器的返回中解析出需要的數據,就和從字典中取出數據一樣,然後使用tb = document.getElementById('T_ins');得到Id爲T_ins的表格,這一步是最重要的,有了這一步,我們就可以對html中的表格爲所欲爲了。使用tb.rows[i].cells[j]可以訪問以類似訪問二維數組的方式訪問表格的各個元素,使用.innerText方法來爲表格中的元素賦值。這樣就實現了表格元素的更新。說起來好像很簡單,但我實現這個功能花費了近兩個小時——我根本不知道要百度的關鍵字是什麼。還好最後摸索出來了。

在後端的代碼如下:

@app.route('/cycle',methods=['POST'])
def next_search():
    session['Cycle']=session['Cycle']+1
    instruction=session['instruction']
    ins_Table,_func,_reg=scoreboard.goto_cycle(session['Cycle'],instruction)
    res=dict()
    res['ins_Table']=ins_Table
    res['func']=_func
    res['reg']=_reg
    res['Cycle']=session['Cycle']
    return  jsonify(res)

服務器監聽/cycle這個url,是ajax要訪問的url。然後把ajax需要的數據打包好,放進字典裏,轉成json傳回去即可。

7、如何自定義刷新頻率?

這需要我們實現另外一個功能,不同於上面的使用js更改html中的值,這裏需要我們輸入來更改js中的值,也就是setInterval的參數a,實際上就是反過來用html更改js。如何實現呢?代碼如下:

<script type="text/javascript">
var set1=setInterval(callcycle,a);
function change(){
    var x=document.getElementById("iptTxt");
	a=parseInt(x.value);
	clearInterval(set1);
	set1=setInterval(callcycle,a);
   }
</script>
<p style="text-align:center;color:#00FF00">Current Cycle Length:</p>
<div style="text-align:center;">
<input type='text' id='iptTxt' onchange="change()" style="width:120px;">
</div>

我們想要一個輸入框,輸入我們想要的週期長度,然後週期就會隨之改變。因此就要有一個input標籤作爲輸入,它有一個屬性onchange,這是幹什麼用的?當input失去焦點,它就會調用js中名爲change()的函數。什麼叫失去焦點?我們要在網頁上輸入一串字符,就要先單擊輸入框,輸入,再單擊輸入框外面。執行完“單擊輸入框外面”這一操作,就叫輸入框失去了焦點。一般來講,不是手滑的話,失去焦點代表着輸入已完成。因此就可以根據輸入來更改a的值了。

要更改a的值,首先要獲取到輸入。使用document.getElementById("iptTxt")函數可以按Id獲取到輸入數據,其value屬性就是數據的值,然後用parseInt將值轉爲int類型,從而可以作爲a的值來用。

一定注意一點:直接修改a的值是沒有效果的。一定要先暫停setInterval,再重新啓動纔可以。暫停Interval使用clearInterval函數,然後再次啓動,使用的就是新的a的值了。

8、利用css定義外觀

我實在是不會設計一個好看的網頁,爲了簡便,就設計成黑底綠字——上世紀的顯示風格了。通過在css中規定不同的類的顯示效果,可以很容易的實現不同元素採用不同的顯示方式,而不用一個一個分別設置。我的css如下:

body {
  font-family:      Verdana, Geneva, Arial, sans-serif;
  font-size:        medium;
  background-color: black;
  margin-top:       5%;
  margin-bottom:    5%;
  margin-left:      10%;
  margin-right:     10%;
  border:           1px dotted #00FF00;
  padding:          10px 10px 10px 10px;
}
a {
  text-decoration:  none; 
  font-weight:      600; 
}
a:hover {
  text-decoration:  underline;
}
a img {
  border:           0;
}
h2 {
  font-size:        150%;
}
table {
  margin-left:      20px;
  margin-right:     20px;
  caption-side:     bottom;
  border-collapse:  collapse;
}
td, th {
  padding:          5px;
  text-align:       left;
}
.copyright {
  font-size:        75%;
  font-style:       italic;
}
.slogan {
  font-size:        75%;
  font-style:       italic;
}
.confirmentry {
  font-weight:      600; 
}
.boxes
{
	font-size:30px; 
	color:#00FF00;
	background-color: black;
	/*width:100%;*/
	margin:0 auto;
	height:500px;
	display:block;
}
.comments {
	font-size:30px; 
	color:#00FF00;
	background-color: black;
	/*width:100%;*/
	margin:0 auto;
	height:250px;
	display:block;
 /*word-break:break-all;/*在ie中解決斷行問題(防止自動變爲在一行顯示,主要解決ie兼容問題,ie8中當設寬度爲100%時,文本域類容超過一行時,當我們雙擊文本內容就會自動變爲一行顯示,所以只能用ie的專有斷行屬性“word-break或word-wrap”控制其斷行)*/
}
/*** Tables ***/

table {
font-size:          1em;
background-color:   #000000;
border:             1px solid #00FF00;
color:              #00FF00;
padding:            5px 5px 2px;
border-collapse:    collapse;
width:75%;
margin:0 auto;
}

td, th {
border:             thin dotted #00FF00;
}

/*** Inputs ***/
input[type=text] {
  /*font-size:        115%;*/
  font-size:30px; 
  background-color:   #000000;
  border:             1px solid #00FF00;
  color:              #00FF00;
  width:            30em;
  margin:0 auto;
}
input[type=submit] {
  /*font-size:        125%;*/
  font-size:30px; 
  background-color:   #000000;
  border:             1px solid #00FF00;
  color:              #00FF00;
  margin:0 auto;
}
select {
  font-size:        125%;
}

很丟人的是,我當時查了很多種實現方法:比如說輸入框如何居中之類,就有了很多的不必要的代碼——但是又不敢刪,誰知道哪句有用哪句沒用呢?這個看看就好了。

四、後端代碼實現

後端代碼主要是監聽上面幾個url,並執行對應函數,返回值即可。

首先是/entry和/這兩個url:

@app.route('/')
@app.route('/entry')
def entry_page() -> 'html':
    session['Cycle']=0
    return render_template('entry.html',the_title='ScoreBoard Algorithm')

在訪問該url時,服務器會在session中建立一個名爲cycle的鍵,用於儲存當前顯示的週期。然後返回entry這個html。

然後是/search4:

@app.route('/search4',methods=['POST'])
def do_search() -> 'html':
    session['Cycle']=session['Cycle']+1
    instructions=request.form['instruction_stream']
    instruction=instructions.split('\n')
    session['instructions']=instructions
    session['instruction']=instruction
    for i in range(0, len(instruction)):
        instruction[i]=instruction[i].replace('\r', '')
    ins_Table,_func,_reg=scoreboard.goto_cycle(session['Cycle'],instruction)
    return render_template('results.html',
                           the_title='Here are your results',
                           Instruction_Stream=instructions,
                           insTable=ins_Table,
                           func=_func,
                           reg=_reg,)

每訪問一次/search4,session中的cycle值就會加一。同時從request中解析出指令流,儲存到session中的instruction中。之後執行scoreboard中的goto_cycle方法,該方法需要兩個參數:目標週期和指令流,將返回在運行到目標週期時的三張表的內容。然後返回result.html和需要的參數即可。

最後是/cycle,這個是ajax需要請求的url:

@app.route('/cycle',methods=['POST'])
def next_search():
    session['Cycle']=session['Cycle']+1
    instruction=session['instruction']
    ins_Table,_func,_reg=scoreboard.goto_cycle(session['Cycle'],instruction)
    res=dict()
    res['ins_Table']=ins_Table
    res['func']=_func
    res['reg']=_reg
    res['Cycle']=session['Cycle']
    return  jsonify(res)

因爲我們最基礎的功能是要實現每一秒刷新一次三張表,表格內容爲以一個週期爲單位進行變化,而生成新的表格需要兩個參數:目標週期和指令流。因此我們就需要在客戶端和服務器保持連接時記錄下這兩個值。並每訪問一次cycle,值就會更新。對於不同的客戶端,這個值是不同的。那麼我們有三種方式實現:

①、用cookie,將這兩個參數保存在瀏覽器的cookie中,在ajax的data中傳回;

②、用js,直接在瀏覽器中運行遞增;

③、用session,在客戶端和服務器的會話不斷開時保存它們的數據。

我選擇了第三種。那麼事情就很簡單了。

表觀上是每隔一週期刷新一次表格,實際上在後臺運行goto_cycle,參數從1,到2,3,4,5,這樣。缺點就是當指令很多、週期很長的時候運行會較爲緩慢。但這是goto_cycle自己的問題,和我們的邏輯沒有關係。

五、總結

第一次自己寫網頁,進行前後端的交互,感覺還是很好玩的。最開始以爲HTML中有變量——那就隨便給變量賦一下值就能更新表格了。然後才發現不是這樣。HTML是靜態語言,沒有變量。想要更改它的值,需要js的幫助。於是又去學js,要用ajax才能實現網頁數據的部分刷新。又去學ajax怎麼用。實現基本功能之後又開始想:能不能讓網頁更好看點?就去搞CSS來自定義網頁的顏色、大小、位置什麼的。網頁畫好了又開始蠢蠢欲動:如果我能修改刷新週期就好了,於是就去學如何用onchange實現輸入的捕捉。每樣東西都學了一點皮毛,積累了一點經驗。

總的來說,我主要學習並使用了以下幾個功能:第一個是通過js更改網頁(html)中的數據,第二個是通過向網頁(html)輸入數據更改js的數據,第三個是重新理解了瀏覽器是如何與服務器進行通信的,第四個是學習了ajax的使用。這些功能很基礎又很重要,有了它們,之後自己再想寫一點東西就會容易一些。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章