乐趣区

关于taro:Taro-cli流程和插件化机制实现原理

前言

自 2.2 开始,Taro 引入了插件化机制,目标是为了让开发者可能通过编写插件的形式来为 Taro 拓展更多功能或为本身业务定制个性化性能。

本文基于 Taro3.4.2 源码解说

CLI 流程

  1. 执行 cli 命令,如 npm run start,实际上在package.jsonscript脚本列表中能够往下解读始终找到 build:weapp 这条脚本所执行的对应具体指令信息,dev模式下区别 prod 模式只是多了一个 --watch 热加载而已,只是辨别了对应的 env 环境,在 webpack 打包的时候别离预设了对应环境不同的打包配置,例如判断生产环境才会默认启用代码压缩等

  2. 那么这个 taro 指令是在哪定义的呢?taro 在你全局装置的时候就曾经配置到环境变量了,咱们我的项目目录上来执行 `package.json 中的 script 脚本命令,它会在当前目录上来找 node 脚本,找不到就向下级找,最终执行该脚本。
  3. taro 的外围指令源码都在 taro/cli 下,罕用的指令有init(创立我的项目)、build(构建我的项目)。启动命令入口在taro/cli/bin/taro

    // @taro/cli/bin/taro
    #! /usr/bin/env node
    
    require('../dist/util').printPkgVersion()
    
    const CLI = require('../dist/cli').default
    new CLI().run()
  4. 启动后,CLI实例先实例化了一个继承 EventEmitterKernel外围类 (ctx),解析脚本命令参数后调用customCommand 办法,传入 kernel 实例和所有我的项目参数相干。

    // taro-cli/src/cli.ts
    // run
    
    const kernel = new Kernel({
      appPath: this.appPath,
      presets: [path.resolve(__dirname, '.', 'presets', 'index.js')
      ]
    })
    
    let plugin
    // script 命令中的 --type 参数
    let platform = args.type
    const {publicPath, bundleOutput, sourcemapOutput, sourceMapUrl, sourcemapSourcesRoot, assetsDest} = args
    // 小程序插件开发,script: taro build --plugin weapp --watch
    customCommand('build', kernel, {
      _: args._,
      platform,
      plugin,
      isWatch: Boolean(args.watch),
      port: args.port,
      env: args.env,
      deviceType: args.platform,
      resetCache: !!args.resetCache,
      publicPath,
      bundleOutput,
      sourcemapOutput,
      sourceMapUrl,
      sourcemapSourcesRoot,
      assetsDest,
      qr: !!args.qr,
      blended: Boolean(args.blended),
      h: args.h
    })
  5. customCommand中将所有的参数整顿后调用Kernel.run,传入整顿后的所有参数。

    kernel.run({
      name: command,
      opts: {
        _: args._,
        options,
        isHelp: args.h
      }
    })
  6. 接下去就是在 Kernel 类中一系列我的项目初始化的工作流程,包含设置参数、初始化相干配置、执行内设的钩子函数、批改 webpack 等,Kernel中的所有属性在插件开发中都能够通过 ctx 拜访,简略了局部代码,如下:

    // taro-service/src/Kernel.ts
    
    async run (args: string | { name: string, opts?: any}) {
          // ...
        // 设置参数,后面 cli.ts 中传入的一些我的项目配置信息参数,例如 isWatch 等
        this.setRunOpts(opts)
        // 重点:初始化相干配置
        await this.init()
        // 留神:Kernel 的前两个生命周期钩子是 onReady 和 onStart,并没有执行操作,开发者在本人编写插件时能够注册对应的钩子
        // 执行 onStart 钩子
        await this.applyPlugins('onStart')
        // name: example: build...
        // 解决 --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}
        })
        // 执行传入的命令
        await this.applyPlugins({
          name,
          opts
        })
      }
    

    其中重点的初始化流程在 Kernel.init 中。

插件次要流程

Kernel.init流程如下:

async init () {this.debugger('init')
  // 初始化我的项目配置,也就是你 config 目录配置的那些
  this.initConfig()
  // 初始化我的项目资源目录,例如:输入目录、依赖目录,src、config 配置目录等,局部配置是在你我的项目的 config/index.js 中的 config 中配置的货色,如
  // sourcePath 和 outputPath
  // https://taro-docs.jd.com/taro/docs/plugin 插件环境变量
  this.initPaths()
  // 初始化预设和插件
  this.initPresetsAndPlugins()
  // 留神:Kernel 的前两个生命周期钩子是 onReady 和 onStart,并没有执行操作,开发者在本人编写插件时能够注册对应的钩子
  // 执行 onReady 钩子
  await this.applyPlugins('onReady')
}

插件环境变量

追溯文档给出的 ctx 应用时可能会用到的次要环境变量实现原理,对于环境变量应用详情👉🏻文档地址

ctx.runOpts

获取以后执行命令所带的参数,例如命令 taro upload --remote xxx.xxx.xxx.xxx,则 ctx.runOpts 值为:

{_: ['upload'],
  options: {remote: 'xxx.xxx.xxx.xxx'},
  isHelp: false
}

runOptstaro-service/src/Kernel.tsrun办法初始化,早于 Kernel.init,因为runOpts 蕴含的命令参数在实例化 Kernel 的时候就曾经解析了,只是在 run 外面给以后上下文 (Kernel) 赋值保存起来,也就是调用时的ctx。源码如下:

// taro-service/src/Kernel.ts

this.setRunOpts(opts)

// 保留以后执行命令所带的参数
setRunOpts (opts) {this.runOpts = opts}

ctx.helper

为包 @tarojs/helper 的快捷应用形式,蕴含其所有 API,次要是一些工具办法和常量,比方 Kernel.ts 中用到的四个办法:

// 常量:node_modules,用作第三方依赖门路变量
NODE_MODULES,
// 查找 node_modules 门路(ctx.paths.nodeModulesPath 的获取起源就是此办法)recursiveFindNodeModules,
// 给 require 注册 babel,在运行时对所有插件进行即时编译
createBabelRegister,
// https://www.npmjs.com/package/debug debug 库的应用别名,用来在控制台打印信息,反对高亮、命名空间等高级用法
createDebug

其中 createBabelRegister 办法在开源我的项目里应用频率较高,其扩大用法:通过 createBabelRegister,反对在app.config.tscommonJs环境中应用 importrequire

ctx.initialConfig

获取我的项目配置。

找到 initialConfig: IProjectConfig 类型定义文件,能够看到构造跟 Taro 我的项目的 config 下的配置文件约定的配置构造统一。

详情👉🏻编译配置详情

// taro/types/compile.d.ts

export interface IProjectBaseConfig {
  projectName?: string
  date?: string
  designWidth?: number
  watcher?: any[]
  deviceRatio?: TaroGeneral.TDeviceRatio
  sourceRoot?: string
  outputRoot?: string
  env?: IOption
  alias?: IOption
  defineConstants?: IOption
  copy?: ICopyOptions
  csso?: TogglableOptions
  terser?: TogglableOptions
  uglify?: TogglableOptions
  sass?: ISassOptions
  plugins?: PluginItem[]
  presets?: PluginItem[]
  baseLevel?: number
  framework?: string
}

export interface IProjectConfig extends IProjectBaseConfig {
  ui?: {extraWatchFiles?: any[]
  }
  mini?: IMiniAppConfig
  h5?: IH5Config
  rn?: IH5Config
  [key: string]: any
}

回头看 Kernel.ts 中的 init 办法,第一个次要流程就是 initConfig 初始化我的项目配置,也就是你我的项目根目录下的 config 目录配置的那些配置项。

// taro-service/src/Kernel.ts

initConfig () {
  this.config = new Config({appPath: this.appPath})
  this.initialConfig = this.config.initialConfig
  this.debugger('initConfig', this.initialConfig)
}

Config类会去找到我的项目的 config/index.js 文件去初始化配置信息

// taro-service/src/Config.ts

constructor (opts: IConfigOptions) {
  this.appPath = opts.appPath
  this.init()}

init () {this.configPath = resolveScriptPath(path.join(this.appPath, CONFIG_DIR_NAME, DEFAULT_CONFIG_FILE))
  if (!fs.existsSync(this.configPath)) {this.initialConfig = {}
    this.isInitSuccess = false
  } else {
    createBabelRegister({
      only: [filePath => filePath.indexOf(path.join(this.appPath, CONFIG_DIR_NAME)) >= 0
      ]
    })
    try {this.initialConfig = getModuleDefaultExport(require(this.configPath))(merge)
      this.isInitSuccess = true
    } catch (err) {this.initialConfig = {}
      this.isInitSuccess = false
      console.log(err)
    }
  }
}

ctx.paths

Kernel.ts中的 init 办法第二个次要流程就是初始化插件环境变量ctx.paths,蕴含以后执行命令的相干门路,所有的门路如下(并不是所有命令都会领有以下所有门路):

  • ctx.paths.appPath,以后命令执行的目录,如果是 build 命令则为以后我的项目门路
  • ctx.paths.configPath,以后我的项目配置目录,如果 init 命令,则没有此门路
  • ctx.paths.sourcePath,以后我的项目源码门路
  • ctx.paths.outputPath,以后我的项目输入代码门路
  • ctx.paths.nodeModulesPath,以后我的项目所用的 node_modules 门路

源码如下:

// taro-service/src/Kernel.ts

initPaths () {
  this.paths = {
    appPath: this.appPath,
    nodeModulesPath: recursiveFindNodeModules(path.join(this.appPath, NODE_MODULES))
  } as IPaths
  if (this.config.isInitSuccess) {
    Object.assign(this.paths, {
      configPath: this.config.configPath,
      sourcePath: path.join(this.appPath, this.initialConfig.sourceRoot as string),
      outputPath: path.join(this.appPath, this.initialConfig.outputRoot as string)
    })
  }
  this.debugger(`initPaths:${JSON.stringify(this.paths, null, 2)}`)
}

ctx.plugins

Kernel.tsinit 办法第三个次要流程就是 initPresetsAndPlugins 初始化预设和插件,也是 init 中最简单的一个流程,次要产物有 ctx.pluginsctx.extraPlugins

在官网文档里介绍的插件性能无关预设这块只是草草几句带过了,而且并没有给出 demo 解释如何应用,然而留下了一个比拟重要的概念 – 预设是一系列插件的汇合。

文档里给出的预设例子如下:

const config = {
  presets: [
    // 引入 npm 装置的插件集
    '@tarojs/preset-sth', 
    // 引入 npm 装置的插件集,并传入插件参数
    ['@tarojs/plugin-sth', {arg0: 'xxx'}],
    // 从本地绝对路径引入插件集,同样如果须要传入参数也是如上
    '/absulute/path/preset/filename',
  ]
}

只是给了 presets 的配置,然而并不分明 '@tarojs/preset-sth' 或者 /absulute/path/preset/filename 插件外部是怎么实现的。于是查阅源码,因为 Taro 外部有一系列内置的预设,在初始化 Kernel 的时候就传给 options 了,在后面 CLI 流程的第四步其实能够看到如下:

// taro-cli/src/cli.ts

const kernel = new Kernel({
  appPath: this.appPath,
  presets: [path.resolve(__dirname, '.', 'presets', 'index.js')
  ]
})

于是找到taro-cli/src/presets/index.ts(省略局部代码):

import * as path from 'path'

export default () => {
  return {
    plugins: [
      // platforms
      path.resolve(__dirname, 'platforms', 'h5.js'),
      path.resolve(__dirname, 'platforms', 'rn.js'),
      path.resolve(__dirname, 'platforms', 'plugin.js'),
      ['@tarojs/plugin-platform-weapp', { backup: require.resolve('@tarojs/plugin-platform-weapp') }],
      ['@tarojs/plugin-platform-alipay', { backup: require.resolve('@tarojs/plugin-platform-alipay') }],
      ['@tarojs/plugin-platform-swan', { backup: require.resolve('@tarojs/plugin-platform-swan') }],
      ['@tarojs/plugin-platform-tt', { backup: require.resolve('@tarojs/plugin-platform-tt') }],
      ['@tarojs/plugin-platform-qq', { backup: require.resolve('@tarojs/plugin-platform-qq') }],
      ['@tarojs/plugin-platform-jd', { backup: require.resolve('@tarojs/plugin-platform-jd') }],

      // commands
      path.resolve(__dirname, 'commands', 'build.js'),
      // ... 省略其余

      // files
      path.resolve(__dirname, 'files', 'writeFileToDist.js'),
      // ... 省略其余
      
      // frameworks
      ['@tarojs/plugin-framework-react', { backup: require.resolve('@tarojs/plugin-framework-react') }],
      // ... 省略其余
    ]
  }
}

那模拟他写一个不就行了?

// projectRoot/src/prests/custom-presets.js

const path = require('path');

module.exports = () => {
  return {
    plugins: [path.resolve(__dirname, '..', 'plugin/compiler-optimization.js'),
      path.resolve(__dirname, '..', 'plugin/global-less-variable-ext.js'),
    ],
  };
};

总结:

  • 预设

    是一些列插件的汇合,一个预设文件应该返回蕴含 plugins 配置的插件数组。

  • 插件

    具备固定的代码构造,返回一个性能函数,其中第一个参数是打包过程中的高低信息 ctx,ctx 中能够拿到一个重要的参数 modifyWebpackChain,通过它批改 webpack 配置,第二个参数是options,能够在config 下的 plugins 中定义插件的中央传入该插件所须要的参数。插件局部能够参考文档,形容的算是比较清楚了。

初始化预设跟插件的流程如下:

initPresetsAndPlugins () {
  const initialConfig = this.initialConfig
  // 框架内置的插在件 taro-cli/src/presets 下
  // 收集预设汇合,一个 preset 是一系列 Taro 插件的汇合。// 将预设的插件跟我的项目 config 下自定义插件收集一块
  const allConfigPresets = mergePlugins(this.optsPresets || [], initialConfig.presets || [])()
  // 收集插件并转化为汇合对象,包含框架内置插件和本人自定义的插件
  const allConfigPlugins = mergePlugins(this.optsPlugins || [], initialConfig.plugins || [])()
  this.debugger('initPresetsAndPlugins', allConfigPresets, allConfigPlugins)
  // 给 require 注册 babel,在运行时对所有插件进行即时编译
  // 扩大用法:通过 createBabelRegister,反对在 app.config.ts 中应用 import 或 require
  process.env.NODE_ENV !== 'test' &&
    createBabelRegister({only: [...Object.keys(allConfigPresets), ...Object.keys(allConfigPlugins)]
  })
  this.plugins = new Map()
  this.extraPlugins = {}
  // 加载了所有的 presets 和 plugin,最初都以 plugin 的模式注册到 kernel.plugins 汇合中(this.plugins.set(plugin.id, plugin))
  // 蕴含了插件办法的初始化
  this.resolvePresets(allConfigPresets)
  this.resolvePlugins(allConfigPlugins)
}

插件办法

诸如 ctx.registerctx.registerMethodctx.registerCommandctx.registerPlatformctx.applyPluginsctx.addPluginOptsSchemactx.generateProjectConfig 这些文档中介绍的插件办法,能够看到都是从插件的 ctx 中取的,那插件的这些办法是在构建中的什么阶段被注册进去,以及它的流转是怎么的呢?

插件办法的定义都在 taro-service/src/Plugin.tsPlugin类中,咱们的自定义插件(包含预设)和 Taro 内置的插件(包含预设)都会在上述初始化预设跟插件办法 initPresetsAndPlugins 中的 resolvePresetsresolvePlugins的流程中被初始化,一一对每个插件进行初始化工作:

// resolvePresets
while (allPresets.length) {const allPresets = resolvePresetsOrPlugins(this.appPath, presets, PluginType.Preset)
  this.initPreset(allPresets.shift()!)
}

// resolvePlugins
while (allPlugins.length) {plugins = merge(this.extraPlugins, plugins)
  const allPlugins = resolvePresetsOrPlugins(this.appPath, plugins, PluginType.Plugin)
  this.initPlugin(allPlugins.shift()!)
  this.extraPlugins = {}}

每个插件在初始化之前都被 resolvePresetsOrPlugins 办法包装过,找到 taro-service/src/utils/index.ts 中该办法的定义:

// getModuleDefaultExport
export function resolvePresetsOrPlugins (root: string, args, type: PluginType): IPlugin[] {return Object.keys(args).map(item => {
    let fPath
    try {
      fPath = resolve.sync(item, {
        basedir: root,
        extensions: ['.js', '.ts']
      })
    } catch (err) {if (args[item]?.backup) {
        // 如果我的项目中没有,能够应用 CLI 中的插件
        // taro 预设的插件局部设置了 backup,也就是备份的,他会通过 require.resolve 查找到模块门路。如果我的项目中没有此插件,就会去拿 taro 框架 CLI 里内置的插件
        fPath = args[item].backup
      } else {console.log(chalk.red(` 找不到依赖 "${item}",请先在我的项目中装置 `))
        process.exit(1)
      }
    }
    return {
      id: fPath, // 插件绝对路径
      path: fPath, // 插件绝对路径
      type, // 是预设还是插件
      opts: args[item] || {}, // 一些参数
      apply () {
        // 返回插件文件外面自身的内容,getModuleDefaultExport 做了一层判断,是不是 esModule 模块 exports.__esModule ? exports.default : exports
        return getModuleDefaultExport(require(fPath))
      }
    }
  })
}

initPresetinitPlugin中,一个比拟重要的流程 –initPluginCtx,它做了初始化插件的上下文的工作内容,其中调用 initPluginCtx 办法时,把 Kernel 当成参数传给了 ctx 属性,此外还有 idpath,咱们曾经晓得,这两个值都是插件的绝对路径。

// taro-service/src/Kernel.ts initPreset

const pluginCtx = this.initPluginCtx({id, path, ctx: this})

正是在 initPluginCtx 中,第一次看到了跟本文主题最严密的一个词—Plugin,关上 Plugin 类定义文件,其中找到了所有在文档中给开发者扩大的那些插件办法,也就是上述中插件办法结尾介绍的那几个办法。

// taro-service/src/Plugin.ts

export default class Plugin {
  id: string
  path: string
  ctx: Kernel
  optsSchema: (...args: any[]) => void
  constructor (opts) {
    this.id = opts.id
    this.path = opts.path
    this.ctx = opts.ctx
  }
  register (hook: IHook) {// ...}
  registerCommand (command: ICommand) {// ...}
  registerPlatform (platform: IPlatform) {// ...}
  registerMethod (...args) {// ...}
    function processArgs (args) {// ...}
  addPluginOptsSchema (schema) {this.optsSchema = schema}
}

等等,不是说所有吗?那 writeFileToDistgenerateFrameworkInfogenerateProjectConfig 怎么没看到?其实在初始化预设的时候,这三个词就曾经呈现过了,之前在介绍 ctx.plugins 的时候提到了 taro-cli/src/presets/index.ts 内置预设文件,其中 files 局部代码被省略了,这里从新贴一下:

// taro-cli/src/presets/index.ts

// files
path.resolve(__dirname, 'files', 'writeFileToDist.js'),
path.resolve(__dirname, 'files', 'generateProjectConfig.js'),
path.resolve(__dirname, 'files', 'generateFrameworkInfo.js')

writeFileToDist 举例,具体看看这个插件实现了什么性能:

// taro-cli/src/presets/files/writeFileToDist.ts

export default (ctx: IPluginContext) => {ctx.registerMethod('writeFileToDist', ({ filePath, content}) => {const { outputPath} = ctx.paths
    const {printLog, processTypeEnum, fs} = ctx.helper
    if (path.isAbsolute(filePath)) {printLog(processTypeEnum.ERROR, 'ctx.writeFileToDist 不能承受绝对路径')
      return
    }
    const absFilePath = path.join(outputPath, filePath)
    fs.ensureDirSync(path.dirname(absFilePath))
    fs.writeFileSync(absFilePath, content)
  })
}

能够看到 writeFileToDist 这个办法是通过 registerMethod 注册到 ctx 了,其余两个办法同理。

registerMethod

ctx.registerMethod(arg: string | { name: string, fn?: Function}, fn?: Function)

Taro官网文档也给了咱们解释—向 ctx 上挂载一个办法可供其余插件间接调用。

回到 Plugin 自身,细究其每个属性办法,先找到registerMethod

// 向 ctx 上挂载一个办法可供其余插件间接调用。registerMethod (...args) {const { name, fn} = processArgs(args)
  // ctx(也就是 Kernel 实例)下来找有没有这个办法,有的话就拿已有办法的回调数组,否则初始化一个空数组
  const methods = this.ctx.methods.get(name) || []
  // fn 为 undefined,阐明注册的该办法未指定回调函数,那么相当于注册了一个 methodName 钩子
  methods.push(fn || function (fn: (...args: any[]) => void) {
    this.register({
      name,
      fn
    })
  }.bind(this))
  this.ctx.methods.set(name, methods)
}

register

ctx.register(hook: IHook)

interface IHook {
  // Hook 名字,也会作为 Hook 标识
  name: string
  // Hook 所处的 plugin id,不须要指定,Hook 挂载的时候会自动识别
  plugin: string
  // Hook 回调
  fn: Function
  before?: string
  stage?: number
}

注册一个可供其余插件调用的钩子,接管一个参数,即 Hook 对象。通过 ctx.register 注册过的钩子须要通过办法 ctx.applyPlugins 进行触发。

Pluginregister 的办法定义如下:

// 注册钩子一样须要通过办法 ctx.applyPlugins 进行触发
register (hook: IHook) {if (typeof hook.name !== 'string') {throw new Error(` 插件 ${this.id} 中注册 hook 失败,hook.name 必须是 string 类型 `)
  }
  if (typeof hook.fn !== 'function') {throw new Error(` 插件 ${this.id} 中注册 hook 失败,hook.fn 必须是 function 类型 `)
  }
  const hooks = this.ctx.hooks.get(hook.name) || []
  hook.plugin = this.id
  this.ctx.hooks.set(hook.name, hooks.concat(hook))
}

通过 register 注册的钩子会主动注入以后插件的 id(绝对路径),最初合并到ctx.hooks 中,待 applyPlugins 调用

registerCommand

ctx.registerCommand(hook: ICommand)

一个感觉很有设想空间的办法,能够自定义指令,例如taro create xxx,能够依照需要疾速生成一些通用模板、组件或者办法等等。

ICommand继承于IHook

export interface ICommand extends IHook {
  alias?: string,
  optionsMap?: {[key: string]: string
  },
  synopsisList?: string[]}

因而 register 也能够间接注册自定义指令,ctx缓存此指令到commands

registerCommand (command: ICommand) {if (this.ctx.commands.has(command.name)) {throw new Error(` 命令 ${command.name} 已存在 `)
  }
  this.ctx.commands.set(command.name, command)
  this.register(command)
}

registerPlatform

ctx.registerPlatform(hook: IPlatform)

注册一个编译平台。IPlatform同样继承于IHook,最初同样被注册到hooks,具体应用办法详见文档。

registerPlatform (platform: IPlatform) {if (this.ctx.platforms.has(platform.name)) {throw new Error(` 适配平台 ${platform.name} 已存在 `)
  }
  addPlatforms(platform.name)
  this.ctx.platforms.set(platform.name, platform)
  this.register(platform)
}

applyPlugins

ctx.applyPlugins(args: string | { name: string, initialVal?: any, opts?: any})

触发注册的钩子。批改类型 增加类型 的钩子领有返回后果,否则不必关怀其返回后果。

应用形式:

ctx.applyPlugins('onStart')
const assets = await ctx.applyPlugins({
  name: 'modifyBuildAssets',
  initialVal: assets,
  opts: {assets}
})

addPluginOptsSchema

ctx.addPluginOptsSchema(schema: Function)

为插件入参增加校验,承受一个函数类型参数,函数入参为 joi 对象,返回值为 joi schema。

在初始化插件 initPlugin 中最终会调用 KernelcheckPluginOpts校验插件入参类型是否失常:

checkPluginOpts (pluginCtx, opts) {if (typeof pluginCtx.optsSchema !== 'function') {return}
  const schema = pluginCtx.optsSchema(joi)
  if (!joi.isSchema(schema)) {throw new Error(` 插件 ${pluginCtx.id}中设置参数查看 schema 有误,请查看!`)
  }
  const {error} = schema.validate(opts)
  if (error) {error.message = ` 插件 ${pluginCtx.id}取得的参数不符合要求,请查看!`
    throw error
  }
}

到这里为止,插件办法的作用及其在源码中的实现形式曾经大抵理解了,其实插件办法结尾说的 initPluginCtx 中的流程才走完第一步。

插件上下文信息获取逻辑

initPluginCtx ({id, path, ctx}: {id: string, path: string, ctx: Kernel}) {const pluginCtx = new Plugin({ id, path, ctx})
  // 定义插件的两个外部办法(钩子函数):onReady 和 onStart
  const internalMethods = ['onReady', 'onStart']
  // 定义一些 api
  const kernelApis = [
    'appPath',
    'plugins',
    'platforms',
    'paths',
    'helper',
    'runOpts',
    'initialConfig',
    'applyPlugins'
  ]
  // 注册 onReady 和 onStart 钩子,缓存到 ctx.methods 中
  internalMethods.forEach(name => {if (!this.methods.has(name)) {pluginCtx.registerMethod(name)
    }
  })
  return new Proxy(pluginCtx, {
    // 参数:指标对象,属性名
    get: (target, name: string) => {if (this.methods.has(name)) {

        // 优先从 Kernel 的 methods 中找此属性
        const method = this.methods.get(name)

        // 如果是办法数组则返回遍历数组中函数并执行的办法
        if (Array.isArray(method)) {return (...arg) => {
            method.forEach(item => {item.apply(this, arg)
            })
          }
        }
        return method
      }
      // 如果拜访的是以上 kernelApis 中的一个,判断是办法则返回办法,扭转了 this 指向,是一般对象则返回此对象
      if (kernelApis.includes(name)) {return typeof this[name] === 'function' ? this[name].bind(this) : this[name]
      }
      // Kernel 中没有就返回 pluginCtx 的此属性
      return target[name]
    }
  })
}

initPluginCtx最终返回了 Proxy 代理对象,后续执行插件办法的时候会把该上下文信息(也就是这个代理对象)当成第一个参数传给插件的 apply 办法调用,apply的第二个参数就是插件参数了。

因而,当咱们在插件开发的时候,从 ctx 中去获取相干属性值,就须要走 Proxy 中的逻辑。能够从源码中看到,属性优先是从 Kernel 实例去拿的,Kernel实例中的 methods 没有此办法,则从 Plugin 对象上去取。

此时插件的上下文中曾经有两个外部的钩子,onReadyonStart

留神:pluginCtx.registerMethod(name),注册 internalMethods 的时候,并没有传回调办法,因而开发者在本人编写插件时能够注册对应的钩子,在钩子里执行本人的逻辑代码

内置插件钩子函数执行机会

初始化预设和插件后,至此,开始执行第一个钩子函数—onReady。此时流程曾经走到上述插件的次要流程中的最初一步:

// Kernel.init

await this.applyPlugins('onReady')

回头看 CLI 流程的第六步,回顾 Kernel.tsrun办法中的执行流程,在执行 onReady 的钩子后就执行了 onStart 钩子,同样,注册此钩子也没有执行操作,如须要开发者能够去增加回调函数在 onStart 时执行操作。

run持续往下执行了 modifyRunnerOpts 钩子,其作用就是:批改 webpack 参数,例如批改 H5 postcss options

执行平台命令

Kernel.run最初一个流程就是执行命令。

// 执行传入的命令
await this.applyPlugins({
  name,
  opts
})

这里能够解释分明最终 yarn startTaro到底做了哪些事,执行了 yarn start 后最终的脚本是 taro build --type xxx,在后面预设和插件初始化的时候提到过,taro 有许多内置的插件(预设)会初始化掉,这些钩子函数会缓存在 Kernel 实例中,taro内置预设寄存在 taro-cli/src/presets/ 下,这次具体看一下到底有哪些内置的插件,先看大体的目录:

在 commands 下能够看到许多咱们眼生的指令名称,如 createdoctorhelpbuild 等等,constants下定义一些内置的钩子函数名称,例如:modifyWebpackChainonBuildStartmodifyBuildAssetsonCompilerMake等等,files下三个插件之前在插件办法中曾经解释了,platforms下次要是注册平台相干的指令,以 h5 平台举例:

// taro-cli/src/presets/platforms/h5.ts

export default (ctx: IPluginContext) => {
  ctx.registerPlatform({
    name: 'h5',
    useConfigName: 'h5',
    async fn ({config}) {const { appPath, outputPath, sourcePath} = ctx.paths
      const {initialConfig} = ctx
      const {port} = ctx.runOpts
      const {emptyDirectory, recursiveMerge, npm, ENTRY, SOURCE_DIR, OUTPUT_DIR} = ctx.helper
      emptyDirectory(outputPath)
      const entryFileName = `${ENTRY}.config`
      const entryFile = path.basename(entryFileName)
      const defaultEntry = {[ENTRY]: [path.join(sourcePath, entryFile)]
      }
      const customEntry = get(initialConfig, 'h5.entry')
      const h5RunnerOpts = recursiveMerge(Object.assign({}, config), {
        entryFileName: ENTRY,
        env: {TARO_ENV: JSON.stringify('h5'),
          FRAMEWORK: JSON.stringify(config.framework),
          TARO_VERSION: JSON.stringify(getPkgVersion())
        },
        port,
        sourceRoot: config.sourceRoot || SOURCE_DIR,
        outputRoot: config.outputRoot || OUTPUT_DIR
      })
      h5RunnerOpts.entry = merge(defaultEntry, customEntry)
      const webpackRunner = await npm.getNpmPkg('@tarojs/webpack-runner', appPath)
      webpackRunner(appPath, h5RunnerOpts)
    }
  })
}

平时咱们在配置 h5 的时候,会给 h5 独自设置入口,只有把入口文件名称改成index.h5.js,配置文件也是如此:index.h5.config,想必当初应该晓得为什么能够这么做了吧。

回到 `taro build --type xxx,由build 指令找到其定义文件所在位置—taro-cli/src/presets/commands/build.ts,插件办法中介绍完 registerCommand 可知:指令(commands)缓存到上下文 commands 后最终也是调用了 regigter 注册了该指令钩子函数,这也是为什么执行命令时调用 applyPlugins 能够执行 build 指令的原由。如下可知 build 指令大抵做了哪些工作:

import {IPluginContext} from '@tarojs/service'
import * as hooks from '../constant'
import configValidator from '../../doctor/configValidator'

export default (ctx: IPluginContext) => {
  // 注册编译过程中的一些钩子函数
  registerBuildHooks(ctx)
  ctx.registerCommand({
    name: 'build',
    optionsMap: {},
    synopsisList: [],
    async fn (opts) {
      // ...
      // 校验 Taro 我的项目配置
      const checkResult = await checkConfig({
        configPath,
        projectConfig: ctx.initialConfig
      })
      // ...
      // 创立 dist 目录
      fs.ensureDirSync(outputPath)
      // ...
      // 触发 onBuildStart 钩子
      await ctx.applyPlugins(hooks.ON_BUILD_START)
      // 执行对应平台的插件办法进行编译
      await ctx.applyPlugins({/** xxx */})
      // 触发 onBuildComplete 钩子,编译完结!await ctx.applyPlugins(hooks.ON_BUILD_COMPLETE)
    }
  })
}

function registerBuildHooks (ctx) {
  [
    hooks.MODIFY_WEBPACK_CHAIN,
    hooks.MODIFY_BUILD_ASSETS,
    hooks.MODIFY_MINI_CONFIGS,
    hooks.MODIFY_COMPONENT_CONFIG,
    hooks.ON_COMPILER_MAKE,
    hooks.ON_PARSE_CREATE_ELEMENT,
    hooks.ON_BUILD_START,
    hooks.ON_BUILD_FINISH,
    hooks.ON_BUILD_COMPLETE,
    hooks.MODIFY_RUNNER_OPTS
  ].forEach(methodName => {ctx.registerMethod(methodName)
  })
}

其中,对于对各个平台代码编译的工作都在 ctx.applyPlugins({name: platform,opts: xxx}) 中,以编译到小程序平台举例:

ctx.applyPlugins({
  name: 'weapp',
  opts: {// xxx}
)

既然要执行钩子 weapp,那么就须要有提前注册过这个钩子,weapp 这个 hooks 是在哪个阶段被注册进去的呢?

解说 ctx.plugin 的时候有介绍初始化预设跟插件的流程—initPresetsAndPlugins,此流程中会初始化框架内置的预设(插件),并且有提过框架内置预设是在 taro-cli/src/presets/index.tsindex.ts 中有对于平台(platform)相干的插件:

export default () => {
  return {
    plugins: [
       // platforms
      path.resolve(__dirname, 'platforms', 'h5.js'),
      path.resolve(__dirname, 'platforms', 'rn.js'),
      path.resolve(__dirname, 'platforms', 'plugin.js'),
      ['@tarojs/plugin-platform-weapp', { backup: require.resolve('@tarojs/plugin-platform-weapp') }],
      ['@tarojs/plugin-platform-alipay', { backup: require.resolve('@tarojs/plugin-platform-alipay') }],
      ['@tarojs/plugin-platform-swan', { backup: require.resolve('@tarojs/plugin-platform-swan') }],
      ['@tarojs/plugin-platform-tt', { backup: require.resolve('@tarojs/plugin-platform-tt') }],
      ['@tarojs/plugin-platform-qq', { backup: require.resolve('@tarojs/plugin-platform-qq') }],
      ['@tarojs/plugin-platform-jd', { backup: require.resolve('@tarojs/plugin-platform-jd') }],

      // commands
      // ...

      // files
      // ...

      // frameworks
      // ...
    ]
  }
}

从中很容易就找到了所有可编译平台的插件源码所在目录,找到 @tarojs/plugin-platform-weapp 所在目录,关上入口文件:

export default (ctx: IPluginContext, options: IOptions) => {
  ctx.registerPlatform({
    name: 'weapp',
    useConfigName: 'mini',
    async fn ({config}) {const program = new Weapp(ctx, config, options || {})
      await program.start()}
  })
}

由此可知,小程序平台编译插件会首先 registerPlatform:weapp,而registerPlatform 操作最终会把 weapp 注册到 hooks 中。随后调用了 program.start 办法,此办法定义在基类中,class Weapp extends TaroPlatformBase,TaroPlatformBase 类定义在 taro-service/src/platform-plugin-base.ts 中,start办法正是调用 mini-runner 开启编译,mini-runner就是 webpack 编译程序,独自开一篇文章介绍,具体平台(platform)编译插件的执行流程和其中具体细节也在后续独自的文章中介绍。

总结

本文依照 Tarocli执行流程程序解说了每个流程中 Taro 做了哪些工作,并针对 Taro 文章中插件开发的章目解说了每个 api 的由来和具体用法,深刻理解 Taro 在编译我的项目过程的各环节的执行原理,为我的项目中开发构建优化、拓展更多功能,为本身业务定制个性化性能夯实根底。

退出移动版