Build a react project step by step

工程源码 git clone -b simple-project https://github.com/wangzhengquan/react-redux-tpl

搭建一个react工程

  • 实现手机的页面切换的过度效果
  • 实现代码分片的懒加载

工程搭建步骤如下

1 下载一个脚手架
git clone -b scaff https://github.com/wangzhengquan/react-redux-tpl,因为这个工程是从旧有的项目中抽取出来的,还是基于react15 和 react-router2,所以修改package.json,如下:

"react": "^15.0.0", 
"react-dom": "^15.0.0",  
"react-router": "^2.8.1",复制代码

2 新建路由文件 src/router.js
该文件时负责配置页面路由访问的

import Modals from 'react-ui/modals'

var  hideNavbar = false

const rootRoute = {
  path: '/',
  component: require('./components/View'),
  indexRoute: {
    //懒加载处理,访问该页面时才会加载相关资源
    getComponents(nextState, cb) {
      //显示loadding
      Modals.showIndicator()

      //webpack split打包处理方式
      Promise.all([
        import('./components/home/HomeNavbar'),
        import('./components/home/HomePage')
      ]).then( ([navbar, page]) => {
        cb(null, {
          navbar: hideNavbar ? null : navbar,
          page: page

        })
        document.querySelector('title').innerHTML='Home'
        Modals.hideIndicator()
      })
    }
  },

  childRoutes: [{
    path: 'product',
    getComponents(nextState, cb) {
      Modals.showIndicator()
      Promise.all([
        import('./components/product/ProductNavbar'),
        import('./components/product/ProductPage')
      ]).then( ([navbar, page])  => {
          cb(null, {
            navbar: hideNavbar ? null : navbar,
            page: page
          })
          document.querySelector('title').innerHTML='Product'
          Modals.hideIndicator()
      })
    }
  }, {
    path: 'order',
    getComponents(nextState, cb) {
      Modals.showIndicator()
      Promise.all([
        import('./components/order/OrderNavbar'),
        import('./components/order/OrderPage')
      ]).then( ([navbar, page])  => {
          cb(null, {
            navbar: hideNavbar ? null : navbar,
            page: page
          })
          document.querySelector('title').innerHTML='Product'
          Modals.hideIndicator()
      })
    }
  }]

}


export default rootRoute;复制代码

3 新建根组件文件 src/components/View.js

import React from 'react'
import ReactTransitionGroup from 'react-addons-transition-group'
import ReactUI from 'react-ui/react-ui'
import classNames from 'classnames';

class View extends React.Component{
  constructor(props) {
    super(props);
  }

  render(){
    let navbar = this.props.navbar
    let page = this.props.page
    let toolbar =this.props.toolbar

    return (

        <div className={classNames('view view-main', this.props.className)} >
          {/*-----navbar------*/}
          <ReactTransitionGroup component="div" className={classNames('navbar', {'navbar-hidden': !!!navbar})}>
             {navbar ? React.cloneElement(navbar, {
                key: (this.props.location ? this.props.location.pathname.concat('/navbar') : ReactUI.guid() ),
                pageName: location.pathname==='/' ? 'index' : location.pathname.charAt(0) === '/' ? location.pathname.substring(1) : location.pathname
              }) : '' }
          </ReactTransitionGroup>

          {/*-----pages-------*/}
          <ReactTransitionGroup className={classNames('pages')} component="div">
            {React.cloneElement(page, {
              className: {'navbar-through': !!navbar, 'toolbar-through': !!toolbar, 'tabbar-labels-through': !!toolbar},
              key: (this.props.location ? this.props.location.pathname : ReactUI.guid() ),
              pageName: location.pathname==='/' ? 'index' : location.pathname.charAt(0) === '/' ? location.pathname.substring(1) : location.pathname
            })}
          </ReactTransitionGroup>

          {/*-----Toobar-------*/}

          { toolbar ? toolbar : ''}
          {/*-----toolbar end -------*/}
        </div>
    );
  }

}

module.exports = View复制代码

其中ReactTransitionGroup 是一个基础的动画组件,当子组件从中添加或移除时会触发相关的钩子函数,如:

componentWillAppear()
componentDidAppear()
componentWillEnter()
componentDidEnter()
componentWillLeave()
componentDidLeave()复制代码

4 新建src/components/Page.js。
在上面ReactTransitionGroup提供的几个hooks(钩子)函数的里面做动画处理,所有的page都继承该组件

  import React  from 'react';
import ReactDOM from 'react-dom';
import history from '../history'
import $ from 'react-ui/dom'
import classNames from 'classnames';
const transitionDuration= 400;
const params = {

 hideNavbarOnPageScroll : true ,
 hideToolbarOnPageScroll : false ,
 hideTabbarOnPageScroll : false ,
 showBarsOnPageScrollEnd : false ,
 showBarsOnPageScrollTop : true
}



export default class Page extends React.Component{
  constructor(props) {
    super(props);
    this.destroyList = []
  }
// Set pages classess for animationEnd
  animatePages(page, action, direction, finishCallback) {
      // Loading new page
      let removeClasses = 'page-on-center page-on-right page-on-left';
      let transitionClass = null;
      let activeClass = null;

      if (direction === 'to-left') {
        if(action ==='leave') {
          transitionClass = 'page-from-center-to-left';
          activeClass = 'page-on-left'
        }
        else if(action === 'enter'){
          transitionClass = 'page-from-right-to-center';
          activeClass = 'page-on-center'
        }
      }
      // Go isBack
      else if (direction === 'to-right') {
        if(action === 'enter'){
          transitionClass = 'page-from-left-to-center';
          activeClass = 'page-on-center'
        }
        else if(action  === 'leave'){
          transitionClass = 'page-from-center-to-right';
          activeClass = 'page-on-right'
        }
      }

      page.removeClass(removeClasses).addClass(transitionClass);
      page.animationEnd(function () {
         page.removeClass(transitionClass).addClass(activeClass);
         finishCallback()
      });
  }

  componentDidMount(){
    this.node = this.node || $(ReactDOM.findDOMNode(this));
    //this.initPageScrollToolbars(node)

  }


  componentWillUnmount(){
    this.destroy()
  }

  componentWillAppear(done) {
    // console.log('componentWillAppear', this.props.location && this.props.location.pathname);
    done()

  }
  componentDidAppear () {
    // console.log('componentDidAppear', this.props.location && this.props.location.pathname);
  }

  componentWillEnter (done) {
    if( !Page.anim ){
      done()
      return;
    }
    var page = $(ReactDOM.findDOMNode(this));
    if(history.isBack){
      this.animatePages(page, 'enter', 'to-right', done)
    }else{
      this.animatePages(page, 'enter', 'to-left', done)
    }


  }
  componentDidEnter () {
    // console.log('componentDidEnter', this.props.location && this.props.location.pathname);
  }
  componentWillLeave (done) {
    // console.log('componentWillLeave', this.props.location && this.props.location.pathname , history.paths);
    if (!Page.anim ){
      setTimeout(function(){
        Page.anim = true
      }, transitionDuration)

      done()
      return;
    }
    var page = $(ReactDOM.findDOMNode(this));
    if(history.isBack){
      this.animatePages(page, 'leave', 'to-right', done)

    } else {
      this.animatePages(page, 'leave', 'to-left', done)

    }

  }
  componentDidLeave () {
    // console.log('componentDidLeave', this.props.location && this.props.location.pathname);
  }




  initPageScrollToolbars  (pageContainer) {
    var me = this
    pageContainer = $(pageContainer);
    var scrollContent = pageContainer.find('.page-content');
    if (scrollContent.length === 0) return;
    var hideNavbar = (params.hideNavbarOnPageScroll || scrollContent.hasClass('hide-navbar-on-scroll') || scrollContent.hasClass('hide-bars-on-scroll')) && !(scrollContent.hasClass('keep-navbar-on-scroll') || scrollContent.hasClass('keep-bars-on-scroll'));
    var hideToolbar = (params.hideToolbarOnPageScroll || scrollContent.hasClass('hide-toolbar-on-scroll') || scrollContent.hasClass('hide-bars-on-scroll')) && !(scrollContent.hasClass('keep-toolbar-on-scroll') || scrollContent.hasClass('keep-bars-on-scroll'));
    var hideTabbar = (params.hideTabbarOnPageScroll || scrollContent.hasClass('hide-tabbar-on-scroll')) && !(scrollContent.hasClass('keep-tabbar-on-scroll'));

    if (!(hideNavbar || hideToolbar || hideTabbar)) return;

    var viewContainer = scrollContent.parents('.views' );
    if (viewContainer.length === 0) return;

    var navbar = viewContainer.find('.navbar'),
        toolbar = viewContainer.find('.toolbar'),
        tabbar;
    if (hideTabbar) {
        tabbar = viewContainer.find('.tabbar');
        if (tabbar.length === 0) tabbar = viewContainer.parents('.views' ).find('.tabbar');
    }

    var hasNavbar = navbar.length > 0,
        hasToolbar = toolbar.length > 0,
        hasTabbar = tabbar && tabbar.length > 0;

    var previousScroll, currentScroll;
        previousScroll = currentScroll = scrollContent[0].scrollTop;

    var scrollHeight, offsetHeight, reachEnd, action, navbarHidden, toolbarHidden, tabbarHidden;

    var toolbarHeight = (hasToolbar && hideToolbar) ? toolbar[0].offsetHeight : 0;
    var tabbarHeight = (hasTabbar && hideTabbar) ? tabbar[0].offsetHeight : 0;
    var bottomBarHeight = tabbarHeight || toolbarHeight;

    function handleScroll() {
        if (pageContainer.hasClass('page-on-left')) return;
        currentScroll = scrollContent[0].scrollTop;
        scrollHeight = scrollContent[0].scrollHeight;
        offsetHeight = scrollContent[0].offsetHeight;
        reachEnd =  currentScroll + offsetHeight >= scrollHeight - bottomBarHeight;
        navbarHidden = navbar.hasClass('navbar-hidden');
        toolbarHidden = toolbar.hasClass('toolbar-hidden');
        tabbarHidden = tabbar && tabbar.hasClass('toolbar-hidden');
        // console.log('reachEnd', reachEnd,  currentScroll + offsetHeight ,  scrollHeight - bottomBarHeight)
        if (reachEnd) {
            if (params.showBarsOnPageScrollEnd) {
                action = 'show';
            }
        }
        else if (previousScroll > currentScroll) {
            if (params.showBarsOnPageScrollTop || currentScroll <= 44) {
                action = 'show';
            }
            else {
                action = 'hide';
            }
        }
        else {
            if (currentScroll > 44) {
                action = 'hide';
            }
            else {
                action = 'show';
            }
        }

        if (action === 'show') {
            if (hasNavbar && hideNavbar && navbarHidden) {
                me.showNavbar(navbar);
                pageContainer.removeClass('no-navbar-by-scroll');
                navbarHidden = false;
            }
            if (hasToolbar && hideToolbar && toolbarHidden) {
                me.showToolbar(toolbar);
                pageContainer.removeClass('no-toolbar-by-scroll');
                toolbarHidden = false;
            }
            if (hasTabbar && hideTabbar && tabbarHidden) {
                me.showToolbar(tabbar);
                pageContainer.removeClass('no-tabbar-by-scroll');
                tabbarHidden = false;
            }
        }
        else {
            if (hasNavbar && hideNavbar && !navbarHidden) {
                me.hideNavbar(navbar);
                pageContainer.addClass('no-navbar-by-scroll');
                navbarHidden = true;
            }
            if (hasToolbar && hideToolbar && !toolbarHidden) {
                me.hideToolbar(toolbar);
                pageContainer.addClass('no-toolbar-by-scroll');
                toolbarHidden = true;
            }
            if (hasTabbar && hideTabbar && !tabbarHidden) {
                me.hideToolbar(tabbar);
                pageContainer.addClass('no-tabbar-by-scroll');
                tabbarHidden = true;
            }
        }

        previousScroll = currentScroll;
    }
    scrollContent.on('scroll', handleScroll);
    scrollContent[0].f7ScrollToolbarsHandler = handleScroll;
  }

  destroyScrollToolbars (pageContainer) {
    pageContainer = $(pageContainer);
    var scrollContent = pageContainer.find('.page-content');
    if (scrollContent.length === 0) return;
    var handler = scrollContent[0].f7ScrollToolbarsHandler;
    if (!handler) return;
    scrollContent.off('scroll', scrollContent[0].f7ScrollToolbarsHandler);
  }

  // Hide/Show Navbars/Toolbars
  hideNavbar (navbarContainer) {
      $(navbarContainer).addClass('navbar-hidden');
      return true;
  }
  showNavbar (navbarContainer) {
      var navbar = $(navbarContainer);
      navbar.addClass('navbar-hiding').removeClass('navbar-hidden').transitionEnd(function () {
          navbar.removeClass('navbar-hiding');
      });
      return true;
  }
  hideToolbar (toolbarContainer) {
      $(toolbarContainer).addClass('toolbar-hidden');
      return true;
  }
  showToolbar (toolbarContainer) {
      var toolbar = $(toolbarContainer);
      toolbar.addClass('toolbar-hiding').removeClass('toolbar-hidden').transitionEnd(function () {
          toolbar.removeClass('toolbar-hiding');
      });
  }


  destroy(){
    this.destroyList.forEach((fun) => {
      fun()
    })
  }

  render(){
    return (
      <div className={classNames('page', this.props.className)} data-page={this.props.pageName}></div>

    );
  }

}
Page.anim = true复制代码

5 新建src/components/Navbar.js。
同上面的page一样的功能,做Navbar的动画处理,所有的Navbar都继承该Navbar

/* eslint no-console: 0 */
import React  from 'react';
import ReactDOM from 'react-dom';
import $ from 'react-ui/dom'
import history from '../history'
// import PARAMS from 'react-ui/params'
import Navbars from 'react-ui/navbars'
const transitionDuration = 400;
const animateNavBackIcon = true

export default  class Navbar extends React.Component{
    constructor(props) {
      super(props);
      this.state = {
        title: this.props.title
      }
    }

   /**
    * Prepare navbar before animarion
    * @param  {[type]} newNavbarInner    [description]
    * @param  {[type]} newNavbarPosition [description]
    * @return {[type]}                   [description]
    */
    prepareNavbar (newNavbarInner, newNavbarPosition) {
      if(newNavbarPosition === 'right'){
        newNavbarInner.addClass('navbar-on-right')
      } else if(newNavbarPosition === 'left'){
        newNavbarInner.addClass('navbar-on-left')
      }

      $(newNavbarInner).find('.sliding').each(function () {
          var sliding = $(this);
          var slidingOffset = newNavbarPosition === 'right' ? this.f7NavbarRightOffset : this.f7NavbarLeftOffset;
          if (animateNavBackIcon) {
              if (sliding.hasClass('left') && sliding.find('.back .icon').length > 0) {
                  sliding.find('.back .icon').transform('translate3d(' + (-slidingOffset) + 'px,0,0)');
              }
          }
          sliding.transform('translate3d(' + slidingOffset + 'px,0,0)');
      });
    }
    /**
     * Set navbars classess for animation
     * @param  {[type]} navbarInner [description]
     * @param  {[type]} action      [description]
     * @param  {[type]} direction   [description]
     * @return {[type]}             [description]
     */
    animateNavbars (navbarInner, action, direction, finishCallback) {
      // Loading new page
      navbarInner = $(navbarInner);
      var removeClasses = 'navbar-on-right navbar-on-center navbar-on-left';
      if (direction === 'to-left') {
          if(action === 'enter'){
            navbarInner.removeClass(removeClasses).addClass('navbar-from-right-to-center');

           // window.setTimeout(function () {
           //    navbarInner.removeClass('navbar-from-right-to-center').addClass('navbar-on-center');
           //    finishCallback()
           // }, transitionDuration)

            navbarInner.find('.sliding').each(function (index) {
              console.log('sliding', arguments)
                var sliding = $(this);
                sliding.transform('translate3d(0px,0,0)');
                if(index===0) {
                  sliding.transitionEnd(function(){
                    navbarInner.removeClass('navbar-from-right-to-center').addClass('navbar-on-center');
                    finishCallback()
                  })
                }
                if (animateNavBackIcon) {
                    if (sliding.hasClass('left') && sliding.find('.back .icon').length > 0) {
                        sliding.find('.back .icon').transform('translate3d(0px,0,0)');
                    }
                }
            });

          } else if (action === 'leave'){
            navbarInner.removeClass(removeClasses).addClass('navbar-from-center-to-left');

           // window.setTimeout(function (e) {
           //    navbarInner.removeClass('navbar-from-center-to-left').addClass('navbar-on-left');
           //    finishCallback()
           // }, transitionDuration);

            let rightNavbarInner = navbarInner.closest('.navbar').find('.navbar-inner:first-child')
            navbarInner.find('.sliding').each(function (index) {
                var sliding = $(this);
                var rightText;
                if (animateNavBackIcon) {
                    if (sliding.hasClass('center') && rightNavbarInner.find('.sliding.left .back .icon').length > 0) {
                        rightText = rightNavbarInner.find('.sliding.left .back span');
                        if (rightText.length > 0) this.f7NavbarLeftOffset += rightText[0].offsetLeft;
                    }
                    if (sliding.hasClass('left') && sliding.find('.back .icon').length > 0) {
                        sliding.find('.back .icon').transform('translate3d(' + (-this.f7NavbarLeftOffset) + 'px,0,0)');
                    }
                }
                // console.log('leave', this, this.f7NavbarLeftOffset)
                sliding.transform('translate3d(' + (this.f7NavbarLeftOffset) + 'px,0,0)');
                if(index === 0) {
                  sliding.transitionEnd(function(){
                    navbarInner.removeClass('navbar-from-center-to-left').addClass('navbar-on-left');
                    finishCallback()
                  })
                }
            });

          }
      }
      // Go back
      if (direction === 'to-right') {
        if(action === 'enter'){
          navbarInner.removeClass(removeClasses).addClass('navbar-from-left-to-center');

          //window.setTimeout(function () {
          //   navbarInner.removeClass('navbar-from-left-to-center').addClass('navbar-on-center');
          //   finishCallback()
          //}, transitionDuration);

          navbarInner.find('.sliding').each(function (index) {
              var sliding = $(this);
              sliding.transform('translate3d(0px,0,0)');
              if (animateNavBackIcon) {
                  if (sliding.hasClass('left') && sliding.find('.back .icon').length > 0) {
                      sliding.find('.back .icon').transform('translate3d(0px,0,0)');
                  }
              }
              if(index === 0) {
                sliding.transitionEnd(function(){
                  navbarInner.removeClass('navbar-from-left-to-center').addClass('navbar-on-center');
                  finishCallback()
                })
              }
          });

        } else if(action === 'leave'){
          navbarInner.removeClass(removeClasses).addClass('navbar-from-center-to-right');

         // window.setTimeout(function () {
         //    navbarInner.removeClass('navbar-from-center-to-right').addClass('navbar-on-right');
         //    finishCallback()
         // }, transitionDuration);
          navbarInner.find('.sliding').each(function (index) {
              var sliding = $(this);
              if (animateNavBackIcon) {
                  if (sliding.hasClass('left') && sliding.find('.back .icon').length > 0) {
                      sliding.find('.back .icon').transform('translate3d(' + (-this.f7NavbarRightOffset) + 'px,0,0)');
                  }
              }
              sliding.transform('translate3d(' + (this.f7NavbarRightOffset) + 'px,0,0)');
              if(index === 0) {
                sliding.transitionEnd(function(){
                  navbarInner.removeClass('navbar-from-center-to-right').addClass('navbar-on-right');
                  finishCallback()
                })
              }
          });

        }
      }
  }

  componentDidMount(){
    console.log('componentDidMount', this.props.location && this.props.location.pathname)
    var node = this.node = $(ReactDOM.findDOMNode(this));
    var onResize =  () => {
      Navbars.sizeNavbar(node)
    }
    $(window).on('resize', onResize);
    this.destroy = () => {
      $(window).off('resize', onResize)
    }
    Navbars.sizeNavbar(node)
  }

  componentWillUnMount(){
    this.destroy()
  }

  componentWillAppear(done) {
    console.log('componentWillAppear', this.props.location && this.props.location.pathname);
    done()

  }
  componentDidAppear () {
    console.log('componentDidAppear', this.props.location && this.props.location.pathname);
    //this._enterStyle();
  }
  componentWillEnter (done) {
    console.log('componentWillEnter', this.props.location && this.props.location.pathname);
    if (!Navbar.anim ){
      setTimeout(function(){
        Navbar.anim = true
      }, transitionDuration)
      done()
      return;
    }
    var node = this.node || $(ReactDOM.findDOMNode(this));
    if(history.isBack){
      this.prepareNavbar (node, 'left')
      setTimeout( () => {
        this.animateNavbars(node, 'enter', 'to-right', done)
      },0)

    }else{
      this.prepareNavbar (node, 'right')
      setTimeout( () => {
        this.animateNavbars(node, 'enter', 'to-left', done)
      }, 0)

    }


  }

  componentDidEnter () {
    console.log('componentDidEnter', this.props.location && this.props.location.pathname);
  }

  componentWillLeave (done) {
    console.log('componentWillLeave', this.props.location && this.props.location.pathname );
    if( !Navbar.anim ){
      done()
      return;
    }
    var node = this.node || $(ReactDOM.findDOMNode(this));
    if(history.isBack){
      this.animateNavbars(node, 'leave', 'to-right', done)
    } else {
      this.animateNavbars(node, 'leave', 'to-left', done)
    }

  }

  componentDidLeave () {
    console.log('componentDidLeave', this.props.location && this.props.location.pathname);
  }

  handleBackClick(e){
    e.preventDefault()
    history.go(-1)
  }

  render(){
    if(this.canBack === undefined){
      this.canBack = history.canBack;
    }

    return (
       <div className="navbar-inner" data-page={this.props.pageName}>
          {
          this.canBack ?
          <div className="left sliding" ><a onClick={this.handleBackClick.bind(this)} className="back link"><i className="icon icon-back" ></i><span>返回</span></a></div> : ''
          }

          <div className="center sliding">{this.state.title || ''}</div>
        </div>
    )
  }
}
Navbar.sizeNavbar = Navbars.sizeNavbar;
Navbar.sizeNavbars = Navbars.sizeNavbars;

Navbar.anim = true复制代码

6 新建 src/history.js

/* eslint no-console: 0 */

import { browserHistory } from 'react-router'
// import { hashHistory } from 'react-router'
// var browserHistory = hashHistory
const PATHS_LENGTH = -1
let paths = browserHistory.paths = browserHistory.paths || []

//防止测试环境加载两次的问题
if(!browserHistory.unlisten){

  browserHistory.unlisten = browserHistory.listen(location => {
    let pathname = location.pathname
    if(pathname.charAt(0) !== '/'){
      pathname = '/'.concat(pathname)
    }

    let len = paths.length

    //isBack标记是否时回退到上一页,还是进入到新一页,以显示不同的动画效果
    browserHistory.isBack = (len > 1 && pathname === paths[len-2])
    if(browserHistory.isBack) {
      paths.pop()
    } else if (len === 0 || paths[len-1] !== pathname){
      if(PATHS_LENGTH === -1 || len < PATHS_LENGTH){
        //不限制历史记录长度
         paths.push(pathname)
      }
      else{
        //超出限制历史记录长度
        for(let i = 0; i < len - 1; i++){
          paths[i] = paths[i+1]
        }
        paths[len - 1] = pathname
      }
    }

    //canback标记是否显示navbar的回退按钮
    browserHistory.canBack = (paths.length > 1)
  })


  window.addEventListener('popstate', function () {

    browserHistory.isBack = true

  })
}

export default browserHistory复制代码

7 新建router里配置的页面和导航条
src/home/HomeNavbar.js
src/home/HomePage.js
src/components/order/OrderNavbar.js
src/components/order/OrderPage.js
src/components/product/ProductNavbar.js
src/components/product/ProductPage.js
具体代码参考git仓库源码

8 测试
npm start

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值