npm启动vue应用开发服务器过程分析

  • Post author:
  • Post category:vue




关于 npm run serve 命令启动vue应用开发环境的过程分析



1、npm run 命令执行时

npm run 命令执行时,会把./node_modules/.bin目录添加到执行环境的PATH变量中。

全局的没有安装的包,在node_modules中安装了,通过npm run 可以调用该命令。

打开./node_modules/.bin 目录,我们看到了很多执行脚本,包括我们认识的

vue-cli-service

image-20220527152903243



2、执行 serve 命令

npm 会从当前目录下的 package.json 文件的 scripts 配置里找命令别名对应的实际脚本命令,我们看到的配置是这样的

"scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "test": "vue-cli-service build --mode test"
},

所以虽然我们敲入的命令是 npm run serve,实际就相当于是执行下面的命令行

node_modules\.bin\vue-cli-service.cmd   serve



3、vue-cli-service.cmd

我们来看下这个文件里的内容,很少的内容,分析下代码, %~dp0 表示当前所执行的脚本文件的路径,这样我们就能大概知道,代码的意思是先判断当前脚本目

录下是否存在node.exe,存在就直接用当前目录下的node.exe来执行一个js,不存在就用环境变量里的node命令来执行js ,

执行的 js 文件位于当前目录下的 @vue\cli-service\bin\vue-cli-service.js

@IF EXIST "%~dp0\node.exe" (
  "%~dp0\node.exe"  "%~dp0\..\@vue\cli-service\bin\vue-cli-service.js" %*
) ELSE (
  @SETLOCAL
  @SET PATHEXT=%PATHEXT:;.JS;=;%
  node  "%~dp0\..\@vue\cli-service\bin\vue-cli-service.js" %*
)

那么我们就知道了,其实最终执行的脚本命令其实就是一个node执行js的命令

node vue-cli-service.js  serve



4、vue-cli-service.js

接下来,我们来分析下 vue-cli-service.js 这个文件的代码是如何执行的。

#!/usr/bin/env node

// 加载 semver 
const semver = require('semver')
// 加载 @vue/cli-shared-utils、
const { error } = require('@vue/cli-shared-utils')
// 加载 @vue/cli-service/package.json里配置要求的node版本号描述 (文件中配置的是 "node": ">=8")
const requiredVersion = require('../package.json').engines.node

// 判断当前使用的node版本号是否符合 @vue/cli-service/package.json里配置的要求 ,不符合就中断执行,并给出错误提示信息
if (!semver.satisfies(process.version, requiredVersion)) {
  error(
    `You are using Node ${process.version}, but vue-cli-service ` +
    `requires Node ${requiredVersion}.\nPlease upgrade your Node version.`
  )
  process.exit(1)
}

// 加载 Service
const Service = require('../lib/Service')


console.log('context:', process.env.VUE_CLI_CONTEXT || process.cwd()) // 输出为:项目主目录如 D:\wamp64\www\SUMEC-HT-YF\CODE\YF-V

// 实例化一个 Service,传入环境变量文件内容
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())

// 获取命令行执行携带的参数, process.argv 这个是命令行数组,调试可知道其值为
/*
[ 'D:\\Program Files\\nodejs\\node.exe',
  'D:\\wamp64\\www\\SUMEC-HT-YF\\CODE\\YF-V\\node_modules\\@vue\\cli-service\\bin\\vue-cli-service.js',
  'serve' ]
*/
const rawArgv = process.argv.slice(2)  

// minimist 是一个用来解析命令行选项的库,这里没有深入了解minimist
const args = require('minimist')(rawArgv, {
  boolean: [
    // build
    'modern',
    'report',
    'report-json',
    'watch',
    // serve
    'open',
    'copy',
    'https',
    // inspect
    'verbose'
  ]
})
const command = args._[0]


console.log('rawArgv:', rawArgv)  // 输出为:rawArgv: [ 'serve' ]
console.log('args:', args)
/* 输出为:
args: { _: [ 'serve' ],
  modern: false,
  report: false,
  'report-json': false,
  watch: false,
  open: false,
  copy: false,
  https: false,
  verbose: false }
  */

// 运行服务  service.run('serve', args, [ 'serve' ])
service.run(command, args, rawArgv).catch(err => {
  error(err)
  process.exit(1)
})



5、Service.js

我们现在来分析下这个Service对象的代码,从上一步我们看到了对应的实例化代码,和执行的service.run() 方法



5.1、实例化 Service 分析

process.cwd() 是指当前node命令执行时所在的文件夹目录,我们后面的分析就以在目录 D:\wamp64\www\SUMEC-HT-YF\CODE\YF-V 下面执行为例

// 实例化一个 Service,传入环境变量文件内容
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())

// 相当于 const service = new Service('D:\wamp64\www\SUMEC-HT-YF\CODE\YF-V') 

下面我们看一下service实例化方法中的代码执行

constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
    
    ......
    
    // 这一句是取出 D:\wamp64\www\SUMEC-HT-YF\CODE\YF-V\package.json配置内容
    this.pkg = this.resolvePkg(pkg)

    // 这一步是把基础构建用的内部插件 和 this.pkg 里面的开发依耐、生产依耐中的@vue/cli-plugin-* 插件的加载初始化
	this.plugins = this.resolvePlugins(plugins, useBuiltIn)
    
    
    //解析用于每个命令的默认模式
    //{ serve: 'development',
    // build: 'production',
    // inspect: 'development' }
    this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
      return Object.assign(modes, defaultModes)
    }, {})
    
    ......

} 



1、resolvePkg

this.resolvePkg(“D:\wamp64\www\SUMEC-HT-YF\CODE\YF-V\package.json”),可以看出这里就是读取json文件,将文件内容读取到对象中,并返回;

resolvePkg (inlinePkg, context = this.context) {
	 
    if (inlinePkg) {
      return inlinePkg
    } else if (fs.existsSync(path.join(context, 'package.json'))) {
			
      const pkg = readPkg.sync({ cwd: context })
      // 这边如果package.json有 vuePlugins 并且配置了 vuePlugins.resolveFrom,就会递归读取
      if (pkg.vuePlugins && pkg.vuePlugins.resolveFrom) {
        this.pkgContext = path.resolve(context, pkg.vuePlugins.resolveFrom)
        return this.resolvePkg(null, this.pkgContext)
      }
      return pkg
    } else {
      return {}
    }
  }



2、resolvePlugins

分析下 this.resolvePlugins(),可以知道该方法主要就是加载相关的插件

resolvePlugins (inlinePlugins, useBuiltIn) {
    const idToPlugin = id => ({
      id: id.replace(/^.\//, 'built-in:'),
      apply: require(id)
    })

    let plugins
	
    // 1、加载下面的基础构建插件到 builtInPlugins 里
    const builtInPlugins = [
      './commands/serve',
      './commands/build',
      './commands/inspect',
      './commands/help',
      './config/base',
      './config/css',
      './config/dev',
      './config/prod',
      './config/app'
    ].map(idToPlugin)
	
	
    if (inlinePlugins) {
      plugins = useBuiltIn !== false
        ? builtInPlugins.concat(inlinePlugins)
        : inlinePlugins
    } else {
      // 2、将package.json中配置的 devDependencies 和 dependencies里的 @vue/cli-plugin-* 插件放到 projectPlugins 里
      const projectPlugins = Object.keys(this.pkg.devDependencies || {})
        .concat(Object.keys(this.pkg.dependencies || {}))
        .filter(isPlugin)
        .map(id => {
          if (
            this.pkg.optionalDependencies &&
            id in this.pkg.optionalDependencies
          ) {
            let apply = () => {}
            try {
               // 根据插件ID加载插件模块js赋值给 apply  
              apply = require(id)
            } catch (e) {
              warn(`Optional dependency ${id} is not installed.`)
            }
            return { id, apply }
          } else {
            return idToPlugin(id)
          }
        }) 
      // 3、builtInPlugins + projectPlugins 合并到 plugins
      plugins = builtInPlugins.concat(projectPlugins)
    } 

    // 4、加载package.json中配置的 vuePlugins 到 plugins中
    if (this.pkg.vuePlugins && this.pkg.vuePlugins.service) { 
		
      const files = this.pkg.vuePlugins.service
      if (!Array.isArray(files)) {
        throw new Error(`Invalid type for option 'vuePlugins.service', expected 'array' but got ${typeof files}.`)
      }
      plugins = plugins.concat(files.map(file => ({
        id: `local:${file}`,
        apply: loadModule(file, this.pkgContext)
      })))
    } 

    return plugins
 }

this.resolvePlugins() 执行后,this.plugins 的值是这样的:

[ { id: 'built-in:commands/serve', apply: { [Function] defaultModes: [Object] } },
  { id: 'built-in:commands/build', apply: { [Function] defaultModes: [Object] } },
  { id: 'built-in:commands/inspect', apply: { [Function] defaultModes: [Object] } },
  { id: 'built-in:commands/help', apply: [Function] },
  { id: 'built-in:config/base', apply: [Function] },
  { id: 'built-in:config/css', apply: [Function] },
  { id: 'built-in:config/dev', apply: [Function] },
  { id: 'built-in:config/prod', apply: [Function] },
  { id: 'built-in:config/app', apply: [Function] },
  { id: '@vue/cli-plugin-babel', apply: [Function] } ]

实际跟踪可以知道 id = ‘built-in:commands/serve’ 的对应 apply 是 ./commands/serve.js 插件模块,serve.js里的定义如下

....
module.exports = (api, options) => {
 
    ......
}
....
module.exports.defaultModes = {
  serve: 'development'
}

同样的 ‘built-in:commands/build’ ,对应的 apply 是 ./commands/build/index.js 插件模块 ,index.js里的定义如下

....
module.exports = (api, options) => {
 
    ......
}
....
module.exports.defaultModes = {
  serve: 'production'
}

其他的也都一样,不再细说了。



3、modes

从第二步得出的 plugins ,把已加载配置的插件中的命令默认模式取到modes里

//解析用于每个命令的默认模式
    //{ serve: 'development',
    // build: 'production',
    // inspect: 'development' }
    this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
      return Object.assign(modes, defaultModes)
    }, {})

执行代码后,this.modes的值是这样的:

{ serve: 'development',
  build: 'production',
  inspect: 'development' }



5.2、service.run 分析

运行方法中,主要执行了四个步骤

async run (name, args = {}, rawArgv = []) {
    
    // 1、得到运行模式(先从命令行参数中获取,所以使用命令行 vue-cli-service build --mode test 执行时,这里的得到的mode就是test)
    const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])

    // 2、根据运行模式加载环境变量参数,加载vue.config,应用插件 
    this.init(mode)

    args._ = args._ || []
    
    // 3、根据命令模式name得到对应的执行命令项
    let command = this.commands[name]
    if (!command && name) {
      error(`command "${name}" does not exist.`)
      process.exit(1)
    }
    if (!command || args.help || args.h) {
      command = this.commands.help
    } else {
      args._.shift() // remove command itself
      rawArgv.shift()
    }
    // 4、取出命令项中的执行方法函数配置
    const { fn } = command
    return fn(args, rawArgv)
  }



1、mode

运行模式

this.modes

中包含的信息是这样的 :

{ serve: 'development',
  build: 'production',
  inspect: 'development' } 

所以,我们使用npm run serve 执行时,这里得到的 mode 就是 = this.modes[‘serve’ ] = ‘development’

//1、得到运行模式(先从命令行参数中获取,所以使用命令行 vue-cli-service build --mode test 执行时,这里的得到的mode就是test)
const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])



2、init


this.init(mode)

这个初始化方法会加载模式对应的环境变量配置文件,并且加载 vue.config 文件配置信息,还会把相关的插件都初始化加载起来

init (mode = process.env.VUE_CLI_MODE) {
    if (this.initialized) {
      return
    }
    this.initialized = true
    this.mode = mode

    // 1、加载环境变量配置文件 .env.development 
    if (mode) {
      this.loadEnv(mode)
    }
    // 2、加载环境变量基础配置文件 .env.local 
    this.loadEnv()

    // 3、加载用户配置。读取vue.config.js文件内部的配置
    const userOptions = this.loadUserOptions()
    this.projectOptions = defaultsDeep(userOptions, defaults())

    debug('vue:project-config')(this.projectOptions)

    // 4、遍历插件集合,应用插件,这一步执行后 this.commands的值就有了
    this.plugins.forEach(({ id, apply }) => {
      apply(new PluginAPI(id, this), this.projectOptions)
    })

    // apply webpack configs from project config file ? 这边还未进一步深入
    if (this.projectOptions.chainWebpack) {
      this.webpackChainFns.push(this.projectOptions.chainWebpack)
    }
    if (this.projectOptions.configureWebpack) {
      this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
    }
}



3、commands

this.init(mode) 执行完后,然后

this.commands

的值是这样的:

{
    serve: {
        fn: [AsyncFunction: serve],
        opts: {
            description: 'start development server',
            usage: 'vue-cli-service serve [options] [entry]',
            options: [Object]
        }
    },
    build: {
        fn: [AsyncFunction],
        opts: {
            description: 'build for production',
            usage: 'vue-cli-service build [options] [entry|pattern]',
            options: [Object]
        }
    },
    inspect: {
        fn: [Function],
        opts: {
            description: 'inspect internal webpack config',
            usage: 'vue-cli-service inspect [options] [...paths]',
            options: [Object]
        }
    },
    help: {
        fn: [Function],
        opts: {}
    }
}

所我们再来看run里的代码 ,就知道this.commands[‘serve’]的值

// 3、根据命令模式name得到对应的执行命令项
    let command = this.commands[name]
    
    .....
    
    if (!command || args.help || args.h) {
      command = this.commands.help
    } else {
      args._.shift() // remove command itself
      rawArgv.shift()
    }
    // 4、取出命令项中的执行方法函数配置
    const { fn } = command
    return fn(args, rawArgv)

command的值就等于:

{
    fn: [AsyncFunction: serve],
    opts: {
        description: 'start development server',
        usage: 'vue-cli-service serve [options] [entry]',
        options: [Object]
    }
}

那么 this.commands.serve.fn 对应的方法是哪个呢?



4、command.fn()

要想知道this.commands.serve.fn 执行的是哪段逻辑代码,首先我们要知道commands对象是怎么生成的;

我们上面说了 commands 是在 init 方法中的下面代码执行后,才生成值的,那我们就分析这段代码吧;

this.plugins.forEach(({ id, apply }) => {
    apply(new PluginAPI(id, this), this.projectOptions)
})

this.plugins 的值我们上面也已经列出来了;apply() 对应的我们也都知道就是 serve.js、build/index.js 里的 module.exports = (api, options) => {} 方法。

new PluginAPI(id, this) 这是实例了一个插件API对象,this.projectOptions 这是项目配置信息;

下面,我们就主要看下 serve.js、build/index.js 里的 module.exports = (api, options) => {} 方法执行了什么,方法里面的代码很长,但仔细一看,其实就是执行

了一句 api.registerCommand(‘serve’, { … }, async function serve (args) { … } )

registerCommand ,看意思应该是注册命令的方法,这里的api,就是前面 new PluginAPI(id, this) 实例化出来的;

module.exports = (api, options) => {
  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`,
      '--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`
    }
      
      ........
      
  }, 

进入 api.registerCommand() 方法定义代码里,我们终于看到了 this.service.commands 的赋值(this.service 是实例化 PluginAPI 传进来的 service,

就是当前run的service)。

registerCommand (name, opts, fn) {
    if (typeof opts === 'function') {
      fn = opts
      opts = null
    }
    this.service.commands[name] = { fn, opts: opts || {}}
  }

这个fn 我们也知道了就是 api.registerCommand(‘serve’, { … }, async function serve (args) { … } ) 里的 async function serve (args) { … } ;



5、serve (args)

serve (args) 这个方法很长,服务和插件的运行都在这里执行;




未完…



版权声明:本文为qzonemen原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。