关键词搜索

源码搜索 ×
×

不依赖框架用vue3空白项目从头打造一个过得去的前端

发布2022-09-29浏览1374次

详情内容

通过全程参与,可以加深对VUE项目的理解。

近期做的一个项目,前端除了UI外,没有使用什么框架。不使用现成的框架是无奈之举,因为找不到合适的。之前用的框架,比较老旧,还是vue2的;新的吧,有学习成本,怕耽误时间,也不知道效果怎么样,存在一定的风险。利用最基本的“空白”项目,按需添加基础功能,代码可控,进度也较有保证,同时还能够消除无框架不会工作恐惧症。现在记录一下心得,以后可以反复使用。

记录重点有:

0、整体结构
1、路由
2、导航条及子菜单
3、ajax请求封装
4、vue.config.js及系统配置
5、登录及退出
6、身份认证与访问控制

一、整体结构

1、创建项目
首先是新创建一个vue3项目。方法是

vue create 项目名称
  • 1

然后选择合适的选项。具体可参考拙作
vue3多个项目共享开发和单个项目独立打包的解决方案

1)按默认方案创建
在这里插入图片描述
项目结构:
在这里插入图片描述
2)创建时增加路由及store支持
在这里插入图片描述
在这里插入图片描述
多了router、store以及一些页面。

3)我们项目的整体结构
实际项目中,当然还会夹带一些私货,额外增加一些东东。
在这里插入图片描述

2、项目入口main.js
注意项目的入口不是App.vue,而是main.js。

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

createApp(App).use(store).use(router).mount('#app')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

main.js是系统约定好的名称,正如一般程序的入口是函数void main()一样。整个项目的入口是main.js,然后每个模块的入口可以是index.js。都是js。本质上,vue是一个大的js语法糖。它有这样那样的结构,让人只把杭州作汴州,但归根到底,它最终是要编译成原始的js,才能被浏览器识别、运行。

从上述代码可以知道,main.js的作用是加载App.vue,引入store、路由,然后绑定到页面id="app"的div。这样该div就是整个项目的活动区域,即展示页面内容的区域了:
在这里插入图片描述
vue项目是单页面应用,只有一张html页面。我们看到的所有内容,都展示在id="app"的这个div上!完全由js控制。

3、我们项目里的main.js
main.js这里的引用,都是全局性的。除了路由,store,还可以引入ui框架,css,全局性组件等等。比如我们项目里的main.js是这样写:

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

import PerfectScrollbar from "vue3-perfect-scrollbar";//滚动条美化
import "vue3-perfect-scrollbar/dist/vue3-perfect-scrollbar.css";
import Antd from "ant-design-vue";//ant design vue,UI框架
import "ant-design-vue/dist/antd.css";

import "@/assets/css/default.css"; //自定义的全局css

createApp(App).use(Antd).use(PerfectScrollbar).use(router).mount("#app");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

二、路由

简单来说,路由就是菜单配置,将菜单项的id,路径,都集中写在了一个配置文件里。

1、系统自动生成的路由

import { createRouter, createWebHashHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  }
]

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

export default router
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

以上完全是系统自动生成的代码。看上去也不难理解。其中路由表routes对应的是导航条
在这里插入图片描述
2、实际项目中的路由
见本文第六章第一条

3、代码中使用路由
如果想多加几个导航条,可以依葫芦画瓢,很容易就能实现。每个路由,name是唯一的ID,在代码里控制跳转的话,引用name就可以,可以不再重复写这个path。比如在某个vue里,可以这样使用路由:

import { defineComponent } from "vue";
import { useRouter } from "vue-router"; //引入useRouter

export default defineComponent({
  setup() {
	const router = useRouter();
    const browseIt = (fd) => {
      const to = router.resolve({
        name: "about", //这里是跳转页面的name,要与路由设置保持一致
        params: { id: fd.id },
      });
      window.open(to.href, "_blank");//新开一个页面,打开about
    };

    return {
      browseIt,
    };
  },
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

三、导航条及子菜单

1、导航条
系统生成的代码,已经做了很好的示范:

<template>
  <!-- 导航条 -->
  <nav>
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>
  </nav>
  
  <!-- 导航的目的地。注意该部分必不可少 -->
  <router-view/>
</template>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

通常,我们的菜单可以从服务器端返回,然后利用循环语句输出。

2、子菜单
导航条是一级菜单,二级或以下是子菜单。子菜单,可以利用UI框架来完成。比如我们利用ant design vue的menu组件来完成
在这里插入图片描述

四、ajax请求封装

我们做的项目,总免不了要从服务器端请求数据。前后端分离,理论上前端的数据都来自于服务器端。

目前一般是结合第三方组件axios对ajax请求进行封装。理由主要有2个:

1)ajax一般有超时、返回代码区别对待等共性操作,封装一个ajax处理方法,统一调用,方便维护和修改;除此之外,axios还可以对ajax请求进行拦截,使得请求前、请求后、结果返回做相应处理。

2)解决跨域问题。axios本身似乎并不解决跨域问题,但由于第一点,它对ajax请求进行了封装,我们可以利用一个统一方法,结合api路径前缀做转发,使得浏览器以为api所在路径与前端是同一台服务器,因而不存在跨域。注意所谓跨域问题,是浏览器的一个安全设置,是一种保护措施。只要它不觉得跨域,那跨域就不存在。

1、统一的ajax封装方法

1)ajax封装代码(src/request/index.js)

import axios from "axios";

// 创建一个 axios 实例
const service = axios.create({
  baseURL: "/api", // 所有的请求地址前缀部分
  timeout: 60000, // 请求超时时间毫秒
  withCredentials: true, // 异步请求携带cookie
  headers: {
    // 设置后端需要的传参类型
    "Content-Type": "application/json",
    //'token': 'your token',
    "X-Requested-With": "XMLHttpRequest",
  },
});

// 添加请求拦截器
service.interceptors.request.use(
  function (config) {
    // 在发送请求之前做些什么
    return config;
  },
  function (error) {
    // 对请求错误做些什么
    console.log(error);
    return Promise.reject(error);
  }
);

// 添加响应拦截器
service.interceptors.response.use(
  function (response) {
    console.log(response);
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    // dataAxios 是 axios 返回数据中的 data
    const dataAxios = response.data;
    // 这个状态码是和后端约定的
    //const code = dataAxios.reset;
    return dataAxios;
  },
  function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    console.log(error);
    return Promise.reject(error);
  }
);

export default service;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

2)使用ajax统一封装方法

import request from "@/request";

export const userLogin = (params) => {
  return request({ //request就是统一封装好的方法
    url: "/sys/login",//注意request方法会在前面加上前缀“/api”,变成实质上是请求 “/api/sys/login”
    params,
    method: 'post'
  })
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2、开发环境中,利用vue.config.js做api路径转发
如上所属,ajax的封装方法request,会在api请求路径前加上前缀“/api”,可以利用这一点来设置转发。转发是为了避免跨域。其原理,表面上,我们请求的是当前服务器的路径“/api/a/b/c”,但我们设置凡“/api”开头的路径,都转发到另一台服务器(即后端所在服务器)。浏览器蒙在鼓里,并没有察觉,因此不会触发所谓跨域警告。

  devServer: {
    //devServer 只是一个webpack插件 只能用于开发环境
    proxy: {
      "/api": {
        target: "192.168.0.22",
        pathRewrite: {
          "^/api": "",
        },
      },
    },
  },
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

3、生产环境中,利用nginx做api路径转发

location /api/ {
	proxy_pass http://192.168.0.22:8090/;#必须斜杠/结尾
	proxy_set_header   X-Forwarded-Proto $scheme;
	proxy_set_header   Host              $http_host;
	proxy_set_header   X-Real-IP         $remote_addr;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

五、vue.config.js及系统配置

有关vue.config.js里面的配置,上面约略提到了一些,这里给出完整代码:

1、系统生成的vue.config.js代码

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true
})
  • 1
  • 2
  • 3
  • 4

2、按项目需要修改后的代码

const { defineConfig } = require("@vue/cli-service");
const path = require("path");
const appConfig = require("./public/config");

const resolve = (dir) => {
  return path.join(__dirname, dir);
};

module.exports = defineConfig({
  transpileDependencies: true,

  devServer: {
    //devServer 只是一个webpack插件 只能用于开发环境
    proxy: {
      "/api": {
        target: appConfig.server,
        pathRewrite: {
          "^/api": "",
        },
      },
    },
  },

  // 项目部署基础
  // 默认情况下,我们假设你的应用将被部署在域的根目录下,
  // 例如:https://www.my-app.com/
  // 默认:'/'
  // 如果您的应用程序部署在子路径中,则需要在这指定子路径
  // 例如:https://www.foobar.com/my-app/
  // 需要将它改为'/my-app/'
  publicPath: "/",
  chainWebpack: (config) => {
    config.resolve.alias
      .set("@", resolve("src")) // key,value自行定义,比如.set('@@', resolve('src/components'))
      .set("_c", resolve("src/components"));

    config.plugin("html").tap((args) => {
      args[0].title = appConfig.app.name;
      return args;
    });
  },
});

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

值得一提的是,里面的devServer元素设置,针对的是开发环境。详见拙作:vue.config.js中的devServer

3、真正的项目配置
按我的理解,vue.config.js只在开发阶段和发布时有用,之后就像被消费了的耗材,没啥用处了。同一项目中,真正的项目配置,是/public/config/index.js,即使是发布、部署到生产环境,仍然可以修改,是真正意义上的配置文件。

/public/config/index.js

exports.app = {
  name: "订餐拿饭抓阄系统",
  owner: "蓬蓬养猪场",
  developer: "一群饭桶",
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这里插入图片描述

详见拙作:vue项目读取全局配置

六、登录及退出

主要是路由的应用。原理是:
将页面分为无须登录可浏览和必须登录方可浏览2种。在路由表中做过滤。当转向必须登录页面时,检查登录状态,如果已登录,放行;未登录,转向登录页。注意登录页要设为无须登录可浏览。流程很简单,大家都明白,就不画流程图了。

1、路由表(/src/router/index.js)
完整的路由表。有关登录控制部分,见所谓“路由守卫”。

import { createRouter, createWebHashHistory } from "vue-router";
import Home from "../views/home/PageIndex.vue";

const routes = [
  {
    path: "/login",
    name: "login",
    component: () => import("../views/login/PageIndex.vue"),
    meta: {
      noLogin: true, //无须登录即可浏览。自定义属性
    },
  },
  {
    path: "/",
    name: "Home",
    component: Home,
  },
  {
    path: "/map",
    name: "Map",
    component: () => import("../views/map/PageIndex.vue"),
  },
  {
    path: "/resource",
    name: "Resource",
    component: () => import("../views/resource/PageIndex.vue"),
  },
  {
    path: "/resource/detail/:id",
    name: "ResourceDetail",
    component: () => import("../views/resource/PageDetail.vue"),
  },
  {
    path: "/sys",
    name: "Sys",
    component: () => import("../views/sys/PageIndex.vue"),
  },
];

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

// 路由守卫
router.beforeEach((to, from, next) => {
  const isLogin = localStorage.isLogin ? true : false;
  if (isLogin) {
    //已经登录的情况下,不能再打开登录页
    to.path === "/login" ? next("home") : next();
  } else {
    //如果无须登录则直接打开,否则转向登录页面
    to.meta.noLogin || to.path === "/login" ? next() : next("/login");
  }
});

export default router;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57

2、登录及登出
注意要使用router本身的方法,不可用原始的js,如window.location.href = *** 这种方法,否则部署到nginx会报错。
1)登录

const router = useRouter();
localStorage.setItem("isLogin", true);
document.cookie = "token=" + res.token;
router.replace({ path: "/" });//转向首页。使用replace,避免登录后回退问题
  • 1
  • 2
  • 3
  • 4

2)登出

const router = useRouter();
localStorage.removeItem("isLogin");
router.replace({ path: "/login" });
  • 1
  • 2
  • 3

七、store

vuex store。

其作用,主要是提供数据共享,使得各组件之间方便传递数据。vue基于组件开发,但组件之间传递参数甚为繁琐。有了store,那么所有组件都能直接从store读取,十分方便。其次,store还可以用来实现数据缓存。比如获取数据时,先看看store里有没有,有的话,直接读取返回;没有的话,从数据库读取,然后存入store再返回。缓存的话,前端存入localStorage也可以,但有个数据过期问题,需要手动清除,或者使用时间戳进行比较。而store,刷新页面即消失,或者关掉浏览器也消失,一般没有数据老化的问题。

1、引入store
在main.js中引入
在这里插入图片描述

2、定义store
如果在创建vue项目的时候,选上支持store,系统自动生成一个文件src/store/index.js,
其代码是这样的:

import { createStore } from "vuex";

export default createStore({
  state: {},
  getters: {},
  mutations: {},
  actions: {},
  modules: {
  },
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

1)全局性定义
这是全局性的。为了细分,可以分模块进行定义,比如A模块一个store定义,B模块一个,然后都放在modules这里。如图所示:

在这里插入图片描述
2)模块定义

/src/store/category.js

export default {
  namespaced: true,
  state: {
    v: "hello store", // for test
    items: [],
  },
  getters: {
    v: (state) => state.v,
    items: (state) => {
      return state.items;
    },
  },
  mutations: {
    // 同步操作
    setV(state, val) {
      state.v = val;
    },
    setItems(state, val) {
      state.items = val;
    },
  },
  actions: {
    keepItems: ({ commit }, val) => {
      commit("setItems", val);
    },
  },
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

用法可参考拙作:vuex动态注册嵌套module提供模块内部组件间的数据共享

3、使用store
1)读

import store from "@/store";

export const getCategory = (callback) => {
  let items = store.state.category.items;//读取<----------------------
  if (items.length > 0) {
    callback(items);
  } else {
	。。。
	//写入<----------------------
	store.dispatch("category/keepItems", items).then(() => {
         callback(items);
       });
  }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

2)写
见上例

八、身份认证与访问控制

1、身份认证
是指向后台请求数据时的身份认证。我们采用JWT机制。登录时,得到token,存放在localStorage;然后在向后台请求或提交请求时,则在http 头部附上该token。这个工作,在上面 第四章ajax请求封装 中实现。

import axios from "axios";
import { getToken } from '@/libs/util'

// 创建一个 axios 实例
const service = axios.create({
  baseURL: "/api", // 所有的请求地址前缀部分
  timeout: 60000, // 请求超时时间毫秒
  withCredentials: true, // 异步请求携带cookie
  headers: {
    // 设置后端需要的传参类型
    "Content-Type": "application/json",
    //'token': 'your token',
    "X-Requested-With": "XMLHttpRequest",
  },
});

// 添加请求拦截器
service.interceptors.request.use(
  function (config) {
    // 在发送请求之前做些什么
    const token = getToken();
    if (token) {
      config.headers['Authorization'] = 'Bearer ' + token;//<----------------------------
    }
    return config;
  },
  function (error) {
    // 对请求错误做些什么
    console.log(error);
    return Promise.reject(error);
  }
);

。。。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

2、访问控制
前端的菜单访问控制,在路由中控制;按钮级别的访问控制,自然可以在每个组件里实现。

后续文章
vue3动态路由及菜单
vue3 + element plus实现侧边栏

相关技术文章

点击QQ咨询
开通会员
返回顶部
×
微信扫码支付
微信扫码支付
确定支付下载
请使用微信描二维码支付
×

提示信息

×

选择支付方式

  • 微信支付
  • 支付宝付款
确定支付下载