Vue与React基础开发知识点总结,如组件编写方法、权限校验方式、监听变量改变等,极其适用于后端工程师。文章结尾有极度提高开发效率的彩蛋(二次封装基于antd的前端初始化模板)

[TOC]

Vue相关内容总结

1.Element Ui Plus - 基于Vue3的前端主键

2.AcroDesign - 有Vue和React版本,字节研发

最佳实践

1.AcroDesign最佳实践

image-20240716163735718
image-20240716163735718

Vue相关开发语法

组件的编写

场景:有一个Markdown编辑器在项目中需要使用到,没有必要在每个页面都使用相同的代码,可以把这个Markdown编辑器抽离出来作为单独的组件。

前提:我们称调用这个组件的界面为父页面,这个Markdown组件为子页面。

需要考虑到的问题:父页面出于业务的需要,我们是需要实时的获取子页面Markdown组件用户所编写的内容去做对应的逻辑处理。

这里所使用到的MarkDown编辑器为BtyeMD - 一款字节出品的MarkDown编辑器。但是需要结合heightlight.js。

对于子组件来说:

1
2
3
4
5
6
7
8
9
<template>
<Editor
:value="value"
:mode="mode"
style="height: 100%"
:plugins="plugins"
@change="handleChange"
/>
</template>

这里我们需要考虑到三个问题来提高组件的通用性:

  • 这个组件所展示的内容,是需要父组件传递过来的,也就是value
  • 这个组件的模式,比如说:可编辑只可观看这样的模式,也是需要父组件进行传递过来的,也就是上面代码中的mode
  • 最重要的一点,我们如何获取到用户输入到的内容,这个组件提供了一个@change属性来监听用户所输入到的内容,这个属性是一个函数,所以我们需要父组件传递过来一个函数,我们再通过子组件传递回去,也就是上诉代码中的mode

知道了需求过后,我们需要编写属于这个组件的属性:

1
2
3
4
5
6
7
8
/**
* 定义组件属性的类型
*/
interface Props {
value: string;
mode?: string;
handleChange: (value: string) => void;
}

并且,需要给这些类型进行初始化,方式父组件没有传递导致错误:

1
2
3
4
5
6
7
8
9
10
import { defineProps, withDefaults } from "vue";

// 设定默认值
const props = withDefaults(defineProps<Props>(), {
value: () => "",
mode: "split",
handleChange: (value: string) => {
console.log(value);
},
});

定义好这些内容之后,可以让父组件进行调用相关的内容,

1
2
3
4
<MdEditor
:value="form.answer"
:handleChange="onAnswerChange"
></MdEditor>

这里我们看见并没有传递mode属性,原因是子组件默认了modelsplit,所以我们可以选择不传递这个属性值。

在这个场景中,我们在form表达中使用了这个主键,这个form表单的属性包括:

1
2
3
4
const form = ref<QuestionAddRequest>({
answer: "",
....
});

onAnswerChange函数为实时改变form中的answer值:

1
2
3
const onAnswerChange = (v: string) => {
form.value.answer = v;
};

那么通过这样的方式我们就完成了一个Vue组件的编写。

权限校验

在前端开发中除了页面的编写还有如何使用用户的状态,这里的状态分为登录权限

在vue中我使用Vuex来保存用户的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export default {
namespaced: true,
state: () => ({
loginUser: { // 初始化状态
userName: "未登录",
},
}),
actions: {
async getLoginUser({ commit, state }, payload) {
// const products = await
const res = await UserControllerService.getLoginUserUsingGet();
if (res.code === 0) {
commit("updateUser", res.data); // 如果登录成功,那么保存后端传递过来的值
} else {
commit("updateUser", { // 如果登录失败,把用户的权限状态设定为未登录
...state.loginUser,
userRole: ACCESS_ENUM.NOT_LOGIN,
});
}
},
},
mutations: {
// 修改状态变量的
updateUser(state, payload) {
state.loginUser = payload;
},
},
} as StoreOptions<any>;

那么,在用户登录的时候就可以调用Vuex来存储对应的用户变量:

1
2
3
4
5
6
7
8
9
10
// 处理登录请求
const handleSubmit = async () => {
const res = await UserControllerService.userLoginUsingPost(loginForm);
if (res.code === 0) {
await store.dispatch("user/getLoginUser"); // 这一步,存储用户的登录状态
Message.success("登录成功");
} else {
Message.error("登录失败" + res.message);
}
};

那么,我们可以在main.ts中来引入自定义编写的权限校验文件,在路由中,我们可以定义每个页面可以允许哪些权限的人进行访问,路由的代码定义如下,我们在meta属性中行定义了一个access属性,我们后续会使用这个属性来进行权限的控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义路由信息
export const routes: Array<RouteRecordRaw> = [
{
path: "/view/question/:id",
name: "在线作题",
component: ViewQuestionsView,
props: true,
meta: {
hideInMenu: true,
access: AccessEnum.USER,
},
},
];

首先我们可以在我们的项目的src目录下创建一个access文件夹,我们创建三个文件

image-20240716173203976
image-20240716173203976

第一个文件accessEnum.ts,用来定义系统中存在哪些权限的用户,以本系统举例:

1
2
3
4
5
6
7
const ACCESS_ENUM = {
NOT_LOGIN: "notLogin",
USER: "user",
ADMIN: "admin",
};

export default ACCESS_ENUM;

第二个文件checkAccess.ts,定义了这个用户是否符合权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import ACCESS_ENUM from "@/access/accessEnum";

const checkAccess = (loginUser: any, needAccess = ACCESS_ENUM.NOT_LOGIN) => {
const loginUserAccess = loginUser?.userRole ?? ACCESS_ENUM.NOT_LOGIN;
// 获取当前用户具有的权限,没有loginUser表示未登录
if (needAccess === ACCESS_ENUM.NOT_LOGIN) {
return true;
}
if (needAccess === ACCESS_ENUM.USER) {
// todo 只需要判断登录就可以
console.log(">>>>", loginUserAccess);
if (loginUserAccess === ACCESS_ENUM.NOT_LOGIN) {
return false;
}
}
if (needAccess === ACCESS_ENUM.ADMIN) {
// 需要管理员权限但是不为管理员
if (loginUserAccess !== ACCESS_ENUM.ADMIN) {
return false;
}
}
return true;
};

export default checkAccess;

第三个文件index.ts,定义了校验逻辑,当路由需要开始进行转变的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import router from "@/router";
import store from "@/store";
import ACCESS_ENUM from "@/access/accessEnum";
import { routes } from "@/router/routes";
import checkAccess from "@/access/checkAccess";

router.beforeEach(async (to, from, next) => {
let loginUser = store.state.user.loginUser;
// 如果之前没有登录过,或者之前登录过可以进行自动登录
if (!loginUser.userRole) {
await store.dispatch("user/getLoginUser");
loginUser = store.state.user.loginUser;
}
// 如果这个页面不需要访问权限
const needAccess = (to?.meta?.access as string) ?? ACCESS_ENUM.NOT_LOGIN;
if (needAccess !== ACCESS_ENUM.NOT_LOGIN) {
if (
loginUser.userRole === ACCESS_ENUM.NOT_LOGIN ||
!loginUser ||
!loginUser.userRole
) {
next(`/user/login?redirect=${to.fullPath}`);
return;
}
if (!checkAccess(loginUser, needAccess)) {
next("/noAuth");
return;
}
}
next();
});

上述的代码可以直接进行复制,只需要loginUser中所定义的属性需要和我的相同,也就是userRole

上诉代码编写完之后,直接在main.ts中引入相关代码即可

1
import "@/access";

监听页面中的某个变量是否有改变

使用watch函数来进行监听,他所监听的变量是props.language,只要这个变量变了,他就会执行下面的操作。

1
2
3
4
5
6
7
8
9
10
watch(
() => props.language,
() => {
monaco.editor.setModelLanguage(
// 踩坑一定要使用toRaw
toRaw(codeEditor.value).getModel(),
props.language
);
}
);

监听好几个变量是否有改变所做的操作

只要loadData函数中所设计到的其中一个变量改变了,就会执行这个操作

1
2
3
watchEffect(() => {
loadData();
});

需要Dom元素加载完后才执行的逻辑

在前端开发中经常见道需要某个dom元素加载完之后才可以执行某些逻辑的情况,在这里我们可以利用onMounted的钩子函数。

1
import { defineProps, onMounted, ref, toRaw, watch, withDefaults } from "vue";

然后我们假设我们需要等下面这个div加载之后才执行之后的逻辑:

1
2
3
<template>
<div id="code-editor" ref="codeEditorRef" style="min-height: 700px"></div>
</template>

我们使用ref获取这个div的实例化对象

1
const codeEditorRef = ref();

然后再使用onMounted,进行监听

1
2
3
4
5
6
7
onMounted(() => {
if (!codeEditorRef.value) {
return;
}
// 执行之后的操作..

});

React相关内容总结

最佳实践

image-20240716180232677
image-20240716180232677
image-20240716180251028
image-20240716180251028

React相关开发语法

定义页面变量

比如说我们页面需要有三个变量basicInfomodelConfigfileConfig这种,在vue3中我们会使用ref语法,但是在react中不一样,会使用下面这种方式:

1
2
3
const [basicInfo, setBasicInfo] = useState<API.GeneratorEditRequest>();
const [modelConfig, setModelConfig] = useState<API.ModelConfig>();
const [fileConfig, setFileConfig] = useState<API.FileConfig>();

其中setxxxx为为某个变量赋值,比如下面这种方式:

1
2
3
4
onFinish={async (values) => {
setBasicInfo(values);
return true;
}}

定义组件

在react中定义组件也是十分重要的,这非常有利于简化代码的复杂程度。

比如以下这个界面: image-20240716181750218

我们要将表格中最右边修改创建按钮改为弹出框,并且作为组件的形式,减少主页面的代码复杂程度。比如创建,我们在新建文件,

image-20240716182005611
image-20240716182005611

然后在主页面中进行引入,这里定义了几个值,首先是createModalVisible,来控制这个弹出框是否可见,columns表单字段信息,onSubmit执行结束之后我们需要做什么事情,onCancel需要之后需要做什么事情:

1
2
3
4
5
6
7
8
9
10
11
<CreateModal
visible={createModalVisible}
columns={columns}
onSubmit={() => {
setCreateModalVisible(false);
actionRef.current?.reload();
}}
onCancel={() => {
setCreateModalVisible(false);
}}
/>

然后我们在CreateModel.tsx页面中定义对应的属性

1
2
3
4
5
6
interface Props {
visible: boolean;
columns: ProColumns<API.Generator>[];
onSubmit: (values: API.GeneratorAddRequest) => void;
onCancel: () => void;
}

定义好了之后,我们需要在主函数中对他进行使用

1
const { visible, columns, onSubmit, onCancel } = props;

整体效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 创建弹窗
* @param props
* @constructor
*/
const CreateModal: React.FC<Props> = (props) => {
const { visible, columns, onSubmit, onCancel } = props;

return (
<Modal
destroyOnClose
title={'创建'}
open={visible}
footer={null}
onCancel={() => {
onCancel?.(); // 这里使用onCancel?.(),含义为要是主页面传了这个函数才进行执行
}}
>
<ProTable
type="form"
columns={columns}
onSubmit={async (values: API.GeneratorAddRequest) => { // 这个是ProTable组件的onSubmit
const success = await handleAdd(values);
if (success) {
onSubmit?.(values); // 这个是调用我们父页面的onSubmit函数,是不一样的。
}
}}
/>
</Modal>
);
};
export default CreateModal;

监听变量是否有改变所做的操作

1
2
3
4
5
6
useEffect(() => {
if (!id) {
return;
}
loadData();
}, [id]);

检查id是否存在:效果函数开始时检查id是否存在(即id不是nullundefined或任何假值)。如果id不存在,函数直接返回并不执行loadData()。这可以防止在没有有效id时进行不必要的数据加载。

条件执行loadData():只有当id存在时,才调用loadData()。这通常用于根据id从服务器加载或请求数据。

依赖于id的执行:通过将id包含在依赖数组中,你告诉React,只有当id的值改变时才需要重新执行useEffect中的函数。这意味着:

  • 如果id在重新渲染间保持不变,useEffect不会再次执行。
  • 如果id改变,无论是从undefined变为具体值,还是从一个具体值变为另一个具体值,useEffect都会执行。

至此,作为后端工程师来说,上面的内容应该足够开发大部分系统了。

由于喜欢React的代码风格,我编写了一套基于antd pro框架的一套模板,[yxinmiralce-antd-frontend-init] - 在Github上初步发布,里面包含了基础的CURD,OpenAPI相关配置,少些各种造轮子的代码。

如果有兴趣使用可以往下接着看,下面是我这个模板的ReadMe

<img height="160" src="https://pec-1300659502.cos.ap-guangzhou.myqcloud.com/logo.svg" />
<h1>YxinMiracle前端模板</h1>
<h3>一个属于后端工程师的前端极简模板。</h3>
基于 React + Ant Design 的项目初始模板,整合了常用框架和主流业务的示例代码。

[!IMPORTANT]

作者:YxinMIiracle 仅分享于 GitHub 本项目基于AntDesignPro-Preview-React进行二次开发,无任何商业用途!!

[TOC]

✈ 版本要求 & 特性

[!NOTE]

最好严格根据版本要求进行使用

  • node >= 16
  • npm >= 7.20.6

📦 安装教程

  1. 将代码拉取到本地
1
git clone https://github.com/YxinMiracle/yxinmiralce-antd-frontend-init.git
  1. 安装相关依赖
1
npm install
  1. 运行
1
npm run start:dev

🛰 相关特性

1. 权限控制

通过config/routes.ts路由中access的属性,来判断该试图是否可被这个用户所访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default [
{ path: '/welcome', icon: 'smile', component: './Welcome', name: '欢迎页' },
{
path: '/admin',
icon: 'crown',
name: '管理页',
access: 'canAdmin',
routes: [
{ path: '/admin', redirect: '/admin/user' },
{ icon: 'table', path: '/admin/user', component: './Admin/User', name: '用户管理' },
],
},
...
];

2. 权限赋予

在项目中src/access.ts的文件中,设置页面力度的权限设置

1
2
3
4
5
6
7
export default function access(initialState: { currentUser?: API.LoginUserVO } | undefined) {
const { currentUser } = initialState ?? {};
return {
canUser: currentUser, // routes.ts路由中要是配置了canUser,那么就执行这个逻辑
canAdmin: currentUser && currentUser.userRole === 'admin',
};
}

全局初始化对象initialState中的currentUser会在登录中所赋予,在注销中被消除,登录赋予逻辑在src/pages/User/Login/index.tsx中所赋予

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const handleSubmit = async (values: API.UserLoginRequest) => {
try {
// 登录
const res = await userLoginUsingPost({
...values,
});

// 保存已登录用户信息
setInitialState({
...initialState,
currentUser: res.data,
});
const urlParams = new URL(window.location.href).searchParams;
history.push(urlParams.get('redirect') || '/');
return;
} catch (error: any) {

}
};

src/compoents/RightContent/AvatarDropdown.tsx的登出逻辑被消除

1
2
3
4
flushSync(() => {
setInitialState((s) => ({ ...s, currentUser: undefined }));
});
loginOut();

3. OpenAPI配置

config/config.ts中的OpenAPI配置更改自己的接口文档配置

1
2
3
4
5
6
7
openAPI: [
{
requestLibPath: "import { request } from '@umijs/max'",
schemaPath: 'http://localhost:8360/api/v2/api-docs',
projectName: 'backend',
},
],

4. 动态主题变化

官方Preview项目中并直接编写相关功能,作者根据Preview项目中的SettingDrawer逻辑进行二次开发

src/compoents/RightContent/AvatarDropdown.tsx中编写主题变化逻辑

构建开关

1
2
3
4
5
6
7
8
const switchTag = (
<Switch
checkedChildren="🌞"
unCheckedChildren="🌜"
onClick={switchDarkMode}
defaultChecked
></Switch>
);

设定开关逻辑

1
2
3
4
5
6
7
8
9
10
const switchDarkMode = async (checked: boolean) => {
const { settings, currentUser } = await getInitialState();
setInitialState(() => ({
settings: {
...settings,
navTheme: checked ? 'realDark' : 'light',
},
currentUser: currentUser,
}));
};

效果:

5. 全局配置悬浮

在文件app.tsx中开启,可动态调整页面布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
childrenRender: (children) => {
// if (initialState?.loading) return <PageLoading />;
return (
<>
{children}
{isDev && (
<SettingDrawer
disableUrlParams
enableDarkTheme
settings={initialState?.settings}
onSettingChange={(settings) => {
setInitialState((preInitialState) => ({
...preInitialState,
settings,
}));
}}
/>
)}
</>
);
},

🚄 更改项目名字

作为喜欢从零到一的专业开发者,项目名字绝对是要自己起的,但是怎么去改呢?

git下来项目之后,先不打开项目,根据下面步骤执行

  • 第一步:先把yxinmiralce-antd-frontend-init替换成自己的项目名字。
  • 第二步:打开项目全局搜索shift+f6yxinmiralce-antd-frontend-init全部替换成项目名字。
  • 第三步:把文件夹下的.idea文件删除,重新打开ide即可获得自己的全新项目。

🛣️ 相关文档