看到一位人士说,未来软件工程师只有两个方向:端工程师、云工程师。公司部门内,也有全栈工程师。前端的技术发展也非常快,很多思想跟后端已经是相通了。

下面以一个react示例,来稍作了解,这个示例是一个中文网的示例,但是我对代码进行了大量的注释,对一些变更进行了重命名,感觉会比较好理解一点。

  • 安装node.js,具体前期操作看上述链接。

  • 添加css文件,内容如下:

body {
    font: 14px "Century Gothic", Futura, sans-serif;
    margin: 20px;
  }
  
  ol, ul {
    padding-left: 30px;
  }
  
  .board-row:after {
    clear: both;
    content: "";
    display: table;
  }
  
  .status {
    margin-bottom: 10px;
  }
  
  .square {
    background: #fff;
    border: 1px solid #999;
    float: left;
    font-size: 24px;
    font-weight: bold;
    line-height: 34px;
    height: 34px;
    margin-right: -1px;
    margin-top: -1px;
    padding: 0;
    text-align: center;
    width: 34px;
  }
  
  .square:focus {
    outline: none;
  }
  
  .kbd-navigation .square:focus {
    background: #ddd;
  }
  
  .game {
    display: flex;
    flex-direction: row;
  }
  
  .game-info {
    margin-left: 20px;
  }  
  • js文件内容如下 :

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

// 【棋盘中的方格】
// 注意函数的写法,
// 1、function打头,不需要内render方法,JSX语法直接return;
// 2、通过对象传参,而不是dom对象value属性:{this.props.value}
// 3、事件响应函数{props.onClick},而不是直接定义函数:{function() { ...; }} 或 {() => {...;}}
// 4、函数没有生命周期,也无法管理状态,意味着,在函数的组件上定义点击事件,在函数内部定义一个子函数,去修改一个内部变量,这个变量的变化,并不会刷新视图
function Square(props) {   
    // 函数写法,无构造方法,无state,避免每个小组件维护数据(同步数据),数据应该交给大脑,中心,更高级别的组件

    return (
    <button className="square" onClick={props.onClick}>
        {props.filled}
    </button>
    );   
}
  
// 【棋盘组件】
class Board extends React.Component {
    // 本组件仍无state,虽然它表示一个棋盘,但是涉及到多个历史(即多个棋盘)的数据互动,则需要进一步抽象,提升等级

    // 内部子函数,封装JSX片段
    renderSquare(i) {
        return (<Square filled={this.props.squares[i]}
        onClick={()=>this.props.onClick(i)}
        />);
    }

    // reder告诉虚拟dom渲染内容
    render() {
        
        return (
        <div>            
            <div className="board-row">
            {this.renderSquare(0)}
            {this.renderSquare(1)}
            {this.renderSquare(2)}
            </div>
            <div className="board-row">
            {this.renderSquare(3)}
            {this.renderSquare(4)}
            {this.renderSquare(5)}
            </div>
            <div className="board-row">
            {this.renderSquare(6)}
            {this.renderSquare(7)}
            {this.renderSquare(8)}
            </div>
        </div>
        );
    }
}
  
class Game extends React.Component {
    constructor(props) {
        // 定义其子类的构造函数时,都需要调用 super 方法
        super(props);

        // 定义数据结构,初始化
        this.state = {
            // squares(棋盘)已经是二维数组,因此外层用一个对象封装,再封装成对象数组(棋盘历史)
            // 初始化第一个棋盘,里面啥也没填
            boards:[
                {
                    squares: Array(9).fill(null),
                }                
            ],
            stepNumber: 0, // 当前显示的棋盘下标
            xIsNext: true,
        }
    }


    handleClick(i) {        
        // 从已有的数组中返回选定的元素,slice(start,end),不包括end元素
        const currentBoards = this.state.boards.slice(0, this.state.stepNumber + 1);

        // jumpto按钮后,改变了state,导致stepNumber的变化,进而render,棋盘重新渲染,这没有问题
        // 但是,如果棋盘的数据没有对应上,棋盘上的点击事件,则不一定会响应,因为你点击的格子可能已经有数据
        // 因此,不能用下面这一行,这种写法,是固定拿最新的棋盘集
        // const duplicatedBoards = this.state.boards.slice();

        // 当前棋盘
        const currentBoard = currentBoards[currentBoards.length - 1];
        const duplicatedSquares = currentBoard.squares.slice();

        // 计算获胜者,或者已经被填满(非null),则不再执行后续内容
        if (calculateWinner(duplicatedSquares) || duplicatedSquares[i]) {
            return;
        } 
        // 增加棋子,形成一个新的棋盘
        duplicatedSquares[i] = this.state.xIsNext ? 'X' : 'O';

        // 每次点击棋盘中的方格,导致数据的变化,然后驱动render方法的再次执行
        this.setState({
            // 当前棋盘下标,连续点击的话,是一路从0加上来
            stepNumber: currentBoards.length,
            xIsNext: !this.state.xIsNext,

            // 当前棋盘集合追加一个新棋盘,concat连接两个或多个数组,返回一个副本
            // array1.concat(array2,array3,...,arrayX) https://www.runoob.com/jsref/jsref-concat-array.html
            // 赋值棋盘集合
            boards: currentBoards.concat([{
                squares: duplicatedSquares,
            }]), 
        });
    }

    jumpTo(step) {
        // 外部按钮,导致数据的变化,也会更新view
        this.setState({
            stepNumber: step,
            xIsNext : (step % 2) === 0
        });
    }

    render() {
        const boards = this.state.boards;
        // 渲染指定棋盘到Board组件
        const board = boards[this.state.stepNumber];

        // 数组的用法array.map(function(currentValue当前元素,index下标,arr当前元素属于的数据组象,可选), thisValue回调函数的this指针,可选)
        // https://www.runoob.com/jsref/jsref-map.html
        const steps = boards.map(
            // 每次render都会遍历渲染一遍
            (board, index) => {
                const desc = index ? 'go to move #' + index : 'go to game start';   // index为0或其它时,显示start             
                // li会以数字打头,比如1.
                // react要求每个li要有key:https://reactjs.org/docs/lists-and-keys.html#keys
                return (<li key={index}><button onClick={()=>this.jumpTo(index)}>{desc}</button></li>);
            }
        );

        // 计算页面文字应该显示的信息
        let status;
        const winner = calculateWinner(board.squares); // 在click事件里也计算了一遍,是否可以考虑用state的变量来记录赢者?

        if (winner) {
            status = 'Winner: ' + winner;
        } else {
            status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
        }

        return (
        <div className="game">
            <div className="game-board">
            <Board 
                squares = { // squares的变化,导致界面的变化
                    board.squares // current变量可以直接引用
                }
                onClick = {
                    (i) => this.handleClick(i) // 最后会转化为棋盘格子dom对象的click事件,这个i是Board的render里硬编码的,从0开始
                }
            />
            </div>
            <div className="game-info">
            <div>{status}</div>
            <ol>{steps}</ol>
            <ol><li>abc</li><li>def</li></ol>
            </div>
        </div>
        );
    }
}

// ========================================

ReactDOM.render(
    <Game />,
    document.getElementById('root')
);

function calculateWinner(squares) {
    const lines = [
        [0,1,2],
        [3,4,5],
        [6,7,8],
        [2,4,6],
        [0,4,8],
        [0,3,6],
        [1,4,7],
        [2,5,8],
    ];

    for (let i  = 0; i < lines.length; i++) {
        const [a,b,c] = lines[i];
        if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
            return squares[a]; // 获胜者
        }
    }
    return null;
}