github地址: github.com/LeeStaySmal… (完整分支:optimize分支)javascript
demo地址: vue-project-demo.eloco.cnphp
安裝css
node >= 8.9 推薦:8.11.0 +html
安裝:npm install -g @vue/cli
前端
檢查:vue --version
vue
若是已安裝舊版本,須要先npm uninstall vue-cli -g
卸載掉舊版本。java
初始化架構node
vue create project-name
注:項目名稱不能駝峯命名。webpack
選擇css 預處理器(Sass/SCSS): ios
選擇eslint 配置(ESLint + Standard config):
選擇何時執行eslint校驗(Lint on save):
選擇以什麼樣的形式配置以上所選的功能(In dedicated config files):
若是選擇 y 會讓輸入名稱,以便下次直接使用,不然直接開始初始化項目。
/src/components
建立 SvgIcon.vue
:
src/
下建立 icons
文件夾,以及在其下建立svg
文件夾用於存放svg文件,建立index.js
做爲入口文件:編寫index.js 的腳本:
import Vue from 'vue' import SvgIcon from '@/components/SvgIcon.vue' // svg組件 // 全局註冊 Vue.component('svg-icon', SvgIcon) const requireAll = requireContext => requireContext.keys().map(requireContext) const req = require.context('./svg', false, /\.svg$/) requireAll(req) 複製代碼
svg-sprite-loader
對項目中使用的svg
進行處理:npm install svg-sprite-loader --save-dev
;
修改默認的webpack
配置, 在項目根目錄建立vue.config.js
,代碼以下;
const path = require('path') function resolve(dir) { return path.join(__dirname, './', dir) } module.exports = { chainWebpack: config => { // svg loader const svgRule = config.module.rule('svg') // 找到svg-loader svgRule.uses.clear() // 清除已有的loader, 若是不這樣作會添加在此loader以後 svgRule.exclude.add(/node_modules/) // 正則匹配排除node_modules目錄 svgRule // 添加svg新的loader處理 .test(/\.svg$/) .use('svg-sprite-loader') .loader('svg-sprite-loader') .options({ symbolId: 'icon-[name]' }) // 修改images loader 添加svg處理 const imagesRule = config.module.rule('images') imagesRule.exclude.add(resolve('src/icons')) config.module .rule('images') .test(/\.(png|jpe?g|gif|svg)(\?.*)?$/) } } 複製代碼
main.js
中引入import '@/icons'
便可;// 使用示例 <svg-icon icon-class="add" /> 複製代碼
PS:至於svg ,我的比較建議使用阿里開源的圖標庫 iconFont
axios
: npm install axios
;src
目錄下建立utils/
, 並建立request.js
用來封裝axios
,上代碼:import axios from 'axios' // 建立axios 實例 const service = axios.create({ baseURL: process.env.BASE_API, // api的base_url timeout: 10000 // 請求超時時間 }) // request 攔截器 service.interceptors.request.use( config => { // 這裏能夠自定義一些config 配置 return config }, error => { // 這裏處理一些請求出錯的狀況 console.log(error) Promise.reject(error) } ) // response 攔截器 service.interceptors.response.use( response => { const res = response.data // 這裏處理一些response 正常放回時的邏輯 return res }, error => { // 這裏處理一些response 出錯時的邏輯 return Promise.reject(error) } ) export default service 複製代碼
axios
,必不可少的須要配置環境變量以及須要請求的地址,這裏能夠簡單的修改poackage.json
:"scripts": { "dev": "vue-cli-service serve --project-mode dev", "test": "vue-cli-service serve --project-mode test", "pro": "vue-cli-service serve --project-mode pro", "pre": "vue-cli-service serve --project-mode pre", "build:dev": "vue-cli-service build --project-mode dev", "build:test": "vue-cli-service build --project-mode test", "build:pro": "vue-cli-service build --project-mode pro", "build:pre": "vue-cli-service build --project-mode pre", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, 複製代碼
同時修改vue.config.js:
const path = require('path') function resolve(dir) { return path.join(__dirname, './', dir) } module.exports = { chainWebpack: config => { // 這裏是對環境的配置,不一樣環境對應不一樣的BASE_API,以便axios的請求地址不一樣 config.plugin('define').tap(args => { const argv = process.argv const mode = argv[argv.indexOf('--project-mode') + 1] args[0]['process.env'].MODE = `"${mode}"` args[0]['process.env'].BASE_API = '"http://47.94.138.75:8000"' return args }) // svg loader const svgRule = config.module.rule('svg') // 找到svg-loader svgRule.uses.clear() // 清除已有的loader, 若是不這樣作會添加在此loader以後 svgRule.exclude.add(/node_modules/) // 正則匹配排除node_modules目錄 svgRule // 添加svg新的loader處理 .test(/\.svg$/) .use('svg-sprite-loader') .loader('svg-sprite-loader') .options({ symbolId: 'icon-[name]' }) // 修改images loader 添加svg處理 const imagesRule = config.module.rule('images') imagesRule.exclude.add(resolve('src/icons')) config.module .rule('images') .test(/\.(png|jpe?g|gif|svg)(\?.*)?$/) } } 複製代碼
src/
下建立api
目錄,用來統一管理全部的請求,好比下面這樣:這樣的好處是方便管理、後期維護,還能夠和後端的微服務對應,創建多文件存放不一樣模塊的api
。剩下的就是你使用到哪一個api時,本身引入即可。
拓展:服務端的cors設置
牽涉到跨域,這裏採用cors
,不少朋友在面試中常常會被問到cors的實現原理,這個網上有不少理論大可能是這樣講的:
其實,這樣理解很抽象,服務器端究竟是怎麼作驗證的?
這裏你們能夠通俗的理解爲後端在接收前端的request
請求的時候,會有一個request
攔截器,像axios response
攔截器同樣。下面以php lumen
框架爲例,來深刻理解一下這個流程:
<?php namespace App\Http\Middleware; use App\Http\Utils\Code; use Closure; use Illuminate\Http\Response; use Illuminate\Support\Facades\Log; class CorsMiddleware { private $headers; /** * 全局 : 解決跨域 * @param $request * @param \Closure $next * @return mixed * @throws \HttpException */ public function handle($request, Closure $next) { //請求參數 Log::info('http request:'.json_encode(["request_all" => $request->all()])); $allowOrigin = [ 'http://47.94.138.75', 'http://localhost', ]; $Origin = $request->header("Origin"); $this->headers = [ 'Access-Control-Allow-Headers' => 'Origin,x-token,Content-Type', 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Credentials' => 'true',//容許客戶端發送cookie 'Access-Control-Allow-Origin' => $Origin, //'Access-Control-Max-Age' => 120, //該字段可選,間隔2分鐘驗證一次是否容許跨域。 ]; //獲取請求方式 if ($request->isMethod('options')) { if (in_array($Origin, $allowOrigin)) { return $this->setCorsHeaders(new Response(json_encode(['code' => Code::SUCCESS, "data" => 'success', "msg" => ""]), Code::SUCCESS)); } else { return new Response(json_encode('fail', 405)); } } $response = $next($request); //返回參數 Log::info('http response:'.json_encode($response)); return $this->setCorsHeaders($response); } /** * @param $response * @return mixed */ public function setCorsHeaders($response) { foreach ($this->headers as $key => $val) { $response->header($key, $val); } return $response; } } 複製代碼
vuex 篇
若是建立項目的時候,選擇了vuex
,那麼默認會在src
目錄下有一個store.js
做爲倉庫文件。但在更多實際場景中,若是引入vuex
,那麼確定避免不了分模塊,先來看一下默認文件代碼:
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { }, mutations: { }, actions: { } }) 複製代碼
那麼如今改造一下,好比先劃分出app
、user
兩個模塊,能夠這樣:
import Vue from 'vue' import Vuex from 'vuex' import app from './store/modules/app' import user from './store/modules/user' import getters from './store/getters' Vue.use(Vuex) const store = new Vuex.Store({ modules: { app, user }, getters }) export default store 複製代碼
在src/
下建立store/
目錄:
app module
能夠用來存儲應用的狀態,好比接下來要講到的全局loading
,或者控制第三方組件的全局大小,好比element ui
中的全局組件size
;
user module
能夠用來存儲當前用戶的信息;
固然,store 配合本地存儲比較完美,這裏採用js-cookie
。
全局loading
上面說完了axios、vuex
,如今結合以前說一下設置全局loading
效果。
日常寫代碼每一個請求以前通常都須要設置loading
,成功以後結束loading
效果,這就迫使咱們不得不寫大量重複代碼,若是不想這樣作,能夠結合axios
和vuex
統一作了。
vuex
的時候,我在src/
下建立了一個store
,如今就在store/modules/app.js
寫這個Loading
效果的代碼;const app = { state: { requestLoading: 0 }, mutations: { SET_LOADING: (state, status) => { // error 的時候直接重置 if (status === 0) { state.requestLoading = 0 return } state.requestLoading = status ? ++state.requestLoading : --state.requestLoading } }, actions: { SetLoading ({ commit }, status) { commit('SET_LOADING', status) } } } export default app 複製代碼
utils/request.js
import axios from 'axios' import store from '@/store' // 建立axios 實例 const service = axios.create({ baseURL: process.env.BASE_API, // api的base_url timeout: 10000 // 請求超時時間 }) // request 攔截器 service.interceptors.request.use( config => { // 這裏能夠自定義一些config 配置 // loading + 1 store.dispatch('SetLoading', true) return config }, error => { // 這裏處理一些請求出錯的狀況 // loading 清 0 setTimeout(function () { store.dispatch('SetLoading', 0) }, 300) console.log(error) Promise.reject(error) } ) // response 攔截器 service.interceptors.response.use( response => { const res = response.data // 這裏處理一些response 正常放回時的邏輯 // loading - 1 store.dispatch('SetLoading', false) return res }, error => { // 這裏處理一些response 出錯時的邏輯 // loading - 1 store.dispatch('SetLoading', false) return Promise.reject(error) } ) export default service 複製代碼
src/components/
下建立 RequestLoading.vue
組件:<template> <transition name="fade-transform" mode="out-in"> <div class="request-loading-component" v-if="requestLoading"> <svg-icon icon-class="loading" /> </div> </transition> </template> <script> import { mapGetters } from 'vuex' export default { name: 'RequestLoading', computed: { ...mapGetters([ 'requestLoading' ]) } } </script> <style lang='scss' scoped> .request-loading-component { position: fixed; left: 0; right: 0; top: 0; bottom: 0; //background-color: rgba(48, 65, 86, 0.2); background-color: transparent; font-size: 150px; display: flex; flex-direction: row; justify-content: center; align-items: center; z-index: 999999; } </style> 複製代碼
最後,在app.vue
中引入便可。
附: 爲了方便演示,項目裏出了初始化包括axios
、vuex
、vue-router
, 項目使用了js-cookie
、element-ui
等,此步驟以後,會改造一下app.vue
;
vue router守衛
vue-router 提供了很是方便的鉤子,可讓咱們在作路由跳轉的時候作一些操做,好比常見的權限驗證。
src/utils/
下建立auth.js
,用於存儲token;import Cookies from 'js-cookie' const TokenKey = 'project-token' export function getToken () { return Cookies.get(TokenKey) } export function setToken (token) { return Cookies.set(TokenKey, token) } export function removeToken () { return Cookies.remove(TokenKey) } 複製代碼
在src/utils/
下建立permission.js
:
import router from '@/router' import store from '@/store' import { getToken } from './auth' import NProgress from 'nprogress' // 進度條 import 'nprogress/nprogress.css' // 進度條樣式 import { Message } from 'element-ui' const whiteList = ['/login'] // 不重定向白名單 router.beforeEach((to, from, next) => { NProgress.start() if (getToken()) { if (to.path === '/login') { next({ path: '/' }) NProgress.done() } else { // 實時拉取用戶的信息 store.dispatch('GetUserInfo').then(res => { next() }).catch(err => { store.dispatch('FedLogOut').then(() => { Message.error('拉取用戶信息失敗,請從新登陸!' + err) next({ path: '/' }) }) }) } } else { if (whiteList.includes(to.path)) { next() } else { next('/login') NProgress.done() } } }) router.afterEach(() => { NProgress.done() // 結束Progress }) 複製代碼
Nginx try_files 以及 404
nginx
配置以下:
location / { root /www/vue-project-demo/; try_files $uri $uri/ /index.html index.htm; } 複製代碼
try_files
: 能夠理解爲nginx 不處理你的這些url地址請求; 那麼服務器若是不處理了,前端要本身作一些404 操做,好比下面這樣:
// router.js import Vue from 'vue' import Router from 'vue-router' import Home from './views/Home.vue' Vue.use(Router) export default new Router({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/404', component: () => import('@/views/404') }, { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "about" */ './views/About.vue') }, { path: '*', redirect: '/404' } ] }) 複製代碼
而後寫一個404 的view 就ok 。
到如今爲止,utils/
目錄下應該有auth.js 、permission.js、request.js
;
那麼對與一些經常使用的方法,你能夠放到utils/common.js
裏,統一install
到vue
實例上,並經過Vue.use()
使用;
對於一些全局的過濾器,你仍能夠放到utils/filters.js
裏,使用Vue.fileter()
註冊到全局;
對於一些全局方法,又不是很長用到的,能夠放到utils/index.js
,哪裏使用哪裏import
直接看代碼吧,要寫奔潰了....
到此時,看我項目裏都用了什麼:
主要就是這些,那麼執行一下打包命令呢?可能這時候你還以爲沒什麼, 單文件最多的還沒超過800kb
呢...
我把項目經過jenkins
部署到服務器上,看一下訪問:
能夠看到,chunk-vendors
加載了將近12秒,這仍是隻有框架沒有內容的前提下,固然你可能說你項目中用不到vuex
、用不到js-cookie
,可是隨着項目的迭代維護,最後確定不比如今小。
那麼,有些文件在生產環境是否是能夠嘗試使用cdn
呢?
爲了方便對比,這裏保持原代碼不動(master
分支),再切出來一個分支改動優化(optimize
分支), 上代碼:
// vue.config.js 修改 const path = require('path') function resolve(dir) { return path.join(__dirname, './', dir) } // cdn預加載使用 const externals = { 'vue': 'Vue', 'vue-router': 'VueRouter', 'vuex': 'Vuex', 'axios': 'axios', 'element-ui': 'ELEMENT', 'js-cookie': 'Cookies', 'nprogress': 'NProgress' } const cdn = { // 開發環境 dev: { css: [ 'https://unpkg.com/element-ui/lib/theme-chalk/index.css', 'https://cdn.bootcss.com/nprogress/0.2.0/nprogress.min.css' ], js: [] }, // 生產環境 build: { css: [ 'https://unpkg.com/element-ui/lib/theme-chalk/index.css', 'https://cdn.bootcss.com/nprogress/0.2.0/nprogress.min.css' ], js: [ 'https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js', 'https://cdn.jsdelivr.net/npm/vue-router@3.0.1/dist/vue-router.min.js', 'https://cdn.jsdelivr.net/npm/vuex@3.0.1/dist/vuex.min.js', 'https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js', 'https://unpkg.com/element-ui/lib/index.js', 'https://cdn.bootcss.com/js-cookie/2.2.0/js.cookie.min.js', 'https://cdn.bootcss.com/nprogress/0.2.0/nprogress.min.js' ] } } module.exports = { chainWebpack: config => { // 這裏是對環境的配置,不一樣環境對應不一樣的BASE_API,以便axios的請求地址不一樣 config.plugin('define').tap(args => { const argv = process.argv const mode = argv[argv.indexOf('--project-mode') + 1] args[0]['process.env'].MODE = `"${mode}"` args[0]['process.env'].BASE_API = '"http://47.94.138.75:8000"' return args }) /** * 添加CDN參數到htmlWebpackPlugin配置中, 詳見public/index.html 修改 */ config.plugin('html').tap(args => { if (process.env.NODE_ENV === 'production') { args[0].cdn = cdn.build } if (process.env.NODE_ENV === 'development') { args[0].cdn = cdn.dev } return args }) // svg loader const svgRule = config.module.rule('svg') // 找到svg-loader svgRule.uses.clear() // 清除已有的loader, 若是不這樣作會添加在此loader以後 svgRule.exclude.add(/node_modules/) // 正則匹配排除node_modules目錄 svgRule // 添加svg新的loader處理 .test(/\.svg$/) .use('svg-sprite-loader') .loader('svg-sprite-loader') .options({ symbolId: 'icon-[name]' }) // 修改images loader 添加svg處理 const imagesRule = config.module.rule('images') imagesRule.exclude.add(resolve('src/icons')) config.module .rule('images') .test(/\.(png|jpe?g|gif|svg)(\?.*)?$/) }, // 修改webpack config, 使其不打包externals下的資源 configureWebpack: config => { const myConfig = {} if (process.env.NODE_ENV === 'production') { // 1. 生產環境npm包轉CDN myConfig.externals = externals } if (process.env.NODE_ENV === 'development') { /** * 關閉host check,方便使用ngrok之類的內網轉發工具 */ myConfig.devServer = { disableHostCheck: true } } // open: true, // hot: true // // https: true, // // proxy: { // // '/proxy': { // // target: 'http://47.94.138.75', // // // changeOrigin: true, // // pathRewrite: { // // '^/proxy': '' // // } // // } // // }, // } return myConfig } } 複製代碼
<!-- public/index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <!-- 使用CDN加速的CSS文件,配置在vue.config.js下 --> <% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.css) { %> <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="preload" as="style"> <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet"> <% } %> <!-- 使用CDN加速的JS文件,配置在vue.config.js下 --> <% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %> <link href="<%= htmlWebpackPlugin.options.cdn.js[i] %>" rel="preload" as="script"> <% } %> <title>vue-project-demo</title> </head> <body> <noscript> <strong>We're sorry but vue-project-demo doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- 使用CDN加速的JS文件,配置在vue.config.js下 --> <% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %> <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script> <% } %> <!-- built files will be auto injected --> </body> </html> 複製代碼
最後去除main.js
中引入的import 'element-ui/lib/theme-chalk/index.css'
OK ,如今執行一下build
:
能夠看到,相對於 793.20KB
,61.94k
小了將近13
倍!!!
把這個分支部署到服務器,話很少說,對比一下就好:
引入 compression-webpack-plugin : npm i -D compression-webpack-plugin
www.webpackjs.com/plugins/com…
修改vue.config.js
,老規矩,上最全的代碼:
const path = require('path') const CompressionWebpackPlugin = require('compression-webpack-plugin') function resolve(dir) { return path.join(__dirname, './', dir) } // cdn預加載使用 const externals = { 'vue': 'Vue', 'vue-router': 'VueRouter', 'vuex': 'Vuex', 'axios': 'axios', 'element-ui': 'ELEMENT', 'js-cookie': 'Cookies', 'nprogress': 'NProgress' } const cdn = { // 開發環境 dev: { css: [ 'https://unpkg.com/element-ui/lib/theme-chalk/index.css', 'https://cdn.bootcss.com/nprogress/0.2.0/nprogress.min.css' ], js: [] }, // 生產環境 build: { css: [ 'https://unpkg.com/element-ui/lib/theme-chalk/index.css', 'https://cdn.bootcss.com/nprogress/0.2.0/nprogress.min.css' ], js: [ 'https://cdn.bootcss.com/vue/2.5.21/vue.min.js', 'https://cdn.bootcss.com/vue-router/3.0.2/vue-router.min.js', 'https://cdn.bootcss.com/vuex/3.0.1/vuex.min.js', 'https://cdn.bootcss.com/axios/0.18.0/axios.min.js', 'https://unpkg.com/element-ui/lib/index.js', 'https://cdn.bootcss.com/js-cookie/2.2.0/js.cookie.min.js', 'https://cdn.bootcss.com/nprogress/0.2.0/nprogress.min.js' ] } } // 是否使用gzip const productionGzip = true // 須要gzip壓縮的文件後綴 const productionGzipExtensions = ['js', 'css'] module.exports = { chainWebpack: config => { // 這裏是對環境的配置,不一樣環境對應不一樣的BASE_API,以便axios的請求地址不一樣 config.plugin('define').tap(args => { const argv = process.argv const mode = argv[argv.indexOf('--project-mode') + 1] args[0]['process.env'].MODE = `"${mode}"` args[0]['process.env'].BASE_API = '"http://47.94.138.75:8000"' return args }) /** * 添加CDN參數到htmlWebpackPlugin配置中, 詳見public/index.html 修改 */ config.plugin('html').tap(args => { if (process.env.NODE_ENV === 'production') { args[0].cdn = cdn.build } if (process.env.NODE_ENV === 'development') { args[0].cdn = cdn.dev } return args }) // svg loader const svgRule = config.module.rule('svg') // 找到svg-loader svgRule.uses.clear() // 清除已有的loader, 若是不這樣作會添加在此loader以後 svgRule.exclude.add(/node_modules/) // 正則匹配排除node_modules目錄 svgRule // 添加svg新的loader處理 .test(/\.svg$/) .use('svg-sprite-loader') .loader('svg-sprite-loader') .options({ symbolId: 'icon-[name]' }) // 修改images loader 添加svg處理 const imagesRule = config.module.rule('images') imagesRule.exclude.add(resolve('src/icons')) config.module .rule('images') .test(/\.(png|jpe?g|gif|svg)(\?.*)?$/) }, // 修改webpack config, 使其不打包externals下的資源 configureWebpack: config => { const myConfig = {} if (process.env.NODE_ENV === 'production') { // 1. 生產環境npm包轉CDN myConfig.externals = externals myConfig.plugins = [] // 2. 構建時開啓gzip,下降服務器壓縮對CPU資源的佔用,服務器也要相應開啓gzip productionGzip && myConfig.plugins.push( new CompressionWebpackPlugin({ test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'), threshold: 8192, minRatio: 0.8 }) ) } if (process.env.NODE_ENV === 'development') { /** * 關閉host check,方便使用ngrok之類的內網轉發工具 */ myConfig.devServer = { disableHostCheck: true } } // open: true, // hot: true // // https: true, // // proxy: { // // '/proxy': { // // target: 'http://47.94.138.75', // // // changeOrigin: true, // // pathRewrite: { // // '^/proxy': '' // // } // // } // // }, // } return myConfig } } 複製代碼
再次運行build
,咱們會發現dist/
下全部的.js
和.css
都會多出一個.js.gz、.css.gz
的文件,這就是咱們須要的壓縮文件,能夠看到最大的只有18.05KB
,想一想是否是比較激動...
固然,這玩意還須要服務端支持,也就是配置nginx
:
gzip on; gzip_static on; gzip_min_length 1024; gzip_buffers 4 16k; gzip_comp_level 2; gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml; gzip_vary off; gzip_disable "MSIE [1-6]\."; 複製代碼
nginx
:
配置成功的話,能夠看到加載的是比較小的Gzip
:
在 response headers
裏會有一個Content-Encoding:gzip
---------------------------- 未完待續 -------------------------------