技术栈
技术栈:
- 项目搭建:React 官方脚手架
create-react-app - react hooks
- 状态管理:redux,以及:
react-redux绑定库、redux-thunk中间件、redux-devtools-extension中间件 - UI 组件库:
antdv4 - ajax请求库:
axios - 路由:
react-router-dom以及history - 富文本编辑器:
react-quill - CSS 预编译器:
sass - CSS Modules 避免组件之间的样式冲突
项目搭建
1. 使用 React CLI 搭建项目:`npx create-react-app geek-pc-89`
2. 进入项目根目录:`cd geek-pc-89`
3. 启动项目:`yarn start`
核心代码
src/index.js 中:
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
ReactDOM.render(
// StrictMode 用来开启 React 严格模式,如果组件中使用了一些比较老的React现在已经不推荐的用法,
// 在控制台中 React 就会给出一些警告提示
<React.StrictMode>
<App />
</React.StrictMode>,
document.querySelector('#root')
)
src/App.js 中:
import './App.css'
function App() {
return <div className="app"></div>
}
export default App
public/index.html 中:
<title>极客园PC</title>
使用CSS预编译器 - SASS
yarn add sass
配置基础路由
yarn add react-router-dom@5.3.0
// 导入路由
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
// 导入页面组件
import Login from './pages/Login'
import Layout from './pages/Layout'
import NotFound from './pages/NotFound'
// 配置路由规则
function App() {
return (
<Router>
<div className="App">
<Switch>
<Route path="/home" component={Layout}></Route>
<Route path="/login" component={Login}></Route>
<Route>
<NotFound />
</Route>
</Switch>
</div>
</Router>
)
}
export default App
组件库-antd
yarn add antd
全局导入组件库的样式:src/index.js 中:
// 先导入 antd 样式文件
import 'antd/dist/antd.css'
// 再导入全局样式文件,防止样式覆盖
import './index.css'
默认展示首页
App.js
import { Redirect } from 'react-router-dom'
<Route exact path="/" render={() => <Redirect to="/home" />} />
配置路径别名
核心代码:
/craco.config.js 中:
const path = require('path')
module.exports = {
// webpack 配置
webpack: {
// 配置别名
alias: {
// 约定:使用 @ 表示 src 文件所在路径
'@': path.resolve(__dirname, 'src')
}
}
}
package.json 中:
// 将 start/build/test 三个命令修改为 craco 方式
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},
@别名路径提示
目标:能够让vscode识别@路径并给出路径提示
分析说明:
在 VSCode 中,jsconfig.json 文件中的有时候会有红色波浪线提示错误,直接忽略即可,不会影响路径提示
步骤:
- 在项目根目录创建
jsconfig.json配置文件 - 在配置文件中添加以下配置
核心代码:
/jsconfig.json 中:
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
}
}
总结:
VSCode 会自动读取 jsconfig.json 中的配置,让 vscode 知道 @ 就是 src 目录
配置 Redux
目标:能够完成Redux的基础配置
步骤:
- 安装 redux 相关的包:
yarn add redux react-redux redux-thunk redux-devtools-extension axios - 在 store 目录中分别创建:actions 和 reducers 文件夹、index.js 文件
- 在 store/index.js 中,创建 store 并导出
- 创建 reducers/index.js 文件,创建 rootReducer 并导出
- 创建 reducers/login.js 文件,创建基础 login reducer 并导出
- 在 src/index.js 中为 React 组件接入 Redux
核心代码:
store 目录结构:
/store
/actions
/reducers
login.js
index.js
index.js
store/index.js 中:
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducers'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
const middlewares = composeWithDevTools(applyMiddleware(thunk))
const store = createStore(rootReducer, middlewares)
export default store
store/reducers/index.js 中:
import { combineReducers } from 'redux'
import login from './login'
const rootReducer = combineReducers({
login
})
export default rootReducer
store/reducers/login.js 中:
// 登录功能,只需要存储 token 即可,所以,状态默认值为:''
const initialState = ''
const login = (state = initialState, action) => {
return state
}
export default login
src/index.js 中:
import { Provider } from 'react-redux'
import store from './store'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querSelector('#root')
)
封装token工具模块
utiles/token.js 中:
const TOKEN_KEY = 'itcast_geek_pc'
// 获取token
const getToken = () => localStorage.getItem(TOKEN_KEY)
// 存储token
const setToken = token => localStorage.setItem(TOKEN_KEY, token)
// 清除token
const clearToken = () => localStorage.removeItem(TOKEN_KEY)
// 是否已登录
const isAuth = () => !!getToken()
export { isAuth, getToken, setToken, clearToken }
封装http工具模块
utils/http.js 中:
import axios from 'axios'
const http = axios.create({
baseURL: 'http://geek.itheima.net/v1_0',
timeout: 5000
})
export { http }
CSSModules
解决组件之间的样式冲突问题
推荐以下方式来将 CSSModules 配合 SASS 使用:
- 每个组件的根节点使用 CSSModules 形式的类名( 根元素的类名:
root) - 其他所有的子节点,都使用普通的 CSS 类名
这样处理的优势:解决组件间样式冲突问题的同时,让给组件添加样式尽量简单
.root {
// 根节点自己的样式
:global {
// 所有子节点的样式,都放在此处,因为是在 global 中,所以,此处的类名不会被 CSSModules 处理
.header {}
.logo {}
.user-info {}
}
}
组件中使用 CSSModules:
import styles from './index.module.scss'
const GeekLayout = () => {
return (
<div className={styles.root}>
<Header className="header">
<div className="logo" />
<div className="user-info"></div>
</Header>
</div>
)
}
11-统一添加token
分析说明:
因为不管是登录时,还是每次刷新页面时,已经将 token 存储在 redux 中了,
所以,可以直接通过 store.getState() 来获取到 redux 状态
步骤:
- 导入 store
- 判断是否是登录请求
- 如果是,不做任何处理
- 如果不是,统一添加 Authorization 请求头
核心代码:
utils/http.js 中:
import store from '@/store'
// 请求拦截器
http.interceptors.request.use(config => {
// 获取token
const { login: token } = store.getState()
// 除了登录请求外,其他请求统一添加 token
if (!config.url.startsWith('/authorizations')) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
统一处理token失效
utils/http.js 中:
import { customHistory } from './history'
import { logout } from '@/store/actions'
http.interceptors.response.use(undefined, error => {
if (!error.response) {
message.error('网络繁忙,请稍后再试')
return Promise.reject(error)
}
if (error.response.status === 401) {
message.error(error.response.data?.message, 1.5, () => {
// 删除token
store.dispatch(logout())
// 跳转到登录页,并携带当前要访问的页面,这样,登录后可以继续返回该页面
customHistory.push('/login', {
from: customHistory.location.pathname
})
})
}
return Promise.reject(error)
})
项目打包和优化
打包命令:`yarn build`
执行命令:`serve -s ./build`,
打包体积分析: yarn analyze
打包体积分析
核心代码:
package.json 中:
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'"
}
路由懒加载
React.Suspense 组件:指定加载中的提示,在动态加载的组件未加载完成前展示加载中效果
import { lazy, Suspense } from 'react'
const Home = lazy(() => import('./Home'))
<Suspense fallback={加载中的提示内容}>
// 懒加载的组件,需要被 Supspense 包裹
<Home />
</Suspense>
05-配置CDN
目标:能够对第三方包使用CDN优化
分析说明:
通过 craco 来修改 webpack 配置,从而实现 CDN 优化
参考文档1:html-webpack-plugin-页面模板
参考文档4:html-webpack-plugin-自定义模板内容
如何查找 CDN 链接地址?
- www.bootcdn.cn/ 国内,目前收录了 4060 个前端开源项目
- unpkg.com/ 任何一个 npm 可以安装的包,在这里面都可以找到对应的 CDN 地址
核心代码:
craco.config.js 中:
const { whenProd, getPlugin, pluginByName } = require('@craco/craco')
// whenProd => when production 针对于生产环境做处理
// whenProd(() => {}, 默认值)
// 第一个参数:内部会判断是否为生产环境,如果是,就执行该参数并拿到该回调函数的返回值
// 第二个参数:如果不是生产环境,就会返回第二个参数中指定的值
module.exports = {
webpack: {
configure: webpackConfig => {
// 生产环境下,配置 externals
webpackConfig.externals = whenProd(() => ({
react: 'React',
'react-dom': 'ReactDOM',
redux: 'Redux',
'react-router-dom': 'ReactRouterDOM'
}), {})
// 拿到 HtmlWebpackPlugin 插件
const { isFound, match } = getPlugin(
webpackConfig,
pluginByName('HtmlWebpackPlugin')
)
// 生产环境下,暴露 CDM 链接到模板页面中
if (isFound) {
// match 表示当前匹配的插件 HtmlWebpackPlugin
// 通过 options 将额外的数据添加给 HtmlWebpackPlugin 插件
// options 可以传递任意数据,比如,此处我们自己写的 cdn 就是要额外传递的数据
match.options.cdn = {
// js 链接
js: whenProd(
() => [
'https://cdn.bootcdn.net/ajax/libs/react/17.0.2/umd/react.production.min.js',
'https://cdn.bootcdn.net/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js',
'https://cdn.bootcdn.net/ajax/libs/redux/4.1.2/redux.min.js',
'https://cdn.bootcdn.net/ajax/libs/react-router-dom/5.3.0/react-router-dom.min.js'
],
[]
),
// css 链接
css: []
}
}
return webpackConfig
}
}
}
public/index.html 中:
<body>
<div id="root"></div>
<!-- 加载第三发包的 CDN 链接 -->
<% htmlWebpackPlugin.options.cdn.js.forEach(cdnURL => { %>
<script src="<%= cdnURL %>"></script>
<% }) %>
</body>
06-使用dayjs优化antd
目标:能够使用 dayjs 替换 moment 来减少打包体积
内容:
步骤:
- 安装包:
yarn add antd-dayjs-webpack-plugin
craco.config.js 中:
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin')
module.exports = {
webpack: {
// ...
plugins: {
add: [new AntdDayjsWebpackPlugin()]
},
}
}
07-生产环境优化
目标:能够根据是否为生产环境对redux中间件进行优化
核心代码:
store/index.js 中:
// import { composeWithDevTools } from 'redux-devtools-extension'
let middlewares
if (process.env.NODE_ENV === 'production') {
// 生产环境,只启用 thunk 中间件
middlewares = applyMiddleware(thunk)
} else {
// 开发环境
const { composeWithDevTools } = require('redux-devtools-extension')
middlewares = composeWithDevTools(applyMiddleware(thunk))
}
08-自定义环境变量
目标:能够通过自定义环境变量配置接口地址
内容:
常用的环境有两个:
- 开发环境:项目开发期间的环境,一般使用
.env.development文件来创建自定义环境变量 - 生产环境:项目打包上线的环境,一般使用
.env.production文件来创建自定义环境变量
在运行 yarn start 时,CRA 会自动读取 .env.development 中的环境变量;在运行 yarn build 时,会自动读取 .env.production 中的环境变量
React CRA 中约定,所有自定义环境变量名称必须以:REACT_APP_ 开头
# 设置接口地址
REACT_APP_URL=http://geek.itheima.net/v1_0
创建好环境变量,将来就可以在代码中来使用了:
const baseURL = process.env.REACT_APP_URL
步骤:
- 在项目根目录中分别创建 .env.development 和 .env.production 文件
- 分别在两个文件中,创建接口地址的环境变量
- 在封装 http 时,读取环境变量中的接口地址作为 baseUrl
- 重启项目,让环境变量生效
09-代理解决跨域问题
目标:能够配置代理解决跨域问题
内容:
通过 CRA 中自带的配置,借助第三方包 http-proxy-middleware 来实现代理功能
src/setupProxy.js 中:
const { createProxyMiddleware } = require('http-proxy-middleware')
const baseURL = process.env.REACT_APP_URL
module.exports = function (app) {
app.use(
// 代理标识
'/api',
// 代理配置
createProxyMiddleware({
// 目标服务器地址
target: baseURL,
changeOrigin: true,
pathRewrite: {
// 去掉接口中的 /api 前缀
'^/api': ''
}
})
)
}
src/utils/http.js 中:
// 注意:通过 http-proxy-middleware 配置的代理只在开发环境下生效
// 一般来说,线上服务器也有代理配置,并且代理配置与本地代理配置相同,此时,就不再需要额外处理
// 但是,对于我们自己的代码来说,打包后本地启动演示项目时是没有代理的,所以,此处可以通过环境变量来判断是否为开发环境,如果是就走代理;否则,直接使用原始地址【针对本地演示的特殊处理】
// 线上部署项目,一般使用: nginx 服务器
const baseURL =
process.env.NODE_ENV === 'development' ? '/api' : process.env.REACT_APP_URL
const http = axios.create({
baseURL,
timeout: 5000
})