这一章我们来学习 vue-cli,目前 vue-cli 的最新版本为 3.0.1,在 3.x 版本的时候 vue-cli 改名为 @vue/cli,要求的运行环境为 node 8.9 以上。所以本章会以最新版本 3.0.1 来为大家讲解,也会以 @vue/cli 来对这个脚手架进行称呼,如果本机 node 版本在 8.9 以下那么就需要大家升级一下版本了。关于 node 和 npm 的升级方法,大家可以点击这里

@vue/cli

老规矩,我们先来明确一下 @vue/cli 是干嘛的。

Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,它主要提供了以下功能:

  1. 通过 @vue/cli 搭建交互式的项目脚手架。
  2. 通过 @vue/cli + @vue/cli-service-global 快速开始零配置原型开发。

而对于 @vue/cli 的主要应用就是第一个——通过 @vue/cli 搭建交互式的项目脚手架,这也是本章的重点内容。

关于 @vue/cli 的更多描述,我们可以点击这里查看

实战项目构建

我们将使用 @vue/cli+webpack 来构建实战项目 AutoChat。

@vue/cli 3.0+ 的环境下如果我们想要使用 webpack 作为模板来构建一个项目框架的话,安装一个桥接工具 @vue/cli-init,用以执行旧版的 vue init 指令:

npm install -g @vue/cli-init

我们可以执行上面的命令来安装 @vue/cli-init,然后通过:

vue init <template-name> <project-name>

指令来构建项目,这里执行:

vue init webpack auto-chat

指令执行完成之后 @vue/cli 会进行一系列的问答交互用来帮助进行最初的项目构建:

Project name (auto-chat) :

确定我们的项目名称是否为 AutoChat ,我们直接回车就可以

Project description:

项目介绍

Author (LGD_Sunday <*******@**.com>) :

确定开发者

Vue build (Use arrow keys)
❯ Runtime + Compiler: recommended for most users 
  Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specific H
TML) are ONLY allowed in .vue files - render functions are required elsewhere 

确定一下构建版本,这里提供了两个选择项 Runtime + Compiler 和 Runtime-only,其中 Runtime-only只 包含一个编译功能,仅能在 .vue 文件中使用,所以一般都会选择 Runtime + Compiler 编译器+运行时

Install vue-router? (Y/n) 

是否需要 vue-router,这里选需要

Use ESLint to lint your code? (Y/n) 

是否需要 ESLint 来规范代码,选择需要

❯ Standard (https://github.com/standard/standard) 
  Airbnb (https://github.com/airbnb/javascript) 
  none (configure it yourself) 

然后让你选择 ESLint 的规范,这里选择 Standard 标准就可以

 Set up unit tests (Y/n) 

是否需要单元测试,选需要

❯ Jest 
  Karma and Mocha 
  none (configure it yourself) 

选择使用的测试框架,这里大家可以看自己的喜好,我这里选择的是 Jest。

Setup e2e tests with Nightwatch?

是否需要 e2e 测试,也选是

Should we run `npm install` for you after the project has been created? (recom
mended) (Use arrow keys)
❯ Yes, use NPM 
  Yes, use Yarn 
  No, I will handle that myself 

最后就是使用的包管理工具,这里使用的是 npm。选择完成之后,npm 就会去下载所有依赖,这里根据网速不同,稍等就可以。

依赖下载完成之后,会得到这样一个结构的项目:

├── .babelrc // babel 配置文件
├── .editorconfig // EditorConfig 配置文件
├── .eslintignore // ESLint 忽略文件配置
├── .eslintrc.js // ESLint 配置文件
├── .gitignore // git 忽略配置
├── .postcssrc.js // postcss 配置文件
├── README.md // readme 
├── build // webpack的主要配置都在这里
├── config // 工程的主要配置都在这里
├── index.html // 启动的html
├── node_modules // npm下载的文件
├── package-lock.json // npm具体下载的依赖库文件
├── package.json // npm 配置文件
├── src // 主要代码的文件夹
├── static // 静态文件存放地点,默认在打包时不参与优化
└── test // 测试内容

先来大概看一下这个项目结构,首先:@vue/cli 默认集成了一些插件,并生成了对应的配置文件,比如 .babelrc。还有在创建项目时选择加入的一些插件,比如 esLint、@vue/cli 也帮助生成了对应的配置 .eslintignore.eslintrc.js。包括 git 上的忽略文件 .gitignore,和 postcss 的配置文件 .postcssrc.js

package.json

我们来分析一下 package.json 这个文件,在 auto-chat 被创建之后,系统会给出这么一段提示:

 cd auto-chat
  npm run dev

意思为 cd 到当前的项目中,然后通过 npm run dev 来运行当前的项目。其中 dev 这个指令就被定义在 package.json 中:

"scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "start": "npm run dev",
    "unit": "jest --config test/unit/jest.conf.js --coverage",
    "e2e": "node test/e2e/runner.js",
    "test": "npm run unit && npm run e2e",
    "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs",
    "build": "node build/build.js"
  },

在 scripts 属性中 @vue/cli 指定了大量的可执行命令,我们可以通过 npm run ** 来执行。

比如当我们执行 npm run dev 时,实际执行的命令为 webpack-dev-server --inline --progress --config build/webpack.dev.conf.js,指定运行位于 build 中的 webpack.dev.conf.js 文件;

当我们执行 npm run start 时,实际执行的命令为 npm run dev

我们也可以通过 npm run build 来打包代码,打包之后的代码为实际生产环境中使用的代码,当执行 npm run build 指令时,webpack 会去优化 src 文件夹中的代码和资源,最终打包代码会被放入到 ./dist 文件夹下。稍后分析 webpack 配置的时候会进一步讲解。

dependencies 属性:

"dependencies": {
    "vue": "^2.5.2",
    "vue-router": "^3.0.1"
  }

dependencies 属性中的内容为我们生产环境中需要的依赖,也就是通过 npm run build 打包之后的生产代码中所依赖的内容。比如这里的 vue 和 vue-router 会被一同打包到生产代码中。

devDependencies 属性:

"devDependencies": {
    ...
  },

这里的内容比较多我们就不贴出来了,大家看一下自己的项目。devDependencies 属性中的内容为:我们开发时用的,不会被部署到生产环境的依赖。比如用于处理 css 浏览器兼容性的 autoprefixer 就不需要被打包到生产环境中。

然后是 engines,它表示该模块运行的平台

"engines": {
    "node": ">= 6.0.0",
    "npm": ">= 3.0.0"
  },

最后是 browserslist,它表示这个项目的浏览器兼容情况,其中的 > 1%last 2 versionsnot ie <= 8 都是查询参数。

"browserslist": [
    "> 1%", // 兼容所有市场占有率大于1%的浏览器
    "last 2 versions", // 兼容所有位于最近2个版本的浏览器
    "not ie <= 8" // 不兼容 IE8 及以下
  ]

build

我们来看一下 build 文件夹。

build 文件夹中定义了 webpack 的基础配置,我们已经知道当运行 npm run dev 时它会去执行 webpack-dev-server --inline --progress --config build/webpack.dev.conf.js,其中的 webpack.dev.conf.js 文件就是 dev 环境下的 webpack 的配置文件。

我们先来分析一下 webpack.dev.conf.js 中的代码。

... 
module.exports = new Promise((resolve, reject) => {
 // 确定dev使用的端口,其中process.env.PORT为我们在命令行中指定的端口,config.dev.port为默认的设定端口
  portfinder.basePort = process.env.PORT || config.dev.port
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
      // publish the new Port, necessary for e2e tests
      process.env.PORT = port
      // add port to devServer config
      devWebpackConfig.devServer.port = port

      // Add FriendlyErrorsPlugin
      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
        compilationSuccessInfo: {
          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
        },
        onErrors: config.dev.notifyOnErrors
        ? utils.createNotifierCallback()
        : undefined
      }))

      resolve(devWebpackConfig)
    }
  })
})

这是 webpack.dev.conf.js 中的部分代码。可以看到它首先使用了 CMD 的方式返回了一个 Promise,在 Promise 中它首先进行了一个 devServer 执行端口的判断,当 err 为 false 时确定了 devServer 的一系列配置。

代码比较简单上面也都有注释我们就不再说了,最后执行了 resolve(devWebpackConfig),其中的 devWebpackConfig 就是 webpack 中真正的配置文件。看一下 devWebpackConfig 的代码:

const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')


const devWebpackConfig = merge(baseWebpackConfig, {
  module: {
      ...
  },
  devtool: config.dev.devtool,
  devServer: {
      ...
  },
  plugins: [
      ...
  ]
})

首先它执行了一个 merge 方法把 baseWebpackConfig 和后面的 object 进行了合并,合并成同一个 object。merge 函数是 webpack-merge 提供的一个合并函数,用于数组对象的合并,并返回一个新的对象。

其中第一个参数 baseWebpackConfig 就是 webpack 的基础配置描述共同的特性,通过 merge 函数同时作用于开发模式 prod 和生产模式 dev。我们直接看一下 webpack.base.conf 中的配置代码:

module.exports = {
  context: path.resolve(__dirname, '../'),
  entry: {
    app: './src/main.js'
  },
  output: {
      ...
  },
  resolve: {
      ...
  },
  module: {
      ...
  },
  node: {
      ...
  }
}

从上面的代码中可以看出他分成了 6 个模块,我们一个个来看:

context

首先是 context,它描述的是 webpack 运行的基础路径,这里指向当前路径的上一级,也就是我们整个项目的根路径。

entry

这个大家应该不陌生了,它描述了入口文件 ./src/main.js

output

output: {
    path: config.build.assetsRoot,
    filename: '[name].js',
    publicPath: process.env.NODE_ENV === 'production'
      ? config.build.assetsPublicPath
      : config.dev.assetsPublicPath
  },

path,它指向了 config.build.assetsRoot,该属性定义在 /config/index.js 中,其实就是 ./dist。filename 默认为当前的入口名,publicPath 此选项指定在浏览器中所引用的「此输出目录对应的公开 URL」。我们可以点击这里来查看 webpack 对它的详细描述。

resolve

resolve: {
    // 自动解析确定的扩展,能够使用户在引入模块时不带扩展.
    extensions: ['.js', '.vue', '.json'],
    // 设置的别名
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
    }
  },

resolve 表示模块应该如何被解析。因为这些配置在官网都有很详细的解释,所以就不再一个个去解释了。

module

 module: {
    rules: [
      ...
  },

module的东西就比较多了,它定义了项目中使用的所有 loader,大家主要看一下这里都用到了哪些 loader,用以处理哪些文件。比如用以处理 .vue 的 vue-loader,和用以做 eslint 验证的 eslint-loader 等。

node

node: {

    setImmediate: false,
    dgram: 'empty',
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
    child_process: 'empty'
  }

关于Node我们在上一章没有讲到,它表示的是本地的 node 环境配置,其中每个属性都是 Node.js 全局变量或模块的名称,比如 setImmediate、dgram 等。其值共有四种选项:true、mock、empty、false,关于这四个选项的含义我们可以在这里查看。

这就是 webpack.base.conf.js 中的所有配置,没有什么太特殊的地方,中规中矩,根据 webpack 官网中的配置一栏,就可以全部理清楚了。

继续回头来看 webpack.dev.conf.js,它主要提供了四个模块 module、devtool、devServer、plugins。

webpack.dev.conf.js 中的 module 指向了 ./utils.js 中的 styleLoaders:

module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
  },
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
// 默认环境配置下options = { sourceMap: true, usePostCSS: true }
  const output = []
  const loaders = exports.cssLoaders(options)

  for (const extension in loaders) {
    const loader = loaders[extension]
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }

  return output
}

它的代码也比较简单,首先通过 cssLoaders 方法来获取所有的非 .vue 文件的独立样式文件(比如 .css、.less、stylus),然后通过一个循环为每一种文件类型设置了 loader,然后把生成的数组 output 返回了出去。

devtool

然后是 devtool

devtool: config.dev.devtool,

它指向 ./config/index.js

devtool: 'cheap-module-eval-source-map',

主要用于控制是否生成,以及如何生成 source map。那么什么是 source map?

先做一个小的实验,首先在 ./src/components/HelloWorld.vue 中实现 created 函数,并在其中添加一个不存在的方法调用,比如在这里调用一个不存在的 abc 方法:

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  created() {
    abc('这是一个错误的方法调用');
  },
}
</script>

在项目中执行 npm run dev 启动 devServer,看一下游览器中的报错提示:

它指明 abc 方法没有被定义,并且指向了 HelloWorld.vue 这个文件。点击错误,会跳转到具体的错误地址。

注释掉 devtool: config.dev.devtool,再重新执行 npm run dev 看一下报错情况:

看出什么区别了吗?我们的错误代码并没有被指出对应的代码地址。这就是 SourceMap 的作用:

SourceMap 就是一个信息文件(.map),里面储存着位置信息。有了它,出错的时候,出错工具将直接显示原始代码(.vue 中的代码),而不是转换后的代码。

devtool 提供了多种的配置,我们可以在官网中详细查看。

devServer

devServer 是开发阶段使用的服务,也就是我们执行 npm run dev 时启动的服务,它提供了多种的配置,比如 host、port 等,这些配置的具体定义都被定义在 /config/index.js 中。

最后就是 plugins,这里提供了开发环境中使用到的多个 plugins:

plugins: [
    // 允许在编译时(compile time)配置的全局常量
    new webpack.DefinePlugin({
      'process.env': require('../config/dev.env')
    }),
    // 启用模块热替换(Enable Hot Module Replacement - HMR)
    new webpack.HotModuleReplacementPlugin(),
    // 当开启 HMR 的时候使用该插件会显示模块的相对路径
    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
    // 在输出阶段时,遇到编译错误跳过
    new webpack.NoEmitOnErrorsPlugin(),
    // https://github.com/ampedandwired/html-webpack-plugin
    // 简单创建 HTML 文件,用于服务器访问
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    }),
    // copy custom static assets
    // 将单个文件或整个目录复制到构建目录
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.dev.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
  ]

我们可以在这里查看各个 plugins 的详细描述。

然后就是 webpack.prod.conf.js,它的内部配置结构与 webpack.dev.conf.js 相差无几,最大的不同就在于 plugins 插件的不同使用。我们在这里就不在和大家进行一遍描述了。

config

config 文件夹中为各种配置的详细定义,在 index.js 中:

module.exports = {
  dev: {
      ...
  },

  build: {
      ...
  }
}

分成了两部分,dev 的配置和 build 的配置,里面定义的都是一些打包的路径,启动的端口号等等。

src

src 文件夹里就是代码所在的地址,比如资源文件夹 assets,(注意这里的 assets 与根目录下的 static 的区别是:根目录中的 static 默认不参与 build 的优化。),组件文件夹 components、路由 router、最初的组件 APP.vue 和入口文件 main.js。

先看一下 main.js 中的代码:

import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

看 new Vue 这一部分,它指定了 el 属性为 #app,这里的 #app 指向的地点为根目录中的 index.html 中的 <div id="app"></div>,然后指定了它的 components 为 App,也就是说默认情况下我们的整个项目都是在组件 App 中去进行开发的。

然后看一下 ./App.vue 中的代码:

<template>
  <div id="app">
    <img src="./assets/logo.png">
    <router-view/>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

在 App.vue 中,它通过 <router-view/> 指定了一个路由的出口,我们看一下路由的配置,在 ./router/index.js 中:

import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    }
  ]
})

它定义了访问根目录时展示的 component 为 HelloWorld。也就是说当使用 npm run dev 启动了整个项目的时候,它展示的层次结构为:

总结

在这一章我们通过 @vue/cli 进行项目的初始化,并且认识了项目的基本结构和基础配置,那么在下一章我们就会在这个基础之上进行实际项目的开发,也就是自动聊天系统 AutoChat。

发表评论

电子邮件地址不会被公开。 必填项已用*标注