阅读视图
vue3 的预编译模板
在 Vue 3 中,预编译模板(Precompiled Templates) 是指在 构建阶段 就将 Vue 模板(<template> 中的内容)编译为渲染函数(Render Function),而非在浏览器运行时动态编译的技术。其核心目标是优化性能、减少运行时开销,并规避浏览器环境的编译限制。
一、先明确:模板的两种编译方式
Vue 模板要被浏览器执行,最终必须转为 JavaScript 渲染函数(虚拟 DOM 描述)。编译过程分两种:
| 类型 | 编译时机 | 执行环境 | 核心特点 |
|---|---|---|---|
| 运行时编译(Runtime) | 浏览器运行时 | 浏览器 | 模板字符串 → 渲染函数,需带编译器,体积大 |
| 预编译(Precompile) | 项目构建阶段 | 构建工具(webpack/vite) |
<template> → 渲染函数,运行时无需编译器 |
| Vue 3 的预编译模板,本质就是选择“构建时编译”,提前完成模板到渲染函数的转换。 |
二、预编译模板的核心作用
1. 减小运行时体积
Vue 3 的核心包分为两种:
-
vue:包含编译器 + 运行时(体积较大,约 10KB+ 额外开销); -
-
@vue/runtime-dom:仅包含运行时(体积更小,约 3KB+)。 预编译后,项目运行时无需编译器,可直接引入@vue/runtime-dom,大幅减小最终打包体积(尤其对小型项目/移动端友好)。
-
2. 提升运行时性能
模板编译是耗时操作(需解析 HTML 字符串、处理指令/插值、生成渲染函数)。预编译将这一步移到构建阶段,浏览器运行时直接执行现成的渲染函数,避免了 runtime 编译的性能开销,首屏渲染和组件更新速度更快。
3. 规避浏览器环境限制
运行时编译依赖 new Function() 解析模板,部分严格的浏览器安全策略(如 CSP 限制)会禁止该 API,导致模板无法编译。预编译生成的是纯 JS 函数,无此限制,兼容性更优。
4. 更早发现模板错误
编译过程中会校验模板语法(如闭合标签、指令格式、变量引用),错误会在构建阶段抛出(而非运行时),便于更早排查问题。
三、预编译模板的工作原理
以 Vue 3 + Vite 项目为例,预编译流程如下:
-
开发/构建阶段: - 项目中的
.vue文件会被@vue/compiler-sfc(Vue 单文件组件编译器)处理; - 编译器解析<template>中的 HTML 结构、指令(v-if/v-for)、插值({{ }})等; - 将模板转换为 优化后的渲染函数(包含虚拟 DOM 创建逻辑、指令处理逻辑)。 -
运行时阶段: - 打包后的代码中,
.vue组件的render选项已直接是预编译好的函数; - Vue 运行时只需执行该函数,生成虚拟 DOM,再渲染为真实 DOM,无需额外编译步骤。
示例:模板 → 预编译后的渲染函数
原始模板(.vue 文件):
<template>
<div class="greeting">
Hello, {{ name }}!
<p v-if="show">Vue 3 预编译模板</p>
</div>
</template>
<script setup>
const name = "Vue";
const show = true;
</script>
预编译后生成的渲染函数(简化版):
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "@vue/runtime-dom";
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createBlock(
"div",
{ class: "greeting" },
[
_createVNode("text", null, "Hello, " + _toDisplayString(_ctx.name) + "!"), _ctx.show
? _createVNode("p", null, "Vue 3 预编译模板")
: _createVNode("text", null, "")
]
);
}
运行时 Vue 直接执行 render 函数,无需解析模板字符串。
四、如何启用预编译模板?
Vue 3 项目(尤其是 Vite 或 Vue CLI 搭建的项目)默认已启用预编译,无需额外配置:
-
Vite:内置
@vitejs/plugin-vue,自动处理.vue文件的模板预编译; -
Vue CLI:基于 webpack,通过
vue-loader集成@vue/compiler-sfc,自动预编译模板。
关键配置验证(以 Vite 为例)
vite.config.js 中只需引入 Vue 插件,即自动开启预编译:
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// 自动处理 .vue 模板预编译 export default defineConfig({ plugins: [vue()], });
手动确认预编译生效
-
打包后查看输出的 JS 文件,不会包含
vue核心包(而是@vue/runtime-dom); -
组件代码中无模板字符串,直接是
render函数。
五、预编译的适用场景与注意事项
适用场景 - 绝大多数 Vue 3 项目(尤其是生产环境),优先使用预编译;
-
对包体积、性能敏感的场景(如移动端、小程序);
-
存在浏览器 CSP 限制的场景。
注意事项
-
仅支持静态模板:预编译仅对
.vue文件中<template>标签的静态模板生效;若通过template选项传入动态字符串(如template: '<div>{{ msg }}</div>'),仍会触发运行时编译(需引入完整vue包)。
// 不推荐:动态模板字符串,无法预编译,需运行时编译
import { createApp } from "vue";
createApp({
template: "<div>{{ msg }}</div>", // 运行时编译,需引入完整 vue
data() { return { msg: "Hello" }; },
}).mount("#app");
-
依赖构建工具:预编译依赖 Vite/webpack 等构建工具,纯 HTML 引入 Vue 3 时(如
<script src="vue.global.js"></script>),无法使用预编译,只能用运行时编译。 -
编译缓存:构建工具会缓存预编译结果,修改模板后需重新构建(开发环境热更新已自动处理)。
总结
Vue 3 的预编译模板,是将模板编译工作从“浏览器运行时”提前到“项目构建时”的优化技术。核心价值是 减小包体积、提升运行时性能、规避环境限制,且 Vue 3 生态的主流构建工具(Vite/Vue CLI)已默认集成,开箱即用。
简单说:预编译 = 提前编译模板 → 运行时直接用 → 更快、更小、更兼容。
浏览器自动化革命:从 Selenium 到 AI Browser 的 20 年进化史
自动化会执行,但不智能;AI 智能,却不会动手。新的时代来临,是时候让两者合体了。
🧱 01|自动化的时代矛盾:机器会“点点点”,但不会“理解你”
如果你做过传统自动化,你一定经历过这些场景:
- 元素定位一天一变
- 等待写到怀疑人生
- 脚本第一天还没完成,完成的部分第二天就失效了
- 页面加一个 loading,脚本全挂
传统自动化有一条隐形天花板:能点、能输、能跳——但完全“不理解你”,像是缺少了灵魂。而 AI 恰好相反:能理解、能推理、能规划——但不会真的动手,像是缺少了身体。浏览器自动化走到 2025 年,迎来了关键提问:如何让“懂你”的 AI,指挥“会动手”的浏览器来大干一场?
🕰 02|技术时间线:浏览器自动化 20 年进化史
从 2004 到 2025,浏览器自动化技术经历了四个时代:
-
2004|Selenium:自动化的“蒸汽机”
Selenium 的诞生标志着浏览器自动化的开端,如同工业革命中的蒸汽机,它首次让测试脚本能够驱动真实浏览器操作,开启了自动化测试的新纪元。 -
2017|Puppeteer:Chrome 的官方遥控器
由 Google 推出,Puppeteer 通过 DevTools 协议深度控制 Chrome,支持无头模式、截图、PDF 生成等,极大提升了前端自动化效率,成为现代 Web 自动化的利器。 -
2019|Playwright:跨浏览器一致性革命
微软推出的 Playwright 支持 Chromium、Firefox 和 WebKit,统一 API 实现跨浏览器自动化,具备自动等待、网络拦截等特性,解决了长期存在的兼容性难题。 -
2024|AI Browser Use:AI 代理正式接管浏览器
随着 AI 技术成熟,AI 开始直接操作浏览器,理解自然语言指令完成复杂任务,如自动购物、表单填写、数据抓取,标志着自动化从“脚本驱动”迈向“智能代理”时代。
每一代框架,都解决前一代的弱点,但也留下新的限制。
🧩 Selenium:自动化的原点
诞生于 2004 年的 Selenium,让程序第一次能像用户一样点按钮、填表单。
它开创了一个时代,但也带来自动化三大噩梦:
- ❌ 等待混乱(sleep 写到怀疑人生)
- ❌ 跨浏览器差异巨大
- ❌ 选择器脆弱,一改版就全挂
但若没有 Selenium,就没有今天的一切。
await driver.get('https://www.google.com');
await driver.findElement(By.name('q'))
.sendKeys('playwright tutorial', Key.RETURN);
await driver.wait(until.elementLocated(By.css('#search a h3')));
可以看到Selenium的一切都是要靠我们的脚本自己管理的,手写等待 + 手写定位 + 依赖 WebDriver。
⚡ Puppeteer:Chrome 的“官方遥控器”
2017 年,Google 团队推出 Puppeteer。
一句话概括:
Puppeteer = 用 Node.js 操控 Chrome 的官方 CDP 遥控器。
对比Selenium,Puppeteer的优势明显:
- 更快
- 更稳定
- 更贴近真实浏览器行为
但问题是它只能操控 chrome,毫无跨端可言。
await page.goto('https://www.google.com');
await page.type('input[name="q"]', 'playwright tutorial');
await page.waitForSelector('#search a h3');
对比上一代产品Selenium,Puppeteer的脚本没有太大改变,依然需要自己手写等待。
🚀 Playwright:真正现代化的自动化框架
Playwright 由微软开发,它带来了浏览器自动化的“质变”。
🧠 01|自动等待:Smart Wait
Playwright 会自动判断:
- 元素是否出现
检查目标元素是否已成功渲染在页面中,可通过 DOM 查询确认其存在性。 - 是否可交互
验证元素是否处于可操作状态,如按钮是否启用、输入框是否可编辑等。 - 页面是否跳转完成
确认当前 URL 是否已更新至目标页面,并确保页面内容加载完毕。 - 渲染是否完成
判断页面或组件的视觉渲染是否结束,避免因异步加载导致的断言失败。
无需再写一堆手动等待,这是 “自动化 → 智能化” 的第一步。
await page.goto('https://www.google.com');
await page.getByRole('combobox').fill('playwright tutorial');
const title = await page.locator('#search a h3').first().textContent();
虽然第三步也是在等待,但Playwright 的“await”不是等待代码执行完,而是等待浏览器进入“正确可操作的状态”。而Puppeteer、Selenium 的 await则是“你告诉我等什么,我就等什么”。Playwright 的 await:“我自动判断所有能够造成失败的因素,并等到安全能操作的时刻。”
🧪 02|Browser Context:一个浏览器,多个独立世界
这是Playwright 最强大的能力之一:
一个 Browser → 多个 Context(互相隔离)
这意味着:
- A 用户登录
- B 用户登录
- C 用户登录
都能在同一个浏览器内无干扰执行,并行能力极强。这是未来 AI Agent 并发执行任务 的核心基础。
🌐 03|跨浏览器一致性:真正的“一次写代码,到处运行”
Playwright 内部维护三大内核的统一协议:
- Chromium
- WebKit
- Firefox
让代码行为在不同浏览器中完全一致。
🕵 04|网络 Mock、抓包、拦截,自动化真正强大起来
- 拦截请求:可捕获并暂停网络请求,便于查看或修改请求内容,实现调试与测试目的
- Mock 响应:自定义返回数据,模拟接口不同状态(如成功、错误、空数据),提升前端独立开发效率
- 模拟弱网:设置网络延迟、低带宽或高丢包率,测试应用在较差网络环境下的表现与稳定性
- 劫持 cookies:读取、修改或注入 Cookie 信息,用于测试登录状态、会话保持等场景
- 修改 headers:自定义请求头字段(如 User-Agent、Authorization),便于测试鉴权、设备适配等逻辑
- 修改 geolocation:模拟不同地理位置,测试基于位置的服务(LBS)功能是否正常响应
自动化从“点击按钮”升级到“控制网络宇宙”。
🎥 05|Trace:自动记录 → 回放 → 调试
Trace 看起来像这样:
- 每一次点击
- 每一帧 DOM 快照
- 网络日志
- console 输出
这些全部可视化,还可回放。
这让自动化调试从“猜谜”变成“看片”。
🤖 AI Browser Use:AI 第一次真正“接管”浏览器
2024 年之后,“Browser Use” 出现了。
它彻底改变了自动化的范式:你不必写脚本,只需说话。AI 会自动控制浏览器。
例如,你说:
“打开xxx,登录账号,搜索xxx”
AI 会自动执行以下步骤:
- 打开网址
- 定位登录输入框
- 输入账号与密码
- 尝试处理验证码(若存在)
- 输入关键词
- 点击搜索按钮
整个过程无需编写任何代码,真正实现“说即执行”。
通过自然语言驱动浏览器操作,自动化变得前所未有的直观与高效。
无论是数据采集、批量操作,还是日常重复任务,只需一句话,AI 即可代为完成。
这标志着人机交互进入新阶段——以对话为中心的自动化时代。
await agent.run(`
打开 Google;
搜索 "playwright tutorial";
读取第一个搜索结果标题;
`);
你写的不是脚本,而是“意图”。Browser Use通过Playwright & Agent 自动规划来实习你的意图。
| 能力 | Selenium | Puppeteer | Playwright | Browser Use |
|---|---|---|---|---|
| 自动化 | ✔ | ✔ | ✔ | ✔ |
| 自动等待 | ❌ | ❌ | ✔ | ✔ |
| 网络 Mock | 弱 | 中 | 强 | 强 |
| 多 Context | ❌ | 一般 | ✔ | ✔ |
| AI 意图理解 | ❌ | ❌ | ❌ | ✔ |
| 自然语言控制 | ❌ | ❌ | ❌ | ✔ |
| 多步骤任务规划 | ❌ | ❌ | ❌ | ✔ |
Browser Use 在多项能力上表现突出,具备自动等待、强网络 Mock、多 Context 支持,并在 AI 相关功能:如 AI 意图理解、自然语言控制和多步骤任务规划方面独占优势。Playwright 与 Browser Use 并列在多数技术能力上领先,尤其在网络控制和多环境支持方面优于 Selenium 和 Puppeteer。Selenium 虽然支持基本自动化,但在高级功能上较为薄弱。Puppeteer 能力适中,网络 Mock 能力为“中”,多 Context 支持一般,缺乏自动等待机制。整体来看,Browser Use 代表了下一代浏览器自动化方向,融合 AI 能力,支持自然语言驱动和智能任务规划,显著提升测试与操作效率。
🔮 未来:浏览器不再是给人用的,而是给 AI 用的
未来三年,你将看到:
• AI 自动化测试
• AI 网页巡检
• AI 数据采集
• AI 企业流程自动化
• AI 批量填表、批量商品录入、批量爬取
浏览器将成为 AI 的主要操作界面,这意味着:浏览器将成为 AI 的操作系统,不再只是给人用的,而是给 Agent 用的。
移除视觉对象里“行的型号”造成的行级筛选,但不移除用户的 slicer 筛选
在 度量值 中,如果你想:
- ❌ 去掉当前表格行的型号筛选(Row Context → Filter Context)
- ✔️ 但保留切片器/页面/报表级筛选器对型号的筛选
这是一个典型需求:
移除视觉对象里“行的型号”造成的行级筛选,但不移除用户的 slicer 筛选。
✅ 最正确和安全的写法:
使用 REMOVEFILTERS( 表[列] ) 或 ALLSELECTED(…)
关键点:
| 方法 | 去掉当前行筛选 | 保留切片器筛选 | 保留交互 | 备注 |
|---|---|---|---|---|
ALL(B_models[型号]) |
✔ | ❌ | ❌ | 会把切片器清掉(不符合你的需求) |
REMOVEFILTERS(B_models[型号]) |
✔ | ✔ | ✔ | 最佳, 用于仅移除当前视觉对象的行筛选 |
ALLSELECTED(B_models[型号]) |
✔ | ✔ | ✔ | 也是可用方案 |
🟦 推荐方式 1(最佳):REMOVEFILTERS
如果你有一个度量:
[量_roll]
你想在计算时:
- 忽略当前行的型号(例如视觉对象的每一行型号 A/B/C/…)
- 但是保留切片器中对型号的筛选(如只选品牌A下的型号)
写法如下:
测算值 =
CALCULATE(
[量_roll],
REMOVEFILTERS('B_models'[型号]) -- 去掉当前行的型号筛选
)
✔ 切片器选了哪些型号 → 保留
✔ 行上下文型号 → 移除
✔ 品牌/日期等其他筛选 → 保留
🟦 推荐方式 2:ALLSELECTED
如果你还想保留 图表之间的交互(例如交叉筛选),用:
测算值 =
CALCULATE(
[量_roll],
ALLSELECTED('B_models'[型号]) -- 保留切片器但忽略行过滤
)
这个会保留切片器和页面层级筛选,但仍然脱离表格自身每一行的型号过滤。
🟥 错误示例(不要这样用)
CALCULATE([量_roll], ALL('B_models'))
这会:
- 去掉 所有切片器
- 去掉 所有维度筛选
导致完全错误的结果。
🧪 验证方式
你可以在一个表格里放:
- 行:B_models[型号]
- 列:你写的度量
如果写法正确:
- 所有型号行都显示同样的汇总值(因为忽略了行型号过滤)
- 切片器选择某些型号 → 这些型号以外的不会显示,且度量值变化(因为保留切片器过滤)
Promise限制重复请求
前端并发控制之:请求去重与共享模式 (Shared Promise)
在前端并发控制中,除了限制“最大并发数”(如限制上传 10 个文件),还有一种常见的场景是限制“重复请求”。
当多个组件(或逻辑)在极短时间内几乎同时调用同一个接口(例如:获取数据字典、用户信息、Token)时,如果每次都发起真实的 HTTP 请求,会导致带宽浪费和后端压力。
核心解决方案:Promise 共享模式。
核心原理
“查缓存 -> 查正在进行中 -> 发起新请求”
该模式将数据获取过程分为三个优先级步骤:
- 命中缓存:如果数据已经拿到了,直接返回数据。
- 命中并发(Promise 共享):如果请求正在进行中(Pending),不发新请求,而是直接返回正在进行的那个 Promise。
- 发起请求:如果既没缓存也没在请求,才真正发起网络请求,并保存 Promise 引用。
代码范式
// 状态变量(通常定义在 Store 或 Service 中)
let dataCache = null; // 1. 数据缓存
let loadingPromise = null; // 2. 保存正在进行的 Promise 引用
async function getData() {
// ①【查缓存】:已有数据,直接返回
if (dataCache) return dataCache;
// ②【查并发】:请求正在进行中,返回同一个 Promise(搭便车)
// 关键点:这一步实现了“多个调用,一个请求”
if (loadingPromise) return loadingPromise;
// ③【发起新请求】:
// 将请求赋值给 loadingPromise,供后续的调用方复用
loadingPromise = fetch('/api/data')
.then(res => res.json())
.then(res => {
dataCache = res; // 写入缓存
return res;
})
.catch(err => {
// 慎重:如果失败,通常需要重置 cache 和 promise,允许重试
dataCache = null;
throw err;
})
.finally(() => {
// ④【清理状态】:无论成功失败,请求结束了,不再处于 loading 状态
// 必须置空,否则下一次请求无法发起
loadingPromise = null;
});
return loadingPromise;
}
场景对比
| 维度 | 并发数限制 (p-limit) | 请求去重 (Shared Promise) |
|---|---|---|
| 针对对象 | 不同的任务 (如上传100张不同的图) | 相同的任务 (如10个组件都要获取UserInfo) |
| 目的 | 保护服务器/浏览器不被撑爆 | 避免做无用功,节省流量 |
| 策略 | 排队执行,慢慢来 | 合并执行,大家共用一份结果 |
| 返回结果 | 每个任务返回各自的结果 | 所有调用方拿到完全相同的结果 |
优缺点分析
优点
-
极致性能:在组件化开发中,避免了
created/mounted钩子中重复发请求的问题。 - 数据一致性:所有订阅者拿到的都是同一时刻的数据。
-
代码简洁:对调用方透明,调用方只需要
await getData(),不需要关心是否是并发。
注意事项
-
finally清理:务必在finally中将loadingPromise置为null。否则一旦请求结束(无论成功失败),后续的调用依然会拿到旧的 promise,导致状态死锁。 - 错误处理:如果请求失败,需要确保缓存不被写入错误数据,且允许下一次重试。
【URP】Unity[内置Shader]粒子简单光照ParticlesSimpleLit
【从UnityURP开始探索游戏渲染】专栏-直达
作用与原理
ParticlesSimpleLit是Unity URP(Universal Render Pipeline)中专门为粒子系统设计的简化光照着色器,主要用于实现高性能的粒子渲染效果。其核心原理是通过简化光照计算模型,牺牲部分物理准确性来换取更高的渲染效率,特别适合移动端或低端设备使用。
该着色器不计算物理正确性和能量守恒,而是采用一个简单的近似照明模型,这使得它在渲染时可以忽略复杂的物理计算。ParticlesSimpleLit主要包含三个关键部分:
- Surface Options:控制材质的基本渲染方式,如颜色和光照模式
- Surface Inputs:描述表面特性,如湿度、粗糙度等
- Advanced选项:提供更底层的渲染设置如阴影和反射
发展历史
ParticlesSimpleLit随着URP的发展经历了多个版本迭代:
- 最初作为URP核心着色器之一引入,替代了传统Built-in渲染管线中的简单粒子着色器
- 在URP 7.x版本中进行了性能优化,特别针对移动平台
- URP 12.0版本后增加了对Shader Graph的支持
- 最新版本(如URP 16.0.6)进一步优化了变体管理和GPU实例化支持
具体使用方法
基本应用示例
在Unity中使用ParticlesSimpleLit的步骤如下:
- 创建或选择粒子系统
- 在粒子系统的Renderer模块中指定材质
- 创建新材质或选择现有材质
- 在材质Inspector窗口的Shader下拉菜单中选择"Universal Render Pipeline > Particles > Simple Lit"
代码说明:这个C#脚本示例展示了如何通过代码动态为粒子系统应用ParticlesSimpleLit着色器。
-
ParticleSimpleLitExample.cs
// 在代码中动态设置材质Shader的示例 using UnityEngine; public class ParticleShaderSetter : MonoBehaviour { public ParticleSystem particleSystem; void Start() { var renderer = particleSystem.GetComponent<ParticleSystemRenderer>(); Material mat = new Material(Shader.Find("Universal Render Pipeline/Particles/Simple Lit")); renderer.material = mat; } } // 以下是Shader Graph的节点设置参考: /* 1. 添加Texture2D节点作为Base Map输入 2. 添加Color节点作为Base Color输入 3. 添加Slider节点控制Specular(0-1范围) 4. 添加Slider节点控制Smoothness(0-1范围) 5. 连接这些节点到PBR Master的对应输入 6. 在PBR Master节点中禁用高级光照计算 */
参数配置
ParticlesSimpleLit提供了多个可调参数:
- Base Map:基础纹理,定义粒子外观
- Base Color:基础颜色,与纹理相乘
- Specular:控制高光强度
- Smoothness:控制表面光滑度
- Emission:控制自发光强度和颜色
Shader Graph中的应用
在Shader Graph中使用ParticlesSimpleLit需要以下步骤:
- 创建新的Shader Graph
- 在Graph Inspector中将"Target"设置为"Universal Render Pipeline"
- 使用"PBR Master"节点并调整设置以匹配SimpleLit特性
- 添加必要的纹理和参数输入
代码说明:这个伪代码描述了在Shader Graph中重建ParticlesSimpleLit基本功能所需的节点配置。
-
ParticleSimpleLitExample.cs
// 在代码中动态设置材质Shader的示例 using UnityEngine; public class ParticleShaderSetter : MonoBehaviour { public ParticleSystem particleSystem; void Start() { var renderer = particleSystem.GetComponent<ParticleSystemRenderer>(); Material mat = new Material(Shader.Find("Universal Render Pipeline/Particles/Simple Lit")); renderer.material = mat; } } // 以下是Shader Graph的节点设置参考: /* 1. 添加Texture2D节点作为Base Map输入 2. 添加Color节点作为Base Color输入 3. 添加Slider节点控制Specular(0-1范围) 4. 添加Slider节点控制Smoothness(0-1范围) 5. 连接这些节点到PBR Master的对应输入 6. 在PBR Master节点中禁用高级光照计算 */
高级应用示例
结合粒子系统的其他模块,如Lights模块和Trails模块,可以创建更复杂的效果。例如创建一个带有拖尾效果的火焰粒子:
- 启用粒子系统的Trails模块
- 使用ParticlesSimpleLit材质并设置适当的Emission值
- 调整Base Color为橙黄色渐变
- 根据需要启用Lights模块为部分粒子添加点光源效果
ParticlesSimpleLit因其高效的性能表现,特别适合需要大量粒子的场景,如魔法效果、烟雾、火焰等视觉效果
火焰与烟雾效果
通过调整Surface Type为Transparent并选择Additive混合模式,配合噪声纹理实现动态火焰形态。关键参数包括_Emission控制发光强度、_MainTex设置火焰贴图序列帧,同时需启用Color over Lifetime模块实现颜色渐变。具体实现步骤:
- 创建Particle System,材质选择Universal Render Pipeline > Particles > Simple Lit
- 在Surface Options中设置Blending Mode为Additive
- 通过脚本控制_Emission强度模拟燃烧波动
雨雪天气效果
采用Opaque表面类型提升性能,结合GPU实例化实现大面积粒子渲染。需配置_MainTex为雨滴/雪花贴图,使用_SoftParticlesNearFade控制粒子淡入距离,并通过Rotation over Lifetime模块添加随机旋转。典型参数:
- _MainTex: 雨滴Alpha贴图 _SoftParticlesNearFade: 0.5 _RenderFace: Both
魔法粒子特效
利用Color Mode的Overlay选项实现材质与粒子颜色混合,配合_Cutoff参数制作闪烁效果。通过脚本动态修改_HitPos和_HitSize数组可实现受击时的波纹扩散。核心代码逻辑包括:
- 声明Shader属性:
_HitPos("HitPos", Vector) = (0,0,0,0) - 在片段着色器中计算距离衰减:
float dist = distance(i.worldPos, _HitPos)
落叶/花瓣效果
需启用Alpha Clipping并设置合适阈值(通常0.3-0.5),结合粒子系统的Shape模块设置为Box发射器覆盖树木范围。通过Size over Lifetime实现下落过程中的尺寸变化,使用Texture Sheet Animation模块添加飘动动画。
受击闪白效果
复制ParticlesSimpleLit着色器后添加受击逻辑,通过Lerp函数混合原始颜色与白色,使用_SinTime控制恢复速度。关键实现参考受击闪白动画方案:
- 添加属性:
_FlashAmount("Flash Amount", Range(0,1)) = 0 - 颜色混合:
finalColor = lerp(originalColor, white, _FlashAmount)
所有效果均需在URP设置中开启Depth Texture和Opaque Texture选项以保证深度交互正常。对于复杂效果,建议结合Shader Graph进行原型设计后再转换为代码实现.
【从UnityURP开始探索游戏渲染】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)
CSS常用函数:从calc到clamp,实现动态渐变、滤镜与变换
大家好,我是大华!今天我们来深入探讨CSS函数,这是现代Web开发中不可或缺的强大工具。
静态的样式只能描述页面,动态的函数才能驱动体验。
传统CSS的局限性
在CSS函数出现之前,我们只能编写静态的样式规则:
.container {
width: 800px;
font-size: 16px;
margin: 20px;
}
这种方式存在明显的问题:
- 无法根据上下文动态计算值
- 难以创建真正响应式的设计
- 代码重复,维护困难
- 缺乏灵活性和适应性
什么是CSS函数?
CSS函数是一种特殊的值生成工具,它接收输入参数,经过计算或处理,返回一个具体的样式值。例如:
color: rgb(255, 0, 0);
width: calc(100% - 20px);
transform: rotate(45deg);
核心特点:
- 标准化:目前CSS不支持用户自定义函数,只能使用规范中定义的标准函数
- 即时求值:函数在应用样式时由浏览器解析并计算结果
- 类型安全:每个函数对参数类型有严格要求
- 声明式语法:通过简洁的声明表达复杂计算逻辑
常用CSS函数深度解析
1. 颜色函数
.primary-button {
background-color: rgb(255, 0, 0);
}
.transparent-card {
background-color: rgba(0, 255, 0, 0.5);
}
.modern-ui {
background-color: hsl(210, 100%, 50%);
color: hsla(210, 100%, 50%, 0.8);
}
颜色函数详解:
- rgb():通过红、绿、蓝三个颜色通道混合出目标颜色,每个通道取值范围0-255
- rgba():在rgb基础上增加alpha透明度通道,取值范围0(完全透明)到1(完全不透明)
- hsl():使用色相、饱和度、亮度三个参数定义颜色,更符合人类直觉
- hsla():hsl的透明版本,提供alpha通道控制透明度
2. calc() 计算器
.container {
width: calc(100% - 40px);
}
.sidebar {
height: calc(100vh - 80px);
}
.grid-item {
width: calc((100% - 60px) / 3);
}
.responsive-spacing {
padding: calc(10px + 2vw);
}
calc()的强大特性:
- 跨单位计算:支持在不同单位之间进行计算,如百分比与像素、视口单位与固定单位等
- 四则运算:支持加(+)、减(-)、乘(*)、除(/)运算
- 嵌套支持:可以在calc函数内部嵌套其他calc函数
- 动态响应:基于视口或其他动态值的计算,实现真正的响应式布局
3. 限制函数 - 边界控制器
.responsive-text {
font-size: clamp(16px, 4vw, 24px);
}
.container {
width: min(100%, 1200px);
height: max(50vh, 400px);
}
.fluid-layout {
padding: min(5vw, 50px);
}
限制函数的应用场景:
- clamp():为值设置最小值、理想值和最大值,确保在合理范围内自适应
- min():从参数列表中选择最小值,常用于设置上限
- max():从参数列表中选择最大值,常用于设置下限
- 响应式设计:特别适合创建在各种屏幕尺寸下都能良好显示的界面
4. 渐变函数
.hero-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.button {
background: radial-gradient(circle, #ff6b6b, #ee5a24);
}
.advanced-gradient {
background: conic-gradient(from 0deg, red, yellow, lime, aqua, blue, magenta, red);
}
渐变函数类型:
- linear-gradient():创建沿直线方向的颜色渐变,可指定角度或方向
- radial-gradient():创建从中心点向外辐射的圆形或椭圆形渐变
- conic-gradient():创建围绕中心点旋转的颜色渐变,适合制作饼图、色轮等效果
- 重复渐变:通过repeating-linear-gradient和repeating-radial-gradient创建图案式背景
5. 变换函数
.card {
transform: translateX(50px) rotate(15deg) scale(1.1);
}
.card:hover {
transform: translateY(-10px);
}
.modal {
transform: translate3d(-50%, -50%, 0);
}
.animated-element {
transform: perspective(500px) rotateY(30deg);
}
变换函数分类:
- 位移函数:translateX(), translateY(), translateZ(), translate3d()
- 旋转函数:rotate(), rotateX(), rotateY(), rotateZ(), rotate3d()
- 缩放函数:scale(), scaleX(), scaleY(), scaleZ(), scale3d()
- 倾斜函数:skew(), skewX(), skewY()
- 透视函数:perspective() 创建3D透视效果
6. 滤镜函数
.image {
filter: brightness(1.2) contrast(1.1) saturate(1.3);
}
.old-photo {
filter: grayscale(1) sepia(0.8);
}
.glass-effect {
filter: blur(5px) brightness(1.2);
}
.vibrant-ui {
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3)) hue-rotate(15deg);
}
常用滤镜效果:
- 基础调整:brightness(), contrast(), saturate() 调整亮度、对比度和饱和度
- 颜色效果:grayscale(), sepia(), hue-rotate() 创建单色、怀旧和色相旋转效果
- 模糊与锐化:blur() 创建模糊效果,适合制作毛玻璃效果
- 投影效果:drop-shadow() 为元素添加投影,比box-shadow更符合元素形状
7. 其他实用函数
/* 自定义属性与var()函数 */
:root {
--primary-color: #3498db;
--spacing-unit: 8px;
}
.component {
color: var(--primary-color);
margin: calc(var(--spacing-unit) * 2);
}
/* 形状函数 */
.clip-path-example {
clip-path: polygon(0 0, 100% 0, 100% 75%, 0 100%);
}
/* 数学函数 */
.math-functions {
width: min(100%, 1200px);
aspect-ratio: 16/9;
}
案例:构建现代化响应式组件
让我们通过一个实际案例来展示CSS函数的能力:
:root {
--primary-color: hsl(210, 100%, 50%);
--container-max-width: 1200px;
--base-spacing: 1rem;
}
.card {
/* 使用clamp确保字体大小在移动端和桌面端都合适 */
font-size: clamp(1rem, 2.5vw, 1.25rem);
/* 使用calc计算动态间距 */
padding: calc(var(--base-spacing) * 1.5);
/* 使用min限制最大宽度 */
max-width: min(100%, var(--container-max-width));
/* 使用渐变创建现代背景 */
background: linear-gradient(135deg,
var(--primary-color) 0%,
hsl(210, 100%, 40%) 100%);
/* 使用变换创建悬停效果 */
transform: translateY(0);
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
}
.card::before {
/* 使用滤镜创建叠加效果 */
filter: brightness(0.8) blur(10px);
}
总结
随着CSS规范的不断发展,更多强大的函数正在被引入:
- 三角函数:sin(), cos(), tan() 等已进入现代浏览器
- 指数与对数函数:pow(), sqrt(), log() 等数学函数
- 颜色函数:color-mix(), color-contrast() 等更先进的颜色操作
- 布局函数:更复杂的布局计算函数
掌握CSS函数,意味着你能够以更高效、更优雅的方式解决复杂的样式问题,创造出既美观又实用的用户界面。
本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《SpringBoot+MySQL+Vue实现文件共享系统》
告别截断与卡顿:我的前端PDF导出优化实践
告别截断与卡顿:我的前端PDF导出优化实践
项目地址:SeamlessPDF
背景
在前端开发中,PDF导出是一个“看着简单,做起来坑多”的需求。最常用的 html2canvas + jsPDF 方案虽然成熟,但在处理长文档时经常面临三个“顽疾”:
- 内容截断:文字、表格经常从中间被“一刀切”,极不美观。
- 页面卡顿:渲染过程阻塞主线程,点击导出后页面直接“假死”。
- 导出缓慢:复杂页面动辄等待 8-10 秒,用户体验很不友好。
为了解决这些问题,我尝试重构了一套生成方案。通过像素级分页分析、多进程渲染以及异步预生成策略,最终将导出时间从 8 秒降至 2 秒左右,配合预生成实现了“点击即下载”的体验。
本文主要分享一下核心思路和关键代码实现。
先看下优化后的效果
传统方案为何“由于”?
在动手优化前,我们需要明确问题的根源:
- 截断原因:传统方案通常按 A4 纸高度固定切割 Canvas。这就像闭着眼睛切蛋糕,不管刀下是文字还是表格,切到哪算哪。
-
卡顿原因:
html2canvas运行在主线程,DOM 树越复杂,计算量越大,UI 渲染必然被阻塞。 - 慢的原因:串行处理(页眉->内容->页脚),无法利用现代浏览器的多核性能。
核心优化方案
针对上述痛点,我设计了三个维度的优化策略:
一、像素级分页分析:解决内容截断
既然固定高度切割不可靠,我们就需要通过算法去寻找“安全”的切割线。
核心思路: 先将内容渲染为完整的 Canvas,然后在理论分页位置附近上下扫描像素。如果某一行全是白色(空白区域)或者是表格底边框,那就是一个完美的切割点。
关键代码实现(page-break-analyzer.ts):
// 寻找最优分页线的核心逻辑
export function findOptimalPageBreak(
startY: number,
canvas: HTMLCanvasElement
): OptimalBreakPointResult {
// 1. 优先向上搜索:保持上一页内容尽可能饱满
for (let y = startY; y > 0; y--) {
const analysis = analyzeLine(y, canvas);
// 如果是纯白行,或者是表格底部的边框,则允许切割
if (analysis.isCleanBreakPoint) {
return { cutY: y + 1 };
}
}
// 2. 向上没找到,尝试向下搜索(避免这一页太短)
for (let y = startY + 1; y < canvas.height; y++) {
// ...同上逻辑
}
// 3. 实在找不到(比如超长表格),只能强制切割,但避开边框区域
return { cutY: safeCutY };
}
// 分析单行像素特征
function analyzeLine(y: number, canvas: HTMLCanvasElement) {
const context = canvas.getContext("2d")!;
// 获取该行像素数据
const lineData = context.getImageData(0, y, canvas.width, 1).data;
// 分析颜色分布:判断是否为纯白,或是否符合表格边框特征
// ... 具体算法省略,主要是对比 RGB 值
return {
isCleanBreakPoint: isPureWhite || isTableBottomBorder
};
}
通过这种“视觉检测”的方式,我们不再依赖 DOM 结构计算,而是直接基于渲染结果,从而彻底解决了文字和表格被腰斩的问题。
二、多进程渲染:利用 Site Isolation 解决卡顿与慢
为了不阻塞主线程,同时提升速度,我利用了浏览器的 Site Isolation(站点隔离) 机制。
核心思路:
创建隐藏的 iframe 来承担渲染任务。现代浏览器会为跨域或特定配置的 iframe 分配独立的渲染进程。我们将页眉、页脚、主体内容分发给不同的 iframe 并行渲染,既不卡顿主页,又快了不少。
并行渲染实现(iframe-renderer.ts):
export async function renderElementsToCanvas(elements: PageElements) {
// 提取当前页面的所有样式,传递给 iframe
const pageStyles = await extractPageStyles();
// 利用 Promise.all 并行启动三个 iframe 进行渲染
const [header, content, footer] = await Promise.all([
renderInIframe(elements.header, "header", pageStyles),
renderInIframe(elements.content, "content", pageStyles),
renderInIframe(elements.footer, "footer", pageStyles),
]);
return { header, content, footer };
}
并行渲染实现 :
// 主线程发送任务
function renderInIframe(element: HTMLElement, id: string, styles: string) {
const iframe = createHiddenIframe();
// 通过 postMessage 传递序列化后的 DOM 和样式
iframe.contentWindow.postMessage(
{
type: "RENDER",
dom: serializeElement(element),
styles: styles,
},
"*"
);
return waitForResponse(iframe); // 等待 Canvas 数据返回
}
通过这种方式,繁重的布局计算和绘制任务被转移到了后台进程,主页面依然保持丝滑响应。
三、异步预生成:实现“零等待”体验
技术上的优化有了,用户体验还能更好吗? 通常用户进入页面后,浏览内容需要时间。我们可以利用这段“空闲时间”偷偷在后台把 PDF 生成好。
策略实现:
// 页面加载完成后,静默启动预生成
let pdfPromise: Promise<jsPDF> | null = null;
function onPageReady() {
// 使用 requestIdleCallback 或延迟执行,不影响首屏加载
setTimeout(() => {
pdfPromise = generateIntelligentPdf({
// ...传入配置
});
}, 1000);
}
// 用户点击下载按钮时
async function handleDownload() {
const btn = document.getElementById("download-btn");
btn.loading = true;
// 直接等待 Promise 结果
const pdf = await pdfPromise;
pdf.save("report.pdf");
btn.loading = false;
}
如果用户点击时预生成已完成,下载是瞬间的;如果未完成,用户也只需等待剩余的时间。
性能对比
在包含表格、图片的长文档(约4页)测试场景下:
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 渲染耗时 | ~8s | ~2s | 75% |
| 页面交互 | 卡死不可动 | 保持响应 | 98% |
| 内容完整度 | 频繁截断 | 智能分页 | - |
总结
这次优化主要通过三个手段解决了 PDF 导出的核心痛点:
- 像素检测代替固定切割,保证了内容的完整性。
- Iframe 多进程代替单线程渲染,解决了卡顿并提升了速度。
- 预生成策略优化了用户的主观等待时长。
虽然引入 iframe 和像素分析增加了代码复杂度,但对于对文档质量有要求的场景,这些投入是值得的。
项目代码已开源,如果你也遇到了类似问题,欢迎参考: 👉 SeamlessPDF
参考资料
js请求的并发控制
JavaScript 前端并发请求控制方案总结
在前端开发中(如批量文件上传、大批量接口请求),为了防止浏览器卡顿或服务器压力过大,通常需要限制同一时刻的最大并发数(例如限制最多 10 个请求同时进行)。
本文总结了两种主流的实现方案:递归队列法 和 Promise.race 竞速法。
方案一:递归队列法 (Recursive Worker)
核心思维:“收银台模式”。 假设有 10 个收银台(Worker),顾客(Tasks)排成一长队。收银台不关闭,处理完一个顾客后,立刻叫号处理下一位,直到队伍排空。
1. 实现原理
-
初始化:根据最大并发数
max,一次性启动max个异步函数(Worker)。 -
状态维护:维护一个全局索引
index,指向任务列表中下一个待处理的任务。 - 自动流转:Worker 完成当前任务后,通过递归调用自己,去领取并执行下一个任务。
-
结束条件:当
index超出任务总数时,Worker 停止递归。
2. 代码示例
/**
* @param {Function[]} tasks 任务数组 (返回 Promise 的函数)
* @param {number} max 最大并发数
*/
function concurrentRun(tasks, max = 10) {
const results = [];
let index = 0; // 全局指针
// 递归执行器
async function worker() {
// 递归出口:任务取完了
if (index >= tasks.length) return;
// 1. 占位:先保存当前索引,然后指针后移
const i = index;
index++;
try {
// 2. 执行:运行任务并保存结果
// console.log(`开始任务 ${i}`);
const res = await tasks[i]();
results[i] = res; // 按索引保存,保证结果顺序
} catch (err) {
results[i] = err; // 捕获错误,防止中断
} finally {
// 3. 接力:无论成功失败,立马递归领取下一个
await worker();
}
}
// 4. 启动:同时开启 max 个并发线程
const workers = [];
const runCount = Math.min(tasks.length, max); // 防止任务数少于并发数
for (let i = 0; i < runCount; i++) {
workers.push(worker());
}
// 5. 等待:所有 worker 都收工了,整体才算完成
return Promise.all(workers).then(() => results);
}
3. 优缺点
- 优点:逻辑清晰,稳定性高,天然保证结果顺序(Result 数组按索引存储)。
- 缺点:需要定义辅助函数,代码量稍多。
- 推荐指数:⭐⭐⭐⭐⭐ (工程落地与面试首选)
方案二:Promise.race 竞速法 (Dynamic Pool)
核心思维:“停车场模式”。
停车场只有 10 个车位。车一辆接一辆来,只要有空位就进。如果满了,门口的栏杆就放下,直到有一辆车出来(Promise.race),才放下一辆车进去。
1. 实现原理
- 遍历:使用循环遍历所有任务。
-
入列:将任务包装后推入一个“正在执行数组” (
executing)。 -
包装:每个任务完成后,必须执行
splice操作将自己从executing中移除。 -
阻塞:判断
executing.length >= max。如果满了,使用await Promise.race(executing)阻塞主线程,等待最快的一个任务完成腾出坑位。
2. 代码示例
JavaScript
/**
* @param {Function[]} tasks 任务数组
* @param {number} max 最大并发数
*/
async function limitRequest(tasks, max = 10) {
const results = [];
const executing = []; // 正在执行的任务队列
// 使用 entries() 拿到索引,为了保证结果顺序
for (const [index, task] of tasks.entries()) {
// 1. 创建任务:执行并存储结果
const p = task().then(res => results[index] = res);
// 2. 包装任务:任务完成后,从 executing 队列中移除自己
// 关键:e 必须引用 p.then 的返回值,确保 race 等待的是“删除操作”完成
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
// 3. 入列
executing.push(e);
// 4. 竞速:如果队列满了,等待最快的一个执行完
if (executing.length >= max) {
await Promise.race(executing);
}
}
// 5. 收尾:等待剩余的任务完成
await Promise.all(executing);
return results;
}
3. 优缺点
-
优点:利用
async/await线性逻辑,代码看起来较精简。 - 缺点:涉及微任务时序问题(必须正确包装 Promise),逻辑稍显绕弯,容易写出 Bug。
- 推荐指数:⭐⭐⭐⭐
总结对比
| 维度 | 递归队列法 (方案一) | Promise.race 竞速法 (方案二) |
|---|---|---|
| 并发维持机制 | 总量守恒:走一个,递归补一个 |
阻塞等待:满了就 await race 暂停循环 |
| 代码结构 | 闭包 + 递归函数 | For 循环 + 动态数组 |
| 执行视角 | 开启 N 个永久的“线程” | 动态维护一个“线程池” |
| 稳定性 | 高,容错率好 | 中,需注意 splice 的时序 |
| 适用场景 | 通用业务、面试手写 | 个人项目、脚本工具 |
附:测试用例代码
可以将上述任意一种方案配合以下代码进行测试:
JavaScript
// 模拟请求:返回一个 Promise,耗时 100~1000ms
const mockRequest = (id) => {
return () => new Promise((resolve) => {
const time = Math.random() * 1000 + 100;
console.log(`🚀 任务 ${id} 开始`);
setTimeout(() => {
console.log(`✅ 任务 ${id} 完成 (耗时 ${Math.floor(time)}ms)`);
resolve(`结果 ${id}`);
}, time);
});
};
// 生成 20 个任务
const tasks = Array.from({ length: 20 }, (_, i) => mockRequest(i));
// 执行测试 (并发数限制为 3)
console.time('总耗时');
concurrentRun(tasks, 3).then(res => {
console.log('--- 所有任务结束 ---');
console.log(res);
console.timeEnd('总耗时');
});
JavaScript 底层探秘:从执行上下文看 `this` 的设计哲学与箭头函数的救赎
在 JavaScript 的学习过程中,this 关键字往往是最令人困惑的机制之一。很多开发者分不清“作用域链”查找变量和 this 指向的区别。
本文将结合 V8 引擎的 执行上下文(Execution Context) 机制,深入剖析 this 的设计初衷、历史包袱,并重点讲解 箭头函数 是如何打破传统规则,回归词法作用域的。
一、 静态的作用域 vs 动态的 this
首先,我们需要区分两个核心概念:作用域(Scope) 和 this。
在 JavaScript 中,变量的查找遵循“词法作用域(Lexical Scope)”。这意味着变量的作用域是由代码声明的位置决定的,在编译阶段就已经确定。
-
作用域链(Outer) :当函数内部使用了一个既不是参数也不是局部变量的“自由变量”时,引擎会沿着作用域链(
outer)向外查找。这个outer指向的是定义该函数时的词法环境。 -
this:与作用域不同,this是在函数执行阶段(运行时)决定的。它是执行上下文中的一个属性。
结论:在普通函数中,谁调用了这个方法,this 就指向谁;这与函数在哪里定义无关,只与函数如何被调用有关。
二、 核心机制:执行上下文
要真正理解 this,必须看向底层。当 JavaScript 执行一段代码时,会创建执行上下文。根据示意图,一个执行上下文包含以下四个部分:
-
变量环境 (Variable Environment) :存放
var声明的变量。 -
词法环境 (Lexical Environment) :存放
let、const变量。 - Outer:指向外部作用域,用于变量查找(作用域链)。
-
ThisBinding:即我们讨论的
this。
这就解释了为什么 this 是动态的——因为每次函数调用都会创建一个新的执行上下文,而 this 是在这个上下文中被绑定的。
三、 为什么要有 this?(设计哲学与缺陷)
在早期的 JavaScript 设计中,虽然它是一门函数式语言,但为了迎合当时的 Java 潮流,需要模拟“面向对象(OOP)”的特性。在对象的方法内,必须有一个机制能操作对象自身的属性,于是引入了 this。
然而,这也带来了一个著名的设计缺陷:普通函数的 this 指向。
当一个函数被“裸调用”(不作为对象方法,也不使用 new)时:
-
非严格模式:
this默认指向全局对象(浏览器中是window)。这容易导致全局变量污染。 -
严格模式:为了修复这个问题,
'use strict'下普通函数的this会被绑定为undefined。
四、 this 绑定的常规规则
根据调用方式的不同,this 的绑定可以归纳为以下几种常规场景:
-
默认绑定:普通函数运行,
this指向全局对象或undefined。 -
隐式绑定:
obj.method(),this指向obj。 -
显式绑定:
call/apply/bind,this指向指定对象。 -
构造函数绑定:
new调用,this指向新实例。 -
事件绑定:DOM 事件中,
this指向绑定事件的元素。
五、 箭头函数:打破规则的特例
ES6 引入的**箭头函数(Arrow Function)**是 this 机制中最大的特例,它彻底改变了 this 的查找规则。
1. 本质区别:回归词法作用域
在普通函数中,this 是执行上下文中的一个特殊属性(ThisBinding),由调用方式决定。
而在箭头函数中,它本身没有自己的 this 绑定。
那么,箭头函数里的 this 是什么?
文档中明确指出:箭头函数中,this 是从定义时的外层词法作用域继承的。
这意味着,箭头函数处理 this 的方式,和处理普通变量(如 myName)是一样的:
-
查找:当在箭头函数中使用
this时,引擎会去查找当前执行上下文。 -
向上追溯:发现当前(箭头函数)上下文不存在
this绑定,于是沿着**作用域链(Outer)**向外层查找。 -
确定:它会使用定义该箭头函数时所在环境的
this。
2. 解决了什么痛点?
在 ES6 之前,我们在回调函数中使用 this 非常痛苦,经常面临“隐式丢失”的问题:
var myObj = {
name: "极客时间",
showThis: function(){
// 如果这里使用 setTimeout(function() { ... }, 1000)
// 普通函数的 this 会指向 window (非严格模式),导致无法获取 this.name
// 传统的“黑魔法”:
var self = this; // 显式保存外层的 this
// 现在的箭头函数:
setTimeout(() => {
// 这里的 this 继承自 showThis 函数的执行上下文
console.log(this.name);
}, 1000);
}
}
myObj.showThis();
由于箭头函数的 this 是静态的(由定义位置决定,类似于词法作用域),它不会因为函数调用的方式(如被 setTimeout 调用)而改变指向。这让代码更加符合直觉。
3. 不可被修改
正因为箭头函数的 this 是基于词法作用域链查找的,所以它无法通过 call、apply 或 bind 来改变。
即使你写了 arrowFunc.call(obj),引擎也会忽略这个 obj,仍然按照词法作用域去寻找 this。
六、 总结
理解 this 的关键在于区分两种模式:
-
动态模式(普通函数) :忘掉“函数在哪里定义”,牢记“函数是如何被执行的”。执行上下文在运行时决定
this的指向。 -
静态模式(箭头函数) :忘掉“函数怎么被调用”,牢记“函数在哪里定义”。它利用**词法作用域链(Outer)**机制,继承外层的
this。
JS 的这一设计演变,展示了语言从“模仿 Java OOP”到“回归函数式与词法作用域”的进化过程。希望通过本文,你能从底层的执行上下文视角,彻底掌握这一核心机制。
从 "渣男" 到 "深情男":Promise 如何让 JS 变得代码变得专一又靠谱
一、JS 曾经是个 "渣男"?
上回咱们聊到 JS 是个 "渣男"—— 同一时间只对一个任务 "深情",但遇到耗时任务就会把它 "挂起"。就像你约男神吃饭,他却说 "等我打完这局游戏",结果你等了半小时还没动静(这就是setTimeout的日常)。
看看这段代码,感受下 "渣男" 的敷衍:
let a = 1
setTimeout(() => {
a = 2 // 1秒后才会执行
}, 1000)
console.log(a); // 立马输出1,根本不等后面的操作
这就像你问男神 "爱我吗",他嘴上说 "爱"(输出 1),心里却想着别人(后面才改 a=2),简直渣得明明白白!
二、回调地狱:深情错付的连环套
后来 JS 想做个 "深情男",想认真处理异步任务,结果用力过猛 —— 搞出了 "回调地狱" 这种奇葩操作。就像为了证明爱意,承诺 "先约会、再告白、再求婚",结果写成了绕口令:
function foo() {
setTimeout(() => {
a = 2
console.log('第一次约会', a);
bar() // 约会完才能告白
}, 1000)
}
function bar() {
setTimeout(() => {
a = 3
console.log('深情告白', a);
baz() // 告白完才能求婚
}, 2000)
}
function baz() {
console.log('准备求婚', a);
}
foo()
这代码嵌套得比俄罗斯套娃还离谱,就像男神说 "等我妈同意了,我就跟你说我爸的意见,然后我们再问我奶奶...",听着就头大!
三、Promise:让 JS 变成 "深情专一男"
Promise 横空出世后,JS 终于学会了 "一心一意"—— 该等的绝不敷衍,该做的一步不落。它就像给感情加了个 "承诺协议",明确了三个阶段:
-
pending(等待中):刚加微信,还没确定关系
-
fulfilled(成功):确认关系,准备下一步
-
rejected(失败):不好意思,我们不合适
看看 Promise 版的 "爱情承诺":
function xq() { // 相亲
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('相亲成功');
// 这里故意写反了,模拟"口是心非"的情况
reject('其实没相中') // 说好的成功,结果拒了?
}, 3000)
})
}
function marry() { // 结婚
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('结婚了');
reject('婚后不愉快') // 又是一个"渣男"操作
}, 2000)
})
}
虽然例子里都是 "渣男行为",但 Promise 的机制保证了:该走的流程一步不少,该给的结果绝不拖欠。
四、链式调用:深情要一步一步来
Promise 最迷人的地方,就是用then实现的 "链式调用"—— 像剥洋葱一样,一层一层推进关系,绝不跳步:
xq()
.then(() => {
console.log('相亲成功,准备结婚');
return marry() // 上一步成了,才会走到这步
})
.then(() => {
console.log('结婚成功,准备生娃');
baby()
})
.catch((err) => {
console.log('感情破裂原因:', err); // 任何一步失败,都能及时止损
})
这段代码就像一份 "恋爱计划书":
-
先相亲(3 秒后)
-
相亲成了才结婚(再等 2 秒)
-
结婚成了才生娃
-
任何一步黄了,立马知道原因
对比之前的回调地狱,就像把 "我妈同意了就找你爸然后问奶奶..." 改成了 "第一步:见家长;第二步:谈婚论嫁;第三步:办婚礼"—— 清爽!
五、Promise 的 "深情秘籍"
其实 Promise 的源码核心很简单,就像男人的 "深情人设" 本质:说到做到,有始有终:
class Promise {
constructor(fn) {
function resolve() { // 成功时调用
// 执行后续操作
}
function reject() { // 失败时调用
// 处理错误情况
}
fn(resolve, reject) // 一开始就许下承诺
}
}
不管遇到什么情况,resolve和reject总会给个说法,绝不玩消失 —— 这才是 "深情男" 的基本素养!
六、最后说句大实话
从回调地狱到 Promise,就像从 "渣男" 到 "深情男" 的进化史。不是说 Promise 完美无缺,但它让异步代码有了 "契约精神":
-
该等的,绝不提前跑路
-
该做的,一步一步推进
-
做错了,敢于直面错误(
catch的作用)
下次写异步代码时,不妨想想 "深情男" 的准则 —— 用 Promise 给代码一份承诺,既让阅读者舒心,也让维护者省心。毕竟,靠谱的代码和靠谱的人一样,都值得被偏爱~
uni-app D8 实战(小兔鲜)
1.填写订单
1.1 渲染基本信息
1.1.1 设置独立的分包(静态结构)
<script setup lang="ts">
import { computed, ref } from 'vue';
// 获取屏幕边界到安全区域距离
const { safeAreaInsets } = uni.getSystemInfoSync();
// 订单备注
const buyerMessage = ref('');
// 配送时间
const deliveryList = ref([
{ type: 1, text: '时间不限 (周一至周日)' },
{ type: 2, text: '工作日送 (周一至周五)' },
{ type: 3, text: '周末配送 (周六至周日)' }
]);
// 当前配送时间下标
const activeIndex = ref(0);
// 当前配送时间
const activeDelivery = computed(() => deliveryList.value[activeIndex.value]);
// 修改配送时间
const onChangeDelivery: UniHelper.SelectorPickerOnChange = (ev) => {
activeIndex.value = ev.detail.value;
};
</script>
<template>
<scroll-view scroll-y class="viewport">
<!-- 收货地址 -->
<navigator v-if="false" class="shipment" hover-class="none" url="/pagesMember/address/address?from=order">
<view class="user">张三 13333333333</view>
<view class="address">广东省 广州市 天河区 黑马程序员3</view>
<text class="icon icon-right"></text>
</navigator>
<navigator v-else class="shipment" hover-class="none" url="/pagesMember/address/address?from=order">
<view class="address">请选择收货地址</view>
<text class="icon icon-right"></text>
</navigator>
<!-- 商品信息 -->
<view class="goods">
<navigator v-for="item in 2" :key="item" :url="`/pages/goods/goods?id=1`" class="item" hover-class="none">
<image class="picture" src="https://yanxuan-item.nosdn.127.net/c07edde1047fa1bd0b795bed136c2bb2.jpg" />
<view class="meta">
<view class="name ellipsis">ins风小碎花泡泡袖衬110-160cm</view>
<view class="attrs">藏青小花 130</view>
<view class="prices">
<view class="pay-price symbol">99.00</view>
<view class="price symbol">99.00</view>
</view>
<view class="count">x5</view>
</view>
</navigator>
</view>
<!-- 配送及支付方式 -->
<view class="related">
<view class="item">
<text class="text">配送时间</text>
<picker :range="deliveryList" range-key="text" @change="onChangeDelivery">
<view class="icon-fonts picker">{{ activeDelivery.text }}</view>
</picker>
</view>
<view class="item">
<text class="text">订单备注</text>
<input class="input" :cursor-spacing="30" placeholder="选题,建议留言前先与商家沟通确认" v-model="buyerMessage" />
</view>
</view>
<!-- 支付金额 -->
<view class="settlement">
<view class="item">
<text class="text">商品总价:</text>
<text class="number symbol">495.00</text>
</view>
<view class="item">
<text class="text">运费:</text>
<text class="number symbol">5.00</text>
</view>
</view>
</scroll-view>
<!-- 吸底工具栏 -->
<view class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">
<view class="total-pay symbol">
<text class="number">99.00</text>
</view>
<view class="button" :class="{ disabled: true }">提交订单</view>
</view>
</template>
<style lang="scss">
page {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background-color: #f4f4f4;
}
.symbol::before {
content: '¥';
font-size: 80%;
margin-right: 5rpx;
}
.shipment {
margin: 20rpx;
padding: 30rpx 30rpx 30rpx 84rpx;
font-size: 26rpx;
border-radius: 10rpx;
background: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/locate.png) 20rpx center / 50rpx no-repeat #fff;
position: relative;
.icon {
font-size: 36rpx;
color: #333;
transform: translateY(-50%);
position: absolute;
top: 50%;
right: 20rpx;
}
.user {
color: #333;
margin-bottom: 5rpx;
}
.address {
color: #666;
}
}
.goods {
margin: 20rpx;
padding: 0 20rpx;
border-radius: 10rpx;
background-color: #fff;
.item {
display: flex;
padding: 30rpx 0;
border-top: 1rpx solid #eee;
&:first-child {
border-top: none;
}
.picture {
width: 170rpx;
height: 170rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.meta {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
}
.name {
height: 80rpx;
font-size: 26rpx;
color: #444;
}
.attrs {
line-height: 1.8;
padding: 0 15rpx;
margin-top: 6rpx;
font-size: 24rpx;
align-self: flex-start;
border-radius: 4rpx;
color: #888;
background-color: #f7f7f8;
}
.prices {
display: flex;
align-items: baseline;
margin-top: 6rpx;
font-size: 28rpx;
.pay-price {
margin-right: 10rpx;
color: #cf4444;
}
.price {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
}
}
.count {
position: absolute;
bottom: 0;
right: 0;
font-size: 26rpx;
color: #444;
}
}
}
.related {
margin: 20rpx;
padding: 0 20rpx;
border-radius: 10rpx;
background-color: #fff;
.item {
display: flex;
justify-content: space-between;
align-items: center;
min-height: 80rpx;
font-size: 26rpx;
color: #333;
}
.input {
flex: 1;
text-align: right;
margin: 20rpx 0;
padding-right: 20rpx;
font-size: 26rpx;
color: #999;
}
.item .text {
width: 125rpx;
}
.picker {
color: #666;
}
.picker::after {
content: '\e6c2';
}
}
/* 结算清单 */
.settlement {
margin: 20rpx;
padding: 0 20rpx;
border-radius: 10rpx;
background-color: #fff;
.item {
display: flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
font-size: 26rpx;
color: #333;
}
.danger {
color: #cf4444;
}
}
/* 吸底工具栏 */
.toolbar {
position: fixed;
left: 0;
right: 0;
bottom: calc(var(--window-bottom));
z-index: 1;
background-color: #fff;
height: 100rpx;
padding: 0 20rpx;
border-top: 1rpx solid #eaeaea;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: content-box;
.total-pay {
font-size: 40rpx;
color: #cf4444;
.decimal {
font-size: 75%;
}
}
.button {
width: 220rpx;
text-align: center;
line-height: 72rpx;
font-size: 26rpx;
color: #fff;
border-radius: 72rpx;
background-color: #27ba9b;
}
.disabled {
opacity: 0.6;
}
}
</style>
结果:
1.1.2 封装订单API
1.1.3 获取订单的函数
1.1.4 封装类型声明文件
import type { AddressItem } from './address'
/** 获取预付订单 返回信息 */
export type OrderPreResult = {
/** 商品集合 [ 商品信息 ] */
goods: OrderPreGoods[]
/** 结算信息 */
summary: {
/** 商品总价 */
totalPrice: number
/** 邮费 */
postFee: number
/** 应付金额 */
totalPayPrice: number
}
/** 用户地址列表 [ 地址信息 ] */
userAddresses: AddressItem[]
}
/** 商品信息 */
export type OrderPreGoods = {
/** 属性文字,例如“颜色:瓷白色 尺寸:8寸” */
attrsText: string
/** 数量 */
count: number
/** id */
id: string
/** 商品名称 */
name: string
/** 实付单价 */
payPrice: string
/** 图片 */
picture: string
/** 原单价 */
price: string
/** SKUID */
skuId: string
/** 实付价格小计 */
totalPayPrice: string
/** 小计总价 */
totalPrice: string
}
1.1.5 存储获取到的订单信息到ref
1.1.6 渲染获取到的数据到标签中
1.1.7 再在先前的购物车代码中添加一行结算代码
结果:
1.2 收货地址
JavaScript 中 `this` 的真相:由调用方式决定的动态指针
JavaScript 中 this 的真相:由调用方式决定的动态指针
在 JavaScript 中,this 是一个看似简单却极易被误解的概念。它不像其他语言中的 this 那样固定指向当前对象,而是一个运行时绑定的动态指针——它的值完全取决于函数如何被调用,而非在哪里定义。
本文将从一段典型代码出发,深入剖析 this 的行为逻辑,并梳理其在不同场景下的指向规则,帮助你彻底掌握这一核心机制。
一、一个“反直觉”的现象
考虑如下代码:
var bar = {
myName: "time.geekbang.com",
printName: function() {
console.log(this.myName);
}
};
let myName = '极客邦';
let _printName = bar.printName;
_printName(); // 输出 undefined(非严格模式下)
bar.printName(); // 输出 "time.geekbang.com"
为什么同一个函数,两次调用结果却完全不同?
关键在于:
-
bar.printName()是作为对象方法调用,此时this指向bar; -
_printName()是作为普通函数调用,此时this指向全局对象(浏览器中为window),而window.myName并未定义(注意:let声明的变量不会挂载到window上),所以输出undefined。
这揭示了 this 的本质:它不关心函数在哪定义,只关心函数怎么被调用。
二、this 的四种常见绑定规则
1. 默认绑定(普通函数调用)
function foo() {
console.log(this); // 非严格模式:window;严格模式:undefined
}
foo();
- 在非严格模式下,
this指向全局对象; - 在严格模式(
'use strict')下,this为undefined,避免意外污染全局。
⚠️ 这也是早期 JavaScript 被诟病的设计之一:函数本可独立存在,却强制赋予一个无意义的
this。
2. 隐式绑定(对象方法调用)
const obj = {
name: '极客时间',
greet() {
console.log(this.name);
}
};
obj.greet(); // "极客时间"
当函数通过 obj.fn() 形式调用时,this 自动绑定到 obj。
⚠️ 丢失绑定:一旦将方法赋值给变量或传入回调,就会退化为默认绑定:
const fn = obj.greet;
fn(); // this 指向 window(或 undefined),输出 undefined
3. 显式绑定(call / apply / bind)
function setName(name) {
this.name = name;
}
const user = {};
setName.call(user, '极客邦');
console.log(user.name); // "极客邦"
通过 call、apply 或 bind,我们可以手动指定 this 的值,实现灵活的上下文控制。
4. 构造函数调用(new 绑定)
function Person(name) {
this.name = name;
}
const p = new Person('极客时间');
console.log(p.name); // "极客时间"
使用 new 调用函数时,JavaScript 引擎会:
- 创建一个新对象;
- 将
this绑定到该对象; - 执行构造函数;
- 返回新对象。
此时 this 指向新创建的实例。
三、特殊场景:事件处理器中的 this
在 DOM 事件处理中,this 指向触发事件的元素:
<a href="#" id="link">点击我</a>
<script>
document.getElementById("link").addEventListener("click", function() {
console.log(this); // <a id="link">...</a>
});
</script>
这是浏览器引擎自动完成的绑定,属于隐式绑定的一种变体。
四、如何安全地访问对象属性?
回到最初的问题:如何确保在方法内部正确访问对象自身的属性?
答案是:始终通过 this 访问,并确保调用方式不会导致 this 丢失。
若需解耦方法引用,可使用以下策略:
- 使用箭头函数(但注意:箭头函数没有自己的
this,会继承外层作用域); - 使用
.bind()提前绑定上下文; - 在类或对象中使用方法引用时,避免直接赋值,改用闭包或代理。
例如:
const safePrint = bar.printName.bind(bar);
safePrint(); // 正确输出 "time.geekbang.com"
结语
this 是 JavaScript 中少有的运行时动态绑定机制,它打破了“词法作用域”的常规思维。理解其绑定规则,不仅能避免常见 bug,还能更自如地操控函数执行上下文。
记住一句话:
this不是你写在哪,而是你叫谁来执行。
掌握这一点,你就真正迈入了 JavaScript 进阶之门。
Vite 环境变量配置经验总结
目录
快速开始
三步上手
第一步:创建 .env 文件
# .env.development
VITE_API_URL=http://localhost:3000
# .env.production
VITE_API_URL=https://api.example.com
第二步:在代码中访问
// 任意 .vue、.tsx、.js 文件中
const apiUrl = import.meta.env.VITE_API_URL;
console.log(apiUrl); // 开发环境输出: http://localhost:3000
第三步:运行项目
npm run dev # 自动加载 .env.development
npm run build # 自动加载 .env.production
核心要点
- ✅ 变量名必须以
VITE_开头 - ✅ 修改
.env文件后需重启开发服务器 - ✅ 使用
import.meta.env.变量名访问
核心概念
Vite 环境变量的工作原理
Vite 在构建时会读取 .env 文件,并将环境变量注入到你的应用中。
关键特性:
-
前缀限制:只有以
VITE_开头的变量才会暴露给客户端代码 - 构建时注入:环境变量在构建时就已经被静态替换到代码中
-
模式驱动:通过
mode参数决定加载哪个.env.[mode]文件
为什么需要 VITE_ 前缀?
这是一个安全机制,防止敏感信息(如数据库密码、API 密钥)意外暴露到客户端代码中。
# ✅ 会暴露给客户端
VITE_API_URL=https://api.example.com
# ❌ 不会暴露给客户端(仅在 vite.config.ts 中可用)
DATABASE_PASSWORD=secret123
API_SECRET_KEY=xyz789
两种访问方式
客户端代码(浏览器):
// 只能访问 VITE_ 开头的变量
const apiUrl = import.meta.env.VITE_API_URL;
const mode = import.meta.env.MODE;
const isDev = import.meta.env.DEV;
配置文件(Node.js):
// vite.config.ts - 可以访问所有变量
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
console.log(env.VITE_API_URL); // ✅ 可以访问
console.log(env.DATABASE_PASSWORD); // ✅ 也可以访问
return { /* ... */ };
});
环境变量文件系统
文件命名规则
Vite 支持以下环境变量文件(按优先级从低到高):
1. .env # 所有环境都会加载(最低优先级)
2. .env.local # 所有环境都会加载,但会被 git 忽略
3. .env.[mode] # 只在指定 mode 时加载
4. .env.[mode].local # 只在指定 mode 时加载,但会被 git 忽略(最高优先级)
加载顺序
后加载的文件会覆盖前面加载的文件中相同的变量
示例:运行 vite --mode development 时
1. .env → 加载(基础配置)
2. .env.local → 加载(本地覆盖,如果存在)
3. .env.development → 加载(开发环境配置)
4. .env.development.local → 加载(开发环境本地覆盖,如果存在,最高优先级)
文件优先级表
| 优先级 | 文件名 | 何时加载 | Git 状态 | 用途 |
|---|---|---|---|---|
| 1(最低) | .env |
所有模式 | 通常提交 | 公共配置 |
| 2 | .env.local |
所有模式 | 不提交 | 本地公共覆盖 |
| 3 | .env.[mode] |
指定 mode | 通常提交 | 环境特定配置 |
| 4(最高) | .env.[mode].local |
指定 mode | 不提交 | 环境特定本地覆盖 |
| - | .env.example |
从不加载 | 提交 | 仅作示例模板 |
.env.example 说明
.env.example 文件不会被 Vite 自动读取,它只是一个示例模板文件,用于:
- 告诉开发者需要配置哪些环境变量
- 作为文档说明
- 通常提交到 Git 仓库
- 开发者可以复制它来创建实际的
.env文件
命令与环境对应
默认命令行为
| 命令 | NODE_ENV | mode | 读取的 .env 文件 |
|---|---|---|---|
vite |
development |
development |
.env → .env.local → .env.development → .env.development.local
|
vite build |
production |
production |
.env → .env.local → .env.production → .env.production.local
|
vite preview |
production |
production |
不读取(使用构建时的值) |
vitest |
test |
test |
.env → .env.local → .env.test → .env.test.local
|
补充说明 1:vite preview 不读取环境变量
vite preview 不会读取新的 .env 文件,原因:
-
vite preview只是预览已经构建好的dist目录 - 环境变量在
vite build时就已经注入到代码中 -
preview阶段不会重新构建,所以不会重新读取.env
如果需要不同的预览环境,需要在构建时指定不同的 --mode:
# 构建时使用不同的 mode
vite build --mode staging
vite preview
补充说明 2:NODE_ENV vs mode
细心的读者会注意到,上面的表格中有 NODE_ENV 和 mode 两列,它们是两个独立的概念,容易混淆:
process.env.NODE_ENV
- Node.js 环境变量,控制构建优化(代码压缩、tree-shaking)
- 由 Vite 根据命令自动设置:
vite→development,vite build→production - 在
vite.config.ts中通过process.env.NODE_ENV访问
mode(模式)
- Vite 的模式参数,决定加载哪个
.env.[mode]文件 - 可以是任意自定义值(如
staging、test) - 通过
--mode参数指定,在客户端通过import.meta.env.MODE访问
关键区别:
| 命令 | NODE_ENV | mode | 说明 |
|---|---|---|---|
vite |
development |
development |
默认相同 |
vite build |
production |
production |
默认相同 |
vite build --mode staging |
production |
staging |
可以不同 ⚠️ |
NODE_ENV=test vite build |
test |
production |
可以不同 ⚠️ |
💡 推荐做法:在 Vite 项目中,优先使用 import.meta.env.MODE 来判断环境,而不是依赖 process.env.NODE_ENV。
最佳实践
1. 完整的文件结构
project/
├── .env # 所有环境的公共配置(提交)
├── .env.development # 开发环境配置(提交)
├── .env.production # 生产环境配置(提交)
├── .env.local # 本地覆盖配置(不提交)
├── .env.example # 示例模板(提交)
└── .gitignore # 添加 .env*.local
示例内容:
# .env - 公共配置
VITE_APP_NAME=My App
# .env.development
VITE_API_URL=http://localhost:3000
VITE_DEBUG=true
# .env.production
VITE_API_URL=https://api.example.com
VITE_DEBUG=false
# .env.example - 示例模板
VITE_API_URL=
VITE_APP_NAME=
2. 命名规范
# ✅ 正确写法
VITE_API_BASE_URL=https://api.example.com
VITE_APP_TITLE=My App
VITE_ENABLE_MOCK=true
# ❌ 错误写法(客户端无法访问)
API_BASE_URL=https://api.example.com
appTitle=My App
规则:
- 必须以
VITE_开头 - 使用全大写字母
- 单词间用下划线分隔
3. 客户端代码使用
// 访问环境变量
const apiUrl = import.meta.env.VITE_API_URL;
// 内置变量
const mode = import.meta.env.MODE; // 当前模式
const isDev = import.meta.env.DEV; // 是否开发环境
const isProd = import.meta.env.PROD; // 是否生产环境
const baseUrl = import.meta.env.BASE_URL; // 公共基础路径
// 带默认值
const debug = import.meta.env.VITE_DEBUG || 'false';
4. 在 vite.config.ts 中使用
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => {
// 加载环境变量
const env = loadEnv(mode, process.cwd(), '');
return {
// 方式1: 使用 define 注入全局常量
define: {
__APP_VERSION__: JSON.stringify('1.0.0'),
__API_URL__: JSON.stringify(env.VITE_API_URL),
},
// 方式2: 根据环境变量配置插件
server: {
port: Number(env.VITE_PORT) || 3000,
proxy: env.VITE_API_URL ? {
'/api': {
target: env.VITE_API_URL,
changeOrigin: true,
}
} : undefined,
},
};
});
5. TypeScript 类型支持
创建 env.d.ts 为环境变量添加类型:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_APP_NAME: string;
readonly VITE_DEBUG: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
常见问题
Q: 为什么我的环境变量在客户端访问不到?
A: 确保变量名以 VITE_ 开头,并且重新启动开发服务器。
Q: 如何在不同环境使用不同的配置?
A: 创建 .env.development、.env.production 等文件,或使用 --mode 参数。
Q: vite preview 能读取新的环境变量吗?
A: 不能,环境变量在构建时就已经注入,需要重新构建才能更新。
Q: .env.example 会被 Vite 加载吗?
A: 不会,它只是示例模板,需要复制为实际的 .env 文件。
扩展:其他加载方式
在非 Vite 项目或 Node.js 脚本中,你可能需要其他方式加载环境变量。
方式对比
| 方式 | 适用场景 | 使用方法 | 说明 |
|---|---|---|---|
| Vite 内置 | Vite 项目 | 自动加载,无需配置 | ✅ 推荐 |
| dotenv 库 | Node.js 脚本 | require('dotenv').config() |
需要安装依赖 |
| Node.js 原生 | Node.js 20.6+ | node --env-file=.env app.js |
无需依赖,原生支持 |
| tsx | TypeScript 文件 | tsx --env-file=.env app.ts |
直接运行 .ts 文件 |
dotenv 库使用
npm install dotenv
// 基本用法
import dotenv from 'dotenv';
dotenv.config();
// 指定文件
dotenv.config({ path: '.env.development' });
// 访问变量
console.log(process.env.DB_HOST);
Node.js 原生支持(20.6+)
# 命令行使用
node --env-file=.env app.js
# package.json
{
"scripts": {
"dev": "node --env-file=.env.development src/index.js"
}
}
TypeScript 类型声明
// env.d.ts
declare global {
namespace NodeJS {
interface ProcessEnv {
DB_HOST: string;
API_KEY: string;
}
}
}
export {};
适用版本:Vite 5.x+, Node.js 20.6+
用 DeepSeek 给 Git 提交做自动 Code Review:从 0 落地一个本地 AI 审查流程
在这篇文章里,我们从一个空目录开始,搭建一套在 git commit 前自动调用 DeepSeek 做代码审查的工作流:
- 使用 Node.js 脚本 读取本次提交的 git diff
- 通过 DeepSeek Chat API 做代码审查,并按 严重程度分级
- 在 pre-commit hook 阶段自动执行,有严重问题就阻止提交
- 审查规则抽象为 rules.json 配置
- 模型配置通过 .env.local 管理,方便本地开发
你可以直接把这里的方案搬进自己的项目。
场景与目标
问题:团队日常开发中,Review 资源有限,小问题容易被漏掉,大问题又可能在合并后才暴露。目标:在每次 git commit 之前,让 AI 帮你快速过一遍变更,提前发现严重问题,并给出可执行的改进建议。我们设计的方案满足这些要求:
- 只审查本次提交的已暂存改动(Staged Diff)
- AI 按 严重 / 一般 / 建议 三个级别输出问题
- 有严重问题时:阻止提交
- 所有规则(关注点、文案、阻断策略)都写在 rules.json,后续可自定义
- 模型 Key & 地址放在 .env.local,便于本地环境管理
项目初始化
在一个空目录(例如 AI-Codereview)下,我们先手写一个简单的 package.json:
{
"name": "ai-codereview",
"version": "0.1.0",
"description": "在 git 提交时调用 DeepSeek 模型进行代码自动审查的简单工具",
"main": "scripts/deepseek-review.js",
"bin": {
"deepseek-review": "scripts/deepseek-review.js"
},
"scripts": {
"review": "node scripts/deepseek-review.js"
},
"license": "MIT"
}
这里有两个点:
- bin 字段:将来可以通过 npm install -g . 安装成全局 CLI,直接用 deepseek-review 命令。
- scripts.review:方便本地调试,npm run review 即可执行审查脚本。
核心:DeepSeek 代码审查脚本
我们在 scripts/deepseek-review.js 里完成几件事:
- 加载 .env.local 和规则配置 rules.json
- 从 git diff --cached 读取当前已暂存的变更
- 构造一份带有「规则说明 + 输出格式」的 Prompt
- 调用 DeepSeek Chat API
- 解析 AI 输出中的 BLOCK: YES/NO,决定是否阻止提交
下面是部分代码实现结构:
// 读取当前已暂存的 diff
function run(cmd) {
return execSync(cmd, { encoding: "utf8" }).trim();
}
function getStagedDiff() {
try {
return run("git diff --cached --unified=0");
} catch (e) {
console.error("[deepseek-review] 读取 git diff 失败:", e.message);
process.exit(1);
}
}
// 构造 Prompt,把规则和输出格式喂给模型
function buildPrompt(diff, rules) {
const focusList = Array.isArray(rules.focus) && rules.focus.length
? rules.focus.map((item) => `- ${item}`).join("\n")
: "- 逻辑正确性与潜在 Bug\n- 边界条件与异常处理\n- 性能问题\n- 安全问题\n- 可维护性与可读性";
const severeDesc = rules.severityLevels?.severe?.description
|| "可能导致功能不可用、数据损坏、安全漏洞或构建失败的问题。";
const majorDesc = rules.severityLevels?.major?.description
|| "可以运行,但存在潜在 Bug、性能隐患或较大维护成本的问题。";
const suggestionDesc = rules.severityLevels?.suggestion?.description
|| "风格、命名、结构优化、更优写法等。";
const of = rules.outputFormat || {};
return `
你是一个严格的资深代码审查专家,请审查下面这次 git 提交的变更 diff。
【审查重点】
${focusList}
【分级规则】
- 严重问题:${severeDesc}
- 一般问题:${majorDesc}
- 建议与优化:${suggestionDesc}
【输出格式(必须严格遵守,方便工具解析)】
${of.overview || "1. 总体评价(1-2 句)"}
${of.severe || "2. 严重问题(如果没有,请写“无严重问题”)\n - 【严重】描述问题 + 涉及的文件/大致位置 + 原因"}
${of.major || "3. 一般问题(如果没有,请写“无一般问题”)\n - 【一般】描述问题 + 涉及的文件/大致位置 + 原因"}
${of.suggestion || "4. 建议与优化(如果没有,可以写“暂无明显优化建议”)"}
${of.blockRule || "5. 最终结论(只保留一行,且用英文 BLOCK 标记,必须大写):\n - 若必须在提交前修复,请输出:BLOCK: YES\n - 若允许提交,仅给出建议,请输出:BLOCK: NO"}
注意:
- 不要输出除上述格式以外的额外总结行。
- “BLOCK: YES/NO” 必须是独立一行,前后不要加其他字符。
下面是本次提交的 diff(统一 diff 格式):
${diff}
`;
}
// 调用 DeepSeek Chat API
function callDeepseek({ apiKey, baseUrl, model, prompt }) {
return new Promise((resolve, reject) => {
const postData = JSON.stringify({
model,
messages: [
{ role: "system", content: "你是一个资深代码审查工程师,擅长发现问题并提出可行建议。" },
{ role: "user", content: prompt }
],
temperature: 0.2
});
const url = new URL("/v1/chat/completions", baseUrl);
const options = {
method: "POST",
hostname: url.hostname,
path: url.pathname,
port: url.port || 443,
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
"Content-Length": Buffer.byteLength(postData)
}
};
const req = https.request(options, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
const json = JSON.parse(data);
const content =
json.choices?.[0]?.message?.content ||
json.choices?.[0]?.text ||
"";
resolve(content.trim());
} catch (e) {
reject(new Error("解析 DeepSeek 响应失败:" + e.message));
}
} else {
reject(
new Error(`DeepSeek API 响应错误:${res.statusCode} ${res.statusMessage} ${data}`)
);
}
});
});
req.on("error", (err) => reject(err));
req.write(postData);
req.end();
});
}
你可以直接通过修改这个文件来:
- 调整审查关注点(例如换成「支付正确性」「权限控制」等)
- 改写分级说明,贴近你们团队的规范
- 修改输出格式,比如增加某个模块的专门小节
- 切换默认阻断策略(blockOnSevereDefault)
模型配置通过 .env.local 管理
为了避免把 Key 写死在代码或 shell 配置里,我们采用 Next.js 常见的 .env.local 风格:
# .env.local(不提交到仓库)
DEEPSEEK_API_KEY=你的_API_Key
DEEPSEEK_API_BASE=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-chat
# 是否在严重问题时阻止提交(1 阻止 / 0 不阻止)
DEEPSEEK_BLOCK_ON_SEVERE=1
脚本启动时会:
- 查找当前工作目录的 .env.local
- 解析每一行 KEY=VALUE,写入 process.env
- 如果系统环境变量里已经有同名 KEY,则以系统环境变量为准
这样做有几个好处:
- 适合前端/Node 项目现有习惯
- 可以按仓库存储不同 Key/配置
- 不需要全局污染 shell 环境
Git Hook:在提交前自动执行审查
最后一步,把脚本接到 Git 的 pre-commit 阶段。
由于是拿本项目来执行验证的这里使用 .githooks/pre-commit + core.hooksPath 的方式,方便版本管理。
在项目根目录创建 .githooks/pre-commit:
#!/bin/sh
# 简单的 pre-commit hook 示例:
# 在执行 git commit 时,自动调用 DeepSeek 代码审查脚本。
echo "[pre-commit] 调用 DeepSeek 进行代码审查..."
node scripts/deepseek-review.js
STATUS=$?
if [ $STATUS -ne 0 ]; then
echo "[pre-commit] DeepSeek 代码审查脚本执行失败或发现严重问题,终止提交。"
exit $STATUS
fi
exit 0
然后在仓库里启用它:
git config core.hooksPath .githooks
chmod +x .githooks/pre-commit
chmod +x scripts/deepseek-review.js
从此以后,每次在这个仓库里执行:
git add .
git commit -m "feat: xxx"
都会自动触发:
- pre-commit → node scripts/deepseek-review.js
- 脚本读取 git diff --cached
- 调用 DeepSeek → 按规则分级输出问题,并在末尾给出 BLOCK: YES/NO
- 如果是 BLOCK: YES 且当前规则/环境要求阻断 → 本次提交直接失败
审核示例
总结与一些延伸想法
到这里,我们落地了一套本地可配置的 AI Code Review 流程,特点是:
- 与 Git 强绑定:在提交前提前拦截严重问题
- 规则可配置:通过 rules.json 抽象了审查维度和分级
- 模型可配置:模型信息放在 .env.local,符合前端/Node 生态的习惯
- 行为可调:既可以强制阻断严重问题,也可以在开发早期只做「提示不阻断」
基于这套结构,你还可以继续扩展:
- 针对不同分支(main / dev)选择不同的规则文件
- 在审查结果中增加代码示例或重构建议模板
- 将结果同步到 Git 平台(GitLab MR comment / GitHub PR comment)
由于工具当前只是 0.1.0 版本还有很多待改进的地方,后续将不断优化改进。
vue5
el-container
el-container是ElementPlus中的布局容器组件,主要用于快速构建基础布局结构,比如:顶部-主体-底部,侧边栏-主体等常见页面结构。把页面按照上下或左右方向组织起来,让布局变得简单、清晰、可读性高。
它通常和以下组件一起使用:
- el-header: 页头区域
- el-aside: 侧边栏区域
- el-main: 主体内容区域
- el-footer: 页脚区域
基本用法举例
上下布局(Header + Main)
<el-container>
<el-header>Header</el-header>
<el-main>Main</el-main>
</el-container>
左右布局(Aside + Main)
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-main>Main</el-main>
</el-container>
v-if
<router-link v-if="!['mixin'].includes(appStore.layoutMode)" to="/">om</router-link>
<router-link v-else to="/">
<span>OM OS</span>
</router-link>
v-if 是vue提供的条件渲染指令,作用是根据表达式的真假来决定是否渲染这个元素(或组件)。当表达式为false时,vue会完全移除这个元素,不会出现在DOM中。
也就是说,在上面的代码中,当layoutMode中不含mixin时,om不会被渲染,显示的文本是OM OS
同理:
<template v-if="mode==='horizontal'">
......
</template>
<template v-if="mode === 'vertical'">
......
</template>
也是在符合条件的情况下,template才会被渲染
至于这里为什么用template,主要是因为v-if只能绑定在一个元素上,但有时你必须包住多个节点,所以vue提供<template v-if="condition">用于对多个子元素统一控制显示/隐藏
这里不用div的原因是,template 不会渲染标签内容,只会渲染内部内容,而div会在页面中渲染一个实际的div
动态绑定css
<el-icon color="#373839" class="nav-bar-left-icon" :class="{'is-active':appStore.sidebar.opened}" title="展开">
<Expand/>
</el-icon>
<el-icon color="#373839" class="nav-bar-left-icon" :class="{'is-active':!appStore.sidebar.opened}" title="折叠">
<Fold/>
</el-icon>
.bar-left-ex {
margin: 0 10px;
.nav-bar-left-icon {
……
}
.is-active {
display: none;
}
}
这里:class="{'is-active': appStore.sidebar.opened}是动态Class绑定,写法含义:
{
'is-active': appStore.sidebar.opened
}
- 如果
appStore.sidebar.opened === true-> 给这个元素添加is-activeclass - 如果为false -> 不添加
通过改变appStore.sidebar.opened的值,来使el-icon显示或者不显示
el-scrollbar
<el-scrollbar>
<el-menu
class="hor-menu"
:mode="mode"
ellipsis
:default-active="currentRoute.path"
background-color="transparent"
>
<MenuItems :routes="filteredRoutes"/>
</el-menu>
</el-scrollbar>
- el-scrollbar
ElementPlus提供的菜单滚动容器,如果菜单太长,超过屏幕宽度,会出现滚动区域,而不是挤到一行显示不下。
- el-menu:ElementPlus菜单组件
-
:mode="mode" 指定菜单模式,horizontal->横向菜单(用于顶部导航),vertical->纵向菜单(用于侧边栏)
-
ellipsis:ElementPlus自带属性,开启文字过长时的省略号效果
-
:default-active="currentRoute.path":菜单默认选中当前路由对应的菜单项,例如当前路由/home,那么"/home"的菜单项会高亮,菜单随着路由变化自动更新状态。
-
导航栏
const props = defineProps({
mode: {
type: String,
default: 'vertical'
// default: 'horizontal'
}
})
prop
prop:父组件传给子组件的数据,就像函数的参数,组件也需要参数才能知道该怎么工作,而props就是这个参数
- 为什么需要props?
因为组件是可以复用的,不能在组件里写死数据,例如做一个按钮组件<MyButton>
- 有的按钮是红色
- 有的是绿色
- 有的是大按钮
- 有的是小按钮
如果都写死,那组件就没法复用了
所以props让这样用:
<MyButton color="red" size="large" />
<MyButton color="green" size="small" />
vue中如何定义
const props = defineProps({
color: String,
size: String
})
例如:
子组件:
<script setup>
const props = defineProps({
mode: {
type: String,
default: 'vertical'
}
})
</script>
<template>
<div>当前模式:{{ props.mode }}</div>
</template>
父组件:
父组件只是传值,不定义props
<MyMenu mode="horizontal" />
除了父组件,任何地方都不能直接修改props 因为vue的设计理念是:父组件负责数据,子组件负责展示和逻辑,子组件不能私自篡改父组件的数据
v-model
v-model = 把组件的值和变量做双向绑定
<el-switch size="default" v-model="appStore.watermarking" @change="appStore.changeWatermarking()"/>
即:
-
el-switch显示的开/关状态 =appStore.watermarking - 你点击 switch → Element Plus 自动把新值写回
appStore.watermarking
也就是说,点switch的同时,ElementPlus自动执行appStore.watermarking = true/false
@change这里用来进行一些存数据库或发日志等辅助操作:
const changeWatermarking = () => {
dbUtils.set('watermarking', watermarking.value);
}
JavaScript核心机制:执行栈、作用域与this指向完全解析
前言
JavaScript作为一门灵活而强大的语言,其底层运行机制常常让开发者感到困惑。为什么变量有时能访问,有时不能?为什么this的指向会"飘忽不定"?本文将深入剖析JavaScript的三大核心机制——执行栈与执行上下文、作用域与作用域链、this指向,帮你彻底理解JavaScript的运行原理。
一、执行栈与执行上下文:代码执行的舞台
1.1 面试直击:谈谈你对JS执行上下文栈的理解
当面试官问这个问题时,他们想考察的是你对JavaScript引擎如何管理和执行代码的理解深度。执行上下文栈是引擎追踪所有函数调用、管理执行流程的核心调度中心。
*什么是执行上下文?
简而言之,执行上下文是评估和执行 js 代码的环境的抽象概念。每当 js 代码在运行的时候,它都是在执行上下文中执行。
执行上下文的类型
js 中有三种执行上下文类型
- 全局执行上下文:这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事,创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
- 函数执行上下文:每当一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每单一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
- eval 函数执行上下文:执行在 eval 函数内部的代码也会有它属于自己的执行上下文。
调用栈
调用栈是解析器(如浏览器中的 js 解析器)的一种机制,可以在脚本调用多个函数时,跟踪每个函数在完成执行时应该返回控制的点。(如什么函数正在执行,什么函数被这个函数调用,下一个调用的函数是谁)
- 当脚本要调用一个函数时,解析器把该函数添加到栈中并且执行这个函数。
- 任何被这个函数调用的函数会进一步添加到调用栈中,并且运行到它们被上个程序调用的位置。
- 当函数运行结束后,解释器将它从堆栈中取出,并且主代码列表中继续执行代码。
- 如果栈占用的空间比分配给它的空间还大,那么则会导致“栈溢出”错误
1.2 什么是执行上下文?
简单来说,执行上下文是评估和执行JavaScript代码的环境的抽象概念。所有的JavaScript代码都是在某个执行上下文中运行的。
1.3 执行上下文的三种类型
// 1. 全局执行上下文
var globalVar = "全局变量"; // 在全局上下文中
// 2. 函数执行上下文
function foo() {
var localVar = "局部变量"; // 在函数上下文中
console.log(localVar);
}
// 3. Eval函数执行上下文(不推荐使用)
eval("var evalVar = 'eval变量'");
全局执行上下文的特点:
-
有且仅有一个
-
创建时会做两件事:
- 在浏览器中创建全局对象
window - 将
this的值设置为这个全局对象
- 在浏览器中创建全局对象
1.4 执行栈(调用栈):后进先出的管理机制
执行栈是一种LIFO(后进先出)的数据结构,用于存储和管理代码执行期间创建的所有执行上下文。
function first() {
console.log('第一个函数开始');
second();
console.log('第一个函数结束');
}
function second() {
console.log('第二个函数开始');
third();
console.log('第二个函数结束');
}
function third() {
console.log('第三个函数开始');
console.log('第三个函数结束');
}
first();
// 执行栈的变化过程:
// 1. 全局上下文入栈
// 2. first()调用,first上下文入栈
// 3. second()调用,second上下文入栈
// 4. third()调用,third上下文入栈
// 5. third()执行完毕,出栈
// 6. second()执行完毕,出栈
// 7. first()执行完毕,出栈
1.5 执行上下文的生命周期
每个执行上下文都经历两个阶段:
创建阶段(此时函数被调用,但未执行内部代码):
function example(a, b) {
var c = "hello";
function d() {}
var e = function() {};
}
// 在创建阶段,执行上下文会进行以下准备工作:
exampleExecutionContext = {
// 1. 创建变量对象
variableObject: {
arguments: { 0: a, 1: b, length: 2 },
a: undefined, // 形参
b: undefined, // 形参
c: undefined, // 变量声明
d: pointer_to_function_d, // 函数声明
e: undefined // 变量声明
},
// 2. 建立作用域链
scopeChain: [...],
// 3. 确定this指向
this: window
}
执行阶段(逐行执行代码):
function example(a = 1, b = 2) {
console.log(c); // undefined(变量提升)
console.log(d); // function d() {}(函数提升)
var c = "hello";
function d() {
console.log("我是函数d");
}
var e = function() {
console.log("我是函数表达式");
};
console.log(c); // "hello"
console.log(d); // function d() {}
console.log(e); // function() { ... }
}
example();
重要规则:
- 函数声明会完全提升
- 变量声明会部分提升(只提升声明,不提升赋值)
- 函数声明的优先级高于变量声明
1.6 栈溢出错误
// 递归调用没有终止条件会导致栈溢出
function infiniteRecursion() {
infiniteRecursion(); // 不断创建新的执行上下文
}
// 调用会抛出:RangeError: Maximum call stack size exceeded
// infiniteRecursion();
二、作用域与作用域链:变量的可见性规则
2.1 面试题:谈谈你对作用域和作用域链的理解
作用域和作用域链决定了变量在何处以及如何被访问,这是JavaScript的基础概念。
什么是作用域?
作用域是在运行时代码中某些特定部分变量、函数和对象的可访问性,决定了代码区块中变量和其他资源的可见性。ES5 中只存在两种作用域:全局作用域和函数作用域,ES6 新增了块级作用域。
什么是作用域链?
当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止。
而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域链对符合访问权限的变量和函数的有序访问。
作用域链有一个非常重要的特征,那就是作用域中的值是在函数创建的时候,就已经被存储了,是静态的。 所谓静态,就是是作用域中的值一旦被确定了,永远不会变。函数可以永远不被调用,但是作用域中的值在函数创建的时候就已经被写入了,并且存储在函数作用域链对象里面。
2.2 什么是作用域?
作用域定义了变量、函数和对象的可访问性范围。
// 全局作用域
var globalVar = "我是全局变量";
function outer() {
// 函数作用域
var outerVar = "我是外部函数变量";
function inner() {
// 另一个函数作用域
var innerVar = "我是内部函数变量";
console.log(globalVar); // 可以访问
console.log(outerVar); // 可以访问
console.log(innerVar); // 可以访问
}
inner();
console.log(globalVar); // 可以访问
console.log(outerVar); // 可以访问
// console.log(innerVar); // 错误!无法访问内部变量
}
outer();
2.3 ES6的块级作用域
// ES5只有全局和函数作用域
function es5Example() {
if (true) {
var varVariable = "var声明的变量";
let letVariable = "let声明的变量";
const constVariable = "const声明的变量";
}
console.log(varVariable); // "var声明的变量" - 可以访问
// console.log(letVariable); // ReferenceError - 不能访问
// console.log(constVariable); // ReferenceError - 不能访问
}
// ES6的块级作用域
{
let blockScoped = "块级作用域变量";
const constant = "常量";
}
// console.log(blockScoped); // ReferenceError
// console.log(constant); // ReferenceError
2.4 作用域链
当访问一个变量时,JavaScript引擎会沿着作用域链从内到外查找:
var global = "全局";
function outer() {
var outer = "外层";
function inner() {
var inner = "内层";
// 查找顺序:inner → outer → global
console.log(inner); // "内层" - 在当前作用域找到
console.log(outer); // "外层" - 在父作用域找到
console.log(global); // "全局" - 在全局作用域找到
console.log(notExist); // ReferenceError - 找不到
}
inner();
}
outer();
2.5 自由变量和词法作用域
自由变量:在当前作用域未定义,需要去父作用域查找的变量。
var x = 10;
function foo() {
console.log(x); // x是自由变量
}
function bar() {
var x = 20;
foo(); // 输出10,而不是20!
}
bar();
// 这是因为JavaScript采用词法作用域(静态作用域)
// 函数的作用域在定义时就确定了,而不是调用时
2.6 作用域 vs 执行上下文
关键区别:
- 作用域:函数定义时确定,静态不变
- 执行上下文:函数调用时创建,动态变化
var color = "blue";
function getColor() {
console.log(this.color); // 执行上下文决定this
console.log(color); // 作用域决定color的值
}
var obj = {
color: "red",
getColor: getColor
};
// 同样的函数,不同的调用方式
getColor(); // this: window, color: "blue"
obj.getColor(); // this: obj, color: "blue"(作用域不变!)
三、this指向:动态绑定的上下文
3.1 this的本质
this关键字总是指向一个对象,具体指向哪个对象取决于函数的调用方式。
关于 this 的指向,有一种广为流传的说法就是“谁调用它,this 就指向谁”。
- 在函数体中,非显示或隐式地简单调用函数时,在严格模式下,函数内的 this 会被绑定到 undefined 上,在非严格模式下则会被绑定到全局对象 window/global 上。
- 一般使用 new 方法调用构造函数时,构造函数内的 this 会被绑定到新创建的对象上。
- 一般通过 call/apply/bind 方法显示调用函数时,函数体内的 this 会被绑定到指定参数的对象上。
- 一般通过上下文对象调用函数时,函数体内的 this 会被绑定到该对象上。
- 在箭头函数中,this 的指向是由外层(函数或全局)作用域来决定的。
3.2 this的绑定规则
1. 默认绑定(普通函数调用)
// 非严格模式
function showThis() {
console.log(this);
}
showThis(); // 浏览器中输出:Window对象
// 严格模式
"use strict";
function strictShowThis() {
console.log(this);
}
strictShowThis(); // undefined
2. 隐式绑定(方法调用)
var obj = {
name: "张三",
sayName: function() {
console.log(this.name);
}
};
obj.sayName(); // "张三" - this指向obj
// 隐式丢失的常见情况
var sayName = obj.sayName;
sayName(); // "" 或 undefined - this指向全局
// 回调函数中的隐式丢失
setTimeout(obj.sayName, 100); // this指向全局
3. 显式绑定(call/apply/bind)
function introduce(lang, year) {
console.log(`我叫${this.name},擅长${lang},${year}年开始编程`);
}
var person1 = { name: "Alice" };
var person2 = { name: "Bob" };
// call - 立即调用,参数逐个传递
introduce.call(person1, "JavaScript", 2015);
// 输出:我叫Alice,擅长JavaScript,2015年开始编程
// apply - 立即调用,参数数组传递
introduce.apply(person2, ["Python", 2018]);
// 输出:我叫Bob,擅长Python,2018年开始编程
// bind - 创建新函数,不立即调用
var boundIntroduce = introduce.bind(person1, "Java");
boundIntroduce(2020);
// 输出:我叫Alice,擅长Java,2020年开始编程
4. new绑定(构造函数调用)
function Person(name, age) {
// new调用时,this指向新创建的对象
this.name = name;
this.age = age;
this.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
}
var p1 = new Person("张三", 25);
p1.sayHello(); // "你好,我是张三"
5. 箭头函数的this
var obj = {
name: "对象",
regularFunc: function() {
console.log("普通函数this:", this.name);
setTimeout(function() {
console.log("setTimeout普通函数this:", this.name);
}, 100);
setTimeout(() => {
console.log("setTimeout箭头函数this:", this.name);
}, 200);
},
arrowFunc: () => {
console.log("箭头函数this:", this.name);
}
};
obj.regularFunc();
// 普通函数this: 对象
// setTimeout普通函数this: (全局的name,通常是空)
// setTimeout箭头函数this: 对象
obj.arrowFunc();
// 箭头函数this: (全局的name)
3.3 this绑定的优先级
// 优先级测试
function test() {
console.log(this.value);
}
var obj1 = { value: "obj1", test: test };
var obj2 = { value: "obj2", test: test };
// 1. 默认绑定(优先级最低)
test(); // undefined 或 全局的value
// 2. 隐式绑定
obj1.test(); // "obj1"
// 3. 显式绑定 > 隐式绑定
obj1.test.call(obj2); // "obj2"
// 4. new绑定 > 显式绑定
var boundTest = test.bind(obj1);
var newObj = new boundTest(); // undefined(new绑定覆盖了bind绑定)
3.4 常见应用场景
DOM事件处理
// HTML: <button id="btn">点击我</button>
document.getElementById('btn').addEventListener('click', function() {
console.log(this); // 指向被点击的button元素
});
// 箭头函数会改变this指向
document.getElementById('btn').addEventListener('click', () => {
console.log(this); // 指向定义时的上下文,通常是window
});
类中的this
class Counter {
constructor() {
this.count = 0;
// 需要绑定this,否则作为回调时会丢失
this.increment = this.increment.bind(this);
}
increment() {
this.count++;
console.log(this.count);
}
// 使用箭头函数自动绑定
decrement = () => {
this.count--;
console.log(this.count);
}
}
const counter = new Counter();
document.getElementById('inc').addEventListener('click', counter.increment);
document.getElementById('dec').addEventListener('click', counter.decrement);
3.5 call、apply、bind的经典应用
// 1. 借用数组方法处理类数组对象
function sum() {
// arguments是类数组对象,没有数组方法
return Array.prototype.reduce.call(arguments, function(total, current) {
return total + current;
}, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
// 2. 获取数组最大/最小值
var numbers = [5, 6, 2, 3, 7];
var max = Math.max.apply(null, numbers); // 7
var min = Math.min.call(null, ...numbers); // 2(使用扩展运算符)
// 3. 继承和构造函数链式调用
function Animal(name) {
this.name = name;
}
function Dog(name, breed) {
Animal.call(this, name); // 调用父类构造函数
this.breed = breed;
}
var myDog = new Dog("旺财", "金毛");
console.log(myDog.name); // "旺财"
// 4. 函数柯里化(Currying)
function multiply(a, b, c) {
return a * b * c;
}
var double = multiply.bind(null, 2); // 固定第一个参数
console.log(double(3, 4)); // 24 (2 * 3 * 4)
console.log(double(5, 6)); // 60 (2 * 5 * 6)
四、综合案例分析
4.1 经典面试题解析
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn(); // 1. 直接调用,this指向全局
arguments[0](); // 2. 通过arguments调用
fn.call(obj); // 3. 显式绑定到obj
}
};
obj.method(fn, 1);
// 输出:
// 10 (全局length)
// 2 (arguments.length)
// 5 (obj.length)
4.2 复杂场景分析
var name = "全局";
var person = {
name: "张三",
sayName: function() {
console.log("外层this:", this.name);
return function() {
console.log("内层this:", this.name);
};
},
sayNameArrow: function() {
console.log("外层this:", this.name);
return () => {
console.log("内层箭头函数this:", this.name);
};
}
};
// 情况1:普通函数嵌套
var func1 = person.sayName();
func1();
// 外层this: 张三
// 内层this: 全局
// 情况2:箭头函数解决
var func2 = person.sayNameArrow();
func2();
// 外层this: 张三
// 内层箭头函数this: 张三
// 情况3:使用bind绑定
var func3 = person.sayName().bind(person);
func3();
// 外层this: 张三
// 内层this: 张三
4.3 性能优化建议
- 避免过度嵌套:深度嵌套的作用域链会增加变量查找时间
- 合理使用闭包:不必要的闭包会延长变量的生命周期
- 注意内存泄漏:意外的全局变量和未清除的引用会导致内存泄漏
- 适当使用严格模式:避免意外的全局绑定和不可预测的this
五、总结
5.1 核心概念回顾
- 执行栈:管理执行上下文的后进先出数据结构
- 执行上下文:代码执行的环境,包含变量对象、作用域链和this
- 作用域:变量和函数的可访问范围,在定义时确定
- 作用域链:从当前作用域到全局作用域的链式结构
- this指向:由调用方式决定,遵循特定的绑定规则
5.2 记忆口诀
- 执行上下文:调用函数才创建,入栈出栈管流程
- 作用域链:内层可访外层,定义位置就决定
- this指向:谁调用就指谁,箭头函数看外层
- 绑定优先级:new > 显式 > 隐式 > 默认
5.3 实战建议
- 使用
const和let替代var,利用块级作用域 - 在回调函数中注意this绑定,使用箭头函数或bind
- 理解闭包原理,避免不必要的内存占用
- 掌握call/apply/bind的适用场景,灵活改变this指向
通过深入理解这三个核心概念,你将能够:
- 准确预测代码的执行结果
- 避免常见的this绑定错误
- 编写更高效、可维护的JavaScript代码
- 在面试中从容应对相关技术问题
JavaScript的运行机制虽然复杂,但只要掌握了这些核心概念,你就能从"知其然"进阶到"知其所以然",真正驾驭这门灵活而强大的语言。
系统性整理组件传参14种方式
标题前言:
在面试时被问到组件传参的方式没有答的很完整全面,在经过很多面试之后发现,面试的回答已经不在于你是否答出来,更高的一个level是要答全,答出其他面试者答不出来的,得有自己的一个框架,于是系统性整理了14种方式 系统化梳理每一种方式的原理、用法、适用场景、优缺点及注意事项,并标注其在 Vue 2 vs Vue 3 中的支持情况,帮助你全面掌握。
📚 Vue 组件传参与通信方式全解析(14 种)
✅ 表示推荐 / 安全
⚠️ 表示谨慎使用
❌ 表示已废弃 / 不推荐
1. props(✅ 推荐)
- 方向:父 → 子
- 原理:声明式属性传递,单向数据流
- Vue 2/3:✅ 全支持
-
示例:
<!-- 父 --> <Child :title="msg" /> <!-- 子 --> defineProps({ title: String }) - 优点:清晰、类型安全、可预测
- 注意:不要直接修改 prop(Vue 会警告)
2. $emit / v-on(✅ 推荐)
- 方向:子 → 父
- 原理:子组件触发自定义事件,父组件监听
-
Vue 2:
this.$emit('event', data) -
Vue 3:
const emit = defineEmits(['event']) -
示例:
<!-- 子 --> emit('update', newValue) <!-- 父 --> <Child @update="handle" /> - 优点:解耦、符合事件驱动思想
3. .sync 修饰符(⚠️ Vue 2 专用,Vue 3 已移除)
-
原理:语法糖,等价于
:prop + @update:prop -
Vue 2 示例:
<Child :title.sync="pageTitle" /> <!-- 子组件需 emit('update:title', newTitle) --> -
Vue 3:❌ 不支持,改用
v-model:propName -
替代方案:多
v-model(见第 4 条)
4. v-model(✅ 推荐,Vue 3 增强)
- 原理:双向绑定语法糖
-
Vue 2:仅支持单个
value+input事件 -
Vue 3:支持多个
v-model:propName<Child v-model:name="userName" v-model:age="userAge" /> <!-- 子组件需 emit('update:name', ...) --> - 优点:简洁、语义清晰,适合表单控件
5. ref(✅ 有限推荐)
- 方向:父 → 子(获取子实例或 DOM)
-
原理:通过
ref引用子组件实例,直接调用方法或访问数据 -
示例:
<Child ref="childRef" /> // 父组件中:this.$refs.childRef.doSomething()(Vue 2) // Vue 3:const childRef = ref(); childRef.value.doSomething() - 适用场景:调用子组件方法(如 focus、validate)
- ⚠️ 注意:破坏封装性,应避免读写子组件内部状态
6. parent(❌ 不推荐)
- 原理:直接访问父子组件实例
-
问题:
-
$children顺序不确定 - 破坏组件独立性
- 难以维护和测试
-
-
Vue 3:❌
$children已移除,$parent仍存在但不鼓励使用 - 替代方案:props / emits / provide-inject
7. listeners(✅ Vue 2;Vue 3 合并为 $attrs)
- 用途:透传未声明的 props 和事件(常用于高阶组件、包装组件)
-
Vue 2:
-
$attrs:未被 props 声明的 attribute -
$listeners:所有v-on事件监听器
-
-
Vue 3:
$listeners被合并进$attrs(包含onXxx事件) -
典型用法:封装第三方 UI 组件
<!-- Wrapper.vue --> <el-input v-bind="$attrs" v-on="$listeners" />
8. provide / inject(✅ 推荐)
- 方向:祖先 → 后代(跨多层)
- 原理:依赖注入,类似 React Context
-
响应式:需传递
ref或reactive对象// 祖先 provide('theme', themeRef) // 后代 const theme = inject('theme') - 适用:主题、语言、用户信息等全局配置
- Vue 2/3:✅ 支持(Vue 3 更简洁)
9. EventBus(事件总线)(⚠️ 谨慎使用)
- 原理:基于发布-订阅模式的全局通信
-
实现:
- Vue 2:
new Vue()作事件中心 - Vue 3:需引入
mitt等库
- Vue 2:
-
问题:
- 难以追踪数据流
- 容易内存泄漏(忘记 off)
- 不利于大型项目维护
- 建议:仅用于小型项目或临时解耦,优先用 Pinia
10. Vuex / Pinia(状态管理)(✅ 大型项目推荐)
- 原理:集中式状态管理
- Vuex:Vue 2 官方方案(较重)
- Pinia:Vue 3 官方推荐(更轻量、TypeScript 友好)
-
优点:
- 状态可预测
- 支持 DevTools 调试
- 逻辑复用(actions/getters)
- 适用:多组件共享状态、持久化、复杂业务逻辑
11. $root(❌ 不推荐)
-
原理:访问根实例(
new Vue()) -
问题:
- 全局耦合
- 难以测试和维护
- 在组件库或微前端中不可靠
-
Vue 3:❌
$root仍存在但强烈不建议使用 - 替代:provide/inject 或 Pinia
12. slot(插槽)(✅ 推荐)
- 方向:父 → 子(内容分发)
-
类型:
- 默认插槽
<slot /> - 具名插槽
<slot name="header" /> -
作用域插槽(关键!):子 → 父传数据
<!-- 子 --> <slot :user="currentUser" /> <!-- 父 --> <Child v-slot="{ user }">{{ user.name }}</Child>
- 默认插槽
- 适用:高度可定制组件(表格、弹窗、卡片)
13. sessionStorage / localStorage(⚠️ 特定场景)
- 原理:通过浏览器存储实现“伪通信”
-
适用场景:
- 页面刷新后保持状态
- 多 Tab 间简单同步(配合
storage事件)
-
缺点:
- 非响应式(需手动监听
storage事件) - 数据类型限制(仅字符串)
- 不适合实时通信
- 非响应式(需手动监听
- 建议:仅用于持久化,非组件通信首选
14. postMessage(⚠️ 跨文档/跨域通信)
- 原理:HTML5 提供的安全跨域通信机制
-
适用场景:
- iframe 与主页面通信
- Web Worker 与主线程
- 跨域窗口通信
-
示例:
// 主页面 iframe.contentWindow.postMessage(data, '*') window.addEventListener('message', handler) -
注意:需验证
event.origin防止 XSS - 与组件通信关系:属于跨上下文通信,非组件内部机制
📊 总结对比表
| 方式 | 方向 | Vue 2 | Vue 3 | 推荐度 | 适用场景 |
|---|---|---|---|---|---|
| props | 父→子 | ✅ | ✅ | ✅✅✅ | 基础数据传递 |
| $emit / v-on | 子→父 | ✅ | ✅ | ✅✅✅ | 事件通知 |
| .sync | 双向 | ✅ | ❌ | ⚠️ | Vue 2 双向绑定(已淘汰) |
| v-model | 双向 | ✅(单) | ✅(多) | ✅✅✅ | 表单、可编辑组件 |
| ref | 父→子(调用) | ✅ | ✅ | ✅✅ | 调用子方法 |
| parent | 双向 | ✅ | ❌/$parent | ❌ | —— |
| listeners | 透传 | ✅ | ✅(合并) | ✅✅ | 高阶组件封装 |
| provide/inject | 祖先→后代 | ✅ | ✅ | ✅✅✅ | 跨层级配置 |
| EventBus | 任意 | ✅ | 需 mitt | ⚠️ | 小型项目临时通信 |
| Vuex/Pinia | 全局 | ✅/✅ | Pinia✅ | ✅✅✅ | 复杂状态共享 |
| $root | 全局 | ✅ | ✅(不推荐) | ❌ | —— |
| slot | 父→子(内容) | ✅ | ✅ | ✅✅✅ | UI 定制 |
| sessionStorage | 持久化 | ✅ | ✅ | ⚠️ | 刷新保活、多 Tab |
| postMessage | 跨上下文 | ✅ | ✅ | ⚠️ | iframe、Worker |
✅ 最佳实践建议
- 优先使用 props + emits:保持组件清晰。
-
跨层级用 provide/inject,而非
$parent。 -
共享状态用 Pinia,而非 EventBus 或
$root。 - 避免直接操作子组件(ref 仅用于方法调用)。
- 作用域插槽是高级组件设计的利器。
- localStorage / postMessage 属于特殊场景,勿滥用。
Vue2简单实现一个权限管理
以下是基于 Vue2 实现的基础权限管理方案,涵盖路由权限控制、按钮权限控制、指令封装等核心功能,适合中小型后台系统:
一、权限数据设计
首先定义用户权限结构(通常从后端接口获取):
// src/store/modules/auth.js
const state = {
// 用户角色(如 ['admin', 'editor'])
roles: [],
// 用户拥有的权限码(如 ['user:add', 'user:edit'])
permissions: []
};
const mutations = {
SET_ROLES(state, roles) {
state.roles = roles;
},
SET_PERMISSIONS(state, permissions) {
state.permissions = permissions;
}
};
const actions = {
// 模拟从后端获取权限(实际项目中替换为接口请求)
getAuth({ commit }) {
return new Promise(resolve => {
// 假设后端返回的权限数据
const authData = {
roles: ['editor'],
permissions: ['dashboard:view', 'user:view', 'user:edit']
};
commit('SET_ROLES', authData.roles);
commit('SET_PERMISSIONS', authData.permissions);
resolve();
});
}
};
export default {
namespaced: true,
state,
mutations,
actions
};
二、路由权限控制
通过路由守卫过滤无权限的路由,结合VueRouter的动态路由添加:
1. 定义路由规则(区分公开 / 私有路由)
// src/router/index.js
import Vue from 'vue';
import Router from 'vue-router';
import store from '@/store';
Vue.use(Router);
// 公开路由(无需权限)
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login')
},
{
path: '/404',
component: () => import('@/views/404')
}
];
// 私有路由(需权限)
export const asyncRoutes = [
{
path: '/',
component: () => import('@/layout'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
component: () => import('@/views/dashboard'),
meta: {
title: '首页',
roles: ['admin', 'editor'], // 需要的角色
permissions: ['dashboard:view'] // 需要的权限码
}
}
]
},
{
path: '/user',
component: () => import('@/layout'),
children: [
{
path: 'list',
component: () => import('@/views/user/list'),
meta: {
title: '用户列表',
permissions: ['user:view']
}
},
{
path: 'edit/:id',
component: () => import('@/views/user/edit'),
meta: {
title: '编辑用户',
permissions: ['user:edit']
}
}
]
},
{ path: '*', redirect: '/404', hidden: true }
];
const createRouter = () => new Router({
routes: constantRoutes
});
const router = createRouter();
export default router;
2. 路由守卫实现权限过滤
// src/permission.js
import router from './router';
import store from './store';
import { asyncRoutes, constantRoutes } from './router';
// 权限判断函数:检查是否有角色/权限
function hasPermission(roles, permissions, route) {
if (route.meta && route.meta.roles) {
// 角色校验
return roles.some(role => route.meta.roles.includes(role));
}
if (route.meta && route.meta.permissions) {
// 权限码校验
return permissions.some(perm => route.meta.permissions.includes(perm));
}
return true; // 无权限配置则默认可见
}
// 过滤私有路由
function filterAsyncRoutes(routes, roles, permissions) {
const res = [];
routes.forEach(route => {
const tmp = { ...route };
if (hasPermission(roles, permissions, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles, permissions);
}
res.push(tmp);
}
});
return res;
}
router.beforeEach(async (to, from, next) => {
// 模拟已登录(实际项目中用token判断)
const hasToken = true;
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' });
} else {
// 判断是否已获取权限
const hasRoles = store.getters['auth/roles'].length > 0;
if (hasRoles) {
next();
} else {
try {
// 获取权限数据
await store.dispatch('auth/getAuth');
const roles = store.getters['auth/roles'];
const permissions = store.getters['auth/permissions'];
// 过滤并添加动态路由
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles, permissions);
router.addRoutes(accessedRoutes);
// 确保路由添加完成后跳转
next({ ...to, replace: true });
} catch (err) {
next(`/login?redirect=${to.path}`);
}
}
}
} else {
// 未登录则跳转登录页
if (to.path === '/login') {
next();
} else {
next(`/login?redirect=${to.path}`);
}
}
});
三、按钮权限控制
通过自定义指令实现按钮级别的权限控制:
1. 注册权限指令
// src/directives/permission.js
import Vue from 'vue';
import store from '@/store';
/**
* v-permission: 按钮权限指令
* 使用:<button v-permission="'user:add'">新增</button>
*/
Vue.directive('permission', {
inserted(el, binding) {
const { value } = binding;
const permissions = store.getters['auth/permissions'];
// 校验权限(支持数组:v-permission="['user:add', 'user:edit']")
if (value) {
const hasPerm = Array.isArray(value)
? permissions.some(perm => value.includes(perm))
: permissions.includes(value);
if (!hasPerm) {
el.parentNode && el.parentNode.removeChild(el); // 无权限则移除元素
}
}
}
});
/**
* v-role: 角色权限指令
* 使用:<button v-role="'admin'">管理员按钮</button>
*/
Vue.directive('role', {
inserted(el, binding) {
const { value } = binding;
const roles = store.getters['auth/roles'];
if (value) {
const hasRole = Array.isArray(value)
? roles.some(role => value.includes(role))
: roles.includes(value);
if (!hasRole) {
el.parentNode && el.parentNode.removeChild(el);
}
}
}
});
2. 全局引入指令
// src/main.js
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import './directives/permission'; // 引入权限指令
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
四、页面中使用权限控制
<!-- src/views/user/list.vue -->
<template>
<div>
<h1>用户列表</h1>
<!-- 按钮权限控制:只有user:add权限才显示 -->
<button v-permission="'user:add'">新增用户</button>
<!-- 角色控制:只有admin角色才显示 -->
<button v-role="'admin'">删除用户</button>
<!-- 表格内容 -->
<el-table :data="userList">
<el-table-column label="操作">
<template slot-scope="scope">
<!-- 编辑权限控制 -->
<el-button v-permission="'user:edit'" @click="editUser(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
五、核心功能总结
-
路由权限:通过
router.addRoutes动态添加有权限的路由,结合beforeEach守卫过滤无权限页面; -
按钮权限:自定义
v-permission/v-role指令,无权限时自动移除 DOM 元素; - 权限存储:通过 Vuex 管理用户角色和权限码,支持从后端动态获取。
扩展建议
- 实际项目中,权限数据需从后端接口获取(如登录后返回);
- 可结合
vue-i18n实现权限相关文案的国际化; - 复杂场景可增加 “数据权限”(如用户只能查看自己创建的数据),通过接口参数过滤。
这套方案轻量且易扩展,适合 Vue2 项目的基础权限管理需求。