普通视图

发现新文章,点击刷新页面。
今天 — 2025年8月16日首页

前端跨域全解析:从 CORS 到 postMessage,再到 WebSocket

作者 yvvvy
2025年8月16日 14:02

小白友好版,跨域不再迷路


1️⃣ 什么是跨域?

跨域,简单理解就是:

“浏览器:哎呀,这请求要跑到别人家去拿数据,我敢不敢让它去呢?”

严格说法:当浏览器从一个源(Origin)请求另一个源(Origin)的资源时,如果两者不一样,就触发同源策略(Same-Origin Policy),这就是跨域。

源(Origin)组成:

源 = 协议(Protocol) + 域名(Host) + 端口(Port)

三者任意一项不同,就算跨域。

为什么有同源策略?

  • 防止 CSRF(跨站请求伪造)
  • 防止 XSS(跨站脚本攻击)
  • 防止隐私泄露(Cookie、账户信息)

举个例子:

http://example.com:80/page1.html  →  http://api.example.com:80/data

域名不同 → 跨域


2️⃣ CORS(跨域资源共享)

CORS 就像浏览器和服务器的“通行证”,谁允许你进,谁说了算。

浏览器:我想拿数据!
服务器:你是安全的,我允许你。
浏览器:好,我拿走数据!

2.1 两种 CORS 请求

🔹 简单请求(Simple Request)

条件(全部满足):

  1. 方法:GET / POST / HEAD
  2. 请求头:只能是浏览器安全集合
  3. 请求体:安全格式(纯文本/表单)
// 简单请求示例
fetch('https://api.example.com/data', {
  method: 'GET',
})
  .then(res => res.json())
  .then(data => console.log(data));

服务器只需允许跨域即可:

Access-Control-Allow-Origin: *

🔹 预检请求(Preflight Request)

触发条件:

  • 方法不是 GET/POST/HEAD
  • 自定义请求头(如 X-Token
  • Content-Type 非简单类型

流程示意:

浏览器:我想PUT数据,可以吗?(OPTIONS预检)
服务器:可以,你的来源、方法、头都允许
浏览器:好,发真正请求

代码示例:

fetch('https://api.example.com/data', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'X-Token': '123456'
  },
  body: JSON.stringify({ name: '跨域小白' })
})
.then(res => res.json())
.then(data => console.log(data));

面试小技巧:能区分“简单请求 vs 预检请求”,CORS 面试题轻松过。


3️⃣ JSONP

JSONP 是老派跨域方式,利用 <script> 标签天生跨域的特性。

<script>
  function handleData(data) {
    console.log('拿到跨域数据啦!', data);
  }
</script>
<script src="https://api.example.com/data?callback=handleData"></script>

优点:兼容老浏览器、实现简单
缺点:只能 GET、XSS 风险、错误处理麻烦

现代项目基本不用 JSONP,直接 CORS + Fetch 就好。


4️⃣ postMessage

当你和不同源的 iframe/窗口要聊聊时,postMessage 就像安全的对讲机。

<!-- 父页面 -->
<iframe id="child" src="https://other.com"></iframe>
<script>
  const iframe = document.getElementById('child');
  iframe.contentWindow.postMessage({ msg: 'Hi小伙伴' }, 'https://other.com');

  window.addEventListener('message', (event) => {
    if(event.origin !== 'https://other.com') return; // 安全检查
    console.log('收到子页面消息:', event.data);
  });
</script>

记住:安全第一,一定要检查 event.origin


5️⃣ WebSocket

WebSocket 就是“前端的即时聊天神器”,浏览器和服务器可以随时互发消息。

const ws = new WebSocket('wss://example.com/socket');

ws.onopen = () => ws.send('hello server!');

ws.onmessage = (msg) => console.log('收到服务器消息:', msg.data);

ws.onclose = () => console.log('连接关闭');

特点:

  • 全双工
  • 单连接
  • 跨域天然支持

6️⃣ 跨域常见应用场景

场景 示例 解决方案 代码示例
前端调用后端 API 开发 localhost → 远端 API CORS / 反向代理 fetch('https://api.example.com')
第三方接口 高德地图、支付 CORS / JSONP fetch('https://maps.com/api')
跨域 iframe 通信 支付 iframe postMessage iframe.contentWindow.postMessage(...)
多窗口/标签页 登录状态同步 postMessage + window.open window.opener.postMessage(...)
Web Worker 跨域 Worker 加载脚本 postMessage + CORS worker.postMessage(...)
静态资源跨域 CDN JS/CSS/图片 允许跨域 <script src="https://cdn.com/lib.js"></script>

7️⃣ 面试问答专栏:跨域篇

1️⃣ 面试官问:什么是跨域?

回答示例

“跨域就是浏览器发现你要去访问别人家的资源,它会先问一句:我敢不敢让它去?
严格来说,就是源(协议 + 域名 + 端口)不同,就触发同源策略。”


2️⃣ 面试官问:什么是 CORS?

回答示例

“CORS 就是浏览器和服务器的通行证,服务器在响应头声明允许的源、方法、头,浏览器通过才交数据给前端。
简单请求直接发,复杂请求会先发 OPTIONS 预检。”

代码示例:

fetch('https://api.example.com/data')
  .then(res => res.json())
  .then(data => console.log(data));

3️⃣ 面试官问:JSONP 和 CORS 有什么区别?

回答示例

“JSONP 是老派方案,靠 <script> 标签跨域,只能 GET,有 XSS 风险。
CORS 是现代方案,更安全灵活,支持 POST/PUT/DELETE。”


4️⃣ 面试官问:postMessage 是什么?

回答示例

“父页面和 iframe 或者窗口与 Worker 需要互相通信时,用 postMessage 传消息。安全重点是检查 event.origin。”

代码示例:

iframe.contentWindow.postMessage({ msg: 'hello' }, 'https://other.com');

5️⃣ 面试官问:WebSocket 跨域吗?

回答示例

“WebSocket 建立的是 TCP 长连接,一旦连接建立就天然跨域,可以双向通信。”


8️⃣ 总结

跨域知识点其实就像“安检关卡”,理解它,你就能安全访问资源,而且面试题轻松拿分。

技术 适用场景 优点 注意点
CORS 前后端接口跨域 简单灵活 后端需支持
JSONP 老 GET 请求 兼容老浏览器 只能 GET、有 XSS 风险
postMessage iframe/窗口/Worker 通信 安全灵活 检查 origin
WebSocket 实时通信 高效全双工 服务端支持

小结:

  • CORS → 现代前端首选
  • JSONP → 老项目遗留
  • postMessage → 窗口/iframe/Worker 通信
  • WebSocket → 实时双向通信

跨域学会了,你就是前端安全小能手!


第二章 虎牢关前初试Composition,吕布持v-model搦战

2025年8月16日 11:28

上回说到桃园三英,以reactive义旗初燃响应之火。未及旬日,消息传遍十八路诸侯:董卓挟旧框架jQuery,虎踞虎牢关,其义子吕布手持方天画戟,戟上双锋刻着“v-model”二字,号称“双向绑定第一猛将”,凡与之交锋者,模板与数据顷刻错乱,士卒(开发者)叫苦不迭。诸侯震惧,聚于酸枣大营,共议破敌之策。

玄德谓关、张曰:“吕布之势,在于旧双向绑定蛮横:一处改值,处处牵连,调试如坠五里雾。吾等新得Vue 3利器,可趁此关试其锋芒。”于是三人随公孙瓒军,星夜抵虎牢关下。

翌日黎明,鼓角齐鸣。吕布匹马出阵,戟指诸侯大喝:“谁敢与我斗绑定!”诸侯阵中,一将应声而出,乃江东孙坚,旧用Angular.js,被吕布一戟挑落马下,数据流当场断链。诸侯失色。

玄德回顾云长:“二弟,汝可出战?”云长丹凤眼微睁,提刀而出,却非青龙偃月,而是一柄新铸长刀,名曰<script setup>。刀背暗藏三大新纹:

  1. 纹一:ref()——化普通值为响应利刃;
  2. 纹二:computed()——凝衍生数据为刀罡;
  3. 纹三:watchEffect()——布追踪暗劲,敌一动则我即知。

云长横刀立马,朗声道:“吕布小儿,敢接我Composition刀法否?”

吕布大笑,挥戟直取。云长举刀迎敌,只见刀光闪处,代码如诗:

<!-- 虎牢关·关云长挑战牌.vue -->
<script setup lang="ts">
import { ref, computed, watchEffect } from 'vue'

const luBuHP = ref(100)          // 吕布血条
const guanYuATK = ref(30)        // 关羽攻击
const isCritical = computed(() => luBuHP.value < 50)
watchEffect(() => {
  if (isCritical.value) console.warn('吕布进入红血,狂暴模式开启!')
})
</script>

<template>
  <div>
    <p>吕布剩余血量:{{ luBuHP }}</p>
    <button @click="luBuHP -= guanYuATK">青龙斩</button>
  </div>
</template>

刀戟相交,吕布顿觉旧法迟钝:每次v-model改动,需层层触发$digest,而云长刀法轻灵,仅在必要处精准更新,DOM重绘大减。三十合后,吕布戟法渐乱。

翼德见云长占上风,大吼一声,挺矛跃马而出。其矛名Teleport,一矛刺出,竟将“

”元素瞬息移至关外战场中央,避开层层嵌套之DOM重围,令吕布措手不及。

玄德亦催马上前,袖中飞出一面小旗,旗上书“Suspense”。旗展处,诸侯军阵忽现异步大军:先遣async setup()轻骑诱敌,随后<Suspense>大军稳压阵脚,待数据从远域(API)归来,三军齐发,吕布旧部顿时崩溃。

吕布见势不妙,拨马欲走。云长刀锋一转,大喝:“留下v-model!”刀落处,吕布戟上“双向绑定”四字应声而碎,化作漫天光屑。诸侯军乘势掩杀,虎牢关大门轰然洞开。

战后,酸枣大营庆功。曹操把盏谓玄德曰:“今日之战,方知Vue 3新特性之利:
<script setup>简军书(样板代码减半);
ref & reactive分兵权(原始与对象各得其所);
computedwatchEffect如伏兵暗哨,料敌先机;
Teleport奇兵突袭,解定位之困;
Suspense则整饬异步之乱,令军容肃整。

有此五利,何愁旧框架不破?”

玄德谦逊答曰:“皆赖众志。然董卓未灭,jQuery余孽犹在,吾等当整军向西,直驱长安。”

众人齐应。夜色下,营火映照着一面新旗——旗上赫然是Vue 3的Logo,而下方绣着一行小字:

“Composition API · 破釜沉舟”

(第二章完)

——下回《凤仪亭密谋自定义ref,貂蝉夜探shallowReactive》

第一章 桃园灯火初燃,响应义旗始揭

2025年8月16日 11:24

却说中平元年,黄巾大乱,页面失序,交互崩坏。时有涿郡涿县义士刘备字玄德,胸怀仁道,常叹 DOM 操作之繁;关羽字云长,力能扛鼎,恨 reflow 之劳形;张飞字翼德,声若巨雷,苦 repaint 之伤神。三人心念苍生,俱欲收拾旧山河,重整乾坤之渲染。

是夜,桃园深处,月色如银。三人焚香再拜,誓以 Vue 3 为号,举响应式义旗。刘备执defineReactive为剑,关羽握ref作刀,张飞抡reactive成矛。金兰结义,共立宏愿:
“自此以后,凡我兄弟,同写单文件组件,同守 Composition API,同赴前端沙场,生死与共,不可背弃!”

誓毕,刘备展卷,出一物示二人,乃《setup()》秘策一卷。卷首云:
“夫响应之道,先立 state,后衍 effect;state 者,民生之本,effect 者,治世之干。”
关羽、张飞拜受,顿首再拜。于是三英于桃园之中,点燃第一簇数据之火——

// 桃园结义·state.ts
import { reactive } from 'vue'

export const peachGarden = reactive({
  brothers: ['刘备', '关羽', '张飞'],
  oath: '上报国家,下安黎庶,同生共死,永不背义'
})

火光照处,页面微颤,旧日静态之 HTML 忽生涟漪。张飞惊曰:“异哉!我但改一字,视图即随动,莫非天命?”
刘备笑曰:“非天命也,Proxy 之力耳。凡入reactive者,皆录于 WeakMap,牵一发而动全身,此即‘响应’二字真谛!”

关羽抚髯而思:“既有响应之兵,尚缺调度之帅。来日当筑effect营寨,使数据之兵随帅旗而行,无令散乱。”

三人言谈未尽,忽闻远处鼓角之声——黄巾残党jQuery余孽,正聚众欲复辟直接操作 DOM 之旧制。刘备拔剑而起:“兄弟,随我出村,初试响应锋芒!”

于是桃园灯火未灭,三骑已扬尘而去。前端乱世,自此拉开序幕。后人有诗赞曰:

桃园一火照前端,
Proxy 初开响应天。
自此 DOM 随令转,
三分代码见真源。

(第一章完)

——下回《虎牢关前初试Composition,吕布持双向绑定搦战》

npm发包自己的组件并安装更新版本应该如何做?

作者 林太白
2025年8月16日 10:42

npm发包组件

发布一个属于自己的npm包吧!接下来我们便使用Vue封装组件并发布到npm仓库

封装NPM组件-验证码

预览

npm-fabao1.png

1、创建账号注册登录

👉注册申请以及登录账号

官网

https://www.npmjs.com/

正常申请注册即可,选择 sign up 进入账户注册页面

npm-fabao2.png

2、创建vue3项目Tbcode

👉搭建项目

yarn create vite NexusCode --template vue

// 安装依赖
yarn 

👉创建组件

<template>
  <div class="necode">
    <div 
      class="codebox"
      :style="{
        'background': codeback,
        'width': width + 'px',
        'height': height + 'px'
      }"
      @click="getCode(length)"
    >
      {{ codevalue }}
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

// 接收父组件传递的 props
defineProps({
  value: {
    type: String,
    required: false,
  },
  length: {
    type: Number,
    default: 4,
    required: false,
  },
  back: {
    type: String,
    required: false,
  },
  width: {
    type: Number,
    default: 120,  // 默认宽度为120px
  },
  height: {
    type: Number,
    default: 40,   // 默认高度为40px
  }
});


const codelength = ref(4);

// 响应式变量
const codevalue = ref(''); // 验证码值
const codeback = ref('');  // 验证码背景色

// onMounted 是 Vue 3 的生命周期钩子,类似于 Vue 2 的 created
onMounted(() => {
  codelength.value=length?length:codelength.value;
  getCode(codelength.value); // 获取验证码
});

// 新增试用码-前端随机生成方法
const getCode = (row) => {
  // 随机背景颜色
  const r = Math.floor(Math.random() * 256);
  const g = Math.floor(Math.random() * 256);
  const b = Math.floor(Math.random() * 256);
  const rgb = `rgb(${r},${g},${b})`;
  codeback.value = rgb;

  const arrall = [
    'A', 'B', 'C', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
  ];
  let str = '';
  for (let i = 0; i < row; i++) {
    str += arrall[Math.floor(Math.random() * arrall.length)];
  }
  codevalue.value = str;
};
</script>

<style scoped>
.codebox {
  text-align: center;
  font-weight: 800;
  line-height: 40px;
  display: inline-block;
  float: left;
  cursor: pointer;
  font-size: 24px;
  color: #fff;
  border-radius: 4px;
}
</style>

👉配置package.json

私人化配置

"private": true,
这就代表私人的包

公共包配置(这里我们使用这个)

{
  "name": "tbcode",
  "version": "0.0.2",
  "files": [
    "dist"
  ],
  "module": "./dist/tbcode.es.js",
  "main": "./dist/tbcode.umd.js",
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/tbcode.es.ts",
      "require": "./dist/tbcode.umd.ts"
    },
    "./dist/style.css": {
      "import": "./dist/tbcode.css",
      "require": "./dist/tbcode.css"
    }
  },
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.5.18"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^6.0.1",
    "vite": "^7.1.2"
  }
}

3、发布组件

👉版本名字

🍎名称规则

包名使用了@开头,一般使用@符号开头的包都是私有包,npm需要收费

加上--access public可直接发布组件,当成公有包,不需要支付费用

name:发布的包名,默认是上级文件夹名。不得与现在npm中的包名重复。包名不能有大写字母/空格/下滑线!

👉版本登录发布

Major version:大版本,代表破坏性变更。
Minor version:小版本,代表向后兼容的新功能。
Patch version:修订版本,代表 bug 修复和小改进。
🍎 版本发布
//查看当前用户是否登录
npm whoami 

//登陆
npm login 

// 或  npm addUser  

//部署
npm publish

👉发布名称冲突报错

下面就是名字被占用了

npm notice
npm notice package: Necode@0.0.2
npm notice Tarball Contents
npm notice 385B README.md
npm notice 169B dist/Necode.css
npm notice 2.0kB dist/necode.es.js
npm notice 1.3kB dist/necode.umd.js
npm notice 613B package.json
npm notice Tarball Details
npm notice name: Necode
npm notice version: 0.0.2
npm notice filename: Necode-0.0.2.tgz
npm notice package size: 2.1 kB
npm notice unpacked size: 4.4 kB
npm notice shasum: 6534df3352b5d299457dbff0b6e363b1b6ab6d4f
npm notice integrity: sha512-UZ1QGtymSFl73[...]rc1luwb77w/4Q==
npm notice total files: 5
npm notice
npm notice Publishing to 
  https://registry.npmjs.org/ with tag latest and default access
npm error code E400
npm error 400 Bad Request - PUT https://registry.npmjs.org/Necode - 
"Necode" is invalid for new packages
npm error A complete log of this run can be found in: 
C:\Users\admin\AppData\Local\npm-cache\_logs\2025-08-15T08_24_42_644Z-debug-0.log

👉更新名字,再次发布

npm publish提示信息如下

npm notice
npm notice package: tbcode@0.0.2
npm notice Tarball Contents
npm notice 385B README.md
npm notice 169B dist/Necode.css
npm notice 2.0kB dist/necode.es.js
npm notice 1.3kB dist/necode.umd.js
npm notice 613B package.json
npm notice Tarball Details
npm notice name: tbcode
npm notice version: 0.0.2
npm notice filename: tbcode-0.0.2.tgz
npm notice package size: 2.1 kB
npm notice unpacked size: 4.4 kB
npm notice shasum: 97d3dc40035c0e3fcbcb590704e7c0d531d9d16e
npm notice integrity: sha512-M4EQ/J8XRHZNT[...]CC0OwXVluzH8Q==
npm notice total files: 5
npm notice
npm notice Publishing to https://registry.npmjs.org/ with tag latest and default access
+ tbcode@0.0.2

搜索已经可以发现我们的npm包了

tbcode

👉更新版本

接下来我们传第二个版本包

添加一些关键词

// 发布一个小的补丁号版本
npm version patch
npm publish

这个时候已经可以看到我们的关键词和版本信息了

Keywords
vuereacttbcodelintaibai林太白
npm version patch -m "xx"
【
    patch增加一位补丁号,
    minor增加一位小版本号,
    major增加一位大版本号
 】

👉取消版本

// 舍弃某个版本的模块  24小时使用这个即可(使用)
npm unpublish tbcode@1.0.0

// 舍弃某个版本的模块
npm deprecate my-npm@"< 1.0.8" "critical bug fixed in v1.0.2"

// 如何撤销发布
npm --force unpublish my_npm

4、使用

发布到 npm 后,安装使用自己封装的组件

安装依赖

npm i tbcode

//或者
yarn add tbcode

项目引入使用

import tbCode from 'tbCode'
import 'tbcode/dist/style.css'

app.component('Tbcode', tbcode) 


<Tbcode/>

问题以及处理

👉组件样式不生效,

问题就出现在下面两点

(1)组件的导出方式配置不对

(2)使用时候引入没有

// 导出方式配置
"exports": {
    ".": {
      "import": "./dist/tbcode.es.js",
      "require": "./dist/tbcode.umd.js"
    },
    "./dist/style.css": {
      "import": "./dist/tbcode.css",
      "require": "./dist/tbcode.css"
    }
},


// 引入使用
import 'tbcode/dist/style.css'

告别jQuery:2025年原生DOM操作最佳实践

作者 艾小码
2025年8月16日 07:49

随着现代浏览器对ECMAScript标准的全面支持,原生JavaScript已能高效替代jQuery的绝大多数功能。本文将深入探讨2025年DOM操作的核心优化策略,助你构建高性能前端应用。

一、选择器性能优化:querySelector陷阱与getElementById的抉择

querySelector的隐藏代价
虽然querySelectorquerySelectorAll提供了类似jQuery的CSS选择器语法,但其性能表现与选择器复杂度直接相关:

// 简单ID选择器
const element = document.querySelector('#myId'); 

// 复杂嵌套选择器
const nestedItems = document.querySelectorAll('div.container > ul.list > li:first-child');

当解析复杂选择器时,浏览器需遍历DOM树进行模式匹配,消耗时间与DOM规模成正比。尤其在万级节点中频繁调用时,可能成为性能瓶颈。

getElementById的极致优化
专为ID查找设计的API具备显著优势:

// 直接通过哈希映射定位元素
const element = document.getElementById('myId');

浏览器内部维护全局ID索引,使得时间复杂度稳定为O(1)。测试表明,其执行速度比querySelector('#id')快约15-30%。

决策矩阵:何时选用何种API

场景 推荐API 性能依据
单元素ID查找 getElementById 直接访问哈希索引,零解析开销
简单类选择(单个元素) querySelector 仅需解析单类选择器
复杂组合选择 querySelectorAll 牺牲部分性能换取开发效率
动态元素集合 getElementsByClassName 返回实时HTMLCollection,响应DOM变化

实践建议:在循环或动画中优先使用getElementByIdgetElementsByClassName;复杂静态元素组可缓存querySelectorAll结果避免重复查询。

二、高效批量操作:DocumentFragmentwill-change的协同

DocumentFragment:离线DOM的原子化操作
作为轻量级虚拟容器,其核心优势在于:

const fragment = document.createDocumentFragment();

// 批量创建节点(不触发重排)
for(let i=0; i<1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.appendChild(li);
}

// 单次插入(仅1次重排)
document.getElementById('list').appendChild(fragment);

通过脱离文档流的特性,使中间操作完全避开渲染管线,将N次重排压缩为1次。实测显示,万级节点插入耗时从12s降至350ms。

will-change:GPU加速的预优化
当需要对现有元素进行连续动画时,CSS提示可触发硬件加速:

.animated-element {
  will-change: transform, opacity; 
  transition: transform 0.3s ease-out;
}

此声明通知浏览器预先将元素提升至独立合成层,避免后续transform/opacity变化引发重排。但需注意过度使用会导致内存暴涨。

双剑合璧技术方案

  1. 静态节点批量插入
    DocumentFragment创建 → 填充内容 → 单次挂载DOM
    (适用:列表初始化、大块模板渲染)

  2. 动态元素连续动画
    添加will-change提示 → 使用transform/opacity驱动动画 → 动画结束移除提示
    (适用:拖拽、滚动特效、渐变过渡)

关键警示will-change应作为最终优化手段,而非预防性添加。过度使用将导致层爆炸(Layer Explosion),移动设备内存开销可超300MB。

三、虚拟滚动:万级列表渲染的核心实现

虚拟滚动通过动态可视区域渲染破解性能困局,核心流程:

1. 布局引擎(Layout Engine)

const container = {
  clientHeight: 800,   // 可视区域高度
  itemHeight: 50,      // 单项预估高度
  bufferSize: 5,       // 渲染缓冲项数
};

// 计算可见项索引
const startIdx = Math.floor(scrollTop / itemHeight);
const endIdx = startIdx + Math.ceil(clientHeight / itemHeight) + bufferSize;

2. 动态渲染(Dynamic Rendering)

<div class="viewport" style="height:800px; overflow-y: auto">
  <!-- 撑开总高度的占位符 -->
  <div class="scroll-holder" style="height:${totalItems * itemHeight}px"></div> 
  
  <!-- 仅渲染可视项 -->
  <div class="visible-items" style="position:relative; top:${startIdx * itemHeight}px">
    ${visibleItems.map(item => `<div class="item">${item.text}</div>`)}
  </div>
</div>

3. 滚动优化(Scroll Optimization)

  • 使用requestAnimationFrame节流滚动事件
  • 持久化已渲染节点避免重复创建
  • 异步加载非可视区数据

性能对比(渲染10,000项)

方案 初始化时间 滚动帧率 内存占用
传统全量渲染 4200ms 8fps 850MB
虚拟滚动 380ms 60fps 95MB

进阶技巧:结合IntersectionObserver实现懒加载,使用ResizeObserver处理动态项高,并采用渐进渲染策略避免跳帧。

四、架构启示:现代DOM操作核心原则

  1. 选择性优化
    仅对滚动容器动画高频区等关键路径实施虚拟化,避免过度工程化

  2. 读写分离
    集中执行样式修改后统一读取布局属性,防止强制同步布局(Forced Synchronous Layout)

    // 错误示范(读写交错)
    elements.forEach(el => {
      el.style.width = (el.offsetWidth + 10) + 'px'; // 触发重排
    });
    
    // 正确做法(批量写 → 批量读)
    elements.forEach(el => el.style.width = '110%');
    const newWidths = elements.map(el => el.offsetWidth);
    
  3. 分层渲染策略

    // 首次加载 → 数据量>500 → 虚拟滚动+骨架屏 → 滚动时增量加载
    // 首次加载 → 数据量<500 → 全量渲染 → 常规交互
    

2025年最佳实践组合

  • 选择器getElementById + 缓存querySelectorAll
  • 批量操作DocumentFragment + CSS contain:content
  • 长列表:虚拟滚动 + IntersectionObserver
  • 动画will-change + transform硬件加速

原生DOM操作并非简单替换jQuery语法,而是重新理解浏览器渲染管线。通过精准选择API、利用硬件加速、动态加载策略,即使处理十万级DOM也能保持60fps流畅体验。在前端框架盛行的今天,掌握原生能力仍是性能优化的终极底牌。

Next.js 嵌套路由与中间件:数据与逻辑的前哨站

作者 LeonGao
2025年8月16日 09:28

在现代 Web 应用的世界里,路由是城市道路,中间件是守在路口的警察,确保一切交通有序、安全。
Next.js 则是那位既懂交通规则、又能修路铺桥的工程师——你不仅可以在它的路网上自由嵌套路线,还可以让中间件在用户抵达目的地前对他们的身份、行李、甚至心情(如果你愿意)做检查。


一、嵌套路由的本质

在 Next.js 中,文件即路由的哲学让你少了很多配置文件的负担,但当你需要结构化复杂页面时,嵌套路由就派上了用场。

比如,你有一个博客系统:

/app
  /blog
    /page.js
    /[slug]
      /page.js
  • /blog → 博客列表页
  • /blog/[slug] → 某篇博客详情页

底层原理:

  • Next.js 会遍历 app 目录下的文件夹结构。
  • 目录名映射为 URL 路径,[param] 形式表示动态路由。
  • 嵌套文件夹会形成嵌套路由,父级路由可以包含 Layout,用来统一头部、底部、导航栏。

Layout 嵌套机制

// app/blog/layout.js
export default function BlogLayout({ children }) {
  return (
    <div>
      <header>Blog Header</header>
      <main>{children}</main>
    </div>
  );
}

这样 /blog/blog/[slug] 都会共享这个 BlogLayout,底层是组件树递归渲染,Next.js 会为每一层 Layout 建立独立 React 节点,从而实现父子关系。


二、中间件(Middleware)的使命

想象一下你有一个高档餐厅(网站),中间件就是门口的保安——

  • 检查身份证(鉴权)
  • 检查预订记录(权限控制)
  • 检查是否穿正装(条件跳转)
  • 甚至可以把迟到的人送去别的餐厅(重定向)

中间件的运行时机

  • 请求到达页面组件之前
  • 运行在 Edge Runtime(轻量、低延迟,全球分布)。
  • 可以读取和修改请求、响应。

底层机制

  • 你在项目根目录(或子目录)下放置一个 middleware.js 文件。
  • Next.js 会在构建时将它编译为 Edge Function。
  • 每次请求进入匹配的路径时,都会先经过中间件逻辑。

三、实战:嵌套路由 + 中间件

假设你有一个 /dashboard 路由和它的嵌套页面 /dashboard/settings,你想在用户进入这些页面前检查是否已登录。

目录结构:

/app
  /dashboard
    /page.js
    /settings
      /page.js
/middleware.js

中间件示例:

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(req) {
  const token = req.cookies.get('token');
  
  if (!token) {
    // 未登录则跳转到登录页
    return NextResponse.redirect(new URL('/login', req.url));
  }
  
  // 已登录则放行
  return NextResponse.next();
}

// 限制中间件只匹配 dashboard 路由
export const config = {
  matcher: ['/dashboard/:path*']
};

四、嵌套路由与中间件的协作

嵌套路由提供结构化的页面层级,而中间件提供请求入口的守卫
就像机场一样:

  • 嵌套路由 → 航站楼结构(国际、国内、贵宾厅等分区)
  • 中间件 → 安检口(拦截违禁品、核对身份、放行)

好处:

  1. 安全:中间件阻挡未授权用户。
  2. 体验:减少无意义的页面渲染。
  3. 性能:Edge Runtime 在边缘节点直接处理,不必每次回到主服务器。

五、最佳实践建议

  1. 中间件逻辑要精简

    • 它运行在边缘节点,不适合做大量计算。
    • 适合做快速判断、重定向、设置 cookie。
  2. 嵌套路由中 Layout 复用 UI

    • 避免重复代码,让不同子页面共享样式和结构。
  3. 分层控制

    • 根目录 middleware.js 管全局规则。
    • 子目录 middleware.js 处理局部规则(Next.js 13+ 支持子目录中间件)。

六、幽默的尾声

嵌套路由像一座大厦的楼层结构,
中间件是大门口的保安,
而 Next.js 是那位能帮你造大厦、请保安、装电梯的承包商。

有人会问:
“那如果我没中间件,直接让所有人进来会怎样?”
——那就像把你家 Wi-Fi 密码贴在电梯里,很快就会发现隔壁邻居比你还熟悉你的路由结构

AI UI 数据展示:Chart.js / Recharts + AI 总结文本的艺术

作者 LeonGao
2025年8月16日 09:25

在现代 Web 应用的世界里,数据展示早已不再是枯燥的表格,而是一场视觉盛宴。
就像数据是食材,AI 是大厨,Chart.js / Recharts 是精致的餐具——最终的 UI 是那道端上用户桌面的米其林级菜肴

本篇文章,我们将从底层原理到代码实践,一起探讨如何用 Chart.js / Recharts 绘制出优雅的数据图表,并用 AI 自动生成人类可读的总结文本


一、为什么 Chart.js 和 Recharts 是好搭档?

在前端图表界,Chart.js 和 Recharts 有点像两个性格不同的朋友:

  • Chart.js

    • 优势:轻量级,原生 Canvas 渲染,动画丝滑。
    • 适合场景:需要快速渲染高性能、交互不太复杂的图表。
    • 底层机制:直接操作 <canvas>,用 2D 渲染上下文绘制像素。
    • 缺点:配置复杂时需要更多手动调整。
  • Recharts

    • 优势:基于 React 组件化开发,易维护,语义化强。
    • 适合场景:React 项目里快速搭建交互性强的图表。
    • 底层机制:基于 D3.js 的计算和 SVG 渲染(矢量图,缩放不失真)。
    • 缺点:在大量数据点时性能可能逊色于 Canvas。

一句话总结

Chart.js 是“性能小钢炮”,Recharts 是“优雅绅士”,你可以根据业务场景选择或混用。


二、AI 在数据展示中的角色

如果 Chart.js 和 Recharts 是负责画画的,那 AI 就是旁白解说员

为什么需要 AI 文本总结?

  • 人眼对趋势敏感,但 AI 可以直接用自然语言告诉你结论
  • 当用户面对一堆数据曲线时,AI 可以说:“看!这个月的销售额比上月增长了 35%,并且主要得益于东南亚市场的爆发式增长。”

AI 的底层工作逻辑:

  1. 获取数据(JSON / API)。
  2. 特征提取:计算平均值、最大值、趋势变化率等。
  3. 语言生成:将这些特征喂给 AI 模型(如 GPT-4、Claude),让它用自然语言总结。
  4. 输出优化:控制字数、调整语气、加上商业或技术背景。

三、数据流的底层原理

一个典型的 AI UI 数据展示系统,数据流是这样的:

[ 数据源 API ][ 前端获取数据 fetch() ][ 数据处理:统计、归一化 ][ Chart.js / Recharts 渲染 ][ AI 调用接口生成总结文本 ][ 页面展示:图表 + 文本 ]

在底层实现里,Chart.js 会直接操作 Canvas 的像素点,而 Recharts 会在 DOM 中生成 <svg> 标签,并通过 D3.js 计算坐标和路径。

AI 部分则通常通过 HTTP 请求调用 LLM API,比如:

const summary = await fetch('/api/ai-summary', {
  method: 'POST',
  body: JSON.stringify({ data }),
});

在服务器上,你可能用 OpenAI API:

import OpenAI from 'openai';
const openai = new OpenAI();

const aiText = await openai.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [
    { role: "system", content: "你是数据分析师,帮我总结趋势" },
    { role: "user", content: JSON.stringify(data) }
  ]
});

四、实战示例:Chart.js + AI 总结

假设我们有一组销售额数据(按月份),我们先用 Chart.js 画出来,再调用 AI 给出文字总结。

import { Chart } from 'chart.js';

// 模拟数据
const salesData = [120, 140, 180, 160, 200, 250, 300];
const labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];

// 1. 绘制图表
new Chart(document.getElementById('salesChart'), {
  type: 'line',
  data: {
    labels,
    datasets: [{
      label: 'Monthly Sales',
      data: salesData,
      borderColor: '#4CAF50',
      fill: false
    }]
  }
});

// 2. 请求 AI 总结
async function getAISummary(data) {
  const res = await fetch('/api/ai-summary', {
    method: 'POST',
    body: JSON.stringify({ salesData: data })
  });
  const { summary } = await res.json();
  document.getElementById('summary').innerText = summary;
}

getAISummary(salesData);

五、Recharts + AI 总结(React 版本)

import { LineChart, Line, XAxis, YAxis, Tooltip } from 'recharts';

const data = [
  { month: 'Jan', sales: 120 },
  { month: 'Feb', sales: 140 },
  { month: 'Mar', sales: 180 },
  { month: 'Apr', sales: 160 },
  { month: 'May', sales: 200 },
  { month: 'Jun', sales: 250 },
  { month: 'Jul', sales: 300 }
];

export default function SalesChart() {
  return (
    <>
      <LineChart width={500} height={300} data={data}>
        <XAxis dataKey="month" />
        <YAxis />
        <Tooltip />
        <Line type="monotone" dataKey="sales" stroke="#4CAF50" />
      </LineChart>
      <div id="summary">AI 正在生成总结...</div>
    </>
  );
}

在 React 中,可以用 useEffect 触发 AI 总结的 API 调用,将数据传过去,再更新到 summary 状态中。


六、幽默的收尾

传统的数据展示是“看图说话”,
AI + 图表的组合是“看图不用说话,AI 替你说完”。

当 Chart.js 像年轻的涂鸦艺术家用画笔在 Canvas 上狂飙,
Recharts 则是那位戴着圆框眼镜、温文尔雅的 SVG 绘图师。
而 AI,就像后台的那位戏精,随时准备为你的数据配上旁白——
甚至会夸张地说你是下一个商业传奇。

Next.js 入门实战:从零构建你的第一个 SSR 应用

作者 遂心_
2025年8月15日 23:26

序言

在当今前端开发中,Next.js 已成为构建高性能、SEO 友好应用的必备框架。今天我将带大家从零开始创建一个 Next.js 项目,并深入解析其服务器端渲染(SSR)机制的优势。

创建 Next.js 项目

Next.js 提供了便捷的脚手架工具,让我们可以快速初始化项目:

npx create-next-app@latest my-todo

这里使用 npx 命令的优势在于:

  • 无痕使用:无需全局安装依赖,避免污染全局环境
  • 即用即走:非常适合快速尝试新技术
  • 版本控制:始终使用最新版本的 create-next-app

当然,你也可以选择全局安装:

npm i -g create-next-app@latest

Next.js 与传统 React 应用的区别

特性 React (CSR) Next.js (SSR)
渲染位置 客户端浏览器 服务器端
初始加载速度 较慢(需下载所有JS) 较快(服务器返回完整HTML)
SEO 友好度 较差(爬虫难以解析) 优秀(直接返回完整内容)
适用场景 后台管理系统 内容型网站、企业站

理解 SSR 的核心优势

1. SEO 优化

传统 React 应用(CSR)在浏览器端渲染时,初始 HTML 只有一个空容器:

<div id="root"></div>

搜索引擎爬虫抓取时,只能看到一个空页面,严重影响 SEO。而 Next.js 的 SSR 在服务器端就完成了渲染,返回的是完整的 HTML 内容:

<h1>首页</h1>
<div>我在秋招,我去字节</div>

2. 性能提升

用户无需等待所有 JavaScript 加载完成就能看到内容,大大提升了首屏加载速度。

实战:创建你的第一个页面

在 Next.js 项目中,页面组件位于 app 目录下。我们创建一个简单的首页:

// app/page.tsx
import Image from "next/image";

export default function Home() {
  return (
    <>
      <h1>首页</h1>
      <div>我在秋招,我去字节</div>
    </>
  );
}

这个组件会在服务器端被渲染成 HTML,然后发送到客户端。注意我们使用了 Next.js 内置的 Image 组件,它可以自动优化图片性能。

运行你的 Next.js 应用

在项目目录下执行:

npm run dev

访问 http://localhost:3000,你将看到服务器渲染的页面。

如何验证 SSR 效果?

  1. 在浏览器中右键点击"查看页面源代码"
  2. 你将看到完整的 HTML 内容,而非空容器
  3. 这意味着搜索引擎爬虫可以直接抓取到页面内容

使用场景推荐

Next.js 特别适合以下场景:

  • 内容型网站:博客、新闻站点(SEO 关键)
  • 电商平台:商品列表页需要被搜索引擎收录
  • 企业官网:需要良好的搜索引擎排名
  • 掘金等技术社区:内容需要被广泛传播和搜索

总结

Next.js 通过 SSR 解决了传统 React 应用的两大痛点:

  1. SEO 不友好:服务器直接返回完整 HTML
  2. 首屏加载慢:用户立即看到内容而非空白页

deepseek_mermaid_20250815_05091b.png

CSS居中布局:从基础到进阶全解析

2025年8月15日 22:13

在前端开发中,居中布局是高频需求,也是面试常考点。今天我们将系统梳理各类居中场景的实现方案,涵盖水平居中、垂直居中及水平垂直居中,并深入分析其原理与适用场景。

一、水平居中:文本与块级元素

1.1 文本水平居中:text-align

适用于行内元素(inline/inline-block)或文本:

.container { 
  text-align: center; /* 子元素继承居中 */ 
} 

特性

  • 作用于父容器,子元素自动继承
  • 仅对行内内容生效(如<span><img>

1.2 块级元素水平居中:margin: auto

适用于固定宽度的块级元素:

.box { 
  width: 200px; /* 必须定义宽度 */ 
  margin: 0 auto; /* 左右外边距自适应 */ 
} 

原理:浏览器自动分配左右剩余空间

二、垂直居中:单行文本的解决方案

2.1 line-height方案

当元素高度确定时:

.container { 
  height: 100px; 
  line-height: 100px; /* 等于容器高度 */ 
} 

限制

  • 仅适用于单行文本
  • 内容高度不能超过容器

2.2 padding方案

通过内边距挤压内容:

.container { 
  padding: 40px 0; /* 上下内边距相等 */ 
} 

优势:无需计算行高,适应多行文本

三、固定宽高元素的水平垂直居中

3.1 绝对定位 + 负边距(经典方案)

.parent { position: relative; } 
.child { 
  position: absolute; 
  width: 300px; 
  height: 200px; 
  top: 50%; 
  left: 50%; 
  margin-top: -100px; /* height/2 */ 
  margin-left: -150px; /* width/2 */ 
} 

缺点

  • 需精确知道元素尺寸
  • 调整尺寸需同步修改边距

3.2 绝对定位 + margin: auto(推荐方案)

.child { 
  position: absolute; 
  width: 300px; 
  height: 200px; 
  top: 0; 
  left: 0; 
  right: 0; 
  bottom: 0; 
  margin: auto; /* 自动填充剩余空间 */ 
} 

优势

  • 代码简洁,易于维护
  • 兼容性好(IE8+)

3.3 绝对定位 + calc()

.child { 
  position: absolute; 
  top: calc(50% - 100px); /* 50% - height/2 */ 
  left: calc(50% - 150px); /* 50% - width/2 */ 
} 

缺点

  • 计算性能较差(频繁重绘时影响渲染)
  • 可读性低

四、未知宽高元素的水平垂直居中

4.1 绝对定位 + transform(现代方案)

.child { 
  position: absolute; 
  top: 50%; 
  left: 50%; 
  transform: translate(-50%, -50%); /* 反向位移自身50% */ 
} 

原理

  1. top/left定位到父容器中心点
  2. translate将元素向左上移动自身宽高的50%
    优势:自适应任意尺寸,无需知道宽高

4.2 line-height + vertical-align

利用文本属性实现:

.parent { 
  line-height: 300px; /* 等于容器高度 */ 
  text-align: center; 
} 
.child { 
  display: inline-block; 
  vertical-align: middle; /* 垂直中线对齐 */ 
  line-height: initial; /* 重置子元素行高 */ 
} 

适用场景:需要兼容旧浏览器的项目

4.3 writing-mode技巧

改变文本流向实现垂直居中:

.parent { 
  writing-mode: vertical-lr; /* 改为垂直流向 */ 
  text-align: center; 
} 
.child { 
  writing-mode: horizontal-tb; /* 改回水平流向 */ 
  display: inline-block; 
} 

注意:此方案会改变文本布局方向,需谨慎使用

4.4 table-cell布局

模拟表格单元格行为:

.parent { 
  display: table-cell; 
  width: 100vw; 
  height: 100vh; 
  vertical-align: middle; /* 垂直居中 */ 
  text-align: center; /* 水平居中 */ 
} 
.child { 
  display: inline-block; 
} 

缺点:父元素需定义明确宽高


五、Flexbox:终极居中方案

.parent { 
  display: flex; 
  justify-content: center; /* 主轴居中 */ 
  align-items: center; /* 交叉轴居中 */ 
} 

优势

  • 三行代码解决所有居中问题
  • 完美支持响应式布局
  • 无需计算尺寸

扩展技巧:多元素居中

.parent { 
  display: flex; 
  flex-direction: column; /* 改为垂直排列 */ 
  justify-content: center; 
} 

六、Grid布局:二维居中控制

.parent { 
  display: grid; 
  place-items: center; /* 行列同时居中 */ 
} 

等价写法

.parent { 
  display: grid; 
  justify-content: center; 
  align-content: center; 
} 

适用场景:复杂网格系统中的居中需求


七、方案对比与选择指南

方案 适用场景 兼容性 灵活性
text-align 行内元素水平居中 所有浏览器 ★★☆
负边距 已知尺寸元素 IE6+ ★☆☆
transform 未知尺寸元素 IE10+ ★★★
Flexbox 现代布局 IE11+ ★★★
Grid 二维复杂布局 IE11+ ★★★
table-cell 兼容旧浏览器 IE8+ ★★☆

选择原则:

  1. 已知宽高:优先使用absolute + margin: auto(性能最佳)

  2. 未知宽高

    • 现代项目:Flexbox
    • 需兼容旧浏览器:transformtable-cell
  3. 文本内容line-heightpadding

总结与思考

居中布局的核心在于理解坐标系定位基准

  1. 水平居中本质是左右空间均等分配
  2. 垂直居中依赖行高控制定位偏移
  3. 绝对定位方案需建立位置参照系(父元素position: relative

现代CSS已大幅简化居中实现:

  • 单元素居中首选transform
  • 多元素排列必用Flexbox
  • 避免滥用calc(),性能敏感场景慎用

看似简单的居中背后,是CSS视觉格式化模型的深刻体现。掌握每种方案的底层原理,方能灵活应对复杂场景。当然也能让你在面试官面前眼前一亮

面试题深度解析:父子组件通信与生命周期执行顺序

作者 言兴
2025年8月15日 22:10

在现代前端框架(如 Vue 和 React)的面试中,“父子组件如何通信?生命周期的执行顺序是怎样的?” 是一道经典且高频的综合题。它不仅考察你对框架 API 的掌握,更深入检验你对组件化思想、数据流、渲染机制和副作用处理的理解。

本文将以 Vue 3 和 React 为例,从基础到原理,全面剖析父子组件通信方式与生命周期执行顺序,助你在面试中脱颖而出。


一、父子组件通信的五大方式

组件通信的核心是数据流的传递与同步。在单向数据流(Unidirectional Data Flow)理念下,父组件通过 props 向下传递数据,子组件通过 事件(Event)向上通信

1. Props Down:父 → 子(数据传递)

Vue 3 示例

<!-- Parent.vue -->
<template>
  <Child :msg="message" :user="userInfo" />
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const message = ref('Hello from Parent')
const userInfo = { name: 'Alice', age: 25 }
</script>

<!-- Child.vue -->
<script setup>
// 接收 props
const props = defineProps({
  msg: String,
  user: Object
})

console.log(props.msg) // 'Hello from Parent'
</script>

React 示例

// Parent.js
function Parent() {
  const [message, setMessage] = useState('Hello from Parent');
  const userInfo = { name: 'Alice', age: 25 };

  return <Child msg={message} user={userInfo} />;
}

// Child.js
function Child({ msg, user }) {
  console.log(msg); // 'Hello from Parent'
  return <div>{msg}</div>;
}

关键点

  • Props 是只读的,子组件不应直接修改。
  • 传递引用类型(对象、数组)时,子组件修改其内部属性会影响父组件(浅共享),需避免。

2. Events Up:子 → 父(事件通知)

Vue 3:emit 事件

<!-- Child.vue -->
<script setup>
const emit = defineEmits(['update', 'close'])

function handleClick() {
  emit('update', 'New Value')
  emit('close')
}
</script>

<!-- Parent.vue -->
<template>
  <Child @update="handleUpdate" @close=" handleClose" />
</template>

<script setup>
function handleUpdate(value) {
  console.log('Received:', value)
}
function handleClose() {
  console.log('Child closed')
}
</script>

React:回调函数(Callback)

// Parent.js
function Parent() {
  const handleUpdate = (value) => {
    console.log('Received:', value);
  };
  const handleClose = () => {
    console.log('Child closed');
  };

  return <Child onUpdate={handleUpdate} onClose={handleClose} />;
}

// Child.js
function Child({ onUpdate, onClose }) {
  return (
    <button onClick={() => {
      onUpdate('New Value');
      onClose();
    }}>
      Click
    </button>
  );
}

关键点:这是最标准、最安全的子 → 父通信方式。


3. v-model / v-model:value(双向绑定)

Vue 3 的语法糖,本质是 :modelValue + @update:modelValue

<!-- Parent.vue -->
<Child v-model="message" />

<!-- 等价于 -->
<Child 
  :modelValue="message" 
  @update:modelValue="value => message = value" 
/>

<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

function handleChange(e) {
  emit('update:modelValue', e.target.value)
}
</script>

适用场景:表单组件(如 Input, Select)。


4. $refs / ref(直接访问子组件实例)

Vue 3

<!-- Parent.vue -->
<template>
  <Child ref="childRef" />
</template>

<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'

const childRef = ref(null)

onMounted(() => {
  childRef.value.someMethod() // 调用子组件方法
})
</script>

<!-- Child.vue -->
<script setup>
import { defineExpose } from 'vue'

function someMethod() {
  console.log('Called from parent')
}

// 暴露给父组件
defineExpose({ someMethod })
</script>

React:useRef + forwardRef

// Child.js
const Child = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({
    someMethod() {
      console.log('Called from parent');
    }
  }));

  return <div>Child</div>;
});

// Parent.js
function Parent() {
  const childRef = useRef();

  useEffect(() => {
    childRef.current.someMethod();
  }, []);

  return <Child ref={childRef} />;
}

⚠️ 注意:应尽量避免使用 ref,破坏了组件的封装性,仅用于 DOM 操作或特定方法调用。


5. Provide / Inject(跨层级通信)

适用于祖孙组件通信,避免“props 逐层透传”。

<!-- App.vue (祖先) -->
<script setup>
import { provide } from 'vue'

provide('theme', 'dark')
provide('user', { name: 'Admin' })
</script>

<!-- AnyChild.vue (任意后代) -->
<script setup>
import { inject } from 'vue'

const theme = inject('theme', 'light') // 第二个参数是默认值
const user = inject('user')
</script>

优点:解耦层级依赖。 ❌ 缺点:数据流不清晰,调试困难。


二、父子组件生命周期执行顺序深度解析

理解生命周期顺序,是避免“子组件未挂载就访问”、“卸载时内存泄漏”等 bug 的关键。

Vue 3 执行顺序(Composition API)

1. 首次挂载(Mount)

// Parent
setup()           // 1
onBeforeMount()   // 3
onMounted()       // 5

// Child
setup()           // 2
onBeforeMount()   // 4
onMounted()       // 6

🔍 流程

  1. 父组件 setup 执行(准备数据和逻辑)。
  2. 子组件 setup 执行。
  3. 父组件进入 onBeforeMount(DOM 未生成)。
  4. 子组件进入 onBeforeMount
  5. 子组件 onMounted 触发(子组件 DOM 已挂载)。
  6. 父组件 onMounted 触发(父组件等待所有子组件挂载完成才算自己挂载完成)。

结论onMounted 的完成顺序是 子 → 父

2. 更新(Update)

// Parent
onBeforeUpdate()  // 1
onUpdated()       // 3

// Child
onBeforeUpdate()  // 2
onUpdated()       // 3

🔍 流程

  1. 父组件触发 onBeforeUpdate
  2. 子组件触发 onBeforeUpdate
  3. 子组件 onUpdated
  4. 父组件 onUpdated

结论:更新也是 子先完成,父后完成

3. 卸载(Unmount)

// Parent
onBeforeUnmount() // 1
onUnmounted()     // 3

// Child
onBeforeUnmount() // 2
onUnmounted()     // 3

结论:卸载顺序 子 → 父 完成。


React 执行顺序(函数组件 + useEffect)

1. 首次渲染

// Parent
render()          // 1
useEffect(() => { /* mount */ }) // 3

// Child
render()          // 2
useEffect(() => { /* mount */ }) // 4

🔍 流程

  1. 父组件 render(生成虚拟 DOM)。
  2. 子组件 render
  3. DOM 提交到页面
  4. useEffect 异步执行:父 → 子。

结论useEffect 执行顺序是 父 → 子

2. 更新

顺序与首次渲染一致:父 render → 子 render → DOM 提交 → 父 useEffect → 子 useEffect

3. 卸载

// cleanup 执行顺序
Parent cleanup   // 1
Child cleanup    // 2

结论useEffect 的 cleanup 函数执行顺序是 父 → 子


三、通信与生命周期的协同应用

场景:父组件等待子组件初始化完成

<!-- Parent.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'

const childRef = ref(null)

onMounted(() => {
  // 此时 childRef.value 已存在,且子组件已挂载
  childRef.value.initialize()
})
</script>

依据onMounted 触发时,所有子组件已挂载完毕。


四、总结:一张表看懂核心要点

维度 Vue 3 React
父 → 子通信 props props
子 → 父通信 emit 事件 回调函数(Callback)
双向绑定 v-model 受控组件 + onChange
直接访问 ref + defineExpose useRef + forwardRef + useImperativeHandle
跨层级通信 provide / inject Context API
挂载完成顺序 子 → 父 (onMounted) 父 → 子 (useEffect)
更新完成顺序 子 → 父 (onUpdated) 父 → 子 (useEffect)
卸载顺序 子 → 父 (onUnmounted) 父 → 子 (cleanup)

面试加分回答

“父子组件通信应遵循单向数据流原则:父传 props,子发 eventv-modelref 是语法糖和特殊手段,应谨慎使用。生命周期顺序的核心是渲染从父到子,完成从子到父(Vue),而 React 的 useEffect 是在渲染后统一执行。理解这一点,能帮助我们正确处理 DOM 操作、事件绑定、资源清理和异步初始化,避免因时机错误导致的 bug。”

掌握这些,你不仅能回答面试题,更能设计出健壮、可维护的组件体系。

高德地图关键字查询和输入提示后查询踩坑记录

2025年8月15日 21:09

最近在接入高德地图1.4.15版本,其中有地图关键字查询输入提示后查询的功能,中间踩了很多坑,一度怀疑是高德的bug,最终发现是自己代码的问题,好在最终解决了,在此记录一下。

地图关键字查询

踩坑1:频繁的创建new AMap.PlaceSearch实例。我一开始将创建new AMap.PlaceSearch实例写在了handleSearchChange事件中,这样会导致placeSearch.search的分页查询不准确,比如我一开始搜索北京,在panel中展示出来的是北京的列表;之后我又搜索济南,一开始在panel中展示的是济南的列表,点击分页页码后,在panel中展示出来的是北京的列表结果。

错误代码如下:
<script>
export default {
  data() {
    return {
      amap: null,
      apanel: null,
      searchValue: "",
    };
  },
  methods: {
    loadScript(url, callback) {
     //省略
    },
    initMap() {
      // 省略
    },
    handleSearchChange() { 
      const placeSearch = new AMap.PlaceSearch({ // 这里这么写是错误的
        pageSize: 5, // 单页显示结果条数
        pageIndex: 1, // 页码
        map: this.amap,
        panel: "panel",
      }); //构造地点查询类
      this.placeSearch = placeSearch;
      this.placeSearch.search(this.searchValue, (status, result) => {
        console.log("status", status);
        console.log("result", result);
      });
    },
  },
  mounted() {
    this.loadScript(
      "https://webapi.amap.com/maps?v=1.4.15&key=你的key值&plugin=AMap.Scale,AMap.OverView,AMap.ToolBar,AMap.Autocomplete,AMap.PlaceSearch",
      this.initMap
    );
  },
};
</script>

错误截图
  1. 第一步:先搜索北京 image.png
  2. 第二步:搜索其他地点,比如济南,出现搜索结果后点击分页 image.png 3.可以看到搜索结果是错误的!!!一切都是因为在input框的change事件里频繁的创建new AMap.PlaceSearch实例。
关键字查询的正确的实现代码如下:

在初始化中建立new AMap.PlaceSearch实例

<template>
  <div style="width: 100%; height: 100vh">
    <div ref="amap" id="container" style="height: 100%" />

    <div ref="apanel" id="panel" class="panel" />
    <div class="search-bar">
      <input
        placeholder="地图检索"
        prefix-icon="el-icon-search"
        v-model="searchValue"
        @change="handleSearchChange"
      />
    </div>
  </div>
</template>
<style>
.panel {
  position: absolute;
  background-color: white;
  max-height: 90%;
  overflow-y: auto;
  top: 10px;
  left: 100px;
  width: 280px;
}
.search-bar {
  position: fixed;
  width: 250px;
  padding: 20px 0;
  right: 20px;
  top: 20px;
  z-index: 10000;
  display: flex;
}

.search-bar .el-input {
  background: rgba(255, 255, 255, 0.2);
}
</style>
<script>
export default {
  data() {
    return {
      amap: null,
      apanel: null,
      searchValue: "",
    };
  },
  methods: {
    loadScript(url, callback) {
      // 加载高德地图js
      let script = document.createElement("script");

      script.type = "text/javascript";

      script.src = url;

      document.getElementsByTagName("head")[0].appendChild(script);

      script.onload = () => {
        callback();
      };
    },
    initMap() {
      let scale = new AMap.Scale({
        visible: true,
      });
      let toolBar = new AMap.ToolBar({
        visible: true,
      });
      let overView = new AMap.OverView({
        visible: true,
      });

      console.log([this.longitude, this.latitude]);
      this.amap = new AMap.Map("container", {
        //center: [longitude, latitude], //地图中心点
        zoom: 15, //地图级别
        mapStyle: "amap://styles/dark", //设置地图的显示样式
        viewMode: "2D", //设置地图模式
        lang: "zh_cn", //设置地图语言类型
        resizeEnable: true,
      });
      this.amap.addControl(scale);
      this.amap.addControl(toolBar);
      this.amap.addControl(overView);
      const placeSearch = new AMap.PlaceSearch({ //在初始化中建立new AMap.PlaceSearch实例
        pageSize: 5, // 单页显示结果条数
        pageIndex: 1, // 页码
        map: this.amap,
        panel: "panel",
      }); //构造地点查询类
      this.placeSearch = placeSearch;
    },
    handleSearchChange() {
      this.placeSearch.search(this.searchValue, (status, result) => {
        console.log("status", status);
        console.log("result", result);
      });
    },
  },
  mounted() {
    this.loadScript(
      "https://webapi.amap.com/maps?v=1.4.15&key=你的key值&plugin=AMap.Scale,AMap.OverView,AMap.ToolBar,AMap.Autocomplete,AMap.PlaceSearch",
      this.initMap
    );
  },
};
</script>

输入提示后查询

踩坑1:创建new AMap.PlaceSearch实例时:
1. 错误写法:将传入的map参数写为存放map的dom,即this.$refs.amap。
2. 正确写法:这里的map参数应该写为创建的new AMap.Map实例。

输入提示后查询的正确的实现代码如下:
<template>
  <div style="width: 100%; height: 100vh">
    <div ref="amap" id="container" style="height: 100%" />

    <div ref="apanel" id="panel" class="panel" />
    <div class="search-bar">
      <input
        placeholder="地图检索"
        prefix-icon="el-icon-search"
        v-model="searchValue"
        @change="handleSearchChange"
        id="tipinput"
      />
    </div>
  </div>
</template>
<style>
.panel {
  position: absolute;
  background-color: white;
  max-height: 90%;
  overflow-y: auto;
  top: 10px;
  left: 100px;
  width: 280px;
}
.search-bar {
  position: fixed;
  width: 250px;
  padding: 20px 0;
  right: 20px;
  top: 20px;
  z-index: 10000;
  display: flex;
}

.search-bar .el-input {
  background: rgba(255, 255, 255, 0.2);
}
</style>
<script>
export default {
  data() {
    return {
      amap: null,
      apanel: null,
      searchValue: "",
    };
  },
  methods: {
    loadScript(url, callback) {
      // 加载高德地图js
      let script = document.createElement("script");
      script.type = "text/javascript";
      script.src = url;
      document.getElementsByTagName("head")[0].appendChild(script);
      script.onload = () => {
        callback();
      };
    },
    initMap() {
      let scale = new AMap.Scale({
        visible: true,
      });
      let toolBar = new AMap.ToolBar({
        visible: true,
      });
      let overView = new AMap.OverView({
        visible: true,
      });

      this.amap = new AMap.Map("container", {
        //center: [longitude, latitude], //地图中心点
        zoom: 15, //地图级别
        mapStyle: "amap://styles/dark", //设置地图的显示样式
        viewMode: "2D", //设置地图模式
        lang: "zh_cn", //设置地图语言类型
        resizeEnable: true,
      });
      this.amap.addControl(scale);
      this.amap.addControl(toolBar);
      this.amap.addControl(overView);
      const autoOptions = {
        input: "tipinput",
      };
      const auto = new AMap.Autocomplete(autoOptions);
      const placeSearch = new AMap.PlaceSearch({
        pageSize: 5, // 单页显示结果条数
        pageIndex: 1, // 页码
        map: this.amap, // 这里填写new AMap.Map的实例
        panel: "panel",
      }); //构造地点查询类
      AMap.event.addListener(auto, "select", select); //注册监听,当选中某条记录时会触发
      function select(e) {
        placeSearch.search(e.poi.name, (status, result) => {
          console.log("status", status);
          console.log("result", result);
        });
      }
      this.placeSearch = placeSearch;
    },
    handleSearchChange() {
      this.placeSearch.search(this.searchValue, (status, result) => {
        console.log("status", status);
        console.log("result", result);
      });
    },
  },
  mounted() {
    this.loadScript(
      "https://webapi.amap.com/maps?v=1.4.15&key=095f388e7a22189c7cb0095485e1ca59&plugin=AMap.Scale,AMap.OverView,AMap.ToolBar,AMap.Autocomplete,AMap.PlaceSearch",
      this.initMap
    );
  },
};
</script>
最终实现截图

image.png

昨天 — 2025年8月15日首页

🔥10 个被忽视的 Vue3 API 开发利器,用过 5 个才算真正入门

2025年8月15日 16:26

Vue3你真的会用吗?这些 API 开发利器,你用过几个
——10 个被忽视却真香的生产力技巧与场景案例

如果你已经能够熟练地把 ref/reactivewatch/computeddefineProps/defineEmits 倒背如流,那么恭喜你,Vue3 的“入门课”已经通关。但 Vue3 在源码层埋了不少小而美的“隐藏关卡”,它们往往能在关键时刻把代码体积、性能、可维护性同时拉高一个档次。下面 10 个技巧,全部来自社区实战沉淀,每一个都附带真实业务场景 + 代码片段,看完直接就能搬进项目。


1. 巨型表格秒开:shallowRef + markRaw

痛点
后台系统一次性拉 5k 条数据,前端还要做排序/过滤,页面直接卡成 PPT。

方案
只读展示层数据用 shallowRef 包起来,再把不会变动的配置markRaw 标记,Vue 会跳过深层 Proxy 创建,首屏渲染时间直接腰斩。

import { shallowRef, markRaw } from 'vue'

const tableData = shallowRef([])
const columns   = markRaw([           // 列配置完全静态
  { key: 'name', title: '姓名' },
  { key: 'age',  title: '年龄' }
])

// 接口回来直接替换引用即可
api.getList().then(res => tableData.value = res)

实测 5k 条 20 字段数据,FPS 从 12 提升到 45。


2. 跨层级通信不再层层透传:provide + inject + 稳定 key

痛点
Form → FormItem → Input 三级组件,rules、modelValue、校验方法都要逐级 props/emits,写吐了。

方案
父级 provide('formCtx', {...}) 一次性把整包逻辑扔下去;子级用 inject 读取。
技巧:把动态数据包在一个 readonly(reactive({...})) 里,防止子组件误改,又能保证引用稳定、不触发多余更新。

// Form.vue
provide('formCtx', readonly(reactive({
  model: formModel,
  rules: formRules,
  validate
})))

3. 弹窗/抽屉永远挂载到 body:Teleport

痛点
position: fixed 遇到父级 transform 直接失效;z-index 战争更是灾难。

方案
把弹窗内容直接 teleport 到 body 末尾,告别样式副作用。

<Teleport to="body">
  <Modal v-if="visible" />
</Teleport>

4. 异步白屏终结者:Suspense

场景
路由懒加载、图表组件、Markdown 渲染器,首次加载总闪一下白屏。

方案
Suspense 把“加载中”和“真正组件”分离,骨架屏/Loading 丝滑切换。

<Suspense>
  <template #default><AsyncChart /></template>
  <template #fallback><Skeleton /></template>
</Suspense>

5. Hooks 内存零泄漏:effectScope + onScopeDispose

痛点
公共 Hook 里 watch / onMounted / addEventListener 一大堆,组件卸载时漏清一个就内存泄漏。

方案
effectScope 把同一业务域的副作用打包,组件销毁时一句 scope.stop() 一键清理。

import { effectScope, onScopeDispose } from 'vue'

export function useMouse() {
  const scope = effectScope()
  const pos   = reactive({ x: 0, y: 0 })

  scope.run(() => {
    const update = (e: MouseEvent) => { pos.x = e.clientX; pos.y = e.clientY }
    window.addEventListener('mousemove', update)
    onScopeDispose(() => window.removeEventListener('mousemove', update))
  })

  onScopeDispose(() => scope.stop())
  return pos
}

不管页面里 useMouse() 调用多少次,都只挂一个 mousemove,性能 & 内存双保险。


6. 巨型列表滑动不卡顿:v-memo

痛点
v-for 渲染 1k+ 行图文卡片,每次筛选都要全量 diff。

方案
v-memo="[item.id, item.title]" 让 Vue 仅当依赖变化时才重新渲染当前行,其余直接复用 DOM。
实测 1k 条滚动帧率从 18 提到 55。


7. 秒杀倒计时秒级更新:computed 缓存 + 懒执行

场景
活动页 10 个倒计时卡片,每秒更新一次,但只更新“剩余秒数”文本。

方案
把“剩余时间”做成 computed,依赖是 Date.now(),再用 setInterval 触发引用变更。
由于 computed 自带缓存,只有真正需要刷新的卡片才会重新计算,其余直接命中缓存。


8. 全局配置防手滑:readonly

场景
团队协作,总有新人直接改 config.apiBaseURL,导致线上 404。

方案
暴露出去的配置统一 readonly,写保护运行时报错,从源头杜绝。

export const config = readonly(reactive({
  apiBaseURL: 'https://prod.api.com',
  timeout: 8000
}))

9. 模板更干净:Fragment + 无根组件

痛点
为了包一层 div 导致 CSS 布局崩坏(flex/grid 直接多一个层级)。

方案
Vue3 支持 Fragment,组件可以返回多个根节点。

<template>
  <h2>{{ title }}</h2>
  <p>{{ content }}</p>
</template>

10. 一行实现防抖指令:Custom Directives

场景
搜索框、按钮防重复点击,每次复制粘贴防抖函数太啰嗦。

方案
封装成指令,模板里一行搞定。

app.directive('debounce', {
  mounted(el, binding) {
    let timer: any
    el.addEventListener(binding.arg || 'click', () => {
      clearTimeout(timer)
      timer = setTimeout(binding.value, binding.modifiers?.wait || 300)
    })
  }
})
<button v-debounce:click.wait="500" @click="submit">提交</button>

小结:一张脑图带走

场景痛点 冷门利器 一句话记忆
巨型数据渲染 shallowRef / markRaw 只追踪引用,跳过深层 Proxy
跨层级通信 provide/inject + readonly 父传子整包逻辑,子改就报错
弹窗样式地狱 Teleport 直接挂到 body,层级永远最上
异步组件白屏 Suspense 骨架屏 & 组件无缝切换
Hooks 内存泄漏 effectScope 副作用打包,一键 stop
长列表卡顿 v-memo 行级缓存,只 diff 变更行
全局配置防误改 readonly 写保护,运行时报错
模板多余根节点 Fragment 不想包 div 就不包
防抖/节流到处复制 自定义指令 模板级一行声明,逻辑集中

把这篇文章收藏起来,下次 Code Review 的时候,看看你又能从团队代码里“薅”出几个优化点吧!

大规模建筑自动贴图+单体化效果,cesium脚本

作者 波泼
2025年8月15日 16:16

模型单体化效果是指,点击模型中的某个楼栋,弹出楼栋信息或楼栋扫光高亮。

常见自制模型单体化方法

这是一个常见的高频问题。常用的自制模型单体化有2种方法

1、在模型层面把分组设计好。你如1栋,2栋,3栋等。点击分组模型得到模型分组名,根据分组名请求api得到单体化信息。
2、从自制模型制作gis白模。点击模型后,根据点击点找白模轮廓。

导出的的贴图模型如何单体化?

用户说使用Geobuilding将建筑白模导出成贴图模型。模型有了,白模也有了。领导说要点击建筑物出信息,如何实现单体化交互效果呢?

现在的情形和上面第2种方法是有不同的。

一个是模型gltf->白模geojson. 这里的geojson一定能包含住模型 (上面说的第2种)
一个是白模->模型gltf,这里的模型不一定和白模重合。

为什么不会重合?因为模型内部是局部坐标体系,不是经纬度坐标。 那么如何实现点击模型中某个楼栋,实现单体化交互效果。不会再基于模型二次生产白模geojson吧?当然不用

峰回路转,cesium端的实践

cesium端点击模型后,有世界坐标。根据模型矩阵可转换为模型内部坐标。根据原始模型构建算法,得到点击的相对经纬度。然后拿着经纬度去找轮廓,最后弹出建筑信息或高亮。

该方法集成在了导出的demo html文件中,可作为js插件直接使用。 js插件包含模型全量加载、动态加载、模型扫光、点击单体化。支持cesium低版本和高版本使用。 点击事件代码如下:

const handler = new Cesium.ScreenSpaceEventHandler(_this.viewer.scene.canvas);
            handler.setInputAction(async function(click) {
                const pickedObject = _this.viewer.scene.pick(click.position);
                if(Cesium.defined(pickedObject) && pickedObject['id']&&pickedObject['id']['geojson']){
                    //点击了单体化围墙
                    alert(JSON.stringify(pickedObject['id']['geojson']))
                }else if (Cesium.defined(pickedObject) && pickedObject.primitive.userData['pick2']) {
                    const worldPoint = _this.viewer.scene.pickPosition(click.position);
                    if (Cesium.defined(worldPoint)) {

                        //1、获取模型点击的经纬度和高度。
                        var modellnglat = _this.gltfInstance.GetClickLnglat(_this.viewer, worldPoint, pickedObject)
                        /*
                            2、查询点所在的轮廓。
                            !!!演示文件通过前端来查询点在轮廓内。!!!实际应用中需通过后端查询,避免GIS数据泄漏!
                            内置属性解释
                            https://i1.hdslb.com/bfs/article/55509bc62ca58bb6a8463819342258cd98081c25.png@1192w.avif
                            如果一个建筑体包含多个gis轮廓数据。在Geobuilding软件内对建筑体的gis数据【选择框】-打组。打组后这些gis数据有相同的属性值groupid
                            根据groupid可找到关联数据
                         */
                        let geojson = await (await fetch('geojson/' + pickedObject.primitive.userData.geojson)).text();
                        let result = geojson.split("\n").map(function(r) {return JSON.parse(r);});
                        var hitgeo;
                        for (let i = 0; i < result.length; i++) {
                            if (turf.booleanPointInPolygon(turf.point([modellnglat.lng, modellnglat.lat]), result[i], {ignoreBoundary: false})){
                                var demheight = result[i].properties.demheight;
                                var clickheight = modellnglat.height - demheight;
                                if(clickheight>= result[i].properties.pfh*result[i].properties.minfloor && clickheight<= result[i].properties.pfh*result[i].properties.minfloor + (result[i].properties.wfh*result[i].properties.floor)){
                                    hitgeo = result[i]
                                    break;
                                }
                            }
                        }
                        if(!hitgeo) {
                            console.log("没有找到轮廓")
                            return;
                        }

                        //原始geojson数据
                        //alert(JSON.stringify(hitgeo))

                        //3、对hitgeo并进行偏移转换(相对于模型)。
                        hitgeo = _this.gltfInstance.TransFeature(hitgeo, pickedObject);

                        //4、将geojson转换成cesium世界坐标,添加单体化高亮围墙。
                        var wallpos = turf.buffer(hitgeo, 1, {
                            units: "meters"
                        }).geometry.coordinates[0].map(function(z) {
                            return Cesium.Cartesian3.fromDegrees(z[0], z[1], hitgeo.properties.gltfbheight)
                        })

                        if (wall) wall.remove();
                        wall = new WallObject.FlowWall(_this.viewer,wallpos,{
                                copyright:'geobuilding',
                                wallHeight: hitgeo.properties.wfh*hitgeo.properties.floor,
                                wallColor: Cesium.Color.fromCssColorString('rgba(0, 255, 26, 0.3)'),
                                duration: 1000,
                                materialType: 3,
                            }
                        );
                        wall.flowWallEntity.geojson = hitgeo;
                    }else {
                        console.log('无法获取点击点的世界坐标');
                    }
                }else {
                    console.log('未点击到带有 pick2 的模型');
                }
            }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

下面操作一遍看下效果。

首先选择导出模型

选择批量贴图方案,这里选择 实景风贴图

导出后有demo文件。模型信息.txt中本机浏览地址。

打开导出的demo页面,点击场景中某个建筑,显示高亮。

支持更多场景

基于地形的建筑白模自动化贴图 + 单体化

基于分层的单体化点击操作

点击出建筑楼栋信息

浏览器跨标签页通信方案详解

作者 gnip
2025年8月15日 00:03

前言

在现代Web应用开发中,多标签页协作变得越来越常见。用户可能会同时打开应用的多个标签页,而这些标签页之间往往需要进行数据同步或状态共享。本文将全面介绍浏览器环境下实现跨标签页通信的各种方案,分析它们的优缺点,并探讨典型的使用场景。

一、为什么需要跨标签页通信?

在单页应用(SPA)盛行的今天,我们经常会遇到这样的需求:

  1. 用户在标签页A中登录后,其他打开的标签页需要同步更新登录状态
  2. 在标签页B中修改了某些数据,标签页C需要实时显示这些变更
  3. 避免用户在不同标签页中执行冲突操作
  4. 同域名下消息通知同步不同标签页

这些场景都需要不同标签页之间能够进行通信和数据交换。以下介绍几种处理方案。

二、跨标签页通信方案

1. localStorage事件监听

原理:利用localStorage的存储事件,当某个标签页修改了localStorage中的数据时,其他标签页可以通过监听storage事件来获取变更。

// 发送消息的标签页
localStorage.setItem('message', JSON.stringify({ 
  type: 'LOGIN_STATUS_CHANGE',
  data: { isLoggedIn: true }
}));

// 接收消息的标签页
window.addEventListener('storage', (event) => {
  if (event.key === 'message') {
    const message = JSON.parse(event.newValue);
    console.log('收到消息:', message);
    // 处理消息...
  }
});

优点

  • 实现简单,兼容性好
  • 无需额外的服务或依赖

缺点

  • 只能监听其他标签页的修改,当前标签页的修改不会触发自己的事件
  • 传输的数据必须是字符串,需要手动序列化和反序列化
  • 容量限制,几M

2. Broadcast Channel API

原理:Broadcast Channel API允许同源的不同浏览器上下文(标签页、iframe、worker等)之间进行通信。

// 创建或加入频道
const channel = new BroadcastChannel('app_channel');

// 发送消息
channel.postMessage({
  type: 'DATA_UPDATE',
  payload: { /* 数据 */ }
});

// 接收消息
channel.onmessage = (event) => {
  console.log('收到消息:', event.data);
  // 处理消息...
};

// 关闭连接
channel.close();

优点

  • 专为跨上下文通信设计,API简洁
  • 支持任意可序列化对象
  • 性能较好

缺点

  • 兼容性有限(不支持IE和旧版Edge)
  • 需要手动管理频道连接

3. window.postMessage + window.opener

原理:通过window.open()或window.opener获得其他窗口的引用,直接使用postMessage通信。

// 父窗口打开子窗口
const childWindow = window.open('child.html');

// 父窗口向子窗口发送消息
childWindow.postMessage('Hello from parent!', '*');

// 子窗口接收消息
window.addEventListener('message', (event) => {
  // 验证来源
  if (event.origin !== 'https://yourdomain.com') return;
  
  console.log('收到消息:', event.data);
  
  // 回复消息
  event.source.postMessage('Hello back!', event.origin);
});

优点

  • 可以实现跨域通信(需双方配合)
  • 点对点通信效率高

缺点

  • 需要维护窗口引用
  • 安全性需要考虑来源验证
  • 只适用于有明确父子或兄弟关系的窗口

4. Service Worker + MessageChannel

原理:利用Service Worker作为中间人,配合MessageChannel实现双向通信。

// 页面代码
navigator.serviceWorker.controller.postMessage({
  type: 'BROADCAST',
  payload: { /* 数据 */ }
});

// Service Worker代码
self.addEventListener('message', (event) => {
  if (event.data.type === 'BROADCAST') {
    self.clients.matchAll().then(clients => {
      clients.forEach(client => {
        client.postMessage(event.data.payload);
      });
    });
  }
});

// 其他页面接收
navigator.serviceWorker.addEventListener('message', (event) => {
  console.log('收到广播:', event.data);
});

优点

  • 可以实现后台同步
  • 支持推送通知
  • 功能强大

缺点

  • 必须使用HTTPS(本地开发除外)
  • 实现复杂度高
  • 需要处理Service Worker生命周期

5. IndexedDB + 轮询

原理:使用IndexedDB作为共享数据库,各标签页定期检查数据变化。

// 写入数据
function writeMessage(db, message) {
  const tx = db.transaction('messages', 'readwrite');
  tx.objectStore('messages').put({
    id: Date.now(),
    message
  });
}

// 读取新消息
function pollMessages(db, lastId, callback) {
  const tx = db.transaction('messages', 'readonly');
  const store = tx.objectStore('messages');
  const index = store.index('id');
  const request = index.openCursor(IDBKeyRange.lowerBound(lastId, true));
  
  request.onsuccess = (event) => {
    const cursor = event.target.result;
    if (cursor) {
      callback(cursor.value);
      cursor.continue();
    }
  };
}

// 初始化数据库
const request = indexedDB.open('messaging_db', 1);
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  if (!db.objectStoreNames.contains('messages')) {
    const store = db.createObjectStore('messages', { keyPath: 'id' });
    store.createIndex('id', 'id', { unique: true });
  }
};

优点

  • 存储容量大
  • 可以存储复杂数据结构
  • 数据持久化

缺点

  • 需要手动实现轮询机制
  • API较复杂
  • 性能不如即时通信方案

三、方案对比

方案 兼容性 实时性 复杂度 数据容量 适用场景
localStorage事件 优秀 小(5MB) 简单状态同步
BroadcastChannel 中等 同源多标签通信
postMessage 优秀 无限制 有窗口引用关系
ServiceWorker 中等 无限制 PWA/后台同步
IndexedDB 良好 大数据量共享

四、典型使用场景

1. 用户登录状态同步

场景描述:当用户在某个标签页完成登录或退出操作时,其他打开的标签页需要立即更新认证状态。

实现方案

// 登录成功后
localStorage.setItem('auth', JSON.stringify({
  isAuthenticated: true,
  user: { name: 'John', token: '...' }
}));

// 所有标签页监听
window.addEventListener('storage', (event) => {
  if (event.key === 'auth') {
    const auth = JSON.parse(event.newValue);
    if (auth.isAuthenticated) {
      // 更新UI显示已登录状态
    } else {
      // 更新UI显示未登录状态
    }
  }
});

2. 多标签页数据编辑冲突避免

场景描述:当用户在多个标签页编辑同一份数据时,需要防止冲突提交。

实现方案

// 使用BroadcastChannel
const editChannel = new BroadcastChannel('document_edit');

// 开始编辑时发送锁定请求
editChannel.postMessage({
  type: 'LOCK_REQUEST',
  docId: 'doc123',
  userId: 'user456'
});

// 接收锁定状态
editChannel.onmessage = (event) => {
  if (event.data.type === 'LOCK_RESPONSE') {
    if (event.data.docId === currentDocId && !event.data.success) {
      alert('文档正在被其他标签页编辑,请稍后再试');
    }
  }
};

3. 多标签页资源预加载

场景描述:主标签页加载的资源可以被其他标签页共享,避免重复加载。

实现方案

// 使用Service Worker缓存资源
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then(response => {
      if (response) {
        // 从缓存返回
        return response;
      }
      
      // 获取并缓存
      return fetch(event.request).then(res => {
        return caches.open('shared-cache').then(cache => {
          cache.put(event.request, res.clone());
          return res;
        });
      });
    })
  );
});

五、总结

浏览器提供了多种跨标签页通信的方案,各有其适用场景:

  • 对于简单的状态同步,localStorage事件是最简单直接的选择
  • 需要更强大的通信能力时,BroadcastChannel API是现代化解决方案
  • 复杂应用可以考虑使用Service Worker作为通信中枢
  • 有明确窗口关系的场景可以使用window.postMessage
  • 大数据量或需要持久化的场景适合使用IndexedDB

运行时模块批量导入

作者 gnip
2025年8月14日 23:50

概述

在工程化项目当中,有时候我们可能需要在运行时态自动批量处理某个文件夹下的所有文件,比如有个如下功能要做:

在一个有不同模块的多语言文件分割的文件中,需要将moudle文件夹下的多语言模块合并到index中进行合并,然后通过i18工具进行多语言注册,常规做法在index中一个个导入对于模块,然后合并,但是随着模块文件越来越多,index里面导入的文件就会变得越来越庞大,并且没有自动化的程度,导致每加一个模块都需要手动导入(很麻烦)。

image.png 针对上面的问题,引入模块运行时自动导入的概念,在工程化项目中(vue、react等),如果需要再运行时获取到工程项目文件的目录相关信息(编译时态),最终经过打包工具(webpack、vite)打包后(运行时态),处理对于文件相关逻辑,因此,根据环境webpack、vite,能够找到两个对于的api

  • require.context
  • import.meta.glob

介绍

(1)require.context(Webpack 特有)

require.context 是 Webpack 提供的一个 API,允许在编译时创建一个上下文,用于匹配指定目录下的文件,并支持动态导入。

(2)import.meta.glob(Vite/Rollup 特有)

import.meta.glob 是 Vite 和 Rollup 提供的功能,用于实现类似 require.context 的模块批量导入,但语法更现代化,支持 ESM(ES Module)。

特性 require.context import.meta.glob
所属工具 Webpack Vite / Rollup
加载方式 同步/动态导入 默认懒加载
返回值 函数 对象(Promise)
适用场景 Webpack 项目 Vite / Rollup 项目

核心用途:为什么需要它们?

(1)自动注册全局组件

在 Vue 项目中,我们通常需要手动注册全局组件:

import Button from './components/Button.vue';
import Input from './components/Input.vue';
// ... 其他组件

app.component('Button', Button);
app.component('Input', Input);
// ... 重复注册

使用 require.context 或 import.meta.glob 可以自动扫描目录并注册,避免重复代码!

(2)动态加载路由

在大型项目中,路由可能非常多,手动导入会很麻烦:

// 传统方式
import Home from './views/Home.vue';
import About from './views/About.vue';
// ... 其他路由

使用批量导入,可以自动生成路由表,提高可维护性。

(3)按需加载语言包/配置文件

例如国际化(i18n)场景,不同语言包可以动态加载:

// 自动加载所有语言包
const locales = import.meta.glob('./locales/*.json');

使用方式对比

(1)require.context(Webpack)

基本语法:

const context = require.context(
  directory,       // 要搜索的目录
  useSubdirectories, // 是否搜索子目录
  regExp,          // 匹配文件的正则表达式
  mode             // 加载模式(可选)
);

示例:自动注册 Vue 组件

const ctx = require.context('./components', true, /.vue$/);

ctx.keys().forEach(path => {
  const component = ctx(path).default;
  const name = path.split('/').pop().replace('.vue', '');
  app.component(name, component);
});

(2)import.meta.glob(Vite)

基本语法:

const modules = import.meta.glob(globPattern, options);

示例 1:懒加载(默认)

const modules = import.meta.glob('./components/*.vue');

for (const path in modules) {
  modules[path]().then((mod) => {
    const name = path.split('/').pop().replace('.vue', '');
    app.component(name, mod.default);
  });
}

示例 2:直接导入(非懒加载)

const modules = import.meta.glob('./components/*.vue', { eager: true });

Object.entries(modules).forEach(([path, mod]) => {
  const name = path.split('/').pop().replace('.vue', '');
  app.component(name, mod.default);
});

核心区别与如何选择?

对比项 require.context import.meta.glob
构建工具 Webpack Vite / Rollup
加载方式 同步(默认) 懒加载(默认)
返回值 函数(context() 对象({ path: Promise }
适用场景 旧项目(Webpack) 新项目(Vite)

如何选择?

  • 如果你的项目使用 Webpack,用 require.context
  • 如果是 Vite / Rollup 项目,用 import.meta.glob
  • import.meta.glob 更现代化,推荐新项目使用。

高级用法:动态路由、国际化等

(1)动态路由(Vite + Vue Router)

const pages = import.meta.glob('../views/**/*.vue');

const routes = Object.entries(pages).map(([path, component]) => {
  const name = path.replace('../views/', '').replace('.vue', '');
  return { path: `/${name}`, component };
});

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

(2)国际化(i18n 自动加载语言包)

const locales = import.meta.glob('./locales/*.json', { eager: true });

const messages = {};
Object.entries(locales).forEach(([path, mod]) => {
  const lang = path.split('/').pop().replace('.json', '');
  messages[lang] = mod.default;
});

const i18n = createI18n({
  locale: 'zh',
  messages,
});

总结

  • require.context 是 Webpack 提供的批量导入方案,适用于传统项目。
  • import.meta.glob 是 Vite/Rollup 的现代化替代方案,默认懒加载,更灵活。
  • 两者都能用于 自动注册组件、动态路由、国际化 等场景。
  • 新项目推荐使用 Vite + import.meta.glob,体验更佳!
  • 对于工程化项目(组件库)使用可以大大提高开发效率(组件注册)。
昨天以前首页

待办事项小程序开发

作者 姑苏洛言
2025年8月14日 22:45

1. 项目规划

  • 功能需求

  • 添加待办事项

  • 标记完成/未完成

  • 删除待办事项

  • 分类或标签管理(可选)

  • 数据持久化(本地存储)

2. 实现功能

  • 添加待办事项

  • 监听输入框和按钮事件,将输入内容添加到列表。

  • 标记完成/未完成

  • 使用复选框或滑动操作,更新事项状态。

  • 删除待办事项

  • 支持左滑删除或长按删除。

  • 数据持久化

  • 使用本地存储待办事项数据。

3. 代码实现

基于微信小程序开发一个待办事项应用,主要包括以下功能:

  1. 创建待办事项
  2. 标记完成状态
  3. 删除待办事项
  4. 本地存储待办事项数据

首先,我会创建以下文件:

  1. app.json - 小程序全局配置

  2. pages/index/index.js - 页面逻辑

  3. pages/index/index.wxml - 页面结构

  4. pages/index/index.wxss - 页面样式

  5. pages/index/index.json - 页面配置

    { "pages": ["pages/index/index"], "window": { "navigationBarTitleText": "待办事项", "navigationBarBackgroundColor": "#ffffff", "navigationBarTextStyle": "black" }, "style": "v2", "sitemapLocation": "sitemap.json" }

    添加 {{item.text}} 删除

    .container { padding: 20px; } .header { display: flex; margin-bottom: 20px; } .header input { flex: 1; margin-right: 10px; padding: 10px; border: 1px solid #ccc; border-radius: 4px; } .todo-list { margin-top: 20px; } .todo-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; margin-bottom: 10px; border: 1px solid #eee; border-radius: 4px; } .todo-item.done { text-decoration: line-through; color: #999; }

    Page({ data: { todos: [], inputValue: '' }, onLoad() { this.loadTodos(); }, loadTodos() { const todos = wx.getStorageSync('todos') || []; this.setData({ todos }); }, saveTodos() { wx.setStorageSync('todos', this.data.todos); }, addTodo() { if (!this.data.inputValue.trim()) return; const newTodo = { id: Date.now(), text: this.data.inputValue, done: false }; this.setData({ todos: [...this.data.todos, newTodo], inputValue: '' }, this.saveTodos); }, toggleTodo(e) { const { id } = e.currentTarget.dataset; const todos = this.data.todos.map(todo => todo.id === id ? { ...todo, done: !todo.done } : todo ); this.setData({ todos }, this.saveTodos); }, deleteTodo(e) { const { id } = e.currentTarget.dataset; const todos = this.data.todos.filter(todo => todo.id !== id); this.setData({ todos }, this.saveTodos); }, inputChange(e) { this.setData({ inputValue: e.detail.value }); } });

❌
❌