8
头图

foreword

Since 2.2, Taro has introduced a plug-in mechanism to allow developers to expand more functions for Taro or customize personalized functions for their own business by writing plug-ins.

This article is based on Taro3.4.2 source code explanation

CLI process

  1. Execute the cli command, such as npm run start . In fact, in the package.json script list in script , you can interpret it down and find the corresponding specific instruction information executed by the script build:weapp . The difference in dev mode is that there is only one --watch hot load in prod mode. The corresponding env environment is distinguished, and different packaging configurations of the corresponding environment are preset when webpack is packaged, for example, code compression will be enabled by default when judging the production environment, etc.

    image-20220228080218285

  2. So where is this taro command defined? taro has already been configured with environment variables when you install it globally. We go to the project directory and execute the `package.json script command in the project directory. It will look for the script script in the current directory. node you can't find it, you will find it from the superior, and finally execute the script.
  3. The source code of taro's core instructions are all under taro/cli , and the commonly used instructions are init (create project) and build (build project). Start command entry at 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. After startup, the CLI instance first instantiates a Kernel core class ( ctx ) that inherits EventEmitter . After parsing the script command parameters, the customCommand method is called, and the kernel instance is related to all project parameters.

    // 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 , call Kernel.run after sorting all the parameters, and pass in all the sorted parameters.

    kernel.run({
      name: command,
      opts: {
        _: args._,
        options,
        isHelp: args.h
      }
    })
  6. Next is the workflow of a series of project initialization in the Kernel class, including setting parameters, initializing related configurations, executing built-in hook functions, modifying webpack, etc. All properties in Kernel can be accessed through ctx during plug-in development, briefly Part of the code is as follows:

    // 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
        })
      }
    

    The key initialization process is in Kernel.init .

The main process of the plug-in

Kernel.init process is as follows:

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')
}

Plugin environment variables

The implementation principle of the main environment variables that may be used when using ctx given in the traceback document, details about the use of environment variables 👉🏻 Document address

ctx.runOpts

Get the parameters of the currently executed command, such as command taro upload --remote xxx.xxx.xxx.xxx , then the value of ctx.runOpts is:

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

runOpts in taro-service/src/Kernel.ts 's run method of initializing, earlier than Kernel.init , because runOpts command parameters included in the instance of Kernel when it is already resolved, but in run inside to the current context ( Kernel ) assignment saved, that is, when the call ctx . The source code is as follows:

// taro-service/src/Kernel.ts

this.setRunOpts(opts)

// 保存当前执行命令所带的参数
setRunOpts (opts) {
  this.runOpts = opts
}

ctx.helper

It is a quick way to use the package @tarojs/helper , including all its APIs, mainly some tool methods and constants, such as the four methods used in Kernel.ts :

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

Among them, the createBabelRegister method is used more frequently in open source projects, and its extended usage: through createBabelRegister , it supports the use of import in app.config.ts environments such as commonJs or require

ctx.initialConfig

Get project configuration.

Find initialConfig: IProjectConfig type definition file, you can see that the structure is consistent with the configuration structure agreed in the configuration file under config of the Taro project.

Details 👉🏻 Compile configuration details

// 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
}

Looking back at the Kernel.ts method in init , the first main process is to initialize the project configuration at initConfig , which is the configuration items configured in the config directory under your project root directory.

// taro-service/src/Kernel.ts

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

Config class will find the config/index.js file of the project to initialize the configuration information

// 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

The second main process of the Kernel.ts method in init is to initialize the plugin environment variable ctx.paths , which contains the relevant paths of the currently executed command. All paths are as follows (not all commands will have all the following paths):

  • ctx.paths.appPath , the directory where the current command is executed, if it is the build command, it is the current project path
  • ctx.paths.configPath , the current project configuration directory, if the init command, there is no such path
  • ctx.paths.sourcePath , the current project source code path
  • ctx.paths.outputPath , the current project output code path
  • ctx.paths.nodeModulesPath , the node_modules path used by the current project

The source code is as follows:

// 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

The third main process of Kernel.ts method of init is initPresetsAndPlugins initialization preset and plug-in, which is also the most complicated process in init . The main products are ctx.plugins and ctx.extraPlugins .

The plug-in function introduced in the official document is just a few words about presets, and demo is not given to explain how to use it, but it leaves a more important concept - presets are a collection of plug-ins .

Examples of presets given in the documentation are as follows:

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

Just gave the presets configuration, but it is not clear how the '@tarojs/preset-sth' or /absulute/path/preset/filename plugin is implemented internally. So check the source code, because Taro has a series of built-in presets, which are passed to Kernel when options is initialized. In the fourth step of the previous CLI process, you can actually see the following:

// taro-cli/src/cli.ts

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

So I found taro-cli/src/presets/index.ts (part of the code omitted):

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') }],
      // ... 省略其他
    ]
  }
}

Wouldn't it be enough to imitate him and write one?

// 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'),
    ],
  };
};

Summarize:

  • preset

    is a collection of plugins, a preset file should return an array of plugins containing the plugins configuration.

  • plugin

    It has a fixed code structure and returns a function function. The first parameter is the upper and lower information ctx in the packaging process. An important parameter modifyWebpackChain can be obtained in ctx, and the webpack configuration can be modified through it. The second parameter is options , which can be Enter the parameters required by the plugin in the place where the plugin is defined in config under plugins . The plug-in part can refer to the documentation, the description is relatively clear.

The process of initializing presets and plugins is as follows:

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)
}

plugin method

Such as ctx.register , ctx.registerMethod , ctx.registerCommand , ctx.registerPlatform , ctx.applyPlugins , ctx.addPluginOptsSchema , ctx.generateProjectConfig these plug-in methods described in the documentation, you can see all the plug- ctx take in these methods is that the plug-what stage in the build is registered into , and how does it flow?

The plugin methods are defined in the taro-service/src/Plugin.ts class of Plugin . Our custom plugins (including presets) and Taro built-in plugins (including presets) will be in the above initialization presets and plugin methods initPresetsAndPlugins and resolvePlugins in the process of resolvePresets is initialized, and each plugin is initialized one by one:

// 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 = {}
}

Each plugin is wrapped by resolvePresetsOrPlugins method before initialization, find the definition of this method in 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))
      }
    }
  })
}

In initPreset and initPlugin , one of the more important processes - initPluginCtx , it did the job content of the context initializing plug-in, which call initPluginCtx time method, the Kernel as a parameter to the ctx property, in addition to id and path , we It is already known that both values are absolute paths to the plugin.

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

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

It was in initPluginCtx that for the first time I saw a word that is most closely related to the subject of this article - Plugin , I opened the class definition file Plugin , and found all the plug-in methods extended to developers in the document, which are the above-mentioned plug-in methods The methods described at the beginning.

// 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
  }
}

Wait, didn't you say all? Why didn't I see writeFileToDist , generateFrameworkInfo , generateProjectConfig ? In fact, when initializing the preset, these three words have already appeared. When I introduced ctx.plugins , I mentioned the built-in preset file of taro-cli/src/presets/index.ts , and part of the code of files was omitted. Repost it here:

// 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')

Take writeFileToDist as an example to see in detail what functions this plugin implements:

// 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)
  })
}

It can be seen that the method writeFileToDist is registered to ctx through registerMethod , and the other two methods are the same.

registerMethod

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

The official documentation of Taro also gave us an explanation—mounting a method to ctx can be called directly by other plugins.

Go back to Plugin itself, study each of its attribute methods, and find registerMethod first:

// 向 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
}

Register a hook that can be called by other plugins, receiving one parameter, the Hook object. Hooks registered through ctx.register need to be triggered through method ctx.applyPlugins .

The method of Plugin in register is defined as follows:

// 注册钩子一样需要通过方法 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))
}

The hook registered through register will automatically inject id (absolute path) of the current plugin, and finally merged into ctx.hooks , to be called by applyPlugins

registerCommand

ctx.registerCommand(hook: ICommand)

A method that feels very imaginative, you can customize the instructions, such as taro create xxx , you can quickly generate some general templates, components or methods according to your needs.

ICommand inherited from IHook

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

Therefore register can also directly register custom instructions, and ctx caches this instruction to 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)

Register a build platform. IPlatform is also inherited from IHook , and finally registered to hooks . For details, see the documentation.

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 })

Hook that triggers registration. modify the type and add the hook of type to have the return result, otherwise don't care about the return result.

How to use:

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

addPluginOptsSchema

ctx.addPluginOptsSchema(schema: Function)

Add validation for plugin input parameters, accept a function type parameter, the function input parameter is joi object, and the return value is joi schema.

In the initialization plug-in initPlugin , Kernel of checkPluginOpts will eventually be called to check whether the input parameter type of the plug-in is normal:

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
  }
}

So far, the function of the plug-in method and its implementation in the source code have been roughly understood. In fact, the process in initPluginCtx at the beginning of the plug-in method has only completed the first step.

Plugin context information acquisition logic

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 finally returns the Proxy proxy object. When the plug-in method is subsequently executed, the context information (that is, the proxy object) will be passed as the first parameter to the apply method call of the plug-in. The second parameter of apply is the plug-in parameter.

Therefore, when we are developing the plug-in, to get the relevant attribute value from ctx , we need to follow the logic in Proxy . It can be seen from the source code that the attribute priority is taken from the Kernel instance. If the methods in the Kernel instance does not have this method, it is taken from Plugin object.

There are already two internal hooks in the context of the plugin at this point, onReady and onStart .

Note: pluginCtx.registerMethod(name) , when registering internalMethods , there is no callback method, so developers can register the corresponding hook when writing a plug-in, and execute their own logic code in the hook

Built-in plug-in hook function execution timing

After initializing the presets and plug-ins, so far, the first hook function is executed — onReady . At this point, the process has reached the last step in the main process of the above plugin:

// Kernel.init

await this.applyPlugins('onReady')

Looking back at the sixth step of the CLI process, and reviewing the execution process in the Kernel.ts method of run , after executing the onReady hook, the onStart hook is executed. Similarly, the registration of this hook does not perform any operations. If necessary, developers can add a callback function Execute the action at onStart .

run continues to execute the modifyRunnerOpts hook, its function is to modify webpack parameters, such as modify H5 postcss options .

execute platform command

Kernel.run The last process is to execute the command.

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

Here we can explain what Taro did after the final yarn start . After executing yarn start , the final script is taro build --type xxx . As mentioned in the previous preset and plugin initialization, taro has many built-in plugins (presets) that will be initialized. , these hook functions will be cached in the Kernel instance, and the built-in presets of taro are stored under taro-cli/src/presets/ . This time, let's take a look at the built-in plugins, first look at the general directory:

image-20220305233407390

In the commands you can see many of our familiar command names, such as create , doctor , help , build etc., constants define some built-in hook function name, for example: modifyWebpackChain , onBuildStart , modifyBuildAssets , onCompilerMake etc., files next three The plug-in has been explained in the plug-in method before. Under platforms , there are mainly instructions related to the registration platform. Take the h5 platform as an example:

// 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)
    }
  })
}

Usually when we configure h5, we will set a separate entry for h5, as long as the entry file name is changed to index.h5.js , the configuration file is also the same: index.h5.config , presumably now you should know why you can do this.

Going back to `taro build --type xxx , the location of its definition file is found by the build instruction— taro-cli/src/presets/commands/build.ts . After registerCommand is introduced in the plug-in method, we can see that the instruction ( commands ) is cached in the context commands , which is also why the regigter is finally called to register the instruction execution hook function after 062276120f617. The reason why the build instruction can be executed when calling applyPlugins . The following shows what the build instruction roughly does:

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)
  })
}

Among them, the work of compiling code for each platform is in ctx.applyPlugins({name: platform,opts: xxx}) , taking the example of compiling to the applet platform:

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

Since the hook weapp is to be executed, then this hook needs to be registered in advance. At which stage was the hooks registered in weapp ?

When explaining ctx.plugin , I introduced the process of initializing presets and plug-ins — initPresetsAndPlugins . In this process, the presets (plug-ins) built into the framework will be initialized, and it is mentioned that the built-in presets of the framework are in taro-cli/src/presets/index.ts and index.ts about the platform ( platform ) Related plugins:

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
      // ...
    ]
  }
}

From it, it is easy to find the directory where the plugin source code of all the compilable platforms is located, find the directory where @tarojs/plugin-platform-weapp is located, and open the entry file:

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()
    }
  })
}

It can be seen that the applet platform compilation plugin will first registerPlatform:weapp , and the registerPlatform operation will eventually register weapp into hooks . Then call program.start method, which is defined in the base class, class Weapp extends TaroPlatformBase , TaroPlatformBase class definition in taro-service/src/platform-plugin-base.ts , the start method is the call mini-runner start the compilation, mini-runner is webpack compiler, a separate article describes the open, platform-specific ( platform ) The execution flow of the compilation plugin and its specific details are also introduced in a separate article.

Summarize

This article explains what Taro does in each process according to the execution process sequence of Taro 's cli , and explains the origin and specific usage of each api in the process of compiling the project Taro to the chapters of plug-in development in the Taro article. The execution principle of the link is used to optimize the development and construction of the project, expand more functions, and lay a solid foundation for customizing personalized functions for its own business.


MangoGoing
774 声望1.2k 粉丝

开源项目:详见个人详情