执行npm run serve命令时做了哪些内容?
执行comman.fn()方法时执行的函数,即
function microServeCommand(api) {
api.registerCommand({
name: 'serve',
description: '启动服务命令',
fn: async function (args) {
api.chainWebpack(config => {
config.plugin('circularDeps').use(new CircularDependencyPlugin({
exclude: /.zmi/
}));
});
await invokeCliService('serve', api, args, service => {
const oldUserOptions = service.loadUserOptions();
const userConfig = api.getUserConfig();
service.loadUserOptions = () => {
// 可以看出来返回的是一个webpack的打包配置
return {
...oldUserOptions,
publicPath: process.env.NODE_ENV === 'production' ? './' : `/e-static/${userConfig.appId}/default/`,
devServer: Object.assign({}, oldUserOptions.devServer, {
headers: {
'Access-Control-Allow-Origin': '*'
},
client: {
overlay: false
}
}, userConfig.devServer ?? {}),
configureWebpack: merge(oldUserOptions.configureWebpack || {}, {
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
vuex: 'Vuex',
axios: 'axios',
},
output: {
library: `${userConfig.appId}-[name]`,
libraryTarget: 'umd'
}
})
};
};
});
}
});
}
invokeCliService实现,onProcess为命令执行过程中执行函数,afterServiceRun为执行完该命令后的回调函数
async function invokeCliService(command, api, cliArgs, onProcess, afterServiceRun) {
// 执行所有key为onGenerateFiles的钩子函数->执行applyPlugin方法
await api.service.applyPlugin({
key: 'onGenerateFiles',
execType: 'asyncParallel'
});
// 生成模板文件
await processHTML.processHTML(api);
const userConfig = api.getUserConfig();
userConfig.chainWebpack && api.service.webpackChainFns.push(userConfig.chainWebpack);
// 创建一个服务,用来自定义配置及构建脚本等
const service = new VueCliService(process.cwd());
service.plugins.push({ id: '@vue/cli-plugin-babel', apply: require('@vue/cli-plugin-babel') }, { id: '@vue/cli-plugin-eslint', apply: require('@vue/cli-plugin-eslint') }, { id: '@vue/cli-plugin-typescript', apply: require('@vue/cli-plugin-typescript') });
service.loadUserOptions = () => ({
chainWebpack: (config) => {
api.service.webpackChainFns.forEach(fn => fn(config));
}
});
onProcess && onProcess(service);
// ... serve阶段添加临时文件热更新
// _内容即为临时生成的文件的入口文件
service
.run(command, {
...cliArgs,
_: [command, path.join(api.service.appData.paths.absTmpPath, 'entry.ts')]
})
.then(() => {
afterServiceRun && afterServiceRun();
//触发afterCommandDone钩子
api.service.applyPlugin({
key: 'afterCommandDone',
initialValue: { command },
execType: 'asyncParallel'
});
})
.catch((err) => {
console.error(err);
process.exit(1);
});
}
packages\core\lib\service.ts
// 触发插件事件
applyPlugin(opts: { key: string; initialValue?: unknown; execType?: ExecType }) {
const hooksForKey = this.hooks[opts.key] || []
// 异步并行
if (opts.execType === 'asyncParallel') {
// tapable库AsyncParallelHook是一种钩子类型,用于异步并行地执行多个回调函数。['_']标识钩子函数会接受一个参数,一个下划线作为占位符,表示参数在执行钩子时会被传递给所有的回调函数
const tEvent = new AsyncParallelHook(['_'])
for (const hook of hooksForKey) {
tEvent.tapPromise({ name: hook.plugin.key }, hook.fn as (args_0: unknown) => Promise<void>)
}
return tEvent.promise(opts.initialValue)
}
// 同步执行
if (opts.execType === 'sync') {
const tEvent = new SyncWaterfallHook(['_'])
for (const hook of hooksForKey) {
tEvent.tap({ name: hook.plugin.key }, hook.fn as (args_0: unknown) => unknown)
}
return tEvent.call(opts.initialValue)
}
// 异步串行
const tEvent = new AsyncSeriesWaterfallHook(['_'])
for (const hook of hooksForKey) {
tEvent.tapPromise({ name: hook.plugin.key }, hook.fn as (args_0: unknown) => Promise<unknown>)
}
return tEvent.promise(opts.initialValue)
}
所有的hooks
查找到所有key为onGenerateFiles的hook
过滤后的hooks有18个
packages\presets-pc\commands\processHTML.ts
export const processHTML = async (api: PluginApi) => {
// 获取用户配置信息
const { userConfig } = api.service.getAppData()
const headScripts = (await api.service.applyPlugin({
key: 'addHTMLHeadScripts',
initialValue: userConfig.headScripts || []
})) as string[]
const scripts = (await api.service.applyPlugin({
key: 'addHTMLScripts',
initialValue: userConfig.scripts || []
})) as string[]
// 创建一系列页面展示需要的内容
const favicon = api.service.applyPlugin({
key: 'modifyFavicon',
initialValue: userConfig.favicon || `${userConfig.publicPath || '/'}favicon.ico`,
execType: 'sync'
})
// 创建模板文件
const tpl = `
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<head>
<link rel="shortcut icon" href="{{ favicon }}">
{{ #links }} {{{.}}} {{ /links }}
{{ #styles }} {{{.}}} {{ /styles }}
{{ #headScripts }} {{{.}}} {{ /headScripts }}
</head>
<title>{{ title }}</title>
</head>
<body>
<noscript>
<strong>We're sorry but {{ title }} doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="${userConfig.appId}"></div>
<!-- built files will be auto injected -->
{{ #scripts }} {{{.}}} {{ /scripts }}
</body>
`
validate('headScripts', headScripts, scriptRegex)
validate('styles', styles, styleRegex)
validate('scripts', scripts, scriptRegex)
validate('links', links, linkRegex)
const content = mustache.render(tpl, { title, headScripts, styles, scripts, links, favicon })
api.chainWebpack(config => {
config.plugin('html').tap(args => {
args[0].templateContent = content
delete args[0].template
return args
})
})
}
userConfig内容和zmi.config.ts内容一致
VueCliService
3. 使用场景
- 自定义构建脚本:如果您需要在构建过程中执行一些自定义的逻辑,可以通过创建
VueCliService实例并使用其提供的方法来扩展或修改默认的构建行为。 - 启动开发服务器:
VueCliService实例还提供了启动 Vue 开发服务器的方法,这对于需要在特定环境下调试或预览 Vue 应用非常有用。 - 构建配置:通过
VueCliService实例,您可以访问和修改 Vue 项目的构建配置,以适应特定的构建需求。
由此可见,zmi的启动过程主要是通过插件来实现的,不同的插件实现不同的功能,通过插件集成各种内置配置生成模板文件,最后通过VueCliService来自定义构建配置