likes
comments
collection
share

Taro源码-cli项目创建的过程

作者站长头像
站长
· 阅读数 17

入口

基于Taro3.5.5找到创建taro项目的入口文件(packages/taro-cli/bin/taro)

// packages/taro-cli/bin/taro
require('../dist/util').printPkgVersion()

const CLI = require('../dist/cli').default
new CLI().run()

Cli

packages/taro-cli/src/cli.ts这个文件的作用就是接受内置命令分解内置命令针对不同的内置命令注册对应的命令插件

首先初始化的时候获取我们的项目路劲

  // packages/taro-cli/src/cli.ts
  constructor (appPath) {
    this.appPath = appPath || process.cwd()
  }

在bin/taro文件里执行new CLI().run()在cli中看到run方法执行了this.parseArgs()parseArgs方法里做的第一件是就是接受内置命令以及分解内置命令

    // packages/taro-cli/src/cli.ts
    const args = minimist(process.argv.slice(2), {
      alias: {
        version: ['v'],
        help: ['h'],
        port: ['p'],
        resetCache: ['reset-cache'], // specially for rn, Removes cached files.
        publicPath: ['public-path'], // specially for rn, assets public path.
        bundleOutput: ['bundle-output'], // specially for rn, File name where to store the resulting bundle.
        sourcemapOutput: ['sourcemap-output'], // specially for rn, File name where to store the sourcemap file for resulting bundle.
        sourceMapUrl: ['sourcemap-use-absolute-path'], // specially for rn, Report SourceMapURL using its full path.
        sourcemapSourcesRoot: ['sourcemap-sources-root'], // specially for rn, Path to make sourcemaps sources entries relative to.
        assetsDest: ['assets-dest'] // specially for rn, Directory name where to store assets referenced in the bundle.
      },
      boolean: ['version', 'help']
    })

打印args的结果可得Taro源码-cli项目创建的过程args._里就是我们的命令这里的aaaaa可以替换成为init、build、inspect这些taro认识的命令

    // packages/taro-cli/src/cli.ts
    const _ = args._
    const command = _[0]
    switch (command) {
        case 'inspect':
        case 'build': {...} // 构建项目
        case 'init': {...} // 创建项目
        ...
      }

bbbbb就是替换我们的项目名称,具体的cli命令可以参考:https://taro-docs.jd.com/taro/docs/cli

当项目能获取的command时获取一些文件的路径(如图已加上路径备注)

      // packages/taro-cli/src/cli.ts
      // 项目路径
      const appPath = this.appPath
      // 插件集路径 taro-cli/src/presets
      const presetsPath = path.resolve(__dirname, 'presets')
      // 插件集路径下的commands taro-cli/src/presets/commands
      const commandsPath = path.resolve(presetsPath, 'commands')
      // 插件集路径下的platforms taro-cli/src/presets/platforms
      const platformsPath = path.resolve(presetsPath, 'platforms')

获取commands文件夹下的文件

   // packages/taro-cli/src/cli.ts
   const commandPlugins = fs.readdirSync(commandsPath)
   const targetPlugin = `${command}.js`

打印commandPlugins是presets/commands文件名数组,数组的各个文件对应不同的taro命令插件Taro源码-cli项目创建的过程

根据命令找到对应的命令插件,就将插件放到kernel(第二个重要的文件Kernel类)

      // packages/taro-cli/src/cli.ts
      // 针对不同的内置命令注册对应的命令插件
      if (commandPlugins.includes(targetPlugin)) {
        kernel.optsPlugins.push(path.resolve(commandsPath, targetPlugin))
      }

当然在将命令插件放到kernel之前还有设置环境变量、实例化Kernel类

之后就是放入到 customCommand 执行 kernel.run (以init为例)customCommand函数主要作用就是整理配置执行kernel.run方法

        // packages/taro-cli/src/cli.ts
        case 'init': {
          // 初始化创建
          customCommand(command, kernel, {
            _,
            appPath,
            projectName: _[1] || args.name,
            description: args.description,
            typescript: args.typescript,
            templateSource: args['template-source'],
            clone: !!args.clone,
            template: args.template,
            css: args.css,
            h: args.h
          })
          break
        }

Kernel

packages/taro-service/src/Kernel.tsKernel类继承自继承EventEmitter,是Taro-Cli的核心类之一,主要的作用是初始化项目配置、参数;收集项目的插件集和插件(插件化机制);修改webpack;执行钩子函数

在cli中Kernel实例化时首先就是初始化项目配置,也就是你config目录配置的那些,初始化项目资源目录,例如:输出目录、依赖目录,src、config配置目录等,部分配置是在你项目的config/index.js中的config中配置的东西,如sourcePath和outputPathhttps://taro-docs.jd.com/taro/docs/plugin 插件环境变量

   // packages/taro-service/src/Kernel.ts
  const kernel = new Kernel({
        appPath,
        presets: [
          path.resolve(__dirname, '.', 'presets', 'index.js')
        ],
        plugins: []
      })
      kernel.optsPlugins ||= []

1.核心的方法run方法大致的执行流程

// packages/taro-service/src/Kernel.ts
async run (args: string | { name: string, opts?: any }) {
    ...
    // 设置参数,前面cli.ts中传入的一些项目配置信息参数,例如isWatch等
    this.setRunOpts(opts)
    // 重点:初始化插件集和插件
    this.initPresetsAndPlugins()
    // 注意:Kernel 的前两个生命周期钩子是 onReady 和 onStart,并没有执行操作,开发者在自己编写插件时可以注册对应的钩子
    // 执行onStart钩子
    await this.applyPlugins('onReady')
    await this.applyPlugins('onStart')
    // 处理 --help 的日志输出 例如:taro build --help
    if (opts?.isHelp) {
      return this.runHelp(name)
    }
    // 获取平台配置
    if (opts?.options?.platform) {
      opts.config = this.runWithPlatform(opts.options.platform)
      // 执行钩子函数 modifyRunnerOpts
      // 作用:修改webpack参数,例如修改 H5 postcss options
      await this.applyPlugins({
        name: 'modifyRunnerOpts',
        opts: {
          opts: opts?.config
        }
      })
    }
    // 执行传入的命令这里是init
    await this.applyPlugins({
      name,
      opts
    })
  }

上述中提及了三个钩子方法,打印出来可以知道钩子方法有很多

Map(15) {
  'onReady' => [ [Function: bound ] ],
  'onStart' => [ [Function: bound ] ],
  'modifyWebpackChain' => [ [Function: bound ] ],
  'modifyBuildAssets' => [ [Function: bound ] ],
  'modifyMiniConfigs' => [ [Function: bound ] ],
  'modifyComponentConfig' => [ [Function: bound ] ],
  'onCompilerMake' => [ [Function: bound ] ],
  'onParseCreateElement' => [ [Function: bound ] ],
  'onBuildStart' => [ [Function: bound ] ],
  'onBuildFinish' => [ [Function: bound ] ],
  'onBuildComplete' => [ [Function: bound ] ],
  'modifyRunnerOpts' => [ [Function: bound ] ],
  'writeFileToDist' => [ [Function (anonymous)] ],
  'generateProjectConfig' => [ [Function (anonymous)] ],
  'generateFrameworkInfo' => [ [Function (anonymous)] ]
}

2.初始化initPresetsAndPlugins方法(官方的备注也很详细)

  // packages/taro-service/src/Kernel.ts
  initPresetsAndPlugins () {
    const initialConfig = this.initialConfig
    // 收集了所有的插件集和插件集合。
    const allConfigPresets = mergePlugins(this.optsPresets || [], initialConfig.presets || [])()
    const allConfigPlugins = mergePlugins(this.optsPlugins || [], initialConfig.plugins || [])()
    ...
    process.env.NODE_ENV !== 'test' && createSwcRegister({
      only: [...Object.keys(allConfigPresets), ...Object.keys(allConfigPlugins)]
    }) // babel转化

    this.plugins = new Map()
    this.extraPlugins = {}

    // 加载插件集和插件导出对应的每一个 plugin 都包含了一个 apply 函数,执行该该函数可以导出对应的 Plugin 模块
    this.resolvePresets(allConfigPresets)
    this.resolvePlugins(allConfigPlugins)
  }

打印 this.plugin 执行了apply就能导出对应的Plugin模块

Map(6) {
  '.../taro/packages/taro-cli/dist/presets/index.js' => {
    id: '.../taro/packages/taro-cli/dist/presets/index.js',
    path: '.../taro/packages/taro-cli/dist/presets/index.js',
    type: 'Preset',
    opts: {},
    apply: [Function: apply]
  },
  ...
}

3.执行钩子函数applyPlugins

// packages/taro-service/src/Kernel.ts
async applyPlugins (args: string | { name: string, initialVal?: any, opts?: any }) {
    let name
    let initialVal
    let opts
    if (typeof args === 'string') {
      name = args
    } else {
      name = args.name
      initialVal = args.initialVal
      opts = args.opts
    }
...
    // 此处打印this.hooks
    const hooks = this.hooks.get(name) || []
    if (!hooks.length) {
      return await initialVal
    }
    const waterfall = new AsyncSeriesWaterfallHook(['arg'])
    if (hooks.length) {
...
    }
    return await waterfall.promise(initialVal)
  }

onReadyonStart是不执行的我没可以在applyPlugins方法里打印this.hooks,打印的结果是出现三次如下,因为我们在initPresetsAndPlugins调用了三次(我们这里没有添加平台配置所以是三次)

Map(1) {
  'init' => [
    {
      name: 'init',
      optionsMap: [Object],
      fn: [Function: fn],
      plugin: '..../taro/packages/taro-cli/dist/presets/commands/init.js'
    }
  ]
}
* 3

通过get方法,name第一次和第二次分别传的是onReadyonStart,所以是直接返回initialVal,第三次传入了init,匹配上我们之前挂载到了kernel上的init

    // packages/taro-service/src/Kernel.ts
    const hooks = this.hooks.get(name) || []
    if (!hooks.length) {
      return await initialVal
    }
      // packages/taro-cli/src/cli.ts (这里是挂载init到kernel)
      // 针对不同的内置命令注册对应的命令插件
      if (commandPlugins.includes(targetPlugin)) {
        kernel.optsPlugins.push(path.resolve(commandsPath, targetPlugin))
      }

匹配到之后在applyPlugins方法的后半段继续执行如下

   // packages/taro-service/src/Kernel.ts
   // 实例化AsyncSeriesWaterfallHook实例一个异步钩子,会传递返回的参数
   const waterfall = new AsyncSeriesWaterfallHook(['arg'])
   if (hooks.length) {
      const resArr: any[] = []
      for (const hook of hooks) {
        waterfall.tapPromise({
          name: hook.plugin!,
          stage: hook.stage || 0,
          // @ts-ignore
          before: hook.before
        }, async arg => {
          // 在这里把我们的插件方法给执行了,也就是执行了上面打印的init里的fn
          // init插件所在的位置packages/taro-cli/src/presets/commonds/init.ts
          const res = await hook.fn(opts, arg)
          
          if (IS_MODIFY_HOOK.test(name) && IS_EVENT_HOOK.test(name)) {
            return res
          }
          if (IS_ADD_HOOK.test(name)) {
            resArr.push(res)
            return resArr
          }
          return null
        })
      }
   }
   return await waterfall.promise(initialVal)

这是我们内置命令输入init,当我输入help、build...也是一样的一个过程,都是初始化项目配置、参数=>收集项目的插件集和插件=>执行钩子


init

packages/taro-cli/src/presets/commonds/init.ts

通过插件机制init挂载到kernelapplyPlugins执行fn,在init里主要的作用就是引入和实例化Project执行create

// packages/taro-cli/src/presets/commonds/init.ts
import { IPluginContext } from '@tarojs/service'

export default (ctx: IPluginContext) => {
  ctx.registerCommand({
    name: 'init',
    optionsMap: {
        ...
    },
    async fn (opts) {
      // init project
      const { appPath } = ctx.paths
      const { options } = opts
      const { projectName, templateSource, clone, template, description, typescript, css, npm } = options
      // 引入和实例化Project
      const Project = require('../../create/project').default
      const project = new Project({
         ...
      })
      // 执行create
      project.create()
    }
  })
}

Project

packages/taro-cli/src/create/project.tsProject继承Creator,在init插件中被实例化并调用了create方法,主要的作用就是Taro项目创建前的项目信息填写,之后就是执行createAppProject的核心方法就是create

  async create () {
    try {
      // 填写项目名称、介绍、选择框架等
      const answers = await this.ask()
      const date = new Date()
      this.conf = Object.assign(this.conf, answers)
      this.conf.date = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
      // 当所有的都已经在控制台询问完成后执行write
      this.write()
    } catch (error) {
      console.log(chalk.red('创建项目失败: ', error))
    }
  }
  write (cb?: () => void) {
    this.conf.src = SOURCE_DIR
    // 创建项目
    createApp(this, this.conf, cb).catch(err => console.log(err))
  }

createApp

packages/taro-cli/src/create/init.ts主要作用选择模板创建项目1.选择模板

const templatePath = creator.templatePath(template)

模板的位置就packages/taro-cli/templates大概有十几种模板

2.创建文件

  // npm & yarn
  const version = getPkgVersion()

  // 遍历出模板中所有文件
  const files = await getAllFilesInFolder(templatePath, doNotCopyFiles)

  // 引入模板编写者的自定义逻辑
  const handlerPath = path.join(templatePath, TEMPLATE_CREATOR)
  const handler = fs.existsSync(handlerPath) ? require(handlerPath).handler : null

  // 为所有文件进行创建
  logs.push(
    ...createFiles(creator, files, handler, {
      ...params,
      framework,
      version,
      templatePath,
      projectPath,
      pageName: 'index',
      period: 'createApp'
    })
  )

主要的文件创建完成之后还有git和rn部分


这一个流程下来以后一个Taro项目就构建完成了