1. 基礎(chǔ)知識
1.1 this指針
this指針大概是javascript中最令初學(xué)者困惑的語法了寇漫,簡單說稳吮,this指針就是指向函數(shù)或方法運行的上下文環(huán)境斧拍。既然叫上下文環(huán)境,肯定和運行的環(huán)境相關(guān)蚁孔。
在瀏覽器環(huán)境下:
- 當(dāng)this出現(xiàn)在函數(shù)調(diào)用中,指向的是他運行的上下文:window對象惋嚎;
- 當(dāng)this出現(xiàn)在對象方法調(diào)用時杠氢,則指向了他運行的上下文:對象本身;
讓我們舉個例子說明一下:
function fn(){
console.log(this);
}
var obj={
fn:fn
}
fn();
obj.fn();
通過運行代碼,你會發(fā)現(xiàn),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方法很麻煩,所以重新定義一個函數(shù) getId,指向document.getElementById方法.
但是你發(fā)現(xiàn),在chrome瀏覽器中調(diào)用getId方法居然系統(tǒng)會拋一個異常:
test.htm:9 Uncaught TypeError: Illegal invocation
發(fā)生異常的原因就在于,getElementById內(nèi)部實現(xiàn)使用了this指針指向document對象.但是,當(dāng)你用自己定義的getId方法時,getElementById已經(jīng)由對象調(diào)用變成了方法調(diào)用,this指針被指向了window對象.
既然this指針有這樣的不確定性鼻百,那么,自然就可以想到如何根據(jù)需要變更他的指向摆尝。
變更this指針指向有兩種方法愕宋,定義時綁定和運行時綁定。
- 定義時綁定 bind
- 運行時綁定 apply/call
還是讓我們看個例子:
對于我們上個getId的例子,你只要這樣調(diào)用就可以了
定義時通過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兩者的區(qū)別是:apply綁定時结榄,傳入的參數(shù)為數(shù)組形式中贝,而call綁定則是采用枚舉的方式。所以如果getId方法需要使用apply方法時,必須將參數(shù)包裝成數(shù)組的形式.
1.2 高階函數(shù)
在javascript語言中,函數(shù)是一類成員,函數(shù)可以作為變量,也可以作為輸入?yún)?shù)和返回參數(shù).將函數(shù)作為輸入?yún)?shù)或輸出參數(shù)的函數(shù)稱之為高階函數(shù).后面我將會帶大家一起了解一下高階函數(shù).
1.2.1 閉包
讓我們看一下第一個高階函數(shù),閉包.
閉包利用了javascript函數(shù)作用域內(nèi)變量被引用不會消除的特性.閉包被應(yīng)用的場景非常多.
讓我們先看一個獲取遞增ID的例子,首先,讓我們看一下傳統(tǒng)的方法,傳統(tǒng)的方法獲取遞增ID,你需要先做一個全局變量.
let globalID=0
function getID(){
return globalID++;
}
console.log(getID(),getID(),getID())
這種方法由于使用了全局變量,任何一個人都有可能不經(jīng)意的修改globalID的值導(dǎo)致你的方法失效.
采用閉包的寫法,你先創(chuàng)建一個crGetID方法通過閉包保存ID,并返回getID函數(shù).然后通過getID方法獲取ID
function crGetID(){
let id=0;
return function(){
return id++;
}
}
var getID=crGetID();
console.log(getID(),getID(),getID())
這樣,沒有人可以直接修改你的ID值.
1.2.2 currying
讓我們再看一個閉包的應(yīng)用:currying,currying解決的問題是把一個函數(shù)的多個參數(shù)轉(zhuǎn)換為單參數(shù)函數(shù).
舉個例子,假設(shè)我們需要累計一個用戶7天的數(shù)據(jù):
function add(d1,d2,d3,d4,d5,d6,d7){
return d1+d2+d3+d4+d5+d6+d7
}
如果是30天,可能需要30個輸入?yún)?shù),如果不定天數(shù)呢?
采用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());
通過將一個函數(shù)currying后,函數(shù)可以隨時被調(diào)用,直到輸入?yún)?shù)為空時才進(jìn)行計算.
閉包的特性使之成為javascript中運用最廣的特性.后續(xù)在代碼中,我們還會繼續(xù)看到大量的閉包用法.
1.3 es6語法
react 大量使用了es6的語法臼朗,如果你對javsascript的印象還停留在原始的印象里邻寿,你可能根本沒法看懂react的代碼。所以在這里我們簡單對用到的es6語法做一些介紹视哑,并盡可能以react實際使用作為學(xué)習(xí)的例子绣否。詳細(xì)的es6語法介紹,可以參考相關(guān)的技術(shù)文檔挡毅。
1.3.1變量解析
- 數(shù)組變量解析
es6 支持對數(shù)組直接進(jìn)行解析蒜撮,舉個例子,如果需要對變量x,y互換值,傳統(tǒng)的做法是:
function(x,y){
var t;
t=x;
x=y;
y=t;
}
如果用數(shù)組解析段磨,就容易多了:
let [x,y]=[y,x]
數(shù)組解析可以用到輸入?yún)?shù)傳遞
function test ([x,y,z]){
return x+y+z;
}
console.log(test([1,2,3]))
上面的例子,打印出來的結(jié)果是6.
還可以用到一次返回多個參數(shù):
function retMult(){
return [1,2,3]
}
let [x,y,z]=retMult();
console.log(x,y,z)
函數(shù)會打印出 1,2,3
- 對象變量解析
對象解析用的更廣泛取逾,讓我們舉個簡單的例子:
let {x:x,y:y}={x:2,y:3}
console.log(x,y);
函數(shù)打印結(jié)果 2,3
還可以簡寫為:
let {x,y}={x:2,y:3}
console.log(x,y);
讓我們看一下下面的例子,如果對象屬性名稱和變量名稱可以更進(jìn)一步進(jìn)行簡寫:
let x=1;
let obj={x}; //相當(dāng)于 obj={x:x}
console.log(obj.x);
和數(shù)組解析一樣,對象解析大量應(yīng)用在輸入?yún)?shù)的傳值上,讓我們舉一個redux的實際應(yīng)用的例子(這個例子里,假設(shè)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箭頭函數(shù)
箭頭函數(shù)可以讓代碼更加簡潔和直觀苹支,另外砾隅,由于箭頭函數(shù)對this指針的特殊處理,因此债蜜,被大量的運用晴埂。
讓我們還是以數(shù)組提供的map函數(shù)為例子說明:
let arr=[1,2,3,4];
let m=arr.map(v=>v*2);
使用箭頭函數(shù),即簡潔又直觀,對數(shù)組中的每個元素直接乘以2.
如果使用傳統(tǒng)的方式,你需要這樣寫:
let n=arr.map(function(v){
return v*2;
})
console.log(n);
使用箭頭函數(shù)需要注意以下幾點:
- 如果是一個參數(shù),可以省略(),如果多個參數(shù)或沒有參數(shù),則必須使用()
- 如果函數(shù)直接返回箭頭后的表達(dá)式,可以不加{},否則,需要在箭頭后使用{}
讓我們再舉個數(shù)組提供的reduce方法的實際例子:
var r=[1,2,3,4].reduce((p,n)=>p+n);
console.log(r);
這個例子通過reduce函數(shù)計算數(shù)字元素的累加和,reduce函數(shù)的輸入?yún)?shù)是一個函數(shù),函數(shù)的輸入?yún)?shù)分別為累加之和p以及下一個元素n.
如果用傳統(tǒng)的方法,你需要這樣寫:
var r=[1,2,3,4].reduce(function(p,n){
return p+n;
})
console.log(r);
1.3.3類
ES6提供了類,類實際上就是一個語法糖.讓我們還是通過一個具體的例子來看一下類的實現(xiàn):
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{
}
你可以根據(jù)需要實現(xiàn)類的構(gòu)建器,構(gòu)建器可以通過super方法引用父類的構(gòu)建器:
constructor(props){
super(props)
}
類的方法可以縮寫為methodName(){}的形式
sub(){
let value=this.state.value-1;
this.setState({value})
}
1.3.4裝飾器
1.4 異步函數(shù)
2. 代碼框架
2.1nodejs
2.2webpack
2.3react
2.3.1第一個react程序
由于react需要依賴大量的依賴庫,如通過babel對es6的轉(zhuǎn)化寻定,css的渲染…對于新手來說儒洛,可能這一大堆配置就讓人望而卻步。為了簡化開發(fā)環(huán)境的搭建狼速,讓我們直接用create-react-app 搭建腳手架(具體命令含義可以先不用理解):
首先,我們安裝create-react-app
npm install -g create-react-app
安裝成功后,就可以開始使用
mkdir basic
create-react-app basic
basic是我們第一個react程序的名稱,create-react-app命令會運行一段時間,幫我們搭建腳手架和開發(fā)環(huán)境.命令執(zhí)行后,我們開始啟動我們的應(yīng)用:
cd basic
yarn start
yarn start命令后,webpack會啟動webpack-dev-server跟蹤我們代碼修改,然后自動啟動一個端口為3000的HttpServer幫我們進(jìn)行調(diào)試,并且很貼心的彈出URL為http://localhost:3000的瀏覽器頁面,顯示我們的第一個react應(yīng)用.
好,讓我們開始學(xué)習(xí)第一個react程序,首先,讓我們看一下腳手架幫我們搭建的目錄結(jié)構(gòu)晶丘。
其中/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組件,系統(tǒng)如何區(qū)分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"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
}
export default App;
好唐含,現(xiàn)在讓我們開發(fā)一個計數(shù)器程序來學(xué)習(xí)一下React開發(fā).首先,讓我們自己定義一個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})
}
}
//將組件導(dǎo)出模塊
export default Counter
然后,修改index.js文件,渲染我們新開發(fā)的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();
頁面會自動刷新修改后的內(nèi)容,(是不是很驚訝)顯示結(jié)果.點擊 + 和 - 按鈕系統(tǒng)會自動顯示最新的counter.
讓我們來分析一下代碼:
首先 ,我們的Counter組件繼承自React.Component.我們的Counter組件繼承Component組件,并且實現(xiàn)了構(gòu)建器.在構(gòu)建器中,完成了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組件的核心部分,主要完成組件的表現(xiàn).在這里,render方法返回了一個jxs的代碼段.所謂的jxs,簡單說就是嵌入了react語句的html代碼段.
在這里,你特別需要注意的是,React會自動跟蹤state值的變化進(jìn)行渲染,因此,你不需要像傳統(tǒng)開發(fā)一樣手動渲染數(shù)據(jù),只需要簡單的標(biāo)明會發(fā)成變更的數(shù)據(jù)即可:
<span>{this.state.value}</span>
在這里,{}表示的是里面的部分是由react代碼構(gòu)成.
這個代碼段有幾個特別需要注意的地方:
- return語句直接返回JSX時,必須用()進(jìn)行包裹,下面的語句由于<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事件命名機(jī)制不同,讓我們對比一下React的寫法:
<button onClick={this.add.bind(this)}>+</button>
下面是HTML的寫法:
<button onclick="test()">test</button>
有三個重要的區(qū)別:
1.React事件觸發(fā)是駱駝命名方式.而HTML的觸發(fā)方式是全部小寫;
2.React事件觸發(fā)是函數(shù)名,而HTML的觸發(fā)方式是函數(shù)執(zhí)行代碼塊;
3.React事件處理函數(shù)this指針不會綁定任何對象,而HTML指針會自動綁定到window對象;
- 如果處理state,則React事件處理函數(shù)需要綁定this指針到組件
這就是以下代碼的原因,通過bind方法將函數(shù)this指針綁定到React Component:
<button onClick={this.add.bind(this)}>+</button>
- state的處理原則
讓我們看一下事件處理方法的實現(xiàn):
desc(){
let value=this.state.value-1;
this.setState({value})
}
add(){
let value=this.state.value+1;
this.setState({value})
}
代碼很簡單,但是有幾個需要注意的地方:
- 不能直接修改state的值,必須通過this.setState方法修改,否則,系統(tǒng)不會進(jìn)行渲染.
let value=this.state.value+1
this.state.value=value
- state的原始值不能修改,因此,以下代碼是無效的:
let value=this.state.value++; //this.state.value++修改了原始的state的值
this.setState({value})
第一個react程序結(jié)束了,React的邏輯很簡單浅浮,每個組件都有一個state值,組件通過監(jiān)控state的狀態(tài)變化實現(xiàn)頁面渲染.
最后,別忘了需要從模塊中導(dǎo)出我們的組件:
export default Counter
導(dǎo)出是,如果不增加default參數(shù),導(dǎo)入時需要將組件名稱用{}括起來.一個模塊中智能有唯一的一個default組件.
2.3.2 React組件間通訊
從第一個例子我們可以發(fā)現(xiàn),React組件開發(fā)很容易,通過監(jiān)控組件state值的變化,實現(xiàn)自動的渲染,極大的減輕了開發(fā)的工作量.但是每個組件都有自己的state,如果多個組件需要通訊,問題就變得復(fù)雜了.
讓我們看下面的這個例子,在這個例子,組件Control由3個Counter構(gòu)成,每個Counter都可以自動增減,CounterControl顯示的值是3個Counter的累加值.
這個例子顯示了組件間如何進(jìn)行通信.
先讓我們看一下CounterControl組件的代碼:
import React,{Component } from "react";
import Counter from "./counter"
class counterControl extends Component{
constructor(){
super();
//設(shè)置組件的初始值
this.state={value:0}
}
//這個地方需要特別注意,change是Counter組件每次點擊發(fā)生變化的值
change(change){
//不可以修改state的原始值
let value=this.state.value;
value+=change;
//必須通過this.setState方法進(jìn)行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組件也發(fā)生了變化,增加了name屬性和change方法.
<Counter name={"three"} change={this.change.bind(this)}/>
由于組件彼此state獨立,因此,組件之間的通訊就落到了change方法里.讓我們看一下change方法的實現(xiàn):
//這個地方需要特別注意,change是Counter組件每次點擊發(fā)生變化的值
change(change){
//不可以修改state的原始值
let value=this.state.value;
value+=change;
//必須通過this.setState方法進(jìn)行state的修改
this.setState({value:value})
}
change方法實際是CounterControl通過屬性傳遞給Counter子組件的回調(diào)函數(shù).他的實現(xiàn)原理是每次Counter組件被點擊時,把Counter組件state值的變化回調(diào)至CounterControl,從而實現(xiàn)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變更的同時,都需要調(diào)用通過props屬性傳遞的change方法回調(diào)CounterControl提供的chanage方法,實現(xiàn)state變化的通知.
2.3.3 優(yōu)化
上面的例子我們發(fā)現(xiàn),Counter組件如果需要整合到CounerControl組件中,就必須進(jìn)行修改.
原來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的回調(diào)方法
this.props.change(-1)
}
add(){
let value=this.state.value+1;
this.setState({value})
//增加了change的回調(diào)方法
this.props.change(1);
}
也就是說,Counter組件并不是一個通用的組件.造成這種情況的原因,是由于Counter組件增加了過多的業(yè)務(wù)邏輯.
如果把組件的顯示和業(yè)務(wù)邏輯進(jìn)行剝離成內(nèi)部組件和容器組件,就可以解決這個問題.內(nèi)部組件只負(fù)責(zé)顯示,容器組件則負(fù)責(zé)具體的業(yè)務(wù)邏輯和state處理.讓我們看一下如何進(jìn)行剝離:
新的NgCounter內(nèi)部組件代碼如下:
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組件不再進(jìn)行任何業(yè)務(wù)邏輯的處理,也不處理state相關(guān)的數(shù)據(jù).它只是按照傳遞的屬性值進(jìn)行顯示或回調(diào).
我們管這種不處理任何state的組件稱之為無狀態(tài)組件.無狀態(tài)組件可以進(jìn)一步簡化為函數(shù),如下所示:
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>
)
}
無狀態(tài)組件函數(shù)由于沒有this指針,屬性props由容器組件傳遞.
讓我們看一下容器組件如何進(jìn)行處理
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;
通過內(nèi)部組件和容器組件的拆分,如果組件需要嵌入其他組件,則只需要修改容器組件即可.內(nèi)部組件不需要進(jìn)行任何調(diào)整.
需要特別注意的是,此時導(dǎo)出的組件為容器組件.
我們把CounterControl也進(jìn)行了改造,相應(yīng)的代碼如下:
import React,{Component} from 'react';
import NgCounter from './ngCounter'
//內(nèi)部無狀態(tài)組件退化為函數(shù)
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>
)
}
//容器組件負(fù)責(zé)具體的業(yè)務(wù)邏輯和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 (
//返回內(nèi)部組件
<NgCounterControl add={this.add.bind(this)}
sub={this.sub.bind(this)}
value={this.state.value} />
)
}
}
//導(dǎo)出容器組件
export default Container;
2.3.4 總結(jié)
通過上面的例子,我們可以發(fā)現(xiàn),雖然我們把組件拆分為內(nèi)部組件和容器組件,實現(xiàn)了內(nèi)部組件的獨立性,但是,由于React組件彼此都維護(hù)自己的state,當(dāng)多個組件需要同步state值的時候,情況還是變得很復(fù)雜,組件必須通過props屬性傳遞回調(diào)方法層層調(diào)用.因此,對于多個組件協(xié)調(diào)工作時,這種實現(xiàn)方法就顯得很笨拙而且效率低下.
2.4Ant.Design的整合
在開始redux模塊前,讓我們先介紹一下Ant.Design模塊.
到目前為止,我們自己開發(fā)的組件都是基于html的原生元素.實際上,Ant.Design已經(jīng)幫我們精心設(shè)計了大量優(yōu)秀的組件,我們可以直接使用,這一部分,我們將介紹給大家如何使用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文件,簡單實現(xiàn)一個無狀態(tài)的組件.
這個組件里我們簡單使用了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文件
//導(dǎo)入剛才新建的組件
import Antd from './antd'
//渲染該組件
ReactDOM.render(<Antd/>, document.getElementById('root'));
現(xiàn)在我們應(yīng)該可以看到Ant.Design開始正式工作了.
2.4redux組件
2.4.1 reduct組件原理
reduct組件由store action reducer構(gòu)成:
- store:全局唯一和共享的數(shù)據(jù)存儲;store的主要方法有
- dispatch:負(fù)責(zé)調(diào)用reducer處理action,生成新的state
- subscribe:負(fù)責(zé)監(jiān)聽state的變化,如果state發(fā)生了變化,則調(diào)用相關(guān)的回調(diào)函數(shù)進(jìn)行處理
- action:由動作類型type和攜帶的數(shù)據(jù)構(gòu)成,reducer負(fù)責(zé)根據(jù)action的type進(jìn)行相應(yīng)的處理;
- reducer:負(fù)責(zé)根據(jù)action處理state,并且保證每次reducer操作,都必須返回新的state;
2.4.2 reduct的使用
讓我們通過一個實際的例子學(xué)習(xí)怎樣使用reduct,
首先,我們需要安裝redux
yarn add redux
實現(xiàn)reducer,創(chuàng)建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);
創(chuàng)建store后,我們需要所有的組件都能夠訪問該store,因此,我們需要把store放到所有組件的父容器上.在這里,我們通過新建一個Provider組件實現(xiàn)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的父組件只要實現(xiàn)getChildContext方法返回Context對象.并且設(shè)置該屬性的類型為PropTypes.object即可.
因為Provider作為父組件,內(nèi)部會嵌套子組件,所以render方法直接返回this.props.children即可.
讓我們 看一下如何使用Provide組件:
<Provider store={store}>
<ReduxCounter name={"one"} />
<ReduxCounter name={"two"} />
<ReduxCounter name={"three"} />
<Panel/>
</Provider>
this.props.children指向的即是Provider組件嵌套的內(nèi)容.
讓我們看一下ReduxCounter組件的實現(xiàn):
function ReduxCounter(props){
return (
<div>
<span>{props.name}:{props.value}</span>
<button onClick={props.add}>+</button>
<button onClick={props.sub}>-</button>
</div>
)
}
ReduxCounter 組件是一個內(nèi)部無狀態(tài)組件,只負(fù)責(zé)渲染
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
}
所有的邏輯和狀態(tài)的處理都由ReduxCounterContainer組件完成.
在這里,需要特別注意的是
ReduxCounterContainer.contextTypes={
store:PropTypes.object
}
如我們之前所說,所有的子組件,如果希望使用父組件提供的context,必須聲明該組件的contextTypes為PropTypes.object
下一步,讓我們看一下Panel組件的實現(xiàn)
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的回調(diào)
context.store.subscribe(this.change.bind(this));
最后,讓我們看一下ReduxCounterControl組件的實現(xiàn)
function ReduxCounterControl(){
return (
<Provider store={store}>
<ReduxCounterContainer name={"one"} />
<ReduxCounterContainer name={"two"} />
<ReduxCounterContainer name={"three"} />
<Panel/>
</Provider>
)
}
export default ReduxCounterControl;
至此,我們完成了redux的使用,總結(jié)一下,redux使用包括以下幾步:
- 創(chuàng)建reducer
- 創(chuàng)建store
- 創(chuàng)建Provider,用來保存和傳遞store
- 需要跟蹤store值的組件,調(diào)用store的subscribe方法監(jiān)控state值的變化
- 各組件通過store.dispatch方法調(diào)用reducer處理并返回state
- 組件發(fā)現(xiàn)state值發(fā)生變化,自動渲染
下一節(jié)我們將學(xué)習(xí)如何使用react-redux簡化這個流程
2.4.3 react-redux的使用
2.5mockjs
2.6dva
2.7taro
2.8taro-ui
3 開發(fā)環(huán)境搭建
3.1搭建taro腳手架項目
首先,讓我們搭建taro的腳手架,這個例子里我們假設(shè)項目名稱為wos,則在命令行下運行
taro init wos
回答系列問題后,命令行會自動創(chuàng)建以wos為文件夾的腳手架項目.
這個教程里,是否使用TypeScript選擇了否,css預(yù)處理器選擇了less,模板選擇了redux
如下所示:
Taro v1.2.4
Taro即將創(chuàng)建一個新項目!
Need help? Go and open issue: https://github.com/NervJS/taro/issues/new
? 請輸入項目介紹! wos
? 是否需要使用 TypeScript 捷枯? No
? 請選擇 CSS 預(yù)處理器(Sass/Less/Stylus) Less
? 請選擇模板 Redux 模板
執(zhí)行成功后,可以開始編譯微信小程序:
cd wos
npm run dev:weapp
命令會執(zhí)行一會兒,當(dāng)出現(xiàn)
監(jiān)聽文件修改中...
說明編譯完成,可以打開微信小程序開發(fā)工具,新建項目,選擇wos文件夾后,可以看到小程序已經(jīng)可以正常運行.
3.2整合tara-ui
在命令行下輸入
yarn add taro-ui
現(xiàn)在,taro-ui已經(jīng)整合成功,我們可以簡單修改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
打開微信小程序開發(fā)工具,這時候你可以看到,首頁里,已經(jīng)多了一個藍(lán)色的按鈕.
至此,我們已經(jīng)搭建了taro的開發(fā)環(huán)境.
3.3整合dva框架
下面的步驟,我們將整合dva框架.首先,我們先安裝相關(guān)的依賴庫
yarn add dva-core dva-loading
我們把所有的配置都集中在一起,在src目錄下新建一個config文件夾,生成index.js,代碼如下:
// 請求連接前綴
export const baseUrl = 'http://localhost:3721';
// 輸出日志信息
export const noConsole = false;
dva會按照路由多model進(jìn)行分層管理,在taro框架里,我們沒有使用umi,所以需要統(tǒng)一管理model,讓我們在/src目錄下新建models文件夾,生成index.js文件,我們現(xiàn)在還沒有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} 】【接口響應(yīng):】`,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(`網(wǎng)絡(luò)請求錯誤滚秩,狀態(tài)碼${statusCode}`);
}
})
}
典型的dva的開發(fā)目錄如下:
每個文件件都需要4個文件,太復(fù)雜,所以我們在根目錄下增加一個腳本template.js,自動生成文件:
/**
* pages模版快速生成腳本,執(zhí)行命令 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}已創(chuàng)建,請手動增加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參數(shù),如 /api/hi?name=tony
username:req.query["name"],
}
)
},
//可以直接使用mockjs生成mock數(shù)據(jù)
'GET /api/mock':mockjs.mock({
'list|10-100':1,
})
}
//使用delay方法可以延遲返回數(shù)據(jù)
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數(shù)據(jù):
{"id":1,"username":"kenny","sex":6}
輸入 http://localhost:3721/api/hi/name='tony',1秒后返回mock數(shù)據(jù):
{"id":1,"username":"tony"}
3. 完善代碼
我們設(shè)計的App分為四個模塊:
- 首頁
- 分析模塊
- 營銷模塊
- 賬戶模塊
- 登陸模塊
我們使用之前增加的模板功能,增加另外的3個頁面:
npm run g home
npm run g analysis
npm run g account
npm run g login
3.1微信小程序首頁
現(xiàn)在我們開始修改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 環(huán)境中開啟 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() 函數(shù)沒有實際作用
// 請勿修改此函數(shù)
render () {
return (
<Provider store={store}>
<Index />
</Provider>
)
}
}
Taro.render(<App />, document.getElementById('app'))
再次編譯運行微信小程序,這次,你將看到微信小程序底部出現(xiàn)了4個圖標(biāo).點擊圖標(biāo)后頁面進(jìn)行了正確的跳轉(zhuǎn).