Vue2.0 專案開發總結

Heaven發表於2017-09-19

專案架構

專案目錄

├── build
├── config
├── dist
│   └── static
│       ├── css
│       ├── fonts
│       ├── images
│       ├── js
│       └── lib
├── src
│   ├── api
│   ├── assets
│   │   ├── global
│   │   └── images
│   │       └── footer
│   ├── components
│   │   ├── common
│   │   ├── news
│   │   └── profile
│   │       └── charge
│   ├── config
│   ├── mixin
│   ├── router
│   ├── service
│   ├── store
│   └── util
└── static
    ├── images
    └── lib複製程式碼

專案目錄是採用 vue-cli 自動生成,其它按需自己新建就好了。

開發實踐

動態修改 document title

在不同的路由頁面,我們需要動態的修改文件標題,可以將每個頁面的標題配置在路由元資訊 meta 裡面帶上,然後在 router.afterEach 鉤子函式中修改:

import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);
const router = new Router({
  mode: 'history',
  routes: [
    { path: '/', component: Index, meta: { title: '推薦產品得豐厚獎金' } },
    {
      path: '/news',
      component: News,
      meta: { title: '公告列表' },
      children: [
        { path: '', redirect: 'list' },
        { path: 'list', component: NewsList },
        { path: 'detail/:newsId', component: NewsDetail, meta: { title: '公告詳情' } }
      ]
    },
    {
      path: '/guide',
      component: GuideProtocol,
      meta: {
        title: '新手指南'
      }
    }
  ]
});

// 使用 afterEach 鉤子函式,保證路由已經跳轉成功之後修改 title
router.afterEach((route) => {
  let documentTitle = '魅族商城會員平臺';
  route.matched.forEach((path) => {
    if (path.meta.title) {
      documentTitle += ` - ${path.meta.title}`;
    }
  });

  document.title = documentTitle;
});複製程式碼

Event Bus 使用場景

我們在專案中引入了 vuex ,通常情況下是不需要使用 event bus 的,但是有一種情況下我們需要使用它,那就是在路由鉤子函式內部的時候,在專案中,我們需要在 beforeEnter 路由鉤子裡面對外丟擲事件。

beforeEnter: (to, from, next) => {
    const userInfo = localStorage.getItem(userFlag);
    if (isPrivateMode()) {
        EventBus.$emit('get-localdata-error');
        next(false);
        return;
    }
})複製程式碼

App.vuemouted 方法中監聽這個事件

EventBus.$on('get-localdata-error', () => {
    this.$alert('請勿使用無痕模式瀏覽');
});複製程式碼

根據 URL 的變化,動態更新資料

通常在一個列表集合頁,我們需要做分頁操作,同時分頁資料需要體現在 URL 中,那麼如何動態的根據 URL 的變動來動態的獲取資料呢,我們可以使用 watch API,在 watch 裡面監聽 $route,同時使用 this.$router.replace API 來改變 URL 的值。下面是示例程式碼 common.js


import qs from 'qs';

export default {
  data() {
    return {
      queryParams: {
        currentPage: 1,
        pageSize: 10
      }
    };
  },
  methods: {
    handlePageNoChange(e) {
      this.queryParams.currentPage = e;
      this.replaceRouter();
    },

    replaceRouter() {
      const query = qs.stringify(this.queryParams);
      this.$router.replace(`${location.pathname}?${query}`);
    },

    routeChange() {
      this.assignParams();
      this.fetchData();
    },

    assignParams() {
      this.queryParams = Object.assign({}, this.queryParams, this.$route.query);
    }
  },
  mounted() {
    this.assignParams();
    this.fetchData();
  },
  watch: {
    $route: 'routeChange'
  }
};複製程式碼

我們將這部分程式碼抽取到一個公共的 mixin 中,在需要的元件那裡引入它,同時實現自定義的同名 fetchData() 方法
mixin API 文件:cn.vuejs.org/v2/guide/mi…

export default DemoComponent {
  mixins: [common],
  data() {
    return {
      // 元件內部自定義同名查詢引數,將會和 mixin 中的預設引數合併
      queryParams: {
        categoryId: '',
        pageSize: 12
      },
    }
  },
  methods: {
    fetchData() {
       // 傳送請求
    }
  }
}複製程式碼

自定義指令實現埋點資料統計

在專案中通常需要做資料埋點,這個時候,使用自定義指令將會變非常簡單

在專案入口檔案 main.js 中配置我們的自定義指令

// 坑位埋點指令
Vue.directive('stat', {
  bind(el, binding) {
    el.addEventListener('click', () => {
      const data = binding.value;
      let prefix = 'store';
      if (OS.isAndroid || OS.isPhone) {
        prefix = 'mall';
      }
      analytics.request({
        ty: `${prefix}_${data.type}`,
        dc: data.desc || ''
      }, 'n');
    }, false);
  }
});複製程式碼

在元件中使用我們的自定義指令

使用路由攔截統計頁面級別的 PV

由於第一次在單頁應用中嘗試資料埋點,在專案上線一個星期之後,資料統計後臺發現,首頁的 PV、UV 遠遠高於其它頁面,資料很不正常。後來跟資料後臺的人溝通詢問他們的埋點統計原理之後,才發現其中的問題所在。

傳統應用,一般都在頁面載入的時候,會有一個非同步的 js 載入,就像百度的統計程式碼類似,所以我們每個頁面的載入的時候,都會統計到資料;然而在單頁應用,頁面載入初始化只有一次,所以其它頁面的統計資料需要我們自己手動上報

解決方案

使用 vue-routerbeforeEach 或者 afterEach 鉤子上報資料,具體使用哪個最好是根據業務邏輯來選擇。

const analyticsRequest = (to, from) => {
  // 只統計頁面跳轉資料,不統計當前頁 query 不同的資料
  // 所以這裡只使用了 path, 如果需要統計 query 的,可以使用 to.fullPath
  if (to.path !== from.path) {
    analytics.request({
      url: `${location.protocol}//${location.host}${to.path}`
    });
  }
};

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // 這裡做登入等前置邏輯判斷
    // 判斷通過之後,再上報資料
    ...
    analyticsRequest(to, from);
  } else {
    // 不需要判斷的,直接上報資料
    analyticsRequest(to, from);
    next();
  }
});複製程式碼

使用過濾器實現展示資訊格式化

如下圖中獎金資料資訊,我們需要將後臺返回的獎金格式化為帶兩位小數點的格式,同時,如果返回的金額是區間型別,需要額外加上 字和 金額符號

在入口檔案 main.js 中配置我們自定義的過濾器

Vue.filter('money', (value, config = { unit: '¥', fixed: 2 }) => {
  const moneyStr = `${value}`;
  if (moneyStr.indexOf('-') > -1) {
    const scope = moneyStr.split('-');
    return `${config.unit}${parseFloat(scope[0]).toFixed(config.fixed).toString()} 起`;
  } else if (value === 0) {
    return value;
  }

  return `${config.unit}${parseFloat(moneyStr).toFixed(config.fixed).toString()}`;
});複製程式碼

在元件中使用:

<p class="price">{{detail.priceScope | money}}</p>
<div :class="{singleWrapper: isMobile}">
    <p class="rate">比率:{{detail.commissionRateScope}}%</p>
    <p class="income">獎金:{{detail.expectedIncome | money}}</p>
</div>複製程式碼

axios 使用配置

在專案中,我們使用了 axios 做介面請求

在專案中全域性配置 /api/common.js

import axios from 'axios';
import qs from 'qs';
import store from '../store';

// 全域性預設配置
// 設定 POST 請求頭
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
// 配置 CORS 跨域
axios.defaults.withCredentials = true;
axios.defaults.crossDomain = true;

// 請求發起前攔截器
axios.interceptors.request.use((config) => {
  // 全域性 loading 狀態,觸發 loading 效果
  store.dispatch('updateLoadingStatus', {
    isLoading: true
  });

  // POST 請求引數處理成 axios post 方法所需的格式
  if (config.method === 'post') {
    config.data = qs.stringify(config.data);
  }

  // 這句不能省,不然後面的請求就無法成功發起,因為讀不到配置引數
  return config;
}, () => {
  // 異常處理
  store.dispatch('updateLoadingStatus', {
    isLoading: false
  });
});

// 響應攔截
axios.interceptors.response.use((response) => {
  // 關閉 loading 效果
  store.dispatch('updateLoadingStatus', {
    isLoading: false
  });

  // 全域性登入過濾,如果沒有登入,直接跳轉到登入 URL
  if (response.data.code === 300) {
    // 未登入
    window.location.href = getLoginUrl();
    return false;
  }

  // 這裡返回的 response.data 是被 axios 包裝過的一成,所以在這裡抽取出來
  return response.data;
}, (error) => {
  store.dispatch('updateLoadingStatus', {
    isLoading: false
  });
  return Promise.reject(error);
});

// 匯出
export default axios;複製程式碼

然後我們在介面中使用就方便很多了 /api/xxx.js

import axios from './common';

const baseURL = '/api/profile';
const USER_BASE_INFO = `${baseURL}/getUserBaseInfo.json`;
const UPDATE_USER_INFO = `${baseURL}/saveUserInfo.json`;

// 更新使用者實名認證資訊
const updateUserInfo = userinfo => axios.post(UPDATE_USER_INFO, userinfo);

// 獲取使用者基礎資訊
const getUserBaseInfo = () => axios.get(USER_BASE_INFO);複製程式碼

vuex 狀態在響應式頁面中的妙用

由於專案是響應式頁面,PC 端和移動端在表現成有很多不一致的地方,有時候單單通過 CSS 無法實現互動,這個時候,我們的 vuex 狀態就派上用場了,

我們一開始在 App.vue 裡面監聽了頁面的 resize 事件,動態的更新 vuex 裡面 isMobile 的狀態值

window.onresize = throttle(() => {
 this.updatePlatformStatus({
   isMobile: isMobile()
 });
}, 500);複製程式碼

然後,我們在元件層,就能響應式的渲染不同的 dom 結構了。其中最常見的是 PC 端和移動端載入的圖片需要不同的規格的,這個時候我們可以這個做

methods: {
  loadImgAssets(name, suffix = '.jpg') {
    return require(`../assets/images/${name}${this.isMobile ? '-mobile' : ''}${suffix}`);
  },
}

<img class="feed-back" :src="loadImgAssets('feed-back')"

<img v-lazy="{src: isMobile ? detail.imgUrlMobile : detail.imgUrlPc, loading: placeholder}">

// 動態渲染不同規格的 dislog
<el-dialog :visible.sync="dialogVisible" :size="isMobile ? 'full' : 'tiny'" top="30%" custom-class="unCertification-dialog">
</el-dialog>複製程式碼

等等

開發相關配置

反向代理

在專案目錄的 config 檔案下面的 index.js 配置我們的本地反向代理和埠資訊

dev: {
  env: require('./dev.env'),
  port: 80,
  autoOpenBrowser: true,
  assetsSubDirectory: 'static',
  assetsPublicPath: '/',
  proxyTable: {
    '/api/profile': {
      target: '[真實介面地址]:[埠號]', // 例如: http://api.xxx.com
      changeOrigin: true,
      pathRewrite: {
        '^/api/profile': '/profile'
      }
    }
    ...
  },複製程式碼

然後我們呼叫介面的形式就會變成如下對映,當我們呼叫 /api/profile/xxxx 的時候,其實是呼叫了 [真實介面地址]/profile/xxxx

/api/profile/xxxx => [真實介面地址]/profile/xxxx複製程式碼

nginx 配置

upstream api.xxx.com
{
 #ip_hash;
  server [介面伺服器 ip 地址]:[埠];
}

server {
  ...
  location ^~ /api/profile {
    index index.php index.html index.html;
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://api.xxx.com;

    rewrite ^/api/profile/(.*)$ /profile/$1 break;
  }
  ...
}複製程式碼

線上部署

如果路由使用的是 history 模式的話,需要在 nginx 裡面配置將所有的請求到轉發到 index.html

nginx.conf 或者對應的站點 vhost 檔案下面配置

location / {
    try_files $uri $uri/ /index.html;
}複製程式碼

優化

開啟靜態資源長快取

location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|woff|ttf|eot|svg)$ {
    expires 1y;
}

location ~ .*\.(js|css)$ {
    expires 1y;
}複製程式碼

開啟靜態資源 gzip 壓縮

// 找到 nginx.conf 配置檔案
vim /data/nginx/conf/nginx.conf

gzip on;
gzip_min_length  1k;
gzip_buffers     4 8k;
gzip_http_version 1.1;
gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;複製程式碼

開啟了 gzip 壓縮之後,頁面資源請求大小將大大減小

Q&A

還有一個小問題,就是在某些瀏覽器隱私模式下,js 是不具備對 localStorage 寫的許可權的,這一點在開發的時候需要特別注意下錯誤的處理。

相關文章