本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是
学习源码整体架构系列,链接: juejin.cn/post/737836… 。
2024.08.27 更新:initPluginCtx 初始化插件 ctx
2024.08.30 更新:registerPlugin 注册插件 resolvePlugins 解析插件 等
- 揭开整个框架的入口-taro init 初始化项目的秘密
- 揭开整个框架的插件系统的秘密
- 每次创建新的 taro 项目(taro init)的背后原理是什么
- Taro 4.0 已正式发布 - 每次 npm run dev:weapp 开发小程序,build 编译打包是如何实现的?
前言:
- 如何合并预设插件集合和插件(CLI、用户项目(config/index.ts)、全局插件
/User/用户名/.taro-gobal-config) - 插件是如何注册的
- 插件是如何调用的
- 等等
项目git地址,环境准备、如何调试--传送门(揭开整个框架的入口-taro init 初始化项目的秘密)
上篇回顾:
CLI最终执行Kernel(内核)中的run函数,其中this.initPresetsAndPlugins初始化预设插件集合和插件本文讲解。
1. new Kernel构造函数
interface IKernelOptions {
appPath: string;
config: Config;
presets?: PluginItem[];
plugins?: PluginItem[];
}
export default class Kernel extends EventEmitter {
constructor(options: IKernelOptions) {
super();
this.debugger =
process.env.DEBUG === "Taro:Kernel"
? helper.createDebug("Taro:Kernel")
: function () {};
this.appPath = options.appPath || process.cwd();
this.optsPresets = options.presets;
this.optsPlugins = options.plugins;
this.config = options.config;
this.hooks = new Map();
this.methods = new Map();
this.commands = new Map();
this.platforms = new Map();
this.initHelper();
this.initConfig();
this.initPaths();
this.initRunnerUtils();
}
async run(args: string | { name: string; opts?: any }) {
// 省略若干代码
this.debugger("initPresetsAndPlugins");
this.initPresetsAndPlugins();
console.log('initPresetsAndPlugins', this);
await this.applyPlugins("onReady");
// 省略若干代码
}
// initPresetsAndPlugins
initPresetsAndPlugins() {
// 初始化插件集和插件
}
}
执行完initPresetsAndPlugins之后,这里以init为例。
使用调试器终端,输入$node ./packages/taro-cli/bin/taro init taro-init-debug命令调试。我们可以在packages/taro-service/dist/Kernel.js打印this出来。
{
_events: {
},
_eventsCount: 0,
_maxListeners: undefined,
debugger: function () { },
appPath: "...",
optsPresets: [],
optsPlugins: [],
config: {
appPath: "...",
disableGlobalConfig: false,
initialConfig: {
env: {
},
},
initialGlobalConfig: {
},
isInitSuccess: false,
configPath: "...",
},
hooks: {
},
methods: {
},
commands: {
},
platforms: {
},
helper: {},
initialConfig: {
env: {
},
},
initialGlobalConfig: {
},
paths: {},
cliCommandsPath: "/Users/suhaoson/Documents/development/suhaoson/source_code/taro/packages/taro-cli/dist/presets/commands",
cliCommands: [],
runOpts: { },
plugins: {
},
extraPlugins: {
},
globalExtraPlugins: {
},
}
本文主要学习initPresetsAndPlugins具体实现:
// packages/taro-service/src/Kernel.ts
// 省略部分代码
initPresetsAndPlugins () {
const initialConfig = this.initialConfig
const initialGlobalConfig = this.initialGlobalConfig
const cliAndProjectConfigPresets = mergePlugins(this.optsPresets || [], initialConfig.presets || [])()
const cliAndProjectPlugins = mergePlugins(this.optsPlugins || [], initialConfig.plugins || [])()
const globalPlugins = convertPluginsToObject(initialGlobalConfig.plugins || [])()
const globalPresets = convertPluginsToObject(initialGlobalConfig.presets || [])()
this.debugger('initPresetsAndPlugins', cliAndProjectConfigPresets, cliAndProjectPlugins)
this.debugger('globalPresetsAndPlugins', globalPlugins, globalPresets)
process.env.NODE_ENV !== 'test' &&
helper.createSwcRegister({
only: [
...Object.keys(cliAndProjectConfigPresets),
...Object.keys(cliAndProjectPlugins),
...Object.keys(globalPresets),
...Object.keys(globalPlugins)
]
})
this.plugins = new Map()
this.extraPlugins = {}
this.globalExtraPlugins = {}
this.resolvePresets(cliAndProjectConfigPresets, globalPresets)
this.resolvePlugins(cliAndProjectPlugins, globalPlugins)
}
// 省略部分代码...
接下来,看CLI调用new Kernel的地方
// packages/taro-cli/src/cli.ts
const kernel = new Kernel({
appPath,
presets: [
path.resolve(__dirname, '.', 'presets', 'index.js')
],
config,
plugins: []
})
kernel.optsPlugins ||= []
// 将自定义的 变量 添加到 config.env 中,实现 definePlugin 字段定义
const initialConfig = kernel.config?.initialConfig
if (initialConfig) {
initialConfig.env = patchEnv(initialConfig, expandEnv)
}
if (command === 'doctor') {
kernel.optsPlugins.push('@tarojs/plugin-doctor')
} else if (commandPlugins.includes(targetPlugin)) {
// 针对不同的内置命令注册对应的命令插件
kernel.optsPlugins.push(path.resolve(commandsPath, targetPlugin))
}
这里以init为例,那么 kernel.optsPlugins注入的就是common/init.js插件
// packages/taro-cli/src/presets/commands/init.ts
import type { IPluginContext } from '@tarojs/service'
export default (ctx: IPluginContext) => {
ctx.registerCommand({
name: 'init',
optionsMap: {
// 省略部分代码
},
async fn (opts) {
// 省略部分代码
}
})
}
传入参数presets预设插件集合如下:
import * as path from 'node:path'
export default () => {
return {
plugins: [
// hooks
path.resolve(__dirname, 'hooks', 'build.js'),
path.resolve(__dirname, 'hooks', 'create.js'),
// 兼容其他平台小程序插件
path.resolve(__dirname, 'files', 'writeFileToDist.js'),
path.resolve(__dirname, 'files', 'generateProjectConfig.js'),
path.resolve(__dirname, 'files', 'generateFrameworkInfo.js')
]
}
}
其中hooks/build.js:
import * as hooks from '../constant'
import type { IPluginContext } from '@tarojs/service'
export default (ctx: IPluginContext) => {
[
hooks.MODIFY_APP_CONFIG,
hooks.MODIFY_WEBPACK_CHAIN,
hooks.MODIFY_VITE_CONFIG,
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.registerMethod 注册方法。其中 ctx 就是 Kernal 实例对象。
使用 swc 的方法之一是通过
require钩子。require钩子会将自身绑定到node的require并自动动态编译文件。不过现在更推荐 @swc-node/register。
源码实现如下,存入到 methods Map 中。后面我们会再次遇到它
registerMethod (...args) {
const { name, fn } = processArgs(args)
const methods = this.ctx.methods.get(name) || []
methods.push(fn || function (fn: Func) {
this.register({
name,
fn
})
}.bind(this))
this.ctx.methods.set(name, methods)
}
2.initPresetsAndPlugins 初始化预设插件集合和插件
// packages/taro-service/dist/Kernel.js
// 省略部分代码
initPresetsAndPlugins() {
const initialConfig = this.initialConfig;
const initialGlobalConfig = this.initialGlobalConfig;
const cliAndProjectConfigPresets = (0, utils_1.mergePlugins)(this.optsPresets || [], initialConfig.presets || [])();
const cliAndProjectPlugins = (0, utils_1.mergePlugins)(this.optsPlugins || [], initialConfig.plugins || [])();
const globalPlugins = (0, utils_1.convertPluginsToObject)(initialGlobalConfig.plugins || [])();
const globalPresets = (0, utils_1.convertPluginsToObject)(initialGlobalConfig.presets || [])();
this.debugger('initPresetsAndPlugins', cliAndProjectConfigPresets, cliAndProjectPlugins);
this.debugger('globalPresetsAndPlugins', globalPlugins, globalPresets);
process.env.NODE_ENV !== 'test' &&
helper.createSwcRegister({
only: [
...Object.keys(cliAndProjectConfigPresets),
...Object.keys(cliAndProjectPlugins),
...Object.keys(globalPresets),
...Object.keys(globalPlugins)
]
});
this.plugins = new Map();
this.extraPlugins = {};
this.globalExtraPlugins = {};
this.resolvePresets(cliAndProjectConfigPresets, globalPresets);
this.resolvePlugins(cliAndProjectPlugins, globalPlugins);
}
// 省略部分代码...
initPresetsAndPlugins做了以下几件事情:
- mergePlugin 合并预设插件集合和插件
- convertPluginsToObject转换全局配置里的插件集合和插件为对象
- 非测试环境,
createSwcRegister使用了 @swc/register 来编译ts等转换成commonjs。可以直接用require读取文件。- resolvePresets解析预设插件集合和resolvePlugins解析插件
3.1 工具函数mergePlugin和convertPluginsToObject
const isNpmPkg = name => !(/^(.|/)/.test(name));
exports.isNpmPkg = isNpmPkg;
function getPluginPath(pluginPath) {
if ((0, exports.isNpmPkg)(pluginPath) || path.isAbsolute(pluginPath))
return pluginPath;
throw new Error('plugin 和 preset 配置必须为绝对路径或者包名');
}
exports.getPluginPath = getPluginPath;
function convertPluginsToObject(items) {
return () => {
const obj = {};
if (Array.isArray(items)) {
items.forEach(item => {
if (typeof item === 'string') {
const name = getPluginPath(item);
obj[name] = null;
}
else if (Array.isArray(item)) {
const name = getPluginPath(item[0]);
obj[name] = item[1];
}
});
}
return obj;
};
}
function mergePlugins(dist, src) {
return () => {
const srcObj = convertPluginsToObject(src)();
const distObj = convertPluginsToObject(dist)();
return (0, lodash_1.merge)(distObj, srcObj);
};
}
接下来,再看看resolvePresets。
3.resolvePresets解析预设插件集合
resolvePresets(cliAndProjectPresets, globalPresets) {
const resolvedCliAndProjectPresets = (0, utils_1.resolvePresetsOrPlugins)(this.appPath, cliAndProjectPresets, constants_1.PluginType.Preset);
while (resolvedCliAndProjectPresets.length) {
this.initPreset(resolvedCliAndProjectPresets.shift());
}
const globalConfigRootPath = path.join(helper.getUserHomeDir(), helper.TARO_GLOBAL_CONFIG_DIR);
const resolvedGlobalPresets = (0, utils_1.resolvePresetsOrPlugins)(globalConfigRootPath, globalPresets, constants_1.PluginType.Plugin, true);
while (resolvedGlobalPresets.length) {
this.initPreset(resolvedGlobalPresets.shift(), true);
}
}
resolvePresets 这个函数主要做了以下几件事情:
- resolvedCliAndProjectPresets解析cli和项目配置的预设插件集合
- resolvedGlobalPresets解析全局的预设插件集合
globalConfigRootPath的路径是/Users/用户名/.taro-global-config
默认是没有配置预设插件集合的:
// packages/taro-cli/templates/global-config/index.json
{
"plugins": [],
"presets": []
}
3.1 工具函数resolvePresetsOrPlugins
resolvePresetsOrPlugins执行后得到的resolvedCliAndProjectPresets
function resolvePresetsOrPlugins(root, args, type, skipError) {
var _a, _b;
// 全局的插件引入报错,不抛出 Error 影响主流程,而是通过 log 提醒然后把插件 filter 掉,保证主流程不变
const resolvedPresetsOrPlugins = [];
const presetsOrPluginsNames = Object.keys(args) || [];
for (let i = 0; i < presetsOrPluginsNames.length; i++) {
const item = presetsOrPluginsNames[i];
let fPath;
try {
fPath = resolve.sync(item, {
basedir: root,
extensions: ['.js', '.ts']
});
}
catch (err) {
if ((_a = args[item]) === null || _a === void 0 ? void 0 : _a.backup) {
// 如果项目中没有,可以使用 CLI 中的插件
fPath = (_b = args[item]) === null || _b === void 0 ? void 0 : _b.backup;
}
else if (skipError) {
// 如果跳过报错,那么 log 提醒,并且不使用该插件
console.log(helper_1.chalk.yellow(`找不到插件依赖 "${item}",请先在项目中安装,项目路径:${root}`));
continue;
}
else {
console.log(helper_1.chalk.red(`找不到插件依赖 "${item}",请先在项目中安装,项目路径:${root}`));
process.exit(1);
}
}
const resolvedItem = {
id: fPath,
path: fPath,
type,
opts: args[item] || {},
apply() {
try {
return (0, helper_1.getModuleDefaultExport)(require(fPath));
}
catch (error) {
console.error(error);
// 全局的插件运行报错,不抛出 Error 影响主流程,而是通过 log 提醒然后把插件 filter 掉,保证主流程不变
if (skipError) {
console.error(`插件依赖 "${item}" 加载失败,请检查插件配置`);
}
else {
throw new Error(`插件依赖 "${item}" 加载失败,请检查插件配置`);
}
}
}
};
resolvedPresetsOrPlugins.push(resolvedItem);
}
return resolvedPresetsOrPlugins;
}
实际主要就是执行resolve.sync获取路径
const resolvedPresetsOrPlugins = [];
const resolvedItem = {
id: fPath,
path: fPath,
type,
opts: args[item] || {},
apply() {
// 插件内容 require()
try{
return getModuleDefaultExport(require(fPath));
} catch(e){
// 省略代码...
}
}
}
resolvedPresetsOrPlugins.push(resolvedItem);
组成类似的数组对象返回。
4.initPreset初始化预设插件集合
initPreset(preset, isGlobalConfigPreset) {
this.debugger('initPreset', preset);
const { id, path, opts, apply } = preset;
const pluginCtx = this.initPluginCtx({ id, path, ctx: this });
const { presets, plugins } = apply()(pluginCtx, opts) || {};
this.registerPlugin(preset);
if (Array.isArray(presets)) {
const _presets = (0, utils_1.resolvePresetsOrPlugins)(this.appPath, (0, utils_1.convertPluginsToObject)(presets)(), constants_1.PluginType.Preset, isGlobalConfigPreset);
while (_presets.length) {
this.initPreset(_presets.shift(), isGlobalConfigPreset);
}
}
if (Array.isArray(plugins)) {
isGlobalConfigPreset
? (this.globalExtraPlugins = (0, lodash_1.merge)(this.globalExtraPlugins, (0, utils_1.convertPluginsToObject)(plugins)()))
: (this.extraPlugins = (0, lodash_1.merge)(this.extraPlugins, (0, utils_1.convertPluginsToObject)(plugins)()));
}
}
initPreset这个方法做了以下几件事情:
- initPluginCtx 初始化插件
- 执行插件,获得预设插件集合和插件
- 使用registerPlugin注册插件,后续可以使用required调用
- 如果预设插件集合是数组,递归调用
- 如果插件集合是数组,判断是全局插件就合并到全局额外的插件
globalExtraPlugins中,否则就合并到额外的插件extraPlugins,后续统一处理插件。
5. initPluginCtx 初始化插件 ctx
initPluginCtx({ id, path, ctx }) {
const pluginCtx = new Plugin_1.default({ id, path, ctx });
const internalMethods = ['onReady', 'onStart'];
const kernelApis = [
'appPath',
'plugins',
'platforms',
'paths',
'helper',
'runOpts',
'initialConfig',
'applyPlugins',
'applyCliCommandPlugin'
];
internalMethods.forEach(name => {
if (!this.methods.has(name)) {
pluginCtx.registerMethod(name);
}
});
return new Proxy(pluginCtx, {
get: (target, name) => {
if (this.methods.has(name)) {
const method = this.methods.get(name);
if (Array.isArray(method)) {
return (...arg) => {
method.forEach(item => {
item.apply(this, arg);
});
};
}
return method;
}
if (kernelApis.includes(name)) {
return typeof this[name] === 'function' ? this[name].bind(this) : this[name];
}
return target[name];
}
});
}
initPluginCtx函数主要做了以下几件事情?
- new Plugin生成插件的pluginCtx
- internalMethods内部方法['onReady', 'onStart']注册到pluginCtx上
- 将this.methods的数组绑定到Kernel实例对象的this上
- kernelApis的方法,在代理绑定中,this指向Kernel ctx上
在Proxy代理之后,可以直接使用ctx[methodName]直接调用方法。
5.1 new Pugin({ id, path, ctx })
export default class Plugin {
id: string
path: string
ctx: Kernel
optsSchema: Func
constructor (opts) {
this.id = opts.id
this.path = opts.path
this.ctx = opts.ctx
}
// 具体方法实现再拆分分解
}
5.1.1 register 注册 hook
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))
}
判断hook.name和hook.fn,最后存入hooks map中。可以通过Kernel ctx.applyPlugins触发插件。
传送门:
5.1.2 registerCommand注册方法
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)
}
存入comands Map中,然后通过this.register(command)存入hooks中。便于在ctx.applyPlugins()使用。
5.1.3 registerPlatform 注册平台
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)
}
同样存入platforms中,同时存入hooks中。
5.1.4 registerMethod 注册方法
registerMethod (...args) {
const { name, fn } = processArgs(args)
const methods = this.ctx.methods.get(name) || []
methods.push(fn || function (fn: Func) {
this.register({
name,
fn
})
}.bind(this))
this.ctx.methods.set(name, methods)
}
上文有提及。如果没有函数,就存入hooks中。
// processArgs函数主要作用统一不同的传参形式,最终都是返回`name`和`fn`
function processArgs (args) {
let name, fn
if (!args.length) {
throw new Error('参数为空')
} else if (args.length === 1) {
if (typeof args[0] === 'string') {
name = args[0]
} else {
name = args[0].name
fn = args[0].fn
}
} else {
name = args[0]
fn = args[1]
}
return { name, fn }
}
5.1.5 addPluginOptsSchema 添加插件的参数 Schema
addPluginOptsSchema (schema) {
this.optsSchema = schema
}
传入插件的参数Schema。为插件加入参数验证。
6. registerPlugin注册插件
registerPlugin (plugin: IPlugin) {
this.debugger('registerPlugin', plugin)
if (this.plugins.has(plugin.id)) {
throw new Error(`插件 ${plugin.id} 已被注册`)
}
this.plugins.set(plugin.id, plugin)
}
registerPlugin方法主要做了如下事情:
- 注册插件到plugins Map中。
最终插件如下:
7.resolvePlugins解析插件
解析插件和解析预设插件类似
resolvePlugins (cliAndProjectPlugins: IPluginsObject, globalPlugins: IPluginsObject) {
cliAndProjectPlugins = merge(this.extraPlugins, cliAndProjectPlugins)
const resolvedCliAndProjectPlugins = resolvePresetsOrPlugins(this.appPath, cliAndProjectPlugins, PluginType.Plugin)
globalPlugins = merge(this.globalExtraPlugins, globalPlugins)
const globalConfigRootPath = path.join(helper.getUserHomeDir(), helper.TARO_GLOBAL_CONFIG_DIR)
const resolvedGlobalPlugins = resolvePresetsOrPlugins(globalConfigRootPath, globalPlugins, PluginType.Plugin, true)
const resolvedPlugins = resolvedCliAndProjectPlugins.concat(resolvedGlobalPlugins)
while (resolvedPlugins.length) {
this.initPlugin(resolvedPlugins.shift()!)
}
this.extraPlugins = {}
this.globalExtraPlugins = {}
}
resolvePlugins这个方法做了以下几件事情: 1. 合并预设插件集合中的插件,cli和项目的配置插件
- resolvedCliAndProjectPlugins解析cli和项目的配置插件
- 合并全局预设插件集合中的插件、全局配置的插件
- 最后遍历所有解析后的插件,依次调用
initPlugin方法初始化插件
8. initPlugin 初始化插件
initPlugin (plugin: IPlugin) {
const { id, path, opts, apply } = plugin
const pluginCtx = this.initPluginCtx({ id, path, ctx: this })
this.debugger('initPlugin', plugin)
this.registerPlugin(plugin)
apply()(pluginCtx, opts)
this.checkPluginOpts(pluginCtx, opts)
}
initPlugin方法做了以下几件事情:
- initPluginCtx初始化插件
- 注册插件
- 执行插件,传入pluginCtx对象,参数opts
- 校验插件的参数是否符合要求
9. checkPluginOpts 校验插件的参数
checkPluginOpts (pluginCtx, opts) {
if (typeof pluginCtx.optsSchema !== 'function') {
return
}
this.debugger('checkPluginOpts', pluginCtx)
const joi = require('joi')
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
}
}
checkPluginOpts这个方法做了一件事情:
- 使用joi最强大的JavaScript模式描述语言和数据验证器。校验插件参数schema。
整个项目中,debug调试之后,发现只有taro-plugin-mini-ci这个插件添加了参数校验。
// 参数验证,支持传入配置对象、返回配置对象的异步函数
ctx.addPluginOptsSchema((joi) => {
return joi.alternatives().try(
joi.function().required(),
joi
.object()
.keys({
/** 微信小程序上传配置 */
weapp: joi.object({
appid: joi.string().required(),
privateKeyPath: joi.string().required(),
type: joi.string().valid('miniProgram', 'miniProgramPlugin', 'miniGame', 'miniGamePlugin'),
ignores: joi.array().items(joi.string().required()),
robot: joi.number(),
setting: joi.object()
}),
/** 字节跳动小程序上传配置 */
tt: joi.object({
email: joi.string().required(),
password: joi.string().required()
}),
/** 阿里小程序上传配置 */
alipay: joi.alternatives().try(
joi.object({
appid: joi.string().required(),
toolId: joi.string().required(),
privateKeyPath: joi.string().required(),
clientType: joi.string().valid('alipay', 'ampe', 'amap', 'genie', 'alios', 'uc', 'quark', 'health', 'koubei', 'alipayiot', 'cainiao', 'alihealth'),
deleteVersion: joi.string().regex(/^\d+.\d+.\d+$/)
}),
joi.object({
appid: joi.string().required(),
toolId: joi.string().required(),
privateKey: joi.string().required(),
clientType: joi.string().valid('alipay', 'ampe', 'amap', 'genie', 'alios', 'uc', 'quark', 'health', 'koubei', 'alipayiot', 'cainiao', 'alihealth'),
deleteVersion: joi.string().regex(/^\d+.\d+.\d+$/)
}),
),
/** 钉钉小程序配置 */
dd: joi.object({
token: joi.string().required(),
appid: joi.string().required(),
devToolsInstallPath: joi.string(),
projectType: joi.string().valid(
'dingtalk-personal',
'dingtalk-biz-isv',
'dingtalk-biz',
'dingtalk-biz-custom',
'dingtalk-biz-worktab-plugin')
}),
/** 百度小程序上传配置 */
swan: joi.object({
token: joi.string().required(),
minSwanVersion: joi.string()
}),
jd: joi.object({
privateKey: joi.string().required(),
robot: joi.number(),
ignores: joi.array().items(joi.string()),
}),
version: joi.string(),
desc: joi.string(),
projectPath: joi.string()
})
.required()
)
})
10. applyCliCommandPlugin暴露taro cli 内部命令插件
applyCliCommandPlugin (commandNames: string[] = []) {
const existsCliCommand: string[] = []
for (let i = 0; i < commandNames.length; i++) {
const commandName = commandNames[i]
const commandFilePath = path.resolve(this.cliCommandsPath, `${commandName}.js`)
if (this.cliCommands.includes(commandName)) existsCliCommand.push(commandFilePath)
}
const commandPlugins = convertPluginsToObject(existsCliCommand || [])()
helper.createSwcRegister({ only: [...Object.keys(commandPlugins)] })
const resolvedCommandPlugins = resolvePresetsOrPlugins(this.appPath, commandPlugins, PluginType.Plugin)
while (resolvedCommandPlugins.length) {
this.initPlugin(resolvedCommandPlugins.shift()!)
}
}
applyCliCommandPlugin这个方法允许开发者为Taro cli添加自定义命令。
例如官方示例:
总结
- 初始化预设插件和插件集合
- 使用工具函数
mergePlugin合并预设插件集合和插件 registerPlugin方法注册插件resolvePlugins方法解析initPlugin初始化插件并执行插件
以command/init.js为例。
使用调试器终端,输入$node ./packages/taro-cli/bin/taro init taro-init-debug命令调试。
import type { IPluginContext } from '@tarojs/service'
export default (ctx: IPluginContext) => {
ctx.registerCommand({
name: 'init',
optionsMap: {
'--name [name]': '项目名称',
'--description [description]': '项目介绍',
'--typescript': '使用TypeScript',
'--npm [npm]': '包管理工具',
'--template-source [templateSource]': '项目模板源',
'--clone [clone]': '拉取远程模板时使用git clone',
'--template [template]': '项目模板',
'--css [css]': 'CSS预处理器(sass/less/stylus/none)',
'-h, --help': 'output usage information'
},
async fn (opts) {
// init project
const { appPath } = ctx.paths
const { options } = opts
const { projectName, templateSource, clone, template, description, typescript, css, npm, framework, compiler, hideDefaultTemplate, sourceRoot } = options
const Project = require('../../create/project').default
const project = new Project({...options})
project.create()
}
})
}
非测试环境,createSwcRegister 使用了 @swc/register 来编译 ts 等转换成 commonjs。可以直接用 require 读取文件。
工具函数resolvePresetsOrPlugins中可以使用require。
export function resolvePresetsOrPlugins (root: string, args: IPluginsObject, type: PluginType, skipError?: boolean): IPlugin[] {
const resolvedPresetsOrPlugins: IPlugin[] = []
// 省略...
const resolvedItem = {
id: fPath,
path: fPath,
type,
opts: args[item] || {},
apply () {
// 插件内容 required
try {
return getModuleDefaultExport(require(fPath))
} catch (error) {
}
}
}
resolvedPresetsOrPlugins.push(resolvedItem)
return resolvedPresetsOrPlugins
}
plugins初始化。
initPlugin (plugin: IPlugin) {
const { id, path, opts, apply } = plugin
const pluginCtx = this.initPluginCtx({ id, path, ctx: this })
this.debugger('initPlugin', plugin)
this.registerPlugin(plugin)
apply()(pluginCtx, opts)
this.checkPluginOpts(pluginCtx, opts)
}
再apply执行插件,传入pluginCtx(包含register、registerCommand、registerMethods等内置方法的对象),和参数opts。
这三个方法最终都会调用Plugin实例对象上的register方法,把方法存入到Kernel实例对象中的hooks属性中。
register (hook: IHook) {
const hooks = this.ctx.hooks.get(hook.name) || []
hook.plugin = this.id
this.ctx.hooks.set(hook.name, hooks.concat(hook))
}
再通过kernel实例对象的run函数里的applyPlugins方法。
再从Kernel的实例对象中,取出相应的hooks,使用tapable的AsyncSeriesWaterfallHook钩子串联起来,依次执行hook.fn方法。
已完结
此文章为2024年08月Day1源码共读,生活在阴沟里,也要记得仰望星空。