最近基于React + Redux + Webpack + ECMAScript6做了丁香调查新版问卷项目。
整个问卷前后端分离,前端渲染完全由数据驱动。
现将经验总结如下:
大体的分享思路为:技术选型说明 -> 简单介绍React -> 使用ES6编写React组件 -> Redux -> 整个项目的工程化构建 -> 一些小技巧 -> 反思总结
项目要求
- 前后端分离
- 移动端、pc端使用同一套代码,要求等比例缩放页面
- 除IE8及IE8以下版本浏览器不需要支持,其余浏览器要表现良好
- 尽可能减少后期维护成本
- 支持问卷的二次定制
项目特点
两个字即可以概括:复杂
- 交互
- 业务逻辑
技术方案
React + Redux + Webpack + ECMAScript6
选择该技术方案原因
选择的原则:选择最合适的
确定方案之前,可选择方案主要有:
- 使用操作DOM的js库。比如:jQuery/Zepto
- 使用MVC(MVVM)框架。比如:Backbone、Angular
- 使用专注于MVC中View层(界面)的解决方案。比如:Vue、React
排除其他方案的主要理由:
jQuery(write less do more)
- 仅仅是一个js库,不适合做交互和业务逻辑很复杂的大中型项目
- 每个人编写的代码质量不同,风格不同增加了维护成本。
题外话: jQuery/Zepto会慢慢(or 已经)过时
过时不代表你就一定不可以再用,或者要从现有项目中清除抛弃掉。
项目维护和管理本身是另一回事情,并不是完全由技术因素决定的。
Backbone
- 要定义多个类才能实现一个功能
- 每个人编写的代码质量不同,风格不同增加了维护成本。
Angular
- 过重
- 移动端性能
- 短期内团队成员接手维护困难
Vue
Vue.js 不支持 IE8 及其以下版本,因为 Vue.js 使用了 IE8 不能实现的 ECMAScript 5 特性
- 项目浏览器兼容要IE8+
- 短期内团队成员接手维护困难
React
React 本身其实还算简单的。
从面向对象编程的角度,一切皆为对象(Object)。对象由一个类(Class)有自己的 属性 和 方法。
延展到React中:
类 —> 组件(Component)
属性 —> state 和 props
方法 —> 方法
最简单的理解,一个组件的渲染函数就是一个基于 state 和 props 的纯函数,state 是自己的,props 是外面来的,
任何 state 或者 props 变了就重新渲染一遍。
一个例子:
我这个人在大千世界中就可以看做为一个 组件(Component)。
我有自己的属性(props),比如:性别男;有两只手;出生在吉林…
我还有自己的状态(state),比如:第一次在团队中做技术分享时,是略紧张并快乐的;16年夏天的我是一个胖子;近视…
如何区分state 和 props?
简单地对每一项数据提出三个问题:
是否是从父级通过 props 传入的?如果是,可能不是 state 。
是否会随着时间改变?如果不是,可能不是 state 。
能根据组件中其它 state 数据或者 props 计算出来吗?如果是,就不是 state
特点
- 作为MVC架构的V层。可以在新项目中完全使用React,也可以作为一个小特征轻易地在已有项目中使用。
- 虚拟DOM
- 单向响应的数据流
- Declarative(声明式) 代码运行效果可预测性更高,更容易debug。(声明式的把视图组件写好,当数据变化时,React去负责正确的渲染页面且仅会更新变化的部分。)
- Component-Based
- Learn Once, Write Anywhere 服务端Node.js渲染/Ract Native
JSX(JavaScript XML)
当对React组件有了基本的概念之后,我们可以设想一下:我们现在已经有了一个用来创建Views的组件(类),如果这个组件
可以做到渲染出页面,那么必然会有一个用于渲染的方法。
如果是我们自己来实现这样一个渲染方法,大概做法是:渲染方法中接收state 或者 props,返回一坨html字符串。
回想在使用jQuery中,或许我们写过很多次类似的函数。
但是这类函数有一些弊端:不易阅读、容易出错、难以复用…
So,创作React的那群哥们儿弄出来一个叫JSX的东西。
JSX 是 JavaScript语法糖。仅此而已,如果非得加一个词来描述这个语法糖,那就是 好用的 语法糖。
对语言功能没有影响,旨在提高代码可读性,使开发者更容易使用,减少代码出错的概率。
生命周期
人(组件)是会生老病死的。无论是在哪一个状态,都是有一些方法来改变这个人(组件)的。 — 李小帅
目的:挂载、更新、移除阶段 改变组件
如何创建一个React组件
第一步:拆分用户界面为一个组件树
- 单一功能原则:理想状态下一个组件应该只做一件事,假如它功能逐渐变大就需要被拆分成更小的子组件。
- 检查数据模型结构是否正确。这是因为用户界面和数据模型在 信息构造 方面都要一致,这意味着将你可以省下很多将 UI 分割成组件的麻烦事。你需要做的仅仅只是将数据模型分隔成一小块一小块的组件,以便它们都能够表示成组件。
第二步: 利用 React ,创建应用的一个静态版本
- 将数据模型渲染到 UI 上,但是没有交互功能
- 仅通过 props 传递数据(state 仅用于实现交互功能)
第三步:识别出最小的(但是完整的)代表 UI 的 state
- 关键点在于精简:不要存储重复的数据。
第四步:确认 state 的生命周期
明确哪个组件会改变或者说拥有这个 state 数据模型。
第五步:添加反向数据流
- 传递一个回调函数
- ReactLink插件
示例
Footer组件
ECMAScript6
let/const
let
- 用来声明变量
- 用法类似于var
- 所声明的变量,只在let命令所在的代码块内有效
- for循环的计数器,很合适使用let
- 不存在变量提升。变量一定要在声明后使用,否则报错
- 不允许重复声明
const
- 声明一个只读的常量
- 一旦声明,常量的值就不能改变
解构赋值
ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
注意,ES6内部使用严格相等运算符(===),判断一个位置是否有值。
所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。
解构可用于数组、对象、字符串、数字、布尔值、函数的参数。
示例:
1 | var user_info = this.props.user_info; |
1 | const { user_info, survey_info, items_info, buttons_info } = this.props; |
or
1 | import { connect } from 'react-redux'; |
字符串的扩展
模板字符串
传统的JavaScript语言,输出模板通常是这样写的。
1 | $('#result').append( |
上面这种写法相当繁琐不方便,ES6引入了模板字符串解决这个问题。
1 | $('#result').append(` |
函数的扩展
箭头函数
ES6允许使用“箭头”(=>)定义函数。
示例:1
2
3let mapStateToProps = (state, ownProps) => {
}
or1
2
3
4
5
6
7
8
9
10
11
12
13const { optionvalue } = this.props.item;
let info = {
rates: [],
unknow: []
};
optionvalue.forEach(optionObj => {
if (isNaN(parseInt(optionObj.label))) {
info.unknow.push(optionObj);
} else {
info.rates.push(optionObj);
}
});
对象的扩展
属性的简洁表示法
ES6允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。
ES6允许在对象之中,只写属性名,不写属性值。这时,属性值等于属性名所代表的变量。
1 | let mapStateToProps = (state, ownProps) => { |
属性名表达式
如果使用字面量方式定义对象(使用大括号), 用表达式作为对象的属性名,即把表达式放在方括号内。
1 | // 实时存储 |
Object.is()
用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
不同之处只有两个:一是+0不等于-0,二是NaN等于自身。
1 | +0 === -0 //true |
Object.assign()
Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
Object.assign拷贝的属性是有限制的,
只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。
Object.assign方法实行的是浅拷贝,而不是深拷贝。
也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
1 | Object.assign(target, source1, ..., sourcen); |
示例:1
2
3case QuestionTypes.RATE_MIX:
Object.assign(errors, mixValidator(item, values));
break;
对象的扩展运算符
ES7有一个提案,将Rest解构赋值/扩展运算符(…)引入对象。Babel转码器已经支持这项功能。
示例:1
2
3
4
5
6
7<input type={ inputType }
autoComplete="off"
className="input-single-text"
disabled={ disabled }
{...field}
onBlur={this.handleBlur.bind(this) }
/>
or
1 | let copyState = {...state}; |
Set和Map数据结构
Set示例:1
2
3
4
5
6
7
8
9
10
11// 用户输入重复值验证
let values = [];
for (let itemid in fieldValues) {
if (fieldValues[itemid]) {
values.push(fieldValues[itemid]);
}
}
const set = new Set(values);
if (set.size < values.length) {
errorInfo = '答案内容请勿重复';
}
Map示例:
1 | case QuestionTypes.RATE_MIX: |
Class
基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,
新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。
1 | import { Component } from 'react'; |
修饰器
修饰器(Decorator)是一个函数,用来修改类的行为。这是ES7的一个提案,目前Babel转码器已经支持。
修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。
示例:1
2
3
4
5
6
7
8
9
10
11
12import { sortable } from 'react-anything-sortable';
class SortableItem extends React.Component {
render() {
return (
<div {...this.props}>
{this.props.children}
</div>
);
}
}
Module
JavaScript开发大型的、复杂的项目的巨大障碍:没有模块(module)体系。
ES6之前,社区制定了一些模块加载方案:
- 浏览器 AMD
- 服务器 CommonJS
ES6在语言规格的层面上,实现了模块功能,而且实现得相当简单,完全可以取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。
ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
CommonJS和AMD模块,都只能在运行时确定这些东西。
Node的默认模块格式是CommonJS,目前还没决定怎么支持ES6模块。
所以,只能通过Babel这样的转码器,在Node里面使用ES6模块。
export default命令
其他模块加载通过export default命令导出的模块时,import命令可以为该匿名函数指定任意名字。
模块的整体加载
示例:
1 | import * as QuestionTypes from '../../constants/QuestionTypes'; |
Redux
跨组件通信?多组件共享状态?多人协作的可维护性?
Webpack
区分开发和生产环境
图片按需加载
1 | module: { |
publicPath
异步请求的状态管理
每道题可能发n个请求,题目内请求取最新即可。
题目间请求结果(最好)保证有序执行。
前端log系统
及时发现并定位bug、记录异常
一些技巧和处理方案
Babel
webpack-dev-server
cross-env
统一Mac、Linux、Windows命令行的差异
reqwest
redux-form
高阶函数的使用
示例:表单预览
移动端、PC端判断
判断:
1 | if (ua.indexOf("Android") != -1 || ua.indexOf("iPhone") != -1 || ua.indexOf("iPad") != -1) { |
使用:
1 | import classnames from 'classnames'; |
配合级联组件使用:
1 | const DesktopOrMobile = window.isMobile ? 'Mobile' : 'Desktop'; |
浏览器类型和版本号判断
1 | export default function Broswer() { |
Google Analytics
没有采用由后台添加到页面,而是前端直接在js代码中添加如下代码:
1 | // Google Analytics |
初始化:
1 | ga('create', userId, 'auto'); |
页面打点:
1 | ga('send', 'pageview', url); |
事件打点:
1 | ga('send', 'event', eventCategory, action); |
微信分享带缩略图
“作弊方法”
常规方法
移动端、PC端两套样式
方案:通过 classnames 和 window.isMobile 决定应用根组件的class名,然后使用less来定制某一端的样式即可。
应用根组件:
import React from 'react';
import classnames from 'classnames';
export default class Div extends React.Component {
render() {
const { app_root } = this.props;
return (
<div className={
classnames({
"app-root": app_root,
"pc": !window.isMobile,
"preview": window.preview
})
}>
{ this.props.children }
</div>
);
}
}
隐藏IE浏览器输入框自带的清除按钮
input::-ms-clear {
display: none;
}
自动定位有错误的题目
在渲染题目时,给其 id 属性,根据 id 获取错误题目的坐标 (x, y)
window.scroll(x, y);
总结
收获
- 整个应用由数据驱动,更快的定位bug
- 较低的后期维护成本
- 一套可复用的React组件
不足
- 较少的使用 PropTypes
- 缺少静态类型检测(Typescript)
- 缺少测试代码
- 编码风格与小组规范不统一
- 应该使用react-router
经验
应用数据结构的设计很重要
- 数据结构尽可能扁平化
类型检查很重要
外来的数据一定要验证数据类型,格式正确才可以继续往下走。
不要完全相信后台给过来的数据。
尽可能记录下来沟通结果
方便出问题时进行沟通
解决问题方法
- 不要带着情绪去解决问题
- 或许需要的仅仅是休息五分钟,或者暂时放下这个问题明天再做
- 再认真冷静的读一读官方文档
- 联系作者(Github、Email等)
- 请教身边伙伴
沟通
目标:为了高效合理的解决问题
与后台:
- 某功能的实现由谁来做?
性能问题?
- 改接口(数据格式、增删字段)
与产品:
某功能更合适(代码角度、设计角度等)的实现方案
时间节点
按时/提前高质量完成PM最重要
反思
React带给我们了什么?
组件化开发方式的一些思考
高度易维护
……
Redux带给我们了什么?
大大减低了状态的保存与管理的成本,此外我们还可以通过一定的手段,复现过去的动作。
……
技术参考
Redux Form - The best way to manage your form state in Redux.