細雨

Taro的CLI

一、CLI 的入口

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

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

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

二、解析命令参数

taro 使用了minimist这个库来解析,解析结果里面的首个key_,它的值是个数组,包含的是所有没有关联选项的参数,比如taro build --type weapp --watch 命令解析的结果是:

{
_: [ 'build' ],
version: false,
v: false,
help: false,
h: false,
'disable-global-config': false,
type: 'weapp',
watch: true,
build: true
}

三、设置环境变量

  • 若命令行参数有传入env字段,则将环境变量NODE_ENV设置为传入的值,若未传入env字段,则根据是否传入watch字段来设置NODE_ENVdevelopment或者production
  • 根据传入的typetplugin字段来设置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实例对象调用,将编译配置里面平台对应的信息铺平,比如minih5这些

五、实例化Kernel 并执行run方法

Kernel 是抽离到 @tarojs/service 包中的一个类,taro运行的各项命令都是围绕它展开

创建类实例的时候传入了一个包含4个属性的对象

  • appPath:process.cwd()返回的当前工作目录
  • presets:taro预设的一些plugins集合
  • config:运行taro命令的相关配置信息,也就是前文实例化后的config对象
  • plugins:初始值空数组

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

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

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

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

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

通过resolvePresetsOrPlugins方法将presetsplugins 转换为对象数组,单个元素的格式如下:

{
id: '', // 同path
path: '', // 单个preset或plugin的文件路径
type: 'Plugin', // 'Preset' 或 'Plugin'
opts: {}, // 传入插件的参数,若未设置参数则默认空对象
apply: [Function: apply] // 调用时返回文件内容
}

然后对于每个preset进行initPreset处理,对于每个plugin进行initPlugin处理

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

六、Plugin

Plugin 类里面,通过ctx 字段保存kernel 上下文对象来访问和修改kernel 的属性,这样kernel 内部可以直接获取到插件信息

  • register:注册一个可供其他插件调用的钩子,接收一个参数,即 Hook 对象
  • registerCommand:注册一个自定义命令
  • registerPlatform:注册一个编译平台
  • registerMethod:向 ctx 上挂载一个方法可供其他插件直接调用
  • addPluginOptsSchema:为插件入参添加校验,接受一个函数类型参数,函数入参为joi对象,返回值为joi schema

taro的插件都有固定的代码结构,通常由一个函数组成,比如这个官网的示例

// https://docs.taro.zone/docs/plugin-custom
export default (ctx, options) => {
// plugin 主体
ctx.onBuildStart(() => {
console.log('编译开始!')
})
ctx.onBuildFinish(() => {
console.log('Webpack 编译结束!')
})
ctx.onBuildComplete(() => {
console.log('Taro 构建完成!')
})
}

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

七、applyPlugins 方法

initPresetsAndPlugins方法执行完成后,我们再回头看 run方法,发现run方法后续只调用了多次 applyPlugins函数,没错,插件执行的架构就是在applyPlugins 里面实现的

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

八、taro init 做了什么

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

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

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

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

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

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

create_options就是初始化的配置信息,这里新添加了几个字段,比如固定page_nameindexall_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 创建项目的流程便走完了