热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

做了一个Nest.js上手项目,很丑,但适合练手和收藏

大厂技术高级前端Node进阶点击上方程序员成长指北,关注公众号回复1,加入高级Node交流群前言最近爱了上Nest.js这个框架,边学边做

 大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

前言

最近爱了上 Nest.js 这个框架,边学边做了一个 nest-todo 这个项目。

https://github.com/haixiangyan/nest-todo

没错,就是一个 UI 很丑陋的 Todo List App。不知道为啥,慢慢开始喜欢上这种原始风味的 UI 样式了,不写 CSS 也挺好看的。

虽然皮肤很丑,但是项目里面包含了大量 Nest.js 文档里的知识点(除了 GraphQL 和微服务,这部分平常用得不多就不瞎整了),能实现的点我基本都想个需求实现了:

为什么

为什么要做这个项目呢?市面上的文章和博客看了不少,很多都浅尝辄止,写个 CRUD 就完事了,也太 easy 了,一行 nest g resource 就搞定。所以,就想实现一个 大而全 的 Nest.js 的 Demo 出来。

除此之外,这个 Demo 还能给很多要马上上手的前端一个示范。虽然 Nest.js 文档也齐全,但是如果你稍微做重一点的业务,它就有点顶不住了,很多东西都要 。那这个时候 nest-todo 就可以站出来说:“不会就抄我吧,我肯定能 Work”。

前端

前端部分主要使用 React 来实现,仅有 0.0000001% 的样式,几乎都是 JS 逻辑,且有 100% TypeScript 类型提示,可大胆学习观看。

由于本项目以后端为主,所以前端也只有这些东西:

后端

后端内容则比较多了,主要就是主角 Nest.js,以及非常多的模块:

下面例举几个我觉得比较重要的模块来说说吧,当然下面都是一些代码片段,想了解更具体的实现,可以到 Github 的 nest-todo 查看。

Todo 模块

最基础的增、删、改、查。相信很多人在一些博客或文章都见过这样的写法。

TodoController 负责路由实现:

@ApiTags('待办事项')
@ApiBearerAuth()
@Controller('todo')
export class TodoController {constructor(private readonly todoService: TodoService) {}@Post()async create(@Request() request,@Body() createTodoDto: CreateTodoDto,): Promise {return this.todoService.create(request.user.id, createTodoDto);}@Get()async findAll(@Request() request): Promise {const { id, is_admin } = request.user;if (is_admin === 1) {return this.todoService.findAll();} else {return this.todoService.findAllByUserId(id);}}@Get(':id')async findOne(@Param('id', ParseIntPipe) id: number): Promise {return this.todoService.findOne(id);}@Patch(':id')async update(@Param('id', ParseIntPipe) id: number,@Body() updateTodoDto: UpdateTodoDto,) {await this.todoService.update(id, updateTodoDto);return updateTodoDto;}@Delete(':id')async remove(@Param('id', ParseIntPipe) id: number) {await this.todoService.remove(id);return { id };}
}

TodoService 则实现更底层的业务逻辑,这里则是要从数据库增、删、改、查:

@Injectable()
export class TodoService {constructor(private todoRepository: TodoRepository,private userRepository: UserRepository,) {}async create(userId: number, createTodoDto: CreateTodoDto): Promise {const user = await this.userRepository.findOne(userId);const { title, description, media } = createTodoDto;const todo = new Todo();todo.title = title;todo.description = description;todo.status = createTodoDto.status || TodoStatus.TODO;todo.media = media;todo.author = user;return this.todoRepository.save(todo);}async findAll(): Promise {return this.todoRepository.find();}async findAllByUserId(userId: number): Promise {const user = await this.userRepository.findOne({relations: ['todos'],where: { id: userId },});return user ? user.todos : [];}async findOne(id: number): Promise {return this.todoRepository.findOne(id);}async update(id: number, updateTodoDto: UpdateTodoDto) {const { title, description, status, media } = updateTodoDto;return this.todoRepository.update(id, {title,description,status: status || TodoStatus.TODO,media: media || '',});}async remove(id: number) {return this.todoRepository.delete({id,});}
}

可惜的是,这些文章和博客到此就结束了,可能作者看到这里也不想再继续搞下去了。不过,我并不打算到此结束,这才刚开始呢。

数据库模块

上面的 TodoService 里用到了数据库,那就来聊聊数据库模块。我这里的选型是 TypeORM + mariadb,为啥不用 mysql 呢?因为我用 M1 的 Mac,装不了 mysql 这个镜像,非常蛋疼。

要使用 TypeORM,就需要在 AppModule 上添加这个配置,然而,明文写配置是个沙雕做法,更好的实现应该用 Nest.js 提供的 ConfigModule 来读取配置。

这里的读取配置目前我先采用读取 .env 的配置实现,其实一般在公司里都应该有个配置中心,里面存放了 username, password 这些敏感字段,ConfigModule 则负责开启应用时读取这些配置。

读取配置这里使用 读取 .env 文件” 实现:

const loadConfig = () => {const { env } = process;return {db: {database: env.TYPEORM_DATABASE,host: env.TYPEORM_HOST,port: parseInt(env.TYPEORM_PORT, 10) || 3306,username: env.TYPEORM_USERNAME,password: env.TYPEORM_PASSWORD,},redis: {host: env.REDIS_HOST,port: parseInt(env.REDIS_PORT) || 6379,},};
};

然后再在 AppModule 使用 ConfigModuleTypeORMModule:

const libModules = [ConfigModule.forRoot({load: [loadConfig],envFilePath: [DOCKER_ENV ? '.docker.env' : '.env'],}),ScheduleModule.forRoot(),TypeOrmModule.forRootAsync({imports: [ConfigModule],inject: [ConfigService],useFactory: (configService: ConfigService) => {const { host, port, username, password, database } =configService.get('db');return {type: 'mariadb',// .env 获取host,port,username,password,database,// entitiesentities: ['dist/**/*.entity{.ts,.js}'],};},}),
];@Module({imports: [...libModules, ...businessModules],controllers: [AppController],providers: [AppService],
})
export class AppModule {}

最后一步,在 Todo 业务模块里注入数据表对应的 Repository,这里一来 TodoService 就可以用 Repository 来操作数据库表了:

@Module({imports: [TypeOrmModule.forFeature([TodoRepository, UserRepository]),UserModule,],controllers: [TodoController],providers: [TodoService],
})
export class TodoModule {}

数据库模块还没完...

除了连接数据库,数据库的迁移与初始化是很多人经常忽略的点。

先说初始化,非常简单,就是一个脚本的事:

const checkExist = async (userRepository: Repository) => {console.log('检查是否已初始化...');const userNum = await userRepository.count();const exist = userNum > 0;if (exist) {console.log(`已存在 ${userNum} 条用户数据,不再初始化。`);return true;}return false;
};const seed = async () => {console.log('开始插入数据...');const connection = await createConnection(ormConfig);const userRepository = connection.getRepository(User);const dataExist = await checkExist(userRepository);if (dataExist) {return;}const initUsers = getInitUsers();console.log('生成初始化数据...');initUsers.forEach((user) => {user.todos = lodash.range(3).map(getRandomTodo);});const users = lodash.range(10).map(() => {const todos = lodash.range(3).map(getRandomTodo);return getRandomUser(todos);});const allUsers = [...initUsers, ...users];console.log('插入初始化数据...');await userRepository.save(allUsers);console.log('数据初始化成功!');
};seed().then(() => process.exit(0)).catch((e) => {console.error(e);process.exit(1);});

当然,最好也提供重置数据库的能力:

const reset = async () => {const connection = await createConnection(ormConfig);await connection.createQueryBuilder().delete().from(Todo).execute();await connection.createQueryBuilder().delete().from(User).execute();
};reset().then(() => process.exit(0)).catch((e) => {console.error(e);process.exit(1);});

这样一来,小白上手完全不慌。只要改坏数据库,一个 reset + seed 的操作,数据库又回来的了。当然,这一步仅仅是针对 数据 来说的。

针对数据库表结构则需要 数据库迁移。令人激动的是 TypeORM 已经提供了一条非常 NB 的迁移命令:

// package.json
"db:seed": "ts-node scripts/db/seed.ts",
"db:reset": "ts-node scripts/db/reset.ts",
"migration:generate": "npm run build && npm run typeorm migration:generate -- -n",
"migration:run": "npm run build && npm run typeorm migration:run"

但是,TypeORM 是从哪知道数据表的结构的呢?这就是 Entity 的作用了,下面就是一个 Todo entity:

@Entity()
export class Todo {@ApiProperty()@PrimaryGeneratedColumn()id: number; // 自增 id@ApiProperty()@Column({ length: 500 })title: string; // 标题@ApiProperty()@Column('text')description?: string; // 具体内容@ApiProperty()@Column('int', { default: TodoStatus.TODO })status: TodoStatus; // 状态@ApiProperty({ required: false })@Column('text')media?: string;@ManyToOne(() => User, (user) => user.todos)author: User;
}

然后在 .env 里添加配置:

# Type ORM 专有变量
# 详情:https://typeorm.io/#/using-ormconfig
# 生产环境在服务器上的容器里配置
TYPEORM_CONNECTION=mariadb
TYPEORM_DATABASE=nest_todo
TYPEORM_HOST=127.0.0.1
TYPEORM_PORT=3306
TYPEORM_USERNAME=root
TYPEORM_PASSWORD=123456
TYPEORM_ENTITIES=dist/**/*.entity{.ts,.js}
TYPEORM_MIGRATIONS=dist/src/db/migrations/*.js
TYPEORM_MIGRATIONS_DIR=src/db/migrations

有了上面的命令,还有什么数据库我不敢删的?遇事不决 npm run migration:run + npm run db:seed 一下。

上传模块

从上面 Demo 可看到,Todo 是支持图片上传的,所以这里还需要提供上传功能。Nest.js 非常给力,直接内置了 multer 这个库:

@ApiTags('文件上传')
@ApiBearerAuth()
@Controller('upload')
export class UploadController {@Post('file')@UseInterceptors(FileInterceptor('file'))uploadFile(@UploadedFile() file: Express.Multer.File) {return {file: staticBaseUrl + file.originalname,};}@Post('files')@UseInterceptors(FileInterceptor('files'))uploadFiles(@UploadedFiles() files: Array) {return {files: files.map((f) => staticBaseUrl + f.originalname),};}
}

当然,必不可少,需要在 UploadModule 里注入模块:

@Module({imports: [MulterModule.register({storage: diskStorage({destination: path.join(__dirname, '../../upload_dist'),filename(req, file, cb) {cb(null, file.originalname);},}),}),],controllers: [UploadController],providers: [UploadService],
})
export class UploadModule {}

静态资源模块

首先,必须说明一下上面的上传应该是要上传到 COS 桶或者 CDN 上,而不应该上传到自己服务器,使用自己服务器来管理文件。这里仅为了用一用这个静态资源模块。

回到主题,上面上传是上传到 /upload_dist 这个文件夹里,那我们静态资源就是要 host 这个文件夹下面的文件:

const uploadDistDir = join(__dirname, '../../', 'upload_dist');@Controller('static')
export class StaticController {@SkipJwtAuth()@Get(':subPath')render(@Param('subPath') subPath, @Res() res) {const filePath = join(uploadDistDir, subPath);return res.sendFile(filePath);}
}

@Module({controllers: [StaticController],
})
export class StaticModule {}

Very easy ~ 过

登录模块

相信细心的你一定看到上面的 @SkipJwtAuth,这是因为我全局开了 JWT 鉴权,只有请求头带有 Bearer Token 才能访问这个接口,而 @SkipJwtAuth 则表示这个接口不需要 JWT 鉴权。不妨来看看普通的鉴权是怎么实现的。

首先,你必要熟悉 Passport.js 里的 StrategyverifyCallback 概念,否则咱还是别聊了。这里 Nest.js 将这个 verifyCallback 封装成了 Strategy 里的 validate 方法,当编写 valiate 则是在写 verifyCallback:

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {constructor(private moduleRef: ModuleRef,private reportLogger: ReportLogger,) {super({ passReqToCallback: true });this.reportLogger.setContext('LocalStrategy');}async validate(request: Request,username: string,password: string,): Promise> {const contextId = ContextIdFactory.getByRequest(request);// 现在 authService 是一个 request-scoped providerconst authService = await this.moduleRef.resolve(AuthService, contextId);const user = await authService.validateUser(username, password);if (!user) {this.reportLogger.error('无法登录,SB');throw new UnauthorizedException();}return user;}
}

上面是用 username + password 实现鉴权的一种策略,当然我们正常服务是可以存在多种鉴权策略的,要使用这个策略,需要用到 Guard:

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

然后将这个 Guard 放在对应的接口头顶就 O 了:

@ApiTags('登录验证')
@Controller('auth')
export class AuthController {constructor(private authService: AuthService) {}@ApiBody({ type: LoginDto })@SkipJwtAuth()@UseGuards(LocalAuthGuard)@Post('login')async login(@Request() req) {return this.authService.login(req.user);}
}

和 local 这个 Strategy 相似的,JWT 也有对应的 Strategy:

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {constructor(private userService: UserService) {super({jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),ignoreExpiration: false,secretOrKey: jwtConstants.secret,});}async validate(payload: any) {const existUser = this.userService.findOne(payload.sub);if (!existUser) {throw new UnauthorizedException();}return { ...payload, id: payload.sub };}
}

而在 JwtGuard 里,用 canActive 实现了 权限控制:

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {constructor(private reflector: Reflector) {super();}canActivate(context: ExecutionContext,): boolean | Promise | Observable {// 自定义用户身份验证逻辑const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [context.getHandler(),context.getClass(),]);// skipif (isPublic) return true;return super.canActivate(context);}handleRequest(err, user) {// 处理 infoif (err || !user) {throw err || new UnauthorizedException();}return user;}
}

格式化输出

写完接口了,就得格式化输出,我比较喜欢的格式是:

{retcode: 0,message: "",data: ...
}

我们更希望不要在 Controller 里重复添加上面的 “格式化” 数据结构。Nest.js 提供了 Interceptor,可以让我们在 数据给前端之前 “加点料”:

export class TransformInterceptorimplements NestInterceptor>
{intercept(context: ExecutionContext, next: CallHandler) {return next.handle().pipe(map((data) => ({retcode: 0,message: 'OK',data,})),);}
}

然后在 main.ts 入口里全局使用:

app.useGlobalInterceptors(new LogInterceptor(reportLogger),new TransformInterceptor(),
);

测试

写完了一个接口,肯定免不了要写测试。我相信绝大部分人是不会写测试,当然他们自己也是不会写的。

它不是 “Jest”,也不是 “Cypress”,而是一个可以研究得很深的领域。它难的点并不在于 “写”,而在于 “造”,以及 测试策略

先来说测试策略吧,请问什么东西应该测?什么东西可以不测?什么东西不应该测?这三问是个人觉得是个玄学问题,没有正确答案,只能根据自己的项目来判断。并不是 100% 的覆盖率就是好的,也要看更新迭代时测试代码的改造成本。

我先给出这个项目的测试原则:

  • 数据库操作不测,因为这个测试内容 TypeORM 能保证 API 的调用是 OK 的

  • 简单实现不测,比如一个函数只有一行,那还测个 P

  • 我只测一个模块,因为我懒,剩下大家自己看我那个模块的测试就能学会了

  • 我的 测试策略 不一定正确,只能说是我目前想到比较好的 测试策略

TodoService 进行测试,比较难的点是对 TypeOrmRepository 进行 Mock,这玩意我自己搞了一整天才搞通,相信没人有耐心整这些了:

const { mockTodos, mockUsers } = createMockDB();describe('TodoService', () => {let mockTodoRepository;let mockUserRepository;let service: TodoService;beforeEach(async () => {mockUserRepository = new MockUserRepository(mockUsers);mockTodoRepository = new MockTodoRepository(mockTodos);const module: TestingModule = await Test.createTestingModule({providers: [TodoService,{provide: TodoRepository,useValue: mockTodoRepository,},{provide: UserRepository,useValue: mockUserRepository,},],}).compile();service = module.get(TodoService);});it('create', async () => {expect(service).toBeDefined();// 创建一个 todoconst returnTodos = await service.create(99, {title: 'title99',description: 'desc99',status: TodoStatus.TODO,});// expectexpect(returnTodos.title).toEqual('title99');expect(returnTodos.description).toEqual('desc99');expect(returnTodos.status).toEqual(TodoStatus.TODO);});it('findAll', async () => {expect(service).toBeDefined();const returnTodos = await service.findAll();// expectexpect(returnTodos).toEqual(mockTodos);});it('findAllByUserId', async () => {expect(service).toBeDefined();// 直接返回第一个 userjest.spyOn(mockUserRepository, 'findOne').mockImplementation(async () => {return mockUsers[0];});// 找到 userId 为 0 的所有 todoconst returnTodos = await service.findAllByUserId(0);const [firstTodo] = returnTodos;// expectexpect(mockUserRepository.findOne).toBeCalled();expect(firstTodo.id).toEqual(0);expect(firstTodo.title).toEqual('todo1');expect(firstTodo.description).toEqual('desc1');});it('findOne', async () => {expect(service).toBeDefined();// 找到一个 todoconst returnTodo = await service.findOne(0);// expectexpect(returnTodo.id).toEqual(0);expect(returnTodo.title).toEqual('todo1');expect(returnTodo.description).toEqual('desc1');});it('update', async () => {expect(service).toBeDefined();// 所有 todoconst allTodos = await service.findAll();// 更新一个 todoawait service.update(0, {title: 'todo99',description: 'desc99',});// expectconst targetTodo = allTodos.find((todo) => todo.id === 0);expect(targetTodo.id).toEqual(0);expect(targetTodo.title).toEqual('todo99');expect(targetTodo.description).toEqual('desc99');});it('remote', async () => {expect(service).toBeDefined();// 删除 todoawait service.remove(0);// 获取所有 todoconst allTodos = await service.findAll();// expectexpect(allTodos.length).toEqual(1);expect(allTodos.find((todo) => todo.id === 0)).toBeUndefined();});
});

TodoController 的单元测试,我觉得这个 class 没什么可测的,因为里面的函数太简单了:

const { mockTodos, mockUsers } = createMockDB();describe('TodoController', () => {let todoController: TodoController;let todoService: TodoService;let mockTodoRepository;let mockUserRepository;beforeEach(async () => {mockTodoRepository = new MockTodoRepository(mockTodos);mockUserRepository = new MockUserRepository(mockUsers);const app: TestingModule = await Test.createTestingModule({controllers: [TodoController],providers: [TodoService,{provide: TodoRepository,useValue: mockTodoRepository,},{provide: UserRepository,useValue: mockUserRepository,},],}).compile();todoService = app.get(TodoService);todoController = app.get(TodoController);});describe('findAll', () => {const [firstTodo] = mockTodos;it('普通用户只能访问自己的 todo', async () => {jest.spyOn(todoService, 'findAllByUserId').mockImplementation(async () => {return [firstTodo];});const todos = await todoController.findAll({user: { id: 0, is_admin: 0 },});expect(todos).toEqual([firstTodo]);});it('管理员能访问所有的 todo', async () => {jest.spyOn(todoService, 'findAll').mockImplementation(async () => {return mockTodos;});const todos = await todoController.findAll({user: { id: 0, is_admin: 1 },});expect(todos).toEqual(mockTodos);});});
});

最后就是 e2e 的测试,难点在于 Bearer Token 鉴权的获取,这玩意也同样搞了我一天时间:

describe('TodoController (e2e)', () => {const typeOrmModule = TypeOrmModule.forRoot({type: 'mariadb',database: 'nest_todo',username: 'root',password: '123456',entities: [User, Todo],});let app: INestApplication;let bearerToken: string;let createdTodo: Todo;beforeAll(async (done) => {const moduleFixture: TestingModule = await Test.createTestingModule({imports: [TodoModule, AuthModule, typeOrmModule],providers: [TodoRepository, UserRepository],}).compile();app = moduleFixture.createNestApplication();await app.init();// 生成测试用户的 tokenrequest(app.getHttpServer()).post('/auth/login').send({ username: 'user', password: 'user' }).expect(201).expect((res) => {bearerToken = `Bearer ${res.body.token}`;}).end(done);});it('GET /todo', (done) => {return request(app.getHttpServer()).get('/todo').set('Authorization', bearerToken).expect(200).expect((res) => {expect(typeof res.body).toEqual('object');expect(res.body instanceof Array).toBeTruthy();expect(res.body.length >= 3).toBeTruthy();}).end(done);});it('POST /todo', (done) => {const newTodo: CreateTodoDto = {title: 'todo99',description: 'desc99',status: TodoStatus.TODO,media: '',};return request(app.getHttpServer()).post('/todo').set('Authorization', bearerToken).send(newTodo).expect(201).expect((res) => {createdTodo = res.body;expect(createdTodo.title).toEqual('todo99');expect(createdTodo.description).toEqual('desc99');expect(createdTodo.status).toEqual(TodoStatus.TODO);}).end(done);});it('PATCH /todo/:id', (done) => {const updatingTodo: UpdateTodoDto = {title: 'todo9999',description: 'desc9999',};return request(app.getHttpServer()).patch(`/todo/${createdTodo.id}`).set('Authorization', bearerToken).send(updatingTodo).expect(200).expect((res) => {expect(res.body.title).toEqual(updatingTodo.title);expect(res.body.description).toEqual(updatingTodo.description);}).end(done);});it('DELETE /todo/:id', (done) => {return request(app.getHttpServer()).delete(`/todo/${createdTodo.id}`).set('Authorization', bearerToken).expect(200).expect((res) => {expect(res.body.id).toEqual(createdTodo.id);}).end(done);});afterAll(async () => {await app.close();});
});

Swagger

Swagger 是一个非常强大的文档工具,可以识别接口的 URL,入参,出参,简直是前端使用者的福音:

首先在 main.ts 里接入 Swagger:

const setupSwagger = (app) => {const config = new DocumentBuilder().addBearerAuth().setTitle('待办事项').setDescription('nest-todo 的 API 文档').setVersion('1.0').build();const document = SwaggerModule.createDocument(app, config);SwaggerModule.setup('docs', app, document, {swaggerOptions: {persistAuthorization: true,},});
};

然后在 nest-cli.json 里也接入 Swagger 的插件,这样才能自动识别,不然就要一个 ApiProperty 一个 ApiProperty 去声明了:

{"collection": "@nestjs/schematics","sourceRoot": "src","compilerOptions": {"plugins": ["@nestjs/swagger"]}
}

最后

还有非常多的模块没讲,我觉得那些并不是那么重要,只要看过文档就会了。上面的模块我是踩了很多坑才实现出来的,中间走走停停花了大概 1 个月左右的时间。

本来是可以上线给大家一个在线 Demo 看的,但是我的域名还在备案,大家先本地 Clone 玩吧。

如果你对 Nest.js 也感兴趣,也想学一下它,不妨 Clone 一下我的 nest-todo 这个项目,抄抄改改学一下吧。

Node 社群

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

   “分享、点赞、在看” 支持一波 


推荐阅读
  • egg实现登录鉴权(七):权限管理
    权限管理包含三部分:访问页面的权限,操作功能的权限和获取数据权限。页面权限:登录用户所属角色的可访问页面的权限功能权限:登录用户所属角色的可访问页面的操作权限数据权限:登录用户所属 ... [详细]
  • 使用 ModelAttribute 实现页面数据自动填充
    本文介绍了如何利用 Spring MVC 中的 ModelAttribute 注解,在页面跳转后自动填充表单数据。主要探讨了两种实现方法及其背后的原理。 ... [详细]
  • 本文探讨了如何在Sitecore 9环境中通过Postman使用API密钥发送请求,包括解决常见错误的方法。 ... [详细]
  • 本文探讨了Flutter和Angular这两个流行框架的主要区别,包括它们的设计理念、适用场景及技术实现。 ... [详细]
  • 在现代移动应用开发中,尤其是iOS应用,处理来自服务器的JSON数据是一项基本技能。无论是使用Swift还是PHP,有效地解析和利用JSON数据对于提升用户体验至关重要。本文将探讨如何在Swift中优雅地处理JSON,以及PHP中处理JSON的一些技巧。 ... [详细]
  • 拖拉切割直线 ... [详细]
  • UVA 401 - 镜像回文字符串
    本题探讨了如何判断一个字符串是否为普通回文、镜像回文或两者都不是。通过特定的字符映射表来实现字符串的镜像转换,并根据转换后的结果进行分类。 ... [详细]
  • SpringBoot新手入门指南
    本文旨在为初次接触SpringBoot的开发者提供一份详细的入门指导,包括如何快速搭建并运行一个简单的SpringBoot应用。通过本文,读者将了解Maven项目的构建、必要的配置文件设置以及基本的应用开发流程。 ... [详细]
  • 在进行模型和动画的导出过程中,需要注意隐藏不必要的场景元素,并确保所需导出的对象位于顶级节点。此外,在导出设置中正确选择节点配置,并为带有动画的对象添加Animator控制器组件,以确保动画能够正常播放。 ... [详细]
  • Kubernetes Services详解
    本文深入探讨了Kubernetes中的服务(Services)概念,解释了如何通过Services实现Pods之间的稳定通信,以及如何管理没有选择器的服务。 ... [详细]
  • LoadRunner中的IP欺骗配置与实践
    为了确保服务器能够有效地区分不同的用户请求,避免多人使用同一IP地址造成的访问限制,可以通过配置IP欺骗来解决这一问题。本文将详细介绍IP欺骗的工作原理及其在LoadRunner中的具体配置步骤。 ... [详细]
  • 在AngularJS中,有时需要在表单内包含某些控件,但又不希望这些控件导致表单变为脏状态。例如,当用户对表单进行修改后,表单的$dirty属性将变为true,触发保存对话框。然而,对于一些导航或辅助功能控件,我们可能并不希望它们触发这种行为。 ... [详细]
  • 如何高效渲染JSON数据
    本文介绍了在控制器中返回JSON结果的方法,并详细说明了如何利用jQuery处理和展示这些数据,为Web开发提供了实用的技巧。 ... [详细]
  • 本文介绍了如何在AngularJS应用中使用ng-repeat指令创建可单独点击选中的列表项,并详细描述了实现这一功能的具体步骤和代码示例。 ... [详细]
  • 本文探讨了 TypeScript 中泛型的重要性和应用场景,通过多个实例详细解析了泛型如何提升代码的复用性和类型安全性。 ... [详细]
author-avatar
6057318491
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有