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

其他章节请看:

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 快速入门 系列

posted @ 2021-08-17 21:51  彭加李  阅读(1187)  评论(1编辑  收藏  举报