vue-cli-service源码的简单分析
高薪就业葵花宝典
用到的包
"dependencies": {
"@babel/helper-compilation-targets": "^7.12.16",
"@soda/friendly-errors-webpack-plugin": "^1.8.0", // 友好的webpack错误输出
"@soda/get-current-script": "^1.0.2",
"@types/minimist": "^1.2.0",
"@vue/cli-overlay": "^5.0.8",
"@vue/cli-plugin-router": "^5.0.8", // 代码模板相关
"@vue/cli-plugin-vuex": "^5.0.8", // 代码模板相关
"@vue/cli-shared-utils": "^5.0.8", // 一些工具方法
"@vue/component-compiler-utils": "^3.3.0",
"@vue/vue-loader-v15": "npm:vue-loader@^15.9.7", // vue2的vue-loader
"@vue/web-component-wrapper": "^1.3.0",
"acorn": "^8.0.5", // js的解析器,用于打包分析
"acorn-walk": "^8.0.2", // js的执行器,用于打包分析
"address": "^1.1.2", // 获取本机的ip地址
"autoprefixer": "^10.2.4", // postcss插件,自动补前缀
"browserslist": "^4.16.3", // 浏览器兼容相关
"case-sensitive-paths-webpack-plugin": "^2.3.0", // 路径大小写敏感
"cli-highlight": "^2.1.10", // 控制台文本高亮
"clipboardy": "^2.3.0", // 访问系统剪切板
"cliui": "^7.0.4", // 创建复杂的多列命令行界面
"copy-webpack-plugin": "^9.0.1", // webpack的复制插件
"css-loader": "^6.5.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"cssnano": "^5.0.0", // 去除无用css
"debug": "^4.1.1",
"default-gateway": "^6.0.3", // 获取系统网关
"dotenv": "^10.0.0", // 读取.env文件相关
"dotenv-expand": "^5.1.0", // 读取.env文件相关
"fs-extra": "^9.1.0", // fs模块的封装版
"globby": "^11.0.2", // 分析文件目录
"hash-sum": "^2.0.0", // 哈希值生成工具
"html-webpack-plugin": "^5.1.0",
"is-file-esm": "^1.0.0",
"launch-editor-middleware": "^2.2.1", // 美化控制台输出
"lodash.defaultsdeep": "^4.6.1",
"lodash.mapvalues": "^4.6.0",
"mini-css-extract-plugin": "^2.5.3", // css拆分插件
"minimist": "^1.2.5", // 读取命令行参数
"module-alias": "^2.2.2", // yarn的读取node_modules机制
"portfinder": "^1.0.26", // 查找可用端口
"postcss": "^8.2.6",
"postcss-loader": "^6.1.1",
"progress-webpack-plugin": "^1.0.12",
"ssri": "^8.0.1",
"terser-webpack-plugin": "^5.1.1", // 压缩插件
"thread-loader": "^3.0.0", // 多线程构建的loader
"vue-loader": "^17.0.0", // vue3的vue-loader
"vue-style-loader": "^4.1.3",
"webpack": "^5.54.0",
"webpack-bundle-analyzer": "^4.4.0", // webpack打包分析工具
"webpack-chain": "^6.5.1", // webpack配置的链式编程
"webpack-dev-server": "^4.7.3",
"webpack-merge": "^5.7.3", // 合并wenpack配置的工具
"webpack-virtual-modules": "^0.4.2",
"whatwg-fetch": "^3.6.2"
},
目录结构
.
├── bin
│ └── vue-cli-service.js // 程序入口文件
├── generator // 代码模板相关的
│ ├── index.js
│ ├── router.js
│ ├── template
│ │ ├── _gitignore
│ │ ├── public
│ │ │ ├── favicon.ico
│ │ │ └── index.html
│ │ └── src
│ │ ├── App.vue
│ │ ├── assets
│ │ │ └── logo.png
│ │ ├── components
│ │ │ └── HelloWorld.vue
│ │ └── main.js
│ └── vuex.js
├── lib
│ ├── commands // 可执行的一些命令
│ │ ├── build // 生产环境构建命令
│ │ │ ├── demo-lib.html
│ │ │ ├── demo-lib-js.html
│ │ │ ├── demo-wc.html
│ │ │ ├── entry-lib.js
│ │ │ ├── entry-lib-no-default.js
│ │ │ ├── formatStats.js
│ │ │ ├── index.js
│ │ │ ├── resolveAppConfig.js
│ │ │ ├── resolveLibConfig.js
│ │ │ ├── resolveWcConfig.js
│ │ │ ├── resolveWcEntry.js
│ │ │ └── setPublicPath.js
│ │ ├── help.js // help命令
│ │ ├── inspect.js // 生成webpack配置的命令
│ │ └── serve.js // 本地开发的命令
│ ├── config // webpack相关配置
│ │ ├── app.js // 主要是html-webpack-plugin的配置
│ │ ├── base.js // 基本webpack配置
│ │ ├── css.js // css相关的webpack配置
│ │ ├── index-default.html // 默认html模板
│ │ ├── prod.js // 生产环境的webpack配置
│ │ └── terserOptions.js // 压缩配置
│ ├── options.js // 默认配置
│ ├── PluginAPI.js // 插件和service之间的桥梁
│ ├── Service.js // vue-cli-service的类定义
│ ├── util // 工具方法
│ │ ├── getAssetPath.js
│ │ ├── getPadLength.js
│ │ ├── isAbsoluteUrl.js
│ │ ├── prepareProxy.js
│ │ ├── prepareURLs.js
│ │ ├── resolveClientEnv.js
│ │ ├── resolveLoaderError.js
│ │ ├── resolveLocal.js
│ │ └── validateWebpackConfig.js
│ └── webpack // 一些自定义的webpack插件
│ ├── analyzeBundle.js
│ ├── CorsPlugin.js
│ ├── DashboardPlugin.js
│ ├── ModernModePlugin.js
│ └── MovePlugin.js
├── LICENSE
├── logo.png
├── package.json
├── README.md
├── types
│ ├── index.d.ts
│ └── ProjectOptions.d.ts
└── webpack.config.js
入口文件
bin/vue-cli-service.js
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, {
// 下面的表示如果命令行有--modern,就表示modern: true
boolean: [
// build
'modern',
'report',
'report-json',
'inline-vue',
'watch',
// serve
'open',
'copy',
'https',
// inspect
'verbose'
]
})
// 获取要执行命令,例如serve,build...
const command = args._[0]
// 开始执行命令
service.run(command, args, rawArgv).catch(err => {
error(err)
process.exit(1)
})
初始化Services实例
- 加载package.json文件
- 加载插件
- 创建模式表
constructor-构造器
/**
*
* @param {*} context 当前工作的环境(目录)
* @param {*} param1 一些命令行传过来的参数(一般无用)
*/
constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
process.VUE_CLI_SERVICE = this
this.initialized = false
// 工作目录
this.context = context
// 命令行传过来的参数,一般没用
this.inlineOptions = inlineOptions
// chainWebpack的配置
this.webpackChainFns = []
// 原生的webpack配置
this.webpackRawConfigFns = []
// devServer的配置
this.devServerConfigFns = []
// 所有可执行的指令,commands文件夹里面
this.commands = {}
// package.json所在的环境(目录)
this.pkgContext = context
// 加载package.json
this.pkg = this.resolvePkg(pkg)
// 加载插件,包括构建的命令和配置,package.json安装的vue插件
this.plugins = this.resolvePlugins(plugins, useBuiltIn)
// 要忽略的插件的id结合
this.pluginsToSkip = new Set()
// 合并插件module.exports.defaultModes得到modes
this.modes = this.plugins.reduce((modes, { apply: { defaultModes } }) => {
return Object.assign(modes, defaultModes)
}, {})
}
resolvePlugins-加载插件
// 加载插件,包括构建的命令和配置,package.json安装的vue插件
resolvePlugins (inlinePlugins, useBuiltIn) {
const idToPlugin = (id, absolutePath) => ({
id: id.replace(/^.\//, 'built-in:'),
apply: require(absolutePath || id)
})
let plugins
const builtInPlugins = [
// 命令
'./commands/serve',
'./commands/build',
'./commands/inspect',
'./commands/help',
// webpack配置
'./config/base',
'./config/assets',
'./config/css',
'./config/prod',
'./config/app'
].map((id) => idToPlugin(id))
if (inlinePlugins) {
plugins = useBuiltIn !== false
? builtInPlugins.concat(inlinePlugins)
: inlinePlugins
} else {
// 加载package.json安装的插件
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 = loadModule(id, this.pkgContext)
if (!apply) {
warn(`Optional dependency ${id} is not installed.`)
apply = () => {}
}
return { id, apply }
} else {
// 一般情况这里返回
return idToPlugin(id, resolveModule(id, this.pkgContext))
}
})
// 将两组插件合并
plugins = builtInPlugins.concat(projectPlugins)
}
// 获取package.json配置中vuePlugins选项的依赖,一般不用
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)
})))
}
debug('vue:plugins')(plugins)
const orderedPlugins = sortPlugins(plugins)
debug('vue:plugins-ordered')(orderedPlugins)
return orderedPlugins
}
执行命令
- 加载环境变量
- 合并用户配置
- 运行插件
- 执行命令
run 方法
// 执行命令
async run (name, args = {}, rawArgv = []) {
// 一般是使用this.modes[name],开发是name为server,构建是name为build
const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])
// 要忽略的插件
this.setPluginsToSkip(args, rawArgv)
// 加载环境变量,合并用户配置,运行插件
await 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)
}
init 方法
init (mode = process.env.VUE_CLI_MODE) {
if (this.initialized) {
return
}
this.initialized = true
this.mode = mode
// 加载对应模式的.env文件
if (mode) {
this.loadEnv(mode)
}
// 加载基本的.env文件
this.loadEnv()
// 加载用户配置(vue.config.js)
const userOptions = this.loadUserOptions()
const loadedCallback = (loadedUserOptions) => {
// 合并用户配置和默认配置
this.projectOptions = defaultsDeep(loadedUserOptions, defaults())
// 校验配置
debug('vue:project-config')(this.projectOptions)
this.plugins.forEach(({ id, apply }) => {
if (this.pluginsToSkip.has(id)) return
apply(new PluginAPI(id, this), this.projectOptions)
})
// 加入配置里的chainWebpack
if (this.projectOptions.chainWebpack) {
this.webpackChainFns.push(this.projectOptions.chainWebpack)
}
// 加入配置里的configureWebpack
if (this.projectOptions.configureWebpack) {
this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
}
}
// 支持promise形式的配置
if (isPromise(userOptions)) {
return userOptions.then(loadedCallback)
} else {
return loadedCallback(userOptions)
}
}
loadEnv 方法
// 加载环境变量文件
loadEnv (mode) {
const logger = debug('vue:env')
const basePath = path.resolve(this.context, `.env${mode ? `.${mode}` : ``}`)
const localPath = `${basePath}.local`
const load = envPath => {
try {
// 读取.env文件的变量
const env = dotenv.config({ path: envPath, debug: process.env.DEBUG })
// 将env的值赋值到process.env
dotenvExpand(env)
logger(envPath, env)
} catch (err) {
// only ignore error if file is not found
if (err.toString().indexOf('ENOENT') < 0) {
error(err)
}
}
}
load(localPath)
load(basePath)
// by default, NODE_ENV and BABEL_ENV are set to "development" unless mode
// is production or test. However the value in .env files will take higher
// priority.
if (mode) {
// always set NODE_ENV during tests
// as that is necessary for tests to not be affected by each other
const shouldForceDefaultEnv = (
process.env.VUE_CLI_TEST &&
!process.env.VUE_CLI_TEST_TESTING_ENV
)
const defaultNodeEnv = (mode === 'production' || mode === 'test')
? mode
: 'development'
if (shouldForceDefaultEnv || process.env.NODE_ENV == null) {
// 将mode赋值给process.env.NODE_ENV
process.env.NODE_ENV = defaultNodeEnv
}
if (shouldForceDefaultEnv || process.env.BABEL_ENV == null) {
// 将mode赋值给process.env.BABEL_ENV
process.env.BABEL_ENV = defaultNodeEnv
}
}
}
PluginAPI
插件执行用到的工具类
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
}
get version () {
return require('../package.json').version
}
assertVersion (range) {
if (typeof range === 'number') {
if (!Number.isInteger(range)) {
throw new Error('Expected string or integer value.')
}
range = `^${range}.0.0-0`
}
if (typeof range !== 'string') {
throw new Error('Expected string or integer value.')
}
if (semver.satisfies(this.version, range, { includePrerelease: true })) return
throw new Error(
`Require @vue/cli-service "${range}", but was loaded with "${this.version}".`
)
}
// 当前工作目录
getCwd () {
return this.service.context
}
/**
* Resolve path for a project.
*
* @param {string} _path - Relative path from project root
* @return {string} The resolved absolute path.
*/
resolve (_path) {
return path.resolve(this.service.context, _path)
}
/**
* Check if the project has a given plugin.
*
* @param {string} id - Plugin id, can omit the (@vue/|vue-|@scope/vue)-cli-plugin- prefix
* @return {boolean}
*/
hasPlugin (id) {
return this.service.plugins.some(p => matchesPluginId(id, p.id))
}
/**
* 注册command,插入到service的commands中
* Register a command that will become available as `vue-cli-service [name]`.
*
* @param {string} name
* @param {object} [opts]
* {
* description: string,
* usage: string,
* options: { [string]: string }
* }
* @param {function} fn
* (args: { [string]: string }, rawArgs: string[]) => ?Promise
*/
registerCommand (name, opts, fn) {
if (typeof opts === 'function') {
fn = opts
opts = null
}
this.service.commands[name] = { fn, opts: opts || {} }
}
/**
* 修改chainWebpack的配置
* Register a function that will receive a chainable webpack config
* the function is lazy and won't be called until `resolveWebpackConfig` is
* called
*
* @param {function} fn
*/
chainWebpack (fn) {
this.service.webpackChainFns.push(fn)
}
/**
* 修改原生Webpack的配置
* Register
* - a webpack configuration object that will be merged into the config
* OR
* - a function that will receive the raw webpack config.
* the function can either mutate the config directly or return an object
* that will be merged into the config.
*
* @param {object | function} fn
*/
configureWebpack (fn) {
this.service.webpackRawConfigFns.push(fn)
}
/**
* Register a dev serve config function. It will receive the express `app`
* instance of the dev server.
*
* @param {function} fn
*/
configureDevServer (fn) {
this.service.devServerConfigFns.push(fn)
}
/**
* 将chainWebpack转成原生的webpack配置
* 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)
}
/**
* 执行webpackChainFns里面的方法
* Resolve an intermediate chainable webpack config instance, which can be
* further tweaked before generating the final raw webpack config.
* You can call this multiple times to generate different branches of the
* base webpack config.
* See https://github.com/mozilla-neutrino/webpack-chain
*
* @return {ChainableWebpackConfig}
*/
resolveChainableWebpackConfig () {
return this.service.resolveChainableWebpackConfig()
}
/**
* 生成cache-loader的配置
* Generate a cache identifier from a number of variables
*/
genCacheConfig (id, partialIdentifier, configFiles = []) {
const fs = require('fs')
const cacheDirectory = this.resolve(`node_modules/.cache/${id}`)
// replace \r\n to \n generate consistent hash
const fmtFunc = conf => {
if (typeof conf === 'function') {
return conf.toString().replace(/\r\n?/g, '\n')
}
return conf
}
// 缓存的hash值对象
const variables = {
partialIdentifier,
'cli-service': require('../package.json').version,
env: process.env.NODE_ENV,
test: !!process.env.VUE_CLI_TEST,
config: [
fmtFunc(this.service.projectOptions.chainWebpack),
fmtFunc(this.service.projectOptions.configureWebpack)
]
}
try {
variables['cache-loader'] = require('cache-loader/package.json').version
} catch (e) {
// cache-loader is only intended to be used for webpack 4
}
if (!Array.isArray(configFiles)) {
configFiles = [configFiles]
}
configFiles = configFiles.concat([
'package-lock.json',
'yarn.lock',
'pnpm-lock.yaml'
])
const readConfig = file => {
const absolutePath = this.resolve(file)
if (!fs.existsSync(absolutePath)) {
return
}
if (absolutePath.endsWith('.js')) {
// should evaluate config scripts to reflect environment variable changes
try {
return JSON.stringify(require(absolutePath))
} catch (e) {
return fs.readFileSync(absolutePath, 'utf-8')
}
} else {
return fs.readFileSync(absolutePath, 'utf-8')
}
}
variables.configFiles = configFiles.map(file => {
const content = readConfig(file)
return content && content.replace(/\r\n?/g, '\n')
})
// 转换成hash值
const cacheIdentifier = hash(variables)
return { cacheDirectory, cacheIdentifier }
}
}
插件
配置文件
base.js
module.exports = (api, options) => {
const cwd = api.getCwd()
const webpack = require('webpack')
const vueMajor = require('../util/getVueMajor')(cwd)
api.chainWebpack(webpackConfig => {
const isLegacyBundle = process.env.VUE_CLI_MODERN_MODE && !process.env.VUE_CLI_MODERN_BUILD
const resolveLocal = require('../util/resolveLocal')
// https://github.com/webpack/webpack/issues/14532#issuecomment-947525539
webpackConfig.output.set('hashFunction', 'xxhash64')
// https://github.com/webpack/webpack/issues/11467#issuecomment-691873586
webpackConfig.module
.rule('esm')
.test(/\.m?jsx?$/)
.resolve.set('fullySpecified', false)
// 入口和出口
webpackConfig
.mode('development')
.context(api.service.context)
.entry('app')
.add('./src/main.js')
.end()
.output
.path(api.resolve(options.outputDir))
.filename(isLegacyBundle ? '[name]-legacy.js' : '[name].js')
.publicPath(options.publicPath)
// 解析模块
webpackConfig.resolve
.extensions
.merge(['.mjs', '.js', '.jsx', '.vue', '.json', '.wasm'])
.end()
.modules
.add('node_modules')
.add(api.resolve('node_modules'))
.add(resolveLocal('node_modules'))
.end()
.alias
.set('@', api.resolve('src'))
// 解析loader
webpackConfig.resolveLoader
.modules
.add('node_modules')
.add(api.resolve('node_modules'))
.add(resolveLocal('node_modules'))
// 下面的包不引用其他包,所以不用去解析
webpackConfig.module
.noParse(/^(vue|vue-router|vuex|vuex-router-sync)$/)
// js is handled by cli-plugin-babel ---------------------------------------
// vue-loader --------------------------------------------------------------
let cacheLoaderPath
try {
cacheLoaderPath = require.resolve('cache-loader')
} catch (e) {}
if (vueMajor === 2) {
// for Vue 2 projects
const partialIdentifier = {
'vue-loader': require('@vue/vue-loader-v15/package.json').version,
'@vue/component-compiler-utils': require('@vue/component-compiler-utils/package.json').version
}
try {
partialIdentifier['vue-template-compiler'] = require('vue-template-compiler/package.json').version
} catch (e) {
// For Vue 2.7 projects, `vue-template-compiler` is not required
}
const vueLoaderCacheConfig = api.genCacheConfig('vue-loader', partialIdentifier)
webpackConfig.resolve
.alias
.set(
'vue$',
options.runtimeCompiler
? 'vue/dist/vue.esm.js'
: 'vue/dist/vue.runtime.esm.js'
)
if (cacheLoaderPath) {
webpackConfig.module
.rule('vue')
.test(/\.vue$/)
.use('cache-loader')
.loader(cacheLoaderPath)
.options(vueLoaderCacheConfig)
}
webpackConfig.module
.rule('vue')
.test(/\.vue$/)
.use('vue-loader')
.loader(require.resolve('@vue/vue-loader-v15'))
.options(Object.assign({
compilerOptions: {
whitespace: 'condense'
}
}, cacheLoaderPath ? vueLoaderCacheConfig : {}))
webpackConfig
.plugin('vue-loader')
.use(require('@vue/vue-loader-v15').VueLoaderPlugin)
// some plugins may implicitly relies on the `vue-loader` dependency path name
// such as vue-cli-plugin-apollo
// <https://github.com/Akryum/vue-cli-plugin-apollo/blob/d9fe48c61cc19db88fef4e4aa5e49b31aa0c44b7/index.js#L88>
// so we need a hotfix for that
webpackConfig
.resolveLoader
.modules
.prepend(path.resolve(__dirname, './vue-loader-v15-resolve-compat'))
} else if (vueMajor === 3) {
// for Vue 3 projects
const vueLoaderCacheConfig = api.genCacheConfig('vue-loader', {
'vue-loader': require('vue-loader/package.json').version
})
webpackConfig.resolve
.alias
.set(
'vue$',
options.runtimeCompiler
? 'vue/dist/vue.esm-bundler.js'
: 'vue/dist/vue.runtime.esm-bundler.js'
)
if (cacheLoaderPath) {
webpackConfig.module
.rule('vue')
.test(/\.vue$/)
.use('cache-loader')
.loader(cacheLoaderPath)
.options(vueLoaderCacheConfig)
}
webpackConfig.module
.rule('vue')
.test(/\.vue$/)
.use('vue-loader')
.loader(require.resolve('vue-loader'))
.options({
...vueLoaderCacheConfig,
babelParserPlugins: ['jsx', 'classProperties', 'decorators-legacy']
})
webpackConfig
.plugin('vue-loader')
.use(require('vue-loader').VueLoaderPlugin)
// feature flags <http://link.vuejs.org/feature-flags>
webpackConfig
.plugin('feature-flags')
.use(webpack.DefinePlugin, [{
__VUE_OPTIONS_API__: 'true',
__VUE_PROD_DEVTOOLS__: 'false'
}])
}
// https://github.com/vuejs/vue-loader/issues/1435#issuecomment-869074949
webpackConfig.module
.rule('vue-style')
.test(/\.vue$/)
.resourceQuery(/type=style/)
.sideEffects(true)
// Other common pre-processors ---------------------------------------------
const maybeResolve = name => {
try {
return require.resolve(name)
} catch (error) {
return name
}
}
webpackConfig.module
.rule('pug')
.test(/\.pug$/)
.oneOf('pug-vue')
.resourceQuery(/vue/)
.use('pug-plain-loader')
.loader(maybeResolve('pug-plain-loader'))
.end()
.end()
.oneOf('pug-template')
.use('raw')
.loader(maybeResolve('raw-loader'))
.end()
.use('pug-plain-loader')
.loader(maybeResolve('pug-plain-loader'))
.end()
.end()
const resolveClientEnv = require('../util/resolveClientEnv')
webpackConfig
.plugin('define')
.use(webpack.DefinePlugin, [
resolveClientEnv(options)
])
webpackConfig
.plugin('case-sensitive-paths')
.use(require('case-sensitive-paths-webpack-plugin'))
// friendly error plugin displays very confusing errors when webpack
// fails to resolve a loader, so we provide custom handlers to improve it
const { transformer, formatter } = require('../util/resolveLoaderError')
webpackConfig
.plugin('friendly-errors')
.use(require('@soda/friendly-errors-webpack-plugin'), [{
additionalTransformers: [transformer],
additionalFormatters: [formatter]
}])
const TerserPlugin = require('terser-webpack-plugin')
const terserOptions = require('./terserOptions')
webpackConfig.optimization
.minimizer('terser')
.use(TerserPlugin, [terserOptions(options)])
})
}
命令
server.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) {
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.
// 是否在docker容器中
const isInContainer = checkInContainer()
// 是否生产环境
const isProduction = process.env.NODE_ENV === 'production'
const url = require('url')
const { chalk } = require('@vue/cli-shared-utils')
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')
// configs that only matters for dev server
api.chainWebpack(webpackConfig => {
if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
webpackConfig
.devtool('cheap-module-eval-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'))
}
}
})
// resolve webpack config
const webpackConfig = api.resolveWebpackConfig()
// check for common config errors
validateWebpackConfig(webpackConfig, api, options)
// load user devServer options with higher priority than devServer
// in webpack config
// 合并配置
const projectDevServerOptions = Object.assign(
webpackConfig.devServer || {},
options.devServer
)
// 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)
}
}
// resolve server options
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 localUrlForBrowser = publicUrl || urls.localUrlForBrowser
const proxySettings = prepareProxy(
projectDevServerOptions.proxy,
api.resolve('public')
)
// inject dev & hot-reload middleware entries
if (!isProduction) {
const sockjsUrl = publicUrl
// explicitly configured via devServer.public
? `?${publicUrl}/sockjs-node`
: isInContainer
// can't infer public network url if inside a container...
// use client-side inference (note this would break with non-root publicPath)
? ``
// otherwise infer the url
: `?` + 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 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
}))
;['SIGINT', 'SIGTERM'].forEach(signal => {
process.on(signal, () => {
server.close(() => {
process.exit(0)
})
})
})
if (args.stdin) {
process.stdin.on('end', () => {
server.close(() => {
process.exit(0)
})
})
process.stdin.resume()
}
// on appveyor, killing the process with SIGTERM causes execa to
// throw error
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)
})
}
})
}
return new Promise((resolve, reject) => {
// log instructions & open browser on first compilation complete
let isFirstCompile = true
compiler.hooks.done.tap('vue-cli-service serve', stats => {
if (stats.hasErrors()) {
return
}
let copied = ''
if (isFirstCompile && args.copy) {
try {
require('clipboardy').writeSync(localUrlForBrowser)
copied = chalk.dim('(copied to clipboard)')
} catch (_) {
/* catch exception if copy to clipboard isn't supported (e.g. WSL), see issue #3476 */
}
}
const networkUrl = publicUrl
? publicUrl.replace(/([^/])$/, '$1/')
: urls.lanUrlForTerminal
console.log()
console.log(` App running at:`)
console.log(` - Local: ${chalk.cyan(urls.localUrlForTerminal)} ${copied}`)
if (!isInContainer) {
console.log(` - Network: ${chalk.cyan(networkUrl)}`)
} else {
console.log()
console.log(chalk.yellow(` It seems you are running Vue CLI inside a container.`))
if (!publicUrl && options.publicPath && options.publicPath !== '/') {
console.log()
console.log(chalk.yellow(` Since you are using a non-root publicPath, the hot-reload socket`))
console.log(chalk.yellow(` will not be able to infer the correct URL to connect. You should`))
console.log(chalk.yellow(` explicitly specify the URL via ${chalk.blue(`devServer.public`)}.`))
console.log()
}
console.log(chalk.yellow(` Access the dev server via ${chalk.cyan(
`${protocol}://localhost:<your container's external mapped port>${options.publicPath}`
)}`))
}
console.log()
if (isFirstCompile) {
isFirstCompile = false
if (!isProduction) {
const buildCommand = hasProjectYarn(api.getCwd()) ? `yarn build` : hasProjectPnpm(api.getCwd()) ? `pnpm run build` : `npm run build`
console.log(` Note that the development build is not optimized.`)
console.log(` To create a production build, run ${chalk.cyan(buildCommand)}.`)
} else {
console.log(` App is served in production mode.`)
console.log(` Note this is for preview or E2E testing only.`)
}
console.log()
if (args.open || projectDevServerOptions.open) {
const pageUri = (projectDevServerOptions.openPage && typeof projectDevServerOptions.openPage === 'string')
? projectDevServerOptions.openPage
: ''
openBrowser(localUrlForBrowser + pageUri)
}
// Send final app URL
if (args.dashboard) {
const ipc = new IpcMessenger()
ipc.send({
vueServe: {
url: localUrlForBrowser
}
})
}
// resolve returned Promise
// so other commands can do api.service.run('serve').then(...)
resolve({
server,
url: localUrlForBrowser
})
} else if (process.env.VUE_CLI_TEST) {
// signal for test to check HMR
console.log('App updated')
}
})
server.listen(port, host, err => {
if (err) {
reject(err)
}
})
})
})
}
function addDevClientToEntry (config, devClient) {
const { entry } = config
if (typeof entry === 'object' && !Array.isArray(entry)) {
Object.keys(entry).forEach((key) => {
entry[key] = devClient.concat(entry[key])
})
} else if (typeof entry === 'function') {
config.entry = entry(devClient)
} else {
config.entry = devClient.concat(entry)
}
}
// https://stackoverflow.com/a/20012536
function checkInContainer () {
const fs = require('fs')
if (fs.existsSync(`/proc/1/cgroup`)) {
const content = fs.readFileSync(`/proc/1/cgroup`, 'utf-8')
return /:\/(lxc|docker|kubepods)\//.test(content)
}
}
function genHistoryApiFallbackRewrites (baseUrl, pages = {}) {
const path = require('path')
const multiPageRewrites = Object
.keys(pages)
// sort by length in reversed order to avoid overrides
// eg. 'page11' should appear in front of 'page1'
.sort((a, b) => b.length - a.length)
.map(name => ({
from: new RegExp(`^/${name}`),
to: path.posix.join(baseUrl, pages[name].filename || `${name}.html`)
}))
return [
...multiPageRewrites,
{ from: /./, to: path.posix.join(baseUrl, 'index.html') }
]
}
module.exports.defaultModes = {
serve: 'development'
}
----------------下方加我,要面试资料--------------------------