在重構腳手架中掌握React/Redux/Webpack2基本套路

本文從屬於筆者的Web Frontend Introduction And Best Practices:前端入門與最佳實踐,項目的Github地址爲Webpack2-React-Redux-Boilerplate.css

Warning!筆者本身構建的基於Webpack+React+Redux的腳手架已經經歷了三個版本,以前的兩個版本參考Webpack實戰之Quick Start以及個人Webpack套裝。在本文文首此處,我必須嚴肅吐槽下,我深入感受到Boilerplate就像當年的Rails,方便入門的同時會給你無盡的束縛,所以筆者不建議任何人在正式項目中直接使用本身不能徹底掌控的腳手架。我以爲我是沒法忘記當初被react-redux-universal-hot-example支配的恐懼。html

Webpack2 React Redux Boilerplate

核心組件代碼與腳手架之間務必存在有機分割,整個程序架構清晰易懂。前端

若是你是徹底的React初學者,那麼建議首先了解下使用Facebook的create-react-app快速構建React開發環境,同時參考筆者的React 入門與最佳實踐以及Redux 入門與最佳實踐。本項目算是個半自動化的腳手架工具,筆者並不但願作成徹底傻瓜式的開箱即用的工具,這隻會給你的項目埋下危險的伏筆,但願每一個可能用這個Boilerplate的同窗都能閱讀文本,至少要保證對文本說起的知識點有個全局的瞭解。node

Features

本部分假設你已經對Webpack有了大概的瞭解,這裏咱們會針對筆者本身在生產環境下使用的Webpack編譯腳本進行的一個總結,在介紹具體的配置方案以前筆者想先概述下該配置文件的設計的目標,或者說是筆者認爲一個前端編譯環境應該達成的特性,這樣之後即便Webpack被淘汰了也能夠利用其餘的譬如JSPM之類的來完成相似的工做。react

  • 考慮到同一項目對多編譯目標的支持,包括開發環境、純前端運行環境(包括Cordova、APICloud、Weapp這種面向移動端的方案)、同構直出環境,而且保證項目能夠在這三個環境之間平滑切換,合理分割腳手架工具與核心應用代碼。webpack

  • 單一的配置文件:不少項目裏面是把開發環境與生產環境寫了兩個配置文件,可能筆者比較懶吧,不喜歡這麼作,所以筆者的第一個特性就是單一的配置文件,而後經過npm封裝不一樣的編譯命令傳入環境變量,而後在配置文件中根據不一樣的環境變量進行動態響應。另外,要保證一個Boilerplate可以在最小修改的狀況下應用到其餘項目。git

  • 多應用入口支持:不管是單頁應用仍是多頁應用,在Webpack中每每會把一個html文件做爲一個入口。筆者在進行項目開發時,每每會須要面對多個入口,即多個HTML文件,而後這個HTML文件加載不一樣的JS或者CSS文件。譬如登陸頁面與主界面,每每能夠視做兩個不一樣的入口。Webpack原生提倡的配置方案是面向過程的,而筆者在這裏是面向應用方式的封裝配置。github

  • 調試時熱加載:這個特性毋庸多言,不過熱加載由於走得是中間服務器,同時只能支持監聽一個項目,所以須要在多應用配置的狀況下加上一個參數,即指定當前調試的應用。web

  • 自動化的Polyfill:這個是Webpack自帶的一個特性吧,不過筆者就加以整合,主要是實現了對於ES六、React、CSS(Flexbox)等等的自動Polyfill。express

  • 資源文件的自動管理:這部分主要指從模板自動生成目標HTML文件、自動處理圖片/字體等資源文件以及自動提取出CSS文件等。

  • 文件分割與異步加載:能夠將多個應用中的公共文件,譬如都引用了React類庫的話,能夠將這部分文件提取出來,這樣前端能夠減小必定的數據傳輸。另外的話還須要支持組件的異步加載,譬如用了React Router,那須要支持組件在須要時再加載。

真的須要Redux嗎?

雖然本項目是面向Webpack+React+Redux的Boilerplate,可是筆者仍是但願在此拋出這個問題,也是便於你們可以理解Redux。對於這個問題筆者沒有明確的答案,可是在這兩年的本身對於Redux的實戰中,也一直在搖把。我堅決的認爲Redux指明瞭解決某類問題的正確方向,可是它真的適合於全部的項目嗎?筆者在個人前端之路一文中說起,從以DOM操做爲核心的jQuery時代到以聲明式組件爲核心的React時代的變遷是聲明式編程對於命令式的慢慢代替,而Redux則是純粹的聲明式編程典範。這裏以某個登陸認證的小例子進行說明,產品的需求是容許用戶在登陸成功以後在登陸頁面上顯示「登陸成功,正在跳轉」,而後延時跳轉到其餘頁面。這裏強調要在登陸頁面上進行回顯是由於不少人習慣將,跳轉做爲Side Effect在Thunk或者Saga中就處理了,並無影響到界面自己。具體的代碼對比能夠參考純粹的React實現的登陸跳轉基於Redux實現的登陸跳轉。首先,若是是純粹的React命令式的話,會是:

class ReactComponent{
  ...
  if(!isValid){ //isValid是外部傳入的狀態變量,存放用戶是否已經登陸
  //若是還沒有登陸,則進行登陸操做
  login().then(()=>{
    //登陸成功以後,顯示文字而且執行跳轉
    show('登陸成功,正在跳轉');
    redirect();
  });
}
}

若是咱們引入Redux,而且將Component中的全部反作用移除的話:

class ReduxComponent{
  ...
  if(!isValid){ 
      login(); //執行登陸操做,其會dispatch某個Action,觸發外部狀態變化
  }
  
  if(shouldRedirect){ //須要添加該變量來記錄是否須要進行跳轉
    show('登陸成功,正在跳轉');
    dispatch({type:'SET_SHOULDREDIRECT_FALSE'});//將控制是否跳轉的狀態變量重置
    redirect();
  }
}
}

從上面的例子中咱們能看出,就好像能量守恆定理同樣,對於任何的業務邏輯的實現要麼以命令的方式,要麼以聲明的方式輔以大量的狀態變量(參考基於變量的循環與基於迭代的循環兩者的代碼複雜度比較)。Redux以函數式編程的強約束將咱們不少的邏輯拆分爲了多個純函數表示,並以數據流驅動整個項目。Redux容許咱們以支離破碎的邏輯代碼與相較於命令式編程膨脹不少的模板代碼爲代價實現百分百的可測試性與可預測性。通過這麼長時間的摸索與社區普遍的討論實踐,Redux的優點與劣勢都已經很明顯了。對於具體的使用者也是見仁見智,以筆者而言由於一直都在中小型企業中,每每對於產品進度的要求會多餘測試,而且更多的以人工測試爲主,所以筆者目前是嘗試在項目中混用MobX與Redux,但願可以有效平衡開發速度與總體的魯棒性/可擴展性。

Personal Best Practice

本部分是列舉一些通用的我的最佳實踐的感覺,不侷限於React或者Redux。具體的關於React與Redux的實踐建議會在下文中介紹。

  • Promise

使用Promise進行異步操做,建議使用await/async做爲Promise語法糖構建異步函數。

  • fetch

使用fetch做爲統一的數據獲取函數,在本項目中使用了筆者的[fluent-fetcher]()做爲fetch的上層封裝使用。

  • 儘量少的使用行內樣式,將每一個組件的樣式文件與組件聲明文件同地存放
    譬如Material-UI這個著名的React樣式組件庫與react-redux-universal-hot-example

以前的版本都是用的CSS-IN-JavaScript,所有內聯樣式。筆者感受仍是須要將CSS與JS剝離開來,一方面是處於職責分割的考慮,另外一方面也是爲了樣式的可變性。經過樣式類的方式來定義方式很方便地能夠經過CSS來修正樣式,而不須要每次都要找半天內聯樣式在哪裏,而後去從新編譯整個項目。

  • 適當合理地編寫純函數,在合理範圍內儘量地將邏輯處理抽象爲純函數

Reference

Boilerplate

Blogs

Quick Start

本部分筆者首先會介紹本項目中全部預置的項目編譯及運行命令。首先須要明確的兩點,本Boilerplate是但願達成如下兩個目標:

(1)將關於應用的配置與關於Webpack的配置剝離開

項目中開發配置主要在dev-config目錄下,若是你要基於本項目進行二次開發,能夠直接拷貝dev-config與package.json到你本身的項目中,而後根據須要配置dev-config/apps.config.js項目。而主要的應用配置信息目前是抽象到了dev-config/app.config.js文件中,主要的可配置項以下:

/**
 * Created by apple on 16/6/8.
 */
const defaultIndexPage = "./dev-config/server/template.html";

module.exports = {
  apps: [
    //HelloWorld
    {
      id: "helloworld",
      src: "./src/simple/helloworld/helloworld.js",
      indexPage: defaultIndexPage,
      compiled: false //控制在執行npm run build時是否會編譯該app
    },
    {
      id: "react",
      src: "./src/react/react_app.js",
      indexPage: defaultIndexPage,
      compiled: true
    },
    {
      id: "redux",
      src: "./src/redux/redux_app.js",
      indexPage: defaultIndexPage,
      compiled: false
    }
  ],

  //開發服務器配置
  devServer: {
    appEntrySrc: "./src/react/react_app.js", //當前待調試的APP的入口文件
    port: 3000 //監聽的Server端口
  },

  //依賴項配置
  proxy: {
    //後端服務器地址 http://your.backend/
    backend: "",
  },

  //若是是生成的依賴庫的配置項
  library: {
    name: "library_portal",//依賴項入口名
    entry: "./src/library/library_portal.js",//依賴庫的入口,
    libraryName: "libraryName",//生成的掛載在全局依賴項下面的名稱
    libraryTarget: "var"//掛載的全局變量名
  }
};

(2)可以以平滑的方式編譯爲三個不一樣的目標,主要是獨立部署(每每做爲單頁應用或者離線WebAPP)與Server Side Rendering這兩種。

Simple

筆者正在逐步採用yarn做爲替代npm的依賴管理工具,不過在目前的README中仍是保留了npm方式,有興趣的朋友能夠本身進行嘗試。

首先使用git clone命令將項目Clone到本地:

git clone https://github.com/wxyyxc1992/Webpack2-React-Redux-Boilerplate
cd Webpack2-React-Redux-Boilerplate

而後使用 npm install / npm link命令安裝依賴項目,同時若是你要實現部署的話還須要一些全局命令,可使用sh install.sh進行安裝。而後將dev-config/app.config.js做以下配置:

//開發服務器配置
  devServer: {
    appEntrySrc: "./src/simple/helloworld/helloworld.js", //當前待調試的APP的入口文件
    port: 3000 //監聽的Server端口
  },

而後使用npm start命令啓動調試服務器,此時在命令行中Webpack DashBoard會自動輸出編譯信息:

而後在瀏覽器中打開http://localhost:3000,你能夠看到以下畫面:

此時在編輯器中實時修改App.js,結果能夠經過熱加載實時反饋到界面上,熱加載主要是利用實時傳送描述熱加載的json與js文件:

這樣當咱們有須要自定義某些熱加載的規則時能夠一樣利用這種方式。咱們經過npm start利用WebpackDevServer來啓動開發服務器,這個很方便咱們進行開發。接下來咱們經過npm run build命令來構建可發佈版本,這種方式編譯得出的基於hashHistory,能夠用於單頁應用(路徑不變)或者離線應用(譬如應用到Cordova中),首先咱們須要在dev-config/apps.config.js中將目標應用編譯狀態設置爲true。注意,若是同時編譯多個應用,那麼CommonsChunkPlugin會將這幾個應用中的公共代碼抽取出來:

//HelloWorld
    {
      id: "helloworld",
      src: "./src/simple/helloworld/helloworld.js",
      indexPage: defaultIndexPage,
      compiled: true //控制在執行npm run build時是否會編譯該app
    },

直接在瀏覽器中打開helloworld.html文件,便可看到與剛纔熱加載時相同的頁面。另外須要注意的是,這裏使用的HTML模板都是統一放置於dev-config/server/template.html文件,筆者建議使用Helmet來爲HTML添加自定義的元標籤或者樣式腳本等。

Library

以上述方式編譯的是獨立可運行的腳本,而在有些狀況下咱們但願以相似於jQuery的方式掛載全局變量/函數的方式使用部分功能,這裏咱們就須要將編譯目標設置爲Library。首先將dev-config/apps.config.js中Library配置以下:

//若是是生成的依賴庫的配置項
  library: {
    name: "library_portal",//依賴項入口名
    entry: "./src/simple/library/library_portal.js",//依賴庫的入口,
    libraryName: "libraryName",//生成的掛載在全局依賴項下面的名稱
    libraryTarget: "var"//掛載的全局變量名
  }

而後使用npm run build:library進行編譯,這裏咱們但願將某個簡單的ES6類導出到頁面中使用:

/**
 * @function 基於ES6的服務類
 */
export class FooService {

    static echo(){

        const fooService = new FooService();

        return fooService.getMessage();
    }

    /**
     * @function 默認構造函數
     */
    constructor() {
        this.message = "This is Message From FooService!";
    }

    getMessage() {
        return this.message;
    }

}

咱們還須要設置專門的入口文件:

/**
 * Created by apple on 16/7/23.
 */
import {FooService} from "./foo";

/**
 * @function 配置須要暴露的API
 * @type {{foo: {echo: FooService.echo}}}
 */
module.exports = {

    foo: {
        echo: FooService.echo
    }

};

而後在須要的頁面中引入編譯好的兩個腳本:

<script src="../../../dist/vendors.bundle.js"></script>
<script src="../../../dist/library_portal.library.js"></script>

此時打開該界面,便可以彈出以下窗口:

Server Side Rendering Support

本部分咱們使用react_app這個應用做爲示例,首先一樣將配置中調試目標設置爲react_app.js:

//開發服務器配置
  devServer: {
    appEntrySrc: "./src/react/react_app.js", //當前待調試的APP的入口文件
    port: 3000 //監聽的Server端口
  },

而後使用npm start命令來啓動開發服務器,而後一樣可使用npm run build命令編譯可發佈版本。而後打開dist/目錄下的react.html文件,便可以看到界面,注意,此時使用的是hashHistory,所以URL的形式爲:

react.html?_ijt=4t0fmg7f6rhsv85efsau6j3t1r#/detail?_k=f9r3og

而後咱們須要以Server Side Rendering的方式發佈項目,其主要區別在於支持browserHistory以及服務端完成渲染。注意,實際上頁面發送到客戶端以後還會依靠加載的JS腳本所有從新渲染,其只是爲了方便SEO/首屏顯示速度/填充初始狀態到界面中。

首先,咱們須要將apps.config.js文件中的ssrServer項目設置爲咱們目標的ssrServer:

//用於服務端渲染的Server路徑
  ssrServer: {
    serverEntrySrc: './src/react/ssr_server.js'
  },

咱們使用npm run build:ssr命令進行編譯,在dist目錄下能夠獲得以下文件:

.
├── react.bundle.js
├── react.css
├── react.html
├── ssr_server.bundle.js
├── ssr_server.bundle.js.map
└── vendors.bundle.js

在本項目中爲了儘量的代碼複用,使用了變量來控制是否支持服務端渲染,咱們直接使用 node dist/ssr_server.bundle.js便可以啓動服務器,此時URL格式爲:

http://localhost:3001/login

Develop Environment:開發環境機制詳解

Webpack2

本項目中使用Webpack 2替代本來的Webpack 1,從Webpack 1到Webpack 2不少的配置項目發生了變化,詳細列表能夠參考引用中提供的連接。而在本項目中,其中幾個典型的修改成:
(1)全部loader的配置提取到了LoaderOptionsPlugin中。

//提取Loader定義到同一地方
  new webpack.LoaderOptionsPlugin({
    minimize: true,
    debug: false,
    options: {
      context: '/',
      postcss: [
        utils.postCSSConfig
      ]
    }
  }),

這裏包含對於本來的UglifyJsPlugin與PostCSS的配置。

(2)loader配置更加靈活。

loaders: [
    {
        test: /\.css$/,
        loaders: [
            "style-loader",
            { loader: "css-loader", query: { modules: true } },
            {
                loader: "sass-loader",
                query: {
                    includePaths: [
                        path.resolve(__dirname, "some-folder")
                    ]
                }
            }
        ]
    }
]

WebpackDevServer & Hot Loader

在前一版本的devServer中,筆者使用了express加上webpack-dev-middleware與webpack-hot-middleware中間件,本版本中是遷移到了WebpackDevServer:

new WebpackDevServer(webpack(config), {
  //設置WebpackDevServer的開發目錄
  contentBase: path.join(__dirname + "/"),
  // publicPath: `http://0.0.0.0:${appsConfig.devServer.port}/`,
  hot: true,
  historyApiFallback: true,
  quiet:true,
  // noInfo: true,
  stats: {colors: true}
}).listen(appsConfig.devServer.port, '0.0.0.0', function (err, result) {
  if (err) {
    return console.log(err);
  }

  console.log(`Listening at http://0.0.0.0:${appsConfig.devServer.port}/`);
});

另外就是對於HotReloader的使用,目前不少熱加載的實現方式仍是基於react-transform,不過該項目已經廢棄了,所以這裏若是要本身添加熱加載組件的話,建議使用react-hot-loader,目前筆者使用了3.0版本。咱們分別須要將上面的WebpackDevServer中的hot設置爲true,而且在Babel配置文件中添加以下配置:

"env": {
    "development": {
      "presets": [
        "react-hmre"
      ],
      "plugins": [
        "react-hot-loader/babel"
      ]
    }
  }

API Proxy

待補充。

React Router & Server Side Rendering

Pure Frontend

咱們首先從應用的入口程序看起:

let history;

//判斷是否爲SSR從而肯定應該選用哪一個History
if (__SSR__) {
  //若是是瀏覽器環境,則使用browserHistory
  history = browserHistory;
} else {
  //若是是獨立環境,則使用hashHistory
  history = hashHistory;
}

//在瀏覽器環境下使用hashHistory
const router = <Router history={history}>
  {getRoutes(localStorage)}
</Router>;

//將組件渲染到DOM中
render(
  router,
  document.getElementById('root')
);

這裏將路由配置提取到單獨文件中,是由於路由配置是須要在服務端與客戶端共享的,所以將多是DOM下獨有的localStorage或者相似的對象以參數方式傳入。對於Route的配置卻是客戶端與服務端保持一致:

return (
    <Route path="/" history={browserHistory} component={Container}>
      <IndexRoute component={Home}/>
      <Route path="home" component={withRouter(Home)}/>
      <Route path="login" component={withRouter(Login)}/>
      <Route path="detail" component={withRouter(Detail)} onEnter={auth}/>
    </Route>
  );

其他的代碼很少,能夠自行瀏覽整個項目。這裏有個關於React Router的點我想說明下,在Route配置時使用withRouter這個方法能夠以HOC方式注入router對象到Props中,這樣咱們在進行頁面跳轉時可使用:

this.props.router.goBack()

Server Side Rendering

首先咱們說幾句廢話,須要瞭解服務端渲染到底作了啥:

(1)Server端只負責首頁的渲染,其餘頁面仍然由客戶端進行渲染。即雖然URL Path發生了變化,可是並未觸發整個頁面的徹底刷新。

(2)以Redux爲表明的狀態管理工具中的Store只是在第一次渲染時將數據傳遞給客戶端,在後續的頁面切換/認證等操做中的全部代碼皆在客戶端運行。

這裏咱們不須要改造上面的客戶端入口文件,而須要添加一個用於服務端運行的文件,其核心代碼爲:

//處理全部的請求地址
app.get('/*', function (req, res) {

  //匹配客戶端路由
  match({routes: getRoutes(), location:req.originalUrl}, (error, redirectLocation, renderProps) => {

    if (error) {

      res.status(500).send(error.message)

    } else if (redirectLocation) {

      res.redirect(302, redirectLocation.pathname + redirectLocation.search)

    } else if (renderProps) {

      let html = renderToString(<RouterContext {...renderProps} />);

      res.status(200).send(renderHTML(html, {key: "value"}, ['/static/vendors.bundle.js', '/static/react.bundle.js']));

    } else {
      res.status(404).send('Not found')
    }
  })
});

能夠看出,便是用戶首次向服務端發起請求時,首先對於首屏展現的組件進行渲染。咱們來作一個對比,服務端渲染以後的獲得的HTML字符串爲:

<div data-reactroot="" data-reactid="1" data-react-checksum="663537196">
    <section class="login__container" data-reactid="2"><!-- react-text: 3 -->登錄界面<!-- /react-text -->
        <div data-reactid="4utton data-reactid=" 5
        ">點擊登錄</button>
        <button data-reactid="6">點擊登出</button>
</div></section></div>

而原始的JSX組件以下,能夠發現事件處理等不少代碼都被過濾了。

/**
 * Created by apple on 16/9/13.
 */
export class Login extends Component {

  /**
   * @function 默認渲染函數
   * @return {XML}
   */
  render() {
    return <section className="login__container">
      登錄界面

      <div>
        <button onClick={()=> {
          //將登錄信息寫入cookies與localStorage
          login().then(()=> {
            //登錄成功跳轉到詳情頁
            this.props.router.push('/detail');
          });
        }}>
          點擊登錄
        </button>

        <button onClick={()=> {
          //將登錄信息寫入cookies與localStorage
          logout();
          //登錄成功跳轉到詳情頁
          this.props.router.push('/');
        }}>
          點擊登出
        </button>
      </div>

    </section>
  }
}

Authentication

有時候咱們須要對某些URL添加權限認證,即只容許認證用戶才能訪問,這裏咱們能夠經過Route中的onEnter屬性進行控制:

<Route path="detail" component={withRouter(Detail)} onEnter={auth}/>

而咱們在上文中傳入的Store對象也是在這個時候派上用場:

/**
   * @function 判斷用戶是否登錄,若是未登錄則強制性跳轉到登陸頁面
   * @param nextState
   * @param replace
   * @param callback
   */
  async function auth(nextState, replace, callback) {

    let userToken = store.userToken;

    //在這裏執行異步認證,假設傳入的store中包含userToken
    //這裏使用Promise執行異步操做
    //若是是SSR,則本部分代碼會在服務端運行

    let isValid = await valid_user(userToken);

    //若是用戶還沒有認證,則進行跳轉操做
    isValid || replace('/login');

    //執行回調函數
    callback();

  }

Isomorphic Redux

筆者目前在本身主導的幾個前端項目中漸漸的轉向MobX與Redux並行.本項目中對於Redux的文件佈局採起的是Ducks這種方式,參考了my-journey-toward-a-maintainable-project-structure-for-react-redux一文。即按照特性來將Reducers、ActionCreators、Actions、Selectors集中到單個文件中:

// src/ducks/auth.js
const AUTO_LOGIN = 'AUTH/AUTH_AUTO_LOGIN'
const SIGNUP_REQUEST = 'AUTH/SIGNUP_REQUEST'
const SIGNUP_SUCCESS = 'AUTH/SIGNUP_SUCCESS'
const SIGNUP_FAILURE = 'AUTH/SIGNUP_FAILURE'
const LOGIN_REQUEST = 'AUTH/LOGIN_REQUEST'
const LOGIN_SUCCESS = 'AUTH/LOGIN_SUCCESS'
const LOGIN_FAILURE = 'AUTH/LOGIN_FAILURE'
const LOGOUT = 'AUTH/LOGOUT'

const initialState = {
  user: null,
  isLoading: false,
  error: null
}

export default (state = initialState, action) => {
  switch (action.type) {
    case SIGNUP_REQUEST:
    case LOGIN_REQUEST:
      return { ...state, isLoading: true, error: null }

    case SIGNUP_SUCCESS:
    case LOGIN_SUCCESS:
      return { ...state, isLoading: false, user: action.user }

    case SIGNUP_FAILURE:
    case LOGIN_FAILURE:
      return { ...state, isLoading: false, error: action.error }

    case LOGOUT:
      return { ...state, user: null }

    default:
      return state
  }
}

export const signup = (email, password) => ({ type: SIGNUP_REQUEST, email, password })
export const login = (email, password) => ({ type: LOGIN_REQUEST, email, password })
export const logout = () => ({ type: LOGOUT })

對於Redux Dev Tools,請自行使用[Browser Extension]()。

Simple Count

咱們首先以簡單的基於Redux的計數器爲例,將dev-config/apps.config.js中的開發配置設置爲以下:

//開發服務器配置
  devServer: {
    appEntrySrc: "./src/redux/redux_app.js", //當前待調試的APP的入口文件
    port: 3000 //監聽的Server端口
  },

而後使用npm start運行開發服務器,界面上的以下表示即爲該示例:

在Redux DevTools中,紅色框線標示出的即爲count相關的狀態,咱們接下來簡單描述下其核心代碼。在Redux開發中,咱們首先須要構建一個Ducks,即包含Action、ActionCreator與Reducer:

/**
 * Created by apple on 16/10/11.
 */
// no changes here ?

/**
 * @function 定義Actions
 * @type {string}
 */
export const INCREMENT_COUNT = 'INCREMENT';

export const DECREMENT_COUNT = 'DECREMENT';

/**
 * @function 定義Reducer
 * @param state
 * @param action
 * @return {number}
 */
export default (state = 0, {type}) => {
  switch (type) {
    case INCREMENT_COUNT:
      return state + 1;
    case DECREMENT_COUNT:
      return state - 1;
    default:
      return state
  }
}

/**
 *@region 定義Action Creator
 */

/**
 * @function 觸發加1操做
 * @return {{type: string}}
 */
export const increment = ()=> {

  return {
    type: INCREMENT_COUNT
  }

};

/**
 * @function 在這裏進行異步加1操做
 * @return {function(*)}
 */
export const incrementAsync = ()=> {

  return dispatch => {
    setTimeout(() => {
      // Yay! Can invoke sync or async actions with `dispatch`
      dispatch(increment());
    }, 1000);
  };
};

/**
 * @function 執行計數器減一操做
 * @return {{type: string}}
 */
export const decrement = ()=> {

  return {
    type: DECREMENT_COUNT
  }

};

這裏爲了簡單起見,咱們是使用了redux-thunk來處理異步Action,實際上在Redux中對於異步Action的處理也有各類各樣的實踐,包括筆者在這裏自定義的promiseMiddleware,也是一種方式。而後咱們須要構建一個Store來存放全局的狀態,Store自己是基於Reducer來遞歸生成狀態樹的,其核心代碼以下:

const store = createStoreWithMiddleware(
    rootReducer,
    initialState,
    typeof window === 'object' && typeof window.devToolsExtension !== 'undefined' && __DEV__ ? window.devToolsExtension() : f => f);

  /**
   * @function 保證Redux Reducer的熱加載
   */
  if (__DEV__ && module.hot) {
    module.hot.accept('./reducer', () => {
      //替換Store中的Reducer
      store.replaceReducer(require('./reducer'));
    })
  }

如今咱們已經寫完了Redux部分的代碼,下面就是須要將狀態導入到界面中:

@connect(
  state => ({
    count: state.count
  }),
  {pushState: push, increment, incrementAsync, decrement}
)
export class Home extends Component {
  render() {

    //在非SSR狀態下導入SCSS文件
    __SSR__ || require('./home.scss');

    const {count, pushState, increment, incrementAsync, decrement} = this.props;

    return <section className="home__container">

      <div>
        王下邀月熊 Webpack2-React-Redux-Boilerplate
      </div>

      <br/>
      <br/>

      <div>導航欄目:</div>

      <li>
        <button onClick={()=> {
          pushState('/detail')
        }}>
          詳情頁(須要先進行登錄操做)
        </button>
      </li>
      <li><Link to="/login">登錄頁</Link></li>

      <br/>
      <br/>

      <div>基於Redux的Count實例</div>
      <div>{count}</div>
      <div>
        <button onClick={increment}>加1</button>
        <button onClick={incrementAsync}>異步加1</button>
        <button onClick={decrement}>減1</button>
      </div>

    </section>
  }
}

React Router Redux

React Router Redux的代碼仍是簡單易懂的,其只是在用戶點擊/跳轉與React Router自身的History之間加上了一層封裝

history + store (redux) → react-router-redux → enhanced history → react-router

若是你須要自定義其餘的Location,譬如若是你須要引入ImmutableJS做爲Store:

import Immutable from 'immutable';
import {
    LOCATION_CHANGE
} from 'react-router-redux';

let initialState;

initialState = Immutable.fromJS({
    locationBeforeTransitions: undefined
});

export default (state = initialState, action) => {
    if (action.type === LOCATION_CHANGE) {
        return state.merge({
            locationBeforeTransitions: action.payload
        });
    }

    return state;
};

SSR

與上文中的Server Side Rendering Server相比,其添加了對於狀態傳遞的支持:

//處理全部的請求地址
app.get('/*', function (req, res) {

  //構建出內存中歷史記錄
  const memoryHistory = createHistory(req.originalUrl);

  //服務端構建出Store
  const store = createStore(memoryHistory);

  //構建出與Store同步的history
  const history = syncHistoryWithStore(memoryHistory, store);

  //匹配客戶端路由
  match({history, routes: getRoutes(), location: req.originalUrl}, (error, redirectLocation, renderProps) => {

    if (error) {

      res.status(500).send(error.message)

    } else if (redirectLocation) {

      res.redirect(302, redirectLocation.pathname + redirectLocation.search)

    } else if (renderProps) {

      let html = renderToString(
        <Provider store={store}>
          <RouterContext {...renderProps} />
        </Provider>
      );

      //設置全局的navigator值
      // global.navigator = {userAgent: req.headers['user-agent']};

      res.status(200).send(renderHTML(html, {key: "value"}, ['/static/vendors.bundle.js', '/static/redux.bundle.js']));

    } else {
      res.status(404).send('Not found')
    }
  })
});

歡迎你們指導與討論,同時再次建議,在不能掌握本項目的狀況慎重直接用於大型項目中,對本身負責。

相關文章
相關標籤/搜索