React18+ & Vite 5+ import 自动导入配置
vite.config.ts
import { defineConfig } from 'vite';
import path from 'path';
import react from '@vitejs/plugin-react';
import AutoImport from 'unplugin-auto-import/vite';
// 页面自动导入,官方有教
import Pages from 'vite-plugin-pages';
// svg 图表自动导入,官方有教
import svgr from 'vite-plugin-svgr';
// Ant Design 自动导入,我这里整理了
import { AntDesignResolver } from './src/utils/antd';
// 组件自动导入,我这里整理了
import importComponent from './src/utils/importComponent';
export default defineConfig({
plugins: [
react(),
svgr(),
Pages({
dirs: './src/pages',
}),
AutoImport({
dts: true,
defaultExportByFilename: false,
include: [
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
],
eslintrc: {
enabled: true,
filepath: './.eslintrc-auto-import.json', // 在google 找答案
globalsPropValue: true,
},
// 驼峰式的名字,比如 NewArticleName
directoryAsNamespace: true,
// 自定义导入
imports: [
...importComponent(),
'react',
'react-router-dom',
// 自定义导入
{
'@/utils/general': ['general'],
'react-redux': ['Provider', 'useDispatch', 'useSelector'],
'@reduxjs/toolkit': [
'configureStore',
'createSlice',
],
lodash: ['_'],
},
],
dirs: [
'./src/composables/**',
'./src/store/**',
],
resolvers: [
AntDesignResolver({
resolveIcons: true,
}),
],
injectAtEnd: true,
}),
],
resolve: {
alias: {
'@': path.resolve('./src'),
},
},
});
import { AntDesignResolver } from ‘./src/utils/antd’;
export function kebabCase(key: string) {
const result = key.replace(/([A-Z])/g, ' $1').trim();
return result.split(' ').join('-').toLowerCase();
}
export type Awaitable<T> = T | PromiseLike<T>;
export interface ImportInfo {
as?: string;
name?: string;
from: string;
}
export type SideEffectsInfo =
| (ImportInfo | string)[]
| ImportInfo
| string
| undefined;
export interface ComponentInfo extends ImportInfo {
sideEffects?: SideEffectsInfo;
}
export type ComponentResolveResult = Awaitable<
string | ComponentInfo | null | undefined | void
>;
export type ComponentResolverFunction = (
name: string,
) => ComponentResolveResult;
export interface ComponentResolverObject {
type: 'component' | 'directive';
resolve: ComponentResolverFunction;
}
export type ComponentResolver =
| ComponentResolverFunction
| ComponentResolverObject;
interface IMatcher {
pattern: RegExp;
styleDir: string;
}
const matchComponents: IMatcher[] = [
{
pattern: /^Avatar/,
styleDir: 'avatar',
},
{
pattern: /^AutoComplete/,
styleDir: 'auto-complete',
},
{
pattern: /^Anchor/,
styleDir: 'anchor',
},
{
pattern: /^Badge/,
styleDir: 'badge',
},
{
pattern: /^Breadcrumb/,
styleDir: 'breadcrumb',
},
{
pattern: /^Button/,
styleDir: 'button',
},
{
pattern: /^Checkbox/,
styleDir: 'checkbox',
},
{
pattern: /^Card/,
styleDir: 'card',
},
{
pattern: /^Collapse/,
styleDir: 'collapse',
},
{
pattern: /^Descriptions/,
styleDir: 'descriptions',
},
{
pattern: /^RangePicker|^WeekPicker|^MonthPicker/,
styleDir: 'date-picker',
},
{
pattern: /^Dropdown/,
styleDir: 'dropdown',
},
{
pattern: /^Form/,
styleDir: 'form',
},
{
pattern: /^InputNumber/,
styleDir: 'input-number',
},
{
pattern: /^Input|^Textarea/,
styleDir: 'input',
},
{
pattern: /^Statistic/,
styleDir: 'statistic',
},
{
pattern: /^CheckableTag/,
styleDir: 'tag',
},
{
pattern: /^TimeRangePicker/,
styleDir: 'time-picker',
},
{
pattern: /^Layout/,
styleDir: 'layout',
},
{
pattern: /^Menu|^SubMenu/,
styleDir: 'menu',
},
{
pattern: /^Table/,
styleDir: 'table',
},
{
pattern: /^TimePicker|^TimeRangePicker/,
styleDir: 'time-picker',
},
{
pattern: /^Radio/,
styleDir: 'radio',
},
{
pattern: /^Image/,
styleDir: 'image',
},
{
pattern: /^List/,
styleDir: 'list',
},
{
pattern: /^Tab/,
styleDir: 'tabs',
},
{
pattern: /^Mentions/,
styleDir: 'mentions',
},
{
pattern: /^Step/,
styleDir: 'steps',
},
{
pattern: /^Skeleton/,
styleDir: 'skeleton',
},
{
pattern: /^Select/,
styleDir: 'select',
},
{
pattern: /^TreeSelect/,
styleDir: 'tree-select',
},
{
pattern: /^Tree|^DirectoryTree/,
styleDir: 'tree',
},
{
pattern: /^Typography/,
styleDir: 'typography',
},
{
pattern: /^Timeline/,
styleDir: 'timeline',
},
{
pattern: /^Upload/,
styleDir: 'upload',
},
];
export interface AntDesignResolverOptions {
/**
* exclude components that do not require automatic import
*
* @default []
*/
exclude?: string[];
/**
* import style along with components
*
* @default 'css'
*/
importStyle?: boolean | 'css' | 'less';
/**
* resolve `antd' icons
*
* requires package `@ant-design/icons-vue`
*
* @default false
*/
resolveIcons?: boolean;
/**
* @deprecated use `importStyle: 'css'` instead
*/
importCss?: boolean;
/**
* @deprecated use `importStyle: 'less'` instead
*/
importLess?: boolean;
/**
* use commonjs build default false
*/
cjs?: boolean;
/**
* rename package
*
* @default 'antd'
*/
packageName?: string;
}
function getStyleDir(compName: string): string {
let styleDir;
const total = matchComponents.length;
for (let i = 0; i < total; i++) {
const matcher = matchComponents[i];
if (compName.match(matcher.pattern)) {
styleDir = matcher.styleDir;
break;
}
}
if (!styleDir) styleDir = kebabCase(compName);
return styleDir;
}
function getSideEffects(
compName: string,
options: AntDesignResolverOptions,
): SideEffectsInfo {
const { importStyle = true, importLess = false } = options;
if (!importStyle) return;
const lib = options.cjs ? 'lib' : 'es';
const packageName = options?.packageName || 'antd';
if (importStyle === 'less' || importLess) {
const styleDir = getStyleDir(compName);
return `${packageName}/${lib}/${styleDir}/style`;
} else {
const styleDir = getStyleDir(compName);
return `${packageName}/${lib}/${styleDir}/style`;
}
}
const primitiveNames = [
'Affix',
'Anchor',
'AnchorLink',
'AutoComplete',
'AutoCompleteOptGroup',
'AutoCompleteOption',
'Alert',
'Avatar',
'AvatarGroup',
'BackTop',
'Badge',
'BadgeRibbon',
'Breadcrumb',
'BreadcrumbItem',
'BreadcrumbSeparator',
'Button',
'ButtonGroup',
'Calendar',
'Card',
'CardGrid',
'CardMeta',
'Collapse',
'CollapsePanel',
'Carousel',
'Cascader',
'Checkbox',
'CheckboxGroup',
'Col',
'Comment',
'ConfigProvider',
'DatePicker',
'MonthPicker',
'WeekPicker',
'RangePicker',
'QuarterPicker',
'Descriptions',
'DescriptionsItem',
'Divider',
'Dropdown',
'DropdownButton',
'Drawer',
'Empty',
'Form',
'FormItem',
'FormItemRest',
'Grid',
'Input',
'InputGroup',
'InputPassword',
'InputSearch',
'Textarea',
'Image',
'ImagePreviewGroup',
'InputNumber',
'Layout',
'LayoutHeader',
'LayoutSider',
'LayoutFooter',
'LayoutContent',
'List',
'ListItem',
'ListItemMeta',
'Menu',
'MenuDivider',
'MenuItem',
'MenuItemGroup',
'SubMenu',
'Mentions',
'MentionsOption',
'Modal',
'Statistic',
'StatisticCountdown',
'PageHeader',
'Pagination',
'Popconfirm',
'Popover',
'Progress',
'Radio',
'RadioButton',
'RadioGroup',
'Rate',
'Result',
'Row',
'Select',
'SelectOptGroup',
'SelectOption',
'Skeleton',
'SkeletonButton',
'SkeletonAvatar',
'SkeletonInput',
'SkeletonImage',
'Slider',
'Space',
'Spin',
'Steps',
'Step',
'Switch',
'Table',
'TableColumn',
'TableColumnGroup',
'TableSummary',
'TableSummaryRow',
'TableSummaryCell',
'Transfer',
'Tree',
'TreeNode',
'DirectoryTree',
'TreeSelect',
'TreeSelectNode',
'Tabs',
'TabPane',
'Tag',
'CheckableTag',
'TimePicker',
'TimeRangePicker',
'Timeline',
'TimelineItem',
'Tooltip',
'Typography',
'TypographyLink',
'TypographyParagraph',
'TypographyText',
'TypographyTitle',
'Upload',
'UploadDragger',
'LocaleProvider',
];
// prefix name here
const prefix = '';
let antdNames: Set<string>;
function genAntdNames(primitiveNames: string[]): void {
antdNames = new Set(primitiveNames.map((name) => `${prefix}${name}`));
}
genAntdNames(primitiveNames);
function isAntd(compName: string): boolean {
return antdNames.has(compName);
}
export function AntDesignResolver(
options: AntDesignResolverOptions = {},
): ComponentResolver {
return {
type: 'component',
resolve: (name: string) => {
if (
options.resolveIcons &&
name.match(/(Outlined|Filled|TwoTone)$/)
) {
return {
name,
from: '@ant-design/icons',
};
}
if (isAntd(name) && !options?.exclude?.includes(name)) {
const importName = name.slice(prefix.length);
const { cjs = false, packageName = 'antd' } = options;
const path = `${packageName}/${cjs ? 'lib' : 'es'}`;
return {
name: importName,
from: path,
sideEffects: getSideEffects(importName, options),
};
}
},
};
}
import importComponent from ‘./src/utils/importComponent’;
import path from 'path';
import fg from 'fast-glob';
import { minimatch } from 'minimatch';
function pascalCaseWithCapitals(str) {
return str
.split('/')
.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
}
function removeExtension(str: string) {
return path.basename(str, path.extname(str));
}
function importComponent() {
const directories = [
{
pattern: './src/components/**/*.{tsx,jsx}',
omit: './src/components',
},
{
pattern: './src/layouts/**/*.{tsx,jsx}',
omit: './src/',
},
];
const entries = fg.sync(
directories.map((x) => x.pattern),
{
dot: true,
objectMode: true,
},
);
const components = entries.map((entry) => {
const dirOptions = directories.find((dir) =>
minimatch(entry.path, dir.pattern),
);
const componentName = entry.path
.replace(new RegExp(dirOptions.omit, 'gi'), '')
.split('/')
.filter(Boolean)
.map(pascalCaseWithCapitals)
.join('');
const fromPath = entry.path.replace(/\.\/src/gi, '@');
return {
[fromPath]: [
[
['default'],
removeExtension(componentName).replace('Index', ''),
],
],
};
});
return components;
}
export default importComponent;
组件demo 示范
// 路径:src/components/alert/msg.tsx, 使用的时候,直接调用 <AlertMsg /> 就可以了,无需使用 import 导入
const alert = (props) => {
let alertMsg = null;
if (props.errorMessage) {
alertMsg = (
<Alert
message={props.errorMessage}
type="error"
showIcon
{...props}
/>
);
} else if (props.sucMessage) {
alertMsg = (
<Alert
message={props.sucMessage}
type="success"
showIcon
{...props}
/>
);
}
return <>{alertMsg}</>;
};
// 为了防止频繁触发组件,我建议你每次放入 memo, 不需要才拿掉
export default memo(alert);
本作品采用《CC 协议》,转载必须注明作者和本文链接