create-react-app+reactHooks+Ts++antd创建一个react基础项目模板

278 阅读5分钟

1. 首先引入create-react-app

 yarn add create-react-app -g

2.创建react项目

 yarn create react-app my-app --template typescript

创建完成之后,进入项目,yarn start启动

3.引入antd和按需加载

  1. 引入 react-app-rewired 并修改 package.json 里的启动配置。由于新的 react-app-rewired@2.x 版本的关系,你还需要安装 customize-cra
yarn add antd
yarn add react-app-rewired customize-cra
  1. 按需加载
修改package.json为
{
//...
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
  },
//...
// 修改完成之后,在根路径中加入config-overrides.js

const { override, fixBabelImports } = require('customize-cra');

module.exports = override(
  fixBabelImports('import', {
    libraryName: 'antd',
    libraryDirectory: 'es',
    style: 'css',
  }),
);

这一步完成后就可以直接在页面中 import { Button } from 'antd' 按需加载了

4.自定义主题

# 有三种方案 
 1:使用antd官网所说craco-less的配置  https://ant.design/docs/react/use-with-create-react-app-cn
 2.yarn eject暴露webpack配置
 3.使用刚才引入的react-app-rewired customize-cra

三种方案都需要 yarn add less less-loader,

此文章采用的第三种方案 在文件最外层新增config-overrides.js

// 详细配置
const { override, fixBabelImports, addLessLoader, adjustStyleLoaders } = require('customize-cra');

module.exports = override(
  fixBabelImports('import', {
    libraryName: 'antd',
    libraryDirectory: 'es',
    style: true,
  }),
  addLessLoader({
    lessOptions: {
      javascriptEnabled: true,
      modifyVars: { 
        '@primary-color': '#1DA57A' 
      },
    }
  }),
  adjustStyleLoaders(({ use: [, , postcss] }) => {
    const postcssOptions = postcss.options;
    postcss.options = { postcssOptions };
  })
);

此步骤 遇到的问题汇总

按官方文档写的配置

addLessLoader({
    lessOptions: {
      javascriptEnabled: true,
      modifyVars: { 
        '@primary-color': '#1DA57A' 
      },
    }
  }),

yarn start 之后 1659062162853.png 有报错,说是我们配置的对象是无效的,且在配置中modifyVars是未知的,

点开addLessLoader可以看到

export function addLessLoader(loaderOptions?: any, customCssModules?: any): OverrideFunc;

查看customize-craissues找到,我们所添加到addLessLoader里面的配置由于less-loader的更新,新版本的写法发生了变化,原来addLessLoader的配置选项现在需要嵌套到一个lessOptions对象中去。

即为

addLessLoader({
    lessOptions: {
      javascriptEnabled: true,
      modifyVars: { 
        '@primary-color': '#1DA57A' 
      },
    }
  })

重新 yarn start之后,出现了新的错误

1659062768895.png

这个问题是由于使用了create-react-app的问题,因为版本升级到了5.0.1,所以webpack的版本也变成了5.0.1,所以我们需要

  adjustStyleLoaders(({ use: [, , postcss] }) => {
    const postcssOptions = postcss.options;
    postcss.options = { postcssOptions };
  })

重新 yarn start之后可以看到运行成功

如果想采用暗黑模式,更改config-overrides.js

const path = require('path')
const {
  override,
  addLessLoader,
  addWebpackAlias,
  overrideDevServer,
  fixBabelImports,
  adjustStyleLoaders
} = require('customize-cra')
const { getThemeVariables } = require('antd/dist/theme');

const devServerConfig = () => config => {
  return {
    ...config,
  }
}

// 修改端口号
process.env.PORT = 3030

module.exports = {
  webpack: override(
    addWebpackAlias({
      // 路径别名,还需要配置tsconfig.json、tsconfig-base.json,在后面,可配置多个路径别名
        '@': path.resolve(__dirname, 'src/')
    }),
    // antd按需加载
    fixBabelImports('import', { 
      libraryName: 'antd',
      libraryDirectory: 'es',
      style: true // 支持less
    }),
    // 配置less
    addLessLoader({
      lessOptions: {
        javascriptEnabled: true, // 启用支持内联javascript
        modifyVars:{
          ...getThemeVariables({
            dark: true, // 开启暗黑模式
            compact: false, // 开启紧凑模式
          }),
          '@font-size-base': '14px',
          '@primary-color': '#FF550',
          '@text-color':'rgba(255, 255, 255, 1)',
        }
      }
    }),
    adjustStyleLoaders(({ use: [, , postcss] }) => {
      const postcssOptions = postcss.options;
      postcss.options = { postcssOptions };
    })
  ),
  devServer: overrideDevServer(devServerConfig()) 
}

下一步为创建路由和Redux或umi,放到下一篇文章中

5. react路由的引入和使用

路由引入

yarn add react-router-dom
yarn add @types/react-router-dom

路由使用

src文件下新增router/index.ts文件 ``` import React, { lazy, Suspense } from 'react';

import { BrowserRouter,Route,Routes  } from 'react-router-dom';
import BaseLayout from '@/layout/BaseLayout';
const Home =  lazy(() => import("@/views/Home"));
const Play = lazy(() => import("@/views/Play"))
const Drink = lazy(() => import("@/views/Drink"))
const Milk = lazy(() =>  import("@/views/Drink/components/Milk"))
const Beer = lazy(() => import("@/views/Drink/components/Beer"))
const Eat = lazy(() => import("@/views/Eat"))

const RouterContainer: React.FC = (props: any) => {
    return (
      <>
        <BrowserRouter>
            <BaseLayout>
              <Suspense fallback={<div>Loading...</div>}>
                <Routes>
                  <Route path="/" element={<Home/>} />
                  <Route path="/Play" element={<Play/>} />

                  <Route path="/Drink" element={<Drink/>} > </Route>
                  <Route path="/Milk" element={<Milk/>} />
                  <Route path="/Beer" element={<Beer/>} />

                  <Route path= '/Eat' element={<Eat/>} />  
                </Routes>
              </Suspense>
            </BaseLayout> 
        </BrowserRouter>
      </>
    )
  }

  export default RouterContainer
  ```
  

新增src/config/menu.ts

  ```
  export const allMenu = [
    {
      id:'0',
      key:'/',
      label:'Home',
    },
    {
      id: '1',
      key: '/Eat',
      label:'Eat',
    },
    {
      id: '2',
      key: '/Drink',
      label:'Drink',
      children:[
        {
          id:'2-1',
          key:'/Milk',
          label:'Milk',
        },
        {
          id:'2-2',

          key:'/Beer',
          label:'Beer',
        }
      ]
    },
    {
      id: '3',
      key: '/Play',
      label:'Play',
    }
]
```

新增src/layout/BaseLayout.tsx

```
import {allMenu} from '../config/menu'
import { Breadcrumb, Layout, Menu } from 'antd';
import React, { useEffect, useState } from 'react';
import { NavLink, useLocation,useNavigate } from 'react-router-dom';
import SubMenu from 'antd/lib/menu/SubMenu';

const { Header, Content, Sider } = Layout;

const rootSubmenuKeys: any = new Set(['/', '/Eat', '/Drink', '/Play'])
console.log(allMenu);

const BaseLayout= (props:any) => {
    const { children } = props
    const [openKeys, setOpenKeys] = useState<any>([''])
    const location = useLocation()
    const navigate = useNavigate()
    // const getMenuNodes = (menuList: any) =>
    // menuList.length > 0 &&menuList.map((item: any) => {
    //     return (
    //         <>
    //         {item.children&&item.children.length>0?(
    //             <Menu.SubMenu title={item.label} key={item.key}>
    //                 { getMenuNodes(item.children) }
    //             </Menu.SubMenu >
    //         ):
    //             <Menu.Item key={item.key} ><NavLink to={item.key}> { item.label}</NavLink> </Menu.Item>
    //         }
    //         </>
    //     )
    // })
    const onOpenChange = (keys: React.ReactText[]) => {
        const latestOpenKey = keys.find((key) => !openKeys.includes(key))
        console.log(latestOpenKey);

        if (!rootSubmenuKeys.has(latestOpenKey)) {
            setOpenKeys(keys)
        } else {
            setOpenKeys(latestOpenKey ? [latestOpenKey] : [])
        }
    }
    const gotoPath = ({ item, key, keyPath, selectedKeys, domEvent }:any)=>{
        console.log(key,keyPath);
        navigate(key)
    }

    return(
        <Layout style={{width:'100%',height:'100%'}}>
        <Header style={{color:'#fff'}} >
             HeaderHeaderHeader
        </Header>
        <Layout>
            <Sider width={200} className="site-layout-background" theme='light'>
            <Menu
                mode="inline"
                openKeys={openKeys}
                onOpenChange={onOpenChange}
                items={allMenu}
                onSelect={gotoPath}
                >
                   {/* {getMenuNodes(allMenu)}  */}
            </Menu>

            </Sider>
            <Layout style={{ padding: '0 24px 24px' }}>
                <Breadcrumb style={{ margin: '16px 0' }}>
                    <Breadcrumb.Item  href="./">Home</Breadcrumb.Item>
                    <Breadcrumb.Item href={location.pathname}>{location.pathname.split('/')[1]}</Breadcrumb.Item>
                </Breadcrumb>
                <Content
                    className="site-layout-background"
                    style={{
                        padding: 24,
                        margin: 0,
                        minHeight: 280,
                    }}>
                    {children}
                </Content>
            </Layout>
        </Layout>
      </Layout>
    )

}

export default BaseLayout;
```

6. redux的引入及使用

引入redux模块

yarn add @reduxjs/toolkit 
yarn add react-redux

reduxjs/toolkit的使用

src文件下新建store文件夹,在store中新建index.ts和自己各个模块所存的文件夹 eg: index.ts新增feature文件夹,其中分别有counterSlice.ts模块和MoviSlice模块

counterSlice.ts

import { createSlice } from '@reduxjs/toolkit'
// initial state interface
export interface InitialStateTypes {
    count:number,
    loading: boolean;
    visible: boolean;
    isEditMode: boolean;
  }
  
  // initial state
  const initialState: InitialStateTypes = {
    count:0,
    loading: false,
    visible: false,
    isEditMode: false,
  };

export const counterSlice = createSlice({
    name:'counter',  // 命名空间,在调用action的时候会默认的设置为action的前缀
    //初始值
    initialState:initialState,
    // 这里的属性会自动的导出为actions,在组件中可以直接通过dispatch进行触发
    reducers:{
         // console.log(action);
        increment(state,{payload}){
            state.count =  state.count + payload.step  // 内置了immutable
        },
        decrement(state) {
            state.count -= 1;
         },
    }
})

// 导出actions
export const { increment, decrement } = counterSlice.actions;

// 内置了thunk插件,可以直接处理异步请求

export const asyncIncrement = (payload:any) => (dispatch:any) => {
    setTimeout(() => {
        dispatch(increment(payload));
    }, 2000);
   };
// 导出reducer,在创建store时使用到
export default counterSlice.reducer;

movieSlice.ts

import {createSlice} from '@reduxjs/toolkit'

export const movieSlice = createSlice({
    name:'movie',
    initialState:{
        nowMovie:'time is god',
        title:'redux toolkit pre'
    },
    reducers:{
        changeMovieName(state,{payload}){
            state.nowMovie =  payload.name  // 内置了immutable
        },
    }
})
export const {changeMovieName}  = movieSlice.actions 

// 内置了thunk插件,可以直接处理异步请求
export const asyncIncrement = (payload:any) => (dispatch:any) => {
    setTimeout(() => {
    dispatch(changeMovieName(payload));
    }, 2000);
};

export default movieSlice.reducer; // 导出reducer,在创建store时使用到

index.ts

import { configureStore } from '@reduxjs/toolkit';
import counterSlice from './features/counterSlice';
import movieSlice from './features/movieSlice';
// configureStore创建一个redux数据
export default configureStore({
 reducer: {
    counter: counterSlice,
    movie: movieSlice,
  },
});

写好这些文件之后,在任意页面引入

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment,decrement, asyncIncrement } from '../../store/features/counterSlice'; // 引入actions
import { useEffect, useState } from 'react';



const Home  = () =>{
    const { count } = useSelector((state:any) => state.counter);
    const dispatch = useDispatch();
    // const [data,setData] = useState<any>([])
    const [debounceData,setDebounceData] = useState<number>(1)


    useEffect(() =>{
        console.log(count);
        setDebounceData(count)
    },[count])
   
    return(
        <div>
            <h2>{count}</h2> 
            <button onClick={() =>{dispatch(increment({step:3}))}}>添加</button>
            <button onClick={() =>{dispatch(decrement()) }}> 异步添加 </button>
            <input style={{width:'200px'}} type="text" />

            <div>debounce</div>
            <div>
               <input type="text" onChange={() =>{}} value={debounceData} />
            </div>
        </div>
    )
}


export default React.memo(Home)

请求拦截及处理,格式统一,严格模式等后续有空补充