react新手demo——TodoList

react-todolist.gif

一: 写在文章开头

今天我们就使用 react 来实现一个简易版的 todolist ,我们可以使用这个 demo 进行 list 的增删改差,实际效果如上图所示。大家可以clone下来查看:react-todolist

这篇文章我们就不使用 redux,因为这个 demo 本身比较简单,不需要通过 redux 来管理我们的状态。

redux中也有非常有名的一句话叫做:

"如果你不知道是否需要 Redux,那就是不需要它。"

我们废话不多说,直接进入正题。


二:项目的目录结构

   .
   ├── app                              // 开发的文件夹,组件放在这个文件夹中
   │   ├── components                   // 项目的组件
   │   │   ├── App.js                   // 最外层包含下面组件的总组件
   │   │   ├── AppFooter.js             // App的三个筛选按钮的组件
   │   │   ├── AppForm.js               // 添加list的form
   │   │   ├── AppList.js               // 显示list数据的智能组件
   │   │   └── AppTodos.js              // 显示list的木偶组件
   ├── css                              // 放css文件的地方。
   │   ├── semantic.css                 // 我们的文件用到了semantic.css,
   ├── node_modules                     // 第三方的依赖
   ├── .babelrc                         // babel配置文件
   ├── .gitignore                       // git上传时忽略的文件
   ├── bundle.js                        // webpack build之后的文件
   ├── index.html                       // 项目的模版文件
   ├── main.js                          // 项目的入口文件
   ├── webpack.config.js                // webpack配置文件
   ├── README.md                        // readme文件
   └── package.json                     // 当前整一个项目的依赖

三:前期准备,安装依赖

1,首先我们新建一个todolist文件夹,根据我的目录结构建好相应的文件,如果大家嫌麻烦,大家可以clone我的项目,然后看着我的代码,我会一一进行说明的。package.json我们可以自己创建。

$ mkdir todolist
$ cd todolist

2,建立package.json文件

npm init

3,安装相应的依赖,我先解释一下这些依赖的作用

  • 首先安装Babel,Babel 是一个 JavaScript 编译器,他可以将es6或者es7的语法转化为浏览器能识别的javascript。

    npm install babel-cli babel-core --save-dev
    
  • 其次安装我们的主角,react

    npm install react react-dom --save-dev
    
  • 安装webpack,打包工具;和webpack-dev-server,用于来给我们开启一个服务的。

    npm install webpack webpack-dev-server --save-dev
    
  • 安装loader打包,通过使用不同的loaderwebpack有能力调用外部的脚本或工具,实现对不同格式的文件的处理,比如说分析转换scss为css,或者把下一代的JS文件(ES6,ES7)转换为现代浏览器兼容的JS文件,对React的开发而言,合适的Loaders可以把React的中用到的JSX文件转换为JS文件。

    大家想了解更多的webpack的内容,可以参考webpack中文文档

    npm install css-loader babel-loader style-loader --save-dev
    

    然后我们在webpack.config.js中引用这些依赖,具体的格参数的意思,大家可以参考webpack各文档说明,我下面也会简单的注释一下。

    module.exports = {
      entry: './main.js',                 // webpack打包的入口文件
      output: {
        filename: './bundle.js'           // 输出之后的文件名
      },
      module: {
        loaders: [
          {
            test: /\.jsx?$/,
            exclude: /node_modules/,
            loader: 'babel-loader'         // babel的loader,jsx文件使用babel-loader处理
          }, {
            test: /\.css$/,
            exclude: /node_modules/,
            loader: 'style!css'              // css和styleloader,对css后缀的文件进行处理
          }
        ]
      },
      devtool: 'cheap-source-map',
    }
    
  • 同时要让我们的 babel 能在 react中生效,同时支持es6,我们需要安装下面的插件

    npm install babel-preset-es2015 babel-preset-react babel-preset-stage-0 --save-dev
    

    安装完依赖后,我们在.babelrc文件中引入这几个依赖

    {
      "presets": ["es2015","react",'stage-0']
    }
    
  • 其次为了当我们每次添加list的时候有一个唯一的id,我们使用uuid

    npm install uuid --save-dev
    


四:组件的编写,是我们的页面能够显示出来

  • 编写模版文件index.html

    在这个模版文件里面,我们引入 semantic.css 文件,然后建立一个 id=app<div> 为了我们后续的 react 操作。

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>react-todolist</title>
      <link rel="stylesheet" type="text/css" href="/css/semantic.css">
      <style>
          .active { color: red }
          .line{
              display: inline-block;
              border-bottom: 1px solid #222222;
              width:100px;
              position: absolute;
              left:0;
              top:7px;
          }
          .ui.comments .comment{
              padding:0;
              margin:2em 0 0;
          }
      </style>
    </head>
    <body>
      <div class="ui container" style="padding:30px;">
          <div id="app"></div>
      </div>
    
      <script src="./bundle.js"></script>
    </body>
    </html>
    
  • 编写入口文件main.js

    这边的data是我们的模拟数据,将其传入到<App/>组件,在子组件中可以通过props.data的方法获取 data。对于react的基础知识,大家可以参考来自一位react新手的react入门须知

    import React from 'react'
    import ReactDOM from 'react-dom'
    
    import App from './app/component/App'
    
    let data = [
     {id: 0, text: '天气不错哦!!!', complete: false},
     {id: 1, text: '天气不错哦!!!', complete: false},
     {id: 2, text: '出去玩啊!!!', complete: true},
    ]
    
    ReactDOM.render(
      <App data={data}/>,
      document.getElementById('app')
    )
    
  • 编写component里面的组件

    1. App.js

      这个组件我们可以认为是一个容器组件,我们会把AppFormAppListAppFooter放在这个组件中。

      import React from 'react'
      import AppList from './AppList.js'
      import AppForm from './AppForm.js'
      import AppFooter from './AppFooter.js'
      
      class App extends React.Component {
        state = {
            choosevalue : 1,
            data: this.props.data
        }
        
        render () {
          const { data } = this.state; 
          return (
            <div className='ui comments'>
              <h1>My Todo with React</h1>
              <div className='ui divider'></div>
              <AppForm />
              <AppList data={data}/>
              <AppFooter />
            </div>
          )
        }
      }
      
      export default App;
      
    2. AppForm.js

      这个组件是我们添加 list 用的一个form 组件,其中下面的styles这个对象那个也是jsx中申明样式的一种方式,我们还可以使用className来添加样式名字。

      import React from 'react';
      import uuid from 'uuid';
      
      var styles = {
        'title': {
          width: 200,
          display: 'inline-block',
          marginRight: 10,
          verticalAlign: 'top'
        }
      }
      
      class AppForm extends React.Component {
        render () {
          return (
            <form className='ui reply form'>
              <div className='field' style={styles.title}>
                <input type='text' placeholder='TODO' ref='text' />
              </div>
      
              <button type='submit' className='ui blue button'>
                  添加
              </button>
            </form>
          )
        }
      }
      
      export default AppForm;
      
    3. AppList.js

      这个组件是我们在react中常说的智能组件,得到数据lists后通过 map 方法遍历数据,然后进行渲染。这里的map方法是用到了es6中的解构赋值,大家可以参考react新手必须懂得es6的基础知识,然后将值一一传递到子组件中去。

      import React from 'react'
      import AppTodos from './AppTodos'
      
      class AppList extends React.Component {
        render () { 
          const a = this.props.data.map(({ id, text, complete }, index) => {
             return  
                 <AppTodos 
                     key={index} 
                     id={id} 
                     text={text} 
                     complete={complete} 
                   />
          })
      
          return (
            <div> { a } </div>
          )
        }
      }
      
      export default AppList;
      
    4. AppTodos.js

      这个组件是我们在react中常说的木偶组件,就是得到数据渲染组件。

      import React from 'react'
      
      var styles = {
        'title': {
          paddingLeft: '20px',
          paddingRight: '50px',
          position: 'relative'
        },
        'delete': {
          marginLeft: '20px',
          marginRight: '50px'
        }
      }
      
      class AppTodos extends React.Component {
        render () {
          return (
            <div className='comment'>
              <div className='content'>
                <span 
                     className='author' 
                      style={styles.title} 
                >
                    {this.props.text}
                    <span 
                         className={this.props.complete ? 'line' : ''} 
                    />
                </span>
                <span className='author' 
                      style={styles.title}>
                      {this.props.complete ? '已完成' : '未完成'}
                </span>
                <span className='author'>{this.props.id}</span>
                <span className='ui blue button' 
                      style={styles.delete} >
                      删除
                </span>  
              </div>
            </div>
          )
        }
      }
      
      export default AppTodos;
      
    5. AppFooter.js

      这个组件就是下面的三个按钮全部未完成已完成

      import React from 'react'
      
      var styles = {
        'title': {
          marginRight: 10,
          fontSize: 20
        },
      
        'top': {
          marginTop: 20
        }
      }
      
      class AppFooter extends React.Component {
      
        render () {
          return (
            <div>
              <h2 style={styles.top}>show</h2>
              <button 
                 type='submit' 
                 style={styles.top} 
                 className='ui blue button' 
                 value='1' 
                 ref='all'
               > 
                 全部 
              </button>
              <button 
                 type='submit' 
                 style={styles.top} 
                 className='ui blue button' 
                 value='2' 
                 ref='active'
              > 
                 还未完成 
              </button>
              <button 
                 type='submit' 
                 style={styles.top} 
                 className='ui blue button' 
                 value='3' 
                 ref='complete'
              > 
                 已完成 
              </button>
            </div>
          )
        }
      }
      
      export default AppFooter;
      

    然后我们在命令行输入,会开启一个服务。

    npm run server
    

    打开浏览器,输入http://localhost:8080,可看到:

    react-demo_1.png

5,实现list的添加操作

  • 首先理一下流程

    首先在form输入待办事情,点击添加触发一个handleSubmit点击函数,但是我们的data是通过<App />组件来分发的,而list是组件AppList渲染的。这里涉及到了从子组件传递值给父组件,其实也很简单,就从父组件中传一个函数给子组件,子组件将值通过函数再传递出去,大家可以参考react父子组件间的交流

  • 在组件App.js中,我们加入一个OnAddTodoItem函数,并传入到AppForm组件中,我们新建函数中将传进来的newItem通过concat()现在的data,然后更新state

      ...
    
      OnAddTodoItem (newItem) {
        let newdata = this.state.data.concat(newItem);
        this.setState({data : newdata});
      }
    
      render () {
        const { data } = this.state; 
        return (
          <div className='ui comments'>
            <h1>My Todo with React</h1>
            <div className='ui divider'></div>
            <AppForm 
              AddTodoItem={this.OnAddTodoItem.bind(this)} />
            <AppList 
              data={data}/>
            <AppFooter />
          </div>
        )
      }
    }
    
    export default App;
    
  • 在组件AppForm.js中,我们加入一个handleSubmit函数,并在form表单添加一个onClick函数,将用户输入的数据,通过uuid生成的id、输入的text、以及是否完成false。通过函数传递给父组件。

    ...
    
    handleSubmit (event) {
        event.preventDefault()
        let text = this.refs.text.value
        
        if (!text.trim()) {
          alert("Input can't be null")
          return
        }
        
        let id = uuid();
        this.props.AddTodoItem({id,text,complete:false});
      }
    
      render () {
        return (
          <form 
                  className='ui reply form' 
                  onSubmit={this.handleSubmit.bind(this)}>
            <div 
              className='field' 
                style={styles.title}>
              <input type='text' placeholder='TODO' ref='text' />
            </div>
            <button type='submit' className='ui blue button'>
                添加
            </button>
          </form>
        )
      }
    }
    
    export default AppForm;
    

    你可以看到如下效果:

    react-add2.gif

6,完成筛选功能

  • 首先里一下流程

    我们给下面的三个按钮设置了不同的value1代表全部、2代表未完成、3代表已完成,然后我们根据相应的value,展示相应的list,给三个按钮分别加上handleAllhandleActivehandleComplete三个方法,在onClick时触发。

  • App.js

    添加一个加上choosevaluestate,默认为1,即全选,同时将其传入到<AppList/>中去,同时添加chooseValue的方法,然后传入到AppFooter组件中去。

    ...
    state = {
          choosevalue : 1,
          data: this.props.data
    }
    
    ChooseValue (id) {
        this.setState({choosevalue:id});
    }
    
    ...
    
    <AppList  
          data={this.state.data} 
          choosevalue={this.state.choosevalue} 
    />
    <AppFooter 
      SubmitChooseValue={this.ChooseValue.bind(this)}
    />
    
      
    
  • AppFooter.js

    ...
    
    handleAll () {
        let all = this.refs.all.value;
        this.props.SubmitChooseValue(all);
    }
    
    handleActive () {
        let active = this.refs.active.value;
        this.props.SubmitChooseValue(active);
    }
    
    handleComplete () {
        let complete = this.refs.complete.value
        this.props.SubmitChooseValue(complete);
    }
    
    
    render () {
        return (
          <div>
            <h2 style={styles.top}>show</h2>
            <button 
              type='submit' 
              style={styles.top} 
              className='ui blue button' 
              value='1' 
              ref='all' 
              onClick={this.handleAll.bind(this)}
            > 
                  全部 
            </button>
            <button 
              type='submit' 
              style={styles.top} 
              className='ui blue button' 
              value='2' 
              ref='active' 
              onClick={this.handleActive.bind(this)}
            > 
              还未完成 
            </button>
            <button 
              type='submit' 
              style={styles.top} 
              className='ui blue button' 
              value='3' 
              ref='complete' 
              onClick={this.handleComplete.bind(this)}
            > 
              已完成 
            </button>
          </div>
        )
      }
    }
    
    export default AppFooter;
    
  • AppList.js

    根据value渲染相应的List

    ...
    
    class AppList extends React.Component {
      render () {  
        var value = this.props.choosevalue;
    
        const a = this.props.data.map(({ id, text, complete }, index) => {
          if (value == '1') {
              return  <AppTodos 
                              key={index} 
                              id={id} 
                              text={text} 
                              complete={complete} 
                        />
          }
    
          if (value == '2' && complete === false) {
              return  <AppTodos 
                              key={index} 
                              id={id} 
                              text={text} 
                              complete={complete} 
                        />
          }
    
          if (value == '3' && complete === true) {
              return  <AppTodos 
                              key={index} 
                              id={id} 
                              text={text} 
                              complete={complete} 
                        />
          }
        })
    
        return (
          <div> { a } </div>
        )
      }
    }
    
    export default AppList;
    

你可以看到如下效果:

react-add3.gif

7,修改list状态和删除list的功能

  • 首先里一下流程

    修改list的状态,其实就是我们找到相应idlist,然后将其的complete改为true就可以了。

    删除list时,就是我们找到相应idlist,我们给他添加一个deleteFlag属性,在AppList渲染的时候,我门判断是否有deleteFlag来惊醒渲染。

  • App.js

    添加两个方法AllChangeComplete:改变状态;AllOnDeleteItem:删除list的方法。然后通过组件传入到相应的子组件。

    ...
      AllChangeComplete (id) {
        let newdata = this.state.data.map((item,index) => {
            if(item.id === id) {
              item.complete = !item.complete;
            }
            return item;
        })
        this.setState({data : newdata});
      }
    
      AllOnDeleteItem (id) {
        console.log(id);
        let newdata = this.state.data.map(function (item) {
          console.log(item);
          if (item.id == id) {
            item.deleteFlag = true
          }
          return item
        })
        this.setState({ data: newdata })
      }
      
    ...
    
    <AppList  
          data={this.state.data} 
            choosevalue={this.state.choosevalue} 
            ChangeCompleteTop={this.AllChangeComplete.bind(this)}
            DeleteItemTop={this.AllOnDeleteItem.bind(this)} 
    />
    
    ...
    
  • AppList.js

    我们根据completedeleteFlag来进行渲染。并在这个组件中,充当一个中间的过度组件,将AppTodos触发的函数传到App.js中去改变状态。

    ...
    
    class AppList extends React.Component {
      SubmitDelete (id) {
        this.props.DeleteItemTop(id)
      }
    
      ChangeDone (id) {
        this.props.ChangeCompleteTop(id);
      }
      render () {  
        var value = this.props.choosevalue;
    
        const a = this.props.data.map(({ id, text, complete }, index) => {
          if (value == '1' && deleteFlag !== true) {
              return  <AppTodos 
                              key={index} 
                              id={id} 
                              text={text} 
                              complete={complete} 
                              ChangeCompleteItem={this.ChangeDone.bind(this)}                         DeleteItem={this.SubmitDelete.bind(this)}
                        />
          }
    
          if (value == '2' && complete === false && deleteFlag !== true) {
              return  <AppTodos 
                              key={index} 
                              id={id} 
                              text={text} 
                              complete={complete} 
                              ChangeCompleteItem={this.ChangeDone.bind(this)}                         DeleteItem={this.SubmitDelete.bind(this)}
                        />
          }
    
          if (value == '3' && complete === true && deleteFlag !== true) {
              return  <AppTodos 
                              key={index} 
                              id={id} 
                              text={text} 
                              complete={complete} 
                              ChangeCompleteItem={this.ChangeDone.bind(this)}                         DeleteItem={this.SubmitDelete.bind(this)}
                        />
          }
        })
    
        return (
          <div> { a } </div>
        )
      }
    }
    
    export default AppList;
    
  • AppTodos.js

    添加两个方法,handleChangeCompletehandleDelete。一个时更改状态,一个是删除。

    ...
    handleChangeComplete () {
        let newComplete = this.props;
        this.props.ChangeCompleteItem(this.props.id);
    }
    
    handleDelete () {
        this.props.DeleteItem(this.props.id);
    }
    
    render () {
        return (
          <div className='comment'>
            <div className='content'>
              <span className='author' 
                    style={styles.title} 
                    onClick={this.handleChangeComplete.bind(this)}>
                  {this.props.text}
                  <span className={this.props.complete ? 'line' : ''} />
              </span>
              <span className='author' 
                    style={styles.title}>
                    {this.props.complete ? '已完成' : '未完成'}
              </span>
              <span className='author'>{this.props.id}</span>
              <span className='ui blue button' 
                    style={styles.delete} 
                    onClick={this.handleDelete.bind(this)}>
                    删除
              </span>  
            </div>
          </div>
        )
      }
    }
    
    export default AppTodos;
    

至此我们完成了所有的功能,如下图所示:

react-add4.gif

8,结语

我们这次没有借助任何的状态管理工具,如redux来做这个demo。因为大家做下来也能发现,其实我们这个demo的层级只有2层,所以我们可以通过props来进行父子间的通信。

但是其实我们也发现了,如果当组件的层级越来越多的时候,我们通过这样来进行父子间的通信就不方便了,在这个时候我们就需要用到redux或者mobx等等的状态管理工具。

其实这边的删除和修改list状态我都是在前端模拟处理的,在实际工作中我们都会通过接口去处理,然后根据返回值进行更改state

希望这篇文章对大家有一点启发,有任何问题可以在简书里私信我哦!

来自一个奔跑在前端路上的前端小白。


9,参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,042评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,996评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,674评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,340评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,404评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,749评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,902评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,662评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,110评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,577评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,258评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,848评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,726评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,952评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,271评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,452评论 2 348

推荐阅读更多精彩内容

  • HTML模版 之后出现的React代码嵌套入模版中。 1. Hello world 这段代码将一个一级标题插入到指...
    ryanho84阅读 6,221评论 0 9
  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,813评论 1 18
  • 目前,react组件有三种写法,分别是es5的createClass写法,es6的class写法,以及statel...
    ZoomFunc阅读 1,625评论 0 1
  • 自己最近的项目是基于react的,于是读了一遍react的文档,做了一些记录(除了REFERENCE部分还没开始读...
    潘逸飞阅读 3,353评论 1 10
  • 深入JSX date:20170412笔记原文其实JSX是React.createElement(componen...
    gaoer1938阅读 8,050评论 2 35