⭐️基础链接导航⭐️

服务器 → ☁️ 阿里云活动地址

看样例 → 🐟 摸鱼小网站地址

学代码 → 💻 源码库地址

一、前言

在本系列文章的早期章节中,我们已经成功地购买了服务器并配置了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的,不需要我们手动生成,非常方便。