现代 CSS

如何编写出优雅的JSX

特别声明:如果您喜欢小站的内容,可以点击申请会员进行全站阅读。如果您对付费阅读有任何建议或想法,欢迎发送邮件至: airenliao@gmail.com!或添加QQ:874472854(^_^)

前段时间在《深入了解JSX》一文中整理了JSX相关的知识和细节。作为一名Web开发者,不管写纯HTML模板代码还是现在在JSX中写JavaScript和HTML混合的代码,我们都应该有追求,那就是怎么写出更优雅、更具可读性和更易于维护的代码。那么在这篇文章中我们就来一起聊聊如何写出更优雅的JSX代码。

HTML语义化和可访问性

HTML语义化有一个专有术语 Semantic HTML(也有人常称其为Semantic Markup)。其主要目的是使用HTML标记来增强Web页面和Web应用程序中信息的语义或含义,而不仅仅是定义其表示形式或外观。

在不同的时间,Web开发者都对语义化有着极大的诉求,也在致力于写出更具语义化的模板。但也有一些开发者在这方面欠缺一定的能力。比如下图所示,是最为明显的对比(无语义化 vs. 有语义化):

刚才也提到过了,在不同的时期,在构建语义化模板时也有所差异,比如HTML4和HTML5:

特别是在HTML5的时代,面对这么多语义化的标签,我们应该如何选择呢?在HTML5 Doctor上提供了一张图,有助于我们如何选择具有语义化的标签:

语义化HTML除了让你的代码变得更为优雅之外,还令你的代码更具可读性,除此之外还能有利于搜索引擎的识别,有利于Web页面的SEO。

另外,语义化HTML和可访问的富交互互联网应用程序(ARIA)的结合为你的内容添加了基本的含义,从而使Web浏览器、搜索引擎、屏幕阅读器、RSS阅读器和最终用户都能够理解。而且可访问性是Web开发过程中首要的思想和基础。除了兼容不同的终端,开发人员有义务让你的用户能通过别的方式(比如键盘,屏幕阅读器等)访问你的网站。随着HTML、CSS和JavaScript的学习,可访问性是一个重要的术语,它并不仅适用于社会的特定人群,而是适用于我们所有人。

如果你对HTML语义化和可访问性方面感兴趣的话,还可以阅读下面这些文章:

React中也会有语义化和可访问性的需求

时至今日,开发Web页面或Web应用不再局限于HTML、JavaScript和CSS了。比如说基于Vue或React框架体系上来开发Web页面或Web应用程序。但这只是改变了开发方式,但有一些基本的诉求或者说基本要求是不会变的。就拿React环境下来说吧。React官方都有专门的内容介绍如何在React环境上开发更具可访问性的应用

除了React官方有这方面的介绍,就在社区也有这方面的诉求。比如@stephaniecodes绘制的手绘图就足以说明:

偶尔发现,在Twitter的 #sketchnote话题下面有很多优秀的手绘稿,感兴趣的可以看看。

相关方面的话题还可以阅读:

在React环境中开发Web应用一般情况之下都是基于JSX来编写模板。接下来回到我们今天的主话题中来:如何编写优雅的JSX?针对这个主题我们分为两个部分来阐述。

语义化JSX

JSX语法提供一个抽象层,这对基于组件系统非常有用。

使用JSX来封装UI片段可以更容易地组合更大的系统,但它也隐藏了应用程序的“骨架”结构。而JSX简单的理解呢就是带有一定逻辑的HTML(在HTML上新增了JavaScript)。前面我们也提到过,在编写HTML的时候都提倡要尽可能的编写带有语义化的HTML和具有可访问性的HTML。那么在React框架体系下,使用JSX进行模板开发时,我们也应该尽可能的编写具有语义化和可访问性的JSX代码。

编写具有语义化和可访问性的JSX和编写具有语义化和可访问性的HTML基本上无较大的差异。比如我们在使用哪个元素时,其重要的原则是:不应该仅依赖于视觉外观。因为任何东西都可以有不同的视觉外观。也就是说,如果我们根据行为和意义来选择元素,那么会更贴合于我们实际。换句话说,根据行为和意义来选择元素,可以编写出更具语义化的模板,不管是JSX还是HTML。

稍微有点历史的Web开发者都应该记得每年的4月9日是CSS的裸奔节(CSS naked day),简单地说,就是让你的网站完全脱去CSS的外衣,将身体彻底的裸露给你的用户:

这也是验证你的Web页面是否具有可读性的最佳验证方式之一。

This is a fun idea, fully in line with the reasons for creating CSS in the first place. While most designers are attracted by the extra presentational capabilities, saving HTML from becoming a presentational language was probably a more important motivation for most people who participated in the beginning. The idea behind this event is to promote Web Standards. Plain and simple. This includes proper use of (x)html, semantic markup, a good hierarchy structure, and of course, a good 'ol play on words. It's time to show off your <body>.

当然,如果你想以最快的方式验证你的网站是否具有较好的可读性,可以尝试通地devtools,禁用页面中所有的CSS。

这可能对于如何编写出具有语义化的JSX看上去没有太多的关系。不过为了能更好的阐述如何编写出一个具有语义化的JSX,我们先来看一个小示例。使用Google搜索猫的图片,在顶部会有一个对猫的类型的一个过滤器:

如果把这些选项定义为一个<Tag />组件,如果是你,你会选择使用什么元素来构建这个组件呢?就该示例而言,单击标记会将你带到另一个新的页面:

那么你可以把它认为是一个<a>链接。另外,你也看到了,用户可以选择多个标签,看上去更像是一个<input type="checkbox" />

用户可以点击label,实际上也对checkbox进行了选择切换(只不过使用CSS将checkbox进行隐藏),比如下面这样的一个示例:

对于这两种情况,使用相同的<Tag />组件可能很有吸引力。为了让它更趋于中立,或许在构建该组件的时候会采用一些中立(没有语义化的HTML标签)span或者div,也可以是HTML5的section,并通过props传递一个click事件函数来处理不同的行为。只不过这样做的话就脱离了语义化。事实上,我们不管是在构建HTMl模板还是在构建JSX模板,采用正确的标记时,浏览器可以帮助我们做更多的事情,而且这些事情是免费的。简单的就是搜索引擎、屏幕阅读器等是非常友好的。

事实上在编写JSX模板时,我们也可以按照HTML的语义化标准来构建。简单地说,一切适合于HTML中的语义化标签或规则都适用于JSX模板。

不管是在什么样的JavaScript框架下,构建组件的主要目的之一就是利用组件可重用性。而在React中,我们在构建组件时应该最大化的利用好(使用好)其props的能力。比如上面所说的<Tag />组件,我们完全可以借助props的能力,有条件地决定使用哪个HTML元素。比如,通过props<Tag />组件传递的是href={url}时,则采用<a>标签;而给<Tag />传递的是value={id}时,则采用<input type="checkbox" />标签。这样一来,视觉上我们保持一致,但其上下文的语义则达到我们实际所需要的含义。

其实类似这样的场景很多,而且不局限于JSX中。就好比社区中有关于button<a>以及其他可点击元素的争议:

如果你对这方面的讨论感兴趣的话,可以花点时间阅读下面这些文章:

事实上,我们很多人都不会考虑这个问题,为了中立直接就使用div这样的标签来构建链接或按钮。在这里,再次强调一下,我们不应该仅依赖于视觉外观,更应该根据实际意义来决定使用什么标签更适合。

在JSX中构建模板时,除了在合适的地方使用合适的标签元素之外以及利用好React的props特性之外,还有一个特性我们不应该遗忘。那就是React的<React.Fragment>特性

<React.Fragment>还没出来之前,如果要给某节点同时渲染多个元素节点时需要额外增加一个标签,比如div之类的标签,用来包裹多个标签。该标签出来之后就没有必要再额外的引入其他标签了。<React.Fragment>帮助你在不向DOM添加额外节点的情况下对子列表进行分组。他实际上就像创建了一个虚拟DOM,帮助子组件向父组件导入时更具语义化。我们来看一个示例,比如在<table><tr>引入多个td。以前的做法像下面这样:

// Table Component
class Table extends React.Component {
    render() {
        return (
            <table>
                <tr>
                    <Columns />
                </tr>
            </table>
        );
    }
}

// Columns Component
class Columns extends React.Component {
    render() {
        return (
            <div>
                <td>Hello</td>
                <td>World</td>
            </div>
        );
    }
}

为了避免无效的HTML错误,<Columns />在渲染时需要返回多个<td>时,需要额外添加一个<div>容器。最终输出的结果如下:

<table>
    <tr>
        <div>
            <td>Hello</td>
            <td>World</td>
        </div>
    </tr>
</table>

如果从语义化上来说的话,这样的结构并不是一个好的结构。有了<React.Fragment>之后,我们可以像下面这样写:

// Table Component
const Table = (props) => {
    return (
        <table>
            <tr>
                <Columns />
            </tr>
        </table>
    )
}

// Columns Component
const Columns = (props) => {
    return (
        <React.Fragment>
            <td>Hello</td>
            <td>World</td>
        </React.Fragment>
    )
}

最终渲染的结果如下:

<table>
    <tr>
        <td>Hello</td>
        <td>World</td>
    </tr>
</table>

这才更适合于表格的结构。

<React.Fragment>还有更简洁的一种书写方式,就用一对尖括号<></>

在Web可访问性方面除了使用具有语义化的HTML标签之外,还有另一个部分,那就是焦点管理。众所周知,在HTML中有很多元素是具有隐藏的焦点特性,比如说表单控件(如input),链接,锚点等,但很多元素是没有焦点特性,比如p。而焦点状态的管理好坏直接决定了你的Web站点或者Web应用是否具有较好的操作(比如使用键盘顺序的浏览你的Web应用)。同时决定网站可访问性的好坏。因此,社区中有很多文章在聊Web应用中的焦点状态应该如何来管理,才能更利用Web应用具有最佳的可访问性。如果你感兴趣,可以花点时间阅读下面这些文章:

在React中,我们可以利用好Ref来帮助我们管理好元素焦点状态。使用Ref允许我们在React中选择并引用一个实际的DOM节点。比如:

<div
    ref ={
        (loadingNames)=> {
            this.loadingNames = loadingNames;
        }
    }
    tabIndex = "-1"
>Loading List of Names...</div>

上面的代码将divref赋值给this.loadingNames这个类属性。然后使用生命周期componentDidMount像下面这样调用ref元素的焦点元素:

componentDidMount(){
    this.loadingNames.focus()
}

它的作用是,当名称列表加载时,键盘焦点指示器会在风容上放一个焦点环。

使用ref的另一个用例是确保使用react-router时将焦点转移到新页面。使用方法是页面顶部调用ref,并使用户从<link>链接到新页面顶部导航。

<div
    ref={
        (topOfNewPage)=>{
            this.topOfNewPage = topOfNewPage;
        }
    }
    tabIndex = "-1"
    aria-labelledby = "pageHeading"
>
    <Header / >
        <h1 id ="pageHeading"> </h1>
    <Footer/>
</div>

ref使用像下面这样:

componentDidMount(){
    this.topOfNewPage.focus()
}

React v16.8版本还提供了React.createRef() API,具体使用可以参阅官方文档

有关于refReact.createRef()更多的介绍可以阅读下面这些文章:

如果想提高整个团队在编写JSX模板时都更趋于编写具有语义化和可访问性,可以借助Lint工具来完成。比如eslint-plugin-jsx-a11y

尽可能的和逻辑解耦

HTML是一种声明式语言,具有较强的可读性。JSX和HTML略有不同,他将JavaScript逻辑和HTML结合在一起,提高了模板能力的同时也增加了阅读性的障碍,特别是逻辑和模板耦合的过于紧密或复杂的时候。另外,随着组件的复杂度提高,JSX会变得更难以理解。如果我们想避免JSX中的混乱或降低代码的复杂度,就需要尽可能的将逻辑和模板解耦。这样一来能让你的JSX代码变得更具可读性和可维护性。同时也就编写出更优雅的JSX代码。

为了更好的向大家如何做到逻辑和模板尽可能的解耦,我们将通过一个简单的列表示例来和大家一起探讨和学习这方面的技巧。

我们需要完成的效果大致如下:

特别声明:该示例的思路来自于@verekia的《Logic-less JSX》一文。

要是用HTML来写的话,其结构如下:

<!-- HTML -->
<ul>
    <li>
        <a href="url/animal/1">Dog</a>: 4 legs(Friendly)
    </li>
    <li>
        <a href="url/animal/2">Bird</a>: 2 legs
    </li>
    <li>
        <a href="url/animal/3">Snake</a>: 0 legs(Unfriendly)
    </li>
    <li>
        <a href="url/animal/4">Centipede</a>: ? legs(Not enough data!)
    </li>
</ul>

假设我们从服务端拿到了下面这样一个data

const data = [
    { 
        id: 1, 
        name: 'Dog', 
        legCount: 4, 
        isFriendly: true 
    },
    { 
        id: 2, 
        name: 'Bird', 
        legCount: 2 
    },
    { 
        id: 3, 
        name: 'Snake', 
        legCount: 0, 
        isFriendly: false 
    },
    { 
        id: 4, 
        name: 'Centipede' 
    }
]

给我们的第一直觉就是应该创建一个组件,并把data中的idnamelegCountisFriendly作为props传递给该组件。比如我们创建一个Animal组件,那么代码可能会像下面这样:

// Animal Component
const Animal = ({id, name, legCount, isFriendly}) => {
    return (
        <li>
            <a href={`url/animal/${id}`}>{name}</a>: {_.toString(legCount) || '?'} legs
            {isFriendly !== undefined && `(${isFriendly ? 'Friendly' : 'Unfriendly'})`}
            {legCount === undefined && isFriendly === undefined && '(Not enough data!)'}
        </li>
    )
}

组件Animal只是创建了一个li。如果我们需要达到示例所显示的列表效果,我们还需要创建另一个组件,比如Animals组件,该组件同样传了一个itemsprops

// Animals Component
const Animals = ({items}) => {
    return (
        <ul>
            {
                items.map(item => <Animal id={item.id} name={item.name} legCount={item.legCount} isFriendly={item.isFriendly} />)
            }
        </ul>
    )
}

这里使用了数组的.map(),对items进行遍历。然后通过将Animals组件挂载到div#app容器中:

ReactDOM.render(<Animals items={data} />, document.getElementById('app'))

这个时候渲染出来的结果如下:

注意:上面的示例使用了Lodash_.toString()方法,可以将legCount这样的数字转换为字符串,还可以避免当它的值为0时出现意外错误。

在《学习React应该具备的JavaScript基础知识》一文中有我们有介绍到,数组的.map().filter().reduce()方法在JSX中可以让我们轻易构建一个列表。

这个示例中,将要用到的JavaScript逻辑和HTML混合在一起。是不是看起来非常的蛋疼(而且该组件算是一个很简单,很小的组件了,要是换成一个更复杂的组件,可想而知,会是什么一个情景)。如果我们把逻辑部分分离出来,转而使用以下这种声明性更强的JSX,是不是会更好一些呢?

// Animal Component
const Animal = ({ id, name, legCount, isFriendly }) => {
const url = `url/animal/${id}`
const legCountStr = _.toString(legCount) || '?'
const hasNotEnoughData = legCount === undefined && isFriendly === undefined
    return (
        <li>
            <a href={url}>{name}</a>: {legCountStr} legs
            {isFriendly !== undefined &&
                `(${isFriendly ? 'Friendly' : 'Unfriendly'})`}
            {hasNotEnoughData && '(Not enough data!)'}
        </li>
    )
}

那么我们应该在哪个地方转换数据更为合适呢?这里归纳了四种常见的方式,他们都有各自的优缺点,所以取决于的使用场景。

变量声明的方式

通过显式的声明一些变量,将JSX中带有逻辑的部分代码分离出来。正如上面示例所示:

// Animal Component
const Animal = ({ id, name, legCount, isFriendly }) => {
const url = `url/animal/${id}`
const legCountStr = _.toString(legCount) || '?'
const hasNotEnoughData = legCount === undefined && isFriendly === undefined
    return (
        <li>
            <a href={url}>{name}</a>: {legCountStr} legs
            {isFriendly !== undefined &&
                `(${isFriendly ? 'Friendly' : 'Unfriendly'})`}
            {hasNotEnoughData && '(Not enough data!)'}
        </li>
    )
}

优点

  • 易于理解,并且不需要引入任何新的概念
  • 逻辑在同一个地方,可访问性更清晰

缺点

  • 如果有很多逻辑,函数体会变得相当得大

拆分成多个组件

虽然上面的示例,我们也使用了多个组件,AnimalAnimals组件。不过我们可以将其优化得更为合理一些,其中一个组件只做模板的事情,另一个组件做一些更智能的事情,比如状态的处理。为了更好的区分,建议在只做模板的组件后面添加Cmp后缀。那么上面的示例,我们可以修改成:

const data = [
    { id: 1, name: 'Dog', legCount: 4, isFriendly: true },
    { id: 2, name: 'Bird', legCount: 2 },
    { id: 3, name: 'Snake', legCount: 0, isFriendly: false },
    { id: 4, name: 'Centipede' }
]

const AnimalCmp = ({ name, url, legCount, friendliness, hasNotEnoughData }) => {
    return (
        <li>
            <a href={url}>{name}</a>:{legCount} legs
            {friendliness && `(${friendliness})`}
            {hasNotEnoughData && '(Not enough data!)'}
        </li>
    )
}

const Animals = ({items}) => {
    return (
        <ul>
            {
                items.map(item => <AnimalCmp
                    name={item.name}
                    url={`url/animal/${item.id}`}
                    legCount={_.toString(item.legCount) || '?'}
                    friendliness={{ true: 'Friendly', false: 'Unfriendly' }[item.isFriendly]}
                    hasNotEnoughData={item.legCount === undefined && item.isFriendly === undefined}
                />)
            }
        </ul>
    )
}

ReactDOM.render(<Animals items={data} />, document.getElementById('app'))

优点

  • 隔离了Cmp组件,只用于无逻辑的渲染
  • 清晰地分离了关注点,可以将两个组件放在两个不同的文件夹中

缺点

  • 一开始会令人感到困惑
  • 为组件增加了一个额外的级别

注意:如果组件有一个状态,建议将该状态放在“转换”(Transforming)组件中,而不是放在Cmp组件中,让后者不受任何逻辑的约束

函数

你也可以尝试着将逻辑部分提取到一个函数中。像下面这样:

const data = [
    { id: 1, name: 'Dog', legCount: 4, isFriendly: true },
    { id: 2, name: 'Bird', legCount: 2 },
    { id: 3, name: 'Snake', legCount: 0, isFriendly: false },
    { id: 4, name: 'Centipede' }
]

const getAnimalRenderData = ({ id, name, legCount, isFriendly }) => ({
    name,
    url: `url/animal/${id}`,
    legCountStr: _.toString(legCount) || '?',
    friendliness: { true: 'Friendly', false: 'Unfriendly' }[isFriendly],
    hasNotEnoughData: legCount === undefined && isFriendly === undefined,
})

const Animal = props => {
    const {
        name,
        url,
        legCountStr,
        friendliness,
        hasNotEnoughData,
    } = getAnimalRenderData(props)

    return (
        <li>
            <a href={url}>{name}</a>: {legCountStr} legs
            {friendliness && `(${friendliness})`}
            {hasNotEnoughData && '(Not enough data!)'}
        </li>
    )
}

const Animals = ({items}) => {

    return (
        <ul>
            {
                items.map(item => <Animal id={item.id} name={item.name} legCount={item.legCount} isFriendly={item.isFriendly} />)
            }
        </ul>
    )
}

ReactDOM.render(<Animals items={data} />, document.getElementById('app'))

这个版本是前两个版本的混合体。它比引入一个新组件更简单,但是没有提供隔离无逻辑JSX代码的选项,而且函数不能保存状态。但是,它的特殊之处是与前面的方法不同,这个函数可以在React上下文之外使用。

正如你所看到的,如查我们解构渲染数据,代码会过长,我们可能不得不将每个属性放在一行上,这将占用大量空间。

这种方式和前面的函数方式非常类似。只不过他使用类的方式来构建我们的组件。代码可能像下面这样:

const data = [
    { id: 1, name: 'Dog', legCount: 4, isFriendly: true },
    { id: 2, name: 'Bird', legCount: 2 },
    { id: 3, name: 'Snake', legCount: 0, isFriendly: false },
    { id: 4, name: 'Centipede' }
]

class AnimalRenderData {
    constructor(data) {
        this.data = data
    }
    get name() {
        return this.data.name
    }
    get url() {
        return `url/animal/${this.data.id}`
    }
    get legCountStr() {
        return _.toString(this.data.legCount) || '?'
    }
    get friendliness() {
        return { true: 'Friendly', false: 'Unfriendly' }[this.data.isFriendly]
    }
    get hasNotEnoughData() {
        return this.data.legCount === undefined && this.data.isFriendly === undefined
    }
}

const Animal = props => {
    const {
        name,
        url,
        legCountStr,
        friendliness,
        hasNotEnoughData,
    } = new AnimalRenderData(props)

    return (
        <li>
            <a href={url}>{name}</a>: {legCountStr} legs
            {friendliness && `(${friendliness})`}
            {hasNotEnoughData && '(Not enough data!)'}
        </li>
    )
}

const Animals = ({items}) => {

    return (
        <ul>
            {
                items.map(item => <Animal id={item.id} name={item.name} legCount={item.legCount} isFriendly={item.isFriendly} />)
            }
        </ul>
    )
}

ReactDOM.render(<Animals items={data} />, document.getElementById('app'))

将样式从JSX中剥离出来

在React中编写CSS的方式有很多种,在《React中编写CSS的姿势》一文中有过这方面的探讨。可能常见的方式有:

  • Inline CSS
  • CSS-in-JS
  • CSS Modules
  • Styled Components

具体的讨论不在这里重复阐述。如果你感兴趣的话,可以阅读该文。我们还是回到JSX中来。

在编写JSX模板的时候,我们也应该像编写逻辑一样,尽可能的从JSX中分离出来。分离出来的目的是尽可能的让你的JSX代码保持干净。换句话说,在JSX中使用CSS-in-JS、CSS Modules和Styled Components等方式都可以很好的帮助你将CSS从JSX中分离出来。同样的,不同的方式有其利弊,建议您根据自己的环境选择最为合适的方式。

其他方式

上面我们聊到仅仅是帮助你构建具有语义化和简洁的JSX代码的部分技巧。事实上,还有很多方式可以帮助我们更好的构建更为轻便和清晰的JSX代码。比如下面这几篇文章中介绍的内容都非常的有用。感兴趣的同学可以花点时间阅读:

小结

我们从如何构建一个语义化和具有可访问性的HTML话题为切入点,然后和大家一起探讨了在构建JSX的时候应该怎么去写一个具有语义化和可访问性的JSX。接着以渲染一个列表的案例为基础,和大家一起聊了聊应该怎么样把逻辑代码从JSX模板中剥离出来,让整个JSX代码更为干净和清晰。从而达到更具阅读和维护的JSX。即编写出优雅的JSX代码

除了文章中提到的一些点之外,编写优雅或者说更具阅读性的JSX代码还有很多技巧。比如怎么将一些新的JavaScript运用进来编写我们的代码,让代码变得更可阅读;比如说怎么编写更干净的代码等等。

如果你在这方面有一些较好的经验,欢迎在下面的评论中与我们一起分享。

扩展阅读

返回顶部