【前端】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的使用。这些功能很基础又很重要,有了它们,之后自己再想写一点东西就会容易一些。

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