React 教學 - 從零開始的 React 生活 Part 2 中,我們學會利用 Webpack 編譯含有 JSX 的 React 專案,讓我們能夠利用 JSX 語法提高開發 React 的效率。

本篇會將 React 教學 - 從零開始的 React 生活 Part 1 中的範例改成 JSX 語法呈現,並且模組化部分組件(components) ,從而達到較高的可維護性。

最後再加上新的外掛 babel/plugin-proposal-class-properties ,讓我們能夠使用 Arrow function 更加簡化 React 程式碼。

本文環境

  • Chrome 81
  • React 16.13.1

Hello React 進化版

結合前 2 篇所學,我們就能夠將 src/app.jsx (詳見 React 教學 - 從零開始的 React 生活 Part 2 部分程式改用 JSX 語法:

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

class HelloWorld extends React.Component {
  constructor(props) {
      super(props);
      this.state = {
        count: 0,
      }
      this.addCount = this.addCount.bind(this)
  }

  addCount() {
      let count = this.state.count + 1
      this.setState({count: count})
  }

  render() {
    return (
        <h1 onClick={this.addCount}>Hello World, {this.props.name} {this.state.count}
        </h1>
    );
  }
}

const domContainer = document.querySelector('#app');
ReactDOM.render(
    <HelloWorld name='React' />,
    domContainer
);

可以看到第 20 行與第 29 行改成以 JSX 語法實作,程式整體看起來也簡潔許多。

p.s. onClick 是 React 規定的寫法,與 Javascript 的 onclick 不一樣,如果想知道 React 支援什麼 event 可以查閱官方文件 Supported Events

模組化

#
目前為止我們都將所有的程式放在 src/app.jsx 中,對於小型專案而言,並沒有太大影響,然而隨著專案越來越大,模組化就會變成一個至關重要的課題。

得益於 Webpack 與 Babel ,我們能夠輕鬆地使用 ES6 的模組功能 達到模組化 React 專案的目標。

目前有 2 派模組化 Javascript 的做法:

  • AMD(Asynchronous Module Definition) 連結
  • CommonJS 連結

ES6 的模組功能並非屬於上述任何一種,儘管它看起來十分像 CommonJS ,不過它其實是參考了前述 2 者作法而發展出來的。

本篇就不多對 ES6 的模組多做贅述,詳細可以參閱 JavaScript modules - JavaScript | MDN 學習模組的基礎。

接著,試著新增一個模組化的組件,用來為文字加上底線的效果,我們新增 1 個檔案 textDeco.jsx :

import React from 'react';

class BottomLine extends React.Component {
  render() {
    return (
      <div style={{borderStyle: "solid", borderWidth: "0 0 1px 0"}}>
        {this.props.children}
      </div>
    )
  }
}

export default BottomLine;

上述範例的重點在最後 export default BottomLine 的部分,代表我們允許其他人使用 BottomLine 這個組件。

另外值得注意的是 React inline CSS 的寫法,規定我們必須傳 map 型態的物件給 style 才行,這也是為何我們用 style=\{\{borderStyle: "solid", borderWidth: "0 0 1px 0"\}\} 設定 CSS。

p.s. React 的 inline CSS 屬性不需要有 - ,所以 border-style 可以改成 borderStyle, border-width 則改成 borderWidth

如果我們將 style 改成 style="border-style: solid; border-width: 0 0 1px 0;" 就會在瀏覽器的開發者工具出現以下錯誤,這時候只要將 style 改為前述寫法即可:

Uncaught Error: The `style` prop expects a mapping from style properties to values, not a string. For example, style={{marginRight: spacing + 'em'}} when using JSX.

再來我們在 app.jsx 試著使用該組件:

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

import BottomLine from './textDeco';

class HelloWorld extends React.Component {
  constructor(props) {
      super(props);
      this.state = {
        count: 0,
      }
      this.addCount = this.addCount.bind(this)
  }

  addCount() {
      let count = this.state.count + 1
      this.setState({count: count})
  }

  render() {
    return (
      <BottomLine>
        <h1 onClick={this.addCount}>Hello World, {this.props.name} {this.state.count}
        </h1>
      </BottomLine>
    );
  }
}

const domContainer = document.querySelector('#app');
ReactDOM.render(
  <HelloWorld name='React' />,
  domContainer
);

上述範例第 4 行代表從 textDeco.jsx 匯入 BottomLine 組件,並且在第 22 及第 26 行使用它。

成功的話將會看到 Hello World, 0 多個底線。

以上就是用 Webpack 加上 ES6 進行模組化的過程。

Arrow function (箭頭函式)

本篇最後談談 @babel/plugin-proposal-class-properties · Babel 這個外掛。

目前為止,我們都會在組件的 constructor 方法內為綁定(bind)組件內的方法,例如前述範例 this.addCount = this.addCount.bind(this) 的部分。

但是很多時候我們都會忘記要綁定,導致類似以下的錯誤出現:

app.jsx:54 Uncaught TypeError: Cannot read property 'state' of undefined

這種錯誤總是讓人覺得煩躁,這時候只要安裝 @babel/plugin-proposal-class-properties · Babel 就能夠使用 Arrow function,利用 Arrow function 的特性,從此再也不需要每次都為 React 組件的方法進行綁定。

安裝方法:

$ npm install --save-dev @babel/plugin-proposal-class-properties

安裝完成之後,必須為 Webpack 的設定檔 webpack.config.js 加上新的設定:

const path = require('path');
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: {
    app: "./src/app.jsx",
  },
  output: {
    path: path.resolve(__dirname, "dist/"),
    filename: "[name].js",
    publicPath: "/"
  },
  resolve: {
    extensions: [".jsx", ".js"]
  },
  module: {
    rules: [
      {
        test: /.jsx$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              "@babel/preset-react",
              "@babel/preset-env"
            ],
            plugins: ['@babel/plugin-proposal-class-properties'],
          }
        }
      },
      {
        test: /.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: { presets: ["@babel/preset-env"] }
        }
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'My React App',
      filename: 'index.html',
      template: "./src/index.html",
    })
  ],
  devServer: {
    contentBase: path.join(__dirname, "dist"),
    compress: false,
    port: 9000
  },
}

新增的設定為第 28 行 plugins: ['@babel/plugin-proposal-class-properties'],

最後讓我們將 app.jsx 的組件方法改為 Arrow function 並且拿掉綁定的部分吧:

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

import BottomLine from './textDeco';

class HelloWorld extends React.Component {
  constructor(props) {
      super(props);
      this.state = {
        count: 0,
      }
  }

  addCount = () => {
      let count = this.state.count + 1
      this.setState({count: count})
  }

  render() {
    return (
      <BottomLine>
        <h1 onClick={this.addCount}>Hello World, {this.props.name} {this.state.count}
        </h1>
      </BottomLine>
    );
  }
}

const domContainer = document.querySelector('#app');
ReactDOM.render(
  <HelloWorld name='React' />,
  domContainer
);

上述範例中:

  addCount = () => {
      let count = this.state.count + 1
      this.setState({count: count})
  }

就是一個不需任何參數的 Arrow function ,另外也可以看到 constructor 不需要綁定也能正常運作。

偉哉 Webpack & ES6 。

以上, Happy Coding!

References

https://reactjs.org/