Front words
The author is doing a complete blog online project, including Reception,Backstage,Back-end interface Server configuration. This article will introduce in detail the blog website made with vue family bucket.
Summary
This project is a set of blog front page based on Vue family bucket (vue, vue-router, vuex, vue SSR). Its main functions include home page display, authentication system, article management, comment management and comment management.
[Access Address]
Domain name: https://xiaohuochai.cc
Github: https://github.com/littlematch0123/blog-client
Or two-dimensional code access can be scanned directly
[Project Introduction]
The content of this project is based on more than 600 blogs written by the author in the process of self-learning front-end, which may be helpful to the students who are also learning front-end. Many blogs have DEMO s that can be operated directly, and the understanding of knowledge may be more intuitive.
The mobile side and the desktop side can be adapted with the mobile-first responsive layout; the font size is in em units, and the text on the desktop side becomes larger accordingly; the mobile side can use sliding screen operation, and the desktop side can enhance the interactive experience through cursor settings, custom scroll bars, carriage return determination, etc.
The whole station adopts server-side rendering SSR, which is beneficial to SEO and reduces the rendering time of the first screen. The offline caching and adding functions of the PWA scheme to the desktop are realized by using service worker and manifest.
According to HTML tag content model, semantic tags are used to minimize tag hierarchy and semantically-free div tags.
CSS uses class selector extensively to minimize selector level, uses CSS module and postCSS in vue component, uses styleLint to standardize CSS code, writes code according to the order of layout class attribute, box model attribute, text class attribute and modifying class attribute, and uses order plug-in to verify it.
Using esLint specification JS code, code style refers to airbnb specification, all naming uses hump writing, common components are prefixed with Base, event function is prefixed with on, async function is suffixed with async, Boolean value is basically prefixed with do or is.
Instead of referring to third-party component libraries, such as bootstrap or element components, they developed their own common components needed in the project. Under the common directory, it encapsulates such components as head image, full screen, loading, mask, search box, linkage selection and so on, which is convenient for development.
Using configuration data, data and application are separated and stored in the constants directory as a constant.
Using Aliyun's short message module, the function of short message verification is realized.
There are two hidden eggs in this project. One is the shake-and-shake function, which can be directly shaken to the background page. The other is the gyroscope function. When shaking the mobile phone up and down, the head will rotate.
The project has been optimized and the final optimization score is as follows
Function demonstration
The main functions include homepage display, authentication system, article management, comment management and comment management.
[Home page display]
Home page includes drag-and-drop rotation map, topic recommendation, article recommendation and category recommendation
[Authentication System]
Authentication system includes user registration, user login, short message verification
1. When the user is not logged in, he can read the article, but he can't comment or comment, otherwise the login box will pop up.
2. User Registration
3. User login
[Article Management]
Article management includes browsing recommended articles, filtering by category, searching for articles, and viewing by directory.
1. Browse Recommended Articles
2. Articles Screening
3. Article Search
4. View by directory
[Praise management]
[Comment Management]
Comment management includes viewing comments, adding comments, modifying comments and deleting comments
directory structure
src directory, including assets (static resources), common (public components), components (functional components), constants (constant configuration), router (routing), store(vuex) and utils (tool methods) seven directories
- assets // Store static resources, mainly pictures -imgs css.png // CSS Article Background Map ... - common // Store common components -SVG // Deposit VUE Icon component SVGAdd.vue // "add to"Button SVGBack.vue // "Return"Button ... BaseArticle.vue // Article component BaseAvatar.vue // Head component ... - components // Storage Functional Components -Post // Article component module.js //Article Status Management Post.vue // Article Display Component PostContent.vue // Article Catalog Component PostList.vue // Article List Component SearchPost.vue // Search article components ... - constants // Storage Constant Configuration API.js // Deposit API Calling address - router // Storage routing index.js - store // Deposit vuex index.js - utils // Storage tool method async.js // axios Method fnVarificate.js // Form validation method util.js // Other tools and methods
[Common Components]
Instead of referring to third-party component libraries, such as bootstrap or element components, they developed their own common components needed in the project
Encapsulated article component, header component, return component, button component, card component, full screen component, input box component, loading component, mask component, search box component, multi-line input box component, Title component, breadcrumb component, button group component, anti-color button component, password box component, input box component and linkage selection component.
BaseAdd.vue // "add to"assembly BaseArticle.vue // Article component BaseAvatar.vue // Head component BaseBack.vue // Return component BaseButton.vue // Button component BaseCard.vue // Card module BaseFullScreen.vue // Full screen module BaseInput.vue // Input box component BaseLoading.vue // loading assembly BaseMask.vue // mask assembly BaseSearchBox.vue // Search box component BaseTextArea.vue // Multi-line Input Box Component BaseTitle.vue // Title Component BreadCrumb.vue // Bread crumb assembly ButtonBox.vue // Button Group Component ButtonInverted.vue // Anti-color Button Component InputPassword.vue // Password Box Component InputWithTest.vue // Input Box Component Containing Detection LinkageSelector.vue // Linkage Selection Component
[Functional components]
Set up the directory according to the function, as follows
Alert Category Management Comment Management Home page (Home) Like Management Post Size
The Header User Management
Overall thinking
[Full screen layout]
Full-screen layout with height setting is implemented mainly through calc
<div id="root" :class="$style.wrap" :style="{height:wrapHeight+'px'}" > ... <TheHeader :class="$style.header"/> <main :class="$style.main"> <transition :name="transitionName"> <router-view :class="$style.router" /> </transition> </main> </div>
.header { height: 40px; } .main { position: relative; height: calc(100% - 40px); overflow: auto; }
[Hierarchical Management]
Hierarchical z-index of the project, using only 0-3
Full-screen pop-up box optimization level is the highest, set to 3; sidebar set to 2; page elements default to 0, if necessary, to set to 1
Global Ejection Layer
Set up the global pop-up layer and load in the entry file App.vue, all components can be shared
// App.vue <template> <div id="root" :class="$style.wrap" :style="{height:wrapHeight+'px'}" > <AlertWithLoading v-show="doShowLoading" /> <AlertWithText v-show="alertText !== ''" :text="alertText" :onClick="() => {$store.commit(HIDE_ALERTTEXT)}" /> <TheHeader :class="$style.header"/> <main :class="$style.main"> <transition :name="transitionName"> <router-view :class="$style.router" /> </transition> </main> </div> </template>
[Routing Management]
vue-router uses static routing table to manage routing. Although it is not flexible as react-router-dom, it is easy to find and see at a glance.
Set up routing to load components on demand and set scrolling behavior
import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) export default function createRouter() { return new Router({ mode: 'history', routes: [ { path: '/', component: () => import(/* webpackChunkName:'home' */ '@/components/Home/Home'), name: 'home', meta: { index: 0 } }, { path: '/posts', component: () => import(/* webpackChunkName:'post' */ '@/components/Post/PostList'), name: 'postlist' }, { path: '/posts/search', component: () => import(/* webpackChunkName:'post' */ '@/components/Post/SearchPost'), name: 'searchpost' }, { path: '/posts/:postid', component: () => import(/* webpackChunkName:'post' */ '@/components/Post/Post'), name: 'post', children: [ { path: 'comments', name: 'commentlist', component: () => import(/* webpackChunkName:'comment' */ '@/components/Comment/CommentList'), children: [ { path: 'add', name: 'addcomment', component: () => import(/* webpackChunkName:'comment' */ '@/components/Comment/AddComment') }, { path: ':commentid/update', name: 'updatecomment', component: () => import(/* webpackChunkName:'comment' */ '@/components/Comment/UpdateComment') }, { path: ':commentid/delete', name: 'deletecomment', component: () => import(/* webpackChunkName:'comment' */ '@/components/Comment/DeleteComment') } ] } ] }, { path: '/categories', component: () => import(/* webpackChunkName:'category' */ '@/components/Category/CategoryList'), name: 'categorylist' }, { path: '/categories/:number', component: () => import(/* webpackChunkName:'category' */ '@/components/Category/Category'), name: 'category' }, { path: '/topics/:number', component: () => import(/* webpackChunkName:'category' */ '@/components/Category/CategoryTopic'), name: 'topic' }, // register { path: '/signup', component: () => import(/* webpackChunkName:'user' */ '@/components/User/AuthSignup'), name: 'signup' }, // Log in by cell phone number { path: '/signin_by_phonenumber', component: () => import(/* webpackChunkName:'user' */ '@/components/User/AuthSigninByPhoneNumber'), name: 'signin_by_phonenumber' }, // Log in by username { path: '/signin_by_username', component: () => import(/* webpackChunkName:'user' */ '@/components/User/AuthSigninByUsername'), name: 'signin_by_username' }, // User page { path: '/users/:userid', component: () => import(/* webpackChunkName:'user' */ '@/components/User/UserDesk'), name: 'user' } ], scrollBehavior(to, from, savedPosition) { if (savedPosition) { return savedPosition } return { x: 0, y: 0 } } }) }
[State Management]
The state management of each component is named module.js and stored in the current component directory
import Vue from 'vue' import Vuex from 'vuex' import auth from '@/components/User/module' import alert from '@/components/Alert/module' import post from '@/components/Post/module' import category from '@/components/Category/module' import like from '@/components/Like/module' import size from '@/components/Size/module' import comment from '@/components/Comment/module' Vue.use(Vuex) export default function createStore() { return new Vuex.Store({ modules: { auth, alert, post, category, like, size, comment } }) }
The state of each component includes state, getters, actions, and mutations fields, with Category component as an example
import { BASE_CATEGORY_URL } from '@/constants/API' import { getNumberWithoutPostPositiveZero, getCategoryNumbers } from '@/utils/util' export const LOAD_CATEGORIES = 'LOAD_CATEGORIES' export const LOAD_CATEGORIES_ASYNC = 'LOAD_CATEGORIES_ASYNC' const category = { state: { docs: [] }, getters: { categoryCount: state => state.docs.length, getCategoriesByNumber: state => state.docs.reduce((obj, t) => { obj[t.number] = t return obj }, {}), getCategoryByNumber: state => number => state.docs.find(doc => doc.number === number), getPosterityCategories: (state, getters) => number => { const reg = new RegExp(`^${getNumberWithoutPostPositiveZero(number)}`) return state.docs.filter(doc => { doc.titleDatas = getCategoryNumbers(doc.number).map(t => getters.getCategoriesByNumber[t].name) return String(doc.number).match(reg) && (doc.posts.length) }) }, getChildrenCategoryies: state => number => { const reference = String(getNumberWithoutPostPositiveZero(number)) const len = reference.length const regExp = new RegExp(`^${reference}(0[1-9]|[1-9][0-9])(0){${8 - len}}`) return state.docs.filter(doc => String(doc.number).match(regExp)) }, getCategoryRootDatas: state => state.docs.filter(doc => Number(String(doc.number).slice(2)) === 0), getRecommendedCategories: state => state.docs.filter(t => t.recommend).sort((a, b) => a.index - b.index) }, actions: { /* Get all category information */ [LOAD_CATEGORIES_ASYNC]({ commit }) { return new Promise((resolve, reject) => { this._vm.$axios({ commit, url: BASE_CATEGORY_URL, doHideAlert: true, success(result) { // Save class commit(LOAD_CATEGORIES, result.docs) // Notify the front-end that the operation was successful resolve(result.docs) }, fail(err) { // Forward notification operation failed reject(err) } }) }) } }, mutations: { /* Save Category Information */ [LOAD_CATEGORIES](state, payload) { state.docs = payload } } } export default category
[Data transfer]
There are generally three ways to transfer data between components, one is to use props in vue and custom events, the other is to use params attributes of routing, and the other is to use vuex.
1. props and custom events
// BaseInput <template> <input :class="$style.input" :value="value" autocomplete="off" autocapitalize="off" @input="$emit('input', $event.target.value)" > </template> <script> export default { props: { value: { type: String, default: '' } } } </script> // InputPassword <input :class="$style.input" :placeholder="placeholder" :value="value" autocomplete="off" autocapitalize="off" type="password" @input="$emit('input',$event.target.value)" >
2. params attributes of routing
// Post.vue <BaseBack @click.native="$router.push($route.params.parentPath || '/')">Return</BaseBack> //AuthSign.vue <template> <router-link :active-class="$style.active" :to="{ name: 'signin', params: { parentPath } }" >Deng record</router-link> </template> <script> export default { computed: { parentPath() { const temp = this.$route.params.parentPath if (temp) { return temp } return '' } } } </script>
3. Use vuex
// Category.vue <template> <article v-if="category" :class="$style.box"> <BaseBack @click.native="$router.push('/categories')">Category list</BaseBack> <BaseTitle>{{ category.name }}Knowledge system</BaseTitle> ... </article> </template> <script> export default { computed: { category() { return this.$store.getters.getCategoryByNumber(Number(this.paramsNumber)) } ... } } </script>
Project optimization
[Offline Caching]
Implementing offline caching through service worker
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin') plugins: [ new SWPrecacheWebpackPlugin({ dontCacheBustUrlsMatching: /\.\w{8}\./, filename: 'service-worker.js', logger(message) { if (message.indexOf('Total precache size is') === 0) { return; } if (message.indexOf('Skipping static resource') === 0) { return; } console.log(message); }, navigateFallback: 'https://www.xiaohuochai.cc', minify: true, navigateFallbackWhitelist: [/^(?!\/__).*/], dontCacheBustUrlsMatching: /./, staticFileGlobsIgnorePatterns: [/\.map$/, /\.json$/], runtimeCaching: [{ urlPattern: '/', handler: 'networkFirst' }, { urlPattern: /\/(posts|categories|users|likes|comments)/, handler: 'networkFirst' } ] }) ]
[Add to Desktop]
Under andriod, the manifest.json file is added to the desktop, while IOS needs to set meta tags
<meta name="theme-color" content="#fff"/> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-title" content="Front-end station"> <link rel="apple-touch-icon" href="/logo/logo_256.png"> <link rel="shortcut icon" href="/logo/favicon.ico"> <link rel="manifest" href="/manifest.json" /> // manifest.json { "name": "Small Match Front End Station", "short_name": "Front-end station", "start_url": "/", "display": "standalone", "description": "", "theme_color": "#fff", "background_color": "#d8d8d8", "icons": [{ "src": "./logo/logo_32.png", "sizes": "32x32", "type": "image/png" }, { "src": "./logo/logo_48.png", "sizes": "48x48", "type": "image/png" }, { "src": "./logo/logo_96.png", "sizes": "96x96", "type": "image/png" }, { "src": "./logo/logo_144.png", "sizes": "144x144", "type": "image/png" }, { "src": "./logo/logo_192.png", "sizes": "192x192", "type": "image/png" }, { "src": "./logo/logo_256.png", "sizes": "256x256", "type": "image/png" } ] }
[Subpage refresh]
When the child page is refreshed, there may be a situation that the data passed from the parent can not be obtained. The author's treatment is to jump to the parent page.
mounted() { if (!this.comment && this.operate === 'update') { this.$router.push(`/posts/${this.postId}/comments`) } else { this.setTextAreaValue() } }
[promise]
Adding Promise to actions to facilitate state change processing
[LOAD_COMMENTS_ASYNC]({ commit }, payload) { return new Promise((resolve, reject) => { this._vm.$axios({ commit, data: payload, url: BASE_COMMENT_URL, doHideAlert: true, success(result) { // Save class commit(LOAD_COMMENTS, result.docs) // Notify the front-end that the operation was successful resolve(result.docs) }, fail(err) { // Forward notification operation failed reject(err) } }) }) }
[Component Sharing]
Because the elements used for editing and building components are the same, but the content is empty when building new components, and you need to add content when editing components, then you can reuse components.
// AddComment.vue <CommentForm operate="add" /> //UpdateComment.vue <CommentForm operate="update" />
[Cleaning up the environment]
If you use addEventListener to bind event handlers, clean up the environment in time when components are destroyed
mounted() { window.addEventListener('devicemotion', throttle(this.testShake)) } beforeDestroy() { window.removeEventListener('devicemotion', throttle(this.testShake)) }
[Application and data separation]
Configuration data is used to separate data from application. Configuration data is mainly API call address, which is stored in the constants directory as a constant.
// API.js let API_HOSTNAME if (process.env.NODE_ENV === 'production') { API_HOSTNAME = 'https://api.xiaohuochai.cc' } else { API_HOSTNAME = '/api' } export const SIGNUP_URL = `${API_HOSTNAME}/auth/signup` export const SIGNIN_BYUSERNAME_URL = `${API_HOSTNAME}/auth/signin_by_username` export const SIGNIN_BYPHONENUMBER_URL = `${API_HOSTNAME}/auth/signin_by_phonenumber` export const VERIFICATE_URL = `${API_HOSTNAME}/auth/verificate` export const BASE_USER_URL = `${API_HOSTNAME}/users` export const BASE_POST_URL = `${API_HOSTNAME}/posts` export const BASE_TOPIC_URL = `${API_HOSTNAME}/topics` export const BASE_CATEGORY_URL = `${API_HOSTNAME}/categories` export const BASE_LIKE_URL = `${API_HOSTNAME}/likes` export const BASE_COMMENT_URL = `${API_HOSTNAME}/comments` export const ADMIN_URL = 'https://admin.xiaohuochai.cc'
[Functional throttling]
Use function throttling for functions with higher trigger frequency
/**
* Function throttling
* @param {fn} function test(){}
* @return {fn} function test(){}
*/
export const throttle = (fn, wait = 100) => function func(...args) {
if (fn.timer) return
fn.timer = setTimeout(() => {
fn.apply(this, args)
fn.timer = null
}, wait)
}
[DNS pre-parsing]
DNS pre-parsing is implemented by setting meta tags
<link rel="dns-prefetch" href="//api.xiaohuochai.cc" /> <link rel="dns-prefetch" href="//static.xiaohuochai.site" /> <link rel="dns-prefetch" href="//demo.xiaohuochai.site" /> <link rel="dns-prefetch" href="//pic.xiaohuochai.site" />
[Image Lazy Loading and webp]
Using vue-lazyload plug-in to realize lazy loading of pictures and conversion of pictures into webp format under andriod system
Vue.use(VueLazyload, { loading: require('./assets/imgs/loading.gif'), listenEvents: ['scroll'], filter: { webp(listener, options) { if (!options.supportWebp) return const isCDN = /xiaohuochai.site/ if (isCDN.test(listener.src)) { listener.src += '?imageView2/2/format/webp' } } } })
Function realization
[Shake-and-shake effect]
Shake-and-shake effect is achieved mainly by monitoring devicemotion events
mounted() { window.addEventListener('devicemotion', throttle(this.testShake)) }, beforeDestroy() { window.removeEventListener('devicemotion', throttle(this.testShake)) }, methods: { testShake(e) { const { x, y, z } = e.accelerationIncludingGravity const { lastX, lastY, lastZ } = this const nowRange = Math.abs(lastX - x) + Math.abs(lastY - y) + Math.abs(lastZ - z) if (nowRange > 80) { window.location.href = ADMIN_URL } this.lastX = x this.lastY = y this.lastZ = z } }
[gyroscopic effect]
The effect of gyroscope is mainly achieved by monitoring deviceorientation events.
mounted() { // Monitoring gyroscope window.addEventListener('deviceorientation', throttle(this.changeBeta)) }, beforeDestroy() { // Cancel monitoring window.removeEventListener('deviceorientation', throttle(this.changeBeta)) }, methods: { changeBeta(e) { if (this.beta !== Math.round(e.beta)) { this.beta = Math.round(e.beta) } } }
[Slow-moving Ejection Layer]
There are two implementations of the transition pop-up layer, including transition and animation. The project is implemented by animation.
<UserMenuList v-if="doShowMenuList" :onExit="() => {doShowMenuList = false}"/>
@keyframes move { 100% { transform: translateY(0); } } @keyframes opacity { 100% { opacity: 1; } } .mask { opacity: 0; animation: opacity linear both .2s; } .list { transform: translateY(-100%); animation: move forwards .2s; }
[icon management]
All icons are stored in the common/SVG directory in SVG format.
// SVGAdd.vue <template> <svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> <path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/> <path d="M0 0h24v24H0z" fill="none"/> </svg> </template>
[axios function encapsulation]
Encapsulating axios function into async.js file under utils directory, integrating loading component and alert component into the whole data acquisition process of axios function
import { SHOW_LOADING, HIDE_LOADING, SHOW_ALERTTEXT, HIDE_ALERTTEXT } from '@/components/Alert/module' import { SIGNOUT } from '@/components/User/module' import axios from 'axios' const async = { install(Vue) { Vue.prototype.$axios = ({ commit, url, method, data, headers, success, fail, doHideAlert }) => { // display loading commit(SHOW_LOADING) let axiosObj = url if (method) { axiosObj = { method, url, data, headers } } axios(axiosObj) .then(res => { const { message, result } = res.data // Close loading commit(HIDE_LOADING) // Display Success Tips !doHideAlert && commit(SHOW_ALERTTEXT, message) // 1 Close prompt automatically after seconds setTimeout(() => { commit(HIDE_ALERTTEXT) }, 1000) // Successful callback function success && success(result) }) .catch(err => { // Close loading commit(HIDE_LOADING) if (err.response) { const { data } = err.response // Customization error if (data.code === 1) { commit(SHOW_ALERTTEXT, data.message) // System error } else if (data.code === 2) { commit(SHOW_ALERTTEXT, data.message) fail && fail(err) // Authentication error } else if (data.code === 3) { commit(SHOW_ALERTTEXT, data.message) commit(SIGNOUT) window.location.href = '/signin_by_username' } else { // Display error prompts commit(SHOW_ALERTTEXT, 'Server failure') // Callback function after failure fail && fail(err) } } else { // Display error prompts commit(SHOW_ALERTTEXT, 'Server failure') // Callback function after failure fail && fail(err) } }) } } } export default async
[Directory Jump]
Using the scrollIntoView() method, when you click on a directory, the article jumps to the relevant section without changing the URL.
<ul :class="$style.list"> <li v-for="(item, index) in titles" :key="item" :class="$style.item" @click="onChangeAnchor(`anchor${index+1}`)" > {{ index + 1 }},{{ item }} </li> </ul>
methods: { onChangeAnchor(id) { document.getElementById(id).scrollIntoView({ behavior: 'smooth' }) } }
Compatible processing
Anchor point
When using anchor to jump inside a page, the URL changes, the page refreshes, and other browsers have no problem. However, the PWA desktop icon under ISO will jump to the safari browser
Using scrollIntoView() instead of anchor #, the page only skips and does not refresh. andriod supports setting smooth scroll behavior for scrollIntoView:'smooth', but IOS does not support it
[Page enlargement]
In IOS, input will zoom in when it gets the focus, and meta-setting user-scalable=no can cancel the zoom-in effect.
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, shrink-to-fit=no">
[rounded corners]
Under IOS, when the input field only displays the bottom border, the bottom corner effect will appear. Setting border-radius:0 will do.
border-radius:0
[outline]
In android browser, when the input domain is in focus, the default outline effect is a circle of light yellow outline.
It can be removed by setting outline:none
outline: none
[Click Background]
On the mobile side, when clicking on clickable elements, a light blue background appears under android and a grey background appears under IOS.
By setting the - webkt-tap-hightlight-color attribute, you can cancel the background effect when clicking
* { -webkit-tap-highlight-color: rgba(0, 0, 0, 0); }
[Local non-rolling]
Under IOS, there may be bug s that local scrolling is not smooth or even local non-scrolling.
It can be solved by setting the overflow-scrolling attribute to touch on this element.
div { -webkit-overflow-scrolling: touch; }
Anchor point
When using anchor to jump inside a page, the URL changes, the page refreshes, and other browsers have no problem. However, the PWA desktop icon under ISO will jump to the safari browser
Using scrollIntoView() instead of anchor #, the page only skips and does not refresh. andriod supports setting smooth scroll behavior for scrollIntoView:'smooth', but IOS does not support it