Nodreame

Nodreame 查看完整档案

广州编辑中山大学南方学院  |  电子信息与通信工程 编辑无业 ing  |  前端开发工程师 编辑 nodreame.github.io/ 编辑
编辑

伪全栈|前端|前软粉
目标是成为神奇宝贝大师,一大口豆奶喝三个月!

个人动态

Nodreame 收藏了文章 · 2月28日

我在阿里招前端,我该怎么帮你?

我是谁?为什么写这篇文章?

我是淘宝技术部的一名普通的前端技术专家,花名磐冲。每年都想给团队内招几个同学,但是努力了几年,一个都没有招进来。是我看简历太少了吗?不是,只算内部简历系统,我看过的简历至少上千。是我要求太严格吗?也许是吧,不过,我电话面试拒绝的同学,只有1位在一段时间后,入职了另一个部门。

好吧,我承认,我自己在招聘上可能是有点没找到方法。但是,看了那么多简历,经历了那么多次面试,我最大的感受却是惋惜。因为有好多同学,在电话那头我听出了努力,听出了能力,听出了激情,但是却没有听到亮点、和让我觉得,能够继续闯过下一关的能力。

我面试过的同学,在结束的时候,我都会指出问题,并给出学习建议。大部分同学不是不够努力,不是不够聪明,而是没有找对方法,没有切中要害。我总结了一下之前所有的面试经历,以及常见的问题,写下这篇文章,希望能够给前端的同学,不论是否来面试阿里的职位,有一个参考。同时,也是写下我自己总结的方法,希望能帮助到其他技术相关的同学。

我们想要的同学

JD

业务背景

淘宝内部最大创新项目之一,大团队已有百人规模,大部分项目处于保密阶段,前景远大

职位描述

1.负责组件库与业务页面开发。
2.带领团队完成技术产品实现。
3.负责大型多应用架构设计。
4.利用前端技术与服务端协同完成团队业务目标。

职位要求

0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。
1.熟练掌握JavaScript。
2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。
3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。
4.熟练掌握react生态常用工具,redux/react-router等。
5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。
6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。
7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。

翻译一下JD

为什么起这个标题呢?因为有很多人看到职位描述,可能就在和自己做的事情一一比对,把关键字都核对上。而很多前端同学看到职位要求第一条里的图形学,可能就开始打退堂鼓了。或者看到几个关键字自己都认识,就觉得没问题,还挺简单的。

就这样望而却步真的好吗?为什么职位描述看着简单,面试却这么难呢?你真的读懂这份职位描述了吗?

现在,不妨先停一下,就上面的问题,我们来细细品一下。什么叫读懂职位描述呢?从我个人的理解,读懂职位描述,应该是读懂这个职位需要哪些基础能力,以及可能遇到哪些挑战。我们写自己简历的时候,“精通react”和“熟练使用react”,相信大家不会随意去写。同样的,JD里面的:掌握、熟练掌握、了解、熟悉,也不是随意写的,这代表了团队对新同学的能力要求。

回想写自己简历的时候,我们会对这个前缀扪心自问一下。因为会担心一旦写了精通,面试官的问题会更难,甚至觉得只有源码倒背如流的人,才能称得上精通。当然也会有同学非常自信,用react做过几个项目,就写上了精通react。

这两种都可以称为精通,也都不可以。没有客观标准,又怎么去衡量呢?而标准在哪里呢?所以在这里,我从阿里面试官角度,给出我认为的标准,尽可能的做到客观可量化。那么,基于上面这份职位标准,我来翻译一下职位要求:

首先,总览全部的要求,会发现这个职位虽然提到了3d相关的技能,但是大部分却是应用开发相关的能力,所以这个职位并不是想找专业的3d领域同学,而是需要一个工程化能力强,对3d有了解的同学。

0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。

初级:

  • 学习过图形学相关知识,知道矩阵等数学原理在动画中的作用,知道三维场景需要的最基础的构成,能用threejs搭3d场景,知道webgl和threejs的关系。
  • 知道canvas是干嘛的,聊到旋转能说出canvas的api。
  • 知道css动画,css动画属性知道关键字和用法(换句话说,电话面试会当场出题要求口喷css动画,至少能说对大概,而不是回答百度一下就会用)。
  • 知道js动画,能说出1~2个社区js动画库,知道js动画和css动画优缺点以及适用场景。
  • 知道raf和其他达到60fps的方法。

中级:

  • 如果没有threejs,你也能基于webgl自己封装一个简单的threejs出来。
  • 聊到原理能说出四元数,聊到鼠标操作能提到节流,聊到性能能提到restore,聊到帧说出raf和timeout的区别,以及各自在优化时候的作用。
  • 知道怎样在移动端处理加载问题,渲染性能问题。
  • 知道如何结合native能力优化性能。
  • 知道如何排查性能问题。对chrome动画、3d、传感器调试十分了解。

高级:

  • 搭建过整套资源加载优化方案,能说明白整体方案的各个细节,包括前端、客户端、服务端分别需要实现哪些功能点、依赖哪些基础能力,以及如何配合。
  • 设计并实现过前端动画引擎,能说明白一个复杂互动项目的技术架构,知道需要哪些核心模块,以及这些模块间如何配合。
  • 有自己实现的动画相关技术方案产出,这套技术方案必须是解决明确的业务或技术难点问题的。为了业务快速落地而封装一个库,不算这里的技术方案。如果有类似社区方案,必须能从原理上说明白和竞品的差异,各自优劣,以及技术选型的原因。
1.熟练掌握JavaScript。

初级:

  • JavaScript各种概念都得了解,《JavaScript语言精粹》这本书的目录都得有概念,并且这些核心点都能脱口而出是什么。这里列举一些做参考:
  • 知道组合寄生继承,知道class继承。
  • 知道怎么创建类function + class。
  • 知道闭包在实际场景中怎么用,常见的坑。
  • 知道模块是什么,怎么用。
  • 知道event loop是什么,能举例说明event loop怎么影响平时的编码。
  • 掌握基础数据结构,比如堆、栈、树,并了解这些数据结构计算机基础中的作用。
  • 知道ES6数组相关方法,比如forEach,map,reduce。

中级:

  • 知道class继承与组合寄生继承的差别,并能举例说明。
  • 知道event loop原理,知道宏微任务,并且能从个人理解层面说出为什么要区分。知道node和浏览器在实现loop时候的差别。
  • 能将继承、作用域、闭包、模块这些概念融汇贯通,并且结合实际例子说明这几个概念怎样结合在一起。
  • 能脱口而出2种以上设计模式的核心思想,并结合js语言特性举例或口喷基础实现。
  • 掌握一些基础算法核心思想或简单算法问题,比如排序,大数相加。
2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。

初级:

  • 知道webpack,rollup以及他们适用的场景。
  • 知道webpack v4和v3的区别。
  • 脱口而出webpack基础配置。
  • 知道webpack打包结果的代码结构和执行流程,知道index.js,runtime.js是干嘛的。
  • 知道amd,cmd,commonjs,es module分别是什么。
  • 知道所有模块化标准定义一个模块怎么写。给出2个文件,能口喷一段代码完成模块打包和执行的核心逻辑。

中级:

  • 知道webpack打包链路,知道plugin生命周期,知道怎么写一个plugin和loader。
  • 知道常见loader做了什么事情,能几句话说明白,比如babel-loader,vue-loader。
  • 能结合性能优化聊webpack配置怎么做,能清楚说明白核心要点有哪些,并说明解决什么问题,需要哪些外部依赖,比如cdn,接入层等。
  • 了解异步模块加载的实现原理,能口喷代码实现核心逻辑。

高级:

  • 能设计出或具体说明白团队研发基础设施。具体包括但不限于:
  • 项目脚手架搭建,及如何以工具形态共享。
  • 团队eslint规范如何设计,及如何统一更新。
  • 工具化打包发布流程,包括本地调试、云构建、线上发布体系、一键部署能力。同时,方案不仅限于前端工程部分,包含相关服务端基础设施,比如cdn服务搭建,接入层缓存方案设计,域名管控等。
  • 客户端缓存及预加载方案。
3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。

初级:

  • 知道react常见优化方案,脱口而出常用生命周期,知道他们是干什么的。
  • 知道react大致实现思路,能对比react和js控制原生dom的差异,能口喷一个简化版的react。
  • 知道diff算法大致实现思路。
  • 对state和props有自己的使用心得,结合受控组件、hoc等特性描述,需要说明各种方案的适用场景。
  • 以上几点react替换为vue或angular同样适用。

中级:

  • 能说明白为什么要实现fiber,以及可能带来的坑。
  • 能说明白为什么要实现hook。
  • 能说明白为什么要用immutable,以及用或者不用的考虑。
  • 知道react不常用的特性,比如context,portal。
  • 能用自己的理解说明白react like框架的本质,能说明白如何让这些框架共存。

高级:

  • 能设计出框架无关的技术架构。包括但不限于:
  • 说明如何解决可能存在的冲突问题,需要结合实际案例。
  • 能说明架构分层逻辑、各层的核心模块,以及核心模块要解决的问题。能结合实际场景例举一些坑或者优雅的处理方案则更佳。
4.熟练掌握react生态常用工具,redux/react-router等。

初级:

  • 知道react-router,redux,redux-thunk,react-redux,immutable,antd或同级别社区组件库。
  • 知道vue和angular对应全家桶分别有哪些。
  • 知道浏览器react相关插件有什么,怎么用。
  • 知道react-router v3/v4的差异。
  • 知道antd组件化设计思路。
  • 知道thunk干嘛用的,怎么实现的。

中级:

  • 看过全家桶源码,不要求每行都看,但是知道核心实现原理和底层依赖。能口喷几行关键代码把对应类库实现即达标。
  • 能从数据驱动角度透彻的说明白redux,能够口喷原生js和redux结合要怎么做。
  • 能结合redux,vuex,mobx等数据流谈谈自己对vue和react的异同。

高级:

  • 有基于全家桶构建复杂应用的经验,比如最近很火的微前端和这些类库结合的时候要注意什么,会有什么坑,怎么解决
5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。

初级:

  • HTML方面包括但不限于:语义化标签,history api,storage,ajax2.0等。
  • CSS方面包括但不限于:文档流,重绘重排,flex,BFC,IFC,before/after,动画,keyframe,画三角,优先级矩阵等。
  • 知道axios或同级别网络请求库,知道axios的核心功能。
  • 能口喷xhr用法,知道网络请求相关技术和技术底层,包括但不限于:content-type,不同type的作用;restful设计理念;cors处理方案,以及浏览器和服务端执行流程;口喷文件上传实现;
  • 知道如何完成登陆模块,包括但不限于:登陆表单如何实现;cookie登录态维护方案;token base登录态方案;session概念;

中级:

  • HTML方面能够结合各个浏览器api描述常用类库的实现。
  • css方面能够结合各个概念,说明白网上那些hack方案或优化方案的原理。
  • 能说明白接口请求的前后端整体架构和流程,包括:业务代码,浏览器原理,http协议,服务端接入层,rpc服务调用,负载均衡。
  • 知道websocket用法,包括但不限于:鉴权,房间分配,心跳机制,重连方案等。
  • 知道pc端与移动端登录态维护方案,知道token base登录态实现细节,知道服务端session控制实现,关键字:refresh token。
  • 知道oauth2.0轻量与完整实现原理。
  • 知道移动端api请求与socket如何通过native发送,知道如何与native进行数据交互,知道ios与安卓jsbridge实现原理。

高级:

  • 知道移动端webview和基础能力,包括但不限于:iOS端uiwebview与wkwebview差异;webview资源加载优化方案;webview池管理方案;native路由等。
  • 登陆抽象层,能够给出完整的前后端对用户体系的整体技术架构设计,满足多业务形态用户体系统一。考虑跨域名、多组织架构、跨端、用户态开放等场景。
  • mock方案,能够设计出满足各种场景需要的mock数据方案,同时能说出对前后端分离的理解。考虑mock方案的通用性、场景覆盖度,以及代码或工程侵入程度。
  • 埋点方案,能够说明白前端埋点方案技术底层实现,以及技术选型原理。能够设计出基于埋点的数据采集和分析方案,关键字包括:分桶策略,采样率,时序性,数据仓库,数据清洗等。
6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。

初级:

  • 知道eslint,以及如何与工程配合使用。
  • 了解近3年前端较重要的更新事件。
  • 面试过程中遇到答不出来的问题,能从逻辑分析上给出大致的思考路径。
  • 知道几个热门的国内外前端技术网站,同时能例举几个面试过程中的核心点是从哪里看到的。

高级:

  • 在团队内推行eslint,并给出工程化解决方案。
  • 面试过程思路清晰,面试官给出关键字,能够快速反应出相关的技术要点,但是也要避免滔滔不绝,说一堆无关紧要的东西。举例来说,当时勾股老师面试我的时候,问了我一个左图右文的布局做法,我的回答是:我自己总结过7种方案,其中比较好用的是基于BFC的,float的以及flex的三种。之后把关键css口喷了一下,然后css就面完了。
7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。
  • 根据了解的深度分初/中/高级。
  • 知道TS是什么,为什么要用TS,有TS工程化实践经验。
  • 知道移动端前端常见问题,包括但不限于:rem + 1px方案;预加载;jsbridge原理等。
  • 能说出大概的服务端技术,包括但不限于:docker;k8s;rpc原理;中后台架构分层;缓存处理;分布式;响应式编程等。

JD的要求很难吗?

首先,感谢你能看到这里,如果你是仔细看的,那么我更加感动了。而且你已经用实际行动,证明了你的学习能力和耐心。上面那么大篇幅的JD翻译,有一个问题,大家应该都有答案了:为什么职位描述看着简单,面试却这么难呢?然而,有些同学可能会嘲讽起来:写了那么多,我认识的有些阿里P6,P7也不是都会啊,大厂都是螺丝钉,也就面试时候问问,实际工作不还是if else,何况我又遇不到这些场景,我怎么可能知道。

在这里,我想严肃的说明的是:

  1. 我所认识的淘宝前端,以及我所在团队的P6同学,上面初级都能做到,中级至少覆盖60%,高级覆盖20%;P6+同学,中级覆盖80%以上,高级覆盖50%以上;P7同学高级覆盖80%以上。
  2. 我们团队的前端,每一个人都负责多个复杂业务项目(客观数据上:至少对接20+服务端接口,5个以上router配置,涉及多个用户角色的综合业务系统),以及一些通用能力,比如组件库等。不存在一个人只接一条业务线,只负责维护某几个组件这种螺丝钉式的工作。我不知道大厂都是螺丝钉的言论为什么会被复用到互联网企业,我个人感受是,如果我在阿里的工作是螺丝钉,那么我以前几份工作可能勉强算是螺纹。另外,如果你想要晋升,那么维护好这几个业务系统只是你的本职工作,晋升时请提供一些更高层面的思考和技术产出。
  3. if else也分鲜花和牛粪。有的人写的是[].reduce,而有的人写的是var temp = ''; for() { temp += 'xxx' }。另外,如果不知道原理,那么类似webpack这种明星级的技术产品,将永远与你无缘。冷静下来想想,webpack难道也只是if else吗?是,又不全是。

聪明的你应该看出来了,上面JD翻译里的初级、中级和高级,对应的就是我认为的,阿里p6/p6+/p7的能力标准,同时也是一张知识图谱。初级的要求更偏实际应用和基础原理,中级的要求是基于原理的拓展和复杂技术架构的应用,高级的要求是对跨栈、跨端,多领域结合产出综合方案的能力。而且,我们对技术的要求,都是能够与实际业务场景结合,或者能对提升工作效率有帮助的。空谈和尬想,或者只是百度来的文章,没有经过内化,那么面试过程中将被瞬间拆穿。

有时我会在boss直聘上直接打字面试,有时我也会听到面试过程中,电话那头传来键盘敲击的声音,甚至有时候我会主动让面试的同学去百度一下,或者挂电话思考一下,过15分钟再聊。我敢这么面试,因为我知道,我要的答案你查不出来,我看的是你真正理解的东西。能搜索到的,我不在乎,我也希望你去查,来为你更好的表现综合能力。

破局的方法

好了,如果看到这里,并没有把你劝退的话,那么让我们来点希望的曙光。这里用一句阿里土话来给大家一些安慰:不难,要你干嘛?

开篇我提到面试过那么多同学之后,我最大的感受是惋惜,因为有很多同学在我看来就差一点点,他有足够的个人能力,可能只是没有找到感觉。这里我例举两个比较典型的问题。

什么是亮点?

我相信这是很多同学心中的疑惑,而且事实上,我看到很多简历下面的面试记录都会写:缺乏亮点,暂不考虑。如果仔细看了上文,到这里还有这个疑惑,那么我觉得你需要再静下心来感受一下。

这里我不对亮点做明确的表述,我举一个例子来让大家更有体感一些:

A: 负责公司前端工作,使用webpack打包代码并发布线上。使用webpack配置对整体性能做优化,用happypack加快了打包速度。

B: 建设内部云构建体系,产出通用命令行指令工具;将发布、环境切换、快速回滚能力平台化,保证了线上环境稳定性;同时将研发流程量化管控,每周产出研发效能报告。

如果你是面试官,在简历的大海里看一个项目描述,什么最吸引你的眼球呢?是webpack,happypack的关键字吗?还是一句话就让你想到这件事的复杂性,和这个系统带来的巨大价值?

没有场景怎么办?

这也是很多同学经常遇到的问题。上面例举了那么多技术点,而我在的环境,前端就我一个,甚至服务端我都要写一点,哪有精力去搞这种大规模团队用到的东西?

首先,时间靠自己合理规划。我和老婆两个人自己带孩子,有两个娃,每天平均9点下班,我每天回家收拾玩具,孩子睡得晚可能需要再陪玩一下,周末我带孩子为主,但是我去年仍然白金了2个ps4的游戏。

在时间问题排除之后,我建议分三个阶段:

  1. 毕业3年以内的阶段:不用着急,你的选择很多,你可以核对上面初级的点,看自己是否都做到了,没做到就去好好学习吧,初级的技术要点对团队规模没有依赖,一个人也能做到极致。如果你所处的环境已经有2个人,可以同时关注中级和高级的点,不要觉得人少就不去尝试,放手去做,过程中会有实打实的收获。
  2. 毕业5年以内的阶段:不论你处的环境团队规模如何,请开始着眼于中级和高级相关能力,人少就不需要研发提效了吗?我在segmentFault上发的第一篇文章,是如何用travis和github做一键部署,那时候我还没有去淘宝,我所在的团队也没有用到这个能力,这篇文章是我自己的个人项目用到的。而整个过程同样涉及到了研发效能的方方面面。
  3. 毕业8年以内的阶段:请开始着眼于高级相关的技术方案产出。我以组件动态化为例,我早年维护手机淘宝的整个交易链路H5页面,所有页面的ui部分都是细粒度组件化抽离,通过配置下发页面结构的。即使一个人维护一个页面,也要竭尽所能去思考好的技术方案。这种高度动态的设计,带来的好处是,每年双十一,80%的需求交给pd自己处理就行了,剩下流转到我手上需要开发的需求,都是新增交互,或者之前抽象不足的组件。所以当时我在的团队,3个人在维护了包括手淘首页、商品详情和正逆向交易链路所有H5页面,同时还有额外精力去支持大促会场页。更好的技术思考和设计,一定能给你带来更多的可能性,而系统的优雅程度,一定不是靠业务代码的堆砌,而是作为技术核心的你,如何去思考。

我想怎么帮你

我相信每个人都是能快速成长的,只是每个人缺少的东西不同。有的人少了些脚踏实地,有的人少了些登高望远的机会,更多的人或许只是没有找到那条正确的路。

我希望这篇文章能够帮助到正在前端领域努力的人,也希望这一篇文章就能成为指路明灯之一。但同时我也深知,每个人都是不一样的,所以,我这里留下联系方式,
需要的同学可以加微信:vianvio
编组.png
加备注:前端同路人。我可以给你做模拟面试,同时给出我认为的,适合你的发展思路和建议,当然也可以帮你内推。

另外,目前我们成立了一个模拟面试群,有定期活动,可以参考 https://github.com/vianvio/FE...
欢迎有兴趣的同学来参加。

介绍一下我所在的团队

我在阿里巴巴淘宝技术部-ihome业务。目前,ihome正在深耕家居家装行业,纵向深入行业内部,希望能给行业带来一些创新。目前可对外公开的产品和业务形态有:躺平App、位于青岛和宁波的桔至生活门店。我们还有更多有趣、充满挑战和超出你想象的业务。我们期待有志之士的加入!

如果你愿意来和我们一起相信,那请发送简历过来,我们一定会一起看见!

前端简历请发送到:yefei.niuyf@alibaba-inc.com 或 lijie.slj@alibaba-inc.com
主攻3d方向的同学,简历请发送到:jiangcheng.wxd@alibaba-inc.com
java简历请发送到:xiaoxian.zzy@taobao.com 或 wuxin.sn@taobao.com
客户端简历请发送到:fangying.fy@alibaba-inc.com

或许有人会觉得奇怪,联系方式写在最后,还有多少人能看到,这里我引用马爸爸和逍遥子大佬对阿里价值观的解读,来解释一下:我们的价值观是为了帮助我们寻找同路的人。

感谢你陪我一起走到这篇文章的最后,如果你觉得这篇文章已经对你有很大帮助了,那就请我喝杯咖啡吧~

查看原文

Nodreame 赞了文章 · 2月28日

我在阿里招前端,我该怎么帮你?

我是谁?为什么写这篇文章?

我是淘宝技术部的一名普通的前端技术专家,花名磐冲。每年都想给团队内招几个同学,但是努力了几年,一个都没有招进来。是我看简历太少了吗?不是,只算内部简历系统,我看过的简历至少上千。是我要求太严格吗?也许是吧,不过,我电话面试拒绝的同学,只有1位在一段时间后,入职了另一个部门。

好吧,我承认,我自己在招聘上可能是有点没找到方法。但是,看了那么多简历,经历了那么多次面试,我最大的感受却是惋惜。因为有好多同学,在电话那头我听出了努力,听出了能力,听出了激情,但是却没有听到亮点、和让我觉得,能够继续闯过下一关的能力。

我面试过的同学,在结束的时候,我都会指出问题,并给出学习建议。大部分同学不是不够努力,不是不够聪明,而是没有找对方法,没有切中要害。我总结了一下之前所有的面试经历,以及常见的问题,写下这篇文章,希望能够给前端的同学,不论是否来面试阿里的职位,有一个参考。同时,也是写下我自己总结的方法,希望能帮助到其他技术相关的同学。

我们想要的同学

JD

业务背景

淘宝内部最大创新项目之一,大团队已有百人规模,大部分项目处于保密阶段,前景远大

职位描述

1.负责组件库与业务页面开发。
2.带领团队完成技术产品实现。
3.负责大型多应用架构设计。
4.利用前端技术与服务端协同完成团队业务目标。

职位要求

0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。
1.熟练掌握JavaScript。
2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。
3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。
4.熟练掌握react生态常用工具,redux/react-router等。
5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。
6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。
7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。

翻译一下JD

为什么起这个标题呢?因为有很多人看到职位描述,可能就在和自己做的事情一一比对,把关键字都核对上。而很多前端同学看到职位要求第一条里的图形学,可能就开始打退堂鼓了。或者看到几个关键字自己都认识,就觉得没问题,还挺简单的。

就这样望而却步真的好吗?为什么职位描述看着简单,面试却这么难呢?你真的读懂这份职位描述了吗?

现在,不妨先停一下,就上面的问题,我们来细细品一下。什么叫读懂职位描述呢?从我个人的理解,读懂职位描述,应该是读懂这个职位需要哪些基础能力,以及可能遇到哪些挑战。我们写自己简历的时候,“精通react”和“熟练使用react”,相信大家不会随意去写。同样的,JD里面的:掌握、熟练掌握、了解、熟悉,也不是随意写的,这代表了团队对新同学的能力要求。

回想写自己简历的时候,我们会对这个前缀扪心自问一下。因为会担心一旦写了精通,面试官的问题会更难,甚至觉得只有源码倒背如流的人,才能称得上精通。当然也会有同学非常自信,用react做过几个项目,就写上了精通react。

这两种都可以称为精通,也都不可以。没有客观标准,又怎么去衡量呢?而标准在哪里呢?所以在这里,我从阿里面试官角度,给出我认为的标准,尽可能的做到客观可量化。那么,基于上面这份职位标准,我来翻译一下职位要求:

首先,总览全部的要求,会发现这个职位虽然提到了3d相关的技能,但是大部分却是应用开发相关的能力,所以这个职位并不是想找专业的3d领域同学,而是需要一个工程化能力强,对3d有了解的同学。

0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。

初级:

  • 学习过图形学相关知识,知道矩阵等数学原理在动画中的作用,知道三维场景需要的最基础的构成,能用threejs搭3d场景,知道webgl和threejs的关系。
  • 知道canvas是干嘛的,聊到旋转能说出canvas的api。
  • 知道css动画,css动画属性知道关键字和用法(换句话说,电话面试会当场出题要求口喷css动画,至少能说对大概,而不是回答百度一下就会用)。
  • 知道js动画,能说出1~2个社区js动画库,知道js动画和css动画优缺点以及适用场景。
  • 知道raf和其他达到60fps的方法。

中级:

  • 如果没有threejs,你也能基于webgl自己封装一个简单的threejs出来。
  • 聊到原理能说出四元数,聊到鼠标操作能提到节流,聊到性能能提到restore,聊到帧说出raf和timeout的区别,以及各自在优化时候的作用。
  • 知道怎样在移动端处理加载问题,渲染性能问题。
  • 知道如何结合native能力优化性能。
  • 知道如何排查性能问题。对chrome动画、3d、传感器调试十分了解。

高级:

  • 搭建过整套资源加载优化方案,能说明白整体方案的各个细节,包括前端、客户端、服务端分别需要实现哪些功能点、依赖哪些基础能力,以及如何配合。
  • 设计并实现过前端动画引擎,能说明白一个复杂互动项目的技术架构,知道需要哪些核心模块,以及这些模块间如何配合。
  • 有自己实现的动画相关技术方案产出,这套技术方案必须是解决明确的业务或技术难点问题的。为了业务快速落地而封装一个库,不算这里的技术方案。如果有类似社区方案,必须能从原理上说明白和竞品的差异,各自优劣,以及技术选型的原因。
1.熟练掌握JavaScript。

初级:

  • JavaScript各种概念都得了解,《JavaScript语言精粹》这本书的目录都得有概念,并且这些核心点都能脱口而出是什么。这里列举一些做参考:
  • 知道组合寄生继承,知道class继承。
  • 知道怎么创建类function + class。
  • 知道闭包在实际场景中怎么用,常见的坑。
  • 知道模块是什么,怎么用。
  • 知道event loop是什么,能举例说明event loop怎么影响平时的编码。
  • 掌握基础数据结构,比如堆、栈、树,并了解这些数据结构计算机基础中的作用。
  • 知道ES6数组相关方法,比如forEach,map,reduce。

中级:

  • 知道class继承与组合寄生继承的差别,并能举例说明。
  • 知道event loop原理,知道宏微任务,并且能从个人理解层面说出为什么要区分。知道node和浏览器在实现loop时候的差别。
  • 能将继承、作用域、闭包、模块这些概念融汇贯通,并且结合实际例子说明这几个概念怎样结合在一起。
  • 能脱口而出2种以上设计模式的核心思想,并结合js语言特性举例或口喷基础实现。
  • 掌握一些基础算法核心思想或简单算法问题,比如排序,大数相加。
2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。

初级:

  • 知道webpack,rollup以及他们适用的场景。
  • 知道webpack v4和v3的区别。
  • 脱口而出webpack基础配置。
  • 知道webpack打包结果的代码结构和执行流程,知道index.js,runtime.js是干嘛的。
  • 知道amd,cmd,commonjs,es module分别是什么。
  • 知道所有模块化标准定义一个模块怎么写。给出2个文件,能口喷一段代码完成模块打包和执行的核心逻辑。

中级:

  • 知道webpack打包链路,知道plugin生命周期,知道怎么写一个plugin和loader。
  • 知道常见loader做了什么事情,能几句话说明白,比如babel-loader,vue-loader。
  • 能结合性能优化聊webpack配置怎么做,能清楚说明白核心要点有哪些,并说明解决什么问题,需要哪些外部依赖,比如cdn,接入层等。
  • 了解异步模块加载的实现原理,能口喷代码实现核心逻辑。

高级:

  • 能设计出或具体说明白团队研发基础设施。具体包括但不限于:
  • 项目脚手架搭建,及如何以工具形态共享。
  • 团队eslint规范如何设计,及如何统一更新。
  • 工具化打包发布流程,包括本地调试、云构建、线上发布体系、一键部署能力。同时,方案不仅限于前端工程部分,包含相关服务端基础设施,比如cdn服务搭建,接入层缓存方案设计,域名管控等。
  • 客户端缓存及预加载方案。
3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。

初级:

  • 知道react常见优化方案,脱口而出常用生命周期,知道他们是干什么的。
  • 知道react大致实现思路,能对比react和js控制原生dom的差异,能口喷一个简化版的react。
  • 知道diff算法大致实现思路。
  • 对state和props有自己的使用心得,结合受控组件、hoc等特性描述,需要说明各种方案的适用场景。
  • 以上几点react替换为vue或angular同样适用。

中级:

  • 能说明白为什么要实现fiber,以及可能带来的坑。
  • 能说明白为什么要实现hook。
  • 能说明白为什么要用immutable,以及用或者不用的考虑。
  • 知道react不常用的特性,比如context,portal。
  • 能用自己的理解说明白react like框架的本质,能说明白如何让这些框架共存。

高级:

  • 能设计出框架无关的技术架构。包括但不限于:
  • 说明如何解决可能存在的冲突问题,需要结合实际案例。
  • 能说明架构分层逻辑、各层的核心模块,以及核心模块要解决的问题。能结合实际场景例举一些坑或者优雅的处理方案则更佳。
4.熟练掌握react生态常用工具,redux/react-router等。

初级:

  • 知道react-router,redux,redux-thunk,react-redux,immutable,antd或同级别社区组件库。
  • 知道vue和angular对应全家桶分别有哪些。
  • 知道浏览器react相关插件有什么,怎么用。
  • 知道react-router v3/v4的差异。
  • 知道antd组件化设计思路。
  • 知道thunk干嘛用的,怎么实现的。

中级:

  • 看过全家桶源码,不要求每行都看,但是知道核心实现原理和底层依赖。能口喷几行关键代码把对应类库实现即达标。
  • 能从数据驱动角度透彻的说明白redux,能够口喷原生js和redux结合要怎么做。
  • 能结合redux,vuex,mobx等数据流谈谈自己对vue和react的异同。

高级:

  • 有基于全家桶构建复杂应用的经验,比如最近很火的微前端和这些类库结合的时候要注意什么,会有什么坑,怎么解决
5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。

初级:

  • HTML方面包括但不限于:语义化标签,history api,storage,ajax2.0等。
  • CSS方面包括但不限于:文档流,重绘重排,flex,BFC,IFC,before/after,动画,keyframe,画三角,优先级矩阵等。
  • 知道axios或同级别网络请求库,知道axios的核心功能。
  • 能口喷xhr用法,知道网络请求相关技术和技术底层,包括但不限于:content-type,不同type的作用;restful设计理念;cors处理方案,以及浏览器和服务端执行流程;口喷文件上传实现;
  • 知道如何完成登陆模块,包括但不限于:登陆表单如何实现;cookie登录态维护方案;token base登录态方案;session概念;

中级:

  • HTML方面能够结合各个浏览器api描述常用类库的实现。
  • css方面能够结合各个概念,说明白网上那些hack方案或优化方案的原理。
  • 能说明白接口请求的前后端整体架构和流程,包括:业务代码,浏览器原理,http协议,服务端接入层,rpc服务调用,负载均衡。
  • 知道websocket用法,包括但不限于:鉴权,房间分配,心跳机制,重连方案等。
  • 知道pc端与移动端登录态维护方案,知道token base登录态实现细节,知道服务端session控制实现,关键字:refresh token。
  • 知道oauth2.0轻量与完整实现原理。
  • 知道移动端api请求与socket如何通过native发送,知道如何与native进行数据交互,知道ios与安卓jsbridge实现原理。

高级:

  • 知道移动端webview和基础能力,包括但不限于:iOS端uiwebview与wkwebview差异;webview资源加载优化方案;webview池管理方案;native路由等。
  • 登陆抽象层,能够给出完整的前后端对用户体系的整体技术架构设计,满足多业务形态用户体系统一。考虑跨域名、多组织架构、跨端、用户态开放等场景。
  • mock方案,能够设计出满足各种场景需要的mock数据方案,同时能说出对前后端分离的理解。考虑mock方案的通用性、场景覆盖度,以及代码或工程侵入程度。
  • 埋点方案,能够说明白前端埋点方案技术底层实现,以及技术选型原理。能够设计出基于埋点的数据采集和分析方案,关键字包括:分桶策略,采样率,时序性,数据仓库,数据清洗等。
6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。

初级:

  • 知道eslint,以及如何与工程配合使用。
  • 了解近3年前端较重要的更新事件。
  • 面试过程中遇到答不出来的问题,能从逻辑分析上给出大致的思考路径。
  • 知道几个热门的国内外前端技术网站,同时能例举几个面试过程中的核心点是从哪里看到的。

高级:

  • 在团队内推行eslint,并给出工程化解决方案。
  • 面试过程思路清晰,面试官给出关键字,能够快速反应出相关的技术要点,但是也要避免滔滔不绝,说一堆无关紧要的东西。举例来说,当时勾股老师面试我的时候,问了我一个左图右文的布局做法,我的回答是:我自己总结过7种方案,其中比较好用的是基于BFC的,float的以及flex的三种。之后把关键css口喷了一下,然后css就面完了。
7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。
  • 根据了解的深度分初/中/高级。
  • 知道TS是什么,为什么要用TS,有TS工程化实践经验。
  • 知道移动端前端常见问题,包括但不限于:rem + 1px方案;预加载;jsbridge原理等。
  • 能说出大概的服务端技术,包括但不限于:docker;k8s;rpc原理;中后台架构分层;缓存处理;分布式;响应式编程等。

JD的要求很难吗?

首先,感谢你能看到这里,如果你是仔细看的,那么我更加感动了。而且你已经用实际行动,证明了你的学习能力和耐心。上面那么大篇幅的JD翻译,有一个问题,大家应该都有答案了:为什么职位描述看着简单,面试却这么难呢?然而,有些同学可能会嘲讽起来:写了那么多,我认识的有些阿里P6,P7也不是都会啊,大厂都是螺丝钉,也就面试时候问问,实际工作不还是if else,何况我又遇不到这些场景,我怎么可能知道。

在这里,我想严肃的说明的是:

  1. 我所认识的淘宝前端,以及我所在团队的P6同学,上面初级都能做到,中级至少覆盖60%,高级覆盖20%;P6+同学,中级覆盖80%以上,高级覆盖50%以上;P7同学高级覆盖80%以上。
  2. 我们团队的前端,每一个人都负责多个复杂业务项目(客观数据上:至少对接20+服务端接口,5个以上router配置,涉及多个用户角色的综合业务系统),以及一些通用能力,比如组件库等。不存在一个人只接一条业务线,只负责维护某几个组件这种螺丝钉式的工作。我不知道大厂都是螺丝钉的言论为什么会被复用到互联网企业,我个人感受是,如果我在阿里的工作是螺丝钉,那么我以前几份工作可能勉强算是螺纹。另外,如果你想要晋升,那么维护好这几个业务系统只是你的本职工作,晋升时请提供一些更高层面的思考和技术产出。
  3. if else也分鲜花和牛粪。有的人写的是[].reduce,而有的人写的是var temp = ''; for() { temp += 'xxx' }。另外,如果不知道原理,那么类似webpack这种明星级的技术产品,将永远与你无缘。冷静下来想想,webpack难道也只是if else吗?是,又不全是。

聪明的你应该看出来了,上面JD翻译里的初级、中级和高级,对应的就是我认为的,阿里p6/p6+/p7的能力标准,同时也是一张知识图谱。初级的要求更偏实际应用和基础原理,中级的要求是基于原理的拓展和复杂技术架构的应用,高级的要求是对跨栈、跨端,多领域结合产出综合方案的能力。而且,我们对技术的要求,都是能够与实际业务场景结合,或者能对提升工作效率有帮助的。空谈和尬想,或者只是百度来的文章,没有经过内化,那么面试过程中将被瞬间拆穿。

有时我会在boss直聘上直接打字面试,有时我也会听到面试过程中,电话那头传来键盘敲击的声音,甚至有时候我会主动让面试的同学去百度一下,或者挂电话思考一下,过15分钟再聊。我敢这么面试,因为我知道,我要的答案你查不出来,我看的是你真正理解的东西。能搜索到的,我不在乎,我也希望你去查,来为你更好的表现综合能力。

破局的方法

好了,如果看到这里,并没有把你劝退的话,那么让我们来点希望的曙光。这里用一句阿里土话来给大家一些安慰:不难,要你干嘛?

开篇我提到面试过那么多同学之后,我最大的感受是惋惜,因为有很多同学在我看来就差一点点,他有足够的个人能力,可能只是没有找到感觉。这里我例举两个比较典型的问题。

什么是亮点?

我相信这是很多同学心中的疑惑,而且事实上,我看到很多简历下面的面试记录都会写:缺乏亮点,暂不考虑。如果仔细看了上文,到这里还有这个疑惑,那么我觉得你需要再静下心来感受一下。

这里我不对亮点做明确的表述,我举一个例子来让大家更有体感一些:

A: 负责公司前端工作,使用webpack打包代码并发布线上。使用webpack配置对整体性能做优化,用happypack加快了打包速度。

B: 建设内部云构建体系,产出通用命令行指令工具;将发布、环境切换、快速回滚能力平台化,保证了线上环境稳定性;同时将研发流程量化管控,每周产出研发效能报告。

如果你是面试官,在简历的大海里看一个项目描述,什么最吸引你的眼球呢?是webpack,happypack的关键字吗?还是一句话就让你想到这件事的复杂性,和这个系统带来的巨大价值?

没有场景怎么办?

这也是很多同学经常遇到的问题。上面例举了那么多技术点,而我在的环境,前端就我一个,甚至服务端我都要写一点,哪有精力去搞这种大规模团队用到的东西?

首先,时间靠自己合理规划。我和老婆两个人自己带孩子,有两个娃,每天平均9点下班,我每天回家收拾玩具,孩子睡得晚可能需要再陪玩一下,周末我带孩子为主,但是我去年仍然白金了2个ps4的游戏。

在时间问题排除之后,我建议分三个阶段:

  1. 毕业3年以内的阶段:不用着急,你的选择很多,你可以核对上面初级的点,看自己是否都做到了,没做到就去好好学习吧,初级的技术要点对团队规模没有依赖,一个人也能做到极致。如果你所处的环境已经有2个人,可以同时关注中级和高级的点,不要觉得人少就不去尝试,放手去做,过程中会有实打实的收获。
  2. 毕业5年以内的阶段:不论你处的环境团队规模如何,请开始着眼于中级和高级相关能力,人少就不需要研发提效了吗?我在segmentFault上发的第一篇文章,是如何用travis和github做一键部署,那时候我还没有去淘宝,我所在的团队也没有用到这个能力,这篇文章是我自己的个人项目用到的。而整个过程同样涉及到了研发效能的方方面面。
  3. 毕业8年以内的阶段:请开始着眼于高级相关的技术方案产出。我以组件动态化为例,我早年维护手机淘宝的整个交易链路H5页面,所有页面的ui部分都是细粒度组件化抽离,通过配置下发页面结构的。即使一个人维护一个页面,也要竭尽所能去思考好的技术方案。这种高度动态的设计,带来的好处是,每年双十一,80%的需求交给pd自己处理就行了,剩下流转到我手上需要开发的需求,都是新增交互,或者之前抽象不足的组件。所以当时我在的团队,3个人在维护了包括手淘首页、商品详情和正逆向交易链路所有H5页面,同时还有额外精力去支持大促会场页。更好的技术思考和设计,一定能给你带来更多的可能性,而系统的优雅程度,一定不是靠业务代码的堆砌,而是作为技术核心的你,如何去思考。

我想怎么帮你

我相信每个人都是能快速成长的,只是每个人缺少的东西不同。有的人少了些脚踏实地,有的人少了些登高望远的机会,更多的人或许只是没有找到那条正确的路。

我希望这篇文章能够帮助到正在前端领域努力的人,也希望这一篇文章就能成为指路明灯之一。但同时我也深知,每个人都是不一样的,所以,我这里留下联系方式,
需要的同学可以加微信:vianvio
编组.png
加备注:前端同路人。我可以给你做模拟面试,同时给出我认为的,适合你的发展思路和建议,当然也可以帮你内推。

另外,目前我们成立了一个模拟面试群,有定期活动,可以参考 https://github.com/vianvio/FE...
欢迎有兴趣的同学来参加。

介绍一下我所在的团队

我在阿里巴巴淘宝技术部-ihome业务。目前,ihome正在深耕家居家装行业,纵向深入行业内部,希望能给行业带来一些创新。目前可对外公开的产品和业务形态有:躺平App、位于青岛和宁波的桔至生活门店。我们还有更多有趣、充满挑战和超出你想象的业务。我们期待有志之士的加入!

如果你愿意来和我们一起相信,那请发送简历过来,我们一定会一起看见!

前端简历请发送到:yefei.niuyf@alibaba-inc.com 或 lijie.slj@alibaba-inc.com
主攻3d方向的同学,简历请发送到:jiangcheng.wxd@alibaba-inc.com
java简历请发送到:xiaoxian.zzy@taobao.com 或 wuxin.sn@taobao.com
客户端简历请发送到:fangying.fy@alibaba-inc.com

或许有人会觉得奇怪,联系方式写在最后,还有多少人能看到,这里我引用马爸爸和逍遥子大佬对阿里价值观的解读,来解释一下:我们的价值观是为了帮助我们寻找同路的人。

感谢你陪我一起走到这篇文章的最后,如果你觉得这篇文章已经对你有很大帮助了,那就请我喝杯咖啡吧~

查看原文

赞 245 收藏 149 评论 25

Nodreame 发布了文章 · 2月24日

「前端考古系列」一个需求引发的前端模块化考古

零、故事的开始

从前有个流行说法是"全国 13 亿人,每人给我一块钱我就是亿万富翁"

现在老板觉得这个主意很棒,所以让张三来做个网页方便收钱,界面简单点如下所示就好~

image-20210131002533398

可以看到这里就两个逻辑,点击红色按钮开始打钱,点击蓝色链接触发举报。是不是很简单~

这时老板跟张三说:"唔使急,最紧要快~ 5分钟后我要看到这个网页",这时候的张三的情绪毫无波动,什么软件工程可维护性模块化直接抛之脑后,满脑子只剩下一句"老夫写代码就是一把梭".

1473308168_167070

一、最初的面条代码

五分钟后张三写完了代码直接 scp 传到了公司服务器某现有全静态前端项目的目录下,把链接发给老板后长出了一口气...

现在的代码大概是这样的:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>亿万富翁计划</title>
</head>
<body style="text-align: center;">
    <h1>帮助 Nodreame 成为亿万富翁</h1>
    <div><button id="pay">话不多说直接打钱</button></div>
    <div><a href="#" id="inform">举报这个帅逼</a></div>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
   <script data-original="微信支付脚本.js"></script>
    <script> var payElem = document.getElementById('pay')
        var informElem = document.getElementById('inform')
        
        var payParams = {} // 支付参数
        var payLogic = _.debounce(function () {
            // 1. 确定支付方式(暂时只支持微信,且以下支付流程仅供学习使用)
            // 2. 确定金额等支付参数
            // 3. 调起微信支付, 等待回调
            // 4. 根据回调结果进入重新支付流程 or 支付完成感谢界面
        }, 200)
        var informLogic = _.debounce(function () {
            // 举报逻辑,PM说直接稍后弹窗即可无需写功能逻辑
            alert('感谢您的举报,处理结果将在7~15个工作日内发送至您的手机')
        }, 400)
        payElem.onclick = payLogic
        informElem.onclick = informLogic </script>
</body>
</html> 

现在的支付逻辑函数 payLogic 看起来似乎还过得去,但是现在老板不满意了:只支持微信支付明显是不够的,要是有土豪就是想要用支付宝、网银、paypal甚至比特币打钱怎么办?

image-20210131013238793

没办法,和老板对线不是明智之举,只能肝上去继续一把梭了,然后代码就变成了这样:

<script data-original="微信支付库.js"></script>
<script data-original="支付宝支付库.js"></script>
<script data-original="paypal支付库.js"></script>
...
<script> var payParams_wx = {} // 微信支付参数
  var payParams_zfb = {} // 支付宝支付参数
  var payParams_pp = {} // Paypal支付参数
  
  var payLogic = _.debounce(function () {
    // 0. 确定支付方式
    if (微信支付) {
      // 1. 确定金额等支付参数
      // 2. 调起微信支付, 等待回调
      // 3. 根据回调结果进入重新支付流程 or 支付完成感谢界面
    } else if (支付宝) {
      // 1. 确定金额等支付参数
      // 2. 调起支付宝支付, 等待回调
      // 3. 根据回调结果进入重新支付流程 or 支付完成感谢界面
    } else if (paypal) {
      // 1. 确定金额等支付参数
      // 2. 调起paypal支付, 等待回调
      // 3. 根据回调结果进入重新支付流程 or 支付完成感谢界面
    } 
    ...
  }, 200) </script> 

随着支付方式的增加,引入的脚本、支付参数、支付逻辑也随之增加. 这个为小需求而生的网页开始变得复杂.

张三想起上次临时接收"祖传屎山"的通宵分析代码经历,决定为了方便以后对项目的维护,现在尝试一下对项目进行一些优化.

当下最重要的当然是分析一下项目当前存在的问题:

  • 无私有空间:各支付渠道的支付参数只需要在模块内可以访问即可;
  • 全局变量污染:不应该将操作接口暴露到全局;
  • 依赖管理:支付逻辑没有显式标记对应的依赖

OK,就从这三个点的优化开始吧~

二、模块化意识的觉醒

由于每个支付渠道都有对应支付参数和支付流程逻辑,所以第一个想到的是可以将不同的逻辑拆分到不同的文件中:

截屏2021-01-31_上午2_12_45

如果是 Java 或者 C# 这个写法确实是能解决 全局变量污染 & 无私有空间 的问题(class 包裹),但是在 JS 中使用上面的分文件写法其实完全没有解决任何问题,即使将支付参数、逻辑函数分到不同文件,它们依旧会被暴露到全局变量中.

为了解决 全局变量污染 & 无私有空间 的问题,有前辈提出了原生解法 -- 利用立即执行函数实现"伪模块". 这样外部就无法访问支付参数,所以 无私有空间 问题就此解决,全局变量污染问题 得到了一部分解决(参数不再暴露,方法依旧挂载到全局):

image-20210131022450619

另外如果对立即执行函数传入 依赖 作为参数(例如 lodash),那么"伪模块"就可以"显式"地依赖某个库了:

截屏2021-01-31_上午2_37_52

但是很明显的,这个"显式依赖"并没有真正解决了依赖管理的问题.

当前 index.html 引入lodash,而"伪模块"文件中无需引用就直接使用了 lodash,那么如果在 index.html 中删除了对 lodash 的引用,那么"伪模块"逻辑的执行必定报错. 故这里的问题在于:未在调用处显式声明依赖项.

为了更好的解决这些问题,张三决定查看到技术社区找找可选方案.

三、了解社区规范

张三到技术论坛看了一圈,了解到了一堆社区方案 CommonJS、AMD、CMD、UMD,决定逐个了解一下再做决定.

1. CommonJS规范

CommonJS规范是 NodeJS 实现模块化参考的标准,看看其模块定义 & 加载的写法:

image-20210131182742977

NodeJS 中一般通过 module.exports 或者 exports 定义模块,再通过 require 加载模块.

由于其模块加载的设计是 同步 的,这对服务端从内存或者硬盘读取模块并无影响,但对于需要通过网络异步下载模块的浏览器端就不太适用了(在网络加载较慢的情况下,模块加载速度过慢会导致长时间白屏)

为了借鉴 CommonJS 的思想来解决浏览器端的模块化问题,大神们提出了 AMD 和 CMD 这两个 "异步加载模块" 的浏览器模块规范. 两者分别是 RequireJS 和 SeaJS 在推广过程中对模块定义的规范化产出.

2. AMD规范

1)概述

  • AMD(Asynchronous Module Definition) 即异步模块定义,是为了解决浏览器端模块化问题提出的规范.
  • 实现库:require.js
  • 主要 API:模块定义 define & 模块加载 require
  • 特点:推崇依赖"依赖前置"

2)实践

张三看完文档和教程后觉得方案可行,于是将支付模块基于 AMD 规范重构了,最新文件目录如下(pay/lib 为第三方提供的支付库):

image-20210201022444719

将原本的 js 逻辑移入 main.js 中,html 中只留下 require.js 的引入(requirejs会自动完成 main.js 的加载):

<script data-main="./main" data-original="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.min.js"></script> 

然后在 main.js 中配置路径(方便引入依赖) & 通过 require(依赖数组,回调函数)的方式写入原先的逻辑,并将支付相关逻辑放入对应的文件并根据 AMD 规范完成定义:

image-20210201022133266

从上面的 define 和 require 可以看出,AMD规范推崇"依赖前置",也就是定义模块 & 编写逻辑前先声明依赖的模块.

完成后刷新页面,可以看到 wx.js 的回调函数中的打印语句已经执行,说明AMD规范的 "依赖前置"会使依赖提前加载并执行其回调函数:

image-20210201022708486

3)小结

AMD 规范通过 define & require 实现了模块的定义和引用,解决了全局污染和私有性的问题.

同时也以"依赖前置"的形式实现了对模块的显式管理,但是写法是相对繁琐的.

3. CMD规范

1)概述

  • CMD(Common Module Definition)即通用模块定义,和 AMD 的目标相似,不过推崇的理念和模块处理的方法与 AMD 不同.
  • 实现库:sea.js
  • 主要 API:模块定义 define & 模块加载 require & use
  • 特点:推崇依赖就近 & 懒执行

2)写法

sea.js 的写法其实和 require.js 的写法很相似所以就不重写了,官方给出了下面的代码:

define(function(require, exports) {
  var a = require('./a'); // 获取模块 a 的接口, 就近书写
  a.doSomething();
}); 

从上面可以看出 CMD 推崇的写法不同于 AMD 的依赖前置,而是在使用到某模块功能附近才对模块进行 require 引用,称为"依赖就近".

CMD define方法里的函数被称为 factory ,包含三个参数 require, exports, modules,用于在 factory 中引用模块和暴露接口.

4. UMD

UMD 全称 Universal Module Definition,见名知义,该规范的目标就是平台通用.

当需要同时支持浏览器端和 NodeJS 端使用的时候一般会选用 UMD 规范打包的代码,例如 Vue:

image-20210201182209023

UMD 规范的代码特点是会通过一些对于 exports 和 define 的判断确定环境,例如 vue.js 中的:

image-20210201182504976

社区方案的提出就是在 JS尚未支持模块化的时期解决模块化问题,虽然够用但是还是略显繁琐,这时张三想起上次去某网站下软件导致整个电脑都是大天使之剑后来养成了凡是信官网的习惯,所以决定还是先看看官方模块化方案再做决定.

四、官逼同

image-20210201183224816

TC39 将 Modules 加入到 ECMAScript 2015 中,将文件区分成 脚本 Scripts & 模块Modules,使用 import & export 实现模块的导入导出,用起来也比 AMD/CMD 直观,ES Module 成为浏览器和服务器通用的模块解决方案。

张三看了ES6文档和大神文章,觉得这个由官方规定的标准肯定是以后的大趋势,以后现在学一波顺便在项目中练练手肯定不亏,于是马上行动了起来.

image-20210201215851498

一切都是那么顺畅美好,并且所有模块都能成功加载,只是在浏览器打开 index.html 运行时出现一点错误:

image-20210203010956967

这是在 index.html 中的 <script> 使用 type="module" 导致的,这里的 CORS 策略不支持 file 协议,所以要在本地起一个服务器:

yarn global add http-server
http-server 

使用 http://127.0.0.1:8080 尝试访问网页:

image-20210201203526947

这个错误已经写得很明显了,就是 lodash 文件不是使用 ES Module 的模式暴露导致的. 这里使用 lodash-es 的CDN来替换即可:

import * as _ from 'https://cdn.jsdelivr.net/npm/lodash-es@4.17.20/lodash.min.js' 

OK现在页面已经能够成功加载,点击支付按钮也能成功触发打印如下:

image-20210201220158387

张三伸了个懒腰长出一口气,这波终于用最新的官方方式实现了前端模块化了,赶紧和老板邀邀功~

五、前端构建

听到张三主动为项目做优化老板频频点头,路过的 Leader 却发现了不对的地方,把他拉到一旁问项目的设备兼容性处理是怎么做的

这时候的张三心里咯噔一下,完蛋没考虑 ES6 的浏览器兼容性!流下冷汗的同时甚至有点想提桶跑路...

image-20210201220934204

Leader 帮张三看了一下代码,告诉他可以用 Webpack + Babel 处理一下项目,即可以用最新语法快乐编码又可以防止低版本浏览器出现不兼容的问题. 时间紧急,Leader 就直接告诉张三 Webpack 和 Babel 的知识,让他听完之后再自己去试试.

1. webpack

之前的 AMD/CMD 方案都是需要浏览器在执行主逻辑前先下载一个 require.js 或者 sea.js,之后再运行主逻辑代码的,这两种方案对于模块依赖关系的解析都是在运行时做的,这样随着项目变大加载速度也会随之变慢,如果没上 HTTP/2.0 的话模块文件过多也会导致加载变慢.

为了解决这个问题,官方设计 ES6 的时候是尽量往静态化的方向靠拢的,这样做的好处是如果能在编译期间完成依赖关系的分析,那么就可以在本地提前做好各种优化,运行时只需直接执行代码即可 (理解这个概念可以参考一下 Java、C++直接编译出可执行文件,想用时只需运行这个可执行文件即可)

既然是往静态化和预编译的方向靠,那么就需要有工具来辅助完成依赖关系管理和优化这件事,webpack 就是众多构建工具中脱颖而出的一个,它专注于打包,并且通过各种loader和插件为开发者提供了更多更强的能力.

2. Babel

Babel 是一个 JS 编译器,JS 标准不断更新,每年都有更新更好的语法和能力供开发者使用,但是如果直接按照最新规范给定的方式编码的话,很多版本稍低的浏览器都会出现兼容性问题,为了解决这个问题就有了Babel.

Babel 让开发者在编码过程中可以使用更加简洁高效的新语法,在发布打包的时候通过转移来生成兼容性更高的执行代码. 其过程如下:

  • 解析:开发者编写的代码经过解析转化为 ES6+ AST(抽象语法树)
  • 转译:ES6+ AST 通过插件转译为兼容性更高的 ES5 AST
  • 生成:基于 ES5 AST 生成兼容性高的最终代码

在实际项目中可以结合 webpack + Babel 构建简单项目打包,便捷开发的同时也能实现较好的兼容性支持.

3. webpack 打包实战

张三听完 Leader 的话觉得自己又行了,马上开始边查资料边实践~

首先是 webpack 的安装和现有代码的稍微调整:

yarn init -y
yarn add -D webpack webpack-cli html-webpack-plugin clean-webpack-plugin
yarn add lodash 

接下来对现有项目做一些微调:

  • index.html 删除引用 main.js 的 script 标签
  • js 文件中的 lodash 引用改为 import _ from 'lodash' 即可(webpack 具备将 CommonJS 模块编译成 ES Module 的能力)
  • 编写 webpack.config.js 文件如下:

    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const { CleanWebpackPlugin } = require('clean-webpack-plugin');
      
      
    module.exports = {
      mode: 'production',
      entry: './main.js',
      output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
      },
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          template: 'index.html'
        }),
      ],
    }; 

OK,至此项目微调已经完成,使用 npx webpack 即可完成打包.

image-20210202003906045

现在可以直接打开编译的 index.html 文件预览页面了,经过测试"支付"功能也能正常使用~

image-20210202004018814

4. webpack + Babel7.x 实战

上面使用 webpack 实现了项目打包,接下来使用 Babel 需要先安装一下依赖:

yarn add -D babel-loader @babel/core @babel/preset-env @babel/plugin-transform-arrow-functions
yarn add @babel/polyfill 

这里逐个解释下依赖:

  • babel-loader:js 代码的预处理器,用 webpack+babel 打包必备.
  • @babel/core:babel 核心库,必备.
  • @babel/preset-env:在.babelrc 中接收配置 target 和 useBuiltIns,用于确定目标浏览器版本和 polyfill 的需求.
  • @babel/polyfill:用于目标环境中添加缺失的特性(虽然 7.4.0 之后不推荐使用但是为不引入过多概念故暂时使用)

接下来在 webpack.config.js 中配置上 babel-loader 用以处理 JS 文件:

截屏2021-02-02_下午11_48_59

然后在根目录创建 .babelrc 文件存放 babel 的配置:

{
    "presets": [
        [
            "@babel/env",
            {
                "targets": { // 目标平台
                    "edge": "17",
                    "firefox": "60",
                    "chrome": "67",
                    "safari": "11.1",
                },
                // usage 表示根据 target 自动确定需要 polyfill 的功能
                "useBuiltIns": "usage",
            }
        ]
    ],
    "plugins": [
        "@babel/plugin-transform-arrow-functions",
    ],
} 

为了方便测试是否 Babel 是否真的生效,在 main.js 末尾中加入两行代码方便对比打包结果:

[1, 2, 3].map((n) => n + 1);
Promise.resolve().finally(); 

最后用 npx webpack 命令打包即可,查看结果如下:

image-20210203000802243

OK,刚刚添加的箭头函数已经转换为普通函数,finally 也能搜索到两个结果,没展示出来的第一个 finnally 应该就是其对应的 polyfill 了.

这样通过这样的终于粗略解决了兼容性问题,张三再次长出了一口气...

Ending

问题搞定!老板表示十分满意,口头表扬了张三一番,张三也开始幻想起自己升职加薪的样子...

夜深了,就像张三对前端模块化的了解一样更深了...

欢迎拍砖,觉得还行也欢迎点赞收藏~ 新开公号:「无梦的冒险谭」欢迎关注(搜索 Nodreame 也可以~) 旅程正在继续 ✿✿ヽ(°▽°)ノ✿
查看原文

赞 2 收藏 0 评论 2

Nodreame 赞了文章 · 2月22日

前端,社招,面淘宝,指南

背景

最近淘系开启社招,我联系了很多的同学,了解了大家一些面大厂的经历后,我觉得大家对于面试大厂是不是有点随意……

面试这种东西并不是投个简历,等着被面就好了,是要做很多准备的,这个准备不只是题目和项目上的准备,心理和意识上的准备同样重要。

不过因为我个人也只在淘宝这个大厂呆过,所以我根据自己的经历,讲讲面试淘宝这种大厂有哪些要注意的点?

为了让内容更加丰富,我还参考了内网多篇面试官的思考总结,整理成这篇文章。

用面试官的思考总结也可以让大家知道面试官是怎么考察大家的,从而反向要求自己。

硬性条件

像淘系这样的大厂部门,为了降低面试成本,其实是有一些相对基础严格的筛选条件的,这些我们统称为硬性条件,它决定了你的简历是否能通过评估,进入到面试环节。不过硬性条件并不是一定要遵守的,但如果你有硬伤,其他方面的要求相对会高很多。这种最常见的情况就是比如你学历偏低,技术就要非常出色……

学历

学历重要吗?当然重要啦,个人认为好的学历代表了你的学习能力,它是用来证明学习能力的,企业要一个人,无法花太多时间去了解你,只能根据你能拿出来的证明材料来证明可以用你这个人,那学历就是第一选择。本科算是目前比较基础的学历要求。

工作年限

工作年限是从你毕业开始算起,不是从你开始做前端相关的职位开始算起。而工作年限决定了你应聘前端的评判标准。

说起评判标准,我们可以从阿里的职业序列开始说起。像技术走的是 P 序列:

或许你比较好奇每个技术序列的标准,我直接网上给你找了一段:

P5

  1. 在专业领域中,对公司职位的标准要求、政策、流程等从业所必需了解的知识基本了解,对于本岗位的任务和产出很了解,能独立完成复杂任务,能够发现并解决问题;
  2. 在项目当中可以作为独立的项目组成员;
  3. 能在跨部门协作中沟通清楚。

P6

  1. 在专业领域中,对公司职位的标准要求、政策、流程等从业所必需了解的知识理解深刻,能够和经理一起探讨本岗位的产出和任务,并对经理具备一定的影响力;
  2. 对于复杂问题的解决有自己的见解,对于问题的识别、优先级分配有见解,善于寻求资源解决问题;也常常因为对于工作的熟练而有创新的办法,表现出解决复杂问题的能力;
  3. 可独立领导跨部门的项目;在专业方面能够培训和教导新进员工。

P7

  1. 在专业领域,对自己所从事的职业具备一定的前瞻性的了解,在某个方面独到,对公司关于此方面的技术或管理产生影响;
  2. 对于复杂问题的解决有自己的见解,对于问题的识别、优先级分配见解尤其有影响力,善于寻求资源解决问题;也常常因为对于工作的熟练而有创新的办法,表现出解决问题的能力;
  3. 可独立领导跨部门的项目;能够培训和教导新进员工;
  4. 是专业领域的资深人士;
  5. 行业外或公司内培养周期较长。

换一种好理解的说法就是,应届生进来是 P5,社招一般 P6 起,对应工作年限大致从 2 年 到 5 年左右,P7 对应的工作年限大致从 5 年 到 8 年。

比如你 2010 年毕业,做了 3 年后端,然后做了 4 年前端,因为从毕业开始算起,工作 7 年,所以是以 P7(技术专家)的标准来评估你的。

这也就是意味着,工作 2 年到 5 年的同学,如果想来淘宝,请赶紧来,过了 5 年,对你的评估标准又会高一级。

跳槽次数

公司会对跳槽次数有一定的要求,这主要是因为频繁的跳槽必定会导致沉淀偏少,因为到一个新的环境是需要时间沉淀的,根据以往的经验来看,5 年内 3 跳就是就比较难接受了,除非你有出众的一面。

内推

如果你满足以上条件,你可以试着投递简历,尽管你有很多种方式可以投递简历,但我真的友情建议,找内推,不要直接就投过去。

找内推会有很多好处:

  1. 帮你内推的同学可以给你介绍团队架构以及负责的业务
  2. 帮你内推的同学才是真正和你站在一条线上的同学呀,找到一个好的内推同学,可以帮你看简历,了解下场面试官是谁,了解面试反馈,给你指导建议,这些都可能或多或少的帮助到你。
  3. 内推成功的话,他们是有内推奖金的,让他请你吃饭

你可能会想,去哪里找内推的同学呢,给大家分享一些技巧:

  1. 关注「淘系前端团队」这个公众号,作为淘系前端的官方公众号,推送的都是淘系同学的原创文章,内容你可能看不懂,但没有关系,重点是很多文章底下都有群、二维码、微信号、邮箱之类的,你完全可以根据自己感兴趣的方向,找相关的文章,然后联系到具体的同学,然后找他们看简历等,他们肯定会非常乐意的,毕竟他们留这些信息就是要招人的
  2. 关注一些你觉得不错的大佬,掘金、Github、知乎都行,招人的时候,他们会发布招聘信息,可以趁机加他们微信,方便交流和内推
  3. 找冴羽呀,微信号:「mqyqingfeng」。冴羽有什么不好的,不信你看这篇文章,《如何轻松拿到淘宝前端 offer》

简历

少写废话

专业技能

  1. 熟悉 React、vueJS 等主流 MVVM 框架
  2. 掌握 localStorage、sessionStorage 等前端存储方案,记录用户操作习惯或常用数据,从而降低 HTTP 请求,提升用户体验;
  3. 熟练使用 git 版本控制工具,进行代码管理,实现敏捷开发;
  4. 熟练使用 Ajax,实现异步页面无刷新抽取数据,同步校验数据; 熟练使用 jQuery 框架,实现页面的交互效果,提高用户体验;
  5. 熟练使用 HTML5 和 CSS3,实现应用一站式跨屏(PC 端与移动端)开发;
  6. 熟练使用 HTML、CSS、JavaScript 等前端技术,完成网页静态布局,开发兼容主流浏览器的页面,以及实现页面的交互,提升用户体验;

责任描述

  1. 根据需求文档完成代码编写
  2. 根据公司规范进行编码,并对开发的代码进行单元测试
  3. 配合测试部门进行测试联调工作,并修复 bug
  4. 维护项目保持该项目的正常运行

自我评价

  1. 具备团队合作精神,人品正直,有责任心,抗压能力强
  2. 工作和学习中善于总结,对新技术有强烈的好奇心
  3. 工作尽职尽责,乐于接受挑战性的工作

切忌弄虚作假

工作项目经历弄虚作假的,一旦发现,肯定被拒,而面试官为了考察是否有弄虚作假,会针对重点的项目经历详细询问面试者,比说首先会让面试者讲述这个项目的背景、设计思路、实施方式等,并在讲述的过程中对某些细节进行追问,如果面试者难于说清楚、吞吞吐吐,那么这些经历大概就是面试者造假或者面试者拿团队的成果来充当自己的成果,这种面试者肯定会被拒。

面对面试官

我在内网看到了很多面试官的分享,他们面过上百人,总结了很多作为面试官的经验,看完这些经验,我不禁感慨,他们到底是“坑害”了多少面试者才总结出来的……

所以我总结一下这些点,希望当面试官做的不足的时候,能尽力的做出避免或者挽救,不要因为面试官经验上的不足而影响你的发挥。

突然袭击

有些面试官会直接电话过去要求面试,由于面试者(尤其是学生)处于弱势方,可能并不会拒绝面试要求,但并不意味着面试者处于最佳的物理和精神状态下,有可能会导致面试者不能很好的发挥自己原有的实力。所以如果面试官突然打电话过来,没有准备好的话,不要怕冒犯面试官,一定要另外约个时间,面试官肯定会再打过来的。

遭到否定

面试官有可能会否定你描述的一些东西,有可能是你字眼写的太过,比如将“熟悉”的东西写成了 “精通”,勾起了面试官的“好奇”,有可能故意试压,考验你的反应,但遇到否定的时候,不要急于反驳或者是手足无措,而是询问自省和主动讨论。

气场不和

有的面试官性格严谨沉闷,而面试者可能思维活跃,整个面试过程可能会变成面试官不停的打断面试者让其思维聚焦,而面试者感到思维不停被打断,很是气恼,影响发挥。这种情况不可避免,但遇到这样的面试官,请保持谦和的态度,重点是展示自己技术的广度、深度,气场不和并不会产生决定性的影响,面试官也不会因为这样的原因而给与错误的评判,尽量留下好的印象。

结束通知

有的面试官会在面试结束后说:“我的问题问完了,如果有进一步消息的话,我们会再联系你”。面试的大部分结果,其实当场就出来了,如果你感觉给出结果的时间点不够明确,可以反问他:“如果三天内没有联系,是不是就代表没有通过?” 省得让你怀有不确切的希望。最后,还可以挽留下面试官,讲讲这场面试中你的一些疑惑,获得面试官的反馈,也可以知道自己目前哪一方面做得不足。

面试

面对不会的问题

引用 《淘系前端校招负责人直播答疑文字实录》 中的回答就是:

面试中遇到问题不会回答,这是非常正常的,毕竟大家在学习过程中遇到的问题,跟我们在工作中遇到的问题是非常不一样的,除此之外,我们看问题的角度,也是有差别很大的。

举个例子,很多人都在看 React Fiber,但是如果问你们,为什么要有 Fiber 这个东西,可能很多人都回答不上来,因为大家只在社区里面看到说,Fiber 是怎么工作的啊,但是为什么要有 Fiber?如果没有会怎样,而这种思考,其实是非常底层的。我相信,很多人可能都没有思考过。

那如果遇到的问题不会,你可以先选择不会,说我觉得自己没有信心能答出这个问题,当然我更希望听到你说,我现在不会,但是你给我两分钟思考一下,我想一下这个问题有没有答案?我觉得这是一个很好的思考习惯,首先你在面对不会的东西是坦然的,我当下不会,很正常,我之前没有思考过这个问题嘛,那我们再花两分钟思考一下,给一个初步的答案。
所以说遇到问题,也不用不会就不会,也可以有一个积极的方式回应。

从面试官的角度来说,面试中考察的题目,并不是仅仅考察这道题目本身,题目的对错并不跟最终结果直接挂钩。面试官更希望以题目为契机, 考察面试者分析问题,解决问题的能力,以及交流过程中所体现的逻辑推理和思维方式等。

问题背后到底在问什么

这里我举一些常见的题目,从面试官的角度来说明背后的考察内容:

  1. 询问过去的工作中碰到过的一个项目或者解决过的问题

面试官的角度:

这类问题的主要目的是通过一个具体的案例考察候选人对于自己业务的熟悉和理解程度,以及碰到业务问题的时候是如何面对问题的。需要注意一定要尽可能按照 STAR(Situation, Task, Action, Result) 的技巧来组织对话,了解问题背后的信息,候选人本人的贡献,以及反映出来的能力。

对于候选人的回答,需要判断其语言的信息量,如果形容词比较多,比如做得很好,客户反应不错之类的回答,应该要求其给出明确的描述,到底好在什么地方,有没有明确的内容和数据,而不是含糊的形容一下。通常如果一个技术人员在得到明确提示情况下还是不能切换到陈述事实的方式,那么我们基本上可以认为这个人对于平常的工作是缺乏理性思考的。

  1. 关于 XXX 你怎么看?

面试官的角度:

这一类问题一般是没有明确的答案的,主要看候选人是否有自己的理解,比如可以问候选人对于一些常见的编程和软件工程理念的看法,来看看候选人平常对于这些问题是否有思考和总结。也可以是对于最近的一些技术热点的关注,等等。

作为面试官,应该引导候选人完成地表述自己的逻辑,了解其观点背后的内容,考察候选人对于概念的理解和实践的程度,看看候选人是否有比较严密的能够自圆其说的逻辑。

  1. 你的朋友或者同事是怎么评价你的?

面试官的角度:

这个问题可以考察候选人是不是一个自省的人,能否认识到自己的长处和短处。有的时候需要深挖。深挖的时候要让候选人描述一个能符合其观点的事实,这样才能正确评估候选人实际的状态。

  1. 最近在看什么书?

面试官的角度:

这个问题看起来简单,但是能够观察候选人的学习方式和习惯,除了让候选人报出书名,一定需要让他讲讲这本书里面让他印象深刻的章节,以及他从中学到了什么?

  1. 工作之余做什么事情?

面试官的角度:

这个问题是观察候选人是不是真的对技术有兴趣和热情的一个很好的方式,一个技术狂的业余生活也很有可能和技术有很大的关系,比如做些开源的软件,DIY 一些设备等等。

跟面试官聊业务

业务跟项目是完全不一样的东西,业务认知和思考也是很重要的一个考察点,这方面面试官可能会问两个点:业务场景和技术突破。

业务场景:面试官会询问面试者他们的产品、业务模式、商业模式等,了解下面试者对于产品的诞生、定位、发展的认知。

技术突破:技术与业务相结合这点在阿里也是一直在说但鲜有人做的非常好的地方,所以有的时候也会考察面试者,看看他们所做的技术是如何从业务出发、给业务带来了哪些改变以及如何去评估两者之间产生的结果。

面试结束

反向互动

面试官在挂断之前,基本会给与候选人反问的机会,一般会表述为 “你有什么要问我的吗”?无论面试的怎样,都希望你能说出心中的疑惑,比如面试中的某个题目、整体的评价反馈、你的建议或者了解所负责的业务,所在的团队等等,而从面试官的角度来看,如果候选人愿意反问你问题,至少能证明他是个好学的人。所以不要放弃这个互动的机会。

面试反馈

每场面试结束后,面试官都会在系统里留下面试反馈,如果面试失败,简历会放在人才库里,很多人会在其中捞取简历,以后的面试官也可以看到之前所有的面试记录,之所以讲到这一点,是希望大家在面试中能够保持谦和积极的心态,认真面对每一场面试,即使这场面试没有成功,但如果展示了好的品质,也会增加被发掘的机会,为你以后面试成功铺路。

保持联络

如果跟面试官聊的比较好,可以请求加个微信之类的,面试是一个长期的事情,哪怕此次面试没有通过,以后还可能面试进来的,与面试官保持联络,也是为以后进阿里铺路。

留下你的疑问

一篇文章也很难面面俱到,如果关于社招你还有什么疑问,可以在评论中留言,问题比较多的话,我会再写一篇答疑,希望能帮助大家。

最后打个广告

淘系前端-消费者平台团队招 Web 前端,大量HC,唤端、搭建、AB、互动、监控等多种技术领域,淘系电商重点业务,是支撑集团财报核心团队,招聘层级P6/P7/P8,工作经验2年起,杭州和北京都有空缺,也有前端TL的岗位,有意向的联系微信:wuxiaorui86

查看原文

赞 8 收藏 5 评论 0

Nodreame 关注了用户 · 2月22日

冴羽 @yayu

JavaScript深入系列 15 篇已完结。

JavaScript专题系列 20 篇已完结。

underscore 系列 8 篇已完结。

ES6 系列 20 篇已完结。

现在写起了各种单篇……

React 系列还在懒惰中……

公众号:冴羽的JavaScript博客

关注 2106

Nodreame 发布了文章 · 2月22日

axios 源码阅读(一)--探究基础能力的实现

axios 是一个通用的宝藏请求库,此次探究了 axios 中三个基础能力的实现,并将过程记录于此.

零. 前置

一. 目标

阅读源码肯定是带着问题来学习的,所以以下是本次源码阅读准备探究的问题:

  • Q1. 如何实现同时支持 axios(config)axios.get(config) 语法
  • Q2. 浏览器和 NodeJS 请求能力的兼容实现
  • Q3. 请求 & 响应拦截器实现

二. 项目结构

简单提炼下项目中比较重要的部分:

三. 从 axios 对象开始分析

当在项目中使用 axios 库时,总是要通过 axios 对象调用相关能力,在项目的 test/typescipt/axios.ts 中有比较清晰的测试用例(以下为简化掉 then 和 catch 后的代码):

axios(config)
axios.get('/user?id=12345')
axios.get('/user', { params: { id: 12345 } })
axios.head('/user')
axios.options('/user')
axios.delete('/user')
axios.post('/user', { foo: 'bar' })
axios.post('/user', { foo: 'bar' }, { headers: { 'X-FOO': 'bar' } })
axios.put('/user', { foo: 'bar' })
axios.patch('/user', { foo: 'bar' }) 

从上面代码可以看出 axios 库 同时支持 axios(config)axios.get(config) 语法, 并且支持多种请求方法;

接下来逐个分析每个能力的实现过程~

3.1. 弄清 axios 对象

同时支持 axios(config)axios.get(config) 语法,说明 axios 是个函数,同时也是一个具备方法属性的对象. 所以接下来我们来分析一下 axios 库暴露的 axios对象.

从项目根目录可以找到入口文件 index.js,其指向 lib 目录下的 axios.js, 这里做了三件事:

  • 1)使用工厂方法创建实例axios

    function createInstance(defaultConfig) {
      // 对 Axios 类传入配置对象得到实例,并作为 Axios.prototype.request 方法的上下文
      // 作用:支持通过 axios('https://xxx/xx') 语法实现请求
      var context = new Axios(defaultConfig);
      var instance = bind(Axios.prototype.request, context); // 手撸版本的 `Function.prototype.bind`
      
      // 为了实例具备 Axios 的能力,故将 Axios的原型 & Axios实例的属性 复制给 instance
      utils.extend(instance, Axios.prototype, context);
      utils.extend(instance, context);
      return instance; // instance 的真面目是 request 方法
    }
      
    var axios = createInstance(defaults); 
  • 2)给实例axios 挂载操作方法

    axios.Axios = Axios; // 可以通过实例访问 Axios 类
    axios.create = function create(instanceConfig) {
      // 使用新配置对象,通过工厂类获得一个新实例
      // 作用:如果项目中有固定的配置可以直接 cosnt newAxios = axios.create({xx: xx})
      //            然后使用 newAxios 按照 axios API 实现功能
      return createInstance(mergeConfig(axios.defaults, instanceConfig));
    };
      
    // 取消请求三件套
    axios.Cancel = require('./cancel/Cancel');
    axios.CancelToken = require('./cancel/CancelToken');
    axios.isCancel = require('./cancel/isCancel');
      
    axios.all = function all(promises) {
      return Promise.all(promises); // 相当直接,其实就是 Promise.all
    };
    axios.spread = require('./helpers/spread'); // `Function.prototype.apply` 语法糖
      
    // 判定是否为 createError 创建的 Error 对象
    axios.isAxiosError = require('./helpers/isAxiosError'); 
  • 3)暴露实例axios

    module.exports = axios;
    module.exports.default = axios; 

从以上分析可以得到结论,之所以能够 "同时支持 axios(config)axios.get(config) 语法" ,是因为:

  • 暴露的 axios 实例本来就是 request 函数,所以支持 axios(config) 语法
  • 工厂方法 createInstance 最终返回的对象,具备复制得的 Axios的原型 & Axios实例的属性,所以也能像 Axios 实例一样直接使用 axios.get(config) 语法

3.2. 支持多种请求方法

axios 支持 get | head | options | delete | post | put | patch 当然并不是笨笨地逐个实现的,阅读文件 lib/core/Axios.js 可以看到如下结构:

function function Axios(instanceConfig) {} // Axios 构造函数

// 原型上只有两个方法
Axios.prototype.request = function request(config) {};
Axios.prototype.getUri = function getUri(config) {};

// 为 request 方法提供别名
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config...)); // 这里省略了 mergeConfig 对入参的整合
  };
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) { 
  // 基本同上
});

module.exports = Axios; 

其中核心实现自然是通用请求方法 Axios.prototype.request,其他函数通过调用 request 方法并传入不同的配置参数来实现差异化.

四、封装请求实现--"适配器"

总所周知,浏览器端请求一般是通过 XMLHttpRequest 或者 fetch 来实现请求,而 NodeJS 端则是通过内置模块 http 等实现. 那么 axios 是如何实现封装请求实现使得一套 API 可以在浏览器端和 NodeJS 端通用的呢?让我们继续看看 Axios.prototype.request 的实现,简化代码结构如下:

Axios.prototype.request = function request(config) {
  // 此处省略请求配置合并代码,最终得到包含请求信息的 config 对象

  // chain 可以视为请求流程数组,当不添加拦截器时 chain 如下
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);
    
  // 下方是拦截器部分,暂时忽略
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {});
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {});
  // 执行请求
  while (chain.length) {
    // 请求流程数组前两个出栈,当前分别为 dispatchRequest 和 undefined
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
}; 

当没有添加任何拦截器的时候,请求流程数组 chain 中就只有 [dispatchRequest, undefined] ,此时在下方的 while 只运行一轮,dispatchRequest 作为 then 逻辑接收 config 参数并运行,继续找到 dispatchRequest 的简化实现(/lib/core/dispatchRequest.js):

module.exports = function dispatchRequest(config) {
  // 取消请求逻辑,稍后分析
  throwIfCancellationRequested(config);
  
  // 此处略去了对 config 的预处理
  
  // 尝试获取 config 中的适配器,如果没有则使用默认的适配器
  var adapter = config.adapter || defaults.adapter;

  // 将 config 传入 adapte 执行,得到一个 Promise 结果
     // 如果成功则将数据后放入返回对象的 data 属性,失败则放入返回结果的 response.data 属性
  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
    response.data = transformData(...); // 此处省略入参
    return response; // 等同于 Promise.resolve(response)
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);
      if (reason && reason.response) {
        reason.response.data = transformData(...); // 此处省略入参
      }
    }
    return Promise.reject(reason);
  });
}; 

简单过完一遍 dispatchRequest 看到重点在于 adapter(config) 之后发生了什么,于是找到默认配置的实现(/lib/default.js):

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // 提供给浏览器用的 XHR 适配器
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // 提供给 NodeJS 用的内置 HTTP 模块适配器
    adapter = require('./adapters/http');
  }
  return adapter;
}

var defaults = {
  adapter: getDefaultAdapter(),
} 

显而易见,适配器通过判断 XMLHttpRequest 和 process 对象来判断当前平台并获得对应的实现. 接下来继续进入 /lib/adapters 目录,里面的 xhr.js 和 http.js 分别对应适配器的浏览器实现和NodeJS 实现,而 README.md 介绍了实现 adapter 的规范:

// /lib/adapters/README.md
module.exports = function myAdapter(config) {
  // 使用 config 参数构建请求并实现派发,获得返回后则交给 settle 处理得到 Promise
  return new Promise(function(resolve, reject) {
    var response = {
      data: responseData,
      status: request.status,
      statusText: request.statusText,
      headers: responseHeaders,
      config: config,
      request: request
    };

    // 根据 response 的状态,成功则执行 resolve,否则执行 reject并传入一个 AxiosError
    settle(resolve, reject, response);
  });
}

// /lib/core/settle.js
module.exports = function settle(resolve, reject, response) {
  var validateStatus = response.config.validateStatus;
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    // 省略参数,createError 创建一个 Error 对象并为其添加 response 相关属性方便读取
    reject(createError(...));
  }
}; 

实现自定义适配器要先接收 config , 并基于 config 参数构建请求并实现派发,获得结果后返回 Promise,接下来的逻辑控制权就交回给 /lib/core/dispatchRequest.js 继续处理了.

五. 拦截器实现

5.1. 探究"请求|响应拦截器"的实现

axios 的一个常用特性就是拦截器,只需要简单的一行 axios.interceptors.request.use(config => return config)就能实现请求/响应的拦截,在项目有鉴权需求或者返回值需要预处理时相当常用. 现在就来看看这个特性是如何实现的,回到刚刚看过的 Axios.js:

// /lib/core/Axios.js
Axios.prototype.request = function request(config) {
  // 此处省略请求配置合并代码,最终得到包含请求信息的 config 对象

  // chain 可以视为请求流程数组,当不添加拦截器时 chain 如下
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);
    
  // 拦截器实现
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    // 往 chain 数组头部插入 then & catch逻辑
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected); // 往 chain 尾部插入处理逻辑
  });
  // 执行请求
  while (chain.length) {
    // 请求流程数组前两个出栈,当前分别为 dispatchRequest 和 undefined
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
}; 

之前说过了项目中使用的 axios 其实就是 Axios.prototype.request,所以当 Axios.prototype.request 触发时,会遍历 axios.interceptors.requestaxios.interceptors.response 并将其中的拦截逻辑添加到"请求流程数组 chain" 中.

在 Axios.prototype.request 中并没有 interceptors 属性的实现,于是回到 Axios 构造函数中寻找对应逻辑(之前说过,工厂函数 createInstance 会将 Axios的原型 & Axios实例的属性 复制给生成的 axios 对象):

// /lib/core/Axios.js
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  // 实例化 Axios 时也为其添加了 interceptors 对象,其携带了 request & response 两个实例
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

// /lib/core/InterceptorManager.js
function InterceptorManager() {
  this.handlers = []; // 数组,用于管理拦截逻辑
}
InterceptorManager.prototype.use = function use(fulfilled, rejected) {} // 添加拦截器
InterceptorManager.prototype.eject = function eject(id) {} // 删除拦截器
InterceptorManager.prototype.forEach = function forEach(fn) {} // 遍历拦截器 

Axios 构造函数在创建实例时会完成 interceptors 属性的创建,实现 axios.interceptors.requestaxios.interceptors.response 对于拦截逻辑的管理.

5.2. 实验:同 axios 内多个拦截器的执行顺序

由于 axios.interceptors.request 遍历添加到 "请求流程数组 chain" 向数组头插入 request 拦截器,所以越后 use 的 request 拦截器会越早执行. 相反,越后 use 的 response 拦截器会越晚执行.

现在假设为 axios 添加两个请求拦截器和两个响应拦截器,那么 "请求流程数组 chain" 就会变成这样(请求拦截器越后 use 的会越先执行):

[
    request2_fulfilled, request2_rejected,
    request1_fulfilled, request1_rejected,
    dispatchRequest, undefined
    response3_fulfilled, response3_rejected,
  response4_fulfilled, response4_rejected,
] 

为此编写个单测用于打印验证:

it('should add multiple request & response interceptors', function (done) {
  var response;

  axios.interceptors.request.use(function (data) {
    console.log('request1_fulfilled, request1_rejected')
    return data;
  });
  axios.interceptors.request.use(function (data) {
    console.log('request2_fulfilled, request2_rejected')
    return data;
  });
  axios.interceptors.response.use(function (data) {
    console.log('response3_fulfilled, response3_rejected')
    return data;
  });
  axios.interceptors.response.use(function (data) {
    console.log('response4_fulfilled, response4_rejected')
    return data;
  });

  axios('/foo').then(function (data) {
    response = data;
  });

  getAjaxRequest().then(function (request) {
    request.respondWith({
      status: 200,
      responseText: 'OK'
    });

    setTimeout(function () {
      expect(response.data).toBe('OK');
      done();
    }, 100);
  });
}); 

结果如下,拦截器的执行打印与期望的结果一致:

5.3. 探究"取消拦截器"实现

使用以下与 setTimeout 和 clearTimeout 相似的语法,即可将一个定义好的拦截器给取消掉:

var intercept = axios.interceptors.response.use(function (data) { return data; }); // 定义拦截器
axios.interceptors.response.eject(intercept); // 取消拦截器 

这里用到了 use 和 eject 两个方法,所以到 InterceptorManager.js 中找找相关实现:

// /lib/core/InterceptorManager.js
function InterceptorManager() {
  this.handlers = []; // 数组,用于管理拦截逻辑
}

// 添加拦截器
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  // 将拦截逻辑包装为对象,推入管理数组 handles 中
  this.handlers.push({ fulfilled, rejected });
  return this.handlers.length - 1; // 返回当前下标
};

// 通过取消拦截器
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null; // 根据下标判断,存在则置空掉
  }
}; 

这里逻辑就比较简单了,由 InterceptorManager 创建一个内置数组的实例来管理所有拦截器,use 推入一个数组并返回其数组下标,eject 时也用数组下标来置空,这样就起到了拦截器管理的效果了~

小结

至此文章就告一段落了,还记得开始提出的三个问题吗?

  • Q1. 如何实现同时支持 axios(config)axios.get(config) 语法
  • A1. axios 库暴露的 axios 对象本来就是一个具备 Axios 实例属性的 Axios.prototype.request 函数. 详见"第三节.从 axios 对象开始分析".
  • Q2. 浏览器和 NodeJS 请求能力的兼容实现
  • A2. 通过判断平台后选择对应平台的适配器实现. 详见"第四节. 封装请求实现--'适配器'"
  • Q3. 请求 & 响应拦截器实现
  • A3. 通过数组的形式管理, 将请求拦截器、请求、响应拦截器都放在"请求流程数组 chain" 中,请求时依次执行直到 "请求流程数组 chain" 为空. 详见"第五节. 拦截器实现".

    欢迎拍砖,觉得还行也欢迎点赞收藏~ 
    新开公号:「无梦的冒险谭」欢迎关注(搜索 Nodreame 也可以~)
    旅程正在继续 ✿✿ヽ(°▽°)ノ✿
查看原文

赞 1 收藏 0 评论 0

Nodreame 发布了文章 · 1月14日

「刷题专栏」扬帆 - 五分钟构建自己的刷题网站

本文前置文章为 「刷题专栏」起航 - 五分钟构建自己的刷题仓库 ,建议花五分钟阅读并完成刷题仓库构建~

上一篇文章 「刷题专栏」起航 - 五分钟构建自己的刷题仓库 介绍了刷题仓库的构建,但是只有刷题仓库是不够的,我们的目标是借助刷题仓库更好地学习算法,如果能通过网站方式浏览自己的刷题记录,那就能更方便地巩固学习效果了~

一. 最简网站构建

当前我们项目的 leetcode 文件夹下已经是满满的一千多道 Leetcode 题目 Markdown 文件了:

经过一番查阅找到 VuePress 可以快速帮我们完成网站的构建,马上按照官方指示开始操作:

cd leetcode-js # 或者你的仓库名 echo '# Hello VuePress' > leetcode/README.md npm i -D vuepres # yarn add -D vuepress

然后打开 package.json 文件,在 scripts 部分插入:

然后命令行运行 npm run docs:dev 或者 yarn docs:dev,等待命令运行完成之后会看到命令行出现可供本地调试的链接:

打开链接,可以看到刚刚用命令行生成的 README.md 文件已经出现在网站上了,说明 VuePress 已经将我们的 leetcode 文件夹的 markdown 解析并生成为网站资源了:

那我们能在网站上看到同在 leetcode 文件夹 的刷题记录吗?答案是肯定的~ 只需要找到一个文件名,作为域名的路径就可以了~

例如我现在找到第一道题 "1_两数之和",放在 "localhost:8080" 后面,结果如下:

网站成功地将这道题目给展示出来了~ 并且还有一个附加的小功能,只要在右上角输入关键字就会自动帮你找到相关的文档哟:

二. 题目列表构建

最简网站构建完成之后,稍微使用一下就会发现不太行。Leetcode 有个能点进去就看到所有题目的列表,我们网站没有这个功能,找一道题还要我输入一次=。=

别急,还记得我刚刚用 README.md 生成的只有一句 Hello VuePress 的首页吗,我们可以在这里编写一个 README.md 的题目列表,这里对这个列表有两个要求 :

  • 按顺序展示所有题目
  • 每个题目都可以跳转到 Leetcode具体题目位置 & 我的题解网页

这些信息之前我们生产刷题仓库的时候就已经保存在缓存文件 /cache/problems.json 里了,现在只需要读取即可~

接下来要做的事情有两件, 分别对应上面两个要求:

  • 从缓存文件中获取题目信息
  • 生成对应的 README.md(题目列表格式暂定为 "题名&链接 + 难度 + 题解&链接")

简单设计之后就开始实操,在 dev 文件夹下创建文件 generateReadme.js,开始编写逻辑:

主逻辑比较简单,基本:拿出数据,然后填入 markdown 形式的字符串模板.

接着给 package.json 添加命令 build-readme 如下:

接着在新开一个终端运行 npm run build-readme 或者 yarn build-readme,结果如下:

再看看原来的页面

直接就是简洁美观的Leetcode 题目列表页面了~

题目列表生成脚本位置在:https://github.com/Nodreame/leetcode-js/blob/master/dev/generateReadme.js

P.S. 本来打算把 打包和 CI 一起写了,但是没想到 leetcode 题目过多居然导致 Vuepress SSR 形式打包失败,并且没找到 CSR 打包方式,所以只能等我处理完再好好地整理出最后一篇发布的文章了 o(╥﹏╥)o

欢迎拍砖,觉得还行也欢迎点赞收藏~
新开公号:「无梦的冒险谭」欢迎关注(搜索 Nodreame 也可以~)
旅程正在继续 ✿✿ヽ(°▽°)ノ✿
查看原文

赞 0 收藏 0 评论 0

Nodreame 发布了文章 · 1月13日

记一次 Github 项目依赖的安全警告修复 & 分析

写这篇文章是觉得在解决问题的过程中 可以补全边缘知识 & 学习开源项目的方法。大家看看自己 Github 的项目如果有安全警告的话可以参考本文思路一起练练手~

提要

修复章节 分别讲述了如何通过 页面 & 命令行 方式(git rebase)处理安全警告

分析章节 对 axios 本次版本升级(v0.21.0 => v0.21.1)所解决的主要问题进行分析、并尝试用 TDD 的方式解决问题。

零、起因

准备刷题准备面试,进到 Github 刷题仓库 发现下面这个安全警告:

翻译一下就是说有"潜在的安全漏洞",打开来看看:

居然是 axios 的漏洞修复小版本升级,作为上周刚刚阅读过 0.21.0 版本 axios 源码的我当然是要了解一下的啦(然后水篇文章

一、修复

可能文章的顺序有点奇怪,毕竟一般问题都是先"分析"后"修复"的。不过项目依赖的安全漏洞修复方法千篇一律,并且后面的分析过程一半以上可能只适合前端方向的同学看,所以就把"修复"环节前置了~

说说修复方法吧,一般就是手动修复完依赖版本提交到主分支或者当前分支嘛(请根据自己项目的发布流程而确定修改分支)。不过现在 Github 检测到 CVE(Common Vulnerabilities & Exposures”通用漏洞披露,本次是CVE-2020-28168)后一般会基于主分支创建一个修复分支.

既然官方已经帮我们改完了那就没必要自己搞了,直接 git rebase 一波杀穿(主分支做了提交限制 or 流程存在门禁就的就老老实实 merge 吧),可以通过网页或者命令行的方式来处理问题:

P.S. 就算是我的分支之前刚做了提交也没问题,如下图所示

1. 网页操作方式

Tip:如果对命令行的操作没有 200% 的信心真的真的请选择这个,规范易操作出错也有提示真心舒服

如果想在网页上解决的话就进入到项目的 Pull requests 标签页,确认无误之后选择合并方式:

image-20210107174545267

这里我选择的是 Rebase adn merge,它会作为一次最新的提交合并入代码中( 而且还可以直接删除掉Github 自动生成安全警告的分支,方便快捷),结果如下:

image-20210107174858775

2. 命令行方式

git fetch # 拉项目最新信息
git pull origin master # 确保主分支最新
git checkout -b tmp origin/dependabot/npm_and_yarn/axios-0.21.1 # 把 "Github 自动修复分支"拉到本地 tmp 分支
git rebase master tmp # 变基操作:在 tmp 分支上执行,将 tmp 的更新内容尾接到 master 上,结果存储于 tmp 分支
git push origin tmp:master # 将 tmp 作为 master , 提交到远程仓库的 master 分支上
git push origin --delete dependabot/npm_and_yarn/axios-0.21.1  # 手动删除"Github 自动修复分支" 

image-20210107181414090

完成之后结果与网页方式是一致的,完成之后再进入到仓库页面,发现安全漏洞警告提示已经消失了(撒花✿✿ヽ(°▽°)ノ✿

image-20210107182651308

二、分析

上面的 Git 操作教学 修复部分 暂时就告一段落了,现在回过头来看看 axios 究竟是出了什么问题导致了安全警告,我们进入到 Pull request 标签页:

image-20210107215126673

Release notes 展示了 v0.21.1 相对于 v0.21.0 发生的更新,其中 Internal and Tests 部分 都是测试用例的修复,与安全警告关系不大,所以让我们把重点放在 Fixes and Functionality 部分,第一项 Hotfix: Prevent SSRF 就是对于安全警告的信息。

从后面的链接 #3410 点进去,就能了解到这个修复的起因、讨论过程和处理方法。接下来我们就从最初始的 issue 来分析这个安全警告。

0. 前置知识:什么是「 跟随重定向(Follow Redirects) 」

Tip:本来应该直接开始分析 issue 的,但是这个知识点可能会影响到对 issue 的分析阅读,所以就前置到这里来了

我先在本地8080启动一个返回 302 的服务器程序,大家脑补一下通过 浏览器Node.JS的内置http模块Node.JS环境下的axios 访问 localhost:8080 链接分别会得到什么结果,程序如下:

const axios = require('axios')
const http = require('http')

http.createServer(function (req, res) { 
    res.writeHead(302, {location: 'http://example.com'})
    res.end()
}).listen(8080) 

AFewMomentsLater

现在揭晓答案:

image-20210108001016060

基于 MDN 的说法

302:该状态码表示所请求的URI资源路径临时改变,并且还可能继续改变.因此客户端在以后访问时还得继续使用该URI.新的URL会在响应的Location:头字段里找到.

加上观察网络请求的过程就能推导出 浏览器 的请求过程如下图所示,其中"请求过程 2"就是 浏览器自动实现了「 跟随重定向(Follow Redirects) 」

浏览器重定向过程

  • Node.JS的内置http模块:得到 302 状态码,说明内置的http模块不具备 「 跟随重定向(Follow Redirects) 」能力,只是执行了第一个请求过程,也就是获取到 302 状态码及重定向地址即可:

    image-20210107235852668

  • Node.JS环境下的axios:和浏览器的最终结果一样,状态码 200 并且能够获取到网页内容

    image-20210108014258352

那么为什么同样是 Node.JS环境下axios内置HTTP模块 对于重定向表现不同呢?在 axios Github 文档 中搜索 redirects ,可以再请求配置 小节找到这样的信息:

image-20210108020451262

意思是 axios 默认开启了「 跟随重定向(Follow Redirects) 」能力(顺便考古到了2015 年是怎么给 axios 加上这个功能的 follow redirects),并且能够通过改变 maxRedirects 字段的值来决定 「 跟随重定向(Follow Redirects) 」 的开启或关闭。遇到 302 状态码时,如果开启了 「 跟随重定向(Follow Redirects) 」 就会获取重定向地址并继续跳转,如果不开启就是直接返回结果.

现在把 maxRedirects 字段置为 0 ,再次运行程序,可以看到 axios 对于重定向的处理表现就和 NodeJS 一致了:

image-20210108021423393

了解什么是 「 跟随重定向(Follow Redirects) 」 之后,我们来开始看看提 issue 的老哥遇到的问题~

1. 初始 issue 分析

先看看这个安全警告的起因:

  • 链接:issue3369 - 重定向后的请求未通过代理传递
  • 说明:提 issue 的老哥提出他使用 "携带代理配置" 的 axios 进行访问,由于请求经过代理服务器,且代理服务器永远返回 302,所以老哥期待的运行现象是:

    • 1)首次请求由于配置了带来,故经过代理服务器地址 localhost:8080,获得 302状态码,由于 axios 默认开启 「 跟随重定向(Follow Redirects) 」 所以暂时不打印结果,并且 axios 会自动尝试访问重定向后的目标地址 http://example.com;
    • 2)但是由于代理配置的存在,第二次访问还是会访问到 localhost:8080 ,再次获得 302 状态码;
    • 3)重复上面两个步骤直到到达 axios 的 Follow Redirects 模式次数上限,然后报错;

那么我们现在来看看提 issue 的老哥提供的完整重现代码:

const axios = require('axios')
const http = require('http')

const PROXY_PORT = 8080
let count = 0

// A fake proxy server
http.createServer(function (req, res) {
      count++ // (我加入的计数逻辑)累加请求次数
    res.writeHead(302, {location: 'http://example.com'})
    res.end()
  }).listen(PROXY_PORT)

// 和我们的前置案例相比新增了一个"携带代理配置的 axios 请求"
axios({
  method: "get",
  url: "http://www.google.com/", // 随便写个链接,都会被代理对象取代
  proxy: { // 重点部分
    host: "localhost",
    port: PROXY_PORT,
  },
})
.then((r) => {
  console.log(count)   // (我加入的打印) 打印成功情况访问代理服务器次数
  console.log(r.status) // (我加入的打印) 用于观察返回状态码
  console.log(r.data)
})
.catch(e => {
  console.log(count) // 打印失败情况访问代理服务器次数
  console.error(e)
}) 

与我们期望的得到 302 报错结果不同,使用 axios v0.21.0 的执行效果如下:

image-20210108171230057

现象为:进入代理服务器一次,然后 axios 的请求结果状态码是 200,内容是 http://example.com 的页面内容.

根据现象推导执行过程为:首次请求成功进入了代理服务器并且累加了一次计数(且只有这一次),但是步骤 2请求却绕过了 axios 配置里的代理 proxy 直接访问了重定向地址 http://example.com.

那么现在问题就很明显了,当获得 302 返回时应该 携带代理配置 重新发起请求,然而 axios v0.21.0 在重新发起请求时却丢失了代理配置,所以要做的事情就是研究下 axios 「 跟随重定向(Follow Redirects) 」 之后为何丢失了代理配置.

2. 问题定位 & 修复方案制定

出现问题的是 NodeJS 环境,那么自然要找到 axios 源码中的 NodeJS 重定向请求配置的位置。

来到 axios 项目的 /lib/default.js 位置,下面的代码赋予了 axios 实现跨平台网络请求能力,它会自动判断运行平台并使用不同平台逻辑实现网络请求:

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    adapter = require('./adapters/xhr'); // 浏览器环境走这里
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    adapter = require('./adapters/http'); // Node.JS 环境走这里
  }
  return adapter;
} 

接下来进入到 /lib/adapters/http.js 后发现只有一个函数,接收 config 参数并返回一个 Promise:

image-20210109183012339

请求逻辑实现应该都在这个函数里面了,接着直接搜索 config.proxy 查找 代理逻辑,找到相关代码并得到如下分析:

截屏2021-01-08_下午5_46_43

这里的 171 行的 httpFollow 来自 follow-redirects 模块,模块官方的描述是:

Drop-in replacement for Node's http and https modules that automatically follows redirects.

也就是说该模块兼具内置 http & https 模块的能力,且还具备了 「 跟随重定向(Follow Redirects) 」 能力(默认 「 跟随重定向(Follow Redirects) 」 上限为 21 次,即 maxRedirects 属性)。

了解了这些之后,想要修复"代理配置丢失"的问题,那么就要去了解 follow-redirects 模块的使用方法了,这里找到官方demo:

const url = require('url');
const { http, https } = require('follow-redirects');

const options = url.parse('http://bit.ly/900913');
options.maxRedirects = 10;
options.beforeRedirect = (options, { headers }) => {
  // 重定向时调整options
  if (options.hostname === "example.com") {
    options.auth = "user:password";
  }
};
http.request(options); 

那么这个 options.beforeRedirect 就是我们要找的东西了,它在执行请求前传入options,运行函数体实现对 options 的修改,所以需要 axios 项目的 /lib/adapters/http.js 中添加 beforeRedirect,在重定向的时候将原本的 代理配置 加入 options 即可.

4. 以 TDD 的方式修复 issue

为了测试修复效果,我们将 axios v0.21.0 版本的源码 (commit 为 94ca24b5b23f343769a15f325693246e07c177d2) 拉到本地,并复制 axios v0.21.1 版本的测试用例 /test/unit/regression/SNYK-JS-AXIOS-1038255.js 的代码,到项目中新建同名文件夹并黏贴,下面是该测试用例的代码及分析:

// https://snyk.io/vuln/SNYK-JS-AXIOS-1038255
// https://github.com/axios/axios/issues/3407
// https://github.com/axios/axios/issues/3369

const axios = require('../../../index');
const http = require('http');
const assert = require('assert');

const PROXY_PORT = 4777; // 代理服务器端口
const EVIL_PORT = 4666; // 重定向 location 地址的端口,代码逻辑正确的话不应该进入该端口

describe('Server-Side Request Forgery (SSRF)', () => {
  let fail = false;
  let proxy;
  let server;
  let location;
  beforeEach(() => {
    server = http.createServer(function (req, res) {
      fail = true;
      res.end('rm -rf /');
    }).listen(EVIL_PORT);
    proxy = http.createServer(function (req, res) {
      // 第一次请求到达代理服务器时,req.url 为 http://www.google.com/,走返回 302 的逻辑
      // 第二次请求由 axios 的「 跟随重定向(Follow Redirects) 」能力发出,url 应为 http://localhost:4666
      if (req.url === 'http://localhost:' + EVIL_PORT + '/') {
        return res.end(JSON.stringify({
          msg: 'Protected',
          headers: req.headers, // 返回请求头
        }));
      }
      res.writeHead(302, { location }) // 第一次请求达返回状态码 302 和 http://localhost:4666
      res.end()
    }).listen(PROXY_PORT);
  });
  afterEach(() => {
    server.close();
    proxy.close();
  });
  it('obeys proxy settings when following redirects', async () => {
    location = 'http://localhost:' + EVIL_PORT;
    let response = await axios({
      method: "get",
      url: "http://www.google.com/",
      proxy: {
        host: "localhost",
        port: PROXY_PORT,
        auth: {
          username: 'sam',
          password: 'password',
        }
      },
    });

    assert.strictEqual(fail, false);
    assert.strictEqual(response.data.msg, 'Protected');
    assert.strictEqual(response.data.headers.host, 'localhost:' + EVIL_PORT);
    assert.strictEqual(response.data.headers['proxy-authorization'], 'Basic ' + Buffer.from('sam:password').toString('base64'));

    return response;
  });
}); 

然后在本地用 npm test 或者 yarn test 跑测试,结果如下:

image-20210109021851406

确实是新的测试用例炸了,因为 axios 中并没有实现相应能力所以没有提示任何问题,现在我们来验证编写修复代码:

问题出在 代理配置 丢失,所以到 /lib/adapters/http.js 找到代理相关代码(148~152行):

截屏2021-01-09_上午2_22_20

复制一份到 beforeRedirect 中(axios 项目有 lint 检查所以改 options 为 tmpOptions)这样应该就不会出现代理丢失的情况了~

if (proxy) {
  options.beforeRedirect = function(tmpOption) {
    tmpOption.hostname = proxy.host;
    tmpOption.host = proxy.host;
    tmpOption.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : '');
    tmpOption.port = proxy.port;
    tmpOption.path = protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path;
  };
} 

将上面代码加入到 /lib/adapters/http.js 的160 行处,再次用 npm test 或者 yarn test 跑测试,这次成功地进行「 跟随重定向(Follow Redirects) 」 请求并且由于超过最大次数而报错了:

image-20210109022614643

之所以没有达到想要的效果,是因为没有为「 跟随重定向(Follow Redirects) 」 请求配置正确的请求目标链接。原本测试用例期望:

但是实际情况是第一次请求获得 302 返回后并没有更新目标链接,所以还是要阅读下 follow-redirects 模块options.beforeRedirect 的调用位置(根目录下的 index.js):

image-20210109235835302

在 357 行可以看到调用 options.beforeRedirect 时传入了 options & 包含重定向响应体的 responseDetails,于是我们从 response 中获取重新获取目标配置并填充 path 和 headers.host :

if (proxy) {
  options.beforeRedirect = function(tmpOption, response) { // response 对应重定向响应体的 responseDetails
    // hostname(host) & port 是请求发送到的服务器的域名 & 端口,现在都是代理配置不变
    // path & headers.host 是目标路径 & 目标host,遇到 302 时应该读 location 然后重新填充
    tmpOption.hostname = proxy.host;
    tmpOption.host = proxy.host;
    tmpOption.port = proxy.port;
    // tmpOption.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : '');
    // tmpOption.path = protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path;
    var redirectInfo = url.parse(response.headers.location);
    tmpOption.path = redirectInfo.href; // 重定向链接
    tmpOption.headers.host = redirectInfo.host; // 重定向的目标 host
  };
} 

再次运行测试,成功通过:

image-20210109023712032

5. 对比官方的问题解决方法

问题已经处理完毕并且通过测试用例,但是可能存在疏漏所以一定要与官方的修复进行对比验证,这样才符合学习闭环。

可以看到 #3410相关的提交

image-20210110003745855

这也是一个 TDD 的过程,首先是编写测试用例重现了 issue,然后对问题进行修复,然后再将代码重构。

当然也可以直接点击最后 File changed 的选项卡,直接看整体修改了哪些代码。

可以看到官方的处理方法除了重构部分之外与我们上面的修复方法基本一致,不过其中有一个点引起了我的兴趣:

image-20210110004324267

options.beforeRedirect 方法体中居然只用一个 redirection 就完成了 redirection.header.host 的赋值,而我们上面是用到了第二个参数的,这里我选择继续到 follow-redirects 模块 中找找,果然在 RedirectableRequest.prototype._processResponse 中找到了这段逻辑:

截屏2021-01-10_上午12_49_37

于是刚刚的修复代码可以不再使用第二个参数 response,并且也不用再重新解析一次 location 了:

if (proxy) {
  options.beforeRedirect = function(tmpOption) {
    var redirectHost = tmpOption.host; // 先拿出来,防止被覆盖
    tmpOption.hostname = proxy.host;
    tmpOption.host = proxy.host;
    tmpOption.port = proxy.port;
    tmpOption.path = tmpOption.href; // 重定向的目标路径
    tmpOption.headers.host = redirectHost; // 重定向的目标 host
  };
} 

再次运行测试,成功通过:

image-20210110005622423

OK 到这里对于 #3410 的分析就全部完成了~ 对第二个问题 Protocol not parsed when setting proxy config from env vars #3070 也可以尝试用这样的方法解析学习哟~

撒花✿✿ヽ(°▽°)ノ✿

欢迎拍砖,觉得还行也欢迎点赞收藏~
新开公号:「无梦的冒险谭」欢迎关注(搜索 Nodreame 也可以~)
旅程正在继续 ✿✿ヽ(°▽°)ノ✿

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 44 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-12-22
个人主页被 1.8k 人浏览