软工作业一——基于HTML、CSS、JS实现的简易计算器

这个作业属于哪个课程2301-计算机学院-软件工程
这个作业要求在哪里软件工程实践第一次作业
这个作业的目标实现一个简易的计算器

目录

界面展示

GitCode 项目地址

PSP表格 

解题思路描述

问题1—— 如何测试代码的覆盖率?     

问题2 ——nodejs项目如何打包成exe文件?

接口设计和实现过程

关键代码展示

单元测试

性能改进

异常处理

心得体会


界面展示

GitCode 项目地址

        My_calculator

PSP表格 

PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划2020
• Estimate• 估计这个任务需要多少时间2020
Development开发12401350
• Analysis• 需求分析 (包括学习新技术)4030
• Design Spec• 生成设计文档200240
• Design Review• 设计复审2020
• Coding Standard• 代码规范 (为目前的开发制定合适的规范)2020
• Design• 具体设计3030
• Coding• 具体编码720800
• Code Review• 代码复审3030
• Test• 测试(自我测试,修改代码,提交修改)180180
Reporting报告5040
• Test Repor• 测试报告2015
• Size Measurement• 计算工作量105
• Postmortem & Process Improvement Plan• 事后总结, 并提出过程改进计划2020
合计13101410

解题思路描述

        本人对后端开发较为熟悉,这次作业希望挑战前端语言,故选择了HTML、CSS、Javascript前端三件套,因有一些基础,故跳过了语言学习这一阶段,具体思路如下:

  1. 先编写HTML代码搭出计算器网页界面的大体架构,再添加css的效果美化界面
  2. 编写js文件实现界面的交互动作
  3. js实现计算的具体逻辑
  4. 编写测试代码并进行代码优化

问题1—— 如何测试代码的覆盖率?     

  • 通过上网查阅资料发现,测试js代码覆盖率的工具有QUnit、Jasmine、JUnit、Jest等等,考虑到Jest较为轻便且操作容易,故选择Jest

在编写测试用例时发现,Jest不支持ES6的语法,于是引入babel转义支持,通过babel进行,编译,使其变成node的模块化代码

npm install @babel/core@7.4.5 @babel/preset-env@7.4.5 -D

在项目根目录新建.babelrc文件,并写入

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}

问题2 ——nodejs项目如何打包成exe文件?

  • Electron是使用JS、HTML和CSS构建跨平台的桌面应用程序框架

导入Electron和Electron builder

npm install electron --save-dev
npm install electron-builder --save-dev

 Electron不兼容ES6,在package.json中配置 "type": "commonjs", 但在main.js的入口程序中,有使用到ES6相关语法,需要通过babel转换生成兼容ES5的代码

.babelrc文件配置如下

//es6转es5使用
{
  "presets": [
    "es2015"
  ],
  "plugins": []
}

 package.json配置如下

  "scripts": {
    //将src目录下的.js文件转换为es5并将其保存在static目录下
    "dev":"babel src --out-dir staic",
    "start": "electron .",
    "test": "jest",
    "dist": "electron-builder",
    "coverage": "jest --coverage"
  },

运行 npm run dev  即可生成es5文件

接口设计和实现过程

  •  界面展示和计算逻辑代码解耦合

为实现代码的解耦合,将代码划分为三个部分

        1. HTML负责网页的架构,如计算器的结构(输入输出显示屏、按键面板等)

        2. CSS负责网页的样式与美化(计算器的各部分位置、大小、形状、颜色等)

        3. JS负责网页的行为(点击按键触发的事件,何时开始计算、何时显示\清空结果等)

三部分依次完成,其中,HTML文件除了开头调用css、js文件外,不嵌入任何css、js代码 

    <script src="calculator.js"></script>
    <link rel="stylesheet" href="calculator.css">
  •  计算过程的实现

由用户按下“=”键触发计算表达式事件,调用getRes()函数

已知用户输入的字符串为中缀表达式,故利用栈,将中缀表达式转为后缀表达式计算,其中sin、cos、tan的优先级等同(),可将其用<>、[]、{}代替,视同()

button.addEventListener("click", function () {
            ......
            if (value === "=") {
                res = transStr(block1.innerHTML);
                try {
                    block2.innerHTML = getRes(res);
                } catch (e) {
                    alert(e);
                    clear(res);
                    block1.innerHTML = "";
                }
            }
            ......
        });

关键代码展示

  • 事件的触发与函数的调用
// 为每个按钮添加点击事件监听器
    buttons.forEach(function (button) {
        button.addEventListener("click", function () {
            let value = button.id;
            if (value === "C") {
                clear(res);
                block1.innerHTML = "";
                block2.innerHTML = "";
            } else if (value === "d") {
                block1.innerHTML = block1.innerHTML.slice(0, -1);
            } else if (value === "=") {
                res = transStr(block1.innerHTML);
                try {
                    block2.innerHTML = getRes(res);
                } catch (e) {
                    alert(e);
                    clear(res);
                    block1.innerHTML = "";
                }
            } else {
                block1.innerHTML += value
            }
        });
    });
  • 表达式计算逻辑
function getRes(res) {
    for (let i = 0; i < res.length; i++) {
        switch (res[i]) {
            case "{":
            case "[":
            case "<":
            case "(":
                opa_stack.push(res[i]);
                break;
            case "}":
                let t6;
                while (opa_stack.length > 0 && (t6 = opa_stack.pop()) !== "{") {
                    opa_stack.push(t6);
                    popCalculator();
                }
                let temp1 = data_stack.pop();
                data_stack.push(Math.sin(temp1));
                break;
            case "]":
                let t7;
                while (opa_stack.length > 0 && (t7 = opa_stack.pop()) !== "[") {
                    opa_stack.push(t7);
                    popCalculator();
                }
                let temp2 = data_stack.pop();
                data_stack.push(Math.cos(temp2));
                break;
            case ">":
                let t8;
                while (opa_stack.length > 0 && (t8 = opa_stack.pop()) !== "<") {
                    opa_stack.push(t8);
                    popCalculator();
                }
                let temp3 = data_stack.pop();
                data_stack.push(Math.tan(temp3));
                break;
            case ")":
                let t1;
                while (opa_stack.length > 0 && (t1 = opa_stack.pop()) !== "(") {
                    opa_stack.push(t1);
                    popCalculator();
                }
                break;
            case "+":
            case "-":
                if (i === 0 ||
                    (res[i] === "-" && isNaN(res[i - 1]) &&
                        res[i - 1] !== ")" && res[i - 1] !== "}" && res[i - 1] !== "]" && res[i - 1] !== ">")) {
                    let [num, size] = getNum(res, i);
                    data_stack.push(num);
                    i += size - 1;
                } else {
                    while (opa_stack.length > 0) {
                        let t3 = opa_stack.pop();
                        opa_stack.push(t3);
                        if (t3 !== "(" && t3 !== "{" && t3 !== "[" && t3 !== "<") {
                            popCalculator();
                        } else {
                            break;
                        }
                    }
                    opa_stack.push(res[i]);
                }
                break;
            case "*":
            case "/":
            case "%":
                while (opa_stack.length > 0) {
                    let t2 = opa_stack.pop();
                    opa_stack.push(t2);
                    if (t2 === "*" || t2 === "/" || t2 === "%" || t2 === "^") {
                        popCalculator();
                    } else {
                        break;
                    }
                }
                opa_stack.push(res[i]);
                break;
            case "^":
                opa_stack.push(res[i]);
                break;
            default:
                if (!isNaN(res[i])) {
                    let [num, size] = getNum(res, i);
                    data_stack.push(num);
                    i += size - 1;
                } else {
                    throw "expression error";
                }
                break;
        }
    }
    while (data_stack.length >= 2 && opa_stack.length >= 1) {
        popCalculator();
    }
    if (data_stack.length === 1 && opa_stack.length === 0) {
        return data_stack.pop();
    }
    throw "expression error";
}

单元测试

  • 采用Jest进行测试,代码中,getRes()函数用于计算中缀表达式的值,而popCalculator()函数进行一次的计算,由getRes()多次调用,二者逻辑较复杂,且调用次数多,为测试的重点对象
  • 数据构建——在一个测试里测试尽可能多的分支,分别测试了在输入复杂表达式、表达式除数为0、表达式语法错误的情况下的值
import {getRes} from './calculate'
import { transStr } from './calculator'

test('test calculate', () => {
    let res1 = "-1+2*3/2+sin(-1*cos(0+0)%1-tan(1.5-1.5*2))*8^(9-0)^-1+2*2^1";
    res1 = transStr(res1);
    expect(getRes(res1)).toBe(7.259116142081281);
});
test('test exception1', () => {
    let res2 = "-1/0";
    res2 = transStr(res2);
    expect(() => getRes(res2)).toThrow("divide by zero");
});
test('test exception2', () => {
    let res3 = "-1*()))";
    res3 = transStr(res3);
    expect(() => getRes(res3)).toThrow("expression error");
});
  • 测试结果 (html报告和命令行)

性能改进

  • 在单元测试的过程中,发现覆盖率一直不变,有几行代码从未被执行,经检查发现是冗余代码,遂删去

  •  使用JS的Math库计算表达式,提高运算的效率
let temp2 = data_stack.pop();
                data_stack.push(Math.cos(temp2));
  •  JS前端语言的计算精度不够(如图),写函数提高计算精度

 通过以下一系列函数,提高js的计算精度

function accDiv(arg1, arg2) {
    var t1 = 0, t2 = 0, r1, r2;
    t1 = arg1.toString().split(".")[1].length;
    t2 = arg2.toString().split(".")[1].length;
    with (Math) {
        r1 = Number(arg1.toString().replace(".", ""));
        r2 = Number(arg2.toString().replace(".", ""));
        return (r1 / r2) * Math.pow(10, t2 - t1);
    }
}

function accAdd(arg1, arg2) {
    var r1, r2, m, c;
    try {
        r1 = arg1.toString().split(".")[1].length;
    }catch (e) {
        r1 = 0;
    }
    try {
        r2 = arg2.toString().split(".")[1].length;
    }catch (e) {
        r2 = 0;
    }
    c = Math.abs(r1 - r2);
    m = Math.pow(10, Math.max(r1, r2));
    if (c > 0) {
        var cm = Math.pow(10, c);
        if (r1 > r2) {
            arg1 = Number(arg1.toString().replace(".", ""));
            arg2 = Number(arg2.toString().replace(".", "")) * cm;
        } else {
            arg1 = Number(arg1.toString().replace(".", "")) * cm;
            arg2 = Number(arg2.toString().replace(".", ""));
        }
    } else {
        arg1 = Number(arg1.toString().replace(".", ""));
        arg2 = Number(arg2.toString().replace(".", ""));
    }
    return (arg1 + arg2) / m;
}

function accMul(arg1, arg2) {
    var m = 0, s1 = arg1.toString(), s2 = arg2.toString();
    m += s1.split(".")[1].length;
    m += s2.split(".")[1].length;
    return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m);
}

 

异常处理

分为除数为0和表达式不合规范两种

通过throw new Error的方式抛出

throw new Error("expression error");
throw new Error("divide by zero");

 在$Function(){}中统一catch,通过弹出提示框通知用户,删除计算器缓存的所有数据

try {
    block2.innerHTML = getRes(res);
} catch (e) {
    alert(e);
    clear(res);
    block1.innerHTML = "";
}

心得体会

        在软件工程的课程中,我开发了一个简易的基于前端三件套的计算器页面,实现了基本运算和科学计算。在完成项目的过程中,我对HTML、CSS、JavaScript的掌握更进了一步,也更加了解了软件开发的基本流程。

        在开发过程中,我了解到制定计划的重要性和独立思考的重要性。首先,在项目的初始阶段,有一个确切的方向是关键,要通过搜索引擎或ai助手明确这个项目需要用的的技术,再查找关于该技术的资料,明确接下来的每一个阶段具体要做哪些工作,在项目刚开始的阶段,我没有了解关于前端开发的相关流程,误入歧途,在无关紧要的内容上耗费了很多时间。

        而进入开发过程后,难免会遇到各式各样的bug,我曾多次在前端的各类开发工具上因版本的缘故出错,在通过查找国内外博客和github上文档对版本和各类函数运用的说明后,逐一解决了问题,虽然过程极为艰辛,但也培养了我耐心阅读开发文档的能力。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值