开源软件的国内镜像站 powered by Vue3
我通过模仿、学习、试错,成功地将一个 Vue2 项目升级到 Vue3。
这个项目的最初想法来自 lework/lemonitor,这个项目用到的主要技术有 Vue CLI、Vue Router、Vuex 和 Vue2。我对构建这个镜像站产生了兴趣,想了解具体实现,但不想继续使用 Vue2,最终决定把这个项目用 Vue3 实现。用到的主要技术有 Vite、Vue Router、Pinia 和 Vue3。Vite 替代 Vue CLI,Pinia 替代 Vuex,Vue 使用当前最新版本。
Vite 是一种项目构建工具,它能提升开发效率,让开发者更快乐。与之类似的有:Webpack、Rollup、Parcel 等。
Vue Router 是一种 Vue.js 官方推荐的路由设置工具。v3 对应 Vue2,v4 对应 Vue3。简单来说,Vue Router 就像是网站的地图,当访客点击按钮或链接,就能被带到对应的网页。
Pinia 是 Vue.js 官方推荐的存储库,它能让 Vue 组件之间共享同一个状态,帮助开发者管理 Web 应用的数据和状态。
Vue3 是 JavaScript 框架 Vue.js 的最新主版本。它能帮助开发构建出更好的用户界面(UI)。
src 文件夹文件层次:
1├── src
2│ ├── App.vue
3│ ├── assets
4│ │ ├── base.css
5│ │ └── main.css
6│ ├── main.ts
7│ ├── router
8│ │ └── index.ts
9│ ├── stores
10│ │ └── loading.ts
11│ └── views
12│ └── HomeView.vue
主要文件是 main.ts, App.vue。
1// main.ts
2import { createApp } from 'vue'
3import { createPinia } from 'pinia'
4
5import App from './App.vue'
6import router from './router'
7
8const app = createApp(App)
9
10app.use(createPinia())
11
12app.use(router)
13
14app.mount('#app')
根据 Vue,Vue Router 和 Pinia 网站的入门教程,可以得到以上结构。app.use() 的作用是:在项目的其他位置可以直接使用对应的模块。
router:
1// router/index.ts
2import { createRouter, createWebHistory } from 'vue-router'
3import HomeView from '../views/HomeView.vue'
4
5const router = createRouter({
6 history: createWebHistory(import.meta.env.BASE_URL),
7 routes: [
8 {
9 path: '/',
10 name: 'Home',
11 component: HomeView
12 }
13 ]
14})
15
16export default router
createWebHistory
产生的链接没有 #,这样更干净。
pinia:
1// stores/loading.ts
2import { ref } from 'vue'
3import { defineStore } from 'pinia'
4
5export const useLoadingStore = defineStore('loading', () => {
6 const isLoading = ref(false)
7
8 const updateLoadingStore = (flag) => {
9 isLoading.value = flag
10 }
11
12 function onLoading(flag) {
13 updateLoadingStore(flag)
14 }
15
16 return {
17 isLoading,
18 updateLoadingStore,
19 onLoading
20 }
21})
在定义 store 时,这里使用的是更灵活的 Setup Stores 写法。
1<!-- App.vue -->
2<script setup>
3import { useLoadingStore } from '@/stores/loading'
4
5const store = useLoadingStore()
6</script>
7
8<template>
9 <header>
10 <div class="container">
11 <div class="title"><router-link to="/">Mirrors China</router-link></div>
12 <div class="title-desc">——开源软件的国内镜像站点</div>
13 </div>
14 </header>
15 <div class="main">
16 <a-spin tip="Loading..." :spinning="store.isLoading" style="margin: 20px">
17 <div class="content">
18 <a-back-top />
19 <router-view />
20 </div>
21 </a-spin>
22 </div>
23
24 <div class="footer">
25 Original from
26 <a href="https://github.com/lework/lemonitor" target="_blank">lework/lemonitor</a>, customed by
27 <a href="https://github.com/tianheg/mirrors-china" target="_blank">tianheg/mirrors-china</a>
28 </div>
29</template>
根据 lework/lemonitor 对应文件的代码结构,得到 template 中的内容。代码中 a-spin
, a-back-top
标签来自 ant-design-vue 包。
接下来,最主要的文件就是 views/HomeView.vue:
1<script setup>
2import { reactive, ref, onMounted } from 'vue'
3import axios from 'axios'
4
5let monitorData = reactive([])
6let providerData = reactive([])
7let softwareList = reactive([])
8let softwareData = reactive({})
9let search_text = ref('')
10const spinning = ref(true)
11
12const pagination = reactive({
13 pageSize: 20,
14 responsive: true,
15 showSizeChanger: false,
16 size: 'small',
17 onChange: (page) => {
18 document.querySelector('#app')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
19 }
20})
21
22const onSearch = (value) => {
23 if (typeof value === 'undefined' || value === null || value === '') {
24 _getData()
25 } else {
26 _getData(value)
27 }
28}
29
30const _getData = (search = '') => {
31 spinning.value = true
32 axios
33 .get('static/data.json')
34 .then((res) => {
35 monitorData = res.data
36 providerData = {}
37 softwareData = {}
38 monitorData.forEach((e) => {
39 let name = e.name
40 let color = e.tag_color
41 let url = e.url
42
43 providerData[name] = { tag_color: color, url: url }
44
45 for (let i = 0; i < e['item'].length; i++) {
46 e['item'][i] = e['item'][i].toLowerCase()
47 let softwareName = e['item'][i]
48 if (!Object.prototype.hasOwnProperty.call(softwareData, softwareName)) {
49 softwareData[softwareName] = []
50 }
51 softwareData[softwareName].push(name)
52 }
53 })
54 softwareList = Object.keys(softwareData)
55
56 if (search !== '') {
57 search = search.trim()
58 let resultList = []
59 softwareList.forEach((e) => {
60 if (e.toLowerCase().indexOf(search.toLowerCase()) !== -1) {
61 resultList.push(e)
62 }
63 })
64 monitorData.forEach((e) => {
65 if (e['name'].toLowerCase().indexOf(search.toLowerCase()) !== -1) {
66 resultList = resultList.concat(e['item'])
67 }
68 })
69
70 softwareList = Array.from(new Set(resultList))
71 }
72 spinning.value = false
73 })
74 .catch(() => {
75 message.error('获取数据失败!')
76 })
77}
78
79onMounted(() => {
80 _getData()
81})
82</script>
83
84<template>
85 <main>
86 <div class="search">
87 <a-input-search
88 v-model="search_text"
89 placeholder="输入软件名称或提供方"
90 enter-button="搜索..."
91 @search="onSearch"
92 allow-clear
93 />
94 </div>
95 <div class="content">
96 <a-spin :spinning="spinning">
97 <a-list item-layout="horizontal" :pagination="pagination" :data-source="softwareList">
98 <template #header>
99 <a-popover title="提供方列表" placement="rightTop">
100 <template #content>
101 <a-list
102 :grid="{ gutter: 16, column: 2 }"
103 :data-source="Object.keys(providerData)"
104 style="width: 240px"
105 >
106 <template #renderItem="{ item }">
107 <a-list-item>
108 <a target="_blank" :href="providerData[item]['url']">{{ item }} </a>
109 </a-list-item></template
110 >
111 </a-list>
112 </template>
113
114 <b>提供方:{{ Object.keys(providerData).length }} </b>
115 </a-popover>
116 <b> 软件数目:{{ softwareList.length }}</b>
117 <div class="header-switch"></div>
118 </template>
119
120 <template #renderItem="{ item }">
121 <a-list-item>
122 <a-list-item-meta>
123 <template #title>
124 <div class="list-title">{{ item }}</div>
125 </template>
126 <template #description>
127 <template v-for="tag in softwareData[item]" :key="`${Math.random()}-${tag}`">
128 <a-tag :color="providerData[tag]['tag_color']" style="margin: 0 2px 2px">
129 <a target="_blank" :href="providerData[tag]['url']">{{ tag }} </a>
130 </a-tag>
131 </template>
132 </template>
133 </a-list-item-meta>
134 </a-list-item>
135 </template>
136 </a-list>
137 </a-spin>
138 </div>
139 </main>
140</template>
关于数据处理的全部逻辑都在 script 中。主要操作:读取存在 static/data.json 文件中的 JSON 数据,通过一定转换反映到页面中。其中少不了 ant-design-vue 这个包提供的一些组件的帮助:List, ListItem, ListItemMeta, Tag。
项目的初步完成不是结束,持续维护才是重要的。
相关链接: