斟酌之后,决定在《嗨猫》项目中引入react,整体项目偏重spa模式,舍弃部分server端的模板渲染,将一部分渲染工作交给前端react实现。

react拥有丰富的组件,虽然不如Backbone和underscore这对老基友成熟,但考虑到嗨猫的前端并不需要很多的MV*架构,目前使用到Backbone的地方只有hash路由而已,所以最终决定使用react-router取代Backbone,underscore也从项目依赖中移除。

下面就以登录&注册页为例,简单讲述整个替代过程。

1. 安装并二次编译react-router

因为项目前端仍然使用AMD规范,使用bower install react-router安装后的react-router是原始的ES6 module规范,不能兼容AMD规范。

react-router源码中提供了编译配置文件scripts/build.js,进入react-router根目录执行:

npm install

安装依赖工具之后执行:

node scripts/build.js

编译成功后生成lib和umd两个文件夹,lib目录下的是CommonJS规范的文件,umd目录下是UMD规范文件,项目中前端使用的是umd目录下的文件。

编译完毕之后配置/assets/global/js/dev/main.es中的requirejs的配置项:

paths: {
"jquery": 'jquery/jquery.min',
"requirejs": 'requirejs/require',
"react": 'react/react',
'react-dom': 'react/react-dom',
"react-router": "react-router/umd/ReactRouter.min",
"jqSlidejs": 'jquery-slide/jqSlide.min',
'jqValidate': 'jquery-validation/dist/jquery.validate.min'
}

配置完毕后便可以在其他js文件中直接使用import关键字引入react-router组件。

2. 引入React并编写前端组件

以下改的均是在登录注册页的主要js文件/assets/components/passport/js/dev/main.es中进行。

2.1 首先引入react和react-dom。

React的新版本将react-dom分离出来专注于组件的render,原来的React.render函数被弃用。

import React from 'react'
import { render } from 'react-dom'
2.2 编写组件

将server端的swig模板进一步简化,除了logo区域之外的UI由react渲染,swig模板只提供一个外层容器:

<div class="hc_pwd_box"></div>

这个容器便是react组件的append dom的目标,我们首先将它储存起来:

let container = $('.hc_pwd_box')[0];

react的render方法不支持jquery对象,必须是原始的dom节点。

2.2.1 nav组件

需要注意,nav是有状态的,tab文字下方的黑条指示当前的显示表单是注册还是登录,所以在编写nav组件是需要用到props,代码如下:

// nav 组件
const Nav = React.createClass({
render(){
let mode = this.props.mode;
let class_login = '',class_signup = '';
if(mode === 'login'){
class_login = 'box_nav_item box_nav_item_login box_nav_item-current';
class_signup = 'box_nav_item box_nav_item_signup';
}else{
class_signup = 'box_nav_item box_nav_item_signup box_nav_item-current';
class_login = 'box_nav_item box_nav_item_login';
}
return(
<ul className="box_nav_list">
<li className={class_signup}>
<a className="item_link" href="#signup">注册</a>
<div className="item_tray"></div>
</li>
<li className={class_login}>
<a className="item_link" href="#login">登录</a>
<div className="item_tray"></div>
</li>
</ul>
);
}
});

jsx中class的声明必须使用className的写法。

上述代码中的this.props.mode是生成nav组件时传入的数据,然后组件内部根据这个数据判断显示哪个指示条。生成Nav组件的代码如下:

<Nav mode={'login'} />

Nav组件内部this.props.mode的值便是'login'

2.2.2 form表单的容器组件

容器组件最大的特性就是可以接收子节点,这里需要用到react中的this.props.children,它的作用于swig模板的block有相似之处,但是不能像block那样命名。FormBox组件代码如下:

// form容器组件
const FormBox = React.createClass({
render(){
let mode = this.props.mode;
let current = 'box_form_container box_form_container_'+mode;
return (
<div className={current}>
<iframe style={{display:'none'}} name='target_ifr'></iframe>
{this.props.children}
</div>
);
}
});
  1. FormBox组件与Nav组件一样,是有状态的,根据状态值控制对应表单的display;
  2. {this.props.children}位置接收子节点,下文后讲解如何实现;
  3. jsx语法不能直接使用style='display:none'这种原始写法,必须写成上述代码中的格式,并且类似margin-top这种属性,必须写成与js语法相同的驼峰式marginTop

2.2.3 登录&注册表单组件

登录&注册form组件有以下几点注意:

  1. Login和Signup组件是render和react-router的入口,所以组件内部需要调用Nav和FormBox以及其他组件;
  2. 表单中的验证码图片需要请求接口获取。

首先贴上代码(以Login组件为例):

// 登录form组件
const Login = React.createClass({
getInitialState(){
return {
verify_img: ""
};
},
componentDidMount(){
$.ajax({
url: '/getverifycode',
type: 'get',
dataType: 'json'
}).done(function(res) {
if(this.isMounted()){
this.setState({
verify_img: res.img
});
}
}.bind(this));
},
render(){
return(
<div>
<Nav mode={'login'} />
<FormBox mode={'login'}>
<form action="/login" method="post" className="login_form" target='target_ifr'>
<div className="form_item form_item_input">
<input type="text" name='signname' className="form_input hc_input hc_input_grey hc_input_borderdash" placeholder="用户名或邮箱" />
<span className="form_input_placeholder"></span>
<div className="form_error"></div>
</div>
<div className="form_item form_item_input">
<input type="password" name='password' className="form_input hc_input hc_input_grey hc_input_borderdash" placeholder="密码" />
<span className="form_input_placeholder"></span>
<div className="form_error"></div>
</div>
<div className="form_item form_item_input">
<input type="text" name='verifycode' className="form_input form_input_verifycode hc_input hc_input_grey hc_input_borderdash" placeholder="验证码"/>
<span className="form_input_placeholder"></span>
<img src={this.state.verify_img} alt="验证码" className="form_img_verifycode"/>
<div className="form_error"></div>
</div>
<div className="form_item form_item_submit">
<button type="submit" className="hc_btn hc_btn_orange form_btn_submit">登录嗨猫</button>
</div>
</form>
</FormBox>
<Thirdparty />
</div>
);
}
});
  1. getInitialState初始化Login组件的state;
  2. componentDidMount在组件绘制时触发,本例中使用jquery实现ajax请求;
  3. jsx中调用state的语法为{this.state.verify_img};
  4. FormBox组件调用时讲子节点写在其闭合标签内部,这一点与swig的block异曲同工。
  5. 另外需要注意的是,jsx中像img、input这类标签,必须不能遗漏闭合的斜杠/,否则会报语法错误。
2.3 配置react-router

react-router的配置比较简单,参照文档编写如下配置项:

const routeConf = [{
path: '/',
component: Pwd,
indexRoute: {component: Signup},
childRoutes: [{
path: 'login',
component: Login
},{
path: 'signup',
component: Signup
}]
}];
  1. 最外层的path指的是根目录,它调用的组件Pwd是一个空白得容器组件;
  2. indexRoute是进入页面默认的路由指向,本例中默认是注册表单;
  3. childRoutes是子路由的分发,path和component分别代表路径和对应的组件,上文提到的Nav组件中的两个a标签的href值分别对应childRoutes的path,本例中我们使用的是hash路由。

然后如下方式生产router:

render((<Router routes={routeConf}/>),container);

以上便是react-router替代Backbone的大概流程,目前遗留的问题有:

  1. 如何配合jquery validation实现表单验证?由于react-router每次的路由都是重新渲染dom节点,原来的dom节点被删除,导致jquery validation失效。
  2. 是否有比jquery validation更好的选择?

2015.12.2更新

3. 使用formsy-react取代jquery-validation

引入React的一个非常麻烦的事情是,react-router每次切换路径都会重绘dom,导致原来由jquery选定并保存的dom对象与重绘后的dom不一致。

如果是事件响应,可以使用dalegation处理,但是jquery validation插件并不支持类似dalegation的机制,这令两者的兼容面对一个死结。最终,奔着劲量减少耦合的目标(其实是没有研究出箭筒react-router和jquery validation的方案),决定使用react的表单验证组件formsy-react(下文简称为formsy)取代jquery validation,下文简单记录一下整个替代过程。

3.1 安装formsy-react并配置依赖

在项目根目录下执行:

bower install formsy-react --save

formsy安装在第三方库目录/assets/global/libs/下,formsy的目录结构如下:

src目录下是CommonJS规范的源文件,release目录下是编译后的umd规范文件,也是我们将在项目中引入的文件。在global/js/dev/main.es中的path中添加如下配置:

'formsy-react': 'formsy-react/release/formsy-react',

formsy安装成功后,便面临一个问题:前端react组件的重构。

3.2 react组件重构

使用formsy的前提是:form表单必须使用<Formsy.Form>生成,所以之前直接使用原始<form>生成的react组件必须重构为formsy格式。

3.2.1 创建组件库

之所以在此时创建组件库,一方面是为了迎合formsy,但这并不是主要目的。随着项目规模的扩大,组件库是必须的一部分。

以formsy的需求为例,组件库的创建过程如下:

1.新建文件global/js/dev/UIComponents.es(目录不固定,暂时存于此);

2.引入依赖:

import React from 'react';
import Formsy from 'formsy-react';

3.创建formsy-react组件HCInput:

/**
* react-input组件
*/
export const HCInput = React.createClass({
mixins: [Formsy.Mixin],
changeValue(event) {
this.setValue(event.currentTarget[this.props.type === 'checkbox' ? 'checked' : 'value']);
},
render(){
const className = 'form_item form_item_input' + (this.props.className || ' ') + (this.showRequired() ? 'required' : this.showError() ? 'error' : null);
const errorMessage = this.getErrorMessage();
return(
<div className={className}>
<input type={this.props.type || 'text'} name={this.props.name} className={this.props.inputClass} placeholder={this.props.placeholder} onChange={this.changeValue} value={this.getValue()} checked={this.props.type === 'checkbox' && this.getValue() ? 'checked' : null}/>
<span className="form_input_placeholder" style={{display:'none'}}></span>
{this.props.children}
<div className="form_error">{errorMessage}</div>
</div>
)
}
});

编写formsy组件时有几点需要注意(规范):

  • this.getErrorMessage() 获取的是调用组件时传入的validationError参数值;
  • onChange事件是不可缺少的,用来触发formsy的验证逻辑;

另外,根据项目需求,验证码部分需要在HCInput组件内安置验证码图片的dom,所以HCInput组件接受children组件this.props.children

4.将组件加入依赖配置

UIComponents组件将会成为项目中的基础依赖被多个场景使用,所以将它加入依赖配置文件,方便开发工作。在global/js/dev/main.es中的path中添加如下配置:

// 自定义组件
'UIComponents': './../js/prod/UIComponents'

3.2.2 Login组件重构

组件库创建完毕后,开始进行前端react组件的重构工作,以下内容以Login组件为例。

Login组件的render方法重构后的结构如下:

render(){
return(
<div>
<Nav mode={'login'} />
<FormBox mode={'login'} >
<Formsy.Form action='/login' method="post" className="login_form" target='target_ifr' onSubmit={this.submit} mapping={this.mapInputs} onValid={this.enableSubimit} onInvalid={this.disableSubimit}>
<HCInput
type="text"
name='signname'
placeholder="用户名或邮箱"
inputClass='form_input hc_input hc_input_grey hc_input_borderdash'
validations='isSignname,isNotEmpty'
validationErrors={{
isSignname: '请输入正确的用户名或邮箱',
isNotEmpty: !!this.state.emptyError ? '请输入用户名或邮箱': ''
}}
/>
<HCInput
type="password"
name='password'
placeholder="密码"
inputClass='form_input hc_input hc_input_grey hc_input_borderdash'
validations='isNotEmpty'
validationErrors={{
isNotEmpty: !!this.state.emptyError ? '请输入密码': ''
}}
/>
<HCInput
type="text"
name='verifycode'
placeholder="验证码"
inputClass='form_input form_input_verifycode hc_input hc_input_grey hc_input_borderdash'
validations='isNotEmpty'
validationErrors={{
isNotEmpty: !!this.state.emptyError ? '请输入验证码': '',
isLength: '验证码不正确'
}}>
<img src={this.state.verify_img} alt="验证码" className="form_img_verifycode"/>
</HCInput>
<div className="form_item form_item_submit">
<button type="submit" className="hc_btn hc_btn_orange form_btn_submit">登录嗨猫</button>
</div>
</Formsy.Form>
</FormBox>
<Thirdparty />
</div>
);
}

下面逐条讲解重构细节。

1.<Formsy.Form>

除了标签不同以外,其他语法与常规react组件相同,需要注意的是几个监听函数:

  • onSubmit:用于拦截表单默认的submit行为,这一点与jquery validation的submitHandler功能相同;
  • mapping:将表单中各个input元素映射为自定义的Object。mapping并不是必须的;
  • onValid:表单中各元素都验证通过后触发;
  • onInvalid:与onValid相反,表单中任何一个元素验证不通过就会触发onInvalid,一般与onValid配合控制submit开关。

2.submit开关控制

前文提到使用onInvalid和onValid对submit进行开关控制,需要配合React组件的State实现。

首先在Login组件的getInitialState()方法中添加canSubmit作为submit开关:

getInitialState(){
return {
verify_img: "",
canSubmit: false
};
}

然后再onValid和onInvalid对应的响应函数中添加开关逻辑:

enableSubimit(){
this.setState({
canSubmit: true
});
},
disableSubimit(){
this.setState({
canSubmit: false
});
}

最后,在onSubmit对应的响应函数中根据开关判断是否提交表单:

submit(data){
//开关off时不提交
if(!this.state.canSubmit){
return;
}
// ajax提交表单
}

3.扩展formsy的验证规则

formsy自带的验证规则并不能完全满足项目的需求,比如登录时支持用户名和邮箱共享一个input,然后通过正则分发。formsy并没有这种混合验证的需求,所以我们需要对其验证规则进行扩展。

formsy提供了addValidationRule()API用来扩展验证规则。以signname为例,简单讲述一下扩展方法。

之前使用jquery validation已经完成了isSignname的验证规则制定,现在我们将它迁移到formsy,在UIComponents.es中添加代码如下:

/**
* @desc 登录名判断 - 纯文字或邮箱
*/
Formsy.addValidationRule('isSignname',function(values,value){
let reg_isemail = /[@]/,
reg_email =
/^[a-z]([a-z0-9]*[-_]?[a-z0-9]+)*@([a-z0-9]*[-_]?[a-z0-9]+)+[\.][a-z]{2,3}([\.][a-z]{2})?$/i;
return !reg_isemail.test(value) || (reg_isemail.test(value) &&
reg_email.test(value));
});

这样,在Login组件内便可以使用isSignname验证规则:

<HCInput
type="text"
name='signname'
placeholder="用户名或邮箱"
inputClass='form_input hc_input hc_input_grey hc_input_borderdash'
validations='isSignname,isNotEmpty'
validationErrors={{
isSignname: '请输入正确的用户名或邮箱',
isNotEmpty: !!this.state.emptyError ? '请输入用户名或邮箱': ''
}}
/>

上述代码中的isNotEmpty也是我们自定义的验证规则,随后将会详细讲解为何不使用formsy自带的required验证规则。

formsy多个验证规则可以按上述代码搬使用逗号分隔,也可以写成类似validationErrors的格式

存在多个validation错误提示文案是必须使用validationErrors,注意是复数形式,如果写成validationError会解析出错。

下面解释一下为何需要自定义的isNotEmpty替代formsy自带的required规则。

  1. isNotEmpty规则的应用场景

简单来说,isNotEmpty规则存在的唯一目的,是保证进入页面之后初始状态没有错误提示信息。因为formsy的在表单创建成功之后立即进行验证。这样的规则之下,每次进入页面或者进行hash路由后,在用户输入信息之前便会显示错误提示信息,这显然是不合理的。

isNotEmpty的验证规则非常简单:

Formsy.addValidationRule('isNotEmpty',function(values,value){
let isNotEmpty = !!value;
return isNotEmpty;
});

isNotEmpty的关键逻辑在Login组件State的转换上面。参照本节最初Login组件的完整代码,将isNotEmpty的错误提示文案取值为this.state.emptyError,验证流程如下:

  1. 进入页面或切换hash路由之后,formsy立即对表单进行验证,此时isNotEmpty规则返回false,显示isNotEmpty错误提示文案,但是我们不想让用户看到这个提示,所以将次文案设置为空字符串,这就是为何this.state.emptyError初始值为''的原因;
  2. 用户输入信息之后点击submit按钮,触发submit函数中emptyError的设置逻辑this.setState({emptyError: '不能为空'});,在此之后,所有的验证逻辑便可以正常进行。

可能会有人疑惑为何this.setState({emptyError: '不能为空'});没有触发组件的重绘?经本人验证,只有在组件中state以某个属性直接使用(比如className={{this.state.emptyError}}这种)的情况下,setState才会触发重绘。formsy组件中,state作为formsy组件的某个配置参数,而不是直接使用,换句换说,this.state.emptyError只是作为值传入,而不是引用传入,并没有绑定关系。这种情况下setState是不会触发重绘的。

目前暂时采用的isNotEmpty方案并不是最优解,并且交互逻辑仍然有细微的问题,后续会深入研究formsy是否有原生可支持场景需求的方案。


2015.12.07更新

去除isNotEmpty验证规则,使用formsy isPristine API弥补空白验证缺陷

前文提到使用isNotEmpty配合组件的state避免hash路由切换后自动进行表单验证,导致初始进入表达后便显示错误提示。isNotEmpty规则配合state虽然能够解决这个问题,但是实现方式有些tricky。仔细研究formsy的API之后发现了isPristine,这个API的作用如下:

By default all formsy input elements are pristine, which means they are not "touched". As soon as the setValue method is run it will no longer be pristine.

默认情况下formsy的input控件都是原始值,换句话说就是它们还没有被触及。当formsy组件的setValue被调用后,input控件便不再是原始的了。

根据这个API的说明,我们可以进行这样的判断:如果input控件是原始的,那么它的错误提示便是空白的,用户便看不到错误提示。一旦组件的setValue被调用,便将错误提示替换为正常的值。根据这个规则,我们去掉与isNotEmpty规则匹配的state操作,修改HCInput控件的错误提示为:

<div className="form_error">{this.isPristine() ? '':errorMessage}</div>

这样,我们就省去了繁琐的state操作,直接使用formsy内部机制实现项目的需求了。

Node.js建站笔记-使用react和react-router取代Backbone的更多相关文章

  1. 源码讲解 node+mongodb 建站攻略(一期)第二节

    源码讲解 node+mongodb 建站攻略(一期)第二节 上一节,我们完成了模拟数据,这次我们来玩儿真正的数据库,mongodb. 代码http://www.imlwj.com/download/n ...

  2. [慕课笔记] node+mongodb建站攻略

    如何利用node+mongodb来快速搭建一个电影网站? 一:后端部分 整个网站的后端是由node.js来驱动的,所以在后端需要安装node.js,以及在这个基础之上的框架express,它能够帮助我 ...

  3. 初级node+express建站

    我的建站经历. 我建站的原因也很简单,就仅仅想有一个linux服务器玩一玩,但是还没有想到要怎么玩,就先搭建一个web服务吧.因为我工作的时候可能会用到. 我就从头开始讲起走. 先买了个云服务器,选择 ...

  4. Node.js API学习笔记(一)

    此文章已经发表于本人博客. Terminal(终端) 说起这个使用过linux系统的兄台一般都会知道的,本人理解:类似Putty这些ssh工具通过 软件来实现远程控制主机,对于我们使用者来说,它会显示 ...

  5. 《深入浅出Node.js》学习笔记(一)

    看了朴灵前辈的node.js系列文章,很开阔视野,虽然能力有限还是有很多不懂,但是还是希望能写下笔记,初步对node.js有点了解. 一.概念 Node.js不是JS应用.而是JS运行平台 Node. ...

  6. node.js day01学习笔记:认识node.js

    Node.js(JavaScript,everywhere) 1.Node.js 介绍 1.1. 为什么要学习Node.js 企业需求 + 具有服务端开发经验更好 + front-end + back ...

  7. Node.JS学习——学习笔记

    Node.JS--学习笔记 2020年02月23日11:52:01 我打算自学NodeJS-通过阅读NodeJS官网来完成. https://nodejs.org/dist/latest-v13.x/ ...

  8. Node.js入门学习笔记(三)

    基于事件驱动的回调 这个问题不好回答,不过这是Node.js原生的工作方式.它是事件驱动的,这也是它为什么这么快的原因.你可以花一点时间阅读一下Felix Geisendörfer的大作 Unders ...

  9. Node.js入门学习笔记(一)

    先来个最常见的"Hello World!". 打开你最喜欢的编辑器(我用的是Sublime Text),创建一个helloWorld.js的文件.我们要做的就是向stdout输出& ...

随机推荐

  1. 虚拟机评估——如何确定一个CPU核上部署的虚拟机数量?

    最近研究虚拟化技术,不可避免遇到一个问题:如何评估物理主机上虚拟主机的容量?下面这篇文章的思路有一定的启发性,转发一下. 如何确定一个CPU核上部署的虚拟机数量? 摘要:本文说明一个CPU核上部署虚拟 ...

  2. 使用Excel制作万年历(可打印)

    先来看看A4纸打印效果,其他功能后续继续完善中. 年份数据字典(农历节日) 农历节日表 年度 春节 元宵节 龙抬头 端午节 六月六 七月七 七月十五 仲秋节 除夕 2010年02月14日 2010年0 ...

  3. LPC43xx I2S

  4. Windows Server 2008 R2 备份和恢复 (转)

    Windows Server Backup : 1.安装Windows Server Backup的方法: 通过"服务器管理器"中的"添加功能"向导进行安装. ...

  5. 致第一次安装RIME的你

    转载自百度RIME吧,作者:半月湾C 原帖地址:http://tieba.baidu.com/p/3288634121   序言 很喜欢小狼毫输入法,喜欢他的简洁,美观以及超强悍的个人定制功能.关于 ...

  6. 参数传递的四种形式----- URL,超链接,js,form表单

    什么时候用GET,  查,删, 什么时候用POST,增,改  (特列:登陆用Post,因为不能让用户名和密码显示在URL上) 4种get传参方式 <html xmlns="http:/ ...

  7. android国际化(多语言)

    2013-03-18 23:45             13390人阅读             评论(0)             收藏              举报 1.  很大程度上,为什么 ...

  8. 【卡西欧Fx5800-p程序】01 坐标转换程序-带注释

    1.程序说明: DDSG  (大地坐标转施工坐标) SGDD  (施工坐标转大地坐标) ↙      (回车命令"EXE") K       (施工坐标转换原点在线性上的桩号,如果 ...

  9. Visual Studio 压力测试注意点

    常用的三个测试:单元测试.web性能分析.压力测试:前两个好说,压力测试的时候如果配置不注意,往往不成功. 默认压力测试的测试结果存储在微软云端的,(visual studio online),国内一 ...

  10. 如何升级TeX Live 2014宏包

    转:人大经济论坛 LATEX论坛 版,详细出处参考: http://bbs.pinggu.org/forum.php?mod=viewthread&tid=3370640&page=1 ...