美文网首页程序员
基于 React + ES6 + Webpack + Node

基于 React + ES6 + Webpack + Node

作者: sylvia_yue | 来源:发表于2018-03-01 16:41 被阅读0次

本实例基于 React + ES6 + Webpack + Node 实现一个井字棋的小游戏,来进一步深刻理解 React 的组件化思想。

1. 效果预览

您可点击预览进行效果预览。
井字棋规则如下:

  • 下棋操作:A / B 两人轮流下棋,下好后该点显示下棋人的名字
  • 成功:当某人下的棋连成三点一线,则判定某人成功
  • 提示:棋盘上方提示处显示下一个下棋人是谁,判定某人成功后,提示出获胜者是谁,并且不可继续进行下棋操作
  • 重开游戏:点击棋盘上方刷新按钮,清楚当前棋盘数据,重新开始游戏
  • 步骤显示及步骤还原:右侧显示步骤按钮,点击即可调到对应步骤对应的棋盘面

2. 项目框架搭建

项目目录结构如下图:


  • 配置 package.json——安装依赖,配置 node 运行语句
    项目框架使用 React + ES6 + Webpack + Node 搭建,首先创建一个 package.json 文件,如下,已添加好所需要安装的依赖目录,直接 npm install 即可安装项目所需依赖。
{
  "name": "game",
  "version": "1.0.0",
  "description": "gama:tic-tac-toe",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server --devtool eval --progress --hot --inline --colors --content-base build",
    "build": "webpack --progress --colors",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "css-loader": "^0.28.10",
    "file-loader": "^1.1.10",
    "node-sass": "^4.7.2",
    "react": "^16.2.0",
    "react-dom": "^16.2.0",
    "react-redux": "^5.0.6",
    "react-router": "^4.2.0",
    "redux": "^3.7.2",
    "redux-logger": "^3.0.6",
    "sass-loader": "^6.0.6",
    "style-loader": "^0.20.2",
    "url-loader": "^0.6.2"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-polyfill": "^6.26.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-2": "^6.24.1",
    "babel-runtime": "^6.26.0",
    "html-webpack-plugin": "^2.30.1",
    "open-browser-webpack-plugin": "0.0.5",
    "webpack": "^3.10.0",
    "webpack-dev-server": "^2.9.7"
  }
}
  • 配置 webpack.config.js,配置项目编译、打包、浏览器自启动等功能
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const OpenBrowserPlugin = require('open-browser-webpack-plugin');
module.exports = {
    entry: './app.js',//
    output: {
        filename: "build/build.js"
    },
    devServer: {
        inline: true,
        port: 8060
    },
    plugins: [new HtmlWebpackPlugin({
        template: 'index.html'
    }),
        new webpack.HotModuleReplacementPlugin(),
        new OpenBrowserPlugin({url: 'http://localhost:8060'})//自动打开浏览器
    ],
    module: {
        rules: [{
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            query: {
                plugins: ['transform-runtime'],
                presets: ['es2015', 'react', 'stage-2']
            }
        }, {
            test: /\.css$/,
            loader: "style-loader!css-loader"
        }, {
            test: /\.scss$/,
            loader: "style-loader!css-loader!sass-loader"
        },{
            // 图片加载器
            test:/\.(png|jpg|gif|jpeg)$/,
            loader:'url-loader?limit=2048'
        }]
    }
};
  • 新建 index.html,作为主页面入口
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>my app</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>
  • 新建 src 文件夹存放项目代码,并在src 文件夹内新建 TicTacToe.js 文件,作为游戏区域外层组件
import React, {Component} from 'react';

class TicTacToe extends Component{
    render(){
        return(
            <div className='game'>
                tic-tac-toe
            </div>
        )
    }
}

export default TicTacToe;
  • 新建 index.scss 文件,在后续的入口文件中引用
  • 新建 app.js 文件作为项目入口文件,引用 src 内的 TicTacToe 组件及 index.scss 文件
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import TicTacToe from './src/TicTacToe';
import './index.scss';

class APP extends Component{
    render(){
        return(
            <TicTacToe/>
        )
    }
}
ReactDOM.render(<APP/>, document.getElementById('app'));
  • 至此,运行 npm start 即可启动项目,浏览器会自动打开 8060 端口,运行项目,效果如下图,接下来就可以进行组件划分。


3. 组件划分

  • 从外到内划分,创建组件框架,完成引用,后进行功能完善
    React 最核心的思想就是组件化,那么首先我们将井字棋游戏区域,按照显示和功能划分为组件。划分时,我采用了从外到内的顺序。



    如上图,井字棋可划分为四部分,对应TicTacToe 、Header、Board、Steps四个组件。

4. 初步创建组件框架

  • 从内到外创建,然后在外层引用
Header 组件:头部提示区

功能:显示当前下棋人、提示有人连线成功、重开游戏
render 代码中,status 为当前提示文字,<button/> 为重开游戏按钮
新建 Header 组件

import React, {Component} from 'react';

class Header extends Component{
    render(){
        const { winner, master } = this.props;
        let status = winner ? `Winner is ${winner}!` : `Next Player: ${master}`;
        return(
            <div>
                {status}
                <button>重玩儿一把</button>
            </div>
        )
    }
}

export default Header;

Header 组件需要从上层传下 winner、master 数据。
在 TicTacToe 组件中调用

import React, {Component} from 'react';
import Header from './Header';

class TicTacToe extends Component{
    constructor(props){
        super(props);
        this.state = {
            master: 'A',
            winner: ''
        }
    }
    render(){
        const { master, winner } = this.state;
        return(
            <div className='game'>
                <Header
                    master={master}
                    winner={winner}
                />
            </div>
        )
    }
}

export default TicTacToe;

效果如下图:


Board 组件:棋盘组件,包含九个小棋盘格
import React, {Component} from 'react';

class Board extends Component{

    /**
     * 渲染多个棋盘格
     */
    renderSquares = () => {
        const { squares, onClickSquare } = this.props;
        let squaresDom = [];
        for(let i=0;i<9;i++){
            squaresDom.push(
                <button
                    key={i}
                    className='square'
                    onClick={e=>onClickSquare(e,i)}
                >
                    {squares[i]}
                </button>
            )
        };
        return squaresDom;
    }

    render(){
        return(
            <div className='board'>
                {this.renderSquares()}
            </div>
        )
    }
}

export default Board;

Board 组件需要从上层组件传入 squares、onClickSquare 数据。
下面在 TicTacToe 组件中调用 Board 组件,并添加 squares 数据和 onClickSquare 函数。

import React, {Component} from 'react';
import Header from './Header';
import Board from './Board';

class TicTacToe extends Component{
    constructor(props){
        super(props);
        this.state = {
            master: 'A',
            winner: '',
            squares: Array(9).fill(undefined)
        }
    }

    /**
     * 下棋触发
     */
    onClickSquare = (e, index) => {
        const {master, squares, winner} = this.state;
        if(squares[index] === undefined && !winner){
            let newSquares = Object.assign([], squares);
            newSquares[index] = master;
            let newMaster = '';
            switch (master){
                case 'A':
                    newMaster = 'B';
                    break;
                case 'B':
                    newMaster = 'A';
                    break;
                default:
                    break;
            }
            this.setState({
                master: newMaster,
                squares: newSquares
            })
        }
    }

    render(){
        const { master, winner, squares } = this.state;
        return(
            <div className='game'>
                <Header
                    master={master}
                    winner={winner}
                />
                <Board
                    squares={squares}
                    onClickSquare={this.onClickSquare}
                />
            </div>
        )
    }
}

export default TicTacToe;

在 index.scss 中加入样式:

.board{
  width: 120px;
  height: 120px;
  border: 1px solid #ccc;
  font-size: 0;
  .square{
    width: 40px;
    height: 40px;
    background: transparent;
    border: 1px solid #ccc;
    outline: none;
    cursor: pointer;
    vertical-align: bottom;
  }
}

运行结果如下:


Steps 组件:步骤列组件,进行一步操作则产生一个步骤条,点击步骤条则跳转到对应棋盘格状态
import React, {Component} from 'react';

class Steps extends Component{

    /**
     * 渲染步骤条
     */
    renderSteps = () => {
        const { history, turnToStep } = this.props;
        let steps = [];
        history['data'].forEach((obj, index) => {
            steps.push(
                <button
                    key={index}
                    onClick={e=>turnToStep(e, index)}
                    className='step-btn'
                >
                    step{index}
                </button>
            )
        })
        return steps;
    }

    render(){
        return(
            <div className='steps'>
                {this.renderSteps()}
            </div>
        )
    }
}

export default Steps;

Steps 组件需要从上层组件传入 history、turnToStep 数据。
下面在 TicTacToe 组件中调用 Steps 组件,并添加 history 数据和 turnToStep 函数。

 import React, {Component} from 'react';
import Header from './Header';
import Board from './Board';
import Steps from './Steps';

class TicTacToe extends Component{
    constructor(props){
        super(props);
        this.state = {
            master: 'A',
            winner: '',
            squares: Array(9).fill(undefined),
            history: {
                flag: false,
                data:[{
                    master: 'A',
                    squares: Array(9).fill(undefined)
                }]
            }
        }
    }

    /**
     * 下棋触发
     * @returns {*}
     */
    onClickSquare = (e, index) => {
        const {master, squares, winner, history} = this.state;
        const { flag, data } = history;
        if(squares[index] === undefined && !winner){
            if(flag){
                let newMaster = data[data.length-1].master;
                let newSquares = data[data.length-1].squares;
                let newHistory = Object.assign([], history);
                newHistory.flag = false;
                this.setState({
                    master: newMaster,
                    squares: newSquares,
                    history: newHistory
                })
            }else{
                let newSquares = Object.assign([], squares);
                newSquares[index] = master;
                let newMaster = '';
                switch (master){
                    case 'A':
                        newMaster = 'B';
                        break;
                    case 'B':
                        newMaster = 'A';
                        break;
                    default:
                        break;
                }

                let newHistory = Object.assign([], history);
                newHistory.data.push({
                    master: newMaster,
                    squares: newSquares
                });

                this.setState({
                    master: newMaster,
                    squares: newSquares,
                    history: newHistory
                })
            }
        }
    }

    /**
     * 跳转到某一步
     * @returns {*}
     */
    turnToStep = (e, index) => {
        const { history } = this.state;
        const { master, squares } = this.state.history.data[index];
        let newHistory = Object.assign({}, history);
        newHistory.flag = true;
        this.setState({
            squares: squares,
            master: master,
            history: newHistory
        })
    }

    render(){
        const { master, winner, squares, history } = this.state;
        return(
            <div className='game'>
                <Header
                    master={master}
                    winner={winner}
                />
                <Board
                    squares={squares}
                    onClickSquare={this.onClickSquare}
                />
                <Steps
                    history={history}
                    turnToStep={this.turnToStep}
                />
            </div>
        )
    }
}

export default TicTacToe;

history.data 存储每一步点击的数据,history.flag 为是否点击了步骤条,若点击了步骤条,再下次点击棋盘时,先将期盼恢复为最后一次下棋结束的样子。
相应地,onClickSquare 函数也进行了更改。

5. 功能完善

基础组件已都已构建完毕,接下来进行功能完善。

  • 判断胜出功能
    思路:通过矩阵位置判断。下棋时,维护每个下棋人对应下棋的位置数据,通过位置数据与获胜位置数据匹配进行判断。
  • 再玩一把功能
    思路:将所有数据置为初始值即可。
    完善后的 TicTacToe 组件代码如下:
import React, {Component} from 'react';
import Board from './Board';
import Steps from './Steps';
import Header from './Header';

class TicTacToe extends Component{
    constructor(props){
        super(props);
        this.state = {
            master: 'A',
            squares: Array(9).fill(undefined),
            winner: '',
            history:[{
                master: 'A',
                squares: []
            }]
        };
        this.squaresA = [];
        this.squaresB = [];
    }

    /**
     * 下棋触发
     */
    clickSquare = (e, value, index) => {
        const { master, winner } = this.state;
        if(value === undefined && !winner){
            let newSquares = Object.assign([], this.state.squares);
            let newHistory = Object.assign([], this.state.history);
            newSquares[index] = master;
            let newMaster = '';
            if(master === 'A'){
                newMaster = 'B';
                this.squaresA.push(index);
            }else if(master === 'B'){
                newMaster = 'A';
                this.squaresB.push(index);
            }
            newHistory.push({
                master: newMaster,
                squares: newSquares
            });

            let winner = this.calculateWinner();

            this.setState({
                master: newMaster,
                squares: newSquares,
                winner: winner,
                history: newHistory
            });
        }

    };

    /**
     * 点击步骤按钮后,跳转到对应的棋盘格状态
     */
    onStepClick = (e, data) => {
        const { master, squares } = data;
        this.setState({
            squares: squares,
            master: master
        });
    }

    /**
     * 判断是否有人连线成功
     */
    calculateWinner = () => {
        let winner = '';
        const { master } = this.state;
        const lines = [
            [0, 1, 2],
            [3, 4, 5],
            [6, 7, 8],
            [0, 3, 6],
            [1, 4, 7],
            [2, 5, 8],
            [0, 4, 8],
            [2, 4, 6],
        ];

        let squares = (master === 'A') ? this.squaresA : this.squaresB;

        lines.forEach(arr => {
            if(squares.indexOf(arr[0]) !== -1 && squares.indexOf(arr[1]) !== -1 && squares.indexOf(arr[2]) !== -1){
                winner = master;
            }
        });
        return winner;
    }

    /**
     * 重新开始一把游戏
     */
    refreshGame = () => {
        this.squaresA = [];
        this.squaresB = [];
        this.setState({
            master: 'A',
            squares: Array(9).fill(null),
            winner: '',
            history:[{
                master: 'A',
                squares: []
            }]
        })
    }

    render(){
        const { master, squares, winner, history } = this.state;
        return(
            <div className='game'>
                <Header
                    winner={winner}
                    master={master}
                    refreshGame={this.refreshGame}
                />
                <Board
                    clickSquare={this.clickSquare}
                    master={master}
                    squares={squares}
                    winner={winner}
                />
                <Steps
                    history={history}
                    onStepClick={this.onStepClick}
                />
            </div>
        )
    }
}

export default TicTacToe;

再给 Header 组件 的 button 添加点击事件。

import React, {Component} from 'react';

class Header extends Component{
    render(){
        const { winner, master, refreshGame } = this.props;
        let status = winner ? `Winner is ${winner}!` : `Next Player: ${master}`;
        return(
            <div className='title'>
                {status}
                <button className='refresh' onClick={refreshGame}/>
            </div>
        )
    }
}

export default Header;

至此,功能完善完毕。

6. 样式调整

最后再进行样式调整。

.game{
  width: 500px;
  height: 380px;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-top: -190px;
  margin-left: -250px;
  .title{
    height: 80px;
    line-height: 80px;
    font-size: 30px;
    text-indent: 50px;
    .refresh{
      width: 30px;
      height: 32px;
      outline: none;
      border: none;
      cursor: pointer;
      margin-left: 100px;
      vertical-align: middle;
      background: url("./src/image/refresh.png")no-repeat 0 0/30px 30px;
    }
  }
  .board{
    width: 300px;
    border: 1px solid #ccc;
    font-size: 0;
    float: left;
    .square{
      width: 100px;
      height: 100px;
      line-height: 60px;
      background: transparent;
      border: 1px solid #ccc;
      outline: none;
      cursor: pointer;
      vertical-align: middle;
    }
  }
  .steps{
    width: 198px;
    float: right;
    .step-btn{
      height: 28px;
      width: 85%;
      background: transparent;
      outline: none;
      border: 1px solid #ccc;
      margin-left: 15%;
      margin-top: 2px;
      border-radius: 5px;
    }
  }
}

效果如下:


完整代码地址

ps: 还有一个更完整的 井字棋/五子棋 切换小游戏,预览网址代码地址

本文参考:
https://reactjs.org/tutorial/tutorial.html

相关文章

网友评论

    本文标题:基于 React + ES6 + Webpack + Node

    本文链接:https://www.haomeiwen.com/subject/taoixftx.html