Vue全栈重构字节跳动招聘网站(上)

初衷

现如今社区上基于Vue的项目已经多如牛毛了,为了提升自己对vue的进一步理解,一直想找一个界面好看,功能完成的项目练练手。本人在逛各大招聘网站的时候发现了字节跳动的官方招聘网站https://job.bytedance.com/society很适合自己练手。我在看到这个网站后翻了翻其源代码发现这个项目并不是由当下比较流行的框架Vue实现的,思考过后那我能不能用Vue重新打造一个复刻版的网站呢?

此项目仅参考了原版网站的界面设计和功能特点,功能实现方式和代码设计均是由本人独立构思开发完成,预览点这里

数据从哪来?

一个完成的上线项目离不开完整的数据,那么我要做的这个项目真实数据要怎么才能拿到呢?于是自己又默默的打开了原版网站的开发者工具,在Network面板里发现了浏览器请求到的官方API接口。找到了API接口就省心读了,问题是像字节跳动这样的公司对服务端API请求肯定会做跨域访问权限的限制,就算某一个接口能成功的请求到数据,对于一个想要长期作为自己项目访问的接口使用来说也是不稳定的。于是我又想到了接口代理的实现方案,大概的实现思路是使用express搭建一个自己的服务器,包括项目上线后静态资源的托管都会用到。

服务端接口代理的小技巧

对于在浏览器端抓到的API数据接口,纯粹的分析其地址和各种各样的参数无疑是很麻烦的一件事情,有没有一种办法可以一键复用它呢?答案是肯定的!由于node端没有原生的fetch请求方法,这里需要借助一个第三方的node模块node-fetch,这个是可以直接用npm安装的类似于浏览器端原生的fetch请求模块。有了它我们在使用浏览器Network面板里面的接口一键复制功能,具体操作请看下面的演示,详细的API代码案例请点击这里

image

此功能只有高版本的chrome 浏览器有此功能,如果您的浏览器没有此复制选项,请您升级到最新的浏览器后在使用

项目技术架构

为了进一步的提高自己的技术水平和更好的加深对Vue的理解,我选择了零代码开发所有的页面功能(没有使用任何第三方UI库)。

  • 项目前端技术栈
    • Vue 主框架
    • vue-router 路由跳转的官方插件
    • lodash 一个javascript的函数工具库
    • axios 负责HTTP请求的插件
  • 服务端技术栈
    • express 搭建web服务器的nodejs框架
    • node-fetch 类似于浏览器端的fetch请求的polyfill
    • connect-history-api-fallback 解决单页面应用程序在history模式下访问服务端出现404的中间件
  • 项目开发工具
    • vue-cli 快速搭建Vue工程的官方脚手架
    • less css预处理器
    • vscode 轻量的代码编辑器
    • postman 测试调试API接口的工具
    • vue-devtools Vue项目官方调试工具
    • chrome 应用运行/调试环境
    • git 开源版本控制系统
  • 部署环境

项目源码目录

image

项目重要功能剖析

分页器组件 components/pagination.vue 查看源代码点这里

本人在开发这个分页器组件之前也是参考了多个网站的分页功能,各种各样的分页功能各不相同,挑选之后最终确定了自己比较认可的一个,此组件实现的功能如下图所示

image

从上图可以看出这是一个功能完成的分页器组件,基础性的代码这里就不过多的介绍了,具体实现点击这里。下面就主要分析一下在开发过程中遇到的难点!

当鼠标点击分页的数字按钮时,整个分页条会做相应的动态切换。当总页数超过一定的数值后,或者页面切换到某一个范围时会出现相应的省略号代替隐藏的页码数字展示。那这个功能逻辑应该怎么实现呢?

良好的思路是写出优雅代码的第一步,我把分页可能出现的状态分为四种情况

  1. 第一个省略号出现在最大页数前面时
  2. 分页条出现两个省略号时
  3. 省略号出现在最小页数后面时
  4. 分页器总页码小于默认展示的页码个数(分页条没有出现省略号)

根据以上列出的几种情况就可以使用代码去实现了,这里我使用Vue 的一个计算属性visiblePagers进行了动态展示所有出现的页码,组件完整的代码如下

<template>
  <div class="pagination">
    <ul class="pagination-list">
      <li
        title="上一页"
        @click="$emit('update:currentPage',Math.max(1,currentPage-1))"
        class="pagination-item"
      >
        <span><</span>
      </li>
      <li
        class="pagination-item"
        :class="{current:currentPage===item}"
        v-for="(item,index) in visiblePages"
        @click="change(item)"
        :key="index"
      >
        <span>{{item}}</span>
      </li>
      <li
        title="下一页"
        @click="$emit('update:currentPage',Math.min(totalPage,currentPage+1))"
        class="pagination-item"
      >
        <span>></span>
      </li>
    </ul>
  </div>
</template>
<script>
export default {
  name: "Pagination",
  props: {
    total: Number,
    perPage: {
      type: Number,
      default: 10
    },
    currentPage: {
      type: Number,
      default: 1
    },
    pagerCount: {
      type: Number,
      default: 9
    }
  },
  computed: {
    totalPage() {
      return Math.ceil(parseInt(this.total) / this.perPage);
    },
    visiblePages() {
      let pages = [];
      const currentPage = Math.max(
        1,
        Math.min(this.currentPage, this.totalPage)
      );
       

      if (this.totalPage <= this.pagerCount) {
        for (let i = 1; i <= this.totalPage; i++) {
          pages.push(i);
        }
        return pages;
      }

      if (currentPage >= this.totalPage - 3) {
        pages.push(1, "...");
        const minPage = Math.min(currentPage - 2, this.totalPage - 4);
        for (let i = minPage, len = this.totalPage; i <= len; i++) {
          pages.push(i);
        }
      } else if (currentPage <= 4) {
        const maxPage = Math.min(Math.max(currentPage + 2, 5), this.totalPage);
        for (let i = 1; i <= maxPage; i++) {
          pages.push(i);
        }
        pages.push("...", this.totalPage);
      } else {
        pages.push(1, "...");
        for (let i = currentPage - 2; i <= currentPage + 2; i++) {
          pages.push(i);
        }
        pages.push("...", this.totalPage);
      }
      return pages;
    }
  },
  methods: {
    change(num) {
      if (typeof num !== "number") {
        return;
      }
      this.$emit("current-change", num);
      this.$emit("update:currentPage", num);
    }
  }
};
</script>
<style lang="less" scoped>
.pagination-list {
  display: flex;
}
.pagination-item {
  margin-right: 4px;
  cursor: pointer;
  padding: 8px;
  &:hover {
    color: @main-color;
  }
  &.current {
    color: @main-color;
  }
}
</style>

复选穿梭框组件 components/checkbox-transfer.vue 源代码地址

效果图

image

实现过程

对于这个组件功能的开发,我真的是煞费苦心,一言难尽。首先市面上没有一样的功能需求用例可供参考,其次在开发的过程中组件各种逻辑的实现也是在摸索着进行实现。在花费了一定时间仍没有较好的思路后,我默默的打开了世界上最大的程序员同性交友平台 github一番搜索,最终在开源项目基于Vue的组件库element-ui中的transfer组件中找到了可参考实现的逻辑实现方式。

重点逻辑分析

template 部分代码

<template>
  <div class="checkbox">
    <h2>{{title}}</h2>

    <ul class="checkbox-list">
      <li class="checkbox-item" v-for="(item, index) in targetData" :key="index">
        <input
          @change="check(item, $event)"
          type="checkbox"
          :id="item[props.key]"
          :checked="checked[index]"
        />
        <label :for="item[props.key]" class="label-text">{{ item[props.label] }}</label>
      </li>
    </ul>
    <div class="search" v-if="sourceData.length">
      <input
        @blur="onInputBlur"
        @focus="focusing = true"
        class="search-input"
        :class="{focusing}"
        :placeholder="placeholder"
        type="text"
        v-model="filterKeyword"
      />
      <ul class="search-list" v-show="focusing">
        <li
          v-for="item in filterableData"
          :key="item[props.key]"
          class="search-item"
          @click="addToTarget(item)"
        >
          <span>{{ item[props.label] }}</span>
        </li>
      </ul>
    </div>
  </div>
</template>

对于这样一个交互复杂的复选穿梭框而言,定义好其基本的初始化数据状态是第一步要做的。对于此组件调用的时候,模板会传入一个属性名为data(这里是一个数组)的props选项作为组件的数据来源,接下来要关注的焦点就转移到了组件内部数据交互的各种实现方式上了。我首先在组件状态data里面定义了一个名叫targets的数组类型的变量用来存储默认展开的复选框列表项key值,然后根据datatargets这两个基础数据状态又可以派生出两个计算属性sourceDatatargetData用来分别渲染展开和隐藏起来的复选框选择项。至此组件基本的静态模板渲染的逻辑就构思完成了。相关部分代码如下

组件script部分

<script>
export default {
  name: "checkbox-transfer",
  data() {
    return {
      focusing: false,
      filterKeyword: "",
      targets: []
    };
  },
  props: {
    data: {
      type: Array,
      default: () => []
    },
  },
  computed: {
    targetData() {
      return this.targets
        .map(key => {
          return this.data.find(item => item[this.props.key] === key);
        })
        .filter(item => item && item[this.props.key]);
    },
    sourceData() {
      return this.data.filter(
        item => this.targets.indexOf(item[this.props.key]) === -1
      );
    },
  },

另外此组件也拥有Vue组件代表性的双向数据绑定的特点,使用v-model的指令可以默认指定复选框选中的选项,有关这一块的逻辑实现在这里就不在赘述了,相关逻辑代码看下面

组件script部分

<script>
export default {
  name: "checkbox-transfer",
  props: {
    value: {
      type: Array,
      default: () => []
    }
  },

  computed: {
    checked() {
      return this.targets.map(key => this.value.includes(key));
    },
  },
 methods:{
     check(item, e) {
      if (!e.target.checked) {
        const delIndex = this.value.indexOf(item[this.props.key]);
        if (delIndex > -1) {
          this.value.splice(delIndex, 1);
        }
      } else {
        if (!this.value.includes(item[this.props.key])) {
          this.value.push(item[this.props.key]);
        }
      }
    
      this.$emit("check", e.target.checked, item[this.props.key]);
      this.$emit("input", this.value);
    }
  }
 }
   

首页头部导航栏交互显示功能

查看源码点这里

效果

image

功能介绍

  1. 顶部的导航栏在页面向下滚动的过程中有一个吸附顶部的功能
  2. banner视频部分滚动出页面可视区域后导航栏更换主题颜色
  3. 导航栏根据页面滚动方向的不同隐藏和显示。

实现思路

导航栏功能的前两个实现起来比较简单,这里就不多介绍了。下面主要分析一下功能中的第三点。

根据页面滚动的方向判断导航栏显示状态要怎么实现呢?单纯的做一个显示状态的切换对于熟练使用Vue的同学来说再简单不过了,这里的坑就在于如何判断页面在滚动过程中的滚动方向呢?说道这里,有人一定能想到可以使用浏览器原生API监听元素滚动的事件,在事件scroll的回调函数中进一步处理逻辑判断。能想到这一点对于我们要实现的最终目标有迈进了一大步,那么浏览器HTML元素的scroll事件能提供给我们使用的回调参数是有限的,就是说这个事件对象没有直接提供此次滚动的方向信息。所以这个问题就需要我们手动去解决了。

手动封装监听元素滚动的函数

为了判断出元素此次滚动事件相对于上次滚动时的方向,我们需要记录上一次的滚动事件信息并存储起来,然后通过比对两次滚动事件的坐标值判断出此次页面滚动的方向(这里只做滚动向下或者向上的判断)。为了让这一个判断元素滚动方向的逻辑有更好的复用性,我把它单独抽离成了一个工具函数,当我们需要用到这个逻辑时就可以直接拿来复用,具体实现的代码如下

helper/untilities.js

export const watchScrollDirection = function(scrollElement, callback) {
  const scrollPos = { x: 0, y: 0 };
  const scrollDirection = {
    directionX: 1,
    directionY: 1,
  };

  function onScroll(e) {
    const scrollTop = scrollElement.scrollTop || scrollElement.pageYOffset;
    const scrollLeft = scrollElement.scrollLeft || scrollElement.pageXOffset;

    if (scrollPos.y > scrollTop) {
      scrollDirection.directionY = -1;
    } else {
      scrollDirection.directionY = 1;
    }
    if (scrollPos.x > scrollLeft) {
      scrollDirection.directionX = -1;
    } else {
      scrollDirection.directionX = 1;
    }
    callback.call(scrollElement, scrollDirection,scrollPos);

    scrollPos.x = scrollLeft;
    scrollPos.y = scrollTop;
  }
  scrollElement.addEventListener("scroll", onScroll);
  return function() {
    scrollElement.removeEventListener("scroll", onScroll);
  };
};

页面代码实现如下

views/home.vue组件script部分

import { watchScrollDirection } from "@/helper/utilities.js";

export default {
    mounted() {
        const rootVm = this.$root;
        rootVm.$emit(
          "home-scrolling",
          { directionX: 1, directionY: -1 },
          { x: document.body.scrollLeft, y: document.body.scrollTop }
        );
        this.unwatch = watchScrollDirection(window, function(...args) {
          rootVm.$emit("home-scrolling", ...args);
        });
      },
    destroyed() {
       this.unwatch();
    }
}

应用截图

首页

image

职位

image

产品与服务

image

职位详情

image

员工故事

image

支持

阅读完本篇文章如果对您有帮助,请您点赞,谢谢!

如果想讨论项目有关问题或者参与共建,欢迎留言!

对本项目如果您有更好的建议或发现bug,欢迎提 issue

应用线上地址: http://123.56.124.33:3000/

项目仓库地址: https://github.com/konglingwen94/vue-bytedanceJob,欢迎starfollow,谢谢!

本篇文章属于个人原创,转载借鉴请注明出处,谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容