记忆中唯独就wwWwww.7777yyyy.com最新06com缺失了,调整入口的www.7777yyyy.com最新06那一段

温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!&&|&&
乐于助人,积极向上!呵呵!
LOFTER精选
网易考拉推荐
用微信&&“扫一扫”
将文章分享到朋友圈。
用易信&&“扫一扫”
将文章分享到朋友圈。
阅读(5017)|
用微信&&“扫一扫”
将文章分享到朋友圈。
用易信&&“扫一扫”
将文章分享到朋友圈。
历史上的今天
loftPermalink:'',
id:'fks_094',
blogTitle:'06 年的一中高考03',
blogAbstract:'\r\n\r\n\r\n601\r\n57\r\n刘中华\r\n烟台大学\r\n\r\n602\r\n01\r\n刘中元\r\n中国协和医科大学\r\n\r\n603\r\n87\r\n刘宗尚\r\n电子科技大学\r\n\r\n604\r\n07\r\n柳夫虎\r\n厦门大学\r\n\r\n605\r\n47\r\n龙科技\r\n山东大学\r\n\r\n606\r\n29\r\n龙文超\r\n山东大学威海分校\r\n\r\n607\r\n99\r\n卢佳龙\r\n山东科技大学泰山科技学院\r\n\r\n',
blogTag:'学校',
blogUrl:'blog/static/',
isPublished:1,
istop:false,
modifyTime:7,
publishTime:8,
permalink:'blog/static/',
commentCount:4,
mainCommentCount:4,
recommendCount:0,
bsrk:-100,
publisherId:0,
recomBlogHome:false,
currentRecomBlog:false,
attachmentsFileIds:[],
groupInfo:{},
friendstatus:'none',
followstatus:'unFollow',
pubSucc:'',
visitorProvince:'',
visitorCity:'',
visitorNewUser:false,
postAddInfo:{},
mset:'000',
remindgoodnightblog:false,
isBlackVisitor:false,
isShowYodaoAd:false,
hostIntro:'乐于助人,积极向上!呵呵!',
hmcon:'0',
selfRecomBlogCount:'0',
lofter_single:''
{list a as x}
{if x.moveFrom=='wap'}
{elseif x.moveFrom=='iphone'}
{elseif x.moveFrom=='android'}
{elseif x.moveFrom=='mobile'}
${a.selfIntro|escape}{if great260}${suplement}{/if}
{list a as x}
推荐过这篇日志的人:
{list a as x}
{if !!b&&b.length>0}
他们还推荐了:
{list b as y}
转载记录:
{list d as x}
{list a as x}
{list a as x}
{list a as x}
{list a as x}
{if x_index>4}{break}{/if}
${fn2(x.publishTime,'yyyy-MM-dd HH:mm:ss')}
{list a as x}
{if !!(blogDetail.preBlogPermalink)}
{if !!(blogDetail.nextBlogPermalink)}
{list a as x}
{if defined('newslist')&&newslist.length>0}
{list newslist as x}
{if x_index>7}{break}{/if}
{list a as x}
{var first_option =}
{list x.voteDetailList as voteToOption}
{if voteToOption==1}
{if first_option==false},{/if}&&“${b[voteToOption_index]}”&&
{if (x.role!="-1") },“我是${c[x.role]}”&&{/if}
&&&&&&&&${fn1(x.voteTime)}
{if x.userName==''}{/if}
网易公司版权所有&&
{list x.l as y}
{if defined('wl')}
{list wl as x}{/list}pure renderpure render阿里数据中台前端团队分享前端相关经验关注专栏更多最新文章{&debug&:false,&apiRoot&:&&,&paySDK&:&https:\u002F\u002Fpay.zhihu.com\u002Fapi\u002Fjs&,&wechatConfigAPI&:&\u002Fapi\u002Fwechat\u002Fjssdkconfig&,&name&:&production&,&instance&:&column&,&tokens&:{&X-XSRF-TOKEN&:null,&X-UDID&:null,&Authorization&:&oauth c3cef7c66aa9e6a1e3160e20&}}{&database&:{&Post&:{&&:{&title&:&重新设计 React 组件库&,&author&:&wei-chang-ran&,&content&:&\u003Cp\u003E在 react + redux 已经成为大部分前端项目底层架构的今天,让我们再回到软件工程界一个永恒的问题上来,那就是如何提升一个开发团队的开发效率?\u003Cbr\u003E从宏观的角度来讲,只有对具体业务的良好抽象才能真正提高一个团队的开发效率,又囿于不同产品所面临的不同业务需求,当我们抽丝剥茧般地将一个个前端项目抽象到最后一层,那么剩下的就只有按钮、输入框、对话框、图标等这些毫无业务意义的纯 UI 组件了。\u003C\u002Fp\u003E\u003Cp\u003E选择或开发一套适合自己团队使用的 UI 组件库应该是每一个前端团队在底层架构达成共识后下一件就要去做的事情,那么我们就以今天为始,分别从以下几个方面来探讨如何构建一套优秀的 UI 组件库。\u003C\u002Fp\u003E\u003Ch2\u003E第一个问题:选择开源 vs 自己造轮子\u003C\u002Fh2\u003E\u003Cp\u003E在 React 界,优秀且开源的 UI 组件库有很多,国外的如 \u003Ca href=\&http:\u002F\u002Fwww.material-ui.com\u002F\&\u003EMaterial-UI\u003C\u002Fa\u003E,国内的如 \u003Ca href=\&https:\u002F\u002Fant.design\u002F\&\u003EAnt Design\u003C\u002Fa\u003E,都是经过众多使用者检验,组件丰富且代码质量过硬的组件库。所以当我们决定再造一套 UI 组件库之前,不妨先尝试下这些在 UI 组件库界口碑良好的标品,再决定是否要进入这个看似简单实则困难重重的领域。\u003C\u002Fp\u003E\u003Cp\u003E在这里,我们并不会去比较任何组件库之间的区别或优劣,但却可以从产品层面给出几个开发自有组\u003Cbr\u003E件库的判断依据,以供参考。\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E产品有独立的设计规范,包括但不限于组件样式、交互模式。\u003C\u002Fli\u003E\u003Cli\u003E产品业务场景较为复杂,需要深度定制某些常用组件。\u003C\u002Fli\u003E\u003Cli\u003E前端团队需要同时支撑多条业务线。\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Ch2\u003E设计思想:规范 vs. 自由\u003C\u002Fh2\u003E\u003Cp\u003E在选择了自己造轮子这样一条路之后,下一个摆在面前的艰难选择就是,要造一个规范的组件库还是一个自由的组件库?\u003C\u002Fp\u003E\u003Cp\u003E规范的组件库可以从源码层面保证产品视觉、交互风格的一致性,也可以很大程度上降低业务开发的复杂度,从而提升团队整体的开发效率。但在遇到一些看似相似实则不同的业务需求时,规范的组件库往往会走入一个难以避免的死循环,那就是实现 A 需求需要使用 a 组件,但是现有的 a 组件又不能完全支持 A 需求。\u003C\u002Fp\u003E\u003Cp\u003E这时摆在工程师面前的就只有两条路:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E重新开发一个完美支持 A 需求的 a+ 组件\u003C\u002Fli\u003E\u003Cli\u003E修改 a 组件源码使其支持 A 需求\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E方法一费时费力,会极大地增加本次项目的开发成本,而方法二又会导致 a 组件代码膨胀速度过快且逻辑复杂,极大地增加组件库后期的维护成本。\u003C\u002Fp\u003E\u003Cp\u003E在多次陷入上面所描述的这个困境之后,在最近的一次内部组件库重构时,我们选择了拥抱自由,这其中既有业务方面的考虑,也有 React 在组件自由组合方面的天然优势,让我们来看一个例子。\u003C\u002Fp\u003E\u003Ch2\u003ESelect\u003C\u002Fh2\u003E\u003Ccode lang=\&js\&\u003E\u002F\u002F traditional select\n&div className={dropdownClass}&\n
className={`${baseClassName}-control ${disabledClass}`}\n
onMouseDown={this.handleMouseDown.bind(this)}\n
onTouchEnd={this.handleMouseDown.bind(this)}\n
&span className={`${baseClassName}-arrow`} \u002F&\n
&\u002Fdiv&\n
{menu}\n&\u002Fdiv&\u003C\u002Fcode\u003E\u003Cp\u003E这是一个非常传统的 Select 组件,触发下拉菜单的区域为一段文字加一个箭头。我们来看下面的一个业务场景:\u003C\u002Fp\u003E\u003Cimg src=\&v2-44f5e7cf112298ddddaccaf2.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&956\& data-rawheight=\&622\&\u003E\u003Cp\u003E这里触发下拉菜单的区域不再是传统的一段文字加一个箭头,而是一个自定义元素,点击后展开下拉列表。虽然它的交互模式和 Select 一模一样,但因为二者在 DOM 结构上的巨大差别,导致我们无法复用上面的这个 Select 来实现它。\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003E\u002F\u002F Customizeable Select\n&div {...filterProps} className={classes} onClick={::this.handleInnerClick}&\n
children\n
&span className={`${prefixCls}-container`}&\n
{label ? &span className={`${prefixCls}-container-label`}&{label}&\u002Fspan& : null}\n
&span className={`${prefixCls}-container-value`} style={valueStyle}&\n
{currentValue !== '' ? currentValue : selectPlaceholder}\n
&\u002Fspan&\n
&\u002Fspan&\n
&Icon className={iconClasses} name=\&angle-down\& \u002F&\n
&\u002Fspan&\n
{this.renderPopup()}\n&\u002Fdiv&\u003C\u002Fcode\u003E\u003Cp\u003E在支持传统的文字加箭头之外,更自由的 Select 添加了对 label 及 children 支持,分别可以对应有名称的 Select\u003C\u002Fp\u003E\u003Cimg src=\&v2-6e04de3fb7f6eb94faebb1e90bec15b5.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&372\& data-rawheight=\&666\&\u003E\u003Cp\u003E及类似前面提到的自定义元素。\u003C\u002Fp\u003E\u003Ch2\u003EDropdown\u003C\u002Fh2\u003E\u003Cp\u003E类似的还有 Select 的孪生兄弟 Dropdown。\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003E\u002F\u002F Customizeable Dropdown\n&div {...filterProps} className={classes}&\n
{data.map((value, idx) =& {\n
return (\n
&ItemComponent\n
data={value} key={idx} datas={data}\n
className={itemClasses}\n
onClick={onSelect.bind(null, value, idx)}\n
onMouseOver={onMouseOver.bind(null, value, idx)}\n
})}\n&\u002Fdiv&\n\n\u002F\u002F Using Dropdown\nconst demoData = [{ text: 'Robb Stark', age: 36 }]\nconst DropdownItem = (props) =& (\n
&div {...props}&\n
&div&{props.data.text}&\u002Fdiv&\n
&div&is {props.data.age} years old.&\u002Fdiv&\n
&\u002Fdiv&\n);\u003C\u002Fcode\u003E\u003Cp\u003E这是一个常见的下拉列表组件,是否允许用户传入 ItemComponent 其实就是一个规范与自由之间的取舍。在选择了拥抱自由之后,组件的使用者终于不会再被组件内部的 DOM 结构所束缚,转而可以自由地定制子元素的 DOM 结构。\u003C\u002Fp\u003E\u003Cp\u003E相较于传统的规范的组件,自由的组件需要使用者在业务项目中多写一些代码,但如果我们往深处再看一层,这些特殊的下拉元素本就是属于某个业务所特有的,将其放在业务代码层恰恰是一种更合适的分层方法。\u003C\u002Fp\u003E\u003Cp\u003E另一方面,我们在这里所定义的自由,绝不仅仅是多暴露几个渲染函数那么简单,这里的自由指的是组件内部 DOM 结构的自由。因为一旦某个组件定死了自己的 DOM 结构,外部使用时除了重写样式去强行覆盖外没有任何其他可行的方式去改变它。\u003C\u002Fp\u003E\u003Cp\u003E虽然我们上面提到了许多自由的好处,但很多时候我们还是会被一个问题所挑战,那就是自由的组件在大部分时候不如规范的组件来得好用,因为调用起来很麻烦。\u003C\u002Fp\u003E\u003Cp\u003E这个问题其实是有解的,那就是默认值。我们可以在组件库中内置许多常用的子元素,当用户不指定子元素时,使用默认的子元素来完成渲染,这样就可以在规范与自由之间达成一个良好的平衡,但这里需要注意的是,添加常用子元素的工作量也非常巨大,团队内部也需要对“常用”这个词有一个统一的认识。\u003C\u002Fp\u003E\u003Cp\u003E或者你也可以选择针对不同的使用场景,做两套不同的解决方案。例如前端开源 UI 框架界的翘楚 antd,其底层依赖的 \u003Ca href=\&https:\u002F\u002Fgithub.com\u002Freact-component\&\u003Ereact-component\u003C\u002Fa\u003E 也是非常解耦的设计,几乎看不到任何固定的 DOM 结构,而是使用自定义组件或 children prop 将 DOM 结构的决定权交给使用者。\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003E\u002F\u002F react-component\u002Fdropdown\nreturn (\n
&Trigger\n
{...otherProps}\n
prefixCls={prefixCls}\n
ref=\&trigger\&\n
popupClassName={overlayClassName}\n
popupStyle={overlayStyle}\n
builtinPlacements={placements}\n
action={trigger}\n
showAction={showAction}\n
hideAction={hideAction}\n
popupPlacement={placement}\n
popupAlign={align}\n
popupTransitionName={transitionName}\n
popupAnimation={animation}\n
popupVisible={this.state.visible}\n
afterPopupVisibleChange={this.afterVisibleChange}\n
popup={this.getMenuElement()}\n
onPopupVisibleChange={this.onVisibleChange}\n
getPopupContainer={getPopupContainer}\n
{children}\n
&\u002FTrigger&\n);\u003C\u002Fcode\u003E\u003Ch2\u003E数据处理:耦合 vs. 解耦\u003C\u002Fh2\u003E\u003Cp\u003E如果你问一个工程师在某个场景下,两个模块是耦合好还是解耦好?我想他甚至可能都不会问你是什么场景就脱口而出:“当然解耦好,耦合的代码根本没办法维护!”\u003C\u002Fp\u003E\u003Cp\u003E但事实上,在传统的组件库设计中,我们一直都默认组件是可以和数据源(一般的组件都会有 data 这个 prop)相耦合的,这样就导致了我们在给某个组件赋值之前,要先写一个数据处理方法,将后端返回回来的数据处理成组件要求的数据结构,再传给组件进行渲染。\u003C\u002Fp\u003E\u003Cp\u003E这时,如果后端返回的或组件要求的数据结构再变态一些(如数组嵌套),这个数据处理方法就很有可能会写得非常复杂,甚至还会导致许多的 edge case 使得组件在获取某个特定的 attribute 时直接报错。\u003C\u002Fp\u003E\u003Cp\u003E如何将组件与数据源解耦呢?答案就是不要在组件代码(不论是视图层还是控制层)中出现 \u003Ccode class=\&inline\&\u003Edata.xxx\u003C\u002Fcode\u003E,而是在回调时将整个对象都抛给调用者供其按需使用。这样组件就可以无缝适配于各种各样的后端接口,大大降低使用者在数据处理时犯错误的可能。\u003C\u002Fp\u003E\u003Cp\u003E承接前文,其实这样的数据处理方式和前面提到的自由的设计思想是一脉相承的,正是因为我们赋予了使用者自由定制 DOM 结构的能力,所以我们同时也可以赋予他们在数据处理上的自由。\u003C\u002Fp\u003E\u003Cp\u003E看到这里,支持规范组件的朋友可能已经有些崩溃了,因为听起来自由组件既不定义 DOM 结构,也不处理数据,那么我为什么还要用这个组件呢?\u003C\u002Fp\u003E\u003Cp\u003E让我们以 Select 组件为例来回答这个问题。\u003C\u002Fp\u003E\u003Cp\u003E是的,自由的 Select 组件需要使用者自定义下拉元素,还需要在回调中自己处理使用 data 的哪个 attribute 来完成下一步的业务逻辑,但 Select 组件真的什么都没有做吗?其实并不是,Select 组件规范了“选择”这个交互方式,处理了什么时候显示或隐藏下拉列表,响应了下拉列表元素的 \u003Ccode class=\&inline\&\u003Ehover\u003C\u002Fcode\u003E 和 \u003Ccode class=\&inline\&\u003Eclick\u003C\u002Fcode\u003E 事件,并控制了绝对定位的下拉列表的弹出位置。这些通用的交互逻辑,才是 Select 组件的核心,至于多变的渲染和数据处理逻辑,打包开放出来反而更利于使用者在多变的业务场景下方便地使用 Select 组件。\u003C\u002Fp\u003E\u003Cp\u003E讲完了组件与数据源之间的解耦,我们再来谈一下组件各个 props 之间解耦的必要性。\u003C\u002Fp\u003E\u003Cp\u003E假设一个需求:按照中国、美国、英国、日本、加拿大的顺序显示当地时间,当地时间需从服务端获取且显示格式不同。\u003C\u002Fp\u003E\u003Cp\u003E我们可以设计一个组件,接收不同国家的时间数据作为其 data prop,展示一个当地时间至少需要英文唯一标识符 \u003Ccode class=\&inline\&\u003Eregion\u003C\u002Fcode\u003E,中文显示名 \u003Ccode class=\&inline\&\u003Ename\u003C\u002Fcode\u003E,当前时间 \u003Ccode class=\&inline\&\u003Etime\u003C\u002Fcode\u003E,显示格式 \u003Ccode class=\&inline\&\u003Eformat\u003C\u002Fcode\u003E 等四个属性,由此我们可以设计组件的 data 属性为:\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003Edata: [{\n
region: 'china'\n
name: '中国',\n
format: 'MMMM Do YYYY, h:mm:ss a',\n}, {\n
...\n}]\u003C\u002Fcode\u003E\u003Cp\u003E看起来不错,但事实真的是这样吗?我相信如果你把这份数据结构拿给后端同事看时,他一定会立刻指出一个问题,那就是后端数据库中是不会保存 \u003Ccode class=\&inline\&\u003Ename\u003C\u002Fcode\u003E 及 \u003Ccode class=\&inline\&\u003Eformat\u003C\u002Fcode\u003E 字段的,因为这是由具体产品定义的展示逻辑,而接口只负责告诉你这个地区是哪里 \u003Ccode class=\&inline\&\u003Eregion\u003C\u002Fcode\u003E 以及这个地区的当前时间是多少 \u003Ccode class=\&inline\&\u003Etime\u003C\u002Fcode\u003E。事情到这里也许还不算那么糟糕,因为我们可以在调用组件前,将异步获取到的数据再重新格式化一遍,补上缺失的字段。但这时一个更棘手的问题来了,那就是接口返回的数组数据一般是不保证顺序的,你还需要按照产品的要求,在补充完缺失的字段后,对整个数组进行一次重排以保证每一次渲染出来的地区都保持同样的顺序。\u003C\u002Fp\u003E\u003Cp\u003E换一种方式,如果我们这样去设计组件的 props 呢?\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003E{\n
china: {\n
timeList: [{\n
region: 'china',\n
name: '中国',\n
format: 'MMMM Do YYYY, h:mm:ss a',\n
...\n}\u003C\u002Fcode\u003E\u003Cp\u003E当我们将需要异步获取的 props 抽离后,这个组件就变得非常 data & api friendly 了,仅通过配置 timeList prop 就可以完美地控制组件的渲染规则及渲染顺序并且再也不需要对接口返回的数据进行补全或定制了。甚至我们还可以通过设置默认值的方式,先将组件同步渲染出来,在异步数据请求完成后再重绘数值部分,给予用户更好的视觉体验。\u003C\u002Fp\u003E\u003Cp\u003E除了分离非必须耦合的 props 之外,细心的朋友可能还会发现上面的 data prop 的数据结构从数组变为了对象,这又是为什么呢?\u003C\u002Fp\u003E\u003Ch2\u003E回调规范:数组 vs. 对象\u003C\u002Fh2\u003E\u003Cp\u003E设计思想可以是自由的,数据处理也可以是自由的,但一个成熟的 UI 组件库作为一个独立的前端项目,在代码层面必须要建立起自己的规范。抛开老生常谈的 JavaScript 及 Sass\u002FLess 层面的代码规范不表,让我们从 CSS 类名、组件类别及回调规范三个方面分享一些最佳实践。\u003C\u002Fp\u003E\u003Cp\u003E在组件库项目中,并不推荐使用 CSS Modules,一方面是因为其编译出来的复杂类名不便于使用者在业务项目里进行简单覆盖,更重要的是我们可以将每一个组件都看作是一个独立的模块,用添加 \u003Ccode class=\&inline\&\u003Exui-componentName\u003C\u002Fcode\u003E 类名前缀的方式来实现一套简化版的 CSS Modules。另外,在 jsx 中我们可以参考 antd 的做法,为每一个组件添加一个名为 \u003Ccode class=\&inline\&\u003EprefixCls\u003C\u002Fcode\u003E 的 prop,并将其默认值也设置为 \u003Ccode class=\&inline\&\u003Exui-componentName\u003C\u002Fcode\u003E,这样就在 jsx 层面也保证了代码的统一性,方便团队成员阅读及维护。\u003C\u002Fp\u003E\u003Cp\u003E在这次内部组件库重构项目中,我们将所有的组件分为了纯渲染组件与智能组件两类,并规范其写法为纯函数与 ES6 class 两种,彻底抛弃了 \u003Ccode class=\&inline\&\u003EReact.createClass\u003C\u002Fcode\u003E 的写法。这样一方面可以进一步规范代码,增强可读性,另一方面也可以让后续的维护者在一秒钟内判断出某个组件是纯渲染组件还是智能组件。\u003C\u002Fp\u003E\u003Cp\u003E在回调函数方面,所有的组件内部函数都以 \u003Ccode class=\&inline\&\u003EhandleXXX\u003C\u002Fcode\u003E(\u003Ccode class=\&inline\&\u003EhandleClick\u003C\u002Fcode\u003E,\u003Ccode class=\&inline\&\u003EhandleHover\u003C\u002Fcode\u003E,\u003Ccode class=\&inline\&\u003EhandleMouseover\u003C\u002Fcode\u003E 等)为命名模板,所有对外暴露的回调函数都以 \u003Ccode class=\&inline\&\u003EonXXX\u003C\u002Fcode\u003E(\u003Ccode class=\&inline\&\u003EonChange\u003C\u002Fcode\u003E,\u003Ccode class=\&inline\&\u003EonSelect\u003C\u002Fcode\u003E 等)为命名模板。这样在维护一些依赖层级较深的底层组件时,就可以在 render 方法中一眼看出某个回调是在处理内部状态,还是将回调至更高一层。\u003C\u002Fp\u003E\u003Cp\u003E在设计回调数据的数据结构时,我们只使用了单一值(如 Input 组件的回调)和对象两种数据结构,尽量避免了使用传统组件库中常用的数组。相较于对象,数组其实是一种含义更为丰富的数据结构,因为它是有向的(包含顺序的),比如在上面的例子中,timeList prop 就被设计为数组,这样它就可以在承载数据的同时包含数据展示的顺序,极大地方便了组件的使用。但在给使用者抛出回调数据时,并不是每一位使用者都能够像组件设计者那样清楚回调数据的顺序,使用数组实际上变相增加了使用者的记忆成本,而且笔者一直都不赞成在代码中出现类似于 \u003Ccode class=\&inline\&\u003Econst value = data[0];\u003C\u002Fcode\u003E 这样的表达式。因为没有人能够保证数组的长度满足需要且当前位上的元素就是要取的值。另一方面,对象因为键值对的存在,在具体到某一个元素的表意上要比数组更为丰富。例如选择日历区间后的回调需要同时返回开始日期及结束日期:\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003E\u002F\u002F array\n['', '']\n\n\u002F\u002F object\n{\n
firstDay: '',\n
lastDay: '',\n}\u003C\u002Fcode\u003E\u003Cp\u003E严格来讲上述的两种方式并没有对错之分,只是对象的数据结构更能够清晰地表达每个元素的含义并消除顺序的影响,更利于不了解组件库内部代码的使用者快速上手。\u003C\u002Fp\u003E\u003Ch2\u003E小结\u003C\u002Fh2\u003E\u003Cp\u003E在本文中,我们从设计思想、数据处理、回调规范三个方面为各位剖析了在前端组件化已经成为既定事实的今天,我们还能在组件库设计方面做出怎样新的尝试与突破。也许这些新的尝试与突破并不会像一个新的框架那样给你带来全新的震撼,但我们相信这些实用的思考与经验可以让你少走许多弯路并打开一些新的思路,并且跳出前端这个“狭小”的圈子,站在软件工程的高度去看待这些看似简单实则复杂的工作。\u003C\u002Fp\u003E\u003Cp\u003E在以后的文章中,我们还会从组件库整体代码架构、组件库国际化方案及复杂组件架构设计等方面为大家带来更多细节上的经验与体会,也会穿插更多的具体的代码片段来阐述我们的设计思想与理念,敬请期待。\u003C\u002Fp\u003E&,&updated&:new Date(&T03:36:08.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:39,&likeCount&:205,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:false,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T11:36:08+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&https:\u002F\u002Fpic4.zhimg.com\u002Fv2-d30c016d856a624c9d3c_r.jpg&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:39,&likesCount&:205},&&:{&title&:&Redux异步方案选型&,&author&:&kpax-qin&,&content&:&\u003Cp\u003E作为react社区最热门的状态管理框架,相信很多人都准备甚至正在使用Redux。\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E由于Redux的理念非常精简,没有追求大而全,这份架构上的优雅却在某种程度上伤害了使用体验:不能开箱即用,甚至是异步这种最常见的场景也要借助社区方案。\u003C\u002Fp\u003E\u003Cp\u003E如果你已经挑花了眼,或者正在挑但不知道是否适合,或者已经挑了但不知道会不会有坑,这篇文章应该适合你。\u003C\u002Fp\u003E\u003Cp\u003E本文会从一些常见的Redux异步方案出发,介绍它们的优缺点,进而讨论一些与异步相伴的常见场景,帮助你在选型时更好地权衡利弊。\u003C\u002Fp\u003E\u003Ch2\u003E简单方案\u003C\u002Fh2\u003E\u003Ch3\u003Eredux-thunk:指路先驱\u003C\u002Fh3\u003E\u003Cp\u003EGithub:\u003Ca href=\&http:\u002F\u002Fgaearon\u002Fredux-thunk\& data-editable=\&true\& data-title=\&gaearon\u002Fredux-thunk\&\u003Egaearon\u002Fredux-thunk\u003C\u002Fa\u003E\u003C\u002Fp\u003E\u003Cp\u003ERedux作者Dan写的中间件,因官方文档出镜而广为人知。\u003C\u002Fp\u003E\u003Cp\u003E它向我们展示了Redux处理异步的原理,即: \u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003ERedux本身只能处理同步的Action,但可以通过中间件来拦截处理其它类型的action,比如函数(Thunk),再用回调触发普通Action,从而实现异步处理,\u003Cstrong\u003E在这点上所有Redux的异步方案都是类似的\u003C\u002Fstrong\u003E。\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E而它使用起来最大的问题,就是重复的模板代码太多:\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003E\u002F\u002Faction types\nconst GET_DATA = 'GET_DATA',\n
GET_DATA_SUCCESS = 'GET_DATA_SUCCESS',\n
GET_DATA_FAILED = 'GET_DATA_FAILED';\n\u002F\u002Faction creator\nconst getDataAction = function(id) {\n
return function(dispatch, getState) {\n
dispatch({\n
type: GET_DATA, \n
payload: id\n
api.getData(id) \u002F\u002F注:本文所有示例的api.getData都返回promise对象\n
.then(response =& {\n
dispatch({\n
type: GET_DATA_SUCCESS,\n
payload: response\n
.catch(error =& {\n
dispatch({\n
type: GET_DATA_FAILED,\n
payload: error\n
}\n}\n\n\u002F\u002Freducer\nconst reducer = function(oldState, action) {\n
switch(action.type) {\n
case GET_DATA : \n
return oldS\n
case GET_DATA_SUCCESS : \n
return successS\n
case GET_DATA_FAILED : \n
return errorS\n
}\n}\n\u003C\u002Fcode\u003E\u003Cp\u003E这已经是最简单的场景了,请注意:我们\u003Cstrong\u003E甚至还没写一行业务逻辑\u003C\u002Fstrong\u003E,如果每个异步处理都像这样,重复且无意义的工作会变成明显的阻碍。\u003C\u002Fp\u003E\u003Cp\u003E另一方面,像GET_DATA_SUCCESS、GET_DATA_FAILED这样的字符串声明也非常无趣且易错。\u003C\u002Fp\u003E\u003Cp\u003E上例中,GET_DATA这个action并不是多数场景需要的,它涉及我们将会提到的乐观更新,保留这些代码是为了和下面的方案做对比\u003C\u002Fp\u003E\u003Ch3\u003Eredux-promise:瘦身过头\u003C\u002Fh3\u003E\u003Cp\u003E由于redux-thunk写起来实在是太麻烦了,社区当然会有其它轮子出现。\u003Ca href=\&http:\u002F\u002Facdlite\u002Fredux-promise\& data-editable=\&true\& data-title=\&redux-promise\&\u003Eredux-promise\u003C\u002Fa\u003E则是其中比较知名的,同样也享受了官网\u003Ca href=\&http:\u002F\u002Fredux.js.org\u002Fdocs\u002Fadvanced\u002F#connecting-to-ui\& data-editable=\&true\& data-title=\&出镜\&\u003E出镜\u003C\u002Fa\u003E的待遇。\u003C\u002Fp\u003E\u003Cp\u003E它自定义了一个middleware,当检测到有action的payload属性是Promise对象时,就会:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E若resolve,触发一个此action的拷贝,但payload为promise的value,并设status属性为”success”\u003C\u002Fli\u003E\u003Cli\u003E若reject,触发一个此action的拷贝,但payload为promise的reason,并设status属性为”error”\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E说起来可能有点不好理解,用代码感受下:\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003E\u002F\u002Faction types\nconst GET_DATA = 'GET_DATA';\n\n\u002F\u002Faction creator\nconst getData = function(id) {\n
return {\n
type: GET_DATA,\n
payload: api.getData(id) \u002F\u002Fpayload为promise对象\n
}\n}\n\n\u002F\u002Freducer\nfunction reducer(oldState, action) {\n
switch(action.type) {\n
case GET_DATA: \n
if (action.status === 'success') {\n
return successState\n
} else {\n
return errorState\n
}\n}\n\u003C\u002Fcode\u003E\u003Cp\u003E进步巨大! 代码量明显减少! 就用它了! ?\u003C\u002Fp\u003E\u003Cp\u003E请等等,\u003Cstrong\u003E任何能明显减少代码量的方案,都应该小心它是否过度省略了什么东西\u003C\u002Fstrong\u003E,减肥是好事,减到骨头就残了。\u003C\u002Fp\u003E\u003Cp\u003Eredux-promise为了精简而做出的妥协非常明显:\u003Ca href=\&http:\u002F\u002FOptimistic%20updates%20%C2%B7%20Issue%20#7%20%C2%B7%20acdlite\u002Fflux-standard-action\& data-editable=\&true\& data-title=\&无法处理乐观更新\& class=\&\&\u003E无法处理乐观更新\u003C\u002Fa\u003E\u003C\u002Fp\u003E\u003Ch4\u003E场景解析之:乐观更新\u003C\u002Fh4\u003E\u003Cp\u003E多数异步场景都是保守更新的,即等到请求成功才渲染数据。而与之相对的乐观更新,则是\u003Cstrong\u003E不等待请求成功,在发送请求的同时立即渲染数据\u003C\u002Fstrong\u003E。\u003C\u002Fp\u003E\u003Cp\u003E最常见的例子就是微信等聊天工具,发送消息时消息\u003Cstrong\u003E立即\u003C\u002Fstrong\u003E进入了对话窗,如果发送失败的话,在消息旁边再作补充提示即可。这种交互”乐观”地相信请求会成功,因此称作乐观更新(Optimistic update)。\u003C\u002Fp\u003E\u003Cp\u003E由于乐观更新发生在用户操作时,要处理它,意味着\u003Cstrong\u003E必须有action表示用户的初始动作\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E在上面redux-thunk的例子中,我们看到了GET_DATA, GET_DATA_SUCCESS、GET_DATA_FAILED三个action,分别表示初始动作、异步成功和异步失败,其中第一个action使得redux-thunk具备乐观更新的能力。\u003C\u002Fp\u003E\u003Cp\u003E而在redux-promise中,最初触发的action被中间件拦截然后过滤掉了。原因很简单,redux认可的\u003Ca href=\&http:\u002F\u002FActions%20%C2%B7%20Redux\& data-editable=\&true\& data-title=\&action\&\u003Eaction\u003C\u002Fa\u003E对象是 \u003Cem\u003Eplain JavaScript objects\u003C\u002Fem\u003E,即简单对象,而在redux-promise中,初始action的payload是个Promise。\u003C\u002Fp\u003E\u003Cp\u003E另一方面,使用status而不是type来区分两个异步action也非常值得商榷,按照redux对action的定义以及社区的普遍实践,个人还是倾向于使用不同的type,用同一type下的不同status区分action额外增加了一套隐形的约定,甚至不符合该redux-promise作者自己所提倡的FSA,体现在代码上则是在switch-case内再增加一层判断。\u003C\u002Fp\u003E\u003Ch3\u003Eredux-promise-middleware:拔乱反正\u003C\u002Fh3\u003E\u003Cp\u003E\u003Ca href=\&http:\u002F\u002Fpburtchaell\u002Fredux-promise-middleware\& data-editable=\&true\& data-title=\&redux-promise-middleware\&\u003Eredux-promise-middleware\u003C\u002Fa\u003E相比redux-promise,采取了更为温和和渐进式的思路,保留了和redux-thunk类似的三个action。\u003C\u002Fp\u003E\u003Cp\u003E示例:\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003E\u002F\u002Faction types\nconst GET_DATA = 'GET_DATA',\n
GET_DATA_PENDING = 'GET_DATA_PENDING',\n
GET_DATA_FULFILLED = 'GET_DATA_FULFILLED',\n
GET_DATA_REJECTED = 'GET_DATA_REJECTED';\n\u002F\u002Faction creator\nconst getData = function(id) {\n
return {\n
type: GET_DATA,\n
payload: {\n
promise: api.getData(id),\n
data: id\n
}\n}\n\n\u002F\u002Freducer\nconst reducer = function(oldState, action) {\n
switch(action.type) {\n
case GET_DATA_PENDING :\n
return oldS \u002F\u002F 可通过action.payload.data获取id\n
case GET_DATA_FULFILLED : \n
return successS\n
case GET_DATA_REJECTED : \n
return errorS\n
}\n}\n\u003C\u002Fcode\u003E\u003Cp\u003E如果不需要乐观更新,action creator可以使用和redux-promise完全一样的,更简洁的写法,即:\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003Econst getData = function(id) {\n
return {\n
type: GET_DATA,\n
payload: api.getData(id) \u002F\u002F等价于 {promise: api.getData(id)}\n
}\n}\n\u003C\u002Fcode\u003E\u003Cp\u003E此时初始actionGET_DATA_PENDING仍然会触发,但是payload为空。\u003C\u002Fp\u003E\u003Cp\u003E相对redux-promise于粗暴地过滤掉整个初始action,redux-promise-middleware选择创建一个\u003Cstrong\u003E只过滤payload中的promise属性\u003C\u002Fstrong\u003E的XXX_PENDING作为初始action,以此保留乐观更新的能力。\u003C\u002Fp\u003E\u003Cp\u003E同时在action的区分上,它选择了回归type的”正途”,_PENDING、_FULFILLED、_REJECTED等后缀借用了promise规范 (当然它们是可配置的) 。\u003C\u002Fp\u003E\u003Cp\u003E它的遗憾则是只在action层实现了简化,对reducer层则束手无策。另外,相比redux-thunk,它还多出了一个_PENDING的字符串模板代码(三个action却需要四个type)。\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E社区有类似\u003Ca href=\&https:\u002F\u002Fgithub.com\u002Ftomatau\u002Ftype-to-reducer\& data-editable=\&true\& data-title=\&type-to-reducer\&\u003Etype-to-reducer\u003C\u002Fa\u003E这样试图简化reducer的库。但由于reducer和异步action通常是两套独立的方案,reducer相关的库无法去猜测异步action的后缀是什么(甚至有没有后缀),社区也没有相关标准,也就很难对异步做出精简和抽象了。\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Ch3\u003Eredux-action-tools:软文预警\u003C\u002Fh3\u003E\u003Cp\u003E无论是redux-thunk还是redux-promise-middleware,模板代码都是显而易见的,每次写XXX_COMPLETED这样的代码都觉得是在浪费生命——你得先在常量中声明它们,再在action中引用,然后是reducer,假设像redux-thunk一样每个异步action有三个type,三个文件加起来你就得写九次! \u003C\u002Fp\u003E\u003Cp\u003E国外开发者也有相同的报怨:\u003C\u002Fp\u003E\u003Cp\u003E有没有办法让代码既像redux-promise一样简洁,又能保持乐观更新的能力呢?\u003C\u002Fp\u003E\u003Cp\u003E\u003Ca href=\&https:\u002F\u002Fgithub.com\u002Fkpaxqin\u002Fredux-action-tools\& data-title=\&redux-action-tools\& class=\&\& data-editable=\&true\&\u003Eredux-action-tools\u003C\u002Fa\u003E是我给出的答案:\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003Econst GET_DATA = 'GET_DATA';\n\n\u002F\u002Faction creator\nconst getData = createAsyncAction(GET_DATA, function(id) {\n
return api.getData(id)\n})\n\n\u002F\u002Freducer\nconst reducer = createReducer()\n
.when(getData, (oldState, action) =& oldState)\n
.done((oldState, action) =& successState)\n
.failed((oldState, action) =& errorState)\n
.build()\n\u003C\u002Fcode\u003E\u003Cp\u003Eredux-action-tools在action层面做的事情与前面几个库大同小异:同样是派发了三个action:GET_DATA\u002FGET_DATA_SUCCESS\u002FGET_DATA_FAILED。这三个action的描述见下表:\u003C\u002Fp\u003E\ntype\nWhen\npayload\nmeta.asyncPhase\n${actionName}\n异步开始前\n同步调用参数\n‘START’\n${actionName}_COMPLETED\n异步成功\nvalue of promise\n‘COMPLETED’\n${actionName}_FAILED\n异步失败\nreason of promise\n‘FAILED’\n\u003Cp\u003EcreateAsyncAction参考了redux-promise作者写的\u003Ca href=\&https:\u002F\u002Fgithub.com\u002Facdlite\u002Fredux-actions\& data-editable=\&true\& data-title=\&redux-actions\&\u003Eredux-actions\u003C\u002Fa\u003E ,它接收三个参数,分别是:\u003C\u002Fp\u003E\u003Col\u003E\u003Cli\u003EactionName \u003Cem\u003E字符串\u003C\u002Fem\u003E,所有派生action的名字都以它为基础,初始action则与它同名\u003C\u002Fli\u003E\u003Cli\u003EpromiseCreator \u003Cem\u003E函数\u003C\u002Fem\u003E,必须返回一个promise对象\u003C\u002Fli\u003E\u003Cli\u003EmetaCreator \u003Cem\u003E函数\u003C\u002Fem\u003E,\u003Cstrong\u003E可选\u003C\u002Fstrong\u003E,作用后面会演示到\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003Cp\u003E目前看来,其实和redux-promise\u002Fredux-promise-middleware大同小异。而真正不同的,是它同时简化了reducer层! 这种简化来自于对异步行为从语义角度的抽象:\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E当(\u003Cstrong\u003Ewhen\u003C\u002Fstrong\u003E)初始action发生时处理同步更新,若异步成功(\u003Cstrong\u003Edone\u003C\u002Fstrong\u003E)则处理成功逻辑,若异步失败(\u003Cstrong\u003Efailed\u003C\u002Fstrong\u003E)则处理失败逻辑\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E抽离出when\u002Fdone\u002Ffailed三个关键词作为api,并使用链式调用将他们串联起来:when函数接收两个参数:actionName和handler,其中handler是可选的,done和failed则只接收一个handler参数,并且只能在when之后调用——他们分别处理 \u003Cem\u003E${actionName}_SUCCESS\u003C\u002Fem\u003E 和 \u003Cem\u003E${actionName}_FAILED\u003C\u002Fem\u003E .\u003C\u002Fp\u003E\u003Cp\u003E无论是action还是reducer层,XX_SUCCESS\u002FXX_FAILED相关的代码都被封装了起来,正如在例子中看到的——你甚至不需要声明它们! \u003Cstrong\u003E创建一个异步action,然后处理它的成功和失败情况,事情本该这么简单。\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E更进一步的,这三个action默认都根据当前所处的异步阶段,设置了不同的meta(见上表中的meta.asyncPhase),它有什么用呢?用场景说话:\u003C\u002Fp\u003E\u003Ch4\u003E场景解析:失败处理与Loading\u003C\u002Fh4\u003E\u003Cp\u003E它们是异步不可回避的两个场景,几乎每个项目会遇到。\u003C\u002Fp\u003E\u003Cp\u003E以异步请求的失败处理为例,每个项目通常都有一套比较通用的,适合多数场景的处理逻辑,比如弹窗提示。同时在一些特定场景下,又需要绕过通用逻辑进行单独处理,比如表单的异步校验。\u003C\u002Fp\u003E\u003Cp\u003E而在实现通用处理逻辑时,常见的问题有以下几种:\u003C\u002Fp\u003E\u003Col\u003E\u003Cli\u003E\u003Cp\u003E底层处理,扩展性不足\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003E function fetchWrapper(args) {\n
return fetch.apply(fetch, args)\n
.catch(commonErrorHandler)\n }\n\u003C\u002Fcode\u003E\u003Cp\u003E 在较底层封装ajax库可以轻松实现全局处理,但问题也非常明显:\u003C\u002Fp\u003E\u003Cp\u003E 一是\u003Cstrong\u003E扩展性不足\u003C\u002Fstrong\u003E,比如少数场景想要绕过通用处理逻辑,还有一些场景错误是前端生成而非直接来自于请求;\u003Cbr\u003E 二是\u003Cstrong\u003E不易组合\u003C\u002Fstrong\u003E,比如有的场景一个action需要多个异步请求,但异常处理和loading是不需要重复的,因为用户不需要知道一个动作有多少个请求。\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E不够内聚,侵入业务代码\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003E \u002F\u002Faction creator\n const getData = createAsyncAction(GET_DATA, function(id) {\n
return api.getData(id)\n
.catch(commonErrorHandler) \u002F\u002F调用错误处理函数\n })\n\u003C\u002Fcode\u003E\u003Cp\u003E 在有业务意义的action层调用通用处理逻辑,既能按需调用,又不妨碍异步请求的组合。但由于通用处理往往适用于\u003Cstrong\u003E多数\u003C\u002Fstrong\u003E场景,这样写会导致业务代码变得冗余,因为几乎每个action都得这么写。\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E高耦合,高风险\u003C\u002Fp\u003E\u003Cp\u003E 也有人把上面的方案做个依赖反转,改为在通用逻辑里监听业务action:\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003E function commonErrorReducer(oldState, action) {\n
switch(action.type) {\n
case GET_DATA_FAILED:\n
case PUT_DATA_FAILED:\n
\u002F\u002F... tons of action type\n
return commonErrorHandler(action)\n
}\n }\n\u003C\u002Fcode\u003E\u003Cp\u003E 这样做的本质是把冗余从业务代码中拿出来集中管理。\u003C\u002Fp\u003E\u003Cp\u003E 问题在于\u003Cstrong\u003E每添加一个请求,都需要修改公共代码\u003C\u002Fstrong\u003E,把对应的action type加进来。且不说并行开发时merge冲突,如果加了一个异步action,但忘了往公共处理文件中添加——这是很可能会发生的——而异常是分支流程不容易被测试发现,等到发现,很可能就是事故而不是bug了。\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003Cp\u003E通过以上几种常见方案的分析,我认为比较完善的错误处理(Loading同理)需要具备如下特点:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E面向异步动作(action),而非直接面向请求\u003C\u002Fli\u003E\u003Cli\u003E不侵入业务代码\u003C\u002Fli\u003E\u003Cli\u003E默认使用通用处理逻辑,无需额外代码\u003C\u002Fli\u003E\u003Cli\u003E可以绕过通用逻辑\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E而借助redux-action-tools提供的\u003Cem\u003Emeta.asyncPhase\u003C\u002Fem\u003E,可以轻易用middleware实现以上全部需求!\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003Eimport _ from 'lodash'\nimport { ASYNC_PHASES } from 'redux-action-tools'\n\nfunction errorMiddleWare({dispatch}) {\n
return next =& action =& {\n
const asyncStep = _.get(action, 'meta.asyncStep');\n\n
if (asyncStep === ASYNC_PHASES.FAILED) {\n
dispatch({\n
type: 'COMMON_ERROR',\n
payload: {\n
next(action);\n
}\n}\n\u003C\u002Fcode\u003E\u003Cp\u003E以上中间件一旦检测到meta.asyncStep字段为\u003Cem\u003EFAILED\u003C\u002Fem\u003E的action便触发新的action去调用通用处理逻辑。面向action、不侵入业务、默认工作 (只要是用createAsyncAction声明的异步) ! 轻松实现了理想需求中的前三点,那如何定制呢?既然拦截是面向meta的,只要在创建action时支持对meta的自定义就行了,而createAsyncAction的第三个参数就是为此准备的:\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003Eimport _ from 'lodash'\nimport { ASYNC_PHASES } from 'redux-action-tools'\n\nconst customizedAction = createAsyncAction(\n
promiseCreator, \u002F\u002Ftype 和 promiseCreator此处无不同故省略\n
(payload, defaultMeta) =& {\n
return { ...defaultMeta, omitError: true }; \u002F\u002F向meta中添加配置参数\n
}\n)\n\nfunction errorMiddleWare({dispatch}) {\n
return next =& action =& {\n
const asyncStep = _.get(action, 'meta.asyncStep');\n
const omitError = _.get(action, 'meta.omitError'); \u002F\u002F获取配置参数\n\n
if (!omitError && asyncStep === ASYNC_PHASES.FAILED) {\n
dispatch({\n
type: 'COMMON_ERROR',\n
payload: {\n
next(action);\n
}\n}\n\u003C\u002Fcode\u003E\u003Cp\u003E类似的,你可以想想如何处理Loading,需要强调的是建议尽量用增量配置的方式进行扩展,而\u003Cstrong\u003E不要轻易删除和修改meta.asyncPhase\u003C\u002Fstrong\u003E。\u003C\u002Fp\u003E\u003Cp\u003E比如上例可以通过删除meta.asyncPhase实现同样功能,但如果同时还有其它地方也依赖meta.asyncPhase(比如loadingMiddleware),就可能导致本意是定制错误处理,却改变了Loading的行为,客观来讲\u003Cstrong\u003E这层风险是基于meta拦截方案的最大缺点\u003C\u002Fstrong\u003E,然而相比多数场景的便利、健壮,个人认为特殊场景的风险是可以接受的,毕竟这些场景在整个开发测试流程容易获得更多关注。\u003C\u002Fp\u003E\u003Ch2\u003E进阶方案\u003C\u002Fh2\u003E\u003Cp\u003E上面所有的方案,都把异步请求这一动作放在了action creator中,这样做的好处是简单直观,且和Flux社区一脉相承(见下图)。因此个人将它们归为相对简单的一类。\u003C\u002Fp\u003E\u003Cp\u003E下面将要介绍的,是相对复杂一类,它们都采用了与上图不同的思路,去追求更优雅的架构、解决更复杂的问题\u003C\u002Fp\u003E\u003Ch3\u003Eredux-loop:分形! 组合!\u003C\u002Fh3\u003E\u003Cp\u003E众所周知,Redux是借鉴自\u003Ca href=\&http:\u002F\u002Fhome\& data-editable=\&true\& data-title=\&Elm\&\u003EElm\u003C\u002Fa\u003E的,然而在Elm中,异步的处理却并不是在action creator层,而是在reducer(Elm中称update)层:\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E图片来源于: \u003Ca href=\&https:\u002F\u002Fgithub.com\u002Fjarvisaoieong\u002Fredux-architecture\& data-editable=\&true\& data-title=\&jarvisaoieong\u002Fredux-architecture\&\u003Ejarvisaoieong\u002Fredux-architecture\u003C\u002Fa\u003E\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E这样做的目的是为了实现彻底的可组合性(composable)。在redux中,reducer作为函数是可组合的,action正常情况下作为纯对象也是可组合的,然而一旦涉及异步,当action嵌套组合的时候,中间件就无法正常识别,这个问题让redux作者Dan也发出感叹 \u003Ca href=\&https:\u002F\u002Fproductpains.com\u002Fpost\u002Fredux\u002Fthere-is-no-easy-way-to-compose-redux-applications\& data-editable=\&true\& data-title=\&There is no easy way to compose Redux applications\&\u003EThere is no easy way to compose Redux applications\u003C\u002Fa\u003E并且开了一个至今仍然open的\u003Ca href=\&https:\u002F\u002Fgithub.com\u002Freactjs\u002Fredux\u002Fissues\u002F1528\& data-editable=\&true\& data-title=\&issue\&\u003Eissue\u003C\u002Fa\u003E,对组合、分形与redux的故事,有兴趣的朋友可以观摩以上链接,甚至了解一下Elm,篇幅所限,本文难以尽述。\u003C\u002Fp\u003E\u003Cp\u003E而redux-loop,则是在这方面的一个尝试,它更彻底的模仿了Elm的模式:引入Effects的概念并将其置入reducer,官方示例如下:\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003Eimport { Effects, loop } from 'redux-loop';\nimport { loadingStart, loadingSuccess, loadingFailure } from '.\u002Factions';\n\nexport function fetchDetails(id) {\n
return fetch(`\u002Fapi\u002Fdetails\u002F${id}`)\n
.then((r) =& r.json())\n
.then(loadingSuccess)\n
.catch(loadingFailure);\n}\n\nexport default function reducer(state, action) {\n
switch (action.type) {\n
case 'LOADING_START':\n
return loop(\n
{ ...state, loading: true },\n
Effects.promise(fetchDetails, action.payload.id)\n
); \u002F\u002F 同时返回状态与副作用\n\n
case 'LOADING_SUCCESS':\n
return {\n
...state,\n
loading: false,\n
details: action.payload\n
case 'LOADING_FAILURE':\n
return {\n
...state,\n
loading: false,\n
error: action.payload.message\n
default:\\n
}\n}\n\u003C\u002Fcode\u003E\u003Cp\u003E注意在reducer中,当处理LOADING_START时,并没有直接返回state对象,而是用loop函数将state和Effect”打包”返回(实际上这个返回值是数组[State, Effect],和Elm的方式非常接近)。\u003C\u002Fp\u003E\u003Cp\u003E然而修改reducer的返回类型显然是比较暴力的做法,除非Redux官方出面,否则很难获得社区的广泛认同。更复杂的返回类型会让很多已有的API,三方库面临危险,甚至combineReducer都需要用redux-loop提供的定制\u003Ca href=\&https:\u002F\u002Fgithub.com\u002Fredux-loop\u002Fredux-loop#use-the-custom-combinereducers-if-you-need-it\& data-editable=\&true\& data-title=\&版本\&\u003E版本\u003C\u002Fa\u003E,这种”破坏性”也是Redux作者Dan没有采纳redux-loop进入Redux核心代码的原因:”If a solution doesn’t work with vanilla combineReducers(), it won’t get into Redux core”。\u003C\u002Fp\u003E\u003Cp\u003E对Elm的分形架构有了解,想在Redux上继续实践的人来说,redux-loop是很好的参考素材,但对多数人和项目而言,最好还是更谨慎地看待。\u003C\u002Fp\u003E\u003Ch3\u003Eredux-saga:难、而美\u003C\u002Fh3\u003E\u003Cp\u003EGithub:
\u003Ca href=\&https:\u002F\u002Fgithub.com\u002Fyelouafi\u002Fredux-saga\& data-editable=\&true\& data-title=\&yelouafi\u002Fredux-saga\&\u003Eyelouafi\u002Fredux-saga\u003C\u002Fa\u003E\u003C\u002Fp\u003E\u003Cp\u003E另一个著名的库,它让异步行为成为架构中独立的一层(称为saga),既不在action creator中,也不和reducer沾边。\u003C\u002Fp\u003E\u003Cp\u003E它的出发点是把副作用 (Side effect,异步行为就是典型的副作用) 看成”线程”,可以通过普通的action去触发它,当副作用完成时也会触发action作为输出。\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003Eimport { takeEvery } from 'redux-saga'\nimport { call, put } from 'redux-saga\u002Feffects'\nimport Api from '...'\n\nfunction* getData(action) {\n
const response = yield call(api.getData, action.payload.id);\n
yield put({type: \&GET_DATA_SUCCEEDED\&, payload: response});\n
} catch (e) {\n
yield put({type: \&GET_DATA_FAILED\&, payload: error});\n
}\n}\n\nfunction* mySaga() {\n
yield* takeEvery(\&GET_DATA\&, getData);\n}\n\nexport default myS\n\u003C\u002Fcode\u003E\u003Cp\u003E相比action creator的方案,它可以保证组件触发的action是纯对象,因此至少在项目范围内(middleware和saga都是项目的顶层依赖,跨项目无法保证),action的组合性明显更加优秀。\u003C\u002Fp\u003E\u003Cp\u003E而它最为主打的,则是\u003Cstrong\u003E可测试性\u003C\u002Fstrong\u003E和强大的异步\u003Cstrong\u003E流程控制\u003C\u002Fstrong\u003E。\u003C\u002Fp\u003E\u003Cp\u003E由于强制所有saga都必须是generator函数,借助generator的next接口,异步行为的每个中间步骤都被暴露给了开发者,从而实现对异步逻辑”step by step”的测试。这在其它方案中是很少看到的 (当然也可以借鉴generator这一点,但缺少约束)。\u003C\u002Fp\u003E\u003Cp\u003E而强大得有点眼花缭乱的\u003Ca href=\&http:\u002F\u002Fyelouafi.github.io\u002Fredux-saga\u002Fdocs\u002Fapi\u002Findex.html\& data-editable=\&true\& data-title=\&API\&\u003EAPI\u003C\u002Fa\u003E,特别是channel的引入,则提供了武装到牙齿级的异步流程控制能力。\u003C\u002Fp\u003E\u003Cp\u003E然而,回顾我们在讨论简单方案时提到的各种场景与问题,redux-saga并没有去尝试回答和解决它们,这意味着你需要自行寻找解决方案。而generator、相对复杂的API和单独的一层抽象也让不少人望而却步。\u003C\u002Fp\u003E\u003Cp\u003E包括我在内,很多人非常欣赏redux-saga。它的架构和思路毫无疑问是优秀甚至优雅的,但使用它之前,最好想清楚它带来的优点(可测试性、流程控制、高度解耦)与付出的成本是否匹配,特别是异步方面复杂度并不高的项目,比如多数以CRUD为主的管理系统。\u003C\u002Fp\u003E\u003Ch4\u003E场景解析:竞态\u003C\u002Fh4\u003E\u003Cp\u003E说到异步流程控制很多人可能觉得太抽象,这里举个简单的例子:竞态。这个问题并不罕见,知乎也有见到\u003Ca href=\&https:\u002F\u002Fwww.zhihu.com\u002Fquestion\u002F\& data-editable=\&true\& data-title=\&类似问题\&\u003E类似问题\u003C\u002Fa\u003E。\u003C\u002Fp\u003E\u003Cp\u003E简单描述为:\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E由于异步返回时间的不确定性,后发出的请求可能先返回,如何确保异步结果的渲染是按照请求发生顺序,而不是返回顺序?\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E这在redux-thunk为代表的简单方案中是要费点功夫的:\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003Efunction fetchFriend(id){\n
return (dispatch, getState) =& {\n
\u002F\u002F步骤1:在reducer中 set state.currentFriend =\n
dispatch({type: 'FETCH_FIREND', payload: id}); \n\n
return fetch(`http:\u002F\u002Flocalhost\u002Fapi\u002Ffirend\u002F${id}`)\n
.then(response =& response.json())\n
.then(json =& { \n
\u002F\u002F步骤2:只处理currentFriend的对应response\n
const { currentFriend } = getState();\n
(currentFriend === id) && dispatch({type: 'RECEIVE_FIRENDS', playload: json})\n
}\n}\n\u003C\u002Fcode\u003E\u003Cp\u003E以上只是示例,实际中不一定需要依赖业务id,也不一定要把id存到store里,只要为每个请求生成key,以便处理请求时能够对应起来即可。\u003C\u002Fp\u003E\u003Cp\u003E而在redux-saga中,一切非常地简单:\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003Eimport { takeLatest } from `redux-saga`\n\nfunction* fetchFriend(action) {\n
...\n}\n\nfunction* watchLastFetchUser() {\n
yield takeLatest('FETCH_FIREND', fetchFriend)\n}\n\u003C\u002Fcode\u003E\u003Cp\u003E这里的重点是\u003Ca href=\&http:\u002F\u002Fyelouafi.github.io\u002Fredux-saga\u002Fdocs\u002Fapi\u002F#saga-helpers\& data-title=\&takeLatest\& class=\&\& data-editable=\&true\&\u003EtakeLatest\u003C\u002Fa\u003E,它限制了同步事件与异步返回事件的顺序关系。\u003C\u002Fp\u003E\u003Cp\u003E另外还有一些基于响应式编程(Reactive Programming)的异步方案(如redux-observable)也能非常好地处理竞态场景,因为\u003Cstrong\u003E描述事件流之间的关系,正是整个响应式编程的抽象基石\u003C\u002Fstrong\u003E,而竞态在本质上就是如何保证同步事件与异步返回事件的关系,正是响应式编程的用武之地。\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E实际项目中可以用高阶函数模仿takeLatest的功能,redux-thunk类方案也可以较低成本地处理竞态\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Ch2\u003E小结\u003C\u002Fh2\u003E\u003Cp\u003E本文包含了一些redux社区著名、非著名 (恩,我的redux-action-tools) 的异步方案,这些其实并不重要。\u003C\u002Fp\u003E\u003Cp\u003E因为方案是一家之作,结论也是一家之言,不可能放之四海皆准。个人更希望文中探讨过的常见问题和场景,比如模板代码、乐观更新、错误处理、竞态等,能够成为你选型时的尺子,为你的权衡提供更好的参考,而不是等到项目热火朝天的时候,才发现当初选型的硬伤。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cp\u003E(注:题图来自网络,侵立删)\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cbr\u003E&,&updated&:new Date(&T03:30:58.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:40,&likeCount&:278,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:false,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T11:30:58+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&https:\u002F\u002Fpic3.zhimg.com\u002Fv2-afe87f162e90a1c54acc87f_r.jpg&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:40,&likesCount&:278},&&:{&title&:&组件库设计实战 - 组件分类、文档管理与打包发布&,&author&:&wei-chang-ran&,&content&:&\u003Cp\u003E在上篇\u003Ca href=\&https:\u002F\u002Fgithub.com\u002FAlanWei\u002Fblog\u002Fissues\u002F1\&\u003E《重新设计 React 组件库》\u003C\u002Fa\u003E中我们从宏观层面一起探讨了结构自由且数据解耦的 React 组件库应当如何设计,在本文中让我们从具体实践的角度来看如何将这样的设计落地。\u003C\u002Fp\u003E\u003Ch2\u003E组件分类\u003C\u002Fh2\u003E\u003Cp\u003E在传统的组件库设计中,组件分类一直都不是一个必选项,大多数人都认为一个组件究竟是属于组件类还是控件类,不过是名字上的不同而已,并没有实际意义。但在将组件代码写法区分为纯函数与 ES6 class 两种之后,我们发现组件的写法同时也代表着组件的类型,这时就可以给予不同组件一个更清晰的定义,分别是:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E不含有内部状态的以纯函数写法表示的无交互的纯渲染组件\u003C\u002Fli\u003E\u003Cli\u003E含有内部状态以 ES6 class 写法表示的有交互的智能控件\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E在进行了这样清晰的分类之后,每当我们需要新增一个组件时,我们都可以从是否含有内部状态,是否有交互等几个方面来将其纳入组件或控件,并以此来确定其相应的代码规范。\u003C\u002Fp\u003E\u003Cp\u003E延伸来说,除了基础的组件与控件的区别之外,我们还推荐大家从业务的角度出发再划分出一种新的组件类型,即容器。\u003C\u002Fp\u003E\u003Cp\u003E举例来说,在 \u003Ca href=\&https:\u002F\u002Fmaterial.io\u002Fguidelines\u002F\&\u003EMaterial Design\u003C\u002Fa\u003E 大行其道的今天,应该不会有人对\u003Cb\u003E卡片\u003C\u002Fb\u003E这样一种基础的内容展示形式感到陌生。对应到前端组件库中,作为展示内容的骨架,卡片本身应当是一个纯渲染组件,但在将其带入具体的业务场景中后就会发现,卡片本身其实是有状态的,常见的如数据加载中、数据为空、数据错误等。这样一个无交互但含有自身状态的组件无论归于上述的哪个分类都会让人感到奇怪,所以我们又引入了容器这样一个新的分类,专门用来存放卡片这类组件。看到这里,相信聪明的你应该能体会到组件分类的真正意义了,那就是用组件分类这样一种形式来强迫工程师去思考每一个组件的本质,然后再利用 pure render 等方法去优化组件性能。作为离用户最近的一批工程师,前端工程师所应该关心的,除了代码本身之外,用户体验,人机交互等领域方面的经验与知识,也是判断前端工程师是否优秀的另一把标尺。\u003C\u002Fp\u003E\u003Cp\u003E另一方面来讲,我们又可以从容器组件延伸出强依赖数据的组件应当如何设计这样一个更加抽象的问题。从组件库设计的角度来讲,正如上一篇文章中所提到的,不建议将数据获取等逻辑放在组件里去做的。但结合业务场景来说,统一数据获取等逻辑确实是提升业务开发效率的不二选择,这方面的具体实践大家可以参考\u003Ca href=\&https:\u002F\u002Fwww.zhihu.com\u002Fpeople\u002Fxile611\&\u003E琼玖\u003C\u002Fa\u003E之前的文章\u003Ca href=\&https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F\&\u003E《React实践 - Component Generator》\u003C\u002Fa\u003E。简而言之,使用高阶组件在这里是一个不错的选择。\u003C\u002Fp\u003E\u003Cp\u003E回到代码本身,抛开纯函数组件不谈,我们这里再来讨论一个编写智能组件时经常会踩到的坑。\u003C\u002Fp\u003E\u003Cp\u003E在 React 的生命周期函数中,有一个功能十分强大的函数,那就是 \u003Ccode class=\&inline\&\u003EcomponentWillReceiveProps\u003C\u002Fcode\u003E,在这个函数中,我们既可以拿到 \u003Ccode class=\&inline\&\u003Ethis.props\u003C\u002Fcode\u003E 又可以拿到 \u003Ccode class=\&inline\&\u003EnextProps\u003C\u002Fcode\u003E,所以理论上来讲,我们可以在这里利用这些数据对组件做任何逻辑上的变更。另一方面,智能组件一般需要支持木偶与智能两种调用方式,以方便使用者在使用时根据是否需要在业务代码中保存组件状态使用。木偶组件标配的 props 一般为 value 加一个回调函数 onChange,这时组件本身就只需要负责根据接收到的 props 进行渲染。而智能组件的标配 props 一般只需要设置一个 defaultValue,也就是外部只负责定义组件的初始状态,接下来组件自己会根据交互来改变内部状态。这里我们可以通过在 \u003Ccode class=\&inline\&\u003EcomponentWillReceiveProps\u003C\u002Fcode\u003E 中同步 props 到 state 的方式来支持两种不同的调用方式,即如果外部直接改变了 \u003Ccode class=\&inline\&\u003Evalue\u003C\u002Fcode\u003E 值,那么就将新的 \u003Ccode class=\&inline\&\u003Evalue\u003C\u002Fcode\u003E 值同步到组件内部的 state 上,如果外部没有改变 \u003Ccode class=\&inline\&\u003Evalue\u003C\u002Fcode\u003E 值,那么就交由组件内部的 state 全权负责组件状态的更新。\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003Econstructor(props) {\n
super(props);\n\n
this.state = {\n
value: props.defaultValue,\n
};\n}\n\ncomponentWillReceiveProps(nextProps) {\n
\u002F\u002F sync state to props\n
if (this.props.value !== nextProps.value) {\n
this.setState({\n
value: nextProps.value,\n
}\n}\n\nhandleChange(value) {\n
this.setState({\n
this.props.onChange(value);\n}\n\nrender() {\n
const { value } = this.\n
return &input value={value} onChange={::this.handleChange} \u002F&;\n}\u003C\u002Fcode\u003E\u003Ch2\u003E文档管理\u003C\u002Fh2\u003E\u003Cp\u003E编写组件库本身并不是最终目的,让更多的人在业务开发中使用起来才是。组件库作为一个自身封装程度较高,内聚性较强的技术项目,开发文档是否足够清晰,完善,也是决定项目成败的另一个关键因素。\u003C\u002Fp\u003E\u003Cp\u003E优秀的组件库文档起码要满足以下两个要求:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E属性全覆盖:属性名、具体描述、数据类型、是否有默认值、是否必须等\u003C\u002Fli\u003E\u003Cli\u003E示例丰富\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E属性全覆盖的重要性在这里不再赘述,使用者在不阅读源码的前提下想要了解组件的所有功能,阅读组件文档是唯一的途径。\u003C\u002Fp\u003E\u003Cp\u003E另一方面,由于 React 组件本身是高度可定制的,所以如果开发者不能够提供具体的示例,使用者在使用组件进行一个复杂业务开发时就将因为缺少指导而变得异常痛苦。从代码质量管控的角度来讲,丰富的示例也是对组件单元测试的一次具象。在未来维护组件增加新功能时,示例丰富的好处就将体现得淋漓尽致:当组件新增了一些逻辑后,原先所有的示例都仍能完美运行时,我们也会对新加的这个功能更有信心并避免 regression 的发生。\u003C\u002Fp\u003E\u003Ch2\u003E打包发布\u003C\u002Fh2\u003E\u003Cp\u003E作为业务项目的基础依赖,组件库一般都需要打包发布至 npm 以方便业务项目使用。在对组件库进行打包时,为了方便业务项目在具体业务场景下的使用,组件库需要支持以下两种打包方式。\u003C\u002Fp\u003E\u003Cp\u003E第一种打包方式是使用 \u003Ccode class=\&inline\&\u003Ewebpack\u003C\u002Fcode\u003E 将所有组件打包成一个文件至 \u003Ccode class=\&inline\&\u003Edist\u002F\u003C\u002Fcode\u003E 文件夹中:\u003C\u002Fp\u003E\u003Ccode lang=\&text\&\u003Edist\u002Fxui.js\ndist\u002Fxui.css\u003C\u002Fcode\u003E\u003Cp\u003E在业务项目中可以通过\u003C\u002Fp\u003E\u003Ccode lang=\&text\&\u003Eimport { XXX } from 'xui';\u003C\u002Fcode\u003E\u003Cp\u003E的方式直接调用相应组件。\u003C\u002Fp\u003E\u003Cp\u003E另一种打包方式是使用 \u003Ccode class=\&inline\&\u003Ebabel\u003C\u002Fcode\u003E 将每个组件都分别编译至对应的 \u003Ccode class=\&inline\&\u003Elib\u002F\u003C\u002Fcode\u003E 文件夹中,并分别编译每个组件的 \u003Ccode class=\&inline\&\u003ECSS\u003C\u002Fcode\u003E 文件:\u003C\u002Fp\u003E\u003Ccode lang=\&text\&\u003Elib\u002Fcarousel\u002Findex.js\nlib\u002Fcarousel\u002Findex.css\nlib\u002Finput\u002Findex.js\nlib\u002Finput\u002Findex.css\n...\u003C\u002Fcode\u003E\u003Cp\u003E在业务项目中可以通过\u003C\u002Fp\u003E\u003Ccode lang=\&text\&\u003Eimport Carousel from 'xui\u002Flib\u002Fcarousel';\nimport 'xui\u002Flib\u002Fcarousel\u002Findex.css';\u003C\u002Fcode\u003E\u003Cp\u003E的方式按需调用组件。\u003C\u002Fp\u003E\u003Ch2\u003E小结\u003C\u002Fh2\u003E\u003Cp\u003E在本文中,我们主要从组件分类、文档管理、打包发布三个方面阐述了如何将结构自由且数据解耦的 React 组件库落到实处。\u003C\u002Fp\u003E\u003Cp\u003E在下一篇文章中,我们将与大家分享组件库国际化方案,敬请期待。\u003C\u002Fp\u003E&,&updated&:new Date(&T03:53:03.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:10,&likeCount&:59,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:false,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T11:53:03+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&https:\u002F\u002Fpic2.zhimg.com\u002Fv2-6a12a30fbe2c73bce41c_r.jpg&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:10,&likesCount&:59},&&:{&title&:&写在 2016 年的最后&,&author&:&arcthur&,&content&:&\u003Cp\u003E2016 年过去了,在这个空前繁盛的前端领域,前端框架依然是那三架马车。大家更关注类型安全了,ES6 成为标配了,异步解决方案越来越清晰了。未来,前端手里握着的是更完整的富客户端工程化方案,向着更多领域植入 Javascript。\u003C\u002Fp\u003E\u003Cp\u003E对于我们专栏,团队和我同样在成长。我分为三个部分,讲到专栏的成长、Recharts 的成长与我的成长。\u003C\u002Fp\u003E\u003Ch2\u003E专栏的成长\u003C\u002Fh2\u003E\u003Cp\u003E专栏是分享的地方,相对于博客我们需要花太多时间打理,挑这么一个地方,因为它合适于我们小团队的品味。\u003C\u002Fp\u003E\u003Cp\u003EPure render 源自于 React,也源自于函数式编程中 pure function。它的理念很简单,输入一定,输出则一定。对于我们专栏也是如此,输入的是我们的学习与思考,输出的是文字,它也是一定的。我希望这些文字,可以继续作为输入,到达新的输出。这是我对专栏的期望。\u003C\u002Fp\u003E\u003Cp\u003EPure render 专栏一年了。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cimg src=\&v2-87edb04c62.png\& data-rawwidth=\&1062\& data-rawheight=\&468\&\u003E\u003Cp\u003E截止今天,有 6800+ 知友关注,对于一个局限在前端领域,更局限在 React 技术栈的专栏来说,的确超出我的期望。\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E我们积累了 23 篇技术分享,其中有一篇是我邀请的。未来,我想 pure render 本身也是一个开源社区,我希望有更多的社区好文可以在专栏中呈现。\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E此外,我们专栏还会复制到 Segmentfault,w3ctech 和前端早读课中,为了能覆盖更多的读者。这一年,有非常多的读者或是评论,或是私信,或是邮件与我们联系着,我们得到了很多交流的机会。\u003C\u002Fp\u003E\u003Cp\u003E办专栏像是开源了自己的工作,我们一直在成长才能不停下来。\u003C\u002Fp\u003E\u003Ch2\u003ERecharts 的成长\u003C\u002Fh2\u003E\u003Cp\u003ERecharts 是 for React 的图表库,对比业界很多作开源图表,Recharts 真没有什么大不了。Echarts 有其无敌丰富的配置,但这种配置式对于我一个熟悉编程的开发者来说真是糟糕的体验,G2 有一套理论依据在为其背书,在图表的构建上也更完备,提供了更亲开发者的命令式编程的方式,感觉又让我用回了 D3 的感觉。\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E的确,我们在做通用软件设计时,出发点一定是弱依赖、灵活通用、跨平台,因此希望软件本身对基础设施有更多掌控。而 Recharts 就反过来了,它就是想利用 React,像 UI 组件那样去写图表库。我可以有配置但配置是与具体组件绑定的,我可以自定义,用 React 熟悉的方式。\u003C\u002Fp\u003E\u003Cp\u003E因为局限性,我们可以更充分地利用它的优势。这就是 Recharts 的理念。\u003C\u002Fp\u003E\u003Cp\u003ERecharts 也走过了一年。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cimg src=\&v2-f17abd506f7.png\& data-rawwidth=\&1704\& data-rawheight=\&1072\&\u003E从去年开始开发到今天,我们都是在预想中小步迭代,很多时间在重构与修复一些问题,一直没有多做所谓的『技术营销』,但没想到在 08\u002F13\u002F2016 那天突然爆发式的增长。那是从 hacker news 的 \u003Ca href=\&https:\u002F\u002Flink.zhihu.com\u002F?target=https%3A\u002F\u002Fnews.ycombinator.com\u002Fitem%3Fid%3D\& class=\&wrap external\& data-title=\&Recharts Report\&\u003ERecharts Report\u003C\u002Fa\u003E 带来的关注度,一度上了 Github trend 第一名。\u003Cb\u003E波澜不惊中成长\u003C\u002Fb\u003E。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cimg src=\&v2-51b8dcd910d76f4355437.png\& data-rawwidth=\&2014\& data-rawheight=\&394\&\u003E\u003Cbr\u003E\u003Cp\u003E\u003Ca href=\&http:\u002F\u002Frecharts.org\& class=\&external\& data-title=\&http:\u002F\u002Frecharts.org\&\u003Ehttp:\u002F\u002Frecharts.org\u002F\u003C\u002Fa\u003E的 uv 曲线也惊人的相似。截止今天周 uv 维持在 3k 左右,英语国家的访问在接近 50% 的比例,其中美国和俄罗斯占了大部分,而国内访问比例只有 15%。\u003Cb\u003E我想明年这个情况会改观了,今天官网中文翻译工作已经完成了。\u003C\u002Fb\u003E\u003C\u002Fp\u003E\u003Cp\u003E我们一共关闭了 284 个 issue,但还开着 30 个 issue。今天的版本号 0.20.1 发布于 12\u002F28,之间一共发布了 60 个版本。我们的背后的小伙伴\u003Ca href=\&https:\u002F\u002Fwww.zhihu.com\u002Fpeople\u002F1f108db216b303ecc4d39e8bef9f7f03\& data-hash=\&1f108db216b303ecc4d39e8bef9f7f03\& class=\&member_mention\& data-hovercard=\&p$b$1f108db216b303ecc4d39e8bef9f7f03\&\u003E@琼玖\u003C\u002Fa\u003E\u003Ca href=\&https:\u002F\u002Fwww.zhihu.com\u002Fpeople\u002F159f6dbb5db874ffe89fff\& data-hash=\&159f6dbb5db874ffe89fff\& class=\&member_mention\& data-hovercard=\&p$b$159f6dbb5db874ffe89fff\&\u003E@逸鹿\u003C\u002Fa\u003E\u003Ca href=\&https:\u002F\u002Fwww.zhihu.com\u002Fpeople\u002F17eec1e046dfbcc70c3486\& data-hash=\&17eec1e046dfbcc70c3486\& class=\&member_mention\& data-hovercard=\&p$b$17eec1e046dfbcc70c3486\&\u003E@Jason\u003C\u002Fa\u003E,很努力。\u003C\u002Fp\u003E\u003Cp\u003ENPM 月下载量 29k,Github star 数 4351。今年年初就想着 1.0 版本一直到今天。真的不远了,我们会对动画作更多优化,更细粒度的子组件支持,敬请期待。\u003C\u002Fp\u003E\u003Cp\u003ERecharts 是我们开源的代表。开源对于我们来说,本身就是成长。\u003Cbr\u003E\u003C\u002Fp\u003E\u003Ch2\u003E我的成长\u003C\u002Fh2\u003E\u003Cp\u003E最后说到我自己。今年年底出版了《深入 React 技术栈》,很长时间的努力终于告一段落。写书,其目的还在作交流,认识新朋友,交流新想法。\u003C\u002Fp\u003E\u003Cp\u003E从团队角度,React 学习与深入只是一件小事,我更想从 React 这一个小点把前端体系化做实,今年终于慢慢有起色了,也许明年我们会有更多分享在这块上。如果可以解放更多生产力,保障更强的产品稳定性,那么我们本身就做了很多非前端的事。这是我预想的团队成长与我的成长。\u003C\u002Fp\u003E\u003Cp\u003E2017 年继续加油。\u003C\u002Fp\u003E&,&updated&:new Date(&T11:15:52.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:13,&likeCount&:153,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:false,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T19:15:52+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&https:\u002F\u002Fpic3.zhimg.com\u002Fv2-d11d60b2c946bce62704abe_r.jpg&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:13,&likesCount&:153},&&:{&title&:&组件库设计实战 - 国际化方案&,&author&:&wei-chang-ran&,&content&:&\u003Cp\u003E放眼全球,中国整体的互联网技术实力毫无疑问仅次于美国并领先剩余所有的国家一大截。但如果我们非要找出一个中国互联网公司做得不够优秀的地方,那么产品国际化一定是其中之一。虽然我们也拥有诸如 AliExpress,天猫国际等成功案例,但不得不说大部分中国公司在选择出海后,都没有能够收获到与预期相匹配的回报。这其中原因自然很多,然而缺乏一套可以平台化,产品化的通用国际化方案一直都是其中一个非常重要的原因。\u003C\u002Fp\u003E\u003Cp\u003E曾经笔者也天真地认为国际化不过是几个 json 文件的键值对匹配,但在深入了解了一些产品的国际化需求后,笔者才意识到要做一套好的国际化方案并没有那么简单。\u003C\u002Fp\u003E\u003Ch2\u003E服务端国际化\u003C\u002Fh2\u003E\u003Cp\u003E对于前端工程师而言,国际化所要面临的第一个挑战就是,并不是所有的数据都可以在前端做国际化。常见的例子如电商类产品的货品或商家信息,这些都是有强更新需求,需要存储在后端数据库中,通过产品后台进行更新的。如果一个商品要销往美国,德国,法国,西班牙,泰国,印度尼西亚,而运营人员又只想维护一套以中文为基准的商品信息,那么这类数据的国际化我们就需要将其做在服务端。\u003C\u002Fp\u003E\u003Cp\u003E我们当然可以麻烦后端工程师帮助我们根据每个请求的域名或 HTTP header 中的 \u003Ccode class=\&inline\&\u003Econtent-language\u003C\u002Fcode\u003E 来返回不同表中的翻译,但如果你是一位致力于向全栈方向发展的前端工程师,不妨可以尝试将国际化这一需求服务化,使用 Node.js 来封装一个国际化中间件,在每个请求返回前对其返回值进行翻译处理。\u003C\u002Fp\u003E\u003Cp\u003E因为每个公司的技术架构不同,我们暂且略过技术细节不表。但我们需要知道的是,相较于前端国际化,后端接口的国际化其实更为关键与重要。因为这涉及到我们是否能将我们的核心数据以用户可理解的语言展现出来,而国际化也绝不仅仅是将几个字符串翻译为对应语言那样简单。\u003C\u002Fp\u003E\u003Ch2\u003E哪些数据需要做国际化\u003C\u002Fh2\u003E\u003Cp\u003E在讨论具体的国际化方案之前,我们首先要明确一个问题,那就是产品中的哪些数据是需要做国际化的。\u003C\u002Fp\u003E\u003Cp\u003E简而言之,除去后端返回的数据,所有在前端渲染的单词,语句,以及嵌套在其中的数据,都需要做相应的国际化。对应到代码层面,需要保证代码中没有任何一行硬编码的字符串与符号。不论是大到一个区块标题,还是小到一个确认按钮的文案,所有的展示信息都需要做国际化。\u003C\u002Fp\u003E\u003Ch2\u003E键值对匹配与多语言支持\u003C\u002Fh2\u003E\u003Cp\u003E回到前端,让我们从最简单的国际化场景说起。\u003C\u002Fp\u003E\u003Cp\u003E例如下拉列表输入框中的“选择”占位符,假设我们需要同时将其翻译为英文与法文,首先我们需要引入两个语言文件:\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003E\u002F\u002F en-US.json\n{\n
\&web_select\&: \&Select\&\n}\n\n\u002F\u002F fr-FR.json\n{\n
\&web_select\&: \&Sélectionner\&\n}\u003C\u002Fcode\u003E\u003Cp\u003E并提供一个全局的 \u003Ccode class=\&inline\&\u003ElocaleUtil.js\u003C\u002Fcode\u003E,支持传入语言类型与 key 值,并返回相应的翻译。\u003C\u002Fp\u003E\u003Cp\u003E这里提供两点最佳实践。\u003C\u002Fp\u003E\u003Cp\u003E一是将不同语言的翻译存在独立的 json 文件中。虽然我们可以使用嵌套的数据结构将所有翻译都存储在一个 locale.json 里面,但考虑到生产环境中语言文件一般都是按需加载的,所以根据不同的语言存在对应的独立的的 json 文件中显然是一个更好的选择。\u003C\u002Fp\u003E\u003Cp\u003E二是同一语言中 key 值的命名,同样不建议采取嵌套的结构。扁平化的语言文件可读性更强,取值时的效率也更高,同时也可以使用下划线来区别不同的层级,如 \u003Ccode class=\&inline\&\u003Eweb_homepage_banner_title\u003C\u002Fcode\u003E,即\u003Ccode class=\&inline\&\u003E平台_页面_模块_值\u003C\u002Fcode\u003E,当然具体情况也可以按需调整。\u003C\u002Fp\u003E\u003Ch2\u003E模板匹配与条件运算符\u003C\u002Fh2\u003E\u003Cp\u003E了解了最简单的场景,我们再来考虑一个复杂些的用例。\u003C\u002Fp\u003E\u003Cp\u003E在显示商品价格时,为了可扩展性等多方面的考虑,后端在设计表结构时,是不会将商品价格直接存储为字符串的,而是拆分为货币符号(\u003Ccode class=\&inline\&\u003Estring\u003C\u002Fcode\u003E 类型)及价格(\u003Ccode class=\&inline\&\u003Efloat\u003C\u002Fcode\u003E 类型)。而在前端显示时,我们经常会遇到要将其渲染为一句促销语的场景,如:\u003C\u002Fp\u003E\u003Ccode lang=\&text\&\u003E日前购买,只需100元。\u003C\u002Fcode\u003E\u003Cp\u003E对于时间类数据的国际化方案,我们这里先暂时按下不表,有兴趣的同学可以研究一下 \u003Ca href=\&https:\u002F\u002Fmomentjs.com\u002F\&\u003Emoment.js\u003C\u002Fa\u003E 的实现,moment.js 也是目前前端届日期国际化的代表。\u003C\u002Fp\u003E\u003Cp\u003E由于100元是一个动态的变量,所以我们的 \u003Ccode class=\&inline\&\u003ElocaleUtil.js\u003C\u002Fcode\u003E 还需要支持传入变量,这里一个常用的调用可以为:\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003ElocaleGet(\n
'en-US', \u002F\u002F locale\n
'web_merchantPage_item_promotion', \u002F\u002F key\n
{ currency: item.currency, promoPrice: item.promoPrice }, \u002F\u002F variable\n);\u003C\u002Fcode\u003E\u003Cp\u003E语言文件中的模板可以为:\u003C\u002Fp\u003E\u003Ccode lang=\&json\&\u003E\&web_merchantPage_item_promotion\&: \&Before YYYY\u002FMM\u002FDD, purchase}

我要回帖

更多关于 www.cmyyyy.com 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信