1、npm init 生成 package.json 文件.
2、安裝各種需要的依賴:
npm install
--save react - 安裝React.
npm install
--save react-dom 安裝React Dom,這個(gè)包是用來(lái)處理virtual DOM。這里提一下用React Native的話,這里就是安裝react-native。
npm install
--save-dev webpack - 安裝Webpack, 現(xiàn)在最流行的模塊打包工具.
npm install
--save-dev webpack-dev-server - webpack官網(wǎng)出的一個(gè)小型express服務(wù)器,主要特性是支持熱加載.
npm install
--save-dev babel-core - 安裝Babel, 可以把ES6轉(zhuǎn)換為ES5,注意Babel最新的V6版本分為babel-cli和babel-core兩個(gè)模塊,這里只需要用babel-cor即可。
安裝其他的babel依賴(babel真心是一個(gè)全家桶,具體的介紹去官網(wǎng)看吧..我后面再總結(jié),這里反正全裝上就是了):
npm install
--save babel-polyfill - Babel includes a polyfill that includes a custom regenerator runtime and core.js. This will emulate a full ES6 environment
npm install
--save-dev babel-loader - webpack中需要用到的loader.
npm install
--save babel-runtime - Babel transform runtime 插件的依賴.
npm install
--save-dev babel-plugin-transform-runtime - Externalise references to helpers and builtins, automatically polyfilling your code without polluting globals.
npm install
--save-dev babel-preset-es2015 - Babel preset for all es2015 plugins.
npm install
--save-dev babel-preset-react - Strip flow types and transform JSX into createElement calls.
npm install
--save-dev babel-preset-stage-2 - All you need to use stage 2 (and greater) plugins (experimental javascript).
3、打開(kāi) package.json 然后添加下面的scripts:
"scripts": { "start": "webpack-dev-server --hot --inline --colors --content-base ./build", "build": "webpack --progress --colors" }
命令行輸入 npm start 將要啟動(dòng)webpack dev server.
命令行輸入 npm build 將會(huì)進(jìn)行生產(chǎn)環(huán)境打包.
4、啟動(dòng)webpack
Webpack是我們的打包工具,在我們的開(kāi)發(fā)環(huán)境中具體很重要的作用,具有很多非常便捷的特性,尤其是熱加載hot reloading. webpack.config.js 是如下所示的webpack的配置文件. 隨著app的不斷變化,配置文件也會(huì)不斷的更新,這里我們就用默認(rèn)的webpack.config.js來(lái)命名這個(gè)配置文件,假如你用別的名字比如webpack.config.prod.js那么上面的腳本build就需要相應(yīng)的改變指定相應(yīng)的配置文件名字:"build": "webpack webpack.config.prod.js --progress --colors"
var webpack = require('webpack'); module.exports = { entry: './src/app.js', output: { path: __dirname + '/build', filename: "bundle.js" }, module: { rules: [{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', query: { plugins: ['transform-runtime'], presets: ['es2015', 'react', 'stage-2'] } }, { test: /\.css$/, loader: "style-loader!css-loader" }] } };
OK,我們項(xiàng)目的基本配置終于完成了,是時(shí)候開(kāi)始寫Reac代碼了.
React 基礎(chǔ) - 建立你的第一個(gè)Component
在上面的項(xiàng)目的基本配置基礎(chǔ)上,我們開(kāi)始書寫React的第一個(gè)組件來(lái)熟悉React的寫法與組件思想。
首先我們?cè)陧?xiàng)目根目錄中新建一個(gè) index.html 文件。 在這個(gè)基礎(chǔ)工程中, 我們使用bootstrap的樣式,直接引入一個(gè)cdn即可. 然后添加一個(gè)html標(biāo)簽 <p id="app"></p>,我們的app就會(huì)注入到這個(gè)p中。 最后再引入 <script src="http://www.argcandargv.com/skin/default/image/lazy.gif" class="lazy" original="http://www.argcandargv.com/skin/default/image/nopic.gif">
以下是完整的代碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>document</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="external nofollow" > </head> <body> <p id="app"></p> <script src="http://www.argcandargv.com/skin/default/image/lazy.gif" class="lazy" original="http://www.argcandargv.com/skin/default/image/nopic.gif" </body> </html>
建立一個(gè)新的文件夾 src. 我們app的大部分代碼都將放在這個(gè)文件夾里面。在 src中建立 app.js,作為React App的根組件, 其他所有的組件都會(huì)注入到這個(gè)跟組件中。
首先我們需要導(dǎo)入react,現(xiàn)在都已經(jīng)用ES6的語(yǔ)法, import React from 'react'; , 然后我們要引入react-dom. 這里面有react中最重要的一個(gè)虛擬dom的概念.引入代碼:import ReactDOM
from 'react-dom';
現(xiàn)在需要引入的依賴都已經(jīng)完畢我們可以寫第一個(gè)組件了:
class App extends React.Component { render(){ // Every react component has a render method. return( // Every render method returns jsx. Jsx looks like HTML, but it's actually javascript and functions a lot like xml, with self closing tags requiring the `/` within the tag in order to work propperly <p> Hello World </p> ); } }
注意這里"Hello World"寫在 p中. 所有的jsx代碼都需要寫在一個(gè)父p中.
最后我們需要把我們寫好的組件render給Dom,這里就需要用到 ReactDOM.render 方法.
在 App.js 的下面添加: ReactDOM.render(<App />, document.getElementById('app'));
第一個(gè)參數(shù)就是我們App的根組件, 寫作<App />的形式. 第二個(gè)參數(shù)就是我們的APP將要主要的DOM元素. 在這個(gè)項(xiàng)目中,就是我們?cè)趇ndex中寫的id為app的 p標(biāo)簽。
Ok,我們的APP結(jié)構(gòu)已經(jīng)出來(lái)了,經(jīng)典的hello world已經(jīng)實(shí)現(xiàn)。馬上我們就在這個(gè)基礎(chǔ)上再實(shí)現(xiàn)經(jīng)典的todo app。大致的原型就有一個(gè)輸入框用來(lái)輸入代辦事項(xiàng)然后添加到事件列表中。事件列表中每一個(gè)代辦事項(xiàng)被點(diǎn)擊就會(huì)標(biāo)注一條刪除線表示完成,點(diǎn)擊后面的刪除按鈕則會(huì)將其從列表中刪除。通過(guò)完成這個(gè)APP的過(guò)程你將學(xué)會(huì)一個(gè)完整的react app的所有的基本構(gòu)建塊。
生命周期方法和兩種形式的組件構(gòu)建
我們從一些小的模塊開(kāi)始起步.一個(gè)組件component就是一個(gè)react app的構(gòu)件塊. 有兩種形式的組件: 類組件(Class)和函數(shù)型組件(Functional). 在這個(gè)項(xiàng)目中,這兩種形式的組件我們都會(huì)使用, 并且使用生命周期的鉤子,同時(shí)也會(huì)使用state和props兩個(gè)react中重要的屬性。
首先在 src文件夾中新建components 文件夾,你的文件結(jié)構(gòu)就是這樣 ~/src/components。
然后在components中新建文件 ToDoApp.js。 對(duì)于所有的react組件我們都需要在頭部引入reactimport React from 'react';。
下面我們寫一個(gè)類組件. 所有的class 組件有一個(gè)render方法用來(lái)返回jsx。
ToDoApp的class就如下所示:
class ToDoApp extends React.Component { render() { return ( <p>To Do App</p> ); } }
為了將這個(gè)組件注入到我們的APP中, 首先我們需要輸出它。 在這個(gè)組件代碼底部添加 export default ToDoApp;。
然后在app.js頂部我們添加 import ToDoApp from '.components/ToDoApp'; 導(dǎo)入組件用來(lái)代替 Hello World 。 render中替換為新的jsx代碼 <ToDoApp />半閉合類型的標(biāo)簽即可。
然后在瀏覽器中你就可以看到"To Do App" 代替了原來(lái)的 "Hello World"!這樣我們就完成了將第一個(gè)子組件嵌入到根組件之中了,這就是構(gòu)建react app的常規(guī)模式。下面繼續(xù)完善我們的組件。
返回到ToDoApp 中來(lái)構(gòu)建我們的第一個(gè)代辦事項(xiàng)列表。首先我們使用bootstrap來(lái)構(gòu)建比較方便且美觀。 用下面的jsx替換當(dāng)前render方法中 return 中的jsx:
<p className="row"> <p className="col-md-10 col-md-offset-1"> <p className="panel panel-default"> <p className="panel-body"> <h1>My To Do App</h1> <hr/> List goes here. </p> </p> </p> </p>
現(xiàn)在打開(kāi)瀏覽器, 你將會(huì)看到一個(gè)標(biāo)題 "My To Do App" 下面跟隨一個(gè)bootstrap的panel組件里面寫有 "List Goes Here",我們將在這個(gè)地方構(gòu)建列表。 那么我們?nèi)绾螌?shù)據(jù)存儲(chǔ)在我們的列表中呢? 答案就是使用 state. 每一個(gè)類組件都有 state 屬性,可以通過(guò) this.state在組件任何位置獲取并且用 this.setState({
key: "value" })這種方法來(lái)更新?tīng)顟B(tài)。但是除非必要我們比較少使用state,這里暫時(shí)先使用作為了解,后期會(huì)使用redux來(lái)管理狀態(tài)。
在ToDoApp中我們可以使用許多生命周期方法的鉤子, 其中一個(gè)就是componentWillMount。 這個(gè)方法的執(zhí)行是在頁(yè)面加載并且render方法之前。可以在其中獲取列表數(shù)據(jù),在我們的APP中直接用一個(gè)虛擬的數(shù)組提供。(值得注意的是componentWillMount會(huì)引起很多小問(wèn)題,因此真實(shí)項(xiàng)目中盡量不要使用,而是應(yīng)該用componentDidMount)。
在 ToDoApp中 render 方法之前添加:
componentWillMount(){ // run before the render method this.setState({ // add an array of strings to state. list: ['thing1', 'thing2', 'thing3'] }) };
現(xiàn)在我們獲取了一個(gè)虛擬列表,需要重點(diǎn)注意的就是react依賴于state和props,只有當(dāng)state和props改變的時(shí)候react組件才會(huì)刷新。
現(xiàn)在我們添加列表到這個(gè)view里,這里不是直接簡(jiǎn)單的在里面修改jsx,而是再創(chuàng)建一個(gè)新的組件來(lái)構(gòu)建列表,這次我們學(xué)習(xí)使用函數(shù)型組件,需要注意的是函數(shù)型組件沒(méi)有生命周期方法和state屬性,它僅僅是一個(gè)返回jsx的函數(shù),并且參數(shù)是props。
那么props到底是什么呢?props是從父組件傳遞進(jìn)子組件的數(shù)據(jù)的名字,這是一個(gè)很重要的概念,也是react app數(shù)據(jù)傳遞的最典型與最推薦的方法。通常我們將數(shù)據(jù)保持在app的頂端組件,通過(guò)組件讓數(shù)據(jù)流下來(lái)保證APP的精確運(yùn)行。這些數(shù)據(jù)和props的一些處理可能會(huì)影響APP的運(yùn)行,但是假如你按照這個(gè)課程的實(shí)踐流程來(lái)做,這些影響都會(huì)很小。
再新建一個(gè)components文件夾并在其中新建一個(gè)List.js作為我們要?jiǎng)?chuàng)建的函數(shù)型組件。用const來(lái)新建一個(gè)函數(shù),參數(shù)名字寫作props。
函數(shù)形式如下所示:
const List = (props) => { // we're using an arrow function and const variable type, a ES6 features return ( <p> I'm a list!!! </p> ) }; export default List;
在 ToDoApp.js引入 List用List 組件替換 List goes here.,寫法為 <List />.現(xiàn)在在瀏覽器中就可以看到"I'm a list!!!"
現(xiàn)在我們來(lái)把這個(gè)變成真實(shí)的列表,首先就需要通過(guò)props傳遞數(shù)據(jù),我們把這個(gè)從state中獲取的數(shù)據(jù)list通過(guò)命名為listItems的props傳遞,寫作: <List listItems={this.state.list} /> ,現(xiàn)在 List 已經(jīng)通過(guò)props獲取了 ToDoApp中的數(shù)據(jù)。
然后在 List 組件中我們需要render一個(gè)列表,先用下面的jsx代碼代替:
<p> <ul> { list // this is a variable we'll define next } </ul> </p>
注意這個(gè)大括號(hào),js可以在這里面執(zhí)行并將返回添加到view里。首先我們定義一個(gè)列表變量:
const list = props.listItems.map((el, i)=>( // All where doing here is getting the items listItems prop // (which is stored in the state of the parent component) // which is an array, and we're running the .map method // which returns a new array of list items. The key attribute is // required, and must be unique. <li key={i}><h2>el</h2></li> ));
完整的組件如下:
import React from 'react'; const List = (props) => { const list = props.listItems.map((el, i)=>( <li key={i}><h2>el</h2></li> )); return ( <p> <ul> { list } </ul> </p> ) }; export default List;
現(xiàn)在打開(kāi)瀏覽器就可以看到一列列表了。接下來(lái)就是給我們的項(xiàng)目加入功能了,包括添加新的事項(xiàng),標(biāo)注事項(xiàng)完成和刪除列表中事項(xiàng)。
給APP添加功能
1.函數(shù)型組件
首先我們需要添加一個(gè)input元素以便可以輸入代辦事項(xiàng)。因此我們?cè)赾omponents文件夾中新建一個(gè)Input.js,然后在其中創(chuàng)建并輸出一個(gè)名叫Input的函數(shù)型組件。
把下面的jsx代碼粘貼到你的函數(shù)型組件return之中:
<form> <p className="form-group"> <label htmlFor="listInput"> Email address </label> <input type="text" className="form-control" id="listItemInput" placeholder="Add new todo" /> <button className="btn btn-primary"> Add Item </button> </p> </form>
2. Input
現(xiàn)在我們的jsx沒(méi)有做任何特殊的事情,僅僅是一個(gè)基本的html視圖,不過(guò)我們先測(cè)試一下把其導(dǎo)入到ToDoApp.js,形式就是<Input/>。
這時(shí)候會(huì)發(fā)現(xiàn)一個(gè)輸入框和按鈕的視圖,這個(gè)組件的靜態(tài)視圖已經(jīng)寫好了,下面就需要添加功能了。
3. Props
首先我們需要做的是如何獲取輸入框的值,因?yàn)檫@個(gè)輸入框的值需要在其他組件中獲取,所以我們并不想要在Input組件中來(lái)處理這個(gè)數(shù)據(jù)存儲(chǔ)。事實(shí)上,在子組件中存儲(chǔ)數(shù)據(jù)在任何時(shí)候都是不推薦的,我們應(yīng)該將數(shù)據(jù)存儲(chǔ)在app的頂端組件并且通過(guò)props傳遞下來(lái)。
另一個(gè)需要記住的是即使我們目前把數(shù)據(jù)存儲(chǔ)在了上層的 ToDoApp 組件,后期還是會(huì)用redux來(lái)代替來(lái)處理整個(gè)app的數(shù)據(jù)。這里先僅僅使用react的state來(lái)實(shí)現(xiàn)。
ok,我們?cè)赥oDoApp的 componentWillMount的setState中新增一個(gè)newToDo屬性用來(lái)存儲(chǔ)輸入框的值。
componentWillMount(){ this.setState({ list: ['thing1', 'thing2', 'thing3'], newToDo: 'test' }) };
同樣的就可以通過(guò)在<Input />上通過(guò)props傳遞下去。
4. 解構(gòu)(Destructuring)
在Input.js中我們通過(guò)參數(shù)props可以獲得上級(jí)組件傳遞下來(lái)的值, 但是還可以用ES6的新特性解構(gòu)來(lái)作為參數(shù),這樣看起來(lái)更加酷!
把Input組件的props參數(shù)修改為({
value })這樣的參數(shù)形式,這樣可以把props這個(gè)對(duì)象參數(shù)解構(gòu)為一個(gè)個(gè)鍵值對(duì)。直接看個(gè)小例子來(lái)就很明白了:
var props = { name: 'hector', age: 21 } function log(props){ console.log(props.name); console.log(props.age); } log(props);
is the same as this:
let props = { name: 'hector', age: 21 } log = ({name, age}) => { console.log(name); console.log(age); } log(props);
5. setState
上面的newToDo僅僅是添加了一個(gè)state用來(lái)存儲(chǔ)輸入框的值,給定一個(gè)值,輸入框就會(huì)顯示,明顯還不是我們要的效果,我們需要做的是基于輸入框的值的改變來(lái)動(dòng)態(tài)改變這個(gè)state。
為了實(shí)現(xiàn)這個(gè)功能,我們需要再添加一個(gè)onChange方法同樣利用props傳進(jìn)Input組件: onChange={onChange}, 然后解構(gòu)參數(shù)就是({ onChange, value })。
然后在 ToDoApp.js的componentWillMount 添加一個(gè)新的方法 onInputChange。這個(gè)方法有一個(gè)參數(shù)event, 它將要捕獲用戶在輸入框輸入的值。
onInputChange = (event) => { this.setState({ newToDo: event.target.value}); // updates state to new value when user changes the input value };
6. 添加新列表事項(xiàng)
現(xiàn)在需要向列表中添加新的事項(xiàng),也就是在提交后能把輸入框的值存儲(chǔ)并顯示到列表中。我們需要再新建一個(gè)onInputSubmit的方法,參數(shù)同樣是event,函數(shù)體內(nèi)首先需要寫 event.preventDefault(),然后用 setState 方法把新事項(xiàng)添加到列表數(shù)組中,但是,一定要注意我們的state應(yīng)該是immutable的,這是react中必須遵循的一個(gè)準(zhǔn)則,這樣才能保證對(duì)比性與可靠性。
為了實(shí)現(xiàn)這個(gè)功能, 需要用到this.setState 回調(diào)函數(shù),參數(shù)為previousState:
this.setState((previousState)=>({ list: previousState.list.push(previousState.newToDo) }))
正如我上面的描述,最開(kāi)始寫state的時(shí)候很多人都會(huì)犯這樣的錯(cuò)誤,直接用push這樣的方法,修改了state,這樣就不算immutable的,我們一定要保證絕不直接修改原state。
這里又可以用到ES6中的新特性了,擴(kuò)展操作符,它通過(guò)遍歷舊數(shù)組返回一個(gè)新數(shù)組,使舊的數(shù)組保持原樣,這樣我們就把事項(xiàng)添加到列表數(shù)組末尾:
this.setState((previousState)=>({ list: [...previousState.list, previousState.newToDo ], // the spread opperator is called by using the ... preceding the array }));
在提交添加新事項(xiàng)的同時(shí),需要將newToDo重置為'':
this.setState((previousState)=>({ list: [...previousState.list, previousState.newToDo ], newToDo: '' }));
7. 劃掉事項(xiàng)
是時(shí)候添加劃掉事項(xiàng)的功能了。為了實(shí)現(xiàn)這個(gè)功能需要添加一個(gè)新的屬性用來(lái)標(biāo)注是否需要?jiǎng)澋簦虼诵枰淖冊(cè)瓉?lái)的數(shù)組為一個(gè)對(duì)象數(shù)組,每一個(gè)事項(xiàng)都是一個(gè)對(duì)象,一個(gè)key為item表示原來(lái)的事項(xiàng)內(nèi)容,一個(gè)key為done用布爾值來(lái)表示是否劃掉。 然后先把原來(lái)的onInputSubmit方法修改,同樣要注意immutable,使用擴(kuò)展操作符如下:
onInputSubmit = (event) => { event.preventDefault(); this.setState((previousState)=>({ list: [...previousState.list, {item: previousState.newToDo, done: false }], // notice the change here newToDo: '' })); };
屬性done添加完成后就需要新增一個(gè)方法當(dāng)點(diǎn)擊事項(xiàng)時(shí)候來(lái)改變這個(gè)值:
onListItemClick = (i) => { // takes the index of the element to be updated this.setState((previousState)=>({ list: [ ...previousState.list.slice(0, i), // slice returns a new array without modifying the existing array. Takes everything up to, but not including, the index passed in. Object.assign({}, previousState.list[i], {done: !previousState.list[i].done}), // Object.assign is a new ES6 feature that creates a new object based on the first param (in this case an empty object). Other objects can be passed in and will be added to the first object without being modified. ...previousState.list.slice(i+1) // takes everything after the index passed in and adds it to the array. ] })) };
然后把這個(gè)方法通過(guò)props傳遞給List 組件,這里就沒(méi)有使用解構(gòu)參數(shù)傳遞,用來(lái)和Input的做對(duì)比。因?yàn)檫@個(gè)函數(shù)需要一個(gè)參數(shù)就是當(dāng)前列表的序列號(hào),但是肯定不能直接call這個(gè)函數(shù)否則會(huì)報(bào)錯(cuò),因此使用bind方法,出入i參數(shù):
onClick={props.onClick.bind(null, i)}
當(dāng)然還有另一種方法:
onClick={() => props.onClick(i)}
然后在事項(xiàng)內(nèi)容的span標(biāo)簽上添加 onClick 方法,改變當(dāng)前事項(xiàng)的done值后,在通過(guò)判斷此布爾值來(lái)進(jìn)行樣式的修改添加或者劃掉刪除線。
<span style={ el.done ? {textDecoration: 'line-through', fontSize: '20px'} : {textDecoration: 'none', fontSize: '20px'} } onClick={props.onClick.bind(null, i)} >
8. 刪除事項(xiàng)
最后我們?cè)谔砑觿h除事項(xiàng)的功能,這個(gè)和劃掉事項(xiàng)非常相似,我們只需要新增一個(gè)刪除按鈕,然后再新增一個(gè)方法修改列表,具體代碼如下:
<button className="btn btn-danger pull-right" > x </button>
deleteListItem = (i) => { this.setState((previousState)=>({ // using previous state again list: [ ...previousState.list.slice(0, i), // again with the slice method ...previousState.list.slice(i+1) // the only diffence here is we're leaving out the clicked element ] })) };
把deleteListItem 方法傳遞到列表組件中然后在刪除按鈕上綁定即可,仿照上一個(gè)自己寫一下就好。
現(xiàn)在我們有一個(gè)完整功能的APP了,是不是感覺(jué)很cool,這個(gè)就是不用redux時(shí)候的形態(tài)了,但是你會(huì)發(fā)現(xiàn)當(dāng)狀態(tài)越來(lái)越復(fù)雜時(shí)候很繁瑣,因此我們下面就要介紹redux來(lái)管理狀態(tài)了。
遷移到redux的準(zhǔn)備工作
截至目前我們已經(jīng)學(xué)會(huì)如何用webpack和babel搭建react應(yīng)用,構(gòu)建類組件和函數(shù)型組件并處理state,添加功能。然而這只是基本滿足一個(gè)小型應(yīng)用的需求,隨著app的增長(zhǎng),處理數(shù)據(jù)和行為會(huì)越來(lái)越吃力,這就是要引入redux的必要性。
那么redux如何處理數(shù)據(jù)?首先,redux給你的app一個(gè)單一的state對(duì)象,與flux等根據(jù)view來(lái)劃分為多個(gè)state對(duì)象正好相反。你可能會(huì)有疑問(wèn),一個(gè)單一的對(duì)象來(lái)處理一個(gè)復(fù)雜的app豈不是非常復(fù)雜?redux采用的方法是把數(shù)據(jù)處理分為reducer functions、action creators和actions然后組合在一起工作流線型的處理數(shù)據(jù)。
1. 首先安裝必須的依賴
首先安裝 redux and react-redux
npm install --save redux npm install --save react-redux
然后安裝 redux middleware,這里就先安裝 redux-logger,它的功能是幫助我們開(kāi)發(fā)。
npm install --save redux-logger
還有一些常用的中間件,比如 redux-thunk and redux-promise, 但是在我們的這個(gè)項(xiàng)目中暫時(shí)先不需要,可以自行去github了解。
2. 構(gòu)建
使用redux構(gòu)建react應(yīng)用一般都有一個(gè)標(biāo)準(zhǔn)的模板,可能不同模板形式上有區(qū)別,但是思想都是一樣的,下面就先按照一種文件結(jié)構(gòu)來(lái)構(gòu)建。
首先我們?cè)趕rc中新建一個(gè)文件夾redux,然后在其中新建一個(gè)文件configureStore.js,添加以下代碼:
import { createStore, applyMiddleware, combineReducers } from 'redux'; import createLogger from 'redux-logger';
createStore 是由redux提供的用來(lái)初始化store的函數(shù), applyMiddleware是用來(lái)添加我們需要的中間件的。
combineReducers 用來(lái)把多個(gè)reducers合并為一個(gè)單一實(shí)體。
createLogger 就是我們這里唯一使用的一個(gè)中間件,可以console出每一個(gè)action后數(shù)據(jù)的詳細(xì)處理過(guò)程,給調(diào)試帶來(lái)了很大方便。
然后添加下面代碼:
const loggerMiddleware = createLogger(); // initialize logger const createStoreWithMiddleware = applyMiddleware( loggerMiddleware)(createStore); // apply logger to redux
這里暫時(shí)沒(méi)有完成,需要后面的模塊寫完了再導(dǎo)入到這里繼續(xù)來(lái)完成。
3. 模塊Modules
在 src/redux/ 新建一個(gè)文件夾 modules。在這個(gè)文件夾中我們將存放所有的reducers,action creators和constants。這里我們使用的redux組織結(jié)構(gòu)叫做ducks,思想就是把相關(guān)的reducers,action creators和constants都放在一個(gè)單獨(dú)的文件中,而不是分開(kāi)放在多個(gè)文件中,這樣修改一個(gè)功能時(shí)候直接在一個(gè)文件中修改就可以。
在 modules 文件中新建 'toDoApp.js',注意這里的命名是依據(jù)容器組件的名字來(lái)命名,這個(gè)也是規(guī)范,容易管理代碼。
現(xiàn)在我們可以開(kāi)始創(chuàng)建initial state和 reducer function,這其實(shí)非常簡(jiǎn)單,state就是一個(gè)js對(duì)象,reducer就是js的switch語(yǔ)句:
const initialState = {}; //The initial state of this reducer (will be combined with the states of other reducers as your app grows) export default function reducer(state = initialState, action){ // a function that has two parameters, state (which is initialized as our initialState obj), and action, which we'll cover soon. switch (action.type){ default: return state; } }
4. 完善Store
現(xiàn)在我們已經(jīng)完成了第一個(gè)reducer,可以將其添加到 configureStore.js 中去了, 導(dǎo)入: import toDoApp from './modules/toDoApp';
然后用combineReducers 來(lái)組合當(dāng)前的reducer,因?yàn)槲磥?lái)會(huì)有更多的模塊加入。
const reducer = combineReducers({ toDoApp });
最后在底部加入下面完整的代碼:
const configureStore = (initialState) => createStoreWithMiddleware(reducer, initialState); export default configureStore; Cool. We're done here.
5. Connect
現(xiàn)在我們已經(jīng)有了一個(gè)reducer,那么怎么和app建立聯(lián)系呢?這需要兩步工作。
前面已經(jīng)講過(guò)類組件和函數(shù)型組件,有時(shí)候也可以稱為smart components和dumb components,這里我們新增一種容器組件,顧名思義,這種組件就是作為一個(gè)容器用來(lái)給組件提供actions和state。
下面來(lái)創(chuàng)建第一個(gè)容器組件,首先在 /src/ 下新增一個(gè)文件夾containers,然后再其下面新建一個(gè)文toDoAppContainer.js。
在文件頂部首先導(dǎo)入 connect 用來(lái)將容器和組件聯(lián)系在一起,
import { connect } from 'react-redux'; import ToDoApp from '../components/ToDoApp.js'
connect 這個(gè)函數(shù)被調(diào)用兩次, 第一次是兩個(gè)回調(diào)函數(shù): mapStateToProps and mapDispatchToProps。 第二次是把state和dispatch傳入組件的時(shí)候。這里的dispatch又是什么呢?
當(dāng)我們需要在redux中發(fā)生某些行為時(shí)候,就需要調(diào)用dispatch函數(shù)傳遞一個(gè)action然后調(diào)用reducer這一套流程。因?yàn)槲覀冞沒(méi)有編寫具體的行為,這里就暫時(shí)空白,后面再補(bǔ),代碼形式如下:
function mapStateToProps(state) { return { toDoApp: state.toDoApp // gives our component access to state through props.toDoApp } } function mapDispatchToProps(dispatch) { return {}; // here we'll soon be mapping actions to props }
然后在底部添加:
export default connect( mapStateToProps, mapDispatchToProps )(ToDoApp);
1、Provider
redux的基本工作已經(jīng)完成,最后一步就是返回到app.js 文件, 首先我們不再需要導(dǎo)入 ToDoApp 組件,而是用容器組件ToDoAppContainer來(lái)替代,然后需要導(dǎo)入 configureStore 函數(shù)和 Provider,在頭部添加代碼:
import { Provider } from 'react-redux'; import ToDoAppContainer from './containers/ToDoAppContainer'; import configureStore from './redux/configureStore';
configureStore is the function we created that takes our combined reducers and our redux middleware and mashes them all together. Let's intialize that with the following line:
const store = configureStore();
然后return的jsx中同樣需要把ToDoApp 改為 ToDoAppContainer,然后需要用Provider 組件將其包裹,它的作用就是將整個(gè)app的state傳遞給它所包裹的容器,從而使容器組件可以獲取這些state。
<Provider store={store}> // we pass the store through to Provider with props <ToDoAppContainer /> </Provider>
現(xiàn)在整個(gè)redux的基本結(jié)構(gòu)已經(jīng)搭建起來(lái),下一步就可以把整個(gè)行為邏輯代碼補(bǔ)充進(jìn)去就可以了。
Redux Actions 和 Reducers
搭建起redux的基本結(jié)構(gòu)后,就可以填充redux的元素了,簡(jiǎn)單來(lái)說(shuō)我們只需要記住四個(gè)概念, Types, Actions, Action Creators, and Reducers。然后把這些元素用ducks的文件組織結(jié)構(gòu)組織起來(lái)就可以了。
Ducks
規(guī)則
在module中我們需要遵循下面的代碼風(fēng)格和命名方式:
須用 export default 輸出名為 reducer()的函數(shù)
須用 export 輸出 函數(shù)形式的action creators
須用 npm-module-or-app/reducer/ACTION_TYPE的命名形式來(lái)命名action types,因?yàn)榈胶笃诤芏鄏educer,不同的人協(xié)同工作難免會(huì)出現(xiàn)命名重復(fù),這樣子加上app和模塊的前綴的話就不會(huì)出現(xiàn)命名沖突的問(wèn)題。
須用大寫的蛇形方式UPPER_SNAKE_CASE來(lái)命名action types。
Types
這個(gè)types就是上面第三條中需要按照ducks的規(guī)范命名的常量名字,將其寫在文件的頂部,當(dāng)action 觸發(fā)時(shí)候會(huì)傳遞給reducer,reducer的switch語(yǔ)句會(huì)根據(jù)這個(gè)type來(lái)進(jìn)行相應(yīng)的數(shù)據(jù)處理。
const ADD_ITEM = 'my-app/toDoApp/ADD_ITEM'; const DELETe_ITEM = 'my-app/toDoApp/DELETE_ITEM';
Actions
Actions 就是一個(gè)至少包含type的簡(jiǎn)單的js對(duì)象,同時(shí)可以包含數(shù)據(jù)以便傳遞給reducer。當(dāng)用戶在頁(yè)面上觸發(fā)了某種行為,一個(gè)aciton creator將會(huì)發(fā)送aciton給reducer做數(shù)據(jù)處理。
action示例如下:
{ type: ADD_ITEM, item: 'Adding this item' } { type: DELETE_ITEM, index: 1 } { type: POP_ITEM }
Action Creators
Action creators 是創(chuàng)建acitons并傳遞給reducer的函數(shù),它通常返回一個(gè)action對(duì)象,有時(shí)候借用thunk這樣的中間件也可以返回dispatch多個(gè)actions,在我們的app中為了簡(jiǎn)化暫時(shí)不涉及這個(gè)模式。
function addItem(item){ return { type: ADD_ITEM, item // this is new ES6 shorthand for when the key is the same as a variable or perameter within the scope of the object. It's the same as item: item } }
Reducers
reducer是唯一可以觸碰store的元素,初始值為initialState,形式上就是一個(gè)簡(jiǎn)單的switch語(yǔ)句,但是注意不能直接改變state,因?yàn)閟tate是immutable。也就是說(shuō)我們不能直接使用.pop or .push這些方法操作數(shù)組。
下面是示例代碼:
const initialState = { list: [] }; export default function reducer(state = initialState, action){ switch (action.type){ case ADD_ITEM: return Object.assign( {}, state, { list: [...state.list, action.item]} // here we see object.assign again, and we're returning a new state built from the old state without directly manipulating it ) default: return state; } }
概念已經(jīng)介紹完畢,下面開(kāi)始將原來(lái)的功能邏輯用redux重寫。
1. Initial state
首先我們?cè)?src/redux/modules/toDoApp中聲明initialState。
const initialState = { list: [{item: 'test', done: false}] // just added this to test that state is being passed down propperly, newToDo: '' }; export default function reducer(state = initialState, action){ switch (action.type){ default: return state; } }
現(xiàn)在在 ToDoApp.js的 render() 方法中return之前添加console.log(this.props) 會(huì)打印出下面的對(duì)象:
toDoApp: Object list: Array[1] 0: "test" length: 1 __proto__: Array[0] __proto__: Object __proto__: Object
測(cè)試通過(guò),我們就可以傳遞這些數(shù)據(jù)給子組件了,這里就可以把原來(lái)List組件的 listItems prop和Input的value prop替換掉了。
<List onClick={this.onListItemClick} listItems={this.props.toDoApp.list} deleteListItem={this.deleteListItem} /> <Input value={this.props.toDoApp.newToDo} onChange={this.onInputChange} onSubmit={this.onInputSubmit} />
這里只是替換掉了數(shù)據(jù),下面還需要把a(bǔ)ction也替換。
3. Input action
這個(gè)過(guò)程就是把我們?cè)瓉?lái)在ToDoApp 組件的行為邏輯全部遷移到redux文件夾下的 toDoApp module中去。
const INPUT_CHANGED = 'INPUT_CHANGED'; export function inputChange(newToDo){ return { type: INPUT_CHANGED, newToDo } }
然后在reducer的switch中新增如下處理:
case INPUT_CHANGED: return Object.assign( {}, state, {newToDo: action.value} );
在 toDoAppContainer.js 的 mapDispatchToProps 函數(shù)就需要返回相應(yīng)的action,首先導(dǎo)入 inputChange, 具體代碼如下:
import { connect } from 'react-redux'; import ToDoApp from '../components/ToDoApp.js' import { inputChange } from '../redux/modules/toDoApp'; // we added this function mapStateToProps(state) { return { toDoApp: state.toDoApp // gives our component access to state through props.toDoApp } } function mapDispatchToProps(dispatch) { return { inputChange: (value) => dispatch(inputChange(value)) // we added this }; } export default connect( mapStateToProps, mapDispatchToProps )(ToDoApp);
這樣state和action都傳遞給了toDoApp然后再通過(guò)props傳遞給子組件就可以使用了,具體都可以看項(xiàng)目最終代碼。
4. 其他 actions
其他acitons的代碼模式跟上面的基本一樣,這里不在贅述。
總結(jié)
到這里一個(gè)使用webpack打包的react+redux(ducks)的基本應(yīng)用模型就出來(lái)了,雖然簡(jiǎn)單但是是我們進(jìn)行更復(fù)雜項(xiàng)目的基礎(chǔ),并且有了這些基礎(chǔ)后面的路程將會(huì)順暢多了,一起加入react的大家庭吧。
相關(guān)推薦:
react項(xiàng)目案例總結(jié)
以上就是簡(jiǎn)單搭建一個(gè)react項(xiàng)目的詳細(xì)內(nèi)容,更多請(qǐng)關(guān)注php中文網(wǎng)其它相關(guān)文章!