本次的候选人两年工作经验,坐标广州,在公司内做过许多基础建设与工程化的工作,比如
- 基于 webpack 构建的前端项目模板
- 基于 pipeline 版本发布的基础建设
候选人期望薪资为 14k+,个人感觉他偏于保守,综合面试下来感觉不错,其中不乏不少亮点:
- 他们使用了 ahooks 作为 hook 库,在使用过程中也会看其源码,在面试过程中对自己常使用过的 hook 源码能答得上来
- 针对两年开发的同学,对于 svg 图标库的开发极具挑战
- 一些基础设施的建设了解颇多,如 CI,一般了解这些表明对技术热情比较高
本次面试记录,不仅包含了山月自己所做的一些面试记录,也包含了候选人的面经,可见 一次模拟面试总结,再推荐下候选人的博客 https://www.hsslive.cn/
视频版: 两年前端,广州求职,要价 14k,依我说,小伙子可以多要点,涉及组件建设、工程化以及丰富的项目经验
以下是简历大致内容:
山月的面试问题记录
- ahooks
- useLocalstorage ✅
- useUpdate
- 如何实现 useUpdate ✅
- useUpdateEffect
- 组件二次封装
- 时间选择器
- 树形选择器
- 上传组件
- 占用内存
- svga -> 动画
- https://svga.io/
- lottie
- 基础建设以及前端工程化流程建设完善
- 版本控制
- 快速回滚 ✅
- changelog
- package 是哪个 -> standard-version/release-it/np ❎
- jenkins
- 版本控制
- 发包的工具
- standard-version/release-it/np ❎
- https://www.npmtrends.com/release-it-vs-standard-version-vs-np
- 业务-直播-活动
- 外企?
- svgo / svg ✅
- path-xml -> dom ?
- https://react-svgr.com/playground/
- https://www.svgviewer.dev/
- 如何更好地使用 svg
- Font-Awesome: https://github.com/FortAwesome/Font-Awesome
- Heroions: https://github.com/tailwindlabs/heroicons
候选人的面经总结
候选人对本次模拟面试也进行了总结,原地址: https://www.hsslive.cn/article/93
感谢up,这次面试获益良多,一开始没有get到up想表达的点,后面才反应过来,没想到svg还能有这么好用的在线工具svgr和svgviewer这些,也是因为有写过这个图标库的经验,再看这些工具,确实大概能知其所以然。
业务
主要是针对公司业务进行各种二次封装或者编写组件,各家公司业务不一样,而且感觉我的业务也没什么亮点的,因此不谈。
基建
主要也是根据业务,封装以及抽离各种组件、各种常用业务方法,统一放到私有库里面以提高开发效率,业务类也不谈(私库发包其实还是有的说的,但我没权限发包)。
规范
规范是每家公司都有的,简单说下
- lint-staged,执行一些操作,如:prettier、eslint。
- husky,git 钩子。
- pre-commit,在 git commit 前执行 lint-staged。
- commit-msg,在 git commit 后执行 commitlint。
- commitizen,执行 git cz 的时候可规范进行提交 commit。
- standard-version,更新 changeLog.md 以及 package.json 的 version。
开发
- 开发完成后,提交代码到远程仓库,远程仓库有三个基本分支(其他的都是功能分支),分别是 beta、preview、master。
- 一般本地开发完成会把自己的功能分支合并到 beta 分支提交上去测试。
- 测试没问题了就合并到主分支,然后执行release命令生成修改版本号以及生成changeLog。
- 最后push到远程仓库。
构建部署
- 主要基于jenkins构建。
- master 分支可以发布三个环境:beta、preview、prod 环境,非 master 只能发布 beta、preview 环境。
- master 分支需要根据 tag 进行发布(可以根据tag进行快速回滚),非 master 分支会根据分支进行发布。
- 公司项目很多,构建很频繁,而且构建的项目的时候非常的占用cpu和内存,因此公司有一个单独的服务器用来做jenkins构建
- 在构建的时候,会先执行构建的项目里面的构建脚本,构建完成后,将构建好的文件压缩,会通过scp命令上传到公司的cdn服务器
- 然后添加对应的nginx配置即可。
上面的东西其实每家公司都有,但是不要停留在他们原本就有了,自己只需要在此基础上写业务就好了,这样其实还不如不说。
最好就是除了业务,其他的规范和版本控制这些自己都实现一遍,然后可以说自己实现过知道怎么做,而不是只是单纯的在此基础上只负责写业务。
Ahooks
面试一般直接说最核心的实现,不考虑边缘情况以及性能(除非面试官问)
useUpdateEffect
useUpdateEffect
用法等同于 useEffect
,但是会忽略首次执行,只在依赖更新时执行。
项目中用到了,但是我自己没用过,个人感觉比较鸡肋,就没看源码。
useDebounce
用来处理防抖值的 Hook。
useThrottle
用来处理节流值的 Hook。
实现 useUpdate
https://github.com/alibaba/hooks/tree/master/packages/hooks/src/useUpdate
主要利用了 js 里面的{}不等于{},在使用这个 hooks 的时候,直接调用导出的 useUpdate(),即可对整个函数式组件进行重新渲染。
import { useState } from "react";
const useUpdate = () => {
const [, setState] = useState({});
return () => setState({});
};
export default useUpdate;
实现 useLocalStorageState
https://github.com/alibaba/hooks/blob/HEAD/packages/hooks/src/useLocalStorageState/index.ts
主要就是使用 useState 的时候同时把数据缓存到 localStorage 里,主要实现就是对 localStorage 进行了一层 useState 包装,然后导出一个 state 和设置 state 回调函数,在调用这个回调函数的时候,同时设置 localStorage.setItem 以及更新内部的 useState。以此达到statte和localStorage同步。
这个 hook 官方做了非常多的边缘处理,这里的实现只是核心思路。
import { useState, useEffect } from "react";
const useLocalStorageState = (key, initValue) => {
const [state, setState] = useState({ key, value: JSON.stringify(initValue) });
const setLocalStorageValue = (value) => {
try {
if (value !== undefined) {
window.localStorage.setItem(key, value);
setState({ key, value: value });
return;
}
window.localStorage.removeItem(key);
} catch (error) {
console.log(error);
}
};
const getLocalStorageValue = () => {
const cache = window.localStorage.getItem(key);
return cache;
};
useEffect(() => {
const cache = getLocalStorageValue() || state!.value;
if (cache !== null) {
setLocalStorageValue(cache);
}
}, []);
const parse = () => {
try {
return JSON.parse(state?.value || initValue);
} catch (error) {
console.log(error);
}
return JSON.parse(initValue);
};
// 这个parse是最简单的容错处理,虽然不太优雅
return [parse(), setLocalStorageValue];
};
export default useLocalStorageState;
svg方案之为什么用svg
- 和图片对比,svg更灵活,以及因为是矢量图不会因为大小导致模糊。
- 搭建组件库的时候,如果使用了图片这种静态资源,除非将图片打成base64,否则直接额外生成图片,然后其他文件通过路径引这个图片使用,这样的话一定是有路径问题的:
- 因为这个组件文件只能是根据相对路径或者绝对路径引这个图片,假设这个路径是../../img/xxx.jpg或者/img/xxx.jpg。这个路径如果在类似vuecli这种脚手架的环境,开发的服务器默认的8080端口,
- 如果是/img/xxx.jpg,对于vuecli来说就是在静态目录里面找,也就是默认的public目录里面找/img/xxx.jpg,但是肯定没有这个图片。
- 如果是../../img/xxx.jpg,这个我没试过,感觉应该会把public目录作为/,会在public的外层找xxx.jpg,因此,肯定也是找不到这个图片的。
svg icons 的组件库方案
主要说下 ant-design-icons 的方案,主要源码:
注意:
- 这里不说具体的实现方式,因为官方组件库实现过程是非常多的,这里只说了核心的实现思路,不管过程如何多,其实要做的事情都是一样的。
- 因为这个组件库最终实现的效果是将 svg 文件转成开箱即用的 vue/react/angular 组件文件,它的主要作用是根据 svg 文件输出相对应组件文件,然后发布到 npm 上面,给其他库使用的。因此这些事情并不能浏览器环境做(至少不合适),而是在 node 环境做的。
步骤一:svg 文件转成 js 文件
-
因为是跑在 node 环境,因此操作文件都是基于流(stream),所有使用了 through2 进行流操作。
-
svg 文件里面有一些无用的属性,首先使用 svgo 对 svg 文件进行了优化(optimizing)
-
将优化好的 svg,使用 parseXML 这个库进行 dom 解析
-
将解析好的 dom 数据,通过固定的格式(比如 ejs 模板)输出成
js
文件,如:- ejs 模板
// ejs模板,iconname是svg名称,iconcontent是解析好的dom数据 const <%= iconname %> = <%= iconcontent %>; export default <%= iconname %>;
- 上面的模板通过解析最终会生成:
// 这个文件是由billd-ui-icons/build-tools/svgo/template/icon-svg/asn.ejs自动生成的,请勿手动修改! const AccountBookFilled = { type: "document", children: [ { type: "element", isRootNode: true, name: "svg", attributes: { class: "icon", viewBox: "0 0 1024 1024" }, children: [ { type: "element", name: "path", attributes: { d: "M880 省略。。。。", }, children: [], }, ], }, ], name: "account-book", theme: "filled", }; export default AccountBookFilled;
步骤二:生成 vue 组件文件
有了保存 svg 的 dom 数据的 js 文件之后,还是一样的办法,通过固定的格式(比如 ejs 模板)输出成 jsx
文件
ejs模板:
// ejs模板,iconname是svg名称,iconcontent是解析好的dom数据
// 这个文件是由build-tools/svgo/template/icon-vue/icon-vue.ejs自动生成的,请勿手动修改!
import <%= iconname %>Svg from '@huangshuisheng/icons-svg/lib/asn/<%= iconname %>';
import BilldIcon from '../billdIcon';
export default {
name: '<%= iconname %>Svg',
displayName: '<%= iconname %>Svg',
functional: true,
props: {},
render(h, ctx) {
return (
<BilldIcon
innerSvgProps={<%= iconname %>Svg.children[0].attributes}
children={<%= iconname %>Svg.children[0].children}></BilldIcon>
);
},
};
上面的模板通过解析最终会生成:
// 这个文件是由build-tools/svgo/template/icon-vue/icon-vue.ejs自动生成的,请勿手动修改!
import AccountBookFilledSvg from "@huangshuisheng/icons-svg/lib/asn/AccountBookFilled";
import BilldIcon from "../billdIcon";
export default {
name: "AccountBookFilledSvg",
displayName: "AccountBookFilledSvg",
functional: true,
props: {},
render(h, ctx) {
return (
<BilldIcon
innerSvgProps={AccountBookFilledSvg.children[0].attributes}
children={AccountBookFilledSvg.children[0].children}
></BilldIcon>
);
},
};
步骤三:编译 jsx 文件
这个没什么好说的,因为是使用的 vue 的 jsx 方式,可以直接使用 babel 进行编译,具体看:
- vue 处理 jsx:https://github.com/vuejs/babel-plugin-jsx
- gulp-typescript:https://github.com/ivogabe/gulp-typescript
非核心
参考了:babel插件:https://github.com/ant-design/antd-tools/blob/master/lib/replaceLib.js
Babel 插件本质上就是编写各种 visitor 去访问 AST 上的节点。当遇到对应类型的节点,visitor 就会做出相应的处理,从而将原本的代码 transform 成最终的代码。
这里因为最终会构建两个版本,一个es,一个lib,但是构建的版本都是通过同一个ejs模板生成的jsx,因此他们引用的路径都是同一个lib,因此需要我在编译es版本的时候使用了这个babel插件,添加了ImportDeclaration和ExportNamedDeclaration两个方法,即当解析到import或者export的时候,我用正则匹配将lib给替换成es,即:
import AccountBookFilledSvg from "@huangshuisheng/icons-svg/lib/asn/AccountBookFilled";
// 替换成:
import AccountBookFilledSvg from "@huangshuisheng/icons-svg/es/asn/AccountBookFilled";
虽然这个babel插件做的事情就是这么简单,但是能比只会用babel以及babel插件好很多,最起码知道babel插件做的事情。
总结
其实 ant-design-icons 官方的组件库核心的实现就是上面的三个步骤,看似做的事情不多,但是过程是比较复杂,只是我为了简洁省略了很多东西,期间最重要的是设计能力以及工程相关的知识。
因为组件库的方案适合大批量的构建 svg 组件,但是平常开发中,可能只需要一两个 svg 组件,而且,可能对这个 svg 有一些定制化的要求,这时候可以使用一些更方便的 svg 方案进行处理
一些svg资源
SVGR
可以快速将svg生成react组件
https://react-svgr.com/playground/
svgviewer
可以快速将svg生成react组件