黑马Vue_day13

35 阅读14分钟

第1个

动态引入组件

在配置路由时,写成 component: () => import('@/views/login/LoginPage.vue') 函数形式,是为了实现 路由懒加载(Route Lazy Loading) 。

这是前端性能优化的核心手段之一。如果对比直接导入的写法,你能更直观地理解它的作用:

一、 两种写法的对比

1. 传统写法:直接导入(静态导入)

JavaScript

import LoginPage from '@/views/login/LoginPage.vue' // 在文件最顶部引入

const routes = [
  { path: '/login', component: LoginPage } // 直接把组件赋值给 component
]
  • 工作原理:当用户访问网站首页(哪怕根本没点击登录页)时,浏览器在一开始就会把所有的页面代码全部下载下来。

2. 函数形式写法:动态导入(路由懒加载)

JavaScript

const routes = [
  // component 变成了一个返回 Promise 的函数
  { path: '/login', component: () => import('@/views/login/LoginPage.vue') }
]
  • 工作原理:利用 ES6 的 import() 动态导入函数。当项目打包时,打包工具(如 Vite 或 Webpack)会把 LoginPage.vue 单独拆分成一个小 JS 文件。只有当用户真正点击或访问 /login 这个网址时,浏览器才会去临时下载对应的 JS 代码。

二、 为什么要用函数形式?(核心优势)

1. 极大地提升首屏加载速度(解决“白屏”问题)

如果你的项目有 50 个页面,如果全部采用传统静态导入:

  • 打包后会生成一个巨型 main.js 文件(包含 50 个页面的所有代码)。

  • 用户第一次打开首页时,必须等这个巨大文件彻底下载完才能看到画面,页面会出现漫长的白屏。

而采用函数形式(懒加载):

  • 打包工具会把 50 个页面拆分成 50 个“小包”。

  • 用户打开首页时,只下载首页用到的代码(可能只有几百 KB),瞬间就能打开页面。

2. 按需加载,节省流量

用户可能进了你的网站只逛了首页,根本没去访问管理后台或设置页面。采用函数形式,那些他没去过的页面代码就永远不会下载,既省用户的手机流量,也节省服务器的带宽。

三、 它的底层原理是什么?

在 JavaScript 中,() => import('...') 是一个箭头函数。

Vue Router 内部在路由切换时,会检查 component 属性:

  1. 如果它发现 component 传的是一个普通组件(对象),它就直接渲染。

  2. 如果它发现 component 传的是一个函数,Vue Router 会在网址匹配成功的这一刻,才去执行这个函数。

  3. 函数执行 import(...) 后,会发下一个网络 HTTP 请求去拉取这个页面组件代码,下载成功后把它渲染到 <router-view> 中。

💡 开发建议

  • 首页 / 登录页:因为是用户一定会访问的页面,有时也可以直接 import 引入。

  • 其他绝大多数业务页面(如:用户中心、设置页、详情页等) :一律强烈建议写成 () => import(...) 的函数形式!

动态导入原理

当你在 Vue Router 中写下 component: () => import('@/views/login/LoginPage.vue') 时,构建工具(Vite 或 Webpack)在编译阶段会识别出这里的 import() 函数,并自动触发 代码分割(Code Splitting) 技术。

它们的具体工作原理和分包机制如下:

一、 静态导入 vs 动态导入在编译时的差异

1. 静态导入 (import Login from '...')

在打包时,打包工具会将所有通过静态 import 引入的代码沿着依赖树递归合并,最终打进同一个主 Bundle JS 文件中(如 index-a1b2c3.js)。

2. 动态导入 (() => import('...'))

import('...') 是 ES6 规范的原生语法,具有语法特殊性:

  • 当打包工具(Vite 底层的 Rollup 或 Webpack)在扫描代码 AST(抽象语法树)时,一旦遇到 import('...'),就会将其认定为一个隐式的分割点(Split Point) 。

  • 编译器会自动切断主文件的依赖链,把 import() 路径所指向的模块及其独有的子依赖,单独打包提取成一个新的独立文件( Chunk File,如 LoginPage-x8y9z0.js)。

二、 Vite 与 Webpack 的具体分包机制

虽然最终目的相同,但 Vite(基于 Rollup)和 Webpack 在实现细节上略有不同:

1. Vite / Rollup 的分包逻辑
  • 自动识别与生成:Rollup 原生对 ES Module 支持极佳。每检测到一个 import(),它就会为该模块自动生成一个新的 Chunk。

  • 自定义文件名:如果不做特殊配置,Vite 会根据组件的文件名或 Hash 生成文件名。如果需要自定义打包后的文件名,可以使用 Rollup 的 manualChunks 配置,或者依赖 Rollup 的智能分包:如果两个懒加载路由共同引用了一个公共组件,Rollup 会自动将该公共组件抽离成第三个共享 Chunk,避免重复打包。

2. Webpack 的分包逻辑

在 Webpack 中,这个功能依赖其内置的 SplitChunksPlugin:

  • 当遇到 import() 时,Webpack 会将其解析为一个异步 Promise 节点。

  • 魔术注释(Magic Comments) :Webpack 允许你在 import() 函数内部写注释来控制打包行为,例如自定义文件名:

    JavaScript

    component: () => import(
      /* webpackChunkName: "login-group" */ 
      '@/views/login/LoginPage.vue'
    )
    

    加上这行注释后,打包出来的文件就会叫 login-group.js。如果有多个路由配置了相同的 webpackChunkName,Webpack 会自动将它们合并打包到同一个 Chunk 文件中(即路由分组打包)。

三、 运行时(浏览器端)是如何加载这个分包的?

打包完成后,原有的 () => import('...') 代码会被编译转换成一段网络加载逻辑:

  1. 路由未匹配时:

    浏览器只加载了核心的 main.js,其他路由的 Chunk 文件(如 LoginPage.js)躺在服务器上,根本不会占用用户的下载流量和内存。

  2. 用户点击链接(匹配到 /login)时:

    • Vue Router 执行 component 函数,内部调用被编译后的 import() 函数。

    • 该函数在浏览器端会动态向 DOM 中插入一个 <script src="/assets/LoginPage-x8y9z0.js"></script> 标签(或通过 fetch 异步获取)。

    • 脚本下载并解析完成后,Promise 状态变为 resolved,返回组件模块。

    • Vue Router 拿到组件,完成渲染并在 <router-view> 中展示。

四、 性能补充:Prefetch 与 Preload(预加载)

既然懒加载需要临时下载文件,会不会导致用户点击时卡顿一下?

打包工具通常支持预加载机制来解决这个问题:

  • Vite:默认会自动为动态导入注入 <link rel="modulepreload">,在浏览器空闲时静默预加载这些 Chunk,等用户点击跳转时,文件已经在浏览器缓存里了,实现“秒开”。

  • Webpack:可以通过魔术注释 /* webpackPrefetch: true */,让浏览器在网络空闲(Idle)时提前把懒加载文件下载好。

第2个

页面请求情况

答案是:不一定。这完全取决于你使用的路由配置方式、是否开启了预加载,以及组件内部的代码逻辑。

我们可以把情况分为以下几种常见场景:

1. 会请求服务器的情况(首次访问懒加载路由)

如果你使用的是前面提到的 路由懒加载 写法:

JavaScript

{ path: '/login', component: () => import('@/views/login/LoginPage.vue') }
  • 首次切换到该组件时:浏览器会向服务器发送 HTTP 请求。

    • 请求的是什么? 请求的是该组件对应的 JS 代码文件/静态资源(例如 LoginPage.a1b2c3.js),而不是数据。

    • 请求的是数据吗? 如果该组件的 onMounted 或 created 生命周期函数里写了 axios.get(...) 发送接口请求,那么组件下载完成后,还会接着向后端服务器发送接口请求去拿数据。

2. 不会请求服务器的情况

情况 A:重复切换已下载过的路由组件
  • 当你从 /home 切换到 /login,再切回 /home 时,不会重新请求组件的 JS 代码文件。因为浏览器和 Vue Router 已经把该组件的代码缓存到了本地内存中。

  • 注意:如果你的组件生命周期(如 onMounted)里写了发送 API 接口请求的代码,虽然不请求 JS 文件,但依然会向后端发送 API 接口请求获取最新数据(除非你做了前端数据缓存)。

情况 B:使用的是静态导入(未配置懒加载)

如果你的路由是用普通方式导入的:

JavaScript

import LoginPage from '@/views/login/LoginPage.vue'
  • 当用户第一次打开网站首页时,所有页面的 JS 代码就已经一次性全部下载完了。

  • 此时在不同页面间切换,完全不需要向服务器请求任何 JS 文件,切换过程纯粹是在前端内存中替换 DOM 节点,速度极快。

情况 C:开启了 <keep-alive> 缓存组件

如果你在 Vue 中使用了 <keep-alive> 将组件包裹起来:

HTML

<router-view v-slot="{ Component }">
  <keep-alive>
    <component :is="Component" />
  </keep-alive>
</router-view>
  • 当组件被缓存后,再次切换回该组件时,既不会重新下载 JS 文件,也不会重新触发 onMounted 里的 API 接口请求(除非写在 onActivated 钩子中)。

总结速查

场景是否向服务器请求组件代码(JS文件)?是否向后端请求业务数据(API接口)?
首次切入“懒加载路由”会(请求拆分出的 JS 包)会(如果在组件挂载时写了 Ajax/Axios)
再次切入已访问过的路由不会(利用浏览器本地缓存)会(只要写了 onMounted 发请求就会重新触发)
静态导入的路由组件不会(首页时已一次性下载完)会(由组件内部逻辑决定)
使用 <keep-alive> 包裹的组件不会不会(组件状态和数据被冻结在内存中)

第3个

element plus的span,offset

在 Element Plus(或 Element UI)的栅格系统(Layout 布局)中,:span 和 :offset 是用来控制组件宽度和位置偏移的两个核心属性。

它的底层逻辑非常简单:Element 把一整行的宽度均匀分成了 24 等份(你可以把整行想象成一个由 24 格组成的栅格)。

一、 两个属性的大白话解释

1. :span="6" (占几格 / 宽度)
  • 含义:指定当前列(<el-col>)占用 24 格中的多少格。

  • 计算:6/24=25%6 / 24 = 25\%。

  • 效果:这个组件的宽度会占据整行宽度的 1/4 (25%) 。

2. :offset="3" (左边空几格 / 偏移量)
  • 含义:指定当前列向右偏移(推开)多少格的距离(相当于在左侧加了 margin-left)。

  • 计算:3/24=12.5%3 / 24 = 12.5\%。

  • 效果:这个组件的左边会空出 12.5% 的空白距离。

二、 组合起来看你的这行代码

HTML

<el-row>
  <el-col :span="6" :offset="3" class="form">
    <!-- 你的表单内容 -->
  </el-col>
</el-row>

实际排版效果图解:

Plaintext

|--- 3格空白 (12.5%) ---|--- 6格表单内容 (25%) ---|---------------- 15格剩余空白 (62.5%) ----------------|
├───────────────  整 行 宽 度(共 24 格) ────────────────────────────────────────────────────────┤
  • 左边先产生 3 格的留白;

  • 紧接着展示 6 格宽度的表单;

  • 剩下右边还空着 15 格(24−3−6=1524 - 3 - 6 = 15)。

三、 常见应用场景

  1. 表单居中/居左微调:比如你想让一个宽度为 12 格(半屏宽)的表单在页面中间显示,就可以设置 :span="12" 并设置 :offset="6"(因为左边空 6 格,中间占 12 格,右边就自动剩下 6 格,实现了居中:6+12+6=246 + 12 + 6 = 24)。

  2. 两列拉开间距:

    HTML

    <el-row>
      <!-- 第一列占 8 格 -->
      <el-col :span="8">左边内容</el-col>
      <!-- 第二列占 8 格,并且往右推 4 格,这样两列中间就有了 4 格的空隙 -->
      <el-col :span="8" :offset="4">右边内容</el-col>
    </el-row>
    

总结:

  • span 控制自己有多宽(最大 24)。

  • offset 控制左边留白多宽(往右推)。

user-select 是 CSS 中的一个样式属性,用来控制用户是否能够在网页上选中文本或元素内容(即用鼠标拖动划选、或者按住双击高亮文本的操作)。

use-select css

一、 常见属性值及效果

CSS

/* 语法示例 */
.element {
  user-select: none;
}
属性值效果说明常见使用场景
none禁止选中。用户无法用鼠标拖拽划选该元素内的任何文本。按钮、菜单、图标、游戏画布、防止用户轻易复制内容。
text允许选中文本(默认行为之一)。用户可以正常划选文本。文章段落、新闻正文、博客内容。
all一键全选。只要用户点击该元素的任意位置,整个元素内的文本就会被一次性全部选中。代码块、长链接、卡密/邀请码框、方便用户一键复制。
auto默认值/自动。根据浏览器的默认行为决定(通常文本可选中,按钮等不可选中)。普通网页元素。

二、 实际应用场景示例

1. 防止按钮文字被意外选中(提升 UI 体验)

当你频繁快速点击一个自定义按钮(比如 + / - 数量按钮,或者连点游戏)时,如果不加 user-select: none,文字很容易被蓝色高亮划选,非常影响视觉体验:

CSS

/* 让按钮看起来更像原生 App,多次点击不会选中文字 */
.custom-button {
  user-select: none;
  cursor: pointer;
}
2. 方便用户复制内容(一键选中)

如果你页面上有一个优惠码或者代码片段,希望用户点一下就能全部高亮方便复制:

CSS

/* 点击一下就会全选 "VIP666" */
.promo-code {
  user-select: all;
}

三、 兼容性与补充说明

  1. 兼容性写法:

    在一些较老的浏览器(如旧版 Safari 或 Edge)中,可能需要加上浏览器前缀:

    CSS

    .no-select {
      -webkit-user-select: none; /* Safari / Chrome 旧版本 */
      -moz-user-select: none;    /* Firefox 旧版本 */
      -ms-user-select: none;     /* IE / Edge 旧版本 */
      user-select: none;         /* 标准语法 */
    }
    
  2. 不能防真正的“防偷 / 防复制” :

    设置 user-select: none 只是限制了前端页面的 UI 交互(鼠标划选)。如果是懂技术的用户,依然可以通过浏览器的 F12 开发者工具直接提取 DOM 里的文字,或者通过禁用 CSS 来绕过。

第4个

表单校验

表单校验规则的本质,就是按顺序列出一组“安检标准” 。只有用户输入的内容依次通过了所有的标准,输入框下方才不会报错。

以下为您逐块拆解每个配置的含义,以及那个看起来最复杂的 validator(自定义校验器):

一、 先看通用属性的大白话解释

每个规则对象 { ... } 里面都有几个常用属性:

  • required: true:非空校验。必须填写,如果不填或删光了,就会报错。

  • message:报错提示信息。当校验不通过时,输入框下方显示的红色小字。

  • trigger: 'blur' :触发时机。blur 表示失去焦点时触发(即用户鼠标点击别处、离开当前输入框的瞬间进行检查)。

  • min / max:长度限制。规定最少和最多输入的字符数。

  • pattern:正则表达式。用来检查格式是否符合特定规则(如:必须是手机号、密码不能带空格等)。

二、 逐条拆解你的校验代码

1. 用户名 (username)

JavaScript

username: [
  // 规则 ①:不能留空,否则提示“请输入用户名”
  { required: true, message: '请输入用户名', trigger: 'blur' },
  // 规则 ②:字数必须在 5 到 10 个字符之间,否则提示“用户名必须是5-10位的字符”
  { min: 5, max: 10, message: '用户名必须是5-10位的字符', trigger: 'blur' }
]
2. 密码 (password)

JavaScript

password: [
  // 规则 ①:不能留空
  { required: true, message: '请输入密码', trigger: 'blur' },
  // 规则 ②:必须符合正则表达式 /^\S{6,15}$/
  {
    pattern: /^\S{6,15}$/, 
    message: '密码必须是6-15位的非空字符',
    trigger: 'blur'
  }
]

💡 正则表达式 /^\S{6,15}$/ 解读:

  • \S:匹配非空白字符(即密码中间绝对不能包含空格、换行或 Tab)。

  • {6,15}:长度要求为 6 到 15 位。

3. 确认密码 (repassword) —— 最核心的逻辑

JavaScript

repassword: [
  // 规则 ①:不能留空
  { required: true, message: '请再次输入密码', trigger: 'blur' },
  
  // 规则 ②:格式也必须是 6-15 位非空字符
  {
    pattern: /^\S{6,15}$/,
    message: '密码必须是6-15的非空字符',
    trigger: 'blur'
  },
  
  // 规则 ③:自定义校验器(解决“两次输入的密码是否一致”问题)
  {
    validator: (rule, value, callback) => {
      // value: 用户在【确认密码】框里填写的字符串
      // formModel.value.password: 用户在【密码】框里填写的字符串
      
      if (value !== formModel.value.password) {
        // 如果两次输入的不一样,抛出错误,提示红色小字
        callback(new Error('两次输入密码不一致!'))
      } else {
        // 如果两次输入的一样,直接调用 callback(),代表校验“通过!”
        callback()
      }
    },
    trigger: 'blur'
  }
]

三、 重点解读:validator(自定义校验函数)

像“字数限制”、“正则匹配”这种简单的需求,Element 给我们提供了 min/max 或 pattern。

但如果遇到需要两个字段相互对比的复杂逻辑(比如:确认密码必须等于密码),自带的规则就不够用了。这时就需要用到 validator 自定义校验。

它接收三个固定参数:

  1. rule:当前的校验规则配置信息(一般用不上)。

  2. value:当前输入框里用户填写的值(即确认密码框里的值)。

  3. callback:通报结果的“裁判”函数(非常关键!):

    • 校验失败:必须调用 callback(new Error('报错信息'))。

    • 校验成功:必须无参调用 callback()。

💡 绝大多数初学者会踩的坑:

在写 validator 时,无论成功还是失败,都必须保证 callback() 一定会被执行!如果漏掉了 else { callback() },表单提交时就会卡住无法完成最终的 validate() 验证。