《花100块做个摸鱼小网站! 》第四篇—前端应用搭建和完成第一个热搜组件

sum墨
• 阅读 86

⭐️基础链接导航⭐️

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

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

学代码 → 💻 源码库地址

一、前言

在本系列文章的早期章节中,我们已经成功地购买了服务器并配置了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环境已经安装完成。 《花100块做个摸鱼小网站! 》第四篇—前端应用搭建和完成第一个热搜组件 由于有些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,它就会创建好项目并且下载好依赖。 《花100块做个摸鱼小网站! 》第四篇—前端应用搭建和完成第一个热搜组件

(5)启动项目,首先切换到summo-sbmy-front-web目录,然后执行npm run serve,项目运行成功后,浏览器会自动打开localhost:8080(如果浏览器没有自动打开 ,可以手动输入)。运行成功后,会看到Welcome to Your Vue.js App页面。 《花100块做个摸鱼小网站! 》第四篇—前端应用搭建和完成第一个热搜组件

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. 组件介绍

首先,成品的热搜组件样式如下,包括标题(图标+名称)、内容区(排序、标题、热度),点击标题可以跳转到指定的热搜文章。 《花100块做个摸鱼小网站! 》第四篇—前端应用搭建和完成第一个热搜组件

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>

最终的项目结构和文件如下 《花100块做个摸鱼小网站! 》第四篇—前端应用搭建和完成第一个热搜组件 代码不难,无非就是使用卡片和列表对热搜进行展示,还有就是我加了一些样式,比如前三名的排序有颜色,字体改了改。

四、小结一下

在本篇文章中,我主要展示前端代码逻辑。至于后端,也做了一些更新,比如新增了queryType接口和跨域请求的处理,但这些内容都是基础的,你下载代码后就能一目了然,不懂的评论区交流,或者加我微信:hb1766435296。之前的准备工作终于开始见到成效,虽然看起来简单,但实际上解决了不少复杂问题。现在,服务器、前端和后端的基础都打好了,接下来我会继续开发,增加更多功能。

关于爬虫部分,我已经成功实现了针对12个不同网站的爬取功能。考虑到爬虫的逻辑相对简单,无需单独撰写文章来详细说明。因此,我计划在每篇文章的附录或额外部分简要介绍各个热搜网站的爬虫逻辑。这样的安排既能保证信息的完整性,又不会让文章显得过于冗长。

番外:知乎热搜爬虫

1. 爬虫方案评估

知乎热搜是这样的, 接口是:https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total

《花100块做个摸鱼小网站! 》第四篇—前端应用搭建和完成第一个热搜组件

数据还算完备,有标题、热度、封面、排序等,知乎的热搜接口返回的数据格式是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的,不需要我们手动生成,非常方便。

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
kenx kenx
3年前
Spring Boot 无侵入式 实现RESTful API接口统一JSON格式返回
前言现在我们做项目基本上中大型项目都是选择前后端分离,前后端分离已经成了一个趋势了,所以总这样·我们就要和前端约定统一的api接口返回json格式,这样我们需要封装一个统一通用全局模版api返回格式,下次再写项目时候直接拿来用就可以了约定JSON格式一般我们和前端约定json格式是这样的json"code":200,"message
Easter79 Easter79
3年前
springboot2之优雅处理返回值
前言最近项目组有个老项目要进行前后端分离改造,应前端同学的要求,其后端提供的返回值格式需形如{"status":0,"message":"success","data":{}}方便前端数据处理。要实现前端同学这个需求,其实也挺简单的,
Stella981 Stella981
3年前
Kerberos无约束委派的攻击和防御
 0x00前言简介当ActiveDirectory首次与Windows2000Server一起发布时,Microsoft就提供了一种简单的机制来支持用户通过Kerberos对Web服务器进行身份验证并需要授权用户更新后端数据库服务器上的记录的方案。这通常被称为Kerberosdoublehopissue(双跃点问题),
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
sum墨 sum墨
1星期前
《花100块做个摸鱼小网站! 》第二篇—后端应用搭建和完成第一个爬虫
大家好呀,我是summo,前面已经教会大家怎么去阿里云买服务器(链接在这,需要自取),以及怎么搭建JDK、Redis、MySQL这些环境。从这篇文章开始就进入正式的编码阶段了,我们从后端开始,先把热搜数据获取到,然后再开始前端部分。
sum墨 sum墨
1星期前
《花100块做个摸鱼小网站! 》第三篇—热搜表结构设计和热搜数据存储
大家好呀,我是summo,第一篇已经教会大家怎么去阿里云买服务器,以及怎么搭建JDK、Redis、MySQL这些环境。第二篇我们把后端的应用搭建好了,并且完成了第一个爬虫(抖音)。那么这一篇我会教大家如何将爬取到的数据保存到数据库,并且可以通过接口获取到,为后面的前端界面提供数据源。
sum墨 sum墨
1星期前
《花100块做个摸鱼小网站! 》第五篇—通过xxl-job定时获取热搜数据
我们已经成功实现了一个完整的热搜组件,从后端到前端,构建了这个小网站的核心功能。接下来,我们将不断完善其功能,使其更加美观和实用。今天的主题是如何定时获取热搜数据。如果热搜数据无法定时更新,小网站将失去其核心价值。之前,我采用了@Scheduled注解来实现定时任务,但这种方式灵活性不足,因此我决定用更灵活的XXLJob组件来替代它。
sum墨 sum墨
1星期前
《优化接口设计的思路》系列:第十一篇—表格的导入导出接口优化
在后端开发中,我们经常处理增删改查的接口,这些操作已经非常熟悉了。然而,有时产品经理会要求增加一个表格数据的导入和导出功能,让用户可以离线处理数据。这类操作也很常见,但处理起来不太简单。尽管一些前端表格组件可以直接实现这类功能,但往往不够灵活,因为前端的数据通常已经过处理。如果要获取原始数据,还是得依靠后端处理。