vue 快速入門 系列 —— vue-cli 下

彭加李發表於2021-08-17

其他章節請看:

vue 快速入門 系列

Vue CLI 4.x 下

vue loader 一文中我們已經學會從零搭建一個簡單的,用於單檔案元件開發的腳手架;本篇,我們將全面學習 vue-cli 這個官方的、成熟的腳手架。

分上下兩篇進行,上篇主要是”基礎“,下篇主要是“開發”

Tip:介紹順序儘可能保持與官方文件一致

準備環境

:本篇所有的實驗都將基於專案 vue-example

vue-example

通過 vue create 命令建立專案 vue-example:

// 專案預設 `[Vue 2] less`, `babel`, `router`, `vuex`, `eslint`
exercise> vue create vue-example

Vue CLI v4.5.13
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Vuex, CSS Pre-processors, Linter
? Choose a version of Vue.js that you want to start the project with 2.x
? Use history mode for router? (Requires proper server setup for index fallback in production) No
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Less
? Pick a linter / formatter config: Standard
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No


Vue CLI v4.5.13
✨  Creating project in D:\aaron\blogv2\exercise\vue-example.
�  Initializing git repository...
⚙️  Installing CLI plugins. This might take a while...


added 1278 packages, and audited 1279 packages in 1m

80 packages are looking for funding
  run `npm fund` for details

11 moderate severity vulnerabilities

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.
�  Invoking generators...
�  Installing additional dependencies...


added 124 packages, and audited 1403 packages in 14s

91 packages are looking for funding
  run `npm fund` for details

11 moderate severity vulnerabilities

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.
⚓  Running completion hooks...

�  Generating README.md...

�  Successfully created project vue-example.
�  Get started with the following commands:

 $ cd vue-example
 $ npm run serve

Tip:專案預設 [Vue 2] less, babel, router, vuex, eslint

webpack.config.development.js

提取出 webpack 開發配置以及生產配置:

// 提取出 webpack開發配置,匯出到 webpack.config.development.js 中
vue-example> npx vue-cli-service inspect --mode development >> webpack.config.development.js

// 提取出 webpack 生產配置
vue-example> npx vue-cli-service inspect --mode production >> webpack.config.production.js

:接下來我們學習過程中會參考這兩個配置檔案

開發

瀏覽器相容性

browserslist

你會發現有 package.json 檔案裡的 browserslist 欄位 (或一個單獨的 .browserslistrc 檔案),指定了專案的目標瀏覽器的範圍。這個值會被 @babel/preset-env 和 Autoprefixer 用來確定需要轉譯的 JavaScript 特性和需要新增的 CSS 瀏覽器字首。

// vue-example/.browserslistrc
> 1%
last 2 versions
not dead

Tip:有關browserslist的更多介紹可以檢視筆者的webpack 實戰一

Polyfill

Tip: 如果初次接觸 Polyfill,可以先看webpack 實戰一 -> js 相容性處理

useBuiltIns: 'usage'

一個預設的 Vue CLI 專案會使用 @vue/babel-preset-app,它通過 @babel/preset-env 和 browserslist 配置來決定專案需要的 polyfill。

預設情況下,它會把 useBuiltIns: 'usage' 傳遞給 @babel/preset-env,這樣它會根據原始碼中出現的語言特性自動檢測需要的 polyfill。這確保了最終包裡 polyfill 數量的最小化。然而,這也意味著如果其中一個依賴需要特殊的 polyfill,預設情況下 Babel 無法將其檢測出來

構建庫或是 Web Component 時的 Polyfills

當使用 Vue CLI 來構建一個庫或是 Web Component 時,推薦給 @vue/babel-preset-app 傳入 useBuiltIns: false 選項。這能夠確保你的庫或是元件不包含不必要的 polyfills。通常來說,打包 polyfills 應當是最終使用你的庫的應用的責任。

現代模式

有了 Babel 我們可以兼顧所有最新的 ES2015+ 語言特性,但也意味著我們需要交付轉譯和 polyfill 後的包以支援舊瀏覽器。這些轉譯後的包通常都比原生的 ES2015+ 程式碼會更冗長,執行更慢。現如今絕大多數現代瀏覽器都已經支援了原生的 ES2015,所以因為要支援更老的瀏覽器而為它們交付笨重的程式碼是一種浪費。

Vue CLI 提供了一個“現代模式”幫你解決這個問題。以如下命令為生產環境構建:

vue-cli-service build --modern

Vue CLI 會產生兩個應用的版本:一個現代版的包,面向支援 ES modules 的現代瀏覽器,另一箇舊版的包,面向不支援的舊瀏覽器。

最酷的是這裡沒有特殊的部署要求。其生成的 HTML 檔案會自動使用 Phillip Walton 精彩的博文中討論到的技術:

  • 現代版的包會通過 <script type="module"> 在被支援的瀏覽器中載入;它們還會使用 <link rel="modulepreload"> 進行預載入。
  • 舊版的包會通過 <script nomodule> 載入,並會被支援 ES modules 的瀏覽器忽略。
  • 一個針對 Safari 10 中 <script nomodule> 的修復會被自動注入。

對於一個 Hello World 應用來說,現代版的包已經小了 16%。在生產環境下,現代版的包通常都會表現出顯著的解析速度和運算速度,從而改善應用的載入效能。

Tip<script type="module"> 需要配合始終開啟的 CORS 進行載入。這意味著你的伺服器必須返回諸如 Access-Control-Allow-Origin: * 的有效的 CORS 頭。如果你想要通過認證來獲取指令碼,可使將 crossorigin 選項設定為 use-credentials

HTML 和靜態資源

HTML
Index 檔案

public/index.html 檔案是一個會被 html-webpack-plugin 處理的模板。在構建過程中,資源連結會被自動注入。另外,Vue CLI 也會自動注入 resource hint (preload/prefetch、manifest 和圖示連結 (當用到 PWA 外掛時) 以及構建過程中處理的 JavaScript 和 CSS 檔案的資源連結。

Tiphtml-webpack-plugin初步認識 webpack 一文中有詳細介紹;prefetch 在webapck 效能一文中有介紹。

插值

因為 index 檔案被用作模板,所以你可以使用 lodash template 語法插入內容:

  • <%= VALUE %> 用來做不轉義插值;
  • <%- VALUE %> 用來做 HTML 轉義插值;
  • <% expression %> 用來描述 JavaScript 流程控制。

除了被 html-webpack-plugin 暴露的預設值之外,所有客戶端環境變數也可以直接使用。例如,BASE_URL 的用法:

<link rel="icon" href="<%= BASE_URL %>favicon.ico">

我們可以給 html-webpack-plugin客戶端環境變數各定義一個變數,並在 index.html 中輸出:

// vue-example/vue.config.js
module.exports = {
  chainWebpack: config => {
    // 客戶端環境變數
    config
      .plugin('define')
      .tap(args => {
        // [ { 'process.env': { NODE_ENV: '"development"', BASE_URL: '"/"' } } ]
        console.log(args);
        // 'aaron'會報錯,需要雙引號。因為 webpack.config.development.js 中也有雙引號 `NODE_ENV: '"development"'`
        args[0]['process.env'].name = '"aaron"' 
        return args
      })

    // html-webpack-plugin
    config
      .plugin('html')
      .tap(args => {
        //  [{title: 'vue-example',...}]
        console.log(args);
        args[0].age = '18'
        return args;
      })
  }
}

Tip:可以通過 webpack.config.development.js 知曉 config.plugin('html') 對應 HtmlWebpackPlugin、config.plugin('define') 對應 DefinePlugin

// vue-example/public/index.html
<body>
  <p>
    <%= htmlWebpackPlugin.options.age %>
    <%= process.env.name %>
  </p>
  ...
</body>

重啟服務(npm run serve),頁面中會顯示 18 aaron

Preload

<link rel="preload"> 是一種 resource hint,用來指定頁面載入後很快會被用到的資源,所以在頁面載入的過程中,我們希望在瀏覽器開始主體渲染之前儘早 preload。

預設情況下,一個 Vue CLI 應用會為所有初始化渲染需要的檔案自動生成 preload 提示。

這些提示會被 @vue/preload-webpack-plugin 注入,並且可以通過 chainWebpackconfig.plugin('preload') 進行修改和刪除。

Tip: preload 預載入就是 link 元素中的一個屬性,目前是非標準,更多資訊可以檢視mdn's link

Prefetch

Tip:本小節示例參考webpack 效能 一文中“懶載入”“預獲取“兩小節

<link rel="prefetch"> 是一種 resource hint,用來告訴瀏覽器在頁面載入完成後,利用空閒時間提前獲取使用者未來可能會訪問的內容。

預設情況下,一個 Vue CLI 應用會為所有作為 async chunk 生成的 JavaScript 檔案 (通過動態 import() 按需 code splitting 的產物) 自動生成 prefetch 提示。

這些提示會被 @vue/preload-webpack-plugin 注入,並且可以通過 chainWebpackconfig.plugin('prefetch') 進行修改和刪除。

接下來我們通過一組實驗來理解一下上文所述。

首先驗證:通過 import() 動態匯入會自動生成 prefetch 提示。步驟如下:

  1. 啟動服務,瀏覽器開啟頁面
  2. 進入Home導航,在網路中能看到 about.js 請求
  3. 然後切換到 About,發現 about.js 這個請求的Size顯示(預取快取)

Tip: About.vue 是通過 import() 動態匯入,所以 about 應該是預獲取。

// src/router/index.js
const routes = [
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

接著修改配置,將 prefetch 關閉:

// vue.config.js
module.exports = {
  chainWebpack: config => {
    // 移除 prefetch 外掛
    config.plugins.delete('prefetch')
  }
}

再次重啟服務,重複上面步驟,發現 about.js 不在來自快取。

若需要單獨開啟 about.js 的 prefetch,可以這麼做:

// src/router/index.js
const routes = [
  {
    path: '/about',
    name: 'About',
    - omponent: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
    + component: () => import(/* webpackChunkName: "about", webpackPrefetch: true */ '../views/About.vue')
  }
]

Tip:Prefetch 連結將會消耗頻寬。如果你的應用很大且有很多 async chunk,而使用者主要使用的是對頻寬較敏感的移動端,那麼你可能需要關掉 prefetch 連結並手動選擇要提前獲取的程式碼區塊。

不生成 index

當基於已有的後端使用 Vue CLI 時,你可能不需要生成 index.html,這樣生成的資源可以用於一個服務端渲染的頁面。

然而這樣做並不是很推薦,因為:

  • 硬編碼的檔名不利於實現高效率的快取控制。
  • 硬編碼的檔名也無法很好的進行 code-splitting (程式碼分段),因為無法用變化的檔名生成額外的 JavaScript 檔案。
  • 硬編碼的檔名無法在現代模式下工作。

修改配置檔案:

// vue.config.js
module.exports = {
  // 去掉檔名中的 hash
  filenameHashing: false,
  // 刪除 HTML 相關的 webpack 外掛
  chainWebpack: config => {
    config.plugins.delete('html')
    config.plugins.delete('preload')
    config.plugins.delete('prefetch')
  }
}

重啟服務,index.html 內容如下:

<!DOCTYPE html>
<html lang="">

<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">
  <title>
    <%= htmlWebpackPlugin.options.title %>
  </title>
</head>

<body>
  <noscript>
    <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> 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>

於是我們也就知道上面所說的”硬編碼“是什麼意思。

構建一個多頁應用

不是每個應用都需要是一個單頁應用。Vue CLI 支援使用 vue.config.js 中的 pages 選項構建一個多頁面的應用。構建好的應用將會在不同的入口之間高效共享通用的 chunk 以獲得最佳的載入效能。

接下來我們給專案增加一個頁面,步驟如下:

首先修改配置檔案:

// vue.config.js
module.exports = {
  pages: {
    index: {
      // page 的入口
      entry: 'src/main.js',
      // 模板來源
      template: 'public/index.html',
      // 在 dist/index.html 的輸出
      filename: 'index.html',
      // 當使用 title 選項時,
      // template 中的 title 標籤需要是 <title><%= htmlWebpackPlugin.options.title %></title>
      title: 'Index Page',
      // 在這個頁面中包含的塊,預設情況下會包含
      // 提取出來的通用 chunk 和 vendor chunk。
      chunks: ['chunk-vendors', 'chunk-common', 'index']
    },
    // 當使用只有入口的字串格式時,
    // 模板會被推導為 `public/subpage.html`
    // 並且如果找不到的話,就回退到 `public/index.html`。
    // 輸出檔名會被推導為 `subpage.html`。
    subpage: 'src/bmain.js'
  }
}

接著新建第二個頁面的模板,以及入口檔案:

// public/subpage.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">
    <title>page b</title>
</head>
<body>
</body>
</html>
// src/bmain.js
console.log('i am bmain.js')

重啟服務,即可通過 http://localhost:8081/subpage 訪問第二個頁面,並在控制檯輸出”i am bmain.js“

vue-example> npm run serve

> vue-example@0.1.0 serve
> vue-cli-service serve

- Local:   http://localhost:8081/
- Network: http://192.168.0.103:8081/
處理靜態資源

靜態資源可以通過兩種方式進行處理:

  • JavaScript 被匯入或在 template/CSS 中通過相對路徑被引用。這類引用會被 webpack 處理。
  • 放置在 public 目錄下或通過絕對路徑被引用。這類資源將會直接被拷貝,而不會經過 webpack 的處理。
從相對路徑匯入

當你在 JavaScriptCSS*.vue 檔案中使用相對路徑 (必須以 . 開頭) 引用一個靜態資源時,該資源將會被包含進入 webpack 的依賴圖中。在其編譯過程中,所有諸如 <img src="...">background: url(...) 和 CSS @import 的資源 URL 都會被解析為一個模組依賴。

例如,url(./image.png) 會被翻譯為 require('./image.png'),而:

<img src="./image.png">

將會被編譯到:

h('img', { attrs: { src: require('./image.png') }})

在其內部,我們通過 file-loader 用版本雜湊值和正確的公共基礎路徑來決定最終的檔案路徑,再用 url-loader 將小於 4kb 的資源內聯,以減少 HTTP 請求的數量。

Tip: url-loader 的詳細介紹請檢視webpack 實戰一->”打包圖片“

你可以通過 chainWebpack 調整內聯檔案的大小限制。例如,下列程式碼會將其限制設定為 10kb:

// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('images')
        .use('url-loader')
          .loader('url-loader')
          .tap(options => Object.assign(options, { limit: 10240 }))
  }
}
URL 轉換規則
  • 如果 URL 是一個絕對路徑 (例如 /images/foo.png),它將會被保留不變。

  • 如果 URL 以 . 開頭,它會作為一個相對模組請求被解釋且基於你的檔案系統中的目錄結構進行解析。

  • 如果 URL 以 ~ 開頭,其後的任何內容都會作為一個模組請求被解析。這意味著你甚至可以引用 Node 模組中的資源:

<img src="~some-npm-package/foo.png">

測試過程中遇到一些問題:

  1. src/assets/logo.png 拷貝到 node_modules/vue 目錄下,然後將<img src="~vue/logo.png" />放入App.vueHome.vue 中生效,但在 public/index.html 中無效,瀏覽器檢視原始碼還是<img src="~vue/logo.png" />
  2. public/favicon.ico 拷貝到 node_modules/vue 目錄下,在 App.vue 中輸入<img src="~vue/favicon.ico" /> 報錯
  • 如果 URL 以 @ 開頭,它也會作為一個模組請求被解析。它的用處在於 Vue CLI 預設會設定一個指向 <projectRoot>/src 的別名 @。(僅作用於模版中)

比如在 App.vue 中新增如下程式碼,你將會在頁面中看見該圖片:

<img src="@/assets/logo.png" alt="" />

Tip:在 webpack.config.development.js 中有如下程式碼:

alias: {
  '@': 'exercise\\vue-example\\src',
  vue$: 'vue/dist/vue.runtime.esm.js'
}
public 資料夾

任何放置在 public 資料夾的靜態資源都會被簡單的複製,而不經過 webpack。你需要通過絕對路徑來引用它們。

:推薦將資源作為你的模組依賴圖的一部分匯入,這樣它們會通過 webpack 的處理並獲得如下好處:

  • 指令碼和樣式表會被壓縮且打包在一起,從而避免額外的網路請求。
  • 檔案丟失會直接在編譯時報錯,而不是到了使用者端才產生 404 錯誤。
  • 最終生成的檔名包含了內容雜湊,因此你不必擔心瀏覽器會快取它們的老版本。

public 目錄提供的是一個應急手段,當你通過絕對路徑引用它時,留意應用將會部署到哪裡。如果你的應用沒有部署在域名的根部,那麼你需要為你的 URL 配置 publicPath 字首。

目前構建後的 index.html 有如下語句:

<link rel="icon" href="/favicon.ico">

通過修改 publicPath

// vue.config.js
module.exports = {
  publicPath: '/a/b'
}

再次構建,將會變成:

<link rel="icon" href="/a/b/favicon.ico">

Tip:預設情況下,Vue CLI 會假設你的應用是被部署在一個域名的根路徑上,例如 https://www.my-app.com/。如果應用被部署在一個子路徑上,你就需要用這個選項指定這個子路徑

:記得還原 publicPath,方便後續實驗。

何時使用 public 資料夾
  • 你需要在構建輸出中指定一個檔案的名字。

  • 你有上千個圖片,需要動態引用它們的路徑。

  • 有些庫可能和 webpack 不相容,這時你除了將其用一個獨立的 <script> 標籤引入沒有別的選擇。

我們驗證一下最後一點:

在 public 資料夾中新建一個 js 檔案:

// public/a.js
console.log('apple');

在 index.html 中引入新建的 js 檔案:

// public/index.html
+ <script src='/a.js'></script>

重啟服務,在瀏覽器控制檯會輸出”apple“

CSS 相關

Vue CLI 專案天生支援 PostCSSCSS Modules 和包含 SassLessStylus 在內的前處理器。

Tip:通過檢視 webapck.config.development.js 就可以看見相關配置

引用一個靜態資源

所有編譯後的 CSS 都會通過 css-loader 來解析其中的 url() 引用,並將這些引用作為模組請求來處理。這意味著你可以根據本地的檔案結構用相對路徑來引用靜態資源。另外要注意的是如果你想要引用一個 npm 依賴中的檔案,或是想要用 webpack alias,則需要在路徑前加上 ~ 的字首來避免歧義。

來個簡單的示例:

建立一個 css 檔案 :

// src/style/a.css
p{color:yellow}

在 main.js 中引入新建立的 css 檔案:

+ import './style/a.css'

重啟服務,在 Home 導航頁面中就能看見黃色文字。

前處理器

你可以在建立專案的時候選擇前處理器 (Sass/Less/Stylus)。如果當時沒有選好,內建的 webpack 仍然會被預配置為可以完成所有的處理。你也可以手動安裝相應的 webpack loader

# Sass
npm install -D sass-loader sass

# Less
npm install -D less-loader less

# Stylus
npm install -D stylus-loader stylus

然後你就可以匯入相應的檔案型別,或在 *.vue 檔案中這樣來使用:

<style lang="scss">
$color: red;
</style>

Tip:更多細節請查閱vue loader->使用前處理器

自動化匯入

如果你想自動化匯入檔案 (用於顏色、變數、mixin……),你可以使用 style-resources-loader

這裡有一個關於 Stylus 的在每個單檔案元件和 Stylus 檔案中匯入 ./src/styles/imports.styl 的例子(雖然你可能只是用 less 開發):

// vue.config.js
const path = require('path')

module.exports = {
  chainWebpack: config => {
    const types = ['vue-modules', 'vue', 'normal-modules', 'normal']
    types.forEach(type => addStyleResource(config.module.rule('stylus').oneOf(type)))
  },
}

function addStyleResource (rule) {
  rule.use('style-resource')
    .loader('style-resources-loader')
    .options({
      patterns: [
        path.resolve(__dirname, './src/styles/imports.styl'),
      ],
    })
}

你也可以選擇使用 vue-cli-plugin-style-resources-loader

Tipstyle-resources-loader,這個載入器是一個用於 webpack 的 CSS 處理器資源載入器,它將你的樣式資源(例如variables, mixins)注入到多個匯入的css, sass, scss, less, stylus模組中。

它主要用於:

  • variables, mixins, functions在所有樣式檔案中共享您的檔案,因此您無需@import手動使用它們。
  • 覆蓋variables其他庫(例如ant-design)提供的樣式檔案並自定義您自己的主題。
PostCSS

Vue CLI 內部使用了 PostCSS。

你可以通過 .postcssrc 或任何 postcss-load-config 支援的配置源來配置 PostCSS。也可以通過 vue.config.js 中的 css.loaderOptions.postcss 配置 postcss-loader。

我們預設開啟了 autoprefixer。如果要配置目標瀏覽器,可使用 package.json 的 browserslist 欄位。

Tip:更多細節請查閱webpack 實戰一->使用 PostCSS

CSS Modules

你可以通過 <style module> 以開箱即用的方式在 *.vue 檔案中使用 CSS Modules

Tip: 更多細節請查閱vue loader 下->css Modules

向前處理器 Loader 傳遞選項

有的時候你想要向 webpack 的前處理器 loader 傳遞選項。你可以使用 vue.config.js 中的 css.loaderOptions 選項。比如你可以這樣向所有 Sass/Less 樣式傳入共享的全域性變數:

// vue.config.js
module.exports = {
  css: {
    loaderOptions: {
      // 給 sass-loader 傳遞選項
      sass: {
        // @/ 是 src/ 的別名
        // 所以這裡假設你有 `src/variables.sass` 這個檔案
        // 注意:在 sass-loader v8 中,這個選項名是 "prependData"
        additionalData: `@import "~@/variables.sass"`
      },
      // 預設情況下 `sass` 選項會同時對 `sass` 和 `scss` 語法同時生效
      // 因為 `scss` 語法在內部也是由 sass-loader 處理的
      // 但是在配置 `prependData` 選項的時候
      // `scss` 語法會要求語句結尾必須有分號,`sass` 則要求必須沒有分號
      // 在這種情況下,我們可以使用 `scss` 選項,對 `scss` 語法進行單獨配置
      scss: {
        additionalData: `@import "~@/variables.scss";`
      },
      // 給 less-loader 傳遞 Less.js 相關選項
      less:{
        // http://lesscss.org/usage/#less-options-strict-units `Global Variables`
        // `primary` is global variables fields name
        globalVars: {
          primary: '#fff'
        }
      }
    } 
  }
}

我們做個實驗:

修改 vue.config.js,給 less 定義一個全域性變數:

// vue.config.js
module.exports = {
  css: {
    loaderOptions: {
      // 給 less-loader 傳遞 Less.js 相關選項
      less: {
        // http://lesscss.org/usage/#less-options-strict-units `Global Variables`
        // `primary` is global variables fields name
        globalVars: {
          primary: 'pink'
        }
      }
    }
  }
}

App.vue 中使用 less 定義的全域性變數:

<style lang="less">
p {
  color: @primary;
}
</style>

重啟服務,在頁面中出現粉色文字。

Loader 可以通過 loaderOptions 配置,包括:

  • css-loader
  • postcss-loader
  • sass-loader
  • less-loader
  • stylus-loader

Tip:這樣做比使用 chainWebpack 手動指定 loader 更推薦,因為這些選項需要應用在使用了相應 loader 的多個地方。

webpack 相關

簡單的配置方式

調整 webpack 配置最簡單的方式就是在 vue.config.js 中的 configureWebpack 選項提供一個物件:

// vue.config.js
module.exports = {
  configureWebpack: {
    plugins: [
      new MyAwesomeWebpackPlugin()
    ]
  }
}

該物件將會被 webpack-merge 合併入最終的 webpack 配置。

:有些 webpack 選項是基於 vue.config.js 中的值設定的,所以不能直接修改。例如你應該修改 vue.config.js 中的 outputDir 選項而不是修改 output.path;你應該修改 vue.config.js 中的 publicPath 選項而不是修改 output.publicPath。這樣做是因為 vue.config.js 中的值會被用在配置裡的多個地方,以確保所有的部分都能正常工作在一起。

如果你需要基於環境有條件地配置行為,或者想要直接修改配置,那就換成一個函式 (該函式會在環境變數被設定之後懶執行)。該方法的第一個引數會收到已經解析好的配置。在函式內,你可以直接修改配置,或者返回一個將會被合併的物件。就像這樣:

修改配置檔案:

// vue.config.js
module.exports = {
  configureWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      // 為生產環境修改配置...
      console.log('生產環境');
    } else {
      // 為開發環境修改配置...
      console.log('開發環境');
    }
  }
}

執行 npm run serve 將會在終端控制檯輸出開發環境

執行 npm run build 將會在終端控制檯輸出生產環境

鏈式操作 (高階)

Vue CLI 內部的 webpack 配置是通過 webpack-chain 維護的。這個庫提供了一個 webpack 原始配置的上層抽象,使其可以定義具名的 loader 規則和具名外掛,並有機會在後期進入這些規則並對它們的選項進行修改。

它允許我們更細粒度的控制其內部配置。接下來有一些常見的在 vue.config.js 中的 chainWebpack 修改的例子。

Tip:npm 包 webpack-chain 介紹:

  • webpack 的核心配置基於建立和修改一個潛在的笨拙的 JavaScript 物件。雖然這適用於單個專案的配置,但嘗試跨專案共享這些物件並進行後續修改會變得混亂,因為您需要深入瞭解底層物件結構才能進行這些更改。
  • webpack-chain嘗試通過提供可連結或流暢的 API 來建立和修改 webpack 配置來改進此過程。API 的關鍵部分可以由使用者指定的名稱引用,這有助於標準化如何跨專案修改配置。
修改 Loader 選項
// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
        .tap(options => {
          console.log(options)
          // 修改它的選項...
          return options
        })
  }
}

重啟服務,將會在終端控制檯輸出:

// 類似
{
  compilerOptions: { whitespace: 'condense' },
  cacheDirectory: 'node_modules\\.cache\\vue-loader',
  cacheIdentifier: 'a4b2cefc'
}

Tip:對於 CSS 相關 loader 來說,我們推薦使用 css.loaderOptions 而不是直接鏈式指定 loader。這是因為每種 CSS 檔案型別都有多個規則,而 css.loaderOptions 可以確保你通過一個地方影響所有的規則。如果你看一下 webpack.config.development.js,你會發現 css-loader 有很多匹配。

新增一個新的 Loader
// vue.config.js
module.exports = {
  chainWebpack: config => {
    // GraphQL Loader
    config.module
      .rule('graphql')
      .test(/\.graphql$/)
      .use('graphql-tag/loader')
        .loader('graphql-tag/loader')
        .end()
      // 你還可以再新增一個 loader
      .use('other-loader')
        .loader('other-loader')
        .end()
  }
}
替換一個規則裡的 Loader

如果你想要替換一個已有的基礎 loader,例如為內聯的 SVG 檔案使用 vue-svg-loader 而不是載入這個檔案:

// vue.config.js
module.exports = {
  chainWebpack: config => {
    const svgRule = config.module.rule('svg')

    // 清除已有的所有 loader。
    // 如果你不這樣做,接下來的 loader 會附加在該規則現有的 loader 之後。
    svgRule.uses.clear()

    // 新增要替換的 loader
    svgRule
      .use('vue-svg-loader')
        .loader('vue-svg-loader')
  }
}
修改外掛選項
// vue.config.js
module.exports = {
  chainWebpack: config => {
    config
      .plugin('html')
      .tap(args => {
        return [/* 傳遞給 html-webpack-plugin's 建構函式的新引數 */]
      })
  }
}

Tip: 更詳細的用法請檢視本篇 HTML 和靜態資源->HTML->插值

審查專案的 webpack 配置

因為 @vue/cli-service 對 webpack 配置進行了抽象,所以理解配置中包含的東西會比較困難,尤其是當你打算自行對其調整的時候。

vue-cli-service 暴露了 inspect 命令用於審查解析好的 webpack 配置。那個全域性的 vue 可執行程式同樣提供了 inspect 命令,這個命令只是簡單的把 vue-cli-service inspect 代理到了你的專案中。

執行下面兩條命令,將會生成 output.jswebpack.config.development.js

vue-example> vue inspect > output.js

vue-example> npx vue-cli-service inspect --mode development >> webpack.config.development.js

兩個檔案生成的內容相同,內容就像這樣:

{
  mode: 'development',
  context: 'exercise\\vue-example',
  node:{
    ...
  },
  resolve: {
    ...
  },
  resovleLoader: {
    ...
  },
  // loader
  module: {
    ...
  },
  // 優化
  optimization: {
    ...
  },
  // 外掛
  plugins: [
    ...
  ],
  entry: {
    app: [
      './src/main.js'
    ]
  }
}

:它輸出的並不是一個有效的 webpack 配置檔案,而是一個用於審查的被序列化的格式

你也可以通過指定一個路徑來審查配置的一小部分:

// 只審查第一條規則
> vue inspect module.rules.0
/* config.module.rule('vue') */
{
  test: /\.vue$/,
  use: [
    /* config.module.rule('vue').use('cache-loader') */
    {
      loader: 'exercise\\vue-example\\node_modules\\cache-loader\\dist\\cjs.js',
      options: {
        cacheDirectory: 'exercise\\vue-example\\node_modules\\.cache\\vue-loader',
        cacheIdentifier: '3a7662ca'
      }
    },
    /* config.module.rule('vue').use('vue-loader') */
    {
      loader: 'exercise\\vue-example\\node_modules\\vue-loader\\lib\\index.js',
      options: {
        compilerOptions: {
          whitespace: 'condense'
        },
        cacheDirectory: 'exercise\\vue-example\\node_modules\\.cache\\vue-loader',
        cacheIdentifier: '3a7662ca'
      }
    }
  ]
}

或者指向一個規則或外掛的名字:

vue inspect --rule vue
vue inspect --plugin html

最後,你可以列出所有規則和外掛的名字:

> vue inspect --rules
[
  'vue',
  'images',
  'svg',
  'media',
  'fonts',
  'pug',
  'css',
  'postcss',
  'scss',
  'sass',
  'less',
  'stylus',
  'js',
  'eslint'
]
> vue inspect --plugins
[
  'vue-loader',
  'define',
  'case-sensitive-paths',
  'friendly-errors',     
  'html',
  'preload',
  'prefetch',
  'copy'
]

模式和環境變數

模式

模式是 Vue CLI 專案中一個重要的概念。預設情況下,一個 Vue CLI 專案有三個模式:

  • development 模式用於 vue-cli-service serve
  • test 模式用於 vue-cli-service test:unit
  • production 模式用於 vue-cli-service buildvue-cli-service test:e2e

你可以通過傳遞 --mode 選項引數為命令列覆寫預設的模式。例如,如果你想要在構建命令中使用開發環境變數:

vue-example> npx vue-cli-service build --mode development

當執行 vue-cli-service 命令時,所有的環境變數都從對應的環境檔案(見下一小節”環境變數“)中載入。如果檔案內部不包含 NODE_ENV 變數,它的值將取決於模式,例如,在 production 模式下被設定為 "production",在 test 模式下被設定為 "test",預設則是 "development"

做個實驗:

新建 .env.development 檔案:

// vue-example/.env.development
NODE_ENV=production

再次執行 npx vue-cli-service build --mode development,你會發現 dist/index.html 變為一行,說明 .env.development 檔案生效。

NODE_ENV 將決定您的應用執行的模式,是開發,生產還是測試,因此也決定了建立哪種 webpack 配置。

例如通過將 NODE_ENV 設定為 "test",Vue CLI 會建立一個優化過後的,並且旨在用於單元測試的 webpack 配置,它並不會處理圖片以及一些對單元測試非必需的其他資源。

同理,NODE_ENV=development 建立一個 webpack 配置,該配置啟用熱更新,不會對資源進行 hash 也不會打出 vendor bundles,目的是為了在開發的時候能夠快速重新構建。

當你執行 vue-cli-service build 命令時,無論你要部署到哪個環境,應該始終把 NODE_ENV 設定為 "production" 來獲取可用於部署的應用程式。

:如果在環境中有預設的 NODE_ENV,你應該移除它或在執行 vue-cli-service 命令的時候明確地設定 NODE_ENV

環境變數

你可以在你的專案根目錄中放置下列檔案來指定環境變數:

.env                # 在所有的環境中被載入
.env.local          # 在所有的環境中被載入,但會被 git 忽略
.env.[mode]         # 只在指定的模式中被載入
.env.[mode].local   # 只在指定的模式中被載入,但會被 git 忽略

Tip:上一小節我們已經使用了.env.[mode]

一個環境檔案只包含環境變數的“鍵=值”對:

FOO=bar
VUE_APP_NOT_SECRET_CODE=some_value

:不要在你的應用程式中儲存任何機密資訊(例如私有 API 金鑰)!環境變數會隨著構建打包嵌入到輸出程式碼,意味著任何人都有機會能夠看到它。

請注意,只有 NODE_ENVBASE_URL 和以 VUE_APP_ 開頭的變數將通過 webpack.DefinePlugin 靜態地嵌入到客戶端側的程式碼中。這是為了避免意外公開機器上可能具有相同名稱的私鑰。

你在 webpack.config.development.js 中能看到如下程式碼:

/* config.plugin('define') */
new DefinePlugin(
  {
    'process.env': {
      NODE_ENV: '"development"',
      BASE_URL: '"/"'
    }
  }
),

想要了解解析環境檔案規則的細節,請參考 dotenv。我們也使用 dotenv-expand 來實現變數擴充套件 (Vue CLI 3.5+ 支援)。

Tipdotenv 是一個零依賴模組,它將環境變數從 .env 檔案載入到 process.env 中;Dotenv-expanddotenv 之上新增了變數擴充套件。如果您發現自己需要擴充套件機器上已經存在的環境變數,那麼 dotenv-expand 就是您的工具。

例如:

FOO=foo
BAR=bar

CONCAT=$FOO$BAR # CONCAT=foobar

被載入的變數將會對 vue-cli-service 的所有命令、外掛和依賴可用。

環境檔案載入優先順序:

  • 為一個特定模式準備的環境檔案 (例如 .env.production) 將會比一般的環境檔案 (例如 .env) 擁有更高的優先順序。
  • Vue CLI 啟動時已經存在的環境變數擁有最高優先順序,並不會被 .env 檔案覆寫
  • .env 環境檔案是通過執行 vue-cli-service 命令載入的,因此環境檔案發生變化,你需要重啟服務
示例:Staging 模式

假設我們有一個應用包含以下 .env 檔案:

VUE_APP_TITLE=My App

.env.staging 檔案:

NODE_ENV=production
VUE_APP_TITLE=My App (staging)
  • vue-cli-service build 會載入可能存在的 .env.env.production.env.production.local 檔案然後構建出生產環境應用。

  • vue-cli-service build --mode staging 會在 staging 模式下載入可能存在的 .env.env.staging.env.staging.local 檔案然後構建出生產環境應用。

這兩種情況下,根據 NODE_ENV,構建出的應用都是生產環境應用,但是在 staging 版本中,process.env.VUE_APP_TITLE 被覆寫成了另一個值。

在客戶端側程式碼中使用環境變數

只有以 VUE_APP_ 開頭的變數會被 webpack.DefinePlugin 靜態嵌入到客戶端側的包中。你可以在應用的程式碼中這樣訪問它們:

console.log(process.env.VUE_APP_SECRET)

在構建過程中,process.env.VUE_APP_SECRET 將會被相應的值所取代。在 VUE_APP_SECRET=secret 的情況下,它會被替換為 "secret"

做個實驗:

新建 .env:

// vue-example/.env
VUE_APP_NAME=aaron

App.vue 中使用環境變數:

// App.vue
...
<script>
console.log(process.env.VUE_APP_NAME)
</script>

重啟服務(記得重啟,否則不生效),瀏覽器控制檯將輸出 aaron

除了 VUE_APP_* 變數之外,在你的應用程式碼中始終可用的還有兩個特殊的變數:

  • NODE_ENV - 會是 "development""production""test" 中的一個。具體的值取決於應用執行的模式。
  • BASE_URL - 會和 vue.config.js 中的 publicPath 選項相符,即你的應用會部署到的基礎路徑。
// App.vue
...
<script>
console.log(process.env.NODE_ENV)
</script>

所有解析出來的環境變數都可以在 public/index.html 中以 HTML 插值中介紹的方式使用。

你可以在 vue.config.js 檔案中計算環境變數。它們仍然需要以 VUE_APP_ 字首開頭。這可以用於版本資訊。

在 vue.config.js 中定義變數 VUE_APP_VERSION:

// vue.config.js
process.env.VUE_APP_VERSION = require('./package.json').version

App.vue 中使用版本變數:

<script>
console.log(process.env.VUE_APP_VERSION)
</script>

重啟服務,瀏覽器控制檯會輸出 0.1.0(package.json 中定義了 "version": "0.1.0")。

只在本地有效的變數

有的時候你可能有一些不應該提交到程式碼倉庫中的變數,尤其是當你的專案託管在公共倉庫時。這種情況下你應該使用一個 .env.local 檔案取而代之。本地環境檔案預設會被忽略,且出現在 .gitignore 中。

.local 也可以加在指定模式的環境檔案上,比如 .env.development.local 將會在 development 模式下被載入,且被 git 忽略。

構建目標

當你執行 vue-cli-service build 時,你可以通過 --target 選項指定不同的構建目標。它允許你將相同的原始碼根據不同的用例生成不同的構建。

應用

應用模式是預設的模式。在這個模式中:

  • index.html 會帶有注入的資源和 resource hint
  • 第三方庫會被分到一個獨立包以便更好的快取
  • 小於 4kb 的靜態資源會被內聯在 JavaScript 中
  • public 中的靜態資源會被複制到輸出目錄中

你可以通過下面的命令將一個單獨的入口構建為一個庫:

vue-cli-service build --target lib --name myLib [entry]

這個入口可以是一個 .js 或一個 .vue 檔案。如果沒有指定入口,則會使用 src/App.vue

我們在 vue-example 中嘗試使用該命令:

vue-example> npx vue-cli-service build --target lib --name myLib

/  Building for production as library (commonjs,umd,umd-min)...

  File                     Size                                      Gzipped 

  dist\myLib.umd.min.js    4.48 KiB                                  1.85 KiB
  dist\myLib.umd.js        18.74 KiB                                 4.56 KiB
  dist\myLib.common.js     18.36 KiB                                 4.46 KiB
  dist\myLib.css           0.25 KiB                                  0.19 KiB

  Images and other types of assets omitted.
  • dist/myLib.common.js:一個給打包器用的 CommonJS 包 (不幸的是,webpack 目前還並沒有支援 ES modules 輸出格式的包)
  • dist/myLib.umd.js:一個直接給瀏覽器或 AMD loader 使用的 UMD 包
  • dist/myLib.umd.min.js:壓縮後的 UMD 構建版本
  • dist/myLib.css:提取出來的 CSS 檔案 (可以通過在 vue.config.js 中設定 css: { extract: false } 強制內聯)
Web Components 元件

你可以通過下面的命令將一個單獨的入口構建為一個 Web Components 元件:

vue-cli-service build --target wc --name my-element [entry]

注意這裡的入口應該是一個 *.vue 檔案。Vue CLI 將會把這個元件自動包裹並註冊為 Web Components 元件,無需在 main.js 裡自行註冊。也可以在開發時把 main.js 作為 demo app 單獨使用。

我們做個實驗:

vue-example> npx vue-cli-service build --target wc --name my-element

|  Building for production as web component...

  File                      Size                                      Gzipped 

  dist\my-element.min.js    9.52 KiB                                  4.04 KiB
  dist\my-element.js        32.19 KiB                                 8.61 KiB

  Images and other types of assets omitted.

dist 目錄還生成了一個 html 檔案,內容如下:

// demo.html
<meta charset="utf-8">
<title>my-element demo</title>
<script src="https://unpkg.com/vue"></script>
<script src="./my-element.js"></script>

<my-element></my-element>

將這個 html 以網頁(需啟動服務,筆者使用 vscode 的外掛 Live server )形式開啟,頁面正常顯示,能看到一張 vue 的圖片,以及 HomeAbout

Tip:這個包依賴了在頁面上全域性可用的 Vue。這裡的 https://unpkg.com/vue 就是 vue 框架。

這個模式允許你的元件的使用者以一個普通 DOM 元素的方式使用這個 Vue 元件。

<!-- vue 庫 -->
<script src="https://unpkg.com/vue"></script>
<!-- 元件 -->
<script src="path/to/my-element.js"></script>

<!-- 可在普通 HTML 中或者其它任何框架中使用 -->
<my-element></my-element>
註冊多個 Web Components 元件的包

當你構建一個 Web Components 元件包的時候,你也可以使用一個 glob 表示式作為入口指定多個元件目標:

vue-cli-service build --target wc --name foo 'src/components/*.vue'

當你構建多個 web component 時,--name 將會用於設定字首,同時自定義元素的名稱會由元件的檔名推導得出。比如一個名為 HelloWorld.vue 的元件攜帶 --name foo 將會生成的自定義元素名為 <foo-hello-world>

非同步 Web Components 元件

當指定多個 Web Components 元件作為目標時,這個包可能會變得非常大,並且使用者可能只想使用你的包中註冊的一部分元件。這時非同步 Web Components 模式會生成一個 code-split 的包,帶一個只提供所有元件共享的執行時,並預先註冊所有的自定義元件小入口檔案。一個元件真正的實現只會在頁面中用到自定義元素相應的一個例項時按需獲取:

vue-cli-service build --target wc-async --name foo 'src/components/*.vue'
File                Size                        Gzipped

dist/foo.0.min.js    12.80 kb                    8.09 kb
dist/foo.min.js      7.45 kb                     3.17 kb
dist/foo.1.min.js    2.91 kb                     1.02 kb
dist/foo.js          22.51 kb                    6.67 kb
dist/foo.0.js        17.27 kb                    8.83 kb
dist/foo.1.js        5.24 kb                     1.64 kb

現在使用者在該頁面上只需要引入 Vue 和這個入口檔案即可:

<script src="https://unpkg.com/vue"></script>
<script src="path/to/foo.min.js"></script>

<!-- foo-one 的實現的 chunk 會在用到的時候自動獲取 -->
<foo-one></foo-one>

部署

通用指南

如果你用 Vue CLI 處理靜態資源並和後端框架一起作為部署的一部分,那麼你需要的僅僅是確保 Vue CLI 生成的構建檔案在正確的位置,並遵循後端框架的釋出方式即可。

如果你獨立於後端部署前端應用——也就是說後端暴露一個前端可訪問的 API,然後前端實際上是純靜態應用。那麼你可以將 dist 目錄裡構建的內容部署到任何靜態檔案伺服器中,但要確保正確的 publicPath

// vue.config.js
module.exports = {
  publicPath: process.env.NODE_ENV === 'production'
    ? '/production-sub-path/'
    : '/'
}
本地預覽

dist 目錄需要啟動一個 HTTP 伺服器來訪問 (除非你已經將 publicPath 配置為了一個相對的值),所以以 file:// 協議直接開啟 dist/index.html 是不會工作的。在本地預覽生產環境構建最簡單的方式就是使用一個 Node.js 靜態檔案伺服器,例如 serve

npm install -g serve
# -s 引數的意思是將其架設在 Single-Page Application 模式下
# 這個模式會處理即將提到的路由問題
serve -s dist

:這裡提到以 serve -s dist 啟動會處理路由問題。到底是什麼問題呢?請接著看:

vue-router 有 hash 模式和 history 模式。hash 比較醜;而 history 的 url 正常,也好看,不過需要後臺配置,因為我們的應用是個單頁客戶端應用,如果後臺沒有正確的配置,當使用者在瀏覽器直接訪問一個不存在的 url(http://oursite.com/user/id) 就會返回 404,這就不好看了。

將路由改為 hash 模式:

// vue-example/src/router/index.js
const router = new VueRouter({
- mode: 'history',
  base: process.env.BASE_URL,
  routes
})

重啟服務(npm run serve),瀏覽器頁面的 url 是 http://localhost:8080/#/,比較醜。修改 url,訪問一個不存在的頁面,還是會”正常“顯示,而不是報 404。

// 訪問不存在的資源
http://localhost:8080/#/9999

而使用 history 模式時,URL 就像正常的 url,例如 http://localhost:8080/about,而不是很醜的 http://localhost:8080/about#/

接下來我們用 express 寫個伺服器來訪問構建後的程式碼,步驟如下:

修改輸出目錄:

// vue.config.js
module.exports = {
  publicPath: process.env.NODE_ENV === 'production'
    ? '/production-sub-path/'
    : '/',
  outputDir: process.env.NODE_ENV === 'production'
    ? 'dist/production-sub-path'
    : 'dist',
}

重新構建,所有資源都在 dist/production-sub-path 目錄中:

vue-example> npm run build

  File                                                     Size                      Gzipped

  dist\production-sub-path\js\chunk-vendors.2c244d24.js    137.04 KiB                47.60 KiB
  dist\production-sub-path\js\app.60a619a9.js              6.43 KiB                  2.31 KiB
  dist\production-sub-path\js\about.994a1f46.js            0.89 KiB                  0.45 KiB
  dist\production-sub-path\a.js                            0.02 KiB                  0.04 KiB
  dist\production-sub-path\css\app.9da74d00.css            0.42 KiB                  0.26 KiB

新建 express 伺服器,即建立 dist/nodeServer.js

// dist/nodeServer.js

const path = require('path')
const express = require('express')
const app = express()
const port = 3000

// 提供服務的目錄的絕對路徑會更安全,否則如果不在 dist 目錄中啟動,就請求不到頁面
app.use('/production-sub-path', express.static(path.join(__dirname, '/production-sub-path')))

app.get('/', (req, res) => {
  const index = path.join(__dirname, '/production-sub-path/index.html')
  // 無需通過 res.render() 渲染 HTML。 你可以通過 res.sendFile() 直接對外輸出 HTML 檔案。
  // 需要絕對路徑
  res.sendFile(index)
})

// 請求不到則返回404
app.use(function (req, res, next) {
  res.status(404).send("Sorry can't find that!")
})

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

安裝 express 包,啟動服務,訪問 url,頁面正常顯示:

vue-example> npm i -D express 

vue-example> nodemon dist\nodeServer.js
...
Example app listening at http://localhost:3000

瀏覽器訪問 http://localhost:3000,如果訪問一個不存在的 url,例如 http://localhost:3000/#/about2 ,頁面也顯示“正常”(不會出現類似 404)

接下來我們改為 history 模式,重新編譯:

// vue-example/src/router/index.js
const router = new VueRouter({
+ mode: 'history',
  base: process.env.BASE_URL,
  routes
})
vue-example> npm run build

再次訪問 http://localhost:3000/about,url 也醜了,現在已經是 history 模式。進行 2 個測試:

  • 訪問不存在的 url(http://localhost:3000/about2),頁面顯示 ”Sorry can't find that!“
  • http://localhost:3000/about ,按 ctrl + r 重新整理頁面,頁面顯示 ”Sorry can't find that!“

以上兩個問題可以通過下面的後端配置來解決,請看操作:

現在我們要配置一下後端,當我們請求不存在的資源時,返回到首頁。

Tip:在 vue-router 中提到:對於 Node.js/Express,請考慮使用 connect-history-api-fallback 中介軟體。

我們首先安裝這個包,然後配置:

> npm i -D connect-history-api-fallback
// vue.config.js
...
const port = 3000

const history = require('connect-history-api-fallback')
app.use(history({
  index: '/'
}))
...

Tip:有關更多後端配置,例如 Apache、nginx等,請參考 vue-router 官網。

CORS

CORS,即跨源資源共享。當前端程式碼部署在一個域,後端伺服器又是另一個域,此時前端是請求不到後端的介面。

我們在上一小節的基礎上做個實驗:

再建一個後端伺服器:

// vue-example/dist/endServer.js

const express = require('express')
const app = express()
const port = 3010

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

啟動後端伺服器:

vue-example\dist> nodemon .\endServer.js
...
Example app listening at http://localhost:3010

瀏覽器訪問 http://localhost:3010,頁面顯示 Hello World!,說明這個介面沒有問題。

接下來由前端給這個伺服器傳送請求(存在跨域,因為埠一個是 3000,一個是 3010):

// 直接修改 dist/production-sub-path/index.html ,給伺服器傳送請求
...
<script>
    var xhr = new XMLHttpRequest(),
        method = "GET",
        url = "http://localhost:3010/";

    xhr.open(method, url, true);
    xhr.onreadystatechange = function () {
        if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
            console.log(xhr.responseText)
        }
    }
    xhr.send();
</script>

瀏覽器開啟頁面 http://localhost:3000/,控制檯報錯如下:

Access to XMLHttpRequest at 'http://localhost:3010/' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

給伺服器配置一下 CORS:

// endServer.js
...
app.get('/', (req, res) => {
+ res.setHeader('Access-Control-Allow-Origin', '*')
  res.send('Hello World!')
})

再次訪問 http://localhost:3000/,瀏覽器控制檯輸出 hello world!

PWA

如果你使用了 PWA 外掛,那麼應用必須架設在 HTTPS 上,這樣 Service Worker 才能被正確註冊。

Tip:更多介紹請看效能->漸進式網路應用程式

平臺指南
雲開發 CloudBase

雲開發 CloudBase 是一個雲原生一體化的 Serverless 雲平臺,支援靜態網站、容器等多種託管能力,並提供簡便的部署工具 CloudBase Framework) 來一鍵部署應用。

步驟一:安裝雲開發 CloudBase CLI
CloudBase CLI 整合了 CloudBase Framework 的能力,全域性安裝 CloudBase CLI 請執行以下命令:

npm install -g @cloudbase/cli

步驟二:一鍵部署

在專案根目錄執行以下命令部署 Vue CLI 建立的應用,在部署之前可以先開通環境

cloudbase init --without-template
cloudbase framework:deploy

CloudBase CLI 首先跳轉到控制檯進行登入授權,然後將會互動式進行以下步驟:

  • 選擇一個環境,如果沒有可以選擇新建環境
  • 自動檢測專案並確認構建指令碼,輸出目錄、部署雲端路徑等資訊

確認資訊後會立即進行部署,部署完成後,可以獲得一個自動 SSL,CDN 加速的網站應用,你也可以搭配使用 Github Action 來持續部署 Github 上的 Vue 應用。

我們現在就來部署,具體步驟如下:

首先建立一個新專案:

exercise> vue create vue-hello
...

�  Successfully created project vue-hello.
�  Get started with the following commands:

 $ cd vue-hello
 $ npm run serve

接著進入專案,全域性安裝 CloudBase CLI:

vue-hello> npm install -g @cloudbase/cli

執行初始化命令,會自動彈出瀏覽器頁面,而終端會停在 獲取授權中...

vue-hello> cloudbase init --without-template       

 Tip: cloudbase 命令可以簡寫為 tcb

CloudBase CLI 1.8.0
CloudBase Framework 1.8.3

 ⚠️ 此命令將被廢棄,請使用新的命令 => tcb new <appName> [template]

i 你還沒有登入,請在控制檯中授權登入
\ 獲取授權中...

瀏覽器頁面會開啟騰訊雲,掃描登入後,出現 CLI 授權,點選 確認授權,CLI 工具授權成功後,點選 前往開發者首頁

現在授權完成了,終端往下走,筆者選擇hello-cloudbase

\vue-hello> cloudbase init --without-template

 Tip: cloudbase 命令可以簡寫為 tcb

CloudBase CLI 1.8.0
CloudBase Framework 1.8.3

 ⚠️ 此命令將被廢棄,請使用新的命令 => tcb new <appName> [template]

i 你還沒有登入,請在控制檯中授權登入
√ 授權登入成功!
? 選擇關聯環境 ... 
> hello-cloudbase - [hello-cloudbase-8gwug930dc0ce23c:按量計費]
  建立新環境

終端顯示初始化專案成功!

...
√ 選擇關聯環境 · hello-cloudbase - [hello-cloudbase-8gwug930dc0ce23c:按量計費]
√ 初始化專案成功!

i � 開發完成後,執行命令 cloudbase framework:deploy 一鍵部署

根據提示我們輸入 cloudbase framework:deploy 一鍵部署(中途需要我們回答幾個問題):

vue-hello> cloudbase framework:deploy

 Tip: cloudbase 命令可以簡寫為 tcb

CloudBase CLI 1.8.0
CloudBase Framework 1.8.3

 ⚠️  此命令將被廢棄,請使用新的命令 tcb framework deploy [module] 代替

   ______ __                   __ ____                             
  / ____// /____   __  __ ____/ // __ ) ____ _ _____ ___           
 / /    / // __ \ / / / // __  // __  |/ __ `// ___// _ \          
/ /___ / // /_/ // /_/ // /_/ // /_/ // /_/ /(__  )/  __/          
\_________\____/ \__,_/ \__,_//_____/ \__,_//____/ \___/       __  
   / ____/_____ ____ _ ____ ___   ___  _      __ ____   _____ / /__
  / /_   / ___// __ `// __ `__ \ / _ \| | /| / // __ \ / ___// //_/
 / __/  / /   / /_/ // / / / / //  __/| |/ |/ // /_/ // /   / ,<   
/_/    /_/    \__,_//_/ /_/ /_/ \___/ |__/|__/ \____//_/   /_/|_|  
                                                                   

 CloudBase Framework  info     Version v1.8.3
 CloudBase Framework  info     Github: https://github.com/Tencent/cloudbase-framework

 CloudBase Framework  info     EnvId hello-cloudbase-8gwug930dc0ce23c
 CloudBase Framework  info     Region ap-shanghai
? 請輸入應用唯一標識(支援 A-Z a-z 0-9 及 -, 同一環境下不能相同) vue-hello
? 檢測到當前專案包含 Vue.js 專案

  � 構建指令碼 `npm run build`    
  � 本地靜態檔案目錄 `dist`     

  是否需要修改預設配置 No       
? 是否需要儲存當前專案配置到專案中 Yes
 CloudBase Framework  info     Validate config file success.
 CloudBase Framework  info     AppName vue-hello
 CloudBase Framework  info     ◆ install plugins
 CloudBase Framework  info     callHooks 'preDeploy' 

added 587 packages in 38s

30 packages are looking for funding
  run `npm fund` for details
 CloudBase Framework  info     ◆ init: vue...
 CloudBase Framework  info     Website 外掛會自動開啟靜態網頁託管能力,需要當前環境為 [按量計費] 模式
 CloudBase Framework  info     Website 外掛會部署應用資源到當前靜態託管的 / 目錄下
 CloudBase Framework  info     ◆ build: vue...
 CloudBase Framework  info     running 'npm install --prefer-offline --no-audit --progress=false' 

up to date in 1s

85 packages are looking for funding
  run `npm fund` for details
 CloudBase Framework  info     running 'npm run build' 

> vue-hello@0.1.0 build
> vue-cli-service build

 DONE  Compiled successfully in 6859ms                                                           9:10:24 ├F10: PM┤

  File                                 Size                                Gzipped  

  dist\js\chunk-vendors.62acfa1e.js    90.99 KiB                           32.57 KiB
  dist\js\app.93515e7b.js              4.58 KiB                            1.64 KiB 
  dist\css\app.fb0c6e1c.css            0.33 KiB                            0.23 KiB 

  Images and other types of assets omitted.

 DONE  Build complete. The dist directory is ready to be deployed.
 INFO  Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html

 CloudBase Framework  info     ◆ compile: vue...
 CloudBase Framework  info     callHooks 'postCompile' 
正在部署[░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 100% 5.1 s
 CloudBase Framework  info     ◆ deploy: vue...
 CloudBase Framework  info     ◆ 網站部署成功
 CloudBase Framework  info     callHooks 'postDeploy' 
 CloudBase Framework  info     ◆ 應用入口資訊:
◆ 網站入口: https://hello-cloudbase-xxxx.com/
 CloudBase Framework  info     ✨ done

部署成功,直接訪問 https://hello-cloudbase-xxxx.com/ 即可看到我們的專案。

下次修改程式碼,直接執行部署的命令,即可看到更新後的效果。

Tip:其他平臺(比如 Github Pages混合部署Docker (Nginx)...)請直接參考 vue-cli 官網

配置參考

Tip: 配置參考比較簡單,這裡只稍微介紹 lintOnSave,其他配置直接檢視官網。

全域性 CLI 配置

有些針對 @vue/cli 的全域性配置,例如你慣用的包管理器和你本地儲存的 preset,都儲存在 home 目錄下一個名叫 .vuerc 的 JSON 檔案。你可以用編輯器直接編輯這個檔案來更改已儲存的選項。

你也可以使用 vue config 命令來審查或修改全域性的 CLI 配置。

// 進入 home 目錄
vue-hello> cd ~    
PS C:\Users\79764>
79764> cat .vuerc
{
  "useTaobaoRegistry": false,
  "latestVersion": "4.5.13",
  "lastChecked": 1606462273301,
  "presets": {
    "presetNameA": {
      "useConfigFiles": true,
      "plugins": {
        "@vue/cli-plugin-babel": {},
        "@vue/cli-plugin-typescript": {
          "classComponent": true,
          "useTsWithBabel": true
        },
        "@vue/cli-plugin-pwa": {},
        "@vue/cli-plugin-router": {
          "historyMode": false
        },
        "@vue/cli-plugin-vuex": {},
        "@vue/cli-plugin-eslint": {
          "config": "standard",
          "lintOn": [
            "save"
          ]
        },
        "@vue/cli-plugin-unit-mocha": {},
        "@vue/cli-plugin-e2e-cypress": {}
      },
      "vueVersion": "2",
      "cssPreprocessor": "less"
    },
    ...
  }
}

vue.config.js

vue.config.js 是一個可選的配置檔案,如果專案的 (和 package.json 同級的) 根目錄中存在這個檔案,那麼它會被 @vue/cli-service 自動載入。

這個檔案應該匯出一個包含了選項的物件:

// vue.config.js

module.exports = {
  // 選項...
}
lintOnSave
  • Type: boolean | 'warning' | 'default' | 'error'
  • Default: 'default'

是否在開發環境下通過 eslint-loader 在每次儲存時 lint 程式碼。這個值會在 @vue/cli-plugin-eslint 被安裝之後生效。

設定為 true'warning' 時,eslint-loader 會將 lint 錯誤輸出為編譯警告。預設情況下,警告僅僅會被輸出到命令列,且不會使得編譯失敗。

如果你希望讓 lint 錯誤在開發時直接顯示在瀏覽器中,你可以使用 lintOnSave: 'default'。這會強制 eslint-loader 將 lint 錯誤輸出為編譯錯誤,同時也意味著 lint 錯誤將會導致編譯失敗。

設定為 error 將會使得 eslint-loader 把 lint 警告也輸出為編譯錯誤,這意味著 lint 警告將會導致編譯失敗。

或者,你也可以通過設定讓瀏覽器 overlay 同時顯示警告和錯誤:

// vue.config.js
module.exports = {
  devServer: {
    overlay: {
      warnings: true,
      errors: true
    }
  }
}

lintOnSave 是一個 truthy 的值時,eslint-loader 在開發和生產構建下都會被啟用。如果你想要在生產構建時禁用 eslint-loader,你可以用如下配置:

// vue.config.js
module.exports = {
  lintOnSave: process.env.NODE_ENV !== 'production'
}

我們來看一下這個屬性:

通過 npm run serve 啟動我們的專案 vue-example,如果你在 main.js 末尾中增加 console.log(1);,終端會編譯失敗,而且瀏覽器也會出現一個錯誤遮罩:

98% after emitting CopyPlugin

 ERROR  Failed to compile with 1 error                                                                                          9:13:31 ├F10: AM┤
 error  in ./src/main.js

Module Error (from ./node_modules/eslint-loader/index.js):

exercise\vue-example\src\main.js
  11:15  error  Extra semicolon  semi

✖ 1 problem (1 error, 0 warnings)
  1 error and 0 warnings potentially fixable with the `--fix` option.

這個特性對應上文提到的 default

如果你想讓其不中斷編譯,可以這樣:

// vue.config.js
module.exports = {
  lintOnSave: process.env.NODE_ENV !== 'production'
}

重複上面操作,終端則不會在中斷,瀏覽器頁面也沒有錯誤遮罩:

98% after emitting CopyPlugin

 WARNING  Compiled with 1 warning 

其他章節請看:

vue 快速入門 系列

相關文章