使用vue全家桶制作博客网站

前面的话

  笔者在做一个完整的博客上线项目,包括前台后台后端接口和服务器配置。本文将详细介绍使用 vue 全家桶制作的博客网站

 

概述

  该项目是基于 vue 全家桶(vue、vue-router、vuex、vue SSR)开发的一套博客前台页面,主要功能包括首页显示、认证系统、文章管理、评论管理和点赞管理

【访问地址】

  域名:https://xiaohuochai.cc

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

  或者可以直接扫描二维码访问

【项目介绍】

  该项目的内容以笔者自学前端的过程中写的 600 多篇博客为基础,对于同样学习前端的同学可能会有所帮助。许多博客都有直接可以操作的 DEMO,对知识的理解可能会更直观

  采用移动优先的响应式布局,移动端、桌面端均可适配;字体大小使用 em 单位,桌面端的文字相应变大;移动端可使用滑屏操作,桌面端通过光标设置、自定义滚动条、回车确定等,提升交互体验

  全站采用服务器端渲染 SSR 的方式,有利于 SEO,减少了首屏渲染时间;使用 service worker 和 manifest 实现了 PWA 方案的离线缓存和添加到桌面的功能

  根据 HTML 标签内容模型,使用语义化标签,尽量减少标签层级,尽量减少无语义的 div 标签

  CSS 大量使用类选择器,尽量减少选择器层级,在 vue 组件中使用 CSS module 和 postCSS,使用 styleLint 规范 CSS 代码,按照布局类属性、盒模型属性、文本类属性、修饰类属性的顺序编写代码,并使用 order 插件进行校验

  使用 esLint 规范 JS 代码,代码风格参照 airbnb 规范,所有命名采用驼峰写法,公共组件以 Base 为前缀,事件函数以 on 为前缀,异步函数以 async 为后缀,布尔值基本以 do 或 is 为前缀

  没有引用第三方组件库,如 bootstrap 或 element 组件,而是自己开发了项目中所需的公共组件。在 common 目录下,封装了头像、全屏、loading、遮罩、搜索框、联动选择等组件,方便开发

  使用配置数据,实现了数据和应用分离,以常量的形式存储在 constants 目录下

  使用了阿里云的短信模块,实现了短信验证功能

  该项目有两个隐藏彩蛋,一个是摇一摇功能,可以直接摇到后台页面,另一个是陀螺仪功能,上下晃动手机时,头像会进行旋转

  项目进行了代码优化,最终优化评分如下所示

 

功能演示

  主要功能包括首页显示、认证系统、文章管理、评论管理和点赞管理

【首页显示】

  首页包括可拖拽轮播图、专题推荐、文章推荐和类别推荐

【认证系统】

   认证系统包括用户注册、用户登录、短信验证

  1、用户处于未登录态时,可以阅读文章,但不能点赞和评论,否则会弹出登录框

  2、用户注册

  3、用户登录

【文章管理】

  文章管理包括浏览推荐文章、按类别筛选、文章搜索、按目录查看

  1、浏览推荐文章

  2、文章筛选

  3、文章搜索

  4、按目录查看

【点赞管理】

【评论管理】

  评论管理包括查看评论、添加评论、修改评论和删除评论

 

目录结构

  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(^<span style="color: #000000;">${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}(<span style="color: #800080;">0</span>[<span style="color: #800080;">1</span>-<span style="color: #800080;">9</span>]|[<span style="color: #800080;">1</span>-<span style="color: #800080;">9</span>][<span style="color: #800080;">0</span>-<span style="color: #800080;">9</span>])(<span style="color: #800080;">0</span>){${<span style="color: #800080;">8</span> -<span style="color: #000000;"> 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

  1、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)"
>

  2、路由的 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>

  3、使用 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}/<span style="color: #000000;">users
export const BASE_POST_URL = ${API_HOSTNAME}/<span style="color: #000000;">posts
export const BASE_TOPIC_URL = ${API_HOSTNAME}/<span style="color: #000000;">topics
export const BASE_CATEGORY_URL = ${API_HOSTNAME}/<span style="color: #000000;">categories
export const BASE_LIKE_URL = ${API_HOSTNAME}/<span style="color: #000000;">likes
export const BASE_COMMENT_URL = ${API_HOSTNAME}/<span style="color: #000000;">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 不支持