源码共读14:Taro源码揭秘 之 每次创建新的 taro 项目(taro init)的背后原理是什么

189 阅读2分钟

Taro Github

本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。

这是学习源码整体架构系列,链接: juejin.cn/post/739033…

  1. 揭开整个框架的入口-taro init 初始化项目的秘密
  2. 揭开整个框架的插件系统的秘密
  3. 每次创建新的 taro 项目(taro init)的背后原理是什么

前言:

学习关键知识点:

  1. Taro init 原理是什么?
  2. 如何调试taro cli init源码?
  3. Nodejs 如何调用rust代码?
  4. 如何调试rust代码?
  5. 如何使用handlebars模版引擎?

准备工作:准备代码、环境、调试代码。参考前两篇文档。

开始,初始化使用taro init 命令:学习这个命令的如何实现的?

查询taro-cli的所有tag版本

$npm dist-tag @tarojs/cli

使用@tarojs/cli@next初始化一个项目。直接使用npx来运行next tag版本。

$npx @tarojs/cli@next init taro4-next

接下来,一步一步分析:

1. 调试taro init

我们在.vscode/launch.json 中的原有的 CLI debug 命令行调试配置,添加 init 配置如下:

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [{
      "type": "node",
      "request": "launch",
      "name": "CLI debug",
      "program": "${workspaceFolder}/packages/taro-cli/bin/taro",
+     "console": "integratedTerminal", // 调试时,可以在终端输入和交互
+     "args": [
+       "init",
+       "taro-init-test",
+     ],
      // 省略若干代码...
      "skipFiles": ["<node_internals>/**"]
    }]
}

2. init 命令行fn函数

前两篇文章,我们已经知道taro init初始化命令,最终调用的是packages/taro-cli/src/presets/commands/init.ts 文件中的 ctx.registerCommand 注册的 init 命令行的 fn 函数。

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

export default (ctx: IPluginContext) => {
  ctx.registerCommand({
    name: 'init',
    optionsMap: {
      '--name [name]': '项目名称',
      '--description [description]': '项目介绍',
      '--typescript': '使用TypeScript',
      '--npm [npm]': '包管理工具',
      '--template-source [templateSource]': '项目模板源',
      '--clone [clone]': '拉取远程模板时使用git clone',
      '--template [template]': '项目模板',
      '--css [css]': 'CSS预处理器(sass/less/stylus/none)',
      '-h, --help': 'output usage information'
    },
    async fn (opts) {
      // init project
      const { appPath } = ctx.paths
      const { options } = opts
      const { projectName, templateSource, clone, template, description, typescript, css, npm, framework, compiler, hideDefaultTemplate, sourceRoot } = options
      const Project = require('../../create/project').default
      const project = new Project({
        sourceRoot,
        // code...
      })

      project.create()
    }
  })
}

fn函数,其中options参数是命令行中的所有参数。其中主要做了如下几件事情:

  • 读取组合各种参数,初始化project对象,并调用create方法。

接下来,重点看packages/taro-cli/src/create/project.ts的Project类的实现,及其create方法。

3. new Project构造函数

// packages/taro-cli/src/create/project.ts
export default class Project extends Creator {
  public rootPath: string
  public conf: IProjectConfOptions

  constructor (options: IProjectConfOptions) {
    super(options.sourceRoot)
    const unSupportedVer = semver.lt(process.version, 'v18.0.0')
    if (unSupportedVer) {
      throw new Error('Node.js 版本过低,推荐升级 Node.js 至 v18.0.0+')
    }
    this.rootPath = this._rootPath

    this.conf = Object.assign(
      {
        projectName: '',
        projectDir: '',
        template: '',
        description: '',
        npm: ''
      },
      options
    )
  }
} 

Project继承了Creator类。

构造函数中,使用semver.lt判断node的版本号是否低于v18.0.0,如果低于就报错。

semver是一个版本比较库,可以用来判断node版本是否符合要求。

其次,初始化this.rootPaththis.conf

继续看Creator类,构造函数调用了init方法。

// packages/taro-cli/src/create/creator.ts
export default class Creator {
  protected _rootPath: string
  public rootPath: string

  constructor (sourceRoot?: string) {
    this.rootPath = this.sourceRoot(sourceRoot || path.join(getRootPath()))
    this.init()
  }
}

init方法,回顾:《源码共读11:Taro源码揭秘 之 揭开整个框架的入口-taro init 初始化项目的秘密》

3.1. project.create 创建项目

// packages/taro-cli/src/create/project.ts
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()}`
                this.write()
        } catch (error) {
                console.log(chalk.red('创建项目失败: ', error))
        }
}

create函数主要做了以下几件事:

  • 调用 ask 询问用户输入项目名称、描述、CSS预处理器、包管理工具等。
  • 把用户反馈的结果和之前的配置合并起来,得到 this.conf
  • 调用 write 方法,写入文件,初始化模板项目。

接下来,往下看ask询问用户输入项目名称、描述等。

4. ask 询问用户输入项目名称、描述等

// packages/taro-cli/src/create/project.ts
async ask () {
    let prompts: Record<string, unknown>[] = []
    const conf = this.conf

    this.askProjectName(conf, prompts)
    this.askDescription(conf, prompts)
    this.askFramework(conf, prompts)
    this.askTypescript(conf, prompts)
    this.askCSS(conf, prompts)
    this.askCompiler(conf, prompts)
    this.askNpm(conf, prompts)
    await this.askTemplateSource(conf, prompts)

    const answers = await inquirer.prompt<IProjectConf>(prompts)

    prompts = []
    const templates = await this.fetchTemplates(answers)
    await this.askTemplate(conf, prompts, templates)
    const templateChoiceAnswer = await inquirer.prompt<IProjectConf>(prompts)

    return {
      ...answers,
      ...templateChoiceAnswer
    }
  }

简单来说,ask方法就是一系列的inquirer交互。

inquirer是一个命令行交互工具,可以用来创建命令行程序。

如果参数中没指定相应参数,那么就询问:

  • 项目名称
  • 项目介绍
  • 选择框架
  • 是否启用TS
  • CSS预处理器
  • 编译工具
  • 包管理工具
  • 选择模版源 (gitee 最快、github最新、CLI内置模版等)
  • 选择模版 (默认模版等)
  • 等等

接下来,重点讲述以下几个方法:

  • askProjectName 询问项目名称
  • askTemplateSource 询问模版源
  • fetchTemplates 询问模版列表
  • askTemplate 询问模板

5.1 askProjectName

未完待续。。。

此文章为2024年10月Day1源码共读,生活在阴沟里,也要记得仰望星空。