在11月初的時候,我給自己定的目標:了解完 Vue3,然后做一個小專案
其中,Vue3 是早就學完了的,然后也寫了兩篇總結或是心得吧,其中有很多都是在做專案中踩出來的坑,所以大家可以看一下,避免之后開發中遇到:
- 快速使用Vue3最新的15個常用API(400+ 個👍)
- 關于Vue3獲取當前組件實體的 getCurrentInstance 方法的補充(30+ 個👍)
然后做的 Vue3 專案也是我自己構思出來,因為當時網上的專案也不多或是大部分都是商城專案,之前也寫過很多類似的了,所以就還是打算自己寫一個,我給它取名叫做 nav-url,顧名思義就是一個網址導航欄,在我寫這篇文章時,專案是已經上線并被我自己以及身邊的小伙伴使用了的,下面放上預覽鏈接 👇👇
點擊即可預覽 👉 專案預覽鏈接
再放上專案原始碼地址 👉:專案原始碼鏈接(歡迎各位 star)
接下來就詳細地介紹一下我的專案
設計初衷
我現在也是個非計算機專業的大四在校生,平時前端都是自學的,所以從初學到現在基本上都是通過白嫖網上的視頻、買書或從圖書館借書看、逛技術博客長長見識等等,這期間我會看到很多實用的工具網站或一些有趣的網站,我都會把他們收藏下來,生怕之后找不到了,但是隨著時間的推移,收藏的網站越來越多,我的瀏覽器收藏夾可能變成了這樣

這些都是我很久之前收藏夾收藏的,要是按照這個勢頭,我的收藏夾不出半年就爆滿了,到時候找網站都不方便,所以我就想做一個我自己的網站導航欄,要求不高 : 簡單大方、方便快捷
于是就有了現在這個專案,如下圖所示:

專案功能 && 特色
畢竟是個網址導航欄,所以功能非常的簡單,但之后我會盡可能地去完善該專案的一些額外的功能
專案的功能:
? 標簽的添加、修改、洗掉
? 網址的添加、修改、洗掉
? 搜索功能
? 配置的匯入、匯出
專案的特色:
? 基于 Vue3 開發
? 頁面簡單大方
? 提供網站圖示、名稱的獲取介面
? 標簽欄支持多種 icon 選擇
? 通過 localStorage 存盤,無需配置資料庫
? 用 Vue3 封裝了 Element UI 的 message 、dialog 、button 、input 、popover 組件
? 通過 Vuex 4 進行狀態管理
? 頁面的滾動影片
? 支持一鍵保存匯出資料、一鍵匯入資料
專案檔案結構
整個專案主要的檔案都在 src 檔案夾下,結構目錄如下:
├── src
├── assets // 存放靜態資源
├── components // 各種組件
│ ├── main // 頁面主要內容相關組件
│ ├── tabs // 標簽欄相關組件
│ └── public // 全域公共組件
├── network // 網路請求
├── store // Vuex
├── utils // 存放自己封裝的工具
├── APP.vue
└── main.jsss
重點介紹
對于專案的邏輯代碼,你們可以直接查看我的原始碼,全部都是用的 Vue3 語法寫的
在最初做這個專案時,還沒找到合適的 Vue3 組件庫,所以我就根據自己的需求,封裝了 message 、dialog 、input 、button 、popover 這樣五個組件,其中重點講一下 message 和 dialog 吧,另外還有這個專案的亮點:配置匯入與匯出
Dilog組件
首先是組件內容:
// lp-dialog.vue
<template>
<div class="lp-confirm-container" ref="lpConfirmAlert">
<div class="lp-confirm-box">
<div class="lp-confirm-title">
<span class="lp-confirm-title-txt">{{ title }}</span>
<span class="lp-confirm-title-close" @click="closeConfirm">✖</span>
</div>
<div class="lp-confirm-content">
<span class="lp-confirm-content-txt">{{ content }}</span>
</div>
<div class="lp-confirm-btn-groups">
<lp-button type="primary" class="lp-confirm-btn" @_click="sureConfirm">確定</lp-button>
<lp-button type="default" class="lp-confirm-btn lp-confirm-btn-cancel" @_click="closeConfirm">取消</lp-button>
</div>
</div>
</div>
</template>
<script>
import lpButton from '../lp-button/lp-button'
import {ref} from 'vue'
export default {
components: {
lpButton
},
props: {
title: {
type: String,
default: '提示'
},
content: {
type: String,
default: '確定關閉嗎?'
}
},
setup() {
const status = ref(-1) // 存盤用戶點的狀態,-1:未點擊;0:取消;1:確定
const lpConfirmAlert = ref(null)
function removeElement() {
lpConfirmAlert.value.parentNode.removeChild(lpConfirmAlert.value)
}
function closeConfirm() {
status.value = 0
removeElement()
}
function sureConfirm() {
status.value = 1
removeElement()
}
return {removeElement, closeConfirm, sureConfirm, status, lpConfirmAlert}
}
}
</script>
<style scoped>
/* 樣式見原始碼,此處省略 */
</style>
這里我在 dialog 組件內設定了一個組件的狀態變數 status,用于確認用戶的點擊情況
再來看看組件的處理代碼:
// lp-dialog.js
import lp_dialog from './lp-dialog.vue'
import {defineComponent, createVNode, render, toRef, watch} from 'vue'
const confirmConstructor = defineComponent(lp_dialog)
export const createDialog = (options) => {
if(!Object.prototype.toString.call(options) === '[Object Object]') {
console.error('Please enter an object as a parameter');
}
options = options ? options : {}
// 生成組件實體
const instance = createVNode(
confirmConstructor,
options
)
// 渲染掛載組件
const container = document.createElement('div')
render(instance, container)
document.querySelector('#app').appendChild(instance.el)
// 初始化組件引數
const props = instance.component.props
Object.keys(options).forEach(key => {
props[key] = options[key]
})
// 獲取組件的 status 狀態變數
const status = toRef(instance.component.setupState, 'status')
// 回傳 promise,方便外部呼叫
return new Promise((resolve, reject) => {
// 監聽組件的按鈕點擊情況
watch(status, (now) => {
if(now == 0) reject();
else if(now == 1) resolve()
})
})
}
接下來把 dialog 作為一個方法注冊到全域中,這個我就把它放在了 App.vue 檔案中,通過 Vue3 的 provide 方法暴露在全域
<template>
<div id="app"></div>
</template>
<script>
import { provide } from 'vue'
import createDialog from './components/public/lp-dialog/lp-dialog.js'
export default {
setup() {
// 全域暴露創建 dialog 組件的方法
provide('confirm', createDialog)
}
}
</script>
然后在別的組件中使用 dialog 組件
<template>
<div class="tabs" @click="btnConfirm"></div>
</template>
<script>
import { inject } from 'vue'
export default {
setup() {
// 接收創建 dialog 組件的方法
let $confirm = inject('confirm')
btnConfirm() {
// 呼叫方法
$confirm({
title: '提示', // 確認框的標題
content: '確認關閉嗎?', // 訊息內容
})
.then(() => {
console.log('確認')
})
.catch(() => {
console.log('取消')
})
}
return { btnConfirm }
}
}
</script>
這樣就實作了一個基于 promise 的鏈式呼叫,可以設定用戶點擊了 確認 或 取消 之后的處理代碼
Message組件
首先是組件內容:
// lp-message.vue
<template>
<div class="message_container"
:class="[
{'show': isShow},
{'hide': !isShow},
{'enter': isEnter},
{'leave': isLeave},
type
]"
:style="{
'top': `${seed * 70}px`
}">
<div class="content">
<i :class="[
`lp-message-${type}`,
'icon',
'fa',
{'fa-info-circle': type == 'info'},
{'fa-check-circle': type == 'success'},
{'fa-times-circle': type == 'err'},
{'fa-exclamation-triangle': type == 'warning'},
]"/>
<div class="txt"
:class="[`txt_${type}`]">
{{content}}
</div>
</div>
</div>
</template>
<script>
export default {
name: "lp-message",
props: {
type: {
type: String,
default: 'info'
},
lastTime: {
type: Number,
default: 2500
},
content: {
type: String,
default: '這是一條提示資訊'
},
isShow: {
type: Boolean,
default: false
},
isLeave: {
type: Boolean,
default: false
},
isEnter: {
type: Boolean,
default: false
},
seed: {
type: Number,
default: 0
}
}
}
</script>
<style scoped>
/* 樣式見原始碼,此處省略 */
</style>
然后是組件的處理代碼:
// lp-message.js
import lp_message from "./lp-message.vue"
import { defineComponent, createVNode, render } from 'vue'
let MessageConstructor = defineComponent(lp_message)
let instance;
const instances = []
export const createMessage = (options) => {
if(!Object.prototype.toString.call(options) === '[object Object]') {
console.error('Please enter an object as a parameter')
}
options = options ? options : {}
instance = createVNode(
MessageConstructor,
options
)
//掛載
const container = document.createElement('div')
render(instance, container)
document.querySelector('#app').appendChild(instance.el)
const cpn = instance.component
const el = instance.el
const props = cpn.props
props.seed = instances.length
// 初始化引數
Object.keys(options).forEach(key => {
props[key] = options[key]
})
// 加入到instances中管理
instances.push(instance)
// 訊息框出現
setTimeout(() => {
props.isShow = true
props.isEnter = true
}, 200)
// 訊息框離開
setTimeout(() => {
props.isEnter = false
props.isShow = false
props.isLeave = true
}, props.lastTime)
// 移除訊息框
setTimeout(() => {
close(el)
}, props.lastTime + 200)
}
// 關閉某個彈框
const close = (el) => {
instances.shift()
instances.forEach((v) => {
v.component.props.seed -= 1
})
document.querySelector('#app').removeChild(el)
}
這里模仿了 element-ui 的思想,把所有的 message 實力管理在一個陣列中
然后我們要把其作為一個方法注冊到全域中,這個我就把它放在了 App.vue 檔案中,通過 Vue3 的 provide 方法暴露在全域
<template>
<div id="app"></div>
</template>
<script>
import { provide } from 'vue'
import createMessage from './components/public/lp-message/lp-message.js'
export default {
setup() {
// 全域暴露創建 message 組件的方法
provide('message', createMessage)
}
}
</script>
使用 message 組件,通過 inject 方法獲取即可
<template>
<div class="main"></div>
</template>
<script>
import { inject } from 'vue'
export default {
setup() {
// 接收創建 message 組件的方法
let $message = inject('message')
// 呼叫方法
$message({
type: 'success', // 訊息框的型別,可選:info | success | err | warning
content: '這是一條成功的訊息', // 訊息內容
lastTime: 5000 // 訊息框持續的時間
})
}
}
</script>
Popover組件
這個組件我沒有模仿 element-ui ,因為我不太喜歡它的那種呼叫方式,所以我就根據自己的奇思妙想設計了一下這個組件:既然這個組件是一個氣泡框,那么必然需要一個元素來確定這個氣泡框的出現位置,因此我想把這個組件做成通過自定義指令 v-popover 來呼叫
接下來看下我的設計程序哈
首先是組件的內容:
// lp-popover.vue
<template>
<div ref="popover"
:class="['lp-popover-container', position]"
:style="{
'top': `${top}px`,
'left': `${left}px`,
}">
<div class="container-proxy">
<div class="lp-popover-title" v-html="title"></div>
<div class="lp-popover-content" v-html="content"></div>
</div>
</div>
</template>
<script>
import {ref, onMounted, reactive, toRefs} from 'vue'
export default {
props: {
title: {
type: String,
default: '我是標題'
},
content: {
type: String,
default: '我是一段內容'
},
position: { // 出現的位置, top | bottom | left | right
type: String,
default: 'bottom'
},
type: { // 觸發方式, hover | click
type: String,
default: 'hover'
}
},
setup({ position, type }) {
const popover = ref(null)
const site = reactive({
top: 0,
left: 0,
})
onMounted(() => {
const el = popover.value
let { top, left, height: pHeight, widht: pWidth } = el.parentNode.getBoundingClientRect() // 獲取目標元素的頁面位置資訊與尺寸大小
let { height: cHeight, width: cWidth } = el.getBoundingClientRect() // 獲取氣泡框的寬高
// 設定氣泡框的位置
switch(position) {
case 'top':
site['left'] = left
site['top'] = top - cHeight - 25
break;
case 'bottom':
site['left'] = left
site['top'] = top + pHeight + 25
break;
case 'left':
site['top'] = top
site['left'] = left - cWidth - 25
break;
case 'right':
site['top'] = top
site['left'] = left + pWidth + 25
break;
}
// 為氣泡框設定觸發方式
switch(type) {
case 'hover':
el.parentNode.addEventListener('mouseover', function() {
el.style.visibility = 'visible'
el.style.opacity = '1'
})
el.parentNode.addEventListener('mouseout', function() {
el.style.visibility = 'hidden'
el.style.opacity = '0'
})
break;
case 'click':
el.parentNode.addEventListener('click', function() {
if(el.style.visibility == 'hidden' || el.style.visibility == '') {
el.style.visibility = 'visible'
el.style.opacity = '1'
} else {
el.style.visibility = 'hidden'
el.style.opacity = '2'
}
})
break;
}
})
return {
...toRefs(site),
popover
}
}
}
</script>
<style scoped>
/* 組件樣式省略,詳情見原始碼 */
</style>
主要思路就是根據 position 定位好氣泡框相對于其父元素的位置,支持的位置一共有四種,即 top | bottom | left | right ,同時根據 type 處理觸發展示氣泡框的方法,一共有兩種觸發方式,即 hover | click
然后再來看一下自定義指令是如何寫的
// lp-popover.js
import lpPopover from './lp-popover.vue'
import {defineComponent, createVNode, render, toRaw} from 'vue'
// 定義組件
const popoverConstructor = defineComponent(lpPopover)
export default function createPopover(app) {
// 在全域上注冊自定義指令v-popover
app.directive('popover', {
// 在元素掛載后呼叫
mounted (el, binding) {
// 獲取外界傳入的指令的值,例如v-popover="data",value獲取的就是data對應的值
let { value } = binding
let options = toRaw(value)
// 判斷傳入的引數是否為物件
if(!Object.prototype.toString.call(options) === '[Object Object]') {
console.error('Please enter an object as a parameter');
}
options = options ? options : {}
const popoverInstance = createVNode(
popoverConstructor,
options
)
const container = document.createElement('div')
render(popoverInstance, container)
el.appendChild(popoverInstance.el)
const props = popoverInstance.component.props
// 通過我們傳入的引數對組件進行資料的初始化
Object.keys(options).forEach(v => {
props[v] = options[v]
})
}
})
}
然后我們再在 main.js 檔案中注冊一下自定義指令
import { createApp } from 'vue';
import App from './App.vue'
import vuex from './store/index'
import vPopover from './components/public/lp-popover/lp-popover'
const app = createApp(App)
// 注冊自定義指令 v-popver
vPopover(app)
app.use(vuex).mount('#app')
再來看一下使用方式
<template>
<div id="app" v-popover="infos">
</div>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
const infos = reactive({
title: '提醒',
content: '這是一條提醒內容',
position: 'left',
type: 'click'
})
return { infos }
}
}
</script>
<style scoped>
</style>
這樣就簡單地實作了氣泡框組件的呼叫,當然其中 content 也是支持 html 的
但總的來說,這個組件的性能可能沒 element-ui 好,因為我是直接對DOM進行了操作,也許后期還需要進行改善
SaveConfig
在介紹配置的匯出與匯入之前, 我先來介紹一下這個專案的資料存盤
我秉承著一種能不用到服務器就不用服務器,能不用資料庫就不用資料庫的原則,想到了 localStorage 可以作為一個本地的資料庫使用,每次換瀏覽器或設備時,只需要將 localStorage 里的資料再匯入一次就好啦,因此我把這個資料稱為配置(Config)
首先我們得擁有配置,所以需要有一個把 localStorage 里資料一鍵匯出保存為一個檔案的功能
該功能我是參考的 MDN 檔案,你們有興趣可以了解一下:Web API——URL.createObjectURL()
我大致是這樣實作的:
// 封裝的下載資料函式
function downLoadFile(fileName, content) {
var aTag = document.createElement('a'); // 獲取 a 元素
var blob = new Blob([content]); // 將資料保存在 blob 物件中
aTag.download = fileName; // 設定保存的檔案名稱
aTag.href = URL.createObjectURL(blob); // 將資料保存在 href 屬性中
aTag.click(); // 模擬點擊 a 元素,進行下載
URL.revokeObjectURL(blob); // 洗掉記憶體中的 blob 物件的資料
}
// 呼叫下載介面
function saveConfig() {
downLoadFile('nav.config.json', window.localStorage.navInfos)
}
試著點擊一下看看效果 😁:

ImportConfig
既然已經手握組態檔,那么走到哪里都不怕了~ 接下來要做的就是將組態檔匯入 localStorage 中
該方法是參考了 MDN 檔案了的,大家可以前去了解一下: Web API——FilerReader
我大致是這樣實作的:
// 匯入配置
function importConfig() {
let reader = new FileReader() // 創建 FileReader 物件
let files = document.querySelector('.file').value.files // 獲取已上傳的檔案資訊
reader.readAsText(files[0]) // 讀取檔案內容
reader.onload = function() { // 讀取操作完成的處理函式
let data = this.result // 獲取檔案讀取結果
window.localStorage.navInfos = data // 將檔案資料存入 localStorage
location.reload() // 重繪頁面
}
}
然后我們把剛才匯出保存的 json 組態檔重新匯入看看效果:

哈哈哈,這樣就成功匯入檔案啦~ ?
Scroll Animation
因為我們所有的 URL 都是在一個頁面內的,并且搭配著側邊欄中的按鈕進行標簽的跳轉,即在左側點哪個標簽,右側的內容就跳到哪個標簽,剛開始我是用錨點實作的,但后來發現這樣的跳轉太生硬了,所以就自己簡單地實作了一下跳轉影片
實作原理大概是這樣:右側內容中每個標簽都有帶有一個 id,并且左側的每個按鈕也是對應著各自的 id 的,所以當點擊了按鈕時,先獲取到對應 id 的元素 el,并獲取 el 離滾動頁面頂部的距離,即 el.scrollTop,然后同時獲取一下當前位置離滾動頁面離頂部的距離,如下圖所示:

那么我們的跳轉距離就是圖中的 Location - Current
我大致是這樣實作的:
// 跳轉到指定標簽
function toID(id) {
const content = document.getElementById('content') // 獲取滾動頁面元素
const el = document.getElementById(`${id}`) // 獲取對應id的標簽元素
let start = content.scrollTop // 獲取當前頁面離頂部的距離
let end = el.offsetTop - 80 // 獲取目標元素離頂部的距離(這里的80是減去了我頂部訊息欄的高度,大家可以不用管)
let each = start > end ? -1 * Math.abs(start - end) / 20 : Math.abs(start - end) / 20 // 考慮滾動方向并計算總共需要滾動的距離,同時將距離平分成20份
let count = 0 // 記錄滾動次數
let timer = setInterval(() => { // 設定定時器,滾動20次
if(count < 20) {
content.scrollTop += each
count ++
} else {
clearInterval(timer)
}
}, 10)
}
我們來看看滾動的效果如何吧~

我感覺滾動還是挺絲滑的 🤔 如果大家有更簡單方便、性能更好的方法可以推薦給我
Get Icons Interface
我前面一直說,本著能不用服務器就不用服務器,能不用資料庫就不用資料庫的原則,但是自動獲取頁面圖示這個功能真的沒有辦法了,要在瀏覽器端訪問別人的網頁還要得到 icon URL,幾乎是不可能的,因為存在跨域問題,所以我就拿自己的服務器暴露了個介面出來用于獲取目標網頁的 icon 地址
代碼這里我就不放上了,因為也比較簡單,就是訪問目標網頁,得到 html 檔案內容,從中篩選出 icon 的地址再回傳就好了,要看代碼的可以在 專案原始碼 中的 app.js 中去查看
這里還要強調的是,雖然我提供了一個介面用于自動獲取對方網頁的圖示,但是有些網頁對外部來路不明的請求都做了處理,例如回傳一個 403 Forbiden 把我的請求給拒絕了,因此一些無法獲得的圖示或者無法加載的圖示,我都是用一個默認圖示統一替代,雖然之前我做過挺久的爬蟲,想辦法對 user-agent 、referer等請求頭都做了處理了,但還是無濟于事,大家如果有好的辦法也可以提供給我嘗試
然后給大家簡單演示一下如何使用的吧~

這個動圖上好像有些模糊或者是樣式的變動,都是因為 gif錄制器的原因哈
其它
對于這個專案,因為剛出來半個月不到嘛,肯定還有需要改進的地方,我也已經列出了之后需要繼續跟進的新功能:
URL的拖拽、排列- 頁面賬號資訊存盤功能
- 提供更多的網址
icon的選擇 - more ……
第一個功能什么意思呢,就是我現在的專案中是不支持添加好后的 URL 重新排序的,但我覺得這個功能是一定要有的,之后會加上,打算想辦法做一個在編輯狀態下拖拽即可完成排列的功能
第二個功能的目的是因為對于很多個網站,你也許會有不同的賬號和密碼,但現在最令人頭疼的就是,總是記不住這個網站我的賬號或密碼是啥,導致每次都要多次嘗試或找回密碼,特別的麻煩;所以我想做一個滑鼠移到對應網址上,有一個查看此網址對應我的賬號密碼的功能
第三個功能就是為了針對那些無法獲取 icon 的網站導致我們導航欄中顯示的圖示為默認圖示,比較丑,所以到時候可以支持大家自行選擇喜歡的圖示
更多的功能還請大家多提建議啦~
最后
有些小伙伴問,為啥不做一個賬號登錄的網址導航欄,這樣到哪都不用帶著組態檔了,只需要記住賬號密碼就可以了,我又要強調本專案的選擇了,能不用服務器就不用服務器,能不用資料庫就不用資料庫,用你自己的本地的 localStorage 作為資料庫存盤,你不是更放心嘛,比如你收藏了一些奇奇怪怪的網站,反正就只有你知道,我反正肯定是不知道的 😏 而且細心的小伙伴有沒有發現,我連靜態頁面都不是用的自己的服務器,直接部署在碼云上的
自學前端這么久了,之前一直做著別人的專案或是模仿一些網站做一個專案,細數一下有這么幾個:淘寶首頁靜態頁面、蘑菇街移動端APP、node社區、elementUi組件以及組件檔案展示等等,這次這個專案也算屬于我自己的了,而且對于我來說是非常實用的一個小工具了,希望大家多多支持~ 給我提提意見,可以的話點個 star 🤞
再放一次專案原始碼地址:專案原始碼
對于這個專案有什么疑問或是專案出現問題的小伙伴可以告知我,vx:Lpyexplore333
關注公眾號:前端印象,領取更多前端資料,我們還能一起學習交流前端技術,分享開發經驗
看到這里了,你們不點個 👍贊 再走忍心嘛,最后謝謝各位的耐心觀看
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/234271.html
標籤:其他
上一篇:第一次總結
