使用 cra 创建 ts 项目
npx create-react-app project-name --template typescript
运行 pnpm start
可以在 localhost:3000
查看
安装 craco 管理 项目
pnpm add @craco/craco
修改 package.json
json
{
// 更改前。。。。。。
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
// 更改后。。。。。
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
}
}
创建 craco.config.js
js
// craco.config.js
module.exports = {}
安装 react-router-dom
新增一下文件
src/utils/lazyLoadComp.tsx
src/router/index.tsx
src/router/Redirect.tsx
ts
// src/utils/lazyLoadComp
import { Suspense } from 'react'
const lazyLoadComp = (children: JSX.Element, loading: JSX.Element = <div>loading</div>) => {
return <Suspense fallback={loading}>{children}</Suspense>
}
export default lazyLoadComp
ts
// src/router/Redirect
import { useEffect } from 'react'
import { useNavigate, To } from 'react-router-dom'
interface RedirectProps {
to: To
}
function Redirect(props: RedirectProps) {
const navigate = useNavigate()
useEffect(() => {
navigate(props.to)
})
return null
}
export default Redirect
ts
// src/router/index.tsx
// 别名配置在后面
import { lazy } from 'react'
import { RouteObject } from 'react-router-dom'
import lazyLoadComp from '@/utils/lazyLoadComp'
import Redirect from './Redirect'
import AppLayout from '@/layout/AppLayout'
const LoginPage = lazy(() => import('@/layout/LoginPage'))
const NotFound = lazy(() => import('@/pages/NotFound'))
const DocsLayout = lazy(() => import('@/layout/DocsLayout'))
const HomePage = lazy(() => import('@/pages/HomePage'))
const TaskList = lazy(() => import('@/pages/TaskList'))
const TaskDetailPage = lazy(() => import('@/pages/TaskDetailPage'))
const DocsList = lazy(() => import('@/pages/DocsList'))
const DocsDetail = lazy(() => import('@/pages/DocsDetail'))
const MemberManage = lazy(() => import('@/pages/MemberManage'))
const routes = [
{ path: '/', element: <Redirect to="/index" /> },
{ path: '/login', element: <LoginPage /> },
{
path: '/index',
element: <AppLayout />,
children: [
{ index: true, element: lazyLoadComp(<HomePage />) },
{ path: 'taskList', element: lazyLoadComp(<TaskList />) },
{ path: 'taskDetail/:id', element: lazyLoadComp(<TaskDetailPage />) },
{
path: 'docs',
element: lazyLoadComp(<DocsLayout />),
children: [
{ index: true, element: lazyLoadComp(<DocsList />) },
{ path: 'docsDetail/:id', element: lazyLoadComp(<DocsDetail />) },
],
},
{ path: 'memberManage', element: lazyLoadComp(<MemberManage />) },
],
},
{ path: '*', element: <NotFound /> },
] as RouteObject[]
export default routes
准备工作做好了,开始使用
tsx
// src/App.tsx
import { ConfigProvider } from 'antd'
import { useRoutes } from 'react-router-dom'
import routes from './router/routes'
import zhCN from 'antd/es/locale/zh_CN' // antd 后面配置
function App() {
return <ConfigProvider locale={zhCN}>{useRoutes(routes)}</ConfigProvider>
}
export default App
tsx
// src/index.tsx
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
import 'dayjs/locale/zh-cn'
import './virtual:windi.css'
import '@icon-park/react/styles/index.css'
import reportWebVitals from './reportWebVitals'
const app = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
app.render(
<BrowserRouter>
<App />
</BrowserRouter>,
)
reportWebVitals()
配置路由权限
配置 mobx 状态管理
pnpm add mobx mobx-react-lite
配置跨域、别名、打包结果
js
const path = require('path')
module.exports = {
devServer: {
// 设置代理
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: {
'^/api': '',
},
},
},
// 设置端口
port: 9527,
},
webpack: {
// 设置别名
alias: {
'@': path.resolve(__dirname, 'src'),
},
// 修改打包后的文件夹名, 默认为 build , 改为 dist
configure: (webpackConfig, { env, paths }) => {
paths.appBuild = 'dist'
webpackConfig.output = {
...webpackConfig.output,
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
}
return webpackConfig
},
},
}
修改 tsConfig.json
json
// tsconfig.json
{
// ......
"compilerOptions": {
// .....
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"extends": "./tsconfig.extend.json"
}
json
// tsconfig.extend.json
{
"compilerOptions": {
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
安装 @icon-park/react、windicss
pnpm add @icon-park/react windicss
pnpm add windicss-webpack-plugin -D
js
// craco.config.js
const WindiCSSWebpackPlugin = require('windicss-webpack-plugin')
const path = require('path')
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: {
'^/api': '',
},
},
},
port: 9527,
},
webpack: {
plugins: {
add: [
// 新增 windicss 插件
new WindiCSSWebpackPlugin({
virtualModulePath: 'src',
}),
],
},
alias: {
'@': path.resolve(__dirname, 'src'),
},
configure: (webpackConfig, { env, paths }) => {
paths.appBuild = 'dist'
webpackConfig.output = {
...webpackConfig.output,
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
}
return webpackConfig
},
},
}
新建 windi.config.ts
ts
import { defineConfig } from 'windicss/helpers'
export default defineConfig({
preflight: false,
extract: {
include: ['**/*.{jsx,tsx,css}'],
exclude: ['node_modules', '.git', '.dist', 'public'],
},
})
添加 windicss 和 @icon-park/react 的基础样式
ts
// src/index.ts
import './virtual:windi.css'
import '@icon-park/react/styles/index.css'
安装 less 和 antd
pnpm add less less-loader craco-less -D
pnpm add antd
修改 craco.config.js
js
// craco.config.js
const WindiCSSWebpackPlugin = require('windicss-webpack-plugin')
const CracoLessPlugin = require('craco-less')
const path = require('path')
module.exports = {
devServer: {
// .....
},
webpack: {
// ....
},
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
// antd 自定义主题
lessOptions: {
modifyVars: {
'@primary-color': '#7c3aed',
},
javascriptEnabled: true,
},
},
},
},
],
babel: {
plugins: [
// antd 按需引入
[
'import',
{
libraryName: 'antd',
libraryDirectory: 'es',
style: true, // true 导入 less, false / css 导入 css
},
],
],
},
}
antd 配置中文语言包
ts
// src/App.tsx
import { ConfigProvider } from 'antd'
import { useRoutes } from 'react-router-dom'
import zhCN from 'antd/es/locale/zh_CN'
function App() {
return (
<ConfigProvider locale={zhCN}>
<div>app</div>
</ConfigProvider>
)
}
export default App
antd dayjs 替换 moment
pnpm add dayjs
pnpm add antd-dayjs-webpack-plugin -D
修改 src/index.tsx
ts
// src/index.tsx
import 'dayjs/locale/zh-cn'
修改 craco.config.js
js
// craco.config.js
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin')
module.exports = {
devServer: {
// .....
},
webpack: {
plugins: {
add: [
new WindiCSSWebpackPlugin({
virtualModulePath: 'src',
}),
new AntdDayjsWebpackPlugin(),
],
},
},
plugins: [
// ......
],
babel: {
// ......
},
}
安装开发环境工具
pnpm add webpackbar -D
修改 craco.config.js
js
// craco.config.js
const WebpackBar = require('webpackbar')
module.exports = {
devServer: {
// .....
},
webpack: {
plugins: {
add: [
new WindiCSSWebpackPlugin({
virtualModulePath: 'src',
}),
new AntdDayjsWebpackPlugin(),
new WebpackBar({
color: '#85d', // 默认green,进度条颜色支持HEX
basic: false, // 默认true,启用一个简单的日志报告器
profile: false, // 默认false,启用探查器。
}),
],
},
},
plugins: [
// ......
],
babel: {
// ......
},
}
安装 axios、nprogress、qs
pnpm add axios nprogress qs
pnpm add @types/qs @types/nprogress -D
ts
// src/utils/helper.ts
/**
* @desc 获取 token
* @returns string | null
*/
export const getToken = () => localStorage.getItem('token') || null
/**
* @desc await-to-js
* @param func Promise
* @param error 自定义错误提示
* @returns [err, res]
*/
export const to = <T, U = Error>(func: Promise<T>, error?: U): Promise<[null, T] | [U, null]> => {
return func.then<[null, T]>((res: T) => [null, res]).catch<[U, null]>((err: U) => [error || err, null])
}
/**
* @desc 获取随机 Id
* @returns string 时间戳 + 16进制随机数
*/
export const getRandomId = (): string =>
Date.now().toString() + Number((Math.random() * 10000 * 10000 * 10000 * 10000).toFixed()).toString(16)
/**
* @desc 校验是不是 FormData
* @param data 任意数据
* @returns boolean
*/
export const isFormData = (data: any) => Object.prototype.toString.call(data) === '[Object FormData]'
ts
// src/utils/http.ts
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
import axios from 'axios'
import Qs from 'qs'
import { getRandomId, isFormData } from '@/utils/helper'
import showHttpStatusMessage from './showHttpStatusMessage'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
const instance = axios.create({
timeout: 30 * 1000,
baseURL: '/api',
})
let httpNum = 0
const addHttp = () => {
if (httpNum === 0) {
NProgress.start()
}
httpNum++
}
const finishHttp = () => {
httpNum--
if (httpNum <= 0) {
NProgress.done()
}
}
// instance request 拦截器
instance.interceptors.request.use(
(config: AxiosRequestConfig<any>) => {
addHttp()
if (config.url) {
const reqId = getRandomId()
config.url = config.url.includes('?') ? `${config.url}&reqId=${reqId}` : `${config.url}?reqId=${reqId}`
}
const token = localStorage.getItem('token') || ''
if (token) {
config.headers!.Authorization = 'Bearer ' + token
}
// 除入参是 fromData 之外的 post 请求,请求参数都需要序列化
if (config.method === 'post' && !isFormData(config.data)) {
config.data = Qs.stringify(config.data)
}
return config
},
(err) => {
return Promise.reject(err)
},
)
// instance response 拦截器
instance.interceptors.response.use(
(res: AxiosResponse<any, any>) => {
finishHttp()
const status = res.status
if (status >= 300 || status <= 200) {
showHttpStatusMessage(status)
}
return res.data
},
(error) => {
return Promise.reject(error)
},
)
export default instance
ts
// src/utils/showHttpStatusMessage.ts
import { message } from 'antd'
const showHttpStatusMessage = (status: number) => {
switch (status) {
case 403:
message.error('登录时间已到期,请重新登录')
localStorage.removeItem('token')
break
case 404:
message.error('请求资源不存在')
break
case 500:
case 501:
case 503:
message.error('请求资源不存在')
break
default:
message.error('出现异常,请联系管理员处理')
break
}
}
export default showHttpStatusMessage
安装 ahooks、lodash-es、crypto-js
pnpm add ahooks lodash-es crypto-js
pnpm add @typs/lodash-es @types/crypto-js -D
ts
// src/utils/crypto.ts
import CryptoJS from 'crypto-js' // 准备跟换为 cryptojs
export interface CryptoType {
encrypt: (word: string) => string
decrypt: (word: string) => string
md5: (word: string) => string
base64: (word: string) => string
}
/**
* @desc aes 加解密
*/
class Crypto implements CryptoType {
private key: CryptoJS.lib.WordArray
private iv: CryptoJS.lib.WordArray
constructor() {
this.key = CryptoJS.enc.Utf8.parse('1234567890abcdef') // 16位16进制数作为密钥
this.iv = CryptoJS.enc.Utf8.parse('qazxsw1234567890') // 16位16进制数作为密钥偏移量
}
/**
* @desc AES 加密
* @param word 要加密的数据
*/
public encrypt = (word: string) => {
const utf8Str = CryptoJS.enc.Utf8.parse(word)
const encrypted = CryptoJS.AES.encrypt(utf8Str, this.key, {
iv: this.iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
})
const result = encrypted.ciphertext.toString().toUpperCase()
return result
}
/**
* @desc AES 解密
* @param word 要解密的数据
*/
public decrypt = (word: string) => {
const encryptedHexStr = CryptoJS.enc.Hex.parse(word)
const base64Str = CryptoJS.enc.Base64.stringify(encryptedHexStr)
const decrypt = CryptoJS.AES.decrypt(base64Str, this.key, {
iv: this.iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
})
const decryptStr = decrypt.toString(CryptoJS.enc.Utf8).toString()
return decryptStr
}
/**
* @desc md5 加密(不可逆)
* @param word 要加密的数据
*/
public md5 = (word: string) => {
return CryptoJS.MD5(word).toString()
}
/**
* @desc base64 加密
* @param word 要加密的数据
*/
public base64 = (word: string) => {
const utf8Str = CryptoJS.enc.Utf8.parse(word)
return CryptoJS.enc.Base64.stringify(utf8Str)
}
}
const crypto = new Crypto()
export default crypto