• 我们现在的应用只有个Hello React的页面,现在需要添加个用于添加用户的页面。首先在/src目录下新增个pages目录,用于存放渲染页面的组件。然后在/src/pages中新增个UserAdd.js文件

    新增页面

    我们现在的应用只有一个Hello React的页面,现在需要添加一个用于添加用户的页面。

    首先在/src目录下新增一个pages目录,用于存放渲染页面的组件。

    然后在/src/pages中新增一个UserAdd.js文件。

    Tips: 关于文件的命名,我采用这样的方式:[模块][功能].xxx,上面是一个添加用户的页面,所以模块是User,功能是Add,良好的命名风格可以让你的项目、代码更容易维护(这里由于是React的一个组件,所以使用大写开头的大驼峰命名法)。

    在这个文件中写入一个基本的React组件:

    import React from 'react';
    
    class UserAdd extends React.Component {
      render () {
        return (
          <div>User add page.</div>
        );
      }
    }
    
    export default UserAdd;

    配置路由

    我们需要使用react-router提供的路由组件来控制当前路由下页面应该渲染的组件。

    修改/src/index.js为:

    import React from 'react';
    import ReactDOM from 'react-dom';
    import { Router, Route, hashHistory } from 'react-router';
    import UserAddPage from './pages/UserAdd';
    
    ReactDOM.render((
      <Router history={hashHistory}>
        <Route path="/user/add" component={UserAddPage}/>
      </Router>
    ), document.getElementById('app'));

    打开浏览器的http://localhost:8000发现页面一片空白,在控制台中可以看到一个错误信息:

    这是因为我们只配了一个/user/add的路由,我们来访问一下http://localhost:8000/#/user/add看看

    新页面出来了。现在需要把上面的那个错误干掉:新建一个主页,并在主页中添加一个链接链到添加用户的页面。

    还是在/src/pages/中新建一个Home.js,写入代码:

    import React from 'react';
    import { Link } from 'react-router';
    
    class Home extends React.Component {
      render () {
        return (
          <div>
            <header>
              <h1>Welcome</h1>
            </header>
    
            <main>
              <Link to="/user/add">添加用户</Link>
            </main>
          </div>
        );
      }
    }
    
    export default Home;

    然后在/src/index.js中添加一个指向Home的Route:

    import HomePage from './pages/Home';
    
    ReactDOM.render((
      <Router history={hashHistory}>
        <Route path="/" component={HomePage}/>
        <Route path="/user/add" component={UserAddPage}/>
      </Router>
    ), document.getElementById('app'));


    首页已经可以正常访问了,并且点击添加用户也正确地跳转到了添加用户页面。

    Q: 为什么我看到的url里有一个’#’?
    A: 这是由于我们给Router组件传入了hashHistory,url中’#’及’#’以后的部分属于hash,hash的变化并不会引起页面的重新刷新,而hashHistory会监听hash的变化使得Router组件能够根据url渲染出正确的组件。除了hash History之外还有browserHistory和memoryHistory。使用browserHistory可以让url变得像标准的url一样(没有#),但是需要在后端做一些特殊处理;memoryHistory是用于做服务端渲染时使用的。

    Q: 为什么Home.js里要用Link组件而不是一个标准的a标签?
    A: 上面说了,我们使用了hashHistory,正确的页面url中应该都是有一个’#’的,如果直接使用a标签,你需要这么写:<a href=”/#/user/add”>添加用户</a>。但是如果我们想要换成browserHistory,就需要把所有标签中的’#’去掉。使用react-router提供的Link组件可以让我们无视history之间的差异性,直接写标准的路由”/user/add”就可以了。此外,由于我们写的是单页面应用(SPA),Link组件会阻止页面的跳转(仅仅只是改变了url,然后改变了渲染的组件)。

    编写页面

    现在开始编写添加用户的逻辑。

    上一篇中,我们定义了User的结构为:

    {
      "id": 10000,
      "name": "一韬",
      "age": 25,
      "gender": "male"
    }

    在新建的时候,我们需要使用post方式将新用户的name、age、gender发送给接口http://localhost:3000/user,因此这个添加用户的页面需要提供一个包含上述3个字段控件的表单:

    class UserAdd extends React.Component {
      render () {
        return (
          <div>
            <header>
              <h1>添加用户</h1>
            </header>
    
            <main>
              <form>
                <label>用户名:</label>
                <input type="text"/>
                <br/>
                <label>年龄:</label>
                <input type="number"/>
                <br/>
                <label>性别:</label>
                <select>
                  <option value="">请选择</option>
                  <option value="male"></option>
                  <option value="female"></option>
                </select>
                <br/>
                <br/>
                <input type="submit" value="提交"/>
              </form>
            </main>
          </div>
        );
      }
    }

    现在我们的页面是这个样子:

    获取表单的值

    现在我们可以在表单中填写数据了,但是还不能获取到我们输入的值。

    在React中处理表单有些不一样,由于React提倡“单向数据流”,React中的表单并不提供双向数据绑定的功能,我们需要给表单绑定它的value,然后提供一个onChange的处理方法来更新value的值。

    这里我们使用组件的state来维护表单的值,在onChange的时候使用setState来更新值,最后,在表单提交事件被触发的时候,我们输出state来观察最终获得的表单值:

    class UserAdd extends React.Component {
      constructor () {
        super();
        this.state = {
          name: '',
          age: 0,
          gender: ''
        };
      }
      handleValueChange (field, value, type = 'string') {
        // 由于表单的值都是字符串,我们可以根据传入type为number来手动转换value的类型为number类型
        if (type === 'number') {
          value = +value;
        }
    
        this.setState({
          [field]: value
        });
      }
      handleSubmit (e) {
        // 阻止表单submit事件自动跳转页面的动作
        e.preventDefault();
        alert(JSON.stringify(this.state));
      }
      render () {
        const {name, age, gender} = this.state;
        return (
          <div>
            <header>
              <h1>添加用户</h1>
            </header>
    
            <main>
              <form onSubmit={(e) => this.handleSubmit(e)}>
                <label>用户名:</label>
                <input type="text" value={name} onChange={(e) => this.handleValueChange('name', e.target.value)}/>
                <br/>
                <label>年龄:</label>
                <input type="number" value={age || ''} onChange={(e) => this.handleValueChange('age', e.target.value, 'number')}/>
                <br/>
                <label>性别:</label>
                <select value={gender} onChange={(e) => this.handleValueChange('gender', e.target.value)}>
                  <option value="">请选择</option>
                  <option value="male"></option>
                  <option value="female"></option>
                </select>
                <br/>
                <br/>
                <input type="submit" value="提交"/>
              </form>
            </main>
          </div>
        );
      }
    }

    在页面中填写表单,点提交后可以看到如下界面:

    调用接口创建用户

    拿到了表单数据,我们离目标已经很近了,就差最后一步:把表单的值通过接口提交

    调用接口可以有很多方法:Ajax、表单提交、fetch。

    由于直接使用表单提交会引起页面的跳转,这不符合我们单页应用的原则。这里我们使用比Ajax更先进易用的fetch。

    修改/pages/UserAdd.js中的handleSubmit方法:

      handleSubmit (e) {
        e.preventDefault();
    
        const {name, age, gender} = this.state;
        fetch('http://localhost:3000/user', {
          method: 'post',
          // 使用fetch提交的json数据需要使用JSON.stringify转换为字符串
          body: JSON.stringify({
            name,
            age,
            gender
          }),
          headers: {
            'Content-Type': 'application/json'
          }
        })
          .then((res) => res.json())
          .then((res) => {
            // 当添加成功时,返回的json对象中应包含一个有效的id字段
            // 所以可以使用res.id来判断添加是否成功
            if (res.id) {
              alert('添加用户成功');
              this.setState({
                name: '',
                age: 0,
                gender: ''
              });
            } else {
              alert('添加失败');
            }
          })
          .catch((err) => console.error(err));
      }

    填写表单然后点击提交后,可以看到弹出一个添加成功的提示框:

    打开http://localhost:3000/user查看所有用户列表,发现用户已经被添加进去了:

    至此就完成了基本的添加用户功能~

    展开全文
  • React社区提供了众多的脚手架,这里我们使用官方推荐的create-react-app。 //安装脚手架 npm install -g create-react-app //生成并运行项目 create-react-app my-app cd my-app npm start 注:create-...

    1.安装

    React社区提供了众多的脚手架,这里我们使用官方推荐的create-react-app

    //安装脚手架
    npm install -g create-react-app
    //生成并运行项目
    create-react-app my-app 
    cd my-app
    npm start

    注:create-react-app 脚手架默认隐藏 config 文件夹,需要通过

    npm run eject
    npm i

    导出可配置的模板

    2.服务端搭建

    这里我们借助json-server这个工具快速搭建后台管理系统的服务端程序:

    • 首先执行npm i json-server -g把json-server作为全局工具安装
    • 新建一个项目目录(后面文中所有的路径根目录都表示该项目目录)
    • 在根目录中执行npm init初始化一个npm项目(会有一些项目配置需要你输入,一直敲回车就行了)
    • 新建/server目录用于放置服务端的文件
    • 新建/server/db.json文件作为服务端的数据库文件
    • /server/db.json文件中写入以下数据:
    {
      "user": [
        {
          "id": 10000,
          "name": "一韬",
          "age": 25,
          "gender": "male"
        },
        {
          "id": 10001,
          "name": "张三",
          "age": 30,
          "gender": "female"
        }
      ],
      "book": [
        {
          "id": 10000,
          "name": "JavaScript从入门到精通",
          "price": 9990,
          "owner_id": 10000
        },
        {
          "id": 10001,
          "name": "Java从入门到放弃",
          "price": 1990,
          "owner_id": 10001
        }
      ]
    }
    
    • 最后在/server目录执行json-server db.json -w -p 8000

    现在打开浏览器,访问网址http://localhost:8000

    点击Routes下面的user会跳转到http://localhost:8000/user,可以看到我们之前在db.json中写入的user数组:

    [
      {
        "id": 10000,
        "name": "一韬",
        "age": 25,
        "gender": "male"
      },
      {
        "id": 10001,
        "name": "张三",
        "age": 30,
        "gender": "female"
      }
    ]
    

    访问http://localhost:8000/book同上

    我们在db.json文件中写入了标准的JSON格式数据,这个json里有一个user数组和一个book数组,这就告诉json-server,我们的数据库里有一个名为user的”表”和一个名为book的”表”,并且表里的数据为xxx,然后json-server就会启动服务器,并且以每个”表”为单位为我们注册一系列标准的RESTFull形式的API接口(路由),以user为例:

    • [GET] /user #获取用户列表的接口
    • [GET] /user/:id #获取单个用户的接口
    • [POST] /user #新增用户的接口
    • [PUT] /user/:id #修改用户的接口
    • [DELETE] /user/:id #删除用户的接口

    在获取列表的接口中也可以追加查询参数,来限定查询的结果,比如:

    • 查询所有男性用户: /user?gender=male
    • 查询年龄大于等于20并且小于等于29的用户:/user?age_gte=20&age_lte=29

    此外还有分页、排序、匹配、关系查询等查询方式,可以在这里查看更多

    至此我们就拥有了一套强大的数据接口。

    转载于:https://www.cnblogs.com/crazycode2/p/8462242.html

    展开全文
  • 图书管理 src / pages / BookAdd.js // 图书添加页 ...import React from 'react'; // 布局组件 import HomeLayout from '../layouts/HomeLayout'; // 编辑组件 import BookEditor from '../components/BookEditor'...

    图书管理

    src / pages / BookAdd.js   // 图书添加页

    /**
     * 图书添加页面
     */
    import React from 'react';
    // 布局组件
    import HomeLayout from '../layouts/HomeLayout';
    // 编辑组件
    import BookEditor from '../components/BookEditor';
    
    class BookAdd extends React.Component {
      render() {
        return (
          <HomeLayout title="添加图书">
            <BookEditor />
          </HomeLayout>
        );
      }
    }
    
    export default BookAdd;

    src / pages / BookList.js   // 图书列表页

    /**
     * 图书列表页面
     */
    import React from 'react';
    // 布局组件
    import HomeLayout from '../layouts/HomeLayout';
    // 引入 prop-types
    import PropTypes from 'prop-types';
    
    class BookList extends React.Component {
      // 构造器
      constructor(props) {
        super(props);
        // 定义初始化状态
        this.state = {
          bookList: []
        };
      }
    
      /**
       * 生命周期
       * componentWillMount
       * 组件初始化时只调用,以后组件更新不调用,整个生命周期只调用一次
       */
      componentWillMount(){
        // 请求数据
        fetch('http://localhost:8000/book')
          .then(res => res.json())
          .then(res => {
            /**
             * 成功的回调
             * 数据赋值
             */
            this.setState({
              bookList: res
            });
          });
      }
    
      /**
       * 编辑
       */
      handleEdit(book){
        // 跳转编辑页面
        this.context.router.push('/book/edit/' + book.id);
      }
    
      /**
       * 删除
       */
      handleDel(book){
        // 确认框
        const confirmed = window.confirm(`确认要删除书名 ${book.name} 吗?`);
        // 判断
        if(confirmed){
          // 执行删除数据操作
          fetch('http://localhost:8000/book/' + book.id, {
            method: 'delete'
          })
          .then(res => res.json())
          .then(res => {
            /**
             * 设置状态
             * array.filter
             * 把Array的某些元素过滤掉,然后返回剩下的元素
             */
            this.setState({
              bookList: this.state.bookList.filter(item => item.id !== book.id)
            });
            alert('删除用户成功');
          })
          .catch(err => {
            console.log(err);
            alert('删除用户失败');
          });
        }
      }
    
      render() {
        // 定义变量
        const { bookList } = this.state;
    
        return (
          <HomeLayout title="图书列表">
            <table>
              <thead>
                <tr>
                  <th>图书ID</th>
                  <th>图书名称</th>
                  <th>价格</th>
                  <th>操作</th>
                </tr>
              </thead>
    
              <tbody>
                {
                  bookList.map((book) => {
                    return (
                      <tr key={book.id}>
                        <td>{book.id}</td>
                        <td>{book.name}</td>
                        <td>{book.price}</td>
                        <td>
                          <a onClick={() => this.handleEdit(book)}>编辑</a>
                           
                          <a onClick={() => this.handleDel(book)}>删除</a>
                        </td>
                      </tr>
                    );
                  })
                }
              </tbody>
            </table>
          </HomeLayout>
        );
      }
    }
    
    /**
     * 任何使用this.context.xxx的地方,必须在组件的contextTypes里定义对应的PropTypes
     */
    BookList.contextTypes = {
      router: PropTypes.object.isRequired
    };
    
    export default BookList;

    src / components / BookEditor.js   // 图书编辑组件

    /**
     * 图书编辑器组件
     */
    import React from 'react';
    import FormItem from '../components/FormItem'; // 或写成 ./FormItem
    // 高阶组件 formProvider表单验证
    import formProvider from '../utils/formProvider';
    // 引入 prop-types
    import PropTypes from 'prop-types';
    
    class BookEditor extends React.Component {
      // 按钮提交事件
      handleSubmit(e){
        // 阻止表单submit事件自动跳转页面的动作
        e.preventDefault();
        // 定义常量
        const { form: { name, price, owner_id }, formValid, editTarget} = this.props; // 组件传值
        // 验证
        if(!formValid){
          alert('请填写正确的信息后重试');
          return;
        }
    
        // 默认值
        let editType = '添加';
        let apiUrl = 'http://localhost:8000/book';
        let method = 'post';
        // 判断类型
        if(editTarget){
          editType = '编辑';
          apiUrl += '/' + editTarget.id;
          method = 'put';
        }
    
        // 发送请求
        fetch(apiUrl, {
          method, // method: method 的简写
          // 使用fetch提交的json数据需要使用JSON.stringify转换为字符串
          body: JSON.stringify({
            name: name.value,
            price: price.value,
            owner_id: owner_id.value
          }),
          headers: {
            'Content-Type': 'application/json'
          }
        })
        // 强制回调的数据格式为json
        .then((res) => res.json())
        // 成功的回调
        .then((res) => {
          // 当添加成功时,返回的json对象中应包含一个有效的id字段
          // 所以可以使用res.id来判断添加是否成功
          if(res.id){
            alert(editType + '添加图书成功!');
            this.context.router.push('/book/list'); // 跳转到用户列表页面
            return;
          }else{
            alert(editType + '添加图书失败!');
          }
        })
        // 失败的回调
        .catch((err) => console.error(err));
      }
    
      // 生命周期--组件加载中
      componentWillMount(){
        const {editTarget, setFormValues} = this.props;
        if(editTarget){
          setFormValues(editTarget);
        }
      }
      
      render() {
        // 定义常量
        const {form: {name, price, owner_id}, onFormChange} = this.props;
        return (
          <form onSubmit={(e) => this.handleSubmit(e)}>
            <FormItem label="书名:" valid={name.valid} error={name.error}>
              <input
                type="text"
                value={name.value}
                onChange={(e) => onFormChange('name', e.target.value)}/>
            </FormItem>
    
            <FormItem label="价格:" valid={price.valid} error={price.error}>
              <input
                type="number"
                value={price.value || ''}
                onChange={(e) => onFormChange('price', e.target.value)}/>
            </FormItem>
    
            <FormItem label="所有者:" valid={owner_id.valid} error={owner_id.error}>
              <input
                type="text"
                value={owner_id.value || ''}
                onChange={(e) => onFormChange('owner_id', e.target.value)}/>
            </FormItem>
            <br />
            <input type="submit" value="提交" />
          </form>
        );
      }
    }
    
    // 必须给BookEditor定义一个包含router属性的contextTypes
    // 使得组件中可以通过this.context.router来使用React Router提供的方法
    BookEditor.contextTypes = {
      router: PropTypes.object.isRequired
    };
    
    // 实例化
    BookEditor = formProvider({ // field 对象
      // 书名
      name: {
        defaultValue: '',
        rules: [
          {
            pattern: function (value) {
              return value.length > 0;
            },
            error: '请输入图书户名'
          },
          {
            pattern: /^.{1,10}$/,
            error: '图书名最多10个字符'
          }
        ]
      },
      // 价格
      price: {
        defaultValue: 0,
        rules: [
          {
            pattern: function(value){
              return value > 0;
            },
            error: '价格必须大于0'
          }
        ]
      },
      // 所有者
      owner_id: {
        defaultValue: '',
        rules: [
          {
            pattern: function (value) {
              return value > 0;
            },
            error: '请输入所有者名称'
          },
          {
            pattern: /^.{1,10}$/,
            error: '所有者名称最多10个字符'
          }
        ]
      }
    })(BookEditor);
    
    export default BookEditor;

    src / pages / BookEdit.js   // 图书编辑页

    /**
     * 编辑图书页面
     */
    import React from 'react';
    // 布局组件
    import HomeLayout from '../layouts/HomeLayout';
    // 引入 prop-types
    import PropTypes from 'prop-types';
    // 图书编辑器组件
    import BookEditor from '../components/BookEditor';
    
    class BookEdit extends React.Component {
      // 构造器
      constructor(props) {
        super(props);
        // 定义初始化状态
        this.state = {
          book: null
        };
      }
    
      // 生命周期--组件加载中
      componentWillMount(){
        // 定义常量
        const bookId = this.context.router.params.id;
        /**
         * 发送请求
         * 获取用户数据
         */
        fetch('http://localhost:8000/book/' + bookId)
        .then(res => res.json())
        .then(res => {
          this.setState({
            book: res
          });
        })
      }
    
      render() {
        const {book} = this.state;
        return (
          <HomeLayout title="编辑图书">
            {
              book ? <BookEditor editTarget={book} /> : '加载中...'
            }
          </HomeLayout>
        );
      }
    }
    
    BookEdit.contextTypes = {
      router: PropTypes.object.isRequired
    };
    
    export default BookEdit;

    项目结构:

    自动完成组件

    找了个例子看一下效果:

    可以发现,这是一个包含一个输入框、一个下拉框的复合控件。

    实现一个通用组件,在动手写代码之前我会做以下准备工作:

    1. 确定组件结构
    2. 观察组件逻辑
    3. 确定组件内部状态(state)
    4. 确定组件向外暴露的属性(props)

    组件结构

    上面提了,这个组件由一个输入框和一个下拉框组成。

    注意,这里的下拉框是一个“伪”下拉框,并不是指select与option。仔细看上面的动图,可以看得出来这个“伪”下拉框只是一个带边框的、位于输入框正下方的一个列表。

    我们可以假设组件的结构是这样的:

    <div>
      <input type="text"/>
      <ul>
        <li>...</li>
        ...
      </ul>
    </div>

    组件逻辑

    观察动图,可以发现组件有以下行为:

    1. 未输入时,与普通输入框一致
    2. 输入改变时如果有建议的选项,则在下放显示出建议列表
    3. 建议列表可以使用键盘上下键进行选择,选择某一项时该项高亮显示,并且输入框的值变为该项的值
    4. 当移出列表(在第一项按上键或在最后一项按下键)时,输入框的值变为原来输入的值(图中的“as”)
    5. 按下回车键可以确定选择该项,列表消失
    6. 可以使用鼠标在列表中进行选择,鼠标移入的列表项高亮显示

    组件内部状态

    一个易用的通用组件应该对外隐藏只有内部使用的状态。使用React组件的state来维护组件的内部状态。

    根据组件逻辑,我们可以确定自动完成组件需要这些内部状态:

    • 逻辑2|3|4:输入框中显示的值,默认为空字符串(displayValue)
    • 逻辑3|6:建议列表中高亮的项目,可以维护一个项目在列表中的索引,默认为-1(activeItemIndex)

    组件暴露的属性

    我们的目标是一个通用的组件,所以类似组件实际的值、推荐列表这样的状态,应该由组件的使用者来控制:

    如上图,组件应向外暴露的属性有:

    • value:代表实际的值(不同于上面的displayValue表示显示的、临时的值,value表示的是最终的值)
    • options:代表当前组件的建议列表,为空数组时,建议列表隐藏
    • onValueChange:用于在输入值或确定选择了某一项时通知使用者的回调方法,使用者可以在这个回调方法中对options、value进行更新

    实现

    确定了组件结构、组件逻辑、内部状态和外部属性之后,就可以着手进行编码了:

    /src/components下新建AutoComplete.js文件,写入组件的基本代码:

    /**
     * 自动完成组件
     */
    import React from 'react';
    // 引入 prop-types
    import PropTypes from 'prop-types';
    
    class AutoComplete extends React.Component {
      // 构造器
      constructor(props) {
        super(props);
        // 定义初始化状态
        this.state = {
          displayValue: '',
          activeItemIndex: -1
        };
      }
    
      // 渲染
      render() {
        const {displayValue, activeItemIndex} = this.state;
        // 组件传值
        const {value, options} = this.props;
        return (
          <div>
            <input value={value}/>
            {options.length > 0 && (
              <ul>
                {
                  options.map((item, index) => {
                    return (
                      <li key={index}>
                        {item.text || item}
                      </li>
                    );
                  })
                }
              </ul>
            )}
          </div>
        );
      }
    }
    
    // 通用组件最好写一下propTypes约束
    AutoComplete.propTypes = {
      value: PropTypes.string.isRequired, // 字符串
      options: PropTypes.array.isRequired, // 数组
      onValueChange: PropTypes.func.isRequired // 函数
    };
    
    // 向外暴露
    export default AutoComplete;

    为了方便调试,把BookEditor里的owner_id输入框换成AutoComplete,传入一些测试数据:

    ...
    import AutoComplete from './AutoComplete';
    
    class BookEditor extends React.Component {
      ...
      render () {
        const {form: {name, price, owner_id}, onFormChange} = this.props;
        return (
          <form onSubmit={this.handleSubmit}>
            ...
            <FormItem label="所有者:" valid={owner_id.valid} error={owner_id.error}>
    
              <AutoComplete
                value={owner_id.value ? owner_id.value + '' : ''}
                options={['10000(一韬)', '10001(张三)']}
                onValueChange={value => onFormChange('owner_id', value)}
              />
            </FormItem>
          </form>
        );
      }
    }
    ...
    

    现在大概是这个样子:

    有点怪,我们来给它加上样式。

    新建/src/styles文件夹和auto-complete.less文件,写入代码:

    .wrapper {
      display: inline-block;
      position: relative;
    }
    
    .options {
      margin: 0;
      padding: 0;
      list-style: none;
      top: 110%;
      left: 0;
      right: 0;
      position: absolute;
      box-shadow: 1px 1px 10px 0 rgba(0, 0, 0, .6);
    
      > li {
        padding: 3px 6px;
    
        &.active {
          background-color: #0094ff;
          color: white;
        }
      }
    }

    AutoComplete.js加上className:

    /**
     * 自动完成组件
     */
    import React from 'react';
    // 引入 prop-types
    import PropTypes from 'prop-types';
    // 引入样式
    import '../styles/auto-complete.less';
    
    class AutoComplete extends React.Component {
      // 构造器
      constructor(props) {
        super(props);
        // 定义初始化状态
        this.state = {
          displayValue: '',
          activeItemIndex: -1
        };
      }
    
      // 渲染
      render() {
        const {displayValue, activeItemIndex} = this.state;
        // 组件传值
        const {value, options} = this.props;
        return (
          <div className="wrapper">
            <input value={displayValue || value}/>
            {options.length > 0 && (
              <ul className="options">
                {
                  options.map((item, index) => {
                    return (
                      <li key={index} className={activeItemIndex === index ? 'active' : ''}>
                        {item.text || item}
                      </li>
                    );
                  })
                }
              </ul>
            )}
          </div>
        );
      }
    }
    
    // 通用组件最好写一下propTypes约束
    AutoComplete.propTypes = {
      value: PropTypes.string.isRequired, // 字符串
      options: PropTypes.array.isRequired, // 数组
      onValueChange: PropTypes.func.isRequired // 函数
    };
    
    // 向外暴露
    export default AutoComplete;

    稍微顺眼一些了吧:

    现在需要在AutoComplete中监听一些事件:

    • 输入框的onChange
    • 输入框的onKeyDown,用于对上下键、回车键进行监听处理
    • 列表项目的onClick
    • 列表项目的onMouseEnter,用于在鼠标移入时设置activeItemIndex
    • 列表的onMouseLeave,用户鼠标移出时重置activeItemIndex
    ...
    // 获得当前元素value值
    function getItemValue (item) {
      return item.value || item;
    }
    
    class AutoComplete extends React.Component {
      // 构造器
      constructor(props) {
        super(props);
        // 定义初始化状态
        this.state = {
          displayValue: '',
          activeItemIndex: -1
        };
    
        // 对上下键、回车键进行监听处理
        this.handleKeyDown = this.handleKeyDown.bind(this);
        // 对鼠标移出进行监听处理
        this.handleLeave = this.handleLeave.bind(this);
      }
    
      // 处理输入框改变事件
      handleChange(value){
        //
      }
    
      // 处理上下键、回车键点击事件
      handleKeyDown(e){
        //
      }
    
      // 处理鼠标移入事件
      handleEnter(index){
        //
      }
    
      // 处理鼠标移出事件
      handleLeave(){
        //
      }
    
      // 渲染
      render() {
        const {displayValue, activeItemIndex} = this.state;
        // 组件传值
        const {value, options} = this.props;
        return (
          <div className="wrapper">
            <input
              value={displayValue || value}
              onChange={e => this.handleChange(e.target.value)}
              onKeyDown={this.handleKeyDown} />
            {options.length > 0 && (
              <ul className="options" onMouseLeave={this.handleLeave}>
                {
                  options.map((item, index) => {
                    return (
                      <li
                        key={index}
                        className={activeItemIndex === index ? 'active' : ''}
                        onMouseEnter={() => this.handleEnter(index)}
                        onClick={() => this.handleChange(getItemValue(item))}
                      >
                        {item.text || item}
                      </li>
                    );
                  })
                }
              </ul>
            )}
          </div>
        );
      }
    }
    ...

    先来实现handleChange方法,handleChange方法用于在用户输入、选择列表项的时候重置内部状态(清空displayName、设置activeItemIndex为-1),并通过回调将新的值传递给组件使用者:

    ...
    handleChange (value) {
      this.setState({activeItemIndex: -1, displayValue: ''});
      this.props.onValueChange(value);
    }
    ...
    

    然后是handleKeyDown方法,这个方法中需要判断当前按下的键是否为上下方向键或回车键,如果是上下方向键则根据方向设置当前被选中的列表项;如果是回车键并且当前有选中状态的列表项,则调用handleChange:

    ...
    handleKeyDown (e) {
      const {activeItemIndex} = this.state;
      const {options} = this.props;
    
      switch (e.keyCode) {
        // 13为回车键的键码(keyCode)
        case 13: {
          // 判断是否有列表项处于选中状态
          if (activeItemIndex >= 0) {
            // 防止按下回车键后自动提交表单
            e.preventDefault();
            e.stopPropagation();
            this.handleChange(getItemValue(options[activeItemIndex]));
          }
          break;
        }
        // 38为上方向键,40为下方向键
        case 38:
        case 40: {
          e.preventDefault();
          // 使用moveItem方法对更新或取消选中项
          this.moveItem(e.keyCode === 38 ? 'up' : 'down');
          break;
        }
      }
    }
    
    moveItem (direction) {
      const {activeItemIndex} = this.state;
      const {options} = this.props;
      const lastIndex = options.length - 1;
      let newIndex = -1;
    
      // 计算新的activeItemIndex
      if (direction === 'up') {
        if (activeItemIndex === -1) {
          // 如果没有选中项则选择最后一项
          newIndex = lastIndex;
        } else {
          newIndex = activeItemIndex - 1;
        }
      } else {
        if (activeItemIndex < lastIndex) {
          newIndex = activeItemIndex + 1;
        }
      }
    
      // 获取新的displayValue
      let newDisplayValue = '';
      if (newIndex >= 0) {
        newDisplayValue = getItemValue(options[newIndex]);
      }
    
      // 更新状态
      this.setState({
        displayValue: newDisplayValue,
        activeItemIndex: newIndex
      });
    }
    ...
    

    handleEnter和handleLeave方法比较简单:

    ...
    handleEnter (index) {
      const currentItem = this.props.options[index];
      this.setState({activeItemIndex: index, displayValue: getItemValue(currentItem)});
    }
    
    handleLeave () {
      this.setState({activeItemIndex: -1, displayValue: ''});
    }
    ...
    

    看一下效果:

    完整的代码:

    src / components / AutoComplete.js

    /**
     * 自动完成组件
     */
    import React from 'react';
    // 引入 prop-types
    import PropTypes from 'prop-types';
    // 引入样式
    import '../styles/auto-complete.less';
    
    // 获得当前元素value值
    function getItemValue (item) {
      return item.value || item;
    }
    
    class AutoComplete extends React.Component {
      // 构造器
      constructor(props) {
        super(props);
        // 定义初始化状态
        this.state = {
          displayValue: '',
          activeItemIndex: -1
        };
    
        // 对上下键、回车键进行监听处理
        this.handleKeyDown = this.handleKeyDown.bind(this);
        // 对鼠标移出进行监听处理
        this.handleLeave = this.handleLeave.bind(this);
      }
    
      // 处理输入框改变事件
      handleChange(value){
        // 选择列表项的时候重置内部状态
        this.setState({
          activeItemIndex: -1,
          displayValue: ''
        });
        // 通过回调将新的值传递给组件使用者
        this.props.onValueChange(value);
      }
    
      // 处理上下键、回车键点击事件
      handleKeyDown(e){
        const {activeItemIndex} = this.state;
        const {options} = this.props;
    
        /**
         * 判断键码
         */
        switch (e.keyCode) {
          // 13为回车键的键码(keyCode)
          case 13: {
            // 判断是否有列表项处于选中状态
            if(activeItemIndex >= 0){
              // 防止按下回车键后自动提交表单
              e.preventDefault();
              e.stopPropagation();
              // 输入框改变事件
              this.handleChange(getItemValue(options[activeItemIndex]));
            }
            break;
          }
          // 38为上方向键,40为下方向键
          case 38:
          case 40: {
            e.preventDefault();
            // 使用moveItem方法对更新或取消选中项
            this.moveItem(e.keyCode === 38 ? 'up' : 'down');
            break;
          }
          default: {
            //
          }
        }
      }
    
      // 使用moveItem方法对更新或取消选中项
      moveItem(direction){
        const {activeItemIndex} = this.state;
        const {options} = this.props;
        const lastIndex = options.length - 1;
        let newIndex = -1;
    
        // 计算新的activeItemIndex
        if(direction === 'up'){ // 点击上方向键
          if(activeItemIndex === -1){
            // 如果没有选中项则选择最后一项
            newIndex = lastIndex;
          }else{
            newIndex = activeItemIndex - 1;
          }
        }else{ // 点击下方向键
          if(activeItemIndex < lastIndex){
            newIndex = activeItemIndex + 1;
          }
        }
    
        // 获取新的displayValue
        let newDisplayValue = '';
        if(newIndex >= 0){
          newDisplayValue = getItemValue(options[newIndex]);
        }
    
        // 更新状态
        this.setState({
          displayValue: newDisplayValue,
          activeItemIndex: newIndex
        });
      }
    
      // 处理鼠标移入事件
      handleEnter(index){
        const currentItem = this.props.options[index];
        this.setState({
          activeItemIndex: index,
          displayValue: getItemValue(currentItem)
        });
      }
    
      // 处理鼠标移出事件
      handleLeave(){
        this.setState({
          activeItemIndex: -1,
          displayValue: ''
        });
      }
    
      // 渲染
      render() {
        const {displayValue, activeItemIndex} = this.state;
        // 组件传值
        const {value, options} = this.props;
        return (
          <div className="wrapper">
            <input
              value={displayValue || value}
              onChange={e => this.handleChange(e.target.value)}
              onKeyDown={this.handleKeyDown} />
            {options.length > 0 && (
              <ul className="options" onMouseLeave={this.handleLeave}>
                {
                  options.map((item, index) => {
                    return (
                      <li
                        key={index}
                        className={activeItemIndex === index ? 'active' : ''}
                        onMouseEnter={() => this.handleEnter(index)}
                        onClick={() => this.handleChange(getItemValue(item))}
                      >
                        {item.text || item}
                      </li>
                    );
                  })
                }
              </ul>
            )}
          </div>
        );
      }
    }
    
    // 通用组件最好写一下propTypes约束
    AutoComplete.propTypes = {
      value: PropTypes.string.isRequired, // 字符串
      options: PropTypes.array.isRequired, // 数组
      onValueChange: PropTypes.func.isRequired // 函数
    };
    
    // 向外暴露
    export default AutoComplete;

    基本上已经实现了自动完成组件,但是从图中可以发现选择后的值把用户名也带上了。

    但是如果吧options中的用户名去掉,这个自动完成也就没有什么意义了,我们来把BookEditor中传入的options改一改:

    ...
    <AutoComplete
      value={owner_id.value ? owner_id.value + '' : ''}
      options={[{text: '10000(一韬)', value: 10000}, {text: '10001(张三)', value: 10001}]}
      onValueChange={value => onFormChange('owner_id', value)}
    />
    ...
    

    刷新看一看,已经达到了我们期望的效果:

    有时候我们显示的值并不一定是我们想要得到的值,这也是为什么我在组件的代码里有一个getItemValue方法了。

    调用接口获取建议列表

    也许有人要问了,这个建议列表为什么一直存在?

    这是因为我们为了方便测试给了一个固定的options值,现在来完善一下,修改BookEditor.js

    import React from 'react';
    import FormItem from './FormItem';
    import AutoComplete from './AutoComplete';
    import formProvider from '../utils/formProvider';
    
    class BookEditor extends React.Component {
      constructor (props) {
        super(props);
        this.state = {
          recommendUsers: []
        };
        ...
      }
      ...
      getRecommendUsers (partialUserId) {
        fetch('http://localhost:8000/user?id_like=' + partialUserId)
          .then((res) => res.json())
          .then((res) => {
            if (res.length === 1 && res[0].id === partialUserId) {
              // 如果结果只有1条且id与输入的id一致,说明输入的id已经完整了,没必要再设置建议列表
              return;
            }
    
            // 设置建议列表
            this.setState({
              recommendUsers: res.map((user) => {
                return {
                  text: `${user.id}(${user.name})`,
                  value: user.id
                };
              })
            });
          });
      }
    
      timer = 0;
      handleOwnerIdChange (value) {
        this.props.onFormChange('owner_id', value);
        this.setState({recommendUsers: []});
    
        // 使用“节流”的方式进行请求,防止用户输入的过程中过多地发送请求
        if (this.timer) {
          clearTimeout(this.timer);
        }
    
        if (value) {
          // 200毫秒内只会发送1次请求
          this.timer = setTimeout(() => {
            // 真正的请求方法
            this.getRecommendUsers(value);
            this.timer = 0;
          }, 200);
        }
      }
    
      render () {
        const {recommendUsers} = this.state;
        const {form: {name, price, owner_id}, onFormChange} = this.props;
        return (
          <form onSubmit={this.handleSubmit}>
            ...
            <FormItem label="所有者:" valid={owner_id.valid} error={owner_id.error}>
              <AutoComplete
                value={owner_id.value ? owner_id.value + '' : ''}
                options={recommendUsers}
                onValueChange={value => this.handleOwnerIdChange(value)}
              />
            </FormItem>
            ...
          </form>
        );
      }
    }
    ...

    看一下最后的样子:

    完整的代码:

    src / components / BookEditor.js

    /**
     * 图书编辑器组件
     */
    import React from 'react';
    import FormItem from '../components/FormItem'; // 或写成 ./FormItem
    // 高阶组件 formProvider表单验证
    import formProvider from '../utils/formProvider';
    // 引入 prop-types
    import PropTypes from 'prop-types';
    // 引入自动完成组件
    import AutoComplete from './AutoComplete';
    
    class BookEditor extends React.Component {
      // 构造器
      constructor(props) {
        super(props);
      
        this.state = {
          recommendUsers: []
        };
      }
    
      // 获取推荐用户信息
      getRecommendUsers (partialUserId) {
        // 请求数据
        fetch('http://localhost:8000/user?id_like=' + partialUserId)
        .then((res) => res.json())
        .then((res) => {
          if(res.length === 1 && res[0].id === partialUserId){
            // 如果结果只有1条且id与输入的id一致,说明输入的id已经完整了,没必要再设置建议列表
            return;
          }
    
          // 设置建议列表
          this.setState({
            recommendUsers: res.map((user) => {
              return {
                text: `${user.id}(${user.name})`,
                value: user.id
              }
            })
          });
        })
      }
    
      // 计时器
      timer = 0;
      handleOwnerIdChange(value){
        this.props.onFormChange('owner_id', value);
        this.setState({
          recommendUsers: []
        });
    
        // 使用"节流"的方式进行请求,防止用户输入的过程中过多地发送请求
        if(this.timer){
          // 清除计时器
          clearTimeout(this.timer);
        }
    
        if(value){
          // 200毫秒内只会发送1次请求
          this.timer = setTimeout(() => {
            // 真正的请求方法
            this.getRecommendUsers(value);
            this.timer = 0;
          }, 200);
        }
      }
    
      // 按钮提交事件
      handleSubmit(e){
        // 阻止表单submit事件自动跳转页面的动作
        e.preventDefault();
        // 定义常量
        const { form: { name, price, owner_id }, formValid, editTarget} = this.props; // 组件传值
        // 验证
        if(!formValid){
          alert('请填写正确的信息后重试');
          return;
        }
    
        // 默认值
        let editType = '添加';
        let apiUrl = 'http://localhost:8000/book';
        let method = 'post';
        // 判断类型
        if(editTarget){
          editType = '编辑';
          apiUrl += '/' + editTarget.id;
          method = 'put';
        }
    
        // 发送请求
        fetch(apiUrl, {
          method, // method: method 的简写
          // 使用fetch提交的json数据需要使用JSON.stringify转换为字符串
          body: JSON.stringify({
            name: name.value,
            price: price.value,
            owner_id: owner_id.value
          }),
          headers: {
            'Content-Type': 'application/json'
          }
        })
        // 强制回调的数据格式为json
        .then((res) => res.json())
        // 成功的回调
        .then((res) => {
          // 当添加成功时,返回的json对象中应包含一个有效的id字段
          // 所以可以使用res.id来判断添加是否成功
          if(res.id){
            alert(editType + '添加图书成功!');
            this.context.router.push('/book/list'); // 跳转到用户列表页面
            return;
          }else{
            alert(editType + '添加图书失败!');
          }
        })
        // 失败的回调
        .catch((err) => console.error(err));
      }
    
      // 生命周期--组件加载中
      componentWillMount(){
        const {editTarget, setFormValues} = this.props;
        if(editTarget){
          setFormValues(editTarget);
        }
      }
      
      render() {
        // 定义常量
        const {recommendUsers} = this.state;
        const {form: {name, price, owner_id}, onFormChange} = this.props;
        return (
          <form onSubmit={(e) => this.handleSubmit(e)}>
            <FormItem label="书名:" valid={name.valid} error={name.error}>
              <input
                type="text"
                value={name.value}
                onChange={(e) => onFormChange('name', e.target.value)}/>
            </FormItem>
    
            <FormItem label="价格:" valid={price.valid} error={price.error}>
              <input
                type="number"
                value={price.value || ''}
                onChange={(e) => onFormChange('price', e.target.value)}/>
            </FormItem>
    
            <FormItem label="所有者:" valid={owner_id.valid} error={owner_id.error}>
              <AutoComplete
                value={owner_id.value ? owner_id.value + '' : ''}
                options={recommendUsers}
                onValueChange={value => this.handleOwnerIdChange(value)} />
            </FormItem>
            <br />
            <input type="submit" value="提交" />
          </form>
        );
      }
    }
    
    // 必须给BookEditor定义一个包含router属性的contextTypes
    // 使得组件中可以通过this.context.router来使用React Router提供的方法
    BookEditor.contextTypes = {
      router: PropTypes.object.isRequired
    };
    
    // 实例化
    BookEditor = formProvider({ // field 对象
      // 书名
      name: {
        defaultValue: '',
        rules: [
          {
            pattern: function (value) {
              return value.length > 0;
            },
            error: '请输入图书户名'
          },
          {
            pattern: /^.{1,10}$/,
            error: '图书名最多10个字符'
          }
        ]
      },
      // 价格
      price: {
        defaultValue: 0,
        rules: [
          {
            pattern: function(value){
              return value > 0;
            },
            error: '价格必须大于0'
          }
        ]
      },
      // 所有者
      owner_id: {
        defaultValue: '',
        rules: [
          {
            pattern: function (value) {
              return value > 0;
            },
            error: '请输入所有者名称'
          },
          {
            pattern: /^.{1,10}$/,
            error: '所有者名称最多10个字符'
          }
        ]
      }
    })(BookEditor);
    
    export default BookEditor;

    .

    展开全文
  • 我们现在的应用只有个Hello React的页面,现在需要添加个用于添加用户的页面。 首先在/src目录下新增个pages目录,用于存放渲染页面的组件。 然后在/src/pages中新增个UserAdd.js文件。 在这个文件中写入...

    1.安装 路由

    npm install -S react-router@3.x

    2.新增页面

    我们现在的应用只有一个Hello React的页面,现在需要添加一个用于添加用户的页面。

    首先在/src目录下新增一个pages目录,用于存放渲染页面的组件。

    然后在/src/pages中新增一个UserAdd.js文件。

    在这个文件中写入一个基本的React组件:

    import React from 'react';
    
    // 添加用户组件
    class UserAdd extends React.Component {
      // 渲染
      render() {
        return (
          <div>User add page.</div>
        );
      }
    }
    
    // 向外暴露
    export default UserAdd;

    3.配置路由

    我们需要使用react-router提供的路由组件来控制当前路由下页面应该渲染的组件。

    修改/src/index.js为:

    // 配置路由
    import React from 'react';
    import ReactDOM from 'react-dom';
    // 引入react-router
    import { Router, Route, hashHistory } from 'react-router';
    import UserAddPage from './pages/UserAdd'
    
    // 渲染
    ReactDOM.render((
      <Router history={hashHistory}>
      	<Route path="/user/add" component={UserAddPage} />
      </Router>
    ), document.getElementById('root'));
    

    打开浏览器的http://localhost:3000发现页面一片空白,在控制台中可以看到一个错误信息: 

    这是因为我们只配了一个/user/add的路由,我们来访问一下http://localhost:3000/#/user/add看看 

    新页面出来了。现在需要把上面的那个错误干掉:新建一个主页,并在主页中添加一个链接链到添加用户的页面。

    还是在/src/pages/中新建一个Home.js,写入代码:

    import React from 'react';
    import { Link } from 'react-router';
    
    class Home extends React.Component {
      render () {
        return (
          <div>
            <header>
              <h1>Welcome</h1>
            </header>
    
            <main>
              <Link to="/user/add">添加用户</Link>
            </main>
          </div>
        );
      }
    }
    
    export default Home;

    然后在/src/index.js中添加一个指向Home的Route:

    // 配置路由
    import React from 'react';
    import ReactDOM from 'react-dom';
    // 引入react-router
    import { Router, Route, hashHistory } from 'react-router';
    import HomePage from './pages/Home';
    import UserAddPage from './pages/UserAdd';
    
    // 渲染
    ReactDOM.render((
      <Router history={hashHistory}>
      	<Route path="/" component={HomePage} />
      	<Route path="/user/add" component={UserAddPage} />
      </Router>
    ), document.getElementById('root'));
    

    首页已经可以正常访问了,并且点击添加用户也正确地跳转到了添加用户页面。

    Q: 为什么我看到的url里有一个’#’? 
    A: 这是由于我们给Router组件传入了hashHistory,url中’#’及’#’以后的部分属于hash,hash的变化并不会引起页面的重新刷新,而hashHistory会监听hash的变化使得Router组件能够根据url渲染出正确的组件。除了hash History之外还有browserHistory和memoryHistory。使用browserHistory可以让url变得像标准的url一样(没有#),但是需要在后端做一些特殊处理;memoryHistory是用于做服务端渲染时使用的。

    Q: 为什么Home.js里要用Link组件而不是一个标准的a标签? 
    A: 上面说了,我们使用了hashHistory,正确的页面url中应该都是有一个’#’的,如果直接使用a标签,你需要这么写:<a href=”/#/user/add”>添加用户</a>。但是如果我们想要换成browserHistory,就需要把所有标签中的’#’去掉。使用react-router提供的Link组件可以让我们无视history之间的差异性,直接写标准的路由”/user/add”就可以了。此外,由于我们写的是单页面应用(SPA),Link组件会阻止页面的跳转(仅仅只是改变了url,然后改变了渲染的组件)。

    4.编写页面

    我们定义了User的结构为:

    {
      "id": 10000,
      "name": "一韬",
      "age": 25,
      "gender": "male"
    }
    

    在新建的时候,我们需要使用post方式将新用户的name、age、gender发送给接口http://localhost:3000/user,因此这个添加用户的页面需要提供一个包含上述3个字段控件的表单:

    src / pages / UserAdd.js

    import React from 'react';
    
    // 添加用户组件
    class UserAdd extends React.Component {
      // 构造器
      constructor(props) {
        super(props);
        // 定义初始化状态
        this.state = {};
      }
      
      render() {
        return (
          <div>
            <header>
              <div>添加用户</div>
            </header>
    
            <main>
              <form>
                <label>用户名:</label>
                <input type="text" />
                <br />
                <label>年龄:</label>
                <input type="number" />
                <br />
                <label>性别:</label>
                <select>
                  <option value="">请选择</option>
                  <option value="male">男</option>
                  <option value="female">女</option>
                </select>
                <br />
                <br />
                <input type="submit" value="提交" />
              </form>
            </main>
          </div>
        );
      }
    }
    
    export default UserAdd;
    

    现在我们的页面是这个样子: 

    获取表单的值

    现在我们可以在表单中填写数据了,但是还不能获取到我们输入的值。

    在React中处理表单有些不一样,由于React提倡“单向数据流”,React中的表单并不提供双向数据绑定的功能,我们需要给表单绑定它的value,然后提供一个onChange的处理方法来更新value的值。

    这里我们使用组件的state来维护表单的值,在onChange的时候使用setState来更新值,最后,在表单提交事件被触发的时候,我们输出state来观察最终获得的表单值:

    src / pages / UserAdd.js

    import React from 'react';
    
    // 添加用户组件
    class UserAdd extends React.Component {
      // 构造器
      constructor(props) {
        super(props);
        // 定义初始化状态
        this.state = {
          name: '',
          age: 0,
          gender: ''
        };
      }
      // 输入框改变事件
      handleValueChange(field, value, type='string') {
        // 由于表单的值都是字符串,我们可以根据传入type为number来手动转换value的类型为number类型
        if(type === 'number'){
          value = + value;
        }
        // 设置状态值
        this.setState({
          [field]: value
        });
      }
      // 按钮提交事件
      handleSubmit(e){
        // 阻止表单submit事件自动跳转页面的动作
        e.preventDefault();
        alert(JSON.stringify(this.state));
      }
      
      render() {
        // 定义常量
        const {name, age, gender} = this.state;
        return (
          <div>
            <header>
              <div>添加用户</div>
            </header>
    
            <main>
              <form onSubmit={(e) => this.handleSubmit(e)}>
                <label>用户名:</label>
                <input
                  type="text"
                  value={name}
                  onChange={(e) => this.handleValueChange('name', e.target.value)} />
                <br />
                <label>年龄:</label>
                <input
                  type="number"
                  value={age || ''}
                  onChange={(e) => this.handleValueChange('age', e.target.value, 'number')} />
                <br />
                <label>性别:</label>
                <select
                  value={gender}
                  onChange={(e) => this.handleValueChange('gender', e.target.value)}>
                  <option value="">请选择</option>
                  <option value="male">男</option>
                  <option value="female">女</option>
                </select>
                <br />
                <br />
                <input type="submit" value="提交" />
              </form>
            </main>
          </div>
        );
      }
    }
    
    export default UserAdd;
    

    在页面中填写表单,点提交后可以看到如下界面:

    调用接口创建用户

    拿到了表单数据,我们离目标已经很近了,就差最后一步:把表单的值通过接口提交

    调用接口可以有很多方法:Ajax、表单提交、fetch。

    由于直接使用表单提交会引起页面的跳转,这不符合我们单页应用的原则。这里我们使用比Ajax更先进易用的fetch。

    修改/pages/UserAdd.js中的handleSubmit方法:

    handleSubmit (e) {
      e.preventDefault();
    
      const {name, age, gender} = this.state;
      fetch('http://localhost:3000/user', {
        method: 'post',
        // 使用fetch提交的json数据需要使用JSON.stringify转换为字符串
        body: JSON.stringify({
          name,
          age,
          gender
        }),
        headers: {
          'Content-Type': 'application/json'
        }
      })
        .then((res) => res.json())
        .then((res) => {
          // 当添加成功时,返回的json对象中应包含一个有效的id字段
          // 所以可以使用res.id来判断添加是否成功
          if (res.id) {
            alert('添加用户成功');
            this.setState({
              name: '',
              age: 0,
              gender: ''
            });
          } else {
            alert('添加失败');
          }
        })
        .catch((err) => console.error(err));
    }
    

    填写表单然后点击提交后,可以看到弹出一个添加成功的提示框:

    /server目录执行json-server db.json -w -p 8000

    打开http://localhost:8000/user查看所有用户列表,发现用户已经被添加进去了:

    至此就完成了基本的添加用户功能~

    注:完成的代码 src / pages / UserAdd.js

    import React from 'react';
    
    // 添加用户组件
    class UserAdd extends React.Component {
      // 构造器
      constructor(props) {
        super(props);
        // 定义初始化状态
        this.state = {
          name: '',
          age: 0,
          gender: ''
        };
      }
      // 输入框改变事件
      handleValueChange(field, value, type='string') {
        // 由于表单的值都是字符串,我们可以根据传入type为number来手动转换value的类型为number类型
        if(type === 'number'){
          value = + value;
        }
        // 设置状态值
        this.setState({
          [field]: value
        });
      }
      // 按钮提交事件
      handleSubmit(e){
        // 阻止表单submit事件自动跳转页面的动作
        e.preventDefault();
        // 定义常量
        const { name, age, gender } = this.state;
        // 发送请求
        fetch('http://localhost:8000/user', {
          method: 'post',
          // 使用fetch提交的json数据需要使用JSON.stringify转换为字符串
          body: JSON.stringify({
            name,
            age,
            gender
          }),
          headers: {
            'Content-Type': 'application/json'
          }
        })
        // 强制回调的数据格式为json
        .then((res) => res.json())
        // 成功的回调
        .then((res) => {
          // 当添加成功时,返回的json对象中应包含一个有效的id字段
          // 所以可以使用res.id来判断添加是否成功
          if(res.id){
            alert('添加用户成功!');
            // 初始化
            this.setState({
              name: '',
              age: 0,
              gender: ''
            });
          }else{
            alert('添加用户失败!');
          }
        })
        // 失败的回调
        .catch((err) => console.error(err));
      }
      
      render() {
        // 定义常量
        const {name, age, gender} = this.state;
        return (
          <div>
            <header>
              <div>添加用户</div>
            </header>
    
            <main>
              <form onSubmit={(e) => this.handleSubmit(e)}>
                <label>用户名:</label>
                <input
                  type="text"
                  value={name}
                  onChange={(e) => this.handleValueChange('name', e.target.value)} />
                <br />
                <label>年龄:</label>
                <input
                  type="number"
                  value={age || ''}
                  onChange={(e) => this.handleValueChange('age', e.target.value, 'number')} />
                <br />
                <label>性别:</label>
                <select
                  value={gender}
                  onChange={(e) => this.handleValueChange('gender', e.target.value)}>
                  <option value="">请选择</option>
                  <option value="male">男</option>
                  <option value="female">女</option>
                </select>
                <br />
                <br />
                <input type="submit" value="提交" />
              </form>
            </main>
          </div>
        );
      }
    }
    
    export default UserAdd;

    .

    转载于:https://www.cnblogs.com/crazycode2/p/8462237.html

    展开全文
  • 开始之前你需要有台安装好node环境(包括npm)的电脑,并且有个趁手的代码编辑器(或IDE),如果没有,请使用Webstorm作为你的第款(也许是最后款)开发工具

    开始之前

    你需要有一台安装好node环境(包括npm)的电脑,并且有一个趁手的代码编辑器(或IDE),如果没有,请使用Webstorm作为你的第一款(也许是最后一款)开发工具。

    注意:请安装node 6.3及以上版本!

    服务端搭建

    在引言中说了,我们要做一个管理系统少不了接口和数据。

    这里我们借助json-server这个工具快速搭建后台管理系统的服务端程序:

    • 首先执行npm i json-server -g把json-server作为全局工具安装
    • 新建一个项目目录(后面文中所有的路径根目录都表示该项目目录)
    • 在根目录中执行npm init初始化一个npm项目(会有一些项目配置需要你输入,一直敲回车就行了)
    • 新建/server目录用于放置服务端的文件
    • 新建/server/db.json文件作为服务端的数据库文件
    • /server/db.json文件中写入以下数据:
    {
      "user": [
        {
          "id": 10000,
          "name": "一韬",
          "age": 25,
          "gender": "male"
        },
        {
          "id": 10001,
          "name": "张三",
          "age": 30,
          "gender": "female"
        }
      ],
      "book": [
        {
          "id": 10000,
          "name": "JavaScript从入门到精通",
          "price": 9990,
          "owner_id": 10000
        },
        {
          "id": 10001,
          "name": "Java从入门到放弃",
          "price": 1990,
          "owner_id": 10001
        }
      ]
    }
    • 最后在/server目录执行json-server db.json -w -p 3000

    现在打开浏览器,访问网址http://localhost:3000,可以看到如下界面:

    这里写图片描述

    点击Routes下面的user会跳转到http://localhost:3000/user,可以看到我们之前在db.json中写入的user数组:

    [
      {
        "id": 10000,
        "name": "一韬",
        "age": 25,
        "gender": "male"
      },
      {
        "id": 10001,
        "name": "张三",
        "age": 30,
        "gender": "female"
      }
    ]

    访问http://localhost:3000/book同上

    我们在db.json文件中写入了标准的JSON格式数据,这个json里有一个user数组和一个book数组,这就告诉json-server,我们的数据库里有一个名为user的”表”和一个名为book的”表”,并且表里的数据为xxx,然后json-server就会启动服务器,并且以每个”表”为单位为我们注册一系列标准的RESTFull形式的API接口(路由),以user为例:

    • [GET] /user #获取用户列表的接口
    • [GET] /user/:id #获取单个用户的接口
    • [POST] /user #新增用户的接口
    • [PUT] /user/:id #修改用户的接口
    • [DELETE] /user/:id #删除用户的接口

    在获取列表的接口中也可以追加查询参数,来限定查询的结果,比如:

    • 查询所有男性用户: /user?gender=male
    • 查询年龄大于等于20并且小于等于29的用户:/user?age_gte=20&age_lte=29

    此外还有分页、排序、匹配、关系查询等查询方式,可以在这里查看更多

    至此我们就拥有了一套强大的数据接口。

    客户端搭建

    搞定了服务端,就可以安心地写客户端代码了~

    • 在根目录执行npm i roadhog -g来安装roadhog,这是一个快速且功能强大的react项目搭建工具
    • 新建/src目录,用于存放客户端代码
    • 新建/public目录,用户存放项目的静态文件(图片等)
    • 新建/src/index.js/public/index.html两个文件,分别作为应用的入口文件和页面的入口文件
    • 执行npm i react react-dom react-router -S,安装基本的react依赖
    • /src/index.js中写入以下代码
    import React from 'react';
    import ReactDOM from 'react-dom';
    
    ReactDOM.render((
      <div>Hello React!</div>
    ), document.getElementById('app'));
    • /public/index.html里写入以下代码(webstorm中只需要输入!并按下tab建就可以快速完成一个html页面模板)
    <!doctype html>
    <html lang="zh-cn">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport"
            content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Hello React</title>
    </head>
    <body>
      <!-- 这个div必须和index.js里的render方法里传入的第二个参数保持一致 -->
      <div id="app"></div>
      <!-- roadhog背后会把你的代码从入口文件开始打包成一个index.js文件 -->
      <script src="./index.js"></script>
    </body>
    </html>

    上面都搞定之后就可以执行roadhog server来启动我们的react应用了!

    启动成功会自动打开http://localhost:8000,如果你看到页面里显示了”Hello React!”,说明你的项目搭建已经大功告成了~

    后续每次我们都需要执行:

    • cd server && json-server db.json -w -p 3000
    • roadhog server

    来开始我们的工作流,我们可以把这两个指令写入package.json的scripts中,来节约我们的脑细胞:

    "scripts": {
      "server": "cd server && json-server db.json -w -p 3000",
      "dev": "roadhog server"
    }

    然后,就可以执行:

    • npm run server
    • npm run dev

    来开始开发工作(两个指令要在两个终端中执行)。

    展开全文
  • 1、首先需要个简单的拼音和汉字对应的字典文件: /** * 收录常用汉字6763个,不支持声调,支持多音字,并按照汉字使用频率由低到高排序 */ var pinyin_dict_notone = {"a":"阿啊呵腌嗄吖锕","e":"额阿俄恶鹅遏...
  • 前面的文章中使用了最原始的写法(基于state的状态管理)来完成了个管理系统。接下来准备给我们的系统引入Redux
  • 今天看了几篇关于这三个主流框架的PK,如标题:react.js,angular.js,vue.js学习哪个好?相信每个人都有这种问题。 现在的前端框架层出不穷,作为前端开发者何去何从?fackbook的react.js盛世火热,react native打开...
  • 图书管理还记得搭建项目的时候在db.json文件里写的book吗
  • 2017年1月9日,对于大多数开发者来说必定是个激动人心的日子,因为微信推出了自己新的布局性产品——小程序。小程序实在太火了,相信每个互联网人,今日得微信群众都充斥着各式各样的小程序。时间,各种问题...
  • 比人本是名Android开发人员,奈何受到创业大潮之洗脑,义无反顾加入了创业大军。辗转反侧,摸爬滚动,偶然机会需要进行前端整站架构开发。浏览器兼容、PC\APP\微信三端兼容、SEO、服务端渲染加速等,从未想过的...
  • 这个能取大部分汉字的首字母, 但还不全, 有些UNICODE的汉字是取不到的, 要想取到所有汉字的首字母,最好是能做个UNICODE汉字和拼音的对照表,通过对照表查. GB2312标准共收录6763个汉字,其中级汉字3755个,二级...
  • 这些书籍 从简单到难都有,还有个人喜欢看的科技,玄幻,悬疑书籍都很不错。
  • 全栈工程师指南

    2017-04-19 11:12:30
    这是本不止于全栈工程师的学习手册,它也包含了如何成为个 Growth Hacker 的知识。 全栈工程师是未来 谨以此文献给每个为成为优秀全栈工程师奋斗的人。 技术在过去的几十年里进步很快,也将在未来的几...
  • 其实说回为什么要写这篇博客,其实很重要的个原因是因为我这学期在申请签证的时候遇到了挑战到了我作为个软件工程本科生认知的件事情。 首先点草这个网站和负责开发维护这个网站的外包公司-- CGI Group Inc....
1 2
收藏数 31
精华内容 12
热门标签