返回
Vue Composables 最佳实践:避免陷阱,提升代码质量
vue.js
2025-01-11 16:46:46
Vue Composables 最佳实践
Composable 是 Vue 3 中强大的特性,它可以将可复用的逻辑提取到组件之外。正确使用 composable 能提升代码的可维护性和可测试性。本文讨论在开发 Vue 应用时使用 composable 时应该避免的常见问题,以及对应的解决办法。
Composable 的依赖关系
Composable 相互调用(如 useAuth
调用 useUser
) 容易形成复杂且难以维护的依赖关系。 当一个 composable 修改或错误时,影响范围会很大。这种模式难以追踪,给调试带来困难。
解决方案:依赖注入和控制反转
将依赖传递到 composable 中,避免直接的模块导入。可以使用 Vue 的 provide
/inject
或者简单的函数参数实现依赖注入。这样可以方便进行单元测试,也可以提高模块的复用性。
示例代码:
// useAuth.ts
import { ref } from 'vue';
import type User from '@/modules/model/User';
interface UserService {
getUserByEmail(email: string): Promise<User | null>;
addUser(user: User, userId: string): Promise<User>;
}
interface AuthService {
signInWithEmailAndPassword(email: string, password: string) : Promise<any>
createUserWithEmailAndPassword(email: string, password:string) : Promise<any>
}
export const useAuth = (authService:AuthService,userService: UserService) => {
const error = ref<string>();
const onLogin = async (email: string, password: string) => {
try {
const cred = await authService.signInWithEmailAndPassword(email, password)
const user = await userService.getUserByEmail(email);
// 简化错误判断
if(!user){
error.value = '用户不存在'
return false;
}
// 在此处处理你的 Store
console.log(user)
return true
} catch (err: any) {
error.value = handleAuthError(err)
return false;
}
};
const onRegister = async (user: User) => {
try {
const userCred = await authService.createUserWithEmailAndPassword(user.email, user.password)
console.log(await userService.addUser(user, userCred.user.uid));
return true
} catch (err: any) {
error.value = handleAuthError(err)
return false;
}
};
function handleAuthError(err:any) {
if (err?.code === 'auth/invalid-credential') {
return '账号或密码错误';
} else if (err?.name === 'FirebaseError'){
return '网络异常'
} else {
return '未知错误';
}
}
return { error, onLogin, onRegister };
};
// useUser.ts
import type User from '@/modules/model/User';
interface Firestore {
getDocs(query: any) : Promise<any>;
setDoc(docRef: any,data:any) : Promise<any>
query(collection:any,whereCondition: any): any
doc(collection: any, id: string): any
}
export const useUser = (firestore:Firestore,usersCollection: any) => {
const getUserByEmail = async (email: string): Promise<User | null> => {
const userDoc = (await firestore.getDocs(firestore.query(usersCollection,firestore.where( 'email', '==', email )))).docs[0]
if (userDoc) {
return {
...(userDoc.data() as User),
uid: userDoc.id,
};
}
return null;
};
const addUser = async (user: User, userId: string): Promise<User> => {
const createdDoc = firestore.doc(usersCollection, userId)
const userToAdd = { ...user, uid: userId };
await firestore.setDoc(createdDoc, userToAdd);
return userToAdd
};
return { getUserByEmail, addUser };
};
// 注入的模块, 使用实际的 Firebase 对象和 Collection
import { ref } from 'vue'
import { getDocs,query, where, collection , doc , setDoc,getFirestore, } from 'firebase/firestore';
import { signInWithEmailAndPassword,createUserWithEmailAndPassword,getAuth } from "firebase/auth";
const firestore = getFirestore()
const usersCollection = collection(firestore,"users")
export const userService = useUser({getDocs,setDoc,query,doc,where},usersCollection)
const auth = getAuth()
export const authService = {
signInWithEmailAndPassword : (email: string, password:string) => signInWithEmailAndPassword(auth, email, password) ,
createUserWithEmailAndPassword: (email:string, password:string) => createUserWithEmailAndPassword(auth, email, password),
}
步骤:
- 定义
UserService
和AuthService
接口来声明所需的操作。 useAuth
和useUser
接收服务接口的实现作为参数,而不是直接引入。- 在
useUser.ts
模块中引入 firestore 的方法来模拟对应的服务方法。 - 在需要的模块进行注入和初始化。
- 单元测试时,可以传入模拟实现。
数据传递到 Composables
允许 composables 接收参数增强了其灵活性,可以针对特定场景进行定制。但参数应谨慎使用,过多参数会降低可读性和维护性,这时考虑使用配置对象可能更清晰。
示例代码:
export const useAuth = ( authService : AuthService, userService: UserService, options?:{
storeKey:string;
}) => {
const storeKey = options?.storeKey ?? 'user'; //默认参数
//...逻辑
return{
... ,storeKey
}
}
const { ... ,storeKey } = useAuth( authService,userService ,{
storeKey:'otherKey'
})
步骤:
- 定义可选的
options
对象作为 composable 函数的参数。 - 使用解构方式在 composable 中设置参数的默认值。
代码可测试性
直接依赖具体实现使 composables 很难进行单元测试,应使用依赖注入增加测试性。
解决方案: Mock 服务
使用测试框架(如 Jest)和mock工具来模拟 Firebase 服务,隔离单元测试。通过控制mock的行为测试composables的逻辑。
示例:
// useAuth.spec.ts
import { useAuth } from './useAuth';
import { AuthServiceInterface , UserServiceInterface } from '@/interfaces'
const mockAuthService: AuthServiceInterface = {
signInWithEmailAndPassword : jest.fn(() => Promise.resolve({})),
createUserWithEmailAndPassword: jest.fn(()=> Promise.resolve({ user: { uid: 'testUID' }}))
}
const mockUserService : UserServiceInterface = {
getUserByEmail : jest.fn(()=> Promise.resolve( {uid:'testId',email:"test@test.com"} ) )
,
addUser : jest.fn(() => Promise.resolve({uid:'testId',email:"test@test.com"})),
}
describe('useAuth', () => {
it('Should register a new user and login it',async () => {
const { onRegister , error} = useAuth(mockAuthService ,mockUserService)
const user = { email : "test@test.com" ,password : "test",name:'test'}
await onRegister(user);
expect(mockAuthService.createUserWithEmailAndPassword).toHaveBeenCalledWith("test@test.com" , "test")
expect(mockUserService.addUser).toHaveBeenCalled();
expect(error.value).toBeUndefined()
})
it("Should not create if an firebase Error is occurred", async () =>{
const mockErrorAuth = {...mockAuthService, createUserWithEmailAndPassword : jest.fn(()=> Promise.reject({ name : "FirebaseError"}))
}
const {onRegister,error } = useAuth( mockErrorAuth ,mockUserService);
const user = { email : "test@test.com" ,password : "test",name:'test'};
await onRegister(user)
expect(error.value).toBe("网络异常")
})
it('should login a valid user', async () =>{
const {onLogin, error} = useAuth( mockAuthService ,mockUserService)
await onLogin('test@test.com','test');
expect(mockUserService.getUserByEmail).toHaveBeenCalledWith('test@test.com')
expect(error.value).toBeUndefined();
})
it('should login and not user find', async () =>{
const mockNoUser = {... mockUserService , getUserByEmail: jest.fn(()=>Promise.resolve(null))}
const {onLogin, error} = useAuth( mockAuthService ,mockNoUser)
await onLogin('test@test.com','test')
expect(error.value).toBe("用户不存在");
})
});
步骤:
- 创建模拟的服务实现来代替实际的 firebase 实现。
- 使用
jest.fn()
函数 mock 关键的signInWithEmailAndPassword
和createUserWithEmailAndPassword
函数。 - 测试
onRegister
和onLogin
方法 ,确保在不同的输入情况下 composables 可以正常工作。
安全建议
在处理用户认证和数据访问时需要格外注意安全性。 确保输入数据验证,并且采取措施保护用户凭证和敏感数据。使用合适错误处理来防止信息泄露。
正确使用 composables 能使你的 Vue 3 代码更加模块化、可测试且易于维护。遵循这些最佳实践,将显著提升开发体验和最终产品的质量。