Node.js建站笔记-使用react和react-router取代Backbone
斟酌之后,决定在《嗨猫》项目中引入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>
);
}
});
- FormBox组件与Nav组件一样,是有状态的,根据状态值控制对应表单的display;
{this.props.children}
位置接收子节点,下文后讲解如何实现;- jsx语法不能直接使用
style='display:none'
这种原始写法,必须写成上述代码中的格式,并且类似margin-top
这种属性,必须写成与js语法相同的驼峰式marginTop
。
2.2.3 登录&注册表单组件
登录&注册form组件有以下几点注意:
- Login和Signup组件是render和react-router的入口,所以组件内部需要调用Nav和FormBox以及其他组件;
- 表单中的验证码图片需要请求接口获取。
首先贴上代码(以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>
);
}
});
getInitialState
初始化Login组件的state;componentDidMount
在组件绘制时触发,本例中使用jquery实现ajax请求;- jsx中调用state的语法为
{this.state.verify_img}
; - FormBox组件调用时讲子节点写在其闭合标签内部,这一点与swig的block异曲同工。
- 另外需要注意的是,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
}]
}];
- 最外层的path指的是根目录,它调用的组件Pwd是一个空白得容器组件;
- indexRoute是进入页面默认的路由指向,本例中默认是注册表单;
- childRoutes是子路由的分发,path和component分别代表路径和对应的组件,上文提到的Nav组件中的两个a标签的href值分别对应childRoutes的path,本例中我们使用的是hash路由。
然后如下方式生产router:
render((<Router routes={routeConf}/>),container);
以上便是react-router替代Backbone的大概流程,目前遗留的问题有:
- 如何配合jquery validation实现表单验证?由于react-router每次的路由都是重新渲染dom节点,原来的dom节点被删除,导致jquery validation失效。
- 是否有比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规则。
- isNotEmpty规则的应用场景
简单来说,isNotEmpty规则存在的唯一目的,是保证进入页面之后初始状态没有错误提示信息。因为formsy的在表单创建成功之后立即进行验证。这样的规则之下,每次进入页面或者进行hash路由后,在用户输入信息之前便会显示错误提示信息,这显然是不合理的。
isNotEmpty的验证规则非常简单:
Formsy.addValidationRule('isNotEmpty',function(values,value){
let isNotEmpty = !!value;
return isNotEmpty;
});
isNotEmpty的关键逻辑在Login组件State的转换上面。参照本节最初Login组件的完整代码,将isNotEmpty的错误提示文案取值为this.state.emptyError
,验证流程如下:
- 进入页面或切换hash路由之后,formsy立即对表单进行验证,此时isNotEmpty规则返回false,显示isNotEmpty错误提示文案,但是我们不想让用户看到这个提示,所以将次文案设置为空字符串,这就是为何
this.state.emptyError
初始值为''
的原因; - 用户输入信息之后点击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的更多相关文章
- 源码讲解 node+mongodb 建站攻略(一期)第二节
源码讲解 node+mongodb 建站攻略(一期)第二节 上一节,我们完成了模拟数据,这次我们来玩儿真正的数据库,mongodb. 代码http://www.imlwj.com/download/n ...
- [慕课笔记] node+mongodb建站攻略
如何利用node+mongodb来快速搭建一个电影网站? 一:后端部分 整个网站的后端是由node.js来驱动的,所以在后端需要安装node.js,以及在这个基础之上的框架express,它能够帮助我 ...
- 初级node+express建站
我的建站经历. 我建站的原因也很简单,就仅仅想有一个linux服务器玩一玩,但是还没有想到要怎么玩,就先搭建一个web服务吧.因为我工作的时候可能会用到. 我就从头开始讲起走. 先买了个云服务器,选择 ...
- Node.js API学习笔记(一)
此文章已经发表于本人博客. Terminal(终端) 说起这个使用过linux系统的兄台一般都会知道的,本人理解:类似Putty这些ssh工具通过 软件来实现远程控制主机,对于我们使用者来说,它会显示 ...
- 《深入浅出Node.js》学习笔记(一)
看了朴灵前辈的node.js系列文章,很开阔视野,虽然能力有限还是有很多不懂,但是还是希望能写下笔记,初步对node.js有点了解. 一.概念 Node.js不是JS应用.而是JS运行平台 Node. ...
- node.js day01学习笔记:认识node.js
Node.js(JavaScript,everywhere) 1.Node.js 介绍 1.1. 为什么要学习Node.js 企业需求 + 具有服务端开发经验更好 + front-end + back ...
- Node.JS学习——学习笔记
Node.JS--学习笔记 2020年02月23日11:52:01 我打算自学NodeJS-通过阅读NodeJS官网来完成. https://nodejs.org/dist/latest-v13.x/ ...
- Node.js入门学习笔记(三)
基于事件驱动的回调 这个问题不好回答,不过这是Node.js原生的工作方式.它是事件驱动的,这也是它为什么这么快的原因.你可以花一点时间阅读一下Felix Geisendörfer的大作 Unders ...
- Node.js入门学习笔记(一)
先来个最常见的"Hello World!". 打开你最喜欢的编辑器(我用的是Sublime Text),创建一个helloWorld.js的文件.我们要做的就是向stdout输出& ...
随机推荐
- C#中服务端接受前端JSON字符串转换成字典集合
我们是否可以把从前端接受的JSON字符串转换成字典集合呢? 比如从前端接收:{'size':'10', 'weight':'10kg'} 在服务端转换成:[{size:"10"}, ...
- Utopian Tree in Java
The Utopian tree goes through 2 cycles of growth every year. The first growth cycle occurs during th ...
- 努力学习 HTML5 (1)—— 初体验
HTML5 代表未来:W3C ( World Wide Web Consortium, 万维网联盟) 已经放弃 XHTML,从而使 HTML5 成为正式标准并得到认可. 最简单的 HTML5 文档 & ...
- Bitmap和Drawable相互转换方法
很多开发者表示,不知道Android的Drawable和Bitmap之间如何相关转换.下面给大家两种比较简单高效的方法. 一.Bitmap转Drawable Bitmap bm=xxx; //xxx根 ...
- 解决adb shell input text 中文输入,unicode转utf-8
https://github.com/senzhk/ADBKeyBoard 上面这个是外国人写的一个输入法,我们把它安装再设置下就ok了 直接下载bin下的ADBKeyBoard.apk文件,或者上面 ...
- Redis监控技巧(转)
来自:http://blog.nosqlfan.com/html/4166.html Redis 监控最直接的方法当然就是使用系统提供的 info 命令来做了,你只需要执行下面一条命令,就能获得 Re ...
- IOS APP 国际化 程序内切换语言实现 不重新启动系统(支持项目中stroyboard 、xib 混用。完美解决方案)
上篇 IOS APP 国际化(实现不跟随系统语言,不用重启应用,代码切换stroyboard ,xib ,图片,其他资源 介绍了纯代码刷新 实现程序内切换语言. 但效率底下,也存在一些问题.暂放弃. ...
- Linux 磁带机备份完全攻略
一.确定数据备份策略 首先必须确定在备份过程中操作哪些文件.在商业环境中,这是非常困难的一个决定,而且会产生严重的影响.如果备份了太多数据,会导致备份系统的成本过于庞大,会削减其他方面的开支.如果没有 ...
- Swift编程语言SequenceType协议中的一些比较有用的接口
在Swift编程语言中,大部分容器类(比如Array.Dictionary)都实现了SequenceType协议.SequenceType协议中有不少有趣且简便的方法可用来实现我们不少实际需求.这里将 ...
- Android开发(二十四)——数据存储SharePreference、SQLite、File、ContentProvider
Android提供以下四种存储方式: SharePreference SQLite File ContentProvider Android系统中数据基本都是私有的,一般存放在“data/data/程 ...