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]
}
})
}
自定义插件
查看文档
可对编译过程、编译平台进行扩展