以前只使用django和jquery做小項目開發,所以決定搞一搞react框架。React 特點:聲明式設計、虛擬DOM、JSX、組件、數據驅動。 一、環境搭建 1.安裝npm、cnpm 2.安裝react全家桶 3.文件目錄結構 項目啟動時,會載入public下的index.html文件,併進而執 ...
以前只使用django和jquery做小項目開發,所以決定搞一搞react框架。React 特點:聲明式設計、虛擬DOM、JSX、組件、數據驅動。
一、環境搭建
1.安裝npm、cnpm
# 安裝node.js 從而安裝npm,它會在當前用戶家目錄下生成node_moudules文件夾,來對npm安裝的全局第三方包進行管理 brew install node # npm安裝cnpm npm install -g cnpm --registry=https://registry.npm.taobao.org npm config set registry https://registry.npm.taobao.org # 測試 cnpm install express -g
2.安裝react全家桶
# 安裝create-react-app
cnpm install create-react-app -g
# 利用create-react-app 來創建一個項目
create-react-app 項目名稱 # 假設這裡的項目名稱是my-app
# 進入項目文件夾
cd my-app
# 生成配置文件
# cnpm run eject # 生成scripts文件夾和config文件夾,cnpm
# 啟動服務
cnpm start # 它相當於執行 scrpits/starts.js,這個已經在package.json中配置好了命
3.文件目錄結構
my-app - config # 項目預設配置文件,由npm run eject生成 - .gitignore # git配置 - node_modules # 本地第三方安裝包 - package.json # npm項目配置,裡面記錄了項目的名字、版本、依賴包、和自定義配置 - package-lock.json - public # 公共資源 - index.html # 靜態頁面 - README.md - scripts # 配置pacakage.json中的"scripts"定義的命令 - build.js # build命令,可由npm build執行,生成靜態文件用於遷移到生產環境 - start.js # start命令 - test.js - src - App.css # 創建項目時自動生成的css樣式,沒用 - App.js # react組件 - App.test.js - index.css # 入口文件的樣式 - index.js # 入口文件 - log.svg - registerServiceWorker.js
項目啟動時,會載入public下的index.html文件,併進而執行index.js,從而完成整個頁面的渲染。
二、react一些概念
<!--public/index.html簡化如下-->
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="theme-color" content="#000000"> <title>React App</title> <style> body { margin: 0; padding: 0; } </style> </head> <body> <div id="root"></div> </body> </html>
react簡單示例只保留簡化後的public/index.html和src/index.js。
1.ReactDOM.render
index.js中的ReactDOM.render函數用於將標簽或者組件渲染到public下的頁面中。第一個參數可以是任意的單個標簽、div包裹的多個標簽,以及自定義的組件Component。
2.React.Component
React.Component是自定義組件的父類,必須重寫render方法來完成組件的聲明和返回。所謂組件,就是用js類定義的一組html標簽、樣式、數據、事件等的整合,該類可以用<類名 />的方式進行標簽化(實例化),實例化時自動調用render方法,並最終交由React.render完成渲染。
3.組件內部參數聲明和使用
組件內部可以聲明參數,無論是textNode還是其它Node,都是以 { 參數 } 的形式被組件調用。組件react虛擬DOM的具體實現方式,它來完成對頁面和業務邏輯的劃分。
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component{
render (){
const string = "hello, react!";
const style1 = {
color: "white"
};
const style2 = {
textAlign:"center",
fontStyle: "border",
backgroundColor: "green"
};
return (
<div style={style1}>
{/*這是一段註釋*/}
<h2 style={style2}>{ string }</h2>
</div>
)
}
}
ReactDOM.render(<div>
<h2>hello, react!</h2>
<App />
</div>, document.getElementById('root'));
組件間可以實現嵌套。
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component{
render (){
return <h2>這是子組件</h2>
}
}
class App2 extends React.Component{
render (){
const style = {
backgroundColor: "blue"
};
return (
<div style={style}>
<h2>這是父組件</h2>
<App />
</div>
)
}
}
ReactDOM.render(<App2 />, document.getElementById('root'));
4.靜態數據傳遞--props
組件間數據通過this.props屬性來進行數據傳遞。父組件在標簽化時通過屬性的方式傳遞數據,子組件通過this.props來獲取所有的數據。
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component{
render (){
return <h2>App: { this.props.name }, { this.props.age }</h2>
}
}
class App2 extends React.Component{
render (){
const name = "Sun", age=5000;
return (
<div>
<h2>App2: { this.props.name }, { this.props.age }</h2>
<App name={name} age={age} />
</div>
)
}
}
ReactDOM.render(<App2 name="Li" age="20" />, document.getElementById('root'));
註意:1.props只能傳遞靜態數據,無法與用戶交互;2.{}不是Object對象,如果想寫<App2 obj={name: "Li", age: 20} />,就必須提前聲明。3.style樣式作為數據傳遞是無效且荒謬的。
const obj = {name: "Li",age: 20};
<App2 obj={obj}/>
5.動態數據交互--state
state用於負責處理動態數據。數據需要在構造函數中通過this.state事先聲明,併在事件中通過this.setSate完成數據更新。
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component{
constructor(props){
super(props);
this.state = {
nameList:["Sun", "Li", "Zhao", "Qian"]
}
}
addPerson(){
this.setState({
nameList: [...this.state.nameList, "zhou"]
})
}
render (){
return (
<div>
<button onClick={()=>this.addPerson()}>加入周先生</button>
<ul>
{this.state.nameList.map((name, index) => <li key={index+1}>{name}</li>)}
</ul>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
6.事件綁定
在react中,事件以屬性的方式直接在標簽中使用,其調用函數要用一層函數包裹。調用函數可以直接寫在對象內部,並且要用bind進行監聽和更新。
在上面的示例中,我們通過這一行代碼代替了bind的過程:
<button onClick={()=>this.addPerson()}>加入周先生</button>
鑒於js中沒有局部作用域只有函數作用域,所以如果直接寫onClick={this.addPerson}時,this指的就是windo對象。用一層函數包裹時,this對象指的就是這個函數。
其它的做法有兩種:
首先,在事件綁定時直接使用this.function:
<button onClick={this.addPerson}>加入周先生</button>
其次可以用箭頭函數來聲明addPerson函數,本質上和上面使用的方法一樣:
addPerson = ()=>{
this.setState({
nameList: [...this.state.nameList, "zhou"]
})
};
或者可以不改addPersonn函數,而是在構造器中添加上這麼一句話:
this.addPerson = this.addPerson.bind(this)
7.條件渲染
render本身是一個實例方法,支持js中的各種代碼邏輯。可以用if-else來控制返回結果。條件渲染在用戶登錄和重定向中使用的比較頻繁。
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component{
constructor(props){
super(props);
this.state = {isLogIn: false}
}
login (){
this.setState({isLogIn: true})
};
logout(){
this.setState({isLogIn: false})
}
render (){
let button=null;
if (this.state.isLogIn){
button = <button onClick={()=>this.logout()}>註銷</button>
}else {
button = <button onClick={()=>this.login()}>登錄</button>
}
return button
}
}
ReactDOM.render(<App />, document.getElementById('root'));
也可以用三元表達式簡寫:
render (){
return (
<div>
{this.state.isLogIn ? <button onClick={()=>this.logout()}>註銷</button> : <button onClick={()=>this.login()}>登錄</button>}
</div>
)
}
註意:由於{}傳遞的數據可以是任意值,所以需要用div包裹。
8、組件的生命周期
組件的聲明周期可分成三個狀態:Mounting,已插入真實 DOM;Updating,正在被重新渲染;Unmounting:已移出真實 DOM。如下圖所示(本圖根據Rosen老師示例改寫)。
各個函數的釋義(摘自菜鳥教程):
componentWillMount 在渲染前調用,在客戶端也在服務端。
componentDidMount : 在第一次渲染後調用,只在客戶端。之後組件已經生成了對應的DOM結構,可以通過this.getDOMNode()來進行訪問。 如果你想和其他JavaScript框架一起使用,可以在這個方法中調用setTimeout, setInterval或者發送AJAX請求等操作(防止異部操作阻塞UI)。
componentWillReceiveProps 在組件接收到一個新的 prop (更新後)時被調用。這個方法在初始化render時不會被調用。
shouldComponentUpdate 返回一個布爾值。在組件接收到新的props或者state時被調用。在初始化時或者使用forceUpdate時不被調用。
可以在你確認不需要更新組件時使用。
componentWillUpdate在組件接收到新的props或者state但還沒有render時被調用。在初始化時不會被調用。
componentDidUpdate 在組件完成更新後立即調用。在初始化時不會被調用。
componentWillUnmount在組件從 DOM 中移除的時候立刻被調用。
9、綜合:使用component請求後臺數據並交給組件
在my-app下建立server/server.js文件,啟動一個後端服務:
const express = require('express');
const app = express();
app.get("/data", function (req, res) {
res.json({"name": "old monkey", "age": 5000})
});
app.listen(3002, function () {
console.log("Node app start at port 3002.")
});
Terminal啟動該服務: node server/server.js。此時可以訪問http://localhost:3002/data來獲取json數據。
安裝axios: cnpm install axios --save。併在package.json的配置文件中添加"proxy"配置,讓它轉換埠到3002:
// package.json
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.18.0",
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-scripts": "1.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"proxy": "http://localhost:3002"
}
在src/index.js中寫組件:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';
class App extends React.Component{
constructor(props){
super(props);
this.state = {name: "monkey", age: 100}
}
// 在component內部使用ajax請求數據,並通過setState傳遞給App。
componentDidMount(){
axios.get("/data").then(res=>{
if(res.status === 200){
console.log(res);
this.setState({name: res.data.name, age: res.data.age});
}
}, err=>{
console.log(err);
})
}
addAge(){
this.setState({age: this.state.age + 1})
};
decAge(){
this.setState({age: this.state.age - 1})
}
render (){
const style={
display: "inline-block",
width: "150px",
height: "40px",
backgroundColor: "rgb(173, 173, 173)",
color: "white",
marginRight: "20px"
};
return (
<div>
<h2>this {this.state.name } is { this.state.age } years old.</h2>
<button style={style} onClick={()=>this.addAge()}>增加一歲</button>
<button style={style} onClick={()=>this.decAge()}>減少一歲</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
三、redux、redux-thunk與react-redux
react本身能夠完成動態數據的監聽和更新,如果不是必要可以不適用redux。
安裝redux: cnpm install redux --save。
1.react基本用法
redux是獨立的用於狀態管理的第三方包,它建立狀態機來對單項數據進行管理。
上圖是個人粗淺的理解。用代碼驗證一下:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from "redux";
function reducer(state={name: "monkey", age: 5000}, action){
switch (action.type){
case "add":
state.age ++;
return state;
case "dec":
if (state.age <= 4995){
state.name = "small monkey";
}
state.age --;
return state;
default:
return state;
}
}
const store = createStore(reducer);
const add = {type: "add"}, dec={type: "dec"};
class App extends React.Component{
render (){
const style={
display: "inline-block",
width: "150px",
height: "40px",
backgroundColor: "rgb(173, 173, 173)",
color: "white",
marginRight: "20px"
};
const store = this.props.store;
// console.log(store.getState());
const obj = store.getState();
// console.log(obj);
return (
<div>
<h2>this { obj.name } is { obj.age } years old.</h2>
<button style={style} onClick={()=>store.dispatch(this.props.add)}>增加一歲</button>
<button style={style} onClick={()=>store.dispatch(this.props.dec)}>減少一歲</button>
</div>
)
}
}
function render() {
ReactDOM.render(<App store={store} add={ add } dec={ dec } />, document.getElementById('root'));
}
render();
store.subscribe(render);
因為action必須是個對象,所以只能寫成add = {type: "add"}的形式,而不能直接寫參數"add"。同樣地,在reducer中寫switch時將action.type作為參數。
action和state一一對應,要使用action必須要在reducer里聲明。
redux沒有用state來實現動態數據更新,而是通過props來傳遞數據,因此在組件內部只能通過props獲取store,以及store.getState()獲取state。
redux將ReactDOM.render進行了一次封裝來設置監聽。
redux對數據和組件進行瞭解耦,因而可以進行文件拆分。
把action寫成函數的形式:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from "redux";
const Add = "add", Dec="dec";
function reducer(state={name: "monkey", age: 5000}, action){
switch (action.type){
case Add:
state.age ++;
if (state.age > 5005){
state.name = "old monkey";
}
return state;
case Dec:
if (state.age <= 4995){
state.name = "small monkey";
}
state.age --;
return state;
default:
return state;
}
}
const store = createStore(reducer);
class App extends React.Component{
render (){
const style={
display: "inline-block",
width: "150px",
height: "40px",
backgroundColor: "rgb(173, 173, 173)",
color: "white",
marginRight: "20px"
};
const store = this.props.store;
const state = store.getState();
return (
<div>
<h2>this { state.name } is { state.age } years old.</h2>
<button style={style} onClick={()=>store.dispatch(add())}>增加一歲</button>
<button style={style} onClick={()=>store.dispatch(dec())}>減少一歲</button>
</div>
)
}
}
function render() {
ReactDOM.render(<App store={store} add={ add } dec={ dec } />, document.getElementById('root'));
}
render();
store.subscribe(render);
2.react非同步
當一個父組件中有多個子組件時,如果每一個組件發生狀態變化,store都要重新渲染一遍。使用非同步可以只重新渲染髮生狀態變化的組件。
安裝redux和thunk的中間件redux-thunk:cnpm install redux-thunk。引入兩個函數,並修改createStore如下,其餘代碼保持不變:
import { createStore, applyMiddleware } from "redux";
import thunk from 'redux-thunk';