├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config ├── jest │ ├── base.json │ └── coverage.json └── webpack │ ├── base.js │ ├── build.js │ ├── defaults.js │ └── dev.js ├── package.json ├── server.js ├── src ├── assets │ └── style │ │ └── reboot.scss ├── components │ └── HeaderNav │ │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── index.test.js.snap │ │ └── index.test.js │ │ ├── index.js │ │ └── style.scss ├── containers │ ├── Home │ │ ├── __tests__ │ │ │ ├── Home.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Home.test.js.snap │ │ └── index.js │ ├── PageA │ │ └── index.js │ ├── PageB │ │ └── index.js │ └── app │ │ └── index.js ├── index.html ├── index.js ├── redux │ ├── actions │ │ ├── __tests__ │ │ │ └── actions.test.js │ │ ├── action_types.js │ │ ├── index.js │ │ └── menu │ │ │ └── index.js │ ├── middleware │ │ ├── __tests__ │ │ │ └── middleware.test.js │ │ └── index.js │ ├── reducers │ │ ├── __tests__ │ │ │ ├── menu.test.js │ │ │ ├── pageA.test.js │ │ │ └── pageB.test.js │ │ ├── index.js │ │ ├── menu.js │ │ ├── pageA.js │ │ └── pageB.js │ └── stores │ │ ├── __tests__ │ │ └── stores.test.js │ │ └── index.js └── routers.js ├── test └── mock │ ├── fileMock.js │ └── styleMock.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", { "modules": false }], 4 | "stage-0", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "react-hot-loader/babel", 9 | "transform-decorators-legacy", 10 | "transform-object-rest-spread", 11 | ["import", { "libraryName": "antd", "style": "css" }] 12 | ], 13 | 14 | "env": { 15 | "test": { 16 | "plugins": ["transform-es2015-modules-commonjs"] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "parser": "babel-eslint", 9 | "parserOptions": { 10 | "ecmaVersion": 6, 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "experimentalObjectRestSpread": true, 14 | "jsx": true, 15 | "modules": true 16 | } 17 | }, 18 | "plugins": [ 19 | "react" 20 | ], 21 | // ref: http://eslint.org/docs/rules/ 22 | "extends": "eslint:recommended", 23 | "rules": { 24 | // 箭头函数中的箭头前后需要留空格 25 | "arrow-spacing": [2, { "before": true, "after": true }], 26 | // 箭头函数中,在需要的时候,在参数外使用小括号(只有一个参数时,可以不适用括号,其它情况下都需要使用括号) 27 | "arrow-parens": [2, "as-needed"], 28 | // 如果代码块是单行的时候,代码块内部前后需要留一个空格 29 | "block-spacing": [2, "always"], 30 | // 大括号语法采用『1tbs』,允许单行样式 31 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 32 | // 在定义对象或数组时,最后一项不能加逗号 33 | "comma-dangle": 1, 34 | // 在写逗号时,逗号前面不需要加空格,而逗号后面需要添加空格 35 | "comma-spacing": [2, { "before": false, "after": true }], 36 | // 如果逗号可以放在行首或行尾时,那么请放在行尾 37 | "comma-style": [2, "last"], 38 | // 在constructor函数中,如果classes是继承其他class,那么请使用super。否者不使用super 39 | "constructor-super": 2, 40 | // 在if-else语句中,如果if或else语句后面是多行,那么必须加大括号。如果是单行就应该省略大括号。 41 | "curly": [2, "multi-line"], 42 | // 该规则规定了.应该放置的位置, 43 | "dot-location": [2, "property"], 44 | // 该规则要求代码最后面需要留一空行,(仅需要留一空行) 45 | "eol-last": 2, 46 | // 使用=== !== 代替== != . 47 | "eqeqeq": [2, "allow-null"], 48 | // 该规则规定了generator函数中星号两边的空白。 49 | "generator-star-spacing": [2, { "before": true, "after": true }], 50 | // 规定callback 如果有err参数,只能写出err 或者 error . 51 | "handle-callback-err": [2, "^(err|error)$" ], 52 | // 这个就是关于用什么来缩进了,规定使用tab 来进行缩进,switch中case也需要一个tab . 53 | "indent": [2, "tab", { "SwitchCase": 1 }], 54 | // 该规则规定了在对象字面量语法中,key和value之间的空白,冒号前不要空格,冒号后面需要一个空格 55 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 56 | // 构造函数首字母大写 57 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 58 | // 在使用构造函数时候,函数调用的圆括号不能够省略 59 | "new-parens": 2, 60 | // 禁止使用Array构造函数 61 | "no-array-constructor": 2, 62 | // 禁止使用arguments.caller和arguments.callee 63 | "no-caller": 2, 64 | // 禁止覆盖class命名,也就是说变量名不要和class名重名 65 | "no-class-assign": 2, 66 | // 在条件语句中不要使用赋值语句 67 | "no-cond-assign": 2, 68 | // 不使用 console 69 | "no-console": 1, 70 | // const申明的变量禁止修改 71 | "no-const-assign": 2, 72 | // 在正则表达式中禁止使用控制符(详见官网) 73 | "no-control-regex": 2, 74 | // 禁止使用debugger语句 75 | "no-debugger": 2, 76 | // 禁止使用delete删除var申明的变量 77 | "no-delete-var": 2, 78 | // 函数参数禁止重名 79 | "no-dupe-args": 2, 80 | // class中的成员禁止重名 81 | "no-dupe-class-members": 2, 82 | // 在对象字面量中,禁止使用重复的key 83 | "no-dupe-keys": 2, 84 | // 在switch语句中禁止重复的case 85 | "no-duplicate-case": 2, 86 | // 禁止使用不匹配任何字符串的正则表达式 87 | "no-empty-character-class": 2, 88 | // 禁止使用eval函数 89 | "no-eval": 2, 90 | // 禁止对catch语句中的参数进行赋值 91 | "no-ex-assign": 2, 92 | // 禁止扩展原生对象 93 | "no-extend-native": 2, 94 | // 禁止在不必要的时候使用bind函数 95 | "no-extra-bind": 2, 96 | // 在一个本来就会自动转化为布尔值的上下文中就没必要再使用!! 进行强制转化了。 97 | "no-extra-boolean-cast": 2, 98 | // 禁止使用多余的圆括号 99 | "no-extra-parens": [2, "functions"], 100 | // 这条规则,简单来说就是在case语句中尽量加break,避免不必要的fallthrough错误,如果需要fall through,那么看官网。 101 | "no-fallthrough": 2, 102 | // 简单来说不要写这样的数字.2 2.。应该写全,2.2 2.0 . 103 | "no-floating-decimal": 2, 104 | // 禁止对函数名重新赋值 105 | "no-func-assign": 2, 106 | // 禁止使用类eval的函数。 107 | "no-implied-eval": 2, 108 | // 禁止在代码块中定义函数(下面的规则仅限制函数) 109 | "no-inner-declarations": [2, "functions"], 110 | // RegExp构造函数中禁止使用非法正则语句 111 | "no-invalid-regexp": 2, 112 | // 禁止使用不规则的空白符 113 | "no-irregular-whitespace": 2, 114 | // 禁止使用__iterator__属性 115 | "no-iterator": 2, 116 | // label和var申明的变量不能重名 117 | "no-label-var": 2, 118 | // 禁止使用label语句 119 | "no-labels": 2, 120 | // 禁止使用没有必要的嵌套代码块 121 | "no-lone-blocks": 2, 122 | // 不要把空格和tab混用 123 | "no-mixed-spaces-and-tabs": 2, 124 | // 顾名思义,该规则保证了在逻辑表达式、条件表达式、 125 | // 申明语句、数组元素、对象属性、sequences、函数参数中不使用超过一个的空白符。 126 | "no-multi-spaces": 2, 127 | // 该规则保证了字符串不分两行书写。 128 | "no-multi-str": 2, 129 | // 空行不能够超过2行 130 | "no-multiple-empty-lines": [2, { "max": 2 }], 131 | // 该规则保证了不重写原生对象。 132 | "no-native-reassign": 2, 133 | // 在in操作符左边的操作项不能用! 例如这样写不对的:if ( !a in b) { // dosomething } 134 | "no-negated-in-lhs": 2, 135 | // 当我们使用new操作符去调用构造函数时,需要把调用结果赋值给一个变量。 136 | "no-new": 2, 137 | // 该规则保证了不使用new Function(); 语句。 138 | "no-new-func": 2, 139 | // 不要通过new Object(),来定义对象 140 | "no-new-object": 2, 141 | // 禁止把require方法和new操作符一起使用。 142 | "no-new-require": 2, 143 | // 当定义字符串、数字、布尔值就不要使用构造函数了,String、Number、Boolean 144 | "no-new-wrappers": 2, 145 | // 禁止无意得把全局对象当函数调用了,比如下面写法错误的:Math(), JSON() 146 | "no-obj-calls": 2, 147 | // 不要使用八进制的语法。 148 | "no-octal": 2, 149 | // 用的少,见官网。http://eslint.org/docs/rules/ 150 | "no-octal-escape": 2, 151 | // 不要使用__proto__ 152 | "no-proto": 2, 153 | // 不要重复申明一个变量 154 | "no-redeclare": 2, 155 | // 正则表达式中不要使用空格 156 | "no-regex-spaces": 2, 157 | // return语句中不要写赋值语句 158 | "no-return-assign": 2, 159 | // 不要和自身作比较 160 | "no-self-compare": 2, 161 | // 不要使用逗号操作符,详见官网 162 | "no-sequences": 2, 163 | // 禁止对一些关键字或者保留字进行赋值操作,比如NaN、Infinity、undefined、eval、arguments等。 164 | "no-shadow-restricted-names": 2, 165 | // 函数调用时,圆括号前面不能有空格 166 | "no-spaced-func": 2, 167 | // 禁止使用稀疏数组 168 | "no-sparse-arrays": 2, 169 | // 在调用super之前不能使用this对象 170 | "no-this-before-super": 2, 171 | // 严格限制了抛出错误的类型,简单来说只能够抛出Error生成的错误。但是这条规则并不能够保证你只能够 172 | // 抛出Error错误。详细见官网 173 | "no-throw-literal": 2, 174 | // 行末禁止加空格 175 | "no-trailing-spaces": 2, 176 | // 禁止使用没有定义的变量,除非在/*global*/已经申明 177 | "no-undef": 2, 178 | // 禁止把undefined赋值给一个变量 179 | "no-undef-init": 2, 180 | // 禁止在不需要分行的时候使用了分行 181 | "no-unexpected-multiline": 2, 182 | // 禁止使用没有必要的三元操作符,因为用些三元操作符可以使用其他语句替换 183 | "no-unneeded-ternary": [2, { "defaultAssignment": false }], 184 | // 没有执行不到的代码 185 | "no-unreachable": 2, 186 | // 没有定义了没有被使用到的变量 187 | "no-unused-vars": 2, 188 | // 禁止在不需要使用call()或者apply()的时候使用了这两个方法 189 | "no-useless-call": 2, 190 | // 不要使用with语句 191 | "no-with": 2, 192 | // 在进行断行时,操作符应该放在行首还是行尾。并且还可以对某些操作符进行重写。 193 | "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }], 194 | // 在使用parseInt() 方法时,需要传递第二个参数,来帮助解析,告诉方法解析成多少进制。 195 | "radix": 2, 196 | // 该规则规定了分号前后的空格,具体规定如下。 197 | "semi-spacing": [2, { "before": false, "after": true }], 198 | // 关键词前后面需要加空格 199 | "keyword-spacing": 2, 200 | // 代码块前面需要加空格 201 | "space-before-blocks": [2, "always"], 202 | // 函数圆括号前面需要加空格 203 | "space-before-function-paren": [2, "never"], 204 | // 圆括号内部不需要加空格 205 | "space-in-parens": [2, "never"], 206 | // 操作符前后需要加空格 207 | "space-infix-ops": 2, 208 | // 一元操作符前后是否需要加空格,单词类操作符需要加,而非单词类操作符不用加 209 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 210 | // 注释`/*` `//`,后面需要留一个空格 211 | "spaced-comment": [2, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], 212 | // 推荐使用isNaN方法,而不要直接和NaN作比较 213 | "use-isnan": 2, 214 | // 在使用typeof操作符时,作比较的字符串必须是合法字符串eg:'string' 'object' 215 | "valid-typeof": 2, 216 | // 立即执行函数需要用圆括号包围 217 | "wrap-iife": [2, "any"], 218 | // yoda条件语句就是字面量应该写在比较操作符的左边,而变量应该写在比较操作符的右边。 219 | // 而下面的规则要求,变量写在前面,字面量写在右边 220 | "yoda": [2, "never"], 221 | 222 | // ref: https://github.com/yannickcr/eslint-plugin-react 223 | // 消除变量只在 jsx 中使用, 而报出 no-unused-vars 的出错 224 | "react/jsx-uses-react": 2, 225 | "react/jsx-uses-vars": 2, 226 | 227 | // ref: https://github.com/airbnb/javascript/tree/master/react 228 | // 使用 class extends React.Component 而非 React.createClass 229 | "react/prefer-es6-class": 2, 230 | // 使用首字母大写来命名 component, 组件实例对象用驼峰式 231 | "react/jsx-pascal-case": 2, 232 | // jsx 属性用冒号 "" 233 | "jsx-quotes": 2, 234 | // jsx 中大括号两边不加空格 235 | "react/jsx-curly-spacing": 1, 236 | // jsx 中若属性值为 true, 则省略 237 | "react/jsx-boolean-value": 1, 238 | // component 没有子元素,则使用自闭标签 239 | "react/self-closing-comp": 1, 240 | // 每个组件属性自占一行 241 | "react/jsx-closing-bracket-location": 1, 242 | // 组件方法声明顺序 243 | "react/sort-comp": 1 244 | } 245 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | build 4 | 5 | test/coverage 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | cache: 5 | directories: node_modules 6 | script: 7 | - npm run test:coverage 8 | - npm run codecov 9 | os: 10 | - linux 11 | - osx 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Disciple Ding 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://img.shields.io/travis/DiscipleD/react-redux-antd-starter/master.svg)](https://travis-ci.org/DiscipleD/react-redux-antd-starter) 2 | [![codecov](https://codecov.io/gh/DiscipleD/react-redux-antd-starter/branch/master/graph/badge.svg)](https://codecov.io/gh/DiscipleD/react-redux-antd-starter) 3 | 4 | # react-redux-antd-starter 5 | **React 全家桶 + Ant.Design 脚手架** 6 | 7 | ## React 全家桶 8 | 9 | * react: ^15.3.1 10 | * react-redux: ^4.4.5 11 | * react-router: ^2.7.0 12 | * react-router-redux: ^4.0.5 13 | * redux: ^3.6.0 14 | * redux-actions: ^0.12.0 15 | * redux-promise: ^0.5.3 16 | * redux-thunk: ^2.1.0 17 | 18 | ## Ant Design 19 | 20 | * antd: ^1.11.0 21 | 22 | ## start 23 | 24 | ```bash 25 | npm install 26 | ``` 27 | 28 | ## run dev server 29 | 30 | ```bash 31 | npm start 32 | ``` 33 | 34 | ## run UT 35 | 36 | ```bash 37 | npm test 38 | ``` 39 | 40 | ## See UT coverage 41 | 42 | ```bash 43 | npm run test:coverage 44 | ``` 45 | 46 | ## build package 47 | 48 | ```bash 49 | npm run build 50 | ``` 51 | 52 | ## More 53 | See more detail example, please ref to the `real-world` branch. 54 | 55 | ```bash 56 | git checkout real-world 57 | ``` -------------------------------------------------------------------------------- /config/jest/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "verbose": true, 4 | "testPathDirs": ["/src"], 5 | "moduleNameMapper": { 6 | "^containers": "/src/containers", 7 | "^components": "/src/components", 8 | "^actions": "/src/redux/actions", 9 | 10 | "^.+\\.(css|less|scss|sass)$": "/test/mock/styleMock.js", 11 | "^.+\\.(gif|ttf|eot|svg)$": "/test/mock/fileMock.js" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /config/jest/coverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "/config/jest/base.json", 3 | "coverageDirectory": "/test/coverage", 4 | "bail": true, 5 | "collectCoverage": true, 6 | "collectCoverageFrom": [ 7 | "src/components/**/*.js", 8 | "src/containers/**/*.js", 9 | "src/redux/**/*.js" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /config/webpack/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-5. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const defaultSettings = require('./defaults'); 8 | 9 | module.exports = { 10 | devtool: 'eval', 11 | entry: { 12 | chunk: ['react', 'react-dom', 'react-redux', 'react-router', 13 | 'react-router-redux', 'redux' , 'redux-actions', 'redux-logger', 'redux-promise'], 14 | app: [defaultSettings.sourcePath + '/index.js'] 15 | }, 16 | output: { 17 | path: defaultSettings.buildPath, 18 | filename: '[name].[hash:8].js', 19 | publicPath: defaultSettings.publicPath 20 | }, 21 | resolve: defaultSettings.resolve, 22 | module: defaultSettings.getDefaultModules(), 23 | plugins: defaultSettings.getDefaultPlugins() 24 | }; 25 | -------------------------------------------------------------------------------- /config/webpack/build.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-5. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const webpack = require('webpack'); 8 | const CleanPlugin = require('clean-webpack-plugin'); 9 | 10 | const baseConfig = require('./base'); 11 | const defaultSettings = require('./defaults'); 12 | 13 | const config = Object.assign({}, baseConfig, { 14 | cache: false, 15 | devtool: 'source-map', 16 | plugins: [ 17 | new CleanPlugin([defaultSettings.buildPath]), 18 | new webpack.DefinePlugin({ 19 | 'process.env.NODE_ENV': '"production"' 20 | }), 21 | new webpack.LoaderOptionsPlugin({ 22 | minimize: true 23 | }), 24 | new webpack.optimize.UglifyJsPlugin(), 25 | new webpack.optimize.AggressiveMergingPlugin() 26 | ].concat(baseConfig.plugins) 27 | }); 28 | 29 | module.exports = config; 30 | -------------------------------------------------------------------------------- /config/webpack/defaults.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-5. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const path = require('path'); 8 | const webpack = require('webpack'); 9 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 10 | const autoprefixer = require('autoprefixer'); 11 | 12 | const sourcePath = path.join(__dirname, '../../src'); 13 | const buildPath = path.join(__dirname, '../../build'); 14 | const publicPath = '/'; 15 | const resolve = { 16 | extensions: ['.js', '.jsx', '.json'], 17 | alias: { 18 | containers: `${sourcePath}/containers/`, 19 | components: `${sourcePath}/components/`, 20 | actions: `${sourcePath}/redux/actions/` 21 | } 22 | }; 23 | 24 | function getDefaultModules() { 25 | return { 26 | rules: [ 27 | { 28 | test: /\.(js|jsx)$/, 29 | loader: 'eslint-loader', 30 | enforce: "pre", 31 | exclude: /node_modules/ 32 | }, 33 | { 34 | test: /\.(js|jsx)$/, 35 | loader: 'babel-loader', 36 | exclude: /node_modules/ 37 | }, 38 | { 39 | test: /\.(sc|c)ss$/, 40 | use: [ 41 | 'style-loader', 42 | 'css-loader?sourceMap', 43 | { 44 | loader: 'postcss-loader?sourceMap', 45 | options: { 46 | plugins: () => [autoprefixer({browsers: ['last 2 versions']})] 47 | } 48 | }, { 49 | loader: 'sass-loader', 50 | options: { 51 | outputStyle: 'expanded' 52 | } 53 | }] 54 | }, 55 | { 56 | test: /\.(jpe?g|png|gif|svg)$/i, 57 | use: [{ 58 | loader: 'file', 59 | options: { 60 | hash: 'sha512', 61 | digest: 'hex', 62 | name: '[path][name]-[hash:8].[ext]' 63 | } 64 | }, { 65 | loader: 'image-webpack', 66 | options: { 67 | bypassOnDebug: true, 68 | optimizationLevel: 7, 69 | interlaced: false 70 | } 71 | }] 72 | }, 73 | { 74 | test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, 75 | loader: 'url?limit=8192&mimetype=application/font-woff&prefix=fonts' 76 | }, 77 | { 78 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 79 | loader: 'url?limit=8192&mimetype=application/octet-stream&prefix=fonts' 80 | }, 81 | { 82 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 83 | loader: 'url?limit=8192&mimetype=application/vnd.ms-fontobject&prefix=fonts' 84 | }, 85 | { 86 | test: /\.(mp4|ogg)$/, 87 | loader: 'file?hash=sha512&digest=hex&name=[path][name]-[hash:8].[ext]' 88 | } 89 | ] 90 | }; 91 | } 92 | 93 | function getDefaultPlugins() { 94 | return [ 95 | new webpack.optimize.CommonsChunkPlugin({ 96 | name: 'chunk', 97 | filename: 'chunk.[hash:8].js' 98 | }), 99 | new HtmlWebpackPlugin({ 100 | template: sourcePath + '/index.html' 101 | }), 102 | new webpack.NoEmitOnErrorsPlugin() 103 | ]; 104 | } 105 | 106 | module.exports = { 107 | sourcePath, 108 | buildPath, 109 | publicPath, 110 | resolve, 111 | getDefaultModules, 112 | getDefaultPlugins 113 | }; 114 | -------------------------------------------------------------------------------- /config/webpack/dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-5. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const webpack = require('webpack'); 8 | 9 | const baseConfig = require('./base'); 10 | 11 | const config = Object.assign({}, baseConfig, { 12 | entry: { 13 | // ...baseConfig.entry, 14 | chunk: baseConfig.entry.chunk, 15 | app: [ 16 | 'webpack-hot-middleware/client?path=/__webpack_hmr&reload=true&timeout=20000r' 17 | ].concat(baseConfig.entry.app) 18 | }, 19 | cache: true, 20 | // eval-source-map can not debug in chrome 21 | devtool: 'inline-source-map', 22 | plugins: [ 23 | new webpack.HotModuleReplacementPlugin(), 24 | new webpack.DefinePlugin({ 25 | 'process.env.NODE_ENV': '"dev"' 26 | }) 27 | ].concat(baseConfig.plugins) 28 | }); 29 | 30 | module.exports = config; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-antd-starter", 3 | "description": "A starter of react project using redux and ant design.", 4 | "author": "Disciple_D", 5 | "private": false, 6 | "version": "0.0.1", 7 | "main": "", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/DiscipleD/react-redux-antd-starter.git" 11 | }, 12 | "license": "MIT", 13 | "keywords": [], 14 | "devDependencies": { 15 | "babel-jest": "^15.0.0", 16 | "codecov": "^1.0.1", 17 | "jest": "^15.1.1", 18 | "react-addons-test-utils": "^15.3.1", 19 | "react-test-renderer": "^15.3.1", 20 | "redux-mock-store": "^1.2.0", 21 | "webpack-dev-middleware": "^1.6.1", 22 | "webpack-hot-middleware": "^2.12.2" 23 | }, 24 | "dependencies": { 25 | "antd": "^1.11.6", 26 | "autoprefixer": "^6.7.7", 27 | "babel-core": "^6.24.1", 28 | "babel-eslint": "^6.1.2", 29 | "babel-loader": "^6.4.1", 30 | "babel-plugin-import": "^1.1.1", 31 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 32 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 33 | "babel-polyfill": "^6.23.0", 34 | "babel-preset-es2015": "^6.24.1", 35 | "babel-preset-react": "^6.24.1", 36 | "babel-preset-stage-0": "^6.24.1", 37 | "clean-webpack-plugin": "^0.1.16", 38 | "css-loader": "^0.24.0", 39 | "eslint": "^3.19.0", 40 | "eslint-loader": "^1.7.1", 41 | "eslint-plugin-react": "^6.10.3", 42 | "express": "^4.15.2", 43 | "file-loader": "^0.9.0", 44 | "html-webpack-plugin": "^2.28.0", 45 | "minimist": "^1.2.0", 46 | "node-sass": "^4.5.2", 47 | "null-loader": "^0.1.1", 48 | "open": "0.0.5", 49 | "postcss": "^5.2.17", 50 | "postcss-loader": "^0.11.1", 51 | "react": "^15.5.4", 52 | "react-dom": "^15.5.4", 53 | "react-hot-loader": "^3.0.0-beta.6", 54 | "react-redux": "^4.4.8", 55 | "react-router": "^2.8.1", 56 | "react-router-redux": "^4.0.8", 57 | "redux": "^3.6.0", 58 | "redux-actions": "^0.12.0", 59 | "redux-logger": "^2.10.2", 60 | "redux-promise": "^0.5.3", 61 | "redux-thunk": "^2.2.0", 62 | "reselect": "^2.5.4", 63 | "sass-loader": "^6.0.3", 64 | "style-loader": "^0.13.2", 65 | "url-loader": "^0.5.8", 66 | "webpack": "^2.4.1" 67 | }, 68 | "scripts": { 69 | "lint": "eslint src", 70 | "build": "webpack --env=build", 71 | "start": "node server.js --env=dev", 72 | "start:build": "node server.js --env=build", 73 | "test": "npm run lint && jest --config config/jest/base.json --watch --onlyChanged --no-cache --env=test", 74 | "test:coverage": "npm run lint && jest --config config/jest/coverage.json", 75 | "codecov": "codecov" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-5. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const express = require('express'); 8 | const webpack = require('webpack'); 9 | const webpackDevMiddleware = require('webpack-dev-middleware'); 10 | const webpackHotMiddleware = require('webpack-hot-middleware'); 11 | const config = require('./webpack.config'); 12 | const open = require('open'); 13 | 14 | const PORT = parseInt(process.env.PORT || 8080); 15 | const app = new express(); 16 | 17 | app.use((req, res, next) => { 18 | req.url = req.url.replace(/^\/[^(.|_)]*$/, '/'); 19 | next(); 20 | }); 21 | 22 | const middlewareSetting = { 23 | publicPath: config.publicPath, 24 | buildPath: config.buildPath, 25 | stats: { 26 | colors: true, 27 | cached: false 28 | } 29 | }; 30 | 31 | const compiler = webpack(config); 32 | app.use(webpackDevMiddleware(compiler, middlewareSetting)); 33 | app.use(webpackHotMiddleware(compiler)); 34 | app.use(express.static(config.output.path)); 35 | 36 | app.listen(PORT, err => { 37 | err && console.log(err); 38 | console.log('Listening at localhost:' + PORT); 39 | console.log('Opening your system browser...'); 40 | open('http://localhost:' + PORT); 41 | }); 42 | -------------------------------------------------------------------------------- /src/assets/style/reboot.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #app{ 4 | height: 100%; 5 | 6 | * { 7 | box-sizing: border-box; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/HeaderNav/__tests__/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`HeaderNav Component match snapshot 1`] = ` 2 |
4 |
6 |
9 | Project Logo 10 |
11 | 55 |
56 |
57 | `; 58 | -------------------------------------------------------------------------------- /src/components/HeaderNav/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-18. 3 | */ 4 | 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import ReactTestUtils from 'react-addons-test-utils'; 8 | import renderer from 'react-test-renderer'; 9 | import HeaderNav from '../index'; 10 | 11 | describe('HeaderNav Component', () => { 12 | let headerNavNode; 13 | const menuSetting = [{ 14 | path: '/a', 15 | name: 'a', 16 | label: 'Page A' 17 | }, { 18 | path: '/b', 19 | name: 'b', 20 | label: 'Page B' 21 | }]; 22 | const selectedItem = menuSetting[1]; 23 | const goHomeFn = jest.fn(); 24 | 25 | beforeAll(() => { 26 | const headerNav = ReactTestUtils.renderIntoDocument( 27 | 32 | ); 33 | 34 | headerNavNode = ReactDOM.findDOMNode(headerNav); 35 | }); 36 | 37 | it('Init component nav link according to props', () => { 38 | expect(headerNavNode.querySelectorAll('li.ant-menu-item').length).toBe(menuSetting.length); 39 | 40 | expect(headerNavNode.querySelector('li.ant-menu-item.ant-menu-item-selected a').textContent).toBe(selectedItem.label); 41 | }); 42 | 43 | it('match snapshot', () => { 44 | const headerNavSnapshot = renderer.create( 45 | 50 | ).toJSON(); 51 | expect(headerNavSnapshot).toMatchSnapshot(); 52 | }); 53 | 54 | it('Logo click', () => { 55 | ReactTestUtils.Simulate.click(headerNavNode.querySelector('.header-nav-logo')); 56 | expect(goHomeFn).toBeCalled(); 57 | }); 58 | 59 | it('no params', () => { 60 | const headerNav = ReactTestUtils.renderIntoDocument( 61 | 62 | ); 63 | const headerNavNode = ReactDOM.findDOMNode(headerNav); 64 | 65 | expect(headerNavNode.querySelectorAll('li.ant-menu-item').length).toBe(0); 66 | expect(headerNavNode.querySelector('.header-nav-logo')).toBeDefined(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/components/HeaderNav/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-6. 3 | */ 4 | 5 | import React from 'react'; 6 | import { Link } from 'react-router'; 7 | import { Menu } from 'antd'; 8 | 9 | import './style.scss'; 10 | 11 | class HeaderNav extends React.Component { 12 | render() { 13 | const menuItem = (this.props.navList || []) 14 | .map(navItem => ( 15 | 16 | {navItem.label} 17 | 18 | )); 19 | return ( 20 |
21 |
22 |
Project Logo
23 | {menuItem} 29 |
30 |
31 | ) 32 | } 33 | } 34 | 35 | export default HeaderNav; 36 | -------------------------------------------------------------------------------- /src/components/HeaderNav/style.scss: -------------------------------------------------------------------------------- 1 | .header-nav { 2 | .header-nav-wrapper { 3 | padding: 0 50px; 4 | background: #404040; 5 | height: 65px; 6 | } 7 | 8 | .header-nav-logo { 9 | padding: 2px 6px; 10 | font-size: 18px; 11 | color: #fff; 12 | width: 120px; 13 | height: 32px; 14 | background: #333; 15 | border-radius: 6px; 16 | margin: 16px 28px 16px 0; 17 | float: left; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/containers/Home/__tests__/Home.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-20. 3 | */ 4 | import React from 'react'; 5 | import renderer from 'react-test-renderer'; 6 | import configureMockStore from 'redux-mock-store'; 7 | 8 | import middlewares from '../../../redux/middleware'; 9 | import Home from '../index'; 10 | 11 | describe('Home Container', () => { 12 | const mockStore = configureMockStore(middlewares); 13 | 14 | it('match snapshot', () => { 15 | // Because of container only includes components 16 | // so, it's enough to test snapshot only. 17 | const store = mockStore({ 18 | menu: {}, 19 | routing: { 20 | locationBeforeTransitions: { 21 | pathname: '/' 22 | } 23 | } 24 | }); 25 | const appSnapshot = renderer.create( 26 | 27 | ).toJSON(); 28 | expect(appSnapshot).toMatchSnapshot(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/containers/Home/__tests__/__snapshots__/Home.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`Home Container match snapshot 1`] = ` 2 |
3 |
5 |
7 |
10 | Project Logo 11 |
12 |
    23 |
24 |
25 |
26 | `; 27 | -------------------------------------------------------------------------------- /src/containers/Home/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-6. 3 | */ 4 | 5 | import React from 'react'; 6 | import {push} from 'react-router-redux'; 7 | import {connect} from 'react-redux'; 8 | import {createSelector} from 'reselect'; 9 | 10 | import HeaderNav from 'components/HeaderNav'; 11 | import menuActions from 'actions/menu'; 12 | 13 | @connect( 14 | state => ({ 15 | ...state.menu, 16 | currentPath: createSelector( 17 | state => state.menu.list, 18 | state => state.routing.locationBeforeTransitions.pathname, 19 | (list = [], pathname) => list.length === 0 ? '' : list.filter(item => item.path === pathname).length > 0 ? pathname : list[0].path 20 | )(state) 21 | }), { 22 | goHome: () => push('/'), 23 | queryMenuList: menuActions.queryMenuList 24 | }) 25 | class Home extends React.Component { 26 | componentWillMount() { 27 | this.props.queryMenuList(); 28 | } 29 | 30 | render() { 31 | const menuList = this.props.list; 32 | return ( 33 |
34 | 39 | {this.props.children} 40 |
41 | ) 42 | } 43 | } 44 | 45 | export default Home; 46 | -------------------------------------------------------------------------------- /src/containers/PageA/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-6. 3 | */ 4 | 5 | import React from 'react'; 6 | import { connect } from 'react-redux'; 7 | import { Button } from 'antd'; 8 | 9 | import actions from 'actions'; 10 | 11 | @connect( 12 | state => state.pageA, 13 | actions 14 | ) 15 | export default class PageA extends React.Component { 16 | render() { 17 | const style = { 18 | margin: '24px 0 0', 19 | position: 'relative', 20 | paddingTop: '24px' 21 | }; 22 | return ( 23 |
24 |

Page A

25 |

count: {this.props.counter}

26 | 30 |
31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/containers/PageB/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-6. 3 | */ 4 | 5 | import React from 'react'; 6 | import { connect } from 'react-redux'; 7 | import { Button } from 'antd'; 8 | 9 | import actions from 'actions'; 10 | 11 | @connect( 12 | state => state.pageB, 13 | actions 14 | ) 15 | export default class PageB extends React.Component { 16 | render() { 17 | const style = { 18 | margin: '24px 0 0', 19 | position: 'relative', 20 | paddingTop: '24px' 21 | }; 22 | return ( 23 |
24 |

Page B

25 |

count: {this.props.counter}

26 | 30 |
31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/containers/app/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-7. 3 | */ 4 | 5 | import React, { PropTypes } from 'react'; 6 | import { Provider } from 'react-redux'; 7 | import { Router } from 'react-router'; 8 | 9 | import routers from '../../routers'; 10 | 11 | class App extends React.Component { 12 | render() { 13 | const { store, history } = this.props; 14 | return ( 15 | 16 | 17 | 18 | ) 19 | } 20 | } 21 | 22 | App.propTypes = { 23 | store: PropTypes.object.isRequired, 24 | history: PropTypes.object.isRequired 25 | }; 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Redux Ant.Design 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-5. 3 | */ 4 | 5 | import React from 'react'; 6 | import { render } from 'react-dom'; 7 | import { browserHistory } from 'react-router'; 8 | import { syncHistoryWithStore } from 'react-router-redux'; 9 | 10 | import configureStore from './redux/stores'; 11 | import './assets/style/reboot.scss'; 12 | import App from 'containers/App'; 13 | 14 | const store = configureStore(); 15 | const history = syncHistoryWithStore(browserHistory, store); 16 | 17 | render( 18 | , 19 | document.getElementById('app') 20 | ); 21 | -------------------------------------------------------------------------------- /src/redux/actions/__tests__/actions.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-19. 3 | */ 4 | 5 | import configureMockStore from 'redux-mock-store'; 6 | 7 | import middlewares from '../../middleware'; 8 | import actions from '../index'; 9 | import menuActions, {QUERY_MENU_LIST} from '../menu'; 10 | import * as actionTypes from '../action_types'; 11 | 12 | describe('test actions', () => { 13 | const mockStore = configureMockStore(middlewares); 14 | 15 | it('Action: asyncIncrement()', () => { 16 | const store = mockStore({}); 17 | const action = [{ 18 | type: actionTypes.INCREMENT, 19 | payload: 1 20 | }]; 21 | store.dispatch(actions.asyncIncrement()); 22 | expect(store.getActions()).toEqual(action); 23 | }); 24 | 25 | it('Action: asyncIncrement(number)', () => { 26 | const store = mockStore({}); 27 | const number = 10; 28 | const action = [{ 29 | type: actionTypes.INCREMENT, 30 | payload: number 31 | }]; 32 | store.dispatch(actions.asyncIncrement(number)); 33 | expect(store.getActions()).toEqual(action); 34 | }); 35 | 36 | it('Action: promiseIncrement()', () => { 37 | const store = mockStore({}); 38 | const action = [{ 39 | type: actionTypes.PROMISE_INCREMENT, 40 | payload: 1 41 | }]; 42 | return store.dispatch(actions.promiseIncrement()) 43 | .then(() => expect(store.getActions()).toEqual(action)); 44 | }); 45 | 46 | it('Action: promiseIncrement(number)', () => { 47 | const store = mockStore({}); 48 | const number = 10; 49 | const action = [{ 50 | type: actionTypes.PROMISE_INCREMENT, 51 | payload: number 52 | }]; 53 | return store.dispatch(actions.promiseIncrement(number)) 54 | .then(() => expect(store.getActions()).toEqual(action)); 55 | }); 56 | 57 | it('Action: decrement', () => { 58 | const action = { 59 | type: actionTypes.DECREMENT 60 | }; 61 | expect(actions.decrement()).toEqual(action); 62 | }); 63 | 64 | it('Action: queryMenuList()', () => { 65 | const store = mockStore({}); 66 | const action = [{ 67 | type: QUERY_MENU_LIST, 68 | payload: [{ 69 | path: '/a', 70 | name: 'a', 71 | label: 'Page A' 72 | }, { 73 | path: '/b', 74 | name: 'b', 75 | label: 'Page B' 76 | }] 77 | }]; 78 | return store.dispatch(menuActions.queryMenuList()) 79 | .then(() => expect(store.getActions()).toEqual(action)); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/redux/actions/action_types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-8. 3 | */ 4 | 5 | const DECREMENT = 'DECREMENT'; 6 | const INCREMENT = 'INCREMENT'; 7 | const PROMISE_INCREMENT = 'PROMISE_INCREMENT'; 8 | 9 | export {DECREMENT, INCREMENT, PROMISE_INCREMENT}; 10 | -------------------------------------------------------------------------------- /src/redux/actions/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-8. 3 | */ 4 | 5 | import { createActions } from 'redux-actions'; 6 | 7 | import * as ACTION_TYPES from './action_types'; 8 | 9 | const { 10 | promiseIncrement, 11 | increment, 12 | decrement 13 | } = createActions({ 14 | [ACTION_TYPES.PROMISE_INCREMENT]: (increment = 1) => Promise.resolve(increment) 15 | }, 16 | ACTION_TYPES.INCREMENT, 17 | ACTION_TYPES.DECREMENT); 18 | 19 | const asyncIncrement = (value = 1) => dispatch => dispatch(increment(value)); 20 | 21 | export default {asyncIncrement, promiseIncrement, decrement}; 22 | -------------------------------------------------------------------------------- /src/redux/actions/menu/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-19. 3 | */ 4 | 5 | import { createActions } from 'redux-actions'; 6 | 7 | export const QUERY_MENU_LIST = 'QUERY_MENU_LIST'; 8 | 9 | const menuSetting = [{ 10 | path: '/a', 11 | name: 'a', 12 | label: 'Page A' 13 | }, { 14 | path: '/b', 15 | name: 'b', 16 | label: 'Page B' 17 | }]; 18 | 19 | const { queryMenuList } = createActions({ 20 | [QUERY_MENU_LIST]: () => Promise.resolve(menuSetting) 21 | }); 22 | 23 | export default {queryMenuList}; 24 | 25 | -------------------------------------------------------------------------------- /src/redux/middleware/__tests__/middleware.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-19. 3 | */ 4 | 5 | import productionMiddlewareList from '../index'; 6 | 7 | describe('middleware', () => { 8 | it('middleware list length should be 3 except env = production', () => { 9 | expect(productionMiddlewareList.length).toBe(3); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/redux/middleware/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-8. 3 | */ 4 | 5 | import { browserHistory } from 'react-router' 6 | import { routerMiddleware } from 'react-router-redux' 7 | import promiseMiddleware from 'redux-promise' 8 | import createLogger from 'redux-logger' 9 | import thunkMiddleware from 'redux-thunk' 10 | 11 | const middleware = [routerMiddleware(browserHistory), thunkMiddleware, promiseMiddleware]; 12 | process.env.NODE_ENV === 'dev' && middleware.push(createLogger()); 13 | 14 | export default middleware; 15 | -------------------------------------------------------------------------------- /src/redux/reducers/__tests__/menu.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-20. 3 | */ 4 | 5 | import reducer from '../menu'; 6 | import {QUERY_MENU_LIST} from '../../actions/menu'; 7 | 8 | describe('Menu reducer', () => { 9 | it('Init State', () => { 10 | const state = {}; 11 | expect(reducer(undefined, {type: ''})).toEqual(state); 12 | }); 13 | 14 | it('Action type: QUERY_MENU_LIST', () => { 15 | const state = {}; 16 | const list = [{ 17 | path: '/a', 18 | name: 'a', 19 | label: 'Page A' 20 | }, { 21 | path: '/b', 22 | name: 'b', 23 | label: 'Page B' 24 | }]; 25 | const action = { 26 | type: QUERY_MENU_LIST, 27 | payload: list 28 | }; 29 | const result = { 30 | list 31 | }; 32 | expect(reducer(state, action)).toEqual(result); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/redux/reducers/__tests__/pageA.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-19. 3 | */ 4 | 5 | import reducer from '../pageA'; 6 | import * as actionTypes from '../../actions/action_types'; 7 | 8 | describe('PageA reducer', () => { 9 | it('Init State', () => { 10 | const state = { 11 | counter: 0 12 | }; 13 | expect(reducer(undefined, {type: ''})).toEqual(state); 14 | }); 15 | 16 | it('Action type: INCREMENT', () => { 17 | const number = 20; 18 | const state = { 19 | counter: 0 20 | }; 21 | const action = { 22 | type: actionTypes.INCREMENT, 23 | payload: number 24 | }; 25 | const result = { 26 | counter: number 27 | }; 28 | expect(reducer(state, action)).toEqual(result); 29 | }); 30 | 31 | it('Action type: DECREMENT', () => { 32 | const number = 20; 33 | const state = { 34 | counter: 30 35 | }; 36 | const action = { 37 | type: actionTypes.DECREMENT, 38 | payload: number 39 | }; 40 | const result = { 41 | counter: 10 42 | }; 43 | expect(reducer(state, action)).toEqual(result); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/redux/reducers/__tests__/pageB.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-19. 3 | */ 4 | 5 | import reducer from '../pageB'; 6 | import * as actionTypes from '../../actions/action_types'; 7 | 8 | describe('PageB reducer', () => { 9 | it('Init State', () => { 10 | const state = { 11 | counter: 0 12 | }; 13 | expect(reducer(undefined, {type: ''})).toEqual(state); 14 | }); 15 | 16 | it('Action type: PROMISE_INCREMENT', () => { 17 | const number = 20; 18 | const state = { 19 | counter: 0 20 | }; 21 | const action = { 22 | type: actionTypes.PROMISE_INCREMENT, 23 | payload: number 24 | }; 25 | const result = { 26 | counter: number 27 | }; 28 | expect(reducer(state, action)).toEqual(result); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-5. 3 | */ 4 | 5 | import { combineReducers } from 'redux'; 6 | import { routerReducer } from 'react-router-redux'; 7 | 8 | import menu from './menu'; 9 | import pageA from './pageA'; 10 | import pageB from './pageB'; 11 | 12 | const reducers = combineReducers({ 13 | menu, 14 | pageA, 15 | pageB, 16 | routing: routerReducer 17 | }); 18 | 19 | export default reducers; 20 | -------------------------------------------------------------------------------- /src/redux/reducers/menu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-19. 3 | */ 4 | 5 | import { handleActions } from 'redux-actions'; 6 | 7 | import {QUERY_MENU_LIST} from 'actions/menu'; 8 | 9 | const reducer = handleActions({ 10 | [QUERY_MENU_LIST]: (state, action) => ({ 11 | list: action.payload 12 | }) 13 | }, {}); 14 | 15 | export default reducer; 16 | -------------------------------------------------------------------------------- /src/redux/reducers/pageA.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-8. 3 | */ 4 | 5 | import { handleActions } from 'redux-actions'; 6 | 7 | import * as ACTION_TYPES from '../actions/action_types'; 8 | 9 | const reducer = handleActions({ 10 | [ACTION_TYPES.INCREMENT]: (state, action) => ({ 11 | counter: state.counter + action.payload 12 | }), 13 | 14 | [ACTION_TYPES.DECREMENT]: (state, action) => ({ 15 | counter: state.counter - action.payload 16 | }) 17 | }, { counter: 0 }); 18 | 19 | export default reducer; 20 | -------------------------------------------------------------------------------- /src/redux/reducers/pageB.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-8. 3 | */ 4 | 5 | import { handleActions } from 'redux-actions'; 6 | 7 | import * as ACTION_TYPES from '../actions/action_types'; 8 | 9 | const reducer = handleActions({ 10 | [ACTION_TYPES.PROMISE_INCREMENT]: (state, action) => ({ 11 | counter: state.counter + action.payload 12 | }) 13 | }, { counter: 0 }); 14 | 15 | export default reducer; 16 | -------------------------------------------------------------------------------- /src/redux/stores/__tests__/stores.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-19. 3 | */ 4 | 5 | import configStore from '../index'; 6 | 7 | describe('config store', () => { 8 | it('create store without init state', () => { 9 | const store = configStore(); 10 | const state = store.getState(); 11 | expect(state.pageA.counter).toBe(0); 12 | }); 13 | 14 | it('create store with init state', () => { 15 | const initState = { 16 | pageA: { 17 | counter: 1 18 | } 19 | }; 20 | const store = configStore(initState); 21 | const state = store.getState(); 22 | expect(state.pageA.counter).toBe(1); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/redux/stores/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-5. 3 | */ 4 | 5 | import { createStore, applyMiddleware } from 'redux' 6 | 7 | import reducers from '../reducers' 8 | import middleware from '../middleware' 9 | 10 | export default initialState => { 11 | const store = createStore( 12 | reducers, 13 | initialState, 14 | applyMiddleware(...middleware) 15 | ) 16 | 17 | return store 18 | } 19 | -------------------------------------------------------------------------------- /src/routers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-6. 3 | */ 4 | 5 | import React from 'react'; 6 | import { Route, Redirect, IndexRoute } from 'react-router'; 7 | 8 | import Home from 'containers/Home'; 9 | import PageA from 'containers/PageA'; 10 | import PageB from 'containers/PageB'; 11 | 12 | export default () => ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | -------------------------------------------------------------------------------- /test/mock/fileMock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-18. 3 | */ 4 | 5 | export default ''; 6 | -------------------------------------------------------------------------------- /test/mock/styleMock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-18. 3 | */ 4 | 5 | export default {}; 6 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jack on 16-9-5. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const path = require('path'); 8 | const args = require('minimist')(process.argv.slice(2)); 9 | 10 | // List of allowed environments 11 | const allowedEnvs = ['dev', 'build']; 12 | 13 | // Set the correct environment 14 | let env; 15 | if (args.env) { 16 | env = args.env; 17 | } else { 18 | env = 'dev'; 19 | } 20 | process.env.REACT_WEBPACK_ENV = env; 21 | 22 | /** 23 | * Build the webpack configuration 24 | * @param {String} wantedEnv The wanted environment 25 | * @return {Object} Webpack config 26 | */ 27 | function buildConfig(wantedEnv) { 28 | const isValid = wantedEnv && wantedEnv.length > 0 && allowedEnvs.indexOf(wantedEnv) !== -1; 29 | const validEnv = isValid ? wantedEnv : 'dev'; 30 | const config = require(path.join(__dirname, 'config/webpack/' + validEnv)); 31 | return config; 32 | } 33 | 34 | module.exports = buildConfig(env); 35 | --------------------------------------------------------------------------------