概述
vue啓動一個項目的時候,需要執行npm run serve,其中這個serve的內容就是vue-cli-service serve
。可見,項目的啓動關鍵是這個vue-cli-service
與它的參數serve
。接下來我們一起看看service中主要寫了什麼東東(主要內容以備註形式寫到代碼中。)。
vue-cli-service.js
const semver = require('semver')
const { error } = require('@vue/cli-shared-utils')
const requiredVersion = require('../package.json').engines.node
// 檢測node版本是否符合vue-cli運行的需求。不符合則打印錯誤並退出。
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)
}
// cli-service的核心類。
const Service = require('../lib/Service')
// 新建一個service的實例。並將項目路徑傳入。一般我們在項目根路徑下運行該cli命令。所以process.cwd()的結果一般是項目根路徑
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這個實例並啓動後續工作。如果我們運行的是npm run serve。則command = "serve"。
service.run(command, args, rawArgv).catch(err => {
error(err)
process.exit(1)
})
Service.js
上面調用了service的run方法,這裏從run開始一路瀏覽即可。
const fs = require('fs')
const path = require('path')
const debug = require('debug')
const chalk = require('chalk')
const readPkg = require('read-pkg')
const merge = require('webpack-merge')
const Config = require('webpack-chain')
const PluginAPI = require('./PluginAPI')
const loadEnv = require('./util/loadEnv')
const defaultsDeep = require('lodash.defaultsdeep')
const { warn, error, isPlugin, loadModule } = require('@vue/cli-shared-utils')
const { defaults, validate } = require('./options')
module.exports = class Service {
constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
process.VUE_CLI_SERVICE = this
this.initialized = false
// 一般是項目根目錄路徑。
this.context = context
this.inlineOptions = inlineOptions
// webpack相關收集。不是本文重點。所以未列出該方法實現
this.webpackChainFns = []
this.webpackRawConfigFns = []
this.devServerConfigFns = []
//存儲的命令。
this.commands = {}
// Folder containing the target package.json for plugins
this.pkgContext = context
// 鍵值對存儲的pakcage.json對象,不是本文重點。所以未列出該方法實現
this.pkg = this.resolvePkg(pkg)
this.plugins = this.resolvePlugins(plugins, useBuiltIn)
// 結果爲{build: production, serve: development, ... }。大意是收集插件中的默認配置信息
// 標註build命令主要用於生產環境。
this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
return Object.assign(modes, defaultModes)
}, {})
}
init (mode = process.env.VUE_CLI_MODE) {
if (this.initialized) {
return
}
this.initialized = true
this.mode = mode
// 加載.env文件中的配置
if (mode) {
this.loadEnv(mode)
}
// load base .env
this.loadEnv()
// 讀取用戶的配置信息.一般爲vue.config.js
const userOptions = this.loadUserOptions()
// 讀取項目的配置信息並與用戶的配置合併(用戶的優先級高)
this.projectOptions = defaultsDeep(userOptions, defaults())
debug('vue:project-config')(this.projectOptions)
// 註冊插件。
this.plugins.forEach(({ id, apply }) => {
apply(new PluginAPI(id, this), this.projectOptions)
})
// wepback相關配置收集
if (this.projectOptions.chainWebpack) {
this.webpackChainFns.push(this.projectOptions.chainWebpack)
}
if (this.projectOptions.configureWebpack) {
this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
}
}
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)
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
}
async run (name, args = {}, rawArgv = []) {
// mode是dev還是prod?
const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
// 收集環境變量、插件、用戶配置
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) {
command = this.commands.help
} else {
args._.shift() // remove command itself
rawArgv.shift()
}
// 執行命令。例如vue-cli-service serve 則,執行serve命令。
const { fn } = command
return fn(args, rawArgv)
}
// 收集vue.config.js中的用戶配置。並以對象形式返回。
loadUserOptions () {
// vue.config.js
let fileConfig, pkgConfig, resolved, resolvedFrom
const configPath = (
process.env.VUE_CLI_SERVICE_CONFIG_PATH ||
path.resolve(this.context, 'vue.config.js')
)
if (fs.existsSync(configPath)) {
try {
fileConfig = require(configPath)
if (!fileConfig || typeof fileConfig !== 'object') {
error(
`Error loading ${chalk.bold('vue.config.js')}: should export an object.`
)
fileConfig = null
}
} catch (e) {
error(`Error loading ${chalk.bold('vue.config.js')}:`)
throw e
}
}
// package.vue
pkgConfig = this.pkg.vue
if (pkgConfig && typeof pkgConfig !== 'object') {
error(
`Error loading vue-cli config in ${chalk.bold(`package.json`)}: ` +
`the "vue" field should be an object.`
)
pkgConfig = null
}
if (fileConfig) {
if (pkgConfig) {
warn(
`"vue" field in package.json ignored ` +
`due to presence of ${chalk.bold('vue.config.js')}.`
)
warn(
`You should migrate it into ${chalk.bold('vue.config.js')} ` +
`and remove it from package.json.`
)
}
resolved = fileConfig
resolvedFrom = 'vue.config.js'
} else if (pkgConfig) {
resolved = pkgConfig
resolvedFrom = '"vue" field in package.json'
} else {
resolved = this.inlineOptions || {}
resolvedFrom = 'inline options'
}
if (typeof resolved.baseUrl !== 'undefined') {
if (typeof resolved.publicPath !== 'undefined') {
warn(
`You have set both "baseUrl" and "publicPath" in ${chalk.bold('vue.config.js')}, ` +
`in this case, "baseUrl" will be ignored in favor of "publicPath".`
)
} else {
warn(
`"baseUrl" option in ${chalk.bold('vue.config.js')} ` +
`is deprecated now, please use "publicPath" instead.`
)
resolved.publicPath = resolved.baseUrl
}
}
// if (typeof resolved.publicPath === 'undefined') {
// resolved.publicPath = '/'
// }
// normalize some options
ensureSlash(resolved, 'publicPath')
if (typeof resolved.publicPath === 'string') {
resolved.publicPath = resolved.publicPath.replace(/^\.\//, '')
}
removeSlash(resolved, 'outputDir')
// deprecation warning
// TODO remove in final release
if (resolved.css && resolved.css.localIdentName) {
warn(
`css.localIdentName has been deprecated. ` +
`All css-loader options (except "modules") are now supported via css.loaderOptions.css.`
)
}
// validate options
validate(resolved, msg => {
error(
`Invalid options in ${chalk.bold(resolvedFrom)}: ${msg}`
)
})
return resolved
}
}
// 路徑中的斜槓槓處理
function ensureSlash (config, key) {
let val = config[key]
if (typeof val === 'string') {
if (!/^https?:/.test(val)) {
val = val.replace(/^([^/.])/, '/$1')
}
config[key] = val.replace(/([^/])$/, '$1/')
}
}
// 路徑中的斜槓槓處理
function removeSlash (config, key) {
if (typeof config[key] === 'string') {
config[key] = config[key].replace(/\/$/g, '')
}
}