In early November, I set myself the goal of learning about Vue3 and then doing a small project

Among them, Vue3 is learned early, and then also wrote two summary or experience, many of which are in the project to step out of the pit, so you can take a look, to avoid the development of the encounter:

  • Quick access to Vue3’s latest 15 commonly used apis (400+ 👍)
  • A supplement to Vue3’s getCurrentInstance method to get the current component instance (30+ 👍)

The Vue3 project was also conceived by me, because there were not many projects on the Internet at that time, or most of them were mall projects. I had written many similar projects before, so I decided to write one by myself. I called it nav-URL, which is a navigation bar for the website as the name implies. The project is already online and used by myself and my friends around me. Here is a preview link 👇👇

Click to preview the 👉 project preview link

Put on the project source address 👉 : project source link (welcome to all stars)

Let me introduce my project in more detail

designed

Now I am also a senior student who is not majoring in computer science. I usually study on my own, so from the beginning to now, I have mostly learned by watching videos on Baijiao online, buying books or borrowing books from the library, browsing technology blogs and so on. During this period, I saw a lot of useful tool websites or some interesting websites, AND I always saved them in case I could not find them later, but as time went on, my browser favorites became more and more like this

These are my favorites a long time ago, if according to this momentum, my favorites within half a year will be full, when the site is not convenient to find, so I want to do a website navigation bar of my own, not high requirements: simple and generous, convenient and fast

Hence the current project, as shown below:

Project functions && Features

After all, it’s a url navigation bar, so it’s pretty simple, but then I’ll try to improve some of the additional features of the project as much as possible

Functions of the project:

✅ Add, modify, and delete labels

Add, modify and delete ✅

✅ Search function

✅ Import and export configurations

Features of the project:

⭐ is developed based on Vue3

⭐ page simple and generous

⭐ Provides the interface for obtaining the website icon and name

The ⭐ TAB bar supports multiple icon choices

⭐ The database is stored through localStorage and does not need to be configured

⭐ encapsulates the Message, Dialog, Button, Input, and popover components of the Element UI with Vue3

⭐ Use Vuex 4 to manage the status

⭐ page scrolling animation

⭐ Supports one-click data export and one-click data import

Project file structure

The main files for the entire project are in the SRC folder, structured as follows:

├ ─ ─ the SRC ├ ─ ─ assets// Store static resources├ ─ ─ the components// Various components│ ├ ─ ─ the main// The main content of the page related components│ ├ ─ ─ tabs// Label bar related components│ └ ─ ─ the public// Global public component├ ─ ─ the network// Network request├ ─ ─ store// Vuex├ ─ ─ utils// Store your own encapsulated tools├ ─ ─ APP. Vue └ ─ ─ main. JSSSCopy the code

Emphasis on the

For the logical code of the project, you can directly view my source code, which is all written in Vue3 syntax

When I first started this project, I had not found a suitable Vue3 component library, so I encapsulated five components such as Message, Dialog, Input, Button and Popover according to my own needs. Let’s focus on Message and Dialog. Another highlight of the project: configuration imports and exports

Dilog components

First is the component content:

// 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">The & # 10006;</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">determine</lp-button>
              <lp-button type="default" class="lp-confirm-btn lp-confirm-btn-cancel" @_click="closeConfirm">cancel</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: 'tip'
        },
        content: {
            type: String.default: 'Are you sure? '}},setup() {
        const status = ref(-1)       // Store the status of the user point, -1: not clicked; 0: Cancel. 1: make sure
        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>
	/* See source code, omit */ here
</style>
Copy the code

Here I set a component’s status variable in the Dialog component to confirm the user’s clicks

Let’s look at the handler code for the component:

// 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 : {}
	
    // Generate component instances
    const instance = createVNode(
        confirmConstructor,
        options
    )
	
    // Render the mount component
    const container = document.createElement('div')
    render(instance, container)
    document.querySelector('#app').appendChild(instance.el)
	
    // Initialize component parameters
    const props = instance.component.props
    Object.keys(options).forEach(key= > {
        props[key] = options[key]
    })
    // Get the component's status variable
    const status = toRef(instance.component.setupState, 'status')
	
    // Return promise to facilitate external calls
    return new Promise((resolve, reject) = > {
    	// Listen for button clicks of the component
        watch(status, (now) = > {
            if(now == 0) reject();
            else if(now == 1) resolve()
        })
    })   
}
Copy the code

Next, register dialog globally as a method, which I put in the app.vue file and expose globally via Vue3’s provide method

<template>
    <div id="app"></div>
</template>

<script>
import { provide } from 'vue'
import createDialog from './components/public/lp-dialog/lp-dialog.js'
export default {
    setup() {
    	// Globally expose the method that created the Dialog component
    	provide('confirm', createDialog)
    }
}
</script>
Copy the code

Then use the Dialog component in another component

<template>
    <div class="tabs" @click="btnConfirm"></div>
</template>

<script>
import { inject } from 'vue'
export default {
    setup() {
    	// Receive the method to create the dialog component
    	let $confirm = inject('confirm')
        
        btnConfirm() {
            // Call the method
            $confirm({
                title: 'tip'.// Confirm the title of the box
                content: 'Are you sure to close? '.// Message content
            })
            .then(() = > {
                console.log('confirm')
            })
            .catch(() = > {
                console.log('cancel')})}return { btnConfirm }
    }
}
</script>
Copy the code

This creates a chain of promise-based calls that can set the processing code after the user clicks confirm or cancel

The Message component

First is the component content:

// 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: 'This is a message.'
            },
            isShow: {
                type: Boolean.default: false
            },
            isLeave: {
                type: Boolean.default: false
            },
            isEnter: {
                type: Boolean.default: false
            },
            seed: {
                type: Number.default: 0}}}</script>

<style scoped>
	/* See source code, omit */ here
</style>
Copy the code

Then there is the handler code for the component:

// 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
    )

    / / a mount
    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
    
    // Initialize parameters
    Object.keys(options).forEach(key= > {
        props[key] = options[key]
    })

    // Add to instances for management
    instances.push(instance)
    
    // The message box appears
    setTimeout(() = > {
        props.isShow = true
        props.isEnter = true
    }, 200)
    
    // The message box leaves
    setTimeout(() = > {
        props.isEnter = false
        props.isShow = false
        props.isLeave = true
    }, props.lastTime)

    // Remove the message box
    setTimeout(() = > {
        close(el)
    }, props.lastTime + 200)}// Close a popup
const close = (el) = > {
    instances.shift()
    instances.forEach((v) = > {
        v.component.props.seed -= 1
    })
    document.querySelector('#app').removeChild(el)
}
Copy the code

This mimics the idea of Element-UI, where all message capabilities are managed in an array

Then we need to register it globally as a method, which I put in the app.vue file and expose it globally via Vue3’s provide method

<template>
    <div id="app"></div>
</template>

<script>
import { provide } from 'vue'
import createMessage from './components/public/lp-message/lp-message.js'
export default {
    setup() {
    	// Globally expose the method that created the Message component
    	provide('message', createMessage)
    }
}
</script>
Copy the code

Use the message component and use the inject method

<template>
    <div class="main"></div>
</template>

<script>
import { inject } from 'vue'
export default {
    setup() {
    	// Receive the method that created the Message component
    	let $message = inject('message')
        
        // Call the method
        $message({
            type: 'success'./ / the type of message box, optional: info | success | err | warning
            content: 'This is a success story.'.// Message content
            lastTime: 5000          // The duration of the message box}}})</script>
Copy the code

Popover components

I didn’t imitate the Element-UI because I didn’t like the way it was called, so I designed it with my own whimsiness: Since this component is a bubble box, there must be an element to determine where the bubble box appears, so I want to make this component called by the custom v-popover directive

Let’s take a look at my design process

The first is the content of the component:

// 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: 'I am the title'
        },
        content: {
            type: String.default: 'I am a piece of content'
        },
        position: {  / / the position, the top | | bottom left | right
            type: String.default: 'bottom'
        },
        type: {    / / trigger mode, 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()  // Get the page location information and size of the target element
            let { height: cHeight, width: cWidth } = el.getBoundingClientRect()  // Get the width and height of the bubble frame
            // Set the position of the bubble box
            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;            
            }

            // Set the trigger mode for the bubble box
            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>
	/* Component style omitted, see source */ for details
</style>
Copy the code

Main idea is a good location according to the position of bubble box position relative to the parent element, support the position of a total of 4 kinds, namely the top | | bottom left | right, at the same time, according to the type of dealing with the trigger box show bubbles method, there are two types of trigger, The hover | click

Then let’s see how the custom instruction is written

// lp-popover.js
import lpPopover from './lp-popover.vue'
import {defineComponent, createVNode, render, toRaw} from 'vue'

// Define the component
const popoverConstructor = defineComponent(lpPopover)

export default function createPopover(app) {
    // Register the custom instruction v-popover globally
    app.directive('popover', {
    	// Called after the element has been mounted
        mounted (el, binding) {
            // Get the value of the instruction passed in. For example, v-popover="data"
            let { value } = binding

            let options = toRaw(value)
            // Check whether the passed parameter is an object
            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
            // Initialize the component with the parameters we pass in
            Object.keys(options).forEach(v= > {
                props[v] = options[v]
            })         
        }
    })  
}
Copy the code

Then we register our custom directive in the main.js file

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)

// Register the custom instruction v-popver
vPopover(app)

app.use(vuex).mount('#app')
Copy the code

Now let’s see how it’s used

<template>
  <div id="app" v-popover="infos">
    
  </div>
</template>

<script>
import { reactive } from 'vue'
export default {
    setup() {
        const infos = reactive({
            title: 'remind'.content: 'Here's a reminder.'.position: 'left'.type: 'click'
        })
        
        return { infos }
    }
}
</script>

<style scoped>

</style>
Copy the code

This makes it easy to call the bubble box component, which also has HTML support for Content

But overall, the performance of this component may not be as good as that of the Element-UI because I’m directly manipulating the DOM, and I may need to improve it later

SaveConfig

Before I cover configuration exports and imports, LET me introduce the data store for this project

I uphold a can not to the server will not use the server, can not use the database database principle, think of the localStorage can be used as a local database, each change the browser or device, only need to import the data in the localStorage again on the good, So I call this data Config.

First we need to have the configuration, so we need to have a one-click export and save the data in localStorage as a file

Web API — url.createObjecturl ()

Here’s how I did it:

// The encapsulated download data function
function downLoadFile(fileName, content) {
    var aTag = document.createElement('a');   // Get the a element
    var blob = new Blob([content]);           // Store data in bloB objects
    aTag.download = fileName;                 // Set the name of the saved file
    aTag.href = URL.createObjectURL(blob);    // Save the data in the href attribute
    aTag.click();                             // Click on element A to download
    URL.revokeObjectURL(blob);                // Delete the data of the bloB object from memory
}

// Call the download interface
function saveConfig() {
    downLoadFile('nav.config.json'.window.localStorage.navInfos)
}
Copy the code

Try clicking on it to see what it looks like 😁 :

ImportConfig

Now that you have the configuration file in your hand, you are not afraid to go anywhere ~ the next thing to do is to import the configuration file to localStorage

This method is referred to the MDN documentation, you can check out: Web API – FilerReader

Here’s how I did it:

// Import the configuration
function importConfig() {
  let reader = new FileReader()           // Create a FileReader object
  let files = document.querySelector('.file').value.files  // Obtain the information about the uploaded files
  reader.readAsText(files[0])             // Read the contents of the file
  reader.onload = function() {            // The handler that reads the completed operation
    let data = this.result                // Get the file read result
    window.localStorage.navInfos = data   // Store the file data to localStorage
    location.reload()                     // Refresh the page}}Copy the code

Then let’s import the json configuration file we exported and saved to see what it looks like:

Right, right, right, right, right

Scroll Animation

Because all of our urls are in one page, and with the sidebar button to jump to the label, that is, click on the left of the tag, the right of the content to jump to the same tag. At first I did it with anchor points, but then I found the jump to be too stiff, so I simply implemented the jump animation myself

It works like this: Each tag in the right side of the content has an ID, and each button on the left has its own ID, so when the button is clicked, the element EL with the corresponding ID is fetched first, and the distance between el and the top of the scroll page is fetched, that is, el. ScrollTop. Then get the distance between the current position and the top of the scrolling page, as shown below:

So our jump distance is location-current in the figure

Here’s how I did it:

// Jump to the specified label
function toID(id) {
    const content = document.getElementById('content')  // Get the scrolling page element
    const el = document.getElementById(`${id}`)         // Get the tag element corresponding to the ID
    let start = content.scrollTop                       // Get the current page distance from the top
    let end = el.offsetTop - 80                         // Get the distance from the top of the target element (80 here is the height of my top message bar minus, you can forget it)
    let each = start > end ? -1 * Math.abs(start - end) / 20 : Math.abs(start - end) / 20   // Consider the direction of the roll and calculate the total distance to be rolled. Divide the distance into 20 parts
    let count = 0       // Record the scrolling times
    let timer = setInterval(() = > {  // Set timer to roll 20 times
        if(count < 20) {
            content.scrollTop += each
            count ++
        } else {
            clearInterval(timer)
        }
    }, 10)}Copy the code

Let’s see how the scrolling works, shall we

I feel that the roll is still quite silky 🤔 if you have a more simple and convenient, better performance method can recommend to me

Get Icons Interface

As I have said before, in line with the principle of no server or database, there is no way to automatically obtain the page icon function. It is almost impossible to access other people’s web pages in the browser and get the icon URL, because of the cross-domain problem. So I exposed an interface on my server to get the icon address of the target web page

I will not put the code here, because it is relatively simple, is to visit the target web page, get the HTML document content, filter the address of icon and return it, to see the code can be viewed in the project source code app.js

It should also be noted that although I provided an interface to automatically retrieve the ICONS of the other web pages, some web pages did not respond to the requests from the outside, such as returning a 403 Forbiden that rejected my request, so some ICONS were not available or could not be loaded. I always use a default icon to replace it. Although I have been working as a crawler for a long time, I have tried to deal with the user-agent, referer and other request headers, but it still doesn’t work. If you have a good idea, you can also provide me with a try

Then give you a simple demonstration of how to use the ~

There seems to be some blur or style changes in this GIF, due to the GIF recorder

other

As for this project, since it has only been out for less than half a month, there must be some improvements to be made. I have also listed the new functions that need to be followed up:

  1. URLDrag, arrange
  2. Page account information storage function
  3. Provide more urlsiconThe choice of
  4. More……

What is the meaning of the first function? It is not supported in my current project to reorder the URL after adding it, but I think this function must have, and I will add it later. I am going to try to make a function that can complete the arrangement by dragging and dropping in the editing state

The purpose of the second function is that for many websites, you may have different accounts and passwords, but now the most troublesome is that I can’t remember my account or password of this website, so I have to try many times or retrieve my password each time, which is particularly troublesome. So I want to do a mouse moved to the corresponding url, there is a view of this url corresponding to my account password function

The third function is for those websites that cannot get the icon, the icon displayed in our navigation bar is the default icon, which is ugly, so we can support you to choose your favorite icon by yourself

Please make suggestions for more features

The last

Some friends ask, why not do an account login url navigation bar, so that you do not have to take the configuration file, only need to remember the account password. I want to emphasize the choice of this project, can not use the server is not the server, can not use the database is not the database, use your own local localStorage as a database storage, you are not more assured, such as you collect some strange websites, anyway, only you know, I certainly do not know 😏 and careful friends have found that I do not even use the static page of their own server, directly deployed in the code cloud

I have taught myself front-end for so long, before I have been doing other people’s projects or imitating some websites to do a project, to count a few: Taobao home static page, mogujie mobile terminal APP, Node community, elementUi components and component document display, and so on, this project also belongs to my own, and for me is a very practical small tool, I hope you can give me a lot of support ~ give me suggestions, if you can click star 🤞

Again put the project source address: project source

If you have any questions or problems with this project, please let me know. Vx: Lpyexplore333

Pay attention to the public number: front-end impression, get more front-end information, we can also learn to exchange front-end technology, share development experience

See here, you do not point 👍 to like before walk the heart, finally thank you for your patience to watch