1、我问了自己一个问题?

  • 为什么命令行输入 vue-cli-service serve 我们的vue项目就能通过http协议访问?

2、带着这个问题,去看看源码,寻找答案

2.1、vue-cli-service源码
  • 代码片段

    const Service = require('../lib/Service')
    const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
    
    const rawArgv = process.argv.slice(2)
    const args = require('minimist')(rawArgv, {
      boolean: [
        // build
        'modern',
        'report',
        'report-json',
        'inline-vue',
        'watch',
        // serve
        'open',
        'copy',
        'https',
        // inspect
        'verbose'
      ]
    })
    const command = args._[0]
    
    service.run(command, args, rawArgv).catch(err => {
      error(err)
      process.exit(1)
    })
    
  • 解读

    • 1)、引入Service类,并实例一个service 对象

    • 2)、解析命令行输入参数

      • 当我们 执行 vue-cli-service serve 命令,打印一下参数看看
      • 在这里插入图片描述
    • 3)、调用service 对象的run方法,传入解析的命令参数

  • 到这里肯定不行,我们继续去看 Service类的run 方法的源码

2.2、Service.run源码
  • 代码片段

    async run (name, args = {}, rawArgv = []) {
        // resolve mode
        // prioritize inline --mode
        // fallback to resolved default modes from plugins or development if --watch is defined
        const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
    
        // --skip-plugins arg may have plugins that should be skipped during init()
        this.setPluginsToSkip(args)
    
        // load env variables, load user config, apply plugins
        this.init(mode)
    
        args._ = args._ || []
        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()
        }
        const { fn } = command
        return fn(args, rawArgv)
      }
    
  • 解读

    • 1)、解析模式
    • 2)、设置需要在执行 init 期间,需要跳过处理的插件
    • 3)、加载环境变量、加载用户配置、应用插件
    • 4)、根据name选择需要执行command(我也没想好用啥名字称呼,就叫他command)
    • 5)、处理异常
    • 6)、执行command
  • 看到这里,最后又执行了command,command干了啥呢?

2.3、寻找command
  • 代码片段0(./node_modules/@vue/cli-service/lib/Service.js)

    
        this.commands = {}
        // If there are inline plugins, they will be used instead of those
        // found in package.json.
        // When useBuiltIn === false, built-in plugins are disabled. This is mostly
        // for testing.
        this.plugins = this.resolvePlugins(plugins, useBuiltIn)
    
  • 解读

    • 1)、在service类内声明了commands存储command
    • 2)、在service类内声明了plugins存储plugins插件
  • 代码片段1 (./node_modules/@vue/cli-service/lib/Service.js)

    resolvePlugins (inlinePlugins, useBuiltIn) {
        const idToPlugin = id => ({
          id: id.replace(/^.\//, 'built-in:'),
          apply: require(id)
        })
    
        let plugins
    
        const builtInPlugins = [
          './commands/serve',
          './commands/build',
          './commands/inspect',
          './commands/help',
          // config plugins are order sensitive
          './config/base',
          './config/css',
          './config/prod',
          './config/app'
        ].map(idToPlugin)
    
        if (inlinePlugins) {
          plugins = useBuiltIn !== false
            ? builtInPlugins.concat(inlinePlugins)
            : inlinePlugins
        } else {
          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 {
                  apply = require(id)
                } catch (e) {
                  warn(`Optional dependency ${id} is not installed.`)
                }
    
                return { id, apply }
              } else {
                return idToPlugin(id)
              }
            })
          plugins = builtInPlugins.concat(projectPlugins)
        }
    
        // Local 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
      }
    
  • 解读

    • 1)、解析内置插件
    • 2)、如果有行内插件,只需解析行内和内置插件行内插件(内置插件包含:内置命令和其他插件)
    • 3)、如果没有行内插件,解析 package.json 中和内置的插件
    • 4)、解析本地的插件
    • 5)、上一步中,将解析的插件存储在plugins上
  • 代码片段2

    init (mode = process.env.VUE_CLI_MODE) {
        // apply plugins.
        this.plugins.forEach(({ id, apply }) => {
          if (this.pluginsToSkip.has(id)) return
          apply(new PluginAPI(id, this), this.projectOptions)
        })
      }
    
  • 解读 (init中部分代码)

    • 1)、使用插件
    • 2)、我们去看下插件内的源码(serve为例)
  • 代码片段3(./node_modules/@vue/cli-service/lib/commands/serve.js)

    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`,
          '--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) {
     	......
      })
    }
    
    
  • 解读

    • 1)、直接调用了第一个参数的registerCommand方法
    • 2)、去看一下registerCommand方法
  • 代码片段4 (./node_modules/@vue/cli-service/lib/PluginAPI.js)

    class PluginAPI {
      /**
       * @param {string} id - Id of the plugin.
       * @param {Service} service - A vue-cli-service instance.
       */
      constructor (id, service) {
        this.id = id
        this.service = service
      }
      registerCommand (name, opts, fn) {
        if (typeof opts === 'function') {
          fn = opts
          opts = null
        }
        this.service.commands[name] = { fn, opts: opts || {}}
      }
    }
    
  • 解读

    • 1)、从上面部分源码结合片段3可以看出,registerCommand方法将 serve 函数 存入的commands中了
  • 至此,当我们运行serve命令时,找到了 Service.run 函数中执行的 command 了,就是 serve.js 中的 serve 函数

  • 接下来,看看 serve.js 的源码

2.4、serve.js 源码
  • 代码片段
    
    const defaults = {
      host: '0.0.0.0',
      port: 8080,
      https: false
    }
    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`,
          '--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) {
          const WebpackDevServer = require('webpack-dev-server')
          // create server
          const server = new WebpackDevServer(compiler, Object.assign({
          logLevel: 'silent',
          clientLogLevel: 'silent',
          historyApiFallback: {
            disableDotRule: true,
            rewrites: genHistoryApiFallbackRewrites(options.publicPath, options.pages)
          },
          contentBase: api.resolve('public'),
          watchContentBase: !isProduction,
          hot: !isProduction,
          injectClient: false,
          compress: isProduction,
          publicPath: options.publicPath,
          overlay: isProduction // TODO disable this
            ? false
            : { warnings: false, errors: true }
        }, projectDevServerOptions, {
          https: useHttps,
          proxy: proxySettings,
          // eslint-disable-next-line no-shadow
          before (app, server) {
            // launch editor support.
            // this works with vue-devtools & @vue/cli-overlay
            app.use('/__open-in-editor', launchEditorMiddleware(() => console.log(
              `To specify an editor, specify the EDITOR env variable or ` +
              `add "editor" field to your Vue project config.\n`
            )))
            // allow other plugins to register middlewares, e.g. PWA
            api.service.devServerConfigFns.forEach(fn => fn(app, server))
            // apply in project middlewares
            projectDevServerOptions.before && projectDevServerOptions.before(app, server)
          },
          // avoid opening browser
          open: false
        }))
    
      })
    }
    
  • 解读
    • 1)、从代码中可以看出,vue-cli 用了 webpack-dev-server 启动开发服务。

3、回答开始提到的问题

  • 1)、执行 vue-cli-service serve 命令
  • 2)、解析参数行内插件、本地插件、package.json中的插件、内置插件
  • 3)、执行run 方法
  • 4)、init方法内配置插件,webpack配置函数存入webpackChainFns,命令函数存入commands
  • 5)、调用 serve 命令 对应的 command 函数
  • 6)、使用webpack-dev-serve 搭建开发服务器

4、vue-cli-service serve 从运行命令到本地访问流程图解

在这里插入图片描述

5、vue-cli-service serve 除了启动一个本地服务器,还做了什么?

  • 代码片段

     // configs that only matters for dev server
        api.chainWebpack(webpackConfig => {
          if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
            webpackConfig
              .devtool('eval-cheap-module-source-map')
    
            webpackConfig
              .plugin('hmr')
                .use(require('webpack/lib/HotModuleReplacementPlugin'))
    
            // https://github.com/webpack/webpack/issues/6642
            // https://github.com/vuejs/vue-cli/issues/3539
            webpackConfig
              .output
                .globalObject(`(typeof self !== 'undefined' ? self : this)`)
    
            if (!process.env.VUE_CLI_TEST && options.devServer.progress !== false) {
              webpackConfig
                .plugin('progress')
                .use(require('webpack/lib/ProgressPlugin'))
            }
          }
        })
    // expose advanced stats
        if (args.dashboard) {
          const DashboardPlugin = require('../webpack/DashboardPlugin')
          ;(webpackConfig.plugins = webpackConfig.plugins || []).push(new DashboardPlugin({
            type: 'serve'
          }))
        }
    // entry arg
        const entry = args._[0]
        if (entry) {
          webpackConfig.entry = {
            app: api.resolve(entry)
          }
        }
    
  • 解读(serve默认)

    • 1)、配置sorcemap,源码映射

    • 2)、配置hmr,热更新

    • 3)、设置globalObject,全局变量

    • 4)、设置progress,进度条

    • 5)、如果参数有dashboard,则设置dashboard,构建过程可视化

    • 6)、设置entry,入口文件

    • 7)、设置webpackDevServer

    • 8)、还会设置用户的webpack配置

      • ./node_modules/@vue/cli-service/lib/commands/serve.js
      // resolve webpack config
          const webpackConfig = api.resolveWebpackConfig()
      
      • ./node_modules/@vue/cli-service/lib/PluginAPI.js
      /**
         * Resolve the final raw webpack config, that will be passed to webpack.
         *
         * @param {ChainableWebpackConfig} [chainableConfig]
         * @return {object} Raw webpack config.
         */
        resolveWebpackConfig (chainableConfig) {
          return this.service.resolveWebpackConfig(chainableConfig)
        }
      
      • ./node_modules/@vue/cli-service/lib/Service.js
        // apply webpack configs from project config file
          if (this.projectOptions.chainWebpack) {
            this.webpackChainFns.push(this.projectOptions.chainWebpack)
          }
        resolveChainableWebpackConfig () {
          const chainableConfig = new Config()
          // apply chains
          this.webpackChainFns.forEach(fn => fn(chainableConfig))
          return chainableConfig
        } 
        resolveWebpackConfig (chainableConfig = this.resolveChainableWebpackConfig()) {
          if (!this.initialized) {
            throw new Error('Service must call init() before calling resolveWebpackConfig().')
          }
          // get raw config
          let config = chainableConfig.toConfig()
          const original = config
          // apply raw config fns
          this.webpackRawConfigFns.forEach(fn => {
            if (typeof fn === 'function') {
              // function with optional return value
              const res = fn(config)
              if (res) config = merge(config, res)
            } else if (fn) {
              // merge literal values
              config = merge(config, fn)
            }
          })
      
          // #2206 If config is merged by merge-webpack, it discards the __ruleNames
          // information injected by webpack-chain. Restore the info so that
          // vue inspect works properly.
          if (config !== original) {
            cloneRuleNames(
              config.module && config.module.rules,
              original.module && original.module.rules
            )
          }
      
          // check if the user has manually mutated output.publicPath
          const target = process.env.VUE_CLI_BUILD_TARGET
          if (
            !process.env.VUE_CLI_TEST &&
            (target && target !== 'app') &&
            config.output.publicPath !== this.projectOptions.publicPath
          ) {
            throw new Error(
              `Do not modify webpack output.publicPath directly. ` +
              `Use the "publicPath" option in vue.config.js instead.`
            )
          }
      
          if (
            !process.env.VUE_CLI_ENTRY_FILES &&
            typeof config.entry !== 'function'
          ) {
            let entryFiles
            if (typeof config.entry === 'string') {
              entryFiles = [config.entry]
            } else if (Array.isArray(config.entry)) {
              entryFiles = config.entry
            } else {
              entryFiles = Object.values(config.entry || []).reduce((allEntries, curr) => {
                return allEntries.concat(curr)
              }, [])
            }
      
            entryFiles = entryFiles.map(file => path.resolve(this.context, file))
            process.env.VUE_CLI_ENTRY_FILES = JSON.stringify(entryFiles)
          }
      
          return config
        }
      

6、模仿vue-cli-service serve ,使用node动手搭建一个很基础的静态资源服务器

node实现搭建静态资源服务器

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐