offer收割之面试准备

·  阅读 624
offer收割之面试准备

自我介绍(仅供参考)

面试官下午好,我叫**,今天来应聘贵公司的前端工程师岗位。我从事前端开发3年多,有3年多的React开发经验,主要技术栈是React + ts,在上家公司主要从事安全交互式扫描系统开发,是属于一个B端的项目,我个人在B端方面的项目经验比较多,以及在项目性能优化方面有比较多的经验,平常喜欢逛一些技术社区丰富自己的技术,比如掘金,我在掘金有两项专栏,前端周刊(主要搜集一些国内外行业新技术热点等)和前端进阶(主要记录总结遇到的技术难点和进阶内容)的专栏,同时我也是掘金的创作者,目前是lv4的等级,然后我自己平时也会把工作中遇到的项目难点记录到我的个人博客网站,为了日后更好的复盘以及成长。面试官以上是我的个人介绍,谢谢.

项目具体介绍

这部分大概率会问,注意语言措辞,做好充足的准备没你就不会慌张,安全感是自己给自己的。  示例:

交互式安全扫描系统(IAST)

项目概述:该项目主要面向B端客户,方便了用户对机器漏洞、异常告警等进行监控管理,助力企业实现智能化、数字化,主要功能包括漏洞管理、异常告警、监控大盘等。

技术栈:Umi3+Ts+AntDesign 个人角色:项目前端负责人

负责内容:

  1. 使用Umi脚手架从0开始搭建项目以及根据业务个性化配置项目
  2. 权限设计方案采用RBAC权限模型
  3. 负责漏洞管理模块 ,模块部署卸载,异常告警模块以及监控大盘等模块的开发
  4. 组内成员任务的分配以及对他们提交代码进行评审

前端项目负责人日常哪些内容?

  1. 首先需要安排划分项目的阶段和迭代,大概每个阶段完成哪些模块,任务排期等
  2. 团队每天的早会总结,主要是同步任务进度以及一些项目难点问题
  3. 就是组员代码的code review

脚手架搭建选型umi的理由?

  1. 首先umi是开箱即用的且内置了中后台开发的所有最佳实践的功能,包括常用的数据流,权限,图标等
  2. 其次它也是蚂蚁金服开源的产品有大厂加持维护,已经经过了包括蚂蚁,字节快手等各大厂无数中台应用的验证

在这个项目使用脚手架起项目中做了哪些个性化的改造?

  1. 自定义了request的请求拦截器(自定义请求header application/json等)和响应拦截器(判断返回的errormsg,未登录和无权限分别弹框提示进入对应地址申请)
  2. 项目引入了loadsh依赖作为工具包,配置webpack使用dayjs-webpack-plugin替换掉antd默认的moment,解决了moment包体积大的问题
  3. 配置webpack.pro,抽取公共资源避免资源,配置splitChunks。
  4. 配置不同的webpack环境,比如本地需要热更新,线上环境需要压缩插件和tree sharking
  5. 统一的错误处理,自定义判断错误类型,对应用message.warn error info 等提示,以及自定义errormsg显示

说明:1.系统登录方案当时考虑到要内部对接其他平台,所以就接入了公司统一的登录平台 2. 路由权限方案是依托于登录系统,主要是根据后端返回cookie信息判断登录状态以及用户信息,在request处做了请求中间处理,扫描系统也有用户和租户模块用于管理租户和用户信息。

路由在运行时配置中做了初始化判断,通过全局状态对路由及组件进行权限分割

定义的access.js定义路由权限,主要也是判断全局初始化状态,isAdmin, 然后通过在router,

{
    path: '/pageA',
    component: 'PageA',
    access: 'canReadPageA', // 权限定义返回值的某个 key
  }
复制代码

组件中通过useAccess()获取权限

权限设计方案RBAC

参考链接:juejin.cn/post/709234…

RBAC其实就是权限方案中的一种,分为大种概念 用户(操作的账号),资源(路由页面,页面内容,数据,接口等),角色(指具有共同特征的一组人,比如运维,研发),角色和资源绑定

角色以及对应的的权限由后台维护,前端维护一份路由和角色的map配置如下:

const menusConfig = [
  {
    path: '/path2',
    meta: {
      title: 'path2',
      roles: ['role1', 'role2']  // 指定每条路由对应的角色
    }
  },
]
复制代码

具体流程

  1. 打开站点,登录
  2. 从后台获取到当前用户的角色
  3. 根据该角色,生成有权限的路由和菜单,进行页面初始化

用户的 角色 属性,需要在创建的时候绑定,在 用户管理 的模块进行配置。

此外还需要在路由层面做权限判断,如果没有权限,则跳转403页面。比如用户没通过面板点击直接通过url进入不属于它权限的页面,这个时候需要做路由拦截,就通过useAccess判断(主要是通过用户角色判断用户角色通过全局状态),如果没权限就重定向到预定义的403页面,

内容权限控制

为了避免后续只要增加一个角色就需要到处改代码,内容权限控制也是通过usePermisesion()这个hook来判断,且在路由配置中增加了包括新增和编辑对应的角色权限,然后定义权限组件包裹如:

{
  path: '/path1',
    meta: {
    title: 'path1',
      permission: { // 对应的操作权限需要的角色
        view: ['role1', 'role2', 'role3'],
        edit: ['role1', 'role2']
        xxxButton: ['roleADD']   // 具体按钮的权限
    }
  }
}

const PermissionControl = ({ permissionkey, children }) => {
  // 通过获取当前路由来找到对应的配置
  const { enableEdit } = usePermisesion(permissionkey); // 可以传入 key 来获取指定特定对资源权限

  if (!enableEdit) {
    return null
  }

  return children
}

// 使用

<PermissionControl><Button /></PermissionControl>
复制代码

项目难点:

1. 通过LightHouse分析FCP,TTI等指标定位漏洞详情页性能问题,并进行优化使得页面加载时间由5s减少到2s

2.规则池批量上传大文件的处理问题

3. 异常告警详情页异常上报列表使用虚拟列表优化用户体验

4.监控大盘图实现随容器大小改变实时更新大小

项目难点1漏洞详情说明:

1.背景是漏洞详情页有大量的渲染数据及图片使得加载时间在5s左右用户体验极差.

我是用谷歌的LightHouse对详情页进行性能分析,通过对FCP,TTI这两个指标分析进行页面卡顿定位,以及LightHouse分析结果中报告发现主要是图片加载影响到了性能,所以我最后使用图片懒加载的方式优化FCP时间,优化后经过LightHouse分析计算FCP由5s减少到2s

常见的网站性能指标

FCP 首屏时间表示从页面开始加载到有页面内容呈现到屏幕 FP 白屏时间 表示从页面开始加载到页面有任何渲染时就触发 TTI 第一次可交互时间


function lazyload() {
  const imgs = document.getElementsByTagName('img');
  const len = imgs.length;
  // 视口的高度
  const viewHeight = document.documentElement.clientHeight;
  // 滚动条高度
  const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;
  for (let i = 0; i < len; i++) {
    const offsetHeight = imgs[i].offsetTop;
    if (offsetHeight < viewHeight + scrollHeight) {
      const src = imgs[i].dataset.src;
      imgs[i].src = src;
    }
  }
}

// 可以使用节流优化一下
window.addEventListener('scroll', lazyload);

复制代码

最后由我自己本地测试,详情页的首屏时间有5s优化到了2s

window.performance.getEntriesByName("first-contentful-paint")[0].startTime - window.performance.navigationStart(http开始请求的时间)

项目难点2大文件上传说明

2.1 上传大文件采用了分片上传的策略,

  • 对文件做切片,即将一个请求拆分成多个请求,每个请求的时间就会缩短,且如果某个请求失败,只需要重新发送这一次请求即可,无需从头开始
  • 通知服务器合并切片,在上传完切片后,前端通知服务器做合并切片操作
  • 控制多个请求的并发量,防止多个请求同时发送,造成浏览器内存溢出,导致页面卡死
  • 做断点续传,当多个请求中有请求发送失败,例如出现网络故障、页面关闭等,我们得对失败的请求做处理,让它们重复发送

文件的标识用文件内容hash,用于文件失败重新上传可以用内容哈希识别到为同一文件,这样后端就从缓存里面直接获取到文件,使用到spark-md5这个库根据文件内容生成hash

备注:1.错误重试的话可以封装一个重试函数,当失败就继续执行请求同时重试次数-1 如果一直失败就返回一个提示

2.或者使用ahooks里面的useRequest自带重试功能

axios.defaults.baseURL = 'http://localhost:3000'
// 选中的文件
var file = null
// 选择文件 文件FIle对象是Blob对象的子类,所以可以用silce()进行拆分
document.getElementById('fileInput').onchange = function({target: {files}}){
    file = files[0] 
}
// 开始上传
document.getElementById('uploadBtn').onclick = function(){
    if (!file) return;
    // 创建切片   
    // let size = 1024 * 1024 * 10; //10MB 切片大小
    let size = 1024 * 50; //50KB 切片大小
    let fileChunks = [];
    let index = 0 //切片序号
    for(let cur = 0; cur < file.size; cur += size){
        fileChunks.push({
            hash: index++,
            chunk: file.slice(cur, cur + size)
        })
    }
    // 控制并发和断点续传
    const uploadFileChunks = async function(list){
        if(list.length === 0){
            //所有任务完成,合并切片
            await axios({
                method: 'get',
                url: '/merge',
                params: {
                    filename: file.name
                }
            });
            console.log('上传完成')
            return
        }
        let pool = []//并发池
        let max = 3 //最大并发量
        let finish = 0//完成的数量
        let failList = []//失败的列表
        for(let i=0;i<list.length;i++){
            let item = list[i]
            let formData = new FormData()
            formData.append('filename', file.name)
            formData.append('hash', item.hash)
            formData.append('chunk', item.chunk)
            // 上传切片
            let task = axios({
                method: 'post',
                url: '/upload',
                data: formData
            })
            task.then((data)=>{
                //请求结束后将该Promise任务从并发池中移除
                let index = pool.findIndex(t=> t===task)
                pool.splice(index)
            }).catch(()=>{
                failList.push(item)
            }).finally(()=>{
                finish++
                //所有请求都请求完成
                if(finish===list.length){
                    uploadFileChunks(failList)
                }
            })
            pool.push(task)
            if(pool.length === max){
                //每当并发池跑完一个任务,就再塞入一个任务
                await Promise.race(pool)
            }
        }
    }
    uploadFileChunks(fileChunks)

}
</script>
</html>
复制代码

项目难点3.虚拟列表说明

参考链接:juejin.cn/post/708594…

为啥不用分页?

因为产品那边的要求,要求在这里实现下拉然后加载数据的效果

  1. 计算容器最大容积数量

就是先要计算出可视区域内最多能够容纳多少个列表项 最多数量= 当前页面可视高度offsetHeight / itemHeight

// 滚动容器高度改变后执行的函数
const changeHeight = useCallback(throttle(() => {
  // 容器高度,通过操作dom元素获取高度是因为它不一定是个定值
  curContainerHeight.current = containerRef.current.offsetHeight
  // 列表最大数量,考虑到列表中顶部和底部可能都会出现没有展现完的item
  curViewNum.current = Math.ceil(curContainerHeight.current / itemHeight) + 1
}, 500), [])

useEffect(() => {
  // 组件第一次挂载需要初始化容器的高度以及最大容纳值
  changeHeight()
  // 因为我们的可视窗口和浏览器大小有关系,所以我们需要监听浏览器大小的变化
  // 当浏览器大小改变之后需要重新执行changeHeight函数计算当前可视窗口对应的最大容纳量是多少
  window.addEventListener('resize', changeHeight)
  return () => {
    window.removeEventListener('resize', changeHeight)
  }
}, [changeHeight])

复制代码

2.只渲染可视区域内的元素

定义计算函数作为监听scroll的处理函数,这里主要是动态计算可视区域起始索引值和结束索引值,然后根据他俩的值渲染可视区域列表项,用总数据slice(startIndex, endIndex +1) startIndex = Math.floor(scrollTop/itemHeight)
endIndex = startIndex + (clientHeight/itemHeight) - 1

3.动态设置上下空白占位 上下空白处设置占位主要是因为仅仅页面10几条数据无法生成滚动条,这里通过设置paddingToppaddingBottom的值来动态的撑开内容区域

paddingTop: ${startIndex * itemHeight}px, // 总数据的length - endIndex 也就是还剩多少数据没渲染 paddingBottom: ${(dataListRef.current.length - 1 - endIndex) * itemHeight}px

4.下拉触发自动请求和加载数据

下拉加载新的数据进来,放置到缓存数据当中,然后我们再根据滚动事件的startIndex和endIndex决定具体渲染哪一部分的数据到页面上去,

滚动事件可以使用requestAnimationFrame来优化渲染动画帧以免卡顿

项目难点4 监控大盘图实现随窗口大

主要是对antd原子组件的二次封装,提供页面级的组件小改变实时更新大小:

echarts自身提供了 resize API,用来改变图表尺寸,然后监听window的resize事件,这里遇到一个坑,首先给事件添加事件处理函数有两个方法, 1.addEventListener('resize', function() { // 调用echarts的resize API改变图表大小 })

2.window.onresize = function()

因为这里用到了echarts组件复用(组件封装),所以使用onresize这种方式添加事件只有最后的一个图表的大小会自适应,后来查MDN才发现onresize会替换掉之前已经定义过的onresize方法,所以改用addEventListener解决(addEventListener可以为一个事件添加多个处理函数)

复杂组件的封装

参考链接:juejin.cn/post/706297…

主要是对antd原子组件的二次封装,提供页面级的组件

为啥不用procomponent?

因为当时的业务需求里很多功能是procomponent没有提供的,比如antd组件只有少数组件支持readonly模式,可能需要设置为disabled模式,然而disabled会把组件变成灰色,而且还会把placerholder显示出来

项目中别的问题:

项目还有哪些优化点?

  • 首屏加载时间过慢,后面可能会考虑使用ssr来改造
  • 引入按需加载配置(组件体积太大影响首屏渲染),对某些组件使用按需加载,umi中开启dynamic配置,然后使用dynamic封装异步组件

授权管理系统

项目描述 :该项目为公司内部云平台授权管理系统,用来解决云平台授权复杂、不易操作等问题,主要功能包括客户管理,激活码管理和用户管理等

技术选型 :使用Umi3 +Ant Design+Typescript

负责内容 :

1. 和产品设计师沟通设计稿,编写系统使用手册

2.负责激活码管理,用户管理模块的开发

项目难点:

  1. 首屏加载时间从7s优化到3s

2. 使用umi-request上传文件失败问题

3. Antd design日期组件无法显示中文问题

难点说明1.1:

首屏性能主要看两个指标:白屏时间(FP)首屏时间(FCP) 其次还有TTI(第一次可交互时间)lighthouse(谷歌浏览器性能调试工具)

  • 白屏时间(FP):是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间
  • 首屏时间(First Contentful Paint):是指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间

主要利用 window.performance计算首屏时间

window.performance.getEntriesByName("first-contentful-paint")[0].startTime - window.performance.navigationStart(http开始请求的时间)
复制代码
  1. 优化打包体积 通过umi analyze 分析打包后的模块,发现有很多公共的模块出现在多个chunk里面,比如antd,loadsh相关的,然后就想着把这些模块打包到一起,具体做法配置config.plugin.ts配置里面的
 cacheGroups: {
          lfpantdesigns: {
            name: 'antdesigns',
            chunks: 'all',
            test: /[\\/]node_modules[\\/](@antv|antd|@ant-design)/,
            priority: 10,
         },
复制代码
  1. 提高打包速度

在config.webpackplugin中引入了多线程打包(happypack)

const HappyPack = require('happypack');
const happyThreadPool = HappyPack.ThreadPool({
  size: require('os').cpus().length,
});

const webpackPlugin = (config: any) => {
  config.plugin('HappyPack').use(HappyPack, [
    {
      id: 'js',
      loaders: ['babel-loader'],
      threadPool: happyThreadPool,
    },
  ]);
  // ... 其他配置
};
复制代码

参考链接: juejin.cn/post/708145…

2.1 在授权码管理模块,上传授权码文件时使用umi-request这个请求库,按照官方的demo上传文件使用 FormData(), 并不会自动添加 header 中对应的 Content-Type,导致这个功能一直有问题,排查了很久也没排查出来,最后还是在github的issue里找到有关的问题,需要在请求中添加requestType: 'form',这应该是官方的demo的疏忽,而且后来发现它们修复了这个。

3.1 Antd design日期组件在配置了antd 国际化以后,日期组件显示仍然为英文,按照文档配置了多次也没生效,最后在查阅了大量资料以后,发现是momentjs的问题,最后使用自定义日期库使用dayjs替换moment,使用dayjs.locale('zh-cn')做国际化解决,且dayjs打包体积减少了330kb

garen-cli(一个脚手架)

项目描述:该项目是一个用来搭建小型前端项目的脚手架,garen意为盖伦--游戏人物.

技术选型:Nodejs

依赖模块:chalk commander download-git-repo inquirer等

负责内容:

项目的搭建,开发以及测试 遇到的问题:

1. npm发布问题

2. download-git-repo使用错误问题

主要实现原理: 1.输入cli指令(通过package.json中的bin字段对应入口文件) 2.自定义命令比如create(commander),然后判断输入的文件名是否存在,存在则移除, 3.询问用户选择模板(inquirer),根据拿到的值 执行对应的拉取命令,拉取远程项目到本地

当然里面还使用chalk 和 ora等美化工具,交互时的loading动画 进度条等

判断文件是否存在fs.existsSync(项目路径) ,

// 获取当前命令行所在目录
const cwd = process.cwd();
// 项目目录为 当前目录+项目名 
const project = path.join(cwd, name);
复制代码

解决办法: 1. npm发布问题显示权限问题,后来把npm源改回去就好了 2. 遇到错误以后我查看了它的源码发现它其实也是使用child_process这个node模块提供的spawn('git' ,'clone'),执行的命令,所以干脆用child_process.spawn()实现了拉取远程模板的功能

被问到没遇到过的场景?

首先不要慌,然后说这个问题确实自己没遇到过,之后会加入到学习计划里,然后可以酌情大胆加入自己的猜测

为什么跳槽

因为感觉我个人遇到了技术上的天花板,希望能找一个技术氛围更好的公司提升技术.

当被分配一个几乎不可能完成的任务时,会怎么做

这种情况下,一般通过下面方式来解决:

  1. 自己先查找资料,寻找解决方案,评估自己需要怎样的资源来完成,需要多长时间
  2. 能不能借助周围同事来解决问题
  3. 拿着分析结果跟上级反馈,寻求帮助或者资源

突出的软技能:分析和解决问题,沟通寻求帮助。

提问环节(提问面试官)

面试是一个双向选择的事情,所以面试后一般会有提问环节。在提问环节,候选人最好不要什么都不问,更不要只问薪水待遇、是否加班之类的问题。

其实这个时候可以反问面试官了解团队情况、团队做的业务、本职位具体做的工作、工作的规划,晋升是怎样的

  • 如果我⼊职这个岗位的话,前三个⽉你希望我能做到些什么?
  • 你对这个职位理想⼈选的要求是什么?

还可以问一些关于公司培训机会和晋升机会之类的问题。如果是一些高端职位,则可以问一下:自己的 leader 想把这个职位安排给什么样的人,希望多久的时间内可以达到怎样的水平。

HR提问环节

你对未来3-5年的职业规划

1.首先我现在的话技术还没达到我个人的天花板,还想用一年的时间提升技术,未来的话应该会往部门管理方向走。然后当前的规划就是希望在咱们公司发光发热了.

如何看待加班(996)?

我认为加班分为紧急加班和⻓期加班,紧急加班的话每个公司都会遇到,我愿意牺牲时间,长期加班的话如果是团队一起我觉得也没问题,因为现在还是技术成长期.

⾯对⼤量超过⾃⼰承受能⼒且时间有限的⼯作时你会怎么办?

1.将任务划分优先级,优先处理紧急的问题,同时和领导沟通把不重要的任务放缓执行或者砍掉,或者派组内新人去处理下

你还有其他公司的Offer吗?

  1. 表明⾃⼰有三四个已经确认过的offer了(没有offer也要吹,但是不要透露具体公司)
  2. 但是第⼀意向还是本公司,如果薪资差距不⼤,会优先考虑本公司
  3. 再透露出,有⼀两个offer催得⽐较急,希望这边快点出结果

如何与HR谈薪资?

  • HR: 您期望的薪资是多少?
  • 你: 就我的⾯试表现,贵公司最⾼可以给多少薪⽔?
  • HR: 这个暂且没法确定,要结合您⼏轮⾯试结果和⽤⼈部⻔的意⻅来综合评定。
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改