从无到有实现一个网页计算器
一、需求:
大概的需求就是制作一个简易版的仿OSX系统的计算器,主要功能点有:
- 重置(AC)。
- +-*/运算。
- 数字和小数点的输入。
- 输入左右运算数及运算符之后点击等号可以进行重复计算。
二、界面设计:
先来看看最终的实现效果:
三、HTML结构设计:
页面结构主要分为输入及计算结果展示区域(monitor)和键盘区域(keyboard)。
<div id="calculator" class="calculator-panel"> <div class="monitor"> <div class="display"></div> </div> <div class="keyboard"> <div class="left"> <div class="top"> <ul> <li class="operator largest">AC</li> </ul> </div> <div class="bottom"> <ul> <li class="number">7</li> <li class="number">8</li> <li class="number">9</li> <li class="number">4</li> <li class="number">5</li> <li class="number">6</li> <li class="number">1</li> <li class="number">2</li> <li class="number">3</li> <li class="number large">0</li> <li class="number">.</li> </ul> </div> </div> <div class="right"> <ul> <li class="operator">÷</li> <li class="operator">×</li> <li class="operator">-</li> <li class="operator">+</li> <li class="operator">=</li> </ul> </div> </div> </div>
四:样式设计:
css代码如下:
* { box-sizing: border-box; } .calculator-panel { width: 200px; height: 350px; color: #fff; font-size: 18px; background: rgba(100, 98, 96); } .calculator-panel ul { padding: 0; margin: 0; list-style: none; } .calculator-panel li { float: left; width: 50px; height: 50px; text-align: center; line-height: 50px; border: 1px solid rgba(72, 74, 65); } .monitor { height: 100px; padding: 60px 10px 0 10px; font-size: 36px; } .monitor > .display { height: 40px; line-height: 40px; text-align: right; } .keyboard { height: 250px; } .keyboard > .left, .keyboard > .right { float: left; height: 100%; } .keyboard > .left { width: 150px; } .keyboard > .left .operator { background: rgba(81, 80, 80); } .keyboard > .left .operator.largest { width: 150px; } .keyboard > .left .number { background: rgba(108, 108, 108); } .keyboard > .left .number.large { width: 100px; } .keyboard > .right { width: 50px; } .keyboard > .right .operator { background: rgba(232, 157, 41); }
五、架构设计:
根据单一职责原则,将功能拆分为4个JS类文件,类之间通过事件通知机制进行通信。
- Monitor.js显示器类,监听显示事件并将内容进行展示。
- Number.js数字输入类,监听数字按键的点击并进行广播。
- Operator.js运算符输入类,监听运算符的点击并进行广播。
- Calculator.js计算器类,监听、广播事件并进行结果的计算。
除此之外,还需要一个EventEmitter.js自定义事件对象,来完成自定义事件的监听和触发。
程序大致的运行流程如下:
- 点击数字按键,Number广播一个数字按键事件。
- Calculator监听数字按键事件,并将输入数字作为左侧运算数。
- 点击运算符,Operator广播一个运算符按键事件。
- Calculator监听运算符按键事件,并将输入作为运算符。
- 点击等号,Operator广播一个运算符按键事件。
- Calculator监听运算符按键事件,并计算结果,广播一个显示内容事件。
- Monitor监听显示内容事件,并将结果进行显示。
六、代码展示:
EventEmitter.js
// 自定义事件监听/触发器 var EventEmitter = { eventLoops: {}, // 事件队列 subscribe: function (eventName, handler) { // 订阅事件 var handlers = this.eventLoops[eventName]; if (!Array.isArray(handlers)) { handlers = this.eventLoops[eventName] = []; } handlers.push(handler); }, emit: function (eventName) { // 触发事件 var args = [].slice.call(arguments, 1); var handlers = this.eventLoops[eventName]; handlers.forEach(function (handler) { handler(...args); }); }, remove: function (eventName) { // 移除事件 delete this.eventLoops[eventName]; } };
Monitor.js
// 监视器构造函数 var Monitor = function Monitor () { this.node = $(".monitor").find(".display"); }; // 初始化 Monitor.prototype.init = function () { this.subscribe(); }; // 订阅事件并进行内容显示 Monitor.prototype.subscribe = function () { EventEmitter.subscribe("calculator.show", (content) => { this.node.text(content); }); }; // 销毁 Monitor.prototype.destroy = function () { this.node = null; EventEmitter.remove("calculator.show"); };
Number.js
// Number输入类构造函数 var Number = function Number () { // 当前输入累加值 this.value = "0"; // dom元素class前缀 this.prefix = "number"; }; // 初始化 Number.prototype.init = function () { this.subscribe(); // 先执行一次事件,将当前值进行显示 EventEmitter.emit("calculator.show", this.value); }; // 订阅事件 Number.prototype.subscribe = function () { var self = this; // 订阅Number按钮的点击事件 $("." + this.prefix).on("click", function (e) { var value = $(e.target).text(); self.value = self.value === "0" ? value : self.value + value; EventEmitter.emit("calculator.show", self.value); EventEmitter.emit("calculator.number", self.value); }); // 订阅value重置事件并重置value值 EventEmitter.subscribe("calculator.number.reset", () => { this.value = "0"; }); }; // 销毁 Number.prototype.destroy = function () { $("." + this.prefix).off("click"); EventEmitter.remove("calculator.number.reset"); };
Operator.js
// 运算符构造函数 var Operator = function Operator () { this.prefix = "operator"; }; // 初始化 Operator.prototype.init = function () { this.subscribe(); }; // 订阅事件 Operator.prototype.subscribe = function () { var self = this; // 订阅运算符点击事件 $("." + this.prefix).on("click", function (e) { var value = $(e.target).text(); EventEmitter.emit("calculator.operator", value); }); }; // 销毁 Operator.prototype.destroy = function () { $("." + this.prefix).off("click"); };
Calculator.js
// 计算器构造函数,主入口 var Calculator = function Calculator () { // 左侧数值 this.left = null; // 右侧数值 this.right = null; // 运算符 this.operator = null; // 当前输入模式,"left"表示当前输入的是左侧数值,"right"表示当前输入的右侧数值 this.mode = "left"; // 匹配等号 this.equals = [ "=" ]; // 特殊运算符 this.specialOperators = [ "AC" ]; // 匹配基本运算符 this.basicOperators = [ "÷", "×", "-", "+" ]; // 基本运算符映射 this.basicOperatorMappings = { "÷": "/", "×": "*", "-": "-", "+": "+" }; }; // 初始化 Calculator.prototype.init = function () { this.monitorInstance = new Monitor(); this.numberInstance = new Number(); this.operatorInstance = new Operator(); this.monitorInstance.init(); this.numberInstance.init(); this.operatorInstance.init(); this.subscribe(); }; // 取消订阅事件 Calculator.prototype.unsubscribe = function () { EventEmitter.remove("calculator.number"); EventEmitter.remove("calculator.operator"); }; // 订阅事件 Calculator.prototype.subscribe = function () { EventEmitter.subscribe("calculator.number", (number) => { this.onNumberInput(number); }); EventEmitter.subscribe("calculator.operator", (operator) => { this.onOperatorInput(operator); }); }; // 监听数值输入 Calculator.prototype.onNumberInput = function (number) { // 当前输入的为左侧数值 if (this.mode === "left") this.left = number; // 当前输入的为右侧数值 if (this.mode === "right") this.right = number; }; // 监听运算符输入 Calculator.prototype.onOperatorInput = function (operator) { // 当前输入的是等号,[ "=" ] if (this.equals.includes(operator)) { // 排除不合法操作 if (this.operator == null) return; if (this.left == null && this.right == null) return; if (this.left == null || this.right == null) return; this.calcResult(); // 当前输入的基本运算符,[ "÷", "×", "-", "+" ] } else if (this.basicOperators.includes(operator)) { // 排除不合法操作 if (this.left == null) return; // 获取真实操作运算符,防止[ "÷", "×" ]这类非法运算符参与计算 this.operator = this.basicOperatorMappings[operator]; // 切换当前输入为右侧数字 this.mode = "right"; // 重置当前Number的value,以便重新输入右侧数值 EventEmitter.emit("calculator.number.reset"); // 特殊运算符[ "AC" ] } else if (this.specialOperators.includes(operator)) { this.reset(); } }; // 计算结果 Calculator.prototype.calcResult = function () { // 根据左侧、右侧数值加上运算符计算出结果 // 将结果作为左侧数值继续参与计算 var result = this.left = eval(`${this.left}${this.operator}${this.right}`); // 切换当前输入为右侧数字 this.mode = "right"; // 重置当前Number的value,以便重新输入右侧数值 EventEmitter.emit("calculator.number.reset"); // 显示计算结果 EventEmitter.emit("calculator.show", result); }; // 重置 Calculator.prototype.reset = function () { this.monitorInstance.destroy(); this.numberInstance.destroy(); this.operatorInstance.destroy(); this.unsubscribe(); this.left = null; this.right = null; this.operator = null; this.mode = "left"; this.init(); EventEmitter.emit("calculator.number.reset"); };
七、总结:
核心思路就是Calculator中定义this.left、this.right、this.operator来存储左、右运算数以及当前运算符,点击等号”=”时通过将拼接的运算表达式传入eval函数得出计算结果。
完整的项目代码在这里:https://github.com/popelnice/calculator
最后,如果各位有更好的实现方式,麻烦在评论中告知在下,不胜感激!!!大家共同进步。