深入了解JSX

发布于 大漠

最近开始学着使用React写东西。在写代码时会使用JSX,不了解JSX的相关知识写起代码的效率还是蛮低的。为了能更好的进入状态,打算先把JSX相关的知识和细节了解一下。在这篇文章中我们主要就是来学习一下JSX的相关知识。希望对于像我这样的初学React(或初次接触JSX)的同学有所帮助。

什么是JSX

JSX是JavaScript中的一种语法扩展。是React组件编写UI逻辑的语言扩展(JSX除了能在React中使用之外还可以用于别的地方)。虽然在不使用JSX的情况之下,React可以完全正常工作,但它是一种理想的组件处理技术,所以React从JSX中获益良多。

首先,你可能认为使用JSX就是将HTML和JavaScript混合在一起,事实并非如此,因为在使用JSX语法时,描述这个UI不是用字符串,使用的是JavaScript。这让我们能做很多事。

将JavaScript和标记(Markup)放在同一位置被认为是一种坏习惯,但事实证明,将视图与功能结合起来可以直接对视图进行推理。

为了了解这意味着什么,假设我们有一个React组件,它只渲染一个<h1>标签。JSX允许我们以种非常类似HTML的方式声明这个元素:

class HelloWorld extends React.Component {
    render() {
        return (
            <h1 className="title">Hello World</h1>
        )
    }
}

HelloWorld组件中的render()函数看起来返回的是HTML,但实际上这是JSX。JSX在运行时被转换为常规则JavaScript。翻译后的组件看起来是这样的:

class HelloWorld extends React.Component {
    render() {
        return (
            React.createElement(
                'h1',
                {className: 'title'},
                'Hello World'
            )
        )
    }
}

虽然JSX看起来像HTML,但实际上它只是一种编写React.createElement()声明的更简洁的方法。当组件渲染时,它输出一个React元素树或该组件输出的HTML元素的虚拟DOM。然后,React将根据这个React元素表示确定对实际DOM进行哪些更改。HelloWorld组件渲染出来的React的HTML看起来像下面这样:

<h1 class='title'>Hello World</h1>

为什么使用JSX

React认为渲染逻辑本质上与其他UI逻辑内在耦合,比如,在UI中需要绑定处理事件、在某些时刻状态发生变化时需要通知到UI,以及需要在UI中展示准备好的数据。

React并没有采用将标记与逻辑进行分离到不同文件这种人为地分离方式,而是通过将二者共同存放在称之为组件的松散耦合单元之中,来实现“关注点分离”。React不强制要求使用JSX,但大多数人发现,在JavaScript代码中将JSX和UI放在一起时,会在视觉上有辅助作用。它还可以使React显示更多有用的错误和警告消息。

JSX原理

在继续往下阅读之前,先稍微的了解一些有关于JSX原理相关的内容。即JSX是经过怎么样的转化变成页面的元素的

比如说下面这个最简单的DOM元素为例,怎么使用JavaScript的对象来表现一个DOM元素的结果:

<h1 class="title">Hello World!</h1>

每个DOM元素的结构都可以用JavaScript的对象来表示。你会发现,DOM元素包含的信息其实只有三个:标签名(HTML元素)属性(HTML元素属性)子元素(HTML元素或文本节点)。那么上面的这个HTML标签对应的所有信息可以用下面这个对象来描述:

{
    tag: 'div',
    attrs: {className: 'title'},
    children: 'Hello World!'
}

你会发现,HTML的信息和JavaScript所包含的结构和信息其实是一样的,我们可以用JavaScript对象来描述所有能用HTML表示的UI信息。只不过写起来有点麻烦,结构看起来不太清晰。

在React中会把类似HTML的JSX结构转换成JavaScript的对象结构。比如:

<h1 class="title">Hello World!</h1>

JSX转换成JavaScript就会像下面这样:

React.createElement(
    "h1", 
    {
        class: "title"
    }, 
    "Hello World!"
);

React.createElement()会构建一个JavaScript对象来描述HTML结构的信息,包括标签名属性子元素等。上面是一个较为简单的示例,对于复杂的示例,我们可以借助在线的工具来帮助我们,比如Babel

所谓的JSX其实就是JavaScript对象

有了之个表示HTML结构和信息的对象以后,就可以去构造真正的DOM元素,然后使用ReactDOM.render()函数把构造好的DOM元素塞到页面中:

ReactDOM.render(
    <HelloWorld />,
    document.getElementById('root')
)

ReactDOM.render()函数就是把组件渲染并且构造DOM树,然后插入到页面上某个特定的元素上div#root

简单地总结一下,JSX经过Babel编译和React构造器转换成了JavaScript对象,然后通过ReactDOM.render()函数转换成DOM元素,再插入到页面中渲染出来:

接下来简单看看JSX的具体使用。

JSX的使用

在JSX中可以像下面这样定义一个包含字符串的<h1>标签:

const element = <h1> Hello World!~</h1>

看起来有点像是JavaScript和HTML的结合特,但实际上它就是JavaScript。在JSX中是用来定义组件及其在标记中的位置的语法糖。事实element是一个Object

JSX中嵌入表达式

在下面的示例中,声明了一个名为eleId的变量,然后在JSX中使用它,并且放置在{}中,比如:

const eleId = 'title'
const element = <h1 id={eleId}>Hello World!</h1>

在JSX语法中,可以在大括号内放置任何有效的JavaScript表达式,比如:

const Hello = (user) => {
    return `Hello ${user}`;
}

const element = <h1>{ Hello('W3cplus.com') }</h1>

在属性中嵌入JavaScript表达式时,不要在大括号外面加上引号。应该仅使用引用(字符串)或大括号(表达式)中的一个,对于同一个属性不能同时使用这两种符号。

JSX中的特定属性

在HTML中标签中有时候会用到带有-中折号的属性,比如aira-hidden,也会有多个词组合在一起的属性,比如tabindex。类似这些属性在JSX中需要使用驼峰写法,比如airaHiddentabIndex

const eleId='title'
const element = <h1 id={eleId} tabIndex="0">Hello World!</h1>

需要特别注意的是,在JSX中要是会用到classfor属性时,需要将class换成 classNamefor换成 htmlFor。那是因为classfor是JavaScript中的关键字。

const element = (
    <div className="control">
        <label htmlFor="user">Name:</label>
        <input id="user" type="text" placholder="Enter your name" />
    </div>
)

在JSX中有些写法和HTML类似,比如一个标签里没有内容,可以使用/>来闭合标签,比如上面的<input>元素:

<input id="user" type="text" placholder="Enter your name" />

如果在ReactDOM.render()包含多个子元素时(JSX标签里包含多个子元素),需要用一个标签元素括起来,比如上面示例中的<div className="control">。另外将其放置在一个括号()中。这是因为render()函数只能返回一个节点,所以如果你想返回两个兄弟节点,就需要添加一个父节点,如上面示例所示。

JSX是一个对象

浏览器不能直接执行包含JSX代码的JavaScript文件。它们必须首先转换成普通的JavaScript。需要一个叫做转置的过程。当然,在React中JSX是可选的,因为对于每个JSX代码都有对应的纯JavaScript代码替代,这就是JSX的换位符

正如文章开头所提到的,Babel会把JSX转译成一个名为React.createElement()函数调用。比如下面的两段代码,起到的作用是同等的:

// JSX代码
ReactDOM.render(
    <div id="box">
        <h1>title</h1>
        <p>paragraph</p>
    </div>,
    document.getElementById('root')
)

// JavaScript
ReactDOM.render(
    React.createElement(
        "div", 
        {id: "box"},
        React.createElement(
            "h1", 
            null, 
            "title"
        ), 
        React.createElement(
            "p", 
            null, 
            "paragraph"
        )
    ), 
    document.getElementById('root')
);

JSX和JavaScript两段代码同样可以借助在线的Babel工具查看。

上面的示例也再次验证了,虽然JSX和JavaScript所起的效果是等效的,但相比而言,JavaScript要比JSX复杂的多。另外React.createElement()会预先执行一些检查,以帮助你编写无错代码,但实际上它创建了一个这样的对象:

// 简化后的结构
React.createElement(
    "h1",
    null, 
    "title"
)

实际上,JSX仅仅只是React.createElement(component, props, ...children)函数的语法糖。如下JSX代码:

// JSX代码
<MyButton color="blue" shadowSize={2}>
    Click Me
</MyButton>

编译成JavaScript代码如下:

// JavaScript代码
React.createElement(
    MyButton, 
    {
        color: "blue",
        shadowSize: 2
    }, 
    "Click Me"
);

JSX中的JS

JSX接受任何混合的JavaScript。当你需要添加一些JavaScript时,只需要将它放到大括号{}中(前面也有简单的提到过)。例如,下面的示例就是向你展示了在JSX中怎么引用其他地方声明的常量:

const name = '大漠'

ReactDOM.render(
    <h1> Hello, { name }</h1>,
    document.getElementById('root')
)

正如你所看到的,我们可以将JavaScript嵌套在JSX中定义的JSX中,而且你想多深就可以多深(你想放置在JSX代码中的位置):

const items = ['大漠', 'w3cplus', '杭州']
ReactDOM.render(
    <ul>
        {items.map((item, i) => {
            return <li>({i}):{item}</li>
        })}
    </ul>,
    document.getElementById('root')
)

JSX中的HTML

JSX看上去和HTML非常相似,但实际上是XML语法。但在页面中最终呈现的依旧是HTML,因此我们有必要了解HTML中如何定义某些内容与在JSX中定义的差异性。

需要关闭所有标记

就像在XHTML中一样,需要关闭所有的标记,比如<img>标签需要使用自闭标记<img />,其他标记也是类似。<img>标记有点特殊,那么对于其他HTML标记也是有相通的原理,比如<div>这样的常规标记,如果没有包含子元素(或子节点)也可以像这样使用<div />来关闭标签,如果有子元素之类的就需要<div></div>这样使用。

驼峰写法是新的标准

在HTML中,你会发现属性没有任何大小写,比如onclick。但在JSX中,它们被重命名,需要用驼峰的方式来命名:

onchange => onChange
onclick => onClick
onsubmit => onSubmit

前面也提到过了,HTML中的classfor是JavaScript的关键词,所以在JSX中需要以新的方式提供class=>classNamefor=>htmlFor

JSX中的表单

JSX中对于表单的部分做一些更改,主要目的是让开发人员更容易地完成工作。

value和defaultValue

value属性始终保存字段的当前值;defaultValue属性保存创建字段时设置的默认值。这有助于解决在检查输入(input)时常规DOM交互的一些奇怪行为。input.valueinput.getAttribute('value') 返回一个当前值和一个初始默认值。

这个也适用于textarea标签,比如:

<textarea>多行文本域的文本</textarea>

需要用下面的方式来替代:

<textarea defaultValue={'多行文本域的文本'} />

<select>也类似:

<select>
    <option value="x" selected></option>
</select>

用下面的这种方式来替代:

<select defaultValue="x">
    <option value="x"></option>
</select>

JSX中的HTML实体符

在HTML中编写代码的时候,有时候会使用到HTML的实体符,比如©&copy;来表示。在JSX中为了降低XSS攻击的风险,JSX强制在表达式中自动会将HTML实体符进行转义。比如:

<p>{'&copy; 2019'}</p>

在JSX中,&copy;会被转义为"\xA9 2019"。那么上面的代码,在JSX中应该:

<p>{'\xA9 2019'}</p>

<!-- 或者 -->
<p>{'\u00A9 2019'}</p>

渲染出来的结果如下:

JSX中的空格符

在JSX中添加空格符有两条规则:

  • 水平空白符会被裁剪为一个空白符:如果在同一行元素之间有空白,则所有元素都会被裁剪为一个空白符
  • 消除垂直换行的空白符:如果是垂直换行的空白符会完全消除

比如:

<!-- JSX -->
<p>Something       becomes               this</p>
<p>
    Something
    becomes
    this
</p>

<!-- 编译后结果 -->
<p>Something becomes this</p>
<p>Somethingbecomesthis</p>

要解决这个问题,需要显式地通过添加一个空格符表达式来处理,像下面这样:

<p>
    Something
    {' '}becomes
    {' '}this
</p>

<!-- 或像下面这样 -->

<p>
    Something
    {' becomes '}
    this
</p>

JSX中的注释

在编码时难免少不了添加一些注释。在不同的语言中有不同的注释方式,比如:

<!-- HTML中的注释方式 -->

/* CSS中的单行注释方式 */

/*
 * CSS 中的多行注释方式
 *
*/

// JavaScript中的注释 

// 1. JavaScript的多行注释
// 2. JavaScript的多行注释

/*
 * JavaScript中的多行注释
*/

JSX中的注释会放在{}中,像下面这样:

{/* JSX中的单行注释*/}

{
    // JSX中的单行注释
}

{
    /* 
     * JSX中的多行注释
    */
}

JSX中的条件渲染

在JavaScript中,对于if ... elseswitch语句并不会感到陌生,在JSX中其实也可以使用它们。

在JSX中根据条件渲染中,组件根据一个或多个条件决定返回哪些元素。例如可以返回一个项目列表,也可以返回一条消息。当组件具有条件渲染时,渲染组件的实例可以具有不同的外观(组件样式不一样)。

JSX中的if ... else

条件渲染最简单的方式就是使用if ... else方式。例如,当没有项列表时,列表组件不应该渲染列表。我们可以使用if语句从渲染生命周期中提前返回。

function List({ list }) {
    if (!list) {
        return null;
    }

    return (
        <div>
            {list.map(item => <ListItem item={item} />)}
        </div>
    );
}

上面的JSX编译后的结果如下:

function List(_ref) {
    var list = _ref.list;

    if (!list) {
        return null;
    }

    return React.createElement(
        "div", 
        null, 
        list.map(function (item) {
            return React.createElement(
                ListItem, 
                {item: item}
            );
        })
    );
}

返回null的组件将不会渲染任何内容。然而,你可能想在列表为空时显示文本,以便为你的应用程序给用户提供一些反馈,提供较好的用户体验:

function List({ list }) {
    if (!list) {
        return null;
    }

    if (!list.length) {
        return <p>对不起,列表是空的!</p>;
    } else {
        return (
            <div>
                {list.map(item => <ListItem item={item} />)}
            </div>
        );
    }
}

JSX中的三元运算符

使用三元运算符可以使if ... else语句更简洁。

来看一个示例,假设组件中一个切换按钮,可以在高亮暗黑两种模式之间切换。决定切换到哪种状态只需要一个简单条件的布尔值,该布尔值可以决定要返回哪个元素(切换到哪种模式):

function Item({ item, mode }) {
    const isDarkMode = mode === 'DARK';

    return (
        <div>
            { isDarkMode
                ? <DarkMode item={item} />
                : <HighlightMode item={item} />
            }
        </div>
    );
}

如果三元运算符中的两个分支会用到多个子元素时,可以使用()括起来:

function Item({ item, mode }) {
    const isDarkMode = mode === 'DARK';

    return (
        <div>
            { 
                isDarkMode ? (
                    <DarkMode item={item} />
                ) : (
                    <HighlightMode item={item} />
                )
            }
        </div>
    );
}

JSX中的&&操作符

在写组件的时候,经常会碰到这种情况,要么渲染出组件,要么什么都不渲染,例如有一个Card组件,它渲染一个卡片或什么都不渲染。我们可以使用上面提到的if ... else语句或三元运算符来实现:

{/* if ... else */}
function CardFun({ isCard }) {
    if (isCard) {
        return (
            <Card />
        )
    } else {
        return null
    }
}

{/* 三元操作符 */}
function CardFun({ isCard }) {
    return (
        <div>
            {isCard ? <Card /> : null}
        </div>
    )
}

但还有一种方法会更简单,可以省略返回null的必要性。即使用&&操作符会更简单:

const result = true && 'Hello World';
console.log(result);
// Hello World

const result = false && 'Hello World';
console.log(result);
// false

使用&&运算符,上面的示例可以像下面这样使用:

function CardFun({ isCard }) {
    return (
        <div>
            {isCard && <Card />}
        </div>
    )
}

JSX中的switch操作符

在JSX中如果要根据多个不同的条件渲染出组件不同的状态,那么可以使用switch ... case的方式来处理,比如:

function Notification({ text, state }) {
    switch(state) {
        case 'info':
            return <Info text={text} />;
        case 'warning':
            return <Warning text={text} />;
        case 'error':
            return <Error text={text} />;
        default:
            return null;
    }
}

在React中,如果组件基于状态的条件渲染时,还可以使用React.PropTypes描述组件接口:

function Notification({ text, state }) {
    switch(state) {
        case 'info':
            return <Info text={text} />;
        case 'warning':
            return <Warning text={text} />;
        case 'error':
            return <Error text={text} />;
        default:
            return null;
    }
}

Notification.propTypes = {
    text: React.PropTypes.string,
    state: React.PropTypes.oneOf(['info', 'warning', 'error'])
}

有关于React中的React.PropTypes相关知识不在这里阐述,后续我们会专门花一定的篇幅来介绍这方面的知识

现在有一个通用组件来显示不同类型的通知。例如,基于state的值,组件可以有不同的外观。error是红色的,warning可能是黄色的,info可能是蓝色的。

在JSX中还可以用内联开关的方式,需要一个自调用JavaScript函数。

function Notification({ text, state }) {
    return (
        <div>
            {(function() {
                switch(state) {
                case 'info':
                    return <Info text={text} />;
                case 'warning':
                    return <Warning text={text} />;
                case 'error':
                    return <Error text={text} />;
                default:
                    return null;
                }
            })()}
        </div>
    );
}

上面的示例,还可以使用ES6箭头函数来写,会更简洁:

function Notification({ text, state }) {
    return (
        <div>
            {(() => {
                switch(state) {
                case 'info':
                    return <Info text={text} />;
                case 'warning':
                    return <Warning text={text} />;
                case 'error':
                    return <Error text={text} />;
                default:
                    return null;
                }
            })()}
        </div>
    );
}

JSX中使用枚举条件来渲染

在JavaScript中,当对象用作键值的映射时,对象可以用作枚举。

const ENUM = {
    a: '1',
    b: '2',
    c: '3',
};

枚举用作多个条件渲染也是一个较好的方式,比如上面的示例,使用枚举可以改写成下面这样:

function Notification({ text, state }) {
    return (
        <div>
            {{
                info: <Info text={text} />,
                warning: <Warning text={text} />,
                error: <Error text={text} />,
            }[state]}
        </div>
    );
}

state属性键帮助我们从对象中检索值。相比前面的示例是不是更简洁,是吧。而且更具可读性。在本例中,我们必须使用内联对象,因为对象的值依赖于text属性。如果不依赖text,就需要使用外部静态枚举方式:

const NOTIFICATION_STATES = {
    info: <Info />,
    warning: <Warning />,
    error: <Error />,
};

function Notification({ state }) {
    return (
        <div>
            {NOTIFICATION_STATES[state]}
        </div>
    );
}

如果依赖text属性可以使用函数来检索值:

const getSpecificNotification = (text) => ({
    info: <Info text={text} />,
    warning: <Warning text={text} />,
    error: <Error text={text} />,
});

function Notification({ state, text }) {
    return (
        <div>
            {getSpecificNotification(text)[state]}
        </div>
    );
}

JSX中多级条件渲染

JSX中还会有多级条件(嵌套条件)渲染的场景。比如下面这个示例,List组件,它既可以显示列表、空文本,也可以什么都不显示:

function List({ list }) {
    const isNull = !list;
    const isEmpty = !isNull && !list.length;

    return (
        <div>
            { isNull
                ? null
                : ( isEmpty
                ? <p>Sorry, the list is empty.</p>
                : <div>{list.map(item => <ListItem item={item} />)}</div>
                )
            }
        </div>
    );
}

// Usage

<List list={null} />            // => <div></div>
<List list={[]} />              // => <div><p>Sorry, the list is empty.</p></div>
<List list={['a', 'b', 'c']} /> // => <div><div>a</div><div>b</div><div>c</div><div>

上面的示例还可以继续优化,将嵌套条件保持在最小值状态。建议将复杂的组件拆分成更小的组件,这些组件自身具备有条件地渲染。

function List({ list }) {
    const isList = list && list.length;

    return (
        <div>
            { isList
                ? <div>{list.map(item => <ListItem item={item} />)}</div>
                : <NoList isNull={!list} isEmpty={list && !list.length} />
            }
        </div>
    );
}

function NoList({ isNull, isEmpty }) {
    return (!isNull && isEmpty) && <p>Sorry, the list is empty.</p>;
}

特别声明:JSX中条件渲染这部分示例代码来自于@rwieruch的《All React Conditional Rendering Techniques》一文。

有关于条件渲染相关的还可以阅读@Kristina Grujic的《React JSX: How to Do It the Right Way, Part II》和《条件渲染》。

JSX中的循环

如果你有一组需要循环的元素来生成JSX部分,可以创建一个循环,然后将JSX添加到一个数组中:

const elements = [] //..some array

const items = []

for (const [index, value] of elements.entries()) {
    items.push(<Element key={index} />)
}

现在,当渲染JSX时,可以通过用大括号{}来嵌套items数组:

const elements = ['one', 'two', 'three'];

const items = []

for (const [index, value] of elements.entries()) {
    items.push(<li key={index}>{value}</li>)
}

return (
    <div>
        {items}
    </div>
)

如果使用map还可以在JSX中直接执行相同的操作:

const elements = ['one', 'two', 'three'];
return (
    <ul>
        {elements.map((value, index) => {
            return <li key={index}>{value}</li>
        })}
    </ul>
)

有关于JSX中的循环相关的使用,更详细的介绍还可以阅读:

JSX中表达式

前面其实提到过,在JSX中可以通过使用大括号{}来引用表达式,用于将变量传递给元素和属性、计算布尔表达式等。比如下面这样的一个示例:

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            address: {
                houseNumber: '12',
                street: 'Oakwood Drive',
                city: 'Red Hook',
                state: 'New York'
                zipcode: '1234',
                country: 'USA'
            }
        }
    }

    formatAddress() {
        const { address } = this.state

        return `${address.houseNumber}
            ${address.street}
            ${address.city}
            ${address.state}
            ${address.zipcode}
            ${address.country}
        `
    }

    render() {
        return (
            <div className="App">
                { this.formatAddress() }
            </div>
        )
    }
}

const rootElement = document.getElementById(root)

ReactDOM.render(<App />, rootElement)

如果想在字符串中显示变量内部的值,那么ES6的模板字符串(Template Literals)就非常有用,比如:

cosnt monthAndYear = `${month} - ${year}`

在标签元素中属性也可以使用表达多:

const profilePicture = <img src = {user.picture} alt="me!" />

另外如果想根据条件给元素添加类名,也可以使用表达式直接处理:

<h1 className={isLogo ? 'logo' : 'title'}></h1>

JSX 中的 Props

在JSX中有多种方式可以指定Props。

JavaScript 表达式作为 Props

你可以把包裹在 {} 中的 JavaScript 表达式作为一个 prop 传递给 JSX 元素。例如,如下的 JSX:

<MyComponent foo={1 + 2 + 3 + 4} />

MyComponent 中,props.foo 的值等于 1 + 2 + 3 + 4 的执行结果 10

if 语句以及 for 循环不是 JavaScript 表达式,所以不能在 JSX 中直接使用。但是,你可以用在 JSX 以外的代码中。比如:

function NumberDescriber(props) {
    let description;
    if (props.number % 2 == 0) {
        description = <strong>even</strong>;
    } else {
        description = <i>odd</i>;
    }
    return <div>{props.number} is an {description} number</div>;
}

字符串字面量

你可以将字符串字面量赋值给 prop。 如下两个 JSX 表达式是等价的:

<MyComponent message="hello world" />

<MyComponent message={'hello world'} />

当你将字符串字面量赋值给 prop 时,它的值是未转义的。所以,以下两个 JSX 表达式是等价的:

<MyComponent message="&lt;3" />

<MyComponent message={'<3'} />

这种行为通常是不重要的,这里只是提醒有这个用法。

Props 默认值为 “True”

如果你没给 prop 赋值,它的默认值是 true。以下两个 JSX 表达式是等价的:

<MyTextBox autocomplete />

<MyTextBox autocomplete={true} />

通常,我们不建议这样使用,因为它可能与 ES6 对象简写混淆,{foo}{foo: foo} 的简写,而不是 {foo: true}。这样实现只是为了保持和 HTML 中标签属性的行为一致。

属性展开

如果你已经有了一个 props 对象,你可以使用展开运算符 ... 来在 JSX 中传递整个 props 对象。以下两个组件是等价的:

function App1() {
    return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
    const props = {firstName: 'Ben', lastName: 'Hector'};
    return <Greeting {...props} />;
}

你还可以选择只保留当前组件需要接收的 props,并使用展开运算符将其他 props 传递下去。

const Button = props => {
    const { kind, ...other } = props;
    const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
    return <button className={className} {...other} />;
};

const App = () => {
    return (
        <div>
            <Button kind="primary" onClick={() => console.log("clicked!")}>
                Hello World!
            </Button>
        </div>
    );
};

在上述例子中,kindprop 会被安全的保留,它将不会被传递给 DOM 中的 <button> 元素。 所有其他的 props 会通过 ...other 对象传递,使得这个组件的应用可以非常灵活。你可以看到它传递了一个 onClickchildren 属性。

属性展开在某些情况下很有用,但是也很容易将不必要的 props 传递给不相关的组件,或者将无效的 HTML 属性传递给 DOM。我们建议谨慎的使用该语法。

JSX 中的子元素

包含在开始和结束标签之间的 JSX 表达式内容将作为特定属性 props.children 传递给外层组件。有几种不同的方法来传递子元素:

字符串字面量

你可以将字符串放在开始和结束标签之间,此时 props.children 就只是该字符串。这对于很多内置的 HTML 元素很有用。例如:

<MyComponent>Hello world!</MyComponent>

这是一个合法的 JSX,MyComponent 中的 props.children 是一个简单的未转义字符串 "Hello world!"。因此你可以采用编写写 HTML 的方式来编写写 JSX。如下所示:

<div>This is valid HTML &amp; JSX at the same time.</div>

JSX 子元素

子元素允许由多个 JSX 元素组成。这对于嵌套组件非常有用:

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

你可以将不同类型的子元素混合在一起,因此你可以将字符串字面量与 JSX 子元素一起使用。这也是 JSX 类似 HTML 的一种表现,所以如下代码是合法的 JSX 并且也是合法的 HTML:

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

React 组件也能够返回存储在数组中的一组元素:

render() {
    // 不需要用额外的元素包裹列表元素!
    return [
        // 不要忘记设置 key :)
        <li key="A">First item</li>,
        <li key="B">Second item</li>,
        <li key="C">Third item</li>,
    ];
}

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 表达式也可以和其他类型的子元素组合。这种做法可以方便地替代模板字符串:

function Hello(props) {
    return <div>Hello {props.addressee}!</div>;
}

函数作为子元素

通常,JSX 中的 JavaScript 表达式将会被计算为字符串、React 元素或者是列表。不过,props.children 和其他 prop 一样,它可以传递任意类型的数据,而不仅仅是 React 已知的可渲染类型。例如,如果你有一个自定义组件,你可以把回调函数作为 props.children 进行传递:

// 调用子元素回调 numTimes 次,来重复生成组件
function 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>
    );
}

你可以将任何东西作为子元素传递给自定义组件,只要确保在该组件渲染之前能够被转换成 React 理解的对象。这种用法并不常见,但可以用于扩展 JSX。

布尔类型、Null 以及 Undefined 将会忽略

false, null, undefinedtrue 是合法的子元素。但它们并不会被渲染。以下的 JSX 表达式渲染结果相同:

<div />

<div></div>

<div>{false}</div>

<div>{null}</div>

<div>{undefined}</div>

<div>{true}</div>

这有助于依据特定条件来渲染其他的 React 元素。例如,在以下 JSX 中,仅当 showHeadertrue 时,才会渲染 <Header />

<div>
    {showHeader && <Header />}
    <Content />
</div>

值得注意的是有一些 “falsy” 值,如数字 0,仍然会被 React 渲染。例如,以下代码并不会像你预期那样工作,因为当 props.messages 是空数组时,0 仍然会被渲染:

<div>
    {props.messages.length &&
        <MessageList messages={props.messages} />
    }
</div>

要解决这个问题,确保 && 之前的表达式总是布尔值:

<div>
    {props.messages.length > 0 &&
        <MessageList messages={props.messages} />
    }
</div>

反之,如果你想渲染 falsetruenullundefined 等值,你需要先将它们转换为字符串:

<div>
    My JavaScript variable is {String(myVariable)}.
</div>

指定 React 元素类型

JSX 标签的第一部分指定了 React 元素的类型。

大写字母开头的 JSX 标签意味着它们是 React 组件。这些标签会被编译为对命名变量的直接引用,所以,当你使用 JSX <Foo /> 表达式时,Foo 必须包含在作用域内。

React 必须在作用域内

由于 JSX 会编译为 React.createElement 调用形式,所以 React 库也必须包含在 JSX 代码作用域内。

例如,在如下代码中,虽然 ReactCustomButton 并没有被直接使用,但还是需要导入:

import React from 'react';
import CustomButton from './CustomButton';

function WarningButton() {
    // return React.createElement(CustomButton, {color: 'red'}, null);
    return <CustomButton color="red" />;
}

如果你不使用 JavaScript 打包工具而是直接通过 <script> 标签加载 React,则必须将 React 挂载到全局变量中。

在 JSX 类型中使用点语法

在 JSX 中,你也可以使用点语法来引用一个 React 组件。当你在一个模块中导出许多 React 组件时,这会非常方便。例如,如果 MyComponents.DatePicker 是一个组件,你可以在 JSX 中直接使用:

import React from 'react';

const MyComponents = {
    DatePicker: function DatePicker(props) {
        return <div>Imagine a {props.color} datepicker here.</div>;
    }
}

function BlueDatePicker() {
    return <MyComponents.DatePicker color="blue" />;
}

用户定义的组件必须以大写字母开头

以小写字母开头的元素代表一个 HTML 内置组件,比如 <div> 或者 <span> 会生成相应的字符串 'div' 或者 'span' 传递给 React.createElement(作为参数)。大写字母开头的元素则对应着在 JavaScript 引入或自定义的组件,如 <Foo /> 会编译为 React.createElement(Foo)

我们建议使用大写字母开头命名自定义组件。如果你确实需要一个以小写字母开头的组件,则在 JSX 中使用它之前,必须将它赋值给一个大写字母开头的变量。

例如,以下的代码将无法按照预期运行:

import React from 'react';

// 错误!组件应该以大写字母开头:
function hello(props) {
    // 正确!这种 <div> 的使用是合法的,因为 div 是一个有效的 HTML 标签
    return <div>Hello {props.toWhat}</div>;
}

function HelloWorld() {
    // 错误!React 会认为 <hello /> 是一个 HTML 标签,因为它没有以大写字母开头:
    return <hello toWhat="World" />;
}

要解决这个问题,我们需要重命名 helloHello,同时在 JSX 中使用 <Hello />

import React from 'react';

// 正确!组件需要以大写字母开头:
function Hello(props) {
    // 正确! 这种 <div> 的使用是合法的,因为 div 是一个有效的 HTML 标签:
    return <div>Hello {props.toWhat}</div>;
}

function HelloWorld() {
    // 正确!React 知道 <Hello /> 是一个组件,因为它是大写字母开头的:
    return <Hello toWhat="World" />;
}

在运行时选择类型

你不能将通用表达式作为 React 元素类型。如果你想通过通用表达式来(动态)决定元素类型,你需要首先将它赋值给大写字母开头的变量。这通常用于根据 prop 来渲染不同组件的情况下:

import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
    photo: PhotoStory,
    video: VideoStory
};

function Story(props) {
    // 错误!JSX 类型不能是一个表达式。
    return <components[props.storyType] story={props.story} />;
}

要解决这个问题, 需要首先将类型赋值给一个大写字母开头的变量:

import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
    photo: PhotoStory,
    video: VideoStory
};

function Story(props) {
    // 正确!JSX 类型可以是大写字母开头的变量。
    const SpecificStory = components[props.storyType];
    return <SpecificStory story={props.story} />;
}

JSX中行内样式

在HTML中,可以通过style给标签元素添加行内样式:

<h1 class="title" style="color:red;">Hello World!~</h1>

在JSX同样可以使用style来给元素添加样式,但和HTML的使用略有不同,需要以一个对象的方式给style赋值,如下:

<h1 className="title" style={{color: 'red'}}>Hello world!~</h1>

可以通过Babel在线转换工具,把下面的代码转换一下:

{/* JSX 转换前*/}
const element = <h1 className="title" style={{color: 'red'}}>Hello world!~</h1>;

ReactDOM.render(
    <div>
        { element }
    </div>,
    document.getElementById('root')
)

// Babel 转换出来的代码

var element = React.createElement(
    "h1", 
    {
        className: "title",
        style: {
            color: 'red'
        }
    }, 
    "Hello world!~"
);
ReactDOM.render(React.createElement("div", null, element), document.getElementById('root'));

编译后在浏览器中看到的效果和我们平时写HTML并无差异:

在JSX中,可以将上面的代码进行优化,可以声明一个对象,比如titleStyles,然后在需要的地方通过style来引用:

const titleStyle = {
    color: 'red'
}
const element = <h1 className="title" style={ titleStyle }>Hello world!~</h1>;

如果h1中有多个样式时,可以在titleStyle对象中添加你所需要的样式:

const titleStyle = {
    color: 'red',
    backgroundColor: '#604D17',
    padding: '5px 10px',
    display: 'inline-flex',
    'border-radius': '5px'
}
const element = <h1 className="title" style={ titleStyle }>Hello world!~</h1>;

注意,如果CSS的属性带有中折号-时,在JSX中需要将其换成驼峰写法或用引号括起来,比如上面例示中的:

background-color => 换成驼峰写法: backgroundColor
border-radius    => 用引号括起来: 'border-radius'

更建议使用驼峰方式来处理。

JSX中还更强大一些,比如说,可以给CSS属性赋值一个函数值,比如下面示例中的backgroundColor就是一个随机生成的颜色:

let rgb = []
for (var i = 0; i < 3; i++) {
    let r = Math.floor(Math.random() * 256)
    rgb.push(r)
}
const titleStyle = {
    color: 'red',
    backgroundColor: `rgb(${rgb})`,
    padding: '5px 10px',
    display: 'inline-flex',
    'border-radius': '5px'
}
const element = <h1 className="title" style={ titleStyle }>Hello world!~</h1>;

在JSX中,可以给CSS属性添加变量、函数等值

除此之外,在JSX中还可以共享一些样式,比如:

const TodoComponent = {
    width: "300px",
    margin: "30px auto",
    backgroundColor: "#44014C",
    minHeight: "200px",
    boxSizing: "border-box"
}

const Header = {
    padding: "10px 20px",
    textAlign: "center",
    color: "red",
    fontSize: "22px"
}

const ErrorMessage = {
    color: "white",
    fontSize: "13px"
}

const styles = {
    TodoComponent: TodoComponent,
    Header: Header,
    ErrorMessage: ErrorMessage
}

const element = <h1 className="title" style={ styles.Header }>Hello world!~</h1>;

这只是JSX中添加行内样式的使用。这种方式也常常被称为inline style。其实在React中处理CSS的方式还有其他方式,比如Styled ComponentsCSS Modules等。据2019年的CSS 状态报告统计,在React中处理样式(CSS-in-JS)就属Styled Components和CSS Modules占比最高:

如果你对2019年CSS状态报告中提到的CSS特性感兴趣的话,可以阅读《从9102年的CSS状态报告中看CSS特性的使用》一文。

上面提到的仅是在JSX中怎么写行内样式,如果你对React项目中怎么写样式,维护样式方面感兴趣的话,欢迎关注后续的更新,我将会花一篇的篇幅来和大家一起探讨如何在React中撸CSS。

小结

对于撸习惯了HTML或其他模板语言的同学来说,使用JSX还是会略有一点不习惯。但JSX本身并不是模板语言,他是JavaScript的扩展。初次使用JSX可能会存在一定疑惑,虽然该文介绍了一些JSX的使用和具有的特性,以及需要注意的一些小细节。但难免无法解决所有的困惑,如果你和我一样,在使用JSX感到困惑的话,可以花点时间阅读@Brad Westfall的这篇博文,还是蛮有帮助的。如果你在这方面有更多的经验和技巧,欢迎在下面的评论中与我们一起共享。