項目地址 vue-cli3-project 歡迎 starjavascript
相信大部分人都已經知道怎麼建立項目的,能夠跳過這一節,看下一節。html
# 全局安裝 vue-cli腳手架 npm install -g @vue/cli
等待安裝完成後開始下一步前端
vue create vue-cli3-project
能夠選擇默認預設,默認預設包含了babel
,eslint
vue
咱們選擇更多功能 Manually select features
java
回車後來到選擇插件node
這邊選擇了(Babel、Router、Vuex、Css預處理器、Linter / Formatter 格式檢查、Unit測試框架)webpack
是否使用 history
模式的路由 (Yes)ios
這邊選擇 ESLint + Standard config
,我的比較喜歡這個代碼規範
nginx
eslint
校驗選擇(Lint on save)保存是檢查
若是你正在使用的vscode編輯器的話,能夠配置eslint插件進行代碼自動格式化
選是的話,下次建立一個vue項目,能夠直接使用這個預設文件,而無需再進行配置。
等待依賴完成
在components
目錄下建立一個global
目錄,裏面放置一些須要全局註冊的組件。
index.js
做用只要是引入main.vue
,導出組件對象
在components
中建立一個index.js
,用來掃描全局對象並自動註冊。
// components/index.js import Vue from 'vue' // 自動加載 global 目錄下的 .js 結尾的文件 const componentsContext = require.context('./global', true, /\.js$/) componentsContext.keys().forEach(component => { const componentConfig = componentsContext(component) /** * 兼容 import export 和 require module.export 兩種規範 */ const ctrl = componentConfig.default || componentConfig Vue.component(ctrl.name, ctrl) })
最後在入口文件main.js
中導入這個index.js
中就能夠了
在 Vue
項目中使用路由,相信想熟的人已經很熟悉怎麼使用了,要新增一個頁面的話,須要到路由配置中配置該頁面的信息。
若是頁面愈來愈多的話,那麼如何讓咱們的路由更簡潔呢?
根據不一樣的業務模塊進行拆分路由
在每一個子模塊中導出一個路由配置數組
在根 index.js
中導入全部子模塊
當咱們的業務愈來愈龐大,每次新增業務模塊的時候,咱們都要在路由下面新增一個子路由模塊,而後在index.js
中導入。
那麼如何簡化這種操做呢?
經過上面的自動掃描全局組件註冊,咱們也能夠實現自動掃描子模塊路由並導入
做爲前端開發者,放着 node
這麼好用的東西若是不能運用起來,豈不是很浪費?
雖然咱們經過上面已經實現了組件的自動註冊,不過每次新建組件的時候,都要建立一個目錄,而後新增一個.vue
文件,而後寫template
、script
、style
這些東西,而後新建一個index.js
、導出vue組件、雖然有插件能實現自動補全,但仍是很麻煩有木有。
那麼咱們能不能經過node
來幫助咱們幹這些事情呢?只要告訴node
幫我生成的組件名稱就好了。其它的事情讓node
來幹
chalk
,這個插件能讓咱們的控制檯輸出語句有各類顏色區分npm install chalk --save-dev
在根目錄中建立一個 scripts
文件夾,
新增一個generateComponent.js
文件,放置生成組件的代碼、
新增一個template.js
文件,放置組件模板的代碼
// template.js module.exports = { vueTemplate: compoenntName => { return `<template> <div class="${compoenntName}"> ${compoenntName}組件 </div> </template> <script> export default { name: '${compoenntName}' } </script> <style lang="scss" scoped> .${compoenntName} { } </style> ` }, entryTemplate: `import Main from './main.vue' export default Main` }
// generateComponent.js` const chalk = require('chalk') const path = require('path') const fs = require('fs') const resolve = (...file) => path.resolve(__dirname, ...file) const log = message => console.log(chalk.green(`${message}`)) const successLog = message => console.log(chalk.blue(`${message}`)) const errorLog = error => console.log(chalk.red(`${error}`)) const { vueTemplate, entryTemplate } = require('./template') const generateFile = (path, data) => { if (fs.existsSync(path)) { errorLog(`${path}文件已存在`) return } return new Promise((resolve, reject) => { fs.writeFile(path, data, 'utf8', err => { if (err) { errorLog(err.message) reject(err) } else { resolve(true) } }) }) } log('請輸入要生成的組件名稱、如需生成全局組件,請加 global/ 前綴') let componentName = '' process.stdin.on('data', async chunk => { const inputName = String(chunk).trim().toString() /** * 組件目錄路徑 */ const componentDirectory = resolve('../src/components', inputName) /** * vue組件路徑 */ const componentVueName = resolve(componentDirectory, 'main.vue') /** * 入口文件路徑 */ const entryComponentName = resolve(componentDirectory, 'index.js') const hasComponentDirectory = fs.existsSync(componentDirectory) if (hasComponentDirectory) { errorLog(`${inputName}組件目錄已存在,請從新輸入`) return } else { log(`正在生成 component 目錄 ${componentDirectory}`) await dotExistDirectoryCreate(componentDirectory) // fs.mkdirSync(componentDirectory); } try { if (inputName.includes('/')) { const inputArr = inputName.split('/') componentName = inputArr[inputArr.length - 1] } else { componentName = inputName } log(`正在生成 vue 文件 ${componentVueName}`) await generateFile(componentVueName, vueTemplate(componentName)) log(`正在生成 entry 文件 ${entryComponentName}`) await generateFile(entryComponentName, entryTemplate) successLog('生成成功') } catch (e) { errorLog(e.message) } process.stdin.emit('end') }) process.stdin.on('end', () => { log('exit') process.exit() }) function dotExistDirectoryCreate (directory) { return new Promise((resolve) => { mkdirs(directory, function () { resolve(true) }) }) } // 遞歸建立目錄 function mkdirs (directory, callback) { var exists = fs.existsSync(directory) if (exists) { callback() } else { mkdirs(path.dirname(directory), function () { fs.mkdirSync(directory) callback() }) } }
"new:comp": "node ./scripts/generateComponent"
若是使用 npm
的話 就是 npm run new:comp
若是使用 yarn
的話 就是 yarn new:comp
經過上面的邏輯代碼咱們能夠經過node
來生成組件了,那麼也能夠觸類旁通來生成頁面組件。只需稍微修改一下生成組件代碼的邏輯。
在scripts
目錄下新建一個generateView.js
文件
// generateView.js const chalk = require('chalk') const path = require('path') const fs = require('fs') const resolve = (...file) => path.resolve(__dirname, ...file) const log = message => console.log(chalk.green(`${message}`)) const successLog = message => console.log(chalk.blue(`${message}`)) const errorLog = error => console.log(chalk.red(`${error}`)) const { vueTemplate } = require('./template') const generateFile = (path, data) => { if (fs.existsSync(path)) { errorLog(`${path}文件已存在`) return } return new Promise((resolve, reject) => { fs.writeFile(path, data, 'utf8', err => { if (err) { errorLog(err.message) reject(err) } else { resolve(true) } }) }) } log('請輸入要生成的頁面組件名稱、會生成在 views/目錄下') let componentName = '' process.stdin.on('data', async chunk => { const inputName = String(chunk).trim().toString() /** * Vue頁面組件路徑 */ let componentVueName = resolve('../src/views', inputName) // 若是不是以 .vue 結尾的話,自動加上 if (!componentVueName.endsWith('.vue')) { componentVueName += '.vue' } /** * vue組件目錄路徑 */ const componentDirectory = path.dirname(componentVueName) const hasComponentExists = fs.existsSync(componentVueName) if (hasComponentExists) { errorLog(`${inputName}頁面組件已存在,請從新輸入`) return } else { log(`正在生成 component 目錄 ${componentDirectory}`) await dotExistDirectoryCreate(componentDirectory) } try { if (inputName.includes('/')) { const inputArr = inputName.split('/') componentName = inputArr[inputArr.length - 1] } else { componentName = inputName } log(`正在生成 vue 文件 ${componentVueName}`) await generateFile(componentVueName, vueTemplate(componentName)) successLog('生成成功') } catch (e) { errorLog(e.message) } process.stdin.emit('end') }) process.stdin.on('end', () => { log('exit') process.exit() }) function dotExistDirectoryCreate (directory) { return new Promise((resolve) => { mkdirs(directory, function () { resolve(true) }) }) } // 遞歸建立目錄 function mkdirs (directory, callback) { var exists = fs.existsSync(directory) if (exists) { callback() } else { mkdirs(path.dirname(directory), function () { fs.mkdirSync(directory) callback() }) } }
scripts
腳本"new:view": "node ./scripts/generateView"
若是使用 npm
的話 就是 npm run new:view
若是使用 yarn
的話 就是 yarn new:view
npm install axios --save // or yarn add axios
在根目錄新建三個環境變量文件
分別輸入不一樣的地址,
好比dev
就寫 dev
的api地址、test
就寫test
的api地址
# // .env NODE_ENV = "development" BASE_URL = "https://easy-mock.com/mock/5c4c50b9888ef15de01bec2c/api"
接着在根目錄中新建一個 vue.config.js
// vue.config.js module.exports = { chainWebpack: config => { // 這裏是對環境的配置,不一樣環境對應不一樣的BASE_URL,以便axios的請求地址不一樣 config.plugin('define').tap(args => { args[0]['process.env'].BASE_URL = JSON.stringify(process.env.BASE_URL) return args }) } }
而後在src
目錄下新建一個 api
文件夾,建立一個 index.js
用來配置 axios
的配置信息
// src/api/index.js import axios from 'axios' import router from '../router' import { Message } from 'element-ui' const service = axios.create({ // 設置超時時間 timeout: 60000, baseURL: process.env.BASE_URL }) // post請求的時候,咱們須要加上一個請求頭,因此能夠在這裏進行一個默認的設置 // 即設置post的請求頭爲application/x-www-form-urlencoded;charset=UTF-8 service.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'' export default service
import axios from 'axios' import router from '../router' import { Message } from 'element-ui' const service = axios.create({ // 設置超時時間 timeout: 60000, baseURL: process.env.BASE_URL }) /** * 請求前攔截 * 用於處理須要在請求前的操做 */ service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }, (error) => { return Promise.reject(error) }) /** * 請求響應攔截 * 用於處理須要在請求返回後的操做 */ service.interceptors.response.use(response => { const responseCode = response.status // 若是返回的狀態碼爲200,說明接口請求成功,能夠正常拿到數據 // 不然的話拋出錯誤 if (responseCode === 200) { return Promise.resolve(response) } else { return Promise.reject(response) } }, error => { // 服務器返回不是 2 開頭的狀況,會進入這個回調 // 能夠根據後端返回的狀態碼進行不一樣的操做 const responseCode = error.response.status switch (responseCode) { // 401:未登陸 case 401: // 跳轉登陸頁 router.replace({ path: '/login', query: { redirect: router.currentRoute.fullPath } }) break // 403: token過時 case 403: // 彈出錯誤信息 Message({ type: 'error', message: '登陸信息過時,請從新登陸' }) // 清除token localStorage.removeItem('token') // 跳轉登陸頁面,並將要瀏覽的頁面fullPath傳過去,登陸成功後跳轉須要訪問的頁面 setTimeout(() => { router.replace({ path: '/login', query: { redirect: router.currentRoute.fullPath } }) }, 1000) break // 404請求不存在 case 404: Message({ message: '網絡請求不存在', type: 'error' }) break // 其餘錯誤,直接拋出錯誤提示 default: Message({ message: error.response.data.message, type: 'error' }) } return Promise.reject(error) }) export default service
Message
方法是 element-ui
提供的一個消息提示組件、你們能夠根據本身的消息提示組件進行替換
在響應攔截中添加處理邏輯
service.interceptors.response.use(response => { const responseCode = response.status // 若是返回的狀態碼爲200,說明接口請求成功,能夠正常拿到數據 // 不然的話拋出錯誤 if (responseCode === 200) { return Promise.resolve(response.data) } else { return Promise.reject(response) } }, error => { // 斷網 或者 請求超時 狀態 if (!error.response) { // 請求超時狀態 if (error.message.includes('timeout')) { console.log('超時了') Message.error('請求超時,請檢查網絡是否鏈接正常') } else { // 能夠展現斷網組件 console.log('斷網了') Message.error('請求失敗,請檢查網絡是否已鏈接') } return } // 省略其它代碼 ······ return Promise.reject(error) })
// src/api/index.js export const uploadFile = formData => { const res = service.request({ method: 'post', url: '/upload', data: formData, headers: { 'Content-Type': 'multipart/form-data' } }) return res }
調用
async uploadFile (e) { const file = document.getElementById('file').files[0] const formdata = new FormData() formdata.append('file', file) await uploadFile(formdata) }
let loading = null service.interceptors.request.use(config => { // 在請求先展現加載框 loading = Loading.service({ text: '正在加載中......' }) // 省略其它代碼 ······ return config }, (error) => { return Promise.reject(error) }) service.interceptors.response.use(response => { // 請求響應後關閉加載框 if (loading) { loading.close() } // 省略其它代碼 ······ }, error => { // 請求響應後關閉加載框 if (loading) { loading.close() } // 省略其它代碼 ······ return Promise.reject(error) })
假設有這樣一個場景,咱們經過 vuex
封裝了獲取新聞列表的 function
import Vue from 'vue' import Vuex from 'vuex' import { getNewsList } from '../api/news' Vue.use(Vuex) const types = { NEWS_LIST: 'NEWS_LIST' } export default new Vuex.Store({ state: { [types.NEWS_LIST]: [] }, mutations: { [types.NEWS_LIST]: (state, res) => { state[types.NEWS_LIST] = res } }, actions: { [types.NEWS_LIST]: async ({ commit }, params) => { const res = await getNewsList(params) return commit(types.NEWS_LIST, res) } }, getters: { getNewsResponse (state) { return state[types.NEWS_LIST] } } })
而後在新聞列表頁,咱們經過 mapAction
、mapGetters
來調用Action
和getters
咱們須要寫上這些代碼
import { mapActions, mapGetters } from 'vuex' computed: { ...mapGetters(['getNewsResponse']) }, methods: { ...mapActions(['NEWS_LIST']) }
在假設,在另外一個頁面又須要從新調用獲取新聞列表的接口,咱們又要在寫一遍上面的代碼對吧?
複製粘貼就是幹有木有?
若是接口忽然加了一個參數,那豈不是每一個要用到這個接口的代碼都得加這個參數。
複製粘貼一時爽,需求一改你就爽
既然是重複的代碼,咱們確定要複用,這時候Vue
提供的Mixin
就起了大做用了
src
下建立一個mixins
目錄,用來管理全部的mixinsnews-mixin.js
import { mapActions, mapGetters } from 'vuex' export default { computed: { ...mapGetters(['getNewsResponse']) }, methods: { ...mapActions(['NEWS_LIST']) } }
而後在須要用到的組件中引入這個mixin
,就能直接調用這個方法了。無論多少個頁面,只要引入這個mixin
,直接就能使用。
需求一改的話,也只須要修改這個mixin
文件
// news/index.vue import Vue from 'vue' import newsMixin from '@/mixins/news-mixin' export default { name: 'news', mixins: [newsMixin], data () { return {} }, async created () { await this.NEWS_LIST() console.log(this.getNewsResponse) } }
除了封裝 vuex
的公用方法,其實還有不少的東西也能作封裝。例如:分頁對象
,表格數據
,公用方法
、等等就不一一舉例了。能夠看github
在多個地方常用,就能夠考慮封裝成mixin
,不過請寫好註釋哦。否則就會有人在背後罵你了!!你懂的~~
compression-webpack-plugin
插件npm install compression-webpack-plugin --save-dev // or yarn add compression-webpack-plugin --dev
// vue.config.js const CompressionPlugin = require('compression-webpack-plugin') module.exports = { chainWebpack: config => { // 這裏是對環境的配置,不一樣環境對應不一樣的BASE_URL,以便axios的請求地址不一樣 config.plugin('define').tap(args => { args[0]['process.env'].BASE_URL = JSON.stringify(process.env.BASE_URL) return args }) if (process.env.NODE_ENV === 'production') { // #region 啓用GZip壓縮 config .plugin('compression') .use(CompressionPlugin, { asset: '[path].gz[query]', algorithm: 'gzip', test: new RegExp('\\.(' + ['js', 'css'].join('|') + ')$'), threshold: 10240, minRatio: 0.8, cache: true }) .tap(args => { }) // #endregion } } }
npm run build
後能看到生成 .gz
文件就OK了。若是你的服務器使用nginx的話,nginx也須要配置開啓GZIP
、下面會講到如何在nginx
中開啓GZIP
對於 vue
、vue-router
、vuex
、axios
和element-ui
等等這些不常常改動的庫、咱們讓webpack
不對他們進行打包,經過cdn
引入,能夠減小代碼的大小、也能夠減小服務器的帶寬,更能把這些文件緩存到客戶端,客戶端加載的會更快。
vue.config.js
const CompressionPlugin = require('compression-webpack-plugin') module.exports = { chainWebpack: config => { // 省略其它代碼 ······ // #region 忽略生成環境打包的文件 var externals = { vue: 'Vue', axios: 'axios', 'element-ui': 'ELEMENT', 'vue-router': 'VueRouter', vuex: 'Vuex' } config.externals(externals) const cdn = { css: [ // element-ui css '//unpkg.com/element-ui/lib/theme-chalk/index.css' ], js: [ // vue '//cdn.staticfile.org/vue/2.5.22/vue.min.js', // vue-router '//cdn.staticfile.org/vue-router/3.0.2/vue-router.min.js', // vuex '//cdn.staticfile.org/vuex/3.1.0/vuex.min.js', // axios '//cdn.staticfile.org/axios/0.19.0-beta.1/axios.min.js', // element-ui js '//unpkg.com/element-ui/lib/index.js' ] } config.plugin('html') .tap(args => { args[0].cdn = cdn return args }) // #endregion } } }
index.html
<!--public/index.html--> <!DOCTYPE html> <html lang="en"> <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"> <% if (process.env.NODE_ENV === 'production') { %> <% for(var css of htmlWebpackPlugin.options.cdn.css) { %> <link href="<%=css%>" rel="preload" as="style"> <link rel="stylesheet" href="<%=css%>" as="style"> <% } %> <% for(var js of htmlWebpackPlugin.options.cdn.js) { %> <link href="<%=js%>" rel="preload" as="script"> <script src="<%=js%>"></script> <% } %> <% } %> <title>vue-cli3-project</title> </head> <body> <noscript> <strong>We're sorry but vue-cli3-project doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body> </html>
咱們已經把第三方庫使用cdn
替代了,那麼咱們build
後生成的 js
,css
之類的文件可否也用cdn
呢?
要想把本身的資源上傳到cdn
上,前提是得有本身的cdn
域名,若是沒有的話,能夠到七牛雲官網上註冊申請一個
js
、css
之類的文件、不過咱們的文件那麼多,一個一個上傳明顯不合理。要你你也不幹。這時候,這些批量又重複的操做應該由咱們的node
出馬,讓咱們來經過 node
來批量上傳咱們的資源文件
在七牛雲官網的文檔中心有介紹如何經過node
上傳文件、感興趣的人能夠本身去研究一下。
AccessKey
和SecretKey
,在你的我的面板 -> 祕鑰管理 ,這兩個祕鑰待會會用到npm install qiniu glob mime --save-dev
scripts
目錄下建立一個 upcdn.js
文件// /scripts/upcdn.js const qiniu = require('qiniu') const glob = require('glob') const mime = require('mime') const path = require('path') const isWindow = /^win/.test(process.platform) let pre = path.resolve(__dirname, '../dist/') + (isWindow ? '\\' : '') const files = glob.sync( `${path.join( __dirname, '../dist/**/*.?(js|css|map|png|jpg|svg|woff|woff2|ttf|eot)' )}` ) pre = pre.replace(/\\/g, '/') const options = { scope: 'source' // 空間對象名稱 } var config = { qiniu: { accessKey: '', // 我的中心 祕鑰管理裏的 AccessKey secretKey: '', // 我的中心 祕鑰管理裏的 SecretKey bucket: options.scope, domain: 'http://ply4cszel.bkt.clouddn.com' } } var accessKey = config.qiniu.accessKey var secretKey = config.qiniu.secretKey var mac = new qiniu.auth.digest.Mac(accessKey, secretKey) var putPolicy = new qiniu.rs.PutPolicy(options) var uploadToken = putPolicy.uploadToken(mac) var cf = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z2 }) var formUploader = new qiniu.form_up.FormUploader(cf) async function uploadFileCDN (files) { files.map(async file => { const key = getFileKey(pre, file) try { await uploadFIle(key, file) console.log(`上傳成功 key: ${key}`) } catch (err) { console.log('error', err) } }) } async function uploadFIle (key, localFile) { const extname = path.extname(localFile) const mimeName = mime.getType(extname) const putExtra = new qiniu.form_up.PutExtra({ mimeType: mimeName }) return new Promise((resolve, reject) => { formUploader.putFile(uploadToken, key, localFile, putExtra, function ( respErr, respBody, respInfo ) { if (respErr) { reject(respErr) } resolve({ respBody, respInfo }) }) }) } function getFileKey (pre, file) { if (file.indexOf(pre) > -1) { const key = file.split(pre)[1] return key.startsWith('/') ? key.substring(1) : key } return file } (async () => { console.time('上傳文件到cdn') await uploadFileCDN(files) console.timeEnd('上傳文件到cdn') })()
修改vue.config.js
的配置信息,讓其publicPath
指向咱們cdn
的域名
const IS_PROD = process.env.NODE_ENV === 'production' const cdnDomian = 'http://ply4cszel.bkt.clouddn.com' module.exports = { publicPath: IS_PROD ? cdnDomian : '/', // 省略其它代碼 ······· }
修改package.json配置,使咱們build
完成後自動上傳資源文件到cdn服務器
"build": "vue-cli-service build --mode prod && node ./scripts/upcdn.js",
npm run build
而後到你的cdn
控制檯的內容管理看看文件是否已經上傳成功
這邊使用的是 centOS7
環境,不過使用的是不一樣的系統,能夠參考一下其它系統的安裝方法
yum update -y
yum install docker
service docker start
// 安裝epel源 yum install -y epel-release // 安裝docker-compose yum install docker-compose
version: '2.1' services: nginx: restart: always image: nginx volumes: #~ /var/local/nginx/nginx.conf爲本機目錄, /etc/nginx爲容器目錄 - /var/local/nginx/nginx.conf:/etc/nginx/nginx.conf #~ /var/local/app/dist 爲本機 build 後的dist目錄, /usr/src/app爲容器目錄, - /var/local/app/dist:/usr/src/app ports: - 80:80 privileged: true
#user nobody; worker_processes 2; #工做模式及鏈接數上線 events { worker_connections 1024; #單個工做進程 處理進程的最大併發數 } http { include mime.types; default_type application/octet-stream; #sendfile 指令指定 nginx 是否調用 sendfile 函數(zero copy 方式)來輸出文件,對於普通應用, sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; # 開啓GZIP gzip on; # # 監聽 80 端口,轉發請求到 3000 端口 server { #監聽端口 listen 80; #編碼格式 charset utf-8; # 前端靜態文件資源 location / { root /usr/src/app; index index.html index.htm; try_files $uri $uri/ @rewrites; } # 配置若是匹配不到資源,將url指向 index.html, 在 vue-router 的 history 模式下使用,就不會顯示404 location @rewrites { rewrite ^(.*)$ /index.html last; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } }
docker-compose -d up
使用docker
+ jenkins
能實現代碼提交到github後自動部署環境、這個要講起來內容太多,有興趣的能夠看我這一篇文章
從零搭建docker+jenkins+node.js自動化部署環境
若是你們還有什麼更好的實踐方式,歡迎評論區指教!!
項目地址 vue-cli3-project 歡迎 star
歡迎關注公衆號「碼上開發」,天天分享最新技術資訊