предисловие
звуковой сигнал - портал
адрес проекта
гитхаб-адрес
Время осень, как же без готового актуального React проекта? Автору как раз недавно довелось читать новости о большом боссе Шэньсаньюань в Наггетс.React Hooks и неизменяемый поток данных в действии, я изучал проект Великого Бога и был вдохновлен, поэтому я использовал React, чтобы просто имитировать приложение 58 Daojia.
Общее использование проекта: react + hooks + redux + mocker-api + koa
Оптимизация: лучшая прокрутка, стилизованный компонент, реакция-конфигурация-маршрутизатор, реакция-ленивая загрузка, защита от сотрясений, маршрутизация отложенной загрузки, памятка и т. д.
Без лишних слов, начнем с результатов:
Общая структура каталогов проекта выглядит следующим образом:
├─ src
│ ├─ api // 数据请求,接口
│ ├─ assets // 静态资源
│ ├─ baseUI //UI组件
│ ├─ common //公用组件
│ ├─ components // 组件
│ ├─ Data // 数据
│ ├─ index.css
│ ├─ index.js
│ ├─ layouts // 布局
│ ├─ pages // 页面
│ ├─ routes // 路由
│ ├─ store
│ └─ Utils // 本地存储
└─
внешний интерфейс
При разработке проектного приложения мы должны сначала уточнить общую структуру проекта, поэтому здесь мы начнем с маршрутизации.
маршрутизация
Мы используем react-router-config для настройки маршрутизации.
настроить
- Часть кода route/index.js выглядит следующим образом:
import React from 'react';
import { Redirect, Link } from 'react-router-dom';
import BlankLayout from '../layouts/BlankLayout';
import Tabbuttom from '../components/tabbuttom/Tabbuttom';
import Main from '../pages/Main/Main';
import Detail from '../pages/details/Detail';
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: Main,
}
],
},
{
path: '/detail',
component: Detail,
routes: [
{
path: "/detail/:id",
component: Detail
}
]
}
]
}];
-
Чтобы маршрут вступил в силу, конфигурация маршрута должна быть импортирована в приложение. Код App.js выглядит следующим образом:
Поскольку метод renderRoutes отображает только первый уровень маршрутов, текущее приложение является первым слоем.Чтобы эффект вступил в силу в основном компоненте, вам нужно только снова вызвать renderRoutes в других подкомпонентах, таких как Main.
import React from 'react';
import { BrowserRouter,HashRouter } from 'react-router-dom';
import {renderRoutes} from 'react-router-config';
import routes from './routes/index.js';
function App() {
return (
<div className="App">
<HashRouter>
{renderRoutes(routes)}
</HashRouter>
</div>
);
}
export default App;
Отложенная загрузка маршрута
Для прекрасного пользовательского опыта мы можем использоватьReact.lazy
а такжеSuspense
Комбинация ленивой загрузки маршрутизации оптимизирована для повышения скорости загрузки первого экрана, чтобы небольшие партнеры, открывающие его в первый раз, могли сократить время ожидания.
Идея состоит в том, что когда мы используем компонент, нам нужна приостановка, чтобы обернуть его. А React.lazy принимает в качестве параметра функцию, указывая на то, что мы динамически внедряем компонент.
Использование Suspense не нужно описывать слишком много, здесь мы уже инкапсулировали одинSuspenseComponent
компонент, просто оберните компонент им, когда вы его используете.
const Main = lazy(() => import('../pages/Main/Main')); // 组件的引入方式
const SuspenseComponent = Component => props => {
return (
<Suspense fallback={null}>
<Component {...props}></Component>
</Suspense>
)
}
{
path: '/Main',
component: SuspenseComponent(Main)
}
redux
Здесь мы используем redux для управления состоянием данных. По мере того, как приложение становится более сложным, функцию редьюсера необходимо разделить, и каждая страница управляет частью состояния независимо. Наконец, используйте вспомогательную функцию **combineReducers**, чтобы объединить объект, состоящий из нескольких различных функций-редьюсеров, в конечную функцию-редуктор.
тогда к этомуreducer
передачаcreateStore
,Создайтеstore
, всякий раз, когда мы находимся вstore
начальствоdispatch
Одинaction
,store
Данные в нем изменятся соответственно. Мы инициализируем в самом внешнем компоненте контейнераstore
,Потомstate
свойства на какprops
переходил слой за слоем.
- Код store/reducer.js выглядит следующим образом:
import { combineReducers } from 'redux';
import { reducer as serverReducer } from "../pages/server/store/index";
import { reducer as orderReducer } from "../pages/details/store/index";
import { reducer as mainReducer } from '../pages/Main/store/index'
import { reducer as searchReducer } from '../pages/search/store/index'
// 将各个reducer合并起来
export default combineReducers({
server: serverReducer,
main: mainReducer,
order: orderReducer,
search: searchReducer
});
- Код store/index.js выглядит следующим образом:
import thunk from 'redux-thunk';
import { createStore, compose, applyMiddleware } from 'redux';
import reducer from "./reducer";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// 创建store
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));
export default store;
Магазин на подстранице использует главную домашнюю страницу в качестве примера:
-
Main/store/constants.js
// 定义常量 export const CHANGE_MAINDATA = 'CHANGE_MAINDATA'; export const CHANGE_INDEX = 'CHANGE_INDEX'; export const CHANGE_LISTITEMDATA = 'CHANGE_LISTITEMDATA'; export const CHANGE_UPLOADING = 'CHANGE_UPLOADING'; export const CHANGE_DOWNLOADING = 'CHANGE_DOWNLOADING'; export const CHANGE_LIST_OFFSET = 'CHANGE_LIST_OFFSET';
-
Main/store/reducer.js
Чистая функция редуктора Возвращает состояние и принимает обновления состояния. Ей соответствует только одно состояние.
import * as actionTypes from './constants'; // 初始状态 const defaultstate = { maindata: [], index: 0, ListItemData: [], listOffset: 0, Uploading: false, Downloading: false } const reducer = (state = defaultstate, action) => { switch (action.type) { case actionTypes.CHANGE_MAINDATA: return {...state, maindata: action.data } case actionTypes.CHANGE_INDEX: return {...state, index: action.data } case actionTypes.CHANGE_LISTITEMDATA: return {...state, ListItemData: action.data } case actionTypes.CHANGE_UPLOADING: return {...state, Uploading: action.data } case actionTypes.CHANGE_DOWNLOADING: return {...state, Downloading: action.data } case actionTypes.CHANGE_LIST_OFFSET: return {...state, listOffset: action.data } default: return state; } } export default reducer;
-
Часть кода main/store/actionCreators.js
После того, как метод reqmain успешно запрашивает данные, diapatch(changeMainData) изменяет данные домашней страницы и передает успешные данные в качестве параметра для изменения данных домашней страницы.
import { reqmain, reqgetmainListoffset } from '../../../api/index'; import * as actionType from './constants.js'; //修改主页数据 export const changeMainData = (data) => { console.log("进去成功..............."); return { type: actionType.CHANGE_MAINDATA, data: data } } //请求主页数据 export const getMainData = () => { return (dispatch) => { reqmain().then((res) => { if (res.data.success) { dispatch(changeMainData(res.data.data)) } else { console.log("失败", res); } }).catch((e) => { console.log("服务页面数据请求错误!"); }) } };
Здесь создан склад, так как им пользоваться?
Здесь используются два объекта, предоставляемые react-redux:Provider
а такжеconnect
.
- В самом внешнем контейнере заверните все в компонент Provider и передайте ранее созданное хранилище в качестве реквизита Provider.
import React from 'react';
import { BrowserRouter,HashRouter } from 'react-router-dom';
import {Provider} from 'react-redux';
function App() {
return (
<Provider store={store}>
<div className="App">
<Main></Main>
</div>
</Provider>
);
}
export default App;
- Любой компонент внутри Provider (такой как Main здесь), если ему нужны данные в хранилище, он должен быть
connect
Передайте компоненты.
import React from 'react';
function Main() {
return (
<div></div>
);
}
//这个函数允许我们将 store 中的数据作为 props 绑定到组件上
const mapStateToProps = (state) => ({
maindata: state.main.maindata
})
//这个函数将 action 作为 props 绑定到 Main上。
const mapDispatchToProps = (dispatch) => {
return {
getMainDataDispatch() {
dispatch(actionTypes.getMainData())
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(memo(Main))
супер полезный свиток
Не отставайте от Sanyuan и примените улучшенную прокрутку к этому проекту, чтобы создать плавное скольжение вверх и вниз Вот код и его использование.
Нажмите, чтобы изучить троичную прокрутку
В проекте нам нужно только обернуть компонент с помощью Scroll. Обратите внимание, что внешний слой Scroll должен иметь слой оборачивающих элементов, а элементы внутри Scroll должны иметь подходящую ширину и высоту.
import React, { forwardRef, useState,useEffect, useRef, useImperativeHandle } from "react"
import PropTypes from "prop-types"
import BScroll from "better-scroll"
import styled from'styled-components';
const ScrollContainer = styled.div`
width: 100%;
height: 100%;
overflow: hidden;
`
const Scroll = forwardRef ((props, ref) => {
const [bScroll, setBScroll] = useState ();
const scrollContaninerRef = useRef ();
const { direction, click, refresh, bounceTop, bounceBottom } = props;
const { pullUp, pullDown, onScroll } = props;
useEffect (() => {
const scroll = new BScroll (scrollContaninerRef.current, {
scrollX: direction === "horizental",
scrollY: direction === "vertical",
probeType: 3,
click: click,
bounce:{
top: bounceTop,
bottom: bounceBottom
}
});
setBScroll (scroll);
return () => {
setBScroll (null);
}
//eslint-disable-next-line
}, []);
useEffect (() => {
if (!bScroll || !onScroll) return;
bScroll.on ('scroll', (scroll) => {
onScroll (scroll);
})
return () => {
bScroll.off ('scroll');
}
}, [onScroll, bScroll]);
useEffect (() => {
if (!bScroll || !pullUp) return;
bScroll.on ('scrollEnd', () => {
// 判断是否滑动到了底部
if (bScroll.y <= bScroll.maxScrollY + 100){
pullUp ();
}
});
return () => {
bScroll.off ('scrollEnd');
}
}, [pullUp, bScroll]);
useEffect (() => {
if (!bScroll || !pullDown) return;
bScroll.on ('touchEnd', (pos) => {
// 判断用户的下拉动作
if (pos.y > 50) {
pullDown ();
}
});
return () => {
bScroll.off ('touchEnd');
}
}, [pullDown, bScroll]);
useEffect (() => {
if (refresh && bScroll){
bScroll.refresh ();
}
});
useImperativeHandle (ref, () => ({
refresh () {
if (bScroll) {
bScroll.refresh ();
bScroll.scrollTo (0, 0);
}
},
getBScroll () {
if (bScroll) {
return bScroll;
}
}
}));
return (
<ScrollContainer ref={scrollContaninerRef}>
{props.children}
</ScrollContainer>
);
})
Scroll.defaultProps = {
direction: "vertical",
click: true,
refresh: true,
onScroll:null,
pullUpLoading: false,
pullDownLoading: false,
pullUp: null,
pullDown: null,
bounceTop: true,
bounceBottom: true
};
Scroll.propTypes = {
direction: PropTypes.oneOf (['vertical', 'horizental']),
refresh: PropTypes.bool,
onScroll: PropTypes.func,
pullUp: PropTypes.func,
pullDown: PropTypes.func,
pullUpLoading: PropTypes.bool,
pullDownLoading: PropTypes.bool,
bounceTop: PropTypes.bool,// 是否支持向上吸顶
bounceBottom: PropTypes.bool// 是否支持向上吸顶
};
export default Scroll;
<div className="main">
<Scroll direction={"vertical"} refresh={false}
</Scroll>
</div>
styled-components
Вместо использования традиционных файлов css мы используемstyled-components
Создайте стилизованные компоненты. Используя styled-components, мы можем писать стили в jsx-файлах, и нам больше не нужно беспокоиться об именовании стилей, потому что каждый style.js независим, и проблема определения слишком большого количества имен классов переменных будет решена. Выйдите из зоны комфорта и попробуйтеstyled-components
Напишите стиль.
Использование заключается в следующем
// jsx
return (
<>
<OrderTab>
<div className='order-tab__icon'></div>
</OrderTab>
</>)
// style.js
import styled from "styled-components";
// 这里将OrderTab 定义为div标签
export const OrderTab = styled.div`
font-family: PingFangSC-Regular;
height: 1.5648rem /* 169/108 */;
background-color: #fff;
& .order-tab__icon{
width: .7407rem /* 80/108 */;
height: .7407rem /* 80/108 */;
}
`
Памятка по оптимизации рендеринга страницы
Если компонент отображает тот же результат с теми же реквизитами, мы можем вызвать его, обернув его в React.memo. Улучшите производительность компонента, визуализируя результат с помощью Memo. В этом случае React пропустит рендеринг компонента и просто повторно использует результат самого последнего рендеринга, чтобыИзбегайте ненужных обновлений дочерних компонентов.
function Main(props) {
}
export default React.memo(Main);
задняя торцевая часть
Во внешнем интерфейсе мы инкапсулируем запросы GET и POST аксиом в функцию для облегчения последующих запросов данных.
- Создайте API/ajax.js
import axios from 'axios';
export default function Ajax(url, data = {}, type) {
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("数据请求异常!", error)
})
})
}
- Создайте api/index.js, ниже приведен реальный запрос данных.
import Ajax from './ajax.js';
export const reqserver = () => {
return Ajax("/home/server", {}, "GET");
}
export const reqmain = () => {
return Ajax("/home/main", {}, 'GET');
}
export const reqdetail = (data) => {
return Ajax("/detail", { data }, 'GET');
}
export const reqgetmainListoffset = (count) => {
return Ajax('/home/main', { count }, 'GET');
};
export const reqsearchkeywords = (keywords) => {
return Ajax("/search", { keywords }, 'GET');
};
export const reqsearchhot = () => {
return Ajax("/hot", {}, 'GET');
};
koa
Используйте koa для создания серверных сервисов
Используйте маршрутизацию для формирования интерфейсов
Мы создаем новый внутренний каталог, и часть кода index.js выглядит следующим образом:
const fs = require('fs')
const ServerData = require('./Data/serverData/ServerData.json')
const Koa = require('koa');// 引入koa模块
const router = require('koa-router')();// 引入路由
const app = new Koa();// 实例化
// 配置路由
router.get('/home/server', async (ctx) => {
ctx.response.body = {
success:true,
data:ServerData
}
})
// 启动路由
app
.use(router.routes())
.use(router.allowedMethods());
//服务在本地9090端口启动
app.listen(9090, () => {
console.log('server is running 9090');
});
перекрестный домен
Так как в этом проекте front-end и back-end разделены, то есть back-end использует локальный порт 9090 для открытия сервиса, а front-end использует порт 3000 для доступа к странице, поэтому front-end использует локальный порт 9090 для открытия сервиса. end, запрашивающий серверные данные, должен быть междоменным, и браузер сообщит об ошибке. Здесь плагин koa2-cors используется для решения междоменной проблемы.
const cors = require('koa2-cors');
app.use(
cors({
origin: function(ctx) { //设置允许来自指定域名请求
// if (ctx.url === '/test') {
return '*'; // 允许来自所有域名请求
// }
// return 'http://localhost:3000'; //只允许http://localhost:8080这个域名的请求
},
maxAge: 5, //指定本次预检请求的有效期,单位为秒。
credentials: true, //是否允许发送Cookie
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], //设置所允许的HTTP请求方法
allowHeaders: ['Content-Type', 'Authorization', 'Accept'], //设置服务器支持的所有头信息字段
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'] //设置获取其他自定义字段
})
)
mockjs
Вы также можете использовать mockjs для имитации запросов данных. Перехватить запрос и вернуть локальные данные.
import Mock from 'mockjs';
export default Mock.mock(/\/home\/server/, 'get', (options) => {
console.log("mock进去", options);
return {
success: true,
data: ServerData
}
});
Конец цветка~ Заинтересованные друзья приглашаются к переездуgithub! Если есть ошибки, поправьте меня, спасибо!