关注

从0到1:一名前端程序员的项目沉思录

大家好,今天来聊一聊从0-1 开发项目,我参与过不止3 个这样的的项目 ,这倒不是说能力出众,只是机缘巧合,来谈谈一些经验吧

过去面试的时候,面试官总爱问:‘你做过0到1,那技术栈怎么选?你对Vue和React怎么看?我通常会侃侃而谈:Vue如何便捷,好上手,不管是底层双向绑定,还是大量语法糖,React如何灵活,原生化,体量大,适用于复杂的…但当我信心满满时,面试官一句‘难道Vue就不适合复杂场景?’就能让我瞬间语塞。一句话给我干懵了,兄弟们。其实我内心在想(这也不是我这一个小小的前端开发来决定用啥框架啊,就好比招聘的时候,已经决定好招聘方向了)。对以上,深感无奈。

但近期一个 0-1的项目,让我对‘技术选型’有了全新的认识。项目背景是重构一个jQuery老项目,需要推翻重构,我寻思这不就二期迭代吗,简简单单,看着老代码删删改改。入职后发现,前端只有我一个,甚至没有旧代码可参考...,只能看着老页面,无产品需求,无测试,无代码的处境开发,面临技术栈的选择。领导说最近公司的项目都是react,那你就用react来写吧。领导说一句话就决定了技术栈的方向(这一刻,才有些感悟,原来,我们程序员,不管我们前端,还是等等php。go,java后端等其他)可能我们握手的技术,我们的身份牌,不过是大领导的一句话,一个想法,就决定了我们在不在这家公司工作,能否有资格参与这个项目,等等。

总结

1:项目0-1 ,框架的选择,一部分来源与公司内部程序员的技术水准,比如有的会写uniap,taro,react,vue等等,包括v3,v2,reactcalss,hooks等等

2,项目后期维护以及开发周期,上手难度等,不可能这一个项目就你一个人写吧

3,公司内部,以及市场,技术栈的流通性,扩展性,生态等,

4,项目的类型,​​超大型、高度复杂的应用类型,可能就更倾向与react了,因其函数式开发,以及react特性不可变原则,开发复杂等交互逻辑项目,在组合性api开发会更加灵活,包括一些特殊需求啊native跨端啊等等,vue就适合以下常规性业务,中后台,快速迭代,vue优势很明显的,易上手,语法糖高,router,pina,vuex,vite集合耦度很高

技术实现:

本期讲解的是react的 项目开发,从脚手架搭建,-路由,布局封装,开发页面,axios,api封装,状态管理,webpack/craco基础配置,eslint代码校验配置,组件封装,开发测试生产环境配置,等这个流程

脚手架

Create React App (CRA):arco

npx create-react-app my-app

npx create-react-app my-app --template typescript




vite

npm create vite@latest

craco.config

const path = require('path');
const { whenDev } = require('@craco/craco');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  webpack: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      // ...其他别名
    },
    plugins: [
      ...whenDev(() => [new BundleAnalyzerPlugin()], []), // 开发环境分析包大小
    ],
    configure: (webpackConfig) => {
      webpackConfig.optimization.splitChunks = { // 优化分包
        chunks: 'all',
        maxInitialRequests: Infinity,
        minSize: 0,
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name(module) {
              const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
              return `npm.${packageName.replace('@', '')}`;
            },
          },
        },
      };
      return webpackConfig;
    },
  },
  jest: {
    configure: {
      moduleNameMapper: { // Jest测试别名映射
        '^@/(.*)$': '<rootDir>/src/$1',
      },
    },
  },
};
*  devServer: (devServerConfig, { env, paths }) => {
        // 添加代理配置
        devServerConfig.proxy = {
            '/api': {
                target: process.env.REACT_APP_API_BASE_URL || 'http://192**********',
                changeOrigin: true,
                pathRewrite: {
                    '^/api': '/api', // 保留 /api 前缀
                },
                onProxyReq: (proxyReq, req, res) => {
                    console.log(`代理请求: ${req.method} ${req.path} -> ${proxyReq.path}`);
                },
                onProxyRes: (proxyRes, req, res) => {
                    console.log(`代理响应: ${proxyRes.statusCode} ${req.path}`);
                },
                onError: (err, req, res) => {
                    console.error('代理错误:', err);
                }
            }
        };

在craaco.config 中做开发代理

分别对应 3个文件 

/  .env.development 文件  本地开发代理,代理后端ip,以及服务器测试地址等
REACT_APP_API_BASE_URL=http:*************
REACT_APP_ENV=development

/ .env.production   生产
REACT_APP_API_BASE_URL=https:// ************ 
REACT_APP_ENV=production

/ .env.test    测试
REACT_APP_API_BASE_URL=https:**********
REACT_APP_ENV=test


路由配置

路由布局有好几种,下只是其中一种 

路由 react-router-dom v6 

import { createBrowserRouter, Navigate } from 'react-router-dom';
import Layout from '@/layouts/MainLayout';
import AuthLayout from '@/layouts/AuthLayout';
import Login from '@/pages/Login';
import Dashboard from '@/pages/Dashboard';
import UserList from '@/pages/user/List';
import UserDetail from '@/pages/user/Detail';
import NotFound from '@/pages/NotFound';
import { checkAuth, checkAdmin } from '@/utils/auth'; 

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />, 
    errorElement: <NotFound />,
    children: [
      { index: true, element: <Navigate to="/dashboard" replace /> },
      {
        path: 'dashboard',
        element: <Dashboard />,
        loader: () => checkAuth(), // 路由守卫:检查登录
      },
      {
        path: 'user',
        children: [
          {
            index: true,
            element: <UserList />,
            loader: () => checkAuth(),
          },
          {
            path: ':id',
            element: <UserDetail />,
            loader: ({ params }) => checkAuth() && checkAdmin()
          },
        ],
      },
    ],
  },
  {
    path: '/auth',
    element: <AuthLayout />, 
    children: [
      { path: 'login', element: <Login /> },
    ],
  },
]);

export default router;

动态路由 ,懒加载 
const AdminDashboard = lazy(() => import('@/pages/admin/Dashboard'));
const FinanceReport = lazy(() => import('@/pages/finance/Report'));
可以做路由首位函数,高阶函数,等,axios 里,每次接口去校验token,
export const checkAuth = (): boolean | Response => {
  const token = localStorage.getItem('authToken');
  if (!token) {
    return redirect('/auth/login'); // 重定向到登录页
  }
  return true;
};

export const checkAdmin = (): boolean | Response => {
  const userRole = getUserRoleFromToken(); // 从token解析角色
  if (userRole !== 'ADMIN') {
    throw new Response('Forbidden', { status: 403 }); // 抛出错误,在errorElement处理
  }
  return true;
};
lyout 函数,组件库有对应的,主要是布局,左右布局,header类似
const MainLayout = () => {
  const collapsed = useAppSelector(selectSidebarCollapsed);

  return (
    <div className="flex h-screen bg-gray-50">
      <Sidebar collapsed={collapsed} />
      <div className={`flex-1 flex flex-col transition-all ${collapsed ? 'ml-16' : 'ml-64'}`}>
        <Header />
        <Breadcrumb />
        <main className="flex-1 overflow-y-auto p-4 bg-white">
          <Outlet /> {/* 核心:内容注入点 */}
        </main>
        <Footer />
      </div>
    </div>
  );
};
 
路由切换 


import { RouterProvider } from 'react-router-dom';
import router from './router';

function App() {
  return (
    <RouterProvider router={router} />
  );
}
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { message } from 'antd'; // 或其他UI库通知组件
import { getToken, clearToken, redirectToLogin } from './auth';

// 1. 创建实例
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量读取
  timeout: 15000,
  withCredentials: true, // 跨域携带cookie
});

// 2. 请求拦截器 (统一注入Token、添加取消令牌)
service.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    const token = getToken();
    if (token && config.headers) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }

    // 为每个请求添加取消令牌 (可选,防重复请求)
    config.cancelToken = new axios.CancelToken((cancel) => {
      // 可以在这里存储cancel函数,用于全局取消请求
    });

    return config;
  },
  (error: AxiosError) => {
    return Promise.reject(error);
  }
);

// 3. 响应拦截器 (统一错误处理、Token过期处理)
service.interceptors.response.use(
  (response: AxiosResponse) => {
    const res = response.data;
    // 根据后端约定处理业务逻辑错误 (例如 code !== 0)
    if (res.code && res.code !== 0) {
      message.error(res.message || '业务错误');
      return Promise.reject(new Error(res.message || 'Error'));
    }
    return res; // 直接返回后端响应的数据部分
  },
  (error: AxiosError) => {
    const status = error.response?.status;
    let errMessage = '请求错误';

    switch (status) {
      case 401:
        errMessage = '未授权,请重新登录';
        clearToken();
        redirectToLogin();
        break;
      case 403:
        errMessage = '拒绝访问';
        break;
      case 404:
        errMessage = '请求资源不存在';
        break;
      case 500:
        errMessage = '服务器内部错误';
        break;
      default:
        errMessage = error.message || '网络连接异常';
    }

    // 处理取消请求的错误 (避免弹出错误提示)
    if (!axios.isCancel(error)) {
      message.error(errMessage);
    }

    return Promise.reject(error);
  }
);


export const get = <T>(url: string, config?: AxiosRequestConfig): Promise<T> => {
  return service.get<T>(url, config).then(res => res as unknown as T);
};

export const post = <T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> => {
  return service.post<T>(url, data, config).then(res => res as unknown as T);
};

// ... 其他方法 (put, delete, etc.)

export default service;
根据 axios 封装一些 接口 


import { get, post, put, del } from '@/utils/request';

// 定义接口参数和响应类型
export interface User {
  id: string;
  name: string;
  email: string;
  role: string;
}

export interface UserQueryParams {
  name?: string;
  role?: string;
  page?: number;
  pageSize?: number;
}

export interface UserListResult {
  list: User[];
  total: number;
}

// 具体API函数
export const fetchUserList = (params: UserQueryParams): Promise<UserListResult> => {
  return get<UserListResult>('/api/users', { params });
};

export const getUserDetail = (id: string): Promise<User> => {
  return get<User>(`/api/users/${id}`);
};

export const createUser = (userData: Omit<User, 'id'>): Promise<User> => {
  return post<User>('/api/users', userData);
};

export const updateUser = (id: string, userData: Partial<User>): Promise<User> => {
  return put<User>(`/api/users/${id}`, userData);
};

export const deleteUser = (id: string): Promise<void> => {
  return del(`/api/users/${id}`);
};
状态管理 

import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';
import { useDispatch } from 'react-redux';

const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false, // 可根据需要开启/关闭
      immutableCheck: false,     // 生产环境可关闭提升性能
    }),
  devTools: process.env.NODE_ENV !== 'production', // 开发环境开启Redux DevTools
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>(); // 推荐使用的Dispatch Hook

export default store;

使用 等 
import React, { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { getUsers, selectUserList, selectUserLoading, selectUserPagination } from '@/store/slices/userSlice';
import { Table, Pagination, Spin } from 'antd';

const UserList = () => {
  const dispatch = useAppDispatch();
  const users = useAppSelector(selectUserList);
  const loading = useAppSelector(selectUserLoading);
  const pagination = useAppSelector(selectUserPagination);

  useEffect(() => {
    dispatch(getUsers({ page: pagination.current, pageSize: pagination.pageSize }));
  }, [dispatch, pagination.current, pagination.pageSize]);

  const handlePageChange = (page: number, pageSize: number) => {
    dispatch(getUsers({ page, pageSize }));
  };

  return (
    <Spin spinning={loading}>
      <Table dataSource={users} columns={[...]} pagination={false} />
      <Pagination
        current={pagination.current}
        pageSize={pagination.pageSize}
        total={pagination.total}
        onChange={handlePageChange}
        showSizeChanger
      />
    </Spin>
  );
};

eslint 配置,等
自己选项式,需要啥,要啥,不要配的太,配置是让我们开发效率变高效不是痛苦


https://eslint.org/ 

官网 可以去看着配置 

module.exports = {
  root: true,
  env: { browser: true, es2021: true, node: true },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended', // 整合Prettier
    'airbnb',
    'airbnb-typescript',
    'airbnb/hooks',
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: { jsx: true },
    ecmaVersion: 'latest',
    sourceType: 'module',
    project: './tsconfig.json', // 重要!用于TS规则
  },
  plugins: ['react', '@typescript-eslint', 'import'],
  rules: {
    // 覆盖或添加规则
    'react/react-in-jsx-scope': 'off', // React 17+
    'react/jsx-uses-react': 'off',    // React 17+
    'react/jsx-props-no-spreading': 'off',
    'import/prefer-default-export': 'off',
    'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
    '@typescript-eslint/no-unused-vars': 'warn',
    'jsx-a11y/click-events-have-key-events': 'off',
    'jsx-a11y/no-static-element-interactions': 'off',
    'react/function-component-definition': [
      'error',
      { namedComponents: 'arrow-function', unnamedComponents: 'arrow-function' },
    ],
    'prettier/prettier': 'error', // 让Prettier规则作为错误
  },
  settings: {
    react: { version: 'detect' },
    'import/resolver': {
      typescript: {}, // 解决ts别名导入问题
      node: {
        paths: ['src'],
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      },
    },
  },
};

组件封装着一些对于一个项目刚开始的阶段也至关重要,我的博客首页也有很对组件,感兴趣可以看看,在这个时代,AI一定要使用起来,以上流程可能会有遗漏,但流程基本上是全的,具体还需要根据项目来,需要啥,装啥,还需要技术栈版本之间,我最近的这个项目就遇到了问题可以看这个我遇到的这个问题, react 19版本 与antd 》5.0+ 组件 问题_react19 使用antd-CSDN博客,以下我封装的一些组件,感兴趣有需要可以看看

react 常用组件searchTable 组合式  

实现React Table(SearchTable2.0)复杂筛选功能的封装组件-CSDN博客

react 实用组件(modal表单)_react modal组件实现-CSDN博客

react 组件之 searchSelect -CSDN博客

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/a1507603776/article/details/152043456

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--