react PC 后台项目总结

289 阅读8分钟

技术栈

技术栈:

  • 项目搭建:React 官方脚手架 create-react-app
  • react hooks
  • 状态管理:redux,以及:react-redux 绑定库、redux-thunk 中间件、redux-devtools-extension 中间件
  • UI 组件库:antd v4
  • 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 文件中的有时候会有红色波浪线提示错误,直接忽略即可,不会影响路径提示

步骤

  1. 在项目根目录创建 jsconfig.json 配置文件
  2. 在配置文件中添加以下配置

核心代码

/jsconfig.json 中:

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

总结

VSCode 会自动读取 jsconfig.json 中的配置,让 vscode 知道 @ 就是 src 目录

配置 Redux

目标:能够完成Redux的基础配置

步骤

  1. 安装 redux 相关的包:yarn add redux react-redux redux-thunk redux-devtools-extension axios
  2. 在 store 目录中分别创建:actions 和 reducers 文件夹、index.js 文件
  3. 在 store/index.js 中,创建 store 并导出
  4. 创建 reducers/index.js 文件,创建 rootReducer 并导出
  5. 创建 reducers/login.js 文件,创建基础 login reducer 并导出
  6. 在 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 状态

步骤

  1. 导入 store
  2. 判断是否是登录请求
  3. 如果是,不做任何处理
  4. 如果不是,统一添加 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-页面模板

参考文档2:ejs 模板语法

参考文档3:craco 配置

参考文档4:html-webpack-plugin-自定义模板内容

如何查找 CDN 链接地址?

  1. www.bootcdn.cn/ 国内,目前收录了 4060 个前端开源项目
  2. 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 来减少打包体积

内容

参考 antd 替换 Moment.js

步骤

  1. 安装包: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-自定义环境变量

目标:能够通过自定义环境变量配置接口地址

内容

参考文档

常用的环境有两个:

  1. 开发环境:项目开发期间的环境,一般使用 .env.development 文件来创建自定义环境变量
  2. 生产环境:项目打包上线的环境,一般使用 .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

步骤

  1. 在项目根目录中分别创建 .env.development 和 .env.production 文件
  2. 分别在两个文件中,创建接口地址的环境变量
  3. 在封装 http 时,读取环境变量中的接口地址作为 baseUrl
  4. 重启项目,让环境变量生效

09-代理解决跨域问题

目标:能够配置代理解决跨域问题

内容

CRA 代理配置参考文档

http-proxy-middleware 文档

通过 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
})