react初学者之项目搭建全过程学习笔记

一.创建项目文件

npx create-react-app react-ts-app --template typescript

二.安装依赖

2.1.安装ant-design前端框架+sass/scss

npm i sass scss antd --save

2.2.安装完成后将src中的css改成scss

App.css => App.scss
index.css => index.scss

相应代码中的import也需要调整。

2.3.安装react-app-rewired、customize-dev插件

npm i react-app-rewired customize-dev --save-dev

2.3.1.react-app-rewired

  • 作用:React App Rewired 允许您在不弹出 react-scripts 的情况下修改 CRA 的默认配置。CRA 通过 react-scripts 封装了 Webpack 和其他构建工具的配置,但是通常情况下这些配置是隐藏的,不容易修改。
  • 使用场景:当您需要自定义 Webpack 配置,例如添加额外的 loader、plugin、alias 等,或者修改默认的 Babel 配置,都可以通过 react-app-rewired 实现。
  • 如何使用:通过在项目中引入 react-app-rewired 和相关的配置文件,您可以在 config-overrides.js(默认名称)中修改 Webpack 配置,而不是直接编辑 CRA 自动生成的配置文件。

2.3.2.customize-cra

  • 作用customize-cra 是一个基于 react-app-rewired 的插件,提供了一些预定义的配置函数,使得定制 CRA 的配置更为简单和模块化。
  • 使用场景:如果您只需执行一些常见的定制化操作(如修改 Babel 配置、添加额外的 loader 或 plugin),customize-cra 提供的 API 可以帮助您更快速地实现这些操作,而无需自行编写复杂的 Webpack 配置。

2.4.替换脚本命令

"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  },

2.5.在根目录创建config.overrides.js文件,然后配置webpack @指向src路径

const {
    override,
    addWebpack
} = require('customize-cra')   //导入override,addWebpack这俩函数用于修改webpack

const path = require('path')   //导入node.js核心模块path,处理和转换文件

module.export = override(      //导出配置,将 @ 符号映射到项目根目录下的 src 文件夹
    addWebpack({
        '@': path.resolve(__dirname,'src')
    })
)

2.6.需要ts生效@写法得在根目录下面创建tscofig.extend.json文件

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

2.7.然后再在tsconfig.json中添加映射

{
    "extend":"./tsconfig.extend.json",
    "compileOnSave":{}
}

三.安装Redux,使用 Redux 的主要作用是管理应用的状态

npm i @reduxjs/toolkit react-redux

3.1.再在src中添加以下文件层级

src
├─ app
│  └─ store.ts
├─ features
│  ├─ counter
│  │  └─ counterSlice.ts

3.2 counterSlice.ts中添加以下代码

import {creatSlice} from '@redux.toolkit'

export interface CounterState {                 //定义状态类型
    value:number
}

export initialState = createSlice({            //初始化状态
    value:0
})

export const counterSlice = createSlice({     //创建 Redux slice counterSlice
    name:'counter',
    initialState,
    reducers:{                                //reducers 是一个包含多个 reducer 函数的对象,                        
        increment:(state) => {                //每个 reducer 函数负责处理一个特定的 action 类
            state.value += state.value        //型对状态的变化
        },
        decrement:(state) => {
            state.value -= state.value
        },
        incrementByAmount:(state,action) => {
            state.value += action.payload
        }
    }
})

export const {increment,decrement,incrementByAmount} = counterSlice.action
export default counterSlice.reducer

3.3.接下来就可以将counterSlice导入store.ts中

import {configureStore} from '@redux.toolkit'
import counterSlice from '@/features/counter/counterSlics'

export default configureStore({
    reducer:{
        counter:counterReducer    // 可以添加更多模块
    }
})

3.4.在src/index.tsx中全局配置store

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.scss';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import store from './app/store';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

四.使用Eslint + Prettier 作为代码检查自动格式化

vscode需要安装 Eslint + Prettier插件

4.1.安装依赖命令

npm i eslint@7.32.0 @typescript-eslint/eslint-plugin@5.0.0 @typescript-eslint/parser@5.0.0 @eslint/js@9.0.0 globals@15.0.0 --save-dev
npm i eslint-config-prettier eslint-plugin-prettier prettier --save-dev

4.2.在根目录新建以下文件

├─ .vscode
│  └─  settings.json
├─ .eslintignore
├─ .eslintrc.js
├─ prettier.config.js

4.3.setting.json

{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}

4.4. .eslintignore

# .eslintignore
build/
dist/
node_modules/

4.5.  .eslintrc.js

// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended'
  ],
  settings: {
    react: {
      version: '999.999.999' //消除npm run lint时的警告信息
    }
  },
  overrides: [
    {
      env: {
        node: true
      },
      files: ['.eslintrc.{js,cjs}'],
      parserOptions: {
        sourceType: 'script'
      }
    }
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module'
  },
  plugins: ['@typescript-eslint', 'react'],
  rules: {
    'no-unused-vars': 'off',
    '@typescript-eslint/no-var-requires': 0,
    '@typescript-eslint/no-unused-vars': 'off',
    '@typescript-eslint/no-explicit-any': ['off']
  }
};

4.6. prettier.config.js

// prettier.config.js
module.exports = {
  // 一行最多 100 字符
  printWidth: 100,
  // 使用 2 个空格缩进
  tabWidth: 2,
  // 不使用缩进符,而使用空格
  useTabs: false,
  // 行尾需要有分号
  semi: true,
  // 使用单引号
  singleQuote: true,
  // 对象的 key 仅在必要时用引号
  quoteProps: 'as-needed',
  // jsx 不使用单引号,而使用双引号
  jsxSingleQuote: false,
  // 末尾不需要逗号
  trailingComma: 'none',
  // 大括号内的首尾需要空格
  bracketSpacing: true,
  // jsx 标签的反尖括号需要换行
  jsxBracketSameLine: false,
  // 箭头函数,只有一个参数的时候,也需要括号
  arrowParens: 'always',
  // 每个文件格式化的范围是文件的全部内容
  rangeStart: 0,
  rangeEnd: Infinity,
  // 不需要写文件开头的 @prettier
  requirePragma: false,
  // 不需要自动在文件开头插入 @prettier
  insertPragma: false,
  // 使用默认的折行标准
  proseWrap: 'preserve',
  // 根据显示样式决定 html 要不要折行
  htmlWhitespaceSensitivity: 'css',
  // 换行符使用 lf
  endOfLine: 'lf'
};

然后重启vscode。

五.路由配置+登录拦截+ant样例+redux样例

5.1.添加多个目录结构

src
├─ components
│  └─ withRouter.tsx
├─ router
│  └─ index.tsx
├─ pages
│  ├─ layout
│  │  ├─ index.scss
│  │  └─ index.tsx
│  ├─ home
│  │  └─ index.tsx
│  ├─ 404
│  │  └─ index.tsx
│  ├─ login
│  │  └─ index.tsx
│  ├─ table
│  │  └─ index.tsx

六.安装路由

6.1. 安装依赖

npm i react-router-dom

6.2.在目录中添加路由高阶组件WithRouter.tsx

import Login from '../pages/login/index';
import Layout from '../pages/layout/index';
import Home from '../pages/home/index';
import Other from '../pages/other/index';
import NotPage from '../pages/404/index';
import { useRoutes, Navigate, RouteObject } from 'react-router-dom';
import React from 'react';

function getItem(
  path: RouteObject['path'],
  element: RouteObject['element'],
  children?: RouteObject['children']
): RouteObject {
  return {
    path,
    element,
    children
  } as RouteObject;
}

// 设置路由的地方
const routers: RouteObject[] = [
  getItem('/login', <Login></Login>),
  getItem('/', <Layout></Layout>, [
    getItem('/', <Home></Home>),
    getItem('/other', <Other></Other>)
  ]),
  getItem('*', <NotPage></NotPage>)
];

const isUserAuthenticated = () => {
  const token = localStorage.getItem('token');
  return token && token.length > 0;
};

const AuthRoute: React.FC<React.PropsWithChildren> = (props: React.PropsWithChildren) => {
  if (!isUserAuthenticated()) {
    return (
      <div>
        {props.children}
        <Navigate to="/login" />
      </div>
    );
  }
  return <div>{props.children}</div>;
};
AuthRoute.displayName = 'AuthRoute';

const RouterInterceptor: React.FC = (props: React.PropsWithChildren) => {
  const routerEls = useRoutes(routers);
  return <AuthRoute>{routerEls}</AuthRoute>;
};
RouterInterceptor.displayName = 'RouterInterceptor';
export default RouterInterceptor;

6.3.添加路由拦截器和路由配置

import Login from '../pages/login/index';
import Layout from '../pages/layout/index';
import Home from '../pages/home/index';
import Other from '../pages/other/index';
import NotPage from '../pages/404/index';
import { useRoutes, Navigate, RouteObject } from 'react-router-dom';
import React from 'react';

function getItem(
  path: RouteObject['path'],
  element: RouteObject['element'],
  children?: RouteObject['children']
): RouteObject {
  return {
    path,
    element,
    children
  } as RouteObject;
}

// 设置路由的地方
const routers: RouteObject[] = [
  getItem('/login', <Login></Login>),
  getItem('/', <Layout></Layout>, [
    getItem('/', <Home></Home>),
    getItem('/other', <Other></Other>)
  ]),
  getItem('*', <NotPage></NotPage>)
];

const isUserAuthenticated = () => {
  const token = localStorage.getItem('token');
  return token && token.length > 0;
};

const AuthRoute: React.FC<React.PropsWithChildren> = (props: React.PropsWithChildren) => {
  if (!isUserAuthenticated()) {
    return (
      <div>
        {props.children}
        <Navigate to="/login" />
      </div>
    );
  }
  return <div>{props.children}</div>;
};
AuthRoute.displayName = 'AuthRoute';

const RouterInterceptor: React.FC = (props: React.PropsWithChildren) => {
  const routerEls = useRoutes(routers);
  return <AuthRoute>{routerEls}</AuthRoute>;
};
RouterInterceptor.displayName = 'RouterInterceptor';
export default RouterInterceptor;

6.4.ant Form + Table结合的页面 table/index.tsx

import React, { useState } from 'react';
import { Button, Table, Form, Row, Col, Space, Input } from 'antd';
import { DownOutlined } from '@ant-design/icons';
import type { TableColumnsType, TablePaginationConfig } from 'antd';

interface DataType {
  key: React.Key;
  name: string;
  age: number;
  address: string;
}

const columns: TableColumnsType<DataType> = [
  {
    title: 'Name',
    dataIndex: 'name'
  },
  {
    title: 'Age',
    dataIndex: 'age'
  },
  {
    title: 'Address',
    dataIndex: 'address'
  }
];

const data: DataType[] = [];
for (let i = 0; i < 46; i++) {
  data.push({
    key: i,
    name: `Edward King ${i}`,
    age: 32,
    address: `London, Park Lane no. ${i}`
  });
}

interface SearchFormFields {
  label: string;
  name: string;
  element: any;
  rule?: object[];
}

const fields: SearchFormFields[] = [
  {
    label: 'Name',
    name: 'name',
    element: <Input placeholder="请输入" />
  },
  {
    label: 'Age',
    name: 'age',
    element: <Input placeholder="请输入" />
  },
  {
    label: 'Address',
    name: 'address',
    element: <Input placeholder="请输入" />
  }
];

const TableList: React.FC = () => {
  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
  const [loading, setLoading] = useState(false);
  const [expand, setExpand] = useState(false);

  const [form] = Form.useForm();

  const getFields = () => {
    const count = expand ? fields.length : 3;
    const children = [];
    for (let i = 0; i < count; i++) {
      const element = fields[i];
      children.push(
        <Col span={8} key={i}>
          <Form.Item name={element.name} label={element.label} rules={element.rule}>
            {element.element}
          </Form.Item>
        </Col>
      );
    }
    return children;
  };

  const pageSetting: TablePaginationConfig = {
    showSizeChanger: true,
    showQuickJumper: true,
    showTotal: (total) => `Total ${total} items`
  };

  const start = () => {
    setLoading(true);
    // ajax request after empty completing
    setTimeout(() => {
      setSelectedRowKeys([]);
      setLoading(false);
    }, 1000);
  };

  const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
    console.log('selectedRowKeys changed: ', newSelectedRowKeys);
    setSelectedRowKeys(newSelectedRowKeys);
  };

  const rowSelection = {
    selectedRowKeys,
    onChange: onSelectChange
  };

  const formStyle: React.CSSProperties = {
    maxWidth: 'none',
    padding: 24
  };

  const onFinish = (values: any) => {
    console.log('Received values of form: ', values);
  };

  const hasSelected = selectedRowKeys.length > 0;
  return (
    <div>
      <Form form={form} name="advanced_search" style={formStyle} onFinish={onFinish}>
        <Row gutter={24}>{getFields()}</Row>
        <div style={{ textAlign: 'right' }}>
          <Space size="small">
            <Button type="primary" htmlType="submit">
              Search
            </Button>
            <Button
              onClick={() => {
                form.resetFields();
              }}
            >
              Clear
            </Button>
            {fields.length > 3 ? (
              <a
                style={{ fontSize: 12 }}
                onClick={() => {
                  setExpand(!expand);
                }}
              >
                <DownOutlined rotate={expand ? 180 : 0} /> Collapse
              </a>
            ) : (
              <span></span>
            )}
          </Space>
        </div>
      </Form>
      <Table
        pagination={pageSetting}
        rowSelection={rowSelection}
        columns={columns}
        dataSource={data}
      />
    </div>
  );
};
export default TableList;

6.5.修改App.tsx

import React from 'react';
import { ConfigProvider } from 'antd';
import dayjs from 'dayjs'
import zhCN from 'antd/locale/zh_CN'
import './App.scss';
import { useRoutes } from 'react-router-dom';
import RouterInterceptor from '@/router/index'

const App: React.FC = () => {
  return (
    <ConfigProvider>
      <div className='App'>
        <RouterInterceptor />
      </div>
    </ConfigProvider>
  )
}
export default App

6.7.修改index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.scss';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import store from './app/store';
import { BrowserRouter } from 'react-router-dom'

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </Provider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值