前言
作为n年小说读者,用过很多小说阅读器,最近几年都用的微信读书,但是...微信读书从一开始可以免费导入500本,到后来限制200本,到现在限制每月3本...
概述
观察了下市场上有的阅读器,大致分为纯移动端页面、阅读APP、阅读小程序等方式,而我们的目的仅仅是满足自身阅读需求,作为一名摸鱼摆烂能手,掌握的技术栈自然很少...哈哈哈,所以专挑了自己熟悉的,毕竟咱们求的是快速解决需求
需求
- 拿出手机就能看
不论是小程序、APP、H5都可以实现 - 支持记录阅读进度、手机上传小说
需要一个后台服务,储存阅读进度,也需要有个地方存放小说 - 共享设备
如果更换手机或需要在平板上阅读也需要支持
综上所述,考虑上多平台,可以选择taro或uniapp等,但是摆烂人不会taro,也不想使用uniapp,再加上ios app上架还是个大问题,最后直接考虑了h5页面,如果后续有app需求或小程序需求再说,毕竟咱们一开始的目的只要能看就行
浏览器看小说不会很难受么?毕竟有底部的搜索栏(safari),正在纠结时,发现了safari支持添加页面添加到主屏幕,添加后效果如图所示
nestjs
掌握的技术栈少之又少,除了当年学校里学的java(现在也忘得差不多了)基本一窍不通,最后选了nestjs来练手
新手第一次写,写错轻喷
CURD
npm i -g @nestjs/cli // 全局安装
Nest nest new project-name // 创建项目
安装完成后,可以通过以下命令生成一个包含CRUD操作的控制器:
nest g resource user
官方也有提供数据的库的连法,可以参考下:
npm install typeorm mysql
@Module({
imports: [
...,
TypeOrmModule.forRootAsync({
useFactory(configService: ConfigService) {
return {
type: 'mysql',
host: configService.get('DB_HOST'),
connectorPackage: 'mysql2', //驱动包
port: configService.get('DB_PORT'), // 端口号
username: configService.get('DB_USER'), // 用户名
password: configService.get('DB_PASSWD'), // 密码
database: configService.get('DB_DATABASE'), //数据库名
entities: [
User,
File,
Dict,
Book,
], //数据库对应的Entity
synchronize: !isProd, //是否自动同步实体文件,生产环境建议关闭
};
},
inject: [ConfigService],
}),
...
],
})
增删改查service实现
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
private readonly fileService: FileService,
) {}
async create(createUserDto: CreateUserDto) {
return await this.userRepository.save(createUserDto);
}
async findAll() {
return await this.userRepository.find();
}
async findOne(id: string) {
const user = await this.userRepository.findOneBy({ id });
let head_img;
if (user.head_img) {
head_img = await this.fileService.findOne(user.head_img);
}
return { ...user, head_img };
}
async findOneByUsername(username: string) {
const user = await this.userRepository.findOneBy({ username });
let head_img;
if (user.head_img) {
head_img = await this.fileService.findOne(user.head_img);
}
return { ...user, head_img };
}
async update(id: string, updateUserDto: UpdateUserDto) {
return await this.userRepository.update({ id }, updateUserDto);
}
async remove(id: string) {
return await this.userRepository.update({ id }, { status: 0 });
}
}
token
用户有了,接下来就是登录,如法炮制写了auth模块,唯一的区别是,我们需要利用缓存存起来用户登录生成的token,并用于后续的验证
利用redis存储token
import { RedisClientType } from 'redis';
@Injectable()
export class CacheService {
constructor(@Inject('REDIS_CLIENT') private redisClient: RedisClientType) {}
//获取值
async get(key) {
let value = await this.redisClient.get(key);
try {
value = JSON.parse(value);
} catch (error) {}
return value;
}
}
token校验
@Module({
...,
providers: [
...,
{
provide: APP_GUARD,
useClass: UserGuard,
},
],
})
@Injectable()
export class UserGuard implements CanActivate {
constructor(
private reflector: Reflector,
private cacheService: CacheService,
) {}
/**
* 判断请求是否通过身份验证
* @param context 执行上下文
* @returns 是否通过身份验证
*/
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
//即将调用的方法
context.getHandler(),
//controller类型
context.getClass(),
]);
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest(); // 获取请求对象
const token = this.extractTokenFromHeader(request); // 从请求头中提取token
if (!token) {
throw new HttpException('token无效', HttpStatus.UNAUTHORIZED); // 如果没有token,抛出验证不通过异常
}
const realToken: any = await this.cacheService.get(token);
if (!realToken) {
throw new HttpException('token无效', HttpStatus.UNAUTHORIZED); // 如果没有token,抛出验证不通过异常
}
return true; // 身份验证通过
}
/**
* 从请求头中提取token
* @param request 请求对象
* @returns 提取到的token
*/
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? []; // 从Authorization头中提取token
return type === 'Bearer' ? token : undefined; // 如果是Bearer类型的token,返回token;否则返回undefined
}
}
跨域问题
解决方式很简单,在main.ts加上
app.enableCors();
文件上传
轻松实现,文件存取
file.controller.ts
代码解读
复制代码
@Post('uploadBook')
@UseInterceptors(
FileInterceptor('file', {
storage: bookStorage,
// 限制图片大小
limits: {
fileSize: 1024 * 1024 * 10, // 2M
},
}),
)
async uploadBook(@UploadedFile() file: Express.Multer.File, @Req() request) {
const _file = {
name: file.originalname,
filename: file.filename,
path: file.path,
mimetype: file.mimetype,
size: file.size,
};
return await this.fileService.create(_file, request);
}
typescript
代码解读
复制代码
export const fileName = (name: string) => {
return `${Date.now()}-${name}`;
};
export const bookStorage = diskStorage({
destination: `./public/books/${dayjs().format('YYYY-MM')}`,
// 自定义上传的文件名字,
filename: (req, file, cb) => {
// const buffer = Buffer.from(file.originalname, 'binary');
return cb(null, fileName(file.originalname));
},
});
txt文件编码问题,显示乱码
还是想的太简单,实际写完存取之后发现,存储的文件名称乱码、读取的txt内容内容乱码/(ㄒoㄒ)/~~
考虑过前端来做这些转码的工作的,然后发现大多数库并不支持浏览器,还是在存储文件的时候做更稳妥
实现思路大致是这样的:
- 检测文件编码,使用
jschardet库对buffer进行编码检测,如果未识别到编码,则默认使用 UTF-8 - 转换为 UTF-8,如果检测到的编码不是 UTF-8:用
iconv-lite库解码原始buffer为字符串,再将解码后的内容重新编码为 UTF-8 格式的buffer - 重写文件,使用
fs.writeFile将转码后的utf8Buffer重写到原文件路径上
file.controller.ts
代码解读
复制代码
@Post('uploadBook')
@UseInterceptors(
FileInterceptor('file', {
storage: bookStorage,
// 限制图片大小
limits: {
fileSize: 1024 * 1024 * 10, // 2M
},
}),
)
async uploadBook(@UploadedFile() file: Express.Multer.File, @Req() request) {
const filePath = file.path;
const buffer = await fs.readFile(filePath); // 读取文件内容
// 检测文件编码
const detection = jschardet.detect(buffer);
const encoding = detection.encoding?.toLowerCase() || 'utf-8';
// 转换为 UTF-8
const utf8Content = iconv.decode(buffer, encoding);
const utf8Buffer = iconv.encode(utf8Content, 'utf-8');
// 重写文件为 UTF-8
await fs.writeFile(filePath, utf8Buffer);
const _file = {
name: Buffer.from(file.originalname, 'latin1').toString('utf8'),
filename: file.filename,
path: file.path,
mimetype: file.mimetype,
size: file.size,
};
return await this.fileService.create(_file, request);
}
utils.ts
代码解读
复制代码
export const fileName = (name: string) => {
const originalName = Buffer.from(name, 'latin1').toString('utf8');
return `${Date.now()}-${originalName}`;
};
export const bookStorage = diskStorage({
destination: `./public/books/${dayjs().format('YYYY-MM')}`,
// 自定义上传的文件名字
filename: (req, file, cb) => {
// const buffer = Buffer.from(file.originalname, 'binary');
return cb(null, fileName(file.originalname));
},
});