Vue全棧重構(gòu)字節(jié)跳動(dòng)招聘網(wǎng)站(上)

初衷

現(xiàn)如今社區(qū)上基于Vue的項(xiàng)目已經(jīng)多如牛毛了,為了提升自己對(duì)vue的進(jìn)一步理解,一直想找一個(gè)界面好看,功能完成的項(xiàng)目練練手。本人在逛各大招聘網(wǎng)站的時(shí)候發(fā)現(xiàn)了字節(jié)跳動(dòng)的官方招聘網(wǎng)站https://job.bytedance.com/society很適合自己練手。我在看到這個(gè)網(wǎng)站后翻了翻其源代碼發(fā)現(xiàn)這個(gè)項(xiàng)目并不是由當(dāng)下比較流行的框架Vue實(shí)現(xiàn)的,思考過(guò)后那我能不能用Vue重新打造一個(gè)復(fù)刻版的網(wǎng)站呢?

此項(xiàng)目?jī)H參考了原版網(wǎng)站的界面設(shè)計(jì)和功能特點(diǎn),功能實(shí)現(xiàn)方式和代碼設(shè)計(jì)均是由本人獨(dú)立構(gòu)思開發(fā)完成,預(yù)覽點(diǎn)這里

數(shù)據(jù)從哪來(lái)?

一個(gè)完成的上線項(xiàng)目離不開完整的數(shù)據(jù),那么我要做的這個(gè)項(xiàng)目真實(shí)數(shù)據(jù)要怎么才能拿到呢?于是自己又默默的打開了原版網(wǎng)站的開發(fā)者工具,在Network面板里發(fā)現(xiàn)了瀏覽器請(qǐng)求到的官方API接口。找到了API接口就省心讀了,問(wèn)題是像字節(jié)跳動(dòng)這樣的公司對(duì)服務(wù)端API請(qǐng)求肯定會(huì)做跨域訪問(wèn)權(quán)限的限制,就算某一個(gè)接口能成功的請(qǐng)求到數(shù)據(jù),對(duì)于一個(gè)想要長(zhǎng)期作為自己項(xiàng)目訪問(wèn)的接口使用來(lái)說(shuō)也是不穩(wěn)定的。于是我又想到了接口代理的實(shí)現(xiàn)方案,大概的實(shí)現(xiàn)思路是使用express搭建一個(gè)自己的服務(wù)器,包括項(xiàng)目上線后靜態(tài)資源的托管都會(huì)用到。

服務(wù)端接口代理的小技巧

對(duì)于在瀏覽器端抓到的API數(shù)據(jù)接口,純粹的分析其地址和各種各樣的參數(shù)無(wú)疑是很麻煩的一件事情,有沒(méi)有一種辦法可以一鍵復(fù)用它呢?答案是肯定的!由于node端沒(méi)有原生的fetch請(qǐng)求方法,這里需要借助一個(gè)第三方的node模塊node-fetch,這個(gè)是可以直接用npm安裝的類似于瀏覽器端原生的fetch請(qǐng)求模塊。有了它我們?cè)谑褂脼g覽器Network面板里面的接口一鍵復(fù)制功能,具體操作請(qǐng)看下面的演示,詳細(xì)的API代碼案例請(qǐng)點(diǎn)擊這里

image

此功能只有高版本的chrome 瀏覽器有此功能,如果您的瀏覽器沒(méi)有此復(fù)制選項(xiàng),請(qǐng)您升級(jí)到最新的瀏覽器后在使用

項(xiàng)目技術(shù)架構(gòu)

為了進(jìn)一步的提高自己的技術(shù)水平和更好的加深對(duì)Vue的理解,我選擇了零代碼開發(fā)所有的頁(yè)面功能(沒(méi)有使用任何第三方UI庫(kù))。

  • 項(xiàng)目前端技術(shù)棧
    • Vue 主框架
    • vue-router 路由跳轉(zhuǎn)的官方插件
    • lodash 一個(gè)javascript的函數(shù)工具庫(kù)
    • axios 負(fù)責(zé)HTTP請(qǐng)求的插件
  • 服務(wù)端技術(shù)棧
    • express 搭建web服務(wù)器的nodejs框架
    • node-fetch 類似于瀏覽器端的fetch請(qǐng)求的polyfill
    • connect-history-api-fallback 解決單頁(yè)面應(yīng)用程序在history模式下訪問(wèn)服務(wù)端出現(xiàn)404的中間件
  • 項(xiàng)目開發(fā)工具
    • vue-cli 快速搭建Vue工程的官方腳手架
    • less css預(yù)處理器
    • vscode 輕量的代碼編輯器
    • postman 測(cè)試調(diào)試API接口的工具
    • vue-devtools Vue項(xiàng)目官方調(diào)試工具
    • chrome 應(yīng)用運(yùn)行/調(diào)試環(huán)境
    • git 開源版本控制系統(tǒng)
  • 部署環(huán)境

項(xiàng)目源碼目錄

image

項(xiàng)目重要功能剖析

分頁(yè)器組件 components/pagination.vue 查看源代碼點(diǎn)這里

本人在開發(fā)這個(gè)分頁(yè)器組件之前也是參考了多個(gè)網(wǎng)站的分頁(yè)功能,各種各樣的分頁(yè)功能各不相同,挑選之后最終確定了自己比較認(rèn)可的一個(gè),此組件實(shí)現(xiàn)的功能如下圖所示

image

從上圖可以看出這是一個(gè)功能完成的分頁(yè)器組件,基礎(chǔ)性的代碼這里就不過(guò)多的介紹了,具體實(shí)現(xiàn)點(diǎn)擊這里。下面就主要分析一下在開發(fā)過(guò)程中遇到的難點(diǎn)!

當(dāng)鼠標(biāo)點(diǎn)擊分頁(yè)的數(shù)字按鈕時(shí),整個(gè)分頁(yè)條會(huì)做相應(yīng)的動(dòng)態(tài)切換。當(dāng)總頁(yè)數(shù)超過(guò)一定的數(shù)值后,或者頁(yè)面切換到某一個(gè)范圍時(shí)會(huì)出現(xiàn)相應(yīng)的省略號(hào)代替隱藏的頁(yè)碼數(shù)字展示。那這個(gè)功能邏輯應(yīng)該怎么實(shí)現(xiàn)呢?

良好的思路是寫出優(yōu)雅代碼的第一步,我把分頁(yè)可能出現(xiàn)的狀態(tài)分為四種情況

  1. 第一個(gè)省略號(hào)出現(xiàn)在最大頁(yè)數(shù)前面時(shí)
  2. 分頁(yè)條出現(xiàn)兩個(gè)省略號(hào)時(shí)
  3. 省略號(hào)出現(xiàn)在最小頁(yè)數(shù)后面時(shí)
  4. 分頁(yè)器總頁(yè)碼小于默認(rèn)展示的頁(yè)碼個(gè)數(shù)(分頁(yè)條沒(méi)有出現(xiàn)省略號(hào))

根據(jù)以上列出的幾種情況就可以使用代碼去實(shí)現(xiàn)了,這里我使用Vue 的一個(gè)計(jì)算屬性visiblePagers進(jìn)行了動(dòng)態(tài)展示所有出現(xiàn)的頁(yè)碼,組件完整的代碼如下

<template>
  <div class="pagination">
    <ul class="pagination-list">
      <li
        title="上一頁(yè)"
        @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="下一頁(yè)"
        @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>

復(fù)選穿梭框組件 components/checkbox-transfer.vue 源代碼地址

效果圖

image

實(shí)現(xiàn)過(guò)程

對(duì)于這個(gè)組件功能的開發(fā),我真的是煞費(fèi)苦心,一言難盡。首先市面上沒(méi)有一樣的功能需求用例可供參考,其次在開發(fā)的過(guò)程中組件各種邏輯的實(shí)現(xiàn)也是在摸索著進(jìn)行實(shí)現(xiàn)。在花費(fèi)了一定時(shí)間仍沒(méi)有較好的思路后,我默默的打開了世界上最大的程序員同性交友平臺(tái) github一番搜索,最終在開源項(xiàng)目基于Vue的組件庫(kù)element-ui中的transfer組件中找到了可參考實(shí)現(xiàn)的邏輯實(shí)現(xiàn)方式。

重點(diǎn)邏輯分析

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>

對(duì)于這樣一個(gè)交互復(fù)雜的復(fù)選穿梭框而言,定義好其基本的初始化數(shù)據(jù)狀態(tài)是第一步要做的。對(duì)于此組件調(diào)用的時(shí)候,模板會(huì)傳入一個(gè)屬性名為data(這里是一個(gè)數(shù)組)的props選項(xiàng)作為組件的數(shù)據(jù)來(lái)源,接下來(lái)要關(guān)注的焦點(diǎn)就轉(zhuǎn)移到了組件內(nèi)部數(shù)據(jù)交互的各種實(shí)現(xiàn)方式上了。我首先在組件狀態(tài)data里面定義了一個(gè)名叫targets的數(shù)組類型的變量用來(lái)存儲(chǔ)默認(rèn)展開的復(fù)選框列表項(xiàng)key值,然后根據(jù)datatargets這兩個(gè)基礎(chǔ)數(shù)據(jù)狀態(tài)又可以派生出兩個(gè)計(jì)算屬性sourceDatatargetData用來(lái)分別渲染展開和隱藏起來(lái)的復(fù)選框選擇項(xiàng)。至此組件基本的靜態(tài)模板渲染的邏輯就構(gòu)思完成了。相關(guān)部分代碼如下

組件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組件代表性的雙向數(shù)據(jù)綁定的特點(diǎn),使用v-model的指令可以默認(rèn)指定復(fù)選框選中的選項(xiàng),有關(guān)這一塊的邏輯實(shí)現(xiàn)在這里就不在贅述了,相關(guān)邏輯代碼看下面

組件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);
    }
  }
 }
   

首頁(yè)頭部導(dǎo)航欄交互顯示功能

查看源碼點(diǎn)這里

效果

image

功能介紹

  1. 頂部的導(dǎo)航欄在頁(yè)面向下滾動(dòng)的過(guò)程中有一個(gè)吸附頂部的功能
  2. banner視頻部分滾動(dòng)出頁(yè)面可視區(qū)域后導(dǎo)航欄更換主題顏色
  3. 導(dǎo)航欄根據(jù)頁(yè)面滾動(dòng)方向的不同隱藏和顯示。

實(shí)現(xiàn)思路

導(dǎo)航欄功能的前兩個(gè)實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單,這里就不多介紹了。下面主要分析一下功能中的第三點(diǎn)。

根據(jù)頁(yè)面滾動(dòng)的方向判斷導(dǎo)航欄顯示狀態(tài)要怎么實(shí)現(xiàn)呢?單純的做一個(gè)顯示狀態(tài)的切換對(duì)于熟練使用Vue的同學(xué)來(lái)說(shuō)再簡(jiǎn)單不過(guò)了,這里的坑就在于如何判斷頁(yè)面在滾動(dòng)過(guò)程中的滾動(dòng)方向呢?說(shuō)道這里,有人一定能想到可以使用瀏覽器原生API監(jiān)聽元素滾動(dòng)的事件,在事件scroll的回調(diào)函數(shù)中進(jìn)一步處理邏輯判斷。能想到這一點(diǎn)對(duì)于我們要實(shí)現(xiàn)的最終目標(biāo)有邁進(jìn)了一大步,那么瀏覽器HTML元素的scroll事件能提供給我們使用的回調(diào)參數(shù)是有限的,就是說(shuō)這個(gè)事件對(duì)象沒(méi)有直接提供此次滾動(dòng)的方向信息。所以這個(gè)問(wèn)題就需要我們手動(dòng)去解決了。

手動(dòng)封裝監(jiān)聽元素滾動(dòng)的函數(shù)

為了判斷出元素此次滾動(dòng)事件相對(duì)于上次滾動(dòng)時(shí)的方向,我們需要記錄上一次的滾動(dòng)事件信息并存儲(chǔ)起來(lái),然后通過(guò)比對(duì)兩次滾動(dòng)事件的坐標(biāo)值判斷出此次頁(yè)面滾動(dòng)的方向(這里只做滾動(dòng)向下或者向上的判斷)。為了讓這一個(gè)判斷元素滾動(dòng)方向的邏輯有更好的復(fù)用性,我把它單獨(dú)抽離成了一個(gè)工具函數(shù),當(dāng)我們需要用到這個(gè)邏輯時(shí)就可以直接拿來(lái)復(fù)用,具體實(shí)現(xiàn)的代碼如下

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

頁(yè)面代碼實(shí)現(xiàn)如下

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

應(yīng)用截圖

首頁(yè)

image

職位

image

產(chǎn)品與服務(wù)

image

職位詳情

image

員工故事

image

支持

閱讀完本篇文章如果對(duì)您有幫助,請(qǐng)您點(diǎn)贊,謝謝!

如果想討論項(xiàng)目有關(guān)問(wèn)題或者參與共建,歡迎留言!

對(duì)本項(xiàng)目如果您有更好的建議或發(fā)現(xiàn)bug,歡迎提 issue

應(yīng)用線上地址: http://123.56.124.33:3000/

項(xiàng)目倉(cāng)庫(kù)地址: https://github.com/konglingwen94/vue-bytedanceJob,歡迎starfollow,謝謝!

本篇文章屬于個(gè)人原創(chuàng),轉(zhuǎn)載借鑒請(qǐng)注明出處,謝謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容