本文读者对象:熟悉javascript/nodejs,react入门,前端框架选型及nodejs 兴趣用户背景综述:react、 vue 和 Angular 是前端较流行的3个javascript框架,本人不是专业前端,伪全栈,选择 react主要出于两个原因:第一 react的语法和编码方式几乎没有新增学习成本,很自然 第二:react native 提供了一种跨平台开发方案,学习react之后再学习react native 可减少一些时间成本。react 的入门学习建议通过阅读官方文档https://react.docschina.org/tutorial/tutorial.html,最好是英文。它的文档非常好,循序渐进,完整阅读官方12小节的指引大约需要2小时,对于狂躁的我来说阅读十几分钟就想要搞事情,掉坑例后又要去看,反而浪费时间,建议大家还是仔细的把文章看完再动手。选择计算器是因为它对所有的用户都非常熟悉,而官方那个井字游戏我是没有get到它的乐趣。window系统上任务栏的搜索框或者命令行直接输入 calc 可以调出系统的计算器工具,本例就参照它,仅实现最简单计算功能,实现结果如下图:segmentfault 貌似不能上传源码附件,那么本例的所有文件尽量详细列出,达到直接复制出来整理到对应文件里就可运行。具体实现:一:环境搭建react开发环境通常有两种方式,一种是通过nodejs 的npm 下载react 和其它依赖包 另一种是直接在浏览器引入 react 库。 本文采用第二种方式,但是因为react 的 jsx 语法在浏览器是不支持的,需要经过babel编译,那么完整环境搭建如下:1:将react 和 react-dom 两个js 下载到本机,放到本机服务器,并在html中引入。<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script><script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>文件较大,生产环境的地址形如:https://unpkg.com/react@16/um...2:项目目录下依次执行如下命令(需要安装node,如果仅是想看看效果,分析下代码,可以跳过此步,直接复制已经编译好的js 文件)npm init -ynpm install babel-cli@6 babel-preset-react-app@3npx babel --watch src --out-dir ./js/ --presets react-app/prodt-app/prod最后一行就是将 src 目录编写的react组件编译到 js 目录,文件名相同3:新建一web服务器,将下载的js 和 html 骨架和自己编写的 react 组件组合到一起,运启动web服务器,项目组织结构如下:我用的python Flask,用它搭建一个简单的web服务器比node 的 express 和 egg 还要简便,即使没有任何了解,直接执行如下几步就可以了:a:进入 python 官网,下载python的windows 安装包,默认安装,注意勾选b: 进入命令行 Python -m pip Flaskc: 运行本文的main.py 文件,命令行输入 python main.py 开启web服务器:# -*- coding: utf-8 -*-from flask import Flaskapp = Flask(__name__)@app.route('/')def index(): return "hello ! first flask service"if __name__ == '__main__': app.debug = True app.run()二:实现思路伊始,我是将计算器的上的所有按钮分为三类,数字类(0-9)、符号类(+-..)、命令类(C=...) ,这样方便区分它们的点击事件,按钮风格 但后来发现它们的界限似乎没那么明确,并且它们是一个非常好的面向对象的继承关系,但如果提炼它们的基类又没有什么可公共的属性和方法,还增加了复杂性。按照react 的指引也是倾向使用组合代替继承,最终这三类还是没有任何关系。全例仅有一个js 文件,整个组件分为3个小部分,输入记录,计算结果 和 20个按钮,完整代码如下/** * simple caculator using * edit 2019.11 neveryield *///数字方块class NumItem extends React.Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this) } handleClick(e) { this.props.onNum(this.props.text); } render() { return <div onClick={this.handleClick}>{this.props.text}</div> }}//运算方块class OperItem extends React.Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick(e) { this.props.onOper(this.props.text); } render() { return ( <div onClick={this.handleClick}> {this.props.text} </div>); }}//命令方块class CommandItem extends React.Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { this.props.onCommand(this.props.text); } render() { return <div onClick={this.handleClick}>{this.props.text}</div> }}//主计算组件class Caculator extends React.Component { constructor(props) { super(props); //this.handleClick = this.handleClick.bind(this); this.handleNum = this.handleNum.bind(this); this.handleCommand = this.handleCommand.bind(this); this.handleOper = this.handleOper.bind(this); this.state = { inputs: [], result: 0, command: '' } this.prevInput = ""; } //计算string 表达式的值 getResult(str) { //return eval(this.state.inputs.join('')); //如果没有输入表达式参数,则直接使用inputs if (!str) str = this.state.inputs.join(''); return (new Function(`return ${str}`))(); } handleCommand(cmd) { if (cmd == "C") //清空所有 { this.prevInput = ""; this.setState({ inputs: [], result: 0 }); } else if (cmd == "DEL") //删除一个字符,只能是数组输入 { this.state.inputs.pop(); this.setState({ inputs: this.state.inputs }); } else if (cmd == "CE") //删除一个节,可能是多个数字 { if (this.state.inputs.length == 0) return; let lchar; let pchar = this.state.inputs.pop(); while (this.state.inputs.length > 0) { console.log("loop ce"); lchar = this.state.inputs[this.state.inputs.length - 1]; //只要上一类型和前一类型相同就继续移出,因为不可能存在连续两个操作符 if (/\d/.test(lchar) != /\d/.test(pchar)) break; this.state.inputs.pop() } this.setState({ inputs: this.state.inputs }); } else if (cmd == "=") this.setState({ result: this.getResult() }); else if (cmd == "+/-") { //首次点击,算负号,第二次算正号,不做处理 if (false == /[\.\d]+/.test(this.prevInput)) return; let inputStr = this.state.inputs.join(''); inputstr = inputStr.replace(/(\-*[\.\d]+)$/, "(-$1)"); this.setState({ result: this.getResult(inputStr) }); } } handleNum(numChar) { this.prevInput += numChar; //state 不能直接修改不准确. 改了有效果,只怕是不能刷新渲染 this.state.inputs.push(numChar); this.setState({ inputs: this.state.inputs }); console.log("handlenum called"); } handleOper(ope) { //第一个数字必须是num,否则oper 无意义 //if(this.state.inputs.length===0) // return; //只有上一次输入是数字,本操作才有意义 if (false == /[\.\d]+/.test(this.prevInput)) { console.log("handleoper 上一步非数字") return; } if (ope == "×") ope = "*"; else if (ope == "÷") ope = "/"; this.state.inputs.push(ope); this.prevInput = ope; this.setState({ inputs: this.state.inputs }); } render() { return ( <div> <div>:<span className="inputs">{this.state.inputs.join('')}</span></div> <div className="header"> {this.state.result} </div> <div className="container"> <CommandItem text="CE" onCommand={this.handleCommand}></CommandItem> <CommandItem text="C" onCommand={this.handleCommand}></CommandItem> <CommandItem text="DEL" onCommand={this.handleCommand}></CommandItem> <OperItem text="÷" onOper={this.handleOper}></OperItem> <NumItem text="7" onNum={this.handleNum}></NumItem> <NumItem text="8" onNum={this.handleNum}></NumItem> <NumItem text="9" onNum={this.handleNum}></NumItem> <OperItem text="+" onOper={this.handleOper}></OperItem> <NumItem text="4" onNum={this.handleNum}></NumItem> <NumItem text="5" onNum={this.handleNum}></NumItem> <NumItem text="6" onNum={this.handleNum}></NumItem> <OperItem text="-" onOper={this.handleOper}></OperItem> <NumItem text="1" onNum={this.handleNum}></NumItem> <NumItem text="2" onNum={this.handleNum}></NumItem> <NumItem text="3" onNum={this.handleNum}></NumItem> <OperItem text="×" onOper={this.handleOper}></OperItem> <CommandItem text="+/-" onCommand={this.handleCommand}></CommandItem> <NumItem text="0" onNum={this.handleNum}></NumItem> <NumItem text="." onNum={this.handleNum}></NumItem> <CommandItem text="=" onCommand={this.handleCommand}></CommandItem> </div> </div> ); }}ReactDOM.render(<Caculator></Caculator>, document.getElementById("div_cacu"))计算器的计算结果是如何得到的呢? 方式1:将用户点击的数字按钮直接用number类型存储,然后点击操作符号时用if 分支来判定做何种操作类似 if( cmd === "+") this.state.inputs[i] + this.state.input[i+1] 方式2:用户输入全部用string 保存,然后调用 eval,形如 eval('3-69+10') 方式3:通常我们都说 eval 不安全,这里似乎不存在安全相关的问题,好吧,用Function 方式,如下: //计算string 表达式的值 getResult(str) { //return eval(this.state.inputs.join('')); //如果没有输入表达式参数,则直接使用inputs if (!str) str = this.state.inputs.join(''); return (new Function(`return ${str}`))(); }下面是完整的cacu.html 内容,去掉了 style 节的详细内容因为占用了太多空间,并且我写的样式好丑~害羞脸~<html><head> <link rel="icon" href="favicon.ico" type="image/x-icon"> <style type="text/css">.....</style></head><body> <div style="padding:10px;width:250px" id="div_cacu"> <div class="header"> 0 </div> <div class="container"> </div></div> <script type="text/javascript" src="./js/react.js"></script><script type="text/javascript" src="./js/react-dom.js"></script><script type="text/javascript" src="./js/babel.js"></script><script type="text/babel" src="./js/cacuitem.js"></script></body></html>三:劝退点滴1:如背景所述,我选择在浏览器引入react.js 也就是不想在本机装一堆模块,https://react.docschina.org/docs/add-react-to-a-website.html文章的开头处也就只引入了两个js,而链接的中的入门代码链接又点不开,而正好我写的是jsx,并不是使用原始的 React.CreateElement,这就导致了页面打开是报如下错误:最终,还是要本地安装babel,一下子 node_modules 就是 575个文件夹,这得是多少程序员的辛苦工作啊2:this.setState({inputs:this.state.inputs.push(this.prevInput)});乍一看去能发现这行代码的问题么? [].push() 并不返回新数组,而是返回加入的元素个数,此处push 返回的数据是13:react 似乎没有官方组件库,但又很多第三方的优秀组件,百花齐放貌似挺好,但对普通开发者却不是好事。当解决问题仅有一个方案时,人们就不得不使用和研究这个方案,当有很多选择时就慌了。哪个方案好呢?吹哪个好的都有,各种都去看一遍?得费多大劲啊。只看一个? 甲公司选择了A,乙公司选择了B,丙公司选择了C, 员工流动咋办?再又学一遍,更可能的是 丁公司参照A又写了库D,戊公司参照B写了个E,这些写框架的人获得了经验和吹牛皮的资本,使用的人呢?又得去学只有在本公司才使用的框架,这也是程序员为何如此辛苦的原因之一吧。 所以我一直呼吁适度封装,减少框架。程序员何苦为难程序员啊四:后记 这个计算器的实现比较粗糙,甚至它和标准的计算器比也还缺一些内容,但作为一个讲述react基础组件的例子,已进够了。如果用户感兴趣可以对之进行完善甚至重构,我特别建议初入职场的同学这么做,可锻炼程序员思维,有一定经验的就不必了。
