和TARO一起做SPA--react+redux+dva+mockjs+taro实践

1. 基础知识

1.1 this指针

this指针大概是javascript中最令初学者困惑的语法了,简单说,this指针就是指向函数或方法运行的上下文环境。既然叫上下文环境,肯定和运行的环境相关。
在浏览器环境下:

  • 当this出现在函数调用中,指向的是他运行的上下文:window对象;
  • 当this出现在对象方法调用时,则指向了他运行的上下文:对象本身;
    让我们举个例子说明一下:
    function fn(){
        console.log(this);
    }
    var obj={
        fn:fn
    }
    fn();
    obj.fn();

通过运行代码,你会发现,fn返回的是window对象,而obj.fn返回的则是obj对象
为了加深理解,我们看一个有点迷惑的问题

<!DOCTYPE html>
<html>
    <div id="root"></div>
</html>
<script type="text/javascript"/>
    var e1=document.getElementById("root");
    var getId=document.getElementById
    var e2=getId("root");
</script>

这是一个常见的场景,每次使用document.getElementById方法很麻烦,所以重新定义一个函数 getId,指向document.getElementById方法.
但是你发现,在chrome浏览器中调用getId方法居然系统会抛一个异常:

test.htm:9 Uncaught TypeError: Illegal invocation 

发生异常的原因就在于,getElementById内部实现使用了this指针指向document对象.但是,当你用自己定义的getId方法时,getElementById已经由对象调用变成了方法调用,this指针被指向了window对象.

既然this指针有这样的不确定性,那么,自然就可以想到如何根据需要变更他的指向。
变更this指针指向有两种方法,定义时绑定和运行时绑定。

  • 定义时绑定 bind
  • 运行时绑定 apply/call
    还是让我们看个例子:
    对于我们上个getId的例子,你只要这样调用就可以了
    定义时通过bind方法绑定
    const getId=document.getElementById.bind(document);
    const e=getId("root");

运行时通过call方法绑定

    const e=getId.call(document,"root")

运行时通过apply方法绑定

     const e=getId.apply(document,["root"])

通过上面的例子也可以看到call和apply两者的区别是:apply绑定时,传入的参数为数组形式,而call绑定则是采用枚举的方式。所以如果getId方法需要使用apply方法时,必须将参数包装成数组的形式.

1.2 高阶函数

在javascript语言中,函数是一类成员,函数可以作为变量,也可以作为输入参数和返回参数.将函数作为输入参数或输出参数的函数称之为高阶函数.后面我将会带大家一起了解一下高阶函数.

1.2.1 闭包

让我们看一下第一个高阶函数,闭包.
闭包利用了javascript函数作用域内变量被引用不会消除的特性.闭包被应用的场景非常多.
让我们先看一个获取递增ID的例子,首先,让我们看一下传统的方法,传统的方法获取递增ID,你需要先做一个全局变量.

let globalID=0
function getID(){
    return globalID++;
}
console.log(getID(),getID(),getID())

这种方法由于使用了全局变量,任何一个人都有可能不经意的修改globalID的值导致你的方法失效.
采用闭包的写法,你先创建一个crGetID方法通过闭包保存ID,并返回getID函数.然后通过getID方法获取ID

function crGetID(){
    let id=0;
    return function(){
        return id++;
    }
}
var getID=crGetID();
console.log(getID(),getID(),getID())

这样,没有人可以直接修改你的ID值.

1.2.2 currying

让我们再看一个闭包的应用:currying,currying解决的问题是把一个函数的多个参数转换为单参数函数.
举个例子,假设我们需要累计一个用户7天的数据:

function add(d1,d2,d3,d4,d5,d6,d7){
    return d1+d2+d3+d4+d5+d6+d7
}

如果是30天,可能需要30个输入参数,如果不定天数呢?
采用currying则可以解决这个问题:

function curryAdd(){
    let s=[];
    return function(...arg){
        if (arg.length==0){
            return s.reduce(function(p,v){return p+v},0)
        }else{
            s.push(...arg)
            console.log("s",s);
        }
    }
}
var ca=curryAdd();
ca(1);
ca(2);
ca(3);
ca(4);
console.log(ca());

通过将一个函数currying后,函数可以随时被调用,直到输入参数为空时才进行计算.
闭包的特性使之成为javascript中运用最广的特性.后续在代码中,我们还会继续看到大量的闭包用法.

1.3 es6语法

react 大量使用了es6的语法,如果你对javsascript的印象还停留在原始的印象里,你可能根本没法看懂react的代码。所以在这里我们简单对用到的es6语法做一些介绍,并尽可能以react实际使用作为学习的例子。详细的es6语法介绍,可以参考相关的技术文档。

1.3.1变量解析

  • 数组变量解析
    es6 支持对数组直接进行解析,举个例子,如果需要对变量x,y互换值,传统的做法是:
function(x,y){
    var t;
    t=x;
    x=y;
    y=t;
}

如果用数组解析,就容易多了:

let [x,y]=[y,x]

数组解析可以用到输入参数传递

    function test ([x,y,z]){
        return x+y+z;
    }
    console.log(test([1,2,3]))

上面的例子,打印出来的结果是6.
还可以用到一次返回多个参数:

    function retMult(){
        return [1,2,3]
    }
    let [x,y,z]=retMult();
    console.log(x,y,z)

函数会打印出 1,2,3

  • 对象变量解析
    对象解析用的更广泛,让我们举个简单的例子:
    let {x:x,y:y}={x:2,y:3}
    console.log(x,y);

函数打印结果 2,3
还可以简写为:

    let {x,y}={x:2,y:3}
    console.log(x,y);

让我们看一下下面的例子,如果对象属性名称和变量名称可以更进一步进行简写:

    let x=1;
    let obj={x};  //相当于 obj={x:x}
    console.log(obj.x);

和数组解析一样,对象解析大量应用在输入参数的传值上,让我们举一个redux的实际应用的例子(这个例子里,假设state对象仅包含一个value属性):

function  reducer({value},{type,payLoader}){
    switch(type){
        case "calc":
            return  {value:value+payLoader}
        default:
            return {value}
    }
}

这个例子里,使用了对象的解析,代码更加简洁和异动.如果不使用对象解析,你的代码是这样的:

function reducer(state,action){
    switch(action.type){
        case "calc":
            return {value:state.value+action.payLoader}
        default:
            return state;
    }
}

1.3.2箭头函数

箭头函数可以让代码更加简洁和直观,另外,由于箭头函数对this指针的特殊处理,因此,被大量的运用。
让我们还是以数组提供的map函数为例子说明:

let arr=[1,2,3,4];
let m=arr.map(v=>v*2);

使用箭头函数,即简洁又直观,对数组中的每个元素直接乘以2.
如果使用传统的方式,你需要这样写:

let n=arr.map(function(v){
    return v*2;
})
console.log(n);

使用箭头函数需要注意以下几点:

  • 如果是一个参数,可以省略(),如果多个参数或没有参数,则必须使用()
  • 如果函数直接返回箭头后的表达式,可以不加{},否则,需要在箭头后使用{}
    让我们再举个数组提供的reduce方法的实际例子:
var r=[1,2,3,4].reduce((p,n)=>p+n);
console.log(r);

这个例子通过reduce函数计算数字元素的累加和,reduce函数的输入参数是一个函数,函数的输入参数分别为累加之和p以及下一个元素n.
如果用传统的方法,你需要这样写:

var r=[1,2,3,4].reduce(function(p,n){
    return p+n;
})
console.log(r);

1.3.3类

ES6提供了类,类实际上就是一个语法糖.让我们还是通过一个具体的例子来看一下类的实现:

class Counter extends Component{
    constructor(props){
        super(props)
    }
    sub(){
        let value=this.state.value-1;
        this.setState({value})
    }
    add(){
        let value=this.state.value+1;
        this.setState({value})
    }
    render(){
        return (
            <div>
                <button onClick={this.add.bind(this)}>+</button>
                <button onClick={this.desc.bind(this)}>-</button>
            </div>
        )
    }
}

这个例子说明了ES6的类的用法.
首先,类的定义语法是:

class  Name{
}

你的类可以继承自另一个类,在刚才的例子里我们的类Counter继承自React的Component类

class Counter extends Component{
}

你可以根据需要实现类的构建器,构建器可以通过super方法引用父类的构建器:

    constructor(props){
        super(props)
    }

类的方法可以缩写为methodName(){}的形式

    sub(){
        let value=this.state.value-1;
        this.setState({value})
    }

1.3.4装饰器

1.4 异步函数

2. 代码框架

2.1nodejs

2.2webpack

2.3react

2.3.1第一个react程序

由于react需要依赖大量的依赖库,如通过babel对es6的转化,css的渲染…对于新手来说,可能这一大堆配置就让人望而却步。为了简化开发环境的搭建,让我们直接用create-react-app 搭建脚手架(具体命令含义可以先不用理解):
首先,我们安装create-react-app

npm install -g create-react-app

安装成功后,就可以开始使用

mkdir basic
create-react-app basic

basic是我们第一个react程序的名称,create-react-app命令会运行一段时间,帮我们搭建脚手架和开发环境.命令执行后,我们开始启动我们的应用:

cd basic
yarn start

yarn start命令后,webpack会启动webpack-dev-server跟踪我们代码修改,然后自动启动一个端口为3000的HttpServer帮我们进行调试,并且很贴心的弹出URL为http://localhost:3000的浏览器页面,显示我们的第一个react应用.
好,让我们开始学习第一个react程序,首先,让我们看一下脚手架帮我们搭建的目录结构。

,create-react-app创建的目录结构

其中/public/index.html就是我们的页面模板,所有的组件都会渲染在这个文件上.
让我们看一下第一个组件:/src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

这个组件很简单,重要的是这一句:

ReactDOM.render(<App />, document.getElementById('root'));

这个是React的语句,意思是在index.html页面ID为root的元素上渲染App组件.其中index.html页面放置在public目录下.
而App就是React组件,系统如何区分React组件和DOM组件呢?很简单,在React中,首字母为大写的就是React组件,小写字母为DOM组件.
App组件非常简单,让我们也看一下:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    );
  }
}
export default App;

好,现在让我们开发一个计数器程序来学习一下React开发.首先,让我们自己定义一个Counter组件.
首先,我们在src根目录下新建一个counter.js文件

import React,{Component} from 'react'
class Counter extends Component{
    constructor(){
        super()
        this.state={value:0}
    }
    render(){
        return (
            <div>
                <span>{this.state.value}</span>
                <button onClick={this.add.bind(this)}>+</button>
                <button onClick={this.desc.bind(this)}>-</button>
            </div>
        )
    }
    desc(){
        let value=this.state.value-1;
        this.setState({value})
    }
    add(){
        let value=this.state.value+1;
        this.setState({value})
    }
} 
//将组件导出模块
export default Counter

然后,修改index.js文件,渲染我们新开发的Counter组件:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
-import App from './App';
+import Counter from './counter'
import * as serviceWorker from './serviceWorker';
-ReactDOM.render(<App />, document.getElementById('root'));
+ReactDOM.render(<Counter />, document.getElementById('root'));
serviceWorker.unregister();

页面会自动刷新修改后的内容,(是不是很惊讶)显示结果.点击 + 和 - 按钮系统会自动显示最新的counter.
让我们来分析一下代码:

首先 ,我们的Counter组件继承自React.Component.我们的Counter组件继承Component组件,并且实现了构建器.在构建器中,完成了state值的初始化.

class Counter extends Component{
    constructor(){
        super()
        this.state={value:0}
    }
}

让我们看接下来的渲染部分的处理:

    render(){
        return (
            <div>
                <span>{this.state.value}</span>
                <button onClick={this.add.bind(this)}>+</button>
                <button onClick={this.desc.bind(this)}>-</button>
            </div>
        )
    }

render方法是React组件的核心部分,主要完成组件的表现.在这里,render方法返回了一个jxs的代码段.所谓的jxs,简单说就是嵌入了react语句的html代码段.
在这里,你特别需要注意的是,React会自动跟踪state值的变化进行渲染,因此,你不需要像传统开发一样手动渲染数据,只需要简单的标明会发成变更的数据即可:

                <span>{this.state.value}</span>

在这里,{}表示的是里面的部分是由react代码构成.
这个代码段有几个特别需要注意的地方:

  • return语句直接返回JSX时,必须用()进行包裹,下面的语句由于<div>前没有(),会直接报错:
    render(){
        return <div>{this.state.value}</div>  
    }
  • html代码段必须有一个根元素,因此,以下的代码是错误的:
    render(){
        return (
                <span>{this.state.value}</span>
                <button onClick={this.add.bind(this)}>+</button>
                <button onClick={this.desc.bind(this)}>-</button>
        )
    }
  • 和HTML事件命名机制不同,让我们对比一下React的写法:
  <button onClick={this.add.bind(this)}>+</button>

下面是HTML的写法:

 <button onclick="test()">test</button>

有三个重要的区别:
1.React事件触发是骆驼命名方式.而HTML的触发方式是全部小写;
2.React事件触发是函数名,而HTML的触发方式是函数执行代码块;
3.React事件处理函数this指针不会绑定任何对象,而HTML指针会自动绑定到window对象;

  • 如果处理state,则React事件处理函数需要绑定this指针到组件
    这就是以下代码的原因,通过bind方法将函数this指针绑定到React Component:
  <button onClick={this.add.bind(this)}>+</button>
  • state的处理原则
    让我们看一下事件处理方法的实现:
    desc(){
        let value=this.state.value-1;
        this.setState({value})
    }
    add(){
        let value=this.state.value+1;
        this.setState({value})
    }

代码很简单,但是有几个需要注意的地方:

  • 不能直接修改state的值,必须通过this.setState方法修改,否则,系统不会进行渲染.
  let value=this.state.value+1
  this.state.value=value
  • state的原始值不能修改,因此,以下代码是无效的:
   let value=this.state.value++; //this.state.value++修改了原始的state的值
   this.setState({value})

第一个react程序结束了,React的逻辑很简单,每个组件都有一个state值,组件通过监控state的状态变化实现页面渲染.
最后,别忘了需要从模块中导出我们的组件:

export default Counter

导出是,如果不增加default参数,导入时需要将组件名称用{}括起来.一个模块中智能有唯一的一个default组件.

2.3.2 React组件间通讯

从第一个例子我们可以发现,React组件开发很容易,通过监控组件state值的变化,实现自动的渲染,极大的减轻了开发的工作量.但是每个组件都有自己的state,如果多个组件需要通讯,问题就变得复杂了.
让我们看下面的这个例子,在这个例子,组件Control由3个Counter构成,每个Counter都可以自动增减,CounterControl显示的值是3个Counter的累加值.
这个例子显示了组件间如何进行通信.
先让我们看一下CounterControl组件的代码:

import React,{Component } from "react";
import Counter from "./counter"
class counterControl extends Component{
    constructor(){
        super();
        //设置组件的初始值
        this.state={value:0}
    }
    //这个地方需要特别注意,change是Counter组件每次点击发生变化的值
    change(change){
        //不可以修改state的原始值
        let value=this.state.value;
        value+=change;
        //必须通过this.setState方法进行state的修改
        this.setState({value:value})
    }
    render(){
        return(
            <div>
                <Counter name={"one"} change={this.change.bind(this)}/>
                <Counter name={"two"}  change={this.change.bind(this)}/>
                <Counter name={"three"}  change={this.change.bind(this)}/>
                <div>value:{this.state.value}</div>
            </div>
        )
    }
}
export default counterControl;

Counter组件也发生了变化,增加了name属性和change方法.

<Counter name={"three"}  change={this.change.bind(this)}/>

由于组件彼此state独立,因此,组件之间的通讯就落到了change方法里.让我们看一下change方法的实现:

    //这个地方需要特别注意,change是Counter组件每次点击发生变化的值
    change(change){
        //不可以修改state的原始值
        let value=this.state.value;
        value+=change;
        //必须通过this.setState方法进行state的修改
        this.setState({value:value})
    }

change方法实际是CounterControl通过属性传递给Counter子组件的回调函数.他的实现原理是每次Counter组件被点击时,把Counter组件state值的变化回调至CounterControl,从而实现CounterControl的State值的变化.
让我们看一下Counter组件:

import React,{Component} from 'react'
class Counter extends Component{
    constructor(props){
        super(props)
        this.state={value:0}
      }
    desc(){
        let value=this.state.value-1;
        this.setState({value})
        this.props.change(-1)
    }
    add(){
        let value=this.state.value+1;
        this.setState({value})
        this.props.change(1);
    }
    render(){
        return (
            <div>
                <span>{this.props.name}:{this.state.value}</span>
                <button onClick={this.add.bind(this)}>+</button>
                <button onClick={this.desc.bind(this)}>-</button>
            </div>
        )
    }
}
export default Counter

重点看一下组件的onClick方法:

    desc(){
        let value=this.state.value-1;
        this.setState({value})
        this.props.change(-1)
    }
    add(){
        let value=this.state.value+1;
        this.setState({value})
        this.props.change(1);
    }

无论是add还是desc方法,Counter在完成自己state变更的同时,都需要调用通过props属性传递的change方法回调CounterControl提供的chanage方法,实现state变化的通知.

2.3.3 优化

上面的例子我们发现,Counter组件如果需要整合到CounerControl组件中,就必须进行修改.
原来Counter组件的事件如下:

    desc(){
        let value=this.state.value-1;
        this.setState({value})
    }
    add(){
        let value=this.state.value+1;
        this.setState({value})
    }

修改后

    desc(){
        let value=this.state.value-1;
        this.setState({value})
        //增加了change的回调方法
        this.props.change(-1)
    }
    add(){
        let value=this.state.value+1;
        this.setState({value})
        //增加了change的回调方法
        this.props.change(1);
    }

也就是说,Counter组件并不是一个通用的组件.造成这种情况的原因,是由于Counter组件增加了过多的业务逻辑.
如果把组件的显示和业务逻辑进行剥离成内部组件和容器组件,就可以解决这个问题.内部组件只负责显示,容器组件则负责具体的业务逻辑和state处理.让我们看一下如何进行剥离:
新的NgCounter内部组件代码如下:

class NgCounter extends Component{
    render(){
        return (
            <div>
                <span>{this.props.name}:{this.props.value}</span>
                <button onClick={this.props.add}>+</button>
                <button onClick={this.props.sub}>-</button>
            </div>
        )        
    }
}

剥离后的NgCounter组件不再进行任何业务逻辑的处理,也不处理state相关的数据.它只是按照传递的属性值进行显示或回调.
我们管这种不处理任何state的组件称之为无状态组件.无状态组件可以进一步简化为函数,如下所示:

import React,{Component} from 'react'
function NgCounter(props){
    return (
        <div>
            <span>{props.name}:{props.value}</span>
            <button onClick={props.add}>+</button>
            <button onClick={props.sub}>-</button>
        </div>
    )
}

无状态组件函数由于没有this指针,属性props由容器组件传递.
让我们看一下容器组件如何进行处理

import React,{Component} from 'react'
class Container extends Component{
    constructor(props){
        super(props);
        this.state={value:0}
    }
    add(){
        this.setState({value:this.state.value+1})
        //如果嵌入CounterControl则需要增加以下方法
        this.props.add();
    }
    sub(){
        this.setState({value:this.state.value-1})
        //如果嵌入CounterControl则需要增加以下方法
        this.props.sub();
    }
    render(){
        return(
            <NgCounter name="ngCounter" add={this.add.bind(this)} sub={this.sub.bind(this)} value={this.state.value}/>
        )
    }
}
export default Container;

通过内部组件和容器组件的拆分,如果组件需要嵌入其他组件,则只需要修改容器组件即可.内部组件不需要进行任何调整.
需要特别注意的是,此时导出的组件为容器组件.

我们把CounterControl也进行了改造,相应的代码如下:

import React,{Component} from 'react';
import NgCounter from './ngCounter'
//内部无状态组件退化为函数
function NgCounterControl(props){
    return(
        <div>
            <NgCounter name={"one"} add={props.add} sub={props.sub}/>
            <NgCounter name={"two"}  add={props.add} sub={props.sub}/>
            <NgCounter name={"three"}  add={props.add} sub={props.sub}/>
            <div>value:{props.value}</div>
        </div>
    )
}
//容器组件负责具体的业务逻辑和state的处理
class Container extends Component{
    constructor(props){
        super(props);
        this.state={value:0}
    }
    add(){
        this.setState({value:this.state.value+1})
    }
    sub(){
        this.setState({value:this.state.value-1})
    }
    render(){
        return (
            //返回内部组件
            <NgCounterControl add={this.add.bind(this)} 
            sub={this.sub.bind(this)}
            value={this.state.value} />
        )
    }
}
//导出容器组件
export default Container;

2.3.4 总结

通过上面的例子,我们可以发现,虽然我们把组件拆分为内部组件和容器组件,实现了内部组件的独立性,但是,由于React组件彼此都维护自己的state,当多个组件需要同步state值的时候,情况还是变得很复杂,组件必须通过props属性传递回调方法层层调用.因此,对于多个组件协调工作时,这种实现方法就显得很笨拙而且效率低下.

2.4Ant.Design的整合

在开始redux模块前,让我们先介绍一下Ant.Design模块.
到目前为止,我们自己开发的组件都是基于html的原生元素.实际上,Ant.Design已经帮我们精心设计了大量优秀的组件,我们可以直接使用,这一部分,我们将介绍给大家如何使用Ant.Design

2.4.1引入Ant.Design

首先,让我们安装Ant.Design

yarn add antd

修改你的App.css,在第一行加入:

@import '~antd/dist/antd.css';

让我们新增加一个antd的组件,简单演示一下如何使用Ant.Design.让我们在/src目录下增加一个antd.js文件,简单实现一个无状态的组件.
这个组件里我们简单使用了Row,Col,Button,Calendar,Rate,Card,Steps 几个组件.

import React from 'react'
import "./App.css"
import {Row,Col,Button,Calendar,Rate,Card,Steps } from 'antd'
export default function(){
    const Step = Steps.Step;
    return (
        <Row>
            <Col span={12}>
                <Card title="Antd 示例">
                    <Rate allowHalf defaultValue={2.5} />
                    <Steps current={1}>
                        <Step title="Finished" description="This is a description." />
                        <Step title="In Progress" description="This is a description." />
                        <Step title="Waiting" description="This is a description." />
                    </Steps>
                    <Button type="primary">hello,world</Button>
                </Card>
            </Col>
            <Col span={12}><Calendar/></Col>
        </Row>
    )
}

下一步,修改index.js文件

//导入刚才新建的组件
import Antd  from './antd'
//渲染该组件
ReactDOM.render(<Antd/>, document.getElementById('root'));

现在我们应该可以看到Ant.Design开始正式工作了.

2.4redux组件

2.4.1 reduct组件原理

reduct组件由store action reducer构成:

  • store:全局唯一和共享的数据存储;store的主要方法有
    • dispatch:负责调用reducer处理action,生成新的state
    • subscribe:负责监听state的变化,如果state发生了变化,则调用相关的回调函数进行处理
  • action:由动作类型type和携带的数据构成,reducer负责根据action的type进行相应的处理;
  • reducer:负责根据action处理state,并且保证每次reducer操作,都必须返回新的state;

2.4.2 reduct的使用

让我们通过一个实际的例子学习怎样使用reduct,
首先,我们需要安装redux

yarn add redux

实现reducer,创建store

import React,{Component} from 'react'
import PropTypes from 'prop-types'
import {createStore} from 'redux'
const reducer=(state,action)=>{
    console.log(state.value)
    switch(action.type){
        case "calc":
            return {value:state.value+action.payload}
        default:
            return state;
    }
}
const initState={value:0};
const store=createStore(reducer,initState);

创建store后,我们需要所有的组件都能够访问该store,因此,我们需要把store放到所有组件的父容器上.在这里,我们通过新建一个Provider组件实现store的保存:

class Provider extends Component{
    getChildContext(){
        return {
            store:this.props.store
        }
    }
    render(){
        return this.props.children
    }
}
Provider.childContextTypes={
    store:PropTypes.object
} 

请注意getChildContext方法,为了简化store的传递,React提供了一个Context对象.需要使用Context的父组件只要实现getChildContext方法返回Context对象.并且设置该属性的类型为PropTypes.object即可.
因为Provider作为父组件,内部会嵌套子组件,所以render方法直接返回this.props.children即可.
让我们 看一下如何使用Provide组件:

        <Provider store={store}>
            <ReduxCounter name={"one"} />
            <ReduxCounter name={"two"} />
            <ReduxCounter name={"three"} />
            <Panel/>
        </Provider>     

this.props.children指向的即是Provider组件嵌套的内容.
让我们看一下ReduxCounter组件的实现:

function ReduxCounter(props){
    return (
        <div>
            <span>{props.name}:{props.value}</span>
            <button onClick={props.add}>+</button>
            <button onClick={props.sub}>-</button>
        </div>        
    )
}

ReduxCounter 组件是一个内部无状态组件,只负责渲染

class ReduxCounterContainer extends Component{
    constructor(props,context){
        super(props,context);
        this.state={value:0}
        this.store=this.context.store;
    }
    add(){
        this.setState({value:this.state.value+1})
        this.store.dispatch({
            type:"calc",
            payload:1
        })
    }
    sub(){
        this.setState({value:this.state.value-1})
        this.store.dispatch({
            type:"calc",
            payload:-1
        })
    }
    render(){
        return(
            <ReduxCounter name={this.props.name} value={this.state.value} add={this.add.bind(this)} sub={this.sub.bind(this)}/>
        )
    }
}
ReduxCounterContainer.contextTypes={
    store:PropTypes.object
}

所有的逻辑和状态的处理都由ReduxCounterContainer组件完成.
在这里,需要特别注意的是

ReduxCounterContainer.contextTypes={
    store:PropTypes.object
}

如我们之前所说,所有的子组件,如果希望使用父组件提供的context,必须声明该组件的contextTypes为PropTypes.object

下一步,让我们看一下Panel组件的实现

class Panel extends Component{
    constructor(props,context){
        super(props,context);
        context.store.subscribe(this.change.bind(this));
    }
    change(){
        this.setState(this.context.store.getState());
    }
    render(){
        return (
            <div>{this.context.store.getState().value}</div>
        )
    }
}
Panel.contextTypes={
    store:PropTypes.object
}

Panel组件很简单,只需要通过跟踪store的state变化,展示子state的value值即可.
因此,Panel组件需要接受store的回调

        context.store.subscribe(this.change.bind(this));

最后,让我们看一下ReduxCounterControl组件的实现

function  ReduxCounterControl(){
    return (
        <Provider store={store}>
            <ReduxCounterContainer name={"one"} />
            <ReduxCounterContainer name={"two"} />
            <ReduxCounterContainer name={"three"} />
            <Panel/>
        </Provider>        
    )
}
export default ReduxCounterControl;

至此,我们完成了redux的使用,总结一下,redux使用包括以下几步:

  • 创建reducer
  • 创建store
  • 创建Provider,用来保存和传递store
  • 需要跟踪store值的组件,调用store的subscribe方法监控state值的变化
  • 各组件通过store.dispatch方法调用reducer处理并返回state
  • 组件发现state值发生变化,自动渲染
    下一节我们将学习如何使用react-redux简化这个流程

2.4.3 react-redux的使用

2.5mockjs

2.6dva

2.7taro

2.8taro-ui

3 开发环境搭建

3.1搭建taro脚手架项目

首先,让我们搭建taro的脚手架,这个例子里我们假设项目名称为wos,则在命令行下运行
taro init wos
回答系列问题后,命令行会自动创建以wos为文件夹的脚手架项目.
这个教程里,是否使用TypeScript选择了否,css预处理器选择了less,模板选择了redux
如下所示:

 Taro v1.2.4

Taro即将创建一个新项目!
Need help? Go and open issue: https://github.com/NervJS/taro/issues/new

? 请输入项目介绍! wos
? 是否需要使用 TypeScript ? No
? 请选择 CSS 预处理器(Sass/Less/Stylus) Less
? 请选择模板 Redux 模板

执行成功后,可以开始编译微信小程序:

cd wos
npm run dev:weapp

命令会执行一会儿,当出现

监听文件修改中...

说明编译完成,可以打开微信小程序开发工具,新建项目,选择wos文件夹后,可以看到小程序已经可以正常运行.

3.2整合tara-ui

在命令行下输入
yarn add taro-ui
现在,taro-ui已经整合成功,我们可以简单修改pages/indexs页面测试一下

+ import {aButton} from 'taro-ui'
  render () {
    return (
      <View className='index'>
      +<AtButton type='primary'>tryme</AtButton>
        <Button className='add_btn' onClick={this.props.add}>+</Button>
        <Button className='dec_btn' onClick={this.props.dec}>-</Button>
        <Button className='dec_btn' onClick={this.props.asyncAdd}>async</Button>
        <View><Text>{this.props.counter.num}</Text></View>
        <View><Text>Hello, World</Text></View>
      </View>
    )
  }

再次运行
npm run dev:weapp
打开微信小程序开发工具,这时候你可以看到,首页里,已经多了一个蓝色的按钮.
至此,我们已经搭建了taro的开发环境.

3.3整合dva框架

下面的步骤,我们将整合dva框架.首先,我们先安装相关的依赖库
yarn add dva-core dva-loading
我们把所有的配置都集中在一起,在src目录下新建一个config文件夹,生成index.js,代码如下:

// 请求连接前缀
export const baseUrl = 'http://localhost:3721';
// 输出日志信息
export const noConsole = false;

dva会按照路由多model进行分层管理,在taro框架里,我们没有使用umi,所以需要统一管理model,让我们在/src目录下新建models文件夹,生成index.js文件,我们现在还没有model需要dva管理,因此,先空着.

export default [
]

我们让dva来管理我们的store,在src目录下新建util文件夹,生成dva.js文件,代码如下.

import Taro from '@tarojs/taro'
import { create } from 'dva-core'
import { createLogger } from 'redux-logger'
import createLoading from 'dva-loading'

let app;
let store;
let dispatch;

function createApp(opt) {
  // redux日志
  opt.onAction = [createLogger()];
  app = create(opt);
  app.use(createLoading({}));

  // 适配支付宝小程序
  if (Taro.getEnv() === Taro.ENV_TYPE.ALIPAY) {
    global = {};
  }
  if (!global.registered) 
    opt.models.forEach(
      model =>{
        app.model(model);
        console.log(model)
      });

  global.registered = true;
  app.start();
  store = app._store;
  app.getStore = () => store;

  dispatch = store.dispatch;
  app.dispatch = dispatch;
  return app;
}

export default {
  createApp:createApp,
  getDispatch() {
    return app.dispatch
  }
}

让我们封装一下http请求你的工具类,同样,在src/util/目录下新建request.js文件,代码如下:

import Taro from '@tarojs/taro';
import { baseUrl, noConsole } from '../config';

const request_data = {
  platform: 'wap',
  rent_mode: 2,
};

export default (options = { method: 'GET', data: {} }) => {
  if (!noConsole) {
    console.log(`${new Date().toLocaleString()}【 M=${options.url} 】P=${JSON.stringify(options.data)}`);
  }
  return Taro.request({
    url: baseUrl + options.url,
    data: {
      ...request_data,
      ...options.data
    },
    header: {
      'Content-Type': 'application/json',
    },
    method: options.method.toUpperCase(),
  }).then((res) => {
    console.log("res:",res);
    const { statusCode, data } = res;
    if (statusCode >= 200 && statusCode < 300) {
      if (!noConsole) {
        console.log(`${new Date().toLocaleString()}【 M=${options.url} 】【接口响应:】`,res.data);
      }
      if (data.status !== 'ok') {
        Taro.showToast({
          title: `${res.data.error.message}~` || res.data.error.code,
          icon: 'none',
          mask: true,
        });
        console.error(`${res.data.error.message}~` || res.data.error.code);
      }
      return data;
    } else {
      throw new Error(`网络请求错误,状态码${statusCode}`);
    }
  })
}

典型的dva的开发目录如下:

每个文件件都需要4个文件,太复杂,所以我们在根目录下增加一个脚本template.js,自动生成文件:

/**
 * pages模版快速生成脚本,执行命令 npm run tep `文件名`
 */

const fs = require('fs');

const dirName = process.argv[2];

if (!dirName) {
  console.log('文件夹名称不能为空!');
  console.log('示例:npm run tep test');
  process.exit(0);
}

// 页面模版
const indexTep = `import Taro, { Component } from '@tarojs/taro';
import { View } from '@tarojs/components';
import { connect } from '@tarojs/redux';
import './index.scss';

@connect(({${dirName}}) => ({
  ...${dirName},
}))
export default class ${titleCase(dirName)} extends Component {
  config = {
    navigationBarTitleText: '${dirName}',
  };

  componentDidMount = () => {

  };

  render() {
    return (
      <View className="${dirName}-page">
        ${dirName}
      </View>
    )
  }
}
`;

// scss文件模版
const scssTep = `@import "../../styles/mixin";

.${dirName}-page {
  @include wh(100%, 100%);
}
`;

// model文件模版
const modelTep = `import * as ${dirName}Api from './service';

export default {
  namespace: '${dirName}',
  state: {

  },

  effects: {
    * effectsDemo(_, { call, put }) {
      const { status, data } = yield call(${dirName}Api.demo, {});
      if (status === 'ok') {
        yield put({ type: 'save',
          payload: {
            topData: data,
          } });
      }
    },
  },

  reducers: {
    save(state, { payload }) {
      return { ...state, ...payload };
    },
  },

};
`;
// service页面模版
const serviceTep = `import Request from '../../utils/request';

export const demo = data => Request({
  url: '路径',
  method: 'POST',
  data,
});
`;
fs.mkdirSync(`./src/pages/${dirName}`); // mkdir $1
process.chdir(`./src/pages/${dirName}`); // cd $1
fs.writeFileSync('index.js', indexTep);
fs.writeFileSync('index.scss', scssTep);
fs.writeFileSync('model.js', modelTep);
fs.writeFileSync('service.js', serviceTep);

console.log(`模版${dirName}已创建,请手动增加models`);

function titleCase(str) {
  const array = str.toLowerCase().split(' ');
  for (let i = 0; i < array.length; i++) {
    array[i] = array[i][0].toUpperCase() + array[i].substring(1, array[i].length);
  }
  const string = array.join(' ');
  return string;
}
process.exit(0);

然后,在package.json文件中的scripts下增加一行
"g":"node template"
然后运行:
npm run g home
可以看到,在pages目录下新增加了 home文件夹以及下面的4个文件.
至此,我们完成了dva项目的整合,后面,我们将开始完善代码.

3.4整合mock

首先,让我们加载mock依赖库
yarn add mocker-api mockjs --dev
在项目根目录下新建 mock文件夹,新建index.js文件。输入以下代码:

const delay = require('mocker-api/utils/delay');
const mockjs=require('mockjs');
const data= {
    'GET /api/user': {
        id: 1,
        username: 'kenny',
        sex: 6
    },  
    'GET /api/hi':(req,res)=>{
        res.json(
            {
                id:1,
                //query 方法获取Get参数,如 /api/hi?name=tony
                username:req.query["name"],
            }
        )
    },
    //可以直接使用mockjs生成mock数据
    'GET /api/mock':mockjs.mock({
        'list|10-100':1,
    })
}
//使用delay方法可以延迟返回数据
module.exports=delay(data,1000);

修改package.json文件,在scripts下新增一行:

"mock": "mocker ./mock"

运行
npm mock
返回

> wos@1.0.0 mock D:\study\taro\wos
> mocker ./mock

> Server Listening at Local: http://localhost:3721/
>           On Your Network: http://192.168.60.1:3721/

我们可以开始在浏览器中测试,输入 http://localhost:3721/api/user,1秒后返回mock数据:

{"id":1,"username":"kenny","sex":6}

输入 http://localhost:3721/api/hi/name='tony',1秒后返回mock数据:

{"id":1,"username":"tony"}

3. 完善代码

我们设计的App分为四个模块:

  • 首页
  • 分析模块
  • 营销模块
  • 账户模块
  • 登陆模块
    我们使用之前增加的模板功能,增加另外的3个页面:
npm run g home
npm run g analysis
npm run g account
npm run g login

3.1微信小程序首页

现在我们开始修改app.js文件:

import '@tarojs/async-await'
import Taro, { Component } from '@tarojs/taro'
import { Provider } from '@tarojs/redux'
import Index from './pages/index'
import './app.less'

-import configStore from './store'
+import dvaApp from './util/dva'
+import models from './models'

// 如果需要在 h5 环境中开启 React Devtools
// 取消以下注释:
// if (process.env.NODE_ENV !== 'production' && process.env.TARO_ENV === 'h5')  {
//   require('nerv-devtools')
// }

+const app=dvaApp.createApp({models})
+const store=app.getStore()
-const store = configStore()

class App extends Component {

  config = {
    //页面路由表
    pages: [
+    'pages/home/index',
+    'pages/analysis/index',
+    'pages/account/index',
+    'pages/market/index'
+    'pages/login/index'
    ],
    window: {
      backgroundTextStyle: 'light',
      navigationBarBackgroundColor: '#fff',
      navigationBarTitleText: 'WeChat',
      navigationBarTextStyle: 'black'
    },
    //底部工具栏
    tabBar: {
+      list: [{
+        pagePath: "pages/home/index",
+        text: "首页",
+        iconPath: "./images/tab/home.png",
+        selectedIconPath: "./images/tab/home-active.png"
+     }, {
+        pagePath: "pages/analysis/index",
+       text: "分析",
+        iconPath: "./images/tab/cart.png",
+       selectedIconPath: "./images/tab/cart-active.png"
+      }, {
+        pagePath: "pages/market/index",
+        text: "营销",
+        iconPath: "./images/tab/cart.png",
+        selectedIconPath: "./images/tab/cart-active.png"
+      },{
+        pagePath: "pages/account/index",
+        text: "账户",
+        iconPath: "./images/tab/user.png",
+        selectedIconPath: "./images/tab/user-active.png"
+      }],
+    }
+  }

  componentDidMount () {}

  componentDidShow () {}

  componentDidHide () {}

  componentCatchError () {}

  componentDidCatchError () {}

  // 在 App 类中的 render() 函数没有实际作用
  // 请勿修改此函数
  render () {
    return (
      <Provider store={store}>
        <Index />
      </Provider>
    )
  }
}

Taro.render(<App />, document.getElementById('app'))

再次编译运行微信小程序,这次,你将看到微信小程序底部出现了4个图标.点击图标后页面进行了正确的跳转.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,783评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,396评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,834评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,036评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,035评论 5 362
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,242评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,727评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,376评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,415评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,463评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,140评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,734评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,809评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,028评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,521评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,119评论 2 341

推荐阅读更多精彩内容

  • 1.1、 分配更多资源 1.1.1、分配哪些资源? Executor的数量 每个Executor所能分配的CPU数...
    miss幸运阅读 3,167评论 3 15
  • YarnYarn产生背景:Yarn直接来自于MR1.0MR1.0 问题:采用的是master slave结构,ma...
    时待吾阅读 5,485评论 2 23
  • 最近写一个微信小程序的项目,由于是协同开发,前期的搭建工作由另一个妹子完成,现在项目阶段一完成了,为了备忘回顾,做...
    陈小生_1017阅读 24,303评论 17 42
  • 时间过得很快,快到在彼此转身之际,却已经相隔天涯。 你是我忘不了的想念,也是我回不去的从前。生日那天来了一个未知的...
    大喵弥弥阅读 277评论 0 0
  • 昨天,你去看七微的主题分享见面会,七微借傅清时对霓喃的话,在书中写道,“在海洋面前,你只能让自己融入,去适应它的一...
    立黄昏阅读 1,505评论 59 42