vue-cli3 从搭建到优化的详细步骤
前言
github地址: https://github.com/leestaysmall/vue-project-demo (完整分支:optimize分支)
demo地址:
安装与初始化架构
安装
node >= 8.9 推荐:8.11.0 +
安装: npm install -g @vue/cli
检查: vue --version
如果已安装旧版本,需要先 npm uninstall vue-cli -g
卸载掉旧版本。
初始化架构
创建: vue create project-name
注:项目名称不能驼峰命名。
选择一个预设(这里我选择更多功能):
选择需要安装的(babel、router、vuex、pre-processors、linter / formatter):
是否使用history路由模式(yes):
选择css 预处理器(sass/scss):
选择eslint 配置(eslint + standard config):
选择什么时候执行eslint校验(lint on save):
选择以什么样的形式配置以上所选的功能(in dedicated config files):
是否将之前的设置保存为一个预设模板(y):
如果选择 y 会让输入名称,以便下次直接使用,否则直接开始初始化项目。
最后,看一下生成的基本架构目录:
在项目中优雅的使用svg 首先在 /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封装api、模块化vuex
axios篇
项目中安装 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、合理利用vue router守卫
全局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
到现在为止, utils/
目录下应该有 auth.js 、permission.js、request.js
;
- 那么对与一些常用的方法,你可以放到
utils/common.js
里,统一install
到vue
实例上,并通过vue.use()
使用; - 对于一些全局的过滤器,你仍可以放到
utils/filters.js
里,使用vue.fileter()
注册到全局; - 对于一些全局方法,又不是很长用到的,可以放到
utils/index.js
,哪里使用哪里import
mixin减少项目冗余代码
直接看代码吧,要写奔溃了....
使用cdn减少文件打包的体积
到此时,看我项目里都用了什么:
主要就是这些,那么执行一下打包命令呢?
可能这时候你还觉得没什么, 单文件最多的还没超过 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 } }
最后去除 main.js
中引入的 import 'element-ui/lib/theme-chalk/index.css'
ok ,现在执行一下 build
:
可以看到,相对于 793.20kb
, 61.94k
小了将近 13
倍!!!
把这个分支部署到服务器,话不多说,对比一下就好:
使用gzip 加速
引入 compression-webpack-plugin : npm i -d compression-webpack-plugin
修改 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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。