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
,可知接下来Service
的run
方法肯定会去执行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
目录下中:
那这就明了多了。好的,我们就来看看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 || {}}
}
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服务器)、配置信息的加载、服务监听的打开。