一、概述
写这个网页的最初动机是我的一门课需要图形界面实现,时间有点紧,去学习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的使用。这些功能很基础又很重要,有了它们,之后自己再想写一点东西就会容易一些。