Project Demo:
Project Online Address:
An interested friends can order on line address have a look at: http://116.62.119.230:8000
Results demonstrate
Preface:
After learning React for some time, I wanted to check my ability to master React, so I decided to imitate the wechat mini program of Xiaopeng Automobile and write React by myself. Redux can be used as a redux project, and redux can be used as a redux project. Redux can be used as a redux project, and redux can be used as a redux project. Also welcome some bigwigs to point out the problems in this article
View before the prompt
Because of the length, most of the code listed in this article is only extracted core code to explain, not a complete JS file code, if interested in the code partners can view the source code
Project introduction
- Technology stack: React Hooks + Redux + Koa
- The front-end page is written using React Hooks, Koa is used to build the background, Redux is used to manage the data flow, styled components are used for the whole project, and React-Router V6 is used for routing configuration. Some new features may be involved
- The project architecture is based on the React Hooks and Immutable data streams, using baseUI and API components.
The project structure
├─ Server // Back End ├─Data // Data Index.js ├─ SRC ├─ API // Network Request code, Tool class functions and related Configuration ├─ Assets // Font Configuration and Global Style ├─baseUI // Basic UI ├─ ├─ exercises // ├─pages // Bass Exercises // ├─ exercises // ├─ exercises // bass Exercises // Bass Exercises // ├─ exercises // Bass Exercises // Bass Exercises // Main.jsx // import fileCopy the code
The front part
The routing configuration
This project uses React – Router v6 to configure routes. Use useRoutes instead of v5’s react-router-config. If you’re not familiar with new features like useRoutes, check out the link here.
Configuration ALLRoutes
- On the Routes configuration interface, the routes/index.js code is as follows:
import React, { lazy, Suspense } from 'react'; import HomeLayout from '.. /layouts/HomeLayout'; import NotFound from '.. /layouts/NotFound'; import { Navigate } from 'react-router-dom'; const SuspenseComponent = (Component) => (props) => { return ( <Suspense fallback={null}> <Component {... props}></Component> </Suspense> ) } const Buycar = lazy(() => import(".. /pages/buycar")); const Home = lazy(() => import(".. /pages/home")) const My = lazy(() => import(".. /pages/my")) const Find = lazy(() => import(".. /pages/find")) const TryCar = lazy(() => import(".. /pages/trycar")) const FindDetail = lazy(() => import(".. /pages/find/findDetail")) const HomeDetail = lazy(() => import(".. /pages/home/detail")) const Shopcar = lazy(() => import(".. /components/main/shopcar/Shopcar")) export default [ { path: "/", element: <HomeLayout />, children: [ { path: "/", element: <Navigate to="/home" /> }, { path: "/home", element: <Suspense fallback={null}><Home></Home></Suspense>, children: [ { path: ":id", element: <Suspense fallback={null} ><HomeDetail></HomeDetail></Suspense> }, { path: 'shopcar', element: <Suspense fallback={null} ><Shopcar></Shopcar></Suspense> } ] }, { path: "/buycar", element: <Suspense fallback={null}><Buycar></Buycar></Suspense> }, { path: "/my", element: <Suspense fallback={null}><My></My></Suspense> }, { path: "/find", element: <Suspense fallback={null}><Find></Find></Suspense>, children: [ { path: ":id", element: <Suspense fallback={null} ><FindDetail></FindDetail></Suspense> } ] }, { path: "/try", element: <Suspense fallback={null}><TryCar></TryCar></Suspense> }, { path: "*", element: <NotFound /> } ] } ];Copy the code
The component must be introduced first, and then packaged by SuspenseComponent, and placed in SuspenseComponent, so as to realize lazy loading and avoid the program loading all the components of a single page application at one time, resulting in a long time of no response.
Introduced in app.jsx
- The code for app.jsx is as follows:
import React from 'react';
import { useRoutes } from 'react-router';
import ALLRoutes from './routers/index'
import { Provider } from "react-redux"
import store from './store';
import { GlobalStyle } from './style';
function App() {
let routes = useRoutes(ALLRoutes)
return (
<Provider store={store}>
<GlobalStyle ></GlobalStyle>
<div>
{
routes
}
</div>
</Provider>
)
}
export default App
Copy the code
Add useRouter to the react-router and add routes/index.js to the react-router. Using useRouter instead of traditional code to manually route each route has two advantages:
- JSX makes the home page code of app.jsx look concise, because the route is configured on the Routes interface, so it makes app.jsx look concise.
- Route configuration becomes more convenient and concise. The route configuration is completed in array mode, and the route is valid through useRoute.
Redux data flow management
Before we use Redux to manage data, I’d like to use a diagram to make Redux easier for beginners to understand.
For more details, you can see Teacher Ruan’s blog Redux tutorial
Set up store central warehouse
The store/index.js code is as follows:
import thunk from 'redux-thunk';
import { createStore, compose, applyMiddleware } from 'redux';
import reducer from "./reducer";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));
export default store;
Copy the code
Configuration Introduction:
- Redux-thunk enables asynchronous actions in REdux.
- ApplyMiddleware allows Redux plus application middleware to make Redux-Thunk work.
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
Will enable the Redux plug-in to perform some debugging of the Redux repository. Thus in the development process through the Redux plug-in to observe the state changes in the Redux warehouse, and do some debugging.
The store/reducer.js code is as follows:
import { combineReducers } from 'redux'; import { reducer as homeReducer } from '.. /pages/home/store/index' import { reducer as activeReducer } from ".. /layouts/store/index" import { reducer as trycarReducer } from ".. /pages/trycar/store/index" import { reducer as buycarReducer } from ".. /pages/buycar/store/index" import { reducer as myReducer } from ".. /pages/my/store/index" import { reducer as findReducer } from '.. /pages/find/store/index'; export default combineReducers({ home: homeReducer, active: activeReducer, trycar: trycarReducer, buycar: buycarReducer, my: myReducer, find: findReducer });Copy the code
This is the way to merge branch Redux. These imported Reducer are all a store repository of our page-level components, which are also branches. Finally, export Default combineReducers({}) was used to merge the branches, thus forming the whole reducer, Passed to const store = createStore(Reducer, composeEnhancers(applyMiddleware(thunk))); To create the Redux data warehouse.
Set up branch warehouse
├─ Store ├─ actionWorks.js // Create Action ├─constants. Js // Declare unique identifiers, control switch Case ├─index.js // Merge the other three JS files As an entry to the branch file, passed to redux central repository ├─reducer.js // Responsible for managing the state of the sub-branchesCopy the code
SRC /pages/ constans
export const CHANGE_HOMEDATA = "changeHomeData"
export const CHANGE_SHOPCAR = "CHANGE_SHOPCAR"
export const DECREMENT_NUM = "DECREMENT_NUM"
export const INCREMENT_NUM = "INCREMENT_NUM"
export const CHANGE_CHECK = "CHANGE_CHECK"
export const CHANGE_ALL = "CHANGE_ALL"
export const COUNT_ALL = "COUNT_ALL"
Copy the code
These custom constant actions are judged as unique values by the Reducer function, which the Reducer uses to perform different REdux data operations. These user-defined constants should be specified and output. If they are used in other files, automatic reminders will pop up to prevent errors in my handwriting, which will lead to errors in the type of action. Therefore, reducer will not operate but carry out default default operations.
SRC/pages/hone actionCreators code is as follows:
import * as actionType from './constants.js'; import { reqhome } from '.. /.. /.. / API/index 'export const InsertHomeData = (data) = > ({/ / the console. The log (" in success..." ); type: actionType.CHANGE_HOMEDATA, data: data }) export const getHomeData = () => { return (dispatch) => { reqhome() .then((res) => { Dispatch (InsertHomeData(res.data.data))}). Catch ((e) => {console.log(' error '); }) } } export const InsertShopcar = (data) => ({ type: actionType.CHANGE_SHOPCAR, data: data }) export const IncrementShopcarNum = (data) => ({ type: actionType.INCREMENT_NUM, data: data }) export const DecrementShopcarNum = (data) => ({ type: actionType.DECREMENT_NUM, data: data }) export const changeCheck = (data) => ({ type: actionType.CHANGE_CHECK, data: data }) export const changeAll = (data) => { return (dispatch) => { // total? dispatch({ type: actionType.CHANGE_ALL, data: data }) dispatch(countAll(data)) } } export const countAll = (data) => ({ type: actionType.COUNT_ALL, data: data })Copy the code
{type:, data:} import {reqHOME} from ‘.. /.. /.. / API /index’, which is a JSON file of the server wrapped in Ajax. We then declare the function reqhome to request back-end interface data via the wrapped AXIOS interface. The ajax wrapper code is as follows:
import axios from 'axios'; Axios. Defaults. BaseURL = 'http://127.0.0.1:9999'; export default function Ajax(url, data = {}, type = 'GET') { return new Promise((resolve, rejet) => { let Promise; if (type === 'GET') { Promise = axios.get(url, { params: data }) } else { Promise = axios.post(url, { params: data }) } Promise.then((response) => { resolve(response); }). Catch ((error) => {console.error(" data request exception!" , error) }) }) }Copy the code
Pass three parameters. The url must be passed to access the back-end interface that the back-end node allows the application to use to fetch JSON data.
SRC /pages/hone/reducer code is as follows:
// import shopcar from '.. /.. /shopcar'; import * as actionTypes from './constants'; const defaultstate = { homedata: [], shopcar: [], totalprice: 0, checkall: false } const reducer = (state = defaultstate, action) => { const { type, data } = action let temp = null; let index = null; let checkalls = null; Switch (type) {case Actiontypes. CHANGE_HOMEDATA: // Initialize data on the home interface // console.log(" enter reducer") return {... state, homedata: data } case actionTypes.COUNT_ALL: let total = 0; console.log(data, "---------------"); console.log(data.length); Console. log(" entered COUNT_ALL"); for (let i = 0; i < data.length; i++) { if (data[i].check ! = false) { total += data[i].num * (data[i].prc * 1); } } return { ... state, totalprice: total } default: return state; } } export default reducer;Copy the code
There are only some reducer methods shown here. First, initialize defaultState, initialize the variables needed by the home branch data warehouse in REdux, and then operate the data warehouse via switch/case based on the action. Type passed in.
SRC /pages/hone/index
import reducer from "./reducer";
import * as constants from "./constants"
import * as actionCreators from "./actionCreators"
export {
reducer,
constants,
actionCreators
}
Copy the code
Import the above three files and merge the output to the central store for branch merging.
Shopping cart function
Add shopping cart data to redux.home.shopcar
Const inputShopcar = () => {let shopcarItem = {desc: desc, id: id, picUrl: picUrl, PRC: prc, size: productSize[checkIndex - 1].size, num: inputNumber, check: false } setItemToShopcar(shopcaritem); } -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / definition of container components: const mapStateToPorps = (state) => { return { shopcar: state.home.shopcar } } const mapDispatchToProps = (dispatch) => { return { setItemToShopcar(item) { dispatch(Action.InsertShopcar(item)); } } } export default connect(mapStateToPorps, mapDispatchToProps)(memo(ShopcarTable))Copy the code
Focus on the core:
- The first step is to create a high-level component through CONNECT that communicates with REUDX to retrieve the value. The data and methods obtained by REUDX are then passed to the UI component, namely ShopcarTable, through mapStateToPorps and mapDispatchToProps through two parameters of the higher-order component.
- In the inputShopcar method, which is the setItemToShopcar method passed by the higher-order component, we created an object artificially before doing so
let shopcaritem = { desc: desc, id: id, picUrl: picUrl, prc: prc, size: productSize[checkIndex - 1].size, num: inputNumber, check: false }
The object is passed to the setItemToShopcar method in actionCreator, which is encapsulated as an action and then sent to reducer for call.
export const InsertShopcar = (data) => ({
type: actionType.CHANGE_SHOPCAR,
data: data
})
Copy the code
- Make logical judgment in reducer and write corresponding operations to change the redux state according to the corresponding type.
CHANGE_SHOPCAR: // Add the shopping cart, and check whether the unique id is temp = state.shopcar; // Add the shopping cart to the case actionTypes. index = state.shopcar.findIndex(state => state.id == data.id); if (index ! Temp [index]. Num = state.shopcar[index]. Num + data.num; temp[index]. return { ... Shopcar temp = [...state. Shopcar, data]; shopcar temp = [...state. Shopcar, data]; return { ... state, shopcar: [...temp] } }Copy the code
Specify which operation to do by passing the unique value type, which is the operation of the code above. First get the previous shopping cart data state. Shopcar (in case adding another previous data disappears). It then iterates through the ID attribute of the previous shopping cart data object. If the previous data has a property identical to that of the passed object data, it does not add a new object to the state. Shopcar array, but adds the number of indexes in the array to the index found in the response. If the same ID cannot be queried, it indicates that the added object is a new object and is directly added to the shopping cart.
Shopping cart interface
Key points: Because we use Redux to save the shopping cart data, the modification of data is no longer the traditional modification of useState to change the state. Instead, we call the higher-order componenter pass method in the component -> method create action-> Reducer accept the action and return the new state -> the component accept the new state. Re-render. Therefore, we made all changes to the shopping cart in reducer.
SRC/Components /shop.jsx: SRC/Components /shop.jsx: SRC/Components /shop.jsx: SRC/Components /shop.jsx: SRC/Components /shop.jsx
const StateToPorps = (state) => { return { shopcar: state.home.shopcar, totalprice: state.home.totalprice, checkall: State.home. checkall}} const DispatchToProps = (dispatch) => {return {decreasenum(id) {// To perform subtracting, pass id dispatch(Action.DecrementShopcarNum(id)); }, incrementnum (id) {/ / add operation, need to pass id dispatch (Action. IncrementShopcarNum (id)); }, changeCheck(id) {// To check the operation, pass id dispatch(action.changecheck (id)); }, changeAll(shopcar) {// Dispatch shopCAR (action.changeAll (shopcar)); }, countAll(shopcar) {// Select shopCAR dispatch(action.countall (shopcar)); } } } export default connect(StateToPorps, DispatchToProps)(memo(Shopcar))Copy the code
The StateToPorps parameter passes state: ShopCAR: is an array in which all elements are a commodity information object. Totalprice: is a Number that represents the prices of all checked items in the current ShopCar. Checkall: is a Boolean value indicating whether the current state is full or not
Page I will not introduce more, interested can view the source code, let’s focus on Redux shopping cart modification.
Add and subtract and check operations
All three operations are essentially the same: based on the id passed in, you iterate through the shopping cart array, find the object with the corresponding ID, and modify that object.
DecrementShopcarNum(),IncrementShopcarNum(),changeCheck() generates actions and, through dispatch(Action), triggers the methods in the Reducer. Take changeCheck as an example, in Reducer:
case actionTypes.CHANGE_CHECK: temp = state.shopcar; index = temp.findIndex(temp => temp.id == data); temp[index].check = ! temp[index].check; return { ... state, shopcar: [...temp] }Copy the code
Go through the loop, look for the ID property, find the item object and change the check property, because check is Boolean so just take! The action completes the modification. I’m not going to do the same thing with addition and subtraction.
ChangeAll and countAll operations
The code in reducer is as follows:
case actionTypes.CHANGE_ALL: checkalls = ! state.checkall; console.log(data[0]); for (let i = 0; i < data.length; i++) { data[i].check = checkalls; } return { ... state, shopcar: data, checkall: checkalls } case actionTypes.COUNT_ALL: let total = 0; for (let i = 0; i < data.length; i++) { if (data[i].check ! = false) { total += data[i].num * (data[i].prc * 1); } } return { ... state, totalprice: total }Copy the code
Unlike the add and subtract methods above, which iterate over an object to make changes, countAll and changeAll iterate over the entire array to make changes to each of its objects, because they work not on a single shopping cart, but on the entire shopping cart.
In the changeAll method: firstly, the variable checkalls is used to obtain the state. Checkall selection state is not, and by traversing the ShopCar array, the check attribute of each object in the array is changed to the state.
In the countAll method: also declare a variable, and then iterate over every object in the ShopCar array to judge if the check property is true, add the quantity and price to count and return.
Think about:
The methods and implementations of shopping cart, such as add, subtract, check and select, are all triggered by onClick events, so how does countAll trigger and monitor changes all the time? The answer is the React lifecycle, which uses useEffect to listen for changes to an object. Without further ado, code:
useEffect(() => {
countAll(shopcar);
}, [shopcar])
Copy the code
Listen to the ShopCar array passed from the container component, every change of ShopCar will cause countAll to re-execute, so no matter the operation of adding or removing all boxes, the ShopCar array will be changed eventually, so useEffect is used to listen to the change of ShopCar. The optimal solution is to recalculate the total price once it changes.
Code optimization
Application scenario: In our daily life, we often visit Taobao. If we want to buy snacks, we can input the key words and refresh the data of several snacks, but not all the data will be loaded at one time. But when we refresh to the bottom, the data request is made again, and the load continues, so as not to request a lot of data at once. Let’s do the simulation with Redux. The SRC /pages/find code is as follows:
const mapStateToPorps = (state) => {
return {
findData: state.find.findData
}
}
const mapDispatchToProps = (dispatch) => {
return {
getFindDatas(page) {
dispatch(actions.getFindData(page))
},
getDetailDatas(item) {
dispatch(actions.getDetailCreate(item))
}
}
}
export default connect(mapStateToPorps, mapDispatchToProps)(memo(Find))
Copy the code
This is similar to the home interface code, except that the function getFindData uses to get the find interface data passes a parameter page, which is passed as a parameter to the URL when requesting the background URL.
export const reqfinddate = (page) => {
return Ajax(`/finddata/${page}`)
}
Copy the code
The server/index.js code is as follows:
const FindData = require('./Data/FindData/FindData.json')
router.get('/finddata/:page', async (ctx) => {
let limit = 10
let { page } = ctx.params
let { active } = FindData
let list2 = active.slice((page - 1) * limit, page * limit)
ctx.response.body = {
success: true,
data: {
newLists: list2,
}
}
})
Copy the code
Limit: indicates how many pieces of data are transmitted to the front-end at a time. Active: All the data we need to deconstruct from the JSON file. Page: indicates the page number and the data to be transmitted to the front end according to the page cutting.
src/pages/find/store/actionCreators
export const ChangeFindData = (data) => { return { type: actionType.CHANGE_FIND_DATA, data: data } export const getFindData = (page) => { return (dispatch) => { reqfinddate(page) .then((res) => { console.log(res) Dispatch (ChangeFindData(res.data.data))}). Catch ((e) => {console.log(' error '); }}})Copy the code
Reqfinddata is the same as the request data in the home page above, except that an extra parameter is passed in the request data, that is, the request data should be passed in the page when the background request data. SRC/pages/find/store/reducer code is as follows:
case actionTypes.CHANGE_FIND_DATA: let newData = { newLists: [ ...state.findData.newLists, ...action.data.newLists ] } return { ... state, findData: newData }Copy the code
Because each pass is only ten (limit=10), so action.data.length is 10, what we need to do in redux warehouse is to add the data of Action. data to newLists while retaining the data of original array newLists. The extension operator is used, because the extension operator generates a new array, and because the array is a reference type, the Redux data warehouse detects the object and rerenders the component corresponding to the changed object.
Use image lazy loading
We’ve managed to paging data and request limited data, so are there any other ways to optimize performance? Yes, it is lazy loading of pictures. This article uses scroll event to monitor lazy loading of pictures.
SRC /pages/find/index.jsx
import Lazyload, {forceCheck} from 'react-lazyload' import loading from '@/assets/Images/1.gif' import loading from '@/assets/Images/1.gif Loading2 from '@ / assets/Images/loading GIF' / / on the introduction of pull to refresh the loading picturesCopy the code
<Lazyload
height={100}
placeholder={<img width="100%" height="100%" src={loading} />}
>
<img src={item.picUrl} alt="" ></img>
</Lazyload>
Copy the code
When traversing the image, wrap a Lazyload around the place where the image is displayed, and place lazy pre-loading on its placeholder tag property.
Conclusion:
This article is also a practice of Redux learning, and I hope my article can bring some gains to readers. The project also has many shortcomings:
- Json files are used to replace the back-end interfaces, without deepening the understanding of KOA.
- It’s not configured
redux-persist
Redux data persistence. - Component development, poor reusability.
I hope to continue my study in the future, find time to learn TS, and write a complete TS full stack exercise project to practice.