第4次作业-结对编程之实验室程序实现
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzu/SE2020 |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/SE2020/homework/11277 |
这个作业的目标 | 根据输入的文本内容在网页页面上呈现树形结构形式的师门树 |
学号 | 031802507 031802509 |
具体分工
031802507 邓家俊 : HTNML 、 CSS
031802509 胡启华 : Javascript 、单元测试 、HTML 、CSS
PSP表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 10 |
Estimate | 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | 720 | 1710 |
Analysis | 需求分析 (包括学习新技术) | 180 | 360 |
Design Spec | 生成设计文档 | 30 | 60 |
Design Review | 设计复审 | 30 | 30 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
Design | 具体设计 | 30 | 60 |
Coding | 具体编码 | 300 | 900 |
Code Review | 代码复审 | 60 | 90 |
Test | 测试(自我测试,修改代码,提交修改) | 60 | 180 |
Reporting | 报告 | 90 | 90 |
Test Report | 测试报告 | 30 | 30 |
Size Measurement | 计算工作量 | 30 | 30 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 820 | 1810 |
一、解题思路描述和设计实现说明
解题思路描述
在解题之前先去了解并学习了必要的知识
在学习完相关知识,再次阅读题目要求后,决定在 web 页面中实现以下功能
-
具有输入文本框。输入文本之后能将文本解析并且保存为可用的数据。
-
具有一个导师列表。导师列表中显示所有从文本中提取出的导师的名字,点击导师的名字可以显示各个导师的师门树。
-
可以从文本解析后生成的数据中读取需要的信息并且生成师门树
流程图
设计实现说明
1、文本解析功能
submit 函数是点击 web 界面内输入文本框下方的提交按钮后执行的函数,
它先对字符串类型的输入文本用 split 函数,以换行符 “\n” 标志将原始的字符串分割成一条条便于处理的字符串并保存在数组内。
之后遍历数组读取各个字符串,提取出其中的关键字,并根据关键字的不同进行不同的操作。
这个函数同时还具有检测文本格式错误的功能,如果输入的格式错误会中止读取并提醒用户。
具体代码如下:
var teacher = {};
var student = {};
function submit() {
var list = document.getElementById("text").value.split("\n");
for(var i = 0;i<list.length;i++)
{
var str = list[i];
//如果此行中包含“导师”关键字,则连续读取下一行直到读取到空行为止
if(str.includes("导师"))
{
var teacher_name = str.substring(str.indexOf(":")+1);
if(!teacher[teacher_name])
teacher[teacher_name] = {};
i += 1;
//当此行不为空的时候执行
while(list[i]!==""&&i<list.length)
{
//"级博士生:","级硕士生:","级本科生:"
var stu_msg = list[i];
if(stu_msg.includes("级博士生:")||stu_msg.includes("级硕士生:")||stu_msg.includes("级本科生:"))
{
//获取学生的类型
var type = stu_msg.substring(0,stu_msg.indexOf(":"));
//获取学生姓名的数组
var stu_list = stu_msg.substring(stu_msg.indexOf(":")+1).split("、");
//如果目前的数据中这个导师没有这个类型的学生,则直接将数组赋值
if(!teacher[teacher_name].hasOwnProperty(type))
teacher[teacher_name][type] = stu_list;
//如果目前的数据中这个导师有这个类型的学生,则将学生数组中的学生插入到已有的数组中,同时过滤掉同名学生
else
{
var pre_stu_list = teacher[teacher_name][type];
for(var stu_id = 0;stu_id<stu_list.length;stu_id++)
{
var flag = 0;
var now_stu = stu_list[stu_id];
for(var pre_stu_id = 0;pre_stu_id<pre_stu_list.length;pre_stu_id++)
{
if(pre_stu_list[pre_stu_id]===now_stu)
{
console.log(now_stu);
flag = 1;
break;
}
}
if(flag===0)
pre_stu_list[pre_stu_list.length] = now_stu;
}
}
}
//如果这行不是空行且内容中不包含"级博士生:","级硕士生:","级本科生:"等关键字,说明有其他信息混进人员信息了,格式错误
else{
show_menu();
alert("输入格式错误!\n若有多组输入,中间空一行。上半部分是人员信息,下半部分是技能树或所在公司历程。\n输入正确的部分已经记录完成")
console.log(teacher);
return;
}
i++;
}
}
//如果该行没有“导师”关键字但是又包含"级博士生:","级硕士生:","级本科生:"等关键字,说明前面有多余的空格,格式错误
else if(str.includes("级博士生:")||str.includes("级硕士生:")||str.includes("级本科生:"))
{
show_menu();
alert("输入格式错误!\n人员信息和导师信息之间不能存在空行\n输入正确的部分已经记录完成");
return;
}
//如果该行不包含“导师”,"级博士生:","级硕士生:","级本科生:"等关键字,说明改行表示的是技能树或公司历程,将数据读取到 student 中
else if(str.length)
{
//获取学生姓名
var stu_name = str.substring(0,str.indexOf(":"));
//获取学生技能树或公司历程
var skill_list = str.substring(str.indexOf(":")+1).split("、");
//如果 student 中没有该学生的记录的话就直接在 student 中创建一个新的元素,key 为学生名, value 为技能树或公司历程
if(!student.hasOwnProperty(stu_name))
student[stu_name] = skill_list;
//如果存在,则将新的技能树或公司历程数组插入到原有的数组中,并过滤已存在的元素。
else
{
var pre_skill_list = student[stu_name];
for(var skill_id = 0;skill_id<skill_list.length;skill_id++)
{
var flag = 0;
var now_skill = skill_list[skill_id];
for(var pre_skill_id = 0;pre_skill_id<pre_skill_list.length;pre_skill_id++)
{
if(pre_skill_list[pre_skill_id]===now_skill)
{
flag = 1;
break;
}
}
if(flag===0)
pre_skill_list[pre_skill_list.length] = now_skill;
}
}
}
}
//menu显示
show_menu();
console.log(teacher);
console.log(student);
}
2、事件处理函数
整个页面需要根据鼠标事件来不断修改页面显示的内容,而鼠标点击的位置便是 HTML 文件中各个节点的位置。
需要对每个节点添加相应的 HTML 事件和出发事件时处理的函数
在本次的作业中,对于各个节点的点击缩放是通过增减子节点的数量实现的。在点击一个师门树的节点后,如果该节点没有展开,则执行的函数会根据这个节点的信息从保存好的数据中提取出需要的子节点的信息,并在该节点下插入子节点。如果该节点已经展开,则执行的函数会删除该节点下所有的子节点。通过这种形式实现节点的缩放
根据不同的导师切换不同的师门树也是通过和上述类似的方法实现地。
具体代码如下:
//显示导师列表
function show_menu(){
//menu显示
var menu = document.getElementById("menu");
//在属性为 menu 的节点下创建子节点,节点的文本内容为各个导师的名字,并添加各种事件
for(var item in teacher){
if(teacher[item]["in_menu"]!=="true")
{
var new_node = document.createElement("div");
new_node.innerHTML = item;
new_node.setAttribute("id","menu_name");
new_node.setAttribute("onclick","menu_name_click(this.innerHTML,this)");
new_node.setAttribute("onmouseover","menu_name_over(this)");
new_node.setAttribute("onmouseout","menu_name_out(this)");
new_node.setAttribute("chose","no");
menu.appendChild(new_node);
teacher[item]["in_menu"] = "true";
}
}
var tree = document.getElementById("tree");
while(tree.children.length!==0)
tree.removeChild(tree.children[0]);
}
//鼠标移动到 menu 上方时触发的事件
function menu_name_over(obj) {
if(obj.getAttribute("chose")==="no")
obj.setAttribute("style","background-color:white;");
}
//鼠标离开 menu 上方时触发的事件
function menu_name_out(obj) {
if(obj.getAttribute("chose")==="no")
obj.setAttribute("style","background-color:lightsteelblue;")
}
//点击 menu 中的导师名字所触发的事件
function menu_name_click(name,obj){
//阻止事件冒泡
event.stopPropagation();
//根据点击的节点的属性得出导师的名字,并以这个名字为文本内容在 tree 中创建新的子节点,并为子节点添加各种点击事件
var tree = document.getElementById("tree");
while(tree.children.length!==0)
tree.removeChild(tree.children[0]);
var new_teacher_node = document.createElement("div");
var inner = document.createElement("div");
inner.innerHTML = name;
inner.setAttribute("onclick","ShowType(this)");
inner.setAttribute("id","teacher_name");
new_teacher_node.appendChild(inner);
new_teacher_node.setAttribute("id","teacher_area");
new_teacher_node.setAttribute("name",name);
tree.appendChild(new_teacher_node);
//处理 menu 菜单的样式变化
var menu_name = document.getElementById("menu").children;
console.log(menu_name);
for(var i = 0;i<menu_name.length;i++)
{
menu_name[i].setAttribute("style","background-color:lightsteelblue;");
menu_name[i].setAttribute("chose","no");
}
obj.setAttribute("style","background-color:aqua;");
obj.setAttribute("chose","yes;");
}
//点击导师名触发的事件
function ShowType(obj) {
//阻止事件冒泡
event.stopPropagation();
//根据该节点缩放状态进行不同的操作
if(obj.getAttribute("open")==="yes")
{
obj.setAttribute("open","no");
obj.removeAttribute("style");
var parent = obj.parentNode;
while(parent.children.length>1)
parent.removeChild(parent.children[1]);
}
else
{
//通过该节点的属性获取到导师的名字,根据导师名字在 teacher 中获取到学生类型的数组,并以每一个学生类型为文本内容在该节点下创建子节点
obj.setAttribute("open","yes");
obj.setAttribute("style","border-left:aqua 5px solid");
var stu_type_list = teacher[obj.innerHTML];
if(stu_type_list.length===0)
return;
for(var type_name in stu_type_list)
{
if(type_name==="in_menu")
continue;
var new_type_node = document.createElement("div");
var inner = document.createElement("div");
inner.innerHTML = type_name;
inner.setAttribute("onclick","ShowStudent(this)");
new_type_node.appendChild(inner);
new_type_node.setAttribute("teacher",obj.parentNode.getAttribute("name"));
new_type_node.setAttribute("type",type_name);
new_type_node.setAttribute("id","type");
obj.parentNode.appendChild(new_type_node);
}
}
}
//点击学生类型节点触发的事件
function ShowStudent(obj){
//阻止事件冒泡
event.stopPropagation();
//根据该节点缩放状态进行不同的操作
if(obj.getAttribute("open")==="yes")
{
obj.setAttribute("open","no");
obj.removeAttribute("style");
var parent = obj.parentNode;
while(parent.children.length>1)
parent.removeChild(parent.children[1]);
}
else
{
//根据该节点的属性获取到导师和学生类型的信息,并根据这两个数据获取到一个学生列表,以这个学生列表内各个学生的名字为文本内容创建新的节点
obj.setAttribute("open","yes");
obj.setAttribute("style","border-left:aqua 5px solid");
var stu_list = teacher[obj.parentNode.getAttribute("teacher")][obj.innerHTML];
//console.log(stu_list);
if(stu_list.length===0)
return;
for(var i=0;i<stu_list.length;i++)
{
var stu_name = stu_list[i];
var new_stu_node = document.createElement("div");
var inner = document.createElement("div");
inner.innerHTML = stu_name;
inner.setAttribute("onclick","ShowSkill(this)");
new_stu_node.appendChild(inner);
new_stu_node.setAttribute("name",stu_name);
new_stu_node.setAttribute("id","student");
new_stu_node.setAttribute("teacher",obj.parentNode.getAttribute("teacher"));
new_stu_node.setAttribute("type",obj.parentNode.getAttribute("type"));
obj.parentNode.appendChild(new_stu_node);
}
}
}
//点击学生节点触发的事件
function ShowSkill(obj) {
//阻止事件冒泡
event.stopPropagation();
//根据该节点缩放状态进行不同的操作
if(obj.getAttribute("open")==="yes")
{
obj.setAttribute("open","no");
obj.removeAttribute("style");
var parent = obj.parentNode;
while(parent.children.length>1)
parent.removeChild(parent.children[1]);
}
else
{
//根据该节点的属性获取到学生名字,并根据学生名字在 student 中获取到该学生的技能树和公司历程列表,以这个列表内各个学生的名字为文本内容创建新的节点
obj.setAttribute("open","yes");
obj.setAttribute("style","border-left:aqua 5px solid;");
var skill_list = student[obj.innerHTML];
if(!skill_list||skill_list.length===0)
return;
for(var i=0;i<skill_list.length;i++)
{
var skill_name = skill_list[i];
var new_skill_node = document.createElement("div");
new_skill_node.innerHTML = skill_name;
new_skill_node.setAttribute("name",skill_name);
new_skill_node.setAttribute("open","no");
new_skill_node.setAttribute("id","skill");
new_skill_node.setAttribute("onclick","f()");
obj.parentNode.appendChild(new_skill_node);
}
}
}
//点击技能树或公司历程触发的事件
function f(){event.stopPropagation();}
3、HTML 页面
使用了 Bootstrap 框架进行编写
具体代码如下:
<!DOCTYPE html>
<html>
<head>
<title>Title</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./index.js"></script>
<link rel="stylesheet" type="text/css" href="./index.css">
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<div id="con"><img id="pic" src="./img.jpg"></div>
<div class="container" id="main">
<div class="row">
<div class="col-sm-4" id="input_area">
<div id="input_head" class="col-sm-12">输入数据</div>
<textarea id="text" class="col-sm-12">请输入数据</textarea>
<div class="col-sm-12" id="space"></div>
<button id="button" class="col-sm-12" onclick="submit()">提交</button>
</div>
<div class="col-sm-2" id="menu_area">
<div id="menu_head">导师列表</div>
<div id="menu" ></div>
</div>
<div class="col-sm-4" id="tree_area">
<div id="tree_head" class="col-sm-12">树形图</div>
<div id="tree" class="col-sm-12"></div>
</div>
</div>
</div>
</body>
</html>
3、CSS
具体代码如下
body{ margin:0; padding:0;}
#pic{ width:100%;}
#main{
position: absolute;
//border: aqua 5px solid;
width: 100%;
top: 10%;
}
#space{
height: 29px;
}
#text{
width: 100%;
height: 290px;
}
#input_head{
text-align: center;
//border-top: gold 5px solid;
background-color: lightsteelblue;
}
#input_area{
background-color: lightsteelblue;
border-top: blue 5px solid;
margin: 2.6%;
}
#button{
width: 100%;
margin-bottom: 29px;
}
#menu_head{
text-align: center;
}
#menu_area{
border-top: blue 5px solid;
background-color: lightsteelblue;
margin: 2.6%;
}
#menu{
background-color: lightsteelblue;
}
#menu_name{
margin-bottom: 5px;
}
#tree_area{
background-color: lightsteelblue;
border-top: blue 5px solid;
margin: 2.6%;
}
#tree_head{
text-align: center;
background-color: lightsteelblue;
}
#teacher_name{
position: relative;
left: -15px;
}
#type{
margin: 5px;
position: relative;
left: 29px;
}
#student{
margin: 5px;
position: relative;
left: 29px;
}
#skill{
margin: 5px;
position: relative;
left: 29px;
}
二、附加特点设计与展示
附加特点设计
- 可以多次提交文本并新增树和节点,但不会创建重复的树和节点
- 意义:可以不断添加新的导师和新的师门树信息,导师列表和每个导师对应的师门树也会随之增添新的树和节点。
- 实现思路:
- 将从文本中读取到的信息都存储在 teacher 和 student 对象中,导师列表和师门树的数据都从 teacher 和 student 中获取。
- 在将读取到的文本信息存入对象中之前先检查这个信息是否已经存在于对象中,如果不存在则插入新的信息,如果存在则跳过该信息。
- 可以检测到文本输入格式错误,并提示用户
- 意义:防止格式错误导致存入不符合要求的信息
- 实现思路:
- 在读取每一条文本信息的时候都先检查格式是否符合要求,如果不符合要求便中止读取,并弹出窗口提示用户格式错误的种类。
- 在错误格式出现之前的数据能够正常存入对象中。
实现功能的代码片段
这两个功能是在读取文本的时候同时进行的,所以也包括在上述的 submit 函数中。
实现成果展示
正常输入
错误输入
三、目录说明和使用说明
目录说明
031802507-031802509
index.html
index.js
index.css
fun.js
fun.test.js
使用说明
- 从仓库中下载文件
git clone https://github.com/Victorique0/031802507-031802509.git
-
解压后选择用谷歌浏览器打开文件中的 index.html 文件即可打开页面
-
在文本框输入文本信息后点击提交按钮即可提交数据
-
提交的文本会被处理成有效的数据并存储在对象中,导师列表会显示对象中所有导师的名字
-
点击导师的名字即可显示这个导师的师门树
-
点击师门树的各个节点便可以进行缩放
-
有需要的话可以继续输入文本添加更多的信息,页面会读取文本并忽略重复的数据,将新的数据存入对象中,导师列表和各个导师的师门树也会相应更新
四、单元测试
单元测试工具
使用 Mocha 进行单元测试
单元测试代码
进行单元测试的函数是对文本输入进行处理并将数据存入对象中的函数,单元测试代码检测函数产生的对象是否和应该得出的正确的对象相同
fun.js
function submit(msg,input_teacher,input_student) {
var teacher = {};
var student = {};
var list = msg.split("\n");
var error_type = 0;
//这部分的内容与 index.js 中的 submit 函数的主体几乎完全一致,只进行了极少量的修改,因为篇幅过长就省略了
//详细内容可以在 https://github.com/Victorique0/031802507-031802509/blob/main/fun.js 中查看
// 验证 teacher
if(teacher.length!==input_teacher.length||student.length!==input_student.length)
return 0;
for(var teacher_name in teacher)
{
if(!input_teacher.hasOwnProperty(teacher_name))
{
console.log("teacher name error\n");
return 0;
}
var type = teacher[teacher_name];
var input_type = input_teacher[teacher_name];
//console.log(type);
//console.log(input_type);
if(type.length!==input_type.length)
{
console.log("type length error\n");
return 0;
}
for(var type_name in type)
{
if(input_teacher.hasOwnProperty(type_name))
{
//console.log(type_name);
console.log("type name error\n");
return 0;
}
var stu_list = type[type_name];
var input_stu_list = input_type[type_name];
if(stu_list.length!==input_stu_list.length)
{
console.log("stu_list length error");
return 0;
}
for(var item = 0;item<stu_list.length;item++)
{
var now_stu = stu_list[item];
for(var input_item = 0;input_item<input_stu_list.length;input_item++)
{
var flag = 0;
var now_input_stu = input_stu_list[input_item];
if(now_stu!==now_input_stu)
continue;
if(now_stu===now_input_stu)
{
flag = 1;
break;
}
}
if(flag===0)
{
console.log("student name error in teacher list\n");
return 0;
}
}
}
}
//验证 student
if(student.length!==input_student.length)
{
console.log("student skill length error\n");
return 0;
}
if(student.length===0)
return 1;
for(var stu_name in student) {
if (!input_student.hasOwnProperty(stu_name))
{
console.log("student name error in student list\n");
return 0;
}
if(student[stu_name].length!==input_student[stu_name].length)
{
console.log("skill length error\n");
return 0;
}
for(var skill_name_item = 0;skill_name_item<student[stu_name].length;skill_name_item++)
{
var now_skill = student[stu_name][skill_name_item];
for(var input_skill_name_item = 0;input_skill_name_item<input_student[stu_name].length;input_skill_name_item++)
{
var flag = 0;
if(input_student[stu_name][input_skill_name_item]!==now_skill)
continue;
if(input_student[stu_name][input_skill_name_item]===now_skill)
{
flag = 1;
break;
}
}
if(flag===0)
{
console.log("skill name error\n");
return 0;
}
}
}
return 1;
}
module.exports = submit;
fun.test.js
var submit = require('./fun.js');
var expect = require('chai').expect;
//这部分的内容的是六次测试中的输入文本和函数处理后对应的正确结果,篇幅过长便省略了,可以在 https://github.com/Victorique0/031802507-031802509/blob/main/fun.test.js 中查看
describe('文本处理函数测试1(标准输入)', function() {
it('函数处理文本后得到的结果和正确结果相一致', function() {
expect(submit(input1,result_teacher1,result_student1)).to.be.equal(1);
});
});
describe('文本处理函数测试2(标准输入)', function() {
it('函数处理文本后得到的结果和正确结果相一致', function() {
expect(submit(input2,result_teacher2,result_student2)).to.be.equal(1);
});
});
describe('文本处理函数测试3(多组标准输入,每组的导师不同)', function() {
it('函数处理文本后得到的结果和正确结果相一致', function() {
expect(submit(input3,result_teacher3,result_student3)).to.be.equal(1);
});
});
describe('文本处理函数测试4(多组标准输入,每组的导师相同)', function() {
it('函数处理文本后得到的结果和正确结果相一致', function() {
expect(submit(input4,result_teacher4,result_student4)).to.be.equal(1);
});
});
describe('文本处理函数测试5(输入错误格式的文本:人员信息和技能树之间没有空行)', function() {
it('函数处理文本后得到的结果和正确结果相一致', function() {
expect(submit(input5,result_teacher5,result_student5)).to.be.equal(1);
});
});
describe('文本处理函数测试6(输入错误格式的文本:导师信息和各级学生信息之间多了空行)', function() {
it('函数处理文本后得到的结果和正确结果相一致', function() {
expect(submit(input6,result_teacher6,result_student6)).to.be.equal(1);
});
});
构造测试数据的思路
测试数据的类型
-
正常单次输入
-
正常多次输入,不输入重复的数据
-
正常多次输入,但存在重复的数据
-
异常输入,人员信息和技能树之间没有空行
-
异常输入,导师信息和各级学生信息之间多了空行
测试结果
五、Github 代码签入记录
六、遇到的代码模块异常或结对困难及解决方法
遇到的问题
-
最开始想要将所有信息都存在一个对象中,但实现的过程中发现每读取一次学生的信息都要遍历整个对象,向每一个导师的学生信息中找到这个学生并添加技能树和公司经历,流程过于复杂。而且写完之后多次检验发现都得不到正确的结构,修改了一整个晚上都没能改好。最后决定放弃原来的设计,将导师-学生类型-学生名单 和 学生-技能树或公司经历 分别存放在 teacher 和 student 中,师门树在读取学生技能树或公司经历时从 student 中根据学生名获取信息。重新编写后降低了 submit 函数和显示学生技能树或公司经历的事件函数 ShowSkill 的复杂性并能够正确实现功能。
-
使用 this 无法获取到当前节点的信息
- 解决方法:重新学习 this 在不同情况向所指向的对象,了解 this 的正确用法
-
addEventListener 参数中只能输入给节点添加的函数的名称,导致无法输入节点要添加的函数的参数
- 解决方法:使用 setAttribute("onclick","fun(arg)") 向节点添加事件和时间出发后执行的函数。
-
页面缩放会导致各个节点重叠
- 解决方法:学习了 Bootstrap 框架的网格系统,让页面能够适应不同的缩放比例并正常显示各个节点
收获
在解决问题的过程中查看了大量相关资料,学习到了很多的新知识