最近在 Vue 專案中用到的一些小技巧,或許有用

MeFelixWang發表於2018-11-03

寫在前面

在最近的 Vue 專案中,為了完成需求使用了一些小技巧,做個筆記,或許也能幫到道友。

閱讀重點

需求一:為路徑配置別名

在開發過程中,我們經常需要引入各種檔案,如圖片、CSS、JS等,為了避免寫很長的相對路徑(../),我們可以為不同的目錄配置一個別名。

找到 webpack.base.config.js 中的 resolve 配置項,在其 alias 中增加別名,如下:

最近在 Vue 專案中用到的一些小技巧,或許有用

建立一個 CSS 檔案,隨便寫點樣式:

.avatar
  display: flex;
  justify-content: center;
  align-items: center;

.avatar-img
  padding 20px
  border solid 1px #ccc
  border-radius 5px
複製程式碼

接著,在我們需要引入的檔案中就可以直接使用了:

<template>
  <div class="avatar">
    <img class="avatar-img" src="~img/avatar.png" alt="">
  </div>
</template>

<script>
  export default {
    name: "Home"
  }
</script>

<style scoped lang="stylus">
  @import "~css/avatar";
</style>
複製程式碼

需要注意的是,如果不是通過 import 引入則需要在別名前加上 ~,效果如下:

最近在 Vue 專案中用到的一些小技巧,或許有用

需求二:要求實現在生產包中直接修改api地址

這個需求,怎麼說呢,反正就是需求,就想辦法實現吧。

假設有一個 apiConfig.js 檔案,用於對 axios 做一些配置,如下:

import axios from 'axios';

axios.defaults.timeout = 10000;
axios.defaults.retry = 3;
axios.defaults.retryDelay = 2000;
axios.defaults.responseType = 'json';
axios.defaults.withCredentials = true;
axios.defaults.headers.post["Content-type"] = "application/json";

// Add a request interceptor
axios.interceptors.request.use(function (config) {
  // Do something before request is sent
  return config;
}, function (error) {
  // Do something with request error
  return Promise.reject(error);
});

// Add a response interceptor
axios.interceptors.response.use(function (response) {
  // Do something with response data
  return response;
}, function (error) {
  // Do something with response error
  return Promise.reject(error);
});

export default axios
複製程式碼

static 資料夾中增加一個 config.json 檔案,用於統一管理所有的 api 地址:

{
  "base": "/api",
  "static": "//static.com/api",
  "news": "//news.com.api"
}
複製程式碼

開啟 main.js,寫入下列程式碼:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import axios from 'js/apiConfig';      //import直接引入,不用新增~

Vue.config.productionTip = false;
Vue.use(ElementUI);

/* eslint-disable no-new */
let startApp = function () {
  let randomStamp = new Date().getTime();
  axios.get(`/static/config.json?t=${randomStamp}`).then((data) => {
    axios.defaults.baseURL = data.base;     //設定一個預設的根路徑
    Vue.prototype.$axios = axios;
    Vue.prototype.$apiURL = data;   //將所有路徑配置掛載到 Vue 原型上
    /* eslint-disable no-new */
    new Vue({
      el: '#app',
      router,
      components: {App},
      template: '<App/>'
    });
  })
};
startApp();

複製程式碼

就是先用 axios 獲取 api 檔案,然後再初始化。

需求三:由後臺根據使用者許可權值返回選單

選單是樹形結構(PS:就算不是樹形結構,你也得處理成樹形結構),我這裡使用的是 ElementUI ,參考了道友的這篇文章,實現如下:

新建一個 Menu.vue 檔案,寫入如下程式碼:

<script>
  export default {
    name: "MenuItem",
    props: {
      data: {
        type: Array
      },
      collapse: {
        type: Boolean
      }
    },
    methods: {
      //生成選單項
      createMenuItem(data, createElement) {
        return data.map(item => {
          if (item.children && item.children.length) {
            return createElement('el-submenu', {props: {index: item.id.toString()}},
              [
                createElement('template', {slot: 'title'}, [
                    createElement('i', {class: item.icon}),
                    createElement('span', [item.title]),
                  ]
                ),
                this.createMenuItem(item.children, createElement)   //遞迴
              ]
            )
          } else {
            return createElement('el-menu-item', {props: {index: item.path}},
              [
                createElement('i', {class: item.icon}),
                createElement('span', {slot: 'title'}, [item.title]),
              ]
            )
          }
        })
      },
      //選中選單
      onSelect(key, keyPath) {
        console.log(key, keyPath);
      }
    },
    render(createElement) {
      return createElement(
        'el-menu',
        {
          props: {
            backgroundColor: "#545c64",
            textColor: "#fff",
            activeTextColor: "#ffd04b",
            collapse: this.collapse,
            router:true
          },
          class:'el-menu-vertical-demo',
          on: {
            select: this.onSelect
          }
        },
        this.createMenuItem(this.data, createElement)
      )
    }
  }
</script>

<style scoped lang="stylus">
  .el-menu-vertical-demo:not(.el-menu--collapse) {
    width: 200px;
    min-height: 400px;
  }
</style>
複製程式碼

這裡主要用到兩個東西,一個是 render 函式,一個是遞迴,如果不熟悉 render 函式的道友請點這裡。可能有道友會問為什麼不用模板,因為······做不到啊?,在 template 中只能有一個根元素,而 Vue 限制了不能對根元素使用 v-for;再者,通過在瀏覽器中檢視程式碼可以知道,選單就是 ul 加上 li,如果有了根元素會破壞標籤結構(雖然不影響功能,但還是覺得不舒服?)。然後,在需要使用的地方:

<template>
  <el-container>
    <el-aside width="auto">
      <Menu :data="menu" :collapse="isCollapsed"></Menu>
    </el-aside>
    <el-container>
      <el-header>
        <el-button type="text" icon="el-icon-d-arrow-left"
                   @click="isCollapsed=!isCollapsed"></el-button>
        <h3>MenuName</h3>
        <span>MeFelixWang</span>
      </el-header>
      <el-main>
        <router-view></router-view>
      </el-main>
    </el-container>
  </el-container>
</template>

<script>
  import Menu from '@/components/Menu';

  export default {
    name: 'App',
    data() {
      return {
        menu: [
          {
            title: '導航一',
            id: 1,
            path: '',
            icon: 'el-icon-search',
            children: [
              {
                title: '導航一槓一', id: 2, path: '', icon: '', children: [
                  {title: '導航一槓一槓一', id: 4, path: '/test', icon: '', children: []},
                  {
                    title: '導航一槓一槓二', id: 5, path: '', icon: '', children: [
                      {title: '導航一槓一槓二槓一', id: 6, path: '/6', icon: '', children: []},
                      {title: '導航一槓一槓二槓二', id: 7, path: '/7', icon: '', children: []},
                    ]
                  },
                ]
              },
              {title: '導航一槓二', id: 3, path: '/3', icon: '', children: []}
            ]
          },
          {title: '導航二', id: 8, path: '/8', icon: 'el-icon-setting', children: []},
          {title: '導航三', id: 9, path: '/9', icon: 'el-icon-document', children: []},
          {
            title: '導航四', id: 10, path: '', icon: 'el-icon-date', children: [
              {title: '導航四槓一', id: 11, path: '/11', icon: '', children: []},
              {
                title: '導航四槓二', id: 12, path: '', icon: '', children: [
                  {title: '導航四槓二槓一', id: 14, path: '/14', icon: '', children: []}
                ]
              },
              {title: '導航四槓三', id: 13, path: '/13', icon: '', children: []},
            ]
          },
        ],
        isCollapsed: false
      }
    },
    methods: {
      handleOpen(key, keyPath) {
        console.log(key, keyPath);
      },
      handleClose(key, keyPath) {
        console.log(key, keyPath);
      }
    },
    components: {
      Menu
    }
  }
</script>

<style lang="stylus">
  *
    margin 0
    padding 0

  html, body, .el-container, .el-aside
    height 100%

  .el-aside
    background-color rgb(84, 92, 100)

  .el-menu
    border-right solid 1px rgb(84, 92, 100)

  .el-header
    display flex
    justify-content space-between
    align-items center
    background-color aliceblue
    .el-button--text
      color: #606266;
      i
        font-weight bold
</style>
複製程式碼

效果如下:

最近在 Vue 專案中用到的一些小技巧,或許有用

需求四:這個 Select 選項是樹形結構,一定得是樹形結構

樹形結構就樹形結構吧,不就是樣式嘛,改改應該就可以了。

<template>
  <div>
    <el-select v-model="tree" placeholder="請選擇活動區域">
      <el-option v-for="(item,index) in options" :key="index" :label="item.label" :value="item.id"
                 :style="{paddingLeft:(item.level*10+20)+'px'}" :class="item.level?'is-sub':''"></el-option>
    </el-select>
    選擇的是:{{tree}}
  </div>
</template>

<script>
  export default {
    name: "Home",
    data() {
      return {
        tree: '',
        options: [],
        originData: [
          {
            label: '這是根一', id: 1, children: [
              {label: '這是莖一一', id: 2, children: []},
              {label: '這是莖一二', id: 3, children: []},
              {
                label: '這是莖一三', id: 4, children: [
                  {label: '這是葉一三一', id: 6, children: []},
                  {label: '這是葉一三二', id: 7, children: []},
                ]
              },
              {label: '這是莖一四', id: 5, children: []},
            ]
          },
          {
            label: '這是根二', id: 8, children: [],
          },
          {
            label: '這是根三', id: 9, children: [
              {label: '這是莖三一', id: 10, children: []},
              {
                label: '這是莖三二', id: 11, children: [
                  {label: '這是葉三二一', id: 12, children: []}
                ]
              },
            ],
          },
        ]
      }
    },
    created() {
      this.options = this.decomposeTree(this.originData, 0);
    },
    methods: {
      //分解樹形結構
      decomposeTree(array, level) {
        let tmpArr = [];

        (function decompose(arr, lev) {
          for (let i = 0; i < arr.length; i++) {
            let tmpObj = {};
            let item = arr[i];
            item.level = lev;
            tmpObj = Object.assign({}, item);
            tmpArr.push(tmpObj);
            if (item.children) {
              decompose(item.children, lev + 1);    //遞迴
            }
            delete tmpObj.children;     //刪掉其 children,避免資料過大(不刪也可以,也許後面有用呢)
          }

        })(array, level);

        return tmpArr;
      }
    }
  }
</script>

<style scoped lang="stylus">
  .is-sub:before
    content '- '
</style>
複製程式碼

因為 option 接收的是一個一維陣列,所以通過遞迴展平樹形結構,在展平的時候設定每項的層級,通過層級來設定縮排及字首符號,效果如下:

最近在 Vue 專案中用到的一些小技巧,或許有用

之所以這樣做,是因為是管理系統,簡單有效,沒必要因為這一個元件引個新的外掛或者自己寫一個(以後用得著的除外哈);也可以用 input 加上 tree 控制元件來模擬(PS:最終還是引入了一個外掛,哈哈?)。

需求五:要讓使用者可以自定義顯示模版

這個需求是讓使用者自己寫模版,是的,沒錯,就是要讓使用者自己寫模版,嗯,和根據使用者手機殼改變介面顏色一樣的需求,看了動態元件和非同步元件,自己試了幾種方式,還是不行,後來在社群論壇看到有位大佬的回答(還真有人遇到和我一樣的需求(┬_┬)),這是地址,實現如下:

<template>
  <component :is="dynComponent" v-bind="data"></component>
</template>

<script>
  export default {
    name: "test",
    props: ['test'],
    data() {
      return {
        template: '',
        data: {}
      }
    },
    created() {
      this.getTemplate()    // 獲取模版
    },
    methods: {
      getTemplate() {
        this.$axios.get('http://localhost:8080/static/test.json').then((result) => {
          this.template = result.template;
          this.data = result;
        });
      }
    },
    computed: {
      dynComponent() {
        const template = this.template ? `<div>${this.template}</div>` : `<div>nothing here yet</div>`;
        return {
          template, // template 就是模版
          props: ['data'] // 傳入資料
        }
      },
    }
  }
</script>

<style scoped lang="stylus">

</style>
複製程式碼

因為 JS 是單執行緒,在非同步模版回來前就會渲染頁面,此方法是通過計算屬性的響應式特性,在取回模版後讓 Vue 重新渲染一次。給這位大佬獻上膝蓋。如果道友們有更好的方法,麻煩告訴我一下,感激不盡!

需求六:遠端圖片載入失敗,設定預設圖片

有些圖片可能來自於另一個網站或者啥的,反正道友們都懂,這個時候要設定一張預設圖,實現如下:

<template>
  <div>
    <img v-bind:src="imgUrl" @error="handleError" alt="">
  </div>
</template>

<script>
  export default {
    name: "userList",
    data() {
      return {
        imgUrl: 'url of the image'
      }
    },
    methods: {
      handleError(e) {
        e.target.src = '/static/default.png'
      }
    }
  }
</script>

<style scoped lang="stylus">

</style>
複製程式碼

static 資料夾中放一張預設圖,然後處理 imgonerror 事件,將 src 設定 static 中預設圖片路徑。

需求七:單頁應用中在切換頁面時應該終止之前的請求

這個需求,很合理!如果不終止之前的請求,就可能在新頁面看到之前請求成功(或失敗)後彈出的一些提示資訊,這肯定是不合理的。怎麼實現呢? axios 提供了取消請求的方法:

最近在 Vue 專案中用到的一些小技巧,或許有用

但這裡有個小問題,一個頁面的請求可能有很多,那麼在切換頁面的時候肯定不能一個一個的取消(而且你也不知道具體呼叫了哪些介面),網友這裡提供了一個方法,我做了一些優化:

 init() {
        let self = this;
        //配置全域性取消陣列
        window.__axiosPromiseArr = [];
        //請求攔截
        this.$axios.interceptors.request.use(function (config) {
          //為每個請求設定 cancelToken
          config.cancelToken = new self.$axios.CancelToken(cancel => {
            window.__axiosPromiseArr.push({cancel})     //放入一個全域性陣列,以便之後統一取消
          });
          return config;
        }, function (error) {
          return Promise.reject(error);
        });
        //響應攔截
        this.$axios.interceptors.response.use((response) => {
          switch (response.status) {
            case 204:
              this.$message.success('操作成功!');
              break;
            default:
              return response.data;
          }
        }, (error) => {
          if (error.message === 'cancel') {     
                //終止請求會丟擲一個錯誤,捕獲一下,不讓其顯示在控制檯
          } 
        });
      },
複製程式碼

然後在路由守衛中:

vueRouter.beforeEach((to, from, next) => {
  //路由切換時終止所有請求
  let axiosPromiseArr = window.__axiosPromiseArr;
  if (axiosPromiseArr) {
    console.log(axiosPromiseArr);
    let len = axiosPromiseArr.length;
    while (len--) {     //從後向前終止請求,並刪除 cancelToken,避免陣列索引帶來的問題
      axiosPromiseArr[len].cancel('cancel');
      axiosPromiseArr.splice(len, 1);
    }
    //或者:window.__axiosPromiseArr = [];
  }
  next()
});
複製程式碼

目前好像這個方法還算實用,如果道友們有更好的方法,請留言告訴我一下。

最後叨叨

本文是我最近用到的一些小技巧,如果道友們有更好的實現方法,歡迎在評論區留言討論,文中錯誤也歡迎指出,共同學習(當然,有疑難需求也可以留言,一起商討解決方案?),本文會不定期更新,就當是筆記本了?。

相關文章