《花100塊做個摸魚小網站! 》第四篇—前端應用搭建和完成第一個熱搜元件

sum墨發表於2024-08-26

⭐️基礎連結導航⭐️

伺服器 → ☁️ 阿里雲活動地址

看樣例 → 🐟 摸魚小網站地址

學程式碼 → 💻 原始碼庫地址

一、前言

在本系列文章的早期章節中,我們已經成功地購買了伺服器並配置了MySQL、Redis等核心中介軟體。緊接著,我們不僅建立了後端服務,還開發了我們的首個爬蟲程式。後面我們還把爬取到的資料進行了儲存,生成了一整套MVC的後端程式碼,並且提供了一個介面出來。

這篇文章呢我要開始前端開發部分了。與後端開發相比,前端開發的優勢在於其直觀性和即時反饋。開發者可以迅速看到自己程式碼的成果,這種“所見即所得”的體驗極大地提升了開發的樂趣和滿足感。在接下來的篇章中,我將展示如何將爬取到的熱搜資料整合到前端介面中,使之以一種使用者友好的方式呈現,大家姑妄看之。

二、前端應用搭建

我的前端技術棧還停留在四年前,那時候我主要使用的是Vue2和ElementUI。並不是說我認為Vue3或React不好,只是我更習慣使用我熟悉的技術。即便如此,這些技術依然能夠帶來不錯的效果。如果你想要嘗試不同的技術或元件庫,那也是完全可以的。

1. 前端環境搭建

(1)安裝node.js,下載相應版本的node.js,下載地址:https://nodejs.org/en/download/ ,下載完雙擊安裝,點選下一步直到安裝完成,建議下載版本的是:v16.20.2

(2)安裝完成後,附件裡選擇命令提示符(或者在開始的搜尋框裡輸入cmd回車調出命令皮膚)輸入:node -v回車,出現相應版本證明安裝成功, node環境已經安裝完成。

由於有些npm有些資源被遮蔽或者是國外資源的原因,經常會導致用npm安裝依賴包的時候失敗,所有我還需要npm的 國內映象---cnpm。在命令列中輸入:npm install -g cnpm --registry=https://registry.npmmirror.com 回車,大約需要3分鐘,如果一直沒有反應使用管理員身份執行cmd重試。

(3)安裝全域性vue-cli腳手架,用於幫助搭建所需的模板框架。輸入命令:cnpm install -g @vue/cli回車等待完成。

(4)建立專案,首先我們要選定目錄,然後再命令列中把目錄轉到選定的目錄,假如我們打算把專案新建在e盤下的vue資料夾中則輸入下面的命令: e:回車,然後cd vue,然後輸入命令:vue create summo-sbmy-front-web,回車,然後它就會讓你選擇vue2還是vue3,選擇vue2後點選enter,它就會建立好專案並且下載好依賴。

(5)啟動專案,首先切換到summo-sbmy-front-web目錄,然後執行npm run serve,專案執行成功後,瀏覽器會自動開啟localhost:8080(如果瀏覽器沒有自動開啟 ,可以手動輸入)。執行成功後,會看到Welcome to Your Vue.js App頁面。

2. 腳手架處理

我的開發工具是VS Code,免費的,下載地址如下:https://code.visualstudio.com/

(1)檔案和程式碼清理

刪除components下的HelloWorld.vue檔案
刪除assets下的logo.png檔案

原始App.vue程式碼如下

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

刪除不必要的程式碼,不然啟動會報錯

<template>
  <div id="app">
  </div>
</template>

<script>

export default {
  name: 'App',
  components: {
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

(2)axios和element-ui依賴引入

執行安裝命令

//axios依賴引入
cnpm install axios

//element-ui依賴引入
cnpm install element-ui

下載完上面這兩個元件後,去main.js中註冊元件,然後才能使用,main.js程式碼如下:

import Vue from 'vue'
import App from './App.vue'

//引入餓了麼UI
import {
  Calendar,
  Row,
  Col,
  Link,
  Button,
  Loading,
  Container,
  Header,
  Footer,
  Main,
  Form,
  Autocomplete,
  Tooltip,
  Card,
  Dialog
}  from 'element-ui';
Vue.use(Calendar)
Vue.use(Row)
Vue.use(Col)
Vue.use(Link)
Vue.use(Button)
Vue.use(Loading)
Vue.use(Container)
Vue.use(Header)
Vue.use(Footer)
Vue.use(Form)
Vue.use(Autocomplete)
Vue.use(Tooltip)
Vue.use(Card)
Vue.use(Main)
Vue.use(Dialog)
import "element-ui/lib/theme-chalk/index.css"

//引入axios
import axios from 'axios';
Vue.prototype.$ajax = axios;

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

(3)封裝apiService.js方便呼叫介面

在src目錄下建立一個資料夾名為config,建立一個apiService.js,程式碼如下:

// apiService.js
import axios from "axios";

// 建立axios例項並配置基礎URL
const apiClient = axios.create({
  baseURL: "http://localhost:80/api",
  headers: {
    "Content-Type": "application/json"
  },
});

export default {
  // 封裝Get介面
  get(fetchUrl) {
    return apiClient.get(fetchUrl);
  }
};

三、前端熱搜元件

1. 元件介紹

首先,成品的熱搜元件樣式如下,包括標題(圖示+名稱)、內容區(排序、標題、熱度),點選標題可以跳轉到指定的熱搜文章。

2. 元件實現

在components目錄下建立HotSearchBoard.vue檔案,程式碼如下:

<template>
  <el-card class="custom-card" v-loading="loading">
    <template #header>
      <div class="card-title">
        <img :src="icon" class="card-title-icon" />
        {{ title }}熱榜
      </div>
    </template>
    <div class="cell-group-scrollable">
      <div
        v-for="item in hotSearchData"
        :key="item.hotSearchOrder"
        :class="getRankingClass(item.hotSearchOrder)"
        class="cell-wrapper"
      >
        <span class="cell-order">{{ item.hotSearchOrder }}</span>
        <span
          class="cell-title hover-effect"
          @click="openLink(item.hotSearchUrl)"
        >
          {{ item.hotSearchTitle }}
        </span>
        <span class="cell-heat">{{ formatHeat(item.hotSearchHeat) }}</span>
      </div>
    </div>
  </el-card>
</template>

<script>
import apiService from "@/config/apiService.js";
export default {
  props: {
    title: String,
    icon: String,
    type: String,
  },
  data() {
    return {
      hotSearchData: [],
      loading:false
    };
  },
  created() {
    this.fetchData(this.type);
  },
  methods: {
    fetchData(type) {
      this.loading = true
      apiService
        .get("/hotSearch/queryByType?type=" + type)
        .then((res) => {
          // 處理響應資料
          this.hotSearchData = res.data.data;
        })
        .catch((error) => {
          // 處理錯誤情況
          console.error(error);
        }).finally(() => {
          // 載入結束
          this.loading = false; 
        });
    },
    getRankingClass(order) {
      if (order === 1) return "top-ranking-1";
      if (order === 2) return "top-ranking-2";
      if (order === 3) return "top-ranking-3";
      return "";
    },
    formatHeat(heat) {
      // 如果 heat 已經是字串,並且以 "萬" 結尾,那麼直接返回
      if (typeof heat === "string" && heat.endsWith("萬")) {
        return heat;
      }
      let number = parseFloat(heat); // 確保轉換為數值型別進行比較
      if (isNaN(number)) {
        return heat; // 如果無法轉換為數值,則原樣返回
      }

      // 如果數值小於1000,直接返回該數值
      if (number < 1000) {
        return number.toString();
      }

      // 如果數值在1000到9999之間,轉換為k為單位
      if (number >= 1000 && number < 10000) {
        return (number / 1000).toFixed(1) + "k";
      }

      // 如果數值大於等於10000,轉換為萬為單位
      if (number >= 10000) {
        return (number / 10000).toFixed(1) + "萬";
      }
    },
    openLink(url) {
      if (url) {
        // 使用window.open在新標籤頁中開啟連結
        window.open(url, "_blank");
      }
    },
  },
};
</script>

<style scoped>
.custom-card {
  background-color: #ffffff;
  border-radius: 10px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  margin-bottom: 20px;
}
.custom-card:hover {
  box-shadow: 0 6px 8px rgba(0, 0, 0, 0.25);
}

>>> .el-card__header {
  padding: 10px 18px;
}
>>> .el-card__body {
  display: flex;
  padding: 10px 0px 10px 10px;
}
.card-title {
  display: flex;
  align-items: center;
  font-weight: bold;
  font-size: 16px;
}

.card-title-icon {
  fill: currentColor;
  width: 24px;
  height: 24px;
  margin-right: 8px;
}

.cell-group-scrollable {
  max-height: 350px;
  overflow-y: auto;
  padding-right: 16px; /* 恢復內容區域的內邊距 */
  flex: 1;
}

.cell-wrapper {
  display: flex;
  align-items: center;
  padding: 8px 8px; /* 減小上下內邊距以減少間隔 */
  border-bottom: 1px solid #e8e8e8; /* 為每個項之間新增分割線 */
}

.cell-order {
  width: 20px;
  text-align: left;
  font-size: 16px;
  font-weight: 700;
  margin-right: 8px;
  color: #7a7a7a; /* 統一非特殊序號顏色 */
}

/* 透過在cell-heat類前面新增更多的父級選擇器,提高了特異性 */
.cell-heat {
  min-width: 50px;
  text-align: right;
  font-size: 12px;
  color: #7a7a7a;
}
.cell-title {
  font-size: 13px;
  color: #495060;
  line-height: 22px;
  flex-grow: 1;
  overflow: hidden;
  text-align: left; /* 左對齊 */
  text-overflow: ellipsis; /* 超出部分顯示省略號 */
}
.top-ranking-1 .cell-order {
  color: #fadb14;
} /* 金色 */
.top-ranking-2 .cell-order {
  color: #a9a9a9;
} /* 銀色 */
.top-ranking-3 .cell-order {
  color: #d48806;
} /* 銅色 */
/* 新增的.hover-effect類用於標題的hover狀態 */
.cell-title.hover-effect {
  cursor: pointer; /* 滑鼠懸停時顯示指標形狀 */
  transition: color 0.3s ease; /* 平滑地過渡顏色變化 */
}

/* 當滑鼠懸停在帶有.hover-effect類的元素上時改變顏色 */
.cell-title.hover-effect:hover {
  color: #409eff; /* 或者使用你喜歡的顏色 */
}
</style>

在App.vue中新增熱搜元件,由於不止一個熱搜,我把它做成了一個陣列

<template>
  <div id="app">
    <el-row :gutter="10">
      <el-col :span="6" v-for="(board, index) in hotBoards" :key="index">
        <hot-search-board
          :title="board.title"
          :icon="board.icon"
          :fetch-url="board.fetchUrl"
          :type="board.type"
        />
      </el-col>
    </el-row>
  </div>
</template>

<script>
import HotSearchBoard from "@/components/HotSearchBoard.vue";
export default {
  name: "App",
  components: {
    HotSearchBoard,
  },
  data() {
    return {
      hotBoards: [
        {
          title: "百度",
          icon: require("@/assets/icons/baidu-icon.svg"),
          type: "baidu",
        },
        {
          title: "抖音",
          icon: require("@/assets/icons/douyin-icon.svg"),
          type: "douyin",
        },
      ],
    };
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
  background: #f8f9fa; /* 提供一個柔和的背景色 */
  min-height: 100vh; /* 使用視口高度確保填滿整個螢幕 */
  padding: 0; /* 保持整體佈局緊湊,無額外內邊距 */
}
</style>

最終的專案結構和檔案如下

程式碼不難,無非就是使用卡片和列表對熱搜進行展示,還有就是我加了一些樣式,比如前三名的排序有顏色,字型改了改。

四、小結一下

在本篇文章中,我主要展示前端程式碼邏輯。至於後端,也做了一些更新,比如新增了queryType介面和跨域請求的處理,但這些內容都是基礎的,你下載程式碼後就能一目瞭然,不懂的評論區交流,或者加我微信:hb1766435296。之前的準備工作終於開始見到成效,雖然看起來簡單,但實際上解決了不少複雜問題。現在,伺服器、前端和後端的基礎都打好了,接下來我會繼續開發,增加更多功能。

關於爬蟲部分,我已經成功實現了針對12個不同網站的爬取功能。考慮到爬蟲的邏輯相對簡單,無需單獨撰寫文章來詳細說明。因此,我計劃在每篇文章的附錄或額外部分簡要介紹各個熱搜網站的爬蟲邏輯。這樣的安排既能保證資訊的完整性,又不會讓文章顯得過於冗長。

番外:知乎熱搜爬蟲

1. 爬蟲方案評估

知乎熱搜是這樣的, 介面是:https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total

資料還算完備,有標題、熱度、封面、排序等,知乎的熱搜介面返回的資料格式是JSON,這種比返回HTML更簡單。

2. 網頁解析程式碼

這個就可以使用Postman生成呼叫程式碼,流程我就不贅述了,直接上程式碼,ZhihuHotSearchJob:

package com.summo.sbmy.job.zhihu;

import java.io.IOException;
import java.util.List;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;

import com.google.common.collect.Lists;
import com.summo.sbmy.dao.entity.SbmyHotSearchDO;
import com.summo.sbmy.service.SbmyHotSearchService;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import static com.summo.sbmy.common.enums.HotSearchEnum.ZHIHU;

/**
 * @author summo
 * @version DouyinHotSearchJob.java, 1.0.0
 * @description 知乎熱搜Java爬蟲程式碼
 * @date 2024年08月09
 */
@Component
@Slf4j
public class ZhihuHotSearchJob {

    @Autowired
    private SbmyHotSearchService sbmyHotSearchService;

    /**
     * 定時觸發爬蟲方法,1個小時執行一次
     */
    @Scheduled(fixedRate = 1000 * 60 * 60)
    public void hotSearch() throws IOException {
        try {
            //查詢知乎熱搜資料
            OkHttpClient client = new OkHttpClient().newBuilder().build();
            Request request = new Request.Builder().url("https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total")
                .method("GET", null).build();
            Response response = client.newCall(request).execute();
            JSONObject jsonObject = JSONObject.parseObject(response.body().string());
            JSONArray array = jsonObject.getJSONArray("data");
            List<SbmyHotSearchDO> sbmyHotSearchDOList = Lists.newArrayList();
            for (int i = 0, len = array.size(); i < len; i++) {
                //獲取知乎熱搜資訊
                JSONObject object = (JSONObject)array.get(i);
                JSONObject target = object.getJSONObject("target");
                //構建熱搜資訊榜
                SbmyHotSearchDO sbmyHotSearchDO = SbmyHotSearchDO.builder().hotSearchResource(ZHIHU.getCode()).build();
                //設定知乎三方ID
                sbmyHotSearchDO.setHotSearchId(target.getString("id"));
                //設定文章連線
                sbmyHotSearchDO.setHotSearchUrl("https://www.zhihu.com/question/" + sbmyHotSearchDO.getHotSearchId());
                //設定文章標題
                sbmyHotSearchDO.setHotSearchTitle(target.getString("title"));
                //設定作者名稱
                sbmyHotSearchDO.setHotSearchAuthor(target.getJSONObject("author").getString("name"));
                //設定作者頭像
                sbmyHotSearchDO.setHotSearchAuthorAvatar(target.getJSONObject("author").getString("avatar_url"));
                //設定文章摘要
                sbmyHotSearchDO.setHotSearchExcerpt(target.getString("excerpt"));
                //設定熱搜熱度
                sbmyHotSearchDO.setHotSearchHeat(object.getString("detail_text").replace("熱度", ""));
                //按順序排名
                sbmyHotSearchDO.setHotSearchOrder(i + 1);
                sbmyHotSearchDOList.add(sbmyHotSearchDO);
            }
            //資料持久化
            sbmyHotSearchService.saveCache2DB(sbmyHotSearchDOList);
        } catch (IOException e) {
            log.error("獲取知乎資料異常", e);
        }
    }

}

知乎的熱搜資料是自帶唯一ID的,不需要我們手動生成,非常方便。

相關文章