從零開始學習 React 高階元件

xpromise發表於2019-03-24

01、介紹

  • React 高階元件也叫做 React HOC(High Order Component), 它是react中的高階技術, 用來重用元件邏輯。
  • 但高階元件本身並不是React API。它只是一種模式,這種模式是由react自身的組合性質必然產生的。
  • 那麼在學習高階元件之前有一個概念我們必須清楚,就是高階函式。

02、高階函式

  • 概念:高階函式是一個函式,它接收函式作為引數或將函式作為輸出返回
  • 舉個栗子:
    • 接收函式作為引數
      function a(x) {
        x();
      }
      function b() {
        alert('hello');
      }
      
      a(b);
      複製程式碼
    • 將函式作為輸出返回
      function a() {
        function b() {
          alert('hello');
        }
        return b;
      }
      
      a()();
      複製程式碼
  • 以上函式a就是一個高階函式, 用法非常簡單, 那麼實際開發中又有哪些是高階函式呢?
    • Array 的 map 、reduce 、filter 等方法
    • Object 的 keys 、values 等方法

03、高階元件

  • 概念:高階元件就是一個函式,且該函式接受一個元件作為引數,並返回一個新的元件
  • 舉個栗子:
    // WrappedComponent 就是傳入的包裝元件
    function withHoc(WrappedComponent) {
      return class extends Component {
        render () {
          return (
            <WrappedComponent />
          )
        }
      }
    }
    複製程式碼
  • withHoc 函式就是一個高階元件。那麼高階元件到底有什麼神奇的魔力,值得我們為之著迷?
  • 開發元件時,我們會遇到相同的功能,使用高階元件則能減少重複程式碼

04、高階元件實訓1

  • 目的: 定義高階元件
  • 元件 Login -- 登陸頁面
    // 受控元件
    class Login extends Component {
      state = {
        username: '',
        password: ''
      }
      
      onUsernameChange = (e) => {
        this.setState({username: e.target.value});
      }
      
      onPasswordChange = (e) => {
        this.setState({password: e.target.value});
      }
      
      login = (e) => {
        // 禁止預設事件
        e.preventDefault();
        // 收集表單資料
        const { username, password } = this.state;
        alert(`使用者名稱: ${username}, 密碼: ${password}`);
      }
      
      render () {
        const { username, password } = this.state;
        return (
          <div>
            <h2>登陸</h2>
            <form onSubmit={this.login}>
              使用者名稱: <input type="text" name="username" value={username} onChange={this.onUsernameChange}/> <br/>
              密碼: <input type="password" name="password" value={password} onChange={this.onPasswordChange}/> <br/>
              <input type="submit" value="登陸"/>
            </form>
          </div>
        )
      }
    }
    複製程式碼
  • 元件 Register -- 註冊頁面
    // 受控元件
    class Register extends Component {
      state = {
        username: '',
        password: '',
        rePassword: ''
      }
      
      onUsernameChange = (e) => {
        this.setState({username: e.target.value});
      }
      
      onPasswordChange = (e) => {
        this.setState({password: e.target.value});
      }
      
      onRePasswordChange = (e) => {
        this.setState({rePassword: e.target.value});
      }
      
      register = (e) => {
        // 禁止預設事件
        e.preventDefault();
        // 收集表單資料
        const { username, password, rePassword } = this.state;
        alert(`使用者名稱: ${username}, 密碼: ${password}, 確認密碼: ${rePassword}`);
      }
      
      render () {
        const { username, password, rePassword } = this.state;
        return (
          <div>
            <h2>註冊</h2>
            <form onSubmit={this.register}>
              使用者名稱: <input type="text" name="username" value={username} onChange={this.onUsernameChange}/> <br/>
              密碼: <input type="password" name="password" value={password} onChange={this.onPasswordChange}/> <br/>
              確認密碼: <input type="password" name="rePassword" value={rePassword} onChange={this.onRePasswordChange}/> <br/>
              <input type="submit" value="註冊"/>
            </form>
          </div>
        )
      }
    }
    複製程式碼
  • 頁面效果
    從零開始學習 React 高階元件
  • 我們發現裡面重複邏輯實在太多了,尤其是 onXxxChange 函式出現太多,我們先優化一下。
    // 我們以 Register 元件為例來看
    class Register extends Component {
      state = {
        username: '',
        password: '',
        rePassword: ''
      }
      // 最終修改狀態資料的函式
      onChange = (stateName, stateValue) => {
        this.setState({[stateName]: stateValue});
      }
      // 高階函式 --> 這樣後面就能一直複用當前函式,而不用重新建立了~
      composeChange = (name) => {
        return (e) => this.onChange(name, e.target.value);
      }
      // 統一所有提交表單函式名
      handleSubmit = (e) => {
        e.preventDefault();
        const { username, password, rePassword } = this.state;
        alert(`使用者名稱: ${username}, 密碼: ${password}, 確認密碼: ${rePassword}`);
      }
      
      render () {
        const { username, password, rePassword } = this.state;
        return (
          <div>
            <h2>註冊</h2>
            <form onSubmit={this.handleSubmit}>
              使用者名稱: <input type="text" name="username" value={username} onChange={this.composeChange('username')}/> <br/>
              密碼: <input type="password" name="password" value={password} onChange={this.composeChange('password')}/> <br/>
              確認密碼: <input type="password" name="rePassword" value={rePassword} onChange={this.composeChange('rePassword')}/> <br/>
              <input type="submit" value="註冊"/>
            </form>
          </div>
        )
      }
    }
    複製程式碼
  • 現在兩個頁面都有 onChange 、 composeChange 、handleSubmit 函式和相關的狀態,我們接下來提取,封裝成高階元件
    // 高階元件 withHoc
    export default function withHoc(WrappedComponent) {
      return class extends Component {
        state = {
          username: '',
          password: '',
          rePassword: ''
        }
      
        onChange = (stateName, stateValue) => {
          this.setState({[stateName]: stateValue});
        }
      
        composeChange = (name) => {
          return (e) => this.onChange(name, e.target.value);
        }
        
        handleSubmit = (e) => {
          e.preventDefault();
          const { username, password, rePassword } = this.state;
          if (rePassword) {
            alert(`使用者名稱: ${username}, 密碼: ${password}, 確認密碼: ${rePassword}`);
          } else {
            alert(`使用者名稱: ${username}, 密碼: ${password}`);
          }
        }
        
        render () {
          // 抽取方法
          const mapMethodToProps = {
            composeChange: this.composeChange,
            handleSubmit: this.handleSubmit,
          }
          // 將狀態資料和操作的方法以 props 的方式傳入的包裝元件中
          return (
            <div>
              {/*提取公共頭部*/}
              <h2>xxx</h2>
              <WrappedComponent {...this.state} {...mapMethodToProps}/>
            </div>
          )
        }
      }
    }
    
    // 元件 Register
    class Register extends Component {
      render () {
        const { handleSubmit, composeChange, username, password, rePassword } = this.props;
        return (
          <form onSubmit={handleSubmit}>
            使用者名稱: <input type="text" name="username" value={username} onChange={composeChange('username')}/> <br/>
            密碼: <input type="password" name="password" value={password} onChange={composeChange('password')}/> <br/>
            確認密碼: <input type="password" name="rePassword" value={rePassword} onChange={composeChange('rePassword')}/> <br/>
            <input type="submit" value="註冊"/>
          </form>
        )
      }
    }
    // 向外暴露的是高階元件的返回值~包裝了 Register 元件返回了一個新元件
    export default withHoc(Register);
    複製程式碼
  • 現在我們提取了公共方法、狀態等資料, 封裝了一個基本的高階元件。 但是還有很多需要問題需要解決,現在開始行動~

05、高階元件實訓2

  • 目的: 向高階元件中傳參
  • 修改高階元件
    // 再次包裹了一層高階函式, 這個高階函式執行後返回值才是高階元件
    // 通過這種方式, 高階元件內部就能獲取引數了~
    export default (title) => (WrappedComponent) => {
      return class Form extends Component {
        ...重複程式碼省略...
        
        render () {
          const mapMethodToProps = {
            composeChange: this.composeChange,
            handleSubmit: this.handleSubmit,
          }
          return (
            <div>
              {/*獲取到引數值就能正常顯示了~*/}
              <h2>{title}</h2>
              <WrappedComponent {...this.state} {...mapMethodToProps}/>
            </div>
          )
        }
      }
    }
    複製程式碼
  • 在 Login / Register 元件中使用
    • export default withHoc('登陸')(Login);
    • export default withHoc('註冊')(Register);

06、高階元件實訓3

  • 目的: 獲取父元件傳遞的 props
  • 修改 App 元件
    class App extends Component {
      render() {
        return (
          <div>
            {/*父元件向子元件傳遞屬性*/}
            <Login name="jack" age={18}/>
            <Register />
          </div>
        );
      }
    }
    複製程式碼
  • 修改高階元件
    export default (title) => (WrappedComponent) => {
      return class Form extends Component {
        ...重複程式碼省略...
        
        render () {
          const mapMethodToProps = {
            composeChange: this.composeChange,
            handleSubmit: this.handleSubmit,
          }
          return (
            <div>
              {/*獲取到引數值就能正常顯示了~*/}
              <h2>{title}</h2>
              {/* 將當前元件接受到的props傳給包裝元件~*/}
              <WrappedComponent {...this.props} {...this.state} {...mapMethodToProps}/>
            </div>
          )
        }
      }
    }
    複製程式碼
  • Login 元件中使用
     class Login extends Component {
       render () {
         const { handleSubmit, composeChange, username, password, name, age } = this.props;
         return (
           <div>
             <p>你的名字: {name}</p>
             <p>你的年齡: {age}</p>
             <form onSubmit={handleSubmit}>
               使用者名稱: <input type="text" name="username" value={username} onChange={composeChange('username')}/> <br/>
               密碼: <input type="password" name="password" value={password} onChange={composeChange('password')}/> <br/>
               <input type="submit" value="登陸"/>
             </form>
           </div>
         )
       }
     }
    複製程式碼

07、高階元件實訓4

  • 目的: 修改在 React-devtool 中高階元件名稱,方便除錯
  • 修改高階元件
    export default (title) => (WrappedComponent) => {
      return class Form extends Component {
        // 定義靜態方法,修改元件在除錯工具中顯示的名稱
        static displayName = `Form(${getDisplayName(WrappedComponent)})`
        
        ...省略重複程式碼...
      }
    }
    // 獲取包裝元件的displayName的方法
    function getDisplayName(WrappedComponent) {
      return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }
    複製程式碼
  • 修改之前名稱
    從零開始學習 React 高階元件
  • 修改之後名稱
    從零開始學習 React 高階元件

08、使用裝飾器

  • 目的: 簡化使用高階元件
  • 下載包
    • npm i react-app-rewired customize-cra @babel/plugin-proposal-decorators -D
  • 在專案根目錄配置 config-overrides.js
    const { override, addDecoratorsLegacy } = require('customize-cra');
    // 修改 create-react-app 的 webpack 的配置
    module.exports = override(
      addDecoratorsLegacy()
    )
    複製程式碼
  • 修改 package.json 的 scripts
    // 將 react-scripts 修改為 react-app-rewired
    "scripts": {
      "start": "react-app-rewired start",
      "build": "react-app-rewired build",
      "test": "react-app-rewired test"
    },
    複製程式碼
  • 以上就是使用 decorator 的配置,修改完後就能使用了~
  • 修改 Login 元件
    @withHoc('登陸')
    class Login extends Component {
      ...省略重複程式碼...
    }
    export default Login;
    複製程式碼
  • 修改 Register 元件
    @withHoc('註冊')
    class Register extends Component {
      ...省略重複程式碼...
    }
    export default Register;
    複製程式碼
  • react-app-rewired customize-cra 是 create-react-app 2.0以上專門用來修改 webpack 的配置
  • decorator 還能做很多事,感興趣朋友可以看看 阮一峰ES6教程 瞭解更多

重複程式碼永遠是我們需要考慮處理的程式碼,所以我們有模組化、元件化、工具類函式等等, 在 React 中再次引入了一個高階元件的概念,都是為了去除掉萬惡的重複程式碼,讓我們程式碼變得更加精簡。 本篇文章所有原始碼都放在了 git倉庫,如果它對你有幫助的話,歡迎點 star ~~

相關文章