Skip to content
On this page

业务篇-简历制作之常用组件设计与简历数据设计


前言

在上一章节,我们已将数据存储的功能实现,本章节继续采用上章节的 chapter-07-op 分支进行开发。

在简历制作之前,我想还是有必要单独写篇文章讲解一下常用组件的封装设计与数据字段设计,大家可能喜欢如上几章节的写作思路:先“粗暴编写”再“思考优化”,但在此章节中,我们需要稍微改变一下思考的方式,不要担心,先往下看。

⚠️ 本章节最重要的是理解和思考,如果你对本章节内容兴趣不大,可以快速阅读或跳过。本章节不强制小伙伴们动手实现,我更希望你看完之后,能自己去思考,去尝试封装,最后再结合👉 chapter-08代码去看。

组件化思想

必须承认一点是:人的精力与能力是有限的,你是很难一次性处理好一大堆复杂问题的。但我们与生具备的一优秀特点,那就是拆解问题。如同写代码一样,我们将所有的处理逻辑均放在一个组件中,那么后续的维护、管理及扩展将会变得困难,我们要学会去“拆”逻辑、“拆”组件。

React 核心思想是组件化,它期望我们通过拆解小颗粒化的组件,进行拼接,从而构造我们的应用。假设我们在一个组件中做完所有的事情,那这个组件属于多职责组件,它不需要区分各种职责,不用规划对应的结构。最终的结果导向为:定位问题时间成本相对较高,代码阅读上,极为痛苦。React 哲学里很明确的说道:组件应当遵循单一功能原则,换言之,一个组件原则上只能负责一个功能。

image.png

以上图为例,我们可以将它适当划分,下图是划分之后的层级关系

image.png

我们将这个页面划分成了 Header(头部)、Content(简历内容)、Toolbar(工具条)三个容器模块,以 Header 为主,它又包含着 Back 返回组件和 ExportButton 导出按钮组件,以这种拆分的形式,对每个模块进行逐层拆解。颗粒化拆分组件,需要思考怎样的颗粒度才合适。粒度不是越小越好,粒度最小太极端,会导致小型组件很多,管理困难。所以这个颗粒,一定是最适合被复用的程度。

组件封装

接下来将会讲解目前简历应用平台的通用组件(当然随着业务开发,可能会越来越多),这里并不会贴代码实现,只会讲解其中的思考过程和为什么要封装。对于组件具体代码在 👉 chapter-08,小伙伴们自行前往阅读查看。

组件封装为了更好书写样式名,这边采用 classnames 库进行处理

说白了,我们可以不用封装通用组件,所有的组件都可称之为“业务组件”,比如你点击了“导出”按钮后,显示弹窗,弹窗底部有两个按钮:确定按钮、取消按钮。

我们可以统称这三个按钮为业务按钮组件,每一个按钮对应自己的业务,自己的逻辑。这是合理的,只是我们自身认为不合理的地方是:他们有很多共性,在差异点上可能就文案的不同,颜色的不同,其余的交互效果一致(比如鼠标 hover 按钮、点击按钮之后的颜色改变等)正因为这些一致,在“下一次”新增业务组件时,我们都手动拷贝一份代码,这会导致项目中存在大量“重复”代码。正因为如此,我们才认为它是不合理的,也正因为这样,我们才要去封装公共组件。

所有的通用组件均存放于 app/renderer/common/components 中,通用组件共有:

  • MyButton 按钮组件
  • MyInput 数据输入组件
  • MyUpload 文件上传组件
  • MyModal 弹窗组件
  • MyScrollBox 固定区域内的滚动组件

MyButton

组件代码地址:👉 查看

之前写过一篇关于按钮组件的文章:前端渣渣的我再也不敢说我会写 Button 组件了,我也在思考,此简历应用是否需要封装一个复杂的按钮组件,思前想后,我觉得没必要,仅是对其做了简单的封装,保证在“一定程度”上实现逻辑功能。对于所有的业务按钮组件,皆是基于此组件进行衍生。

image.png

ts
<MyButton size="middle" onClick={() => console.log('点击按钮')}>
  导出PDF
</MyButton>

MyInput 组件

组件代码地址:👉 查看

在制作简历过程中,最重要的是用户信息的输入,我们可以通过 HTML 提供的 input 元素加以实现,这会造成的问题是:

  • 在组件中大量编写 input 代码
  • 需要写一大段的 css 代码加以覆盖原生样式
  • 可能需要重复编写一些额外操作功能的样式代码,如清空输入内容

当一个东西重复出现,在交互、样式上都基本一致,那么我们就需要思考斟酌一下:能否做成通用?

其次对于内容的输入,除 input 外,我们还会使用 textarea 实现,它们最直观的区别莫过于单行文本与多行文本的差异。我们思考一下,能否将两种进行合并,在 MyInput 组件中实现各自的逻辑功能,业务端使用时,不需要根据场景去编写对应的处理逻辑,仅通过一个 type 属性就能得到对应的组件效果。

  • 单行输入框
ts
<MyInput
  value={base.username}
  placeholder="请输入姓名" // 占位文本
  allowClear={true} // 是否显示清除icon
  onChange={(e) => console.log(e.target.value)}
/>
  • 多行输入框
ts
<MyInput
  value={hobby || ''}
  placeholder="你有什么特长爱好呢" // 占位文本
  allowClear={true} // 是否显示清除icon
  onChange={(e) => console.log(e.target.value)}
  type="textarea" // 类型为多行文本
  rows={5} // 输入文本的行数
  maxLength={200} // 最多支持的文本长度
  allowCount={true} // 是否显示底部文本字数
/>

MyUpload

组件代码地址:👉 查看

我们期望能给 HR 留下一个良好的第一印象,照片是最能体现的一个人的精神面貌,所以我们会存在一个简历头像的上传功能,那么问题点在于:如何实现本地文件上传并显示

可能有小伙伴觉得,没必要单独搞个上传组件,这要看你的后期其他的需求上是否也存在上传的功能,我很明确的知悉,后续会有一些功能点,如:导入文件、图片,所以提前实现上传组件。

经验老道的程序员第一反应就是:<input type="file" accept="image/*" />,很快的,第一个版本的 <MyUpload /> 组件实现了,如你所想,该组件职责就是用于图片上传。

随即带来了问题,由于我们写死了 accept="image/*",假设将来有其他资源的文件上传,该怎么办?当然有很多解决方案,这里我采用的解决方案是:封装基础的上传组件,基于此组件衍生出图片类型的上传组件,将来如果有其他资源类型的上传组件,只需要衍生即可。

image.png

会不会有小伙伴存在疑问:为什么不将该 Upload 组件做得更加通用,所有东西都从由业务通过 props 决定呢?我有想过,之所以没这么设计的原因在于:

  • 部分逻辑要在业务端处理
  • 样式 UI 的高度复用

我举个例子,我们默认的 input 样式如下

image.png

而往往我们都会自己实现一套 UI 样式,假设这里我们的样式效果为:

image.png

如果在模版一是这种效果,是不是我得在模版一中实现这个 UI 样式(写一坨 CSS),那模版二呢?模版三呢?包括选择文件之后,隶属于文件处理的部分逻辑,是不是也需要在业务端处理呢?

这很好理解,我举个例子:当我选择一张图片之后,需要得到文件名、文件类型、文件大小,这些需要通过工具函数处理才能得到,这块逻辑放于业务层去处理,这属于业务层的工作吗?小伙伴们细品细品。

资源上传中还实现了一个 FileEvent 类,具体实现如下

ts
class FileEvent {
  public constructor(file: any) {
    this.file = file;
    this.uuid = createUID();
    const types = file?.type?.split('/') || [];
    this.fileType = types.length ? types[0] : '';
    this.base64URL = window.URL.createObjectURL(file); // 本地预览地址
  }

  // 释放创建过的URL,不然会存在性能问题
  // 详情可见 : https://developer.mozilla.org/zh-CN/docs/Web/API/URL/createObjectURL
  public revokeFileBase64URL(base64URL: string) {
    window.URL.revokeObjectURL(base64URL);
  }

  // 上传/取消上传/重试
  public upload() {}
  public cancel() {}
  public retry() {}
}

export default FileEvent;

MyModal

组件代码地址:👉 查看

设计篇-需求功能设计与数据存储方案设计章节中表明,通过弹窗作为简历信息录入的交互效果,静下心思考,貌似这也算是通用的公共组件,主要用于内容信息展示的载体。由于弹窗类型有多种,比如 Toast 消息提示类型,再比如 Confirm 对话确定弹窗,为此 MyModal 组件各自实现了多套弹窗组件。

对于弹窗深度思考,小伙伴们可以阅读优化篇-公共弹窗拆解优化,让职能更加单一

ts
/**
 * @description 所有弹窗组件集合
 * 方式一:
 * import MyModal from '@components/MyModal';
 * <MyModal.Confirm />
 *
 * 方式二:
 * import { Confirm } from '@components/MyModal';
 * <Confirm />
 */
import MyDialog from './MyDialog';
import MyConfirm from './MyConfirm';

export const Dialog = MyDialog;
export const Confirm = MyConfirm;

export default {
  Dialog: MyDialog,
  Confirm: MyConfirm,
};

而在业务中可以很简单的使用

ts
<MyModal.Confirm
  title="确定要打印简历吗?"
  description="请确保信息的正确,目前仅支持单页打印哦~"
  config={{
    cancelBtn: {
      isShow: true,
      callback: () => {},
    },
    submitBtn: {
      isShow: true,
      callback: () => toPrintPdf('彭道宽+前端工程师'),
    },
  }}
/>

MyScrollBox

组件代码地址:👉 查看

该组件的职责功能是:在给定的一个最大高度内,超出高度滚动展示。

我们常常会有一些交互效果是给容器定个最大高度,如果展示内容超出此高度,则在此容器内进行滚动,但往往我们会出现默认的滚动条,及其不美观,于是在去掉滚动条的基础上进行组件封装,从而达到我们期望的效果。下面是业务中的使用

ts
import MyScrollBox from '@common/components/MyScrollBox';

function Resume() {
  const HEADER_HEIGHT = 60;
  const height = document.body.clientHeight;

  return (
    <div styleName="container">
      <MyScrollBox maxHeight={height - HEADER_HEIGHT}>
        <Template.TemplateOne />
      </MyScrollBox>
    </div>
  );
}

简历数据设计

一份简历最为重要的莫过于数据字段的设计,在说字段设计之前,我们往 redux 中添加一份简历信息的 model,进入 app/renderer/store 文件夹中,新增一份代码文件,取名为: resumeModel,然后将其添加到 reducerList 中。

这边通过 TSRcReduxModel 与 TSResume 对其进行了类型约束,小伙伴们可前往 types 查看

ts
// app/renderer/store/index.ts
// 👇 引入我们写好的 model
import globalModel from './globalModel';
import resumeModel from './resumeModel';

// 👇 这里只需要调用 RcReduxModel 实例化一下得到最后的 reduxModel
const reduxModel = new RcReduxModel([globalModel, resumeModel]);

// 👇 无侵入式的使用 Redux,即使你写最原始的 reducer 也照样支持
const reducerList = combineReducers(reduxModel.reducers);

export default createStore(reducerList, applyMiddleware(reduxModel.thunk, logger));
ts
// app/renderer/store/resumeModel.ts
const resumeModel: TSRcReduxModel.Props<TSResume.IntactResume> = {
  namespace: 'resumeModel',
  openSeamlessImmutable: true,
  state: {
    // 简历信息字段
  },
};

export default resumeModel;

接下来让我们愉快的讨论一下简历数据有哪些字段吧?如果按照模块来分,我们是否能划分出下面几大模块?

  • 基本信息
  • 联系方式
  • 求职意向
  • 技能清单
  • 个人评价
  • 荣誉证书
  • 在校经验
  • 工作经验
  • 项目经验

模块划分出来后,剩下的就好办了,下面是一份完整的简历数据格式,关于类型定义可看 resume.d.ts

ts
const userResume = {
  base: {
    avatar: '',
    username: '彭道宽',
    area: '海南·海口',
    school: '湖南瞎说大学',
    major: '软件工程',
    degree: '本科',
    hometown: '汉族',
    onSchoolTime: {
      beginTime: '2015.09',
      endTime: '2019.06',
    },
  },
  contact: {
    phone: '176****2612',
    email: '1063137960@qq.com',
    github: 'https://github.com/PDKSophia',
    juejin: 'https://juejin.cn/user/1838039171075352',
  },
  work: {
    job: '前端工程师',
    city: '广州|成都|海口',
    cityList: ['广州', '成都', '海口'],
  },
  hobby: '篮球、爬山、健身、吉他、街舞',
  skill: '熟悉 Vue.js,了解数据双向绑定原理、阅读过 NextTick 源码|熟悉 React,了解并使用 Hooks 特性,阅读过 redux 源码,开发 rc-redux-model 中间件|阅读过 Antd 部分优秀组件源码,并参考借鉴,开发组内 UI 组件库|了解 Vscode,开发组内项目辅助工具 vscode-beehive-extension 插件|了解 Webpack 编译原理,了解 babel 转码原理,编写过 babel 插件|了解 Electron,了解 Node.js 以及 Git 团队协作开发工具|了解设计模式,对于特定场景,能采用合适的设计模式进行解决|了解 MYSQL,了解数据库优化常用方法|了解基于微信公众号应用开发,采用 Taro 开发微信小程序,具备良好的网络基础知识',
  skillList: [
    '熟悉 Vue.js,了解数据双向绑定原理、阅读过 NextTick 源码',
    '熟悉 React,了解并使用 Hooks 特性,阅读过 redux 源码,开发 rc-redux-model 中间件',
    '阅读过 Antd 部分优秀组件源码,并参考借鉴,开发组内 UI 组件库',
    '了解 Vscode,开发组内项目辅助工具 vscode-beehive-extension 插件',
    '了解 Webpack 编译原理,了解 babel 转码原理,编写过 babel 插件',
    '了解 Electron,了解 Node.js 以及 Git 团队协作开发工具',
    '了解设计模式,对于特定场景,能采用合适的设计模式进行解决',
    '了解 MYSQL,了解数据库优化常用方法',
    '了解基于微信公众号应用开发,采用 Taro 开发微信小程序,具备良好的网络基础知识',
  ],
  evaluation: '投身开源,rc-redux-model 库作者,SugarTurboS Club 开源组织负责人| 掘金 lv3 博主,掘金文章 10w+ 阅读量,github blog 300+ star | 具备良好语言表达能力和沟通能力,能快速融入团队,适应新环境|具有代码洁癖,前后端分离,自我学习能力强,对新技术具有钻研精神',
  evaluationList: [
    '投身开源,rc-redux-model 库作者,SugarTurboS Club 开源组织负责人',
    '掘金 lv3 博主,掘金文章 10w+ 阅读量,github blog 300+ star',
    '具备良好语言表达能力和沟通能力,能快速融入团队,适应新环境。',
    '具有代码洁癖,前后端分离,自我学习能力强,对新技术具有钻研精神',
  ],
  certificate: '广州第一届喝酒大赛参与奖',
  certificateList: ['广州第一届喝酒大赛参与奖'],
  schoolExperience: [
    {
      beginTime: '2016.09',
      endTime: '2017.09',
      post: '文艺部会长',
      department: '校团委学生会',
      content: '计划、组织、协调各年级学生组织的文艺和文化娱乐活动|承办好学生会部的学生文艺晚会。有效地与社团部开展合作项目',
      parseContent: [
        '计划、组织、协调各年级学生组织的文艺和文化娱乐活动',
        '承办好学生会部的学生文艺晚会。有效地与社团部开展合作项目',
      ],
    },
  ],
  workExperience: [
    {
      beginTime: 1504195200000,
      endTime: 1559318400000,
      post: '前端工程师',
      department: '湖南瞎说大学网络中心',
      content: '担任TickNet工作室前端工程师,与湖南瞎说大学网络中心合作,围绕微信企业号开发或主导多个应用|任职期间基于微信企业号开发校内闲余市场,采用Vue.js主导开发,并与湖南xxx科技有限公司合作,主导开发该公司官网及后台管理',
      parseContent: [
        '担任TickNet工作室前端工程师,与湖南瞎说大学网络中心合作,围绕微信企业号开发或主导多个应用',
        '任职期间基于微信企业号开发校内闲余市场,采用Vue.js主导开发,并与湖南xxx科技有限公司合作,主导开发该公司官网及后台管理',
      ],
    },
  ],
  projectExperience: [
    {
      beginTime: '2021.03',
      endTime: '2021.05',
      projectName: 'visResumeMook 可视化简历平台',
      post: '前端工程师',
      content:
        'Electron + React Hooks 打造简历平台,只需输入一次信息,套用多份模版|通过 jsonfile 方式实现主题换肤,支持导出 PDF 简历文档|通过 indexDB 方式实现历史简历缓存,通过可视化拖拽形式,自定义组件模版',
      parseContent: [
        'Electron + React Hooks 打造简历平台,只需输入一次信息,套用多份模版',
        '通过 jsonfile 方式实现主题换肤,支持导出 PDF 简历文档',
        '通过 indexDB 方式实现历史简历缓存,通过可视化拖拽形式,自定义组件模版',
      ],
      date: 1621145137865,
    },
  ],
};

简历模版

简历模板就不带大家开发了,无非就是 HTML + CSS 实现的一套静态简历模版,由于时间有限,目前暂时先支持一份模版,该模版地址在: 👉 template-one

总结

本章节更多的是将部分通用组件、类型定义文件、Utils 工具函数、icon 图标等与主流程功能关系不大的事先在项目中准备好,也许有小伙伴会有疑问,你为什么知道要封装这些组件?

在确定产品原型以及交互效果之后,我们会进入一个叫做技术评审环节,我会将项目中,每个功能点进行拆分,通过什么技术能实现这个功能点,这个功能是否通用,在这样反复提问自己的过程中,抽丝剥茧,最后确定好部分通用模块,从而进行实现。

这章节动手较少,更多的是思考和代码阅读,我希望你不要一下子去看通用组件的实现,而是尝试自己动手写一写,只有动手才能知道何为“乐趣”。最后再结合项目中的组件代码 review 一遍。

如果你真的想了解一个通用组件如何去设计去思考去开发,也许我这篇前端渣渣的我再也不敢说我会写 Button 组件了文章能给你一点帮助。

如果您在边阅读边实践时,发现代码报错或者 TS 报错,那么小伙伴们可以根据报错信息,去线上看看相应的代码。

如果对本章节存在疑问,欢迎在评论区留言。