七天接手react專案-起步

彭加李發表於2022-03-13

七天接手react專案-起步

背景

假如七天後必須接手一個 react 專案(spug - 一個開源運維平臺),而筆者只會 vue,之前沒有接觸過 react,此刻能做的就是立刻展開一個“7天 react 掃盲活動”。

react 活動掃盲方針

  1. 以讀懂 spug 專案為目標
  2. 無需對每個知識點深究
  3. 功能優先能實現,程式碼質量無需太苛刻

專案準備

將 spug 克隆到本地:

exercise> git clone https://github.com/openspug/spug spug-dev-demo
Cloning into 'spug-dev-demo'...
fatal: unable to access 'https://github.com/openspug/spug/': OpenSSL SSL_read: Connection was reset, errno 10054

克隆失敗,HTTPS 模式換成 SSH 再次下載:

exercise> git clone git@github.com:openspug/spug.git spug-dev-demo
Cloning into 'spug-dev-demo'...
remote: Enumerating objects: 11675, done.
remote: Counting objects: 100% (4184/4184), done.
remote: Compressing objects: 100% (1161/1161), done.
remote: Total 11675 (delta 3157), reused 3939 (delta 2991), pack-reused 7491
Receiving objects: 100% (11675/11675), 5.09 MiB | 2.32 MiB/s, done.
Resolving deltas: 100% (8460/8460), done.

目錄結構如下:

exercise\spug-dev-demo> dir

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----         2022/3/12      9:52                .github
d-----         2022/3/12      9:52                docs
d-----         2022/3/12      9:52                spug_api
d-----         2022/3/12      9:52                spug_web
-a----         2022/3/12      9:52              9 .gitignore
-a----         2022/3/12      9:52          35184 LICENSE
-a----         2022/3/12      9:52           3732 README.md

我們前端主要關注 spug_web 這個專案。首先安裝依賴包:

spug_web> cnpm i
/ [7/23] Installing @babel/plugin-transform-function-name@^7.8.3platform unsupported react-scripts@3.4.3 › babel-jest@24.9.0 › @jest/transform@24.9.0 › jest-haste-map@24.9.0 › fsevents@^1.2.7 Package require os(darwin) not compatible with your platform(win32)
- [7/23] Installing @babel/plugin-transform-classes@^7.16.7[fsevents@^1.2.7] optional install error: Package require os(darwin) not compatible with your platform(win32)
....

本地啟動專案:

spug_web> npm run start

> spug_web@3.0.0 start   
> react-app-rewired start

spug1.png

:由於沒有後端 api 的支援,所以不能登入進去。但至少可以從程式碼上分析這個前端專案。

hello-world

直接使用 script 的方式引入 react:

// 新建 hello-world.html
<body>
    <div id="root">
        <!-- 此元素的內容將替換為您的元件 -->
    </div>

    <!-- react 庫  -->
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <!-- 用於處理 Dom 的 react 包 -->
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <!-- Babel 能夠轉換 JSX 語法 -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

    <script type="text/babel">
        ReactDOM.render(
            // 注:無需新增字串
            <h1>Hello, world!</h1>,
            document.getElementById('root')
        );
    </script>
</body>

訪問頁面,瀏覽器顯示“Hello, world!”

這裡我們引入了三個庫,分別是 react 核心庫、處理 dom 的 react以及用於轉換 jsx。

Tip

  • 筆者在 vscode 中安裝 “open in browser” 外掛,直接右鍵選擇 “Open with Live Server” 即可。
  • 此示例來自 react 官網 hello-world
  • React 和 ReactDOM 的 cdn 來自 react 官網-CDN 連結
  • unpkg 是一個快速的全球內容交付網路,適用於 npm 上的所有內容。 使用它可以快速輕鬆地從任何包中載入任何檔案
  • react 和 vue 都是 javascript 庫,都能用於構建使用者介面
    • React 用於構建使用者介面的 JavaScript 庫 —— 官網
    • 漸進式 JavaScript 框架 —— Vue 官網

babel

Babel 是一個 JavaScript 編譯器 —— 官網

babel 之前叫 6to5。意把 es6 轉為 es5,後來目標變成支援 ECMAScript 所有語法,後來還支援將 JSX 轉成 js。2015年2月,改名為 Bable。

Tip:6to5 is now Babel —— not-born-to-die

使用 Babel 最容易上手的是直接在 html 頁面中通過 cdn 引入它。就像這樣:

<body>
    <div id="output"></div>
    <!-- Load Babel -->
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <!-- Your custom script here -->
    <script type="text/babel">
        const getMessage = () => "Hello World";
        document.getElementById("output").innerHTML = getMessage();
    </script>
</body>

瀏覽器頁面顯示:“Hello World”。

當在瀏覽器中載入時,@babel/standalone 將自動編譯並執行所有型別為 text/babel 或 text/jsx 的指令碼標籤。

Tip@babel/standalone 提供了一個獨立的 Babel 構建,用於瀏覽器和其他非 Node.js 環境

為什麼使用 JSX

我們建議在 React 中配合使用 JSX,JSX 可以很好地描述 UI 應該呈現出它應有互動的本質形式 —— react 官網-JSX 簡介

JSX 僅僅只是 React.createElement(component, props, ...children) 函式的語法糖 —— react 官網-深入 JSX

語法糖,通常用起來更方便,功能或許也更強大。比如類(Class)只是我們自定義類的一個語法糖。jsx 是 React.createElement 的語法糖,意義應該也差不多。

在 hello-world 中我們使用的是語法糖(jsx)。就像這樣:

// jsx
const reactElem = <h1>Hello, world!</h1>
ReactDOM.render(
    reactElem,
    document.getElementById('root')
);

若不使用語法糖(使用 React.createElement)。就像這樣:

const reactElem = React.createElement(
    'h1',
    {/* className: 'greeting' */ },
    'Hello, world!'
);
ReactDOM.render(reactElem, ...)

對比發現,jsx 更簡潔。

React 元素

react 元素是建立起來開銷極小的普通物件,用於描述你在螢幕上想看到的內容。與瀏覽器的 dom 元素不同。

react 元素是構成 React 應用的最小磚塊 —— react 官網-元素渲染

:不要混淆元素與元件,react 元件是由 react 元素構成的。

React.createElement 建立並返回指定型別的新的 react 元素。下面我們將 react 元素列印出來:

const reactElem = React.createElement(
    'h1',
    {},
    'Hello, world!'
)

console.log('reactElem: ', reactElem)

react 元素:

reactElem: {$$typeof: Symbol(react.element), type: 'h1', key: null, ref: null, props: {…}, …}
            $$typeof: Symbol(react.element)
            key: null
            props: {children: 'Hello, world!'}
            ref: null
            type: "h1"
            _owner: null
            _store: {validated: false}
            _self: null
            _source: null
            [[Prototype]]: Object

Tip:使用 jsx,輸出也是一樣的:

    const reactElem = <h1>Hello, world!</h1>
    console.log('reactElem: ', reactElem)

react 元素只有極少的幾個屬性。而真實 dom 元素上的屬性卻要多得多。你可以通過瀏覽器執行以下程式碼,將滑鼠移到 rootreactElem 對比檢視屬性數量。

const reactElem = <h1>Hello, world!</h1>
let root = document.getElementById('root')
// 打個斷點
debugger

react 元素是不可變物件。一旦被建立,你就無法更改它的子元素或者屬性。

React 只更新它需要更新的部分

React DOM 會將元素和它的子元素與它們之前的狀態進行比較,並只會進行必要的更新來使 DOM 達到預期的狀態。

JSX 語法規則

JSX,是一個 JavaScript 的語法擴充套件。JSX 可能會使人聯想到模板語言,但它具有 JavaScript 的全部功能。

不要寫引號

JSX 標籤語法既不是字串也不是 HTML。

// 正確。頁面顯示“Hello, world!”
const reactElem = <h1>Hello, world!</h1>
// 錯誤。頁面顯示“<h1>Hello, world!</h1>”
const reactElem = '<h1>Hello, world!</h1>'

引入 js 使用 {}

let _class = 'greeting'
let cnt = 'Hello, world!'
const reactElem = (
    <h1 className={_class}>
        {cnt.toLocaleUpperCase()}
    </h1>
)

瀏覽器顯示:”HELLO, WORLD!“。元素內容:

<h1 class="greeting">HELLO, WORLD!</h1>

樣式類名請使用 className

倘若是用 class。就像這樣:

const reactElem = (
    <h1 class={_class}>
        {cnt.toLocaleUpperCase()}
    </h1>
)

元素內容正常:

<h1 class="greeting">HELLO, WORLD!</h1>

但瀏覽器控制檯報錯:Warning: Invalid DOM property 'class'. Did you mean 'className'?

內聯樣式請使用 style={{ color: 'pink' }}

倘若使用 style="color:pink"

const reactElem = (
    <h1 className={_class} style="color:pink">
        {cnt.toLocaleUpperCase()}
    </h1>
    
)

控制檯報錯:

react-dom.development.js:2716 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.

react-dom.development.js:2716 未捕獲的錯誤:“樣式”道具需要從樣式屬性到值的對映,而不是字串。 例如,使用 JSX 時 style={{marginRight: spacing + 'em'}}。

改成 {{ color: 'pink' }} 則正常:

const reactElem = (
    <h1 className={_class} style={{ color: 'pink' }}>
        {cnt.toLocaleUpperCase()}
    </h1>
)

只能有一個根標籤

const reactElem = (
    <h1 className={_class} style={{ color: 'pink' }}>
        {cnt.toLocaleUpperCase()}
    </h1>
    <p>apple</p>
)

vscode 紅波浪線提示:JSX expressions must have one parent element(JSX 表示式必須有一個父元素)

包裹一個 div 即可。就像這樣:

const reactElem = (
    <div>
        <h1 className={_class} style={{ color: 'pink' }}>
            {cnt.toLocaleUpperCase()}
        </h1>
        <p>apple</p>
    </div>
)

不要忘記閉合標籤

<div>
    <h1 className={_class} style={{ color: 'pink' }}>
        {cnt.toLocaleUpperCase()}
    </h1>
    /* input 標籤未閉合 */
    <input type="text">
</div>

vscode 紅波浪線提示:JSX element 'input' has no corresponding closing tag(JSX 元素“輸入”沒有相應的結束標記)。

瀏覽器執行控制檯報錯:Uncaught SyntaxError: /Inline Babel script: Unterminated JSX contents(未終止的 JSX 內容)。

以下兩種閉合方式都可以:

<input type="text" />

<input type="text"></input>

自定義元件使用大寫字母開頭

const reactElem = (
    <div>
        <mybutton>18</mybutton>
    </div>
)

頁面顯示:“18”。瀏覽器報錯:

react-dom.development.js:61 Warning: The tag <mybutton> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.

react-dom.development.js:61 警告:標籤 <mybutton> 在此瀏覽器中無法識別。 如果您打算渲染一個 React 元件,請以大寫字母開頭。

以小寫字母開頭的元素代表一個 HTML 內建元件,比如 <div> 或者 <span> 會生成相應的字串 'div' 或者 'span' 傳遞給 React.createElement(作為引數)。大寫字母開頭的元素則對應著在 JavaScript 引入或自定義的元件,如 <Foo /> 會編譯為 React.createElement(Foo)

JSX 括號

官網示例中的 jsx 用括號包圍起來。就像這樣:

// 有括號
const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

感覺就是在寫 html,而且還有縮排。

筆者嘗試將括號去除:

const reactElem =
    <h1 className="greeting">
        Hello, world!
    </h1>

ReactDOM.render(
    reactElem,
    document.getElementById('root')
)

瀏覽器還是正常顯示:“Hello, world!”。

我們建議將內容包裹在括號中,雖然這樣做不是強制要求的,但是這可以避免遇到自動插入分號陷阱 —— 官網-JSX 簡介

JSX 小練習

JavaScript 表示式可以被包裹在 {} 中作為子元素。例如,以下表示式是等價的:

<MyComponent>foo</MyComponent>

<MyComponent>{'foo'}</MyComponent>

例如將下面 ul 列表改為動態:

<ul>
    <li>finish doc</li>
    <li>submit pr</li>
    <li>nag dan to review</li>
</ul>
let todos = ['finish doc', 'submit pr', 'nag dan to review'];
const reactElem = (
    <ul>
        {
            todos.map(item => {
                return <li>{item}</li>
            })
        }
    </ul>
)

瀏覽器控制檯告警:

Warning: Each child in a list should have a unique "key" prop.

警告:列表中的每個孩子都應該有一個唯一的“key”屬性。

新增 key 即可:

todos.map((item, index) => {
    return <li key={index}>{item}</li>
})

Tip:相同功能,官網(JavaScript 表示式作為子元素)是這樣實現的:

function Item(props) {
    return <li>{props.message}</li>;
}

function TodoList() {
    const todos = ['finish doc', 'submit pr', 'nag dan to review'];
    return (
        <ul>
            {todos.map((message) => <Item key={message} message={message} />)}
        </ul>
    );
}

const reactElem = TodoList()

元件

在 vue 中我們會這樣使用元件:

// 註冊元件
Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })

new Vue({ el: '#app' })
<div id="app">
  <component-a></component-a>
  <component-b></component-b>
  <component-c></component-c>
</div>

Tip:僅作示意,通常我們會使用 spa 單頁面應用開發。

於是可以將應用介面抽象成一棵元件樹:

元件樹

對比 vue 專案和react 專案的入口檔案,其實都是將 App 元件掛載到 dom 元素上:

// vue專案/main.js
import App from './App.vue'
...
new Vue({
  router,
  store,
  // 將 App 掛載到 #app
  render: h => h(App)
}).$mount('#app')
// spug_web/src/index.js
import App from './App';
...

// 將 App 掛載到 #root
ReactDOM.render(
  <Router history={history}>
    <ConfigProvider locale={zhCN} getPopupContainer={() => document.fullscreenElement || document.body}>
      <App/>
    </ConfigProvider>
  </Router>,
  document.getElementById('root')
);

函式元件與 class 元件

定義元件最簡單的方式是使用函式。請看示例:

<script type="text/babel">
    // 函式元件
    function MyComponent(props) {
        return <h1>Hello, {props.name}</h1>;
    }

    ReactDOM.render(
        <div>
            <MyComponent name="peng" />
        </div>,
        document.getElementById('root')
    );
</script>

頁面顯示“Hello, peng”。元件對應的 html 為:<h1>Hello, peng</h1>

我們還可以使用 es6 的 class 來定義元件。就像這樣:

// class 元件。
class MyComponent extends React.Component {
    // 必須定義 render()。否則會報錯:
    // MyComponent(...): No `render` method found on the returned component instance: you may have forgotten to define `render`.
    render() {
        return <h1>Hello, {this.props.name}</h1>
    }
}

// 省略。用法相同

Tip

  • React.Component 的子類中,必須定義 render() 函式
  • 我們強烈建議你不要建立自己的元件基類。 在 React 元件中,程式碼重用的主要方式是組合而不是繼承。 —— 官網
  • class 元件目前提供了更多的功能

元件中的 this

首先看函式元件中的 this:

// 函式元件
function MyComponent(props) {
  + console.log('this', this)
    return <h1>Hello, {props.name}</h1>;
}

瀏覽器制臺輸出:this undefined。說明沒有 this。

上文我們已經在 class 元件的 render() 中使用了 this,我們將其列印出來看一下:

// class 元件
class MyComponent extends React.Component {
    render() {
      + console.log('this', this)
        return <h1>Hello, {this.props.name}</h1>
    }
}
this MyComponent {props: {…}, context: {…}, refs: {…}, updater: {…}, _reactInternals: FiberNode, …}
    context: {}
    props: {name: 'peng'}
    refs: {}
    state: null
    updater: {isMounted: ƒ, enqueueSetState: ƒ, enqueueReplaceState: ƒ, enqueueForceUpdate: ƒ}
    _reactInternalInstance: {_processChildContext: ƒ}
    _reactInternals: FiberNode {tag: 1, key: null, stateNode: MyComponent, elementType: ƒ, type: ƒ, …}
    isMounted: (…)
    replaceState: (…)
    [[Prototype]]: Component
為什麼函式元件中的 this 是 undifined

將函式元件 MyComponent 放入 bable 的試一試中,會被翻譯成:

"use strict";

function MyComponent(props) {
  console.log('this', this);
  return /*#__PURE__*/React.createElement("h1", null, "Hello, ", props.name);
}

:jsx 被 babel 識別了處理啊,因為翻譯成了 React.createElement

嚴格模式下,如果沒有指定 this 的話,它值是 undefined

我們將 class 元件 MyComponent 也翻譯一下:

"use strict";

class MyComponent extends React.Component {
  render() {
    console.log('this', this);
    return /*#__PURE__*/React.createElement("h1", null, "Hello, ", this.props.name);
  }

}

props

我們首先回憶一下 vue 中的 props:

  • props 用於接收來自父元件的資料。就像這樣:
<div id='app'>
    <button-counter :msg='message'></button-counter>
</div>

<script>
  Vue.component('button-counter', {
    props: ['msg'],
    template: `<div>
                來自父元件的資訊: {{msg}}
              </div>`
  })

  var app = new Vue({
    el: '#app',
    data: {
      message: 'hello'
    }
  })
</script>
  • 可以使用.sync 修飾符來實現一個 prop 進行“雙向繫結”。即子元件不要直接更改父元件的這個屬性,而應該通知父元件,讓父元件自己去更改這個屬性。
  • prop 驗證。就像這樣:
Vue.component('my-component', {
  props: {
    // 基礎的型別檢查 (`null` 和 `undefined` 會通過任何型別驗證)
    propA: Number,
    // 必填的字串
    propC: {
      type: String,
      required: true
    },
    // 帶有預設值的數字
    propD: {
      type: Number,
      default: 100
    },
    // 帶有預設值的物件
    propE: {
      type: Object,
      // 物件或陣列預設值必須從一個工廠函式獲取
      default: function () {
        return { message: 'hello' }
      }
    },
    ...
  }
})

在 react 中,props 的作用也類似,即用於接收父元件傳來的屬性。

props 基本用法

給元件 MyComponent 定義了兩個 prop(屬性):

  • name,字串型別,預設值是 defaultName
  • say,函式型別,必填
<body>
    <div id="root"></div>

    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <!-- 使用 PropTypes 進行型別檢查 -->
    <script src="https://unpkg.com/prop-types@15.6/prop-types.js"></script>

    <script type="text/babel">
        class MyComponent extends React.Component {
            render() {
                const { name, say } = this.props
                return <h1>Hello, {this.props.name}, {say()}</h1>
            }
        }
        // 屬性型別
        MyComponent.propTypes = {
            name: PropTypes.string,
            // 注:函式不是 function,而是 func
            // 函式型別,並且必填
            say: PropTypes.func.isRequired
        }
        // 預設值
        MyComponent.defaultProps = {
            name: 'defaultName'
        }

        const props2 = { say: () => 'I know you' }

        ReactDOM.render(
            <div>
                <MyComponent name="peng" say={() => 'I love you'} />
                <MyComponent {...props2} />
            </div>,
            document.getElementById('root')
        );
    </script>
</body>

頁面顯示:

Hello, peng, I love you
Hello, defaultName, I know you

自 React v15.5 起,React.PropTypes 已移入另一個包中。請使用 prop-types 庫 代替 —— 官網

Tip:有關型別檢測更多介紹請看 使用 PropTypes 進行型別檢查

Props 的只讀性

元件無論是使用函式宣告還是通過 class 宣告,都決不能修改自身的 props。倘若我們嘗試修改 props 屬性,就像這樣:

class MyComponent extends React.Component {
    render() {
      + this.props.name = 'aName'
        ...
    }
}

瀏覽器控制檯將報錯如下:

Inline Babel script:10 Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'

內聯 Babel 指令碼:10 未捕獲的型別錯誤:無法分配給物件 '#<Object>' 的只讀屬性 'name'
使用 static 優化型別檢測

上文中,型別檢測的相關程式碼,從語法上來說就是給 MyComponent 類增加兩個靜態屬性:

// 型別檢測相關程式碼
MyComponent.propTypes = {
    name: PropTypes.string,
    say: PropTypes.func.isRequired
}
MyComponent.defaultProps = {
    name: 'defaultName'
}

我們可以使用 static 語法來對其優化。就像這樣:

// class 元件
class MyComponent extends React.Component {
    // 屬性型別
    static propTypes = {
        name: PropTypes.string,
        say: PropTypes.func.isRequired
    }
    // 預設值
    static defaultProps = {
        name: 'defaultName'
    }
    render() {
        ...
    }
}

函式元件中的 props

上文我們已經使用過了:

function MyComponent(props) {
    return <h1>Hello, {props.name}</h1>;
}

雖然函式元件中沒有 this,不能像 class 元件那樣通過 this 去使用 props(this.props.name),但函式元件可以通過引數接收 props。

super(props)

在 React 元件掛載之前,會呼叫它的建構函式。在為 React.Component 子類實現建構函式時,應在其他語句之前呼叫 super(props)。否則,this.props 在建構函式中可能會出現未定義的 bug —— 官網

這句話什麼意思?請看示例:

class MyComponent extends React.Component {
    constructor(props) {
        super(props)
        // 輸出 {}。表明 this.props 有值
        console.log(this.props.name)
    }
    render() {
        ...
    }
}

super(props) 改為 super()

class MyComponent extends React.Component {
    constructor(props) {
        super()
        // 輸出 undefined
        console.log(this.props)
    }
    render() {
        ...
    }
}

相關文章