# Redux + React 環境建置 ###### tags: `redux` `react.js` `javaScript` `font-end` > [time= 2019 11 08 ] > 原文 & 參考: > https://chentsulin.github.io/redux/docs/basics/UsageWithReact.html <br> 使用 Mac 建置環境: 找一個你想建立此專案的位置 此範例目錄 `~/你的本機路徑/redux-base/` ## 建置環境 *終端機目前位置`~/你的本機路徑/redux-base/`* npm版本: ```= $ npm -- version ``` >*6.9.0* <br><br><br> 初始化環境: ```= $ npm init -y ``` <br><br><br> 安裝 webpack 和 webpack cli: ```= $ npm i webpack webpack-cli --save-dev ``` >*webpack@4.41.2* >*webpack-cli@3.3.10* <br><br><br> 現在開啟 `package.json` 配置 scripts: 將 ```= "scripts": { "test": "echo \"Error: no test specified\" && exit 1" } ``` 改成 ```javascript= "scripts": { "dev": "webpack --mode development ./project/frontend/src/index.js --output ./project/frontend/static/frontend/main.js", "build": "webpack --mode production ./project/frontend/src/index.js --output ./project/frontend/static/frontend/main.js" } ``` <br><br><br> 安裝babel來編譯我們的代碼: ```= $ npm i @babel/core babel-loader @babel/preset-env @babel/preset-react babel-plugin-transform-class-properties --save-dev ``` >*babel-loader@8.0.6* >*@babel/preset-react@7.6.3* >*@babel/core@7.6.4* >*@babel/preset-env@7.6.3* >*babel-plugin-transform-class-properties@6.24.1* <br><br><br> 安裝 React 和 prop-types: ```= $ npm i react react-dom prop-types --save-dev ``` >*prop-types@15.7.2* >*react@16.11.0* >*react-dom@16.11.0* <br><br><br> 配置Babel,新增一個檔案命名為`.babelrc` 在裡面寫入: ```bable= { "presets": [ "@babel/preset-env", "@babel/preset-react" ], "plugins": [ "transform-class-properties" ] } ``` <br><br><br> 配置babel-loader,新增一個檔案命名為`webpack.config.js` 在裡面寫入: ```javascript= module.exports = { module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: "babel-loader" } } ] } }; ``` <br><br><br> 安裝 rudux 穩定版本 ``` npm install --save redux ``` > *redux@4.0.4* <br> 大多數情況,你也會需要 React 的綁定和開發者工具 ``` npm install --save react-redux npm install --save-dev redux-devtools ``` <br><br><br> 都安裝完成後目錄結構會是這樣 ``` redux-base ├──node_modules ├──.babelrc ├──package.json ├──package-lock.json └──webpack.config.js ``` <br><br><br> ## 開始選寫基礎的 Redux *終端機目前位置`~/你的本機路徑/redux-base/`* 先建立選寫程式的目錄結構 ``` $ mkdir -p ./project/frontend/static/frontend $ mkdir -p ./project/frontend/src/ ``` ``` redux-base ├──node_modules ├──project │ └──fontend │ ├──src │ └──static │ └──frontend │ ├──.babelrc ├──package.json ├──package-lock.json └──webpack.config.js ``` <br> 新增一個檔案命名為 `./project/frontend/index.html` ```htmlmixed= <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css"> <title>Redux</title> </head> <body> <section class="section"> <div class="container"> <div id="root" class="columns"> <!-- React --> </div> </div> </section> </body> <script src="./static/frontend/main.js"></script> </html> ``` `main.js` 在建置 webpack 時才會產生 <br><br><br> ### Actions 新增一個檔案命名為 `./project/frontend/src/actions.js` ```javascript= /* * action type */ export const ADD_TODO = 'ADD_TODO' export const TOGGLE_TODO = 'TOGGLE_TODO' export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER' /* * 其他常數 */ export const VisibilityFilters = { SHOW_ALL: 'SHOW_ALL', SHOW_COMPLETED: 'SHOW_COMPLETED', SHOW_ACTIVE: 'SHOW_ACTIVE' } /* * action creator */ export function addTodo(text) { return { type: ADD_TODO, text } } export function toggleTodo(index) { return { type: TOGGLE_TODO, index } } export function setVisibilityFilter(filter) { return { type: SET_VISIBILITY_FILTER, filter } } ``` <br><br><br> ### Reducers 新增一個檔案命名為 `./project/frontend/src/reducers.js` ```javascript= import { combineReducers } from 'redux' import { ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions' const { SHOW_ALL } = VisibilityFilters function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter default: return state } } function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { text: action.text, completed: false } ] case TOGGLE_TODO: return state.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }) } return todo }) default: return state } } const todoApp = combineReducers({ visibilityFilter, todos }) export default todoApp ``` <br><br><br> ### Store 新增一個檔案命名為 `./project/frontend/src/App.js` ```javascript= import { createStore } from 'redux' import todoApp from './reducers' import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions' let store = createStore(todoApp) // 記錄初始 state console.log(store.getState()) // 每次 state 變更,就記錄它 // 記得 subscribe() 會回傳一個用來撤銷 listener 的 function let unsubscribe = store.subscribe(() => console.log(store.getState()) ) // Dispatch 一些 action store.dispatch(addTodo('Learn about actions')) store.dispatch(addTodo('Learn about reducers')) store.dispatch(addTodo('Learn about store')) store.dispatch(toggleTodo(0)) store.dispatch(toggleTodo(1)) store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED)) // 停止監聽 state 的更新 unsubscribe() ``` <br><br><br> webpack 入口 新增一個檔案命名為 `./project/frontend/src/index.js` ```javascript= import App from "./App"; ``` <br><br><br> 程式都寫好後,下指令 ``` $ npm run dev ``` 執行完會在 `./frontend/static/frontend/` 新增一個 `main.js` 目錄結構 ``` redux-base ├──node_modules ├──project │ └──frontend │ ├──src │ │ ├──actions.js │ │ ├──App.js │ │ ├──reducers.js │ │ └──index.js │ │ │ ├──static │ │ └──frontend │ │ └──main.js │ │ │ └──index.html │ ├──.babelrc ├──package.json ├──package-lock.json └──webpack.config.js ``` <br><br><br> 都沒問題後,用 Chrome 開啟 `index.html`,到 Consol 就可以看到 `Store` 所執行的結果 ![](https://i.imgur.com/xwZaPam.png) <br><br><br> ## 搭配 React 運用 把 `./project/frontend/src/` 目錄裡的檔案都**刪儲** *(你要另開資料夾備份起來也可以)* 建立選寫程式的目錄結構 ``` $ mkdir -p ./project/frontend/src/components $ mkdir -p ./project/frontend/src/containers $ mkdir -p ./project/frontend/src/actions $ mkdir -p ./project/frontend/src/reducers ``` ``` redux-base ├──node_modules ├──project │ └──frontend │ ├──src │ │ ├──actions │ │ ├──components │ │ ├──containers │ │ └──reducers │ │ │ ├──static │ │ └──frontend │ │ └──main.js │ │ │ └──index.html │ ├──.babelrc ├──package.json ├──package-lock.json └──webpack.config.js ``` <br><br><br> ### 進入點 新增 `./project/frontend/src/index.js` ```javascript= import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' import { createStore } from 'redux' import todoApp from './reducers' import App from './components/App' let store = createStore(todoApp) render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ) ``` <br><br><br> ### Action Creator 新增 `./project/frontend/src/actions/index.js` ```javascript= let nextTodoId = 0 export const addTodo = (text) => { return { type: 'ADD_TODO', id: nextTodoId++, text } } export const setVisibilityFilter = (filter) => { return { type: 'SET_VISIBILITY_FILTER', filter } } export const toggleTodo = (id) => { return { type: 'TOGGLE_TODO', id } } ``` ### Reducer 新增 `reducers/todos.js` ```javascript= const todo = (state = {}, action) => { switch (action.type) { case 'ADD_TODO': return { id: action.id, text: action.text, completed: false } case 'TOGGLE_TODO': if (state.id !== action.id) { return state } return Object.assign({}, state, { completed: !state.completed }) default: return state } } const todos = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return [ ...state, todo(undefined, action) ] case 'TOGGLE_TODO': return state.map(t => todo(t, action) ) default: return state } } export default todos ``` <br><br><br> 新增 `reducers/visibilityFilter.js` ```javascript= const visibilityFilter = (state = 'SHOW_ALL', action) => { switch (action.type) { case 'SET_VISIBILITY_FILTER': return action.filter default: return state } } export default visibilityFilter ``` <br><br><br> 新增 `reducers/index.js` ```javascript= import { combineReducers } from 'redux' import todos from './todos' import visibilityFilter from './visibilityFilter' const todoApp = combineReducers({ todos, visibilityFilter }) export default todoApp ``` <br><br><br> ### Presentational Component 新增 `components/Todo.js` ```javascript= import React, { PropTypes } from 'react' const Todo = ({ onClick, completed, text }) => ( <li onClick={onClick} style={{ textDecoration: completed ? 'line-through' : 'none' }} > {text} </li> ) Todo.propTypes = { onClick: PropTypes.func.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired } export default Todo ``` <br><br><br> 新增 `components/TodoList.js` ```javascript= import React, { PropTypes } from 'react' import Todo from './Todo' const TodoList = ({ todos, onTodoClick }) => ( <ul> {todos.map(todo => <Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} /> )} </ul> ) TodoList.propTypes = { todos: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.number.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired }).isRequired).isRequired, onTodoClick: PropTypes.func.isRequired } export default TodoList ``` <br><br><br> 新增 `components/Link.js` ```javascript= import React, { PropTypes } from 'react' const Link = ({ active, children, onClick }) => { if (active) { return <span>{children}</span> } return ( <a href="#" onClick={e => { e.preventDefault() onClick() }} > {children} </a> ) } Link.propTypes = { active: PropTypes.bool.isRequired, children: PropTypes.node.isRequired, onClick: PropTypes.func.isRequired } export default Link ``` <br><br><br> 新增 `components/Footer.js` ```javascript= import React from 'react' import FilterLink from '../containers/FilterLink' const Footer = () => ( <p> Show: {" "} <FilterLink filter="SHOW_ALL"> All </FilterLink> {", "} <FilterLink filter="SHOW_ACTIVE"> Active </FilterLink> {", "} <FilterLink filter="SHOW_COMPLETED"> Completed </FilterLink> </p> ) export default Footer ``` <br><br><br> 新增 `components/App.js` ```javascript= import React from 'react' import Footer from './Footer' import AddTodo from '../containers/AddTodo' import VisibleTodoList from '../containers/VisibleTodoList' const App = () => ( <div> <AddTodo /> <VisibleTodoList /> <Footer /> </div> ) export default App ``` <br><br><br> ### Container Component 新增 `containers/VisibleTodoList.js` ```javascript= import { connect } from 'react-redux' import { toggleTodo } from '../actions' import TodoList from '../components/TodoList' const getVisibleTodos = (todos, filter) => { switch (filter) { case 'SHOW_ALL': return todos case 'SHOW_COMPLETED': return todos.filter(t => t.completed) case 'SHOW_ACTIVE': return todos.filter(t => !t.completed) } } const mapStateToProps = (state) => { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) } } const mapDispatchToProps = (dispatch) => { return { onTodoClick: (id) => { dispatch(toggleTodo(id)) } } } const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList) export default VisibleTodoList ``` <br><br><br> 新增 `containers/FilterLink.js` ```javascript= import { connect } from 'react-redux' import { setVisibilityFilter } from '../actions' import Link from '../components/Link' const mapStateToProps = (state, ownProps) => { return { active: ownProps.filter === state.visibilityFilter } } const mapDispatchToProps = (dispatch, ownProps) => { return { onClick: () => { dispatch(setVisibilityFilter(ownProps.filter)) } } } const FilterLink = connect( mapStateToProps, mapDispatchToProps )(Link) export default FilterLink ``` <br><br><br> ### 其它 Component 新增 `containers/AddTodo.js` ```javascript= import React from 'react' import { connect } from 'react-redux' import { addTodo } from '../actions' let AddTodo = ({ dispatch }) => { let input return ( <div> <form onSubmit={e => { e.preventDefault() if (!input.value.trim()) { return } dispatch(addTodo(input.value)) input.value = '' }}> <input ref={node => { input = node }} /> <button type="submit"> Add Todo </button> </form> </div> ) } AddTodo = connect()(AddTodo) export default AddTodo ``` 程式都寫好後,下指令 ``` $ npm run dev ``` <br> 目錄結構會如下 ``` redux-base ├──node_modules ├──project │ └──frontend │ ├──src │ │ ├──actions │ │ │ └──index.js │ │ │ │ │ ├──components │ │ │ ├──App.js │ │ │ ├──Footer.js │ │ │ ├──Link.js │ │ │ ├──Todo.js │ │ │ └──TodoList │ │ │ │ │ ├──containers │ │ │ ├──AddTodo.js │ │ │ ├──FilterLink.js │ │ │ └──VisibleTodoList │ │ │ │ │ ├──reducers │ │ │ ├──index.js │ │ │ ├──todos.js │ │ │ └──visibilityFilter │ │ │ │ │ └──index.js │ │ │ ├──static │ │ └──frontend │ │ └──main.js │ │ │ └──index.html │ ├──.babelrc ├──package.json ├──package-lock.json └──webpack.config.js ``` 都沒問題後,一樣用 Chrome 開啟 index.html,就可以看到簡單地的 Todo 應用了 <br><br><br>