vue-element-admin源碼解讀——項目啓動過程

vue-element-admin是一款優秀的前端框架,使用了最新的前端技術,內置了(Vue-i18)國際化解決方案,動態路由(Vue Route)、狀態管理(Vuex)等等方案使得整個框架結構非常清晰。不僅如此,該框架還有Typescript實現版本(vue-typescript-admin-template),具有強制類型約束、接口規範統一等等功能,對今後項目的拓展與模塊化、與後端的對接等等方面將起到必不可少的作用。

雖然vue-element-admin官方也編寫了相關的用戶指南,不過只是單純地介紹瞭如何修改當前項目結構來滿足業務需求,並未對項目的代碼實現層面做太多的贅述,所以這篇文章就是來從源碼的角度,來一步步深入vue-element-admin框架,瞭解整個項目的構成,來實現更深度地定製。

vue-element-admin是一個具有完備功能的示例框架,如果只需要基礎框架結構,請使用vue-admin-template

代碼入口

根據官方提供的命令在執行npm install模塊安裝之後,使用npm run dev來啓動項目。我們來看下這個dev腳本對應的命令在哪:

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

可以看到dev腳本啓動就是vue-cli-service serve這個控制檯命令,我們進一步跟蹤看看:

# 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

從這裏就可以看出vue-cli-service serve其實就是執行了node vue-cli-service.js serve這個命令。

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

到這裏就到命令的最後一步了,調用Service對象的run方法執行對應的指令。在深入這個run方法之前,我們來看一下Service這個對象在初始化的時候做了些什麼:

// Service的初始化就是讀取了Package.json和Vue.config.js
// 文件中的配置信息,這裏需要對比着Vue.config.js文件內容看
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
    //從package.json中獲取依賴包信息
    this.pkg = this.resolvePkg(pkg)
    //從package.json中加載插件,同時合併內建插件
    this.plugins = this.resolvePlugins(plugins, useBuiltIn)
    this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
      return Object.assign(modes, defaultModes)
    }, {})
  }
  ...
}

// Vue.config.js
module.exports = {
  //下面這一塊都屬於inlineOptions
  publicPath: '/',
  outputDir: 'dist',
  assetsDir: 'static',
  lintOnSave: process.env.NODE_ENV === 'development',
  productionSourceMap: false,
  //這是用於配置WebpackDevServer的配置
  devServer: {
    port: port,
    open: true,
    overlay: {
      warnings: false,
      errors: true
    },
    // 這裏是爲了啓動監聽服務之前啓動mock服務器返回模擬數據
    before: require('./mock/mock-server.js')
  },
  // 這一部分配置適用於傳遞給webpack
  configureWebpack: {
    //這個配置很簡單就是爲src目錄取了個別名
    name: name,
    resolve: {
      alias: {
        '@': resolve('src')
      }
    }
  },
  //這裏是配置webpack的loader(資源加載鏈)
  chainWebpack(config) {
    ...
  }
}

看完上面,我們就知道了在Service初始化時會將配置文件中的配置加載到上下文之中,這個過程是屬於Vue框架完成的。根據之前傳入的參數Serve,可知接下來Servicerun方法肯定會去執行Serve.

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

    //這個方法就是加載用戶的環境變量配置文件.env
    this.init(mode)

    args._ = args._ || []
    //到這裏我們就可以知道了serve命令被存儲在了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)
  }

從上面我們知道了Serve命令被存放在了Commands數組中。在讀源碼的過程中,我好像在resolvePlugins方法中發現了些蛛絲馬跡。

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

看來serve命令就存放在同模塊下的同目錄的command目錄下中:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-lFaHgXUf-1591717893053)(./command.png)],那這就明瞭多了。好的,我們就來看看serve.js是如何綁定的。

詳解serve.js文件

由於整個文件比較長,我將將其拆分爲幾個部分來進行說明:

命令註冊

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`
    }
  },
  ...
}
//上述代碼就是通過registerCommand將serve命令註冊到了command命令中去了。
registerCommand (name, opts, fn) {
    if (typeof opts === 'function') {
      fn = opts
      opts = null
    }
    this.service.commands[name] = { fn, opts: opts || {}}
}

Serve工作過程

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

    // 獲取webpack的配置,在Service初始化時就獲取了
    const webpackConfig = api.resolveWebpackConfig()

    //檢查配置信息是否有問題
    validateWebpackConfig(webpackConfig, api, options)

    //配置webpack的devServer配置選項,這個選項是從Vue.config.js獲取的
    const projectDevServerOptions = Object.assign(
      webpackConfig.devServer || {},
      options.devServer
    )
    ......
    // 配置服務器的選項
    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')
    )

    // 配置webpack-dev-server選項
    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)

    // 創建服務器,並注入配置信息
    const server = new WebpackDevServer(compiler, Object.assign({
      clientLogLevel: 'none',
      historyApiFallback: {
        disableDotRule: true,
        rewrites: genHistoryApiFallbackRewrites(options.publicPath, options.pages)
      },
      //指定根目錄路徑
      contentBase: api.resolve('public'),
      //啓動是否監視根目錄文件變化
      watchContentBase: !isProduction,
      //開發環境下啓動熱更新
      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`
        )))
        //指定vue.config.js中配置的插件
        api.service.devServerConfigFns.forEach(fn => fn(app, server))
        //應用項目中配置的中間件,vue.config.js中的devServer.before中指定的mock服務器就是在這被執行的
        projectDevServerOptions.before && projectDevServerOptions.before(app, server)
      }
    }))
    // 監聽系統信號
    ;['SIGINT', 'SIGTERM'].forEach(signal => {
      process.on(signal, () => {
        server.close(() => {
          process.exit(0)
        })
      })
    })

    //監聽關閉信號
    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)
          })
        }
      })
    }

在上面的serve函數中該被加載的插件和中間件都被執行了之後,將會返回一個Promise對象用於啓動http監聽。

return new Promise((resolve, reject) => {
      //這裏省略了很大部分console.log調試信息的輸出   
      server.listen(port, host, err => {
        if (err) {
          reject(err)
        }
      })
    })

到這裏呢,整個框架的基礎工作就完成了,包括插件、中間件(Mock服務器)、配置信息的加載、服務監聽的打開。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章