React專題:JSX

JsTheGreat發表於2018-08-20

本文是『horseshoe·React專題』系列文章之一,後續會有更多專題推出
來我的 GitHub repo 閱讀完整的專題文章
來我的 個人部落格 獲得無與倫比的閱讀體驗

話說PHP是世界上最好的語言(笑)。

因為它的入門門檻極低。

<?php
$str = `<ul>`;
foreach ($fruits as $fruit) {
    $str += `<li>` . $fruit . `</li>`;
}
$str += `</ul>`;
?>

很多年前,這種字串拼接開發網頁的方式非常流行。

但是這種寫法有兩個問題:

  • 容易造成XSS注入,有極大的安全風險。
  • 拼接的寫法很繁瑣。

於是facebook的工程師開始動歪腦筋了。

XHP

他們的解決方案也很新穎,就是在程式碼裡直接寫標籤,而不是將標籤視為字串。

前面說到,字串拼接很容易造成XSS注入。那麼什麼是XSS注入呢?

比如惡意使用者輸入這麼一段內容:<script>code</script>,就可能被程式識別為一段指令碼,他可以在指令碼里面幹任何事情。

於是人們想到的辦法是對所有輸入轉義,轉義的作用就是讓所有標籤無法被識別為標籤,而只是標籤寫法的字串。使用者的輸入就會原原本本的展示在頁面上。

但是輸入轉義也有問題,就是容易把字串拼接的標籤也給轉義了。大家應該看過頁面上大段大段的標籤寫法的文字吧。

我們來看看XHP的寫法。

<?hh
$post =
    <div class="post">
        <h2>{$post}</h2>
        <p><span>Hey there.</span></p>
        <a href={$like_link}>Like</a>
    </div>;

誒,是不是有點眼熟?

XHP把標籤與字串區別開來了,變成指令碼語法的一部分。

這正好解決了前面提到的兩個問題:

  • 標籤就是標籤,字串就是字串,再也別想渾水摸魚。
  • 像寫指令碼一樣寫標籤,是不是爽多了?

JSX

其實facebook一直在前端元件化方面做各種嘗試,但都不是特別成功。

直到2013年,工程師Jordan Walke提出一個大膽的想法:把XHP的寫法遷移到JavaScript中來。即便有XHP的案例在前,大家還是覺得這個想法很瘋狂。

不過,facebook極為優秀的工程師文化最終促成了這種嘗試。這一嘗試不得了,開了天眼。

自此之後就開啟了React的開掛之路。

const element = <h1>Hello React!</h1>;

這不就是facebook一直在苦苦求索的前端元件化方案嗎?

刀耕火種時期的前端,入口是HTML,指令碼和樣式被引入到HTML頁面上。這是一種分離化的思想,以語言為最小顆粒。

然而經過大量痛苦的實踐,人們發現以內容為最小顆粒才是正解。以元件為單位,頁面結構、樣式和功能都被整合在元件內部,對開發者來說元件就是一個黑匣子,只能通過暴露出來的介面使用元件。這是一種封裝的思想,目的當然是為了複用。

當然,目前React還無法實現真正意義上的CSS封裝,不過以當下前端的關注度,CSS被徹底招安也指日可待。

語法

標籤的寫法和HTML一樣,只不過融入到了JavaScript中。

元件,其實就是自定義標籤,首字母必須大寫,為了與原生標籤區別開來。

如果標籤或元件沒有包含內容,可以採用自閉合標籤寫法。

const element = <App />;

JSX會自動忽略falsenullundefined

標籤的class屬性和for屬性要用className屬性和htmlFor屬性代替。

元件返回多個標籤或多個元件必須要用一個標籤或元件包裹,也就是說只能有一個頂層元素。

但是,React16以上的版本支援用空標籤包裹或者直接返回陣列。這樣的好處就是不必新增很多無用的標籤使頁面變得更加臃腫。

import React, { Fragment } from `react`;

const App = () => {
    return (
        <Fragment>
            <div>React</div>
            <div>Vue</div>
            <div>Angular</div>
        </Fragment>
    );
}

export default App;
import React from `react`;

const App = () => {
    return (
        <>
            <div>React</div>
            <div>Vue</div>
            <div>Angular</div>
        </>
    );
}

export default App;
import React from `react`;

const App = () => {
    return [
        <div key="1">React</div>,
        <div key="2">Vue</div>,
        <div key="3">Angular</div>,
    ];
}

export default App;

表示式

標籤裡肯定要寫一些變數,要不然頁面就是死的。

怎麼寫變數呢?用花括號包圍。

const name = `React`;
const element = <h1>Hello {name}!</h1>;

如果我想插入一個物件字面量怎麼辦?

很簡單,再包裹一層花括號。

const obj = { name: `React` };
const element = <h1 style={{ color: `#f66` }}>Hello {name}!</h1>;

實際上花括號語法支援所有的表示式。

那麼問題來了,什麼是表示式?

簡單來講,表示式的主要作用是計算和宣告,總是有返回值。與之相對,語句的主要作用是邏輯和動作,沒有返回值。

以下表示式JSX都支援。

const a = <button onClick={() => console.log(`react`)}>click</button>;

const b = <button onClick={function (){ console.log(`react`) }}>click</button>;

const c = <div>{popular ? `react` : `vue`}</div>;

const d = <div>{popular && `react`}</div>;

const e = <div>{renderSomething()}</div>;

像賦值語句、判斷語句和迴圈語句JSX都不支援。

那開發者要渲染一個列表怎麼辦?

for迴圈語句肯定是不行的,好在我們有map函式。因為從上例我們知道,JSX是支援函式執行表示式的。

forEach函式行不行呢?不行,因為它沒有返回值。也就是說,filter、find、reduce等有返回值的遍歷函式都是可以的。

import React, { Component } from `react`;

const list = [`react`, `vue`, `angular`];

class App extends Component {
    render() {
        return (
            <div>{list.map(value => <div key={value}>{value}</div>)}</div>
        );
    }
}

export default App;

編譯

不知道你們有沒有這樣的疑問:

  • 為什麼返回多個標籤或元件必須要用一個標籤或元件包裹?
  • 為什麼在根本沒有使用React這個變數的情況下還要import React

這裡就要講到JSX的編譯。

因為JSX不是正確的JavaScript語法,它要經過編譯才能被瀏覽器識別。

目前JSX的編譯工作是由babel來完成的。

我們來看看編譯都做了哪些工作。

下面的例子,後者是前者編譯後的結果。

const app = (
    <div className="form">
        <input type="text" />
        <button>click</button>
    </div>
);
const app = React.createElement(
    "div",
    { className: "form" },
    React.createElement("input", { type: "text" }),
    React.createElement(
        "button",
        null,
        "click",
    ),
);

可以看到,標籤最後變成了一個函式執行表示式,第一個引數是標籤名,第二個引數是屬性集合,之後的引數都是子標籤。

看到這裡,相信也不用我解釋了,前面提出的兩個問題恍然大悟。

整個UI實際上是通過層層巢狀的React.createElement方法返回的,所以我們要在檔案開頭import React,否則編譯後就會發現createElement沒有定義。

React.createElement執行的結果是一個物件,物件的屬性描述了標籤或元件的性狀,物件再巢狀子物件。如果頂層返回多個標籤,就無法表達為一個物件了。

由於React16引入了Fiber機制,使得返回多標籤成為可能(並不清楚原因)。

同時也回答了為什麼標籤的class屬性和for屬性要用className屬性和htmlFor屬性代替。在標籤裡屬性怎麼寫都無所謂,但是classfor是JavaScript中的關鍵字,所以要換一種寫法。

React裡面傳遞props有一種寫法,如果傳遞的是一個物件,可以用擴充套件運算子很方便的傳遞。

下面的例子,value先是被擴充套件運算子將屬性分解,然後又被一個物件包裹。這裡只是做了一個淺拷貝,並沒有其他的含義。所以最終傳遞給元件的仍然是一個物件。

所以疑問就來了,通常給元件傳遞屬性都是鍵值對的形式,直接傳遞一個物件也可以嗎?

其實所有的屬性最後都會放到一個物件裡面,所以兩種寫法殊途同歸。React只不過給了一種快捷方式。

瞭解編譯的過程,很多寫法都很好理解了。

const value = { a: 1, b: 2 };
const element = <App a={value.a} b={value.b} />;
const value = { a: 1, b: 2 };
const element = <App {...value} />;

React專題一覽

什麼是UI
JSX
可變狀態
不可變屬性
生命週期
元件
事件
操作DOM
抽象UI

相關文章