阅读 6926
Vite+Vue3+TS搭建一个后台管理系统架子

Vite+Vue3+TS搭建一个后台管理系统架子

这是我参与更文挑战的第2天

前言

  • vite 作为vue祖师爷尤大的又一神作。值得我们使用
  • 这篇文章主要通过 vite + vue3 + element-plus + ts搭建一个后台管理系统架子

1、安装

  • 通过vite脚手架搭建我们第一个项目
yarn create @vitejs/app my-vue-app(自己项目的名称) --template vue-ts
复制代码
  • 这串命令可以让我们生成一个基于TS的项目
  • 目录结构如下

image.png

2、集成vue-router@next

  • 安装支持vue3语法的 vue-router
    yarn add vue-router@next
复制代码
  • 接下来我们在src目录下创建routes文件夹,并在routes文件夹下创建index.ts文件,写入一段测试路由信息
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
    {
        path: '/login',
        component: () => import("../views/login/index.vue")
    },
    {
        path: '/',
        component: () => import("../views/home/index.vue")
    },
]

const router = createRouter({
    history: createWebHistory(),
    routes
})

export default router
复制代码
  • 现在我们在路由文件中添加了登录组件和首页组件的路由
  • 然后我们去main.ts中把导出的路由挂载到app中,并修改app.vue
    //main.ts
    import { createApp } from 'vue'
    //引入我们导出的路由
    import router from './routes'
    import App from './App.vue'

    createApp(App)
    // 通过use的方法把路由信息挂载到app中
        .use(router)
        .mount('#app')
        
    // app.vue
    <template>
      <router-view />
    </template>

复制代码
  • 做完这些就可以写login.vue和home.vue的测试组件了
// login.vue
    <template>
        <div>login</div>
    </template>
 // home.vue
    <template>
        <div>home</div>
    </template>
复制代码

image.png

  • 我们启动项目访问localhost:3000/login看到login字段,就证明路由已经集成成功了,我们直接输入localhost:3000就可以进入到首页了

image.png

3、集成vuex@next

  • 安装支持vue3语法的 vuex
    yarn add vuex@next
复制代码
  • 接下来我们在src目录下创建store文件夹,并在store文件夹下创建index.ts文件,写入一段测试测试数据
  • 举个🌰,我们实现一个累加的功能,在登录组件点击自增,在home组件中展示
import { createStore } from 'vuex'

const store = createStore({
    state() {
        return {
            count: 0
        }
    },
    mutations: {
        // 累加功能
        increment(state) {
            state.count++
        }
    }
})

export default store
复制代码

image.png

  • 这个时候我们会看到TS类型报错,这样修改即可
  import { createStore } from 'vuex'

    //定义一个state的接口
    export interface State {
        count: number
    }

    const store = createStore<State>({
        state() {
            return {
                count: 0
            }
        },
        mutations: {
            // 累加功能
            increment(state) {
                state.count++
            }
        }
    })
    export default store
复制代码
  • 接下来我们修改该main.ts
    import { createApp } from 'vue'
    //引入我们导出的路由
    import router from './routes'
    // 引入我们导出的vuex
    import store from './store'
    import App from './App.vue'

    createApp(App)
    // 通过use的方法把路由信息挂载到app中
        .use(router)
    // 通过use的方法把vuex信息挂载到vue中
        .use(store)
        .mount('#app')
复制代码
  • 接下来我们在home组件中获取vuex中的内容
    <template>
        <div>vuex中的count:{{count}}</div>
    </template>
    <script setup>
    import { useStore } from 'vuex'
    const store = useStore()
    const count = store.state.count
    </script>
复制代码
  • 接下来,我们在login组件中设置自增操作
    <template>
      <div>
        login
        <button @click="handleClickIncrement">累加</button>
        <router-link to="/">去首页</router-link>
      </div>
    </template>
    <script setup>
    import { useStore } from "vuex";
    const store = useStore();
    function handleClickIncrement() {
      store.commit("increment");
    }
    </script>
复制代码
  • 最后我们启动项目,👁看看效果图是啥样的

image.png

image.png

image.png

  • 首先我们进入首页 count是0,然后我们进入login页面点击自增,再点击去首页,那么首页中的count就会实现累加的功能,由0变为1,实现了vuex的数据共享

4、集成element-plus

  • 安装
    yarn add element-plus
复制代码
  • 引入方式 - 完整引入
import { createApp } from 'vue'
//引入我们导出的路由
import router from './routes'
// 引入我们导出的vuex
import store from './store'
// elementPlus完整引入
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';

import App from './App.vue'
createApp(App)
// 通过use的方法把路由信息挂载到app中
    .use(router)
// 通过use的方法把vuex信息挂载到vue中
    .use(store)
    .use(ElementPlus)
    .mount('#app')
复制代码
  • 修改login的代码
    <el-button type="primary" @click="handleClickIncrement">累加</el-button>
复制代码

image.png

  • 这个时候element-plus就已经引入成功了
  • 引入方式 - 按需加载
  • 因为我们的项目是基于vite搭建的,不能使用阿里基于webpackplugin-babel-import这个按需加载包,奈何社区总有轮子哥vite-plugin-babel-import一个基于vite的按需加载包
  • 安装
 yarn add vite-plugin-babel-import -D
复制代码
  • 修改vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// 按需加载包
import vitePluginBabelImport from 'vite-plugin-babel-import'

export default defineConfig({
  plugins: [vue(),
  vitePluginBabelImport([
    {
      libraryName: 'element-plus',
      libraryDirectory: 'es',
      ignoreStyles: [],
      style(name) {
        return `element-plus/lib/theme-chalk/${name}.css`;
      },
    }
  ])]
})
复制代码
  • 按需加载已经配置完成,接下来我们就把main.ts中的全局引入方式相关代码注释掉
    // import ElementPlus from 'element-plus';
    // import 'element-plus/lib/theme-chalk/index.css';
    // .use(ElementPlus)
复制代码
  • 接下来我们仍旧修改login组件中的代码
    <template>
      <div>
        login
        <el-button type="primary" @click="handleClickIncrement">累加</el-button>
        <router-link to="/">去首页</router-link>
      </div>
    </template>
    <script setup>
    import { ElButton } from 'element-plus'
    import { useStore } from "vuex";
    const store = useStore();
    function handleClickIncrement() {
      store.commit("increment");
    }
    </script>
复制代码
  • 这个时候我们再看效果图,依旧是没问题的

image.png

5、vite相关配置

  • 支持vue-jsx语法
    // 安装
    yarn add vite-plugin-babel-import -D
    //引包
    // vuejsx支持
    import vueJsx from "@vitejs/plugin-vue-jsx"
    // 添加到plugin中
    vueJsx()
复制代码
  • 支持别名
 // 安装 path 和 @types/node, @types/node是为了防止在vite.config.js中使用__dirname报错
 yarn add path
 yarn add @types/node -D
 
    import { defineConfig } from "vite";
    import path from 'path'
    // vue支持
    import vue from "@vitejs/plugin-vue";
    // vuejsx支持
    import vueJsx from "@vitejs/plugin-vue-jsx";

    // 按需加载包
    import vitePluginBabelImport from "vite-plugin-babel-import";

    const resolve = dir => path.resolve(__dirname, dir)
    export default defineConfig({
      resolve: {
        alias: {
          "@": resolve("./src")
        },
      },
      plugins: [
        vue(),
        vueJsx(),
        vitePluginBabelImport([
          {
            libraryName: "element-plus",
            libraryDirectory: "es",
            ignoreStyles: [],
            style(name) {
              return `element-plus/lib/theme-chalk/${name}.css`;
            },
          },
        ]),
      ],
    });
复制代码

这样我们就可以在项目中使用@符号了 image.png

  • server相关配置
    import { defineConfig } from "vite";
    import path from "path";
    // vue支持
    import vue from "@vitejs/plugin-vue";
    // vuejsx支持
    import vueJsx from "@vitejs/plugin-vue-jsx";

    // 按需加载包
    import vitePluginBabelImport from "vite-plugin-babel-import";

    const resolve = (dir) => path.resolve(__dirname, dir);
    export default defineConfig({
      resolve: {
        alias: {
          "@": resolve("./src"),
        },
      },
      server: {
        port: 8080, //项目启动端口
        open: true, //项目启动时是否自动打开浏览器
        proxy: {
          // 代理
          "/foo": "http://localhost:4567/", //代理方式 /foo --> http://localhost:4567/foo
          // 选项写法
          "/api": {
            target: "http://123.456.com", //代理方式 /api --> http://123.456.com
            changeOrigin: true,
            rewrite: (path) => path.replace(/^\/api/, ""),
          },
        },
      },
      build: {
        outDir: resolve("./dist1"), // 打包输出目录, 默认dist
      },
      plugins: [
        vue(),
        vueJsx(),
        vitePluginBabelImport([
          {
            libraryName: "element-plus",
            libraryDirectory: "es",
            ignoreStyles: [],
            style(name) {
              return `element-plus/lib/theme-chalk/${name}.css`;
            },
          },
        ]),
      ],
    });
复制代码

6、css预编译(less)

  • 安装
    yarn add less -D
复制代码
  • 如果是用的是单文件组件,可以通过 <style lang="sass">(或其他预处理器)自动开启。
  • Vite 为 Sass 和 Less 改进了 @import 解析,以保证 Vite 别名也能被使用。另外,url() 中的相对路径引用的,与根文件不同目录中的 Sass/Less 文件会自动变基以保证正确性。

7、Glob导入

  • 现在我们来看一个问题,当页面达到了一定的规模,routes/index.ts中的页面路由会非常的多,这个时候我们就会根据不同的模块去拆分成不同的文件存放对应的路由信息,然后再统一引入index.ts中,这个时候我们就在想能不能通过一种方式,去扫描某一个文件夹下的指定规则的文件,按照这种规则把当前文件夹下的文件动态的引入到当前组件,实现一个自动挂载的操作。如果大家对node中的fs模块熟悉的话,就可以实现这样的功能(常见的栗子有nextjs、egg、nuxt这些基于约定由于配置的开发)。但我们前端的工程化项目最终是要打包在浏览器中的,没法利用node的fs模块,vite是基于原生浏览器模块开发的。Vite 支持使用特殊的 import.meta.glob 函数从文件系统导入多个模块, 我们可以利用这一特性实现,前端项目动态引包的功能。
  • 首先我们改造一下页面结构

image.png

image.png

  • 这个时候我们对路由进行了拆分

-接下老我们把modules下所有TS后缀名的文件动态的引入routes/index.ts中

// 加载当前文件夹下modules文件夹下所有的ts结尾的文件
const  modules = import.meta.glob("./modules/*.ts")
// 遍历 modules 对象的 key 值来访问相应的模块
for (const path in modules) {
   const mod =  await modules[path]()
   console.log(mod);
}
复制代码
  • 改造好后启动页面,会发现控制台报错

image.png

  • 原因是 vite-plugin-babel-import这个插件2.0.5版本不支持顶级await的写法

解决方案就是把 vite-plugin-babel-import 插件版本降级到2.0.2

image.png -这时vite.config.ts又报错了 image.png

  • 我们需要把 ignoreStyles 参数删掉就好了

image.png

  • 再次启动项目,我们打开控制台可以看到信息routes/index.ts的打印信息了

image.png

image.png

  • 我们展开module会看到default数组中存放的就是我们module文件夹下login和home TS文件中导出的数据了,

既然能拿到数据,那么我们就来改造下routes/index.ts吧

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
// 加载当前文件夹下modules文件夹下所有的ts结尾的文件
const  modules = import.meta.glob("./modules/*.ts")

const routes: RouteRecordRaw[] = []

// 遍历 modules 对象的 key 值来访问相应的模块
for (const path in modules) {
   const mod =  await modules[path]()
   // 数据添加到routes中
  routes.push(...mod.default)
}

const router = createRouter({
    history: createWebHistory(),
    routes
})

export default router
复制代码

-这个时候我们再次访问 login 和首页都是没问题的

image.png

image.png

  • 到这个时候vite+vue基本架子就搭好了

8、后台管理系统-登录

  • 登录的相关操作比较简单,我就直接贴代码了
<template>
  <el-form
    status-icon
    label-width="100px"
    class="login-form"
  >
    <el-form-item label="用户名">
      <el-input
        v-model="form.name"
      ></el-input>
    </el-form-item>
    <el-form-item label="密码">
      <el-input
        type="password"
        v-model="form.pwd"
      ></el-input>
    </el-form-item>
    <el-form-item>
      <el-button class="block" type="primary" @click="submitForm"
        >提交</el-button
      >
    </el-form-item>
  </el-form>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
interface Form {
  name: string,
  pwd: string
}
const form = reactive<Form>({
  name: "",
  pwd: ""
});
const router = useRouter()
const route = useRoute()
function submitForm() {
  const { name, pwd } = form
  if(name && pwd) {
    // 定时器模拟登录
  setTimeout(()=>{
      // 模拟信息存储到本地
      sessionStorage.setItem("user",JSON.stringify({name,pwd}))
      // 跳转到上一次访问的页面 如果用户直接进入登录页面,那么就跳转到 '/'首页
      const path: string= (route.query as {url?: string}).url || "/"
      router.push(path)
  })
  }else {
    ElMessage.error("用户名密码不能🙅‍♀️为空")
  }
}
</script>
<style lang="less">
.login-form{
width: 500px;
margin: 200px auto;
}
.block{
  width:100%
}
</style>

复制代码

image.png

  • 页面就这样的,还挺好看的,哈哈

9、首页改造

    <template>
  <div>
    <el-row>
      <el-col :span="24"
        ><div class="layout-head pdlr28">{{ userName || "--" }}</div></el-col
      >
    </el-row>
    <el-row>
      <el-col :span="4">
        <el-menu
          :uniqueOpened="true"
          default-active="2"
          class="menu-wrap"
          background-color="#545c64"
          text-color="#fff"
          active-text-color="#ffd04b"
        >
          <el-submenu index="1">
            <template #title>
              <i class="el-icon-location"></i>
              <span>导航一</span>
            </template>
            <el-menu-item-group>
              <template #title>分组一</template>
              <el-menu-item index="1-1">选项1</el-menu-item>
              <el-menu-item index="1-2">选项2</el-menu-item>
            </el-menu-item-group>
            <el-menu-item-group title="分组2">
              <el-menu-item index="1-3">选项3</el-menu-item>
            </el-menu-item-group>
            <el-submenu index="1-4">
              <template #title>选项4</template>
              <el-menu-item index="1-4-1">选项1</el-menu-item>
            </el-submenu>
          </el-submenu>
          <el-menu-item index="2">
            <i class="el-icon-menu"></i>
            <template #title>导航二</template>
          </el-menu-item>
          <el-menu-item index="3" disabled>
            <i class="el-icon-document"></i>
            <template #title>导航三</template>
          </el-menu-item>
          <el-menu-item index="4">
            <i class="el-icon-setting"></i>
            <template #title>导航四</template>
          </el-menu-item>
          <el-submenu index="5">
            <template #title>
              <i class="el-icon-location"></i>
              <span>导航一</span>
            </template>
            <el-menu-item-group>
              <template #title>分组一</template>
              <el-menu-item index="5-1">选项1</el-menu-item>
              <el-menu-item index="5-2">选项2</el-menu-item>
            </el-menu-item-group>
            <el-menu-item-group title="分组2">
              <el-menu-item index="5-3">选项3</el-menu-item>
            </el-menu-item-group>
          </el-submenu>
        </el-menu>
      </el-col>
      <el-col :span="20" class="pdl28">
        <el-breadcrumb separator="/" class="pdt28 pdb28">
          <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
          <el-breadcrumb-item><a href="/">活动管理</a></el-breadcrumb-item>
          <el-breadcrumb-item>活动列表</el-breadcrumb-item>
          <el-breadcrumb-item>活动详情</el-breadcrumb-item>
        </el-breadcrumb>
        <div class="com">
          <el-table :data="tableData" style="width: 100%">
            <el-table-column prop="date" label="日期" width="180">
            </el-table-column>
            <el-table-column prop="name" label="姓名" width="180">
            </el-table-column>
            <el-table-column prop="address" label="地址"> </el-table-column>
          </el-table>
        </div>
      </el-col>
    </el-row>
  </div>
</template>
<script  lang="ts" setup>
import { reactive } from "vue";
interface User {
  name: string;
  pwd: string;
}

interface Table {
  date: string;
  name: string;
  address: string;
}

const user: string | null = sessionStorage.getItem("user");
const userName: string | null = user && (JSON.parse(user) as User).name;
const tableData = reactive<Table[]>([
  {
    date: "2016-05-02",
    name: "王小虎",
    address: "上海市普陀区金沙江路 1518 弄",
  },
  {
    date: "2016-05-04",
    name: "王小虎",
    address: "上海市普陀区金沙江路 1517 弄",
  },
  {
    date: "2016-05-01",
    name: "王小虎",
    address: "上海市普陀区金沙江路 1519 弄",
  },
  {
    date: "2016-05-03",
    name: "王小虎",
    address: "上海市普陀区金沙江路 1516 弄",
  },
]);
</script>
<style lang="less" scoped>
.layout-head {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: flex-end;
  height: 56px;
  background-color: #99a9bf;
}
.text-center {
  text-align: center;
}
.menu-wrap {
  height: calc(100vh - 56px);
}
.pdlr28 {
  padding-left: 28px;
  padding-right: 28px;
}
.pdt28 {
  padding-top: 28px;
}

.pdr28 {
  padding-right: 28px;
}
.pdb28 {
  padding-bottom: 28px;
}
.pdl28 {
  padding-left: 28px;
}
</style>
复制代码

10、 首页效果图

image.png

11、路由拦截

  • 这时我们需要对路由进行拦截,没登录的用户进入首页会重定向到登录页
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
// 加载当前文件夹下modules文件夹下所有的ts结尾的文件
const  modules = import.meta.glob("./modules/*.ts")

const routes: RouteRecordRaw[] = []

// 遍历 modules 对象的 key 值来访问相应的模块
for (const path in modules) {
   const mod =  await modules[path]()
//    数据添加到routes中
  routes.push(...mod.default)
}

const router = createRouter({
    history: createWebHistory(),
    routes
})

router.beforeEach((to, format, next) => {
    // 判断路由是否要去登录页面
    // 是的话 清除sessionStorage存储 放行
    // 否则进行验证,用户是否已经登录
        // 有登陆记录的话放行
        // 否则清除sessionStorage存储,并重定向到login
    if(to.path === '/login') {
        //清除sessionStorage存储
        sessionStorage.clear()
        next()
    }else {
       let user = sessionStorage.getItem('user')
        if(user) {
            next()
        }else {
            //清除sessionStorage存储
            sessionStorage.clear()
            // 重定向到login
            next("/login?url="+to.path)
        }
    }
})

export default router
复制代码

12、完整代码

写在最后

  • 当你看到这里的时候,首先你是个很有毅力的人,这篇文章没有插图,都是干货,从头看到尾的话给自己点个赞吧
  • 这篇文章主要搭建了一个vite + vue3 + ts的架子,从布局到样式再到接口转发,基本功能都已经搭建完毕了
  • 欢迎大家评论,指出不完善的地方
文章分类
前端
文章标签