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>
   );
 }
});
  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内部机制实现项目的需求了。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java成神之路

计算机_01_常用快捷键

         Windows+L   : 锁(look)    屏               

10330
来自专栏张戈的专栏

WordPress开启颜色评论但不造成XSS漏洞的小方法

前段时间分享过一些 XSS 漏洞的修复技巧,而且也说到了 WordPress 开启颜色评论需要在 functions.php 中插入如下代码,也就是禁用 Wor...

362100
来自专栏Maroon1105

WordPress之去掉顶部工具栏

用WP搭建自己博客的人都会发现网站上面有一个黑色的工具栏,影响网站美观度,那么怎么去掉顶部工具栏呐?

64900
来自专栏码生

LaunchScreen.storyboard 启动页设置图片不显示 启动页白屏

启动页设置方式有两种 一是通过LaunchScreen.storyboard设置 二是通过 Assets.xcassets 增加 iOS Launch Im...

1.3K30
来自专栏前端架构

再谈DOMContentLoaded与渲染阻塞—分析html页面事件与资源加载

浏览器的多线程中,有的线程负责加载资源,有的线程负责执行脚本,有的线程负责渲染界面,有的线程负责轮询、监听用户事件。

2K150
来自专栏咸鱼不闲

python爬虫之初恋 selenium

selenium 是一个web应用测试工具,能够真正的模拟人去操作浏览器。 用她来爬数据比较直观,灵活,和传统的爬虫不同的是, 她真的是打开浏览器,输入表单,点...

15210
来自专栏Android知识点总结

01-React搭建react环境及SCSS的配置

71020
来自专栏小程序的道路

小程序生命周期

小程序并不是 HTML5 应用,而是更偏向于传统的 CS 架构,它是基于数据驱动的模式,一切皆组件(视图组件)。下面是小程序与普通 Web App 的对比。 ...

16110
来自专栏mySoul

微信小程序插件

微信小程序插件是对一组js接口,自定义组件或页面的封装,用来嵌入微信小程序中,用来被开发者调用。

2.2K30
来自专栏极客编程

简单介绍一下vue2.0

Vue是用于构建用户界面的渐进框架。作者尤雨熙特别强调它与其他的框架不同,Vue是渐进式的框架,可以逐步采用,不必一下就通过框架去重构项目。 另外Vue的核心库...

19620

扫码关注云+社区

领取腾讯云代金券