在前端开发中,React、Redux 和 Webpack 是目前最流行的技术栈之一。React 作为视图层的框架,可以帮助开发者快速构建复杂的用户界面。Redux 则用来管理应用的状态,并提供可预测性和一致性。Webpack 是打包工具,可以将各种资源打包成最终的应用程序。在本文中,我们将深入探讨如何使用这三种技术,以 React + Redux + Webpack 为例,开发一个简单的 Todo 应用,并介绍一些关键性的概念和技巧。
项目结构
首先,我们需要创建一个项目。在本文中,我们采用一个简单的目录结构,它包括:
// www.javascriptcn.com code example
- node_modules/
- src/
- components/
- AddTodo.js
- Filter.js
- Todo.js
- TodoList.js
- containers/
- VisibleTodoList.js
- reducers/
- index.js
- todos.js
- visibilityFilter.js
- actions/
- index.js
- todos.js
- visibilityFilter.js
- index.js
- index.html
- main.css
- webpack.config.js
- package.json在上述目录结构中,src 目录是我们的源代码,它包含组件、容器、reducer 和 action 等文件。webpack.config.js 是 Webpack 的配置文件,package.json 则是项目的依赖配置文件。
编写组件
在 React 中,一个组件就是一个 JavaScript 函数或类,它负责渲染特定的 UI 部分。我们的 Todo 应用包含以下四个组件:
AddTodo组件:用于添加新的 Todo 到列表中。TodoList组件:显示列表中的所有 Todo。Todo组件:显示单个 Todo 的详细信息。Filter组件:用于切换显示的 Todo 列表。
下面我们将逐一介绍这些组件的实现。
AddTodo.js
AddTodo 组件渲染一个 input 元素和一个 button 元素,让用户添加新的 Todo。
// www.javascriptcn.com code example
import React from 'react'
const AddTodo = ({ onAddClick }) => {
let input
return (
<div>
<input ref={node => {
input = node
}} />
<button onClick={() => {
onAddClick(input.value)
input.value = ''
}}>
Add Todo
</button>
</div>
)
}
export default AddTodo在上面的代码中,我们使用一个 ref 属性来获取 input 元素的引用,并将其赋值给 input 变量。当用户点击“Add Todo”按钮时,会调用 onAddClick 回调,该回调将会传入输入框中的文本。之后,我们将文本框的值清空。这样,用户就可以一遍又一遍地添加 Todo 了。
Todo.js
Todo 组件渲染一个单独的 Todo 项,包括它的文本内容和一个删除按钮。
// www.javascriptcn.com code example
import React from 'react'
const Todo = ({ onClick, completed, text, onDeleteClick }) => (
<li>
<span
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
onClick={onClick}>
{text}
</span>
<button onClick={onDeleteClick}>Delete</button>
</li>
)
export default Todo在上面的代码中,我们使用了一个 JavaScript 对象作为样式,并将其传递给 span 元素。当用户点击 Todo 时,会调用 onClick 回调,该回调将会标记当前 Todo 项为已完成或未完成。当用户点击删除按钮时,会调用 onDeleteClick 回调,该回调将会从列表中删除当前 Todo。
TodoList.js
TodoList 组件渲染整个 Todo 列表。它从 todos 属性中获取一个包含所有 Todo 的数组,并使用 map 函数将其转换成一个个 Todo 组件。
// www.javascriptcn.com code example
import React from 'react'
import Todo from './Todo'
const TodoList = ({ todos, onTodoClick, onDeleteClick }) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
onDeleteClick={() => onDeleteClick(todo.id)} />
)}
</ul>
)
export default TodoList在上面的代码中,我们使用展开运算符 ...,将 todo 对象的所有属性传递给 Todo 组件,而不是一个一个地传过去。这样做可以减少代码量,并保持代码的简洁性。在 map 函数中,我们还使用了 key 属性,它用于标识每个 Todo 组件,以便 React 可以对其进行高效更新。
Filter.js
Filter 组件渲染三个链接,用于切换 Todo 列表的显示方式。当用户点击某个链接时,会调用 onClick 回调,该回调将会改变当前的过滤条件。
// www.javascriptcn.com code example
import React from 'react'
const Filter = ({ filter, onFilterChange }) => (
<p>
Show:
{' '}
<a href="#"
onClick={e => {
e.preventDefault()
onFilterChange('SHOW_ALL')
}}
style={{
textDecoration: filter === 'SHOW_ALL' ? 'underline' : 'none'
}}>
All
</a>
{', '}
<a href="#"
onClick={e => {
e.preventDefault()
onFilterChange('SHOW_ACTIVE')
}}
style={{
textDecoration: filter === 'SHOW_ACTIVE' ? 'underline' : 'none'
}}>
Active
</a>
{', '}
<a href="#"
onClick={e => {
e.preventDefault()
onFilterChange('SHOW_COMPLETED')
}}
style={{
textDecoration: filter === 'SHOW_COMPLETED' ? 'underline' : 'none'
}}>
Completed
</a>
</p>
)
export default Filter在上面的代码中,我们使用 href 属性和 preventDefault 函数来防止调用默认的链接点击行为。当链接被点击时,我们将调用 onFilterChange 回调函数来更新过滤条件。如果当前链接对应的过滤条件与当前选中的过滤条件匹配,则我们使用 underline 样式来标识链接。
编写容器
在 React 和 Redux 中,容器组件是将组件连接到 Redux store 的组件。容器组件被用于将 Redux 和 React 组件集成在一起。我们的 Todo 应用中包含一个名为 VisibleTodoList 的容器组件,它用于将 TodoList 组件连接到 Redux store。
VisibleTodoList.js
VisibleTodoList 容器组件从 Redux store 中获取 todos 和 visibilityFilter 两个状态。在 componentDidMount 生命周期函数中,它将注册一个 Redux store 的监听器,并在组件卸载时取消注册该监听器。当状态发生改变时,VisibleTodoList 会重新渲染 TodoList 组件,并将过滤条件和需要显示的 Todo 传递给它。
// www.javascriptcn.com code example
import { connect } from 'react-redux'
import { toggleTodo, deleteTodo } 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)
default:
throw new Error('Unknown filter: ' + filter)
}
}
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter),
filter: state.visibilityFilter
}
}
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
},
onDeleteClick: id => {
dispatch(deleteTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList在上面的代码中,我们使用了 connect 函数将 TodoList 组件连接到 Redux store。mapStateToProps 函数将 store 中的状态映射到组件的 props 属性,mapDispatchToProps 函数映射 dispatch 函数。
编写 Reducer 和 Action
在 Redux 中,我们使用 action 来描述发生了什么,使用 reducer 来更新应用的状态。在我们的 Todo 应用中,有两种类型的 action:ADD_TODO 和 TOGGLE_TODO;同时,有一个过滤条件的 action:SET_VISIBILITY_FILTER。
actions/todos.js
ADD_TODO action 表示添加一个新的 Todo。因此,它需要一个唯一的 id 和一个 Todo 的 text。
let nextTodoId = 0
export const addTodo = text => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
})TOGGLE_TODO action 表示切换 Todo 的状态(completed 或未 completed)。因此,它需要一个 Todo 的 id。
export const toggleTodo = id => ({
type: 'TOGGLE_TODO',
id
})DELETE_TODO action 表示删除一个 Todo。因此,它需要一个 Todo 的 id。
export const deleteTodo = id => ({
type: 'DELETE_TODO',
id
})actions/visibilityFilter.js
SET_VISIBILITY_FILTER action 表示设置显示的过滤条件。因此,它需要一个过滤条件值(SHOW_ALL、SHOW_ACTIVE 或 SHOW_COMPLETED)。
export const setVisibilityFilter = filter => ({
type: 'SET_VISIBILITY_FILTER',
filter
})reducers/todos.js
todos reducer 用于接收 ADD_TODO、TOGGLE_TODO 和 DELETE_TODO 三种 action,并返回新的 todos 状态。在 ADD_TODO action 中,我们创建一个新的 Todo 对象,将其添加到列表中。在 TOGGLE_TODO action 中,我们遍历列表,找到要切换的 Todo,并将其 completed 属性切换为相反的值。在 DELETE_TODO action 中,我们遍历列表,找到要删除的 Todo,并将其从列表中删除。
// www.javascriptcn.com code example
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
]
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
)
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.id)
default:
return state
}
}
export default todosreducers/visibilityFilter.js
visibilityFilter reducer 用于接收 SET_VISIBILITY_FILTER action,并更新过滤条件。
// www.javascriptcn.com code example
const visibilityFilter = (state = 'SHOW_ALL', action) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
export default visibilityFilter编写入口文件
最后,我们需要编写一个入口文件,将我们的组件和容器组件集成在一起,并渲染到页面中。
index.js
// www.javascriptcn.com code example
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './components/App'
import rootReducer from './reducers'
const store = createStore(rootReducer)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)在上面的代码中,我们使用 createStore 函数创建一个 Redux store,并将 rootReducer 传递给它。然后,我们将 store 传递给 Provider 组件,并将 App 组件渲染到页面中(这里 App 组件包含了所有的组件和容器组件)。
编写 Webpack 配置
最后,我们需要定义一个 Webpack 配置文件,用于将 src 目录下的代码打包成一个 bundle.js 文件,并将其引入到 index.html 文件中。
webpack.config.js
// www.javascriptcn.com code example
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
devServer: {
contentBase: path.join(__dirname, 'public'),
compress: true,
port: 9000
}
}在上面的代码中,我们定义了入口文件,输出文件,模块定义和 devServer 选项。其中,我们使用了 babel-loader 和 @babel/preset-env 和 @babel/preset-react 两个 Babel 插件,来将我们的 ES6 和 JSX 代码转换成普通的 JavaScript。我们还使用了 style-loader 和 css-loader 两个插件来处理 CSS 文件。最后,我们定义了 devServer 选项,用于运行一个开发服务器,方便我们开发和测试。
整合所有代码
在上述的代码编写完成后,我们可以将它们整合起来,来创建我们的 Todo 应用。
src/index.js
// www.javascriptcn.com code example
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './components/App'
import rootReducer from './reducers'
const store = createStore(rootReducer)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)src/components/App.js
// www.javascriptcn.com code example
import React from 'react'
import AddTodo from './AddTodo'
import Filter from './Filter'
import VisibleTodoList from '../containers/VisibleTodoList'
import '../main.css'
const App = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Filter />
</div>
)
export default Appsrc/components/AddTodo.js
// www.javascriptcn.com code example
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'
const AddTodo = ({ dispatch }) => {
let input
return (
<div>
<input ref={node => {
input = node
}} />
<button onClick={() => {
dispatch(addTodo(input.value))
input.value = ''
}}>
Add Todo
</button>
</div>
)
}
export default connect()(AddTodo)src/components/Filter.js
// www.javascriptcn.com code example
import React from 'react'
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
const Filter = ({ filter, dispatch }) => (
<p>
Show:
{' '}
<a href="#"
onClick={e => {
e.preventDefault()
dispatch(setVisibilityFilter('SHOW_ALL'))
}}
style={{
textDecoration: filter === 'SHOW_ALL' ? 'underline' : 'none'
}}>
All
</a>
{', '}
<a href="#"
onClick={e => {
e.preventDefault()
dispatch(setVisibilityFilter('SHOW_ACTIVE'))
}}
style={{
textDecoration: filter === 'SHOW_ACTIVE' ? 'underline' : 'none'
}}>
Active
</a>
{', '}
<a href="#"
onClick={e => {
e.preventDefault()
dispatch(setVisibilityFilter('SHOW_COMPLETED'))
}}
style={{
textDecoration: filter === 'SHOW_COMPLETED' ? 'underline' : 'none'
}}>
Completed
</a>
</p>
)
const mapStateToProps = state => {
return {
filter: state.visibilityFilter
}
}
export default connect(mapStateToProps)(Filter)src/components/Todo.js
// www.javascriptcn.com code example
import React from 'react'
const Todo = ({ onClick, completed, text, onDeleteClick }) => (
<li>
<span
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
onClick={onClick}>
{text}
</span>
<button onClick={onDeleteClick}>Delete</button>
</li>
)
export default Todosrc/components/TodoList.js
// www.javascriptcn.com code example
import React from 'react'
import Todo from './Todo'
const TodoList = ({ todos, onTodoClick, onDeleteClick }) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
onDeleteClick={() => onDeleteClick(todo.id)} />
)}
</ul>
)
export default TodoListsrc/containers/VisibleTodoList.js
// www.javascriptcn.com code example
import { connect } from 'react-redux'
import { toggleTodo, deleteTodo } 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)
default:
throw new Error('Unknown filter: ' + filter)
}
}
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter),
filter: state.visibilityFilter
}
}
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
},
onDeleteClick: id => {
dispatch(deleteTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoListsrc/reducers/index.js
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
export default combineReducers({
todos,
visibilityFilter
})src/reducers/todos.js
// www.javascriptcn.com code example
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
]
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
)
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.id)
default:
return state
}
}
export default todossrc/reducers/visibilityFilter.js
// www.javascriptcn.com code example
const visibilityFilter = (state = 'SHOW_ALL', action) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
export default visibilityFiltersrc/index.html
// www.javascriptcn.com code example
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Todo List</title>
</head>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>src/main.css
// www.javascriptcn.com code example
body {
font-family: Arial, sans-serif;
background-color: #EEE;
}
input[type=text] {
padding: 10px;
font-size: 16px;
border-radius: 5px;
border: 1px solid #CCC;
outline: none;
}
button {
padding: 10px 20px;
font-size: 16px;
background-color: #0066CC;
color: #FFF;
border-radius: 5px;
border: none;
cursor: pointer;
}
ul {
list-style: none;
padding: 0;
}
li {
padding: 10px;
font-size: 16px;
border-bottom: 1px solid #CCC;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
a {
color: #0066CC;
text-decoration: none;
cursor: pointer;
}
a:hover {
text-decoration: underline;
}结语
在本文中,我们介绍了如何使用 React、Redux 和 Webpack 来开发一个完整的 Todo 应用程序。通过这个实例,我们深入学习了组件、容器、reducer 和 action 等关键性概念和技巧。同时,我们还学习了如何通过 Webpack 来打包和部署我们的应用程序。希望通过这篇文章的学习,读者们能够更加深入地掌握 React、Redux 和 Webpack 的使用方法,并能够使用它们来开发更为复杂的应用程序。
来源:JavaScript中文网 ,转载请注明来源 https://www.javascriptcn.com/post/67d7fd2fa941bf7134e43481