在重构脚手架中掌握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')
    }
  })
});

欢迎你们指导与讨论,同时再次建议,在不能掌握本项目的状况慎重直接用于大型项目中,对本身负责。

相关文章
相关标签/搜索