標籤:

React 初窺:JSX 詳解

React 初窺:JSX 詳解 從屬於筆者的 React 與前端工程化實踐系列文章,本文引用借鑒的以及更多 React 相關資料參考 React 學習與實踐資料索引。

JSX

我們在上文中已經很多次的提及了 JSX,大家也對於基本的基於 JSX 編寫 React 組件所有了解。實際上在 JSX 推出之初飽受非議,很多人覺得其很怪異。的確雖然與正統的 HTML 相比其都是類 XML語法的聲明式標籤語言,但是其對於類名強制使用 className、強制要求標籤閉合等特點會讓不少的傳統前端開發者不太適應。JSX 的引入對筆者之前的工作流的衝擊在於不能夠直接使用 UI 部門提供的頁面模板,並且因為組件化的分割與預編譯,UI 比較麻煩地直接在瀏覽器開發工具中調整CSS樣式然後保存到源代碼中。JSX 本質上還是屬於 JavaScript,這就避免了我們重複地學習不同框架或庫中的指令約定,而可以直接使用 JavaScript 來描述模板渲染邏輯;而在前端框架的工作流中,往往將 JSX 的轉化工作交託於 Babel 等轉化工具,我們可以通過如下方式指定 JSX 使用的構建函數:

/** @jsx h */

JSX 的前世今生

JSX 語言的名字最早出現在遊戲廠商 DeNA,不過其偏重於加入增強語法使得JavaScript 變得更快、更安全、更簡單。而 React 則是依賴於 ECMAScript 語法本身,並沒有添加擴充語義。React 引入 JSX 主要是為了方便 View 層組件化,承載了構建 HTML 結構化頁面的職責。這一點與其他很多的 JavaScript 模板語言異曲同工,不過React 將 JSX 映射為虛擬元素,並且通過創建與更新虛擬元素來管理整個 Virtual DOM 系統。譬如我們 JSX 語法聲明某個虛擬組件時,會被轉化為React.createElement(component,props,...children) 函數調用,譬如我們定義了某個MyButton:

// 必須要在 JSX 聲明文件中引入 Reactimport React from "react";<MyButton color="blue" shadowSize={2}> Click Me</MyButton>

會被編譯為:

React.createElement( MyButton, {color: "blue", shadowSize: 2}, "Click Me")

而如果我們直接聲明某個DOM元素,同樣會轉化為createElement函數調用:

React.createElement( "div", {className: "sidebar"}, null)

實際上除了最著名的 Babel JSX 轉換器之外,我們還可以使用 JSXDOM 與 Mercury JSX 這兩個同樣的可以將 JSX 語法轉化為 DOM 或者 Virtual DOM。在 JSXDOM 中,只支持使用 DOM 元素,允許在 DOM 標籤中直接使用 JavaScript 變數,譬如當我們需要聲明某個列表時,可以使用如下語法:

/** @jsx JSXDOM */ var defaultValue = "Fill me ..."; document.body.appendChild( <div> <input type="text" value={defaultValue} /> <button onclick="alert("clicked!");">Click Me!</button> <ul> {["un", "deux", "trois"].map(function(number) { return <li>{number}</li>; })} </ul> </div>);

這裡我們還想討論另一個問題,為什麼需要引入 JSX。在 ECAMScript 6 的 ECMA-262 標準中引入了所謂的模板字元串(Template Literals),即可以在 ECMAScript 中使用內嵌的 DSL 來引入 JavaScript 變數,不過雖然模板字元串對於較長的嵌入式 DSL 作用極佳,但是對於需要引入大量作用域中的 ECMAScript 表達式會造成大量的噪音副作用,譬如如果我們要聲明某個評論框布局,使用 JSX 的方式如下:

// JSXvar box = <Box> { shouldShowAnswer(user) ? <Answer value={false}>no</Answer> : <Box.Comment> Text Content </Box.Comment> } </Box>;

而使用模板字元串的方式如下:

// Template Literalsvar box = jsx` <${Box}> ${ shouldShowAnswer(user) ? jsx`<${Answer} value=${false}>no</${Answer}>` : jsx` <${Box.Comment}> Text Content </${Box.Comment}> ` } </${Box}>`;

其主要缺陷在於因為存在變數的嵌套,需要在作用域中進進出出,很容易造成語法錯誤,因此還是 JSX 語法為佳。

JSX 語法

JSX 的官方定義是類 XML 語法的 ECMAscript 擴展,完美地利用了 JavaScript 自帶的語法和特性,並使用大家熟悉的 HTML 語法來創建虛擬元素。JSX 基本語法基本被 XML 囊括了,但也有很多的不同之處。React 在定義標籤時,標籤一定要閉合,否則無法編譯通過。這一點與標準的 HTML 差別很大,HTML 在瀏覽器渲染時會自動進行補全,而強大的 JSX 報錯機制則直接在編譯階段就以報錯的方式指明出來。HTML 中自閉合的標籤(如 <img> )在 JSX 中也遵循同樣規則,自定義標籤可以根據是否有子組件或文本來決定閉合方式。另外 DOCTYPE 頭也是一個非常特殊的標誌,一般會在使用 React 作為服務端渲染時用到。在 HTML 中,DOCTYPE 是沒有閉合的,也就是說我們無法直接渲染它。常見的做法是構造一個保存 HTML 的變數,將 DOCTYPE 與整個 HTML 標籤渲染後的結果串聯起來。使用JSX聲明組件時,最外層的組件根元素只允許使用單一根元素。這一點我們在上文中也陳述過,因為 JSX 語法會被轉化為 React.createElement(component,props,...children) 調用,而該函數的第一個參數只允許傳入單元素,而不允許傳入多元素。

變數使用

  • 注釋

在 HTML 中,我們會使用 <!-- --> 進行注釋,不過 JSX 中並不支持:

render() { return ( <div> <!-- This doesn"t work! --> </div> )}

我們需要以 JavaScript 中塊注釋的方式進行注釋:

{/* A JSX comment */}{/* Multi line comment*/}

  • 數組

JSX 允許使用任意的變數,因此如果我們需要使用數組進行循環元素渲染時,直接使用 map、reduce、filter 等方法即可:

function NumberList(props) { const numbers = props.numbers; return ( <ul> {numbers.map((number) => <ListItem key={number.toString()} value={number} /> )} </ul> );}

  • 條件渲染

在JSX中我們不能再使用傳統的if/else條件判斷語法,但是可以使用更為簡潔明了的Conditional Operator運算符,譬如我們要進行if操作:

{condition && <span>為真時進行渲染</span> }

如果要進行非操作:

{condition || <span>為假時進行渲染</span> }

我們也可以使用常見的三元操作符進行判斷:

{condition ? <span>為真時進行渲染</span> : <span>為假時進行渲染</span>}

如果對於較大的代碼塊,建議是進行換行以提升代碼可讀性:

{condition ? ( <span> 為假時進行渲染 </span>) : ( <span> 為假時進行渲染 </span>)}

元素屬性

  • style 屬性

JSX 中的 style 並沒有跟 HTML 一樣接收某個 CSS 字元串,而是接收某個使用 camelCase 風格屬性的 JavaScript 對象,這一點倒是和DOM 對象的 style 屬性一致。譬如:

const divStyle = { color: "blue", backgroundImage: "url(" + imgUrl + ")",};function HelloWorldComponent() { return <div stylex={divStyle}>Hello World!</div>;}

注意,內聯樣式並不能自動添加前綴,這也是筆者不太喜歡使用 CSS-in-JS 這種形式設置樣式的的原因。為了支持舊版本瀏覽器,需要提供相關的前綴:

const divStyle = { WebkitTransition: "all", // note the capital "W" here msTransition: "all" // "ms" is the only lowercase vendor prefix};function ComponentWithTransition() { return <div stylex={divStyle}>This should work cross-browser</div>;}

  • className

React 中是使用 className 來聲明 CSS 類名,這一點對於所有的 DOM 與 SVG 元素都起作用。不過如果你是將 React 與 Web Components 結合使用,也是可以使用 class 屬性的。

  • htmlFor

因為 for 是JavaScript中的保留關鍵字,因此 React 元素是使用 htmlFor 作為替代。

  • Boolean 系列屬性

HTML 表單元素中我們經常會使用 disabled、required、checked 與 readOnly 等 Boolean 值性質的書,預設的屬性值會導致 JSX 認為 bool 值設為 true。當我們需要傳入 false 時,必須要使用屬性表達式。譬如 <input type="checkbox" checked={true}> 可以簡寫為<input type="checkbox" checked>,而 <input type="checkbox" checked={falsed}> 即不可以省略 checked 屬性。

  • 自定義屬性

如果在 JSX 中向 DOM 元素中傳入自定義屬性,React 是會自動忽略的:

<div customProperty="a" />

不過如果要使用HTML標準的自定義屬性,即以 data-* 或者 aria-* 形式的屬性是支持的。

<div data-attr="attr" />

子元素

JSX 表達式中允許在一對開放標籤或者閉合標籤之間包含內容,這即是所謂的子元素,本部分介紹 JSX 支持的不同類別的子元素使用方式。

  • 字元串

我們可以將字元串放置在一對開放與閉合的標籤之間,此時所謂的 props.children 即就是字元串類型;譬如:

<MyComponent>Hello World!</MyComponent>

就是合法的 JSX 聲明,此時 MyComponent 中的 props.children 值就是字元串 Hello World!;另外需要注意的是,JSX 會自動移除行首與行末的空格,並且移除空行,因此下面的三種聲明方式渲染的結果是一致的:

<div>Hello World</div><div> Hello World</div><div> Hello World</div><div> Hello World</div>

  • JSX 嵌套我們可以嵌套地使用 JSX,即將某些 JSX 元素作為子元素,從而允許我們方便地展示嵌套組件:

<MyContainer> <MyFirstComponent /> <MySecondComponent /></MyContainer>

我們可以混合使用字元串與 JSX,這也是 JSX 很類似於 HTML 的地方:

<div> Here is a list: <ul> <li>Item 1</li> <li>Item 2</li> </ul></div>

某個 React 組件不可以返回多個 React 元素,不過單個 JSX 表達式是允許包含多個子元素的;因此如果我們希望某個組件返回多個並列的子元素,就需要將它們包裹在某個 div 中。

  • JavaScript 表達式

    我們可以傳入包裹在 {} 內的任意 JavaScript 表達式作為子元素,譬如下述聲明方式渲染的結果是相同的:

<MyComponent>foo</MyComponent><MyComponent>{"foo"}</MyComponent>

這種模式常用於渲染 HTML 列表:

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> );}

  • JavaScript 函數

    正常情況下 JSX 中包含的 JavaScript 表達式會被解析為字元串、React 元素或者列表;不過 props.children 是允許我們傳入任意值的,譬如我們可以傳入某個函數並且在自定義組件中調用:

// Calls the children callback numTimes to produce a repeated componentfunction Repeat(props) { let items = []; for (let i = 0; i < props.numTimes; i++) { items.push(props.children(i)); } return <div>{items}</div>;}function ListOfTenThings() { return ( <Repeat numTimes={10}> {(index) => <div key={index}>This is item {index} in the list</div>} </Repeat> );}

  • 布爾值與空值

    false,null,undefined 與 true 是有效的子元素,不過它們並不會被渲染,而是直接被忽略,如下的 JSX 表達式會被渲染為相同結果:

<div /><div></div><div>{false}</div><div>{null}</div><div>{undefined}</div><div>{true}</div>

避免 XSS 注入攻擊

最後需要提及的是,React 中 JSX 能夠幫我們自動防護部分 XSS 攻擊,譬如我們常見的需要將用戶輸入的內容再呈現出來:

const title = response.potentiallyMaliciousInput;// This is safe:const element = <h1>{title}</h1>;

在標準的 HTML 中,如果我們不對用戶輸入作任何的過濾,那麼當用戶輸入 <script>alert(1)<script/> 這樣的可執行代碼之後,就存在被 XSS 攻擊的危險。而 React 在實際渲染之前會幫我們自動過濾掉嵌入在 JSX 中的危險代碼,將所有的輸入進行編碼,保證其為純字元串之後再進行渲染。不過這種安全過濾有時候也會對我們造成不便,譬如如果我們需要使用 &copy; 這樣的實體字元時,React 會自動將其轉移最後導致無法正確渲染,我們可以尋找如下幾種解決方法:

  • 直接使用 UTF-8 字元或者使用對應字元的 Unicode 編碼

  • 使用數組封裝

  • 直接插入原始的 HTML,React 為我們提供了 dangerouslySetInnerHTML 屬性,其類似於 DOM 的 innerHTML 屬性,允許我們聲明強制直接插入 HTML 代碼:

function createMarkup() { return {__html: "First &middot; Second"};}function MyComponent() { return <div dangerouslySetInnerHTML={createMarkup()} />;}

推薦閱讀:

基於 Webpack 的應用包體尺寸優化
React 實現一個漂亮的 Table
React Conf 2017 不能錯過的大起底——Day 1!
解析 Redux 源碼

TAG:React |