├── 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
自動生成,其它按需本身新建就行了。javascript
在不一樣的路由頁面,咱們須要動態的修改文檔標題,能夠將每一個頁面的標題配置在路由元信息 meta
裏面帶上,而後在 router.afterEach
鉤子函數中修改:php
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 = 'xxx商城會員平臺'; route.matched.forEach((path) => { if (path.meta.title) { documentTitle += ` - ${path.meta.title}`; } }); document.title = documentTitle; });
一般在一個列表集合頁,咱們須要作分頁操做,同時分頁數據須要體如今 URL 中,那麼如何動態的根據 URL 的變更來動態的獲取數據呢,咱們可使用 watch
API,在 watch
裏面監聽 $route
,同時使用 this.$router.replace
API 來改變 URL 的值。下面是示例代碼 common.js
:css
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 文檔:https://cn.vuejs.org/v2/guide...html
export default DemoComponent { mixins: [common], data() { return { // 組件內部自定義同名查詢參數,將會和 mixin 中的默認參數合併 queryParams: { categoryId: '', pageSize: 12 }, } }, methods: { fetchData() { // 發送請求 } } }
咱們在項目中引入了 vuex
,一般狀況下是不須要使用 event bus
的,可是有一種狀況下咱們須要使用它,那就是在路由鉤子函數內部的時,在項目中,咱們須要在 beforeEnter
路由鉤子裏面對外拋出事件,在這個鉤子函數中咱們沒法去到 this
對象。vue
beforeEnter: (to, from, next) => { const userInfo = localStorage.getItem(userFlag); if (isPrivateMode()) { EventBus.$emit('get-localdata-error'); next(false); return; } })
在 App.vue
的 mouted
方法中監聽這個事件java
EventBus.$on('get-localdata-error', () => { this.$alert('請勿使用無痕模式瀏覽'); });
在項目中一般須要作數據埋點,這個時候,使用自定義指令將會變很是簡單ios
在項目入口文件 main.js
中配置咱們的自定義指令nginx
// 坑位埋點指令 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 遠遠高於其它頁面,數據很不正常。後來跟數據後臺的人溝通詢問他們的埋點統計原理以後,才發現其中的問題所在。git
傳統應用,通常都在頁面加載的時候,會有一個異步的 js 加載,就像百度的統計代碼相似,因此咱們每一個頁面的加載的時候,都會統計到數據;然而在單頁應用,頁面加載初始化只有一次,因此其它頁面的統計數據須要咱們本身手動上報github
解決方案
使用 vue-router
的 beforeEach
或者 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(); } });
在組件中使用咱們的自定義指令
以下圖中獎金數據信息,咱們須要將後臺返回的獎金格式化爲帶兩位小數點的格式,同時,若是返回的金額是區間類型,須要額外加上 <span style="color:red;font-weight: bold;">起</span> 字和 <span style="color:red;font-weight: bold;">¥</span> 金額符號
在入口文件 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 作接口請求
在項目中全局配置 /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);
因爲項目是響應式頁面,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>
下圖分別是 PC 端和移動短的表現形式,而後配合 CSS 媒體查詢實現各類佈局
在項目目錄的 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 壓縮以後,頁面資源請求大小將大大減少,以下圖所示,表示已經開啓了 gzip
壓縮
文章到這就結束了,若是有遺漏或者錯誤的地方,歡迎私信指出。但願這篇文章能帶給你們一絲絲收穫。