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

LorSeder 9年前發布 | 36K 次閱讀 Redux 前端技術 webpack

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

Webpack2 React Redux Boilerplate

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

如果你是完全的React初學者,那么建議首先了解下 使用非死book的create-react-app快速構建React開發環境 ,同時參考筆者的React入門與最佳實踐 以及 Redux入門與最佳實踐 。本項目算是個半自動化的腳手架工具,筆者并不希望做成完全傻瓜式的開箱即用的工具,這只會給你的項目埋下危險的伏筆,希望每個可能用這個Boilerplate的同學都能閱讀文本,至少要保證對文本提及的知識點有個全局的了解。

Features

本部分假設你已經對Webpack有了大概的了解,這里我們會針對筆者自己在生產環境下使用的Webpack編譯腳本進行的一個總結,在介紹具體的配置方案之前筆者想先概述下該配置文件的設計的目標,或者說是筆者認為一個前端編譯環境應該達成的特性,這樣以后即使Webpack被淘汰了也可以利用其他的譬如JSPM之類的來完成類似的工作。

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

  • 單一的配置文件:很多項目里面是把開發環境與生產環境寫了兩個配置文件,可能筆者比較懶吧,不喜歡這么做,因此筆者的第一個特性就是單一的配置文件,然后通過npm封裝不同的編譯命令傳入環境變量,然后在配置文件中根據不同的環境變量進行動態響應。另外,要保證一個Boilerplate能夠在最小修改的情況下應用到其他項目。

  • 多應用入口支持:無論是單頁應用還是多頁應用,在Webpack中往往會把一個html文件作為一個入口。筆者在進行項目開發時,往往會需要面對多個入口,即多個HTML文件,然后這個HTML文件加載不同的JS或者CSS文件。譬如登錄頁面與主界面,往往可以視作兩個不同的入口。Webpack原生提倡的配置方案是面向過程的,而筆者在這里是面向應用方式的封裝配置。

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

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

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

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

真的需要Redux嗎?

  • 思考:我需要怎樣的前端狀態管理工具?

  • 你不一定需要Redux

雖然本項目是面向Webpack+React+Redux的Boilerplate,但是筆者還是希望在此拋出這個問題,也是便于大家能夠理解Redux。對于這個問題筆者沒有明確的答案,但是在這兩年的自己對于Redux的實戰中,也一直在搖把。我堅定的認為Redux指明了解決某類問題的正確方向,但是它真的適合于所有的項目嗎?筆者在我的前端之路一文中提及,從以DOM操作為核心的jQuery時代到以聲明式組件為核心的React時代的變遷是聲明式編程對于命令式的慢慢代替,而Redux則是純粹的聲明式編程典范。這里以某個登錄認證的小例子進行說明,產品的需求是允許用戶在登錄成功之后在登錄頁面上顯示“登錄成功,正在跳轉”,然后延時跳轉到其他頁面。這里強調要在登錄頁面上進行回顯是因為很多人習慣將,跳轉作為Side Effect在Thunk或者Saga中就處理了,并沒有影響到界面本身。首先,如果是純粹的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"http://掛載的全局變量名
  }
};

(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"http://掛載的全局變量名
  }

然后使用 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

  • What's new in webpack 2

本項目中使用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

  • React路由解決方案React-Router介紹與實踐

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')
    }
  })
});

歡迎大家指導與討論,同時再次建議,在不能掌握本項目的情況慎重直接用于大型項目中,對自己負責。

 

來自:https://segmentfault.com/a/1190000007166607

 

 本文由用戶 LorSeder 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!