【新手教程】React+Webpack 的 TodoMVC

#1

首先我们看一下我们完成后的最终形态:TodoMvc

学习必要条件:略懂node.js,略懂ES6,然后你的电脑必须安装有较新版本node,没有的同学赶紧安装。
好了,废话不多说,直接开始。

第一部分源码:todoMvc-1step

#webpack的配置

1. 介绍:

Webpack 是当下最热门的前端资源模块化管理和打包工具。详细见官网

2. 安装:

$ npm install webpack -g

此时 Webpack 已经安装到了全局环境下,可以通过命令行 webpack -h 试试。但通常我们会将 Webpack 以及相关依赖以这种方式安装,如下:

# 进入项目目录
# 确定已经有 package.json,没有就通过 npm init 创建
# 安装 webpack 依赖
$ npm install webpack --save-dev
# 安装react.js依赖(i是install的简写,-S是--save的简写)
$ npm i react react-dom -S

剩余的依赖组件参照我源码中的package.json的依赖添加就好。最终,我们得到的package.json应该如下图:


确保红框中的内容一样即可。

3. 配置

现在我们已经安装好了依赖,下面我们需要先把项目的目录建好:

.
├── node_modules  #  npm install 安装的东西都跑着里面来了
├── src  
    ├── components
        ├── app.js  # react组件
    ├── styles
        ├── main.styl  # stylus文件(类似于sass)
    ├── entry.js  #  入口js文件
├── index.html  # 入口页面
├── package.json  #  项目描述文件(内有相关依赖)
└── webpack.config.js # webpack配置文件

然后我们在webpack.config.js中添加配置:

module.exports = {
  entry: [
    "./src/entry.js"
  ],
  output: {
    path: './out/',
    filename: "bundle.js"
  },
  module: {
    loaders: [
      { test: /\.js[x]?$/, loader: "babel-loader?presets[]=es2015&presets[]=react", include: /src/},
      { test: /\.css$/, loader: "style!css"},
      { test: /\.styl$/, loader: "style-loader!css-loader!stylus-loader"},
      { test: /\.(png|jpg)$/, loader: 'url?limit=8192'}
    ]
  }
}

配置文件将我们的入口文件entry.js打包输出到 ./out/bundle.js,我们直接在页面index.html中引入bundle.js就好了。

不懂得话可以参考webpack的文档:webpack-usagewebpack-loader。关于/src目录下的文件内容可以直接到源码中查看。然后就可以小试牛刀啦,在终端中输入:
$ webpack
然后我们看到我们的目录下多了个./out/bundle.js文件,然后我们在浏览器打开目录下的index.html文件可以看到内容并alert('success')

那么恭喜你,第一步圆满完成!

React如何双向绑定

todoMvc-2step源码
todoMvc-2step演示
上一章主要说了下react+webpack的环境搭建,这一章主要讲一下如何双向绑定。对vue和angular略有了解的都知道,这两个框架都是支持双向绑定的,而react是单向绑定的,知乎有一篇关于单向绑定和双向绑定可以拓展一下:单向数据绑定和双向数据绑定的优缺点,适合什么场景。下面分析如何具体实现:
进入我们的app.js文件,在之前我们搭建环境的时候已经安装了react相关的依赖以及babel编译工具,所以我们可以直接在这里使用ES6JSX语法。
####1. 引入react核心内容

import React from 'react'
import ReactDOM from 'react-dom'

其中,react.js 是 React 的核心库,react-dom.js 是提供与 DOM 相关的功能。

####2. 生成组件
先介绍react三个比较重要的知识点:
1.ReactDOM.render()
ReactDOM.render 是 React 的最基本方法,用于将模板转为 HTML 语言,并插入指定的 DOM 节点。举个例子:

ReactDOM.render(
        <h1>Hello, world!</h1>,
        document.getElementById('example')
);

上面代码将一个 h1 标题,插入 example 节点。
2.JSX 语法
HTML 语言直接写在 JavaScript 语言之中,不加任何引号,这就是 JSX 的语法,它允许 HTML 与 JavaScript 的混写,上面的<h1>Hello, world!</h1>,就是使用了jsx语法。
3.组件
React 允许将代码封装成组件(component),然后像插入普通 HTML 标签一样,在网页中插入这个组件。React.createClass 方法就用于生成一个组件类。举个:chestnut:

//es5写法
var HelloMessage = React.createClass({
    render: function() {
      return <h1>Hello React</h1>;
  }
});
//es6写法
Class HelloMessage extends React.Component {
  render() {
    return <h1>Hello, React</hr>;
  }
}

当然,这里的HelloMessage我们也可以当做HTML标签用ReactDOM.render()渲染出来。

app.js:

class App extends React.Component { //定义组件,继承父类
  constructor() {//constructor 是和 class 一起用来创建和初始化对象的特殊方法。
    super()//在装载组件(mounting)之前调用会React组件的构造函数。当实现React.Component子类的构造函数时,应该在任何其他语句之前调用super(props)
    this.state = {//设置初始状态
      todos: []
    }
  }
  // 绑定键盘回车事件,添加新任务
  handlerKeyUp(e) {
    if(e.keyCode == 13) {
      let value = e.target.value;
      if(!value) return false;
      let newTodoItem = {
        text: value,
        isDone: false
      };
      e.target.value = '';
      this.state.todos.push(newTodoItem)
      this.setState({todos: this.state.todos});  //修改状态值,每次修改以后,自动调用 this.render 方法,再次渲染组件。
    }
  }
  render(){
    return (
      <div className="todo-input">
        <input type="text" placeholder="请输入待办事项" onKeyUp={this.handlerKeyUp.bind(this)}/>
        <ul>
          {this.state.todos.map((todo,index) => {{
            return (
                <li key={index}>{todo.text}</li>//Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity
            )
          }})}
        </ul>
      </div>
    )
  }
}
ReactDOM.render(<App/>,document.getElementById('app'))

####3. 测试
运行

$ webpack

然后打开index.html,如果可以在input输入,按下回车可以在下方生成list

那么恭喜你,双向绑定功能完成!
#组件化
todoMvc-3step源码
todoMvc-3step演示
上一章主要介绍了下React如何进行双向绑定以及如何生成一个组件,我们第三步的目标就是需要把之前做的内容抽象出更细的组件,这样便于解耦,各个组件各司其职,互不干扰。
先看下抽象后src/components下的目录

组件目录

先看下我们的app.js修改过后的内容:

import React from 'react'
import ReactDOM from 'react-dom'
import TodoHeader from './TodoHeader'  // 引入TodoHeader组件
import TodoMain from './TodoMain'  // 引入TodoMain组件

class App extends React.Component { // 定义组件,继承父类
  constructor() {
    super()
    this.state = {
      todos: []
    }
  }
  addTodo(item) { // 新增了添加todo事项的方法
    this.state.todos.push(item)
    this.setState({todos: this.state.todos});  //设置状态
  }
  render(){
    return (
      <div className="todo-wrapper">
        // 将原内容写在组件中并引入进行渲染
        // 把addTodo方法传递到TodoHeader组件中,bind(this)是为了把该React实例绑定到this上
        <TodoHeader addTodo={this.addTodo.bind(this)}/>
        // 把 state.todos 传入到TodoMain 中
        <TodoMain todos={this.state.todos}/>
      </div>
    )
  }
}

ReactDOM.render(<App/>,document.getElementById('app'))

TodoHeader:
import React from ‘react’

class TodoHeader extends React.Component {
  // 绑定键盘回车事件,添加新任务
  handlerKeyUp(e) {
    if(e.keyCode == 13) { // enter键的 keyCode 为13
      let value = e.target.value;
      if(!value) return false;
      let newTodoItem = {
        text: value,
        isDone: false
      };
      e.target.value = '';
      this.props.addTodo(newTodoItem) // 通过 this.props 来调用父组件传递过来的addTodo方法
    }
  }
  render(){
    return (
        <div className="todo-header">
          <input onKeyUp={this.handlerKeyUp.bind(this)} type="text" placeholder="请输入你的任务名称,按回车键确认"/>
        </div>
    )
  }
}
export default TodoHeader // 将TodoHeader导出,否则父组件无法导入

TodoMain修改后内容:

import React from 'react'
import TodoItem from './TodoItem'

class TodoMain extends React.Component {
  render(){
    if(this.props.todos.length == 0) {
      return (
        <div className="todo-empty">恭喜您,目前没有待办任务</div>
      )
    } else {
      return (
        <ul className="todo-main">
          {
            this.props.todos.map((todo,index) => {
              //{...this.props} 用来传递TodoMain的todos属性和delete、change方法。
              return <TodoItem text={todo.text} isDone={todo.isDone} index={index} {...this.props} key={index}/>
            })
          }
        </ul>
      )
    }
  }
}
export default TodoMain

TodoItem
import React from ‘react’

class TodoItem extends React.Component {
  render() {
    let className = this.props.isDone?'task-done':''
    return (
      <li>
        <label>
          <input type="checkbox"/>
          <span className={className}>{this.props.text}</span>
        </label>
      </li>
    )
  }
}

export default TodoItem

这一步时webpack先编译,然后打开index.html,如果页面像下图这样的odoMvc-3step演示,那就说明成功了。
第三部截图

做到这里应该对react组件组件化的有个大概的了解了。新手们基本可以对着源码按照这种思路继续做下去。以完善【删除】、【清除已完成】、【未完成数量】等功能了,由于代码类似,故不做赘述了,不太清楚的地方可以参考源码。
#Antd
todoMvc-4step源码
todoMvc-4step演示

这一章主要以【删除】键为例讲一下如何使用以 React 封装了一套 Ant Design 的组件库:

1. 安装

推荐使用 npm 的方式进行开发,不仅可在开发环境轻松调试,也可放心地在生产环境打包部署使用,享受整个生态圈和工具链带来的诸多好处。
可以通过 npm 直接安装到项目中,使用 importrequire 进行引用。

$ npm install antd --save

2. 加载

可以通过以下的写法来按需加载组件。

import Button from 'antd/lib/button';
import 'antd/lib/button/style'; // 或者 antd/lib/button/style/css 加载 css 文件

但我推荐使用更简便的写法:
首先需要安装babel-plugin-import 依赖

$ npm install babel-plugin-import --save-dev

然后在我们的根目录下新建.babelrc

{
  "plugins": [["import", {"libraryName": "antd", "style": "css"}]] //import js and css modularly
}

这时我们需要什么UI组件,即可如下这么写以达到按需加载jscss

import { Button } from 'antd';

3. 使用

由于Antd组件已经油React封装好了,用法和原生html标签没差:

<Button type="danger" size="small" onClick={this.handlerDelete.bind(this)}>删除</Button>

剩余的样式我们就可以对着antd components的demo来开发。
#使用leancloud登录注册
todoMvc-5step源码
todoMvc-5step演示

这一章主要将上一章已经成型的TodoMvc增加【注册】、【登陆】、【数据储存】的功能,这里我们把数据保存到leancloud

1. 创建 LeanCloud 账户

你需要去 https://leancloud.cn 创建一个账户。
创建成功后,你需要验证你的邮箱,否则无法创建应用。

2. 创建TodoMVC应用

如下图操作:
创建应用
创建成功后就放在那里,因为接下来我们要按照 LeanCloud 的「JavaScript SDK 文档」来开发登录、注册功能。

3. 准备HTML页面

登陆和注册的页面同样也以组件的形式单独抽离出来,样式如图:

登录注册

组件Login.js代码如下:

import React from 'react'
import { Form, Icon, Input, Button } from 'antd';

const FormItem = Form.Item;

const Login = Form.create()(React.createClass({
  handleSubmit(e) {  // 提交操作
    e.preventDefault();
    this.props.form.validateFields((err, values) => {
      if (!err) {
        this.props.loginOrSignUp(values)
      }
    });
  },
  render() {
    const { getFieldDecorator } = this.props.form;
    let text = this.props.value == 1 ?'注册':'登陆' // 判断“登陆”或者注册功能
    return (
        <Form onSubmit={this.handleSubmit} className="login-form"> // antdUI的表单
          <FormItem>
            {getFieldDecorator('userName', {
              rules: [{ required: true, message: 'Please input your username!' }],
            })(
                <Input addonBefore={<Icon type="user" />} placeholder="Username" />
            )}
          </FormItem>
          <FormItem>
            {getFieldDecorator('password', {
              rules: [{ required: true, message: 'Please input your Password!' }], // 必须填写项
            })(
                <Input addonBefore={<Icon type="lock" />} type="password" placeholder="Password" />
            )}
          </FormItem>
          <FormItem>
            <Button type="primary" htmlType="submit" className="login-form-button">
              {text}
            </Button>
          </FormItem>
        </Form>
    );
  },
}));
export default Login

app.js中做判断,如果已登录,则显示ToDo应用界面,否则显示登陆界面:

render(){
  if (!this.state.currentUser){ // 判断是否已经登录
    const RadioGroup = Radio.Group;
    return (
        <div className="form-wrapper">
          <h1 className="todo-title">React-Todos</h1>
          <RadioGroup className="radio-wrapper" onChange={this.onChange.bind(this)} value={this.state.value}>
            <Radio value={1}>注册</Radio>
            <Radio value={2}>登入</Radio>
          </RadioGroup>
          <Login loginOrSignUp={this.loginOrSignUp.bind(this)} value={this.state.value}/>
        </div>
    )
  } else {
    let info = {
      isAllChecked: this.state.isAllChecked,
      todoCount: this.state.todos.length || 0,
      todoDoneCount: (this.state.todos && this.state.todos.filter((todo) => todo.isDone)).length || 0
    }
    return (
        <div className="todo-wrapper">
          <TodoHeader addTodo={this.addTodo.bind(this)} currentUser={this.state.currentUser} logout={this.logout.bind(this)}/>
          <TodoMain todos={this.state.todos} changeTodoState={this.changeTodoState.bind(this)} deleteTodo={this.deleteTodo.bind(this)} saveOrUpdateTodos={this.saveOrUpdateTodos.bind(this)}/>
          <TodoFooter {...info} clearDone={this.clearDone.bind(this)} changeTodoState={this.changeTodoState.bind(this)}/>
        </div>
    )
  }
}

4. 注册&登陆

1.安装 LeanCloud SDK
https://leancloud.cn/docs/sdk_setup-js.html

$ npm install leancloud-storage --save

2.初始化
https://leancloud.cn/docs/sdk_setup-js.html#初始化
app.js:

import AV from 'leancloud-storage'

const appId = 'XXXXXXXXXXXXXXXXXXXXXX' //这里的appId就是刚才我们创建的应用的Id,每个人都不一样
const appKey = 'XXXXXXXXXXXXXXXXXXX';

AV.init({ appId, appKey });

3.写入注册登陆的方法
我们先要通读一下 LeanCloud 关于注册的文档,然后按照里面的demo去做修改。
app.js:

//登陆或者注册
loginOrSignUp(values){
  //判断是登陆还是注册
  if (this.state.value === 1){
    let user = new AV.User();
    user.setUsername(values.userName);
    user.setPassword(values.password);
    user.signUp().then((loginedUser) => {
      this.state.currentUser = this.getCurrentUser()
      this.setState({currentUser: this.state.currentUser})
    }, function (error) {
      alert("注册失败")
    })
  } else if (this.state.value === 2){
    console.log("执行登陆")
    AV.User.logIn(values.userName, values.password).then((loginedUser) => {
      this.state.currentUser = this.getCurrentUser()
      this.setState({currentUser: this.state.currentUser})
      this.fetchTodos()
    }, function (error) {
      alert("登陆失败")
    });
  }
}

下面还需要去做【登出】、【保存Todo】等功能。这里我就不贴出来代码了,可以直接去github上面去看我的app.js源码
至此,我们React+Webpack+Antd 的一个TodoMVC的思路就讲解完毕了。希望能帮助小伙伴。

4 Likes
#2

windows下webpack提示./out/不是绝对路径,请问一下楼主遇过这个问题吗:grin:

#3

解决了。。原来是中文路径的问题

1 Like
#4

分享一下开始打包遇到webpack提示./out/不是绝对路径,修改成如下绝对路径
output: {
filename: “./out/bundle.js”
},
其他的内容非常全面,就是具体语法不太懂,还需要研究,谢谢LZ

2 Likes
#5

太感谢你了,新手卡在这里,差点就进行不下去了。再次表示感谢

#6

个人感觉在打包的配置文件webpack.config.js中的
output: {
path: __dirname+’/out/’,//输出文件路径
filename: “bundle.js” //输出文件名称
},
最好改成这样,因为在任何模块文件内部,可以使用__dirname变量获取当前模块文件所在目录的完整绝对路径,这样的话在打包的时候就不会因为路径而导致的问题

#7

react技术交流 QQ群 15073987

#8

做完这个教程,感觉对新手来讲有点微坑,毕竟作者也是业余写得。推荐 react零基础试试这个https://www.kirupa.com/react/simple_todo_app_react.htm

1 Like
#9

谢谢,这个网址对我很有效