本文读者对象:熟悉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 -y
npm install babel-cli@6 babel-preset-react-app@3
npx 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 Flask
c: 运行本文的main.py 文件,命令行输入 python main.py 开启web服务器:
# -*- coding: utf-8 -*-
from flask import Flask
app = 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 返回的数据是1
3:react 似乎没有官方组件库,但又很多第三方的优秀组件,百花齐放貌似挺好,但对普通开发者却不是好事。
当解决问题仅有一个方案时,人们就不得不使用和研究这个方案,当有很多选择时就慌了。哪个方案好呢?吹哪个好的都有,各种都去看一遍?得费多大劲啊。只看一个? 甲公司选择了A,乙公司选择了B,丙公司选择了C, 员工流动咋办?再又学一遍,更可能的是 丁公司参照A又写了库D,戊公司参照B写了个E,这些写框架的人获得了经验和吹牛皮的资本,使用的人呢?又得去学只有在本公司才使用的框架,这也是程序员为何如此辛苦的原因之一吧。 所以我一直呼吁适度封装,减少框架。程序员何苦为难程序员啊
四:后记
这个计算器的实现比较粗糙,甚至它和标准的计算器比也还缺一些内容,但作为一个讲述react基础组件的例子,已进够了。如果用户感兴趣可以对之进行完善甚至重构,我特别建议初入职场的同学这么做,可锻炼程序员思维,有一定经验的就不必了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。