使用vue全家桶製做博客網站

前面的話

  筆者在作一個完整的博客上線項目,包括前臺後臺後端接口和服務器配置。本文將詳細介紹使用vue全家桶製做的博客網站css

 

概述

  該項目是基於vue全家桶(vue、vue-router、vuex、vue SSR)開發的一套博客前臺頁面,主要功能包括首頁顯示、認證系統、文章管理、評論管理和點贊管理前端

【訪問地址】vue

  域名:https://xiaohuochai.ccreact

  Github: https://github.com/littlematch0123/blog-clientandroid

  或者能夠直接掃描二維碼訪問webpack

【項目介紹】ios

  該項目的內容以筆者自學前端的過程當中寫的600多篇博客爲基礎,對於一樣學習前端的同窗可能會有所幫助。許多博客都有直接能夠操做的DEMO,對知識的理解可能會更直觀git

  採用移動優先的響應式佈局,移動端、桌面端都可適配;字體大小使用em單位,桌面端的文字相應變大;移動端可以使用滑屏操做,桌面端經過光標設置、自定義滾動條、回車肯定等,提高交互體驗github

  全站採用服務器端渲染SSR的方式,有利於SEO,減小了首屏渲染時間;使用service worker和manifest實現了PWA方案的離線緩存和添加到桌面的功能web

  根據HTML標籤內容模型,使用語義化標籤,儘可能減小標籤層級,儘可能減小無語義的div標籤

  CSS大量使用類選擇器,儘可能減小選擇器層級,在vue組件中使用CSS module和postCSS,使用styleLint規範CSS代碼,按照佈局類屬性、盒模型屬性、文本類屬性、修飾類屬性的順序編寫代碼,並使用order插件進行校驗

  使用esLint規範JS代碼,代碼風格參照airbnb規範,全部命名採用駝峯寫法,公共組件以Base爲前綴,事件函數以on爲前綴,異步函數以async爲後綴,布爾值基本以do或is爲前綴

  沒有引用第三方組件庫,如bootstrap或element組件,而是本身開發了項目中所需的公共組件。在common目錄下,封裝了頭像、全屏、loading、遮罩、搜索框、聯動選擇等組件,方便開發

  使用配置數據,實現了數據和應用分離,以常量的形式存儲在constants目錄下

  使用了阿里雲的短信模塊,實現了短信驗證功能

  該項目有兩個隱藏彩蛋,一個是搖一搖功能,能夠直接搖到後臺頁面,另外一個是陀螺儀功能,上下晃動手機時,頭像會進行旋轉

  項目進行了代碼優化,最終優化評分以下所示

 

功能演示

  主要功能包括首頁顯示、認證系統、文章管理、評論管理和點贊管理

【首頁顯示】

  首頁包括可拖拽輪播圖、專題推薦、文章推薦和類別推薦

【認證系統】

   認證系統包括用戶註冊、用戶登陸、短信驗證

  一、用戶處於未登陸態時,能夠閱讀文章,但不能點贊和評論,不然會彈出登陸框

  二、用戶註冊

  三、用戶登陸

【文章管理】

  文章管理包括瀏覽推薦文章、按類別篩選、文章搜索、按目錄查看

  一、瀏覽推薦文章

  二、文章篩選

  三、文章搜索

  四、按目錄查看

【點贊管理】

【評論管理】

  評論管理包括查看評論、添加評論、修改評論和刪除評論

 

目錄結構

  src目錄下,包括assets(靜態資源)、common(公共組件)、components(功能組件)、constants(常量配置)、router(路由)、store(vuex)和utils(工具方法)這7個目錄

- assets // 存放靜態資源,主要是圖片
    -imgs
      css.png // CSS文章背景圖
     ...
- common // 存放公共組件
    -SVG // 存放VUE圖標組件
        SVGAdd.vue // "添加到"按鈕
        SVGBack.vue // "返回"按鈕
        ...
    BaseArticle.vue // 文章組件
    BaseAvatar.vue // 頭像組件
    ...
- components // 存放功能組件
    -Post // 文章組件      
      module.js //文章狀態管理    
      Post.vue // 文章顯示組件
      PostContent.vue // 文章目錄組件
      PostList.vue // 文章列表組件
      SearchPost.vue // 搜索文章組件
      ...
- constants // 存放常量配置
    API.js // 存放API調用地址
- router // 存放路由
    index.js 
- store // 存放vuex
    index.js
- utils // 存放工具方法
    async.js // axios方法
    fnVarificate.js // 表單驗證方法
    util.js // 其餘工具方法

【公共組件】

  沒有引用第三方組件庫,如bootstrap或element組件,而是本身開發了項目中所需的公共組件

  封裝了文章組件、頭像組件、返回組件、按鈕組件、卡片組件、全屏組件、輸入框組件、loading組件、遮罩組件、搜索框組件、多行輸入框組件、標題組件、麪包屑組件、按鈕組組件、反色按鈕組件、密碼框組件、包含檢測的輸入框組件和聯動選擇組件

BaseAdd.vue // "添加到"組件
BaseArticle.vue  // 文章組件
BaseAvatar.vue // 頭像組件
BaseBack.vue // 返回組件
BaseButton.vue // 按鈕組件
BaseCard.vue // 卡片組件
BaseFullScreen.vue // 全屏組件
BaseInput.vue  // 輸入框組件
BaseLoading.vue  // loading組件
BaseMask.vue // 遮罩組件
BaseSearchBox.vue  // 搜索框組件
BaseTextArea.vue // 多行輸入框組件
BaseTitle.vue  // 標題組件
BreadCrumb.vue // 麪包屑組件
ButtonBox.vue  // 按鈕組組件
ButtonInverted.vue // 反色按鈕組件
InputPassword.vue  // 密碼框組件
InputWithTest.vue // 包含檢測的輸入框組件
LinkageSelector.vue // 聯動選擇組件

【功能組件】

  按照功能來設置目錄,以下所示

彈出框(Alert)
類別管理(Category)
評論管理(Comment)
主頁(Home)
點贊管理(Like)
文章管理(Post)
頁面尺寸(Size)
公共頭部(TheHeader) 用戶管理(User)

 

總體思路

【全屏佈局】

  使用設置高度的全屏佈局方式,主要經過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;
}

【層級管理】

  項目的層級z-index,只使用0-3

  全屏的彈出框優化級最高,設置爲3;側邊欄設置爲2;頁面元素默認爲0,若有須要,要設置爲1

【全局彈出層】

  在入口文件App.vue中設置全局的彈出層和loading,全部組件均可以共用

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

【路由管理】

  vue-router使用靜態路由表的形式對路由進行管理,雖然沒有react-router-dom靈活,但方便尋找,一目瞭然

  按路由設置按需加載組件,並設置滾動行爲

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'
      },
      // 註冊
      {
        path: '/signup',
        component: () => import(/* webpackChunkName:'user' */ '@/components/User/AuthSignup'),
        name: 'signup'
      },
      // 按手機號登陸
      {
        path: '/signin_by_phonenumber',
        component: () => import(/* webpackChunkName:'user' */ '@/components/User/AuthSigninByPhoneNumber'),
        name: 'signin_by_phonenumber'
      },
      // 按用戶名登陸
      {
        path: '/signin_by_username',
        component: () => import(/* webpackChunkName:'user' */ '@/components/User/AuthSigninByUsername'),
        name: 'signin_by_username'
      },
      // 用戶頁面
      {
        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 }
    }
  })
}

【狀態管理】

  每一個組件的狀態管理命名爲module.js,保存在當前組件目錄下

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

  每一個組件的狀態包括state、getters、actions和mutations字段,以Category組件爲例

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: {
    /* 獲取所有類別信息 */
    [LOAD_CATEGORIES_ASYNC]({ commit }) {
      return new Promise((resolve, reject) => {
        this._vm.$axios({
          commit,
          url: BASE_CATEGORY_URL,
          doHideAlert: true,
          success(result) {
            // 保存類別
            commit(LOAD_CATEGORIES, result.docs)
            // 向前端通知操做成功
            resolve(result.docs)
          },
          fail(err) {
            // 向前端通知操做失敗
            reject(err)
          }
        })
      })
    }
  },
  mutations: {
    /* 保存類別信息 */
    [LOAD_CATEGORIES](state, payload) {
      state.docs = payload
    }
  }
}
export default category

【數據傳遞】

  組件間的數據傳遞方式通常有三種,一種是使用vue中的props和自定義事件,另外一種是使用路由的params屬性,還有一種是經過vuex

  一、props和自定義事件

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

  二、路由的params屬性

// Post.vue
 <BaseBack @click.native="$router.push($route.params.parentPath || '/')">返回</BaseBack>

//AuthSign.vue
<template>
    <router-link
        :active-class="$style.active"
        :to="{ name: 'signin', params: { parentPath } }"
    >登&nbsp;錄</router-link>
</template>
<script>
export default {
  computed: {
    parentPath() {
      const temp = this.$route.params.parentPath
      if (temp) {
        return temp
      }
      return ''
    }
  }
}
</script>

  三、使用vuex

// Category.vue
<template>
  <article v-if="category" :class="$style.box">
    <BaseBack @click.native="$router.push('/categories')">類別列表</BaseBack>
    <BaseTitle>{{ category.name }}知識體系</BaseTitle>
    ...
  </article>
</template>
<script>
export default {
  computed: {
    category() {
      return this.$store.getters.getCategoryByNumber(Number(this.paramsNumber))
    }
    ...
  }
}
</script>

 

項目優化

【離線緩存】

  經過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'
      }
    ]
  })
]

【添加到桌面】

  andriod下,經過設置manifest.json文件添加到桌面,而IOS則須要設置meta標籤

<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="前端小站">
<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": "小火柴的前端小站",
  "short_name": "前端小站",
  "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"
    }
  ]
}

【子頁面刷新】

  子頁面刷新時,可能會出現得不到從父級傳遞過來的數據的狀況,筆者的處理是跳轉到父級頁面

mounted() {
  if (!this.comment && this.operate === 'update') {
    this.$router.push(`/posts/${this.postId}/comments`)
  } else {
    this.setTextAreaValue()
  }
}

【promise】

  爲actions添加Promise,方便狀態改變後的處理

[LOAD_COMMENTS_ASYNC]({ commit }, payload) {
  return new Promise((resolve, reject) => {
    this._vm.$axios({
      commit,
      data: payload,
      url: BASE_COMMENT_URL,
      doHideAlert: true,
      success(result) {
        // 保存類別
        commit(LOAD_COMMENTS, result.docs)
        // 向前端通知操做成功
        resolve(result.docs)
      },
      fail(err) {
        // 向前端通知操做失敗
        reject(err)
      }
    })
  })
}

【組件共用】

  因爲編輯和新建組件用到的元素是同樣的,只不過,新建組件時內容爲空,編輯組件時須要添加內容,這時就能夠複用組件

// AddComment.vue
<CommentForm operate="add" />

//UpdateComment.vue
<CommentForm operate="update" />

【清理環境】

  若是使用addEventListener綁定了事件處理函數,在組件銷燬的時候,要及時清理環境

mounted() {
  window.addEventListener('devicemotion', throttle(this.testShake))
}
beforeDestroy() {
  window.removeEventListener('devicemotion', throttle(this.testShake))
}

【應用和數據分離】

  使用配置數據,實現數據和應用分離,配置數據主要是API調用地址,以常量的形式存儲在constants目錄下

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

【函數節流】

  爲觸發頻率較高的函數使用函數節流

/**
 * 函數節流
 * @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預解析】

  DNS預解析經過設置meta標籤實現

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

【圖片懶加載和webp】

  經過vue-lazyload插件實現圖片懶加載和andriod系統下圖片轉換成webp格式

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'
      }
    }
  }
})

 

功能實現

【搖一搖效果】

  搖一搖效果主要經過監測devicemotion事件實現

  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
    }
  }

【陀螺儀效果】

  陀螺儀效果主要經過監測deviceorientation事件實現

  mounted() {
    // 監測陀螺儀
    window.addEventListener('deviceorientation', throttle(this.changeBeta))
  },
  beforeDestroy() {
    // 取消監測
    window.removeEventListener('deviceorientation', throttle(this.changeBeta))
  },
  methods: {
    changeBeta(e) {
      if (this.beta !== Math.round(e.beta)) {
        this.beta = Math.round(e.beta)
      }
    }
  }

【緩動彈出層】

  過渡彈出層有兩種實現方式,包括transition和animation,該項目使用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;
}

【圖標管理】

  全部的圖標都使用SVG格式,存儲在common/SVG目錄下

// 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函數封裝】

  封裝axios函數到utils目錄下的async.js文件中,將loading組件、alert組件整合到axios函數的整個數據獲取過程當中

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 }) => {
      // 顯示loading
      commit(SHOW_LOADING)
      let axiosObj = url
      if (method) {
        axiosObj = { method, url, data, headers }
      }
      axios(axiosObj)
        .then(res => {
          const { message, result } = res.data
          // 關閉loading
          commit(HIDE_LOADING)
          // 顯示成功提示
          !doHideAlert && commit(SHOW_ALERTTEXT, message)
          // 1秒後自動關閉提示
          setTimeout(() => { commit(HIDE_ALERTTEXT) }, 1000)
          // 成功後的回調函數
          success && success(result)
        })
        .catch(err => {
          // 關閉loading
          commit(HIDE_LOADING)
          if (err.response) {
            const { data } = err.response
            // 自定義錯誤
            if (data.code === 1) {
              commit(SHOW_ALERTTEXT, data.message)
              // 系統錯誤
            } else if (data.code === 2) {
              commit(SHOW_ALERTTEXT, data.message)
              fail && fail(err)
              // 認證錯誤
            } else if (data.code === 3) {
              commit(SHOW_ALERTTEXT, data.message)
              commit(SIGNOUT)
              window.location.href = '/signin_by_username'
            } else {
              // 顯示錯誤提示
              commit(SHOW_ALERTTEXT, '服務器故障')
              // 失敗後的回調函數
              fail && fail(err)
            }
          } else {
            // 顯示錯誤提示
            commit(SHOW_ALERTTEXT, '服務器故障')
            // 失敗後的回調函數
            fail && fail(err)
          }
        })
    }
  }
}

export default async

【目錄跳轉】

  使用scrollIntoView()方法,點擊目錄時,文章跳轉到相關部分,且不改變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' })
  }
}

 

兼容處理

【錨點】

  使用錨點進行頁面內跳轉時,URL發生改變,頁面刷新,其餘瀏覽器沒有問題。可是,ISO下的PWA桌面圖標會跳轉到safari瀏覽器中

  使用scrollIntoView()方法來替代錨點#,頁面內只跳轉不刷新。andriod下支持給scrollIntoView設置平滑滾動behavior: 'smooth',但IOS不支持

【頁面放大】

  IOS下,input獲取焦點時會放大,meta設置user-scalable=no,可取消放大效果

<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, shrink-to-fit=no">

【圓角】

  IOS下,input域只顯示底邊框時,會出現底邊圓角效果,設置border-radius:0便可

border-radius:0

【輪廓outline】

  android瀏覽器下,input域處於焦點狀態時,默認會有一圈淡黃色的輪廓outline效果

  經過設置outline:none可將其去除

outline: none

【點擊背景】

  在移動端,點擊可點擊元素時,android下會出現淡藍色背景,IOS下會出現灰色背景

  能夠經過-webkt-tap-hightlight-color屬性的設置,取消點擊時出現的背景效果

* {
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

【局部不滾動】

  IOS下,可能會出現局部滾動不流暢,甚至局部不滾動的bug

  經過在該元素上設置overflow-scrolling屬性爲touch便可解決

div {
  -webkit-overflow-scrolling: touch;
}

【錨點】

  使用錨點進行頁面內跳轉時,URL發生改變,頁面刷新,其餘瀏覽器沒有問題。可是,ISO下的PWA桌面圖標會跳轉到safari瀏覽器中

  使用scrollIntoView()方法來替代錨點#,頁面內只跳轉不刷新。andriod下支持給scrollIntoView設置平滑滾動behavior: 'smooth',但IOS不支持

相關文章
相關標籤/搜索