analytics plugin 源码分析
看了下analytics的写法,发现这个plugin的写法比较简单,先从这个开始了解
api.describe
在插件注册阶段( initPresets or initPlugins stage )执行,用于描述插件或者插件集的 key、配置信息和启用方式等。
api.describe({ key?:string, config?: { default , schema, onChange }, enableBy? })
key
是配置中该插件配置的键名config.default
是插件配置的默认值,当用户没有在配置中配置 key 时,默认配置将生效。config.schema
用于声明配置的类型,基于 joi 。 如果你希望用户进行配置,这个是必须的 ,否则用户的配置无效config.onChange
是 dev 模式下,配置被修改后的处理机制。默认值为api.ConfigChangeType.reload
,表示在 dev 模式下,配置项被修改时会重启 dev 进程。 你也可以修改为api.ConfigChangeType.regenerateTmpFiles
, 表示只重新生成临时文件。你还可以传入一个方法,来自定义处理机制。enableBy
是插件的启用方式,默认是api.EnableBy.register
,表示注册启用,即插件只要被注册就会被启用。可以更改为api.EnableBy.config
,表示配置启用,只有配置插件的配置项才启用插件。你还可以自定义一个返回布尔值的方法( true 为启用 )来决定其启用时机,这通常用来实现动态生效。
const enableBy = (opts: any) => {
// 有analytics 或者 GA_KEY返回true
return opts.config.analytics || GA_KEY;
};
api.describe({
key: 'analytics',
config: {
// 声明入参的类型
schema({ zod }) {
return zod
.object({
baidu: zod.string(),
ga: zod.string(),
ga_v2: zod.string(),
})
.partial();
},
// 表示在 dev 模式下,配置项被修改时会重启 dev 进程
// 或者修改为 `api.ConfigChangeType.regenerateTmpFiles`, 表示只重新生成临时文件
onChange: api.ConfigChangeType.reload,
},
// 默认值是api.EnableBy.register,为true的话表示注册就启用
// 也可是设置为api.EnableBy.config,表示配置插件的配置项才启用插件
enableBy,
});
api.addHTMLHeadScripts
往 HTML 的
<head>
元素里添加 Script。传入的 fn 不需要参数,且需要返回一个 string(想要加入的代码) 或者{ async?: boolean, charset?: string, crossOrigin?: string | null, defer?: boolean, src?: string, type?: string, content?: string }
或者它们的数组
// 根据配置,往head注入不同的script内容,非常好理解
api.addHTMLHeadScripts(() => {
const analytics = (api.config.analytics || {}) as IAnalyticsConfig;
const { baidu, ga = GA_KEY, ga_v2 = GA_V2_KEY } = analytics;
const scripts: IScripts = [];
if (baidu) {
scripts.push({
content: 'var _hmt = _hmt || [];',
});
}
if (api.env !== 'development') {
if (baidu) {
scripts.push({
content: baiduTpl(baidu),
});
}
if (ga) {
scripts.push({
content: gaTpl(ga),
});
}
if (ga_v2) {
scripts.push(
{
async: true,
src: `//www.googletagmanager.com/gtag/js?id=${ga_v2}`,
},
{
content: gaV2Tpl(ga_v2),
},
);
}
}
return scripts.filter(Boolean);
});
使用
// .umirc.ts
import { defineConfig } from 'umi';
export default defineConfig({
// 这里是本地的地址,真实使用应该是'@umijs/plugins/dist/analytics',记得安装pnpm add -D @umijs/plugins, 用@umi/max就不用了,因为已经内置
plugins: ['../../packages/create-plugin/dist/cjs/analytics'],
analytics: {
baidu: 'test444',
},
});
antd plugin 源码分析
以上的demo比较简单,看一个较为复杂的antd
api.describe
看到这样的类型定义,你可能会有些懵圈, 其实是用了zod, 比如
- 简单demo
const mySchema = z.string();
// 解析
mySchema.parse("tuna"); // => "tuna"
const mySchema1 = z.boolean();
mySchema.parse(true); // => true
- 原始值
// 原始值
z.string();
z.number();
z.bigint();
z.boolean();
z.date();
// 空类型
z.undefined();
z.null();
z.void(); // 接受null或undefined
// 全能类型
// 允许 any value
z.any();
z.unknown();
// never 类型
// 允许没有 values
z.never();
- object
import { z } from "zod";
const User = z.object({
username: z.string(),
});
User.parse({ username: "Ludwig" });
// 提取出推断的类型
type User = z.infer<typeof User>;
// { username: string }
- union
用于合成 "OR" 类型
const stringOrNumber = z.union([z.string(), z.number()]);
stringOrNumber.parse("foo"); // 通过
stringOrNumber.parse(14); // 通过
还可以使用.or
,但是如果多了的话,肯定是union
好些
const stringOrNumber = z.string().or(z.number());
- refine
// 自定义校验逻辑
.refine(validator: (data:T)=>any, params?: RefineParams)
const myString = z.string().refine((val) => val.length <= 255, {
message: "String can't be more than 255 characters",
});
- deepPartial
让对象的属性可选(深层次的)
const user = z.object({
username: z.string(),
location: z.object({
latitude: z.number(),
longitude: z.number(),
}),
});
const deepPartialUser = user.deepPartial();
// {
// username?: string | undefined,
// location?: {
// latitude?: number | undefined;
// longitude?: number | undefined;
// } | undefined
// }
- record
和Record 是一个意思,ts中Record<string,string>
,这里省略了key的定义
const NumberCache = z.record(z.number());
type NumberCache = z.infer<typeof NumberCache>;
// => { [k: string]: number }
api.describe({
config: {
schema({ zod }) {
const commonSchema: Parameters<typeof zod.object>[0] = {
// 这里比较简单,省略,可以去https://github.com/umijs/umi/blob/master/packages/plugins/src/antd.ts 看看完整的
};
const createZodRecordWithSpecifiedPartial = (
partial: Parameters<typeof zod.object>[0],
) => {
const keys = Object.keys(partial);
return zod.union([
zod.object(partial),
zod.record(zod.any()).refine((obj) => {
return !keys.some((key) => key in obj);
}),
]);
};
const createV5Schema = () => {
// Reason: https://github.com/umijs/umi/pull/11924
// Refer: https://github.com/ant-design/ant-design/blob/master/components/theme/interface/components.ts
const componentNameSchema = zod.string().refine(
(value) => {
const firstLetter = value.slice(0, 1);
return firstLetter === firstLetter.toUpperCase(); // first letter is uppercase
},
{
message:
'theme.components.[componentName] needs to be in PascalCase, e.g. theme.components.Button',
},
);
// themeSchema
// inherit,
// algorithm,
// token,
// compoents: {
// Layout: {
// itemBg: 'red',
// ....
// },
// // other Component
// }
// https://ant.design/docs/react/customize-theme#theme
const themeSchema = createZodRecordWithSpecifiedPartial({
components: zod.record(componentNameSchema, zod.record(zod.any())),
});
// configProvider,
// prefixCls,
// ...
// ...themeSchema
// https://ant.design/components/config-provider
const configProvider = createZodRecordWithSpecifiedPartial({
theme: themeSchema,
});
return zod
.object({
...commonSchema,
theme: themeSchema.describe('Shortcut of `configProvider.theme`'),
appConfig: zod
.record(zod.any())
.describe('Only >= antd@5.1.0 is supported'),
momentPicker: zod
.boolean()
.describe('DatePicker & Calendar use moment version'),
styleProvider: zod.record(zod.any()),
configProvider,
})
.deepPartial();
};
const createV4Schema = () => {
return zod
.object({
...commonSchema,
configProvider: zod.record(zod.any()),
})
.deepPartial();
};
if (isV5) {
return createV5Schema();
}
if (isV4) {
return createV4Schema();
}
return zod.object({});
},
},
enableBy({ userConfig }) {
// 由于本插件有 api.modifyConfig 的调用,以及 Umi 框架的限制
// 在其他插件中通过 api.modifyDefaultConfig 设置 antd 并不能让 api.modifyConfig 生效
// 所以这里通过环境变量来判断是否启用
return process.env.UMI_PLUGIN_ANTD_ENABLE || userConfig.antd;
},
});
如此看来声明这块就非常好理解了
api.modifyAppData
修改 app 元数据。传入的 fn 接收 appData 并且返回它。
api.modifyAppData((memo) => {
checkPkgPath();
const version = require(`${pkgPath}/package.json`).version;
memo.antd = {
pkgPath,
version,
};
return memo;
});
api.modifyConfig
修改配置,相较于用户的配置,这份是最终传给 Umi 使用的配置。传入的 fn 接收 config 作为第一个参数,并且返回它。另外 fn 可以接收 { paths }
作为第二个参数。paths
保存了 Umi 的各个路径。
针对用户传入的参数做处理
api.modifyConfig((memo) => {
checkPkgPath();
let antd = memo.antd || {};
// defaultConfig 的取值在 config 之后,所以改用环境变量传默认值
if (process.env.UMI_PLUGIN_ANTD_ENABLE) {
const { defaultConfig } = JSON.parse(process.env.UMI_PLUGIN_ANTD_ENABLE);
// 通过环境变量启用时,保持 memo.antd 与局部变量 antd 的引用关系,方便后续修改
memo.antd = antd = Object.assign(defaultConfig, antd);
}
// antd import
memo.alias.antd = pkgPath;
// antd 5 里面没有变量了,less 跑不起来。注入一份变量至少能跑起来
if (isV5) {
const theme = require('@ant-design/antd-theme-variable');
memo.theme = {
...theme,
...memo.theme,
};
if (memo.antd?.import) {
const errorMessage = `Can't set antd.import while using antd5 (${antdVersion})`;
api.logger.fatal(
'please change config antd.import to false, then start server again',
);
throw Error(errorMessage);
}
}
// 只有 antd@4 才需要将 compact 和 dark 传入 less 变量
if (isV4) {
if (antd.dark || antd.compact) {
const { getThemeVariables } = require('antd/dist/theme');
memo.theme = {
...getThemeVariables(antd),
...memo.theme,
};
}
memo.theme = {
'root-entry-name': 'default',
...memo.theme,
};
}
// allow use `antd.theme` as the shortcut of `antd.configProvider.theme`
if (antd.theme) {
assert(isV5, `antd.theme is only valid when antd is 5`);
antd.configProvider ??= {};
// priority: antd.theme > antd.configProvider.theme
// 果然这里做了合并
antd.configProvider.theme = deepmerge(
antd.configProvider.theme || {},
antd.theme,
);
// https://github.com/umijs/umi/issues/11156
assert(
!antd.configProvider.theme.algorithm,
`The 'algorithm' option only available for runtime config, please move it to the runtime plugin, see: https://umijs.org/docs/max/antd#运行时配置`,
);
}
if (antd.appConfig) {
if (!appComponentAvailable) {
delete antd.appConfig;
api.logger.warn(
`antd.appConfig is only available in version 5.1.0 and above, but you are using version ${antdVersion}`,
);
} else if (
!appConfigAvailable &&
Object.keys(antd.appConfig).length > 0
) {
api.logger.warn(
`versions [5.1.0 ~ 5.3.0) only allows antd.appConfig to be set to \`{}\``,
);
}
}
// 如果使用静态主题配置,需要搭配 ConfigProvider ,否则无效,我们自动开启它
if (antd.dark || antd.compact) {
antd.configProvider ??= {};
}
return memo;
});
api.chainWebpack
通过 webpack-chain 的方式修改 webpack 配置。传入一个fn,该 fn 不需要返回值。它将接收两个参数:
memo
对应 webpack-chain 的 configargs:{ webpack, env }
arg.webpack
是 webpack 实例,args.env
代表当前的运行环境。
// Webpack
const day2MomentAvailable = semver.gte(antdVersion, '5.0.0');
api.chainWebpack((memo) => {
if (api.config.antd.momentPicker) {
// 如果是5.0.0 的版本,则注入antd-moment-webpack-plugin插件
if (day2MomentAvailable) {
memo.plugin('antd-moment-webpack-plugin').use(AntdMomentWebpackPlugin);
} else {
api.logger.warn(
`MomentPicker is only available in version 5.0.0 and above, but you are using version ${antdVersion}`,
);
}
}
return memo;
});
api.addExtraBabelPlugins
添加额外的 Babel 插件。 传入的 fn 不需要参数,且需要返回一个 Babel 插件或插件数组。
// 添加 babel-plugin-import
// https://github.com/umijs/babel-plugin-import umi 社区自己写的插件,按需加载,有意思
api.addExtraBabelPlugins(() => {
const style = api.config.antd.style || 'less';
if (api.config.antd.import && !api.appData.vite) {
return [
[
require.resolve('babel-plugin-import'),
{
libraryName: 'antd',
libraryDirectory: 'es',
...(isV5 ? {} : { style: style === 'less' || 'css' }),
},
'antd',
],
];
}
return [];
});
api.onGenerateFiles
生成临时文件时,随着文件变化会频繁触发,有缓存。 传入的 fn 接收的参数接口如下:
args: { isFirstTime?: boolean; files?: { event: string; path: string; } | null; }
// antd config provider & app component
api.onGenerateFiles(() => {
// 省略代码
api.writeTmpFile({
path: 'types.d.ts',
context: {
withConfigProvider,
withAppConfig,
},
tplPath: winPath(join(ANTD_TEMPLATES_DIR, 'types.d.ts.tpl')),
});
api.writeTmpFile({
path: RUNTIME_TYPE_FILE_NAME,
content: `
import type { RuntimeAntdConfig } from './types.d';
export type IRuntimeConfig = {
antd?: RuntimeAntdConfig
};
`,
});
// 拥有 `ConfigProvider` 时,我们默认提供修改 antd 全局配置的便捷方法(仅限 antd 5)
// 只要antd 5 才会在写入index.tsx 和 context.tsx
if (antdConfigSetter) {
api.writeTmpFile({
path: 'index.tsx',
content: `import React from 'react';
import { AntdConfigContext, AntdConfigContextSetter } from './context';
export function useAntdConfig() {
return React.useContext(AntdConfigContext);
}
export function useAntdConfigSetter() {
return React.useContext(AntdConfigContextSetter);
}`,
});
api.writeTmpFile({
path: 'context.tsx',
content: `import React from 'react';
import type { ConfigProviderProps } from 'antd/es/config-provider';
export const AntdConfigContext = React.createContext<ConfigProviderProps>(null!);
export const AntdConfigContextSetter = React.createContext<React.Dispatch<React.SetStateAction<ConfigProviderProps>>>(
() => {
console.error(\`The 'useAntdConfigSetter()' method depends on the antd 'ConfigProvider', requires one of 'antd.configProvider' / 'antd.dark' / 'antd.compact' to be enabled.\`);
}
);
`,
});
}
});
runtime.ts.tpl 部分片段
const getAntdConfig = () => {
if(!cacheAntdConfig){
cacheAntdConfig = getPluginManager().applyPlugins({
key: 'antd',
type: ApplyPluginsType.modify,
initialValue: {
{{#configProvider}}
...{{{configProvider}}},
{{/configProvider}}
{{#appConfig}}
appConfig: {{{appConfig}}},
{{/appConfig}}
},
});
}
return cacheAntdConfig;
}
生成模板后
const getAntdConfig = () => {
if(!cacheAntdConfig){
cacheAntdConfig = getPluginManager().applyPlugins({
key: 'antd',
type: ApplyPluginsType.modify,
initialValue: {
...{"theme":{"token":{"colorPrimary":"#1DA57A"}}},
appConfig: {"message":{"maxCount":3}},
},
});
}
return cacheAntdConfig;
}
api.addRuntimePlugin
添加运行时插件,传入的 fn 不需要参数,返回 string ,表示插件的路径。
// path: /Users/username/Documents/com-project/umi/examples/with-antd-5/.umi/plugin-antd/runtime.tsx
api.addRuntimePlugin(() => {
if (
api.config.antd.styleProvider ||
api.config.antd.configProvider ||
(appComponentAvailable && api.config.antd.appConfig)
) {
return [withTmpPath({ api, path: 'runtime.tsx' })];
}
return [];
});
api.addEntryImportsAhead
在入口文件中添加 import 语句 (import 最前面)。传入的 fn 不需要参数,其需要返回一个 {source: string, specifier?: string}
或其数组。
api.addEntryImportsAhead(() => {
const style = api.config.antd.style || 'less';
const imports: Awaited<
ReturnType<Parameters<IApi['addEntryImportsAhead']>[0]['fn']>
> = [];
if (isV5) {
// import antd@5 reset style
imports.push({ source: 'antd/dist/reset.css' });
} else if (!api.config.antd.import || api.appData.vite) {
// import antd@4 style if antd.import is not configured
imports.push({
source: style === 'less' ? 'antd/dist/antd.less' : 'antd/dist/antd.css',
});
}
return imports;
});
在.umi/umi.ts
下面可以看到
这样用户在注入插件后, 用起来特别简单
使用
// umirc.ts
plugins: ['@umijs/plugins/dist/antd'],
antd: {
// valid for antd5.0 only
theme: {
token: {
colorPrimary: 'red',
},
},
styleProvider: {
hashPriority: 'high',
legacyTransformer: true,
},
// dark: true,
compact: true,
/**
* antd@5.1.0 ~ 5.2.3 仅支持 appConfig: {}, 来启用 <App /> 组件;
* antd@5.3.0 及以上才支持 appConfig: { // ... } 来添加更多 App 配置项;
*/
appConfig: {
message: {
maxCount: 3,
},
},
},
暂时没有想到,自己要写一个什么样的插件,不过通过这些源码的阅读, 对 plugin的各种api 有了比较深的印象