我实现了一个web前端面试系统

前言

大家好,我是真正的大乾。最近面试时发现市面上好像没有特别好用的前端面试系统呀(也可能是我没见识)。灵机一动,那我为什么不实现一个呢?那说干就干...

先调研一下

根据我的经历和调查,现在web前端在线面试方式有两种:一种是请候选人通过IM工具分享电脑桌面从而观察候选人答题,一种是使用牛客等通用程序员面试网站。两种方式各有优缺点。

1.通过IM工具分享桌面。

这种方式的优点是面试官能直观观察到面试者的答题过程比较方便评估面试者水平,面试者也可以使用自己熟悉的开发环境。这种方式的缺点是面试官不能实时获取面试者的代码而不能实时获取代码执行情况,造成面试官对代码的质量评估比较困难。同时因为分享桌面时无法开启视频,不利于面试双方畅通沟通,也不容易看到面试者是否在作弊。另外这种方式要求候选人提前下载软件,面试的准备工作比较多,沟通成本比较高。最后因为隐私等原因,很多面试者不愿意分享桌面。

这种方式需要改进的是代码实时同步的能力以及桌面分享和视频通话同时进行的能力。但因为这种方式依赖于IM工具,难以实现。

2.使用通用程序员面试网站。

这种方式的优点是音视频通话服务通畅,并且有题库可供面试官选择。缺点是这些网站不能在线编辑和执行代码,面试者难以表现出自己的真实水平,面试官也难以对面试者的技术能力进行有效评估。

这种方式需要改进的是代码在线编辑在线执行的能力。

确定一下需求

实现一个web前端开发工程师面试系统,除能实现视频通话功能外,还能实现面试官与候选人协同编辑代码并在线执行代码等功能。这样可以极大提升面试的效率,提高面试官对候选人评估的精准度。

进行需求分析

(一) 用户角色分析

本系统支持用户注册登录,注册登录后不区分角色。在使用本系统进行面试时区分面试官和候选人角色。来个UML用例图:

(二)面试流程分析

直接上流程图,大家自己看:

(三)功能性需求

1.用户注册与登录功能

用户注册系统账号和通过用户名密码为凭证登录本系统的功能。

2.代码协同编辑功能

面试过程中面试官与面试者可以同时编辑代码,系统会自动同步两者的编辑结果到双方的编辑器界面上。

3.代码在线执行功能

编辑的代码可以在线执行并打印执行结果的功能。

4.视频通话功能

面试官与面试者发起视频通话请求,然后进行视频面试的功能。

5.代码保存查阅功能

面试完毕后保存本次面试的代码,供以后查阅的功能。

设计系统架构

分析完需求一看还挺复杂,仔细想了想,系统的架构应该是这个样子的:

(一)系统架构

系统分web前端与web后端两部分,前后端通过http协议和websocket通信。

前端为一个单页面应用;以typescript为主要语言,以webpack做为打包工具,以reactjs为框架,用react-router、react-router-dom处理路由,以mobx为数据管理工具,以antd为UI框架搭建系统框架;基于以上框架为各业务模块编写业务逻辑代码;基于各业务模块,编写页面UI及交互。

后端为一个Node.js应用,使用MySQL数据库;以nestjs为主要框架,使用passport做验证框架,使用squelize做为ORM框架,使用ws做为websockt库,使用yjs和y-websockt处理文档协同;在此基础上建立用户功能服务、面试功能服务、验证功能服务和事件处理功能服务四个基础服务。

然后再画个架构图:

(二)系统模块划分

系统根据功能主要分为用户注册登录与验证功能模块和面试模块。

1.用户注册、登录与验证功能模块

(1)用户注册平台账户功能。

(2)用户登录系统功能。

(3)对用户各项操作进行权限验证功能。

2.面试功能模块

(1)用户通过面试列表查看过去面试的功能。

(2)用户做为面试官,创建面试的功能。

(3)用户做为面试者,加入面试的功能。

(4)面试者与面试官协同编辑代码功能。

(5)面试者与面试官视频通话功能。

(6)面试者与面试官文字通信功能。

(三) 数据库设计

数据库很简单应该有两个表:用户表和面试表。

(1)用户表包含ID(主键)、name(用户名)、password(密码)、email(邮箱)字段;

(2)面试表包含ID(主键)、interviewerId(外键,面试官)、intervieweeId(外键,面试者)、code(用户编辑的代码)、name(面试名称)、info(面试说明)、time(面试时间);

画个ER图:

设计系统功能

1.用户注册、登录与验证功能模块

(1)新用户进入平台,自动跳转到登录注册页面,用户输入用户名密码,点击注册按钮进行注册,数据库保存用户信息后,前端页面跳转个人主页。

(2)未登录用户进入平台,自动跳转到登录注册页面,用户输入用户名密码,点击登录,后端校验登录信息后把token返回给前端, 前端保存token并跳转个人主页。

(3)用户在进行各种操作时,后端都对token进行验证,如验证不通过返回401状态码,前端跳转登录页。

2.面试功能模块

(1)个人主页左侧展示面试者参加过的面试,左侧有两个tab分别以列表形式展示用户做为面试官和做为面试者参加过的面试,列表下面有分页器。

(2)用户点击列表中面试条目,进入面试页,查看过往面试详情或重新加入面试。

(3)个人主页右侧展示创建面试按钮,用户点击后弹出创建面试对话框;用户填写面试名称、面试说明信息后点击确定按钮,创建一个面试并进入面试页面,此用户为当前面试的面试官;用户点击取消按钮,关闭对话框。

(4)面试官进入面试页面,可以编辑代码,在代码编辑器中填写面试题,并等待面试者加入。

(5)个人主页右侧展示面试ID输入框、请求说明输入框和加入面试按钮,用户输入面试ID和请求说明后点击加入面试按钮向面试官发送一个面试请求,并等待面试官回复。

(6)面试官等待面试者进入,当收到面试请求时,页面上弹出一个面试请求弹窗;面试官点击同意按钮或不同意按钮后向请求人回复同意或不同意;回复同意时系统将请求用户加为面试者。

(7)面试者收到面试官的同意回复后,进入面试页;收到不同意回复,则提示用户面试官不同意面试者加入面试。

(8)当面试者与面试官都进入面试后,面试官页面上的视频通话按钮从置灰状态变高亮状态,面试官点击按钮,打开视频连接开始视频面试。

(9)面试者与面试官也可在聊天框中输入文字,点击发送,进行文字交流。

(10)当前视频连接断开时,面试官可以点击视频通话按钮,重新进行视频通话。

(11)面试中面试者和面试官都可在代码编辑框中输入代码,系统自动同步两人的编辑内容和光标位置。期间双方可以点击保存按钮,随时将代码保存到数据库中。

(12)面试结束后,面试官点击结束面试按钮,结束面试。

编码

擦汗,从头开发一个产品真的好麻烦,光前期工作都做了好长好长好长时间。现在终于能开发?

搭建项目

1.开始编写代码之前,我们需要先安装好 MySQL,并创建一个interview数据库。

2.使用nest-cli工具创建nestjs项目基本结构。然后引入sequelize-cli使用migration管理数据结构的变更。然后创建user表和interview表。

3.在项目中添加sequelize、ws、y-websocket、passport、passport-jwt、passport-local、yjs等依赖库。

  1. 在项目主模块中引入SequelizeModule,并配置数据库的链接信息。
arduino 复制代码
*// 配置数据库的链接信息*

    SequelizeModule.forRoot({

      dialect: 'mysql', *// 数据库类型*

      host: 'localhost', *// 地址*

      port: 3306, *// 端口*

      username: 'root', *// 用户名*

      password: '******', *// 密码*

      database: 'interview', *// 数据库*

      models: [User, Interview], *// 数据模型*

    }),

5.在项目根目录下新建client目录,用create-react-app初始化一个react前端项目,并在其中添加mobx, mobx-react, react-router,antd,yjs,axios,console-feed,monaco-editor,y-monaco,y-websocket等依赖库。并对webpack的打包配置文件进行更改,使前端代码打包到后端的静态文件目录中。同时配置webpack-dev-server的代理配置,使前端开发时的请求能被代理到后端的端口上。

用户注册、登录与验证功能模块

本模块主要包括注册功能实现,登录功能实现和验证功能实现。实现过程中主要运用passport做为验证工具。

1. 实现用户注册的后端部分。编写user模块,包括user.model,user.service, user.controller。

(1)user.model表示user表的模型,用于service操作数据库用户表。

(2)user.service包括对user数据表进行各种操作的方法,首先引入user.model,并向其中添加register方法用于新增用户,其过程是先判断用户表中是否有同名用户,有的话抛出错误,没有的话用uuid库给密码加密后保存在用户表中。

typescript 复制代码
*/***

** 注册用户方法*

*** **@param** ***createUser* *用户信息*

*** **@returns** *Promise*

**/*

  **async** register(createUser: { name: string; password: string }) {

    **const** { name, password } = createUser;

 

    *// 判断用户表中是否有同名用户,有的话抛出错误*

    **const** existUser = await this.userModel.findOne({

      where: { name },

    });

    if (existUser) {

      throw new HttpException('用户名已存在', HttpStatus.BAD_REQUEST);

    }

 

    *// 用uuid库给密码加密,然后保存在用户表中*

    return await this.userModel.create({

      ...createUser,

      id: v4(),

      password: encrypt(password),

    });

  }

(3)user.controller用于处理各种请求。先给其添加@Controller('api/user')装饰符,使其能接受/api/user的请求。然后加入register方法,并添加@Post('register')装饰符使其处理path为/api/user/register的post请求。然后调用user.service中的register方法,保存请求参数中的用户。这样就实现了用户注册功能。

less 复制代码
** 处理注册请求*

*** **@param** ***request* *请求对象*

*** **@returns** *Promise<User> 用户信息*

**/*

  @SkipAuth()

  @Post('register')

  **async** register(@Body() request: any): Promise<User> {

    **const** res = await this.userService.register({

      name: request.name,

      password: request.password,

    });

    return res;

  }

2. 实现用户登录的后端部分。编写auth模块,包括auth.service、local.strategy、local-authguard三部分;

(1)首先实现AuthService类,AuthService注入有@nestjs/jwt的JwtService实际并且有两个方法,validateUser方法用于校验用户名密码是否与数据库中保存的用户名密码一致;login方法调用jwtService实例的sign方法把用户名用户ID信息加密生成jwt token并返回。

typescript 复制代码
*// 方法用于校验用户名密码是否与数据库中保存的用户名密码一致*

  **async** validateUser(name: string, pass: string): Promise<any> {

    **const** user = await this.usersService.findOneByname(name);

    *// 校验密码是否一致*

    if (user && user.password === encrypt(pass)) {

      **const** { password, ...result } = user.toJSON();

      return result;

    }

    return null;

  }

 

  *// 调用jwtService实际的sign方法把用户名用户ID信息加密生成jwt token并返回*

  **async** login(user: any) {

    **const** payload = { username: user.name, sub: user.id };

    return {

      access_token: this.jwtService.sign(payload),

    };

  }

(2)第二步实现LocalStrategy类,LocalStrategy类继承自PassportStrategy类,用以实现passport的本地验证策略。其主要包括一个validate方法,用于调用AuthService中的validateUser方法校验用户名密码是否合法。不合法抛出验证错误,合法的话返回用户信息。

typescript 复制代码
*// 用于调用AuthService中的validateUser方法校验用户名密码是否合法。*

  **async** validate(name: string, password: string): Promise<any> {

    **const** user = await this.authService.validateUser(name, password);

    *// 不合法抛出验证错误,*

    if (!user) {

      throw new UnauthorizedException();

    }

    *// 合法的话返回用户信息。*

    return user;

  }

(3) 实现继承自AuthGuard('local')的LocalAuthGuard类, LocalAuthGuard会按照默认逻辑调用localStrategy的相应方法对参数中的用户名密码进行校验,并把校验通过后的用户信息注入到request的对象中供controller使用。

(4)在user.controller中实现login方法,添加@UseGuards(LocalAuthGuard)装饰符,接受登录请求并实现登录功能。

less 复制代码
*/***

** 处理登录请求*

*** **@param** ***req* *请求对象*

*** **@returns** *token 返回token*

**/*

  @SkipAuth()

  @UseGuards(LocalAuthGuard)

  @Post('login')

  **async** login(@Request() req) {

    **const** token = this.authService.login(req.user);

    return token;

  }

*/***

** 处理登录请求*

*** **@param** ***req* *请求对象*

*** **@returns** *token 返回token*

**/*

  @SkipAuth()

  @UseGuards(LocalAuthGuard)

  @Post('login')

  **async** login(@Request() req) {

    **const** token = this.authService.login(req.user);

    return token;

  }
  

3.实现校验的后端部分

在auth模块中添加jwt-auth.guard.ts文件和jwt.strategy.ts文件分别实现JwtStrategy类与JwtAuthGuard类。

(1)JwtStrategy类继承自PassportStrategy类,用以实现passport的jwt验证策略。在其构建函数中为super传入jwtFromRequest等配置,jwtStrategy会自动从请求中获取jwt token并解析成用户信息。其validate方法直接返回用户信息。

typescript 复制代码
*/***

** jwt验证策略*

**/*

@Injectable()

export **class** JwtStrategy **extends** PassportStrategy(Strategy) {

  *// 为其构建函数中为super传入jwtFromRequest等配置,*

  *// jwtStrategy会自动从请求中获取jwt token并解析成用户信息*

  **constructor**() {

    super({

      jwtFromRequest: ExtractJwt.fromHeader('authorization'), *// token从authorization请求头获取*

      ignoreExpiration: false, *// token会过期*

      secretOrKey: jwtConstants.secret, *// 密钥*

    });

  }

 

  *// validate直接返回用户信息*

  **async** validate(payload: any) {

    return { userId: payload.sub, username: payload.username };

  }

}

(2) 实现继承自AuthGuard('jwt')的JwtAuthGuard类。其中实现canActivate方法:首先判断如果当前是无需验证的公共请求则直接通过,否则继续判断如果当前是websocket通信且token合法则通过;最后调用父类canActivate方法,走jwtStrategy的默认验证逻辑。

ini 复制代码
*// 当前是否是无需验证的公共请求*

    **const** isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [

      context.getHandler(),

      context.getClass(),

    ]);

    if (isPublic) return true;

 

    *// 判断当前是否是websocket通信,是的话解析数据中的token合法的话通过*

    if (context.contextType === 'ws') {

      **const** res = jwt.verify(context.args[1].token, jwtConstants.secret);

      if (res) {

        context.args[1].user = {

          username: res.username,

          userid: res.sub,

        };

        return true;

      }

    }

    return super.canActivate(context);

(3) 将JwtAuthGuard做为公共守卫注册进authModule中,这样除标记为公共请求的路径都会经过JwtAuthGuard的验证。此时完成后端的验证功能。

less 复制代码
@Module({

  providers: [

    {

      provide: APP_GUARD, // 注册为全局守卫

      useClass: JwtAuthGuard,

    },

  ],

  exports: [AuthService],

})

export class AuthModule {}

4.实现前端页面与逻辑。

(1)首先实现login登录页面,添加用户名、密码表单项和登录、注册按钮。按钮点击后校验输入是否合法,合法的话分别请求/api/user/register和/api/user/login接口进行注册或登录。

(2)登录请求成功返回token后,把token存在localStorage中。

(3)注册成功返回用户信息,接着调用登录方法进行登录。

(4)改造统一的请求方法,对每一次请求都从localStorage里取出token加入请求头中,这样就实现了登录验证功能;对每一次请求如果返回401状态码,则跳转登录页面。

(5)新建profile模块,实现Profile类;profile中实现queryUser方法,获取当前用户信息并保存在user属性上。

typescript 复制代码
*/***

** 用户信息model,用于处理用户信息相关逻辑*

**/*

**class** Profile {

  *// 保存当前用户信息*

  @observable.ref

  user: IUser | null = null;

 

  *// 获取用户信息*

  **async** queryUser() {

    this.setLoading(true);

    **const** res = await getProfile();

    this.setUser(res);

    this.setLoading(false);

  }

(6)在前端程序主入口处调用profile的queryUser方法获取当前用户信息。实现从任意入口进入程序,如果没有登录则跳转登录页面,如果登录了则获取当前用户信息,提供给其它模块使用的能力。

(二)面试功能模块的实现

面试功能模块的实现比较复杂,分成面试管理模块,代码编辑与同步模块,视频通话模块与文字通信模块四个模块分别说明。

1.面试管理模块

主要包括面试的创建、加入、重新进入及面试信息的保存。

(1)在后端编写 interview模块,包括interview.model,interview.service, interview.controller。

interview.model表示interview表的模型,用于service操作数据库表。interview.service包括对interview数据表进行各种操作的方法,如对interview表的增删改查方法。interview.controller用于处理各种对面试的请求。先给其添加@Controller('api/interview')装饰符,使其能接受/api/interview的请求,然后加入方法调用service处理对面试的增删改查请求。

(2)interview模块只处理有关面试的http请求。因为面试过程是一个强交互过程,因此本系统使用websocket实现面试过程中的大部分通信任务。在后端创建events模块,用于处理websocket通信。

①创建interviewManager.ts文件。首先添加ConnectUser类,对加入面试的用户进行抽象,主要包括id、name、socket属性,和向该用户发送信息的send方法。

再向interviewManager.ts中添加Interview类,抽象一个面试。里面包括面试双方通信以及面试创建的各种方法。

typescript 复制代码
*// 请求加入面试*

  requestInter(socket: Socket, name: string, id: string, msg: string) {

    **const** user = new ConnectUser(name, id, socket);

    this.waitingUser.push(user);

 

    this.interviewer.socket.send(

      JSON.stringify({

        event: 'request-inter',

        data: {

          name,

          msg,

          id,

        },

      }),

    );

  }

 

  *// 转发面试中双方的信息*

  retransmission(originUserId: string, type: string, data: any);

  retransmission(originSocket: Socket, type: string, data: any);

  retransmission(query: any, type: string, data: any) {

    *// 根据userId或者socket获取对方User实例,然后发送信息*

    if (typeof query === 'string') {

      **const** my = this.getRoleById(query);

      if (my) {

        **const** opposite = this.getOppositeById(query);

        opposite.send(type, data);

      }

    } else {

      **const** my = this.getRoleBySocket(query);

      if (my) {

        **const** opposite = this.getOppositeBySocket(query);

        opposite.send(type, data);

      }

    }

  }

最后,添加InterviewManager类管理所有面试,主要实现代码如下:

typescript 复制代码
*// 管理所有面试的类*

export **class** InterviewManager {

  *// 所有面试map,以面试ID为KEY*

  interviews = new Map<string, Interview>();

  *// 所有面试map,以参加者socket为KEY*

  sockInterviewMap = new Map<Socket, Interview>();

  server: Socket = null;

 

  */***

** 创建面试*

*** **@param** ***data* *创建面试相关信息*

**/*

  createInterview(data: {

    id: string; *// 面试ID*

    socket: Socket; *// 创建者socket*

    user: { username: string; userid: string }; *// 创建者信息*

  }) {

    **const** interviewer = new ConnectUser(

      data.user.username,

      data.user.userid,

      data.socket,

    );

    **const** interview = new Interview(data.id, interviewer, this.server);

    this.interviews.set(interview.id, interview);

    this.interviews.set(data.socket, interview);

  }

②创建events.gateway.ts文件,加入EventsGateway类处理websocket通信。加入onEvent方法处理websocket接受的event类型的信息即面试创建相关信息。

当收到创建面试的请求时,通过调用interview模块的service在interview表中创建一个面试。同时调用interviewManager的创建面试方法,创建一个以请求者为面试官的Interview类型实例并保存在interviewManager中。

当收到加入面试的请求时,先调用interview.service查询是否存在此面试,不存在则报错。存在时从interviewManager中取出此面试命名为currentInterview。再判断请求人是否是以面试官的身份加入面试,如果是则判断数据库中本面试的面试官是否是请求者,不是则返回错误信息。是则检测是否存在currentInterview,有则重新设置面试的面试官,没有则新创建面试。如果请求人是以面试者身份请求加入面试,且本面试无面试者则执行currentInterview.requestInter方法,向面试官发送请求加入面试的信息并等待面试官确认。如果面试官不同意,则把信息返回给面试者。如果面试官同意,则创建面试并把请求者加为本面试的面试者身份

kotlin 复制代码
*/***

** 处理websocket来的event类型的信息即面试创建相关信息*

*** **@param** ***client* *客户端socket*

*** **@param** ***data* *请求数据*

*** **@returns** *回复内容*

**/*

  @UseGuards(JwtAuthGuard)

  @SubscribeMessage('events')

  **async** onEvent(client: Socket, data: any): Promise<any> {

    switch (type) {

      *// 创建面试请求*

      case 'create-interview':

        **const** res = await this.interviewService.createOne(data.user.userid);

        manager.createInterview({

          socket: client,

          user: data.user,

          id: res.id,

        });

 

        return { event: 'create-interview-success', data: res };

      *// 加入面试请求*

      case 'inter-interview':

        **const** { role, id, user, msg } = data;

        **const** inter = await this.interviewService.findOne(id);

        if (!inter) {

          return {

            event: 'inter-interview-fail',

            data: {

              msg: 'no interview',

            },

          };

        }

 

        **const** interview = manager.getInterviewById(id);

        *// 判断请求者面试中的角色,当是面试官时*

        if (role === 'interviewer') {

          *// 存在面试时,加入面试*

          if (interview) {

            interview.addNewInterviewer(user.username, user.userid, client);

            manager.saveSocketInterview(client, interview);

            return {

              event: 'inter-interview-success',

              data: {

                interviewId: interview.id,

              },

            };

            *// 不存在面试时,创建面试*

          } else {

            manager.createInterview({

              id: inter.id,

              socket: client,

              user,

            });

            return {

              event: 'inter-interview-success',

            };

          }

        } else if (role === 'interviewee') {

          *// 当请求者是面试者时,并且存在面试时,调用面试的请求加入方法*

          if (

            interview &&

            inter &&

            (!inter.intervieweeId || inter.intervieweeId === user.userid)

          ) {

            interview.requestInter(client, user.username, user.userid, msg);

          }

(3)面试管理模块的前端部分主要是创建interview模块。创建Interview类,实现initSocket方法创建一个websocket实例,send方法用于抽离使用websocket向服务端发送信息的公共逻辑,然后实现创建面试请求、加入面试请求等方法。在个人主页上添加创建面试按钮、加入面试按钮和表单,并监听按钮点击事件调用interview的相应方法完成对应功能。

2. 视频面试模块的实现

视频面试模块主要使用webRTC技术,为了使用webRTC首先用node-turn库创建一个turn服务,为后续使用做准备。

(1)服务端在events.gateway中添加onWebrtc方法监听所有有关webrtc的通讯,从interviewManger中找到对应面试,然后交给Interview类的retransmission方法,把请求转发给面试的另一方。

less 复制代码
*/***

** 监听所有有关webrtc的通讯,从interviewManger中找到对应面试,*

** 然后交给Interview类的retransmission方法,把请求转发给面试的另一方。*

*** **@param** ***data* *传递的数据*

**/*

  @UseGuards(JwtAuthGuard)

  @SubscribeMessage('webrtc')

  **async** onWebrtc(data: any): Promise<any> {

    **const** { scope, user } = data;

    *// 找到请求者所在面试,调用面试的方法转发数据*

    if (scope) {

      **const** interview2 = manager.getInterviewById(data.scope);

      interview2.retransmission(user.userid, 'webrtc', data.data);

    }

  }

(2)前端在interview模块中创建Webrtc类,在实例化Webrtc时就传入Interview类中保存的websocket实例,用于和面试的另一方交换webRTC技术需要的各种信息。

首先实现init方法,初始化RTCPeerConnection实例connection,设置connection的icecandidate事件监听函数把icecandidate传给面试对端;

csharp 复制代码
*/***

** 初始化RTCPeerConnection实例connection;*

**/*

  init() {

    this.connection = new RTCPeerConnection({

      iceServers: [

        {

          urls: 'turn:43.142.118.202:3478', *// turn服务*

          username: 'ymrdf', *// 服务的用户名*

          credential: '*****', *// 服务的密码*

        },

      ],

    });

 

    *// 监听icecandidate事件把icecandidate传给面试对端*

    this.connection.addEventListener('icecandidate', (event) **=>**  {

      if (event.candidate) {

        if (event.candidate.candidate === '') {

          return;

        }

        this.send({

          iceCandidate: event.candidate,

        });

      }

    });

监听connection的track事件,设置把传入的视频流显示到页面上的video标签上。

csharp 复制代码
*// 监听connection的track事件,设置传入的视频流到页面上的video上。*

    this.connection.addEventListener('track', **async** (event) **=>**  {

      **const** [remoteStream] = event.streams;

      if (remoteVideo) {

        remoteVideo.srcObject = remoteStream;

      }

    });

然后监听websocket传来的webrtc 相关信息,如里是offer信息则保存对端offer且把answer返回对端,如果是icecandidate信息则设置对端icecandidate。

kotlin 复制代码
*// 监听websocket传来的webrtc 相关信息,*

  this.socket!.addEventListener('message', **async** (ev: MessageEvent) **=>**  {

    **const** message = JSON.parse(ev.data);

 

    **const** { event, data } = message;

 

    if (event !== 'webrtc') return;

 

    *// 如里是answer信息则保存对端answer*

    if (data.answer) {

      **const** remoteDesc = new RTCSessionDescription(data.answer);

      await this.connection!.setRemoteDescription(remoteDesc);

    }

    *// 如里是offer信息则保存对端offer则返回给对端answer,*

    if (data.offer) {

      this.connection!.setRemoteDescription(

        new RTCSessionDescription(data.offer),

      );

      **const** answer = await this.connection!.createAnswer();

      await this.connection!.setLocalDescription(answer);

      this.send({ answer: answer });

    }

 

    *// 如果是icecandidate信息则设置对端icecandidate。*

    if (data.iceCandidate) {

      try {

        await this.connection!.addIceCandidate(data.iceCandidate);

      } catch (e) {

        console.error('Error adding received ice candidate', e);

      }

    }

  });

Webrtc增加callRemote方法,用于创建offer信息发送给对端发起连接请求。页面上增加video标签及发起视频按钮,调用Webrtc类的相应方法。

3.代码编辑、执行和在线协同模块的实现

(1)前端新建Editor组件,添加id 为monaco-container的 div标签;引入monaco-editor库并在组件的useEffect函数中创建editor实例将编辑器渲染到页面上。监听编辑器的内容变化事件,将变化的输入内容保存在state上。

php 复制代码
*// 创建editor实例,将编辑器渲染到页面上*

  **const** editor = monaco.editor.create(

    document.getElementById('monaco-container')!,

    {

      value: '',

      language: 'javascript',

      theme: 'vs-dark',

    },

  );

 

  *// 监听编辑器的内容变化事件,将变化的输入内容存在state上*

  editor.onDidChangeModelContent(() **=>**  {

    setValue(editor.getValue());

  });

(2)引入yjs和y-monaco仓库,在上面的useEffect函数中初始化Y.Doc文档和WebsocketProvider实例,并把文档、websocketProvider实例与editor实例通过MonacoBinding联系起来,进行文档协同。

typescript 复制代码
*// 初始化Y.Doc文档和WebsocketProvider实例*

  **const** doc = new Y.Doc();

  **const** type = doc.getText('monaco');

  **const** wsProvider = new WebsocketProvider(

    'ws://192.168.10.217:3000/',

    `room?${interviewId}`,

    doc,

  );

  *// 把文档、websocketProvider实例与editor实例通过MonacoBinding联系起来,进行文档协同*

  **const** monacoBinding = new MonacoBinding(

    type,

    editor.getModel()!,

    new Set([editor]),

    wsProvider.awareness,

  );

后端在events模块创建CollaborateGateway,在gateway初始化websocket后,调用y-websocket库的setupWSConnection函数以使用y-websocket库处理文档协同功能。

php 复制代码
*// 初始化websocket后,处理文档协同*

  afterInit(server: Server) {

    server.on('connection', (client: Socket, request: any) **=>**  {

      **const** docName = request.url.split('?')[1];

      if (!docName) return

 

      *// 调用y-websocket的setupWSConnection函数用y-websocket库处理文档协同的逻辑。*

      wutils.setupWSConnection(client, request, { docName, gc: true });

    });

  }

(3)创建Console组件,组件接收一个code参数做为要执行和展示结果的代码。并实现run方法,run方法的逻辑是首先改写console中的各个打印方法把结果打印内容存到logs变量中,然后使用eval函数把code做为代码执行,执行完毕后,把console的方法复原。引入console-feed库中的Console组件,用console组件展示logs中的内容。添加run按钮,监听其click事件执行run方法,完成代码执行和打印执行结果的功能。

scss 复制代码
*// 代码执行方法*

  **const** run = () **=>**  {

    *// 改写console中的各个打印方法把结果打印内容存到logs变量中*

    Hook(

      window.console,

      (log) **=>**  setLogs((currLogs) **=>**  [...currLogs, log]),

      false,

    );

    *// 使用eval执行把code做为代码执行*

    eval(code);

    *// 执行完毕后,把console的方法改回来*

    Unhook(window.console);

  };

4. 文字通讯模块实现

(1)前端在interview模块下添加ChatManager类,实现sendMsg方法可把数据通过interview类实例化过的socket发送给服务端;同时监听socket 的 message事件,当数据类型为chat时把数据保存到messages属性中用以展示。

(2) 创建Chat组件,展示chatManager中的messages信息,其中添加一个textarea和一个发送按钮,点击按钮点调用chatManager的sendMsg方法把textarea中的用户输入发送到服务端。

(3)服务端events模块的events.gateway中添加一个onChat方法,转发所有chat类型的数据,完成文字通讯模块。

less 复制代码
*/***

**转发所有chat类型的数据*

**/*

  @UseGuards(JwtAuthGuard)

  @SubscribeMessage('chat')

  **async** onChat(data: any): Promise<any> {

    **const** { scope, user } = data;

    *// 找到请求者所在面试,调用面试的方法转发数据*

    if (scope) {

      **const** interview2 = manager.getInterviewById(data.scope);

      interview2.retransmission(user.userid, 'chat', data.data);

    }

  }

总结

一个人从零实现一个系统的难度超出了我的预估,写到这里本系统只实现了基本的功能,经过简单测试就有挺多bug, UI也没设计。细节只能以后有时间再慢慢补了。

项目的代码地址为:,欢迎感兴趣的同学提PR, 共同完善专门为我们前端开发工程师面试设计的系统。

相关推荐
魔术师ID12 分钟前
vue2.0 组件生命周期
前端·javascript·vue.js·学习·visual studio code
胜玲龙25 分钟前
单点登录是是什么?具体流程是什么?
java·服务器·前端
小浪学编程28 分钟前
C#学习9——接口、抽象类
前端·学习·c#
Dontla30 分钟前
《黑马前端ajax+node.js+webpack+git教程》(笔记)——ajax教程(axios教程)
前端·ajax·node.js
打小就很皮...34 分钟前
基于 Vue 和 Node.js 实现图片上传功能:从前端到后端的完整实践
前端·vue.js·node.js
ange20171 小时前
前端工程的相关管理 git、branch、build
前端·git
C+ 安口木2 小时前
纯前端实现图文识别 OCR
前端·javascript·ocr
白熊1882 小时前
【通用智能体】Lynx :一款基于终端的纯文本网页浏览器
前端·人工智能·chrome·通用智能体
二川bro2 小时前
Cursor 模型深度分析:区别、优缺点及适用场景
前端
NoneCoder2 小时前
正则表达式与文本处理的艺术
前端·javascript·面试·正则表达式