Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

项目总结 ( 上 ) #7

Open
konglingwen94 opened this issue Sep 10, 2020 · 0 comments
Open

项目总结 ( 上 ) #7

konglingwen94 opened this issue Sep 10, 2020 · 0 comments

Comments

@konglingwen94
Copy link
Owner

初衷

现如今社区上基于Vue的项目已经多如牛毛了,为了提升自己对vue的进一步理解,一直想找一个界面好看,功能完成的项目练练手。本人在逛各大招聘网站的时候发现了字节跳动的官方招聘网站很适合自己练手。当我发现这个网站是由react实现的,想了一下那我能不能用Vue的技术栈完整的重构出来呢?

预览点这里

数据从哪来?

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

服务端接口代理的小技巧

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

此功能只有高版本的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 开源版本控制系统
  • 部署环境

项目源码目录

项目重要功能剖析

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

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


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

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

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

  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 源代码地址

效果图

实现过程

对于这个组件功能的开发,我真的是煞费苦心,一言难尽。首先市面上没有一样的功能需求用例可供参考,其次在开发的过程中组件各种逻辑的实现也是在摸索着进行实现。在花费了一定时间仍没有较好的思路后,我默默的打开了世界上最大的程序员同性交友平台 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);
    }
  }
 }
   

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

查看源码点这里

效果

功能介绍

  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();
    }
}

应用截图

首页

职位

产品与服务

职位详情

员工故事

支持

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

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

关于本项目出现的bug或者有好的建议,欢迎提 issue

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

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

@konglingwen94 konglingwen94 changed the title Vue全栈技术重构字节跳动招聘网站 ( 上 ) 项目总结 ( 上 ) Sep 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant