前端实习周记11—多为自己的项目踩坑分享

284 阅读4分钟

本周公司业务方面做的没有什么挑战性的事务,多为调试接口,也没遇到特别的困难,故以下专注于对自己项目学到的新知识和踩坑进行总结。

Nest.js

设置跨域

在main.ts中添加,origin为允许跨域访问的地址

 app.enableCors({
    origin:'http://localhost:8888'
  })

封装统一拦截器

前后端统一传递数据规则,因为我前端项目使用ts约束并封装了axios,后端相关返回的数据规则格式也要跟上,由于nest.js自带默认返回的失败数据规则适用,这里只封装请求成功的拦截器,新建一个response.ts文件:

interface data<T>{
    data:T
}
 
@Injectable()
export class Response<T = any> implements NestInterceptor {
    intercept(context: any, next: CallHandler):Observable<data<T>> {
        return next.handle().pipe(map(data => {
            return {
               data,
               statusCode:200,
               message:"请求成功"
            }
        }))
    }
}

在main.ts中引入

 app.useGlobalInterceptors(new Response())

其他踩坑

jwt引入secret要为字符串

在设置jwt_secret时,在.env中设置了,并且在使用的user.module中引入了但并没有作用

@Module({
  imports:[
    TypeOrmModule.forFeature([User]),
    JwtModule.register({
      secret:process.env.jwt_secret,
      signOptions:{expiresIn:'1d'}
​
    })],
  controllers: [UserController],
  providers: [UserService,JwtService]
})

经查看相关issue和stackoverflow,发现secret要为字符串,修改为

@Module({
  imports:[
    TypeOrmModule.forFeature([User]),
    JwtModule.register({
      secret:""+process.env.jwt_secret, //或者使用``${}也可以
      signOptions:{expiresIn:'1d'}
​
    })],
  controllers: [UserController],
  providers: [UserService],
})

不要重复引入Service

同样的在上述代码中,我在UserService中已经引入了JwtService

private jwtService: JwtService

所以在这里的provider就没必要引入,不然会报错

登陆interceptor拦截器不生效

这里的拦截器和上面统一返回信息的拦截器不一致,指的是我们在user.entity中排除了密码字段,使其返回给前端的时候不传输密码,具体实现如下:

user.entity.ts

@Entity('user')
export class User{
    @PrimaryGeneratedColumn('uuid')  //使用uuid为每一位用户生成独立唯一的id
    id:number;
​
    @Column({length:100})
    username:string;
​
    @Exclude() //排除密码字段
    @Column({length:100})
    password:string;}

user.controller.ts

//@UseInterceptors(ClassSerializerInterceptor)拦截entity中column为exclude的字段,不让其返回
  @UseInterceptors(ClassSerializerInterceptor)
  @Post('register')
    register(@Body() createUser: CreateUserDto) {
      return this.userService.register(createUser);
    }

而在上周我编写的user.service中

 //登录
  async login(createUser:CreateUserDto){
    const { username,password } = createUser;
    
    const payload = { username };
​
    const existUser = await this.userRepository.findOne({
      where: { username }
  });
    if(existUser && await bcrypt.compare(password,existUser.password)){
      return {
        ...existUser, //返回了一个浅复制的对象,而不是user.entity本身
        accessToken:this.jwtService.sign(payload)
      }
    }
    throw new HttpException("用户名或密码错误", HttpStatus.BAD_REQUEST)
}

返回了一个浅复制的对象,而不是user.entity本身,所以拦截器没生效,返回给前端的数据中仍有密码,修改为

 //登录
  async login(createUser:CreateUserDto){
    const { username,password } = createUser;
    
    const existUser = await this.userRepository.findOne({
      where: { username }
  });
   
    if(!existUser || !await bcrypt.compare(password,existUser.password)){
      throw new HttpException("用户名或密码错误", HttpStatus.BAD_REQUEST)
    }
​
    const payload = { sub:existUser.id,username:username }
      return {
        user:existUser, //返回本身即可
        accessToken:this.jwtService.sign(payload)
      }
    
​
}
}

前端部分

封装axios

这部分参考了前端架构带你 封装axios,一次封装终身受益「美团后端连连点赞」 - 掘金 (juejin.cn)大佬写的文章,但实际运用上也根据自己的需求,修复了一些错误、进行了进一步的封装和修改。

首先定义一个interface统一管理后端传递过来的数据,这里就要和上面我所提到的nest.js中返回请求的拦截器格式统一了。

export interface IResponse<T> {
    data: T;
    statusCode: string;
    message: string;
}

我们最终需要实现的是:

const [err, res] = await request('GET', url, {});

将err和res集成统一返回并resolve,在调用接口时不需要进行冗长的try catch操作,同时也能够区分错误类型,使用axios拦截器统一处理请求头、请求体等功能,首先编写request方法将err和res集成统一返回并resolve:

export const request = (
    method: 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH', //restful api
    url: string, //相对路径
    data: IAnyObject, 
    params: IAnyObject = {}, 
    clearFn?: IFn //对传递过来的数据进行进一步处理
): Promise<[any, any]> => {
    return new Promise((resolve) => {
        let requestPromise: Promise<AxiosResponse>;
​
        switch (method) {
            case 'GET': {
                requestPromise = axios.get(url, { params });
                break;
            }
            case 'POST': {
                requestPromise = axios.post(url, data, { params });
                break;
            }
            case 'DELETE': {
                requestPromise = axios.delete(url, { data, params });
                break;
            }
            case 'PUT': {
                requestPromise = axios.put(url, data, { params });
                break;
            }
            case 'PATCH': {
                requestPromise = axios.patch(url, data, { params });
                break;
            }
            default: {
                resolve([new Error('Unsupported method'), undefined]);
                return;
            }
        }
​
        requestPromise
            .then((result) => {
                let res: any;
                if (clearFn !== undefined) {
                    res = clearFn(result.data);
                } else {
                    res = result.data; //结合我们返回的数据类型,我们只关心data中的具体数据而不关心状态码等其他内容,避免使用时嵌套这里直接返回即可
                }
                resolve([null, res]);
            })
            .catch((err) => {
                resolve([err, undefined]);
            });
    });
};

这样在api中调用的时候,则为:

/**
 * 用户登录
 * @body {username:string,password:string}
 */
export const login = async (body: { username: string; password: string }) => {
    return request('POST', config.login, body);
};

具体页面中:

const loginRequest = async (loginData: authData) => {
            const [err, res]: [any, loginedData | undefined] = await login(loginData);
            if (!err && res) {
                message.info('登录成功');
                //处理登陆成功相关信息
​
            }
        };
​
        loginRequest(loginData);//调用方法

写到这里可能就会有朋友疑惑,那么如果请求出了问题,我们应该怎么处理呢?

一方面,可以在每个请求中单独处理

if(err && !res){
    message.info(err.data.message)
}

另一方面,我们何尝不在请求返回时使用axios拦截器统一处理呢?

axios.interceptors.request.use(
    (config) => {
        config = handleRequestHeader(config); //统一配置请求头
        return config;
    },
    (err) => {
        return Promise.reject(err);
    }
);
​
axios.interceptors.response.use(
    (response) => {
        if (response.data.statusCode === 200 || response.data.statusCode === 201) {
            return response.data;
        } else {  //展示状态码错误
            handleGeneralError(response.data.statusCode, response.data.message);
        }
    },
    (err) => { //展示其他错误
        handleNetworkError(err.response.status, err.response.data.message);
        return Promise.reject(err.response.data);
    }
);

上述使用到的函数方法可以写在另一个文件中引入:

export const handleRequestHeader = (config: any) => {
    config['Content-Type'] = 'application/json;charset=utf-8';
    config['timeout'] = 60000;
    return config;
};
​
export const handleNetworkError = (
    errStatus?: number,
    errmsg?: string
): void => {
    const networkErrMap: any = {
        '401': '未授权,请重新登录',
        '403': '拒绝访问',
        '404': '请求错误,未找到该资源',
        '405': '请求方法未允许',
        '408': '请求超时',
        '500': '服务器端出错',
        '501': '网络未实现',
        '502': '网络错误',
        '503': '服务不可用',
        '504': '网络超时',
        '505': 'http版本不支持该请求'
    };
    if (errStatus) {
        message.error(networkErrMap[errStatus] ?? `错误: ${errmsg}`);
        return;
    }
​
    message.error('无法连接到服务器!');
};
​
export const handleGeneralError = (
    statusCode: string,
    errmsg: string
): boolean => {
    if (statusCode !== '0') {
        message.error(errmsg);
        return false;
    }
​
    return true;
};
​

最后,不要忘记为我们的axios配置baseUrl,这里使用环境变量配置

// axios baseURL
const { VITE_BASE_API_URL } = import.meta.env;
axios.defaults.baseURL = VITE_BASE_API_URL;

路由

路由守卫

使用路由守卫,通过判断相应逻辑,使得满足条件时展现一个路由,未满足条件时展示另一个路由。这里我实现的了一个PrivateComponent,通过localstorage中的token判断是否用户是否登录,如果用户没有登录,则默认跳转到需要其登录的页面,如果用户登录了才展示真实页面

privateComponent.ts

export const PrivateRoute = ({ component }: any) => {
    const isLogined = localStorage.getItem('token');
    if (!isLogined) {
        return <Navigate to="/auth" />;
    }
    const Component = component;
    return <Component />;
};
​

在route-index.ts中为相应的路由使用

const routers=[
    {
        path: '/add',
        element: <PrivateRoute component={Add} />
    }
]
​
const Router = () => {
    return createBrowserRouter(routers);
};

路由懒加载

采用 懒加载 Lazy 实现路由懒加载效果,可以优化路由加载效率,减少一些初始化的加载过程。在实现懒加载时,需要结合 Suspense 组件 一起使用。既然要使用Suspense组件,何必不封装一个懒加载组件直接调用呢?

说干就干:

import { lazy, ComponentType, Suspense } from 'react';
import { Spin } from 'antd';
export const LazyLoad = <T extends ComponentType<any>>(
    importer: () => Promise<{ default: T }>
): React.FC => {
    const LazyComponent = lazy(() => importer()) as unknown as React.FC;
​
    return () => (
        <Suspense
            fallback={
                <Spin
                    size="large"
                    style={{
                        display: 'flex',
                        alignItems: 'center',
                        justifyContent: 'center',
                        height: '100%'
                    }}
                />
            }
        >
            <LazyComponent />
        </Suspense>
    );
};
​

引入组件时:

const Add = LazyLoad(() => import('&/views/Add'));

路由相关其他踩坑

登录后跳转页面navigate不生效

原本的项目结构是在Me组件下分为unAuth和Auth两个组件,但这样route中没办法配置,即便配置了也只会默认跳转到auth页面,现修正Me和Auth分为两个组件、两个页面

Outlet和相对路由结合用法

嵌套路由,可以保证子路由共享父路由的界面而不会覆盖。为此React提供了Outlet组件,将其用于父组件中可以为子路由的元素占位,并最终渲染子路由的元素,所以当我们在路由中配置了子路由如下时:

const routers = [
    {
        path: '/home',
        element: <Home />,
        children: [
            {
                path: 'recommend',
                element: <Recommend />
            },
            {
                path: 'focus',
                element: <Focus />
            }
        ]
    },]

在Home组件中直接:

<Header />
<Outlet /> //即可根据子路由展示内容
<Footer />

此外,还写了默认路由跳转和找不到路由的页面,因较为简单故不放上来了。

其他踩坑

console.log不显示

经排查,是因为在vite配置中(vite.config.ts)没有对当前环境判断直接就写了移除console的语句

esbuild: {
        drop: ['console', 'debugger']
    },

可以添加一个变量

export default defineConfig(({ command, mode }) => {
    const isBuild = mode === 'production' && command === 'build';
​
    return {
        esbuild: {
            drop: isBuild ? ['console'] : []
        },
    }

Vite环境变量引用

环境变量是指根据当前的代码环境变化的变量,在webpack中,webpack使浏览器可以直接识别node的process.env变量,从而实现了浏览器识别环境变量的功能。但是在vite中,我们的代码运行在浏览器环境中,因此是无法识别process.env变量的。

vite在 import.meta.env 对象上暴露环境变量,所以引入环境变量的时候需要

const { VITE_BASE_API_URL } = import.meta.env;

此外,环境变量必须以VITE开头!!😢不然是读取不到的

Warning: Functions are not valid as a React child

在我编写PrivateComponent的时候,原先写法

interface PrivateRouteProps<T extends React.FC<{}>> {
    component: T;
}
export const PrivateRoute = <T extends React.FC<{}>>({
    component
}: PrivateRouteProps<T>) => {
    const isLogined = localStorage.getItem('token');
    if (!isLogined) {
        return <Navigate to="/auth" />;
    }
    return <>{component}</>;
};

结果报错我返回的是一个函数,经过排查修改为

export const PrivateRoute = ({ component }: any) => {
    const isLogined = localStorage.getItem('token');
    if (!isLogined) {
        return <Navigate to="/auth" />;
    }
    const Component = component;
    return <Component />;
};