Frontend Development: Building a Vue2 Hot‑Search Component with Element UI and Axios
This tutorial walks through setting up a Vue2 front‑end environment, installing Node.js, Vue‑CLI, Element UI, and Axios, then creates a reusable hot‑search board component, integrates it into the main app, and even shows a Java‑based Zhihu crawler for data collection.
Introduction
In the earlier parts of this series we purchased a server, configured core middleware such as MySQL and Redis, built backend services, and created a crawler that stores the scraped data. This article shifts the focus to front‑end development, highlighting its visual immediacy and rapid feedback.
Front‑End Application Setup
The author continues to use Vue2 together with Element UI, acknowledging that Vue3 or React are also viable choices. The following steps describe how to prepare the development environment.
1. Front‑End Environment Setup
(1) Install Node.js – download the appropriate version from https://nodejs.org/en/download/ . The recommended version is v16.20.2 .
(2) Verify the installation by opening a command prompt (or typing cmd in the start menu) and running node -v . The displayed version confirms a successful installation.
Because some npm resources are blocked abroad, a domestic mirror is required. Install the mirror with: npm install -g cnpm --registry=https://registry.npmmirror.com If the command hangs, retry with administrator privileges.
(3) Install the global Vue‑CLI scaffolding tool: cnpm install -g @vue/cli
(4) Create the project. Navigate to the desired directory (e.g., E:\vue ) and run: vue create summo-sbmy-front-web Select Vue2 when prompted.
(5) Start the project: npm run serve The application will launch at http://localhost:8080 and display the default "Welcome to Your Vue.js App" page.
2. Scaffold Adjustments
The development tool is VS Code (download from https://code.visualstudio.com/ ). Remove the default HelloWorld.vue component and the logo image, then replace the App.vue template with a minimal structure:
<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>After cleaning up the unnecessary code, the project builds without errors.
3. Adding Axios and Element‑UI
Install the required dependencies:
// axios
cnpm install axios
// element‑ui
cnpm install element-uiRegister the components in main.js :
import Vue from 'vue'
import App from './App.vue'
// Element‑UI components
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(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')4. API Service Wrapper
Create src/config/apiService.js to centralise HTTP calls:
// apiService.js
import axios from "axios";
const apiClient = axios.create({
baseURL: "http://localhost:80/api",
headers: { "Content-Type": "application/json" }
});
export default {
get(fetchUrl) { return apiClient.get(fetchUrl); }
};5. Hot‑Search Board Component
In src/components/HotSearchBoard.vue implement the UI that displays a list of hot‑search items with ranking colours and click‑through links.
<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(err => { console.error(err); })
.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) {
if (typeof heat === "string" && heat.endsWith("万")) return heat;
let number = parseFloat(heat);
if (isNaN(number)) return heat;
if (number < 1000) return number.toString();
if (number >= 1000 && number < 10000) return (number / 1000).toFixed(1) + "k";
return (number / 10000).toFixed(1) + "万";
},
openLink(url) { if (url) window.open(url, "_blank"); }
}
};
</script>
<style scoped>
.custom-card { background:#fff; 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); }
.card-title { display:flex; align-items:center; font-weight:bold; font-size:16px; }
.card-title-icon { 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; border-bottom:1px solid #e8e8e8; }
.cell-order { width:20px; font-size:16px; font-weight:700; margin-right:8px; color:#7a7a7a; }
.top-ranking-1 .cell-order { color:#fadb14; }
.top-ranking-2 .cell-order { color:#a9a9a9; }
.top-ranking-3 .cell-order { color:#d48806; }
.cell-title { font-size:13px; color:#495060; flex-grow:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.cell-title.hover-effect { cursor:pointer; transition:color .3s; }
.cell-title.hover-effect:hover { color:#409eff; }
.cell-heat { min-width:50px; text-align:right; font-size:12px; color:#7a7a7a; }
</style>6. Integrating the Component in App.vue
Register the component and render multiple boards (e.g., Baidu and Douyin) in a responsive grid:
<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" :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; }
</style>Additional Note: Zhihu Hot‑Search Crawler (Java)
The author also provides a scheduled Spring‑Boot job that fetches Zhihu hot‑search data via its public API, parses the JSON response, builds SbmyHotSearchDO objects, and persists them to the database.
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;
/**
* Zhihu hot‑search Java crawler
*/
@Component
@Slf4j
public class ZhihuHotSearchJob {
@Autowired
private SbmyHotSearchService sbmyHotSearchService;
/** Execute every hour */
@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
list = Lists.newArrayList();
for (int i = 0; i < array.size(); i++) {
JSONObject object = (JSONObject) array.get(i);
JSONObject target = object.getJSONObject("target");
SbmyHotSearchDO doObj = SbmyHotSearchDO.builder().hotSearchResource(ZHIHU.getCode()).build();
doObj.setHotSearchId(target.getString("id"));
doObj.setHotSearchUrl("https://www.zhihu.com/question/" + doObj.getHotSearchId());
doObj.setHotSearchTitle(target.getString("title"));
doObj.setHotSearchAuthor(target.getJSONObject("author").getString("name"));
doObj.setHotSearchAuthorAvatar(target.getJSONObject("author").getString("avatar_url"));
doObj.setHotSearchExcerpt(target.getString("excerpt"));
doObj.setHotSearchHeat(object.getString("detail_text").replace("热度", ""));
doObj.setHotSearchOrder(i + 1);
list.add(doObj);
}
sbmyHotSearchService.saveCache2DB(list);
} catch (IOException e) {
log.error("Error fetching Zhihu data", e);
}
}
}The article concludes by encouraging readers to download the code, experiment with the front‑end and back‑end components, and join the discussion via comments or the author's WeChat ID.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.