├── .gitignore ├── 1_default ├── README.md ├── index.html ├── src │ ├── actions │ │ └── TodoActions.js │ ├── components │ │ ├── CreateTodoFieldContainer.js │ │ ├── InputField.js │ │ ├── TodoApp.js │ │ ├── TodoHeader.js │ │ ├── TodoHeaderContainer.js │ │ ├── TodoItem.js │ │ ├── TodoList.js │ │ └── TodoListContainer.js │ ├── constants │ │ └── ActionTypes.js │ ├── main.js │ └── reducers │ │ ├── index.js │ │ └── todos.js └── todos.json ├── 2_webpack ├── .babelrc ├── README.md ├── index.html ├── package.json ├── server.dev.js ├── src │ ├── actions │ │ └── TodoActions.js │ ├── components │ │ ├── CreateTodoFieldContainer.js │ │ ├── InputField.js │ │ ├── TodoApp.js │ │ ├── TodoHeader.js │ │ ├── TodoHeaderContainer.js │ │ ├── TodoItem.js │ │ ├── TodoList.js │ │ └── TodoListContainer.js │ ├── constants │ │ └── ActionTypes.js │ ├── main.js │ └── reducers │ │ ├── index.js │ │ └── todos.js ├── todos.json ├── webpack.config.dev.js └── webpack.config.prod.babel.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /1_default/README.md: -------------------------------------------------------------------------------- 1 | # 第一章. 當前端還沒有 Build System 時 2 | 3 | 在以前,當我們沒有建置工具時,所有的 JS/CSS 檔都必須從 CDN 抓取,並將 URL 設定在 [index.html](https://github.com/shiningjason1989/react-quick-tutorial/blob/master/level-24_immutablejs/index.html) 上: 4 | 5 | ```html 6 | 7 | 8 | 9 | ``` 10 | 11 | 因為要手動管理這些檔案和依賴關係,我們很難將程式寫的模組化。 12 | 13 | 但是當 CommonJS Modules 或是 ES6 Modules 這樣的規格出來以後,撰寫模組化的程式就方便多了(你可以從這份 [commit](https://github.com/shiningjason1989/react-build-systems-tutorial/commit/2ac2dccf0b1988b45f1b1c605977b882b43db21b) 中看到 ES6 Modules 的前後差別)。 14 | 15 | ```js 16 | /* a.js */ 17 | import b from './b.js'; // 優點 1. 幫我們管理模組的依賴關係 18 | export default { text: b }; // 優點 2. 每個模組的變數也不會與 global 變數混在一起 19 | 20 | /* b.js */ 21 | export default const b = 'hello, world'; 22 | 23 | /* index.html */ 24 | // 優點 3. 25 | // 可以使用工具將所有相依的模組打包, 26 | // 瀏覽器只需要請求一支檔案,減少負擔, 27 | // 而且你不再需要在 index.html 手動管理哪些檔案要引入,哪些不用 28 | ``` 29 | 30 | 只不過,這樣的語法現今瀏覽器是不支援的,我們需要工具協助我們處理以下問題(這些問題皆是為了開發便利而付出的代價): 31 | 32 | 1. 轉譯 ES6/ES7/JSX 語法 33 | 2. 關連所有相依模組,並將它們打包成一支程式(a.js, b.js, c.js => bundle.js) 34 | 3. 執行 uglify,壓縮打包程式(bundle.js => bundle.min.js) 35 | 4. 提供 Hot Reload 功能,讓開發者有所見即所得的開發體驗 36 | 5. 將設定檔區分為 development 和 production 37 | 38 | > :bowtie::因此接下來,我將會筆記每套 Build System 是如何處理這些前端開發問題的,歡迎大家與我討論囉 :beers: 39 | 40 | 41 | ## :rocket: 42 | 43 | | :raising_hand: [我想問問題](https://github.com/shiningjason1989/react-build-systems-tutorial/issues/new) | 44 | 45 | | [主頁](../../../) | [下一章. 使用 webpack 建置 Build System](../2_webpack) | 46 | -------------------------------------------------------------------------------- /1_default/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TodoApp 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /1_default/src/actions/TodoActions.js: -------------------------------------------------------------------------------- 1 | import ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default { 4 | loadTodos() { 5 | return (dispatch) => { 6 | fetch('./todos.json') 7 | .then((response) => response.json()) 8 | .then((todos) => dispatch({ 9 | type: ActionTypes.LOAD_TODOS_SUCCESS, 10 | todos 11 | })); 12 | }; 13 | }, 14 | createTodo(title) { 15 | return { 16 | type: ActionTypes.CREATE_TODO, 17 | title 18 | }; 19 | }, 20 | updateTodo(id, title) { 21 | return { 22 | type: ActionTypes.UPDATE_TODO, 23 | id, 24 | title 25 | }; 26 | }, 27 | toggleTodo(id, completed) { 28 | return { 29 | type: ActionTypes.TOGGLE_TODO, 30 | id, 31 | completed 32 | }; 33 | }, 34 | deleteTodo(id) { 35 | return { 36 | type: ActionTypes.DELETE_TODO, 37 | id 38 | }; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /1_default/src/components/CreateTodoFieldContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import InputField from './InputField'; 5 | import TodoActions from '../actions/TodoActions'; 6 | 7 | class CreateTodoFieldContainer extends React.Component { 8 | render() { 9 | return ( 10 | 14 | ); 15 | } 16 | } 17 | 18 | export default connect(undefined, { 19 | createTodo: TodoActions.createTodo 20 | })(CreateTodoFieldContainer); 21 | -------------------------------------------------------------------------------- /1_default/src/components/InputField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class InputField extends React.Component { 4 | static propTypes = { 5 | onSubmitEditing: React.PropTypes.func 6 | }; 7 | 8 | constructor(props, context) { 9 | super(props, context); 10 | this.state = { value: props.value || '' }; 11 | } 12 | 13 | handleChange(e) { 14 | this.setState({ value: e.target.value }); 15 | } 16 | 17 | handleKeyDown(e) { 18 | const { 19 | onKeyDown, 20 | onSubmitEditing 21 | } = this.props; 22 | const { value } = this.state; 23 | switch (e.keyCode) { 24 | case 13: 25 | if (value.trim()) { 26 | onSubmitEditing && onSubmitEditing(value); 27 | } 28 | this.setState({ value: '' }); 29 | break; 30 | } 31 | onKeyDown && onKeyDown(e); 32 | } 33 | 34 | render() { 35 | return ( 36 | 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /1_default/src/components/TodoApp.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import CreateTodoFieldContainer from './CreateTodoFieldContainer'; 5 | import TodoHeaderContainer from './TodoHeaderContainer'; 6 | import TodoListContainer from './TodoListContainer'; 7 | import TodoActions from '../actions/TodoActions'; 8 | 9 | class TodoApp extends React.Component { 10 | componentDidMount() { 11 | this.props.loadTodos(); 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 | 18 | 19 | 20 |
21 | ); 22 | } 23 | } 24 | 25 | export default connect(undefined, { 26 | loadTodos: TodoActions.loadTodos 27 | })(TodoApp); 28 | -------------------------------------------------------------------------------- /1_default/src/components/TodoHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class TodoHeader extends React.Component { 4 | static propTypes = { 5 | title: React.PropTypes.string, 6 | username: React.PropTypes.string, 7 | todoCount: React.PropTypes.number 8 | }; 9 | 10 | static defaultProps = { 11 | title: '我的待辦清單', 12 | username: 'Guest', 13 | todoCount: 0 14 | }; 15 | 16 | render() { 17 | const { 18 | title, 19 | username, 20 | todoCount 21 | } = this.props; 22 | return ( 23 |
24 |

{title}

25 | 哈囉,{username}:你有 {todoCount} 項未完成待辦事項 26 |
27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /1_default/src/components/TodoHeaderContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import TodoHeader from './TodoHeader'; 5 | 6 | class TodoHeaderContainer extends React.Component { 7 | render() { 8 | return ( 9 | !todo.completed).size} 13 | /> 14 | ); 15 | } 16 | } 17 | 18 | export default connect( 19 | (state) => ({ todos: state.todos }) 20 | )(TodoHeaderContainer); 21 | -------------------------------------------------------------------------------- /1_default/src/components/TodoItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import InputField from './InputField'; 4 | 5 | export default class TodoItem extends React.Component { 6 | static propTypes = { 7 | title: React.PropTypes.string.isRequired, 8 | completed: React.PropTypes.bool.isRequired, 9 | onUpdate: React.PropTypes.func, 10 | onToggle: React.PropTypes.func, 11 | onDelete: React.PropTypes.func 12 | }; 13 | 14 | constructor(props, context) { 15 | super(props, context); 16 | this.state = { editable: false }; 17 | } 18 | 19 | toggleEditMode() { 20 | this.setState({ editable: !this.state.editable }); 21 | } 22 | 23 | renderViewMode() { 24 | const { 25 | title, 26 | completed, 27 | onToggle, 28 | onDelete 29 | } = this.props; 30 | return ( 31 |
32 | onToggle && onToggle(!completed)} 36 | /> 37 | {title} 38 | 39 |
40 | ); 41 | } 42 | 43 | renderEditMode() { 44 | const { title, onUpdate } = this.props; 45 | return ( 46 | { 52 | if (e.keyCode === 27) { 53 | e.preventDefault(); 54 | this.toggleEditMode(); 55 | } 56 | }} 57 | onSubmitEditing={(content) => { 58 | onUpdate && onUpdate(content); 59 | this.toggleEditMode(); 60 | }} 61 | /> 62 | ); 63 | } 64 | 65 | render() { 66 | return this.state.editable ? 67 | this.renderEditMode() : 68 | this.renderViewMode(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /1_default/src/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TodoItem from './TodoItem'; 4 | 5 | export default class TodoList extends React.Component { 6 | static propTypes = { 7 | todos: React.PropTypes.object.isRequired, 8 | onUpdateTodo: React.PropTypes.func, 9 | onToggleTodo: React.PropTypes.func, 10 | onDeleteTodo: React.PropTypes.func 11 | }; 12 | 13 | render() { 14 | const { 15 | todos, 16 | onUpdateTodo, 17 | onToggleTodo, 18 | onDeleteTodo 19 | } = this.props; 20 | const todoElements = todos.map((todo) => ( 21 |
  • 22 | onUpdateTodo && onUpdateTodo(todo.id, content)} 26 | onToggle={(completed) => onToggleTodo && onToggleTodo(todo.id, completed)} 27 | onDelete={() => onDeleteTodo && onDeleteTodo(todo.id)} 28 | /> 29 |
  • 30 | )); 31 | return
      {todoElements}
    ; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /1_default/src/components/TodoListContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import TodoList from './TodoList'; 5 | import TodoActions from '../actions/TodoActions'; 6 | 7 | class TodoListContainer extends React.Component { 8 | render() { 9 | const { 10 | todos, 11 | updateTodo, 12 | toggleTodo, 13 | deleteTodo 14 | } = this.props; 15 | return ( 16 | 22 | ); 23 | } 24 | } 25 | 26 | export default connect( 27 | (state) => ({ todos: state.todos }), 28 | { 29 | updateTodo: TodoActions.updateTodo, 30 | toggleTodo: TodoActions.toggleTodo, 31 | deleteTodo: TodoActions.deleteTodo 32 | } 33 | )(TodoListContainer); 34 | -------------------------------------------------------------------------------- /1_default/src/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | LOAD_TODOS_SUCCESS: 'LOAD_TODOS_SUCCESS', 3 | CREATE_TODO: 'CREATE_TODO', 4 | UPDATE_TODO: 'UPDATE_TODO', 5 | TOGGLE_TODO: 'TOGGLE_TODO', 6 | DELETE_TODO: 'DELETE_TODO' 7 | }; 8 | -------------------------------------------------------------------------------- /1_default/src/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | 6 | import TodoApp from './components/TodoApp'; 7 | import reducers from './reducers'; 8 | 9 | const thunkMiddleware = ({ dispatch, getState }) => { 10 | return (next) => (action) => { 11 | if (typeof action === 'function') { 12 | return action(dispatch, getState); 13 | } 14 | return next(action); 15 | }; 16 | }; 17 | 18 | const composedReducer = combineReducers(reducers); 19 | const store = createStore( 20 | composedReducer, 21 | applyMiddleware(thunkMiddleware) 22 | ); 23 | 24 | ReactDOM.render( 25 | 26 | 27 | , 28 | document.getElementById('app') 29 | ); 30 | -------------------------------------------------------------------------------- /1_default/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import todos from './todos'; 2 | 3 | export default { 4 | todos 5 | }; 6 | -------------------------------------------------------------------------------- /1_default/src/reducers/todos.js: -------------------------------------------------------------------------------- 1 | import { List, Record } from 'immutable'; 2 | 3 | import ActionTypes from '../constants/ActionTypes'; 4 | 5 | const TodoRecord = Record({ 6 | id: undefined, 7 | title: undefined, 8 | completed: false 9 | }); 10 | 11 | const _findIdxById = (todos, id) => todos.findIndex((todo) => todo.id === id); 12 | 13 | const _createTodo = (todos, title) => 14 | todos.push(new TodoRecord({ 15 | id: todos.last().id + 1, 16 | title, 17 | completed: false 18 | })); 19 | 20 | const _updateTodo = (todos, id, title) => 21 | todos.setIn([ _findIdxById(todos, id), 'title' ], title); 22 | 23 | const _toggleTodo = (todos, id, completed) => 24 | todos.setIn([ _findIdxById(todos, id), 'completed' ], completed); 25 | 26 | const _deleteTodo = (todos, id) => 27 | todos.delete(_findIdxById(todos, id)); 28 | 29 | export default (state = new List(), action) => { 30 | switch (action.type) { 31 | case ActionTypes.LOAD_TODOS_SUCCESS: 32 | return new List(action.todos).map((todo) => new TodoRecord(todo)); 33 | case ActionTypes.CREATE_TODO: 34 | return _createTodo(state, action.title); 35 | case ActionTypes.UPDATE_TODO: 36 | return _updateTodo(state, action.id, action.title); 37 | case ActionTypes.TOGGLE_TODO: 38 | return _toggleTodo(state, action.id, action.completed); 39 | case ActionTypes.DELETE_TODO: 40 | return _deleteTodo(state, action.id); 41 | default: 42 | return state; 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /1_default/todos.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 0, 4 | "title": "Item 1", 5 | "completed": false 6 | }, 7 | { 8 | "id": 1, 9 | "title": "Item 2", 10 | "completed": false 11 | }, 12 | { 13 | "id": 2, 14 | "title": "Item 3", 15 | "completed": false 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /2_webpack/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["react-hot-loader/babel"] 4 | } 5 | -------------------------------------------------------------------------------- /2_webpack/README.md: -------------------------------------------------------------------------------- 1 | # 2. 使用 webpack 建置 Build System 2 | 3 | 4 | ## :coffee: tl;dr 5 | 6 | 使用 webpack 的解決方案有: 7 | 8 | 1. 轉譯 ES6/ES7/JSX 語法 => ***使用 babel-loader*** 9 | 2. 關連所有相依模組,並將它們打包成一支程式 => ***使用 webpack*** 10 | 3. 執行 uglify,壓縮打包程式 => ***使用 webpack 的 uglifyjs plugin*** 11 | 4. 提供 Hot Reload 功能 => ***使用 webpack-dev-server 和 React Hot Loader*** 12 | 5. 將設定檔區分為 development 和 production => ***撰寫不同 webpack config 區別,並讓 package.json 提供不同指令*** 13 | 14 | 15 | ## :spaghetti: 步驟 16 | 17 | ### 1. 使用 npm 初始專案 18 | 19 | ``` 20 | $ npm init 21 | ``` 22 | 23 | ### 2. 安裝建置工具 24 | 25 | ###### a. 安裝 webpack 26 | 27 | ``` 28 | $ npm install -D webpack webpack-dev-server 29 | ``` 30 | 31 | > :bowtie::我們使用 webpack-dev-server 替代原本靜態檔案使用的 live-server 或 http-serevr。 32 | 33 | ###### b. 安裝 babel 34 | 35 | [Babel](http://babeljs.io/) 用來將 ES6/ES7/JSX 等進階語法轉譯成 ES5,所以我們下載以下套件: 36 | 37 | ``` 38 | $ npm install -D babel-cli babel-core babel-loader babel-preset-es2015 babel-preset-stage-0 babel-preset-react 39 | ``` 40 | 41 | 1. ***babel-cli*** 可以讓你直接下 command 執行 ES6/ES7/JSX 檔案,如:`$ babel-node source.babel.js` 42 | 2. ***babel-core*** 是 Babel 的核心程式庫,所有 babel- 相關程式庫(babel-cli, babel-loader)都依賴它 43 | 3. ***babel-loader*** 是 webpack 的 loader 套件,當 webpack 在建置原始碼時,必須倚靠它介接 babel 來轉譯原始碼 44 | 4. ***babel-preset-es2015***, ***babel-preset-stage-0***, ***babel-preset-react*** 這三個套件分別是轉譯不同語法會用到的工具 45 | 46 | ###### c. 安裝第三方程式庫 47 | 48 | 專案依賴的第三方程式庫不再是從 CDN 抓取,全部統一由 npm 管理: 49 | 50 | ``` 51 | $ npm install -S react react-dom redux react-redux immutable 52 | ``` 53 | 54 | 第一步安裝的 webpack-dev-server 雖然有 hot reload 功能;但是**開發 React 時,我們很常需要保留元件狀態**,不希望因為 reload 而讓其流失(舉實例說,當我們修改第五頁的元件時,因為 reload 無法保留狀態而讓頁面跳回第一頁,我們必須在層層進入到第五頁才能看到結果,這真的是非常的困擾呢!)。 55 | 56 | 所以我們需要 ***React Hot Loader*** 協助我們處理該議題: 57 | 58 | ``` 59 | $ npm install -S react-hot-loader@3.0.0-beta.1 60 | ``` 61 | 62 | ### 3. 建置專案設定檔 63 | 64 | 最後,為了讓我們在 development 和 production 可以做不同的事,因此根據這兩種環境設定 webpack: 65 | 66 | ###### a. Development 67 | 68 | 參考這份 [commit](https://github.com/shiningjason1989/react-build-systems-tutorial/commit/0d77246521291c0c3adb702df18daf875564fc2b): 69 | 70 | 1. ***新增 .babelrc***,這份檔案是 Babel 的設定檔 71 | 2. ***新增 webpack.config.dev.js***,這份是 webpack 設定檔(:bowtie::我習慣用 .dev/.prod 區分檔案環境) 72 | 3. ***新增 server.dev.js***,這是開發用的本地端伺服器 73 | 4. ***在 package.json 加入 start 指令***,因此之後可以用 `$ npm start` 開啟本地端伺服器 74 | 5. ***在 main.js 加入 hot reload 的程式***,並且判斷只有 development 會執行 75 | 76 | ###### b. Production 77 | 78 | 參考這份 [commit](https://github.com/shiningjason1989/react-build-systems-tutorial/commit/72e5f4edb79c7c8a9c843979821a0351dae6cee4): 79 | 80 | 1. ***新增 webpack.config.prod.babel.js***,因為設定檔是由 ES6 撰寫,因此需要加 .babel 讓 webpack 指令可以自己轉譯 81 | 2. ***在 package.json 加入 build 指令***,因此之後可以用 `$ npm run build` 打包正式檔案 82 | 83 | 84 | ## :wine_glass: 參考連結 85 | 86 | ### 1. Webpack 相關文章 87 | 88 | 1. [Webpack — The Confusing Parts](https://medium.com/p/58712f8fcad9) 89 | 2. [Beginner’s guide to Webpack](https://medium.com/p/b1f1a3638460) 90 | 3. [Webpack’s HMR & React-Hot-Loader — The Missing Manual](https://medium.com/p/232336dc0d96) 91 | 92 | ### 2. React Hot Loader 3 範例 93 | 94 | 1. [TodoMVC](https://github.com/gaearon/redux-devtools/tree/master/examples/todomvc) 95 | 2. [React Hot Boilerplate](https://github.com/gaearon/react-hot-boilerplate/tree/next) 96 | 97 | 98 | ## :rocket: 99 | 100 | | :raising_hand: [我想問問題](https://github.com/shiningjason1989/react-build-systems-tutorial/issues/new) | 101 | 102 | | [主頁](../../../) | [上一章](../1_default) | ***下一章. gulp/grunt/jspm/???*** :see_no_evil: ***Coming soon!*** | 103 | -------------------------------------------------------------------------------- /2_webpack/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TodoApp 6 | 7 | 8 |
    9 | 10 | 11 | -------------------------------------------------------------------------------- /2_webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-build-systems-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "babel-node server.dev.js", 8 | "build": "webpack --config ./webpack.config.prod.babel.js" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "babel-cli": "^6.8.0", 14 | "babel-core": "^6.8.0", 15 | "babel-loader": "^6.2.4", 16 | "babel-preset-es2015": "^6.6.0", 17 | "babel-preset-react": "^6.5.0", 18 | "babel-preset-stage-0": "^6.5.0", 19 | "webpack": "^1.13.0", 20 | "webpack-dev-server": "^1.14.1" 21 | }, 22 | "dependencies": { 23 | "immutable": "^3.8.1", 24 | "react": "^15.0.2", 25 | "react-dom": "^15.0.2", 26 | "react-hot-loader": "^3.0.0-beta.1", 27 | "react-redux": "^4.4.5", 28 | "redux": "^3.5.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /2_webpack/server.dev.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import WebpackDevServer from 'webpack-dev-server'; 3 | import config from './webpack.config.dev'; 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | historyApiFallback: true 9 | }).listen(3000, 'localhost', (err, result) => { 10 | if (err) return console.log(err); 11 | console.log('Listening at http://localhost:3000/'); 12 | }); 13 | -------------------------------------------------------------------------------- /2_webpack/src/actions/TodoActions.js: -------------------------------------------------------------------------------- 1 | import ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default { 4 | loadTodos() { 5 | return (dispatch) => { 6 | fetch('./todos.json') 7 | .then((response) => response.json()) 8 | .then((todos) => dispatch({ 9 | type: ActionTypes.LOAD_TODOS_SUCCESS, 10 | todos 11 | })); 12 | }; 13 | }, 14 | createTodo(title) { 15 | return { 16 | type: ActionTypes.CREATE_TODO, 17 | title 18 | }; 19 | }, 20 | updateTodo(id, title) { 21 | return { 22 | type: ActionTypes.UPDATE_TODO, 23 | id, 24 | title 25 | }; 26 | }, 27 | toggleTodo(id, completed) { 28 | return { 29 | type: ActionTypes.TOGGLE_TODO, 30 | id, 31 | completed 32 | }; 33 | }, 34 | deleteTodo(id) { 35 | return { 36 | type: ActionTypes.DELETE_TODO, 37 | id 38 | }; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /2_webpack/src/components/CreateTodoFieldContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import InputField from './InputField'; 5 | import TodoActions from '../actions/TodoActions'; 6 | 7 | class CreateTodoFieldContainer extends React.Component { 8 | render() { 9 | return ( 10 | 14 | ); 15 | } 16 | } 17 | 18 | export default connect(undefined, { 19 | createTodo: TodoActions.createTodo 20 | })(CreateTodoFieldContainer); 21 | -------------------------------------------------------------------------------- /2_webpack/src/components/InputField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class InputField extends React.Component { 4 | static propTypes = { 5 | onSubmitEditing: React.PropTypes.func 6 | }; 7 | 8 | constructor(props, context) { 9 | super(props, context); 10 | this.state = { value: props.value || '' }; 11 | } 12 | 13 | handleChange(e) { 14 | this.setState({ value: e.target.value }); 15 | } 16 | 17 | handleKeyDown(e) { 18 | const { 19 | onKeyDown, 20 | onSubmitEditing 21 | } = this.props; 22 | const { value } = this.state; 23 | switch (e.keyCode) { 24 | case 13: 25 | if (value.trim()) { 26 | onSubmitEditing && onSubmitEditing(value); 27 | } 28 | this.setState({ value: '' }); 29 | break; 30 | } 31 | onKeyDown && onKeyDown(e); 32 | } 33 | 34 | render() { 35 | return ( 36 | 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /2_webpack/src/components/TodoApp.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import CreateTodoFieldContainer from './CreateTodoFieldContainer'; 5 | import TodoHeaderContainer from './TodoHeaderContainer'; 6 | import TodoListContainer from './TodoListContainer'; 7 | import TodoActions from '../actions/TodoActions'; 8 | 9 | class TodoApp extends React.Component { 10 | componentDidMount() { 11 | this.props.loadTodos(); 12 | } 13 | 14 | render() { 15 | return ( 16 |
    17 | 18 | 19 | 20 |
    21 | ); 22 | } 23 | } 24 | 25 | export default connect(undefined, { 26 | loadTodos: TodoActions.loadTodos 27 | })(TodoApp); 28 | -------------------------------------------------------------------------------- /2_webpack/src/components/TodoHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class TodoHeader extends React.Component { 4 | static propTypes = { 5 | title: React.PropTypes.string, 6 | username: React.PropTypes.string, 7 | todoCount: React.PropTypes.number 8 | }; 9 | 10 | static defaultProps = { 11 | title: '我的待辦清單', 12 | username: 'Guest', 13 | todoCount: 0 14 | }; 15 | 16 | render() { 17 | const { 18 | title, 19 | username, 20 | todoCount 21 | } = this.props; 22 | return ( 23 |
    24 |

    {title}

    25 | 哈囉,{username}:你有 {todoCount} 項未完成待辦事項 26 |
    27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /2_webpack/src/components/TodoHeaderContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import TodoHeader from './TodoHeader'; 5 | 6 | class TodoHeaderContainer extends React.Component { 7 | render() { 8 | return ( 9 | !todo.completed).size} 13 | /> 14 | ); 15 | } 16 | } 17 | 18 | export default connect( 19 | (state) => ({ todos: state.todos }) 20 | )(TodoHeaderContainer); 21 | -------------------------------------------------------------------------------- /2_webpack/src/components/TodoItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import InputField from './InputField'; 4 | 5 | export default class TodoItem extends React.Component { 6 | static propTypes = { 7 | title: React.PropTypes.string.isRequired, 8 | completed: React.PropTypes.bool.isRequired, 9 | onUpdate: React.PropTypes.func, 10 | onToggle: React.PropTypes.func, 11 | onDelete: React.PropTypes.func 12 | }; 13 | 14 | constructor(props, context) { 15 | super(props, context); 16 | this.state = { editable: false }; 17 | } 18 | 19 | toggleEditMode() { 20 | this.setState({ editable: !this.state.editable }); 21 | } 22 | 23 | renderViewMode() { 24 | const { 25 | title, 26 | completed, 27 | onToggle, 28 | onDelete 29 | } = this.props; 30 | return ( 31 |
    32 | onToggle && onToggle(!completed)} 36 | /> 37 | {title} 38 | 39 |
    40 | ); 41 | } 42 | 43 | renderEditMode() { 44 | const { title, onUpdate } = this.props; 45 | return ( 46 | { 52 | if (e.keyCode === 27) { 53 | e.preventDefault(); 54 | this.toggleEditMode(); 55 | } 56 | }} 57 | onSubmitEditing={(content) => { 58 | onUpdate && onUpdate(content); 59 | this.toggleEditMode(); 60 | }} 61 | /> 62 | ); 63 | } 64 | 65 | render() { 66 | return this.state.editable ? 67 | this.renderEditMode() : 68 | this.renderViewMode(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /2_webpack/src/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TodoItem from './TodoItem'; 4 | 5 | export default class TodoList extends React.Component { 6 | static propTypes = { 7 | todos: React.PropTypes.object.isRequired, 8 | onUpdateTodo: React.PropTypes.func, 9 | onToggleTodo: React.PropTypes.func, 10 | onDeleteTodo: React.PropTypes.func 11 | }; 12 | 13 | render() { 14 | const { 15 | todos, 16 | onUpdateTodo, 17 | onToggleTodo, 18 | onDeleteTodo 19 | } = this.props; 20 | const todoElements = todos.map((todo) => ( 21 |
  • 22 | onUpdateTodo && onUpdateTodo(todo.id, content)} 26 | onToggle={(completed) => onToggleTodo && onToggleTodo(todo.id, completed)} 27 | onDelete={() => onDeleteTodo && onDeleteTodo(todo.id)} 28 | /> 29 |
  • 30 | )); 31 | return
      {todoElements}
    ; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /2_webpack/src/components/TodoListContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import TodoList from './TodoList'; 5 | import TodoActions from '../actions/TodoActions'; 6 | 7 | class TodoListContainer extends React.Component { 8 | render() { 9 | const { 10 | todos, 11 | updateTodo, 12 | toggleTodo, 13 | deleteTodo 14 | } = this.props; 15 | return ( 16 | 22 | ); 23 | } 24 | } 25 | 26 | export default connect( 27 | (state) => ({ todos: state.todos }), 28 | { 29 | updateTodo: TodoActions.updateTodo, 30 | toggleTodo: TodoActions.toggleTodo, 31 | deleteTodo: TodoActions.deleteTodo 32 | } 33 | )(TodoListContainer); 34 | -------------------------------------------------------------------------------- /2_webpack/src/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | LOAD_TODOS_SUCCESS: 'LOAD_TODOS_SUCCESS', 3 | CREATE_TODO: 'CREATE_TODO', 4 | UPDATE_TODO: 'UPDATE_TODO', 5 | TOGGLE_TODO: 'TOGGLE_TODO', 6 | DELETE_TODO: 'DELETE_TODO' 7 | }; 8 | -------------------------------------------------------------------------------- /2_webpack/src/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { AppContainer } from 'react-hot-loader'; 6 | 7 | import TodoApp from './components/TodoApp'; 8 | import reducers from './reducers'; 9 | 10 | const thunkMiddleware = ({ dispatch, getState }) => { 11 | return (next) => (action) => { 12 | if (typeof action === 'function') { 13 | return action(dispatch, getState); 14 | } 15 | return next(action); 16 | }; 17 | }; 18 | 19 | const composedReducer = combineReducers(reducers); 20 | const store = createStore( 21 | composedReducer, 22 | applyMiddleware(thunkMiddleware) 23 | ); 24 | 25 | const rootEl = document.getElementById('app'); 26 | 27 | ReactDOM.render( 28 | 29 | 30 | 31 | 32 | , 33 | rootEl 34 | ); 35 | 36 | if (module.hot) { 37 | module.hot.accept( 38 | './reducers', 39 | () => store.replaceReducer(combineReducers(require('./reducers').default)) 40 | ); 41 | 42 | module.hot.accept( 43 | './components/TodoApp', 44 | () => { 45 | const NextApp = require('./components/TodoApp').default; 46 | ReactDOM.render( 47 | 48 | 49 | 50 | 51 | , 52 | rootEl 53 | ); 54 | } 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /2_webpack/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import todos from './todos'; 2 | 3 | export default { 4 | todos 5 | }; 6 | -------------------------------------------------------------------------------- /2_webpack/src/reducers/todos.js: -------------------------------------------------------------------------------- 1 | import { List, Record } from 'immutable'; 2 | 3 | import ActionTypes from '../constants/ActionTypes'; 4 | 5 | const TodoRecord = Record({ 6 | id: undefined, 7 | title: undefined, 8 | completed: false 9 | }); 10 | 11 | const _findIdxById = (todos, id) => todos.findIndex((todo) => todo.id === id); 12 | 13 | const _createTodo = (todos, title) => 14 | todos.push(new TodoRecord({ 15 | id: todos.last().id + 1, 16 | title, 17 | completed: false 18 | })); 19 | 20 | const _updateTodo = (todos, id, title) => 21 | todos.setIn([ _findIdxById(todos, id), 'title' ], title); 22 | 23 | const _toggleTodo = (todos, id, completed) => 24 | todos.setIn([ _findIdxById(todos, id), 'completed' ], completed); 25 | 26 | const _deleteTodo = (todos, id) => 27 | todos.delete(_findIdxById(todos, id)); 28 | 29 | export default (state = new List(), action) => { 30 | switch (action.type) { 31 | case ActionTypes.LOAD_TODOS_SUCCESS: 32 | return new List(action.todos).map((todo) => new TodoRecord(todo)); 33 | case ActionTypes.CREATE_TODO: 34 | return _createTodo(state, action.title); 35 | case ActionTypes.UPDATE_TODO: 36 | return _updateTodo(state, action.id, action.title); 37 | case ActionTypes.TOGGLE_TODO: 38 | return _toggleTodo(state, action.id, action.completed); 39 | case ActionTypes.DELETE_TODO: 40 | return _deleteTodo(state, action.id); 41 | default: 42 | return state; 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /2_webpack/todos.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 0, 4 | "title": "Item 1", 5 | "completed": false 6 | }, 7 | { 8 | "id": 1, 9 | "title": "Item 2", 10 | "completed": false 11 | }, 12 | { 13 | "id": 2, 14 | "title": "Item 3", 15 | "completed": false 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /2_webpack/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import webpack from 'webpack'; 4 | 5 | export default { 6 | devtool: 'eval', 7 | entry: [ 8 | 'webpack-dev-server/client?http://localhost:3000', 9 | 'webpack/hot/only-dev-server', 10 | 'react-hot-loader/patch', 11 | './src/main' 12 | ], 13 | output: { 14 | path: path.join(__dirname, 'dist'), 15 | filename: 'bundle.js', 16 | publicPath: '/' 17 | }, 18 | plugins: [ 19 | new webpack.HotModuleReplacementPlugin() 20 | ], 21 | module: { 22 | loaders: [ 23 | { 24 | test: /\.js$/, 25 | loader: 'babel', 26 | exclude: /node_modules/ 27 | } 28 | ] 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /2_webpack/webpack.config.prod.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import webpack from 'webpack'; 4 | 5 | export default { 6 | entry: './src/main', 7 | output: { 8 | path: path.join(__dirname, 'dist'), 9 | filename: 'bundle.js', 10 | publicPath: '/' 11 | }, 12 | plugins: [ 13 | new webpack.DefinePlugin({ 14 | 'process.env': { 15 | NODE_ENV: JSON.stringify('production'), 16 | } 17 | }), 18 | new webpack.optimize.UglifyJsPlugin({ 19 | compress: { 20 | warnings: false 21 | } 22 | }) 23 | ], 24 | module: { 25 | loaders: [ 26 | { 27 | test: /\.js$/, 28 | loader: 'babel', 29 | exclude: /node_modules/ 30 | } 31 | ] 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Build Systems Tutorial 2 | 3 | 4 | ## :scroll: 目錄 5 | 6 | [第一章. 當前端還沒有 Build System 時](https://github.com/shiningjason1989/react-build-systems-tutorial/tree/master/1_default) 7 | [第二章. 使用 webpack 建置 Build System](https://github.com/shiningjason1989/react-build-systems-tutorial/tree/master/2_webpack) 8 | 9 | > :loudspeaker::未來期望再加入 gulp/grunt/jspm/??? 等章節,盡請期待囉!^ ^ 10 | 11 | 12 | ## :bowtie: 關於我 13 | 14 | 目前我正在連載「[24 小時,快速入門 React](https://github.com/shiningjason1989/react-quick-tutorial)」的文字教材,如果你想學習 ***React/Flux/Redux/ImmuatbleJS/ES6*** 歡迎您前往:) 15 | 16 | 如果你希望有「[帶著您學習的 React 線上綜合教材](https://4cats.teachable.com/courses/24hrs-react-101)」,也歡迎您支持我們,目前***早鳥優惠價 50%off***。 17 | 18 | 其它聯絡我的方式: 19 | 20 | - Email: shiningjason1989@gmail.com 21 | - Slack: [加入 softnshare 社群](https://softnshare.wordpress.com/slack/),私訊 ***shiningjason1989*** 22 | - [建立 issue](https://github.com/shiningjason1989/react-build-systems-tutorial/issues/new),歡迎你在 github 上與我討論***任何相關問題***:) 23 | --------------------------------------------------------------------------------