做为服务端同学,接触前端代码较少,刚毕业的时候用过 jQuery + Bootstrap2/3,当时的感觉就是,容易上手,学习门槛相对较低,另外就是有一个非常成熟的 jQuery 插件库,在这里,几乎可以找到日常需要的大部分插件(这里的插件,不是简单的按钮/表单组件,而是像抽奖/大转盘/推箱子游戏这样的完整的功能块,这些避免了自己去写“困难”的 CSS),组合组合就可以搭出一个完善的页面,改改 Ajax 请求,就能实现一个小项目了。
jQuery插件库地址:https://www.jq22.com/
后端:SpringBoot
前端:Vue + TypeScript(简称 ts) + Vite + VueRouter + Axios + ElementPlus + Pinia
所选技术 | 版本号 | 技术定位 | 对比技术 | 差异点 |
Vue | 3.2.45 | 渐进式的 js 框架,支持响应式 | Vue2 | 性能优/包体积更小/对 ts 的支持好/漂亮的语法糖 |
React | 简单易学,详细对比 | |||
TypeScript | 4.7.4 | 基于 js 的可强类型的编程语言 | js | ts 是 js 的超集,ts 面向对象,可强类型,支持泛型、接口、类等,熟悉 Java 的同学很容易上手 ts 代码可转换为 js 代码,然后在浏览器进行执行 |
Vite | 4.0.0 | 工具链:实现构建/开发服务器等功能 | webpack | 速度快、Vue3 推荐使用 |
Pinia | 2.0.28 | 状态管理器 | Vuex | Vue3 推荐,Pinia 可看做是新版的 Vuex,具体见 官网描述 |
VueRouter | 4.1.6 | 页面路由器 | ||
Axios | 1.2.2 | 调用后端服务的客户端工具 | ||
ElementPlus | 2.2.28 | 基于 Vue3 的组件库 |
Vue与React详细对比:https://www.mindk.com/blog/react-vs-vue/
Pinia与Vuex关系描述:https://vuex.vuejs.org/
搭建开发环境
本人使用 mac 版进行介绍
▐ 安装 Node.js 和 npm
在官网下载 Node.js,一路点击安装,会安装 Node.js 和 npm。最后显示如下信息,成功。
官网地址:https://nodejs.org/en
Node.js v18.12.1 to /usr/local/bin/nodenpm v8.19.2 to /usr/local/bin/npm
// 查看当前数据源npm config get registry // 默认是https://registry.npmjs.org/// 设置 npm 数据源为淘宝数据源npm config set registry https://registry.npm.taobao.org
▐ 安装开发工具 VisualStudioCode
在 官网下载 VisualStudioCode(简称 vsCode),解压安装。之后安装 Vue 插件 volar。
官网地址:https://code.visualstudio.com/ volar 地址:https://marketplace.visualstudio.com/items?itemName=Vue.volar
▐ 使用脚手架初始化代码
Vue 有两个脚手架:一个基于 Vite,一个基于 Vue CLI(基于 webpack),后者已经处于维护状态,新项目官方建议使用 Vite。
输入如下命令使用 Vite 来初始化一个项目。
npm init vue@latest //初次执行该命令,会安装和执行 create-vue,它是 Vue 提供的官方脚手架工具✔ Project name: … ${projectName} // 项目名称✔ Add TypeScript? … Yes // 选择 ts✔ Add Vue Router for Single Page Application development? … Yes // 增加 VueRouter✔ Add Pinia for state management? … Yes // 增加 PiniaScaffolding project in /Users/jigang/Desktop/vue-study/vue-boot...Done. Now run:// 项目初始化之后,执行如下命令,可以安装相关的依赖,启动项目cd vue-bootnpm installnpm run dev
VITE v4.0.4 ready in 306 ms➜ Local: http://localhost:5177/➜ Network: use --host to expose➜ press h to show help
代码结构
如果是小型项目,用单一项目(前后端放一起)进行开发就好,这里使用常见的前后端分离的方式。
前端代码: https://github.com/zhaojigang/vue-boot (单页面应用)
后端代码: https://github.com/zhaojigang/vue-springboot
├── index.html 界面入口(定义根div)├── public 公共静态资源包│ └── favicon.ico├── src│ ├── main.ts 入口ts文件(创建应用/use各种插件/挂载到index.html的根div上)│ ├── App.vue 根组件│ ├── assets 静态资源包│ │ └── main.css│ ├── layout 布局组件│ │ ├── Menu.vue 菜单组件│ │ └── index.vue 基础布局组件(header/aside/footer等)│ ├── views 业务视图组件(路由跳转页面)│ │ ├── book│ │ │ └── BookListView.vue book视图组件│ │ ├── home│ │ │ └── HomeView.vue 首页视图组件│ │ └── login│ │ └── LoginView.vue 登录页视图组件│ ├── components 业务组件│ │ └── book book组件│ │ └── AddBookDialog.vue 新增书籍弹窗组件│ ├── router 路由│ │ ├── index.ts 业务路由│ │ └── permission.ts 路由守卫│ ├── stores 状态存储器│ │ └── user 业务状态存储│ │ └── user.ts user状态存储(state/getter/actions)│ ├── types 接口定义(制定接口标准)│ │ ├── book.ts book相关接口│ │ └── user.ts user相关接口│ ├── utils 工具│ │ ├── constants.ts 常量│ │ └── login.ts 登录/退出接口封装│ └── api axios请求封装api│ ├── baseRequest.ts axios基本封装(axios对象创建/拦截器)│ ├── book.ts book业务请求api│ └── login.ts login业务请求api├── .env.development 开发环境配置文件├── .env.production 生产环境配置文件├── package.json└── vite.config.ts
下面分类别来看下各个文件
▐ 框架文件
三个核心框架文件 index.html / main.ts / App.vue
界面入口
index.html:整个单页面应用入口,提供容器入口。
<html lang="en">...<!-- 根容器 --><div id="app"></div><!-- 引入 ts 入口文件 --><script type="module" src="/src/main.ts"></script>...</html>
入口 ts 文件
main.ts:基于根组件创建应用/use各种插件/挂载应用到index.html的根div上
/* 引入根组件创建函数 */import { createApp } from 'vue'/* 引入状态管理器 Pinia 创建函数 */import { createPinia } from 'pinia'/* 引入根组件 */import App from './App.vue'/* 引入 VueRouter */import router from './router'/* 引入主 css */import './assets/main.css'/* 引入路由守卫 */import '@/router/permission'/* 基于根组件创建应用 */const app = createApp(App)/* 使用 Pinia 做状态管理 */app.use(createPinia())/* 使用 VueRouter 做路由 */app.use(router)/* 应用管理 index.html 中的 id=app 的 div */app.mount('#app')
根组件
App.vue:Vue 单页面应用推畅组件化编程,App.vue 是所有组件的父组件,被称为根组件,main.ts 基于该根组件创建应用
<!-- 根组件 --><!-- 使用 setup 语法糖,使用 TypeScript --><script setup lang="ts">/* 引入router函数 */import { RouterView } from 'vue-router'</script><template><!-- 外层路由到的页面会在此处渲染 --><RouterView></RouterView></template>
▐ 界面/组件划分
引入 ElementPlus
1. 安装npm install element-plus --save2. 使用按需引入方式,安装如下插件npm install -D unplugin-vue-components unplugin-auto-import3. 配置 vite.config.tsimport { defineConfig } from 'vite'// 增加如下配置import AutoImport from 'unplugin-auto-import/vite'import Components from 'unplugin-vue-components/vite'import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'export default defineConfig({// ...plugins: [// ...AutoImport({resolvers: [ElementPlusResolver()],}),Components({resolvers: [ElementPlusResolver()],}),],})4. 使用组件在组件库(https://element-plus.gitee.io/zh-CN/component/button.html)寻找相应的组件,引入相关代
界面/组件划分
如“项目效果”部分所见,一共三个界面:LoginView(登录界面)/HomeView(首页界面)/BookListView(书籍管理界面)。
其中 HomeView 和 BookListView 界面需要展示在一个具有 header(页头) / aside(菜单) / main(主展示区)的布局(layout)中。
图片来自 element-plus container 布局容器
各个业务界面中需要抽取成的组件放置在 components 业务组件包下(eg. 新增书籍弹窗组件),个人会将这两类特征的功能抽取成组件:
高内聚原则:功能具有一定的复杂度和隔离度,将这些代码内聚起来进行开发,不腐化外层组件。
可复用原则:功能被其他多个组件引入复用,则需要将这些代码形成组件,避免重复代码散落,降低维护成本。
所以最终形成了如下的界面布局相关的代码包结构。
└── src├── layout 布局组件│ ├── Menu.vue 菜单组件│ └── index.vue 基础布局组件(header/aside/footer等)├── views 业务视图组件(路由跳转页面)│ ├── book│ │ └── BookListView.vue book视图组件│ ├── home│ │ └── HomeView.vue 首页视图组件│ └── login│ └── LoginView.vue 登录页视图组件└── components 业务组件└── book book组件└── AddBookDialog.vue 新增书籍弹窗组件
<template><el-container class="layout-container"><el-header><div class="toolbar"><el-dropdown><el-icon><setting /></el-icon><template #dropdown><el-dropdown-menu><el-dropdown-item @click="logout">退出</el-dropdown-item></el-dropdown-menu></template></el-dropdown><span>张三</span></div></el-header><el-container><el-aside width="200px"><el-scrollbar><!-- 菜单组件 --><Menu /></el-scrollbar></el-aside><el-main><!-- main 界面展示区 --><RouterView /></el-main></el-container></el-container><!-- <Menu /> --></template><!-- 使用 setup 语法糖,使用typescript --><script setup lang="ts">/* 引入router函数 */import { RouterView } from 'vue-router'/* 引入子组件 */import Menu from '@/layout/Menu.vue'import { Setting } from '@element-plus/icons-vue'import { logout as userLogout } from '@/utils/login'import { useUserStore } from '@/stores/user/user'function logout() {const token = useUserStore().getToken();if (token) {userLogout(token)}}</script><style scoped>... 布局组件相关 css</style>
组件间通信
<script setup lang="ts">/* 引入属性 */const props = defineProps<{bookValue: Book | undefined}>()/* 监听父组件传递数据,渲染表单 */watch(form.value,() => {if (props.bookValue) {form.value = props.bookValue}},{ deep: true, immediate: true })</script>
在“书籍管理组件”中进行属性定义赋值并通过引入组件进行传递。
<template><!-- 传递属性 --><AddBookDialog ... :bookValue="bookValue" /></template><script setup lang="ts">/* 定义属性并赋值 */const bookValue = ref<Book>()// 新增或者更新数据function addOrUpdateBook(book: Book | undefined) {if (!book) {bookValue.value = undefined} else {bookValue.value = book}}</script>
<script setup lang="ts">/* 引入方法 */const emits = defineEmits(['getBooks'])/* 新增或者编辑数据成功之后,刷新列表 */const addOrUpdateBookInner = async () => {await addOrUpdate(form.value).../* 使用方法 */emits('getBooks')}</script>
在“书籍管理组件”中进行方法定义并通过引入组件进行传递。
<template><!-- 传递方法 --><AddBookDialog ... @getBooks="getBooks" /></template><script setup lang="ts">/* 定义方法 */const getBooks = async () => {await getByRequest(queryParams.value).then(resp => {tableData.value = resp.data.dataListtotalCount.value = resp.data.totalCount})}</script>
▐ 路由配置
基本原理
main.ts 引入 VueRouter,之后=> 通过 <RouterLink to="${path}"> 配置指定元素的跳转路径 path=> 访问中心路由 ts 文件,根据 path 找到相关的组件以及 <RouterView> 区=> 将组件展示在相应的 <RouterView> 区
/* 引入 VueRouter */import router from './router'/* 使用 VueRouter 做路由 */app.use(router)
<!-- 菜单组件 --><template><el-menu default-active="1" class="el-menu-vertical-demo" router><!-- index 指定跳转路径 --><el-menu-item index="/"><el-icon><icon-menu /></el-icon><span>首页</span></el-menu-item><el-menu-item index="/bookList"><el-icon><document /></el-icon><span>书籍管理</span></el-menu-item></el-menu></template><script lang="ts" setup>import { Document, Menu as IconMenu } from '@element-plus/icons-vue'import { RouterLink } from 'vue-router'</script>
<RouterLink to="/bookList"><el-menu-item index="/bookList"><el-icon><document /></el-icon><span>书籍管理</span></el-menu-item></RouterLink>
import { createRouter, createWebHashHistory } from 'vue-router'const router = createRouter({history: createWebHashHistory(import.meta.env.BASE_URL),routes: [/* 登录页 path => LoginView.vue 展示在根组件的 <RouterView> 区 */{path: '/login',name: 'login',component: () => import('@/views/login/LoginView.vue')},/* 其他路由 */{path: '/',name: 'layout',/* 嵌套路由,当访问 children 中的 path 时,相关的组件展示在父组件(此处是 layout/index.vue)的 <RouterView> 区 */component: () => import('@/layout/index.vue'),children: [{path: '/',name: 'home',component: () => import('@/views/home/HomeView.vue'),}, {path: '/bookList',name: 'bookList',component: () => import('@/views/book/BookListView.vue')}]}]})export default router
<template><el-container class="layout-container">...<el-main><!-- 路由展示区 --><RouterView /></el-main>...</el-container></template>
路由守卫
作用:根据是否登录来控制用户对页面的访问,如果已登录,可进行目标页面访问;如果未登录,跳转到登录页
import router from "./index";import { useUserStore } from '@/stores/user/user'/* 白名单 */const whiteList = ['/login']/* userStore */// to - 要去的导航// from - 当前的导航// next - 函数,可指定去往任意的导航router.beforeEach((to, from, next) => {const userStore = useUserStore()if(userStore.getToken()) {/* 如果 token 存在 */if(to.path === '/login') {/* 跳转到首页 */next('/')} else {/* 继续跳转到 to */next()}} else {/* 如果 token 不存在 */if(whiteList.includes(to.path)) {next()} else {next('/login')}}})
/* main.ts 引入路由守卫 */import '@/router/permission'
▐ 状态存储
当一个数据需要在多个组件进行使用时,就可以考虑使用 Pinia 实现状态存储。如果没有在生成脚手架的时候选择 Pinia,需要先按照官网进行安装。
/* 引入状态管理器Pinia创建函数 */import { createPinia } from 'pinia'/* 使用 pinia 做状态管理 */app.use(createPinia())
在项目中会有多个组件使用用户登录信息,可以将该数据存储在 userStore 中。
首先定义业务存储,store/user/user.ts
import { defineStore } from 'pinia'import { TOKEN_DURATION_KEY, TOKEN_DURATION } from '@/utils/constants'// 命名规范:建议使用 useXxxStoreexport const useUserStore = defineStore('user', () => {// 在 Setup Store 中:// ref() 就是 state 属性// computed() 就是 getters// function() 就是 actionsfunction saveToken(token: string) {localStorage.setItem('token_vue_springboot', token)}function getToken(): string | null {return localStorage.getItem('token_vue_springboot')}function removeToken() {localStorage.removeItem('token_vue_springboot')}/* 登录时设置 */function saveCurrentTokenTime() {localStorage.setItem(TOKEN_DURATION_KEY, Date.now().toString())}function getTokenTime(): number | null {const tokenTime = localStorage.getItem(TOKEN_DURATION_KEY)if (tokenTime) {return Number(tokenTime)}return null}function removeTokenTime() {localStorage.removeItem(TOKEN_DURATION_KEY)}/* 判断登录 token 时间是否过期 */function tokenTimeIsExpire(): boolean {const tokenTime = getTokenTime();/* 如果不存在,则表示未登录,与过期等价,需要重新登录 */if (!tokenTime) {return true}return Date.now() - tokenTime > TOKEN_DURATION}/* 暴露方法 */return { saveToken, getToken, removeToken, saveCurrentTokenTime, getTokenTime, removeTokenTime, tokenTimeIsExpire }})
import { login as userLogin } from '@/api/login'import { useUserStore } from '@/stores/user/user'import router from '@/router'import type { Ref } from 'vue'export function login(loginForm: Ref<LoginUser>) {/* 使用 Pinia Store */const userStore = useUserStore()/* 定义函数 */async function loginTrue() {await userLogin(loginForm.value).then(resp => {/* 保存 token 到浏览器缓存 */userStore.saveToken(resp.data.token)/* 保存当前时间 tokenTime 到浏览器缓存 */userStore.saveCurrentTokenTime()/* 跳转到首页 */router.replace('/')})}/* 调用登录函数 */loginTrue()}
▐ 服务请求封装
请求后端服务使用 Axios,需要进行安装
Axios 客户端封装
/* 封装 axios 对象 */import axios from "axios";import { useUserStore } from '@/stores/user/user'import { logout } from '@/utils/login'/* 创建请求对象,填入全局配置参数 */const axiosService = axios.create({baseURL: import.meta.env.VITE_DOMAIN_URL_VUE_BOOT, // 根据环境读取不同的文件,具体见“环境配置”部分timeout: 3000})/* 请求前拦截器 */axiosService.interceptors.request.use(function (config) {// 在发送请求之前做些什么,eg.token校验;token传递到header,进行免登操作等/* 如果 token 过期 */const userStore = useUserStore()if (userStore.tokenTimeIsExpire()) {logout()return Promise.reject(new Error('token expired'))}return config;}, function (error) {// 对请求错误做些什么return Promise.reject(error);})/* 响应拦截器,拦截之后,业务方法基于该返回进行数据处理(此处可进行服务端返回数据的统一处理,例如,统一报错等) */axiosService.interceptors.response.use(function (response) {// 2xx 范围内的状态码都会触发该函数。// return response.datareturn response}, function (error) {// 超出 2xx 范围的状态码都会触发该函数。return Promise.reject(error);})export default axiosService
业务 api 封装
为了代码比较好治理,可以将不同的业务对服务端的调用封装为不同的 ts 文件,eg. login.ts 封装登录相关的调用
/* 封装 login 相关api */import axiosService from './baseRequest'// 登录export function login(data: {}) {return axiosService.request({url: 'user/login',method: 'post',data: data})}// 退出export function logout(data: {}) {return axiosService.request({url: 'user/logout',method: 'post',data})}
▐ 接口定义标准
ts 提供了接口能力,可用于实现编写代码的时候,就出现一些提示和错误标识;同时可以定义一些标准调用。
// 定义Book接口export interface Book {id: number,name: string}// 定义查询请求参数接口export interface BookRequest {/* 模糊查询 */name: string,pageNum: number,pageSize: number}
// 导入外部接口定义import type { Book,BookRequest } from '@/types/book'// 构造列表查询数据const queryParams = ref<BookRequest>({name: "",pageNum: 1,pageSize: 2})// 获取列表数据const totalCount = ref(0)const tableData = ref<Book[]>([])const getBooks = async () => {await getByRequest(queryParams.value).then(resp => {tableData.value = resp.data.dataListtotalCount.value = resp.data.totalCount})}getBooks()
▐ 环境配置
根路径├── .env.development 开发环境配置文件├── .env.production 生产环境配置文件└── package.json
# 开发环境配置VITE_NAME='开发环境'VITE_DOMAIN_URL_VUE_BOOT=http://localhost:8082
VITE_NAME='生产环境'VITE_DOMAIN_URL_VUE_BOOT=http://localhost:8082
{..."scripts": {"dev": "vite --mode development", // 开发启动 "npm run dev""build": "run-p type-check build-only", // 生产构建静态资源到 /dist 目录 "npm run build""preview": "vite preview", // 本地运行 /dist 目录文件,进行提前验证 "npm run preview""build-only": "vite build --mode production","type-check": "vue-tsc --noEmit"},...}
const axiosService = axios.create({baseURL: import.meta.env.VITE_DOMAIN_URL_VUE_BOOT,timeout: 3000})
build: {rollupOptions: {input: "./src/main.ts",output: {dir: "dist",/* 去除hash值 */entryFileNames: "assets/[name].js",}}
Vue3 后台管理系统(地址:https://www.bilibili.com/video/BV1pq4y1c7oy/)
TS 快速入口 + Vue3 快速入门(Vue部分需要结合 Vue3 官方文档看。地址:https://www.bilibili.com/video/BV1ra4y1H7ih/?vd_source=480e34eaa7e8621cbd83d5f3163fc061)
技术栈部分列出的各种官网
¤ 拓展阅读 ¤