入口
基于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的结果可得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命令插件
根据命令找到对应的命令插件,就将插件放到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和outputPath
https://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)
}
onReady
和 onStart
是不执行的我没可以在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
第一次和第二次分别传的是onReady
和onStart
,所以是直接返回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
挂载到kernel
在applyPlugins
执行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项目创建前的项目信息填写,之后就是执行createApp
Project的核心方法就是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项目就构建完成了