1

本文读者对象:熟悉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 可以调出系统的计算器工具,本例就参照它,仅实现最简单计算功能,实现结果如下图:

image.png

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服务器,项目组织结构如下:
image.png
我用的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,这就导致了页面打开是报如下错误:
image.png
image.png
最终,还是要本地安装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基础组件的例子,已进够了。如果用户感兴趣可以对之进行完善甚至重构,我特别建议初入职场的同学这么做,可锻炼程序员思维,有一定经验的就不必了。


neveryield
49 声望4 粉丝

资深互联网 noiser