umi源码阅读2:实例化过程

552 阅读7分钟

umi阅读分为5个部分,分别是:

以下代码以umi的3.5.20版本为例,主要内容以源码+个人解读为主

上一篇:umi源码阅读1:启动过程

实例化过程

内容:在上一模块的基础上,发现umi的核心类是Service,接下来内容主要探究Service是干什么,实例化的过程以及调用run()的过程。

找到packages/core,该包index文件导出了一系列的对象,目前我们只关注Service,于是找到packages/core/Serive/Serive,代码较多,一步步来解读。

export default class Service extends EventEmitter{
  constructor(opts: IServiceOpts){
    // 获取一些环境参数
    this.cwd = opts.cwd || process.cwd();
    this.pkg = opts.pkg || this.resolvePackage();
    this.env = opts.env || process.env.NODE_ENV;

    // 注册babel
    this.babelRegister = new BabelRegister();

    // 获取用户配置(未校验)
    const configFiles = opts.configFiles;
    this.configInstance = new Config({
      cwd: this.cwd,
      service: this,
      localConfig: this.env === 'development',
      configFiles:
        Array.isArray(configFiles) && !!configFiles[0]
          ? configFiles
          : undefined,
    });
    this.userConfig = this.configInstance.getUserConfig();

    // 初始化插件
    const baseOpts = {
      pkg: this.pkg,
      cwd: this.cwd,
    };
    // 通过pathToObj返回后的presets的合并对象
    this.initialPresets = resolvePresets({
      ...baseOpts,
      presets: opts.presets || [],
      userConfigPresets: this.userConfig.presets || [],
    });
    通过pathToObj返回后的plugins的合并对象
    this.initialPlugins = resolvePlugins({
      ...baseOpts,
      plugins: opts.plugins || [],
      userConfigPlugins: this.userConfig.plugins || [],
    });
		// ...
  }
}

首先Serive继承了EventEmitter,EventEmitter是一个事件总线的库,用于事件订阅触发的操作,表明service是也是一个事件总线管理的库。

在获取用户配置步骤,实例化了config实例,传递了相关的serive实例,接着getUserConfig(),这里是根据umi提供的获取配置的各种规则获取到用户定义的当前环境下的配置。

规则:从以下几个文件中加载配置信息,优先级是'.umirc.ts','.umirc.js','config/config.ts','config/config.js'

总:service的过程就是获取相关的配置和初始化插件。

回答:defineConfig的返回值如何传递给Service?

umi根据文件路径规则获取到导出的默认的config。为什么这样做呢?umi框架的一大特点是约定大于配置,也就是说按照他说明的格式创建文件即可开通和使用功能,比如创建mock文件夹则开启mock数据功能。 好处有:项目代码结构清晰,需要什么知道找到文件夹/文件即可;减少配置的负担。

Service实例化完了,按照启动流程,service会执行service.run(),找到run函数

   args._ = args._ || [];
    // shift the command itself
    if (args._[0] === name) args._.shift();

    this.args = args;
  	// 初始化
    await this.init();
  	// 设置状态
    this.setStage(ServiceStage.run);
		// 启动onStart插件
    await this.applyPlugins({
      key: 'onStart',
      type: ApplyPluginsType.event,
      args: {
        name,
        args,
      },
    });
  	// 运行dev命令
    return this.runCommand({ name, args });
  }

run主要操作:初始化,设置状态,然后启用onStartplugin,最后运行命令。一一来看。

init是初始化各种插件,并依次调用onPluginReadymodifyDefaultConfigmodifyConfigmodifyPaths,同时初始化了hooks,hooks的作用和来自哪里后续继续研究。

umi的另一特色就是umi本身的功能也是通过插件来实现的,因此内置了许多处理事件的插件,具体的插件使用后续研究。 先看插件调用的写法

import { Plugin, ApplyPluginsType } from 'umi';

// 注册插件
Plugin.register({
  apply: { dva: { foo: 1 } },
  path: 'foo',
});
Plugin.register({
  apply: { dva: { bar: 1 } },
  path: 'bar',
});

// 执行插件
// 得到 { foo: 1, bar: 1 }
Plugin.applyPlugins({
  key: 'dva',
  type: ApplyPluginsType.modify,
  initialValue: {},
  args: {},
  async: false,
});

参数属性包含:

  • key,坑位的 key
  • type,执行方式类型,详见 ApplyPluginsType
  • initialValue,初始值
  • args,参数
  • async,是否异步执行且返回 Promise

ApplyPluginsType

主要在插件利用,项目代码中一般用不到。

运行时插件执行类型,enum 类型,包含三个属性:

  • compose,用于合并执行多个函数,函数可决定前序函数的执行时机
  • modify,用于修改值
  • event,用于执行事件,前面没有依赖关系

前面执行的plugins,从名字和备注可看出,具体的实现需要研究具体的插件实现。

onPluginReady:event,触发插件准备事件。

modifyDefaultConfig:modify,修改默认配置

modifyConfig:modify,修改配置并校验

modifyPaths:modify,修改outputPath路径

onStart:event,触发启动事件

最后一行代码是runCommand,根据名字是执行命令的,而我们的命令是umi dev

async runCommand({ name, args = {} }: { name: string; args?: any }) {
    args._ = args._ || [];
    // shift the command itself
    if (args._[0] === name) args._.shift();

    const command =
      typeof this.commands[name] === 'string'
        ? this.commands[this.commands[name] as string]
        : this.commands[name];
  
    const { fn } = command as ICommand;
    return fn({ args });
  }

按照我们的命令,应该是从this.commands对象中取出了dev的命令函数执行,那找找commands对象。在Service文件中,没有找到commonds包含了dev对象地方,同样从入口文件走过来,代码执行了一系列的plugins,显然,是在那里注册了这些命令和插件。那么接下来找找注册命令的地方,完成umi dev的流程。

v3.umijs.org/zh-CN/plugi… 官网提供了registerCommand的命令,显然是用来注册命令的,那dev应该是通过这个方法注册了命令,那在什么时候注册的呢?往回走packages/umi/src/ServiceWithBuiltIn.ts入口文件实例化service的时候,提供了一个@umijs/preset-built-in的presets,然后在packages/preset-built-in/src/index.ts找到了提供的一系列内置插件和命令

export default function () {
  return {
    plugins: [
    	//...
      // commands
      require.resolve('./plugins/commands/dev/dev'),
    ],
  };
}

显然在初始化插件集的过程中,同时把dev命令注册了进去。

看到插件地址dev文件

// 创建插件的方式,会默认注入一个api对象,可通过api对象创建自己的插件,显然这里提供的也是dev的插件
export default (api: IApi) => {
    //...
    // 注册dev命令
    api.registerCommand({
    	name: 'dev',
    	description: 'start a dev server for development',
    	fn: async function ({ args }) {'
        // 具体操作  
        // 环境参数获取

        // 开启热更新
        if(watch){
          // 监听package.json变化
          const unwatchPkg = watchPkg({
            cwd: api.cwd,
            onChange() {
              console.log();
              api.restartServer();
            },
          });
          unwatchs.push(unwatchPkg);
          
          // 监听config变化
          const unwatchConfig = api.service.configInstance.watch({
          userConfig: api.service.userConfig,
          onChange: async ({ pluginChanged, userConfig, valueChanged }) => {
            // 监听变化,决定是否需要重启服务
          }
        }
      }

      // dev 上面是dev的准备工作,下面是dev执行核心
      // 1.调用webpack方法,获取webpack实例化编译实例complier
      // 2.使用webpack-dev-middleware封装complier,并处理成server能接受的结构
      const { bundler, bundleConfigs, bundleImplementor } =
        await getBundleAndConfigs({ api, port });
      const opts: IServerOpts = bundler.setupDevServerOpts({
        bundleConfigs: bundleConfigs,
        bundleImplementor,
      });

    	
      const beforeMiddlewares = [
        ...(await api.applyPlugins({
          key: 'addBeforeMiddewares',
          type: api.ApplyPluginsType.add,
          initialValue: [],
          args: {},
        })),
        ...(await api.applyPlugins({
          key: 'addBeforeMiddlewares',
          type: api.ApplyPluginsType.add,
          initialValue: [],
          args: {},
        })),
      ];
      const middlewares = [
        ...(await api.applyPlugins({
          key: 'addMiddewares',
          type: api.ApplyPluginsType.add,
          initialValue: [],
          args: {},
        })),
        ...(await api.applyPlugins({
          key: 'addMiddlewares',
          type: api.ApplyPluginsType.add,
          initialValue: [],
          args: {},
        })),
      ];
                                     
			// 实例化server
      server = new Server({
        ...opts,
        compress: true,
        https: !!isHTTPS,
        headers: {
          'access-control-allow-origin': '*',
        },
        proxy: api.config.proxy,
        beforeMiddlewares,
        afterMiddlewares: [
          ...middlewares,
          createRouteMiddleware({ api, sharedMap }),
        ],
        ...(api.config.devServer || {}),
      });
      const listenRet = await server.listen({
        port,
        hostname,
      });
      return {
        ...listenRet,
        compilerMiddleware: opts.compilerMiddleware,
        destroy,
      };                  
    })
}

dev命令进行了文件变更监听,初始化编译中间件,实例化服务网络server。

了解上面的内容,需要了解一些前置内容:webpack和webpack-dev-middleware

我们知道umi默认采用的是webpack编译,而webpack-dev-middleware则是实现热更新的基础库,webpack-dev-middleware是一个容器,可以把webpack处理后的文件传递给一个服务器,特点如下:

1.监听资源变更,如果代码变化,则停止提供旧版bundle然后自动打包,直到编译完成

2.快速编译,打包后文件直接写入内存

webpack-dev-middleware的简单使用

const webpack = require("webpack");
const express = require("express");
const WebpackDevMiddleware = require("webpack-dev-middleware");
const app = express();

const compiler = webpack(config);
app.use(WebpackDevMiddleware(compiler,{
	//...各种配置
}));
app.listen(8000,function(){
	console.log("example app listening on port 8000\n")
})

明白了webpack-dev-middleware的简单使用之后,再来看dev命令中的代码,结构就清晰了,初始化了一个bundler,然后调用budler.setupDevServerOpts来构建server需要的对象,对象中包含了编译中间件。

packages/bundler-webpack/index.ts中的setupDevServerOpts函数

setupDevServerOpts({
    bundleConfigs,
    bundleImplementor = defaultWebpack,
  }: {
    bundleConfigs: defaultWebpack.Configuration[];
    bundleImplementor?: typeof defaultWebpack;
  }): IServerOpts {
    // 实例化webpack的complier
  	const compiler = bundleImplementor.webpack(bundleConfigs);
    const { ssr, devServer } = this.config;
    // 构建webpackDevMiddleware中间件
    const compilerMiddleware = webpackDevMiddleware(compiler, {
      // must be /, otherwise it will exec next()
      publicPath: '/',
      logLevel: 'silent',
      // if `ssr` set false, next() into server-side render
      ...(ssr ? { index: false } : {}),
      writeToDisk: devServer && devServer?.writeToDisk,
      watchOptions: {
        // not watch outputPath dir and node_modules
        ignored: this.getIgnoredWatchRegExp(),
      },
    });
  	// 其他代码
}

而在packages/server/Server/Server.ts中,可以看到server使用了这个编译中间件

compilerMiddleware: () => {
        if (this.opts.compilerMiddleware) {
          // @ts-ignore
          this.app.use(this.opts.compilerMiddleware);
        }
      },

webpack热更新的内容比较多,这里只提炼了核心的流程。

umi源码阅读3:渲染过程

参考资料:

UMI3源码解析系列之构建原理_大转转FE的博客-CSDN博客

umi源码解析

if 我是前端Leader,谈谈前端框架体系建设 - 掘金