vue-cli阅读笔记

阅读思路

看结构

根据这个结构可以看出是基于lerna创建的多包的格式。

其中,.github提供规范,_mock_提供插件测试环境,docs是通过vitepress创建的官方文档(https://cli.vuejs.org/),scripts是使用node来实现package.json当中npm script集成的对应的能力的一些文件。

看package.json

"private": true, // 说明整个大包不是真正引用的方式 "workspaces": [ "packages/@vue/*", "packages/test/*", "packages/vue-cli-version-marker" ], // 这三个包才是真正cli集成的功能

接着看devDependencies,这块是重点,要分别去npm官网去搜这些依赖项的作用。

看官网

Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,提供:

  • 通过 @vue/cli 实现的交互式的项目脚手架。

  • 通过 @vue/cli + @vue/cli-service-global 实现的零配置原型开发。

  • 一个运行时依赖 (@vue/cli-service),该依赖:

    • 可升级;

    • 基于 webpack 构建,并带有合理的默认配置;

    • 可以通过项目内的配置文件进行配置;

    • 可以通过插件进行扩展。

  • 一个丰富的官方插件集合,集成了前端生态中最好的工具。

  • 一套完全图形化的创建和管理 Vue.js 项目的用户界面。

我们要读的是如何用webpack去集成的,插件是怎么去使用的。

插件和preset

看一个项目的好坏通过插件和 preset 是否完善。

如果你查阅一个新创建项目的 package.json,就会发现依赖都是以 @vue/cli-plugin- 开头的。插件可以修改 webpack 的内部配置,也可以向 vue-cli-service 注入命令。在项目创建的过程中,绝大部分列出的特性都是通过插件来实现的。

可以直接去npm官网去搜@vue/cli-plugin-开头的插件。

官网

配置指南

vue.config.js

导出一个json对象,也可以用@vue/cli-service 提供的 defineConfig ,获得更好的类型提示

// vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({ // 选项 })

publicPath

publicPath是部署的路径,默认为'/',也可以设置成相对路径,以便于部署到任意路径。

outputDir

build生成的文件目录,默认是'dist'

assetsDir

js/css/img/fonts等静态资源相对于outputDir的目录

indexPath

生成的index.html的路径,相对于outputDir

filenameHashing

在静态资源文件名上添加hash以便更好的控制缓存,默认为true

pages

一个对象,在multi-page模式下使用,每个page对应一个js入口文件,其值应该是一个对象,对象的 key 是入口的名字,value 是:

  • 一个指定了 entry, template, filename, titlechunks 的对象 (除了 entry 之外都是可选的);

  • 或一个指定其 entry 的字符串。

lintOnSave

是否需要每次保存时lint代码,需要安装@vue/cli-plugin-eslint

常用值

  • true'warning',lint警告输出到命令行,且不会编译失败

  • 'default',lint错误输出为编译错误,编译失败

  • 'error',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'
}

runtimeCompiler

是否使用包含运行时编译器的 Vue 构建版本。设置为 true 后就可以在 Vue 组件中使用 template 选项了,但是这会让应用额外增加 10kb 左右,默认为false

transpileDependencies

接收布尔值或一个包含字符串和正则的数组,转译 node_modules 中的第三方依赖。

productionSourceMap

生产环境的source map,默认为true

crossorigin

生成的HTML中<link rel="stylesheet"><script> 标签的 crossorigin 属性。是一个字符串,参考CORS settings attributes

integrity

在生成的 HTML 中的 <link rel="stylesheet"><script> 标签上启用SRI。如果你构建后的文件是部署在 CDN 上的,启用该选项可以提供额外的安全性。

configureWebpack

如果这个值是一个对象,则会通过webpack-merge合并到最终的配置中。

如果这个值是一个函数,则会接收被解析的配置作为参数。该函数既可以修改配置并不返回任何东西,也可以返回一个被克隆或合并过的配置版本。

chainWebpack

是一个函数,会接收一个基于webpack-chain的 ChainableConfig 实例。允许对内部的 webpack 配置进行更细粒度的修改。

css.requireModuleExtension

默认情况下,只有 *.module.[ext] 结尾的文件才会被视作 CSS Modules 模块。设置为 false 后可以去掉 .module 并将所有的 *.(css|scss|sass|less|styl(us)?)文件视为 CSS Modules 模块。

css.extract

Default: 生产环境下是 true,开发环境下是 false

是否将组件中的 CSS 提取至一个独立的 CSS 文件中 (而不是动态注入到 JavaScript 中的 inline 代码)。

css.sourceMap

是否为 CSS 开启 source map。设置为 true 之后可能会影响构建的性能。

css.loaderOptions

向 CSS 相关的 loader 传递选项。

支持的 loader 有:

  • css-loader

  • postcss-loader

  • sass-loader

  • less-loader

  • stylus-loader

另外,也可以使用 scss 选项,针对 scss 语法进行单独配置(区别于 sass 语法)。

devServer

支持所有 webpack-dev-server。

devServer.proxy

在开发环境下将API请求代理到API服务器,解决跨域问题,可以接受url字符串,也可以是一个对象。

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: '<url>',
        ws: true,
        changeOrigin: true
      },
      '/foo': {
        target: '<other_url>'
      }
    }
  }
}

parallel

  • Type: boolean

  • Default: require('os').cpus().length > 1

是否为 Babel 或 TypeScript 使用 thread-loader

pwa

  • Type: Object

PWA 插件传递选项。

pluginOptions

传递任何第三方插件选项

源码

packages

vue-cli-version-marker

用来标记对应版本号,可以理解成一个空包

test

测试用的包

@vue

这个包是重点

cli

脚手架基本功能

cli-init

项目初始化集成的脚本

cli-plugin-*

cli官方插件

cli-service

基于webpack构建的打包配置项

阅读思路

从vue create命令输入开始的流程入手去读。

  1. 执行vue create指令

  2. git初始化项目

  3. 安装对应的cli插件

  4. 运行generator,创建模版

  5. 安装对应的依赖(node_modules)

  6. 调用安装之后的hooks

cli

根据官网,cli当中集成的是终端里的vue命令

在 packages /@vue/cli 的 package.json 中可以看到我们vue create其实是运行了/@vue/cli/bin 目录下的 vue.js (要根据报错提示安装依赖)。接下来就要看vue.js

vue.js

vue.js 使用 commander 包,这是一个主流的实现脚手架能力的包,并且最终返回的也是创建的 commander 实例。

在loadCommand中,实现了一个查找依赖的功能,具体代码如下:

try {
  return require(moduleName)
} catch (err) {
  if (isNotFoundError(err)) {
    try {
      return require('import-global')(moduleName)
    } catch (err2) {
      if (isNotFoundError(err2)) {
        const installCommand = getGlobalInstallCommand()
        console.log()
        console.log(
          `  Command ${chalk.cyan(`vue ${commandName}`)} requires a global addon to be installed.\n` +
          `  Please run ${chalk.cyan(`${installCommand} ${moduleName}`)} and try again.`
        )
        console.log()
        process.exit(1)
      } else {
        throw err2
      }
    }
  } else {
    throw err
  }
}

可以看到首先使用require(module)尝试引入依赖,如果没有找到就用 import-global 包去找有没有全局依赖,最后报错,提示安装依赖,安装提示通过 getGlobalInstallCommand 文件判断 yarn/pnpm/npm。

接着会有一个checkNodeVersion函数判断node版本,对应的是package.json中的engines。

"engines": { "node": "^12.0.0 || >= 14.0.0" }

然后读取对应参数的输入,进入lib下create文件中。

create.js

create.js就是根据用户的输入进行集成的能力,首先包含一些项目名是否存在,是否合法的判断,并且输出对应的报错和警告。

对于已存在的文件询问是否需要覆盖或合并,并做相应处理,完成后进入lib下的Creator.js中的create函数。

Creator.js

核心就是我们输入create之后的流程。

首先判断是否使用preset和default,如果使用了,那么就调用resolvePreset

然后安装cli集成的核心的service

preset.plugins['@vue/cli-service'] = Object.assign({
  projectName: name
}, preset)
if (cliOptions.bare) {
  preset.plugins['@vue/cli-service'].bare = true
}
// legacy support for router
if (preset.router) {
  preset.plugins['@vue/cli-plugin-router'] = {}
  if (preset.routerHistoryMode) {
    preset.plugins['@vue/cli-plugin-router'].historyMode = true
  }
}
// legacy support for vuex
if (preset.vuex) {
  preset.plugins['@vue/cli-plugin-vuex'] = {}
}

初始化创建:

log(`✨  Creating project in ${chalk.yellow(context)}.`)
this.emit('creation', { event: 'creating' })

获取最新的cli版本

const { latestMinor } = await getVersions()

生成package.json以及devDepenencies

const pkg = {
  name,
  version: '0.1.0',
  private: true,
  devDependencies: {},
  ...resolvePkg(context)
}
const deps = Object.keys(preset.plugins)
deps.forEach(dep => {
  if (preset.plugins[dep]._isPreset) {
    return
  }
  let { version } = preset.plugins[dep]
  if (!version) {
    if (isOfficialPlugin(dep) || dep === '@vue/cli-service' || dep === '@vue/babel-preset-env') {
      version = isTestOrDebug ? `latest` : `~${latestMinor}`
    } else {
      version = 'latest'
    }
  }
  pkg.devDependencies[dep] = version
})

生成pnpm的.npmrc文件

if (packageManager === 'pnpm') {
  const pnpmConfig = hasPnpmVersionOrLater('4.0.0')
    ? 'shamefully-hoist=true\nstrict-peer-dependencies=false\n'
    : 'shamefully-flatten=true\n'
  await writeFileTree(context, {
    '.npmrc': pnpmConfig
  })
}

在安装依赖前要创建git仓库,以便于vue-cli-service能设置git hooks

const shouldInitGit = this.shouldInitGit(cliOptions)
if (shouldInitGit) {
  log(`🗃  Initializing git repository...`)
  this.emit('creation', { event: 'git-init' })
  await run('git init')
}

然后安装cli-plugins

log(`⚙\u{fe0f}  Installing CLI plugins. This might take a while...`)
log()
this.emit('creation', { event: 'plugins-install' })

再往后执行generator,触发create中的invoking-generators事件

log(`🚀  Invoking generators...`)
this.emit('creation', { event: 'invoking-generators' })
const plugins = await this.resolvePlugins(preset.plugins, pkg)
const generator = new Generator(context, {
  pkg,
  plugins,
  afterInvokeCbs,
  afterAnyInvokeCbs
})
await generator.generate({
  extractConfigFiles: preset.useConfigFiles
})

所以接下来要去看generator做了什么。

Generator.js

Generator会根据用户的输入去添加对应的文件,例如babel.

babel: new ConfigTransform({
  file: {
    js: ['babel.config.js']
  }
}),

还有vue.config.js.

const reservedConfigTransforms = {
  vue: new ConfigTransform({
    file: {
      js: ['vue.config.js']
    }
  })
}

其中ConfigTransform是对应的写文件的动作,而写文件的模板集成在插件中的generator下的template当中了

cli-service

CLI 服务是构建于 webpack 和 webpack-dev-server 之上的。它包含了:

  • 加载其它 CLI 插件的核心服务;

  • 一个针对绝大部分应用优化过的内部的 webpack 配置;

  • 项目内部的 vue-cli-service 命令,提供 servebuildinspect 命令。

和cli一样,cli-service指令其实是运行了bin目录下的 vue-cli-service.js

vue-cli-service.js

一样也是先判断node版本。

然后通过Service的实例去执行对应的指令(serve, build, lint)

Service.js

这里是最重要的内容。

首先进行初始化,初始化包括加载对应的环境(解读cross-env对应的配置),根据环境进行对应的配置。

const defaultNodeEnv = (mode === 'production' || mode === 'test')
  ? mode
  : 'development'
if (shouldForceDefaultEnv || process.env.NODE_ENV == null) {
  process.env.NODE_ENV = defaultNodeEnv
}
if (shouldForceDefaultEnv || process.env.BABEL_ENV == null) {
  process.env.BABEL_ENV = defaultNodeEnv
}

接着根据启动的参数配置插件

const builtInPlugins = [
  './commands/serve',
  './commands/build',
  './commands/inspect',
  './commands/help',
  // config plugins are order sensitive
  './config/base',
  './config/assets',
  './config/css',
  './config/prod',
  './config/app'
].map((id) => idToPlugin(id))

这块的内容在command文件夹下,下面以serve为例,这块代码很长。

serve.js

首先接收参数,这块对应的是vue-cli-service中的参数。

api.registerCommand('serve', {
  description: 'start development server',
  usage: 'vue-cli-service serve [options] [entry]',
  options: {
    '--open': `open browser on server start`,
    '--copy': `copy url to clipboard on server start`,
    '--stdin': `close when stdin ends`,
    '--mode': `specify env mode (default: development)`,
    '--host': `specify host (default: ${defaults.host})`,
    '--port': `specify port (default: ${defaults.port})`,
    '--https': `use https (default: ${defaults.https})`,
    '--public': `specify the public network URL for the HMR client`,
    '--skip-plugins': `comma-separated list of plugin names to skip for this run`
  }
}, async function serve (args) {
})

回到Service.js,接下来是接受用户在vue.config.js当中的配置

loadUserOptions () {
  const { fileConfig, fileConfigPath } = loadFileConfig(this.context)
  if (isPromise(fileConfig)) {
    return fileConfig
      .then(mod => mod.default)
      .then(loadedConfig => resolveUserConfig({
        inlineOptions: this.inlineOptions,
        pkgConfig: this.pkg.vue,
        fileConfig: loadedConfig,
        fileConfigPath
      }))
  }
  return resolveUserConfig({
    inlineOptions: this.inlineOptions,
    pkgConfig: this.pkg.vue,
    fileConfig,
    fileConfigPath
  })
}

可以看到使用的是loadFileConfig函数,这块的能力被集成在utils目录当中。

function loadFileConfig (context) {
  let fileConfig, fileConfigPath

  const possibleConfigPaths = [
    process.env.VUE_CLI_SERVICE_CONFIG_PATH,
    './vue.config.js',
    './vue.config.cjs',
    './vue.config.mjs'
  ]
  for (const p of possibleConfigPaths) {
    const resolvedPath = p && path.resolve(context, p)
    if (resolvedPath && fs.existsSync(resolvedPath)) {
      fileConfigPath = resolvedPath
      break
    }
  }

  if (fileConfigPath) {
    const { esm } = isFileEsm.sync(fileConfigPath)

    if (esm) {
      fileConfig = import(pathToFileURL(fileConfigPath))
    } else {
      fileConfig = loadModule(fileConfigPath, context)
    }
  }

  return {
    fileConfig,
    fileConfigPath
  }
}

这块代码不多,就是分commonjs和ESmodule来解析用户在vue.config.js/mjs/cjs当中的配置项,并且进行导入,最后返回fileConfig。

cli-plugin

按照官网的介绍,我们的插件都是以 @vue/cli-plugin- (内建插件) 或 vue-cli-plugin- (社区插件) 开头,列在 package.json 中的,在运行vue-cli-service 命令时,被解析的。

这部分的功能在@vue下的 cli-shared-utils 的 lib 目录中的 pluginResolution.js 当中。

pluginResolution.js
exports.resolvePluginId = id => {
  // already full id
  // e.g. vue-cli-plugin-foo, @vue/cli-plugin-foo, @bar/vue-cli-plugin-foo
  if (pluginRE.test(id)) {
    return id
  }
  // 如果是基本的service
  if (id === '@vue/cli-service') {
    return id
  }
  // 如果是官网的插件
  if (officialPlugins.includes(id)) {
    return `@vue/cli-plugin-${id}`
  }
  // 如果是社区的插件
  if (id.charAt(0) === '@') {
    const scopeMatch = id.match(scopeRE)
    if (scopeMatch) {
      const scope = scopeMatch[0]
      const shortId = id.replace(scopeRE, '')
      return `${scope}${scope === '@vue/' ? `` : `vue-`}cli-plugin-${shortId}`
    }
  }
  // default short
  // e.g. foo
  return `vue-cli-plugin-${id}`
}

官网的插件包括:

const officialPlugins = [
  'babel',
  'e2e-cypress',
  'e2e-nightwatch',
  'e2e-webdriverio',
  'eslint',
  'pwa',
  'router',
  'typescript',
  'unit-jest',
  'unit-mocha',
  'vuex',
  'webpack-4'
]

下面以vuex为例看一下插件是如何集成的

cli-plugin-vuex

前面说了本质上就是将 template 中的模板注入到我们最后生成的项目文件中去。

先看入口文件index.js

index.js
module.exports = (api, options = {}, rootOptions = {}) => {
  api.injectImports(api.entryFile, `import store from './store'`)
  // 判断vue版本,根据版本安装插件的配置项,并且按模板注入
  if (rootOptions.vueVersion === '3') {
    api.transformScript(api.entryFile, require('./injectUseStore'))
    api.extendPackage({
      dependencies: {
        vuex: '^4.0.0'
      }
    })
    api.render('./template-vue3', {})
  } else {
    api.injectRootOptions(api.entryFile, `store`)

    api.extendPackage({
      dependencies: {
        vuex: '^3.6.2'
      }
    })

    api.render('./template', {})
  }

  if (api.invoking && api.hasPlugin('typescript')) {
    /* eslint-disable-next-line node/no-extraneous-require */
    const convertFiles = require('@vue/cli-plugin-typescript/generator/convert')
    convertFiles(api)
  }
}

然后通过vue add 添加到项目中,而 vue add 的能力集成在 cli/lib 下的 add.js 中

add.js

依赖模块的加载

// for `vue add` command in 3.x projects
  const servicePkg = loadModule('@vue/cli-service/package.json', context)
  if (servicePkg && semver.satisfies(servicePkg.version, '3.x')) {
    // special internal "plugins"
    if (/^(@vue\/)?router$/.test(pluginToAdd)) {
      return addRouter(context)
    }
    if (/^(@vue\/)?vuex$/.test(pluginToAdd)) {
      return addVuex(context)
    }
  }

接着根据插件版本进行安装

if (pluginVersion) {
  await pm.add(`${packageName}@${pluginVersion}`)
} else if (isOfficialPlugin(packageName)) {
  const { latestMinor } = await getVersions()
  await pm.add(`${packageName}@~${latestMinor}`)
} else {
  await pm.add(packageName, { tilde: true })
}

安装是通过PackageManager类来实现的。

具体包括运行判断是否需要PeerDepsFix,判断 npm/yarn 并执行,

vue add 和 npm install 的区别

vue add 是 cli 提供的能力,他包括以下的能力

  1. 根据插件判断版本,会进行插件名称的转换

  2. 进行插件安装,包含对包管理工具的判断(根据lock文件来看)

  3. 生成插件中提供的模板

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值