Taro的CLI
一、CLI 的入口

首先,在package.json里面定义了taro命令和对应的可执行文件,然后我们看可执行文件中的内容

先调用一遍printPkgVersion 方法,在每次执行taro命令时先打印一遍taro版本号

后面新建了一个CLI类的实例并执行了 run 方法也就是 parseArgs 方法, parseArgs 方法解析命令行参数并作对应处理

二、解析命令参数

taro 使用了minimist这个库来解析,解析结果里面的首个key是_,它的值是个数组,包含的是所有没有关联选项的参数,比如taro build --type weapp --watch 命令解析的结果是:
三、设置环境变量

- 若命令行参数有传入
env字段,则将环境变量NODE_ENV设置为传入的值,若未传入env字段,则根据是否传入watch字段来设置NODE_ENV为development或者production - 根据传入的
type或t或plugin字段来设置TARO_ENV - 自动导入默认的
env环境文件.env和.env.local,同时根据传入mode字段或者前面设置的NODE_ENV导入对应的env文件
四、获取项目配置

taro 将获取配置信息封装成了一个类并且抽离到了 @tarojs/service 包中,实例化后,调用了 init 方法

init 方法里面首先判断当前路径下是否有 config/index 文件,如果没有,比如我们是在空白文件夹下运行 taro init 命令,它会去执行 initGlobalConfig 方法来尝试获取全局配置,如果有,则通过@swc/register即时编译,然后读取配置文件内容保存到initialConfig中

getConfigWithNamed方法会在Kernel类里面通过Config实例对象调用,将编译配置里面平台对应的信息铺平,比如mini,h5这些
五、实例化Kernel 并执行run方法
Kernel 是抽离到 @tarojs/service 包中的一个类,taro运行的各项命令都是围绕它展开

创建类实例的时候传入了一个包含4个属性的对象
- appPath:
process.cwd()返回的当前工作目录 - presets:
taro预设的一些plugins集合 - config:运行
taro命令的相关配置信息,也就是前文实例化后的config对象 - plugins:初始值空数组

在构造方法中接收参数保存到内部属性中,同时进行了一些初始化操作

在实例化完成后,通过对传入的命令行参数进行判断,收集执行本次命令需要使用到的所有插件,保存到optsPlugins 属性中,然后执行customCommand方法

customCommand 方法就是对 kernel 的 run 方法进行了一个封装,代码执行到这里,正式进入到kernel 内部

在Kernel 类的 run 方法里面,首先通过 initPresetsAndPlugins 方法进行初始化项目中使用到的插件

合并实例化时传入的presets和项目配置的presets并转换为一个JSON对象cliAndProjectConfigPresets,合并实例化时传入的plugins和项目配置的plugins并转换为一个JSON对象cliAndProjectPlugins,然后分别调用resolvePresets和resolvePlugins进行处理

通过resolvePresetsOrPlugins方法将presets和plugins 转换为对象数组,单个元素的格式如下:
然后对于每个preset进行initPreset处理,对于每个plugin进行initPlugin处理

可以看到这2个函数都是先调用了initPluginCtx方法来获取插件上下文,initPluginCtx方法实例化了一个Plugin 类作为插件上下文,同时通过Proxy 对上下文对象进行了扩展,kernelApis 里面的这些属性和方法可以直接被插件上下文对象调用,当有插件监听了某个hook,就会触发proxy的get函数, 然后调用method里面的方法也就是Plugin类里面的register方法,将这个hook保存到kernel中的hooks字段里

六、Plugin 类
在Plugin 类里面,通过ctx 字段保存kernel 上下文对象来访问和修改kernel 的属性,这样kernel 内部可以直接获取到插件信息
- register:注册一个可供其他插件调用的钩子,接收一个参数,即 Hook 对象
- registerCommand:注册一个自定义命令
- registerPlatform:注册一个编译平台
- registerMethod:向
ctx上挂载一个方法可供其他插件直接调用 - addPluginOptsSchema:为插件入参添加校验,接受一个函数类型参数,函数入参为
joi对象,返回值为joi schema

taro的插件都有固定的代码结构,通常由一个函数组成,比如这个官网的示例
源码里面在获取到initPluginCtx 方法返回的插件上下文后,通过apply方法获取插件返回的函数,然后执行这个函数并将插件上下文作为第一个参数传入,这个上下文能做很多事情,最直接的,可以调用Plugin类里面的方法,然后,前文也提到了,还可以访问Kernel 类里面部分属性和方法,除此之外,还可以访问taro内置的通过registerMethod挂载的方法或者hooks,这些挂载的方法内容都保存在kernel的 methods属性里,比如上例编译生命周期这些hooks,如果有插件监听的话,就会执行methods里面的函数,fn就是我们监听某个hook的处理函数,跟name一起作为参数调用register方法,这样hooks信息就保存了起来,所有的方法介绍可以看 官网
七、applyPlugins 方法
在 initPresetsAndPlugins方法执行完成后,我们再回头看 run方法,发现run方法后续只调用了多次 applyPlugins函数,没错,插件执行的架构就是在applyPlugins 里面实现的

applyPlugins函数里面先判断this.hooks里面是否存在指定name对应的hooks ,也就是查询是否有插件通过Plugin注册过这个name 的钩子,如果没有注册过name则直接返回,如果有注册,则使用 Tapable 的AsyncSeriesWaterfallHook (异步串行瀑布式钩子)来调用,在插件里面也经常会用到applyPlugins函数来触发不同插件注册的钩子
八、taro init 做了什么

从 customCommand到kernel.run再到 applyPlugins ,最终执行的是taro内部自带的init插件

插件里面代码很简单,获取下命令行参数,实例化 Project类,执行create方法

在ask函数里面通过inquirer这个交互式命令行工具让用户选择项目的配置,然后执行write方法创建项目文件

这里的handler是用于控制是否生成某文件,或给文件传入特定参数,详细介绍可以参考官网

write函数其实也是对createProject方法的封装,createProject方法是从 @tarojs/binding 库中引入的,@tarojs/binding是通过napi-rs框架实现的Node.js扩展

create_project里面先是调用了结构体Project 的impl 的关联函数new创建一个实例,Project 是从taro_init这个 Package 中引入的,然后调用实例的create 函数

create_options就是初始化的配置信息,这里新添加了几个字段,比如固定page_name为index,all_files就是模板文件夹下面的所有文件组成的数组,比如选择默认模板,获取的就是 taro cli自带模板default目录下所有文件:

然后就调用create_files函数并把all_files作为第一个参数传入,第二个参数template_path是模板文件夹的路径,第三个参数options是新建项目的配置信息,第四个参数js_handlers 就是前文说的handler ,进入函数后,便对所有文件进行遍历操作

file_relative_path 就是将完整的绝对路径file 移除template_path 转换成相对路径,handler 里面的key就是用的相对路径,这样就可以进行匹配了,如果这个文件在js_handlers 里面匹配到了,就对其在hander里面定义的函数进行调用,函数支持返回一个布尔类型或者一个JSON对象,布尔类型可以控制是否创建文件,可以看到,taro将返回的结果保存在 need_create_file 里,当need_create_file 为真值是才会创建文件,这样可以实现指定某个文件只有在使用指定框架或者使用Typescript时才会创建, 如果返回的是JSON对象,会获取对象里面指定key的值进行操作,这里目前支持4个固定key:
- setPageName:设置文件的输出路径
- changeExt:是否自动替换文件后缀
- setSubPkgPageName:分包文件的输出路径
- subPkg:分包页面路径
整理完信息后,便正式开始创建文件,可以看到,create_files 最终是执行的self.tempate 函数来创建文件,传入3个参数, from_path就是模板文件的路径,dest_path就是要生成文件的路径,options 还是创建项目的配置信息, self.tempate 函数里面又执行了generate_with_template函数

这里在读取到模板文件内容后,还调用了HANDLEBARS.render_template 来转换模板文件,这样生成的文件内容才是我们想要的

在 create_files 函数创建文件完成后,回到create函数里,又调用了init_git 函数进行git初始化

最后,执行install_deps 函数安装项目依赖

至此,taro init 创建项目的流程便走完了