react实战系列——React中的表单和路由的原理

博客 动态
0 120
优雅殿下
优雅殿下 2022-08-24 23:03:33
悬赏:0 积分 收藏

react实战系列 —— React 中的表单和路由的原理

其他章节请看:

react实战 系列

React 中的表单和路由的原理

React 中的表单是否简单好用,受控组件和非受控是指什么?

React 中的路由原理是什么,如何更好的理解 React 应用的路由?

请看下文:

简单的表单

你有见过在生成环境中没有涉及任何表单的应用吗?大多 web 应用都会涉及表单。比如登录、注册、提交信息。

表单由于难用有时名声不好,于是许多框架针对表单做了一些神奇的事情来减轻程序员的负担。

React 并未采用神奇的方法,但它却能让表单更容易使用。

在做实验测试 react 中表单是否真的容易使用之前,我们在稍微聊一下表单。

不同框架处理表单的方式都不尽相同,很难说一种比另一种要好。有的需要我们了解很多框架的内部实现,有的很容易使用但是可能不够灵活。

开发者需要有一个思维模型(针对表单),该模型能让开发者创建可维护的代码,并在 bug 出现时及时修复他们。

当涉及表单时,React 不会提供太多“魔法”,并且在过多了解表单和过少了解之间找到了一个中间带。React 中表单的思维模型其实是你已经了解的东西,并没有特别的 api。表单就是我们看到的东西。开发者使用组件、状态、属性来创建表单。

我们在回顾下 React 部分思维模式:

  • React 有两种主要处理数据的方式:状态属性
  • 组件是 js 类,除了 react 提供的生命周期钩子、render(),组件还可以拥有自定义的类方法,可以用来相应事件,或者做任何其他事
  • 与常规的 dom 元素一样,可以在 React 组件上注册事件,例如 onClick、onChange等
  • 父组件可以将回调函数作为属性传给子组件,使组件之间通信。

下面我们通过实验测试 react 中表单是否真的简单。

表单小示例

创建一个子组件 CreateCommentComponet,用户能通过它来提交评论。

<script type="text/babel">    class CreateCommentComponet extends React.Component {        constructor(props) {            super(props);            this.state = { text: "" };            this.onInputChange = this.onInputChange.bind(this);        }        onInputChange(e) {            // e 是 React 合成事件,对用户来说就像原生的 event。            const text = e.target.value;            this.setState(() => ({ text: text })); // {1}        }        render() {            return <div className="CreateCommentComponet">                <p>您输入的评论是:{this.state.text}</p>                <textarea                    value={this.state.text}           /* {2} */                    placeholder="请输入评论"                    onChange={this.onInputChange}                />            </div>        }    }    ReactDOM.render(        <CreateCommentComponet />,        document.getElementById('root')    );</script>

页面内容如下:

<div id="root">    <div >        <p>您输入的评论是:</p>        <textarea placeholder="请输入评论"></textarea>    </div></div>

当我们在 textarea 中输入文字,例如 111,文字也会同步到 p 元素中。就像这样:

<div id="root">    <div >        <p>您输入的评论是:111</p>        <textarea placeholder="请输入评论">111</textarea>    </div></div>

为什么我输入不了字符?

比如现在我们将 this.setState(行{1})注释,然后给 textarea 输入字符,页面什么也没发生。

初学者这时就很困惑,为什么我输不了字符,什么鬼?

其实这是正常的,也正是 React 尽职的表现。

React 保持虚拟 dom真实 dom 的同步,现在用户给 textarea 输入字符,尝试更改 dom,但用户并没有更新虚拟 dom,所以 React 也不会对用户做任何改变。

假如此时 textarea 变了,那岂不是又回到老的做事方式,由我们自己管理真实 dom。而非现在面向 React 编程,即通过声明组件在不同状态下的行为和外观,React 根据虚拟 DOM 生成和管理真实 dom。

如果注释 value={this.state.text}(行{2}),此刻就由受控组件变成非受控组件,也就是说 textarea 的值不在受 React 控制。

通过事件和事件处理器更新状态来严格控制如何更新,按照这种设计的组件称为受控组件。因为我们严格控制了组件。非受控组件,组件保持自己的内部状态,不在使用 value 属性设置数据。

Tip:有关受控组件和非受控组件的介绍请看 这里

表单验证和清理

表单得加上前端校验,告诉用户提供的数据不能满足要求或无意义。

至于清理,笔者这里自定义了一个 Filter 类,用于清理冒犯性的内容,比如将 fuck 清理为 ****。

Tip:清理的功能,笔者最初想用 npm 包 bad-words,但它好像只支持 require 这种构建的环境。

<script type="text/babel">    /*    bad-words    自定义清理函数。        用法如下:        let filter = new Filter()        filter.clean('a b fuck c fuck') => a b **** c ****    */    class Filter {        constructor() {            this.cleanWord = ['fuck']            this.placeHolder = '*'        }        // 增加过滤单词        addCleanWord(...words){            this.cleanWord = [...this.cleanWord, ...words]        }        clean(msg) {            this.cleanWord.forEach(                item => msg = msg.replace(new RegExp(item, 'g'),                    new Array(item.length).fill(this.placeHolder).join('')))            return msg        }    }    class CreateCommentComponet extends React.Component {        constructor(props) {            super(props);            this.state = { text: "", valid: false };            this.handleSubmit = this.handleSubmit.bind(this)            this.onInputChange = this.onInputChange.bind(this);        }        handleSubmit = () => {            if (!this.state.valid) {                console.log('校验失败,不能提交')                return            }            console.log('提交')        }        // e 是 React 合成事件,对用户来说就像原生的 event。        onInputChange(e) {            // 清理输入。            const filter = new Filter()            const text = filter.clean(e.target.value);            this.setState(() => ({ text: text, valid: text.length <= 10 }));        }        render() {            return <div className="CreateCommentComponet">                <p>您输入的评论是:{this.state.text}</p>                <textarea                    value={this.state.text}           /* {2} */                    placeholder="请输入评论"                    onChange={this.onInputChange}                />                <p><button onClick={this.handleSubmit}>submit</button></p>            </div>        }    }    ReactDOM.render(        <CreateCommentComponet />,        document.getElementById('root')    );</script>

当用户输入 1 2 fuc fuck 时,则会显示 您输入的评论是:1 2 fuc ****

最终版本

最后加上父组件,子组件将提交的评论发送给父组件,并重置自己。再由父组件提交评论到后端。

<script type="text/babel">    class CommentComponet extends React.Component {        // 默认没有评论        state = { comments: [] }        handleCommontSubmit = (commont) => {            // 本地模拟提交            this.setState({ comments: [...this.state.comments, commont] })        }        render() {            return <div>                <p>已发表评论有:</p>                {                    this.state.comments.length === 0                        ? <p>暂无评论</p>                        : <ul>{this.state.comments.map((item, i) => <li key={i}>{item}</li>)}</ul>                }                <CreateCommentComponet handleCommontSubmit={this.handleCommontSubmit} />            </div>        }    }    class CreateCommentComponet extends React.Component {        constructor(props) {            super(props);            this.state = { text: "", valid: false };            this.handleSubmit = this.handleSubmit.bind(this)            this.onInputChange = this.onInputChange.bind(this);        }        handleSubmit = () => {            if (!this.state.valid) {                console.log('校验失败,不能提交')                return            }            this.props.handleCommontSubmit(this.state.text)            // 重置            this.setState({ text: '' })        }        // e 是 React 合成事件,对用户来说就像原生的 event。        onInputChange(e) {            const text = e.target.value;            this.setState(() => ({ text: text, valid: text.length <= 10 })); // {1}        }        render() {            return <div className="CreateCommentComponet">                <p>您输入的评论是:{this.state.text}</p>                <textarea                    value={this.state.text}           /* {2} */                    placeholder="请输入评论"                    onChange={this.onInputChange}                />                <p><button onClick={this.handleSubmit}>submit</button></p>            </div>        }    }    ReactDOM.render(        <CommentComponet />,        document.getElementById('root')    );</script>

页面结构如下:

<div id="root">    <div>        <p>已发表评论有:</p>        <p>暂无评论</p>        <div >            <p>您输入的评论是:</p><textarea placeholder="请输入评论"></textarea>            <p><button>submit</button></p>        </div>    </div></div>

当我们输入两条评论后,页面结构如下:

<div id="root">    <div>        <p>已发表评论有:</p>        <ul>            <li>评论1</li>            <li>评论2...</li>        </ul>        <div >            <p>您输入的评论是:</p><textarea placeholder="请输入评论"></textarea>            <p><button>submit</button></p>        </div>    </div></div>

Tip:按照现在的写法,如果有 10 个 input,则需要定义 10 个 onInputChange 事件,其实是可以优化成一个,请看 这里。

React 路由

根据前面两篇博文的学习,我们会创建 react 组件,也理解了 react 的数据流和生命周期。似乎还少点什么?

平时总说的 SPA(单页面应用)就是前后端分离的基础上,再加一层前端路由

Tip:在新的 Web 应用框架中,服务器最初会下发 html、css、js等资源,之后客户端应用“接管”工作,服务器只负责发送原始数据(通常是 json)。从这里开始,除非用户手动刷新页面,否则服务器只会下发 json 数据。

路由有许多含义和实现,对我们来说,它是一个资源导航系统。如果你使用浏览器,它会根据不同的 url(网址) 返回不同的页面(数据)。在服务端,路由着重将传入的请求路径匹配到源自数据库的资源。对于 React ,路由通常意味着将组件(人们想要的资源)匹配到 url(将用户想要的东西告诉系统的方式)

Tip:需要路由的原因有很多,例如:

  • 界面的不同部分需要。用户需要在浏览器历史中前进和后退
  • 网站的不同部分需要他们自己的 url,以便轻松的将人们路由到正确的地方
  • 按页面拆分代码有助于促进模块化,从而拆分应用

下面我们构建一个简单的路由,以便更好的理解 React 应用的路由。

比如之前学习 react 路由中有这么一段代码:

<Router>    <div>        <h2>About</h2>        <hr />        <ul>            <li>                <Link to="/about/article1">article1</Link>            </li>            <li>                <Link to="/about/article2">article2</Link>            </li>        </ul>        <Switch>            <Route path="/about/article1">                文章1...            </Route>            <Route path="/about/article2">                文章2...            </Route>        </Switch>    </div></Router>

这里有 Router、Route、Link,为什么这就是一个嵌套路由,里面发生了什么?

自定义路由效果展示

效果展示

Tip:为了方便,笔者就在开源项目 spug 中进行。用 react cli 创建的项目也都可以。

创建路由 Route.js

以下是 Route.js 的完整代码。功能很简单,就是作为 url 和组件映射的数据容器

import PropTypes from 'prop-types';import React from 'react';// package.json 没有,或许像 prop-types 自动已经引入了 import invariant from 'invariant';/** * Route 组件主要作为 url 和 组件映射的数据容器 * Route 不渲染任何东西,如果渲染,就报错。好奇怪! * 其实这只是一种 React 可以理解,开发者也能通过它将路由和组件关联在一起的方式而已。 *  * 用法:<Route path="/home" component={Home} />。路径 `/home` 指向 `Home` 组件 */class Route extends React.Component {    static propTypes = {        path: PropTypes.string,        // React 元素或函数        component: PropTypes.oneOfType([PropTypes.element, PropTypes.func])    };    // 一旦被调用,我们就知道事情不对了。    render() {        return invariant(false, "<Route> elements are for config only and shouldn't be rendered");    }}export default Route;

Tip:invariant 一种在开发中提供描述性错误但在生产中提供一般错误的方法。这里一旦调用了 render() 就会报错,我们就知道事情不对了。

var invariant = require('invariant'); invariant(someTruthyVal, 'This will not throw');// No errors invariant(someFalseyVal, 'This will throw an error with this message');// Error: Invariant Violation: This will throw an error with this message

第一个参数是假值就报错,真值不会报错。

创建路由器 Router.js

Router 用于管理路由。请看这段代码:

<Router location={this.state.location}>    <Route path="/" component={Home} />    <Route path="/test" component={Test} /></<Router>

当 Router 的 location 是 /,则渲染 Home 组件。如果是 /test 则渲染 Test 组件。

大概思路是:通过一个变量 routes 来存储路由信息,比如 / 对应一个 Home,/test 对应 Test,借助 enroute(微型路由器),根据不同的 url 渲染出对应的组件。

完整代码如下:

import PropTypes from 'prop-types';import React, { Component } from 'react';// 微型路由器,使用它将路径匹配到组件上import enroute from 'enroute';import invariant from 'invariant';export default class Router extends Component {    // 定义两个属性。必须有子元素 和 location。其中子元素至少有2个,否则就不是数组类型。    // 你换成其他规则也没问题    static propTypes = {        children: PropTypes.array.isRequired,        location: PropTypes.string.isRequired    };    constructor(props) {        super(props);                /**         * 用来存储路由信息         * 例如:{/test: render(), /profile: render(), ...}         */        this.routes = {};        // 添加路由        this.addRoutes(props.children);        // 注册路由器。当匹配对应 url,则会调用对应的方法,比如匹配 /test,则调用相应的 render() 方法。render() 方法会返回相应的 React 组件        this.router = enroute(this.routes);    }    // 向路由器中添加路由。需要两个东西:正确的 url 和 对应的组件    addRoute(element, parent) {        // Get the component, path, and children props from a given child        const { component, path, children } = element.props;        // 没有 component 就会报错        invariant(component, `Route ${path} is missing the "path" property`);        // path 必须是字符串        invariant(typeof path === 'string', `Route ${path} is not a string`);        // Set up Ccmponent to be rendered        // 返回组件。参考 enroute 的用法。        const render = (params, renderProps) => { // {1}                        // 如果匹配 <Route path="/test">,this 则是父组件 Router            const finalProps = Object.assign({ params }, this.props, renderProps);            // Or, using the object spread operator (currently a candidate proposal for future versions of JavaScript)            // const finalProps = {            //   ...this.props,            //   ...renderProps,            //   params,            // };            // finalProps 有父组件的 location、children 和 enroute 传来的 params            const children = React.createElement(component, finalProps);            // parent.render 父路由的 render(及行 {1} 定义的 render() 方法)            return parent ? parent.render(params, { children }) : children;        };        // 有父路由,则连接父路由        const route = this.normalizeRoute(path, parent);        // If there are children, add those routes, too        if (children) {            // 注册路由            this.addRoutes(children, { route, render });        }        // 将路由和 render 关联        this.routes[this.cleanPath(route)] = render;    }    addRoutes(routes, parent) {        // 每个 routes 中的元素将调用一次回调函数(即下面的第二个实参)        // 下面这个 this 是什么?是这个组件的实例,箭头函数是没有 this 的。        React.Children.forEach(routes, route => this.addRoute(route, parent));    }    // 将// 替换成 /    cleanPath(path) {        return path.replace(/\/\//g, '/');    }    // 确保父路由和子路由返回正确的 url。例如:`/a` 和 `b` => `/a/b`    normalizeRoute(path, parent) {        // 绝对路由,直接返回        if (path[0] === '/') {            return path;        }        // 没有父路由,直接返回        if (!parent) {            return path;        }        // 连接父路由        return `${parent.route}/${path}`;    }    // 这里需要有 location 属性    // 将 url 对应的组件渲染出来    render() {        const { location } = this.props;        invariant(location, '<Router/> needs a location to work');        return this.router(location);    }}

Router 组件说明:

  • render() - 将 url 对应的组件渲染出来
  • cleanPath()normalizeRoute() - 用于路径处理
  • addRoutes() - 依次注册子路由
  • addRoute() - 注册路由,最后存入变量 routes 中。例如 / 对应 / 的 render()/test 对应 /test 的 render()。对于嵌套路由,只会返回父路由对应的组件。
  • constructor() - 定义变量 routes 存储路由信息,通过 addRoutes 添加路由,最后利用 enroute 返回 this.router。于是 render() 就能将 url 对应的组件渲染出来。

TipReact.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。例如 forEach、map等

入口 App.js

最终测试的入口文件 App.js 代码如下:

import React, { Component } from 'react';import Route from './myrouter/Route'import Router from './myrouter/Router'// 这个库能更改浏览器中的 urlimport { history } from './myrouter/history'// 链接import Link from './myrouter/Link'// 类似 404 的组件import NotFound from 'myrouter/NotFound';// 以下都是路由切换的组件(或子页面)import Home from './myrouter/Home'import Test from './myrouter/Test'import Post from './myrouter/Post'import Profile from './myrouter/Profile'import EmailSetting from './myrouter/EmailSetting'class App extends Component {  componentDidMount() {    // 地址变化时触发    history.listen((location) => {      this.setState({ location: location.pathname })    });  }  // window.location.pathname,包含 URL 中路径部分的一个DOMString,开头有一个“/"。  // 例如 https://developer.mozilla.org/zh-CN/docs/Web/API/Location?a=3 的 pathname 是 /zh-CN/docs/Web/API/Location  state = { link: '', location: window.location.pathname }  handleChange = (e) => {    this.setState({ link: e.target.value })  }  handleClick = () => {    history.push(this.state.link)  }  render() {    return (      <div style={{margin: 20}}>        <div style={{ border: '1px solid red', marginBottom: '20px' }}>          <h3>导航1</h3>          <p>请输入要跳转的导航(例如 /、/test、/posts/:postId、/profile/email、不存在的url):<br />             <input value={this.setState.link} onChange={this.handleChange} />            <button onClick={this.handleClick}>导航跳转</button></p>        </div>        <div style={{ border: '1px solid red', marginBottom: '20px' }}>          <h3>导航2</h3>          <p>            <Link to="/">主页</Link> <Link to="/test">测试</Link>          </p>        </div>        <main style={{ border: '1px solid blue' }}>          <h3>不同的子页面:</h3>          {/* 有一个绑定到组件的路由组成的路由器 */}          <Router location={this.state.location}>            <Route path="/" component={Home} />            <Route path="/test" component={Test} />            <Route path="/posts/:postId" component={Post} />            <Route path="/profile" component={Profile}>              <Route path="email" component={EmailSetting} />            </Route>            {/* 都没有匹配到,就渲染 NotFound */}            <Route path="*" component={NotFound}/>          </Router>        </main>      </div>    );  }}export default App;

Router 的 location 初始值是 window.location.pathname,点击导航跳转时调用会通过 history 更改浏览器的 url,接着会触发 history.listen,于是通过 this.setState 来更改 Router 的 location,React 则会渲染 url 相应的组件。

Tip:其他组件都在与 App.js 同级目录 myrouter 中。

Link.js

一个简单的封装。点击 a 时,调用 history.push() 方法。

import PropTypes from 'prop-types';import React from 'react';import { navigate } from './history';function Link({ to, children }) {    return <a href={to} onClick={e => {        e.preventDefault()        navigate(to)    }}>{children}</a>}Link.propTypes = {    to: PropTypes.string,    children: PropTypes.node};export default Link;

history.js

对 history 库简单处理:

import { createBrowserHistory } from "history";const history = createBrowserHistory();const navigate = to => history.push(to);export {history, navigate}

NotFound.js

import React  from "react";import Link from './Link'export default function(){    return <div>        <p>404 !什么也没有。</p>        <Link to='/' children="主页"/>    </div>}

其他组件

Home.js

// spug 的函数组件都有 `import React from 'react';`,尽管没有用到 React,奇怪!import React from 'react';class Home extends React.Component {  render() {    return (      <div className="home">       主页      </div>    );  }}export default Home;

Post.js

import React from 'react';class Post extends React.Component {  render() {    return (      <div className="post-component">       <p>post</p>       <p>postId:{this.props.params.postId}</p>      </div>    );  }}export default Post;

Profile.js

import React from 'react';class Profile extends React.Component {  render() {    return (      <div className="Profile-component">       <p>个人简介</p>       {this.props.children}      </div>    );  }}export default Profile;

Tip: 嵌套路由笔者其实没有实现。比如 http://localhost:3000/profile 就会报错。

EmailSetting.js

import React from 'react';class EmailSetting extends React.Component {  render() {        return (      <div className="EmailSetting-component">       <p>个人简介 {'->'} 设置邮件</p>      </div>    );  }}export default EmailSetting;

Test.js

用于测试 invariant、enroute 等库。

import React from 'react';import invariant from 'invariant';import enroute from 'enroute';function edit(params, props){    // params {id: "3"}    console.log('params', params)    // props {additional: "props"}    console.log('props', props)}const router = enroute({    '/users/new': function(){},    '/users/:id': function(){},    '/users/:id/edit': edit,    '*': function(){}})router('/users/3/edit', {additional: 'props'})class Test extends React.Component {    render() {        this.addRoutes()        // import invariant from 'invariant';        // return invariant(false, '这个值是假值就会抛出错误')        return <p>测试页</p>    }    log(v){        console.log('v', v)    }    addRoutes() {        [...'abc'].forEach(item => {this.log(item)})        // [...'abc'].forEach(function(item){this.log(item)}, this)    }}export default Test;

其他章节请看:

react实战 系列

posted @ 2022-08-24 22:44 彭加李 阅读(6) 评论(0) 编辑 收藏 举报
回帖
    优雅殿下

    优雅殿下 (王者 段位)

    2018 积分 (2)粉丝 (47)源码

    小小码农,大大世界

     

    温馨提示

    亦奇源码

    最新会员