react+typescript+creat-react-app 搭建实战项目脚手架
梳理一下要完成的功能
- 安装 cra typescript 环境
- 覆盖默认的webpack配置
- 配置前端环境变量
- 优化webpack 配置
- 封装 axios 请求方法
- 封装 localStorage
- 配置 redux
- 可配置的前端路由+鉴权
后台页面基础框架 安装 cra typescript 环境
官方文档直接安装 官方链接
不弹出(ejec)webpack配置 通过
customize-cra eact-app-rewired覆盖默认的webpack配置yarn add customize-cra react-app-rewired -D并在根目录添加config-overrides.js文件
/* config-overrides.js */
const { override, addLessLoader } = require('customize-cra')
module.exports = override(
addLessLoader()
)
配置前端环境变量
管理环境变量有很多工具,下面简单分析一下常用工具 dotenv、cross-env 和 env-cmd 的优势与不足:
- dotenv 可以解决跨平台和持久化的问题,但使用场景有限,只适用 node 项目,且和项目代码强耦合,需要在 node 代码运行后手动执行触发
- cross-env 支持在命令行自定义环境变量。问题也非常明显,不能解决大型项目中自定义环境变量的持久化问题
- env-cmd 也可以解决跨平台和持久化的问题,支持定义默认环境变量,不足的是不支持在命令行中自定义环境变量 这里使用env-cmd
yarn add env-cmd 在根目录下创建.env-cmdrc.json文件
{
"dev": {
"REACT_APP_ENV":"dev",
"PORT": 5055,
},
"test": {
"REACT_APP_ENV":"test",
"PORT": 5055,
},
"prod": {
"REACT_APP_ENV":"prod",
}
}
修改packge.json文件
"scripts": {
"start": "env-cmd -e dev react-app-rewired start",
"start:test": "env-cmd -e test react-app-rewired start",
"start:prod": "env-cmd -e prod react-app-rewired start",
"build:prod": "env-cmd -e prod react-app-rewired build",
"build:qa": "react-app-rewired build",
"test": "react-app-rewired test"
},
or yarn add dotenv-cli
// 添加.env.production .env.development文件
"start": "react-app-rewired start",
"start:pro": "dotenv -e .env.production react-app-rewired start",
"build:dev": "dotenv -e .env.development react-app-rewired build",
"build": "dotenv -e .env.production react-app-rewired build",
优化webpack 配置
- 增加gzip打包
- 增加路径别名
- 增加
webpack-bundle-analyzer打包分析 - (使用antd)自定义antd主题样式
yarn add compression-webpack-plugin webpack-bundle-analyzer
yarn add less less-loader -D
// config-overrides.js
const { override, addLessLoader, fixBabelImports, addWebpackAlias } = require('customize-cra')
const CompressionWebpackPlugin = require('compression-webpack-plugin')
const path = require('path')
const paths = require('react-scripts/config/paths')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const appBuildConfig = () => (config) => {
if (config.mode === 'production') {
// 关闭sourceMap
config.devtool = false
// 配置打包后的文件位置修改path目录
paths.appBuild = path.join(path.dirname(paths.appBuild), 'dist')
config.output.path = path.join(path.dirname(config.output.path), 'dist')
//增加`webpack-bundle-analyzer`打包分析
config.plugins.push(new BundleAnalyzerPlugin())
// 添加js打包gzip配置
config.plugins.push(
new CompressionWebpackPlugin({
test: /\.js$|\.css$/,
threshold: 1024,
})
)
}
return config
}
module.exports = override(
addWebpackAlias({
//增加路径别名的处理
'@': path.resolve('./src'),
}),
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
// style 的选项 ‘css' 表示引入的css文件 true 表示引入的less
style: true,
}),
// 这里设置less
// 同时是定制ant-design的主题
// ant-design 定制主题变量: https://ant.design/docs/react/customize-theme-cn
addLessLoader({
lessOptions: {
javascriptEnabled: true,
modifyVars: {
'@primary-color': '#FE5023', // 全局主色
'@link-color': '#FE5023', // 链接色
'@success-color': '#52c41a', // 成功色
'@warning-color': '#faad14', // 警告色
'@error-color': '#f5222d', // 错误色
'@font-size-base': '14px', // 主字号
'@heading-color': 'rgba(0, 0, 0, 0.85)', // 标题色
'@text-color': 'rgba(0, 0, 0, 0.65)', // 主文本色
'@text-color-secondary': 'rgba(0, 0, 0, 0.45)', // 次文本色
'@disabled-color': 'rgba(0, 0, 0, 0.25)', // 失效色
'@border-radius-base': '2px', // 组件/浮层圆角
'@border-color-base': '#d9d9d9', // 边框色
'@box-shadow-base': '0 3px 6px -4px rgba(0,0,0,.12),0 6px 16px 0 rgba(0,0,0,.08),0 9px 28px 8px rgba(0,0,0,.05)', // 浮层阴影
},
cssModules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]', // if you use CSS Modules, and custom `localIdentName`, default is '[local]--[hash:base64:5]'.
},
},
}),
appBuildConfig()
)
现在一个基础功能的typescript+react项目就搭建完成了,接下来封装项目中常用的工具函数
封装 axios 请求方法 包含取消重复请求和主动取消请求
yarn add axios @types/axios
import axios, { AxiosError, AxiosPromise, AxiosRequestConfig, AxiosResponse, Canceler, Method } from 'axios'
import qs from 'qs'
/**
* @type 请求类型
* @url 请求地址
* @params get参数
* @data post等参数
* @config 请求配置
*/
interface ICommonRequest {
type: Method
url: string
params?: Object
data?: Object | null
config?: Object
}
/**
* @type 请求类型
* @url 请求地址
* @options 请求参数
* @config 请求配置
*/
interface IFetchData {
type: Method
url: string
options?: any
config?: Object
}
interface requestMap {
[key: string]: Canceler
}
// 存放请求实例
let requestMaps: requestMap[] = []
/**
* 创建xhr实例
* 路径前缀
* 超时失败时间
*/
const service = axios.create({
// timeout: 5000,
})
/**
* @desc 设置服务请求拦截器
* 定义token请求设置
*/
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
// get请求添加了随机参数另外处理
if (config.method !== 'get' && config.method !== 'GET') {
// 取消重复请求
const request: string = JSON.stringify(config.url) + JSON.stringify(config.method) + JSON.stringify(config.data || config.params || '')
cancelRequest(request, requestMaps)
// 配置cancelToken属性
config.cancelToken = new axios.CancelToken((cancel) => {
requestMaps.push({ [request]: cancel })
requestMaps = requestMaps.filter((v) => v)
})
}
// post请求时转为formdata格式参数
// config.headers['Content-Type'] = 'application/x-www-form-urlencoded'
// console.log(config.headers['Content-Type'],'config.headers')
if (config.method === 'post') {
if (!config.headers['Content-Type']) {
config.data = qs.stringify(config.data)
}
}
return config
},
(error) => {
Promise.reject(error)
}
)
/**
* @desc 设置服务响应拦截器
* 截取返回状态 统一定义成功失败
*/
service.interceptors.response.use(
(response: AxiosResponse) => {
const data = response.data
const code = data.code || 0
if (code === 200) {
return Promise.resolve(data.data)
} else {
return Promise.reject(data)
}
},
(error: AxiosError) => {
if (error.message && typeof error.message === 'string' && error.message.includes('取消了')) {
console.log(error.message)
} else {
return Promise.reject(error)
}
}
)
/**
* @desc 取消进行中的请求
* @param {string} url 请求标记(此处以请求的url为例) 传递字符串`all`代表取消进行中的所有请求
* @param {requestMap[]} requestMaps 现存的所有的请求中实例
* @returns
*/
export function cancelRequest(url: string, requestMaps: requestMap[]) {
return new Promise((res) => {
requestMaps.forEach((ele: requestMap, index: number) => {
const key = Object.keys(ele)[0]
if (url && url === key) {
// 根据url进行定向取消请求
ele[url](`取消了 ${url} 请求`)
// console.log(`取消了 ${url} 请求`)
delete requestMaps[index]
requestMaps.splice(index, 1)
} else if (url === 'all') {
ele[key](`取消了 ${key} 请求`)
// 取消全部请求
console.log(`取消了 all 请求`)
delete requestMaps[index]
}
})
requestMaps = requestMaps.filter((v) => v)
res()
})
}
/**
* @desc 删除已完成的请求标记
* @param {string} url 请求标记(此处以请求的url为例)
* @param {requestMap[]} requestMaps 所有的请求实例
*/
export function deleteCompleteRequest(url: string, requestMaps: requestMap[]) {
requestMaps.forEach((ele: requestMap, index: number) => {
const key = Object.keys(ele)[0]
if (url && url === key) {
delete requestMaps[index]
} else if (url === 'all') {
delete requestMaps[index]
}
})
}
/**
* @param {IFetchData} param 请求配置
* @param {Method} param.type 请求类型
* @param {string} param.url 请求地址
* @param {Object} param.options 请求体参数
* @param {Object} param.config 请求配置
* @param {boolean} iscancel 是否取消
*/
const fetchData = async ({ type, url, options, config }: IFetchData, iscancel: boolean): Promise<any> => {
const request: string = JSON.stringify(url) + JSON.stringify(type) + JSON.stringify(options.data || options.params || '' || '')
// 主动取消请求
if (iscancel) {
cancelRequest(request, requestMaps)
return Promise.resolve()
} else {
const result = await service({
method: type,
url,
...options,
...config,
})
result && deleteCompleteRequest(request, requestMaps)
return result
}
}
/**
*
* @param {ICommonRequest} param 请求配置
* @param {Method} param.type 请求类型
* @param {string} param.url 请求地址
* @param {Object} param.params 请求参数 添加在url上(get)
* @param {Object} param.data 请求体参数
* @param {Object} param.config 请求配置
* @param {boolean} iscancel 是否取消
*/
const commonRequest = ({ type, url, params, data, config }: ICommonRequest, iscancel: boolean): AxiosPromise => {
console.log(data, 'data')
// 合并一下参数
let options: Object = {
params,
data,
}
if (params && data === undefined) {
options = {
data: params,
}
}
if (data === null) {
options = {
params,
}
}
return fetchData({ type, url, options, config }, iscancel)
}
/**
* @desc get请求方法的封装
* @param {string} url 请求地址
* @param {any} params 传参
* @param {Object} config 请求配置
* @param {boolean} iscancel 是否取消请求
*/
export function get(url: string, params: any = {}, config: AxiosRequestConfig | undefined, iscancel: boolean = false) {
// 取消重复请求
if (!params) return fetchData({ type: 'get', url, options: {}, config }, iscancel)
const request: string = JSON.stringify(url) + 'get' + JSON.stringify(params || '')
// 因为get请求,很有可能会被缓存,所以我们需要给它加一个随机参数,
// 实现: 因为params 是已经存在的, 我们只需要给它扩展一个随机数的变量即可
const newParams = Object.assign(params, {
[`recache${new Date().getTime()}`]: 1,
})
cancelRequest(request, requestMaps)
// 这里配置了cancelToken属性,覆盖了原请求中的cancelToken
return fetchData(
{
type: 'get',
url,
options: { params: newParams },
config: {
cancelToken: new axios.CancelToken((cancel) => {
requestMaps.push({ [request]: cancel })
requestMaps = requestMaps.filter((v) => v)
}),
...config,
},
},
iscancel
)
}
/**
* @desc post请求方法的封装
* @param {string} url 请求地址
* @param {any} data 传参
* @param {Object} config 请求配置
* @param {boolean} iscancel 是否取消请求
*/
export function post(url: string, data: any = {}, config: AxiosRequestConfig | undefined, iscancel: boolean = false) {
return commonRequest({ type: 'post', url, params: {}, data, config }, iscancel)
}
/**
* @param {string} url 请求地址
* @param {any} data 传参
* @param {Object} config 请求配置
* @param {boolean} iscancel 是否取消请求
*/
export function put(url: string, data: any = {}, config: AxiosRequestConfig | undefined, iscancel: boolean = false) {
return commonRequest({ type: 'put', url, params: {}, data, config }, iscancel)
}
/**
* @param {string} url 请求地址
* @param {any} data 传参
* @param {Object} config 请求配置
* @param {boolean} iscancel 是否取消请求
*/
export function patch(url: string, data: any = {}, config: AxiosRequestConfig | undefined, iscancel: boolean = false) {
return commonRequest({ type: 'patch', url, params: {}, data, config }, iscancel)
}
/**
* @param {string} url 请求地址
* @param {any} data 传参
* @param {Object} config 请求配置
* @param {boolean} iscancel 是否取消请求
*/
export function deleteRequest(url: string, data: any = {}, config: AxiosRequestConfig | undefined, iscancel: boolean = false) {
return commonRequest({ type: 'delete', url, params: {}, data, config }, iscancel)
}
封装 localStorage
const store: Storage = window.localStorage
class LocalStore {
/*
* 设置数据: 如果value 是 object 会调用JSON.stringify 自动转换为字符串
* */
public static set<T>(key: string, value: T) {
if (!store) {
return false
}
try {
if (typeof value === 'string') {
localStorage.setItem(key, value)
} else if (typeof value === 'number' || typeof value === 'boolean') {
localStorage.setItem(key, value.toString())
} else {
//object
localStorage.setItem(key, JSON.stringify(value))
}
return true
} catch (e) {
return false
}
}
/*
* 直接获取 --- 原始数据
* */
public static get(key: string) {
if (!store) {
return
}
return store.getItem(key)
}
/*
* 获取的同时 转换为JOSN
* */
public static get2Json(key: string) {
if (!store) {
return
}
const data = store.getItem(key)
if (data) {
try {
return JSON.parse(data)
} catch (error) {
// do ..
}
}
return null
}
/*
* 删除
* */
public static remove(key: string) {
if (!store) {
return
}
try {
store.removeItem(key)
} catch (error) {
// do...
console.log(`localstorage 删除${key}失败`)
}
}
public static clear() {
localStorage.clear()
}
}
export default LocalStore
配置 redux
yarn add redux react-redux redux-thunk
yarn add @types/react-redux -D
yarn add connected-react-router history react-router-dom react-loadable
yarn add @types/react-router-config @types/react-router-dom @types/react-loadable -D