01-前端万用模板
1. 初始化
本前端初始模板使用 Ant Design Pro
# 初始化命令
npm i @ant-design/pro-cli -g
pro create yuzi-generator-web-frontend
- 注意:一定要选择 umi@4!
- 项目版本最好一致:6.0.0
- 使用
npm, yarn, pnpm, cnpm
安装依赖,建议版本不要过低,node 版本在 16 及以上
- 运行
npm dev
测试执行;运行npm start
可以开启 Mock 数据
1. 初始化可能遇到的问题
- 可能会因为缺少
.git
文件导致 husky 执行报错,忽略即可
2. 开发规范
- Prettier 格式化工具,用来统一代码格式
- 格式化快捷键建议勾选,Vue 技术栈同学建议手动加
.Vue
后缀
- 格式化快捷键建议勾选,Vue 技术栈同学建议手动加
- Eslint 保持代码风格,减少代码出错
- Vue 技术栈手动加上即可
3. 模板瘦身
一次移除后,重启一下项目,看看能否正常运行,控制变量法,如果不行,直接回滚,一次性删除太多,容易找不到源头
1. 移除模块
1. 移除husky
一个用来提交前检查代码的规范,保证代码的一致性,一般用于团体协作,个人没有必要
移除相关的命令
2. 移除mock
mock 是官方提供的模拟数据,有真实的后端接口要对接
3. 移除icons, manifest.json
图标和适配移动端所需要的 Json,直接删除即可
4. 移除cname
域名映射,官方提供的,和自己的域名无关
5. 移除国际化
一般项目只对本国用户去开放,而且访问人数也较少,没有必要去用国际化,会增加打包体积,而且页面加载速度也会变慢
- 前端本地执行:
yarn add eslint-config-prettier --dev
yarn add eslint-plugin-unicorn --dev
node_modules/@umijs/lint/dist/config/eslint/index.js
,注释掉 es2022 那行
- 执行
i18n-remove
命令
- 删除
locales
文件夹
6. 移除单元测试
Jest 相关的全部移除
7. 移除types
自己会用 OpenAPI 规划去生成接口和类型
8. 移除swagger
9. 移除openapi.json
有自己的后端接口地址,不需要官方提供
4. 基本类型
修改 typings.d.ts
declare module 'slash2';
declare module '*.css';
declare module '*.less';
declare module '*.scss';
declare module '*.sass';
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
declare module 'omit.js';
declare module 'numeral';
declare module '@antv/data-set';
declare module 'mockjs';
declare module 'react-fittext';
declare module 'bizcharts-plugin-slider';
declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;
/**
* 分页信息
*/
interface PageInfo<T> {
current: number;
size: number;
total: number;
records: T[];
}
/**
* 分页请求
*/
interface PageRequest {
current?: number;
pageSize?: number;
sortField?: string;
sortOrder?: 'ascend' | 'descend';
}
/**
* 删除请求
*/
interface DeleteRequest {
id: number;
}
/**
* 返回封装
*/
interface BaseResponse<T> {
code: number;
data: T;
message?: string;
}
/**
* 全局初始化状态
*/
interface InitialState {
currentUser?: API.LoginUserVO;
}
5. 请求
1. 请求代码生成
注意:由于 Swagger V3 版本文件上传请求有 BUG,还是推荐用 Swagger 2 版本
openAPI: [
{
requestLibPath: "import { request } from '@umijs/max'",
schemaPath: "http://localhost:8101/api/v2/api-docs",
projectName: 'yuzi-generator-web-backend',
},
],
生成结果:
2. 全局请求处理
- 修改
requestErrorConfig.ts
的名称为requestConfig.ts
- 修改昵称至
requestConfig
app.tsx
中引入
- 创建
index.ts
常量区别开发环境和部署环境
- 引入环境变量,根据环境变量区分不同环境请求地址
- 删除错误处理,官方给的过于复杂,这边直接删除
- 删除官方的拼接 Token,一般要使用 Token,可以直接在 Authorization 请求头中携带 Token
// 请求拦截器
requestInterceptors: [
(config: RequestOptions) => {
// 拦截请求配置,进行个性化处理。
return config;
},
],
- 修改封装好的响应拦截器
// 响应拦截器
responseInterceptors: [
(response) => {
// 请求地址
const requestPath: string = response.config.url ?? '';
// 响应
const { data } = response as unknown as ResponseStructure;
if (!data) {
throw new Error('服务异常');
}
// 错误码处理
const code: number = data.code;
// 未登录,且不为获取用户登录信息接口
if (
code === 40100 &&
!requestPath.includes('user/get/login') &&
!location.pathname.includes('/user/login')
) {
// 跳转至登录页
window.location.href = `/user/login?redirect=${window.location.href}`;
throw new Error('请先登录');
}
if (code !== 0) {
throw new Error(data.message ?? '服务器错误');
}
return response;
},
],
6. 临时登录
- 修改
app.tsx
中的getInitialState()
,获取用户初始化状态,利用 Mock 模拟数据,替换后将报错的 username、avatar 等全局替换即可
/**
* @see https://umijs.org/zh-CN/plugins/plugin-initial-state
* */
export async function getInitialState(): Promise<InitialState> {
const initialState: InitialState = {
currentUser: undefined,
};
// 如果不是登录页面,执行
const { location } = history;
if (!location.pathname.startsWith(loginPath)) {
// 获取当前登录用户
const res = await getLoginUser();
initialState.currentUser = res.data;
const mockUser: API.LoginUserVO = {
userAvatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
userName: 'yupi',
userRole: 'admin',
};
initialState.currentUser = mockUser;
}
return initialState;
}
- 先用
@ts-ignore
忽略 ts 类型提示错误- 移除无用配置。eg:setting drawers 可以不要
- 可视化得到配置,使用在线地址:分析页 - Ant Design Pro
- 注释请求后端的代码,访问主页面,不会再重定向到登录页
- 如果侧边栏没有展示,给路由加上 name 属性即可
- 查看登录效果
- 优化 layout 的配置
// ProLayout 支持的api https://procomponents.ant.design/components/layout
// @ts-ignore
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
return {
actionsRender: () => [<Question key="doc" />],
avatarProps: {
src: initialState?.currentUser?.userAvatar,
title: <AvatarName />,
render: (_, avatarChildren) => {
return <AvatarDropdown>{avatarChildren}</AvatarDropdown>;
},
},
waterMarkProps: {
content: initialState?.currentUser?.userName,
},
footerRender: () => <Footer />,
menuHeaderRender: undefined,
// 自定义 403 页面
// unAccessible: <div>unAccessible</div>,
// 增加一个 loading 的状态
...defaultSettings
};
};
7. 基础布局
1. 右上角信息
- 修改
app.tsx
的 layout
// ProLayout 支持的api https://procomponents.ant.design/components/layout
// @ts-ignore
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
return {
avatarProps: {
render:()=>{
return <AvatarDropdown />
}
},
waterMarkProps: {
content: initialState?.currentUser?.userName,
},
footerRender: () => <Footer />,
menuHeaderRender: undefined,
// 自定义 403 页面
// unAccessible: <div>unAccessible</div>,
// 增加一个 loading 的状态
...defaultSettings
};
};
- 修改
AvatarDropdown.tsx
import {userLogoutUsingPost} from '@/services/backend/userController';
import {LogoutOutlined, SettingOutlined, UserOutlined} from '@ant-design/icons';
import {history, useModel} from '@umijs/max';
import {Avatar, Button, Space} from 'antd';
import {stringify} from 'querystring';
import type {MenuInfo} from 'rc-menu/lib/interface';
import React, {useCallback} from 'react';
import {flushSync} from 'react-dom';
import {Link} from 'umi';
import HeaderDropdown from '../HeaderDropdown';
export type GlobalHeaderRightProps = {
menu?: boolean;
children?: React.ReactNode;
};
export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({menu}) => {
/**
* 退出登录,并且将当前的 url 保存
*/
const loginOut = async () => {
await userLogoutUsingPost();
const {search, pathname} = window.location;
const urlParams = new URL(window.location.href).searchParams;
/** 此方法会跳转到 redirect 参数所在的位置 */
const redirect = urlParams.get('redirect');
// Note: There may be security issues, please note
if (window.location.pathname !== '/user/login' && !redirect) {
history.replace({
pathname: '/user/login',
search: stringify({
redirect: pathname + search,
}),
});
}
};
const {initialState, setInitialState} = useModel('@@initialState');
const onMenuClick = useCallback(
(event: MenuInfo) => {
const {key} = event;
if (key === 'logout') {
flushSync(() => {
setInitialState((s) => ({...s, currentUser: undefined}));
});
loginOut();
return;
}
history.push(`/account/${key}`);
},
[setInitialState],
);
const {currentUser} = initialState || {};
// 未登录
if (!currentUser) {
return (
<Link to="/user/login">
<Button type="primary" shape="round">
登录
</Button>
</Link>
);
}
const menuItems = [
...(menu
? [
{
key: 'center',
icon: <UserOutlined/>,
label: '个人中心',
},
{
key: 'settings',
icon: <SettingOutlined/>,
label: '个人设置',
},
{
type: 'divider' as const,
},
]
: []),
{
key: 'logout',
icon: <LogoutOutlined/>,
label: '退出登录',
},
];
return (
<HeaderDropdown
menu={{
selectedKeys: [],
onClick: onMenuClick,
items: menuItems,
}}
>
<Space>
{currentUser?.userAvatar ? (<Avatar size="small" src={currentUser?.userAvatar} />):
(<Avatar size="small" icon={<UserOutlined/>}/>)}
<span className="anticon">{currentUser?.userName ?? '无名'}</span>
</Space>
</HeaderDropdown>
);
};
2. 底部信息
修改成自己的个人博客,GitHub等地址就可以了
import { GithubOutlined } from '@ant-design/icons';
import { DefaultFooter } from '@ant-design/pro-components';
import '@umijs/max';
import React from 'react';
const Footer: React.FC = () => {
const defaultMessage = '程序员谷牛';
const currentYear = new Date().getFullYear();
return (
<DefaultFooter
copyright={`${currentYear} ${defaultMessage}`}
links={[
{
key: 'codeNav',
title: '个人主页',
href: 'http://listao.cn',
blankTarget: true,
},
{
key: 'git',
title: (
<>
<GithubOutlined /> listao源码
</>
),
href: 'https://gitee.com/listao',
blankTarget: true,
},
]}
/>
);
};
export default Footer;
8. 权限管理
修改 access.ts
,给管理员添加:access:canAdmin
/**
* @see https://umijs.org/zh-CN/plugins/plugin-access
* */
export default function access(initialState: InitialState) {
const { currentUser } = initialState ?? {};
return {
canUser: currentUser,
canAdmin: currentUser?.userRole === 'admin',
};
}
9. 用户登录
- 移除手机号登录,验证码错误也可以一并删除
- 移除自动登录,将忘记密码改为新用户注册
- 移除其他登录方式和一些无用的模块,清除不需要导入的模块和包,按
ctrl + alt + o
- 调整新用户注册的位置,移除浮动,父标签加
textAlign: right
- 修改
logo.svg
,替换即可,将标题和副标题自己替换
- 移除错误提示,修改官方的用户名
username
和password
为 userAccount 和 userPassword
- 移除其他不必要的代码
- 修改登录按钮后触发的事件
const handleSubmit = async (values: API.UserLoginRequest) => {
try {
// 登录
const res = await userLogin({
...values,
});
if (res.code === 0) {
const defaultLoginSuccessMessage = '登录成功!';
message.success(defaultLoginSuccessMessage);
// 保存已登录用户信息
setInitialState({
...initialState,
currentUser:res.data
})
const urlParams = new URL(window.location.href).searchParams;
history.push(urlParams.get('redirect') || '/');
return;
}
// 如果失败去设置用户错误信息
} catch (error) {
const defaultLoginFailureMessage = '登录失败,请重试!';
console.log(error);
message.error(defaultLoginFailureMessage);
}
};
- 输入账号和密码,与后端数据库的一致即可,登录成功效果图
- 之前由于初始化时,获取的是模拟状态。刷新后,用户状态会失效。将
app.tsx
的代码进行修改
export async function getInitialState(): Promise<InitialState> {
const initialState: InitialState = {
currentUser: undefined,
};
// 如果不是登录页面,执行
const { location } = history;
if (!location.pathname.startsWith(loginPath)) {
// // 获取当前登录用户
try{
const res = await getLoginUser();
initialState.currentUser = res.data;
}catch (error:any){
}
// const mockUser: API.LoginUserVO = {
// userAvatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
// userName: 'xiaobaitiao',
// userRole: 'admin',
// };
// initialState.currentUser = currentUser;
}
return initialState;
}
- 修改官方的退出登录接口,修改为自己后端的退出登录方法
- 给登录按钮添加跳转链接
- 根据后端的接口返回相对应的错误信息,再次修改
index.tsx
的handleSubmit()
const handleSubmit = async (values: API.UserLoginRequest) => {
try {
// 登录
const res = await userLogin({
...values,
});
const defaultLoginSuccessMessage = '登录成功!';
message.success(defaultLoginSuccessMessage);
// 保存已登录用户信息
setInitialState({
...initialState,
currentUser: res.data,
});
const urlParams = new URL(window.location.href).searchParams;
history.push(urlParams.get('redirect') || '/');
return;
// 如果失败去设置用户错误信息
} catch (error: any) {
const defaultLoginFailureMessage = `登录失败,请重试!${error.message}`;
message.error(defaultLoginFailureMessage);
}
};
错误提示效果图:
- 修改 ts 类型
10. 用户管理
- 调整添加节点的代码
/**
* 添加节点
* @param fields
*/
const handleAdd = async (fields: API.RuleListItem) => {
const hide = message.loading('正在添加');
try {
await addRule({
...fields,
});
hide();
message.success('创建成功');
return true;
} catch (error:any) {
hide();
message.error('创建失败,'+error.message);
return false;
}
};
- 调整更新节点的方法
/**
* 更新节点
* @param fields
*/
const handleUpdate = async (fields: FormValueType) => {
const hide = message.loading('正在更新');
try {
await updateRule({
name: fields.name,
desc: fields.desc,
key: fields.key,
});
hide();
message.success('更新成功');
return true;
} catch (error:any) {
hide();
message.error('更新失败,'+error.message);
return false;
}
};
- 调整删除节点的代码
/**
* 删除节点
* @param selectedRows
*/
const handleRemove = async (selectedRows: API.RuleListItem[]) => {
const hide = message.loading('正在删除');
if (!selectedRows) return true;
try {
await removeRule({
key: selectedRows.map((row) => row.key),
});
hide();
message.success('删除成功');
return true;
} catch (error:any) {
hide();
message.error('删除失败,'+error.message);
return false;
}
};
- 移除获取详情和批量删除的代码
- 删除其余批量选择和显示详情的代码
- 修改路由配置,将用户管理移动到管理员的二级路由,将原有的
TableList
文件夹下的index.tsx
和components
组件一并移动到Admin/User
文件夹下
export default [
{
path: '/user',
layout: false,
routes: [{ name: '登录', path: '/user/login', component: './User/Login' }],
},
{ path: '/welcome', name: '欢迎', icon: 'smile', component: './Welcome' },
{
path: '/admin',
name: '管理页',
icon: 'crown',
access: 'canAdmin',
routes: [
{ path: '/admin', redirect: '/admin/user'},
{ name: '用户管理', icon: 'table', path: '/admin/user', component: './Admin/User' },
],
},
{ path: '/', redirect: '/welcome' },
{ path: '*', layout: false, component: './404' },
];
- 修改 ProTable 表格,请求自己的后台真实接口,修改 TS 类型
request={
async (params, sort, filter)=>{
const sortField = Object.keys(sort)?.[0];
const sortOrder = sort?.[sortField]?? undefined;
const {data,code} = await listUserByPage({
...params,
sortField,
sortOrder,
...filter
} as API.UserQueryRequest);
return {
success:code===0,
data:data?.records||[],
total:Number(data?.total)||0,
}
}
}
- 修改 columns 的配置,让前端的属性值和后端传递的对应即可,调整一下修改和删除的样式
const columns: ProColumns<API.RuleListItem>[] = [
{
title: 'id',
dataIndex: 'id',
valueType: 'text',
},
{
title: '账号',
dataIndex: 'userAccount',
valueType: 'text',
},
{
title: '用户名',
dataIndex: 'userName',
valueType: 'text',
},
{
title: '头像',
dataIndex: 'userAvatar',
valueType: 'image',
fieldProps: {
width: 64,
},
hideInSearch: true,
},
{
title: '简介',
dataIndex: 'userProfile',
valueType: 'textarea',
},
{
title: '权限',
dataIndex: 'userRole',
valueEnum: {
user: {
text: '用户',
},
admin: {
text: '管理员',
},
},
},
{
title: '创建时间',
sorter: true,
dataIndex: 'createTime',
valueType: 'dateTime',
hideInSearch: true,
},
{
title: '更新时间',
sorter: true,
dataIndex: 'updateTime',
valueType: 'dateTime',
hideInSearch: true,
},
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
render: (_, record) => (
<Space size={"middle"}>
<Typography.Link
key="config"
onClick={() => {
handleUpdateModalOpen(true);
setCurrentRow(record);
}}
>
修改
</Typography.Link>
<Typography.Link type="danger" key="subscribeAlert" href="@/pages/Admin/User/index">
删除
</Typography.Link>
</Space>
),
},
];
数据展示效果:
- 添加删除用户的功能
- 先给链接添加触发事件,传入对应的数据行,方法名称修改一下,handleRemove -> handleDelete
编写对应的删除代码,注意这边 handleDelete 从外面移到了里面,为了使用 actionRef 删除成功后自动页面加载
const handleDelete = async (row: API.User) => {
const hide = message.loading('正在删除');
if (!row) return true;
try {
await deleteUser({
id: row.id,
});
hide();
message.success('删除成功');
actionRef?.current?.reload();
return true;
} catch (error: any) {
hide();
message.error('删除失败,' + error.message);
return false;
}
};
- 新增添加用户的功能。首先创建一个
createModal.tsx
编写代码,把 index.tsx
的 handleAdd
移动到此组件即可
import { addUser } from '@/services/backend/userController';
import { ProColumns, ProTable } from '@ant-design/pro-components';
import { message, Modal } from 'antd';
import React from 'react';
interface Props {
modalVisible: boolean;
columns: ProColumns<API.User>[];
onSubmit: () => void;
onCancel: () => void;
}
/**
* 添加节点
* @param fields
*/
const handleAdd = async (fields: API.UserAddRequest) => {
const hide = message.loading('正在添加');
try {
await addUser({
...fields,
});
hide();
message.success('创建成功');
return true;
} catch (error) {
hide();
message.error('创建失败');
return false;
}
};
/**
* 创建数据弹窗
* @param props
* @constructor
*/
const CreateModal: React.FC<Props> = (props) => {
const { columns, modalVisible, onCancel, onSubmit } = props;
return (
<Modal title={'新建'} open={modalVisible} destroyOnClose footer={null} onCancel={onCancel}>
<ProTable<API.UserAddRequest>
columns={columns}
type="form"
onSubmit={async (value) => {
const success = await handleAdd(value);
if (success) {
onSubmit?.();
}
}}
/>
</Modal>
);
};
export default CreateModal;
编写是否显示的布尔值
// 是否显示新建窗口
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
添加 createModal 组件
<CreateModal
modalVisible={createModalVisible}
columns={columns}
onSubmit={() => {
setCreateModalVisible(false);
actionRef.current?.reload();
}}
onCancel={() => {
setCreateModalVisible(false);
}}
/>
修改点击按钮的事件,让弹窗显示即可
利用 hideInForm 去除不必要显示的属性,eg:id、更新时间、创建时间
新建用户效果图
用户简介由于后端的 UserAddRequest 没有 UserProfile 字段,因此不能新增,但用户可以自己修改,管理员不能去改用户简介
- 新增更新用户的功能,先将
index.tsx
的handleUpdate()
剪切到UpdateModal.tsx
然后做适当修改即可
import { updateUser } from '@/services/backend/userController';
import { ProColumns, ProTable } from '@ant-design/pro-components';
import { message, Modal } from 'antd';
import React from 'react';
interface Props {
oldData?: API.User;
modalVisible: boolean;
columns: ProColumns<API.User>[];
onSubmit: () => void;
onCancel: () => void;
}
/**
* 更新节点
*
* @param fields
*/
const handleUpdate = async (fields: API.UserUpdateRequest) => {
const hide = message.loading('更新中');
try {
await updateUser(fields);
hide();
message.success('更新成功');
return true;
} catch (error: any) {
hide();
message.error('更新失败,' + error.message);
return false;
}
};
/**
* 更新数据弹窗
* @param props
* @constructor
*/
const UpdateModal: React.FC<Props> = (props) => {
const { oldData, columns, modalVisible, onCancel, onSubmit } = props;
if (!oldData) {
return <></>;
}
return (
<Modal title={'更新'} open={modalVisible} destroyOnClose footer={null} onCancel={onCancel}>
<ProTable<API.UserUpdateRequest>
columns={columns}
form={{
initialValues: oldData,
}}
type="form"
onSubmit={async (values) => {
const success = await handleUpdate({
...values,
id: oldData.id,
});
if (success) {
onSubmit?.();
}
}}
/>
</Modal>
);
};
export default UpdateModal;
- 添加显示更新窗口的一个布尔值
// 是否显示更新窗口
const [updateModalVisible, setUpdateModalVisible] = useState<boolean>(false);
- 更新用户行 TS 类型
修改对应的 Click 事件
添加组件
<UpdateModal
modalVisible={updateModalVisible}
columns={columns}
oldData={currentRow}
onSubmit={() => {
setUpdateModalVisible(false);
setCurrentRow(undefined);
actionRef.current?.reload();
}}
onCancel={() => {
setUpdateModalVisible(false);
}}
/>
更新效果图:
11. 其他
- 修改 title 和 logo
- 修改 footer 的背景色为 none
- BUG 修复:如果当前用户为管理员,路径为
admin/user
,注销后再次登录,会发现无权限。将重定向取消后即可修复 BUG,这段代码在AvatarDropdown.tsx
中
99. 集成
1. ReactPlayer
npm install react-player
import ReactPlayer from 'react-player';
import VideoList from "@/components/video/VideoList";
const videos: string[] = [
'http://listao.cn:10000/i/0/2024/99/1073_1715009580.mp4',
'http://listao.cn:20000/download/video/private/04_JinFan.mp4',
];
<div style={{marginTop: 20, border: '1px solid #ccc'}}>
<ReactPlayer
url={ videos }
controls
light
/>
</div>
<VideoList/>
1. VideoList
import React, { useState } from 'react';
import ReactPlayer from 'react-player';
const VideoList = () => {
// 视频列表数据
const videos = [
'http://listao.cn:10000/i/0/2024/99/1073_1715009580.mp4',
'http://listao.cn:10000/i/0/2024/99/001_shi_marriage.mp4',
'http://listao.cn:10000/i/0/2024/99/003_dog.mp4',
];
// 当前播放视频的索引
const [currentIndex, setCurrentIndex] = useState(0);
// 播放选中的视频
const handlePlayVideo = (index: React.SetStateAction<number>) => {
setCurrentIndex(index);
};
return (
<div>
{videos.map((video, index) => (
// eslint-disable-next-line react/button-has-type
<button key={index} onClick={() => handlePlayVideo(index)}>
{`Play Video ${index + 1}`}
</button>
))}
<div>
<ReactPlayer
url={videos[currentIndex]}
width="100%" // 宽度设置为100%
height="500px" // 高度设置为500px
controls // 显示控件
/>
</div>
</div>
);
};
export default VideoList;
2. Build bug
fatal - Found conflicts in esbuild helpers: q (735.350caa53.async.js, reactPlayerFilePlayer.fa054de9.async.js), Y (735.350caa53.async.js, reactPlayerTwitch.2fb09500.async.js), k (735.350caa53.async.js, reactPlayerMixcloud.48e67d4d.async.js, reactPlayerSoundCloud.91e9166a.async.js), K (reactPlayerDailyMotion.cfc377d1.async.js, reactPlayerMixcloud.48e67d4d.async.js, reactPlayerPreview.5abe5c52.async.js, reactPlayerSoundCloud.91e9166a.async.js, reactPlayerVidyard.23c9140f.async.js), g (reactPlayerDailyMotion.cfc377d1.async.js, reactPlayerFacebook.6f913577.async.js, reactPlayerVidyard.23c9140f.async.js, reactPlayerWistia.4a1cb367.async.js, reactPlayerYouTube.b8f1d229.async.js), R (reactPlayerDailyMotion.cfc377d1.async.js, reactPlayerMux.f565c9c8.async.js, reactPlayerVimeo.f4d5eb6c.async.js, reactPlayerYouTube.b8f1d229.async.js), P (reactPlayerDailyMotion.cfc377d1.async.js, reactPlayerFacebook.6f913577.async.js, reactPlayerMixcloud.48e67d4d.async.js, reactPlayerPreview.5abe5c52.async.js, reactPlayerSoundCloud.91e9166a.async.js, reactPlayerVimeo.f4d5eb6c.async.js), v (reactPlayerDailyMotion.cfc377d1.async.js, reactPlayerFacebook.6f913577.async.js, reactPlayerFilePlayer.fa054de9.async.js, reactPlayerMixcloud.48e67d4d.async.js), _ (reactPlayerFilePlayer.fa054de9.async.js, reactPlayerSoundCloud.91e9166a.async.js, reactPlayerVimeo.f4d5eb6c.async.js), J (reactPlayerFilePlayer.fa054de9.async.js, reactPlayerYouTube.b8f1d229.async.js), A (reactPlayerFilePlayer.fa054de9.async.js, reactPlayerMixcloud.48e67d4d.async.js), N (reactPlayerMixcloud.48e67d4d.async.js, reactPlayerVidyard.23c9140f.async.js, reactPlayerWistia.4a1cb367.async.js), f (reactPlayerMixcloud.48e67d4d.async.js, reactPlayerTwitch.2fb09500.async.js, reactPlayerWistia.4a1cb367.async.js), m (reactPlayerMixcloud.48e67d4d.async.js, reactPlayerSoundCloud.91e9166a.async.js, reactPlayerTwitch.2fb09500.async.js, reactPlayerVidyard.23c9140f.async.js, reactPlayerVimeo.f4d5eb6c.async.js, reactPlayerWistia.4a1cb367.async.js), T (reactPlayerMux.f565c9c8.async.js, reactPlayerVidyard.23c9140f.async.js, reactPlayerVimeo.f4d5eb6c.async.js, reactPlayerWistia.4a1cb367.async.js), U (reactPlayerMux.f565c9c8.async.js, reactPlayerPreview.5abe5c52.async.js, reactPlayerSoundCloud.91e9166a.async.js, reactPlayerYouTube.b8f1d229.async.js), w (reactPlayerMux.f565c9c8.async.js, reactPlayerPreview.5abe5c52.async.js), L (reactPlayerPreview.5abe5c52.async.js, reactPlayerTwitch.2fb09500.async.js), V (reactPlayerSoundCloud.91e9166a.async.js, reactPlayerVimeo.f4d5eb6c.async.js), b (reactPlayerSoundCloud.91e9166a.async.js, reactPlayerVidyard.23c9140f.async.js), W (reactPlayerTwitch.2fb09500.async.js, reactPlayerWistia.4a1cb367.async.js, reactPlayerYouTube.b8f1d229.async.js)
info - please set esbuildMinifyIIFE: true in your config file
fatal - [esbuildHelperChecker] Found conflicts in esbuild helpers.
export default defineConfig({
/**
* react-player 组件打包必须配置
*/
esbuildMinifyIIFE: true,
}