Taro 3.1.0 源码分析

@tarojs/taro

import Taro, { useDidShow } from '@tarojs/taro'

我们用的最频繁的包就是 @tarojs/taro

// taro/index.js
const { CurrentReconciler } = require('@tarojs/runtime')
const taro = require('@tarojs/api').default

if (typeof CurrentReconciler.initNativeApi === 'function') {
  CurrentReconciler.initNativeApi(taro)
}

module.exports = taro
module.exports.default = module.exports

// taro-runtime/reconciler.ts
export const CurrentReconciler: Reconciler<any> = Object.assign({
  getLifecyle (instance, lifecyle) {
    return instance[lifecyle]
  },
  getPathIndex (indexOfNode) {
    return `[${indexOfNode}]`
  },
  getEventCenter (Events) {
    return new Events()
  }
}, defaultReconciler)

// shared/utils.ts
export const defaultReconciler = {}

export function mergeReconciler (hostConfig) {
  Object.assign(defaultReconciler, hostConfig)
}

// taro-weapp/src/runtime/ts
import { mergeReconciler, mergeInternalComponents } from '@tarojs/shared'
import { hostConfig, components } from './runtime-utils'

mergeReconciler(hostConfig)
mergeInternalComponents(components)

Taro 根据不同的编译环境变量,引入了对应的编译包。
编译包引入的是一个 initNativeAPI 初始化函数,用于初始化对应平台的原生 API。

@tarojs/taro-weapp

微信小程序平台initNativeApi所做的事情就是将微信的 API 进行一些二次封装,然后转成挂载在 Taro 对象下,开发者调用 Taro 的 API 即可调用到微信官方 API。

export function initNativeApi (taro) {
  processApis(taro, wx, {
    noPromiseApis,
    needPromiseApis
  })
  taro.cloud = wx.cloud
}

processApis 收集了当前运行平台的 _noPromiseApis_needPromiseApis,将_needPromiseApis的api Promise 化,都绑定到Taro对象上。

request

processApis也做了Taro.request 请求改造,将Taro.request 绑定 new taro.Link

/**
 * 挂载常用 API
 * @param taro Taro 对象
 * @param global 小程序全局对象,如微信的 wx,支付宝的 my
 */
function equipCommonApis (taro, global, apis: Record<string, any> = {}) {
  ...
  // request & interceptors
  const request = apis.request ? apis.request : getNormalRequest(global)
  function taroInterceptor (chain) {
    return request(chain.requestParams)
  }
  const link = new taro.Link(taroInterceptor)
  taro.request = link.request.bind(link)
  taro.addInterceptor = link.addInterceptor.bind(link)
  taro.cleanInterceptors = link.cleanInterceptors.bind(link)
  taro.miniGlobal = global
}

// taro-api/interceptor/index.js
export default class Link {
  constructor (interceptor) {
    this.taroInterceptor = interceptor
    this.chain = new Chain()
  }

  request (requestParams) {
    this.chain.interceptors = this.chain.interceptors.filter(interceptor => interceptor !== this.taroInterceptor)
    this.chain.interceptors.push(this.taroInterceptor)
    return this.chain.proceed({ ...requestParams })
  }

  addInterceptor (interceptor) {
    this.chain.interceptors.push(interceptor)
  }

  cleanInterceptors () {
    this.chain = new Chain()
  }
}

到这里,@tarojs/taro 就已经解析完成了


@tarojs/cli

@taro/cli 提供了 taro 命令如 init,bulid 能力。

// bin/taro
const CLI = require('../dist/cli').default
new CLI().run()

// cli.ts
export default class CLI {
  appPath: string
  constructor (appPath) {
    this.appPath = appPath || process.cwd()
  }

  run () {
    this.parseArgs()
  }

  parseArgs () {
    const args = minimist(process.argv.slice(2), {
    alias: {
        version: ['v'],
        help: ['h']
    },
    boolean: ['version', 'help']
    })
    /** args = {
         _: [ 'build' ],
        version: false,
        v: false,
        help: false,
        h: false,
        type: 'weapp',
        watch: true
    }
        */
    const _ = args._
    const command = _[0]
    if (command) {
        // 记住这里,cli创建一个内核实例,传入了一个preset,这个presets加载了所有内置命令
        const kernel = new Kernel({
            appPath: this.appPath,
            presets: [
                path.resolve(__dirname, '.', 'presets', 'index.js')
            ]
        })
        switch (command) {
            ...
        }
    }
  }
}

Cli 主要是做了 parseArgs,获取到了命令参数后,调用对应方法。
最终所有方法都是调用 Kernel.run 。

kernel.run({
  name: 'init',
  opts: {
    appPath,
    projectName,
    typescript,
    templateSource,
    clone,
    template,
    css,
    isHelp
  }
})

kernel.run({
  name: 'build',
  opts: {
    platform,
    isWatch,
    port,
    blended,
    envHasBeenSet,
    // plugin,
    // release,
    isHelp
  }
})

Kernel.run

async run (args: string | { name: string, opts?: any }) {
    let name
    let opts
    if (typeof args === 'string') {
        name = args
    } else {
        name = args.name
        opts = args.opts
    }
    this.setRunOpts(opts) // 为 this.runOpts 赋值
    await this.init() // 初始化,Config、Path、插件,并执行 onReady 钩子
    await this.applyPlugins('onStart') // 执行 onStart 钩子
    if (opts && opts.platform) {
        opts.config = this.runWithPlatform(opts.platform) // 初始化目标平台config
        ...
    }
    await this.applyPlugins({
        name,
        opts
    })
}

Kernel.run 方法,会先 init 进行初始化。
初始化项目配置、项目路径、Presets、Plugins,执行 'onReady' 钩子。
执行 onStart 钩子。
执行目标平台初始化。
最后通过applyPlugins执行 build 命令。
初始化后 Kernel 会把 build 命令挂载,执行的时候执行挂载文件cli/command/xxx文件下的fn方法。

每一个执行命令、构建平台,在 taro 体系中都是一个 Plugin,初始化时会注册。
applyPlugins 就是执行命令、钩子的入口方法。

Taro 以及很多脚手架的命令行交互功能都是通过 inquirer 库实现的。

async init () {
  this.initConfig() // 初始化项目配置
  this.initPaths() // 初始化项目路径
  this.initPresetsAndPlugins() // 初始化Presets、Plugins
  await this.applyPlugins('onReady') // 执行 'onReady' 钩子
}

一个 preset 是一系列 Taro 插件的集合,配置语法同 plugins。

// Kenel.ts
resolvePresets (presets) {
  // preset 集合
  const allPresets = resolvePresetsOrPlugins(this.appPath, presets, PluginType.Preset)
  while (allPresets.length) {
    // 从头开始一个一个初始化插件
    this.initPreset(allPresets.shift()!)
  }
}
// @taro-service/utils/index.ts
export function resolvePresetsOrPlugins (root: string, args, type: PluginType): IPlugin[] {
  return Object.keys(args).map(item => {
    const fPath = resolve.sync(item, {
      basedir: root,
      extensions: ['.js', '.ts']
    })
    return {
      id: fPath,
      path: fPath,
      type,
      opts: args[item] || {},
      apply () {
         /**
          * getModuleDefaultExport 就是 exports.default 的封装
          * fPath 是 插件所在目录
          * 插件定义是 export default (ctx, pluginOpts) => {
          *   ...
          * }
          * apply 方法是 将插件 return 出去
          */
        return getModuleDefaultExport(require(fPath))
      }
    }
  })
}
// Kenel.ts
initPreset (preset: IPreset) {
  const { id, path, opts, apply } = preset // 解构
  // 初始化插件的 ctx(上下文),initPluginCtx 方法返回的是一个 Proxy 对象
  const pluginCtx = this.initPluginCtx({ id, path, ctx: this })
  /**
   * apply将返回,后又传入插件的 ctx(上下文)和 插件的option
   * export default (ctx, pluginOpts) => {
      ctx.addPluginOptsSchema(joi => {
        return joi.object().keys({...})
      })
     }
   * 在插件中导出的方法,反过来调用了内核方法进行操作
   * 设计模式中 控制反转 Ioc 模式。
   */
  const { presets, plugins } = apply()(pluginCtx, opts) || {}
  // 放入 this.plugins 集合
  this.registerPlugin(preset)
  // 如果当前插件返回了 presets, plugins,继续进行初始化
  if (Array.isArray(presets)) {
    const _presets = resolvePresetsOrPlugins(this.appPath, convertPluginsToObject(presets)(), PluginType.Preset)
    while (_presets.length) {
      this.initPreset(_presets.shift()!)
    }
  }
  if (Array.isArray(plugins)) {
    this.extraPlugins.push(...resolvePresetsOrPlugins(this.appPath, convertPluginsToObject(plugins)(), PluginType.Plugin))
  }
}

平台在初始化插件时,运用了IOC模式,使插件成功调用了平台的方法,使插件功能得到进一步扩展。

Kernel.methods

在初始化插件上下文时,会返回一个代理对象。
定义了一些内置方法,如果调用的方法不在内置方法里,就调用传入的target对象的方法。

initPluginCtx ({ id, path, ctx }: { id: string, path: string, ctx: Kernel }) {
  const pluginCtx = new Plugin({ id, path, ctx })
  const internalMethods = ['onReady', 'onStart']
  const kernelApis = [
    'appPath',
    'plugins',
    'platforms',
    'paths',
    'helper',
    'runOpts',
    'initialConfig',
    'applyPlugins'
  ]
  internalMethods.forEach(name => {
    if (!this.methods.has(name)) {
      pluginCtx.registerMethod(name)
    }
  })
  return new Proxy(pluginCtx, {
    get: (target, name: string) => {
      if (this.methods.has(name)) return this.methods.get(name)
      if (kernelApis.includes(name)) {
        return typeof this[name] === 'function' ? this[name].bind(this) : this[name]
      }
      return target[name]
    }
  })
}

自定义插件

查看文档
可对编译过程、编译平台进行扩展


待续

posted @ 2022-02-18 18:15  远方的少年🐬  阅读(250)  评论(0编辑  收藏  举报