你的第一个前端入门项目,保姆级教程!Vue3用户中心网站开发

大家好,我是程序员鱼皮。

原用户中心教程中,采用了 React + Ant Design Pro 脚手架技术开发前端,由于框架迭代更新,会增加基础较差的同学的学习成本,因此鱼皮选择录制一套全新的、更适合入门的前端项目教程。

注意事项

如何配合原视频教程使用呢?

  • 如果你是后端方向的同学、或者完全没有前端基础:建议先忽略本节教程,优先从 0 开始学习后端教程、并且看到前端内容就跳过,直到学完并保证理解了后端,最后再单独看本节教程即可。这点非常重要,啥都不会的情况下直接去跟学前端框架的效率不高,做项目前请大家先完整阅读鱼皮的项目学习建议,事半功倍。

  • 如果你是后端方向的同学、并且有一定的前端框架基础:学习前端时可以直接看本节教程(Vue 框架),或者完整观看原教程(React 框架)

  • 如果你是前端方向的同学,有一定的前端框架基础:可以根据需要看本节教程(Vue 框架),或者完整观看原教程(React 框架),按需学习对应的技术。

其实原视频教程的 React 前端版本依然是可以学习的,注意环境的配置就行,可以参考 该视频 配置前后端环境。

开发环境配置: https://www.bilibili.com/video/BV14SUNYREv8

本节重点

本节的前端教程中,会假设已经完成了用户中心项目后端,并提供了 API 接口,在此基础上去开发前端项目,功能和特性与之前的前端教程(React 框架)保持一致,因此可以单独学习。

一般企业中开发,前后端是同时开发的,但如果想保证前端的运行效果,最好是后端先完成开发,独立开发全栈项目时也建议先开发后端。(当然前端同学也可以使用 Mock 技术模拟后端接口,不用干等着后端)。

本节大纲:

  • 需求分析

  • Web 前端技术选型

  • Web 前端项目初始化(简易 Vue 前端万用模板开发)

    • 脚手架
    • 通用布局
    • 路由
    • 请求
  • 前端页面开发

    • 用户登录(Cookie / Session 讲解)
    • 用户注册
    • 用户管理
  • 项目部署(多环境)

一、Web 前端技术选型

  • Vue 3:主流前端框架
  • Vue-CLI 脚手架:快速启动项目
  • Ant Design 组件库:快速开发 UI 界面
  • Axios 请求库:向后端发送请求
  • Pinia 状态管理:维护前端全局数据
  • ⭐️ 前端工程化:ESLint + Prettier + TypeScript,保证前端项目开发规范

二、Web 前端项目初始化

自主打造一套简易的前端开发项目模板

确认环境!!!

nodeJS 版本 >= 10 均可

检测命令:

node -v

推荐使用快速切换和管理 node 版本的工具 NVM ,可以参考 该视频 配置前端环境。

创建项目

使用 Vue-CLI 脚手架快速创建 Vue3 的项目:https://cli.vuejs.org/zh/

为什么选择该脚手架?

  1. 常用的标准脚手架,开源、并且 star 数多
  2. 目前进入维护模式,相对稳定,对低版本的 Node 兼容性好,不容易出现因为环境不同而导致的问题
  3. 相对轻量,整合了一些前端项目开发常用的工具,并且可以按需选取

该脚手架自动整合了 vue-router 路由、TypeScript、前端工程化等库:

img

可以 参考官方文档 来安装脚手架工具:

npm install -g @vue/cli

检测是否安装成功:

vue -V

如图,鱼皮的版本是 5.0.8,尽量保持一致:

img

如果找不到命令,那么建议重新到官网安装 npm,会自动帮你配置环境变量。

创建项目:

vue create user-center-frontend-vue

手动选择特性:

img

选择如下特性:

img

img

会自动生成代码并安装依赖:

img

然后用 WebStorm 打开项目,在终端执行 npm run serve,能访问网页就成功了。

img

前端工程化配置

脚手架已经帮我们配置了 Prettier 代码美化、ESLint 自动校验、TypeScript 类型校验、格式化插件等,无需再自行配置。

但是需要在 webstorm 里开启代码美化插件:

img

在 vue 文件中执行格式化快捷键,不报错,表示配置工程化成功。

如果发现格式化效果不好,也没关系,之后可以使用另外一种格式化快捷键:

img

如果想关闭 ESLint 校验导致的编译错误(项目无法运行),可以修改 vue.config.js关闭 lintOnsave

const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
  transpileDependencies: true,
  lintOnSave: "warning",
});

修改 .eslintrc.js 和 tsconfig.json 可以改变校验规则。

如果不使用脚手架,就需要自己整合这些工具:

  • 代码规范:https://eslint.org/docs/latest/use/getting-started
  • 代码美化:https://prettier.io/docs/en/install.html
  • 直接整合:https://github.com/prettier/eslint-plugin-prettier#recommended-configuration(包括了 https://github.com/prettier/eslint-config-prettier#installation)

引入组件库

引入 Ant Design Vue 组件库,参考 官方文档 快速上手。

注意,本教程使用的是 v4.2.6 的组件库版本,如果后续阅读本教程中发现有组件或语法不一致,以官方文档为主,或者在网站右上角切换对应版本的文档即可:

img

执行安装:

npm i --save ant-design-vue@4.x

改变主入口文件 main.ts,全局注册组件(为了方便):

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import Antd from "ant-design-vue";
import "ant-design-vue/dist/reset.css";

createApp(App).use(Antd).use(router).mount("#app");

随便引入一个组件,如果显示出来,就表示引入成功。

比如引入按钮:

<a-button type="primary">Primary Button</a-button>

效果如图:

img

开发规范

建议遵循 Vue3 的组合式 API (Composition API),而不是 选项式 API,开发更自由高效一些。

示例代码:

<template>
  <div id="xxPage">

  </div>
</template>

<script setup lang="ts">

</script>

<style scoped>
#xxPage {
}

</style>

全局通用布局

1、基础布局结构

在 layouts 目录下新建一个布局 BasicLayout.vue, 在 App.vue 全局页面入口文件中引入。

App.vue 代码如下:

<template>
  <div id="app">
    <BasicLayout />
  </div>
</template>

<script setup lang="ts">
import BasicLayout from "@/layouts/BasicLayout.vue";
</script>

可以移除页面内的默认样式,防止样式污染:

<style>
#app {
}
</style>

选用 Ant Design 组件库的 Layout 组件 ,先把【上中下】布局编排好,然后再填充内容:

img

代码如下:

<template>
  <div id="basicLayout">
    <a-layout style="min-height: 100vh">
      <a-layout-header>Header</a-layout-header>
      <a-layout-content>Content</a-layout-content>
      <a-layout-footer>Footer</a-layout-footer>
    </a-layout>
  </div>
</template>

<script setup lang="ts"></script>

样式:

<style scoped>
#basicLayout {
}
</style>
2、全局底部栏

通常用于展示版权信息:

<a-layout-footer class="footer">
  <a href="https://www.codefather.cn" target="_blank">
    编程导航 by 程序员鱼皮
  </a>
</a-layout-footer>

样式:

#basicLayout .footer {
  background: #efefef;
  padding: 16px;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  text-align: center;
}
3、动态替换内容

项目使用了 Vue Router 路由库,可以在 router/index.ts 配置路由,能够根据访问的页面地址找到不同的文件并加载渲染。

修改 BasicLayout 内容部分的代码如下:

<a-layout-content class="content">
  <router-view />
</a-layout-content>

修改样式,要和底部栏保持一定的外边距,否则内容会被遮住:

<style scoped>
#basicLayout .content {
  background: linear-gradient(to right, #fefefe, #fff);
  margin-bottom: 28px;
  padding: 20px;
}
</style>
4、全局顶部栏

由于顶部栏的开发相对复杂,可以基于 Ant Design 的菜单组件 来创建 GlobalHeader 全局顶部栏组件,组件统一放在 components 目录中

先直接复制现成的组件示例代码到 GlobalHeader 中即可。

img

在基础布局中引入顶部栏组件:

<a-layout-header class="header">
  <GlobalHeader />
</a-layout-header>

引入代码如下:

<script setup lang="ts">
import GlobalHeader from "@/components/GlobalHeader.vue";
</script>

效果如下:

img

可以修改下全局 Header 的样式,清除一些默认样式(比如背景色等),样式代码如下:

#basicLayout .header {
  padding-inline: 20px;
  margin-bottom: 16px;
  color: unset;
  background: white;
}

接下来要修改 GlobalHeader:

1)给菜单外套一层元素,用于整体控制样式:

<div id="globalHeader">
  <a-menu v-model:selectedKeys="current" mode="horizontal" :items="items" />
</div>

2)根据我们的需求修改菜单配置,key 为要跳转的 URL 路径:

<script lang="ts" setup>
import { h, ref } from "vue";
import { CrownOutlined, HomeOutlined } from "@ant-design/icons-vue";
import { MenuProps } from "ant-design-vue";

const current = ref<string[]>(["home"]);
const items = ref<MenuProps["items"]>([
  {
    key: "/",
    icon: () => h(HomeOutlined),
    label: "主页",
    title: "主页",
  },
  {
    key: "/user/login",
    label: "用户登录",
    title: "用户登录",
  },
  {
    key: "/user/register",
    label: "用户注册",
    title: "用户注册",
  },
  {
    key: "/admin/userManage",
    icon: () => h(CrownOutlined),
    label: "用户管理",
    title: "用户管理",
  },
  {
    key: "others",
    label: h(
      "a",
      { href: "https://www.codefather.cn", target: "_blank" },
      "编程导航"
    ),
    title: "编程导航",
  },
]);
</script>

效果如图:

img

3)完善全局顶部栏,左侧补充网站图标和标题。

先把 logo.png 放到 src/assets 目录下,替换掉原本的默认 Logo:

img

修改 GlobalHeader 代码,补充 HTML:

<div class="title-bar">
  <img class="logo" src="../assets/logo.png" alt="logo" />
  <div class="title">鱼皮用户中心</div>
</div>

补充 CSS 样式:

<style scoped>
.title-bar {
  display: flex;
  align-items: center;
}

.title {
  color: black;
  font-size: 18px;
  margin-left: 16px;
}

.logo {
  height: 48px;
}
</style>

4)完善顶部导航栏,右侧展示当前用户的登录状态(暂时用登录按钮代替):

<div class="user-login-status">
  <a-button type="primary" href="/user/login">登录</a-button>
</div>

5)优化导航栏的布局,采用 栅格组件的自适应布局(左中右,左侧右侧宽度固定,中间菜单栏自适应)

<a-row :wrap="false">
  <a-col flex="200px">
    <div class="title-bar">
      <img class="logo" src="../assets/logo.png" alt="logo" />
      <div class="title">鱼皮用户中心</div>
    </div>
  </a-col>
  <a-col flex="auto">
    <a-menu
      v-model:selectedKeys="current"
      mode="horizontal"
      :items="items"
    />
  </a-col>
  <a-col flex="80px">
    <div class="user-login-status">
      <a-button type="primary" href="/user/login">登录</a-button>
    </div>
  </a-col>
</a-row>

效果如图:

img

路由

目标:点击菜单项后,可以跳转到对应的页面;并且刷新页面后,对应的菜单自动高亮。

1)修改路由配置

修改 router/index.ts 文件的 routes 配置,定义我们需要的页面路由,每个 path 对应一个 component(要加载的组件),暂时可以先用 HomeView 代替。

const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "home",
    component: HomeView,
  },
  {
    path: "/user/login",
    name: "userLogin",
    component: HomeView,
  },
  {
    path: "/user/register",
    name: "userRegister",
    component: HomeView,
  },
  {
    path: "/admin/userManage",
    name: "adminUserManage",
    component: HomeView,
  },
];

2)给 GlobalHeader 的菜单组件绑定跳转事件:

import { useRoute, useRouter } from "vue-router";
const router = useRouter();

// 路由跳转事件
const doMenuClick = ({ key }: { key: string }) => {
  router.push({
    path: key,
  });
};

修改 HTML 模板,绑定事件:

<a-menu
  v-model:selectedKeys="current"
  mode="horizontal"
  :items="items"
  @click="doMenuClick"
/>

3)同步路由的更新到菜单项高亮

同步高亮原理:

  1. 点击菜单时,Ant Design 组件已经通过 v-model 绑定 current 变量实现了高亮。
  2. 刷新页面时,需要获取到当前 URL 路径,然后修改 current 变量的值,从而实现同步。

使用 Vue Router 的 afterEach 路由钩子实现,每次改变路由或刷新页面时都会自动更新 current 的值,从而实现高亮:

const router = useRouter();
// 当前选中菜单
const current = ref<string[]>([]);
// 监听路由变化,更新当前选中菜单
router.afterEach((to, from, failure) => {
  current.value = [to.path];
});

💡思考:大家有没有发现,路由和菜单配置中,有一些是重复的呢?有没有更好地方式来配置路由和菜单项,不用每次修改时都要改两边的代码呢?后续的项目中有讲解哦~

请求

引入 Axios 请求库

一般情况下,前端只负责界面展示和动效交互,尽量避免写复杂的逻辑;当需要获取数据时,通常是向后端提供的接口发送请求,由后端执行操作(比如保存数据)并响应数据给前端。

前端如何向后端发送请求呢?最传统的方式是使用 AJAX 技术。但其代码有些复杂,我们可以使用第三方的封装库,来简化发送请求的代码,比如主流的请求工具库 Axios。

1、请求工具库

安装请求工具类 Axios

官方文档:https://axios-http.com/docs/intro

代码:

npm install axios
2、全局自定义请求

需要自定义全局请求地址等,参考 Axios 官方文档,编写请求配置文件 request.ts。包括全局接口请求地址、超时时间、自定义请求响应拦截器等。

响应拦截器的应用场景:我们需要对接口的 通用响应 进行统一处理,比如从 response 中取出 data;或者根据 code 去集中处理错误。这样不用在每个接口请求中都去写相同的逻辑。

比如可以在全局响应拦截器中,读取出结果中的 data,并校验 code 是否合法,如果是未登录状态,则自动登录。

示例代码如下,其中 withCredentials: true 一定要写,否则无法在发请求时携带 Cookie,就无法完成登录。

代码如下:

import axios from "axios";

const myAxios = axios.create({
  baseURL: "http://localhost:8080",
  timeout: 10000,
  withCredentials: true,
});

// Add a request interceptor
myAxios.interceptors.request.use(
  function (config) {
    // Do something before request is sent
    return config;
  },
  function (error) {
    // Do something with request error
    return Promise.reject(error);
  }
);

// Add a response interceptor
myAxios.interceptors.response.use(
  function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    console.log(response);

    const { data } = response;
    console.log(data);
    // 未登录
    if (data.code === 40100) {
      // 不是获取用户信息接口,或者不是登录页面,则跳转到登录页面
      if (
        !response.request.responseURL.includes("user/current") &&
        !window.location.pathname.includes("/user/login")
      ) {
        window.location.href = `/user/login?redirect=${window.location.href}`;
      }
    }
    return response;
  },
  function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    return Promise.reject(error);
  }
);

export default myAxios;
3、编写请求代码

在 src 目录下新建 api/user.ts,存放所有和用户有关的 API 接口。按照 Axios 的规则,根据后端接口信息编写对应的代码即可:

import myAxios from "@/request";

/**
 * 用户注册
 * @param params
 */
export const userRegister = async (params: any) => {
  return myAxios.request({
    url: "/api/user/register",
    method: "POST",
    data: params,
  });
};

/**
 * 用户登录
 * @param params
 */
export const userLogin = async (params: any) => {
  return myAxios.request({
    url: "/api/user/login",
    method: "POST",
    data: params,
  });
};

/**
 * 用户注销
 * @param params
 */
export const userLogout = async (params: any) => {
  return myAxios.request({
    url: "/api/user/logout",
    method: "POST",
    data: params,
  });
};

/**
 * 获取当前用户
 */
export const getCurrentUser = async () => {
  return myAxios.request({
    url: "/api/user/current",
    method: "GET",
  });
};

/**
 * 获取用户列表
 * @param username
 */
export const searchUsers = async (username: any) => {
  return myAxios.request({
    url: "/api/user/search",
    method: "GET",
    params: {
      username,
    },
  });
};

/**
 * 删除用户
 * @param id
 */
export const deleteUser = async (id: string) => {
  return myAxios.request({
    url: "/api/user/delete",
    method: "POST",
    data: id,
    // 关键点:要传递 JSON 格式的值
    headers: {
      "Content-Type": "application/json",
    },
  });
};

然后可以尝试在任意页面代码中调用 API:

import { getCurrentUser } from "@/api/user";

getCurrentUser().then((res) => {
  console.log(res);
});

按 F12 打开开发者工具查看请求,如果发现请求错误,要查看错误信息具体分析。比如这里我们显然遇到了 跨域问题,这是由于前端网页地址和后端请求接口地址不同导致的。

可以通过修改后端代码,增加跨域注解来解决:

img

再次发送请求,看到如下输出则表示请求成功:

img

💡思考:大家有没有发现,一个一个自己编写请求代码,非常麻烦,而且如果后端接口信息修改了,前端也要进行相应的更改。其实我们可以利用一些工具,根据后端 Swagger 接口文档自动生成前端请求代码。后面的项目中会讲到~

全局状态管理

什么是全局状态管理?

所有页面全局共享的变量,而不是局限在某一个页面中。

适合作为全局状态的数据:已登录用户信息(每个页面几乎都要用)

Pinia 是一个主流的状态管理库,使用更简单,可参考入门文档

1)先按照文档引入 Pinia,安装后修改 main.ts 为:

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { createPinia } from "pinia";
import Antd from "ant-design-vue";
import "ant-design-vue/dist/reset.css";

const pinia = createPinia();
createApp(App).use(pinia).use(Antd).use(router).mount("#app");

2)定义状态。在 src/store 目录下定义 user 模块,定义了用户的存储、远程获取、修改逻辑:

import { defineStore } from "pinia";
import { ref } from "vue";
import { getCurrentUser } from "@/api/user";

export const useLoginUserStore = defineStore("loginUser", () => {
  const loginUser = ref<any>({
    username: "未登录",
  });

  async function fetchLoginUser() {
    const res = await getCurrentUser();
    if (res.data.code === 0 && res.data.data) {
      loginUser.value = res.data.data;
    }
  }

  function setLoginUser(newLoginUser: any) {
    loginUser.value = newLoginUser;
  }

  return { loginUser, setLoginUser, fetchLoginUser };
});

3)使用状态。直接使用 store 中导出的状态变量和函数。

可以在首次进入到页面时,尝试获取登录用户信息。修改 App.vue,编写远程获取数据代码:

const loginUserStore = useLoginUserStore();
loginUserStore.fetchLoginUser();

在任何页面中都可以使用数据,比如在页面中直接展示:

{{ JSON.stringify(loginUserStore.loginUser) }}

在 userStore 中编写测试代码,测试用户状态的更新:

  async function fetchLoginUser() {
    const res = await getLoginUserUsingGet();
    if (res.data.code === 0 && res.data.data) {
      loginUser.value = res.data.data;
    } else {
      setTimeout(() => {
        loginUser.value = { username: "测试用户", id: 1 };
      }, 3000);
    }
  }

修改顶部导航栏,在右侧展示登录状态:

<div class="user-login-status">
  <div v-if="loginUserStore.loginUser.id">
    {{ loginUserStore.loginUser.username ?? "无名" }}
  </div>
  <div v-else>
    <a-button type="primary" href="/user/login">登录</a-button>
  </div>
</div>

等待 3 秒后,右上角就能看到用户名“测试用户”了。测试完之后记得删除或注释掉测试代码。


至此,一个入门级的前端项目就初始化好了,下面可以进行页面开发。

三、前端页面开发

欢迎页开发

欢迎页的内容很简单,我们主要通过这个页面,了解页面开发的流程。

新建 src/pages 目录,用于存放所有的页面文件。

每次新建页面时,需要在 router/index.ts 中配置路由,比如欢迎页的路由为:

const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "home",
    component: HomeView,
  },
  ...
]

然后在 pages 目录下新建页面文件,将所有页面按照 url 层级进行创建。注意,页面名称尽量做到“见名知意”。

结构如图,可以先提前建出我们需要的页面文件,也可以随写随建:

img

然后在路由文件中,引入页面:

import HomePage from "@/pages/HomePage.vue";

const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "home",
    component: HomePage,
  },
  ...
]

任意修改页面代码:

<template>
  <div id="homePage">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script setup lang="ts">
const msg = "欢迎来到编程导航,你将从这里开始项目学习之旅~";
</script>

<style scoped>
#homePage {
}
</style>

页面效果如图:

img

用户登录页面

新建 UserLoginPage.vue,可以基于 Ant Design 的表单组件 快速开发登录页面。

先开发基础页面,可以按照需要增加一些前端校验,代码如下:

<template>
  <div id="userLoginPage">
    <h2 class="title">用户登录</h2>
    <a-form
      style="max-width: 480px; margin: 0 auto"
      label-align="left"
      :label-col="{ span: 4 }"
      :wrapper-col="{ span: 20 }"
      :model="form"
      @finish="handleSubmit"
    >
      <a-form-item
        name="userAccount"
        label="账号"
        :rules="[{ required: true, message: '请输入账号' }]"
      >
        <a-input v-model:value="form.userAccount" placeholder="请输入账号" />
      </a-form-item>
      <a-form-item
        name="userPassword"
        label="密码"
        :rules="[
          { required: true, message: '请输入密码' },
          { min: 8, message: '密码不少于 8 位' },
        ]"
      >
        <a-input-password
          v-model:value="form.userPassword"
          placeholder="请输入密码"
        />
      </a-form-item>
      <a-form-item :wrapper-col="{ offset: 4, span: 20 }">
        <a-button type="primary" html-type="submit">登录</a-button>
      </a-form-item>
    </a-form>
  </div>
</template>

需要定义一个响应式变量来接受表单输入的值:

/**
 * 表单信息
 */
const form = reactive({
  userAccount: "",
  userPassword: "",
});

编写表单提交后执行的函数,登陆成功后需要在全局状态中记录当前登录用户的信息,并跳转到主页:

const router = useRouter();
const loginUserStore = useLoginUserStore();

/**
 * 提交表单
 * @param data
 */
const handleSubmit = async () => {
  const res = await userLogin(form);
  // 登录成功,跳转到主页
  if (res.data.code === 0 && res.data.data) {
    await loginUserStore.fetchLoginUser();
    message.success("登录成功");
    router.push({
      path: "/",
      replace: true,
    });
  } else {
    message.error("登录失败");
  }
};

页面效果如图:

img

如何知道是哪个用户登录了?

javaweb 这一块的知识

  1. 连接服务器端后,得到一个 session 状态(匿名会话),返回给前端
  2. 登录成功后,得到了登录成功的 session,并且给该session设置一些值(比如用户信息),返回给前端一个设置 cookie 的 ”命令“
  3. 前端接收到后端的命令后,设置 cookie,保存到浏览器内
  4. 前端再次请求后端的时候(相同的域名),在请求头中带上cookie去请求
  5. 后端拿到前端传来的 cookie,找到对应的 session
  6. 后端从 session 中可以取出基于该 session 存储的变量(用户的登录信息、登录名)

用户注册页面

同样使用表单组件,在用户登录页面的基础上调整即可,涉及到更多表单项的填写:

<template>
  <div id="userRegisterPage">
    <h2 class="title">用户注册</h2>
    <a-form
      style="max-width: 480px; margin: 0 auto"
      label-align="left"
      :label-col="{ span: 4 }"
      :wrapper-col="{ span: 20 }"
      :model="form"
      @finish="handleSubmit"
    >
      <a-form-item
        name="userAccount"
        label="账号"
        :rules="[{ required: true, message: '请输入账号' }]"
      >
        <a-input v-model:value="form.userAccount" placeholder="请输入账号" />
      </a-form-item>
      <a-form-item
        name="userPassword"
        label="密码"
        :rules="[
          { required: true, message: '请输入密码' },
          { min: 8, message: '密码不少于 8 位' },
        ]"
      >
        <a-input-password
          v-model:value="form.userPassword"
          placeholder="请输入密码"
        />
      </a-form-item>
      <a-form-item
        name="checkPassword"
        label="确认密码"
        :rules="[
          { required: true, message: '请输入确认密码' },
          { min: 8, message: '确认密码不少于 8 位' },
        ]"
      >
        <a-input-password
          v-model:value="form.checkPassword"
          placeholder="请输入密码"
        />
      </a-form-item>
      <a-form-item
        name="planetCode"
        label="编号"
        :rules="[{ required: true, message: '请输入编号' }]"
      >
        <a-input v-model:value="form.planetCode" placeholder="请输入编号" />
      </a-form-item>
      <a-form-item :wrapper-col="{ offset: 4, span: 20 }">
        <a-button type="primary" html-type="submit">注册</a-button>
      </a-form-item>
    </a-form>
  </div>
</template>

定义表单信息变量:

const form = reactive({
  userAccount: "",
  userPassword: "",
  checkPassword: "",
  planetCode: "",
});

编写提交表单函数,可以增加一些前端校验,并且在注册成功后跳转到用户登录页:

const handleSubmit = async () => {
  // 可以增加一些前端校验
  if (form.checkPassword !== form.userPassword) {
    message.error("二次输入的密码不一致");
    return;
  }
  const res = await userRegister(form);
  // 注册成功,跳转到登录页面
  if (res.data.code === 0 && res.data.data) {
    message.success("注册成功");
    router.push({
      path: "/user/login",
      replace: true,
    });
  } else {
    message.error("注册失败," + res.data.description);
  }
};

页面效果如图:

img

用户管理页面

需求:允许管理员查看已注册的用户信息,能够根据用户名称搜索用户,并删除非法用户。

需要注意,要防止普通用户也能看到用户信息,所以要增加一定的权限控制,可以分为页面开发和权限控制两个步骤来实现。

1、页面开发

编写页面:上方搜索栏,下方表格。

1)先利用 Ant Design 的表格组件,展示全部用户信息。

img

只需要根据自己的数据表,编写 columns 表格列,并传入获取到的 data 数据,组件就能自动帮我们展示出表格,非常方便。

定义表格列:

const columns = [
  {
    title: "id",
    dataIndex: "id",
  },
  {
    title: "用户名",
    dataIndex: "username",
  },
  {
    title: "账号",
    dataIndex: "userAccount",
  },
  {
    title: "头像",
    dataIndex: "avatarUrl",
  },
  {
    title: "性别",
    dataIndex: "gender",
  },
  {
    title: "创建时间",
    dataIndex: "createTime",
  },
  {
    title: "用户角色",
    dataIndex: "userRole",
  },
  {
    title: "操作",
    key: "action",
  },
];

从后端获取数据:

// 数据
const data = ref([]);

// 获取数据
const fetchData = async () => {
  const res = await searchUsers("");
  if (res.data.data) {
    data.value = res.data.data || [];
  } else {
    message.error("获取数据失败");
  }
};

fetchData();

效果如图:

img

显然展示效果并不好,对于图片、用户角色、创建时间之类的数据,我们可以有更好的展示方式。

表格组件支持我们使用 Vue 的插槽自定义列的展示,参考 Demo 有样学样修改即可。

<template #bodyCell="{ column, record }">
  <template v-if="column.dataIndex === 'avatarUrl'">
    <a-image :src="record.avatarUrl" :width="120" />
  </template>
  <template v-else-if="column.dataIndex === 'userRole'">
    <div v-if="record.userRole === 1">
      <a-tag color="green">管理员</a-tag>
    </div>
    <div v-else>
      <a-tag color="blue">普通用户</a-tag>
    </div>
  </template>
  <template v-if="column.dataIndex === 'createTime'">
    {{ dayjs(record.createTime).format("YYYY-MM-DD HH:mm:ss") }}
  </template>
  <template v-else-if="column.key === 'action'">
    <a-button danger>删除</a-button>
  </template>
</template>

效果如图:

img

2)利用 Ant Design 的搜索组件,实现对数据的搜索。

HTML 结构代码:

<a-input-search
  style="max-width: 320px"
  v-model:value="searchValue"
  placeholder="输入用户名搜索"
  enter-button="搜索"
  size="large"
  @search="onSearch"
/>

改造获取数据函数,支持传入参数(搜索条件):

// 获取数据
const fetchData = async (username = "") => {
  const res = await searchUsers(username);
  if (res.data.data) {
    data.value = res.data.data || [];
  } else {
    message.error("获取数据失败");
  }
};

定义搜索变量,点击搜索按钮时会触发搜索,获取数据:

const searchValue = ref();
const onSearch = () => {
  fetchData(searchValue.value);
};

效果如图:

img

3)开发删除功能

编写点击删除按钮后的处理函数:

// 删除数据
const doDelete = async (id: string) => {
  if (!id) {
    return;
  }
  const res = await deleteUser(id);
  if (res.data.code === 0) {
    message.success("删除成功");
  } else {
    message.error("删除失败");
  }
};

绑定事件:

<a-button danger @click="doDelete(record.id)">
  删除
</a-button>
2、权限控制

有 2 种实现方式 —— 单页面控制权限,或者全局控制权限。

思路都是一致的,在进入某个页面时判断用户是否具有该页面的权限,无非是把权限校验相关的代码写在单个页面内,还是写到一个独立的文件中罢了。

建议编写独立的全局权限控制文件,可以利用 Vue Router 的路由守卫实现,每次切换并进入页面前,都会检查一下当前用户是否具有特定页面的权限。

在 src 下编写 access.ts 权限校验文件,可以自己定义逻辑,比如用页面前缀来统一判断:

import { useLoginUserStore } from "@/store/user";
import { message } from "ant-design-vue";
import router from "@/router";

/**
 * 全局权限校验
 */
router.beforeEach(async (to, from, next) => {
  const loginUserStore = useLoginUserStore();
  const loginUser = loginUserStore.loginUser;
  const toUrl = to.fullPath;
  if (toUrl.startsWith("/admin")) {
    if (!loginUser || loginUser.userRole !== 1) {
      message.error("没有权限");
      next(`/user/login?redirect=${to.fullPath}`);
      return;
    }
  }
  next();
});

在 main.ts(全局入口文件)中引入:

import "./access";

用一个未登录的用户来测试,尝试访问用户管理页面,会报权限错误:

img

四、项目部署

多环境

多环境:指同一套项目代码在不同的阶段需要根据实际情况来调整配置并且部署到不同的机器上。

为什么需要多环境?

  1. 每个环境互不影响

  2. 区分不同的阶段:开发 / 测试 / 生产

  3. 对项目进行定制优化,比如:

    1. 本地日志级别
    2. 精简依赖,节省项目体积
    3. 项目的环境 / 参数可以调整,比如 JVM 参数

总之,就是针对不同环境做不同的事情。

多环境分类:

  1. 本地环境(自己的电脑)localhost
  2. 开发环境(远程开发)大家连同一台机器,为了大家开发方便
  3. 测试环境(测试)开发 / 测试 / 产品,单元测试 / 性能测试 / 功能测试 / 系统集成测试,独立的数据库、独立的服务器
  4. 预发布环境(体验服):和正式环境一致,正式数据库,更严谨,查出更多问题
  5. 正式环境(线上,公开对外访问的项目):尽量不要改动,保证上线前的代码是 “完美” 运行
  6. 沙箱环境(实验环境):为了做实验

前端多环境实战

对于前端来说,多环境很重要的一个目的就是区分请求地址,比如:

如何实现多环境呢?

前端不同的启动方式,对应的 process.env.NODE_ENV 变量的值也不同,可以在代码中根据这个值来判断不同的环境。

  • 开发环境:npm run serve(本地启动,监听端口、自动更新),NODE_ENV == development
  • 线上环境:npm run build(项目构建打包),NODE_ENV == production

此外,有些框架支持多套配置文件,可以在配置文件后添加对应的环境名称后缀来区分开发环境和生产环境。比如对于 Umi 这种框架,配置文件规则如下(参考文档):

  • 开发环境:config.dev.ts
  • 生产环境:config.prod.ts
  • 公共配置:config.ts(不带后缀)

所以,只需要修改 request.ts 请求配置文件,就能统一更改请求地址啦:

const myAxios = axios.create({
  // 区分开发环境和线上环境
  baseURL:
    process.env.NODE_ENV === "development"
    ? "http://localhost:8080"
    : "线上地址",
  timeout: 10000,
  withCredentials: true,
});

打包

执行 npm run build 后,会得到 dist 目录,这就是可以部署到服务器上的网站成品包啦~

img

部署上线

可以阅读我之前分享过的上线方法汇总:https://www.codefather.cn/post/1808578179510697986

更多编程学习资源