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