从 auto-ellipsis 看 React 组件开发
关于 React
随着 React 的火热,随之而来的负面消息也变得更多。之前网上就有人批评说 React 的鼓吹者很多,甚至被定性为『无脑』,这就如同当年批评 jQuery 一样。 @H_502_6@
React 对我而言,不仅仅是一个前端 View 库,它对我的影响主要有以下几方面: @H_502_6@
-
拥抱前沿技术 - babel 让我在项目中可以提前使用 ES2015+;webpack-dev-server 和 react-hot-loader 让我的开发过程无比顺畅;webpack 让我的打包上线变得极其方便;redux 让我能更好的管理应用状态。也许你会说这些和 React 没有绝对关系,但事实上,正是 React 的巨大的生态圈活力使得我能够接触并拥抱这些前沿技术; @H_502_6@
-
享受开发 SPA - 我之前尝试过 Angular,但 React 才是适合我的,我可以自己实践开发 SPA,并且有兴趣去探索相关的技术(比如:构建universal apps); @H_502_6@
-
组件化思想 - React 将组件化能够真正用于开发中,实践中才能对组件化思想体会更深; @H_502_6@
-
前端开发的思考:Flux 的单向数据流的思想,以 FRP 为指导思想的 Redux。这些都让我尝试去思考索前端开发。 @H_502_6@
下面开始介绍 auto-ellipsis 的开发过程。 @H_502_6@
CSS 中的 ellipsis
.truncate { width: 250px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
老实说,我所遇到的需求,CSS 中的 ellipsis 基本很少能够满足: @H_502_6@
目前,auto-ellipsis 基本无法优雅地通过 CSS 来实现。但是,仔细想想这个需求原本就不是纯样式上的问题。我们不仅仅希望自适应截断(不管尾部加...),还希望有提示信息(tooltip or title),这是一个功能需求,可以封装成一个组件。 @H_502_6@
如何实现
既然 CSS 无法实现,那就只有依靠 JS 来实现。最简单的想法就是:从后向前不断的裁剪文本,检查文本是否溢出,一旦不溢出,我们就终止这个过程。考虑<div>content</div>,这个过程主要分为两部分: @H_502_6@
-
裁剪文本:直接暴力的把 div 节点的 text 节点进行替换(<div>content</div>-><div>conten</div>); @H_502_6@
-
检查文本是否溢出:我最先想到的是在 div 元素外面套一层 div,设置外层 divoverflow: hidden,内层 divoverflow: visible,外层 div 定宽高,这样比较内外层元素的高度或者相对于视口的 bottom 就好。 @H_502_6@
显然上面的方法是有效的,但也极其暴力的。首先多套一个 div 就会让人很不爽,于是我们注意到 text 节点也是 dom,可以比较 div 节点和 text 节点吗?可惜 text 节点没办法获得其高度和位置信息。 @H_502_6@
这时,也许你记得《JavaScript 高级程序设计》中有介绍 Range 这个概念。老实说,我当时看的时候没多大感觉。是的,Range 派上用场了。 @H_502_6@
Range 属于 dom 对象,通过 Range 可以选择文档中的一个区域,而不必考虑节点的界限。我们可以通过 Range 实现文本的裁剪(比暴力替换文本节点要高效)。 Range 的高度和位置信息可以获取,我们可以通过 getBoundingClientRect() 来获取 div 节点和 Range 相对于视口的 bottom,进行位置比较。而且, Range 的创建对用户透明,这意味着整个裁剪检查的过程 UI 不会有变化。 @H_502_6@
我们还可以做一些优化:考虑 div 元素的 padding-buttom 和 border-bottom-width;匹配文本减去三个字符用于存放...;考虑word-break,最终文本截取到空格处(考虑到中文等其他语言,不好实现...)。 @H_502_6@
React 组件的封装
首先,组件的属性 props 就是组件的对外接口。对于 auto-ellipsis,我们的对外接口包括:tag(组件的标签),content(文本信息),addTitle(截断时是否加 title 属性),styles(自定义样式)。 @H_502_6@
其次,组件的状态 state 是随着时间而变化的,一般来说基础组件(dumb component)最好是状态无关的,由上层业务组件(smart component)来管理状态。通常,组件状态的改变是由用户交互造成的,所以组件只需要暴露用户交互结束后相应的处理接口(比如:handleClick)就好。 @H_502_6@
对于 auto-ellipsis,我们基本没有与用户交互(如果元素宽高不是定值,如百分比,那么视口大小变化是会造成影响的,我们这里不考虑这种情形)。实际上我们更多的是对 DOM 的直接操作,那么我们何时重新渲染组件,何时需要重新剪裁文本? @H_502_6@
React 对组件生命周期的管理非常强大,我们只需要考虑怎么做比较合适就好。首先,我们需要在组件初始化挂载结束时(componentDidMount,可操作 DOM)尝试裁剪文本;其次,组件更新时,我们需要在组件更新完毕后(componentDidUpdate,可操作 DOM)尝试裁剪文本;最后,我们需要考虑是否要使用 shouldComponentUpdate,这主要是基于性能考虑。我觉得,对于基础组件,考虑到这三点就足够了,任何更复杂的设计只会让你的组件变得不那么通用,甚至引入一些潜藏的 bug。实际大多数情况下,基础组件连 shouldComponentUpdate 都不该使用,因为虚拟 DOM 已经很快了。但是 auto-ellipsis 比较特殊,它的每次更新需要重新操作 DOM,所以还是可以考虑进行优化的。 @H_502_6@
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 模块的输入和输出。 @H_502_6@
css-modules通过生成唯一的 className,从工程角度上解决了 CSS 局部作用域的问题。css-modules 的输入和输出都是 JS 对象,这个对象是一系列local-className: global-className的映射(注意:输入输出不包含全局样式,可以通过css-loader?modules来开启默认局部样式,:global 开头是全局样式)。CSS 模块之间通过 composes 来组合。 @H_502_6@
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 中处理,这部分讨论参见讨论。 @H_502_6@
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 的关于前端开发中“模块”和“组件”概念的思考。 @H_502_6@
测试
前端组件的测试,按照宿主一般可分为浏览器环境 和 Node.js 环境。测试框架的话,我推荐 mocha。 @H_502_6@
浏览器环境可以实际生成 DOM,测试真实有效。可以使用 webpack 配合 mocha-loader,使得测试和开发统一。但是,不方便使用 travis-ci 等一些集成工具。 @H_502_6@
Node.js 环境下需要模拟 DOM(jsdom),React 组件下可以和 react-addons-test-utils 配合使用。再者,一些涉及到 dom 位置的组件,无法使用模拟测试(比如:jsdom 中的 getBoundingClientRect 返回的都是 0)。 @H_502_6@
auto-ellipsis 显然依赖于 dom 位置信息,所以采用了浏览器环境测试。 @H_502_6@
项目地址:https://github.com/ideal-react/auto-ellipsis。 @H_502_6@