歡迎移步個人博客閱讀:《React 入門實踐》css
在寫這篇文章以前,我已經接觸 React 有大半年了。在初步學習 React 以後就正式應用到項目中,當時就想把本身的一些想法寫出來分享一下,無奈不太會寫文章,再則時間不是很充裕,因此也就擱下了。html
本篇文章比較基礎,沒有深刻的分析,大神們輕看。廢話就很少說了,那麼讓咱們來進入正題。node
首先想要介紹的是 React,看到這篇文章的朋友想必都有一些關於 React 的瞭解了,但對於剛接觸的新人而言,在這就要簡要地介紹一下了。而後就是關於使用 React 構建一個簡單單頁應用(下文用 SPA 代替,Single Page Application)的一些介紹和講解。react
React 起源於 Facebook 的內部項目,由於該公司對市場上全部 JavaScript MVC 框架,都不滿意,就決定本身寫一套,用來架設Instagram 的網站。作出來之後,發現這套東西很好用,就在2013年5月開源了。(更多相關介紹請看這)webpack
特色:git
僅僅只是 UIgithub
虛擬 DOM:最大限度減小與 DOM 的交互(相似於使用 jQuery 操做 DOM)web
單向數據流:很大程度減小了重複代碼的使用express
組件化:npm
可組合(Composeable):一個組件易於和其它組件一塊兒使用,或者嵌套在另外一個組件內部。若是一個組件內部建立了另外一個組件,那麼說父組件擁有(own)它建立的子組件,經過這個特性,一個複雜的UI能夠拆分紅多個簡單的UI組件
可重用(Reusable):每一個組件都是具備獨立功能的,它能夠被使用在多個UI場景
可維護(Maintainable):每一個小的組件僅僅包含自身的邏輯,更容易被理解和維護
生命週期:
Mounting:已插入真實 DOM
Updating:正在被從新渲染
Unmounting:已移出真實 DOM
React 爲每一個狀態都提供了兩種處理函數,will 函數在進入狀態以前調用,did 函數在進入狀態以後調用,三種狀態共計五種處理函數。
componentWillMount()
componentDidMount()
componentWillUpdate(object nextProps, object nextState)
componentDidUpdate(object prevProps, object prevState)
componentWillUnmount()
此外,React 還提供兩種特殊狀態的處理函數。
componentWillReceiveProps(object nextProps):已加載組件收到新的參數時調用
shouldComponentUpdate(object nextProps, object nextState):組件判斷是否從新渲染時調用
那麼進入正題,花了點時間去寫一個簡單的 SPA,也算是一個比較完整 React 骨架,但不包括測試(測試的教程能夠看這個),相關源碼能夠查看 react-start-kit。
接下來看看咱們這個項目的構建須要用到些什麼:
react
redux
webpack
react-router
ant design
babel
...
還有一些沒有列舉出來,具體能夠看倉庫源碼的 package.json
。其中的詳細介紹會在文尾列出一些我所看過的文章或是官方介紹。
說到 React 項目的構建就不得不提 Webpack 這個神器。構建工具備不少,例如 Grunt,Gulp,Brunch 等,相比這些構建工具,Webpack 感受就是和 React 不謀而合,尤爲是 react-hot-loader 這樣的神器(熱加載),讓 Webpack 成爲最主流的 React 構建工具。
關於 Webpack 的特性以及介紹這裏就不贅述了,咱們能夠從下圖看出 Webpack 的做用:
接着咱們從項目代碼中來看 Webpack。
entry: { app: [__dirname + '/src/index'], }, output: { path: __dirname + '/_dist', filename: '[name]_[hash].js', }
這部分主要是指定入口和出口文件。entry
做爲項目的入口文件;output
做爲文件編譯後的出口,其中 path
表明輸出的路徑,filename
表明文件名稱,而 [name]_[hash]
保證了瀏覽器不會存在緩存(即修改文件後效果不生效)。
module: { loaders: [{ test: /\.js$/, loaders: ['babel'], exclude: /node_modules/, }, { test: /\.css$/, loaders: ['style', 'css'], include: /components/, }, { test: /\.(jpe?g|png|gif|svg|ico)/i, loader: 'file', }, { test: /\.(ttf|eot|svg|woff|woff2)/, loader: 'file', }, { test: /\.(pdf)/, loader: 'file', }, { test: /\.(swf|xap)/, loader: 'file', }], }
而這部分會幫助咱們去處理不一樣類型的文件,其中 test
就是文件的後綴,loaders
是「轉譯器」,include
是指定文件的目錄,exclude
是排除某個目錄。咱們能夠看出,全部的 .js
文件都會經過 babel 去轉譯,也就是咱們在項目中使用 ES6+ 語法會經過 babel 轉譯成瀏覽器能夠識別的 ES5 代碼。
最後配置好的 config 是這樣的:
var HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: { app: [__dirname + '/src/index'], }, output: { path: __dirname + '/_dist', filename: '[name]_[hash].js', }, resolve: { root: [ __dirname + '/src', __dirname + '/node_modules', __dirname, ], extensions: ['', '.js'], }, module: { loaders: [{ test: /\.js$/, loaders: ['babel'], exclude: /node_modules/, }, { test: /\.css$/, loaders: ['style', 'css'], include: /components/, }, { test: /\.(jpe?g|png|gif|svg|ico)/i, loader: 'file', }, { test: /\.(ttf|eot|svg|woff|woff2)/, loader: 'file', }, { test: /\.(pdf)/, loader: 'file', }, { test: /\.(swf|xap)/, loader: 'file', }], }, plugins: [ new HtmlWebpackPlugin({ template: __dirname + '/src/index.html', favicon: __dirname + '/src/favicon.ico', inject: false, }), ], };
Node.js web 應用開發框架 Express 做爲項目的 web 服務器,有 Node.js 開發經驗的同窗應該挺熟悉的了,這裏也很少作贅述。
最終的啓動代碼是這樣的:
var express = require('express'); var webpack = require('webpack'); var webpackConfig = require('./webpack.development'); var app = express(); var compiler = webpack(webpackConfig); app.use(require('webpack-dev-middleware')(compiler, { stats: { colors: true, }, })); app.use(require('webpack-hot-middleware')(compiler)); //熱加載 app.listen(process.env.PORT, function(err) { //在沒有端口的狀況下,會自動給出一個隨機端口 if (err) { console.log(err); } });
爲了方便咱們的訪問,項目使用了 minihost
進行啓動,方便快捷。值得一提的是,使用 h -- npm start
命令啓動時,訪問的是項目文件夾的名稱做爲連接,例如項目叫 myproject
,那麼此時能夠訪問 myproject.t.t
。
對於複雜的 SPA,狀態(state)管理很是重要。state 可能包括:服務端的響應數據、本地對響應數據的緩存、本地建立的數據(好比,表單數據)以及一些 UI 的狀態信息(好比,路由、選中的 tab、是否顯示下拉列表、頁碼控制等等)。若是 state 變化不可預測,就會難於調試(state 不易重現,很難復現一些 bug)和不易於擴展(好比,優化更新渲染、服務端渲染、路由切換時獲取數據等等)。
state 爲單一對象,使得 Redux 只須要維護一棵狀態樹,服務端很容易初始化狀態,易於服務器渲染。state 只能經過 dispatch(action) 來觸發更新,更新邏輯由 reducer 來執行。
在使用 Redux 後,state 就變得很容易維護,並且數據流很是清晰,容易解決遇到的 BUG。
咱們能夠看下圖來簡要地理解 Redux:
咱們能夠在項目中看到的結構是:
├─store ├─actions ├─reducers ├─constants ├─helpers ├─components ├─app.js ├─favicon.ico ├─index.html ├─index.js └─routes.js
最終咱們的 store 是:
import {createStore, applyMiddleware, combineReducers, compose} from 'redux'; import thunk from 'redux-thunk'; import {reduxReactRouter} from 'redux-router'; import createHistory from 'history/lib/createHashHistory'; import routes from '../routes'; import * as reducers from '../reducers'; let middlewares = [thunk]; if (process.env.NODE_ENV === 'development') { //在開發環境下能夠看到 state 的 log const logger = require('redux-logger'); middlewares = [...middlewares, logger]; } const finalCreateStore = compose( //組合多個函數 applyMiddleware(...middlewares), reduxReactRouter({routes, createHistory}), )(createStore); //建立 store 來管理全部的 state export default function configureStore(initialState) { const reducer = combineReducers(reducers); //把一個由多個不一樣 reducer 函數做爲 value 的 object,合併成一個最終的 reducer 函數 const store = finalCreateStore(reducer, initialState); if (process.env.NODE_ENV === 'development' && module.hot) { //開發環境下的熱加載 module.hot.accept('../reducers', () => { const nextReducers = require('../reducers'); const nextReducer = combineReducers(nextReducers); store.replaceReducer(nextReducer); }); } return store; }
獲取 state 須要在組件中調用 connect
函數,能夠自行定義須要獲取的 state。(這用於區分展現型和容器型組件)
... @connect( state => ({ data: state.data }) ) export default class ComponentOne extends Component { ... }
注意:connect
必須緊跟 component 的定義,否則會報錯。
爲項目添加路由系統,使用了 react-router 來管理路由。在開發項目的時候,比較推薦的作法是使用路由去跳轉頁面,而且建立 store 的同時咱們就把 router 加入其中,而後咱們根據路由的變化去更新視圖。
咱們能夠看看路由的源碼:
import React from 'react'; import Route from 'react-router/lib/Route'; //import {Route} from 'react-router'; import Base from 'components/base/Base'; import Home from 'components/home/Home'; export default ( <Route component={Base}> <Route path="/" component={Home} /> <Route path="/home" component={Home} /> </Route> );
path
是跳轉路徑,component
是與路徑相匹配的組件。
由螞蟻金服技術部出品的一個 UI 設計語言,也是項目中所用到的 UI 組件庫。
特性:
Designed as Ant Design,提煉和服務企業級中後臺產品的交互語言和視覺風格
React Component 上精心封裝的高質量 UI 庫
基於 npm + webpack + babel 的工做流,支持 ES2015
選擇理由:
有很好的技術支持
簡潔的樣式
基本涵蓋經常使用組件
...
組件做爲 React 渲染的一個基本組成,咱們一般把它們分爲兩類,容器型和展現型。相較於容器型,展現型是經過容器型傳遞 props 來獲取數據,而容器型能夠直接從 store 中獲取,處理並傳遞給下級組件。
在實際應用中會發現,定義一個容器型組件負責處理數據,而後分發給下級展現型組件,當須要更新數據時,那麼容器型組件發生變化會引發下級展現型組件的變化,這樣就對咱們業務上形成了必定的困擾(在不須要更新的部分組件上也發生了更新)。所以,咱們選擇在須要獲取數據的組件中使用 connect
,這樣則會方便不少(感受有些違反規則)。
在項目中咱們會這麼定義組件:
import React, {Component} from 'react'; import {connect} from 'react-redux'; import Presentational from 'components/common/Presentational'; @connect( state => ({ data: state.data }) ) export default class Container extends Component { render() { const {data} = this.props; return ( <Presentational data={data} /> ) } }
上面是能夠從 store 獲取數據的組件,並嵌套另外一個組件,將數據傳遞給它。
import React, {Component, PropTypes} from 'react'; export default class Presentational extends Component { static propTypes = { data: PropTypes.string, } render() { const {data} = this.props; return ( <div> {data} </div> ) } }
獲取上一個組件傳遞過來的數據,並展現出來。
這是一篇科普文(哈哈~囧),並無深刻去分析各項技術的具體內容,但願能幫助剛入手 React 的新手們。實踐項目的源碼能夠在 react-start-kit 看到,你能夠下載這個項目進行本身的一些探索和開發。還在努力探索中,文中有措辭不當或是疏漏,歡迎提出意見和建議。
react 官網
Babel 官網
redux 介紹
redux 中文文檔
Ant design 官網
React 入門實例教程
react-router 中文文檔
Webpack 傻瓜式指南(一)
CSS Modules 詳解及 React 中實踐
一看就懂的 ReactJs 入門教程(精華版)
深刻淺出React(二):React開發神器Webpack