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
Execute the
cli
command, such asnpm run start
. In fact, in thepackage.json
script list inscript
, you can interpret it down and find the corresponding specific instruction information executed by the scriptbuild:weapp
. The difference indev
mode is that there is only one--watch
hot load inprod
mode. The correspondingenv
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.- 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 thescript
script in the current directory.node
you can't find it, you will find it from the superior, and finally execute the script. The source code of taro's core instructions are all under
taro/cli
, and the commonly used instructions areinit
(create project) andbuild
(build project). Start command entry attaro/cli/bin/taro
// @taro/cli/bin/taro #! /usr/bin/env node require('../dist/util').printPkgVersion() const CLI = require('../dist/cli').default new CLI().run()
After startup, the
CLI
instance first instantiates aKernel
core class (ctx
) that inheritsEventEmitter
. After parsing the script command parameters, thecustomCommand
method is called, and thekernel
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 })
customCommand
, callKernel.run
after sorting all the parameters, and pass in all the sorted parameters.kernel.run({ name: command, opts: { _: args._, options, isHelp: args.h } })
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 inKernel
can be accessed throughctx
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 thebuild
command, it is the current project pathctx.paths.configPath
, the current project configuration directory, if theinit
command, there is no such pathctx.paths.sourcePath
, the current project source code pathctx.paths.outputPath
, the current project output code pathctx.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 isoptions
, which can be Enter the parameters required by the plugin in the place where the plugin is defined inconfig
underplugins
. 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 registeringinternalMethods
, 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:
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.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。