从 auto-ellipsis 看 React 组件开发

auto-ellipsis 是一个用于解决文本超长溢出截断并加 ... 的 React 组件。

关于 React

随着 React 的火热,随之而来的负面消息也变得更多。之前网上就有人批评说 React 的鼓吹者很多,甚至被定性为『无脑』,这就如同当年批评 jQuery 一样。

React 对我而言,不仅仅是一个前端 View 库,它对我的影响主要有以下几方面:

  1. 拥抱前沿技术 – babel 让我在项目中可以提前使用 ES2015+;webpack-dev-server 和 react-hot-loader 让我的开发过程无比顺畅;webpack 让我的打包上线变得极其方便;redux 让我能更好的管理应用状态。也许你会说这些和 React 没有绝对关系,但事实上,正是 React 的巨大的生态圈活力使得我能够接触并拥抱这些前沿技术;

  2. 享受开发 SPA – 我之前尝试过 Angular,但 React 才是适合我的,我可以自己实践开发 SPA,并且有兴趣去探索相关的技术(比如:构建 universal apps);

  3. 组件化思想 – React 将组件化能够真正用于开发中,实践中才能对组件化思想体会更深;

  4. 前端开发的思考:Flux 的单向数据流的思想,以 FRP 为指导思想的 Redux。这些都让我尝试去思考索前端开发。

下面开始介绍 auto-ellipsis 的开发过程。

CSS 中的 ellipsis

.truncate {
    width: 250px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

老实说,我所遇到的需求,CSS 中的 ellipsis 基本很少能够满足:

  1. 它只针对单行。但实际中更多的是希望在指定宽高的区域自动截断并加 ...

  2. 它不能生成提示信息,比如 title。你不能寄希望于用户从审查元素中获得完整的文本信息。

目前,auto-ellipsis 基本无法优雅地通过 CSS 来实现。但是,仔细想想这个需求原本就不是纯样式上的问题。我们不仅仅希望自适应截断(不管尾部加 ...),还希望有提示信息(tooltip or title),这是一个功能需求,可以封装成一个组件。

如何实现

既然 CSS 无法实现,那就只有依靠 JS 来实现。最简单的想法就是:从后向前不断的裁剪文本,检查文本是否溢出,一旦不溢出,我们就终止这个过程。考虑 <div>content</div>,这个过程主要分为两部分:

  1. 裁剪文本:直接暴力的把 div 节点的 text 节点进行替换(<div>content</div> -> <div>conten</div>);

  2. 检查文本是否溢出:我最先想到的是在 div 元素外面套一层 div,设置外层 div overflow: hidden, 内层 div overflow: visible,外层 div 定宽高,这样比较内外层元素的高度或者相对于视口的 bottom 就好。

显然上面的方法是有效的,但也极其暴力的。首先多套一个 div 就会让人很不爽,于是我们注意到 text 节点也是 dom,可以比较 div 节点和 text 节点吗?可惜 text 节点没办法获得其高度和位置信息。

这时,也许你记得《JavaScript 高级程序设计》中有介绍 Range 这个概念。老实说,我当时看的时候没多大感觉。是的,Range 派上用场了。

Range 属于 dom 对象,通过 Range 可以选择文档中的一个区域,而不必考虑节点的界限。我们可以通过 Range 实现文本的裁剪(比暴力替换文本节点要高效)。 Range 的高度和位置信息可以获取,我们可以通过 getBoundingClientRect() 来获取 div 节点和 Range 相对于视口的 bottom,进行位置比较。而且, Range 的创建对用户透明,这意味着整个裁剪检查的过程 UI 不会有变化。

我们还可以做一些优化:考虑 div 元素的 padding-buttom 和 border-bottom-width;匹配文本减去三个字符用于存放 ...;考虑 word-break ,最终文本截取到空格处(考虑到中文等其他语言,不好实现…)。

React 组件的封装

首先,组件的属性 props 就是组件的对外接口。对于 auto-ellipsis,我们的对外接口包括:tag(组件的标签),content(文本信息),addTitle(截断时是否加 title 属性),styles(自定义样式)。

其次,组件的状态 state 是随着时间而变化的,一般来说基础组件(dumb component)最好是状态无关的,由上层业务组件(smart component)来管理状态。通常,组件状态的改变是由用户交互造成的,所以组件只需要暴露用户交互结束后相应的处理接口(比如:handleClick)就好。

对于 auto-ellipsis,我们基本没有与用户交互(如果元素宽高不是定值,如百分比,那么视口大小变化是会造成影响的,我们这里不考虑这种情形)。实际上我们更多的是对 DOM 的直接操作,那么我们何时重新渲染组件,何时需要重新剪裁文本?

React 对组件生命周期的管理非常强大,我们只需要考虑怎么做比较合适就好。首先,我们需要在组件初始化挂载结束时(componentDidMount,可操作 DOM)尝试裁剪文本;其次,组件更新时,我们需要在组件更新完毕后(componentDidUpdate,可操作 DOM)尝试裁剪文本;最后,我们需要考虑是否要使用 shouldComponentUpdate,这主要是基于性能考虑。我觉得,对于基础组件,考虑到这三点就足够了,任何更复杂的设计只会让你的组件变得不那么通用,甚至引入一些潜藏的 bug。实际大多数情况下,基础组件连 shouldComponentUpdate 都不该使用,因为虚拟 DOM 已经很快了。但是 auto-ellipsis 比较特殊,它的每次更新需要重新操作 DOM,所以还是可以考虑进行优化的。

shouldComponentUpdate(nextProps, nextState) {
    return JSON.stringify(this.props) !== JSON.stringify(nextProps)
}

CSS modules

CSS 模块化一直是组件封装的难题。webpack(style-loader, css-loader) 提供了使用 JS module loader 来加载 CSS 的功能。但这只更多的只是对资源的声明依赖和加载,并不是 CSS 模块化。解决 CSS 模块化要解决:CSS 局部作用域的问题;CSS 模块的输入和输出。

css-modules 通过生成唯一的 className,从工程角度上解决了 CSS 局部作用域的问题。css-modules 的输入和输出都是 JS 对象,这个对象是一系列 local-className: global-className 的映射(注意:输入输出不包含全局样式,可以通过 css-loader?modules 来开启默认局部样式,:global 开头是全局样式)。CSS 模块之间通过 composes 来组合。

React-css-modules 通过 high-order component 的方式将 css-modules 自然地应用于 React component,并且使用 styleName 和 className 来区分 local CSS 和 global CSS。我给 react-css-modules 提了一个 PR,用于解决自定义组件的样式,通过样式的声明顺序(先 import 组件,再 import 自定义 CSS 模块)来确保相同选择器下自定义样式具有更高的优先级(可以使用 css-loader?modules&localIdentName=[local]-[hash:base64:5],这样可以通过 [local] 标识 local-className,方便自定义样式)。注:PR 未通过,作者认为有些 hack,最终实现是可以给组件传递 styles 属性,不过是直接替换默认 styles。那么,如果我想在默认 styles 基础上修改一些样式,则需要在 css-modules 中处理,这部分讨论参见 讨论

import React from 'react'
import ReactDOM from 'react-dom'
import CSSModules from 'react-css-modules'
import styles from './auto-ellipsis.css'

@CSSModules(styles)
export default class AutoEllipsis extends React.Component {
    static propTypes = {
        tag: React.PropTypes.string,
        content: React.PropTypes.string.isRequired,
        addTitle: React.PropTypes.bool,
        styles: React.PropTypes.object,
    }
    render() {
        const props = {
            styleName: 'root',
        }
        const {tag, content} = this.props
        return React.createElement(tag, props, content)
    }
}

关于 CSS 模块化 和 CSS 局域化可具体参考 hax 的 关于前端开发中“模块”和“组件”概念的思考

测试

前端组件的测试,按照宿主一般可分为浏览器环境 和 Node.js 环境。测试框架的话,我推荐 mocha。

浏览器环境可以实际生成 DOM,测试真实有效。可以使用 webpack 配合 mocha-loader,使得测试和开发统一。但是,不方便使用 travis-ci 等一些集成工具。

Node.js 环境下需要模拟 DOM(jsdom),React 组件下可以和 react-addons-test-utils 配合使用。再者,一些涉及到 dom 位置的组件,无法使用模拟测试(比如:jsdom 中的 getBoundingClientRect 返回的都是 0)。

auto-ellipsis 显然依赖于 dom 位置信息,所以采用了浏览器环境测试。

项目地址:https://github.com/ideal-reac…

    原文作者:ustccjw
    原文地址: https://segmentfault.com/a/1190000003782410
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞