Source Code Interpretation of vue-element-admin--Project Startup Process

Keywords: Vue Webpack JSON TypeScript

vue-element-admin Is an excellent front-end framework, using the latest front-end technology, built-in (Vue-i18) Internationalized Solutions, Dynamic Routing (Vue Route) , State Management (Vuex) The scheme such as these makes the whole frame structure very clear.What's more, the framework also has Typescript's Implementation Version (vue-typescript-admin-template) With the functions of enforcing type constraints, unifying interface specifications, etc., it will play an essential role in the expansion and modularization of future projects, docking with backend, etc.

although vue-element-admin The official user guide has also been written, but it simply describes how to modify the current project structure to meet business needs without too much detail on the code implementation level of the project, so this article is to step into the vue-element-admin framework from the source point of view to understand the composition of the entire project to achieve deeper customization.

vue-element-admin is a fully functional sample framework, if only the infrastructure is required, use vue-admin-template

Code Entry

Use npm run dev to start the project after executing the npm install module installation according to an official command.Let's see where the command for this dev script is:

// package.json
{
    ...,
    "scripts": {
        "dev": "vue-cli-service serve",
        "build:prod": "vue-cli-service build",
        "build:stage": "vue-cli-service build --mode staging",
        ...
  },
  ...
}

As you can see that the dev script startup is the console command vue-cli-service, let's take a closer look:

# vue-cli-service
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")

case `uname` in
    *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;;
esac

if [ -x "$basedir/node" ]; then
  "$basedir/node"  "$basedir/../@vue/cli-service/bin/vue-cli-service.js" "$@"
  ret=$?
else 
  node  "$basedir/../@vue/cli-service/bin/vue-cli-service.js" "$@"
  ret=$?
fi
exit $ret

From here you can see that vue-cli-service serve r actually executes node vue-cli-Service.jsServer this command.

// vue-cli-service.js
const semver = require('semver')
const { error } = require('@vue/cli-shared-utils')
const requiredVersion = require('../package.json').engines.node

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)
}

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',
    'watch',
    // serve
    'open',
    'copy',
    'https',
    // inspect
    'verbose'
  ]
})
const command = args._[0]

service.run(command, args, rawArgv).catch(err => {
  error(err)
  process.exit(1)
})

This is the last step of the command, calling the run method of the Service object to execute the corresponding command.Before going into this run method, let's look at what the Service object did when it was initialized:

// Service initialization is read Package.json andVue.config.js
// Configuration information in the file, which needs to be compared here Vue.config.js File Content View
module.exports = class Service {
  constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
    process.VUE_CLI_SERVICE = this
    this.initialized = false
    this.context = context
    this.inlineOptions = inlineOptions
    this.webpackChainFns = []
    this.webpackRawConfigFns = []
    this.devServerConfigFns = []
    this.commands = {}
    this.pkgContext = context
    //fromPackage.jsonGet dependent package information in
    this.pkg = this.resolvePkg(pkg)
    //fromPackage.jsonLoad plug-ins in and merge built-in plug-ins
    this.plugins = this.resolvePlugins(plugins, useBuiltIn)
    this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
      return Object.assign(modes, defaultModes)
    }, {})
  }
  ...
}

// Vue.config.js
module.exports = {
  //The following block belongs to inlineOptions
  publicPath: '/',
  outputDir: 'dist',
  assetsDir: 'static',
  lintOnSave: process.env.NODE_ENV === 'development',
  productionSourceMap: false,
  //This is the configuration used to configure WebpackDevServer
  devServer: {
    port: port,
    open: true,
    overlay: {
      warnings: false,
      errors: true
    },
    // This is to start the mock server to return simulated data before starting the listening service
    before: require('./mock/mock-server.js')
  },
  // This part of the configuration applies to delivering to the webpack
  configureWebpack: {
    //This configuration is simple enough to alias the src directory
    name: name,
    resolve: {
      alias: {
        '@': resolve('src')
      }
    }
  },
  //Here is the loader for configuring the webpack (resource load chain)
  chainWebpack(config) {
    ...
  }
}

Looking at the above, we know that the configuration in the configuration file is loaded into the context when the service is initialized, which is done by the Vue framework.Based on the parameter Serve passed in earlier, you know that the next Service run method will definitely execute the Serve.

async run (name, args = {}, rawArgv = []) {
    const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])

    //This is done by loading the user's environment variable profile.env
    this.init(mode)

    args._ = args._ || []
    //So here we know that the serve r command is stored in the command
    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)
  }

From above we know that the Serve command is stored in the Commands array.While reading the source code, I seem to have found some clues in the resolvePlugins method.

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/dev',
      './config/prod',
      './config/app'
    ].map(idToPlugin)
    ....
}

It appears that the serve r command is stored in the same directory command directory under the same module:
[External chain picture transfer failed, source station may have anti-theft chain mechanism, it is recommended to save the picture and upload it directly (img-lFaHgXUf-1591717893053)(./command.png)], that's a lot clearer.Okay, let's have a lookServe.jsHow to bind.

DetailedServe.jsfile

Because the whole file is long, I'll break it up into several parts to illustrate:

Command Registration

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`
    }
  },
  ...
}
//The above code registers the serve r command with the command command through registerCommand.
registerCommand (name, opts, fn) {
    if (typeof opts === 'function') {
      fn = opts
      opts = null
    }
    this.service.commands[name] = { fn, opts: opts || {}}
}

Serve Work Process

async function serve (args) {
    info('Starting development server...')

    // although this is primarily a dev server, it is possible that we
    // are running it in a mode with a production env, e.g. in E2E tests.
    const isInContainer = checkInContainer()
    const isProduction = process.env.NODE_ENV === 'production'

    const url = require('url')
    const chalk = require('chalk')
    const webpack = require('webpack')
    const WebpackDevServer = require('webpack-dev-server')
    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')

    // Get the configuration of the webpack, which you get when Service initializes
    const webpackConfig = api.resolveWebpackConfig()

    //Check configuration information for problems
    validateWebpackConfig(webpackConfig, api, options)

    //Configure the devServer configuration option for the webpack from Vue.config.js Acquired
    const projectDevServerOptions = Object.assign(
      webpackConfig.devServer || {},
      options.devServer
    )
    ......
    // Options for configuring the server
    const useHttps = args.https || projectDevServerOptions.https || defaults.https
    const protocol = useHttps ? 'https' : 'http'
    const host = args.host || process.env.HOST || projectDevServerOptions.host || defaults.host
    portfinder.basePort = args.port || process.env.PORT || projectDevServerOptions.port || defaults.port
    const port = await portfinder.getPortPromise()
    const rawPublicUrl = args.public || projectDevServerOptions.public
    const publicUrl = rawPublicUrl
      ? /^[a-zA-Z]+:\/\//.test(rawPublicUrl)
        ? rawPublicUrl
        : `${protocol}://${rawPublicUrl}`
      : null

    const urls = prepareURLs(
      protocol,
      host,
      port,
      isAbsoluteUrl(options.publicPath) ? '/' : options.publicPath
    )

    const proxySettings = prepareProxy(
      projectDevServerOptions.proxy,
      api.resolve('public')
    )

    // Configure webpack-dev-server options
    if (!isProduction) {
      const sockjsUrl = publicUrl
        ? `?${publicUrl}/sockjs-node`
        : isInContainer
          ? ``
          : `?` + url.format({
            protocol,
            port,
            hostname: urls.lanUrlForConfig || 'localhost',
            pathname: '/sockjs-node'
          })
      const devClients = [
        // dev server client
        require.resolve(`webpack-dev-server/client`) + sockjsUrl,
        // hmr client
        require.resolve(projectDevServerOptions.hotOnly
          ? 'webpack/hot/only-dev-server'
          : 'webpack/hot/dev-server')
        // TODO custom overlay client
        // `@vue/cli-overlay/dist/client`
      ]
      if (process.env.APPVEYOR) {
        devClients.push(`webpack/hot/poll?500`)
      }
      // inject dev/hot client
      addDevClientToEntry(webpackConfig, devClients)
    }

    // create compiler
    const compiler = webpack(webpackConfig)

    // Create a server and inject configuration information
    const server = new WebpackDevServer(compiler, Object.assign({
      clientLogLevel: 'none',
      historyApiFallback: {
        disableDotRule: true,
        rewrites: genHistoryApiFallbackRewrites(options.publicPath, options.pages)
      },
      //Specify root directory path
      contentBase: api.resolve('public'),
      //Startup monitors root file changes
      watchContentBase: !isProduction,
      //Start hot update in development environment
      hot: !isProduction,
      quiet: true,
      compress: isProduction,
      publicPath: options.publicPath,
      overlay: isProduction // TODO disable this
        ? false
        : { warnings: false, errors: true }
    }, projectDevServerOptions, {
      https: useHttps,
      proxy: proxySettings,
      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, sepcify the EDITOR env variable or ` +
          `add "editor" field to your Vue project config.\n`
        )))
        //AppointVue.config.jsPlug-ins configured in
        api.service.devServerConfigFns.forEach(fn => fn(app, server))
        //Apply the middleware configured in the project,Vue.config.jsInDevServer.beforeThis is where the mock server specified in the
        projectDevServerOptions.before && projectDevServerOptions.before(app, server)
      }
    }))
    // Monitor System Signals
    ;['SIGINT', 'SIGTERM'].forEach(signal => {
      process.on(signal, () => {
        server.close(() => {
          process.exit(0)
        })
      })
    })

    //Listen for shutdown signal
    if (process.env.VUE_CLI_TEST) {
      process.stdin.on('data', data => {
        if (data.toString() === 'close') {
          console.log('got close signal!')
          server.close(() => {
            process.exit(0)
          })
        }
      })
    }

After the loaded plug-in and middleware are executed in the serve r function above, a Promise object is returned to start http listening.

return new Promise((resolve, reject) => {
      //A large part is omitted hereConsole.logOutput of debug information   
      server.listen(port, host, err => {
        if (err) {
          reject(err)
        }
      })
    })

Here, the basic work of the whole framework is completed, including plug-ins, middleware (Mock server), loading of configuration information, opening of service monitoring.

Posted by fekaduw on Tue, 09 Jun 2020 10:00:47 -0700