2025.01 zmi创建服务流程-2

64 阅读3分钟

接上节

执行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

image-20250212211726348.png

查找到所有keyonGenerateFileshook

过滤后的hooks有18个

image-20250212211946627.png

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内容一致

image-20250212211946627.png

VueCliService

3. 使用场景

  • 自定义构建脚本:如果您需要在构建过程中执行一些自定义的逻辑,可以通过创建 VueCliService 实例并使用其提供的方法来扩展或修改默认的构建行为。
  • 启动开发服务器VueCliService 实例还提供了启动 Vue 开发服务器的方法,这对于需要在特定环境下调试或预览 Vue 应用非常有用。
  • 构建配置:通过 VueCliService 实例,您可以访问和修改 Vue 项目的构建配置,以适应特定的构建需求。

由此可见,zmi的启动过程主要是通过插件来实现的,不同的插件实现不同的功能,通过插件集成各种内置配置生成模板文件,最后通过VueCliService来自定义构建配置