本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是
学习源码整体架构系列,链接: juejin.cn/post/739033… 。
- 揭开整个框架的入口-taro init 初始化项目的秘密
- 揭开整个框架的插件系统的秘密
- 每次创建新的 taro 项目(taro init)的背后原理是什么
前言:
学习关键知识点:
- Taro init 原理是什么?
- 如何调试taro cli init源码?
- Nodejs 如何调用rust代码?
- 如何调试rust代码?
- 如何使用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.rootPath和this.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源码共读,生活在阴沟里,也要记得仰望星空。