Vue CLI 源码探索 [三]

3,101 阅读1分钟

本系列一共6篇文章


下面正文开始啦 ^_^

vue serve [entry]

serve a .js or .vue file in development mode with zero config

在开发模式下零配置的调试一个 .js 或者 .vue 文件

调试配置

{
  // 使用 IntelliSense 了解相关属性。 
  // 悬停以查看现有属性的描述。
  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "启动程序",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "program": "${workspaceFolder}/packages/@vue/cli/bin/vue",
      // 对应着在 test 目录下新建了 App.vue 文件
      "args": ["serve", "packages/test/App.vue"]
    }
  ]
}

源码探索

命令注册

program
  .command('serve [entry]')
  .description('serve a .js or .vue file in development mode with zero config')
  .option('-o, --open', 'Open browser')
  .option('-c, --copy', 'Copy local url to clipboard')
  .option('-p, --port <port>', 'Port used by the server (default: 8080 or next available port)')
  .action((entry, cmd) => {
    // 载入了 @vue/cli-service-global 这个包,并调用 serve 方法
    loadCommand('serve', '@vue/cli-service-global').serve(entry, cleanArgs(cmd))
  })

@vue/cli-service-global

serve 方法就两行,加载入口,然后实例化服务,执行 run('serve', args) 方法

exports.serve = (_entry, args) => {
  const entry = resolveEntry(_entry, 'serve')
  createService(entry).run('serve', args)
}

看下创建服务的代码

function createService (entry, asLib) {
  // 实例化 Service
  return new Service(context, {
    projectOptions: {
      compiler: true,
      lintOnSave: 'default'
    },
    plugins: [
      babelPlugin,
      eslintPlugin,
      globalConfigPlugin(context, entry, asLib)
    ]
  })
}

被实例化的 Sericeconst Service = require('@vue/cli-service') 引入进来的,所以后面最终做事的还是 @vue/cli-servcie 类, @vue/cli-service 的分析在 vue inspect 中已经提到了,不赘述。

@vue/cli-service/lib/commands/serve.js

我们这里着重看下 serve 命令回调函数中的逻辑,

    const url = require('url')
    const { chalk } = require('@vue/cli-shared-utils')
    // 这里载入了 webpack
    const webpack = require('webpack')
    // 这里载入了 webpack-dev-server
    const WebpackDevServer = require('webpack-dev-server')
    // 这个portfinder 解决了多个项目需要手动修改 port 的痛点
    const portfinder = require('portfinder')
    const prepareURLs = require('../util/prepareURLs')
    const prepareProxy = require('../util/prepareProxy')
    const launchEditorMiddleware = require('launch-editor-middleware')
    const validateWebpackConfig = require('../util/validateWebpackConfig')
    const isAbsoluteUrl = require('../util/isAbsoluteUrl')

    // 这里设置了webpack入口
    const entry = args._[0]
    if (entry) {
      webpackConfig.entry = {
        app: api.resolve(entry)
      }
    }
    // 这里通过 portfinder 获取了可用端口
    const port = await portfinder.getPortPromise()
    
    // 这里创建了 webpack 实例
    const compiler = webpack(webpackConfig)

    // 实例创建了 WebpackDevServer 实例
    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
    }))


    // todo 这里的这段代码在 webpack 相关阅读完成后再回头看
    compiler.hooks.done.tap('vue-cli-service serve', stats => {

小插曲

调试的过程中报这个错误,官方已经将该问题修复了

Module build failed (from ./node_modules/eslint-loader/index.js):
Error: BaseConfig:
	Configuration for rule "no-debugger" is invalid:
	Severity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '"process.env.NODE_ENV === "production" ? "error" : "off""').

    at validateRuleOptions (/Users/didi/Documents/project/llccing-demo/vue-cli/node_modules/eslint/lib/shared/
    ...

vue build [entry]

build a .js or .vue file in production mode with zero config

在生产模式下零配置的构建一个 .js 或者 .vue 文件

源码探索

命令注册

program
  .command('build [entry]')
  .description('build a .js or .vue file in production mode with zero config')
  .option('-t, --target <target>', 'Build target (app | lib | wc | wc-async, default: app)')
  .option('-n, --name <name>', 'name for lib or web-component mode (default: entry filename)')
  .option('-d, --dest <dir>', 'output directory (default: dist)')
  .action((entry, cmd) => {
    // 载入 @vue/cli-service-global,执行 build 方法
    loadCommand('build', '@vue/cli-service-global').build(entry, cleanArgs(cmd))
  })

@vue/cli-service-global

build 方法的逻辑很简单,解析入口,创建 Service 实例再执行 run 方法。

exports.build = (_entry, args) => {
  const entry = resolveEntry(_entry, 'build')
  const asLib = args.target && args.target !== 'app'
  if (asLib) {
    args.entry = entry
  }
  return createService(entry, asLib).run('build', args)
}

@vue/cli-service/lib/commands/build/index.js

Service 实例的 run 方法传入 build 参数,执行的是 build 命令。

命令注册

api.registerCommand('build', {
    description: 'build for production',
    usage: 'vue-cli-service build [options] [entry|pattern]',
    // 这里可以看到有非常多的选项
    options: {
      '--mode': `specify env mode (default: production)`,
      '--dest': `specify output directory (default: ${options.outputDir})`,
      ...
    }
  }, () => {
    // 这里主要是处理了默认配置,然后会走到 build 函数
    await build(Object.assign({}, args, {
      modernBuild: false,
      keepAlive: true
    }), api, options)
  } 
)

我们看下 build 函数

// 这里载入了 webpack 配置
webpackConfig = require('./resolveAppConfig')(api, args, options)

// report 选项用到了 webpack-bundle-analyzer 插件,便于查看项目的资源依赖及大小
if (args.report || args['report-json']) {
  const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
  modifyConfig(webpackConfig, config => {
    const bundleName = args.target !== 'app'
      ? config.output.filename.replace(/\.js$/, '-')
      : isLegacyBuild ? 'legacy-' : ''
    config.plugins.push(new BundleAnalyzerPlugin({
      logLevel: 'warn',
      openAnalyzer: false,
      analyzerMode: args.report ? 'static' : 'disabled',
      reportFilename: `${bundleName}report.html`,
      statsFilename: `${bundleName}report.json`,
      generateStatsFile: !!args['report-json']
    }))
  })
}

// 这里执行了webpack的构建
webpack(webpackConfig, (err, stats) => {

感谢阅读

原文地址

感谢你阅读到这里,翻译的不好的地方,还请指点。希望我的内容能让你受用,再次感谢。by llccing 千里