preface

After learning react for some time, I plan to write a fake project to practice my skills and prepare for spring recruitment. Next, I will share my project and difficulties ENCOUNTERED.

Project introduction

  • Technology stack: React hooks + Redux + Koa

  • Redux is used to centrally manage data, Koa builds the backend, and Mockjs simulates the back-end data interface

  • Style is written using the Styled – Components style component

  • Write front-end routes using React-Router V5

  • Stick to MVVM, componentization, modular ideas, pure handwriting functional components to write pages

Project Online Address

http://1.12.217.128

Projects show

The project structure

├─ Server // Data // Data Index.js ├─ SRC ├─ API // Data Request code, Tool class functions and related Configuration ├─ Assets // Font Configuration, Static Resources ├─ baseUI // Basic UI ├─ Common // Heavy Metal // Heavy Metal // Heavy Metal // Heavy Metal // Heavy Metal // Heavy Metal // Heavy Metal // Heavy Metal // Heavy Metal // Heavy Metal // Heavy Metal // Heavy Metal // Heavy Metal // Heavy Metal // Heavy metal // Heavy metal // Heavy metal // Heavy metal // ├─ routes // Route configuration file ├─ store // redux // app.css // app.jsx //Copy the code

The front part

The routing configuration

  • Routes Indicates the route configuration
import React, { lazy, Suspense } from 'react';
import BlankLayout from '.. /layouts/BlankLayout';
import { Redirect, Link } from 'react-router-dom';
const Main = lazy(() = > import('.. /pages/Main/Main'));
const Detail = lazy(() = > import('.. /pages/details/Detail'));
import Tabbuttom from '.. /components/tabbuttom/Tabbuttom';

const SuspenseComponent = Component= > props= > {
    return (
        <Suspense fallback={null}>
            <Component {. props} ></Component>
        </Suspense>)}export default [{
    component: BlankLayout,
    routes:[
        {
            path:'/'.exact: true.render: () = > < Redirect to = { "/home"}, {} / >,path:'/home'.component: Tabbuttom,
            routes: [{path: '/home'.exact: true.render: () = > < Redirect to = { "/home/main"}, {} / >,path: '/home/main'.component: SuspenseComponent(Main),
                }
                ......
            ]
        },
        {
            path: '/detail'.component: SuspenseComponent(Detail),
            routes: [{path: '/detail/:id'.component: SuspenseComponent(Detail)
                }
            ]
        }
    ]
}]
Copy the code

Lazy loading of components is implemented through react. lazy. Encapsulate the SuspenseComponent function by wrapping components that are going to be lazy, that is, behavior during loading, using Suspense tags to dynamically import components and optimize interactions.

  • Render subordinate routes using renderRouter

For routes to take effect, use renderRoutes where child routes need to be enabled

Code for app.jsx

import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import { renderRoutes } from 'react-router-config'
import routes from './routes/index'

function App() {

  return (
    <Provider>
      <div className='App'>
        <BrowserRouter>
          {renderRoutes(routes)}
        </BrowserRouter>
      </div>
    </Provider>)}export default App
Copy the code

This is the tabbar of our project. The route is changed by the Link to in Tabbuttom. A click event is written on the icon, and a value is dispatched when clicked to change the default index value in the store, thus realizing the page switch.

  • Note that we need to locate the page based on the current path of the page, otherwise the page will return to the home page as soon as another page is refreshed (lost state). Used hereuseLocationListen for URL changes to solve this problem. Here is the main code:
    // Tabbuttom.jsx.const Bottom = (props) = > {
    const { route, totalnum } = props
    const { pathname } = useLocation()
    const index = route.routes.findIndex(item= > item.path === pathname) - 1
    console.log(props)
    const { setIndexDispatch } = props
    return (
        <>} {renderRoutes(route.routes)}<ul className="Botton-warper">.<li className="Botton-warper-warp" key="3"
                    onClick={()= > { setIndexDispatch(2) }}>
                    <Link to='/home/cart' style={{ textDecoration: "none}} ">
                        <div className="icon">
                            {
                                index === 2 ? <img className='icon-img' src={CartIconActive} alt=' ' /> :
                                <img className='icon-img' src={CartIcon} alt=' ' />
                            }
                        </div>
                        <div className="planet" style={index= = =2 ? { color: "#ec564b"}: {}} >The shopping cart<HeadNumIcon display="" top="-0.92rem" left="1.5 rem." " totalnum={totalnum} />
                        </div>
                        
                    </Link>
                </li>.</ul>{/* tabbar */}</>)}const mapStateToProps = (state) = > {
    console.log(state);
    return {
        totalnum: state.cart.totalnum,
        index: state.main.index
    }
}
// setIndex changeIndex

const mapDispatchToProps = (dispatch) = > {
    return {
        setIndexDispatch(index) {
            dispatch(actionCreators.setIndex(index))
        }
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(memo(Bottom))
Copy the code

Mobile adaptation

Use lib-flexible and PostCSS-pxtoREM to convert PX units into REM units to achieve mobile adaptation

  1. Install the lib – flexible postcss – pxtorem

    npm install postcss-pxtorem –save-dev npm install lib-flexible

  2. Import lib-fiexible in the main.js file

    import ‘lib-flexible/flexible’

  3. Create the postcss.config.js file in the root directory

module.exports = {
    "plugins": [
        require("postcss-pxtorem") ({rootValue: 37.5.propList: [The '*'].selectorBlackList: ['.norem']]}})Copy the code

This completes the mobile adaptation 🤗

Data Flow management

In this project, we split reducer, each page has an independently managed warehouse store, and then merge it into a total store. Whenever we dispatch an action on the store, the data in the store will change accordingly, and the data drives the interface.

  • store/reducer.js
import { combineReducers } from 'redux'
import { reducer as mainReducer } from '.. /pages/Main/store/index'
import { reducer as cateReducer } from '.. /pages/Cate/store/index'
import { reducer as detailReducer } from  '.. /pages/details/store/index'
import { reducer as cartReducer } from '.. /pages/Cart/store/index'
import { reducer as userReducer } from '.. /pages/User/store/index'

export default combineReducers({
    main: mainReducer,
    cate: cateReducer,
    detail: detailReducer,
    cart: cartReducer,
    user: userReducer
});
Copy the code

The Redux Store only supports synchronous data streams. Using middleware such as Thunk can help achieve asynchrony in Redux applications.

  • store/index.js
import thunk from 'redux-thunk';
import { createStore, compose, applyMiddleware } from 'redux';
import reducer from "./reducer";
// Debug using redux-devtolls-extensions
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));

export default store;
Copy the code

One of the pages has the following structure:

├─ Main ├─ Store ActionOffers.js constants.js index.js reducer.js main.css main.jsxCopy the code
  • Initialize thestateAnd definereducerfunction
// reducer.js
import * as actionTypes from './constants';

const defaultstate = {
    maindata: [].// Page data
    index: 0 // flag tabbar activation
}

const reducer = (state = defaultstate, action) = > {
    switch (action.type) {
        case actionTypes.SET_INDEX:
            return{... state,index: action.data }
        case actionTypes.CHANGE_MAINDATA:
            return{... state,maindata: action.data }
        default:
            returnstate; }}export default reducer;
Copy the code
  • defineconstans
// constans.js
export const CHANGE_MAINDATA = 'CHANGE_MAINDATA';
export const SET_INDEX = 'SET_INDEX';
Copy the code
// actionCreators.js
import * as actionType from './constants.js'
import { reqmain } from '@/api/index'

// Home page data
export const changeMainData = (data) = > {
    return {
        type: actionType.CHANGE_MAINDATA,
        data: data
    }
}

export const setIndex = (data) = > {
    return {
        type: actionType.SET_INDEX,
        data: data
    }
}

export const getMainData = () = > {
    // dispatch 
    return (dispatch) = > {
        reqmain()
            .then((res) = > {
                // console.log(res)
                dispatch(changeMainData(res.data.data))
            })
            .catch((e) = > {
                console.log('Wrong')}}}Copy the code
// api/index.js
// A list of all interface methods
import Ajax from './ajax.js'

export const reqmain = () = > {
    return Ajax('/home/main')}Copy the code

Then you can connect to Redux. The best way to connect the view and data layers is to use the Connect function, which essentially provides a store for Connect.

// App.jsx
import { Provider } from 'react-redux'
import store from './store/index.js'
import { BrowserRouter } from 'react-router-dom'
import { renderRoutes } from 'react-router-config'
import routes from './routes/index'

function App() {

  return (
    // Enable each route to extract data from the main warehouse
    <Provider store={store}>
      <div className='App'>
        <BrowserRouter>
          {renderRoutes(routes)}
        </BrowserRouter>
      </div>
    </Provider>)}export default App
Copy the code

Each page then wraps the component with connect and uses the store data, which means the page can get the data it wants from the backend interface.

import React, { useState, useEffect, memo } from 'react'
import { connect } from 'react-redux'

const Main = (props) = > {
    
    return (
        <div className="main">.</div>)}const mapStateToDispatch = (dispatch) = > {
    return {
        getMainDataDispatch() {
            dispatch(actionTypes.getMainData())
        }
    }
}
const mapStateToProps = (state) = > {
    return {
        maindata: state.main.maindata
    }
}
export default connect(mapStateToProps, mapStateToDispatch)(memo(Main))
Copy the code

Page development

Home page

There’s an Scroll component wrapped around it, so if you wrap the Scroll style component around it, you can slide it on your phone. Swiper7 is used to realize the running lights of the search box and the rotation map.

import { Swiper, SwiperSlide } from "swiper/react"
import { Autoplay } from 'swiper'
import 'swiper/css'
import 'swiper/css/autoplay'

<Swiper
    modules={[Autoplay]}
    autoplay={{ delay: 1000 }}
    direction="vertical"
    loop
    >
        {    
            searchPlaceholder.map((item, index) = > {
                return (
                    <SwiperSlide key={index} className='home__search-placeholder'>
                        {item.text}</SwiperSlide>
                )
            })
        }
</Swiper>
Copy the code
  • To change the default style, we need to change the class names of its default tags. Swiper-pagination-bullet and.swiper.
.swiper-pagination-bullets {
    bottom: -6px ! important; } .swiper-pagination-bullet {width: 0.6481rem ! important; height:0.0926rem ! important; margin:0! important; border-radius: 10px ! important; } .swiper-pagination-bullet-active { background-color: #f64949; height:0.0926rem;
}
  
.swiper {
  --swiper-pagination-color: #fdb3a9;
  width: 9.388rem;
}
Copy the code

We request 20 items of commodity data at one time. When we slide to the last item, then pull up, the data will be requested to the background, and the commodity will be displayed all the time. In this case, we need to use useEffect to listen to the page, which will change, and then request data to the background, and the commodity list will be displayed. When we pull up and load the list of items, we do a lot of shaking to prevent a lot of repeated requests from being sent in a short period of time.

const Main = (props) = >{...// The number of pages requested
    let [page, setPage] = useState(1)
    // Request data to add pages
    const fetchList = () = > {
        api
            .reqlist(page)
            .then(res= > {
                // console.log(res);
                setList([
                    ...list,
                    ...res.data.data.list
                ])
            })
    }
    // Refresh data
    const fetchListUpdate = () = > {
        api
            .reqlist(page)
            .then(res= > {
                setList([...res.data.data.list])
            })
    }
    useEffect(() = > {
        if(! maindata.length) { getMainDataDispatch() } fetchList() }, []) useEffect(() = > {
        fetchList()
    }, [page])

    // Pull up to load more
    const handlePullUp = () = > {
        console.log('on!)
        setPage(++page)
    }
    // Drop refresh
    const handlePullDown = () = > {
        console.log('Pull refresh')}const handleOnclick = () = > {
        setType(type + 1)
    }
    useEffect(() = > {
        fetchListUpdate()
    }, [type])
Copy the code

Category pages

The classification page of our project is a sibling component value transfer problem. In the parent component, a value of useState is used as the default value and then passed to the two child components through prop. Click different menu options of the left component to change the default value, MVVM data bidirectional binding, and the data on the right side will change accordingly.

<div className="cate-menu">
                    {
                        cateMenu.map((item, index) = > {
                            const active = item.id === curNav
                            return (
                                <div key={index} onClick={setCurNav.bind(null, index)} className={classNames("cate-menu__item", active&&. ""cate-menu__item--active")} >
                                    <p className={classNames("cate-menu__item-name", active&&. ""cate-menu__item-name--active")} >
                                        {item.text}
                                    </p>
                                </div>
                            )
                        })
                    }
                </div>
Copy the code

Shopping cart page

We implement the shopping cart with the Redux data stream, which dispatches an action for each operation the cart takes, and the data in the Store changes accordingly. Here is the logic for the shopping cart:

// Cart/store/pai.js
import { floatAdd } from ".. /.. /.. /api/utils"
// Solve the decimal precision problem
export const change_logo = (cartItem, cartdata = []) = > {
    const {id} = cartItem
    let index = cartdata.findIndex((item) = > item.id == id)
    cartdata[index].isChecked = ! cartdata[index].isChecked
    return cartdata
}
/ / total price
export const allmoney = (cartdata) = > {
    let arr = cartdata.filter(item= > item.isChecked)
    return arr.reduce((sum, cur) = > floatAdd(sum, cur.price * cur.num), 0)}/ /
export const reduce_num = (id, cartdata) = > {
    let index = cartdata.findIndex((item) = > item.id == id)
    cartdata[index].isChecked = true;

    // When num of the item changes to 1, it cannot be reduced. Change the button to disabel:false
    if (cartdata[index].num == 1) {
            cartdata[index].isChecked = false
            return cartdata
    }
    cartdata[index].num--;
    if(cartdata[index].num == 1)  cartdata[index].isChecked = false
    return cartdata
}
/ / add
export const add_num = (id, cartdata) = > {
    let index = cartdata.findIndex((item) = > item.id == id)

    // isChecked changed to true after the number of business logic clicks increased
    cartdata[index].isChecked = true;
    cartdata[index].num++;
    return cartdata
}

export const change_num = (data,cartdata) = > {

    let {num,id} =data
    let index = cartdata.findIndex((item) = > item.id == id)
    
    cartdata[index].num = num
    cartdata[index].isChecked = true
    return cartdata
}

export const allSelected = (cartdata) = > {
    // select all if index == -1
    let index = cartdata.findIndex((item) = >! item.isChecked)if(index == -1) return true
}
/ / all
export const SelectedAll = (cartdata) = >{
    let index = cartdata.findIndex((item) = >! item.isChecked)if(index == -1) {    // Select all
        cartdata.map(item= >item.isChecked = ! item.isChecked) }else{ // Not selected or partially selected
        cartdata.map(item= > 
          item.isChecked = true)}return cartdata
}
// Go to the shopping cart interface
export const goToCart = (data,cartdata) = >{
    return cartdata;
}
/ / delete
export const deleteItem = (id,data) = >{
    let index = data.findIndex(item= > item.id == id)
    data.splice(index,1)
    return data
}

// Details page click to go to shopping cart
export const goToCart_btn = (data,cartdata) = >{
    let {id} = data
    let index = cartdata.findIndex(item= > item.id == id)

    if(index == -1){
        cartdata.push(data)
        console.log(cartdata);
        return cartdata
    }
   return cartdata
}

// Total number of tabbars
export const totalnum = (data) = >{
    let num = data.reduce((acc,item) = > acc+item.num,0)
    return num
}
Copy the code

Interested point here can look at the source code

  • Lost accuracy of floating point number while making shopping cart total price

The mathJS library is used to solve the accuracy loss problem.

import * as math from 'mathjs'
const floatAdd = (arg1, arg2) = > {
  // Solve the accuracy problem
  const ans = math.add(arg1, arg2)
  return math.format(ans, {precision: 14})}Copy the code

The backend part

Koa builds the background

At the back end, part of the data we need is stored in JSON format, and mockJS is used to simulate part of the data. In order to solve the cross-domain problem, CORS is used.

// server/index.js
const Koa = require('koa')
const router = require('koa-router') ()const app = new Koa()
const MainData = require('./Data/mainData/mainData.json')
const cors = require('koa2-cors')
const Mock = require('mockjs')
const Random = Mock.Random

app.use(cors({
    origin: function(ctx) { // Set requests from the specified domain to be allowed
        // if (ctx.url === '/test') {
        return The '*'; // Allow requests from all domain names
        // }
        // return 'http://localhost:3000'; // only requests for http://localhost:8080 are allowed
    },
    maxAge: 5.// Specifies the validity period of this precheck request, in seconds.
    credentials: true.// Whether cookies can be sent
    allowMethods: ['GET'.'POST'.'PUT'.'DELETE'.'OPTIONS'].// Sets the allowed HTTP request methods
    allowHeaders: ['Content-Type'.'Authorization'.'Accept'].// Set all header fields supported by the server
    exposeHeaders: ['WWW-Authenticate'.'Server-Authorization'] // Set to get additional custom fields
}))

router.get('/home/main'.async (ctx) => {
    ctx.response.body = {
        success: true.data: MainData
    }
})
router.get('/home/list'.async (ctx) => {
    let { limit = 20, page = 1 } = ctx.request.query
    // console.log(limit, page);
    let data = Mock.mock({
        'list|20': [{
            'id': '@increment'.'title': '@ctitle(12, 15)'.'price': '@float(60, 1000, 0, 1)'.'imgsrc': Random.image('160x160')
        }]
    })
    ctx.body = {
        success: true,
        data
    }
})
router.get('/detail/:id'.async (ctx) => {
    // console.log(ctx.params);
    const { id } = ctx.params;
    if(! id) { ctx.response.body = {success: false.mag: 'Request data'}}// to be continue
    ctx.response.body = {
        success: true.data: Mock.mock({   // Details page data
            id,
            title: '@ctitle(5, 10)'.price: '@float(60, 1000, 0, 2)'.rate: '@float(60, 100, 0, 1)'.desc: '@csentence(6, 12)'.attrValue: '@ ctitle (2, 6)'
        }), DetailData
    }
})

app
    .use(router.routes())
    .use(router.allowedMethods())
// 1. HTTP service
// 2. Simple routing module
// 3. cors
// 4. Return data
app.listen(9000.() = > {
    console.log('server is running 9000');
})
Copy the code

To optimize the

alias

As the project grows larger, the direct reference relationship between files will be complicated. In this case, alias is used instead of SRC, which can effectively improve the development efficiency.

// The alias is not configured
import MainBanner from '.. /.. /components/main/mainBanner/MainBanner'

// After configuring the alias
import MainBanner from '@/components/main/mainBanner/MainBanner'
Copy the code

Alias Settings are as follows:

// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  alias: {
    The '@': path.resolve(__dirname, './src')}})Copy the code

memo

The react. memo wrapped component props will reuse the results of the most recent execution in the same case. The React. Memo helps us cache components to avoid unnecessary duplicate rendering of components.

import memo from 'react'

const Main = () = > {}

export default memo(Main)
Copy the code

Lazy loading

Lazy loading, also known as lazy loading, refers to the lazy loading of images on long web pages and is a great way to optimize web page performance.

When the visual area is not rolled to where the resource needs to be loaded, resources outside the visual area are not loaded. By avoiding blocking requests by loading too many images at once, you can reduce the load on the server, which will improve the site’s loading speed and user experience.

  • Use the React-LazyLoad library to load images lazily
import LazyLoad from 'react-lazyload'
import loading from '@/assets/loading.gif'

<div className="ListItem-content__img">
    <LazyLoad style={{ 'height': '160px', 'width': '160px'}}placeholder={<img width="100%" height="100%" src={loading} alt=""/>} ><img style={{ 'borderRadius': '9px'}}src={item.imgsrc} alt="" />
    </LazyLoad>
</div>
Copy the code
  • Use lazy and suspense to implement lazy loading components
import React, { lazy, Suspense } from 'react';
const Main = lazy(() = > import('.. /pages/Main/Main'));

const SuspenseComponent = Component= > props= > {
    return (
        <Suspense fallback={null}>
            <Component {. props} ></Component>
        </Suspense>)}... {path: '/home/main'.component: SuspenseComponent(Main),
    }
    .......
Copy the code

conclusion

It was the first time to practice a relatively complete React project. Although the business was not complete yet, we still gained a lot. Is also a summary of this period of learning, I hope to help dig friends oh!!

The source code

  • Gitee address, welcome to star😘