v-blog 前端框架搭建

158 阅读5分钟

前端框架搭建

项目初始化

生成项目模板

这里我们直接使用 Vue CLI 初始化我们的骨架, 然后在此基础上进行修改

npm init vue@latest
Need to install the following packages:
  create-vue@3.3.4
Ok to proceed? (y) y

Vue.js - The Progressive JavaScript Framework

✔ Project name: … ui
✔ Add TypeScript? … NoAdd JSX Support? … NoAdd Vue Router for Single Page Application development? … Yes
✔ Add Pinia for state management? … Yes
✔ Add Vitest for Unit Testing? … NoAdd Cypress for both Unit and End-to-End testing? … NoAdd ESLint for code quality? … Yes
✔ Add Prettier for code formatting? …  Yes

Scaffolding project in /Users/yumaojun/Workspace/Golang/go-course/extra/vblog...

Done. Now run:

  cd vblog
  npm install
  npm run lint
  npm run dev

清理模板页面

在做Home 页面之前,先清理掉脚手架为我们生成的页面

  1. 清理 App.vue , 只保留 router 视图部分, 其他部分删除掉
<script setup>
import { RouterView } from "vue-router";
</script>

<template>
  <RouterView />
</template>

<style scoped>
</style>
  1. Views 只保留了 HomeView, 内容留白:
<script setup></script>

<template>
  <main>Home</main>
</template>
  1. 删除其他页面和无用的组件:
  • AboutView 删除
  • components 目录下的所有组件
  • router/index 里的 routes 清理掉 AboutView 路由
  1. 清理样式

base.css 设置全局样式

* {
  box-sizing: border-box;
}

html,
body {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  font-size: 14px;
  background-color: var(--color-bg-1);
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
}

#app {
  width: 100%;
  height: 100%;
}

main.css 只保留基础样式

@import "./base.css";

安装UI插件

接下来就需要编写我们的业务页面, 这我们选择的UI组件为: Arco Design

安装 Arco Design

# npm
npm install --save-dev @arco-design/web-vue
# yarn
yarn add --dev @arco-design/web-vue

main.js 内添加

// 加载 UI 组件
import ArcoVue from "@arco-design/web-vue";
import "@arco-design/web-vue/dist/arco.css";

// 引入 ICON 组件
import ArcoVueIcon from "@arco-design/web-vue/es/icon";

app.use(ArcoVue);
app.use(ArcoVueIcon);

验证插件

修改HomeView, 引入buttom UI组件进行测试

<template>
  <main>
    <a-space>
      <a-button type="primary">Primary</a-button>
      <a-button>Secondary</a-button>
      <a-button type="dashed">Dashed</a-button>
      <a-button type="outline">Outline</a-button>
      <a-button type="text">Text</a-button>
    </a-space>
  </main>
  <IconDoubleUp />
</template>

image.png

错误页面

这里我们需要补充2种异常页面:

  • 404页面: 当用户输入的URL并没有匹配页面时
  • 403页面: 当用户未登陆就访问管理页面时

我们使用HTTP 状态码组件进行封装

我们在 Views 文件夹下创建 errors 文件夹专门存放错误页面

404页面

errors/NotFound.vue:

<template>
  <div class="content">
    <a-result class="result" status="404" :subtitle="'not found'">
      <template #extra>
        <a-button key="back" type="primary" @click="back"> 返回主页 </a-button>
      </template>
    </a-result>
  </div>
</template>

<script setup>
import { useRouter } from "vue-router";

const router = useRouter();
const back = () => {
  // 通过push 跳转到 home页面
  router.push({ name: "home" });
};
</script>

<style scoped lang="less">
.content {
  position: absolute;
  top: 50%;
  left: 50%;
  margin-left: -95px;
  margin-top: -121px;
  text-align: center;
}
</style>

这里我们使用到了less, 一种css编译器(css扩展), 因此需要安装less的编译器

npm install --save-dev less

最后我们在路由上补充上404路由

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      name: "home",
      component: HomeView,
    },
    {
      path: "/:pathMatch(.*)*",
      name: "notFound",
      component: () => import("@/views/errors/NotFound.vue"),
    },
  ],
});

403页面

errors/PermissionDeny.vue

<template>
  <div class="content">
    <a-result
      class="result"
      status="403"
      :subtitle="'你无权限访问该页面, 请登陆后重试'"
    >
      <template #extra>
        <a-button key="back" type="primary" @click="back"> 返回主页 </a-button>
      </template>
    </a-result>
  </div>
</template>

<script setup>
import { useRouter } from "vue-router";

const router = useRouter();
const back = () => {
  // warning: Go to the node that has the permission
  router.push({ name: "home" });
};
</script>

<style scoped lang="less">
.content {
  position: absolute;
  top: 50%;
  left: 50%;
  margin-left: -95px;
  margin-top: -121px;
  text-align: center;
}
</style>

补充路由:

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      name: "home",
      component: HomeView,
    },
    {
      path: "/errors/403",
      name: "PermissionDeny",
      component: () => import("@/views/errors/PermissionDeny.vue"),
    },
    {
      path: "/:pathMatch(.*)*",
      name: "NotFound",
      component: () => import("@/views/errors/NotFound.vue"),
    },
  ],
});

image.png

Layout布局

界面分为前台和管理后台

博客前台:

博客管理后台:

博客前台

因为前台与后台布局样式不一样, 因此分别使用独立布局模版.

Layout

src 文件夹下新建 layout 文件夹 前台布局模板: FrontendLayout.vue

<script setup>
import { RouterView } from "vue-router";
</script>

<template>
  <div>
    <!-- 顶部导航 -->
    <div class="header">
      <div class="logo">我的博客</div>
      <div class="right-header">
        <div>
          <!-- 登录后台进行博客管理 -->
          <a-button size="mini" type="text">后台管理</a-button>
        </div>
      </div>
    </div>
    <!-- 显示博客列表 -->
    <div class="content">
      <RouterView />
    </div>
  </div>
</template>

<style scoped>
.header {
  display: flex;
  align-content: center;
  justify-content: flex-start;
  align-items: center;
  border-bottom: 1px solid rgb(229, 230, 235);
  height: 45px;
}

.logo {
  margin-left: 8px;
  font-size: 14px;
  font-weight: 500;
}

.right-header {
  margin-left: auto;
}

.content {
  margin: 20px;
  display: flex;
  align-content: center;
  justify-content: center;
  align-items: center;
}
</style>

blog占位页

Views 文件夹内新建 frontend 文件夹
补充一个前台展位页面: BlogView.vue

<script setup></script>

<template>
  <main>
    博客页面
  </main>
</template>

Blog路由

    {
      path: "/",
      name: "home",
      redirect: "/frontend",
    },
    {
      path: "/frontend",
      name: "frontend",
      component: FrontendLayout,
      children: [
        {
          path: "",
          name: "frontend",
          component: BlogView,
        },
      ],
    },

image.png

博客后台

Loayout

博客后台使用的布局模板: BackendLayout.vue

这里我们需要使用到侧边栏导航:菜单Menu

<script setup>
import { RouterView, useRouter } from "vue-router";

const router = useRouter();
const clickMenu = (key) => {
  router.push(key);
};
</script>

<template>
  <div>
    <div class="header">
      <div class="logo">我的博客</div>
      <div class="right-header">
        <div>
          <a-button size="mini" type="text">前台</a-button>
        </div>
      </div>
    </div>
    <div class="main">
      <div class="sidebar">
        <a-menu
          :style="{ width: '200px', height: '100%' }"
          :default-open-keys="['0']"
          :default-selected-keys="['/backend/blogs']"
          show-collapse-button
          breakpoint="xl"
          @menu-item-click="clickMenu"
        >
          <a-sub-menu key="0">
            <template #icon><icon-apps></icon-apps></template>
            <template #title>文章管理</template>
            <a-menu-item key="/backend/blogs">文章列表</a-menu-item>
            <a-menu-item key="/backend/tags">标签管理</a-menu-item>
          </a-sub-menu>
        </a-menu>
      </div>
      <div class="content">
        <RouterView />
      </div>
    </div>
  </div>
</template>

<style scoped>
.header {
  display: flex;
  align-content: center;
  justify-content: flex-start;
  align-items: center;
  border-bottom: 1px solid rgb(229, 230, 235);
  height: 45px;
}

.logo {
  margin-left: 8px;
  font-size: 14px;
  font-weight: 500;
}

.right-header {
  margin-left: auto;
}

.main {
  display: flex;
  align-content: center;
  justify-content: flex-start;
  align-items: flex-start;
  height: calc(100vh - 45px);
}

.sidebar {
  height: 100%;
  border-right: 1px solid rgb(229, 230, 235);
}

.content {
  margin: 8px;
  width: 100%;
  height: 100%;
}
</style>

blog占位页

Views 文件夹内新建 backend 文件夹
添加 BlogList.vue

<script setup></script>

<template>
  <main>博客列表</main>
</template>

添加 TagList.vue

<script setup></script>

<template>
  <main>标签列表</main>
</template>

blog路由

    {
      path: "/backend",
      name: "backend",
      component: BackendLayout,
      children: [
        {
          path: "blogs",
          name: "BlogList",
          component: BlogList,
        },
        {
          path: "tags",
          name: "TagList",
          component: TagList,
        },
      ],
    },

image.png

切换到前台

无效任务 后台可以直接切换到前台:

layout/BackendLayout.vue

const jumpToFrontend = () => {
  router.push("/frontend");
};

// 前台 按钮添加点击事件
<a-button size="mini" type="text" @click="jumpToFrontend">前台</a-button>

切换到后台

但是前台切换到后台,是需要认证的, 因此需要先做登录页面

登录页面

这里我们将要使用到: 表单Views 文件夹内新建 login 文件夹
添加 LoginPage.vue

<template>
  <div class="login-form">
    <a-form
      ref="loginForm"
      :model="form"
      :style="{ width: '400px', height: '100%', justifyContent: 'center' }"
      @submit="handleSubmit"
    >
      <a-form-item>
        <div class="title">登录博客管理后台</div>
      </a-form-item>
      <a-form-item
        field="username"
        label=""
        :rules="[{ required: true, message: '请输入用户名' }]"
        hide-asterisk
      >
        <a-input v-model="form.username" placeholder="请输入用户名">
          <template #prefix>
            <icon-user />
          </template>
        </a-input>
      </a-form-item>
      <a-form-item
        field="password"
        label=""
        :rules="[{ required: true, message: '请输入密码' }]"
        hide-asterisk
      >
        <a-input
          type="password"
          v-model="form.password"
          placeholder="请输入密码"
        >
          <template #prefix>
            <icon-lock />
          </template>
        </a-input>
      </a-form-item>
      <a-form-item>
        <a-button style="width: 100%" type="primary" html-type="submit"
          >登录</a-button
        >
      </a-form-item>
    </a-form>
  </div>
</template>

<script setup>
import { reactive } from "vue";
import { Message } from "@arco-design/web-vue";
import { useRouter } from "vue-router";

const router = useRouter();

const form = reactive({
  username: "",
  password: ""
});
const handleSubmit = (data) => {
  if (data.errors === undefined) {
    let form = data.values;
    if (form.username === "admin" && form.password === "123456") {
      // 保存登录状态
      localStorage.setItem("username", form.username);
      localStorage.setItem("password", form.password);

      // 登录成功直接跳转到后台页, 如果URL页面带有重定向参数, 则直接路由到重定向的页面
      const { redirect, ...othersQuery } = router.currentRoute.value.query;
      router.push({
        name: redirect || "BlogList",
        query: {
          ...othersQuery,
        },
      });
    } else {
      Message.error("用户名或者密码不正确");
    }
  }
};
</script>

<style scoped>
.login-form {
  height: 100%;
  display: flex;
  align-content: center;
  justify-content: center;
  align-items: center;
}

.title {
  display: flex;
  justify-content: center;
  align-items: center;
  align-content: center;
  width: 100%;
  font-weight: 500;
}
</style>

添加路由:

    {
      path: "/login",
      name: "LoginPage",
      component: () => import("@/views/login/LoginPage.vue"),
    },

切换到后台

前台页面添加登录跳转: fronend/FrontendLayout.vue:

<script setup>
import { RouterView, useRouter } from "vue-router";

const router = useRouter();

const jumpToBackend = () => {
  const username = localStorage.getItem("username");
  const password = localStorage.getItem("password");
  if (
    username !== null &&
    password !== null &&
    username !== "" &&
    password !== ""
  ) {
    // 直接跳转后台管理页面
    router.push("/backend/blogs");
  } else {
    // 跳转去登录页面
    router.push("/login");
  }
};
</script>

在响相应的 按钮上添加点击事件

现在可以任意出入前后台了!

退出登录

有了登录我们也需要支持退出登录, 我们是通过 localStorage 来保持状态的, 因此删除对于数据就可以退出了

补充注销按钮: layout/BackendLayout.vue

 <a-button @click="logout" size="mini" type="text">注销</a-button>

补充注销逻辑: 注销成功后 跳转到登录页面:

const logout = () => {
  localStorage.removeItem("username");
  localStorage.removeItem("password");
  router.push("/login");
};

导航守卫

之前的流程 用户通过前台 跳转到后台管理时, 补充了认证, 但是如果用户直接通过 URL 访问后台管理页面喃?

这时候我们就需要做一个全局的导航守卫, 保护所有的 backend 的页面, 凡事访问到backend的页面 都需要检查登录状态, 避免用户直接绕开访问

关于导航守卫

后台页面守卫

单独起一个模块: router/permession.js 来定义导航守卫钩子函数

// 定义导航守卫

export async function beforeEachHandler(to, from, next) {
  if (to.fullPath.indexOf("/backend") === 0) {
    // 如果未登陆 重定向到登录页面, 并且把目标页面作为重定向参数传递下去
    const username = localStorage.getItem("username");
    const password = localStorage.getItem("password");
    if (
      username === null ||
      password === null ||
      username === "" ||
      password === ""
    ) {
      console.log("not login");
      next({
        path: "/login",
        query: {
          redirect: to.name,
          ...to.query,
        },
      });
    } else {
      // 已经登录的用户直接放行
      next();
    }
  } else {
    // 不属于/backend的页面 直接放开
    next();
  }
}

export function afterEachHandler(to, from) {
  console.log(to, from);
}

然后在 router/index.js 中 添加该 Hookrouter 实例上

import { beforeEachHandler, afterEachHandler } from "./permession";

// 补充导航守卫
router.beforeEach(beforeEachHandler);
router.afterEach(afterEachHandler);

export default router;

页面加载进度条

安装 process bar

npm install --save nprogress

router/permession.js 进入页面前 开启进度条, 离开后结束进度条

import NProgress from "nprogress"; // progress bar
import "nprogress/nprogress.css"; // progress bar style

export async function beforeEachHandler(to, from, next) {
  NProgress.start();
  ...
}

export function afterEachHandler(to, from) {
  NProgress.done();
  console.log(to, from);
}