阅读视图

发现新文章,点击刷新页面。

Vue模板知识点

前言

Vue的两大特征是模板化、组件化。模板的优点是提高开发效率,按照规定的结果书写代码就能够快速完成页面开发。缺点也很明显,就是固定的模板结构牺牲了一定的灵活性,Vue提供了一系列的API来增加模板的灵活性。

一、动态数据

Mustache

 <template>
   <span>{{msg}}</span>
   <span>{{count > 99 ? 99: count}}</span>
 </template>

Mustache不仅支持数据、还支持method、computed、逻辑运算等。这是因为Vue在解析模板的时候会对Mustache里面的内容进行判断,包装成不同的解析函数,如果是数据直接返回,如果是逻辑运和函数则取运算结果。

computed和watch

思想上,computed注重运算结果,watch注重过程。watch直接进行拦截监听,数据变化时运行一些逻辑,computed则需要先进行依赖收集,对依赖进行监听,当依赖发变化的时候触发重新计算。computed是vue自发的进行依赖的收集监听。并在依赖变化时执行对应的渲染函数触发视图的更新,而watch则可以添加更多开发者自定义的逻辑。

特点:computed自发的,不可以控制,watch灵活可控,wathch有immediate、once、deep等。

<template>
 <div>选择了{{ choose }}件商品</div>
 <div>总价是:{{ totalPrice }}</div>
 <button @click="addChoose">add</button>
</template>

<script>
export default {
 name: "App",
 data() {
   return {
     choose: 0,
     price: 19.9,
   };
 },
 methods: {
   addChoose() {
     this.choose++;
   },
 },
 computed: {
   totalPrice() {
     return this.choose * this.price;
   },
 },
 watch: {
   choose(newVal, oldVal) {
     console.log(`choose从${oldVal}变成了${newVal}`);
   },
 },
};
</script>

watch中使用deep、immediate、once

  watch: {
   goodsInfo: {
     handler(newVal, oldVal) {},
     deep: true, // 深度监听,监听子属性
     immediate: true, // 初次赋值的时候也触发监听回调
     once: true, // 只监听一次
   },
 },

二、 动态结构(slot)

插槽: 父组件定义结构,子组件指定结构的位置。插槽分为三种:

  • 默认插槽
  • 具名插槽
  • 作用域插槽

2.1 默认插槽

// 父组件
<template>
 <HelloWorld>
   <span>这是传递给子组件的默认插槽</span>
 </HelloWorld>
</template>

<script>
import HelloWorld from "./HelloWorld";
export default {
 name: "App",
 components: {
   HelloWorld,
 },
};
</script>

// 子组件
<template>
 <div class="child-component">
   <slot></slot>
 </div>
</template>

<script>
export default {
 name: "HelloWorld",
};
</script>

2.2 具名插槽

默认插槽只能定义一个结构,如果想定义多个结构就要使用具名插槽。其中v-slot:header可以缩写为#header。

// 父组件
<template>
 <HelloWorld>
   <template v-slot:header> <span>header</span> </template>
   <template v-slot:content> <span>header</span> </template>
   <template #footer> <span>header</span> </template>
 </HelloWorld>
</template>

<script>
import HelloWorld from "./HelloWorld";
export default {
 name: "App",
 components: {
   HelloWorld,
 },
};
</script>

// 子组件
<template>
 <div>
   <span>HelloWorld</span>
   <slot name="header"></slot>
   <slot name="content"></slot>
   <slot name="footer"></slot>
 </div>
</template>

<script>
export default {
 name: "HelloWorld",
 data() {
   return {
     user: {
       name: "jack",
       age: 21,
     },
     date: Date.now(),
   };
 },
};
</script>

默认插槽只有一个所以没必要给它起名字,但实际上默认插槽也有自己的名字:default

   <slot></slot>

   // 等同于上面写法
   <slot name="default"></slot>

2.3 作用域插槽

作用域插槽也分为匿名作用域插槽和具名作用域插槽, 作用域插槽就是在默认插槽和具名插槽的基础上增加了“数据”的能力,即在父组件定义的插槽结构中可以使用子组件提供的数据。

  1. 默认作用域插槽
// 父组件
<template>
  <HelloWorld v-slot:default="slotProps">
    {{ slotProps.user.name }}
    {{ slotProps.user.age }}
    {{ slotProps.date }}
  </HelloWorld>
</template>

<script>
import HelloWorld from "./HelloWorld";
export default {
  name: "App",
  components: {
    HelloWorld,
  },
};
</script>

// 子组件
<template>
  <div>
    <slot :user="user" :date="date"></slot>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      user: {
        name: "jack",
        age: 21,
      },
      date: Date.now(),
    };
  },
};
</script>

针对默认具名插槽下面的三种书写是等效的:

  <HelloWorld v-slot:default="slotProps">
    {{ slotProps.user.name }}
    {{ slotProps.user.age }}
    {{ slotProps.date }}
  </HelloWorld>

   <HelloWorld v-slot="slotProps">
    {{ slotProps.user.name }}
    {{ slotProps.user.age }}
    {{ slotProps.date }}
  </HelloWorld>

    <HelloWorld #default="slotProps">
    {{ slotProps.user.name }}
    {{ slotProps.user.age }}
    {{ slotProps.date }}
  </HelloWorld>
  1. 具名作用域插槽
// 父组件
<template>
  <HelloWorld>
    <template #user="userProps">
      {{ userProps.user.name }}
      {{ userProps.user.age }}
    </template>
    <template #date="dateProps">
      {{ dateProps.date }}
    </template>
  </HelloWorld>
</template>

<script>
import HelloWorld from "./HelloWorld";
export default {
  name: "App",
  components: {
    HelloWorld,
  },
};
</script>

// 子组件
<template>
  <div>
    <slot name="user" :user="user"></slot>
    <span>content</span>
    <slot name="date" :date="date"></slot>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      user: {
        name: "jack",
        age: 21,
      },
      date: Date.now(),
    };
  },
};
</script>

三、模板的编译过程

Vue是如何将 .vue文件渲染成html的?

  1. 对tempalte进行编译,结合依赖收集和作用域插槽,生成渲染函数。
  2. 调用渲染函数,生成虚拟dom。
  3. 将虚拟dom渲染成真实dom。

虽然在vue中可以直接写render函数,但最好还是使用vue模板,因为这更符合vue的模板理念,更重要的是vue在对模板的编译过程中做了很多优化,比如dom diff算法,事件处理,渲染函数缓存等。

使用render函数的写法如下:

<script setup>
import { h } from 'vue';
</script>

<script>
export default {
  name: 'HelloWorld',
  render() {
    return h('div', 'Hello, World!');
  }
};
</script>

eslint配置文件的名字是eslintrc.cjs,但是有的名字是eslint.config.js

以下是关于 ESLint 配置文件名称不同的解释:

1. 传统的 ESLint 配置文件:.eslintrc.cjs

1.1 历史背景

  • 早期版本

    • 早期的 ESLint 配置文件通常使用 .eslintrc 加上扩展名的形式,如 .eslintrc.json.eslintrc.js 或 .eslintrc.yaml。其中 .eslintrc.cjs 是使用 CommonJS 模块格式的 JavaScript 文件。

    • 使用 .eslintrc.cjs 的原因是在某些项目中,特别是使用 Node.js 环境时,需要使用 CommonJS 模块系统(require 和 module.exports)来配置 ESLint。例如:

收起

javascript

//.eslintrc.cjs
module.exports = {
  "rules": {
    "semi": ["error", "always"],
    "indent": ["error", 2]
  }
};

1.2 特性

  • CommonJS 模块系统

    • 使用 .eslintrc.cjs 可以使用 require 来引入其他模块,适用于 Node.js 项目或需要使用 CommonJS 的情况。例如,如果你需要根据环境或项目的不同动态加载不同的 ESLint 规则,可以使用 require 函数。

2. 新的 ESLint 配置文件:eslint.config.js

2.1 新的配置方式

  • ESLint v8 引入

    • 在 ESLint v8 及以后,引入了 eslint.config.js 作为一种新的配置文件格式。这种配置文件使用 ES 模块(import 和 export)。例如:

收起

javascript

// eslint.config.js
export default [
  {
    "rules": {
      "semi": ["error", "always"],
      "indent": ["error", 2]
    }
  }
];

2.2 优势

  • 使用 ES 模块

    • 对于使用现代 JavaScript 开发,尤其是使用 ES 模块的项目,eslint.config.js 提供了更自然的配置方式,符合现代 JavaScript 的开发习惯。

3. 选择使用哪种配置文件

3.1 项目环境和需求

  • Node.js 项目或 CommonJS 环境

    • 如果你的项目使用 Node.js 或依赖 CommonJS 模块系统,使用 .eslintrc.cjs 可能更方便,因为你可以利用 Node.js 的模块加载机制,方便地引入其他模块和进行动态配置。
  • 现代 JavaScript 项目

    • 对于使用 ES 模块的现代 JavaScript 项目,使用 eslint.config.js 更合适,它与项目的模块系统相匹配,避免了在使用 ES 模块时可能出现的配置文件导入导出问题。

3.2 兼容性和工具支持

  • 工具支持

    • 大多数 ESLint 工具和编辑器插件都支持这两种配置文件,但有些旧的工具可能对 .eslintrc.cjs 支持更好,而有些新的工具可能更倾向于 eslint.config.js

4. 转换和迁移

4.1 从 .eslintrc.cjs 到 eslint.config.js

  • 转换示例

    • 如果你想从 .eslintrc.cjs 转换到 eslint.config.js,可以将配置从 module.exports 转换为使用 export default。例如:

收起

javascript

//.eslintrc.cjs
module.exports = {
  "rules": {
    "semi": ["error", "always"],
    "indent": ["error", 2]
  }
};
  • 可以转换为:

收起

javascript

// eslint.config.js
export default [
  {
    "rules": {
      "semi": ["error", "always"],
      "indent": ["error", 2]
    }
  }
];

4.2 注意事项

  • 配置结构的变化

    • eslint.config.js 的配置结构可能有些不同,它支持配置的扁平化和更多高级特性,在迁移时要注意这些细节。

5. 总结

  • .eslintrc.cjs 是传统的 ESLint 配置文件,使用 CommonJS 模块系统,适用于 Node.js 或 CommonJS 环境。
  • eslint.config.js 是 ESLint v8 引入的新配置文件,使用 ES 模块,适用于现代 JavaScript 开发。
  • 根据项目的模块系统和开发环境,选择合适的配置文件,并注意配置文件的迁移和转换。

Vue 项目开发全攻略:从搭建到上线的技术盛宴

一、项目搭建

在开始开发 Vue 项目时,首先要进行项目搭建。这里我们选用 vite 来负责工程化,它能极大地提升项目构建和开发的效率。

使用 vite 搭建 Vue 项目非常简单,只需在命令行中输入 npm init vite 这一指令,就能快速初始化一个全新的 Vue 项目框架。vite 是新一代的前端构建工具,它基于 ES 模块导入,在开发环境下无需打包操作,可直接启动开发服务器,实现快速冷启动。在生产环境中,vite 又能利用 Rollup 进行高效的打包,为项目提供优化后的代码输出。通过这种方式,我们能轻松搭建起一个基础的 Vue 项目架构,为后续的开发工作奠定坚实的基础。

二、核心技术栈

2.1 Vue 核心语法

Vue 的核心语法是构建项目的基石 ,在本项目中,响应式原理通过ref和reactive两个函数来实现。例如,当需要创建一个简单的响应式数据时,使用ref函数:

import { ref } from 'vue';
const count = ref(0);

若要处理复杂的对象或数组,reactive则更为合适:

import { reactive } from 'vue';
const userInfo = reactive({
  name: 'John',
  age: 30
});

组件化开发让代码的可维护性和复用性大大提高。在项目里,我们将页面拆分成多个组件,每个组件都有独立的逻辑和视图。以一个按钮组件为例,其template部分定义了按钮的外观:

<template>
  <button>{{ buttonText }}</button>
</template>

script部分则负责组件的逻辑,如:

<script setup>
import { ref } from 'vue';
const buttonText = ref('点击我');
</script>

指令方面,v - if、v - show用于控制元素的显示与隐藏。v - for则常用于列表的渲染,假设我们有一个用户列表:

const userList = reactive([
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
]);

在模板中使用v - for进行渲染:

<template>
  <ul>
    <li v - for="user in userList" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

通过这些核心语法的运用,我们能够构建出灵活且高效的 Vue 应用程序。

2.2 Vue - Router 路由

在 Vue - Router 的配置中,多级路由的设置让页面结构更加清晰。例如,我们有一个主页面Home,其下包含About和Contact两个子页面。在路由配置文件中可以这样定义:

import { createRouter, createWebHistory } from 'vue - router';
import Home from './views/Home.vue';
import About from './views/About.vue';
import Contact from './views/Contact.vue';
const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/home',
      component: Home,
      children: [
        { path: 'about', component: About },
        { path: 'contact', component: Contact }
      ]
    }
  ]
});

路由懒加载是提升性能的关键。我们使用import()函数来实现,当访问特定路由时,对应的组件才会被加载。比如:

const About = () => import('./views/About.vue');

添加路由守卫则能有效控制页面的访问权限。以登录验证为例,在全局前置守卫中可以这样实现:

router.beforeEach((to, from, next) => {
  const isLoggedIn = localStorage.getItem('token');
  if (to.meta.requiresAuth &&!isLoggedIn) {
    next('/login');
  } else {
    next();
  }
});

在需要验证的路由中,设置meta字段:

{
  path: '/dashboard',
  component: Dashboard,
  meta: { requiresAuth: true }
}

通过这样的配置,确保了只有登录用户才能访问受保护的页面。

2.3 Pinia 状态管理

Pinia 在项目中负责状态的管理,极大地简化了状态共享的过程。首先,安装 Pinia 并在main.js中进行配置:

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount('#app');

接着,定义一个store来管理用户相关的状态。例如:

import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null,
    isLoggedIn: false
  }),
  actions: {
    login(user) {
      this.userInfo = user;
      this.isLoggedIn = true;
      localStorage.setItem('token', 'valid - token');
    },
    logout() {
      this.userInfo = null;
      this.isLoggedIn = false;
      localStorage.removeItem('token');
    }
  }
});

在组件中使用该store时,只需引入并调用相应的方法:

import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
userStore.login({ name: 'John', age: 30 });

Pinia 的优势在于其简洁的 API 和良好的模块化设计,使得状态管理变得轻松且高效。

2.4 Element3 UI 组件库

Element3 是一个功能强大的 UI 组件库,为项目提供了丰富的组件。在使用时,我们采用按需加载的方式来优化性能。首先,安装相关的插件:

npm install -D unplugin - vue - components unplugin - auto - import

然后,在vue.config.js中进行配置:

const AutoImport = require('unplugin - auto - import/webpack');
const Components = require('unplugin - vue - components/webpack');
const { ElementPlusResolver } = require('unplugin - vue - components/resolvers');
module.exports = {
  configureWebpack: {
    plugins: [
      AutoImport({
        resolvers: [ElementPlusResolver()]
      }),
      Components({
        resolvers: [ElementPlusResolver()]
      })
    ]
  }
};

这样,在组件中使用 Element3 组件时,如按钮组件,只需直接引入:

<template>
  <el - button type="primary">点击我</el - button>
</template>

Element3 组件以el -开头,通过按需加载,我们避免了引入不必要的组件,有效减少了项目的打包体积,提升了页面的加载速度。

2.5 Stylus CSS 预处理器

Stylus 作为 CSS 预处理器,为项目带来了诸多便利。它允许我们使用变量、混入、嵌套等功能,使 CSS 代码更加简洁和易于维护。例如,定义一个颜色变量:

$primaryColor = #1890ff

在样式中使用该变量:

button {
  background - color: $primaryColor;
  color: white;
}

混入功能可以复用一些常用的样式,如圆角样式:

border - radius() {
  border - radius: 5px;
}
.box {
  +border - radius();
}

样式的嵌套则让代码结构更加清晰,以导航栏为例:

.nav {
  display: flex;
  justify - content: space - between;
  li {
    list - style: none;
    a {
      text - decoration: none;
      color: #333;
      &:hover {
        color: $primaryColor;
      }
    }
  }
}

通过 Stylus 的这些特性,我们能够高效地编写和管理项目的样式。

2.6 Axios AJAX 请求封装库

Axios 用于与后端进行数据交互,我们对其进行了封装,以提高代码的复用性和可维护性。首先,创建一个api.js文件,设置基础 URL 和请求拦截器:

import axios from 'axios';
const service = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000
});
service.interceptors.request.use(
  config => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

然后,封装常用的请求方法,如get和post:

export const get = (url, params = {}) => {
  return service.get(url, { params });
};
export const post = (url, data = {}) => {
  return service.post(url, data);
};

在组件中使用时,只需引入相应的方法:

import { get } from '@/api';
get('/user/info').then(response => {
  console.log(response.data);
}).catch(error => {
  console.error(error);
});

通过这样的封装,我们能够方便地进行各种 AJAX 请求,与后端进行稳定的数据交互。

三、项目亮点展示

3.1 ES6 风格的全面应用

在整个项目中,我们全面采用了 ES6 风格的代码编写方式,这使得代码在简洁性、易读性和易维护性上都有了显著提升。例如,在定义路由时,使用对象解构的方式简化了代码结构。原本需要完整书写routes: routes,现在直接写成routes即可。这种简洁的写法不仅减少了冗余代码,还让代码逻辑更加清晰,开发者能够一眼看清路由的配置关系。

在函数定义方面,ES6 的箭头函数也被广泛应用。比如在处理一些简单的回调函数时,箭头函数的使用使得代码更加紧凑。例如,在数组的map方法中,使用箭头函数可以快速对数组中的每个元素进行处理:

const numbers = [1, 2, 3, 4];
const squaredNumbers = numbers.map((number) => number * number);

相比于传统的函数定义方式,箭头函数的语法更加简洁,同时也避免了this指向的问题,让代码的维护更加轻松。

3.2 良好的注释与代码可读性

良好的注释是提高代码可读性的关键。在项目中,我们在关键的代码块、函数定义以及复杂的逻辑处都添加了详细的注释。例如,在路由守卫的代码中,我们添加了注释来说明其作用和逻辑:

// 全局前置守卫,用于验证用户是否登录
router.beforeEach((to, from, next) => {
  const isLoggedIn = localStorage.getItem('token');
  if (to.meta.requiresAuth &&!isLoggedIn) {
    // 如果目标路由需要登录且用户未登录,则重定向到登录页面
    next('/login');
  } else {
    // 否则,允许用户访问目标路由
    next();
  }
});

这样的注释使得其他开发者在阅读代码时,能够快速理解代码的意图和功能,降低了代码的理解成本。同时,对于一些自定义的函数和组件,我们也添加了注释来解释其输入参数、返回值以及功能用途,确保代码的每一部分都清晰易懂。

3.3 规范的 Git 提交记录和习惯

在项目开发过程中,我们始终保持着规范的 Git 提交记录和良好的提交习惯。每次提交都有明确的提交信息,描述本次提交所做的修改内容。例如,“修复登录页面的验证码验证问题”“优化首页的加载速度” 等。这样的提交信息使得项目的版本历史清晰可追溯,团队成员能够快速了解每个提交的目的和影响范围。

同时,我们遵循一定的分支管理策略,如使用master分支作为主分支,用于发布稳定版本;develop分支用于开发新功能,通过创建特性分支进行功能开发,开发完成后再合并到develop分支。这种规范的分支管理和提交习惯,不仅有助于团队协作开发,还能在出现问题时快速定位和解决,提高了项目的开发效率和质量。

四、实战技巧与注意事项

4.1 表单组件的使用

在项目中,表单组件的使用非常频繁。我们使用 :model来收集表单数据,这是一种双向数据绑定的方式,能够实时同步表单输入与数据模型。例如:

<el - form :model="formData">
  <el - form - item label="用户名">
    <el - input v - model="formData.username"></el - input>
  </el - form - item>
  <el - form - item label="密码">
    <el - input type="password" v - model="formData.password"></el - input>
  </el - form - item>
</el - form>

在上述代码中,formData是一个包含username和password字段的对象,通过v - model指令,表单输入框的值会实时更新到formData中,反之亦然。

通过ref可以获取表单实例,这在需要手动操作表单时非常有用。在模板中,使用ref标记表单组件:

<el - form ref="formRef" :model="formData">
  <!-- 表单内容 -->
</el - form>

在script部分,通过ref获取表单实例:

import { ref } from 'vue';
const formRef = ref(null);

当表单挂载后,formRef就会获取到实际的表单实例。此时,我们可以调用表单实例的方法,如validate方法进行表单校验:

formRef.value.validate((valid) => {
  if (valid) {
    // 校验通过,提交表单或执行其他操作
    console.log('表单校验通过');
  } else {
    // 校验失败,提示用户错误信息
    console.log('表单校验失败');
  }
});

表单的校验规则通过rules属性来定义。例如,对用户名和密码设置必填校验:

const formData = reactive({
  username: '',
  password: ''
});
const rules = {
  username: [
    { required: true, message: '用户名不能为空', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '密码不能为空', trigger: 'blur' }
  ]
};

在表单组件中,将rules属性绑定到对应的form - item上:

<el - form :model="formData" :rules="rules">
  <el - form - item label="用户名" prop="username">
    <el - input v - model="formData.username"></el - input>
  </el - form - item>
  <el - form - item label="密码" prop="password">
    <el - input type="password" v - model="formData.password"></el - input>
  </el - form - item>
</el - form>

这样,当用户输入完成并离开输入框(blur事件触发)时,表单会根据设置的规则进行校验,并显示相应的错误提示信息。

4.2 布局组件的应用

布局组件在构建页面结构时起着关键作用。我们常用的布局组件包括Elcontainer、Elheader、ElAside、ElMain等。

以一个常见的后台管理页面布局为例,使用Elcontainer作为容器,将页面分为头部、侧边栏和主体内容区域:

<el - container>
  <el - header>
    <!-- 头部内容,如导航栏 -->
    <h1>后台管理系统</h1>
  </el - header>
  <el - container>
    <el - aside width="200px">
      <!-- 侧边栏菜单 -->
      <el - menu :default - active="activeIndex" class="el - menu - vertical - demo" @select="handleSelect">
        <el - menu - item index="1">菜单1</el - menu - item>
        <el - menu - item index="2">菜单2</el - menu - item>
      </el - menu>
    </el - aside>
    <el - main>
      <!-- 主体内容区域 -->
      <p>这里是主要内容</p>
    </el - main>
  </el - container>
</el - container>

在上述代码中,Elheader定义了页面的头部,通常包含导航栏等信息。Elaside作为侧边栏,设置了固定的宽度为200px,并在其中放置了菜单组件。Elmain则用于展示主体内容。

对于页面内的布局,ElRow和ElCol经常被用于实现栅格化布局。例如,将一行分为两列,左列占 8 格,右列占 4 格:

<el - row>
  <el - col :span="8">
    <p>左列内容</p>
  </el - col>
  <el - col :span="4">
    <p>右列内容</p>
  </el - col>
</el - row>

通过span属性可以灵活调整每列所占的比例,从而实现各种复杂的页面布局。

4.3 性能优化策略

在项目开发过程中,性能优化至关重要。我们采用了多种策略来提升项目的性能。

按需加载是其中一个重要的策略。在引入 Vue 组件库 Element3 时,我们通过配置实现了按需加载,避免一次性加载所有组件,从而减少了初始加载时间。在路由方面,也采用了懒加载技术,只有当用户访问特定路由时,对应的组件才会被加载。例如:

const Home = () => import('./views/Home.vue');
const About = () => import('./views/About.vue');
const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/home', component: Home },
    { path: '/about', component: About }
  ]
});

这样,在应用启动时,只有必要的路由组件会被加载,大大提高了应用的启动速度。

此外,还对静态资源进行了优化。通过使用 Webpack 插件对 JavaScript 和 CSS 文件进行压缩,减少了文件体积,加快了文件的传输速度。同时,合理利用缓存机制,对于不经常变化的静态资源设置较长的缓存时间,避免用户每次访问都重新下载。

在图片处理方面,对图片进行了压缩和格式优化,选择合适的图片格式(如 WebP 格式,在保证图片质量的前提下,文件体积更小),并根据不同的设备屏幕尺寸提供相应分辨率的图片,避免加载过大的图片资源,从而提升页面的加载速度和用户体验。

五、总结与展望

通过本次项目的开发,我收获了许多宝贵的经验。从项目搭建到技术栈的运用,再到项目亮点的打造和实战技巧的积累,每一个环节都让我对 Vue 开发有了更深入的理解。在项目中,我学会了如何高效地运用各种工具和技术,解决实际开发中遇到的问题。同时,也深刻体会到团队协作、代码规范以及性能优化的重要性。

展望未来,我希望能够进一步优化项目。在性能方面,持续探索更有效的优化策略,如进一步优化图片加载、减少 HTTP 请求等,以提升用户体验。在功能上,根据用户反馈和业务需求,不断添加新的功能模块,使项目更加完善。同时,也会关注 Vue 技术的发展动态,及时引入新的特性和最佳实践,保持项目的技术先进性。

🍉🍉🍉快来吃瓜,原来国外开发者也会因为哪个框架好吵的不可开交

前言

事情是这样的,今天浏览到了一篇dev上Ryan Carniato大佬(SolidJS库作者)的文章,在文章中,作者阐述了自己对2025年的前段框架的展望,并且提到React、Svelte都在以增加编译的复杂性为代价简化开发。

And interestingly both choices come at the expense of increased complexity in tooling compared to their existing approaches. The verdict is still out on whether these will ultimately be good moves for these projects. The common ground is the foundation we build upon continues to get more complicated as we attempt to create solutions to make development easier.

有趣的是,与现有方法相比,这两种选择都以增加工具复杂性为代价。这些举措最终是否对这些项目有利尚无定论。共同点是,随着我们尝试创建解决方案以简化开发,我们所依赖的基础变得越来越复杂。

回顾下前端框架的区别

为了方便清楚的了解下面的内容,我先给大家简单回顾下前段框架的现状。

欢迎关注我的公众号:萌萌哒草头将军

以前的文章中提到,根据运行时和编译时,前端框架主要分为三类:重编译时(SvelteSolidJS)、重运行时(React)、介于两者之间(AngularVue)。

image.png

但是随着Svelte 5引入了符文(runes),使得Svelte不再是完美的进入运行时即可执行的javascript片段,因为需要动态解析runes的内容。这也是Ryan Carniato觉得Svelte变复杂的原因。

根据不同的更新粗细粒度,他们又被分为:

粒度 成员
粗粒度 React
中粒度 Vue
细粒度 SolidJSSvelte

Vue为了细粒度更新,正在努力的研发vaporReact就目前的趋势来看,打算一条道走到黑了。

Angular起初的处境和Vue相似。但是在发布于 2023 年 5月Angular 16,已经借助Signals实现了翻身,提前完成了细粒度更新的框架。

Signals是一种类似vue3proxyReactivity响应式的发布订阅模式,有响应式更新方面粒度更细,性能更好

好了有了上面的铺垫,相信大家都可以看明白下面的评论。

结果评论区出现了一批弃坑并谴责React的老哥。

好戏开场了

下面是第一位老哥的发言:React已死,SvelteSolidJS当立!

image.png

这种回答,立马招惹了Angular粉丝的不满,原因是Angular使用了Signals技术,性能提高了30%!

image.png

可以看到作为粉丝是很自豪的,甚至提到了vue vopar。说明这位开发者对前端框架的有一定深入的了解。

不过由于Angular自身的笨重,即使有了Signals的加持,还是取得了倒数第二的好成绩!

image.png

图片来源:github.com/transitive-…

接着,伊桑先表达了自己不喜欢 React 的原因:

  • useEffect 破坏代码可读性,难以预测的执行时机
  • 状态管理库混乱,这里是在批评 Redux
  • 不关系CSR。CSR(Client-Side Rendering 指客户端渲染,
  • 虽然入门门槛低,但是存在步兵生成问题,这是指React不能很好的控制最小任务单元,需要借助外部工具,比如状态管理库。

image.png

然后表达了喜欢 Angular 的原因:

  • Angular的就业市场更好(国外)
  • Angular的特性:事件重放、路由级渲染模式、Signals等优秀的功能
  • 依赖注入很棒!
  • 稳定性强,功能丰富

image.png

再来看看其他比较温和的评论

image.png

尹桑又单独评论了一次,这次的评论让我觉得他的水平有点低了,JavaScript 本身就是个脚本语言,不需要构建,边解释边执行。无可厚非的事!

image.png

这位评论者说的也很中肯,文章中,作者的确提出了Signals的缺点: Signal 的生态尚未完全成熟、需要面对一些新的学习曲线和社区支持不足的问题

image.png

这位评论者的赞扬得到了原作者的肯定,并且推荐阅读更多文章,我也进主页看了下,

image.png

我打算抽空好好研究下,抽空在分享给大家!

image.png

最后

一个和谐友爱的社区会帮助开发者们更好的成长,希望大家有理有据的讨论不要踩一捧一。

这是原文连接:dev.to/this-is-lea…

好了,如果你觉得这篇文章对你有帮助,请记得给我点赞,或者关注我的公众号:萌萌哒草头将军

使用vue2和vuex做一个真实的管理系统,需要怎么开展项目

使用 Vue 2 和 Vuex 构建一个真实的管理系统,可以按照以下步骤开展项目:

1. 项目初始化

  1. 安装 Node.js:确保你的开发环境安装了 Node.js,它是运行 Vue 项目所必需的。
  2. 创建 Vue 项目:使用 Vue CLI 快速创建一个 Vue 2 项目。如果没有安装 Vue CLI,可以通过 npm install -g @vue/cli 进行全局安装。然后在命令行中执行 vue init webpack your - project - name,按照提示完成项目初始化。
  3. 安装 Vuex:进入项目目录,执行 npm install vuex --save,将 Vuex 安装到项目中。

2. 项目架构设计

  1. 确定功能模块:分析管理系统的需求,确定系统的主要功能模块,例如用户管理、权限管理、数据统计等。

  2. 规划组件结构:根据功能模块,规划组件的层次结构。通常可以将每个功能模块划分为一个或多个 Vue 组件,如列表组件、表单组件、详情组件等。

  3. 设计路由:使用 Vue Router 来管理应用的路由。根据不同的功能模块和页面,设计合理的路由配置,确保用户能够方便地在不同页面之间导航。例如,在 router/index.js 中:

收起

javascript

import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/components/Home.vue';
import UserList from '@/components/user/UserList.vue';

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/users',
      name: 'UserList',
      component: UserList
    }
  ]
});

3. 状态管理设计(Vuex)

  1. 定义状态(State) :在 Vuex 的 store.js 文件中,定义应用的状态。例如,对于用户管理系统,可能需要定义当前登录用户的信息、用户列表等状态。

收起

javascript

const state = {
  currentUser: null,
  userList: []
};
  1. 定义突变(Mutations) :Mutations 用于修改状态,必须是同步操作。例如,添加用户到用户列表的突变:

收起

javascript

const mutations = {
  setCurrentUser(state, user) {
    state.currentUser = user;
  },
  addUserToList(state, newUser) {
    state.userList.push(newUser);
  }
};
  1. 定义行动(Actions) :Actions 用于处理异步操作,通过 commit 调用 Mutations 来修改状态。例如,从服务器获取用户列表的行动:

收起

javascript

import axios from 'axios';

const actions = {
  async fetchUserList({ commit }) {
    try {
      const response = await axios.get('/api/users');
      commit('setUserList', response.data);
    } catch (error) {
      console.error('Error fetching user list:', error);
    }
  }
};
  1. 定义 Getter:Getters 用于从状态中派生出一些状态,类似于计算属性。例如,获取已登录用户的用户名:

收起

javascript

const getters = {
  getCurrentUserName: state => {
    return state.currentUser? state.currentUser.username : 'Guest';
  }
};
  1. 创建 Vuex 存储(Store) :将上述定义的状态、突变、行动和 Getter 组合成一个 Vuex 存储对象:

收起

javascript

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state,
  mutations,
  actions,
  getters
});

4. 组件开发

  1. 创建组件:根据之前规划的组件结构,在 src/components 目录下创建相应的 Vue 组件。例如,创建一个用户列表组件 UserList.vue

收起

vue

<template>
  <div>
    <h1>用户列表</h1>
    <ul>
      <li v - for="user in userList" :key="user.id">{{ user.username }}</li>
    </ul>
  </div>
</template>

<script>
import { mapState } from 'vuex';

export default {
  computed: {
  ...mapState(['userList'])
  }
};
</script>
  1. 组件通信:通过 Vuex 进行组件间的状态共享和通信。例如,在用户列表组件中,可以通过 mapState 辅助函数将 Vuex 中的 userList 状态映射到组件的计算属性中,方便在模板中使用。

5. 数据请求

  1. 选择 HTTP 库:推荐使用 axios 进行 HTTP 请求。在项目中安装 axiosnpm install axios --save

  2. 封装请求:在 src 目录下创建一个 api 目录,封装各种 API 请求。例如,创建一个 user.js 文件来处理与用户相关的请求:

收起

javascript

import axios from 'axios';

const baseUrl = '/api';

export const getUserList = () => {
  return axios.get(`${baseUrl}/users`);
};

export const addUser = data => {
  return axios.post(`${baseUrl}/users`, data);
};
  1. 在组件中使用请求:在组件中调用封装好的 API 函数。例如,在 UserList.vue 组件的 created 生命周期钩子中调用 getUserList 函数获取用户列表:

收起

vue

<template>
  <div>
    <h1>用户列表</h1>
    <ul>
      <li v - for="user in userList" :key="user.id">{{ user.username }}</li>
    </ul>
  </div>
</template>

<script>
import { mapState } from 'vuex';
import { getUserList } from '@/api/user';

export default {
  computed: {
  ...mapState(['userList'])
  },
  created() {
    this.fetchUserList();
  },
  methods: {
    async fetchUserList() {
      try {
        const response = await getUserList();
        this.$store.commit('setUserList', response.data);
      } catch (error) {
        console.error('Error fetching user list:', error);
      }
    }
  }
};
</script>

6. 样式设计

  1. 选择样式方案:可以使用 CSS 预处理器(如 Sass、Less)来编写样式,也可以使用 UI 框架(如 Element - UI)来快速搭建页面样式。
  2. 全局样式和组件样式:在 src/assets 目录下创建 styles 目录,存放全局样式文件。在组件内部,可以使用 <style> 标签编写组件级别的样式,并且可以通过 scoped 属性来限制样式的作用范围。

7. 测试与优化

  1. 单元测试:使用测试框架(如 Jest)和 Vue 测试库(如 @vue/test - utils)对组件进行单元测试。例如,对 UserList.vue 组件进行测试,验证其是否正确显示用户列表。
  2. 性能优化:优化组件的渲染性能,避免不必要的重新渲染。可以使用 computed 属性和 watch 来优化数据监听和处理逻辑。同时,对图片、脚本等资源进行压缩,提高页面加载速度。

8. 部署

  1. 构建项目:在项目根目录下执行 npm run build,将项目构建为生产环境可用的静态文件。

  2. 选择部署方式:根据项目需求,选择合适的部署方式,如部署到服务器(如阿里云、腾讯云),或者使用云服务(如 Firebase Hosting)。配置好服务器环境,将构建后的文件上传到相应的目录,确保项目能够正常访问。

通过以上步骤,你可以使用 Vue 2 和 Vuex 构建一个功能完整的管理系统。在实际开发过程中,还需要根据具体的业务需求进行详细的设计和开发,确保系统的稳定性和可靠性。


使用Vue 2和Vuex构建一个真实的管理系统是一个很好的选择,因为Vue 2提供了强大的前端框架支持,而Vuex则很好地解决了状态管理的问题。下面我会给出一个大致的步骤指南来帮助你启动这个项目。

1. 项目规划

  • 需求分析:首先明确系统需要实现哪些功能,比如用户登录、权限控制、数据展示等。
  • 界面设计:根据需求绘制出系统的UI草图或原型图,这有助于后续开发时有清晰的方向。
  • 技术选型:确认除了Vue 2和Vuex之外还需要用到的技术栈,如路由库(vue-router)、样式解决方案(CSS预处理器如Sass/LESS)、HTTP请求库(axios)等。

2. 环境搭建

  • 使用vue-cli快速创建项目基础结构:

    1vue create project-name
    

    在创建过程中可以选择添加Vuex支持。

  • 安装必要的依赖包:

    1npm install vuex vue-router axios
    

3. 构建应用架构

  • 配置路由:定义好每个页面对应的组件,并设置相应的路由规则。
  • 状态管理:利用Vuex来存储全局的状态信息,例如用户的登录状态、菜单列表等。定义好state, mutations, actions, getters。
  • API接口服务层:封装所有与后端交互的服务,统一处理错误及响应格式。
  • 组件化开发:将页面拆分成多个可复用的小组件进行开发。

4. 开发阶段

  • 按照设计稿逐步实现各个页面的功能。
  • 注意保持代码整洁,适当使用注释说明逻辑。
  • 对于复杂的业务逻辑,可以考虑编写单元测试以保证质量。

5. 测试

  • 功能性测试:确保所有功能按预期工作。
  • 性能优化:检查并优化加载速度慢或者卡顿的地方。
  • 兼容性测试:在不同浏览器上查看效果是否一致。

6. 部署上线

  • 根据服务器环境选择合适的打包方式。
  • 配置域名解析、HTTPS证书等。
  • 监控网站运行状况,及时发现并解决问题。

7. 维护更新

  • 收集用户反馈,不断改进产品。
  • 定期发布新版本修复已知问题并增加新特性。

小贴士

  • 利用Element UI或其他UI库可以加快开发进度。
  • 学会查阅官方文档和社区资源,遇到难题时不要害怕求助。
  • 考虑采用持续集成/持续部署(CI/CD)工具提高效率。

通过遵循以上步骤,你应该能够顺利地使用Vue 2和Vuex完成一个实用且高效的管理系统开发。

为了解决内存泄露,我把 vue 源码改了

前言

彦祖们,好久不见,最近一直忙于排查单位业务的终端内存泄露问题,已经吃了不下 10 个 bug

但是排查内存泄露在前端领域属于比较冷门的领域了

这篇文章笔者将带你一步步分享业务实践中遇到的内存泄露问题以及如何修复的经历

本文涉及技术栈

  • vue2

场景复现

如果之前有看过我文章的彦祖们,应该都清楚

笔者所在的单位有一个终端叫做工控机(类似于医院挂号的终端),没错!所有的 bug 都源自于它😠

因为内存只有 1G 所以一旦发生内存泄露就比较可怕

不过没有这个机器 好像也不会创作这篇文章😺

复现 demo

彦归正传,demo 其实非常简单,只需要一个最简单的 vue2 demo 就可以了

  • App.vue
<template>
  <div id="app">
    <button @click="render = true">render</button>
    <button @click="render = false">destroy</button>
    <Test v-if="render"/>
  </div>
</template>
<script>
import Test from './test.vue'
export default {
  name: 'App',
  components: {
    Test
  },
  data () {
    return {
      render: false
    }
  }
}
</script>
<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
</style>

  • test.vue
<template>
  <div class="test">
    <div>{{ total }}</div>
    <div
      v-for="(item,index) in 1000"
      :key="`${item}-${index}`"
      class="item"
    >
      {{ item }}ipc-prod2.8
    </div>
  </div>
</template>

<script>
export default {
  name: 'Test',
  data () {
    return {
      total: 1000
    }
  },
  mounted () {
    this.timer = setTimeout(() => {
      this.total = 10000
    }, 500)
  },
  beforeDestroy () {
    clearTimeout(this.timer)
  }
}
</script>

复现流程

以下流程建议彦祖们在 chrome 无痕模式下执行

  1. 我们点击 render 按钮渲染 test 组件,此时我们发现 dom 节点的个数来到了 2045

image.png

考虑到有彦祖可能之前没接触过这块面板,下图展示了如何打开此面板

image.png

  1. 500ms 后(定时器执行完成后,如果没复现可以把 500ms 调整为 1000ms, 1500ms),我们点击 destroy 按钮
  2. 我们点击面板这里的强制回收按钮(发现节点并没有回收,已发生内存泄露)

image.png

如果你的浏览器是最新的 chrome,还能够点击这里的 已分离的元素(detached dom),再点击录制

image.png

我们会发现此时整个 test 节点已被分离

image.png

问题分析

那么问题到底出在哪里呢?

vue 常见泄露场景

笔者搜遍了全网,网上所说的不外乎以下几种场景

1.未清除的定时器

2.未及时解绑的全局事件

3.未及时清除的 dom 引用

4.未及时清除的 全局变量

好像第一种和笔者的场景还比较类似,但是仔细看看代码好像也加了

beforeDestroy () {
  clearTimeout(this.timer)
}

这段代码啊,就算不加,timer 执行完后,事件循环也会把它回收掉吧

同事提供灵感

就这样笔者这段代码来回测试了半天也没发现猫腻所在

这时候同事提供了一个想法说"total 更新的时候是不是可以提供一个 key"

改了代码后就变成了这样了

  • test.vue
<template>
  <div class="test">
    <div :key="renderKey">{{ total }}</div>
    <div
      v-for="(item,index) in 1000"
      :key="`${item}-${index}`"
      class="item"
    >
      {{ item }}ipc-prod2.8
    </div>
  </div>
</template>

<script>
export default {
  name: 'Test',
  data () {
    return {
      renderKey: 0,
      total: 1000
    }
  },
  mounted () {
    this.timer = setTimeout(() => {
      this.total = 10000
      this.renderKey = Date.now()
    }, 500)
  },
  beforeDestroy () {
    clearTimeout(this.timer)
  }
}
</script>

神奇的事情就这样发生了,笔者还是按以上流程测试了一遍,直接看结果吧

image.png

我们看到这个 DOM 节点曲线,在 destroy 的时候能够正常回收了

问题复盘

最简单的 demo 问题算是解决了

但是应用到实际项目中还是有点困难

难道我们要把每个更新的节点都手动加一个 key 吗?

其实仔细想想,有点 vue 基础的彦祖应该了解这个 key 是做什么的?

不就是为了强制更新组件吗?

等等,强制更新组件?更新组件不就是 updated 吗?

updated 涉及的不就是八股文中我们老生常谈的 patch 函数吗?(看来八股文也能真有用的时候😺)

那么再深入一下, patch 函数内部不就是 patchVnode 其核心不就是 diff 算法吗?

首对首比较,首对尾比较,尾对首比较,尾对尾比较 这段八股文要是个 vuer 应该都不陌生吧?😺

动手解决

其实有了问题思路和想法

那么接下来我们就深入看看 vue 源码内部涉及的 updated 函数到底在哪里吧?

探索 vue 源码

我们找到 node_modules/vue/vue.runtime.esm.js

image.png

我们看到了 _update 函数真面目,其中有个 __patch__ 函数,我们再重点查看一下

image.png

image.png

createPatchFunction 最后 return 了这个函数

image.png

我们最终来看这个 updateChildren 函数

image.png

其中多次出现了上文中所提到的八股文,每个都用 sameVnode进行了对比

  • function sameVnode
function sameVnode (a, b) {
    return (a.key === b.key &&
        a.asyncFactory === b.asyncFactory &&
        ((a.tag === b.tag &&
            a.isComment === b.isComment &&
            isDef(a.data) === isDef(b.data) &&
            sameInputType(a, b)) ||
            (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))));
}

果然这里我们看到了上文中 key 的作用

key 不一样就会认作不同的 vnode

那么就会强制更新节点

对应方案

既然找到了问题的根本

在判定条件中我们是不是直接加个 || a.text !== b.text

强制对比下文本节点不就可以了吗?

修改 sameVnode

看下我们修改后的 sameVnode

function sameVnode (a, b) {
    if(a.text !== b.text) return false // 文本不相同 直接 return
    return (a.key === b.key &&
        a.asyncFactory === b.asyncFactory &&
        ((a.tag === b.tag &&
            a.isComment === b.isComment &&
            isDef(a.data) === isDef(b.data) &&
            sameInputType(a, b)) ||
            (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))));
}

方案效果

让我们用同样的代码来测试下

image.png

测试了几次发现非常的顺利,至此我们本地的修改算是完成了

如何上线?

以上的方案都是基于本地开发的,那么我们如何把代码应用到线上呢?

其他开发者下载的 vue 包依旧是 老的 sameVnode

不慌,接着看

patch-package

对比了好几种方式,最终我们选择了这个神器

其实使用也非常简单

1.npm i patch-package

2.修改 node_modules/vue 源码

3.在根目录执行 npx patch-package vue(此时如果报错,请匹配对应 node 版本的包)

我们会发现新增了一个这样的文件

image.png

4.我们需要在package.json scripts 新增以下代码

  • package.json
"scripts": {
    +"postinstall":"patch-package"
}

至此上线后,其他开发者执行 npm i 后便能使变动的补丁生效了

优化点

其实我们的改造还有一定的进步空间,比如说在指定节点上新增一个 attribute

在函数内部判断这个 attributereturn false

这样就不用强制更新每个节点了

当然方式很多种,文章的意义在于解决问题的手段和耐心

写在最后

最后再次感谢同事 juejin.cn/user/313102… 的提供的灵感和协助

感谢彦祖们的阅读

个人能力有限

如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟

Vue + ElementPlus 实现权限管理系统(十一): 实现个人中心页面

在后台管理系统中,一般都会有个人中心页面用于修改查看我们的账户信息、修改密码等。本篇文章将使用vueelementplus来实现我们后台权限管理系统的个人中心页面。最终效果如下 image.png

可以查看我们当前的账户信息,更换头像以及修改基本信息及密码。接下来我们就来实现这些功能。

接口引入

这个页面需要四个接口,分别是获取当前用户信息、头像上传、修改密码、修改基本信息接口。访问本地地址http://localhost:3000/fs_admin/api可以看到对应的丝袜哥接口文档

image.png

src/api/user/index.ts导出这三个接口(上传接口使用element提供的upload组件)。

//获取用户信息
export const getProfile = () => {
  return request({
    url: "/user/profile",
    method: "get",
    loading: false,
  });
};

//修改密码
export const updatePassword = (data: {
  oldPassword: string,
  newPassword: string,
}) => {
  return request({
    url: "/user/updatePassword",
    method: "put",
    data,
  });
};

//修改个人信息
export const updateUserInfo = (data: Form) => {
  return request({
    url: "/user/updateUserInfo",
    method: "put",
    data,
  });
};

页面布局

页面布局使用elementplusel-col将页面分为左右两部分。左边是使用el-card组件实现个人信息展示区域。右边是修改个人信息区域,同时使用el-tabs实现修改密码和修改基本信息两个选项卡。我们src/views下新建profile/index.vue来实现这个页面。

<template>
  <el-row class="fs_profile" :gutter="20">
    <el-col :span="8">
      <el-card>
        <template #header>
          <div class="card-header">
            <span>我的信息</span>
          </div>
        </template>
        <el-upload
          class="avatar-uploader"
          :action="uploadParams.uploadUrl"
          :headers="uploadParams.headers"
          :on-success="handleAvatarSuccess"
          :show-file-list="false"
        >
          <img
            v-if="proFileData.avatar"
            :src="proFileData.avatar"
            class="avatar"
          />
          <el-icon v-else class="avatar-uploader-icon">
            <Plus />
          </el-icon>
        </el-upload>
        <ul>
          <li class="profile">
            <div class="profile_label">
              <User class="profile_label_icon" />
              <div class="profile_label_text">用户名</div>
            </div>
            <div>{{ proFileData.username }}</div>
          </li>
          <li class="profile">
            <div class="profile_label">
              <Avatar class="profile_label_icon" />
              <div class="profile_label_text">昵称</div>
            </div>
            <div>{{ proFileData.nickname }}</div>
          </li>
          <li class="profile">
            <div class="profile_label">
              <Iphone class="profile_label_icon" />
              <div class="profile_label_text">手机号</div>
            </div>
            <div>{{ proFileData.telephone }}</div>
          </li>
          <li class="profile">
            <div class="profile_label">
              <Message class="profile_label_icon" />
              <div class="profile_label_text">邮箱</div>
            </div>
            <div>{{ proFileData.email }}</div>
          </li>
          <li class="profile">
            <div class="profile_label">
              <Calendar class="profile_label_icon" />
              <div class="profile_label_text">创建时间</div>
            </div>
            <div>{{ proFileData.create_time }}</div>
          </li>
        </ul>
      </el-card>
    </el-col>
    <el-col :span="16">
      <el-card>
        <template v-slot:header>
          <div>
            <span>个人资料</span>
          </div>
        </template>
        <el-tabs v-model="activeName">
          <el-tab-pane label="基本资料" name="userinfo">
            <userInfo :user="proFileData" @submit="updateUser" />
          </el-tab-pane>
          <el-tab-pane label="修改密码" name="resetPwd">
            <resetPassWord @submit="resetPwd" />
          </el-tab-pane>
        </el-tabs>
      </el-card>
    </el-col>
  </el-row>
</template>

其中userInforesetPassWord是两个子组件,分别用来实现修改基本信息和修改密码的功能,后面再讲。

同时这次样式我们使用 scss 来写(用 tailwind 太麻烦)。

.fs_profile {
  min-width: 1000px;

  .avatar-uploader {
    text-align: center;
    margin: 30px 0;

    .el-upload {
      border: 1px dashed var(--el-border-color);
      border-radius: 50%;
      cursor: pointer;
      position: relative;
      overflow: hidden;
      transition: var(--el-transition-duration-fast);
      margin: 0 auto;
    }

    .avatar {
      width: 150px;
      height: 150px;
      object-fit: cover;
    }
  }

  .avatar-uploader .el-upload:hover {
    border-color: var(--el-color-primary);
  }

  .el-icon.avatar-uploader-icon {
    font-size: 28px;
    color: #8c939d;
    width: 178px;
    height: 178px;

    text-align: center;
  }

  .profile {
    display: flex;
    justify-content: space-between;
    padding: 15px 5px;
    border-top: 1px solid lightgray;

    .profile_label {
      display: flex;
      color: gray;
      align-items: center;

      .profile_label_icon {
        width: 20px;
        height: 20px;
        margin-right: 4px;
      }

      .profile_label_text {
        flex: none;
      }
    }
  }
}

信息查询

我们先看信息查询功能,实现还是比较简单的,调用接口获取用户信息,然后将数据绑定到页面上即可

import { getProfile } from "@/api/user/index";
import type { ResetForm, UserInfo } from "./types";
const proFileData = ref < Partial < UserInfo >> {};
const getProFileData = async () => {
  const { data } = await getProfile();
  proFileData.value = data;
};

其中用到了UserInfo类型,这些类型我们放在types目录下

export type UserInfo = {
  username: string,
  nickname: string,
  telephone: string,
  email: string,
  avatar: string,
  create_time: string,
};

export type ResetForm = {
  oldPassword: string,
  newPassword: string,
  confirmPassword?: string,
};

这样我们的查询功能就实现了。

基本信息修改

前面我们提到了userInforesetPassWord两个子组件,分别用来实现修改基本信息和修改密码的功能。我们先来看基本信息userInfo组件。

<template>
  <el-form ref="userRef" :model="user" label-width="80px">
    <el-form-item label="用户昵称" prop="nickName">
      <el-input v-model="user.nickname" maxlength="30" />
    </el-form-item>
    <el-form-item label="手机号码" prop="telephone">
      <el-input v-model="user.telephone" maxlength="11" />
    </el-form-item>
    <el-form-item label="邮箱" prop="email">
      <el-input v-model="user.email" maxlength="50" />
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="submit">保存资料</el-button>
    </el-form-item>
  </el-form>
</template>

<script lang="ts" setup>
  import { toRefs } from "vue";
  import type { UserInfo } from "../types";

  const userProps = defineProps<{ user: Partial<UserInfo> }>();
  //结构解出user且是响应式的
  const { user } = toRefs(userProps);
  type Emits = {
    (e: "submit", user: Partial<UserInfo>): void;
  };
  const emits = defineEmits<Emits>();
  const submit = () => {
    emits("submit", userProps.user);
  };
</script>

然后在父组件中使用它,同时传入user参数以及接收submit修改事件

<userInfo :user="proFileData" @submit="updateUser" />

最后在updateUser函数中实现修改基本信息的功能即可

const updateUser = async (data: Partial<UserInfo>) => {
  await updateUserInfo(data);
  ElMessage.success("修改成功");
  getProFileData();
};

修改密码

修改密码需要输入旧密码、新密码、确认密码。我们先来看子组件resetPassWord

<template>
  <el-form ref="pwdRef" :model="user" :rules="rules" label-width="80px">
    <el-form-item label="旧密码" prop="oldPassword">
      <el-input
        v-model="user.oldPassword"
        placeholder="请输入旧密码"
        type="password"
        show-password
      />
    </el-form-item>
    <el-form-item label="新密码" prop="newPassword">
      <el-input
        v-model="user.newPassword"
        placeholder="请输入新密码"
        type="password"
        show-password
      />
    </el-form-item>
    <el-form-item label="确认密码" prop="confirmPassword">
      <el-input
        v-model="user.confirmPassword"
        placeholder="请确认密码"
        type="password"
        show-password
      />
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="submit(pwdRef)">确认修改</el-button>
    </el-form-item>
  </el-form>
</template>

<script setup lang="ts">
  import { FormInstance } from "element-plus";
  import { ref } from "vue";
  import { ResetForm } from "../types";
  const pwdRef = ref<FormInstance>();
  const user = ref<ResetForm>({
    oldPassword: "",
    newPassword: "",
    confirmPassword: "",
  });

  type Emits = {
    (e: "submit", user: ResetForm): void;
  };
  const emits = defineEmits<Emits>();
  const equalToPassword = (
    rule: any,
    value: string,
    callback: (arg0?: Error) => void
  ) => {
    if (user.value.newPassword !== value) {
      callback(new Error("两次输入的密码不一致"));
    } else {
      callback();
    }
  };
  const rules = ref({
    oldPassword: [
      { required: true, message: "旧密码不能为空", trigger: "blur" },
    ],
    newPassword: [
      { required: true, message: "新密码不能为空", trigger: "blur" },
      { min: 6, max: 20, message: "长度在 6 到 20 个字符", trigger: "blur" },
    ],
    confirmPassword: [
      { required: true, message: "确认密码不能为空", trigger: "blur" },
      { required: true, validator: equalToPassword, trigger: "blur" },
    ],
  });

  function submit(pwdRef: FormInstance | undefined) {
    if (!pwdRef) return;
    pwdRef.validate((valid) => {
      if (valid) {
        emits("submit", user.value);
      }
    });
  }
</script>

这里使用了element-plus的表单验证功能,验证规则在rules中,定义了新旧密码不能为空及两次输入的密码一不一致等情况。比如如果不满足页面就会有所提示。

image.png

验证通过将会给父组件发送一个submit事件同时传入修改后的用户信息。父组件拿到这些信息调用修改密码接口即可。

const resetPwd = async (data: ResetForm) => {
  await updatePassword(data);
  ElMessage.success("修改成功");
  getProFileData();
};

头像上传

头像上传使用了element-plusupload组件,在页面是我们是这么使用的

<el-upload
  class="avatar-uploader"
  :action="uploadParams.uploadUrl"
  :headers="uploadParams.headers"
  :on-success="handleAvatarSuccess"
  :show-file-list="false"
></el-upload>

其中action是上传地址,headers是上传接口的请求头,这里我们需要和其它接口一样加上token不然是禁止请求的。handleAvatarSuccess是上传成功后的回调函数,可以接收到成功后的返回信息,show-file-list是是否显示文件列表。

我们看一下实现逻辑代码

import { Storage } from "@/utils/storage";

// 上传相关参数
const uploadParams = {
  uploadUrl: import.meta.env.VITE_APP_API + "/user/uploadAvatar",
  headers: { authorization: "Bearer " + Storage.get("token") },
};

//上传成功回调

const handleAvatarSuccess = (res: any) => {
  if (res.code == 200) {
    ElMessage.success("头像设置成功");
    getProFileData();
  } else {
    ElMessage.error(res.describe);
  }
};

就是这么简单。

到这里就实现了个人中心页面。下一篇文章我们将介绍如何使用 NestJS 来实现这些接口。

源码地址

vue路由入门2.0

引言

之前已经介绍过了路由的基本使用,vue路由基础入门 ,今天通过一个小案例再来熟练一下路由的使用,并且更加全面的了解路由的各种配置选项。

小栗子

screenshots.gif 目录结构:

image.png

基本的切换效果

  • App.vue代码如下
<template>
  <div class="app">
    <h2 class="title">Vue路由测试</h2>
    <header>
      <!-- 导航区 -->
      <nav class="navigate">
        <RouterLink to="/home" active-class="active">首页</RouterLink>
        <RouterLink :to="{name:'news'}" active-class="active">新闻</RouterLink>
        <RouterLink :to="{path:'/about'}" active-class="active">关于</RouterLink>
      </nav>
    </header>
    <!-- 展示区 -->
    <main class="main-content">
      <RouterView></RouterView>
    </main>
  </div>
</template>

<script setup>

</script>
  • main.js代码如下:
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router).mount('#app')

  • 路由配置文件代码如下:
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
    {
        path: '/home',
        name: 'home',
        component: () => import('../views/Home.vue')
    },
    {
        path: '/about',
        name: 'about',
        component: () => import('../views/About.vue')
    },
    {
        path: '/news',
        name: 'news',
        component: () => import('../views/News.vue')
    },
]

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

export default router
  • 路由组件通常存放在pagesviews文件夹,一般组件通常存放在components文件夹。
  • 通过点击导航,视觉效果上“消失” 了的路由组件,默认是被卸载掉的,需要的时候再去挂载

路由的工作模式

  1. history模式

    优点:URL更加美观,不带有#,更接近传统的网站URL

    缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有404错误。

     const router = createRouter({
         history:createWebHistory(), //history模式
     })
    
  2. hash模式

    优点:兼容性更好,因为不需要服务器端处理路径。

    缺点:URL带有#不太美观,且在SEO优化方面相对较差。

     const router = createRouter({
         history:createWebHashHistory(), //hash模式
     })
    

嵌套路由和路由传参

要在新闻页面展示新闻详情

  1. 编写News的子路由:Details.vue
  2. 配置路由规则,添加children配置项:
children: [
            {
                 path: 'detail',
                 name: 'detail',
                 component: () => import('../views/Details.vue'),
            }
        ]

3. 记得去News组件中预留一个<RouterView/> 4. 路由传参

1.query参数

传递参数

<template>
    <div class="news">
        <ul>
            <li v-for="item in newsList" :key="item.id">
                <!-- query传参 -->
                <!-- to的对象写法-->
                <RouterLink :to="{name:'detail', query:{
                    id:item.id,
                    title:item.title,
                    content:item.content
                }}">{{item.title}}</RouterLink>
            </li>
        </ul>
        <div class="content">
            <RouterView/>
        </div>  
    </div>
</template>
  • to的字符串写法:
<router-link :to="`/news/detail?id=${item.id}&title=${item.title}&content=${ item.content}`">{{item.title}}</router-link>

接收参数

  • 在子路由中通过useRoute()方法拿到当前路由实例
<template>
    <div>
        <ul>
            <li>编号:{{ route.query.id }}</li>
            <li>编号:{{ route.query.title }}</li>
            <li>编号:{{ route.query.content }}</li>
        </ul>
    </div>
</template>
<script setup>
import {useRoute} from 'vue-router'
let route = useRoute()
console.log(route.query)
</script>

image.png

  • 使用 query 传参时,参数不会成为路径的一部分,而是作为查询字符串附加在 URL 的末尾。
  • URL 形式

image.png

  • 访问参数:可以通过 route.query.title 和 route.query.content 在组件内部访问这些参数
2.params参数
  • 传递params参数时,需要提前在规则中占位。
path:'detail/:id/:title/:content',

传递参数

<RouterLink :to="{name:'detail', params:{
     id:item.id,
     title:item.title,
     content:item.content
}">{{item.title}}</RouterLink>
  • to的字符串写法:
<RouterLink :to="`/news/detail/${item.id}/${item.title}/${item.content}`">{{item.title}}</RouterLink>

接收参数

  • 在子路由中通过useRoute()方法拿到当前路由实例
<template>
    <div>
        <ul>
            <li>编号:{{ route.params.id }}</li>
            <li>编号:{{ route.params.title }}</li>
            <li>编号:{{ route.params.content }}</li>
        </ul>
    </div>
</template>

<script setup>
import {useRoute } from 'vue-router'
let route = useRoute()
console.log("params:",route.params)
</script>

image.png

  • 使用 params 传参时,参数会成为路径的一部分,并且是通过定义的动态段(dynamic segments)来表示的
  • URL 形式

image.png

  • 访问参数:可以通过 route.params.id 在组件内部访问这个参数。
  • 传递params参数时,若使用to的对象写法,必须使用name配置项,不能用path

props配置

作用:让路由组件更方便的收到参数(可以将路由参数作为props传给组件),不需要在组件内部手动从 route 对象中提取这些值

  • 三种写法
{
name:'detail',
path:'detail/:id/:title/:content',
component:Detail,

  // props的对象写法,作用:把对象中的每一组key-value作为props传给Detail组件
  // props:{id:1,title:新闻1,conten:新闻内容1}, 

  // props的布尔值写法,作用:把收到了每一组params参数,作为props传给Detail组件
     props:true
  
  // props的函数写法,作用:把返回的对象中每一组key-value作为props传给Detail组件
  props(route){
    return route.query
  }
}
  • Details路由组件更方便的收到参数
<template>
    <div>
        <ul>
            <li>编号:{{ id }}</li>
            <li>编号:{{ title }}</li>
            <li>编号:{{ content }}</li>
        </ul>
    </div>
</template>
<script setup>
defineProps(['id','title','content'])
</script>
  • props 和 params 的区别:当使用 props: true 时,只有路径参数 (params) 会被传递给组件。如果你想传递查询参数 (query) 或者其他的 $route 属性,你需要使用对象或者函数的方式来定义 props
  • 确保 props 名称匹配:确保你在路由配置中指定的 props 名称与组件内声明的 props 相匹配,否则组件将无法正确接收这些属性。

编程式导航

编程式路由导航指的是在 Vue 应用中通过 JavaScript 代码来实现页面之间的跳转,而不是依赖于用户点击 <RouterLink> 组件

<template>
    <div class="news">
        <ul>
            <li v-for="item in newsList" :key="item.id">
                   <button @click="showNewsDetail(item)">新闻详情</button>
            </li>
        </ul>
        <div class="content">
            <RouterView/>
        </div>
      
    </div>
</template>

<script setup>
import{reactive} from 'vue'
import {useRouter} from 'vue-router'
const newsList = reactive([
    {
        id:1,
        title:'新闻1',
        content:'新闻内容1'
    },
    {
        id:2,
        title:'新闻2',
        content:'新闻内容2'
    },
    {
        id:3,
        title:'新闻3',
        content:'新闻内容3'
    }
])

const router = useRouter()
const showNewsDetail = (item) => {
    router.push({
        name:'detail', 
        params:{
            id:item.id, 
            title:item.title, 
            content:item.content}
    })
}
</script>
  • to能怎么写,push就能怎么写(字符串路径,路径描述对象,命名路由)。

重定向

  1. 作用:将特定的路径,重新定向到已有路由。

  2. 具体编码:

     {
         path:'/',
         redirect:'/home'
     }
    
  • 项目启动后自动跳转到首页

路由元信息和全局前置守卫

路由元信息(Route Meta Fields)是 Vue Router 提供的一种机制,允许你在定义路由时附加额外的信息到每个路由对象上。这些元信息可以包含任何你想要的数据,比如权限控制所需的标识、页面标题、是否需要认证等。它们不会影响路由的行为,但可以在导航守卫或组件内部访问,以实现更复杂的应用逻辑。

定义路由元信息

在定义路由时,你可以通过 meta 字段为每个路由添加任意数量的自定义属性。

const routes = [
    {
        path: '/home',
        name: 'home',
        meta: {
            title: '首页'  //标题
        },
        component: () => import('../views/Home.vue')
    },
    {
        path: '/about',
        name: 'about',
        meta: {
            title: '关于', //标题
            requiredAuth: true  //权限
        },
        component: () => import('../views/About.vue')
    },
    {
        path: '/news',
        name: 'news',
        meta: { 
            title: '新闻'  // 标题
        },
        component: () => import('../views/News.vue'),
        children: [
            {
                 path: 'detail',
                 name: 'detail',
                 meta: {
                     title: '新闻详情' //标题
                 }
                 component: () => import('../views/Details.vue'),
            }
        ]
    },
    {
        path: '/',
        redirect:'/home'
    }
]
在全局前置守卫中使用

全局前置守卫(Global Before Guards)是 Vue Router 提供的一种导航守卫,用于在每次路由导航开始前执行某些逻辑。它允许你在用户从一个路由跳转到另一个路由之前对这次跳转进行拦截和处理。全局前置守卫对于实现权限控制、加载指示器、页面标题设置等全局性的操作非常有用。 可以通过 router.beforeEach 方法来注册一个全局前置守卫。这个方法接受一个回调函数作为参数,该回调函数会在每次路由变化时被调用。回调函数接收三个参数:

  • to: 即将要进入的目标路由对象。

  • from: 当前导航正要离开的路由对象。

  • next: 一个函数,必须调用它来解析当前的守卫。next() 可以带有一个参数:

    • 不带参数:继续下一个钩子或完成导航。
    • 带有路径字符串或位置描述对象:重定向到新的路径。
    • false: 中断当前的导航。
router.beforeEach((to, from, next) => {
    document.title = to.meta.title || 'Vue Router'  //设置标题
    if (to.meta.requiredAuth) {
         next('/') // 权限控制
    }else next()
})

next 回调:确保每个全局前置守卫中的 next 回调只被调用一次,否则会导致警告甚至在生产环境中使导航失败

❌