├── .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 ;
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 ;
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 |
--------------------------------------------------------------------------------