一个服务端同学的Vue框架入门及实践

61dd4a13110cffc0fe518ef8d7c5072a.gif

做为服务端同学,接触前端代码较少,刚毕业的时候用过 jQuery + Bootstrap2/3,当时的感觉就是,容易上手,学习门槛相对较低,另外就是有一个非常成熟的 jQuery 插件库,在这里,几乎可以找到日常需要的大部分插件(这里的插件,不是简单的按钮/表单组件,而是像抽奖/大转盘/推箱子游戏这样的完整的功能块,这些避免了自己去写“困难”的 CSS),组合组合就可以搭出一个完善的页面,改改 Ajax 请求,就能实现一个小项目了。

前端技术日新月异,用 jQuery 的日渐变少,现在有两个流行的技术框架 Vue 和 React。个人选择学习了解相对简单的 Vue,目的是可以独立做一个简单的管理系统,后续可沉淀成个人顺手的一个管理系统的脚手架。

 jQuery插件库地址:https://www.jq22.com/

adc0b965cfc5ca2a8a7b4216c6ee2941.png

项目效果

用户首先到登录页,登录后,进入系统首页,点击左侧菜单,可以切换界面,包括一个书籍的增删改查操作。

c4a375eb5342b3f3a1483dbef3fc4cc0.pngc2a07be8679eacaeb63bb4ed05e56f0f.png

ac8450c4053bfb822482c43d1ffe048f.png

af79726ca4db3539eca8efd374c7a2fe.png

e2fa22983584b2c8104868178c8fde33.png

技术栈

后端: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/

5d9071800d1124ec2a7b41ad0dc416d8.png

搭建开发环境

本人使用 mac 版进行介绍

  安装 Node.js 和 npm

在官网下载 Node.js,一路点击安装,会安装 Node.js 和 npm。最后显示如下信息,成功。

官网地址:https://nodejs.org/en

Node.js v18.12.1 to /usr/local/bin/node
npm v8.19.2 to /usr/local/bin/npm

安装完毕之后,在控制台输入 node -v,返回版本号。

配置 npm 数据源(加速 npm install)

// 查看当前数据源
npm config get registry // 默认是https://registry.npmjs.org/
// 设置 npm 数据源为淘宝数据源
npm config set registry https://registry.npm.taobao.org

  安装开发工具 VisualStudioCode

在 官网下载 VisualStudioCode(简称 vsCode),解压安装。之后安装 Vue 插件 vol‍ar。

官网地址: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 // 增加 Pinia


Scaffolding project in /Users/jigang/Desktop/vue-study/vue-boot...
Done. Now run:
// 项目初始化之后,执行如下命令,可以安装相关的依赖,启动项目
  cd vue-boot
  npm install
  npm run dev

执行 npm run dev,之后看到如下信息,表示服务启动成功,浏览器访问 http://localhost:5177/ 即可看到界面。

VITE v4.0.4  ready in 306 ms
  ➜  Local:   http://localhost:5177/
  ➜  Network: use --host to expose
  ➜  press h to show help

到这里我们整个开发环境(包括脚手架生成的项目结构)就准备就绪了,后续将脚手架生成的项目引入 vsCode 就可以进入开发了。

7d15aaaaef13332b2a3fe70a1e939f9f.png

代码结构

如果是小型项目,用单一项目(前后端放一起)进行开发就好,这里使用常见的前后端分离的方式。

前端代码:https://github.com/zhaojigang/vue-boot(单页面应用)

后端代码:https://github.com/zhaojigang/vue-springboot

后端服务提供了简单的服务接口,通过访问 swagger 展示如下:

f7606b8785fd3c256c05b18c11367bdd.png

下面介绍前端代码。

基于上述的脚手架 + 编码最终形成的目录结构如下:

├── 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 --save


2. 使用按需引入方式,安装如下插件
npm install -D unplugin-vue-components unplugin-auto-import


3. 配置 vite.config.ts
import { 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)中。

cea0fa51894b2c30203e30d1527f40ed.png

图片来自 element-plus container 布局容器

各个业务界面中需要抽取成的组件放置在 components 业务组件包下(eg. 新增书籍弹窗组件),个人会将这两类特征的功能抽取成组件:

  1. 高内聚原则:功能具有一定的复杂度和隔离度,将这些代码内聚起来进行开发,不腐化外层组件。

  2. 可复用原则:功能被其他多个组件引入复用,则需要将这些代码形成组件,避免重复代码散落,降低维护成本。

所以最终形成了如下的界面布局相关的代码包结构。

└── 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 新增书籍弹窗组件

当访问登录链接的时候,LoginView.vue 界面组件展示在 App.vue 的 <RouterView /> 处;而当访问首页或者书籍管理链接的时候,相应的 HomeView.vue 和 BookListView.vue 需要展示在布局文件 layout/index.vue 的 main区(该区也会配置 <RouterView />),layout 的 aside 部分的菜单需要封装为单独的 Menu.vue 组件。以上的访问路径到界面的映射以及当项目中具有多个 <RouterView/> 时,怎么定位到合适的 <RouterView/> 组件展示区,可见“路由配置”部分。

来看下 layout/index.vue。界面使用 ElementPlus 的页面布局容器组件。

<template>
    <el-container class="layout-container">
        <el-header style="text-align: right; font-size: 12px">
            <div class="toolbar">
                <el-dropdown>
                    <el-icon style="margin-right: 8px; margin-top: 1px">
                        <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>
  • 组件间通信

组件进行拆分后,组件间需要进行数据通信。比如,书籍管理和新增书籍弹框组件就需要进行如下的通信。

24f62baa84d1d9f12c0e8e1bd7204e31.png

通信的数据有两种:一种是属性;一种是方法。

对于属性,例如行数据的传递:

在“新增弹窗组件”(承担新增和编辑功能)中进行传递属性的引入并使用:

<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.dataList
        totalCount.value = resp.data.totalCount
    })
}
</script>

▐  路由配置

  • 基本原理

如果没有在生成脚手架的时候选择 VueRouter,需要先按照官网进行安装。
main.ts 引入 VueRouter,之后
  => 通过 <RouterLink to="${path}"> 配置指定元素的跳转路径 path
  => 访问中心路由 ts 文件,根据 path 找到相关的组件以及 <RouterView> 区
  => 将组件展示在相应的 <RouterView> 区

整个路由的使用需要在 main.ts 引入路由器

/* 引入 VueRouter */
import router from './router'
/* 使用 VueRouter 做路由 */
app.use(router)

菜单组件 layout/Menu.vue(引入 ElementPlus 的菜单组件,菜单路径配置有简化写法,以下使用简化写法)

<!-- 菜单组件 -->
<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>

页面路由 router/index.ts

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

展示区 layout/index.vue

<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 中引入就可生效。

/* main.ts 引入路由守卫 */
import '@/router/permission'

  状态存储

当一个数据需要在多个组件进行使用时,就可以考虑使用 Pinia 实现状态存储。如果没有在生成脚手架的时候选择 Pinia,需要先按照官网进行安装。

Pinia 的使用需要在 main.ts 进行引入

/* 引入状态管理器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'
// 命名规范:建议使用 useXxxStore
export const useUserStore = defineStore('user', () => {
    // 在 Setup Store 中:
    // ref() 就是 state 属性
    // computed() 就是 getters
    // function() 就是 actions
    function 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 }
})

然后,使用 useUserStore 状态存储器

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 客户端封装

api/baseRequest.ts

/* 封装 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.data
    return 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 提供了接口能力,可用于实现编写代码的时候,就出现一些提示和错误标识;同时可以定义一些标准调用。

由于接口定义可能会在多个文件中进行调用,同时为了方便管理,建议接口写在 types 目录下的相关业务文件中。

Book 接口定义:types/book.ts

// 定义Book接口
export interface Book {
    id: number,
    name: string
}


// 定义查询请求参数接口
export interface BookRequest {
    /* 模糊查询 */
    name: string,
    pageNum: number,
    pageSize: number
}

使用接口:BookListView.vue

// 导入外部接口定义
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.dataList
        totalCount.value = resp.data.totalCount
    })
}
getBooks()

  环境配置

环境配置相关的代码包结构。

根路径
├── .env.development 开发环境配置文件
├── .env.production 生产环境配置文件
└── package.json

.env.development

# 开发环境配置  
VITE_NAME='开发环境'
VITE_DOMAIN_URL_VUE_BOOT=http://localhost:8082

.env.production

VITE_NAME='生产环境'
VITE_DOMAIN_URL_VUE_BOOT=http://localhost:8082

package.json 配置命令使用的模式

{
  ...
  "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"
  },
  ...
}

使用配置文件使用环境变量 baseRequest.ts

const axiosService = axios.create({
  baseURL: import.meta.env.VITE_DOMAIN_URL_VUE_BOOT, 
  timeout: 3000
})

8872b6ab5f4162ebef29a4da07920aa3.png

项目部署

本人通过以上这样的开发模式研发了多人实时玩法管理后台。开发完成后,就要去做应用部署,前后端分离项目的常规部署方式如下图所示。

19a8dd632efdaae0b4a2ee09026cabb1.png

前端资源(静态资源/css/js等)单独部署在CDN,服务端使用模板引擎(例如,Thymeleaf)来编写入口文件 index.html(代码与前端代码中编译后的 index.html 几乎相同,只是其中引入的 css 和 main.js 资源路径是资源的CDN 地址),这样通过服务端域名访问项目的时候,就会执行后端的 index.html,进而执行到 main.js(main.js 是 main.ts 的编译产物)。

这样的前后端分离方式是基于“index.html 代码几乎不怎么变动” 的前提下进行的,否则,后端代码需要因为这个文件的变动不断发布。而默认情况下,Vite 在构建文件的时候会生成带 hash 值的文件,例如,main.ts 被编译为 main-[hash].js,而 index.html 需要引入这个 js 文件,为了保证这个文件路径的不变性,需要在 vite.config.ts 文件中增加几行配置。

build: {
    rollupOptions: {
      input: "./src/main.ts",
      output: {
        dir: "dist",
        /* 去除hash值 */
        entryFileNames: "assets/[name].js",
      }
    }

0de674c961df1aad9eec624fc151e4f8.png

参考资料

  1. Vue3 后台管理系统(地址:https://www.bilibili.com/video/BV1pq4y1c7oy/)

  2. TS 快速入口 + Vue3 快速入门(Vue部分需要结合 Vue3 官方文档看。地址:https://www.bilibili.com/video/BV1ra4y1H7ih/?vd_source=480e34eaa7e8621cbd83d5f3163fc061)

  3. 技术栈部分列出的各种官网

d638ecea659859e42f3190c6d1e1367e.png

团队介绍

我们是大淘宝技术投放平台团队,负责双11、618、造物节等天猫淘宝大促和各类营销活动业务,覆盖几亿消费者、千万商家。这里是创造新商业的前沿阵地,同时充满着各种技术挑战 - 百万级峰值qps的页面投放、亿级规模的权益发放、T级大规模的数据供给等。我们致力于营销选品、投放、权益等技术体系的建设,打造一套灵活的业务解决方案,让业务创新更高效。

¤ 拓展阅读 ¤

3DXR技术 | 终端技术 | 音视频技术

服务端技术 | 技术质量 | 数据算法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/5299.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue集成tui.calendar日历组件

vue集成tui.calendar日历组件前言一、简介、效果图二、vue简单集成(集成js版本,没有使用官方的vue2版本)1.引包2.简单示例三、自定义功能1.需求分析、效果展示2.实现思路前言 vue2的集成在git上官方已经给出了demo这里就不贴代码了。本次主要是vue3集成 最近有个功能需要一个日…

重发布实验

基础配置&#xff1a; [r1]int l0 [r1-LoopBack0]ip add 1.1.1.1 24 [r1-LoopBack0]int g0/0/0 [r1-GigabitEthernet0/0/0]ip ad 192.168.12.1 24 [r1-GigabitEthernet0/0/0]int g0/0/1 [r1-GigabitEthernet0/0/1]ip add 192.168.123.1 24 [r1]ospf 1 router-id 1.1.1.1 [r1-o…

自学大数据第12天~Hbase

先留个问题~ERROR: KeeperErrorCode ConnectionLoss for /hbase/master 稍后解决 找到了问题的根因: 查看报错日志 事关tmp文件夹的配置,所以去找一下hbase配置文件中关于这个文件夹的配置项 我的策略是将这个配置项注销掉 然后启动hbase ,之后hmaster就成功启动了; 接着s…

熟练Redis之无处不在的锁

为了保证并发访问的正确性&#xff0c;Redis提供了两种方法,分别是加锁和原子操作 Redis加锁两个问题:一个是&#xff0c;如果加锁操作多&#xff0c;会降低系统的并发访问性能;第二个是&#xff0c;Redis客户端要加锁时&#xff0c;需要用到分布式锁&#xff0c;而分布式锁实…

Coremail奇安信发布2022中国企业邮箱安全性研究:应对ChatGPT带来的安全挑战

日前&#xff0c;广东盈世科技计算机有限公司与奇安信集团联合编写发布《2022中国企业邮箱安全性研究报告》。 报告数据显示&#xff1a;2022年&#xff0c;全国企业邮箱用户共收到各类钓鱼邮件约425.9亿封&#xff0c;相比2021年收到各类钓鱼邮件的342.2亿封增加了24.5%。 一…

华为OD机试用java实现 -【RSA 加密算法】

最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为od机试,独家整理 已参加机试人员的实战技巧本篇题解:RSA 加密算法 题目 RSA 加密…

【愚人节专场】Java实现定时发送小情话

首先&#xff0c;感谢大佬的帮助~附上大佬的博客以示尊敬https://blog.csdn.net/qq_38591577/article/details/128164308?spm1001.2014.3001.5502 功能实现&#xff1a; 在名为愚人节&#xff0c;实为告白/情人节的日子里&#xff0c;怎么样才能引起TA的关注呢&#xff1f;不…

49天精通Java,第21天,Java内部类,看看文心一言、ChatGPT怎么说

目录文心一言谈Java内部类ChatGPT谈Java内部类下面来聊聊哪吒的见解。一、为什么需要内部类&#xff1f;二、内部类分为四种三、成员内部类1、什么是成员内部类2、代码实例3、成员内部类进阶代码实例4、控制台显示5、外部类访问内部类四、局部内部类五、匿名内部类1、匿名内部类…

Dragonfly 最新正式版本 v2.0.9 已经发布!

作者&#xff1a;戚文博-蚂蚁集团 Dragonfly 最新正式版本 v2.0.9 已经发布&#xff01;感谢 Dragonfly 的贡献者们&#xff0c;同时也感谢默默支持 Dragonfly 项目的各个公有云团队。欢迎访问 d7y.io [ 1] 网站来了解详情&#xff0c;下面具体介绍 v2.0.9 版本带来了那些更新。…

【Redis】十大数据类型(下篇)

文章目录redis位图(bitmap) --- 底子还是string基本命令图示setbit key offset value setbit 键 偏移位 只能零或者1getbit key offset 查看获取字符串长度 strlen统计key中包含1的个数 bitcount keybitop 统计两个比特key是否都为1技术落地&#xff1a;打卡签到&#xff0c;频…

【C语言蓝桥杯每日一题】——等差数列

【C语言蓝桥杯每日一题】——等差数列&#x1f60e;前言&#x1f64c;等差数列&#x1f64c;解题思路分析&#xff1a;&#x1f60d;解题源代码分享&#xff1a;&#x1f60d;总结撒花&#x1f49e;&#x1f60e;博客昵称&#xff1a;博客小梦 &#x1f60a;最喜欢的座右铭&…

让ChatGPT帮我写shell脚本, 结局很感人

七问ChatGPT, 剑指shell脚本编写 step1: 初问step2: 再问step3: 三问step4: 四问step5: 五问step6: 问个derstep7: 解决问题step8: 小问一下关于ChatGPT思考昨天浏览一篇关于脚本的技术文章的时候, 偶然看见一篇文章中写道关于mysql备份的脚本. 但是这个脚本时基于本地的MySQL服…

Idea+maven+spring-cloud项目搭建系列--13 整合MyBatis-Plus多数据源dynamic-datasource

前言&#xff1a;对于同一个系统&#xff0c;不同的租户需要自己独立分隔的数据库&#xff08;每个数据库的表结构可以是相同的&#xff09;&#xff0c;同时也要支持跨数据源的查询&#xff1b;并且支持分布式事务&#xff0c;如果这里不使用分库分表插件&#xff0c;需要怎样…

使用dd复制将乌班图系统(Ubuntu22.04)完整迁移到新硬盘并扩容

我的折磨历程 开始的时候用乌班图的时候&#xff0c;不懂事&#xff0c;根目录太小了&#xff0c;后来就满了&#xff0c;就就感觉完全没法用&#xff0c;看着现在硬盘贼便宜&#xff0c;去狗东买了个新的硬盘。感觉挂载硬盘并不能解决我的问题&#xff0c;最后选择了保留系统数…

ython和PyTorch实现ChatGPT批量AI智能写作

怎么实现用chatgpt批量写作 ChatGPT是一种针对文本生成的自然语言处理工具&#xff0c;它可以用于生成大量的文本内容。但是&#xff0c;由于ChatGPT需要的计算资源较大&#xff0c;处理时间较长&#xff0c;因此在批量写作时需要考虑花费的时间和资源。 以下是一些步骤&…

又一个免费GPT-4工具 Cursor,程序员写代码将被颠覆

每天都被openai震撼到&#xff0c; 他们家被广为人知的产品是chatgpt&#xff0c;就是那个聊天工具。现在已经开始有越来越多的产品集成openai&#xff0c;比如微软的office&#xff0c;bing。现在又一个工具出现&#xff0c;一个叫Cursor的编辑器已经集成了openai的GPT-4&…

Spring系列(六) --- SpringBoot 与 Servlet 的比较及 Spring 读取配置文件的方式

SpringSpringBoot VS ServletSpring 读取配置文件的方式yml 和 properties 的区别SpringBoot VS Servlet Spring 读取配置文件的方式 1 Value 注解获取单个配置项 如在 yml 中定义一个 qq 音乐的 token; 然后输出, 如下: 2 针对对象的读取: ConfigurationProperties 在 yml 中…

YOLOv5添加辅助训练头

1. 介绍 思路 添加 Aux head 的主要原因是让网络中间层学到更多信息,有更丰富的梯度信息帮助训练。这里要注意,好的梯度信息能够让相同参数量的网络学的更好。 作者原文为: By letting the shallower auxiliary head directly learn the information that lead head has l…

【C#基础】泛型的概念?有什么例子?在游戏中有什么可以使用的地方?

概念 让chatGpt来为我们讲解。 在C#中&#xff0c;泛型是一种允许开发人员编写可重用代码&#xff0c;可以处理多种数据类型的特性。 使用泛型&#xff0c;可以创建类、方法、接口和委托这种不属于任何特定数据的类型&#xff0c;但可以处理满足某些约束条件的任何数据类型。…

手机银行评测系列:北京银行“京彩生活”7.0从用户视角出发,实现沉浸式体验重塑

易观&#xff1a;2023年3月28日&#xff0c;北京银行发布“京彩生活”APP 7.0版本&#xff0c;从旅程再造、特色金融、场景生态、平台联动、协同经营、体验管理和安全守护七大方面全面升级&#xff0c;从用户视角出发&#xff0c;重塑用户旅程&#xff0c;简化操作流程&#xf…