大家好,今天来聊一聊从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 Table(SearchTable2.0)复杂筛选功能的封装组件-CSDN博客
react 实用组件(modal表单)_react modal组件实现-CSDN博客
react 组件之 searchSelect -CSDN博客
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/a1507603776/article/details/152043456