diff --git a/.gitignore b/.gitignore index e29155f..195849c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .idea .DS_Store tmp -spm_modules node_modules examples/**/dist boilerplate/dist +*.log diff --git a/.npmignore b/.npmignore index 8869ccb..fb23fce 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,5 @@ .idea .DS_Store tmp -spm_modules node_modules examples diff --git a/README.md b/README.md index 9fccc5b..95cf4b6 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,9 @@ Generate boilerplate. ```bash $ mkdir foo && cd foo $ antd-init +// or +$ antd-init --type plain-react +$ antd-init --type redux ``` Start development server. diff --git a/bin/antd-init b/bin/antd-init index 792231d..941b4cb 100755 --- a/bin/antd-init +++ b/bin/antd-init @@ -4,6 +4,7 @@ var vfs = require('vinyl-fs'); var fs = require('fs'); var through = require('through2'); var path = require('path'); +var inquirer = require('inquirer'); var join = path.join; var basename = path.basename; @@ -13,17 +14,43 @@ if (process.argv.length === 3 && return; } -var cwd = join(__dirname, '../boilerplate'); -var dest = process.cwd(); +if (process.argv.length === 4 && process.argv[2] === '--type') { + init(process.argv[3]); + return; +} -vfs.src('**/*', {cwd: cwd, cwdbase: true, dot: true}) - .pipe(template(dest)) - .pipe(vfs.dest(dest)) - .on('end', function() { - fs.renameSync(path.join(dest,'gitignore'),path.join(dest,'.gitignore')); - require('../lib/install'); +inquirer.prompt({ + name: 'type', + type: 'list', + message: 'Please select boilerplate type', + choices: [ + { + name: 'plain react - for simple project', + value: 'plain-react', + }, + { + name: 'redux - for complex project', + value: 'redux', + }, + ], }) - .resume(); + .then(function(answers) { + init(answers.type); + }); + +function init(type) { + var cwd = join(__dirname, '../boilerplates', type); + var dest = process.cwd(); + + vfs.src('**/*', {cwd: cwd, cwdbase: true, dot: true}) + .pipe(template(dest)) + .pipe(vfs.dest(dest)) + .on('end', function() { + fs.renameSync(path.join(dest,'gitignore'),path.join(dest,'.gitignore')); + require('../lib/install'); + }) + .resume(); +} function template(dest) { return through.obj(function (file, enc, cb) { diff --git a/boilerplate/.editorconfig b/boilerplates/plain-react/.editorconfig similarity index 100% rename from boilerplate/.editorconfig rename to boilerplates/plain-react/.editorconfig diff --git a/boilerplate/.eslintrc b/boilerplates/plain-react/.eslintrc similarity index 100% rename from boilerplate/.eslintrc rename to boilerplates/plain-react/.eslintrc diff --git a/boilerplate/README.md b/boilerplates/plain-react/README.md similarity index 100% rename from boilerplate/README.md rename to boilerplates/plain-react/README.md diff --git a/boilerplate/gitignore b/boilerplates/plain-react/gitignore similarity index 100% rename from boilerplate/gitignore rename to boilerplates/plain-react/gitignore diff --git a/boilerplate/index.html b/boilerplates/plain-react/index.html similarity index 100% rename from boilerplate/index.html rename to boilerplates/plain-react/index.html diff --git a/boilerplate/package.json b/boilerplates/plain-react/package.json similarity index 100% rename from boilerplate/package.json rename to boilerplates/plain-react/package.json diff --git a/boilerplate/proxy.config.js b/boilerplates/plain-react/proxy.config.js similarity index 100% rename from boilerplate/proxy.config.js rename to boilerplates/plain-react/proxy.config.js diff --git a/boilerplate/src/common/lib.js b/boilerplates/plain-react/src/common/lib.js similarity index 100% rename from boilerplate/src/common/lib.js rename to boilerplates/plain-react/src/common/lib.js diff --git a/boilerplate/src/component/App.jsx b/boilerplates/plain-react/src/component/App.jsx similarity index 100% rename from boilerplate/src/component/App.jsx rename to boilerplates/plain-react/src/component/App.jsx diff --git a/boilerplate/src/component/App.less b/boilerplates/plain-react/src/component/App.less similarity index 100% rename from boilerplate/src/component/App.less rename to boilerplates/plain-react/src/component/App.less diff --git a/boilerplate/src/entry/index.jsx b/boilerplates/plain-react/src/entry/index.jsx similarity index 100% rename from boilerplate/src/entry/index.jsx rename to boilerplates/plain-react/src/entry/index.jsx diff --git a/boilerplate/webpack.config.js b/boilerplates/plain-react/webpack.config.js similarity index 100% rename from boilerplate/webpack.config.js rename to boilerplates/plain-react/webpack.config.js diff --git a/boilerplates/redux/.eslintrc b/boilerplates/redux/.eslintrc new file mode 100644 index 0000000..4f1e575 --- /dev/null +++ b/boilerplates/redux/.eslintrc @@ -0,0 +1,20 @@ +{ + "parser": "babel-eslint", + "extends": "eslint-config-airbnb", + "rules": { + "spaced-comment": [0], + "no-unused-vars": [0], + "no-empty": [0], + "react/wrap-multilines": [0], + "react/no-multi-comp": [0], + "no-constant-condition": [0], + "react/jsx-no-bind": [0], + "react/prop-types": [0], + "arrow-body-style": [0], + "react/prefer-stateless-function": [0], + "semi": [0] + }, + "ecmaFeatures": { + "experimentalObjectRestSpread": true + } +} diff --git a/boilerplates/redux/README.md b/boilerplates/redux/README.md new file mode 100644 index 0000000..9c74844 --- /dev/null +++ b/boilerplates/redux/README.md @@ -0,0 +1,103 @@ +# react-redux-boilerplate + +A boilerplate with react, redux, redux-saga, react-router, webpack, babel, css-modules ... + +## 环境准备 + +先安装依赖 + +```bash +$ npm install +``` + +想要更好的开发体验,还需安装两个 Chrome 插件:[Redux DevTools](https://chrome.google.com/webstore/detail/lmhkpmbekcpmknklioeibfkpmmfibljd) 和 [LiveReload](https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei) 。 + +## 启动调试 + +```bash +$ npm start +$ open http://localhost:8989/ +``` + +## 构建代码 + +```bash +$ npm run build + +// 构建但不压缩 +$ npm run build -- --no-compress +``` + +## 目录结构 + +``` +. +├── /dist/ # 构建输出的文件会在这里 +├── /node_modules/ # 第三方类库和工具 +├── /src/ # 应用源码 +│ ├── /components/ # React components +│ ├── /entries/ # 应用入口 +│ ├── /layouts/ # 布局信息 +│ ├── /reducers/ # reducers +│ ├── /routes/ # 路由信息 +│ ├── /sagas/ # redux-sagas +│ └── /services/ # 处理和服务器的交互 +├── proxy.config.js # 配置 dora-plugin-proxy,用于 mock 和在线调试 +├── webpack.config.js # 扩展 webpack 配置 +└── package.json # 配置入口文件、依赖和 scripts +``` + +## 系统组织 + +![](https://camo.githubusercontent.com/068c4ff126977b861cff3338428bdde6927f7dad/68747470733a2f2f6f732e616c697061796f626a656374732e636f6d2f726d73706f7274616c2f43684d775a42755a6c614c725377652e706e67) + +详见:[React + Redux 最佳实践](https://github.com/sorrycc/blog/issues/1) + +## 内置类库 + +- [react](https://github.com/facebook/react) +- [redux](https://github.com/reactjs/redux) +- [redux-saga](https://github.com/yelouafi/redux-saga) +- [redux-actions](https://github.com/acdlite/redux-actions) +- [react-router](https://github.com/reactjs/react-router) +- [classnames](https://github.com/JedWatson/classnames) +- [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch) +- [react-router](https://github.com/reactjs/react-router) +- [react-router-redux](https://github.com/reactjs/react-router-redux) + +## 工具特性 + +热替换和 LiveReload + +> 基于 [Webpack Vanilla HMR](https://webpack.github.io/docs/hot-module-replacement-with-webpack.html),支持 `components`, `reducers`, `routers`, `sagas`, `layouts` 目录的模块热替换,其余目录的修改则会自动刷新页面。 + +> CSS 的自动刷新需通过 [LiveReload](https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei) Chrome 插件配合使用。 + +> - [Why Vanilla HMR](https://github.com/reactjs/redux/pull/1455) + +支持 css-modules + +> 所有 less 文件会被解析为 css-modules + +运行错误和语法错误的提醒 + +> 通过 [redbox-react](https://github.com/KeywordBrain/redbox-react) 和 webpack hmr overlay 提示运行错误和语法错误 + +自动引入 `reducer` 和 `saga` + +> 通过 webpack 的 `require.context` 黑魔法批量引入 `reducer` 和 `saga`,新增、删除和重命名时会更方便 + +自动安装 npm 依赖 + +> ![](https://camo.githubusercontent.com/898e02d6539900efe65fadbfd15e2a1d7ce4dccf/68747470733a2f2f6f732e616c697061796f626a656374732e636f6d2f726d73706f7274616c2f4b6541474f776a70746a6152684d6d2e676966) + +数据 mock 和线上调试 + +> 通过 dora-plugin-proxy 实现,详见:https://github.com/dora-js/dora-plugin-proxy#规则定义 + +... + +## License + +MIT + diff --git a/boilerplates/redux/gitignore b/boilerplates/redux/gitignore new file mode 100644 index 0000000..a7cf257 --- /dev/null +++ b/boilerplates/redux/gitignore @@ -0,0 +1,4 @@ +dist +node_modules +.DS_Store +npm-debug.log diff --git a/boilerplates/redux/package.json b/boilerplates/redux/package.json new file mode 100644 index 0000000..01092d7 --- /dev/null +++ b/boilerplates/redux/package.json @@ -0,0 +1,51 @@ +{ + "private": true, + "entry": {}, + "dependencies": { + "antd": "^1.0.1", + "atool-build": "^0.7.1", + "babel-plugin-antd": "^0.4.0", + "babel-plugin-transform-runtime": "^6.8.0", + "babel-runtime": "^6.6.1", + "classnames": "^2.2.3", + "history": "^2.0.1", + "isomorphic-fetch": "^2.2.1", + "js-cookie": "^2.1.1", + "react": "^15.0.2", + "react-dom": "^15.0.2", + "react-redux": "4.4.x", + "react-router": "^2.0.1", + "react-router-redux": "^4.0.1", + "redux": "^3.5.2", + "redux-actions": "0.9.x", + "redux-saga": "^0.10.4" + }, + "devDependencies": { + "atool-test-mocha": "^0.1.4", + "babel-eslint": "^6.0.2", + "dora": "0.3.x", + "dora-plugin-browser-history": "^0.1.1", + "dora-plugin-livereload": "^0.4.0", + "dora-plugin-proxy": "^0.6.1", + "dora-plugin-webpack": "0.6.x", + "dora-plugin-webpack-hmr": "^0.1.0", + "eslint": "^2.10.1", + "eslint-config-airbnb": "^9.0.1", + "eslint-plugin-import": "^1.8.0", + "eslint-plugin-jsx-a11y": "^1.2.0", + "eslint-plugin-react": "^5.1.1", + "expect": "^1.20.1", + "glob": "^7.0.3", + "pre-commit": "^1.1.3", + "redbox-react": "^1.2.2" + }, + "pre-commit": [ + "lint" + ], + "scripts": { + "start": "dora --plugins 'proxy,webpack,webpack-hmr,livereload?enableJs=false&injectHost=localhost,browser-history?index=/src/entries/index.html'", + "build": "atool-build", + "lint": "eslint --ext .js,.jsx src/", + "test": "atool-test-mocha ./src/**/__tests__/*-test.js" + } +} \ No newline at end of file diff --git a/boilerplates/redux/proxy.config.js b/boilerplates/redux/proxy.config.js new file mode 100644 index 0000000..1762116 --- /dev/null +++ b/boilerplates/redux/proxy.config.js @@ -0,0 +1,27 @@ +// Learn more on how to config. +// - https://github.com/dora-js/dora-plugin-proxy#规则定义 + +module.exports = { + '/api/todos': function(req, res) { + setTimeout(function() { + res.json({ + success: true, + data: [ + { + id: 1, + text: 'Learn antd', + isComplete: true, + }, + { + id: 2, + text: 'Learn ant-tool', + }, + { + id: 3, + text: 'Learn dora', + }, + ], + }); + }, 500); + }, +}; diff --git a/boilerplates/redux/src/components/App.jsx b/boilerplates/redux/src/components/App.jsx new file mode 100644 index 0000000..d6f9f73 --- /dev/null +++ b/boilerplates/redux/src/components/App.jsx @@ -0,0 +1,17 @@ +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; +import Todos from './Todos/Todos'; +import MainLayout from '../layouts/MainLayout/MainLayout'; + +const App = ({ location }) => { + return ( + + + + ); +}; + +App.propTypes = { +}; + +export default connect()(App); diff --git a/boilerplates/redux/src/components/NotFound.jsx b/boilerplates/redux/src/components/NotFound.jsx new file mode 100644 index 0000000..a17c355 --- /dev/null +++ b/boilerplates/redux/src/components/NotFound.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Button } from 'antd'; +import styles from './NotFound.less'; + +const NotFound = () => { + return ( +
+
+

404

+

未找到该页面

+ +
+
+ ); +}; + +export default NotFound; diff --git a/boilerplates/redux/src/components/NotFound.less b/boilerplates/redux/src/components/NotFound.less new file mode 100644 index 0000000..fd8d23b --- /dev/null +++ b/boilerplates/redux/src/components/NotFound.less @@ -0,0 +1,25 @@ +.normal { + width: 100%; + height: 100%; + min-height: 100vh; + padding-top: 120px; +} + +.container { + padding: 0; + margin: 0 auto; + width: 620px; + height: 300px; + text-align: center; +} + +.title { + font-size: 80px; + color: #666; + margin-top: 20px; + margin-bottom: 10px; +} + +.desc { + font-size: 14px; +} diff --git a/boilerplates/redux/src/components/Todos/Todo.jsx b/boilerplates/redux/src/components/Todos/Todo.jsx new file mode 100644 index 0000000..41a02bd --- /dev/null +++ b/boilerplates/redux/src/components/Todos/Todo.jsx @@ -0,0 +1,35 @@ +import React, { Component, PropTypes } from 'react'; +import classnames from 'classnames'; +import styles from './Todo.less'; + +const Todo = ({ data, onToggleComplete }) => { + const { text, isComplete } = data; + const todoCls = classnames({ + [styles.normal]: true, + [styles.isComplete]: isComplete, + }); + + return ( +
+
+ +
+
+ {text} +
+
+ ); +}; + +Todo.propTypes = { + data: PropTypes.object.isRequired, + onToggleComplete: PropTypes.func.isRequired, +}; + +export default Todo; + diff --git a/boilerplates/redux/src/components/Todos/Todo.less b/boilerplates/redux/src/components/Todos/Todo.less new file mode 100644 index 0000000..b527fbc --- /dev/null +++ b/boilerplates/redux/src/components/Todos/Todo.less @@ -0,0 +1,10 @@ +.normal { + display: flex; +} +.checkbox { + margin-right: 6px; +} +.isComplete { + color: #ccc; + text-decoration: line-through; +} diff --git a/boilerplates/redux/src/components/Todos/Todos.jsx b/boilerplates/redux/src/components/Todos/Todos.jsx new file mode 100644 index 0000000..e68d917 --- /dev/null +++ b/boilerplates/redux/src/components/Todos/Todos.jsx @@ -0,0 +1,61 @@ +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { Spin } from 'antd'; +import Todo from './Todo'; +import styles from './Todos.less'; + +const Todos = ({ todos, dispatch }) => { + const handleToggleComplete = (id) => { + dispatch({ + type: 'todos/toggleComplete', + payload: id, + }); + }; + + const renderList = () => { + const { list, loading } = todos; + if (loading) { + return ; + } + + return ( +
+ {list.map(item => + )} +
+ ); + }; + + return ( +
+ {renderList()} +
+ ); +}; + +Todos.propTypes = {}; + +function filter(todos, pathname) { + const newList = todos.list.filter(todo => { + if (pathname === '/actived') { + return !todo.isComplete; + } + if (pathname === '/completed') { + return todo.isComplete; + } + return true; + }); + return { ...todos, list: newList }; +} + +function mapStateToProps({ todos }, { location }) { + return { + todos: filter(todos, location.pathname), + }; +} + +export default connect(mapStateToProps)(Todos); diff --git a/boilerplates/redux/src/components/Todos/Todos.less b/boilerplates/redux/src/components/Todos/Todos.less new file mode 100644 index 0000000..8af4569 --- /dev/null +++ b/boilerplates/redux/src/components/Todos/Todos.less @@ -0,0 +1,3 @@ +.normal { + +} diff --git a/boilerplates/redux/src/entries/index.html b/boilerplates/redux/src/entries/index.html new file mode 100644 index 0000000..f65f403 --- /dev/null +++ b/boilerplates/redux/src/entries/index.html @@ -0,0 +1,16 @@ + + + + + Demo + + + + +
+ + + + + + diff --git a/boilerplates/redux/src/entries/index.js b/boilerplates/redux/src/entries/index.js new file mode 100644 index 0000000..4e2da00 --- /dev/null +++ b/boilerplates/redux/src/entries/index.js @@ -0,0 +1,72 @@ +import './index.html'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; +import createSagaMiddleware from 'redux-saga'; +import { browserHistory } from 'react-router'; +import { syncHistoryWithStore, routerReducer as routing } from 'react-router-redux'; +import reducers from '../reducers/index'; +import SagaManager from '../sagas/SagaManager'; +import './index.less'; + +////////////////////// +// Store + +const sagaMiddleware = createSagaMiddleware(); +const initialState = {}; +const enhancer = compose( + applyMiddleware(sagaMiddleware), + window.devToolsExtension ? window.devToolsExtension() : f => f +); +const store = createStore(combineReducers({ + ...reducers, routing, +}), initialState, enhancer); +SagaManager.startSagas(sagaMiddleware); + +if (module.hot) { + module.hot.accept('../reducers', () => { + const reducers = require('../reducers'); + const combinedReducers = combineReducers({ ...reducers, routing }); + store.replaceReducer(combinedReducers); + }); + module.hot.accept('../sagas/SagaManager', () => { + SagaManager.cancelSagas(store); + require('../sagas/SagaManager').default.startSagas(sagaMiddleware); + }); +} + +////////////////////// +// Render + +const history = syncHistoryWithStore(browserHistory, store); + +let render = () => { + const Routes = require('../routes/index'); + ReactDOM.render( + + + + , document.getElementById('root')); +}; + +if (module.hot) { + const renderNormally = render; + const renderException = (error) => { + const RedBox = require('redbox-react'); + ReactDOM.render(, document.getElementById('root')); + }; + render = () => { + try { + renderNormally(); + } catch (error) { + console.error('error', error); + renderException(error); + } + }; + module.hot.accept('../routes/index', () => { + render(); + }); +} + +render(); diff --git a/boilerplates/redux/src/entries/index.less b/boilerplates/redux/src/entries/index.less new file mode 100644 index 0000000..2e6520a --- /dev/null +++ b/boilerplates/redux/src/entries/index.less @@ -0,0 +1,6 @@ + +:global { + html, body, #root { + height: 100%; + } +} diff --git a/boilerplates/redux/src/layouts/MainLayout/MainLayout.jsx b/boilerplates/redux/src/layouts/MainLayout/MainLayout.jsx new file mode 100644 index 0000000..9a36cfa --- /dev/null +++ b/boilerplates/redux/src/layouts/MainLayout/MainLayout.jsx @@ -0,0 +1,34 @@ +import React, { Component, PropTypes } from 'react'; +import { Router, Route, IndexRoute, Link } from 'react-router'; +import styles from './MainLayout.less'; + +const MainLayout = ({ children }) => { + return ( +
+
+

Todo App

+
+
+
+

Filters:

+ All
+ Actived
+ Completed
+ 404
+
+
+ {children} +
+
+
+ Built with redux, react and antd. +
+
+ ); +}; + +MainLayout.propTypes = { + children: PropTypes.element.isRequired, +}; + +export default MainLayout; diff --git a/boilerplates/redux/src/layouts/MainLayout/MainLayout.less b/boilerplates/redux/src/layouts/MainLayout/MainLayout.less new file mode 100644 index 0000000..f367da8 --- /dev/null +++ b/boilerplates/redux/src/layouts/MainLayout/MainLayout.less @@ -0,0 +1,38 @@ + +.normal { + display: flex; + flex-direction: column; + height: 100%; +} + +.head { + margin-bottom: 20px; + background: cadetblue; + height: 80px; + padding: 8px; + color: #fff; +} + +.content { + flex: 1; + display: flex; +} + +.side { + padding: 8px; + width: 20%; + min-width: 200px; + background: #fafafa; + margin-right: 20px; +} + +.main { + padding: 8px; + flex: 1 0 auto; +} + +.foot { + margin-top: 20px; + background: greenyellow; + padding: 8px; +} diff --git a/boilerplates/redux/src/reducers/__tests__/todos-test.js b/boilerplates/redux/src/reducers/__tests__/todos-test.js new file mode 100644 index 0000000..ad4638e --- /dev/null +++ b/boilerplates/redux/src/reducers/__tests__/todos-test.js @@ -0,0 +1,18 @@ +import expect from 'expect'; +import todos from '../todos'; + +describe('todos', () => { + + it('todos/get', () => { + expect(todos({}, { type: 'todos/get' })).toEqual({ loading: true }); + }); + + it('todos/get/success', () => { + const newState = todos({ list: 1, loading: true }, { type: 'todos/get/success', payload:2 }); + expect(newState).toEqual({ + loading: false, + list: 2, + }); + }); + +}); diff --git a/boilerplates/redux/src/reducers/index.js b/boilerplates/redux/src/reducers/index.js new file mode 100644 index 0000000..9a4e66f --- /dev/null +++ b/boilerplates/redux/src/reducers/index.js @@ -0,0 +1,11 @@ +// Use require.context to require reducers automatically +// Ref: https://webpack.github.io/docs/context.html +const context = require.context('./', false, /\.js$/); +const keys = context.keys().filter(item => item !== './index.js'); + +const reducers = keys.reduce((memo, key) => { + memo[key.match(/([^\/]+)\.js$/)[1]] = context(key); + return memo; +}, {}); + +export default reducers; diff --git a/boilerplates/redux/src/reducers/todos.js b/boilerplates/redux/src/reducers/todos.js new file mode 100644 index 0000000..baea8de --- /dev/null +++ b/boilerplates/redux/src/reducers/todos.js @@ -0,0 +1,45 @@ +import { handleActions } from 'redux-actions'; +import { combineReducer } from 'redux'; + +const todos = handleActions({ + ['todos/get'](state) { + return { ...state, loading: true, }; + }, + ['todos/get/success'](state, action) { + return { ...state, list: action.payload, loading: false, }; + }, + ['todos/get/failed'](state, action) { + return { ...state, err: action.err, loading: false, }; + }, + ['todos/delete'](state, action) { + const id = action.payload; + const newList = state.list.filter(todo => todo.id !== id); + return { ...state, list: newList, }; + }, + ['todos/create'](state, action) { + const text = action.payload; + const newTodo = { text, }; + return { ...state, list: [newTodo, ...state.list], }; + }, + ['todos/toggleComplete'](state, action) { + const id = action.payload; + const newList = state.list.map(todo => { + if (id === todo.id) { + return { ...todo, isComplete: !todo.isComplete }; + } else { + return todo; + } + }); + return { ...state, list: newList, }; + }, + ['todos/toggleCompleteAll'](state, action) { + const isComplete = action.payload; + const newList = state.list.map(todo => ({ ...todo, isComplete })); + return { ...state, list: newList, }; + }, +}, { + list: [], + loading: false, +}); + +export default todos; diff --git a/boilerplates/redux/src/routes/index.js b/boilerplates/redux/src/routes/index.js new file mode 100644 index 0000000..1db19c9 --- /dev/null +++ b/boilerplates/redux/src/routes/index.js @@ -0,0 +1,18 @@ +import React, { PropTypes } from 'react'; +import { Router, Route, IndexRoute, Link } from 'react-router'; +import App from '../components/App'; +import NotFound from '../components/NotFound'; + +const Routes = ({ history }) => + + + + + + ; + +Routes.propTypes = { + history: PropTypes.any, +}; + +export default Routes; diff --git a/boilerplates/redux/src/sagas/SagaManager.js b/boilerplates/redux/src/sagas/SagaManager.js new file mode 100644 index 0000000..026c96a --- /dev/null +++ b/boilerplates/redux/src/sagas/SagaManager.js @@ -0,0 +1,31 @@ +import rootSaga from './index'; +import { take, fork, cancel } from 'redux-saga/effects'; + +const sagas = [rootSaga]; + +export const CANCEL_SAGAS_HMR = 'CANCEL_SAGAS_HMR'; + +function createAbortableSaga (saga) { + if (process.env.NODE_ENV === 'development') { + return function* main () { + const sagaTask = yield fork(saga); + yield take(CANCEL_SAGAS_HMR); + yield cancel(sagaTask); + }; + } else { + return saga; + } +} + +const SagaManager = { + startSagas(sagaMiddleware) { + sagas.map(createAbortableSaga).forEach(saga => sagaMiddleware.run(saga)); + }, + cancelSagas(store) { + store.dispatch({ + type: CANCEL_SAGAS_HMR + }); + } +}; + +export default SagaManager; diff --git a/boilerplates/redux/src/sagas/index.js b/boilerplates/redux/src/sagas/index.js new file mode 100644 index 0000000..3b507fc --- /dev/null +++ b/boilerplates/redux/src/sagas/index.js @@ -0,0 +1,12 @@ +import { fork } from 'redux-saga/effects'; + +// Use require.context to require sagas automatically +// Ref: https://webpack.github.io/docs/context.html +const context = require.context('./', false, /\.js$/); +const keys = context.keys().filter(item => item !== './index.js' && item !== './SagaManager.js'); + +export default function* root() { + for (let i = 0; i < keys.length; i ++) { + yield fork(context(keys[i])); + } +} diff --git a/boilerplates/redux/src/sagas/todos.js b/boilerplates/redux/src/sagas/todos.js new file mode 100644 index 0000000..13674cb --- /dev/null +++ b/boilerplates/redux/src/sagas/todos.js @@ -0,0 +1,35 @@ +import { takeLatest } from 'redux-saga'; +import { take, call, put, fork, cancel } from 'redux-saga/effects'; +import { getAll } from '../services/todos'; +import { message } from 'antd'; + +function* getTodos() { + try { + const { jsonResult } = yield call(getAll); + if (jsonResult.data) { + yield put({ + type: 'todos/get/success', + payload: jsonResult.data, + }); + } + } catch (err) { + message.error(err); + //yield put({ + // type: 'todos/get/failed', + // err, + //}); + } +} + +function* watchTodosGet() { + yield takeLatest('todos/get', getTodos) +} + +export default function* () { + yield fork(watchTodosGet); + + // Load todos. + yield put({ + type: 'todos/get', + }); +} diff --git a/boilerplates/redux/src/services/todos.js b/boilerplates/redux/src/services/todos.js new file mode 100644 index 0000000..cf7e699 --- /dev/null +++ b/boilerplates/redux/src/services/todos.js @@ -0,0 +1,5 @@ +import xFetch from './xFetch'; + +export async function getAll() { + return xFetch('/api/todos'); +} diff --git a/boilerplates/redux/src/services/xFetch.js b/boilerplates/redux/src/services/xFetch.js new file mode 100644 index 0000000..8be02ee --- /dev/null +++ b/boilerplates/redux/src/services/xFetch.js @@ -0,0 +1,46 @@ +import fetch from 'isomorphic-fetch'; +import cookie from 'js-cookie'; + +const errorMessages = (res) => `${res.status} ${res.statusText}`; + +function check401(res) { + if (res.status === 401) { + location.href = '/401'; + } + return res; +} + +function check404(res) { + if (res.status === 404) { + return Promise.reject(errorMessages(res)); + } + return res; +} + +function jsonParse(res) { + return res.json().then(jsonResult => ({ ...res, jsonResult })); +} + +function errorMessageParse(res) { + const { success, message } = res.jsonResult; + if (!success) { + return Promise.reject(message); + } + return res; +} + +function xFetch(url, options) { + const opts = { ...options }; + opts.headers = { + ...opts.headers, + authorization: cookie.get('authorization') || '', + }; + + return fetch(url, opts) + .then(check401) + .then(check404) + .then(jsonParse) + .then(errorMessageParse); +} + +export default xFetch; diff --git a/boilerplates/redux/webpack.config.js b/boilerplates/redux/webpack.config.js new file mode 100644 index 0000000..bc58cd5 --- /dev/null +++ b/boilerplates/redux/webpack.config.js @@ -0,0 +1,39 @@ +// Learn more on how to config. +// - https://github.com/ant-tool/atool-build#配置扩展 + +const webpack = require('atool-build/lib/webpack'); +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); + +module.exports = function(webpackConfig) { + webpackConfig.babel.plugins.push('transform-runtime'); + webpackConfig.babel.plugins.push(['antd', {style: 'css'}]); + + // Enable this if you have to support IE8. + // webpackConfig.module.loaders.unshift({ + // test: /\.jsx?$/, + // loader: 'es3ify-loader', + // }); + + // Parse all less files as css module. + webpackConfig.module.loaders.forEach(function(loader, index) { + if (typeof loader.test === 'function' && loader.test.toString().indexOf('\\.less$') > -1) { + loader.test = /\.dont\.exist\.file/; + } + if (loader.test.toString() === '/\\.module\\.less$/') { + loader.test = /\.less$/; + } + }); + + // Load src/entries/*.js as entry automatically. + const files = glob.sync('./src/entries/*.js'); + const newEntries = files.reduce(function(memo, file) { + const name = path.basename(file, '.js'); + memo[name] = file; + return memo; + }, {}); + webpackConfig.entry = Object.assign({}, webpackConfig.entry, newEntries); + + return webpackConfig; +}; diff --git a/package.json b/package.json index 47c5759..d9f3fb5 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "test": "mocha --no-timeouts" }, "dependencies": { + "inquirer": "^1.0.0", "through2": "2.0.x", "vinyl-fs": "^2.4.3", "which": "1.2.x"