Tianhe Gao

开源软件的国内镜像站 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。

项目的初步完成不是结束,持续维护才是重要的。

相关链接:


No notes link to this note

Welcome to tell me your thoughts via "email"
UP