vue 快速入門 系列 —— 使用 vue-cli 3 搭建一個專案(上)

彭加李發表於2021-11-12

其他章節請看:

vue 快速入門 系列

使用 vue-cli 3 搭建一個專案(上)

前面我們已經學習了一個成熟的腳手架(vue-cli),筆者希望通過這個腳手架快速搭建系統(或專案)。而展開搭建最好的方法是向優秀的專案學習,依葫蘆畫瓢。

這裡通過研究 vue-admin-template 專案,逐一引入 element-uiaxiosmockiconfontnprogress許可權控制佈局多環境(.env)跨域vue.config.js,一步一步打造我們自己的架構。

Tip: vue-element-admin 是一個優秀的後臺前端解決方案,把平時用到的一些元件或者經驗分享給大家。而 vue-admin-template 就是它的一個簡易版本。

:由於篇幅過長,決定將文字拆分為上下兩篇

模板專案 - vue-admin-template

vue-admin-template 以 vue-cli webpack 模板為基礎開發,並引入如下依賴:

  • element-ui 餓了麼出品的 vue pc UI框架
  • axios 一個現在主流並且很好用的請求庫 支援Promise
  • js-cookie 一個輕量的JavaScript庫來處理cookie
  • normalize.css 格式化css
  • nprogress 輕量的全域性進度條控制
  • vuex 官方狀態管理
  • vue-router 官方路由
  • iconfont 圖示字型
  • 許可權控制
  • lint

Tip:vue-cli webpack模板:

  • 這個模板是 vue-cli verison 2.* 的主要模板
  • Vue-cli 3 包含此模板提供的所有功能(以及更多功能)
  • Vue-cli 3 來了,此模板現在被視為已棄用

下載專案並啟動:

> git clone https://github.com/PanJiaChen/vue-admin-template.git vue-admin-template
> cd vue-admin-template
vue-admin-template> npm i
vue-admin-template> npm run dev

> vue-admin-template@4.4.0 dev
> vue-cli-service serve
...

建立專案

我們的專案 - myself-vue-admin-template

通過 vue-cli 建立專案

// 專案預設 `[Vue 2] less`, `babel`, `router`, `vuex`, `eslint`
$ vue create  myself-vue-admin-template

目錄結構如下:

 myself-vue-admin-template
- mode_modules
- public
    - favicon.ico
    - index.html
- src
    - assets
        - logo.png
    - components
        - HelloWorld.vue
    - router
        - index.js
    - store
        - index.js
    - views
        - Aobut.vue
        - Home.vue
    - App.vue
    - mains.js
- .browerslistrc
- .editorconfig
- .eslintrc.js
- .gitignore
- babel.config.js
- package-lock.json
- package.json
- README.md

我們的專案 Vs 模板專案

專案 vue-admin-templatemyself-vue-admin-template 多瞭如下目錄和檔案,其他都相同:

vue-admin-template
+ build
+ mock
+ src/api
+ src/icons
+ src/layout
+ src/styles
+ src/utils
+ src/permission.js
+ src/settings.js
+ .env.development
+ .env.production
+ .env.staging
+ .travis.yml 
+ jest.config.js 
+ jsconfig.json
+ postcss.config.js
+ README-zh.md
+ vue.config.js

使用的 @vue/cli 都是 4.x :

//  myself-vue-admin-template
"devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-vuex": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
// vue-admin-template
"devDependencies": {
    "@vue/cli-plugin-babel": "4.4.4",
    "@vue/cli-plugin-eslint": "4.4.4",
    "@vue/cli-plugin-unit-jest": "4.4.4",
    "@vue/cli-service": "4.4.4",
    "@vue/test-utils": "1.0.0-beta.29",

element-ui

模板專案如何使用 element-ui

// package.json
"dependencies": {
    "element-ui": "2.13.2",
}
// main.js
// ps: 無關程式碼未展示
import Vue from 'vue'

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
// 國際化-英文
import locale from 'element-ui/lib/locale/lang/en' // lang i18n

import App from './App'

// set ElementUI lang to EN
Vue.use(ElementUI, { locale })
// 如果想要中文版 element-ui,按如下方式宣告
// Vue.use(ElementUI)

new Vue({
  el: '#app',
  render: h => h(App)
})
  • 這裡引入 Element 是完整引入,另一種是按需引入
  • Element 元件內部預設使用中文,這裡使用了英文
    • element 的國際化其實就是對 element 中元件的國際化(檢視檔案 node_modules/element-ui/lib/locale/lang/en 就清楚了)

新增 element-ui

思路如下:

  • 完整引入 element
  • 無需提供翻譯,預設使用中文
  • 利用 vue-cli 提供的外掛安裝 element-ui

通過 vue-cli 直接安裝

myself-vue-admin-template> vue add vue-cli-plugin-element

?  Installing vue-cli-plugin-element...

✔  Successfully installed plugin: vue-cli-plugin-element
// 配置
? How do you want to import Element? Fully import
? Do you wish to overwrite Element's SCSS variables? No
? Choose the locale you want to load zh-CN

✔  Successfully invoked generator for plugin: vue-cli-plugin-element

:也可以使用 vue-cli GUI 的方式安裝外掛 vue-cli-plugin-element

接著通過 git status 就能檢視 vue-cli 替我們修改的程式碼:

myself-vue-admin-template> git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   package-lock.json
        modified:   package.json
        modified:   src/App.vue
        modified:   src/main.js

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        src/plugins/

no changes added to commit (use "git add" and/or "git commit -a")

核心程式碼與模板專案中的相同,只是將 element 的引入封裝到了 plugins/element.js 檔案中。

啟動服務,頁面顯示:

...
if Element is successfully added to this project, you'll see an <el-button> below // {1}

...

我們會在行({1})下一行看見一個 element 的按鈕,說明 element-ui 引入成功。

axios

模板專案如何使用 axios

// package.json
"dependencies": {
    "axios": "0.18.1",
}

對 axios 進行封裝:

// src/utils/request.js
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'

// create an axios instance
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  // withCredentials: true, // send cookies when cross-domain requests
  timeout: 5000 // request timeout
})

// request interceptor
service.interceptors.request.use(
  config => {
    ...
  },
  error => {
    ...
  }
)

// response interceptor
service.interceptors.response.use(
  response => {
    const res = response.data

    // if the custom code is not 20000, it is judged as an error.
    if (res.code !== 20000) {
      Message({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      })

      // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
        // to re-login
        MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
          confirmButtonText: 'Re-Login',
          cancelButtonText: 'Cancel',
          type: 'warning'
        }).then(() => {
          ...
        })
      }
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res
    }
  },
  error => {
    console.log('err' + error) // for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

export default service
// api/table.js
import request from '@/utils/request'

export function getList(params) {
  return request({
    url: '/vue-admin-template/table/list',
    method: 'get',
    params
  })
}
// views/table/index.vue
<script>
import { getList } from '@/api/table'
...
</script>

新增 axios

vue-cli 安裝外掛
myself-vue-admin-template> vue add vue-cli-plugin-axios

?  Installing vue-cli-plugin-axios...
...
✔  Successfully installed plugin: vue-cli-plugin-axios
\myself-vue-admin-template> git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   package-lock.json
        modified:   package.json
        modified:   src/main.js

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        src/plugins/axios.js

其中 axios.js 中的 Plugin 在 vscode 中提示已棄用,所以乾脆把模板專案中有關 axios 的搬過來

照搬模板專案中的 axios

Tip: 先將 vue-cli 安裝 axios 的程式碼還原

myself-vue-admin-template> npm i -D axios@0.18.1

新建 request.js(來自模板專案 utils/request,註釋掉和許可權相關的程式碼):

// utils/request.js
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
// import store from '@/store'
// import { getToken } from '@/utils/auth'

// create an axios instance
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  // withCredentials: true, // send cookies when cross-domain requests
  timeout: 5000 // request timeout
})

// request interceptor
service.interceptors.request.use(
  config => {
    // do something before request is sent

    // if (store.getters.token) {
    //   // let each request carry token
    //   // ['X-Token'] is a custom headers key
    //   // please modify it according to the actual situation
    //   config.headers['X-Token'] = getToken()
    // }
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

// response interceptor
service.interceptors.response.use(
  /**
   * If you want to get http information such as headers or status
   * Please return  response => response
  */

  /**
   * Determine the request status by custom code
   * Here is just an example
   * You can also judge the status by HTTP Status Code
   */
  response => {
    const res = response.data

    // if the custom code is not 20000, it is judged as an error.
    if (res.code !== 20000) {
      Message({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      })

      // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
        // to re-login
        MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
          confirmButtonText: 'Re-Login',
          cancelButtonText: 'Cancel',
          type: 'warning'
        }).then(() => {
          // store.dispatch('user/resetToken').then(() => {
          //   location.reload()
          // })
        })
      }
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res
    }
  },
  error => {
    console.log('err' + error) // for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

export default service

Tip: VUE_APP_BASE_API 請查閱本篇的 多環境->base_url

新建 table.js,定義一個請求:

// api/table.js
import request from '@/utils/request'

export function getList(params) {
  return request({
    url: '/vue-admin-template/table/list',
    method: 'get',
    params
  })
}

About.vue 中引用 api/table.js

// views/About.vue
...
<script>
import { getList } from '@/api/table'
export default {
  created() {
    this.fetchData()
  },
  methods: {
    fetchData() {
      getList().then(response => {
        console.log('載入資料', response);

      })
    }
  }
}
</script>

Tip:儲存程式碼可能會遇到如下資訊,可以通過配置 lintOnSave 生產構建時禁用 eslint-loader

myself-vue-admin-template\src\views\About.vue
   7:1   error  More than 1 blank line not allowed         no-multiple-empty-lines
  12:10  error  Missing space before function parentheses  space-before-function-paren
  16:14  error  Missing space before function parentheses  space-before-function-paren
  18:38  error  Extra semicolon                            semi
  20:7   error  Block must not be padded by blank lines    padded-blocks

✖ 5 problems (5 errors, 0 warnings)
  5 errors and 0 warnings potentially fixable with the `--fix` option.
// vue.config.js
module.exports = {
  lintOnSave: process.env.NODE_ENV !== 'production'
}

在 App.vue 中引入 About.vue,重啟伺服器,發現頁面(About.vue)會報 404 的錯誤,所以接下來我們得引入 mock。

Tip: 請查閱本篇的 新增 mock 小節。

新增完 mock,接著啟動服務,頁面不會再輸出 404 之類的提示,控制檯會輸出 mock 中模擬的資料,至此,表明 axiosmock 都已生效。

mock

模板專案如何使用 mock

這裡使用的 npm 包是 mockjs,將需要攔截的請求統一放在 mock 目錄中,最後在 main.js 中引入 mock。

裡面有關於 mock 缺陷的修復,還有 mock-serve.js,有點複雜。

以下是一些核心程式碼:

// main.js
/**
 * If you don't want to use mock-server
 * you want to use MockJs for mock api
 * you can execute: mockXHR()
 *
 * Currently MockJs will be used in the production environment,
 * please remove it before going online ! ! !
 */
if (process.env.NODE_ENV === 'production') {
  const { mockXHR } = require('../mock')
  mockXHR()
}
// mock/index.js
const Mock = require('mockjs')
const { param2Obj } = require('./utils')

const user = require('./user')
const table = require('./table')

const mocks = [
  ...user,
  ...table
]

// for front mock
// please use it cautiously, it will redefine XMLHttpRequest,
// which will cause many of your third-party libraries to be invalidated(like progress event).
function mockXHR() {
  // mock patch
  // https://github.com/nuysoft/Mock/issues/300
  Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
  ...
}

module.exports = {
  mocks,
  mockXHR
}
// vue.config.js
devServer: {
    before: require('./mock/mock-server.js')
  },
// api/table.js
import request from '@/utils/request'

export function getList(params) {
  return request({
    url: '/vue-admin-template/table/list',
    method: 'get',
    params
  })
}
// views/table/index.vue
import { getList } from '@/api/table'

新增 mock

筆者換一種方式,直接通過 vue-cli 外掛安裝:

myself-vue-admin-template> vue add vue-cli-plugin-mock

?  Installing vue-cli-plugin-mock...

✔  Successfully installed plugin: vue-cli-plugin-mock

修改的檔案有:

myself-vue-admin-template> git status
        modified:   package-lock.json
        modified:   package.json

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        mock/

根據 vue-cli-plugin-mock 在 npm 中介紹,這裡 mock 有兩種寫法,自動生成的程式碼使用寫法一,筆者為了和模板專案中的相同,將 mock/index.js 改為寫法二。

// 寫法一
module.exports = {
  'GET /api/user': {
    // obj
    id: 1,
    username: 'kenny',
    sex: 6,
  },
  ...
};

// 寫法二
module.exports = [
  {
    path: '/api/user',
    handler: (req, res) => {
      return res.json({ username: 'admin', sex: 5 });
    },
  },
  ...
];

mock已經安裝完畢,可以直接使用,具體用法請看上面的 新增 axios 小節。

Tip: 可以將 mock 進一步優化,就像模板專案一樣:

  • 將 mock 檔案分模組,統一通過 mock/index.js 整合
  • mock 只在開發環境下生效
// vue-admin-template/src/main.js
if (process.env.NODE_ENV === 'production') {
  const { mockXHR } = require('../mock')
  mockXHR()
}
// vue-admin-template/mock/index.js
const Mock = require('mockjs')

const user = require('./user')
const table = require('./table')

const mocks = [
  ...user,
  ...table
]

module.exports = {
  mocks,
  mockXHR
}

iconfont

以前圖示是用圖片,後來出現了雪碧圖,比如將很多小圖示放在一張圖片中,減少請求。在後來專案中甚至不使用本地圖片,而使用font庫,比如 font awesome、iconfont。

模板專案中的登入頁,使用了4個圖示

// login/index.vue
<svg-icon icon-class="user"/>

<svg-icon icon-class="password" />

<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />

新增 iconfont

官網下載並初步體驗

iconfont 官網選中2個圖示放到購物車中,然後點選下載程式碼,解壓後,雙擊開啟 index.html 則會告訴我們如何使用,我們選擇第三種方式(Symbol)。

將 iconfont 加入專案

新建 SvgIcon.vue:

// src/components/SvgIcon.vue
<template>
  <svg class="svg-icon" aria-hidden="true">
    <use :xlink:href="iconName"></use>
  </svg>
</template>

<script>
export default {
  name: 'icon-svg',
  props: {
    iconClass: {
      type: String,
      required: true
    }
  },
  computed: {
    iconName() {
      return `#icon-${this.iconClass}`
    }
  }
}
</script>

<style>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}
</style>

iconfont.js 放入 src/utils 資料夾中,並修改 main.js

...
import './utils/iconfont.js'

//引入svg元件
import IconSvg from '@/components/SvgIcon'

//全域性註冊icon-svg
Vue.component('icon-svg', IconSvg)

使用 icon,例如在 About.vue 中:

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <icon-svg icon-class="password" />
    <icon-svg icon-class="user" />
  </div>
</template>
改造

iconfont.js 內容如下:

!function(e){var t,n,o,c,i,d='<svg><symbol id="icon-password" viewBox="0 0 1024 1024"><path d="M780.8 354.58H66..."  ></path></symbol><symbol id="icon-user" viewBox="0 0 1032 1024"><path d="M494.8704..."  ></path></symbol></svg>',...

如果還需要新增第三個圖片,就得修改 iconfont.js 檔案,而且需要使用哪個 svg 也不直觀,得看程式碼才知道。

模板專案中是直接引入 svg 檔案,而且也沒有 iconfont.js 檔案,相關的包有兩個:

"devDependencies": {
    // 建立 SVG sprites
    "svg-sprite-loader": "4.1.3",
    // 基於Nodejs的SVG向量圖形檔案優化工具
    "svgo": "1.2.2",
  },

我們也將 iconfont 改成這種方式,首先將之前的引入 iconfont 的程式碼去除,然後通過vue-cli 安裝外掛 vue-cli-plugin-svg-sprite:

myself-vue-admin-template> vue add vue-cli-plugin-svg-sprite                   

?  Installing vue-cli-plugin-svg-sprite...
...
✔  Successfully installed plugin: vue-cli-plugin-svg-sprite

? Add SVGO-loader to optimize your icons before the sprite is created? Yes

?  Invoking generator for vue-cli-plugin-svg-sprite...
?  Installing additional dependencies...

外掛安裝成功後,我們檢視改變的檔案:

myself-vue-admin-template> git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   package-lock.json
        // 得知安裝了三個包:svgo、svgo-loader、vue-cli-plugin-svg-sprite
        modified:   package.json
        modified:   vue.config.js

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        // svg 封裝的元件
        src/components/SvgIcon.vue

Tip:vue.config.js 會新增如下內容:

module.exports = {
  // 略
  chainWebpack: config => {
    config.module
      .rule('svg-sprite')
      .use('svgo-loader')
      .loader('svgo-loader')
  }
}

接著我們需要全域性註冊 svg 元件:

// main.js
import SvgIcon from '@/components/SvgIcon'// svg component

// register globally
Vue.component('svg-icon', SvgIcon)

最後我們得測試 iconfont 是否生效。

先將 svg 檔案下載並儲存到 assets/icons 目錄,然後修改 About.vue:

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <svg-icon name="password" />
    <svg-icon name="open" />
  </div>
</template>

重啟服務,頁面成功顯示兩張 svg 圖片,至此,我們的 iconfont 就引入成功了。

nprogress

在模板專案中,切換試圖,在頁面頂部會有一個進度條的東西,使用的就是 nprogress(Ajax'y 應用程式的細長進度條。受 Google、YouTube 和 Medium 的啟發)。

模板專案:

  "dependencies": {
    "nprogress": "0.2.0",
  }

新增 nprogress

由於 vue-cli ui 搜尋不到 nprogress 相應的外掛,所以我們只能通過 npm 老老實實的安裝:

$ npm i -D nprogress

接下來使用 nprogress,給 About.vue 新增如下程式碼,重啟服務即可看到效果:

// About.vue
<script>
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
// 通過將其設定為 false 來關閉載入微調器。
NProgress.configure({ showSpinner: false }) // NProgress Configuration

export default {
  created () {
    // Simply call start() and done() to control the progress bar.
    NProgress.start();
    // 可以嘗試直接呼叫 NProgress.done() 或者不執行 NProgress.done()
    setTimeout(function(){
      NProgress.done()
    }, 10000)
  },
}
</script>

Tip:直接在模板專案中搜尋關鍵字 NProgress 就能找到上面的程式碼。NProgress.start() 和 NProgress.done() 出現在模板專案中的 permission.js 檔案裡面,並且也在路由中。

normalize.css

normalize.css,CSS 重置的現代替代方案

新增 normalize.css

$ npm i -D normalize.css

在入口檔案中引入 normalize.css

// main.js
import 'normalize.css/normalize.css' // A modern alternative to CSS resets

重啟服務,body 的 margin 會重置為 0,說明已生效。

js-cookie,用於處理 cookie,簡單的、輕量級的 JavaScript API。

相關程式碼如下:

// package.json
"dependencies": {
    "js-cookie": "2.2.0",
  },
// src/utils/auth.js
import Cookies from 'js-cookie'

const TokenKey = 'vue_admin_template_token'

export function getToken() {
  return Cookies.get(TokenKey)
}

export function setToken(token) {
  return Cookies.set(TokenKey, token)
}

export function removeToken() {
  return Cookies.remove(TokenKey)
}
$ npm i -D js-cookie

About.vue 中使用一下:

// About.vue
<script>
import Cookies from 'js-cookie'

export default {
  created () {
    Cookies.set('sex', 'man')
    alert(Cookies.get('sex'))
  },
}
</script>

重啟服務,頁面彈出 man 則說明成功引入。

其他

npm 必須使用 TLS 1.2 or higher

某天執行 npm i 報錯如下:

npm notice Beginning October 4, 2021, all connections to the npm registry - including for package installation - must use TLS 1.2 or higher. You are currently using plaintext http to connect. Please visit the GitHub blog for more information: https://github.blog/2021-08-23-npm-registry-deprecating-tls-1-0-tls-1-1/  

可以檢視這篇文章:npm registry正在棄用TLS 1.0和TLS 1.1

Uncaught (in promise) Error: Redirected when going from

Uncaught (in promise) Error: Redirected when going from "/login" to "/" via a navigation guard.

可以檢視這篇文章:Uncaught (in promise) Error: Redirected when going from

其他章節請看:

vue 快速入門 系列

相關文章