• 转载: 使用vue全家桶制作博客网站 HTML5 移动网站制作的好教程


    使用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(`^${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

      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}/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不支持

    https://www.cnblogs.com/xiaohuochai/p/9228543.html

  • 相关阅读:
    java的锁机制
    视图生命周期
    UIButton @selector 想要传递多个参数
    UIButton @selector 想要传递多个参数
    UITableView 实现A1A2---Z1Z2.。。。。
    iOS 代理
    PickerView
    照片墙
    分栏控制器
    XIB 拖控件
  • 原文地址:https://www.cnblogs.com/porter/p/9232553.html
Copyright © 2020-2023  润新知