一、概述
寫這個網頁的最初動機是我的一門課需要圖形界面實現,時間有點緊,去學習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的使用。這些功能很基礎又很重要,有了它們,之後自己再想寫一點東西就會容易一些。