阅读视图

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

如何聊懒加载,只说个懒可不行


React 的“懒”哲学:从 React.lazySuspense,一场重构前端性能认知的深度革命

我们生活在一个“即时满足”的时代。用户期望点击即响应、滑动即加载、搜索即呈现。任何超过 300ms 的延迟,都可能被解读为“卡顿”或“失败”。在这样的苛刻要求下,前端开发早已超越“功能实现”的范畴,进入一场关于时间、资源与体验的精密博弈

而在这场博弈中,懒加载(Lazy Loading) 不再是可有可无的“优化技巧”,而是现代 Web 应用的生存底线

React,作为当今最主流的 UI 框架,不仅拥抱了懒加载,更将其内化为框架的核心哲学。从 React.lazySuspense,从动态 import()IntersectionObserver 集成,React 正在构建一个“按需供给、延迟执行、优先调度”的全新运行时体系。

这不仅是性能优化,更是一场对前端工程“贪婪文化”的彻底清算。


一、懒加载的本质:从“全量预载”到“按需供给”

在 Web 开发的早期,我们信奉“预加载一切”:

  • 所有 JS 打包成一个 bundle.js
  • 所有图片在 HTML 中直接 src
  • 所有组件在应用启动时全部引入

这种“急加载”(Eager Loading)模式的逻辑是:提前加载,避免等待。但现实是残酷的:

⚠️ 用户不会看 80% 的内容,却要为这 80% 买单——带宽、内存、首屏时间。

懒加载的出现,是对这种“资源浪费主义”的反叛。它主张:

只在真正需要时,才加载所需资源。

这不是“偷懒”,而是对用户、设备与网络的极致尊重


二、React 中的懒加载全景图

在 React 生态中,懒加载已渗透到每一个层级,形成一套完整的“延迟执行体系”:

层级 技术方案 目标
路由 React.lazy + import() 减少首屏 JS 体积
组件 React.lazy + Suspense 延迟重组件渲染
图片 loading="lazy" / IntersectionObserver 避免无效图片下载
数据 useEffect + 分页 / React Cache(实验) 控制 API 调用时机
模块 Webpack Code Splitting 实现 chunk 级拆分

下面我们逐层深入,剖析其原理与最佳实践。


三、路由懒加载:SPA 的生命线

1. 问题:单页应用的“首屏诅咒”

在传统 SPA 中,即使用户只访问 /,Webpack 也会将所有路由组件打包进主 chunk。一个中型应用的 bundle.js 轻松突破 2MB,导致:

  • 首屏白屏时间长
  • TTI(Time to Interactive)延迟
  • 移动端流量消耗巨大

2. 解决方案:动态 import() + React.lazy

const Home = React.lazy(() => import('./pages/Home'));
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Settings = React.lazy(() => import('./pages/Settings'));
  • import('./pages/Home') 是一个 Promise,返回组件模块
  • React.lazy 接收该 Promise,返回一个“可挂起”的组件
  • Webpack 自动将每个 import() 拆分为独立 chunk

3. 必须搭配 Suspense

function App() {
  return (
    <Routes>
      <Route path="/dashboard" element={
        <Suspense fallback={<Spinner size="lg" />}>
          <Dashboard />
        </Suspense>
      } />
    </Routes>
  );
}

🔥 SuspenseReact.lazy 的“安全气囊”

它处理三种状态:

  • Pending:组件加载中,显示 fallback
  • Resolved:组件加载成功,渲染真实 UI
  • Rejected:组件加载失败,需配合 Error Boundary

4. 高级技巧

(1)预加载关键路由
// 鼠标悬停时预加载
const handleMouseEnter = () => {
  import('./pages/Dashboard'); // 不赋值,仅触发加载
};
(2)Prefetching(构建时优化)
React.lazy(() => import(/* webpackPreload: true */ './Dashboard'));
// 或
<link rel="prefetch" href="Dashboard.chunk.js" as="script" />
(3)Chunk 分组(避免 chunk 爆炸)
React.lazy(() => import(/* webpackChunkName: "user-section" */ './Profile'));
React.lazy(() => import(/* webpackChunkName: "user-section" */ './Settings'));

四、组件懒加载:重组件的“按需唤醒”

并非所有组件都适合初始加载。以下类型应考虑懒加载:

  • 富文本编辑器(如 Slate.js
  • 数据可视化(EChartsD3
  • 视频播放器(video.js
  • 模态框、抽屉、复杂表单

实现方式与路由懒加载一致:

const Chart = React.lazy(() => import('./components/Chart'));
const Editor = React.lazy(() => import('./components/Editor'));

function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(true)}>查看图表</button>
      
      {showChart && (
        <Suspense fallback={<Skeleton height="400px" />}>
          <Chart data={data} />
        </Suspense>
      )}
    </div>
  );
}

💡 关键洞察:懒加载不仅用于“路由级”组件,也适用于“功能级”组件

它让页面保持轻量,只在用户明确表达“需要”时,才唤醒重型组件。


五、图片懒加载:从被动到主动的控制权争夺

1. 原生方案:loading="lazy"

<img src="photo.jpg" loading="lazy" alt="风景" />
  • ✅ 简单、无需 JS、浏览器原生支持
  • ❌ 无法控制加载时机、无法集成骨架屏、无法处理错误

📉 适合内容型网站(如博客),不适合复杂 Web 应用

2. React 主导方案:自定义 LazyImage 组件

import { useState, useRef, useEffect } from 'react';

function LazyImage({ src, alt, placeholder = '#eee', threshold = 0.1 }) {
  const [status, setStatus] = useState('loading'); // loading | loaded | error
  const imgRef = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            const img = new Image();
            img.src = src;
            img.onload = () => setStatus('loaded');
            img.onerror = () => setStatus('error');
          }
        });
      },
      {
        root: null,
        rootMargin: '50px',
        threshold
      }
    );

    if (imgRef.current) observer.observe(imgRef.current);

    return () => observer.disconnect();
  }, [src]);

  if (status === 'error') {
    return <FallbackImage />;
  }

  return (
    <div ref={imgRef} style={{ background: placeholder, minHeight: '200px' }}>
      {status === 'loaded' ? (
        <img src={src} alt={alt} style={{ width: '100%', height: 'auto' }} />
      ) : (
        <Skeleton height="100%" />
      )}
    </div>
  );
}

优势:

  • ✅ 精确控制加载时机(rootMargin 提前触发)
  • ✅ 集成骨架屏、占位符、错误处理
  • ✅ 可配合优先级调度(如首屏图片优先加载)

六、数据懒加载:从副作用到声明式等待

1. 传统方式:useEffect 副作用驱动

function UserProfile({ id }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${id}`).then(r => r.json()).then(setUser);
  }, [id]);

  return user ? <div>{user.name}</div> : <Spinner />;
}

问题:数据获取与渲染耦合,无法中断,无法优先级调度。

2. 未来方向:React Cacheuse(实验性)

// 实验性 API,未来可能变化
const userResource = createResource(fetchUser);

function UserProfile({ id }) {
  const user = userResource.read(id); // 可能抛出 Promise
  return <div>{user.name}</div>;
}

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserProfile id={1} />
    </Suspense>
  );
}

🔮 这是 React 的终极愿景:让数据获取像组件渲染一样,可中断、可调度、可 Suspense

它将“等待”提升为一等公民,实现 UI + Data 的统一异步模型


七、懒加载的代价与权衡:没有银弹

任何技术都有代价,懒加载也不例外。

1. Chunk 爆炸与网络开销

  • 过多小 chunk 增加 HTTP 请求
  • 可能抵消 code splitting 的收益

对策

  • 合理分组 chunk(webpackChunkName
  • 使用 HTTP/2 多路复用
  • Prefetching 关键路径

2. 加载态的 UX 设计挑战

  • 到处是 spinner,用户体验割裂
  • 骨架屏设计成本高

对策

  • 语义化骨架屏(模拟真实结构)
  • 预加载用户可能访问的页面
  • 使用 SuspenseList 协调多个 fallback

3. 错误处理复杂化

  • chunk 加载失败(404、网络中断)
  • 需全局错误边界捕获
<ErrorBoundary fallback={<ErrorPage />}>
  <Suspense fallback={<Spinner />}>
    <LazyComponent />
  </Suspense>
</ErrorBoundary>

八、结语:React 的“懒”,是一种高级的克制

我们曾以为,前端的进化是“功能越来越多,包越来越大,加载越来越快”。但 React 用 lazySuspense 告诉我们:

真正的进步,是学会“不做什么”

React.lazy 的伟大,不在于它节省了 500KB,而在于它重塑了开发者的心智模型

  • 从“全量加载”到“按需供给”
  • 从“功能优先”到“体验优先”
  • 从“我能做”到“我该做”

在信息过载的时代,克制,才是最高级的优雅

React 教会我们的,不仅是如何写代码,更是如何用技术节制贪婪,用延迟换取尊严

因为最好的加载,是让用户感觉不到加载的存在——就像空气,看不见,却让一切呼吸顺畅。

🌿 懒,不是怠惰,而是智慧;延迟,不是拖延,而是尊重


写一个Chrome插件

最近一直用的cookie同步插件被禁用了,然后利用chatGPT自己搞了一个

1. 文件清单(直接在项目目录放这些文件)

  • manifest.json

  • background.js (service worker)

  • popup.html

  • popup.js

  • popup.css(可选,可以直接写在html里面)

  • README.md(可选,使用说明)

  • icons(放展示图标)

将上面的文件放入文件夹中,只保留必要的文件,最终结果如下:

image.png

其中icons可以通过网上的免费图标网站自己设计一些

image.png

1. manifest.json

{
  "manifest_version": 3,
  "name": "Multi Cookie Sync",
  "version": "1.0",
  "description": "Sync all cookies from one site to another with multiple rules.",
  "permissions": [
    "cookies",
    "storage",
    "scripting"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "background": {
    "service_worker": "background.js"
  }
}

2. background.js

Chrome 扩展 (Manifest V3) 里:

  • background.js 并不是强制必须的
  • 只有当你需要在后台执行逻辑(比如监听消息、操作 cookie、下载文件等 需要扩展 API 权限 的动作)时,才需要它作为 service worker 存在。

我的需求其实只是将某个网站的cookie同步到开发环境的页面中,绕开SSO登录的逻辑,不需要一些文件的导入导出,或者下载之类的操作,所以我这个文件是空的。

其中监听事件是扩展 API、长生命周期的监听(cookie 变化、tab 更新、消息监听)

比如:

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === "EXPORT_COOKIES") {
    // 读取 cookie
  }
});

注意:该文件中不能访问DOM

3. popup.html

这个就是打开插件后展示的界面

image.png

其中代码如下:

<!DOCTYPE html>
<html>
<head>
  <title>Multi Cookie Sync</title>
  <meta charset="UTF-8">
  <style>
    body { font-family: sans-serif; width: 360px; padding: 10px; }
    input, button { width: 100%; margin: 5px 0; }
    label { font-weight: bold; display: block; margin-top: 8px; }
    .pair { margin-bottom: 10px; border-bottom: 1px solid #ccc; padding-bottom: 10px; }
    .input-actions { display: flex; gap: 10px; }
    .input-actions button { flex: 1; }
  </style>
</head>
<body>
  <h3>添加同步规则</h3>

  <input type="text" id="sourceUrl" placeholder="源站 URL,例如 https://a.example.com">
  <input type="text" id="targetUrl" placeholder="目标站 URL,例如 https://b.example.com">

  <div class="input-actions">
    <button id="addPairBtn">添加配对</button>
    <button id="clearInputBtn">清空输入</button>
  </div>

  <h3>配对规则列表</h3>
  <div id="pairList"></div>

  <button id="syncAllBtn">同步全部 Cookie</button>
  <p id="status"></p>

  <script src="popup.js"></script>
</body>
</html>

4. popup.js

负责 和用户交互(点按钮、输入网址)

const sourceInput = document.getElementById("sourceUrl");
const targetInput = document.getElementById("targetUrl");
const statusEl = document.getElementById("status");
const pairListEl = document.getElementById("pairList");

function renderPairs(pairs) {
  pairListEl.innerHTML = "";
  pairs.forEach((pair, index) => {
    const div = document.createElement("div");
    div.className = "pair";
    div.innerHTML = `
      <div><strong>源:</strong>${pair.source}</div>
      <div><strong>目标:</strong>${pair.target}</div>
      <button data-index="${index}" class="deleteBtn">删除</button>
    `;
    pairListEl.appendChild(div);
  });

  document.querySelectorAll(".deleteBtn").forEach(btn => {
    btn.addEventListener("click", (e) => {
      const i = parseInt(e.target.dataset.index);
      chrome.storage.sync.get({ sitePairs: [] }, ({ sitePairs }) => {
        sitePairs.splice(i, 1);
        chrome.storage.sync.set({ sitePairs }, () => renderPairs(sitePairs));
      });
    });
  });
}

document.getElementById("addPairBtn").addEventListener("click", () => {
  const sourceUrl = sourceInput.value.trim();
  const targetUrl = targetInput.value.trim();

  if (!sourceUrl || !targetUrl) {
    statusEl.innerText = "请填写完整的源站和目标站 URL";
    return;
  }

  chrome.storage.sync.get({ sitePairs: [] }, ({ sitePairs }) => {
    sitePairs.push({ source: sourceUrl, target: targetUrl });
    chrome.storage.sync.set({ sitePairs }, () => {
      renderPairs(sitePairs);
      statusEl.innerText = "已添加规则";
    });
  });

  // ❌ 不再清空输入框(除非用户点击“清空”按钮)
});

document.getElementById("clearInputBtn").addEventListener("click", () => {
  sourceInput.value = "";
  targetInput.value = "";
});

document.getElementById("syncAllBtn").addEventListener("click", () => {
  chrome.storage.sync.get({ sitePairs: [] }, async ({ sitePairs }) => {
    for (const pair of sitePairs) {
      try {
        const cookies = await chrome.cookies.getAll({ url: pair.source });
        for (const cookie of cookies) {
          await chrome.cookies.set({
            url: pair.target,
            name: cookie.name,
            value: cookie.value,
            domain: new URL(pair.target).hostname,
            path: cookie.path || "/",
            secure: cookie.secure,
            httpOnly: cookie.httpOnly,
            sameSite: cookie.sameSite || "no_restriction",
            expirationDate: cookie.expirationDate || (Date.now() / 1000 + 3600)
          });
        }
      } catch (e) {
        console.warn(`同步失败: ${pair.source}${pair.target}`, e);
      }
    }

    statusEl.innerText = "所有规则 Cookie 同步完成。";
  });
});

chrome.storage.sync.get({ sitePairs: [] }, ({ sitePairs }) => {
  renderPairs(sitePairs);
});

// 1. 监听输入变化并存入 storage
sourceInput.addEventListener("input", () => {
  chrome.storage.local.set({ currentSourceUrl: sourceInput.value });
});
targetInput.addEventListener("input", () => {
  chrome.storage.local.set({ currentTargetUrl: targetInput.value });
});

// 2. 页面打开时恢复输入框内容
chrome.storage.local.get(["currentSourceUrl", "currentTargetUrl"], (data) => {
  if (data.currentSourceUrl) sourceInput.value = data.currentSourceUrl;
  if (data.currentTargetUrl) targetInput.value = data.currentTargetUrl;
});
document.getElementById("clearInputBtn").addEventListener("click", () => {
  sourceInput.value = "";
  targetInput.value = "";
  chrome.storage.local.remove(["currentSourceUrl", "currentTargetUrl"]);
});

3. 安装和使用

  • 将上面文件放到一个文件夹(例如 cookie-syncer)。

  • 打开 Chrome → chrome://extensions/

  • 打开右上角「开发者模式」。

  • 点击「加载已解压的扩展程序」,选择该文件夹。

  • 点击浏览器工具栏扩展图标打开弹窗。

其中目标站就是想要从其同步cookie的网站,比如公司内登录过的SSO网站

一套代码如何同时适配移动端和pc端

1. 媒体查询(Media Query)+ CSS Flex/Grid(适用于简单布局)

利用 CSS 的 @media 进行响应式设计,使页面在不同设备上适配。例如:


/* 默认 PC 端样式 */
.container {
  width: 1200px;
  margin: 0 auto;
}

/* 移动端适配 */
@media screen and (max-width: 768px) {
  .container {
    width: 100%;
    padding: 0 16px;
  }
}

适用于:
✅ 结构简单、仅靠 CSS 即可完成适配的项目。
❌ 需要手动写多个 @media 规则,维护成本较高。


2. rem / vw+vh 适配(适用于移动端为主的场景)

  • rem 方案:根据 htmlfont-size 变化来调整页面比例。常与 postcss-pxtorem 配合使用。
  • vw / vh 方案:使用视口单位 vw(相对于视口宽度的 1%)进行布局。

示例(rem 方案):


html {
  font-size: calc(100vw / 375 * 16); /* 以 375px 设计稿为基准 */
}
.container {
  width: 20rem; /* 根据设计稿换算成 rem */
}

适用于:
✅ 主要针对移动端,兼容 PC。
❌ 适配 PC 时可能会遇到一些尺寸问题。


3. 使用 useMediaQuery 监听设备类型(更灵活的方式)

可以用 Vue3 的 refcomputed 结合 window.matchMedia 来监听屏幕尺寸,动态切换组件或布局:


import { ref, onMounted, onUnmounted } from "vue";

export function useDeviceType() {
  const isMobile = ref(window.matchMedia("(max-width: 768px)").matches);

  const updateDeviceType = () => {
    isMobile.value = window.matchMedia("(max-width: 768px)").matches;
  };

  onMounted(() => {
    window.addEventListener("resize", updateDeviceType);
  });

  onUnmounted(() => {
    window.removeEventListener("resize", updateDeviceType);
  });

  return { isMobile };
}

在组件中使用:


<script setup>
  import { useDeviceType } from "@/hooks/useDeviceType";

  const { isMobile } = useDeviceType();
</script>

<template>
  <div v-if="isMobile">移动端界面</div>
  <div v-else>PC 端界面</div>
</template>

适用于:
✅ 适配不同设备时可以动态渲染不同组件,减少无效的 DOM。
❌ 需要写额外的逻辑,稍微增加代码复杂度。


4. 基于 vite-plugin-style-import 按需加载不同端的样式

如果 UI 组件库支持不同端(如 Vant 针对移动端,Ant Design Vue 针对 PC 端),可以按需加载:


import styleImport from 'vite-plugin-style-import'

export default defineConfig({
  plugins: [
    styleImport({
      libs: [
        {
          libraryName: 'vant',
          esModule: true,
          resolveStyle: (name) => `vant/es/${name}/style/index`,
        },
        {
          libraryName: 'ant-design-vue',
          esModule: true,
          resolveStyle: (name) => `ant-design-vue/es/${name}/style/index`,
        },
      ],
    }),
  ],
});

适用于:
✅ 需要在 PC 端和移动端使用不同 UI 组件库的情况。
❌ 不能解决布局适配问题。


5. 自动适配的 lib-flexible + postcss-pxtorem

如果项目主要是移动端,同时希望在 PC 端也适配,可以用 lib-flexible + postcss-pxtorem 来自动转换 px:

  1. 安装:

npm install amfe-flexible postcss-pxtorem --save
  1. main.ts 引入:

import 'amfe-flexible'
  1. postcss.config.js 配置:

module.exports = {
  plugins: {
    "postcss-pxtorem": {
      rootValue: 37.5, // 设计稿是 375px
      propList: ["*"], // 需要转换的属性
    },
  },
};

适用于:
✅ 主要是移动端项目,同时希望 PC 端也能展示。
❌ 依赖 amfe-flexible,部分 UI 组件库可能不兼容。


6. Vue3 + Tailwind CSS(推荐)

如果不想写一堆 @media,可以用 Tailwind CSS 的 smmdlg 进行适配:


<template>
  <div class="w-full p-4 text-center sm:bg-blue-200 md:bg-green-200 lg:bg-red-200">
    <p class="text-sm md:text-lg lg:text-2xl">不同屏幕尺寸显示不同颜色</p>
  </div>
</template>

适用于:
✅ 开箱即用,开发速度快。
❌ 需要学习 Tailwind 语法。


7. 使用 VueUse useWindowSize 进行监听

VueUse 提供了 useWindowSize 来动态适配:


import { useWindowSize } from '@vueuse/core';

const { width } = useWindowSize();
const isMobile = computed(() => width.value < 768);

适用于:
✅ 需要动态判断设备类型的场景。
❌ 需要引入 VueUse 依赖。


8. 双端组件(PC / Mobile 组件分开管理)

可以维护两个独立的组件,在 App.vue 里按条件渲染:


<template>
  <MobileLayout v-if="isMobile" />
  <PCLayout v-else />
</template>

适用于:
✅ 适配复杂项目,PC 和移动端 UI 差异较大时。
❌ 代码量增加,维护成本高。


总结

方案 适用场景 维护成本 适配能力
Media Query 适用于简单布局 ⭐⭐⭐
rem/vw 适配 主要移动端 ⭐⭐⭐⭐
useMediaQuery 监听 组件级适配 ⭐⭐⭐⭐
vite-plugin-style-import UI 组件按需加载 ⭐⭐⭐
lib-flexible + pxtorem H5 项目 ⭐⭐⭐⭐
Tailwind CSS 快速开发 ⭐⭐⭐⭐
useWindowSize 监听 动态调整 ⭐⭐⭐⭐
双端组件 复杂项目 ⭐⭐⭐⭐⭐

推荐方案

  • 移动端为主rem + vw/vhlib-flexible
  • PC & 移动端共存useMediaQuery + Tailwind CSS
  • PC & 移动端布局差异大双端组件

结论: 取决你的项目主要是 PC 还是移动端?

这个Database Transaction功能多多,你用过吗?

Vona 是一款直观、优雅、强大的 Node.js Web 框架,用于快速开发任何规模的企业级应用。首创 DTO 动态推断与生成能力,从而显著提升开发效率和开发体验。Vona ORM 对数据库事务提供了完整的支持,提供了直观、优雅、强大的特性:

  1. 使用装饰器启用事务
  2. 事务传播机制
  3. 事务补偿机制
  4. 确保数据库与缓存数据一致性

使用装饰器启用事务

import { Database } from 'vona-module-a-orm';

class ServicePost {
  @Database.transaction()
  async transaction() {
    // insert
    const post = await this.scope.model.post.insert({
      title: 'Post001',
    });
    // update
    await this.scope.model.post.update({
      id: post.id,
      title: 'Post001-Update',
    });
  }
}  

手工启用事务

1. 使用当前数据源

class ServicePost {
  async transactionManually() {
    const db = this.bean.database.current;
    await db.transaction.begin(async () => {
      await this.scope.model.post.update({ id: 1, title: 'Post001_Update' });
    });
  }
}

2. 使用指定数据源

class ServicePost {
  async transactionManually() {
    const db = this.bean.database.getDb({ clientName: 'default' });
    await db.transaction.begin(async () => {
      const modelPost = this.scope.model.post.newInstance(db);
      await modelPost.update({ id: 1, title: 'Post001_Update' });
    });
  }
}

事务参数

class ServicePost {
  @Database.transaction({
+   isolationLevel: 'READ_COMMITTED',
+   propagation: 'REQUIRED'
  })
  async transaction() {
    ...
  }
}  
class ServicePost {
  async transactionManually() {
    const db = this.bean.database.getDb({ clientName: 'default' });
    await db.transaction.begin(
      async () => {
        ...
      },
      {
+       isolationLevel: 'READ_COMMITTED',
+       propagation: 'REQUIRED',
      }
    );
  }
}  

事务参数:isolationLevel

名称 说明
DEFAULT 数据库相关的缺省isolationLevel
READ_UNCOMMITTED
READ_COMMITTED
REPEATABLE_READ
SERIALIZABLE
SNAPSHOT

事务参数:propagation

Vona ORM 支持数据库事务传播机制

名称 说明
REQUIRED 默认的事务传播级别。如果当前存在事务, 则加入该事务。如果当前没有事务, 则创建一个新的事务
SUPPORTS 如果当前存在事务,则加入该事务. 如果当前没有事务, 则以非事务的方式继续运行
MANDATORY 强制性。如果当前存在事务, 则加入该事务。如果当前没有事务,则抛出异常
REQUIRES_NEW 创建一个新的事务。如果当前存在事务, 则把当前事务挂起。也就是说不管外部方法是否开启事务,总是开启新的事务, 且开启的事务相互独立, 互不干扰
NOT_SUPPORTED 以非事务方式运行。如果当前存在事务,则把当前事务挂起(不用)
NEVER 以非事务方式运行。如果当前存在事务,则抛出异常

事务补偿机制

当事务成功或者失败时执行一些逻辑

1. 成功补偿

this.bean.database.current.commit(async () => {
  // do something when success
});

2. 失败补偿

this.bean.database.current.compensate(async () => {
  // do something when failed
});

事务与Cache数据一致性

许多框架使用最简短的用例来证明是否高性能,而忽略了业务复杂性带来的性能挑战。随着业务的增长和变更,项目性能就会断崖式下降,各种优化补救方案让项目代码繁杂冗长。而 Vona 正视大型业务的复杂性,从框架核心引入缓存策略,并实现了二级缓存Query缓存Entity缓存等机制,轻松应对大型业务系统的开发,可以始终保持代码的优雅和直观

Vona 系统对数据库事务与缓存进行了适配,当数据库事务失败时会自动执行缓存的补偿操作,从而让数据库数据与缓存数据始终保持一致

针对这个场景,Vona 提供了内置的解决方案

1. 使用当前数据源

class ServicePost {
  @Database.transaction()
  async transaction() {
    // insert
    const post = await this.scope.model.post.insert({
      title: 'Post001',
    });
    // cache
    await this.scope.cacheRedis.post.set(post, post.id);
  }
}  
  • 当新建数据后,将数据放入 redis 缓存中。如果这个事务出现异常,就会进行数据回滚,同时缓存数据也会回滚,从而让数据库数据与缓存数据保持一致

2. 使用指定数据源

class ServicePost {
  async transactionManually() {
    const db = this.bean.database.getDb({ clientName: 'default' });
    await db.transaction.begin(async () => {
      const modelPost = this.scope.model.post.newInstance(db);
      const post = await modelPost.insert({ title: 'Post001' });
      await this.scope.cacheRedis.post.set(post, post.id, { db });
    });
  }
}  
  • 如果对指定的数据库进行操作,那么就需要将数据库对象db传入缓存,从而让缓存针对数据库对象db执行相应的补偿操作。当数据库事务回滚时,让数据库数据与缓存数据保持一致

【渲染流水线】[逐片元阶段]-[透明度测试]以UnityURP为例

透明度测试与透明度混合(Blend)互斥,需根据需求选择其一!

【从UnityURP开始探索游戏渲染】专栏-直达

透明度测试(Alpha Test)是一种通过阈值筛选片元的硬性透明方案,其核心逻辑是通过clip()函数比较片元Alpha值与预设阈值_Cutoff:当Alpha低于阈值时直接丢弃片元,反之则完整渲染。

  • 二元判定:呈现"全有或全无"的视觉效果,适用于栅栏、树叶等需要锐利边缘的物体
  • 性能优势:无需混合计算,在移动端和低配设备上表现优异

核心应用场景

  • 游戏美术优化
    • 植被系统:实现树叶镂空效果(性能较Alpha Blend提升40%)
    • 建筑遮罩:快速创建栅栏、铁丝网等规则镂空结构
  • 特效制作
    • 粒子消融:配合噪声图实现燃烧/腐蚀动画
    • UI遮罩:用于异形界面元素裁剪
  • 技术美术方案
    • 低成本半透模拟:通过抖动阈值(dithering)实现伪半透效果
    • 遮挡剔除辅助:标记透明区域实现自定义遮挡逻辑

历史简述

Built-in管线阶段(2018年前)

  • 早期Unity内置管线通过Queue="AlphaTest"标签实现基础功能,但存在:
    • 着色器代码冗余,需手动处理光照模型兼容性
    • 与延迟渲染路径存在兼容性问题

URP标准化阶段(2019-2022)

  • URP 7.x版本引入关键改进:
    • Shader Graph集成:可视化配置Alpha Clip Threshold属性
    • 渲染队列优化:自动处理AlphaTest物体在3000-3500队列的排序逻辑
    • 跨平台一致性:在OpenGL ES 3.0与Metal API中实现相同裁剪行为

现代URP增强(2023-2025)

  • 最新URP 14+版本新增特性:
    • 多重裁剪通道:支持基于R/G/B通道的复合测试条件
    • GPU实例化支持:透明测试物体可参与实例化渲染批次
    • VFX Graph兼容:粒子系统可直接引用透明度测试材质

如果启用了AlphaTest 那么Early-Z则被弃用

  • 因为AlphaTest的clip()操作是在片元中,而Early-Z是在光栅化和片元之前做的测试,如果在片元着色中主动抛弃了片元,那么被遮挡的片元就可见了,但是Early-Z中已经因为提前计算不可见给重新可见的片元丢弃了,这样就无法正常进行计算。那么GPU在优化算法时,如果检查到片元着色器中存在抛弃片元和改写片元深度的操作时,放弃Early-Z的操作。
  • Early-Z由GPU自动实现,涉及两个pass,第一个Z-pre-pass,对于所有写入深度数据的物体先用一个超级简单的pass写入深度缓存。第二个pass关闭深度写入,开启深度测试,正常渲染流程渲染。

使用注意

  • AlphaTest 物体建议标记为 "Queue"="AlphaTest"(值2450),确保在透明物体前渲染‌
  • 使用 Always/Never 可完全绕过测试,减少GPU分支判断‌
  • 复杂比较函数(如 NotEqual)可能增加片元着色器指令数‌
  • 根据alpha值阈值决定片元保留/丢弃
  • 常用于植被、镂空材质等效果实现‌

Unity 中的透明度测试(AlphaTest)具体过程

SubShader 与 Pass 设置

  • 配置渲染通道(Pass)启用 AlphaTest,指定比较函数(如 Greater 或 Less)‌

比较函数列表‌

函数指令 逻辑条件 典型应用场景
Greater alpha > 阈值 (_Cutoff) 硬边遮罩(如树叶镂空)‌
GEqual alpha ≥ 阈值 半透明边缘抗锯齿优化‌
Less alpha < 阈值 反向遮罩(如洞孔效果)‌
LEqual alpha ≤ 阈值 透明渐变剔除‌
Equal alpha == 阈值 精确匹配特定透明度‌
NotEqual alpha != 阈值 排除特定透明值‌
Always 始终通过测试 强制禁用透明度测试‌
Never 始终丢弃片元 完全隐藏物体‌

‌配置示例‌

  • 在 SubShader 的 Pass 中声明比较函数与阈值:
SubShader {
    Pass {
        // 启用AlphaTest并设置比较模式
        AlphaTest Greater 0.5  // 仅保留alpha>0.5的片元
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        uniform float _Cutoff; // 阈值变量

        fixed4 frag (v2f i) : SV_Target {
            fixed4 col = tex2D(_MainTex, i.uv);
            clip(col.a - _Cutoff); // 等效于AlphaTest Greater
            return col;
        }
        ENDCG
    }
}

注:clip() 函数在片元着色器中可直接实现阈值比较,与 AlphaTest 指令效果等价‌

自定义shader中片元着色器中剔除或混合

Shader 属性定义

  • 在 Shader 代码中声明阈值属性(如 _Cutoff),用于控制 alpha 的临界值‌

片元着色器处理

  • 在片元着色器中,计算片元 alpha 值并与 _Cutoff 比较;若未达标,则使用 clip() 或 discard 指令丢弃片元,阻止其进入后续测试‌

渲染输出

  • 通过测试的片元参与颜色混合等后续流程,否则被剔除‌

【从UnityURP开始探索游戏渲染】专栏-直达

(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)

本文由博客一文多发平台 OpenWrite 发布!

巧用 CSS 伪元素,让背景图自适应保持比例

在前端页面开发中,经常会遇到需要背景图自适应容器且保持原始比例的需求。今天分享一个实用技巧,利用 CSS 伪元素结合 padding-top 来实现,以 .section 为例详细说明。

一、需求场景与初始设置

(一)基础布局

.section 容器需要设置背景图,要求宽度铺满(width: 100% ),高度根据背景图原始比例自适应,同时背景图顶部居中对齐(background-position: top center ) ,并且要隐藏溢出内容(overflow: hidden ),初始结构如下:

css

.section {
  position: relative;
  width: 100%; 
  background-image: url(./bg.png);
  background-repeat: no-repeat;
  background-size: 100% auto; 
  background-position: top center; 
  overflow: hidden;
}

这里移除了固定高度,因为要让高度随背景图比例动态变化。

二、关键实现:伪元素维持比例

(一)原理说明

利用伪元素 ::before 的 padding-top 属性来维持比例。padding-top 的百分比值是相对于父元素宽度计算的,通过  (图片原始高度 ÷ 图片原始宽度)× 100%  这个公式,就能让伪元素的高度和背景图原始比例对应,进而撑起父容器 .section 的高度,让背景图完整且按比例展示。

(二)代码实现

css

.section::before {
  content: "";
  display: block;
  /* 计算方式:(图片原始高度 ÷ 图片原始宽度) × 100%
     示例中图片 1920px(宽) × 2863px(高),则 2863 ÷ 1920 × 100% ≈ 149.11% */
  padding-top: 149.11%; 
}

比如示例里背景图实际尺寸是 1920px(宽)× 2863px(高) ,代入公式计算得到 padding-top 的值约为 149.11% ,替换为你项目中背景图的实际比例值即可。这样设置后,.section 容器的高度就会根据宽度和背景图比例自动调整,背景图也能完美适配,不会出现拉伸变形导致比例失调的问题。

三、完整代码整合

把上述代码整合起来,完整的 CSS 样式如下:

css

.section {
  position: relative;
  width: 100%; 
  background-image: url(./bg.png);
  background-repeat: no-repeat;
  background-size: 100% auto; 
  background-position: top center; 
  overflow: hidden;
}

.section::before {
  content: "";
  display: block;
  padding-top: 149.11%; 
}

通过这种方式,就能轻松实现背景图自适应容器且保持原始比例的效果,在处理 banner 图、产品介绍模块等需要背景图比例精准控制的场景中非常实用,让页面布局更灵活、美观,快去试试吧!

Nuxt3 功能篇

Nuxt3 功能篇

1、加载效果

🍎添加单页面加载效果

单页面进行使用

<div v-show="isLoading" class="preloader fixed inset-0 bg-white text-primary flex justify-center items-center z-50">
  <NuxtImg src="/images/preloader.gif" alt="w-20" />
</div>


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

// 响应式状态
const isLoading = ref(true)

// 预加载器
onMounted(() => {
  // 模拟预加载器消失
  setTimeout(() => {
    isLoading.value = false
  }, 500)
})
</script>

🍎全局加载效果

app\layouts\default.vue放到这里

<!-- layouts/default.vue -->
<template>
  <div>
    <!-- <Header/> -->
    <!-- 页面主体内容 -->
    <GlobalLoading/>
    <NuxtPage/>
    <!-- <Footer /> -->
  </div>
</template>
<script setup>
</script>
<style scoped>
</style>

加载组件GlobalLoading.vue

<!-- 预加载器 -->
<template>
  <!-- preloader -->
  <div v-if="isLoading" class="preloader fixed inset-0 bg-white text-primary flex justify-center items-center z-50">
    <NuxtImg src="/images/preloader.gif" alt="w-20" />
  </div>
  <!-- preloader end -->
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
const isLoading = ref(true)
onMounted(() => {
  setTimeout(() => {
    isLoading.value = false
  },100)
})
</script>
<style scoped></style>

2、返回顶部

<!-- back to top -->
<button id="back-to-top" ref="backToTop" @click="scrollToTop" class="fixed bottom-10 right-5 z-10 h-12 w-12 bg-primary rounded-full flex items-center justify-center text-white transition-all duration-300 invisible">
  <i class="las la-arrow-up"></i>
</button>


const backToTop = ref(null) //返回顶部

// 返回顶部
const scrollToTop = () => {
  if (window.pageYOffset < 50) {
    backToTop.value.classList.add("invisible")
  } else {
    backToTop.value.classList.remove("invisible")
  }
  window.scrollTo({
    top: 0,
    behavior: 'smooth'
  })
}

3、添加主题设置面板

主题设置面板能帮助我们设置想要的主题和颜色

<!-- Theme settings -->
<div ref="settings"  class="fixed top-1/2 -translate-y-1/2 -right-60 z-20 flex gap-4 transition-all duration-300 settings-wrapper" id="settings">
  <button @click="toggleSettings" type="button" id="settings_toggler" class="h-10 w-10 rounded-md text-white shrink-0 bg-primary flex items-center justify-center">
    <i class="las la-cog text-white text-2xl animate-spin" style="animation-duration: 2.5s"></i>
  </button>
  <div class="bg-white rounded-lg p-4 shadow-md w-60 space-y-2">
    <h2 class="text-lg font-semibold text-gray-700">Theme Color</h2>

    <div class="space-y-1">
      <h4 class="text-base font-normal text-gray-500">Primary Color</h4>
      <ul class="flex gap-4">
        <button data-color="79 70 229" class="h-8 w-8 rounded-full bg-[rgb(79,70,229)] block change_prime_color text-white">
          <i class="las la-check invisible"></i>
        </button>
        <li>
          <button data-color="52 152 219" class="h-8 w-8 rounded-full bg-[rgb(52,152,219)] block change_prime_color text-white">
            <i class="las la-check invisible"></i>
          </button>
        </li>
        <li>
          <button data-color="26 188 156" class="h-8 w-8 rounded-full bg-[rgb(26,188,156)] block change_prime_color text-white">
            <i class="las la-check invisible"></i>
          </button>
        </li>
        <li>
          <button data-color="247 159 31" class="h-8 w-8 rounded-full bg-[rgb(247,159,31)] block change_prime_color text-white">
            <i class="las la-check invisible"></i>
          </button>
        </li>
      </ul>
    </div>

    <div class="space-y-1">
      <h4 class="text-base font-normal text-gray-500">Secondary Color</h4>
      <ul class="flex gap-4">
        <li>
          <button data-color="24 24 27" class="h-8 w-8 rounded-full bg-[rgb(24,24,27)] block change_secon_color text-white">
            <i class="las la-check invisible"></i>
          </button>
        </li>
        <li>
          <button data-color="30 41 59" class="h-8 w-8 rounded-full bg-[rgb(30,41,59)] block change_secon_color text-white">
            <i class="las la-check invisible"></i>
          </button>
        </li>
        <li>
          <button data-color="55 65 81" class="h-8 w-8 rounded-full bg-[rgb(55,65,81)] block change_secon_color text-white">
            <i class="las la-check invisible"></i>
          </button>
        </li>
        <li>
          <button data-color="68 64 60" class="h-8 w-8 rounded-full bg-[rgb(68,64,60)] block change_secon_color text-white">
            <i class="las la-check invisible"></i>
          </button>
        </li>
      </ul>
    </div>
  </div>
</div>
<!-- Theme settings end -->

样式写好以后调整一我们的按钮事件

const settings = ref(null)
// 设置面板切换
const toggleSettings = (e) => {
e.stopPropagation();
console.log("toggle",settings.value);
  if (settings.value.classList.contains("right-4")) {
    settings.value.classList.replace("right-4", "-right-60")
  } else {
    settings.value.classList.replace("-right-60", "right-4")
  }
}

现在我们的面板就可以点开了,接下来只需要点击颜色以后更新我们本地存储的颜色即可


// 颜色主题
const activePrimaryColor= ref(null) // 激活的颜色
const activeSecondaryColor = ref(null) // 激活的颜色


const primaryColors= ref([
{ name: 'Indigo', value: '79,70,229',numvalue:'79 70 229'},
{ name: 'Blue', value: '52,152,219',numvalue:'52 152 219'},
{ name: 'Teal', value: '26,188,156',numvalue:'26 188 156'},
{ name: 'Orange', value: '247,159,31',numvalue:'247 159 31'}
])
const secondaryColors = ref([
  { name: 'Black', value: '24,24,27', numvalue: '24 24 27' },
  { name: 'Dark Blue', value: '30,41,59', numvalue: '30 41 59' },
  { name: 'Gray', value: '55,65,81', numvalue: '55 65 81' },
  { name: 'Brown', value: '68,64,60', numvalue: '68 64 60' }
])


// 主要颜色切换
const changePrimaryColor = (item) => {
console.log(item,'changePrimaryColor');

// 更新活动颜色
  activePrimaryColor.value = item.numvalue;

  console.log(activePrimaryColor.value,'activePrimaryColor');

let lsName="nefte_primary_color";
root_theme.value.style.setProperty(`--color-primary`,activePrimaryColor.value)
  localStorage.setItem(lsName, activePrimaryColor.value)

toggleSettings(); // 关闭设置栏
}

// 次要颜色切换
const changeSecondaryColor = (item) => {
  activeSecondaryColor.value = item.numvalue; // 更新活动颜色
let lsName="nefte_secondary_color";
root_theme.value.style.setProperty(`--color-secondary`,activeSecondaryColor.value)
  localStorage.setItem(lsName, activeSecondaryColor.value)
toggleSettings(); // 关闭设置栏
}

// 设置面板切换
const toggleSettings = (e) => {
// e.stopPropagation();
// console.log("toggleSettings",settings.value);
  if (settings.value.classList.contains("right-4")) {
    settings.value.classList.replace("right-4", "-right-60")
  } else {
    settings.value.classList.replace("-right-60", "right-4")
  }
}

ok ,尝试一下,我们的颜色切换主题ok

4、发送邮件

🍎配置文件.env

这里我们以自己的qq为例,可以看看之前的邮件文章

.env

EMAIL_USER=xxx@qq.com
EMAIL_PASS=xxx
EMAIL_HOST=smtp.qq.com
EMAIL_PORT=587

🍎接口server\api\send-email.post.ts 配置

这里我们写一下我们接口,前端传过来的部分,其实最主要的就是其中的email字段

{
    "name": "林太白",
    "email": "xxx@qq.com",
    "phone": "xxx",
    "message": "内容信息"
}
// server/api/send-email.post.ts
import nodemailer from 'nodemailer'

// 创建邮件传输器
const transporter = nodemailer.createTransport({
  host: process.env.EMAIL_HOST,
  port: parseInt(process.env.EMAIL_PORT || '465'),
  secure: false, // true for 465, false for other ports
  auth: {
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASS,
  },
})

export default defineEventHandler(async (event) => {
  console.log('发送邮件');// 发送邮件
  try {
    // 读取请求体
    const body = await readBody(event)
    
    // 基本验证
    if (!body.email || !body.name ||!body.phone || (!body.text && !body.message)) {
      throw createError({
        statusCode: 400,
        statusMessage: '缺少必要字段邮箱、姓名、电话、内容',
      })
    }
    // 准备邮件选项
    const mailOptions = {
      from: `${body.name}<${process.env.EMAIL_USER}>`, // 发件人
      to: body.email,
      subject: body.name,
      text: body.message,
      html: body.message,
      // 添加附件(如果有)
      attachments: body.attachments || [],
    }
    
    // 发送邮件
    const info = await transporter.sendMail(mailOptions)
    
    // 返回成功响应
    return {
      success: true,
      code: 200,
      msg: '邮件发送成功',
      // data: {
      //   messageId: info.messageId,
      //   response: info.response,
      // },
    }
    
  } catch (error) {
    console.error('邮件发送错误:', error)
    
    // 返回错误响应
    return {
      success: false,
      msg:  '邮件发送失败',
      statusCode:  500,
    }
  }
})

🍎前端调用

在页面上写好直接提交from即可

const submitForm = async () => {
console.log("Form submitted",form.value);
  try {
    // 调用 API
    const res = await $fetch('/api/send-email', {
      method: 'POST',
      body: form.value
    })
console.log(res,'res');
    if(res.code == 200) {}else{}
  } catch (error) {

} finally {
    
  }
}

5、拨号功能

方式一

<a :href="'tel:' + 110" rel="external nofollow">
    拨打电话(123) 123-456
</a>

方式二

方法二
<a @click="callPhone" rel="external nofollow">拨打电话</a>
callPhone(){
      window.location.href = 'tel://110'
},

报错处理

🍎'allow-scripts' permission is not set

Blocked script execution in '<URL>' because the document's 
frame is sandboxed and the 'allow-scripts' permission is not set.

这里我们使用的代码如下

<iframe class="w-full h-96" 
  src="https://www.amap.com/" sandbox="" allowfullscreen="" loading="lazy">
</iframe>

🍎多跟节点报错

Transition renders non-element root node that cannot be animated.

runtime-core.esm-bun…er.js?v=3bb06854:50 [Vue warn]: Component inside 
<Transition> renders non-element root node that cannot be animated.
这个Vue警告提示你:
  在<Transition>组件内渲染了一个非元素根节点,
  这会导致动画无法正常工作。

错误原因:
<Transition>组件只能用来包裹单个元素节点,而你尝试在其中渲染了多个节点或非元素节点
(如文本节点、注释节点或多个元素节点)。

🍎处理方式,在提示的部分,最外层使用div包裹

<div>
  内容
  xxx 
</div>

🍎造成这种现象的复盘

产生这个现象的过程

点击这个没有跟的页面,再点击其他页面的时候,首次是空白的
HeaderSection.vue 之中

<header class="header fixed w-full left-0 top-0 shadow z-10"></header>


about页面之中
<template>
    <ThemeSetting />
<HeaderSection/>
<div>我是身体</div>
    <FooterSection />
</template>
 

06.TypeScript 元组类型、枚举类型

06.TypeScript 元组类型、枚举类型

1. 元组类型

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。元组(Tuple)是固定数量的不同类型的元素的组合。

1.1 实例

// 声明一个元组类型
let x: [string, number];

// 初始化
x = ['hello', 10]; // 正确

// 访问已知索引的元素
console.log(x[0].substring(1)); // 输出: ello
console.log(x[1].toString()); // 输出: 10

1.2 编译后效果

输入图片说明

1.3 输出结果

输入图片说明

1.4 越界的元素他的类型被限制为联合类型

输入图片说明

1.5 元组数量固定

输入图片说明输入图片说明输入图片说明

1.6 可以使用 push,但是会报错提示

输入图片说明输入图片说明输入图片说明输入图片说明

1.7 总结

  1. 元组数量固定;
  2. 元组越界元素被限制在元组内元素的联合类型;
  3. 可以使用 push 一类的方法,但是不建议使用,直接访问元素的时候,编译依然会报错。

2. 枚举类型

枚举类型是对 JavaScript 标准数据类型的一个补充,用于定义数值集合。

2.1 数字枚举

2.1.1 代码
// 不指定值时,从 0 开始递增
enum Direction2 {
  Up,
  Down,
  Left,
  Right
}

console.log(Direction2.Up); // 输出: 0
console.log(Direction2.Down); // 输出: 1
console.log(Direction2.Left); // 输出: 2
console.log(Direction2.Right); // 输出: 3

enum Direction {
  Up = 1,
  Down,
  Left,
  Right
}

// 使用枚举
console.log(Direction2.Up); // 输出: 1
console.log(Direction2.Down); // 输出: 2
console.log(Direction2.Left); // 输出: 3
console.log(Direction2.Right); // 输出: 4
2.1.2 编译后效果

输入图片说明

2.1.3 输出结果

输入图片说明

2.2 字符串枚举

2.2.2 代码
enum DirectionStr {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT"
}

console.log(DirectionStr.Up); // 输出: UP
console.log(DirectionStr.Down); // 输出: DOWN
console.log(DirectionStr.Left); // 输出: LEFT
console.log(DirectionStr.Right); // 输出: RIGHT

// 字符串枚举可以很好地序列化
console.log(DirectionStr.Up === "UP"); // 输出: true
2.2.2 编译后效果

输入图片说明

2.2.3 输出结果

输入图片说明

2.3 异构枚举

// 枚举可以混合字符串和数字成员
enum BooleanLikeHeterogeneousEnum {
  No = 0,
  Yes = "YES",
}

console.log(BooleanLikeHeterogeneousEnum.No); // 输出: 0
console.log(BooleanLikeHeterogeneousEnum.Yes); // 输出: YES
2.3.2 编译后效果

输入图片说明

2.3.3 输出结果

输入图片说明

2.4 接口枚举

2.4.1 代码
// 使用接口定义枚举
enum EnumTypes {
  A = 'A',
  B = 'B'
}

interface TypesInterface {
  A: EnumTypes.A
  B: EnumTypes.B
}

// 为避免变量名重复,修改变量名为 newEnumConst
const newEnumConst: TypesInterface = {
  A: EnumTypes.A,
  B: EnumTypes.B
}

console.log('接口枚举', newEnumConst.A) // 'A'
console.log('接口枚举', newEnumConst.B) // 'B'
2.4.2 编译后效果

输入图片说明

2.4.3 输出结果

输入图片说明

2.5 常量枚举

2.5.1 实现代码
// 使用 const enum 定义常量枚举,会在编译阶段被删除,提升性能
// 常量枚举
const enum DirectionConst {
  Up,
  Down,
  Left,
  Right
}

let directions = [DirectionConst.Up, DirectionConst.Down, DirectionConst.Left, DirectionConst.Right]
console.log('常量枚举', directions) // [0, 1, 2, 3]

// 普通枚举
enum DirectionConstCommon {
  Up,
  Down,
  Left,
  Right
}

let directionsCommon = [
  DirectionConstCommon.Up,
  DirectionConstCommon.Down,
  DirectionConstCommon.Left,
  DirectionConstCommon.Right
]
console.log('普通枚举', directionsCommon) // [0, 1, 2, 3]
2.5.2 编译后效果

输入图片说明

2.5.3 输出结果

输入图片说明

2.5.4 总结
  1. 使用 const enum 定义常量枚举,会在编译阶段被删除,提升性能!,使用和普通枚举一样,只是编译后的结果不同。
  2. let 和 var 都是不允许的声明,只能使用 const。
  3. const 声明的枚举会被编译成常量。
  4. 普通声明的枚举编译完后是个对象。

2.6 反向映射

2.6.1 代码
// 数字枚举成员具有反向映射
enum Enum1 {
  A
}

let a = Enum1.A
let nameOfA = Enum1[a]
console.log('反向映射', nameOfA) // 'A'
console.log('反向映射', a) // 0
2.6.2 数字枚举成员具有反向映射编译效果

输入图片说明

2.6.3 输出结果

输入图片说明

2.6.4 字符串枚举成员不具有反向映射编译效果

输入图片说明

3. 总结

  1. 元组类型表示一个已知元素数量和类型的数组,各元素的类型不必相同;
  2. 枚举类型用于定义数值集合,包括数字枚举、增长枚举、字符串枚举、异构枚举、接口枚举、常量枚举和反向映射;
  3. 数字枚举默认从 0 开始递增,字符串枚举可以很好地序列化;
  4. 增长枚举允许按需定义枚举值;
  5. 接口枚举提供了一种定义枚举的替代方式;
  6. 常量枚举在编译阶段会被删除,有助于提升性能;
  7. 数字枚举支持反向映射,可以通过值获取枚举成员的名称;
  8. 每种枚举类型都有其适用场景,选择合适的枚举类型可以提高代码的可读性和维护性。

图片大图预览就该这样做

在管理系统开发过程中,我们不可避免需要预览一些上传的图片。小编在开发过程中就遇到需要实现在列表上点击“查看”按钮预览该条记录上的所有图片。

项目所用技术栈:vue、element ui

我们都知道el-image组件可以通过previewSrcList 开启预览大图的功能,那么是否可以基于该组件实现预览功能呢?当然是可以的。

下面就是小编开发的图片预览组件,其原理就是通过手动触发el-image的预览大图功能

<template>
  <!-- 图片预览 -->
  <el-image
    id="previewImage"
    style="
      opacity: 0;
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
    "
    :src="logo"
    :preview-src-list="imagesList"
    fit="fill"
    @click.stop="handleClick"
  />
</template>
<script lang="ts">
import Vue from 'vue'
// 给个默认的图片地址,使el-image相关的dom元素都能存在
import logo from '@/assets/images/logo.png'
import { Loading } from 'element-ui'
import { showPic } from '@/api/common'

export default Vue.extend({
  name: 'SeeImages',
  props: ['imagesDialogVisible', 'imageIdList'],
  data() {
    return { logo, imagesList: [], loadingInstance: null }
  },
  created() {
    // 根据图片id调用接口获取到图片的url
    this.loadingInstance = Loading.service({ fullscreen: true })
    const promiseArr = []
    for (const i of this.imageIdList) {
      // thumbnailType缩略图类型0-非缩略图 1-缩略图
      promiseArr.push(showPic({ id: i, thumbnailType: 0 }))
    }
    Promise.all(promiseArr)
      .then((res) => {
        const images = []
        res.forEach((item) => {
          images.push(
            'data:image/png;base64,' +
              btoa(
                new Uint8Array(item.data).reduce(
                  (data, byte) => data + String.fromCharCode(byte),
                  ''
                )
              )
          )
        })
        this.imagesList = images
        this.$nextTick(() => {
          // 触发el-image的预览功能
          const element = document.querySelectorAll(
            '#previewImage'
          )[0] as HTMLElement
          if (element) {
            element.click()
          }
        })
      })
      .finally(() => {
        this.loadingInstance.close()
      })
  },
  methods: {
    handleClick() {
      // 关闭预览的时候移除组件
      const that = this
      setTimeout(function () {
        const domImageMask = document.querySelector('.el-image-viewer__wrapper')
        if (!domImageMask) {
          return
        }
        domImageMask.addEventListener('click', (e) => {
          if (
            [
              'el-image-viewer__btn el-image-viewer__prev',
              'el-image-viewer__btn el-image-viewer__next',
              'el-image-viewer__btn el-image-viewer__actions',
              'el-image-viewer__actions__inner',
              'el-image-viewer__canvas'
            ].includes((e.target as any).parentNode?.className || '')
          ) {
            return
          }
          that.$emit('update:imagesDialogVisible', false)
        })
      }, 300)
    }
  }
})
</script>

学Python必须迈过的一道坎:类和对象到底是什么鬼?

很多同学一听到 “类、对象、实例、实例化、OOP(面向对象编程)” 这些词,脑袋里瞬间黑屏,觉得像是掉进了程序员的玄学世界。 别慌,其实这些概念并没有那么高冷,如果你能听懂“买车”和“养狗”的故事,那类和对象也就一清二楚了。

今天我们就来用最接地气的方式,讲清楚 Python 的类和对象——这可是写大型程序的必修课。 保证你看完这篇文章,能拍着胸脯说一句:“原来OOP就是这么回事啊!”

ChatGPT_Image_2025年8月21日_11_04_17.png


一、为什么要学类和对象?

学过一阵 Python 的人,可能已经能写一些小程序: 比如做个计算器、搞个猜数字游戏、写个小爬虫。

但是当项目越来越复杂,比如写一个小游戏,里面要有“角色、怪物、武器、地图”,再比如你想写一个电商网站,里面要有“用户、商品、购物车、订单”…… 这时候,如果你还在用函数和变量一股脑往里塞,代码很快就会变成一锅大杂烩,谁也看不懂。

这就好比造房子,你不可能靠一堆砖和水泥随便堆,要有设计图纸、要有结构。 类(Class)就是这份图纸,对象(Object)就是按照图纸造出来的房子。

所以说,类和对象的出现,就是为了让我们能够管理复杂性,像搭积木一样组织代码。


二、类是什么?对象又是什么?

打个比方:

  • 类(Class) :就像汽车的设计图纸,告诉你一辆车该有几个轮子、方向盘怎么装、引擎放哪。
  • 对象(Object) :就是根据设计图纸造出来的一辆辆具体的车。

用 Python 写出来就是:

# 汽车的图纸(类)
class Car:
    pass

# 按照图纸造车(对象)
my_car = Car()
your_car = Car()

这里 Car 是类,my_caryour_car 是对象。

同一个类可以造出无数对象,就像世界上有成千上万辆不同颜色、不同配置的汽车,但它们都有一个共同的蓝本。


三、类的基本骨架:名字、属性、方法

一个完整的类,通常会包含:

  • 类名(class Dog:)
  • 构造函数__init__)——造对象时自动执行
  • 属性(比如名字、年龄、品种)
  • 方法(对象能干的事,比如吃饭、叫、跑)

举个例子:

class Dog:
    # 构造函数:创建对象时自动执行
    def __init__(self, name, breed):
        self.name = name    # 属性:名字
        self.breed = breed  # 属性:品种
    
    # 方法:狗的行为
    def bark(self):
        print(f"{self.name}:汪汪汪!")
    
    def eat(self, food):
        print(f"{self.name}正在吃{food}")

这里的 Dog 类就是“狗的设计图”:

  • 它规定了每只狗都要有 名字品种
  • 它还规定了狗能 、能 吃东西

于是我们就能根据这个图纸造狗:

dog1 = Dog("大黄""金毛")
dog2 = Dog("小黑""哈士奇")

dog1.bark()      # 输出:大黄:汪汪汪!
dog2.eat("骨头") # 输出:小黑正在吃骨头

这时候,dog1dog2 就是对象,也叫实例。


四、对象 vs 实例:傻傻分不清?

很多同学第一次学类的时候,常常纠结:“对象和实例到底有什么区别?”

其实答案很简单:

  • 对象(Object) :强调它是个具体的东西。
  • 实例(Instance) :强调它是从某个类派生出来的例子。

说白了,在 Python 的语境下,这两个词 几乎是同义词

当你写下:

dog1 = Dog("大黄""金毛")

这一行代码就完成了两件事:

  1. 造了一个对象(内存里有个实体);
  2. 这个对象是 Dog 类的一个实例。

所以以后再遇到这两个词,别慌,记住:对象 = 实例


五、“实例化”到底是个啥?

“实例化”这个词听起来有点高大上,其实意思就是: 按照类的蓝图,造一个对象的过程。

比如:

dog1 = Dog("大黄""金毛")

这一行代码就叫“实例化”:

  • Python 调用了 Dog 这个类;
  • 自动执行 __init__ 构造函数;
  • 给对象分配内存;
  • 设置好 namebreed 属性。

最终,dog1 就诞生了。

所以别再把“实例化”当成一个玄学词,它就是“创建对象”的过程。


六、OOP:面向对象编程是怎么回事?

到这里你已经能理解类和对象了,但为什么我们要折腾出这么一套“面向对象编程(OOP)”?

答案只有两个字:复杂性

想象一下,你要开发一个 RPG 游戏:

  • 如果用面向过程的老方法,你可能写一堆函数:移动角色()、攻击怪物()、计算伤害()、掉落装备()…… 数据和逻辑全都混在一起,代码像一盘炒面,越写越乱。

  • 但如果用面向对象:

    • 有“角色对象”,有生命值、位置、背包,还能移动、攻击、使用道具;
    • 有“怪物对象”,有攻击力、防御力,还能巡逻、追击;
    • 有“武器对象”,有伤害值、耐久度,可以被角色使用;

角色.攻击(怪物) 的时候,其实就是角色对象和怪物对象在对话: “我要打你了!”——角色说。 “好,我掉血了。”——怪物说。

是不是很直观? 这就是 OOP 的精髓:让程序像现实世界一样,把一切都抽象成对象,让对象之间去交互。


七、常见的学习误区

  1. 把类当函数用 很多新手一开始写类,总觉得它就是一个“大函数”,其实类是“图纸”,方法才是它的行为。
  2. 写了一堆属性,没有行为 有的同学写 Dog 类,里面全是属性 name、age、breed,结果发现和字典没啥区别。 记住:类最重要的就是“数据 + 行为”的结合。
  3. 分不清类变量和实例变量 这块我先挖个坑,后面文章再展开。先记住一句:大多数情况下,属性要写在 __init__ 里。

八、总结一句话

类和对象听起来很抽象,但记住一个公式就行:

类 = 图纸,对象 = 产品,实例化 = 按图造物。

Python 的 OOP 就是让你用这种方式组织代码,把复杂的系统拆成一个个能互动的对象。

今天我们先理解了类、对象、实例、实例化这些核心概念,后面我们还会深入讲:

  • 类变量 vs 实例变量
  • 继承与多态
  • 魔法方法(__str____len__
  • 封装与抽象

学懂这几招,你就能真正迈入“写出大型项目”的门槛。


思维金句(帮你记忆)

  • 类是图纸,对象是成品。
  • 对象 = 实例,这俩就是一个东西。
  • 实例化 = 按照图纸造东西。
  • OOP 不是玄学,而是让代码更像现实世界。

参考资料


如何在 web 应用中使用 GDAL (二)

上一篇已经把编译搞定了,这一篇来看看怎么用。

WebAssembly 基本用法

实例化 wasm

WebAssembly 名字带 assembly ,确实很像汇编语言,它位于中间表达和机器码之间。跟使用其他 JavaScript 库不同, WebAssembly 并不能像 esmodule 那样通过 import 指令将代码加载到线程中,也不能使用 <script> 加载,因为它并不是 JavaScript 。

Image description 配图来自 Creating and working with WebAssembly modules,非常好的文章,使我大脑旋转

浏览器提供了一套完整的 WebAssembly 接口用于加载代码,也不复杂,假设我们有一个 some.wasm 的文件,加载只需要如下几行代码:

fetch("some.wasm")
  .then((response) => response.arrayBuffer())
  .then((bytes) => WebAssembly.instantiate(bytes, options))
  .then(({instance}) => {
    // 假设导出了一个 some_func 函数
    instance.exports.some_func();
  });

所有的导出都会挂载在 instance.exports 上,通过查阅源码或者 wasm 作者提供的文档,我们就可以知道可以调用的接口有哪些,也可以知道接口参数是什么。

内存管理

JavaScript 开发者向来不太关心内存,仿佛有一个专门的管家在管理着内存。和 JavaScript 不同的是,WebAssembly 需要手动管理内存,才能正常地读写。WebAssembly 的内存是连续无类型的线性内存,犹如一个数组,有专门的指令进行读写,这与 C/C++ 指针如出一辙。内存在 JavaScript 中申请,加载的时候传入 wasm 的实例中:

const memory = new WebAssembly.Memory({ initial: 10, maximum: 100 });

fetch("some.wasm")
  .then((response) => response.arrayBuffer())
  .then((bytes) => WebAssembly.instantiate(bytes, {
    { memory: memory }
  }));

WebAssembly 和调用它的代码运行在同一个线程,和使用 transfer 的 worker 不同,WebAssembly 和 JavaScript 可以访问同一块内存。我们可以利用这一点,通过读写同一块内存进行数据传输。另外要注意的是,WebAssembly 只能访问由 JavaScript 申请并在实例化时传入的内存,访问不了其他位置的内存。

但 WebAssembly 实例化提供初始 memory 并不是必须的, wasm 也可以自己申请内存。使用 JavaScript 提供内存的好处在于数据共享和复制,在图形、音视频处理等场景下需要大量传输数据,如果通过函数参数传输,数据需要先经过序列化/反序列换操作,再深拷贝,效率远不及传输一个内存地址。此外,JavaScript 创建的内存可以提供给多个 wasm 模块使用,利用这个特性可以实现 wasm 不同模块间的协作。

表机制

除了向外暴露的接口以外,WebAssembly 还可能需要调用 JavaScript 的功能,比如果将 console.log 函数映射到 C 中作为标准输出,可以这么操作:

WebAssembly.instantiate(wasmBlob, {
  env: {
    js_callback: (value) => console.log(value), // 直接注入JS函数
  },
});

在 C 中运行这个 js_callback 便可以在控制台输出信息。但是这个方式有很大的风险,

  1. 可以基于 js_callback 篡改函数指针,泄露浏览器内部指针地址,随意执行未知代码
  2. 一旦绑定函数,就无法更改
  3. wasm 中并不知道函数是否被回收,如果某个绑定的函数已经被垃圾回收,wasm 调用将产生崩溃

WebAssembly 使用 来保证代码运行安全。将函数地址改为函数引用,不能直接读写指针,保障了浏览器安全。这里展示一个例子:

// C

// 定义与 console.log 匹配的函数签名
typedef void (*log_func_ptr)(const char* message);

void safe_log(const char* message) {
    // 获取全局日志函数索引(在JS中设置)
    extern uint32_t log_function_index;

    // 指针声明
    log_func_ptr log_ptr;

    // 调用
    log_ptr(message);
}
// JavaScript

function sanitizedConsoleLog(messagePtr) {
  // 边界检查 - 读取C字符串的长度限制
  const maxLen = 256;
  let length = 0;

  // 安全读取字符串
  while (length < maxLen) {
    const byte = wasmMemory.getUint8(messagePtr + length);
    if (byte === 0) break;
    length++;
  }

  // 提取安全范围内的字符串
  const messageBytes = new Uint8Array(
    wasmMemory.buffer,
    messagePtr,
    Math.min(length, maxLen)
  );

  // 转换为字符串
  const message = new TextDecoder("utf-8", { fatal: true }).decode(
    messageBytes
  );

  // 真正的调用
  console.log(message);
}

const table = new WebAssembly.Table({
  initial: 3,
  maximum: 10,
  element: "anyfunc", // 仅存储函数引用
});

let index = 0;
table.set(index, sanitizedConsoleLog); // table 索引 0 绑定到 sanitizedConsoleLog

// ...

const { instance } = await WebAssembly.instantiate(bytes, {
  env: {
    table: table,
    memory: wasmMemory,
    log_function_index: index, // log_func_ptr 指针指向 table 索引 0
  },
});

// ...

// 调用安全的日志函数
instance.exports.safe_log(messagePtr);

线程的管理

WebAssembly 代码执行时间长度无法预测,且 WebAssembly 和调用方同一个线程中运行,如果在 JavaScript 主线程上调用 wasm 接口,大概率阻塞 UI 线程。所以一般地,我们会启用一个 worker 来执行 WebAssembly ,避免卡死。

emscripten 胶水代码

这么看,WebAssembly 的使用还是比较复杂的,要搞清楚导出接口列表和参数,熟悉内存的使用,做好 JavaScript 函数的映射,除了要能用以外,还要保障安全。有没有办法简化这些操作呢?有的,朋友,有的。

还记得上一篇我们得到的编译产物里面有一个 .js 结尾的文件吗?这个文件就是所谓的胶水代码,胶水代码是 JavaScript 和 WebAssembly 之间的信使,简化了 WebAssembly 的操作流程,它提供这些功能:

  • 加载和初始化 WebAssembly 模块
  • 提供 JavaScript 和 WebAssembly 之间的接口
  • 实现 C/C++ 标准库的函数(如文件 I/O、内存管理)

胶水代码的输出是一个函数,接受一个注入对象作为参数,输出一个包含所有 wasm 导出功能对象。函数的声明为:

(moduleArg = {}): Module

实际上胶水代码会将输入的 moduleArg 注入到 Module 对象中,moduleArg 用于提供必要的 JavaScript 函数供 wasm 代码调用,比如指定 std::printconsole.log ,我们就可以在初始化时这样定义:

let moduleArg = {
  print: function (text: string) {
    console.log("stdout: " + text);
  },
};

胶水代码可以导出为立即执行函数,适合全局注入,注入的对象名称是 Module ;也可以导出为 esmodule、umd 等模块。导出的模式取决于编译参数的设置,全局注入的对象名称也可以通过参数设置,这一块将在编译优化篇章介绍。

WebAssembly 实例的导出函数全部都挂载到输出的 Module 上,通过 ccall 调用,或者使用 cwrap 转换成 JavaScript 函数。除此之外,根据编译参数 -s EXPORTED_RUNTIME_METHODS 配置挂载工具函数,比如虚拟文件系统 I/O ,内存管理接口等。

清楚了胶水代码的运行方式,那么如何使用 WebAssembly 也就呼之欲出了:

食用指南

小试牛刀

这里展示一个调用的例子,我们读取一个 tiff 文件,并将 tiff 的信息读取出来。文件目录:

├── CANYrelief1-geo.tif
├── gdal.worker.ts
├── gdal3WebAssembly.data
├── gdal3WebAssembly.js
├── gdal3WebAssembly.wasm
└── index.ts

index.ts 为入口,通过实例化一个 GdalWorker 开启线程,gdal3WebAssembly 开头的 3 个文件是通过编译得到的产物,如果忘了这部分内容可以去看看上一篇,全部的调用功能都在 gdal.worker.ts 文件里,代码如下:

import CModule from "./gdal3WebAssembly.js";
// 以资源地址的方式引入 wasm 文件
import wasm from "./gdal3WebAssembly.wasm?url";

// GDAL 对象,映射 GDAL 导出函数
let GDAL = {};

// 指向 emscripten 虚拟文件系统
let FS = {};
const SRCPATH = "/src";

let Module = {
  locateFile: () => wasm,
  onRuntimeInitialized() {
    // 注册 GDAL 全部驱动
    Module.ccall("GDALAllRegister", null, [], []);

    GDAL.GDALOpen = Module.cwrap("GDALOpen", "number", ["string"]);
    GDAL.GDALClose = Module.cwrap("GDALClose", "number", ["number"]);
    // 注册 gdalinfo 指令
    GDAL.GDALInfo = Module.cwrap("GDALInfo", "string", ["number", "number"]);

    // 挂载 FS 对象
    FS = Module.FS;
  },
};

/**
 * 初始化 Module
 */
function init() {
  return CModule(Module);
}

/**
 * 读取 tiff 文件信息
 * @param files tiff 文件
 */
function getTiffInfo(files: [File]) {
  // 创建工作目录
  FS.mkdir(SRCPATH);
  // 挂载 tiff 文件
  FS.mount(
    Module.WORKERFS,
    {
      files: files,
    },
    SRCPATH
  );

  // 打开文件,获得文件句柄
  const dataset = GDAL.GDALOpen(SRCPATH + "/" + files[0].name);
  // 读取信息
  const info = GDAL.GDALInfo(dataset);

  return info;
}

/**
 * 拉取资源
 */
function fetchtiff() {
  return fetch("/api/tiff/CANYrelief1-geo.tif").then((res) => res.blob());
}

self.onmessage = () => {
  fetchtiff().then((blob) => {
    console.log(blob);
    const file = new File([blob], "CANYrelief1-geo.tiff", {
      type: "image/tiff",
    });

    init().then(() => {
      const result = getTiffInfo([file]);
      console.log(result);
    });
  });
};

emscripten 在构建胶水代码的时候会将 wasm 文件的位置默认设置在和胶水代码同一个目录,如果我们更改文件位置,或者使用工程化工具管理代码的时候,胶水代码会找不到 wasm 文件,最终会加载失败。注意看第 13 行,我们在注入 Module 的时候提供一个 locateFile 函数,函数的返回值是 wasm 资源地址,这里的写法非常适合工程化。胶水代码在寻找 wasm 文件之前会调用这个函数,如果这个函数返回值有效,则会使用这个返回值作为 wasm 路径去加载文件。

当代码顺利读取到 wasm 文件,实例化成功之后,会执行 onRuntimeInitialized() 函数,此时 WebAssembly 代码已经悉数加载完毕,且 Module 的注入也已经完成,可以在这里执行初始化的代码。从第 15 行我们开始在 JavaScript 中挂载 GDAL ,第一步便是注册 GDAL 驱动,这部分可以参考 GDAL 的文档,示例代码仅仅展示一下 GDAL 的功能,所以只注册了 GDALOpenGDALCloseGDALInfo 三个函数,然后注入到 GDAL 对象中。最后注入 FS 对象。

注意,cwrap 只是将 C 函数封装成 JavaScript 函数,没有被 cwrap 封装的其他 C 函数也已经加载到内存里了。

第 39 行开始使用 GDAL 。gdalinfo 命令接受一个 <dataset_name> 参数,这个参数代表文件句柄。在这里我们使用虚拟文件系统挂载 tiff 文件,并用 GDALOpen 命令读取这个文件得到 dataset,最后作为参数调用 GDALInfo 。以下是输出信息:

Driver: GTiff/GeoTIFF
Files: /src/CANYrelief1-geo.tiff
Size is 2800, 2800
Coordinate System is:
ENGCRS["WGS 84 / Pseudo-Mercator",
    EDATUM["Unknown engineering datum"],
    CS[Cartesian,2],
        AXIS["(E)",east,
            ORDER[1],
            LENGTHUNIT["metre",1,
                ID["EPSG",9001]]],
        AXIS["(N)",north,
            ORDER[2],
            LENGTHUNIT["metre",1,
                ID["EPSG",9001]]]]
Data axis to CRS axis mapping: 1,2
Origin = (-12249462.599999999627471,4629559.794860946945846)
Pixel Size = (13.284000000000001,-13.285397060378999)
Metadata:
  AREA_OR_POINT=Area
  TIFFTAG_DATETIME=2017:04:01 20:24:57
  TIFFTAG_RESOLUTIONUNIT=2 (pixels/inch)
  TIFFTAG_SOFTWARE=Adobe Photoshop CC (Macintosh)
  TIFFTAG_XRESOLUTION=72
  TIFFTAG_YRESOLUTION=72
Image Structure Metadata:
  COMPRESSION=LZW
  INTERLEAVE=PIXEL
  PREDICTOR=2
Corner Coordinates:
Upper Left  (-12249462.600, 4629559.795)
Lower Left  (-12249462.600, 4592360.683)
Upper Right (-12212267.400, 4629559.795)
Lower Right (-12212267.400, 4592360.683)
Center      (-12230865.000, 4610960.239)
Band 1 Block=2800x31 Type=Byte, ColorInterp=Red
Band 2 Block=2800x31 Type=Byte, ColorInterp=Green
Band 3 Block=2800x31 Type=Byte, ColorInterp=Blue

结语

本篇介绍了如何 WebAssembly ,在后面的篇章,将会介绍

  1. 如何优化编译,使产物更小
  2. emscripten 胶水代码有哪些魔法

这些内容。

浏览器里出现 .angular/cache/19.2.6/abap_test/vite/deps 路径究竟说明了什么

概览——一句话看懂这条路径

Angular 17 及以上版本默认启用基于 Vite 与 esbuild 的新构建系统,它会把对依赖包的 预打包结果 存进工作区根目录的 .angular/cache,并按 CLI 版本号项目唯一标识构建器名称 等维度分层。vite/deps 子目录正是 Vite 的 optimize Deps 预打包输出,Chrome 通过 Source Map 把浏览器加载到的伪装 URL 映射回来,于是你便在 DevTools 里看到这条完整路径。Angular 开发服务器依靠这些缓存实现秒级冷启动、热更新与依赖变更检测。(Angular, vitejs, Angular Blog)


ℹ️ 路径逐段拆解

片段 物理/逻辑含义 关键要点
.angular Angular CLI 自 v13 起引入的工作区隐藏目录,用于 磁盘级构建缓存CLI 运行中间产物。(Medium, Stack Overflow)
cache CLI 缓存根目录,位置可通过 ng config cli.cache.path 修改。(Angular)
19.2.6 当前使用的 Angular CLI 版本号,用于区分不同版本的缓存,防止升级后因旧产物导致的不一致。(GitHub)
abap_test 工作区内 项目或构建目标的唯一标识。Angular 支持多项目/多目标缓存隔离,名称通常来自 angular.json 中的 name 字段或派生哈希。(Angular)
vite 指示本层缓存产物来源于 Vite 构建器(Angular 17 起为默认 application builder)。(Angular, Angular Love)
deps Vite 的 optimizeDeps/pre-bundle 输出目录,里面是 esbuild 打包后的单文件 ESM 依赖,后缀常见 *.mjschunk-*.js。(vitejs, Vite)

Angular CLI 的磁盘缓存体系

缓存为何出现

  • 自 v13 起,CLI 将编译、类型检查、代码生成等可复用步骤写入 .angular/cache,显著缩短重复构建的耗时。(Medium, GitHub)

  • 缓存按 命令、构建配置、依赖图哈希 等维度切分;每次变更仅命中必要片段。Git 官方示例甚至建议把该目录加入 .gitignore。(Stack Overflow)

19.2.6 层的意义

当你升级到 @angular/cli 19.2.6,CLI 会在缓存根目录下新建同名子文件夹;旧版本产物不会互相污染,便于并行维护多分支或回滚。(GitHub)


新构建系统与 Vite 的角色

Angular 17 将 Vite + esbuild 正式推为默认 ng serve/ng build 实现,官方称其为 application builder,带来平均 67 % 的冷启动提速。(Angular, Angular Blog)

Vite 在开发模式下会:

  1. 解析项目入口,收集外部依赖列表。

  2. 用 esbuild 将依赖 预打包cacheDir/deps(默认 node_modules/.vite/deps,Angular 则指向 .angular/cache/.../vite/deps)。(vitejs, Vite)

  3. 建立文件系统到虚拟模块的映射表,供浏览器通过 import / @id/... URL 访问;Source Map 将该 URL 再映射到原始 TypeScript。(DEV Community, vitejs)

如此一来:

  • 第一次运行:预打包耗时但被缓存。

  • 后续运行:若 package.jsonvite.config.* 或依赖文件指纹未变,则直接复用缓存,冷启动可达秒级。([GitHub](github.com/vitejs/vite… "`The file does not exist at "node_modules/.vite/deps/chunk*" which is ..."), DEV Community)


Chrome DevTools 为何展示这条路径

浏览器加载的是 Vite dev server 返回的虚拟路径,如 /@id/lodash。在 Source Map 反向查找、Angular 插件与 VS Code 调试协议的多重协作下,DevTools 把它标注成 真实磁盘路径,并在 UI 中折叠成 .angular/cache/.../vite/deps/lodash.js。这能够:

  • 让你直接在浏览器中设置断点调试预打包后的依赖。

  • 快速定位缓存失效或冲突场景,例如库作者本地调试时需手动 ng cache clean。([GitHub](github.com/vitejs/vite… "`The file does not exist at "node_modules/.vite/deps/chunk*" which is ..."), Stack Overflow)


案例研究:abap_test 项目中的调试场景

假设你有一个名为 abap_test 的 Angular 19 单页应用,启用了默认 Vite 构建器,且依赖 SAP UI5 SDK 的 EsModule 发行版。

  1. 第一次执行 ng serve

    • CLI 检测到 node_modules/@sap/ui5 等大块依赖。

    • Vite 将其与其内部深度导入一起打包进 .angular/cache/19.2.6/abap_test/vite/deps/ui5_sdk.mjs

  2. 在 Chrome 中打开 http://localhost:4200

    • Sources 面板下,你会看到与上述路径完全一致的虚拟文件。

    • 对 UI5 运行时做步进调试时,断点命中处就是预打包后的 ESM,让调试体验接近源码级细节。

若你临时修改了 @sap/ui5 源码,但版本号不变,为防止旧缓存被继续复用,可执行 ng cache clean 或直接删除 .angular/cache/19.2.6/abap_test。(Stack Overflow, [GitHub](github.com/vitejs/vite… "`The file does not exist at "node_modules/.vite/deps/chunk*" which is ..."))


日常开发中该路径带来的启示

  • 加速本地循环:理解缓存层级能帮助你在需要时精准清理,而非粗暴 rm -rf node_modules

  • CI/CD 优化:在 CI 服务器上持久化 .angular/cache 目录,可大幅减少构建时间;一旦升级 CLI,仅需保存新版本子目录即可。

  • 依赖调试:当你调试未发布的本地库或者链接包时,若发现代码无效,请优先怀疑 Vite deps 缓存并强制失效。

  • 跨版本隔离:多分支并行开发中,同时安装 18.x 与 19.x CLI 时不会相互污染,得益于版本层目录。

  • 空间管理:长期使用后 .angular/cache 体积可能失控,可结合 ng cache info 或定期清理策略避免磁盘占用。(GitHub, Medium)


结语

.angular/cache/19.2.6/abap_test/vite/deps 并非神秘乱码,而是 Angular CLI 版本化磁盘缓存 + Vite 依赖预打包 + 项目隔离 的全息快照。透彻理解这条路径,你就能更高效地控制缓存、加速构建、提升调试体验,甚至为 CI/CD 与多项目仓库制定更合理的缓存持久化策略。下一次当你在 DevTools 中遇到类似路径时,心中自会浮现其背后的缓存机制与构建流程。

C++之auto 关键字

1. auto 是什么?

一句话:
让编译器帮你看一眼“右边是什么类型”,然后自动把变量声明成那个类型。

auto x = 42;        // 右边是 int 字面量 -> x 的类型就是 int
auto y = 3.14;      // 右边是 double 字面量 -> y 就是 double
auto z = "hello";   // 右边是 const char* -> z 就是 const char*

写起来像动态语言(Python/JavaScript),但编译期就确定了具体类型,所以不会损失性能。


2. 基本语法

语法模板:

auto 变量名 = 初始值;     // 必须有初始值,否则编译器没法推断

注意:

  • 不能写成 auto x; —— 会报错!
  • 可以加上 const&* 等修饰符,写法如下:
const auto  a = 10;          // const int
auto&       b = a;           // 引用,类型是 const int&
auto*       p = &a;          // 指针,类型是 const int*

3. 常见场景逐个击破

3.1 迭代器——最常用

传统写法又长又臭:

std::map<std::string, std::vector<int>> m;
// 传统写法
for (std::map<std::string, std::vector<int>>::iterator it = m.begin();
     it != m.end(); ++it) { ... }

auto 写法:

for (auto it = m.begin(); it != m.end(); ++it) { ... }

再升级(C++11 range-for):

for (const auto& kv : m) {
    // kv.first  -> std::string
    // kv.second -> std::vector<int>
}

3.2 返回值类型很长

std::shared_ptr<std::unordered_map<int, std::string>>
createTable();   // 返回类型巨长

auto table = createTable();   // 一行搞定

3.3 泛型代码 / 模板

template<typename Container>
void printFirst(const Container& c) {
    auto it = c.begin();   // 不用关心 Container 是什么
    if (it != c.end())
        std::cout << *it;
}

4. 容易被坑的 3 个细节

4.1 忘记引用导致拷贝

std::vector<int> v = {1,2,3};
for (auto x : v)  x *= 2;        // 改的是拷贝,原 vector 不变
for (auto& x : v) x *= 2;        // OK,改的是原数据

4.2 类型推断与“顶层 const”被丢弃

const int ci = 100;
auto a = ci;   // a 是 int,顶层 const 被丢掉
auto& b = ci;  // b 是 const int&,保留底层 const

4.3 auto 不能用来推导数组

int arr[5];
auto a = arr;   // a 退化成 int*,不是 int[5]

5. C++14/C++17 加强补充(了解即可)

  • 返回值自动推导(C++14)

    auto add(int x, int y) { return x + y; }  // 编译器自己看 return 语句
    
  • 泛型 lambda(C++14)

    auto lambda = [](auto x, auto y) { return x + y; };
    
  • 结构化绑定(C++17)

    std::map<int, std::string> mp;
    for (const auto& [key, val] : mp) { ... }   // 直接解包 key 和 val
    

6. 一张思维导图(文字版)

auto
├─ 基本:auto x = 初始值;
├─ 修饰:const auto&, auto*, ...
├─ 场景
│  ├─ 迭代器
│  ├─ 返回值
│  ├─ 模板 / 泛型
│  └─ lambda
└─ 坑
   ├─ 忘引用
   ├─ const 被丢
   └─ 数组退化

7. 小结口诀(背下来)

“右边是啥我就啥,必须初始化;引用加 &,只读加 const,别忘范围 for 用 &!”

无虚拟dom怎么又流行起来了?

最近看到了许多这样的评论:

Screenshot_2025-03-14-00-01-29-03_9fe6e22b2196b45.jpg

Screenshot_2025-07-14-21-07-37-65_9fe6e22b2196b45.jpg

Screenshot_2025-04-13-13-41-57-52_d138286dbdf585e.jpg

Screenshot_2025-07-14-11-53-13-09_e39d2c7de19156b.jpg

Screenshot_2025-03-31-09-43-34-19_d138286dbdf585e.jpg

咱们来问问AI,让AI来解释一下为什么会出现这种现象。

ChatGPT

image.png

它甚至还贴心的问道:

image.png

就是有点太磨叽了:

SCR-20250820-oijd.png

SCR-20250820-oisw.png

最后给大家奉上它画的图:

ChatGPT Image 2025年8月20日 15_25_48.png

通义千问

有人可能会说 ChatGPT 中文支持到底行不行?毕竟它生成的中文:

SCR-20250820-okcq.png

SCR-20250820-okdi.png

SCR-20250820-okea.png

所以咱就换国产:

SCR-20250820-olms.png

SCR-20250820-oloj.png

SCR-20250820-olpx.png

DeepSeek

SCR-20250820-ondt.png

感觉都是与虚拟 DOM 的对比

咱们问的是为什么前端又流行起来了无虚拟dom?,重点是“”,也就是说早就有无虚拟 DOM 了,咱们想问的是无虚拟 DOM 明明被虚拟 DOM 淘汰了,但现在却又死灰复燃了的原因。但感觉这些个 AI 都只说虚拟 DOM 的缺点和 无虚拟 DOM 的优点,咱们重新再来问一下:

SCR-20250820-opdw.png

ChatGPT

SCR-20250820-opwl.png

SCR-20250820-oqgj.png

总结

根据各大 AI 给出的答案,咱们已经可以得出无虚拟 DOM 怎么又流行起来了的结论了:

就是早期的无虚拟 DOM 和现在的无虚拟 DOM 压根儿就不是一回事儿,以前那是纯纯的手动操作 DOM,写过 jQuery 的应该都知道:

$('.xxx').class({ xx: xx }).child(1).xxxxxxxxxxx

而现在的无虚拟 DOM 则根本不用自己操作 DOM,跟虚拟 DOM 时代的写法没什么两样:

<template>
  <div :class="{ red }">{{ text }}</div>
</template>

<script setup vapor>
import { ref } from 'vue'

const red = ref(true)
const text = ref('')
</script>

<style scoped>
.red {
  color: red;
}
</style>

也就是相当于结合了以前手动 DOM 的性能优势(当然大部分人写的时候都没考虑过性能,所以才会被虚拟 DOM 所超越,但要是好好写的话还是可以超过虚拟 DOM 的性能的,不过可维护性就差点意思了)以及虚拟 DOM 的写法优势,这才是现在的无虚拟 DOM

手动操作 DOM 的性能 + 虚拟 DOM 的写法 = 现在的无虚拟 DOM

往期精彩文章

webSocket Manager

WebSocket 连接管理器,用于统一管理多个 WebSocket 连接。这个管理器特别适用于需要同时维护多个 WebSocket 连接的复杂应用。

该连接管理器请配合,封装好的 uniapp websocket封装 使用。

import WS from '@/utils/websocket.js'

class WebSocketManager {
  constructor() {
    this.connections = new Map()
  }

  /**
   * 建立或复用 WebSocket 连接
   * @param {string} url WebSocket 地址
   * @param {Function} onMessage 收到消息回调
   */
  connect(url, onMessage) {
    if (this.connections.has(url)) {
      console.log(`复用已有连接: ${url}`)
      return this.connections.get(url)
    }

    console.log(`新建连接: ${url}`)
    const ws = new WS({
      url,
      onConnected: () => {},
      onMessage: (data) => {
        if (onMessage) {
          onMessage(data)
        }
      },
    })

    this.connections.set(url, ws)

    // 自动清理无效连接
    const cleanup = () => {
      if (ws && ws.socketTask) {
        ws.socketTask.onClose(() => {
          console.log(`连接关闭,移除缓存: ${url}`)
          this.connections.delete(url)
        })
      }
    }

    setTimeout(cleanup, 0)

    return ws
  }

  /**
   * 发送消息
   * @param {string} url 目标地址
   * @param {any} message 消息内容
   */
  sendMessage(url, message) {
    const ws = this.connections.get(url)
    if (ws && ws.socketTask) {
      ws.socketTask.send({
        data: JSON.stringify(message),
        fail: (err) => {
          console.error('发送消息失败:', err)
        },
      })
    } else {
      console.warn(`未找到可用连接: ${url}`)
    }
  }

  /**
   * 主动关闭某个连接
   * @param {string} url 地址
   */
  close(url) {
    const ws = this.connections.get(url)
    if (ws) {
      ws.close()
      this.connections.delete(url)
    }
  }

  /**
   * 关闭所有连接(如页面卸载时)
   */
  closeAll() {
    for (const [url, ws] of this.connections.entries()) {
      ws.close()
    }
    this.connections.clear();
    console.log('所有websocket已关闭!')
  }
}

// 导出单例
const wsManager = new WebSocketManager()
export default wsManager

使用

从utils 里面引用连接管理器: import wsManager from '@/utils/webSocketManager.js'

      let wsUrl = `${wsUrl}/websocket/handheld/ws/notify/${deviceCode}`; // ws地址
      
      // 连接 ws
      wsManager.connect(wsUrl, (data) => {
        console.log('*******ws消息:',data);
      })

  onUnload() {
    // 关闭所有ws链接
    wsManager.closeAll()
  }

腾讯地图组件使用说明文档

概述

本文档介绍如何在vue3+ts的芋道源码后台管理项目中集成和使用腾讯地图组件,包括位置选择、位置查看、静态地图显示等功能。

腾讯地图标记组件的隐藏参数: showDownload: 是否显示下载按钮 (0-否,1-是),此参数为隐藏参数,不在文档里显示,referer设为dianping也会隐藏下载按钮。

环境配置

1. 获取腾讯地图API密钥

  1. 访问 腾讯位置服务控制台

  2. 创建应用并获取API密钥

  3. 在系统配置中设置 tencentLbsKey

2. 域名配置

在小程序或Web应用中,需要将以下域名添加到白名单:

  • apis.map.qq.com

  • lbs.qq.com

基础用法

1. 位置选择组件

用于让用户在地图上选择位置并获取经纬度坐标。

<template>

  <el-dialog v-model="mapDialogVisible" title="选择位置" append-to-body>

    <IFrame class="h-609px" :src="locPickerUrl" />

  </el-dialog>

</template>



<script setup>

import * as ConfigApi from '@/api/mall/trade/config'



const mapDialogVisible = ref(false)

const locPickerUrl = ref('')

const selectedLocation = ref({

  latitude: null,

  longitude: null,

  address: ''

})



// 初始化选点组件

const initLocationPicker = async () => {

  const data = await ConfigApi.getTradeConfig()

  const key = data.tencentLbsKey

  locPickerUrl.value = `https://apis.map.qq.com/tools/locpicker?type=1&key=${key}&referer=myapp`

}



// 处理位置选择结果

const selectAddress = (loc) => {

  if (loc.poiname === '我的位置') {

    ElMessage.warning('请移动地图选择位置')

    return

  }



  selectedLocation.value = {

    latitude: loc.latlng?.lat,

    longitude: loc.latlng?.lng,

    address: loc.poiname

  }



  mapDialogVisible.value = false

}



// 监听地图消息

onMounted(() => {

  window.selectAddress = selectAddress

  window.addEventListener('message', (event) => {

    const loc = event.data

    if (loc && loc.module === 'locationPicker') {

      window.parent.selectAddress(loc)

    }

  }, false)



  initLocationPicker()

})

</script>

2. 位置查看组件

用于显示已知位置的标记点。

<template>

  <el-dialog v-model="mapDialogVisible" title="查看位置" append-to-body>

    <IFrame class="h-609px" :src="poiMarkerUrl" />

  </el-dialog>

</template>



<script setup>

const props = defineProps({

  latitude: Number,

  longitude: Number,

  title: String,

  address: String

})



const mapDialogVisible = ref(false)

const poiMarkerUrl = ref('')



// 生成标记地图URL

const generatePoiMarkerUrl = async () => {

  if (!props.latitude || !props.longitude) return



  const data = await ConfigApi.getTradeConfig()

  const key = data.tencentLbsKey



  poiMarkerUrl.value = `https://apis.map.qq.com/tools/poimarker?type=0&marker=coord:${props.latitude},${props.longitude};title:${encodeURIComponent(props.title || '位置')};addr:${encodeURIComponent(props.address || '地址')};&key=${key}&referer=myapp`

}



// 打开地图

const openMap = () => {

  if (!props.latitude || !props.longitude) {

    ElMessage.warning('暂无位置信息')

    return

  }

  generatePoiMarkerUrl()

  mapDialogVisible.value = true

}



defineExpose({ openMap })

</script>

3. 静态地图组件

用于显示静态地图图片,无交互功能。

<template>

  <div class="static-map">

    <img

      :src="staticMapUrl"

      alt="位置地图"

      class="w-full h-400px object-cover rounded"

      @error="handleMapError"

    />

    <div class="map-info">

      <p><strong>地址:</strong>{{ address }}</p>

      <p><strong>坐标:</strong>{{ longitude }}, {{ latitude }}</p>

    </div>

  </div>

</template>



<script setup>

const props = defineProps({

  latitude: Number,

  longitude: Number,

  address: String

})



const staticMapUrl = ref('')



// 生成静态地图URL

const generateStaticMapUrl = async () => {

  if (!props.latitude || !props.longitude) return



  const data = await ConfigApi.getTradeConfig()

  const key = data.tencentLbsKey



  const markers = `markers=size:large|color:red|${props.latitude},${props.longitude}`

  const center = `center=${props.latitude},${props.longitude}`

  const zoom = 'zoom=16'

  const size = 'size=750x400'

  const format = 'format=png'



  staticMapUrl.value = `https://apis.map.qq.com/ws/staticmap/v2/?${center}&${zoom}&${size}&${markers}&${format}&key=${key}`

}



const handleMapError = () => {

  ElMessage.error('地图加载失败')

}



watch([() => props.latitude, () => props.longitude], () => {

  generateStaticMapUrl()

}, { immediate: true })

</script>



<style scoped>

.map-info {

  margin-top: 16px;

  padding: 16px;

  background-color: #f8f9fa;

  border-radius: 8px;

}



.map-info p {

  margin: 8px 0;

  font-size: 14px;

  color: #606266;

}

</style>

API参考

腾讯地图API端点

1. 选点组件 (LocationPicker)

官网链接:地图组件 | 腾讯位置服务

apis.map.qq.com/tools/locpi…

参数说明:

  • type: 组件类型,1为选点组件

  • key: 腾讯地图API密钥

  • referer: 调用来源标识,可填写任意字符串 (myapp)

  • coord_type: 坐标类型(可选)

  • policy: 显示策略(可选)

编辑

示例:

https://apis.map.qq.com/tools/locpicker?type=1&key=YOUR_KEY&referer=myapp

2. 标记组件 (PoiMarker)

官网链接:地图组件 | 腾讯位置服务

apis.map.qq.com/tools/poima…

参数说明:

  • type: 组件类型,0为标记组件

  • marker: 标记点信息,格式:coord:纬度,经度;title:标题;addr:地址

  • key: 腾讯地图API密钥

  • referer: 调用来源标识,建议为myapp(dianping、meituan、qunhuodong、wexinmp_profile、parking、myapp)

- showDownload: 是否显示下载按钮 (0-否,1-是),此参数为隐藏参数,不在文档里显示,referer设为dianping也会隐藏下载按钮。

如果想隐藏去这里跟查看周边按钮,只需要用白色元素覆盖就行

编辑

示例:

https://apis.map.qq.com/tools/poimarker?type=0&marker=coord:39.908823,116.39747;title:天安门;addr:北京市东城区&key=YOUR_KEY&referer=myapp

3. 静态地图 (StaticMap)

官方文档:WebService API | 腾讯位置服务

apis.map.qq.com/ws/staticma…

参数说明:

  • center: 地图中心点,格式:纬度,经度

  • zoom: 缩放级别,1-18

  • size: 图片尺寸,格式:宽x高

  • markers: 标记点,格式:size:large|color:red|纬度,经度

  • format: 图片格式,png/jpg

  • key: 腾讯地图API密钥

编辑

示例:

https://apis.map.qq.com/ws/staticmap/v2/?center=39.908823,116.39747&zoom=16&size=750x400&markers=size:large|color:red|39.908823,116.39747&format=png&key=YOUR_KEY

组件示例

完整的位置管理组件

以下是一个完整的位置管理组件示例,包含选择、查看、编辑等功能:

<template>

  <div class="location-manager">

    <!-- 位置信息显示 -->

    <el-form-item label="商户位置" prop="address">

      <el-input

        v-model="formData.address"

        :disabled="isDetail"

        placeholder="请输入详细地址"

        class="w-120! mr-2"

      />

      <el-button

        type="primary"

        v-if="!isDetail"

        @click="openLocationPicker"

      >

        获取经纬度

      </el-button>

      <el-button

        type="primary"

        v-else

        @click="openLocationViewer"

        :disabled="!hasLocation"

      >

        查看位置

      </el-button>

    </el-form-item>



    <!-- 经纬度信息显示 -->

    <el-form-item label="经纬度信息" v-if="hasLocation">

      <div class="location-info">

        <div class="location-item">

          <span class="label">经度:</span>

          <span class="value">{{ formData.longitude }}</span>

        </div>

        <div class="location-item">

          <span class="label">纬度:</span>

          <span class="value">{{ formData.latitude }}</span>

        </div>

        <div class="location-actions">

          <el-button type="text" size="small" @click="copyLocation">

            复制坐标

          </el-button>

          <el-button type="text" size="small" @click="openExternalMap">

            外部地图查看

          </el-button>

        </div>

      </div>

    </el-form-item>



    <!-- 地图弹窗 -->

    <el-dialog

      v-model="mapDialogVisible"

      :title="dialogTitle"

      append-to-body

      width="800px"

    >

      <div class="map-wrapper">

        <IFrame class="h-609px" :src="mapUrl" />

        <!-- 按钮遮罩层 -->

        <div class="map-overlay">

          <div class="button-mask bottom-right"></div>

          <div class="button-mask top-right"></div>

          <div class="button-mask bottom-left"></div>

        </div>

      </div>

    </el-dialog>

  </div>

</template>



<script setup>

import * as ConfigApi from '@/api/mall/trade/config'



const props = defineProps({

  modelValue: {

    type: Object,

    default: () => ({

      address: '',

      latitude: null,

      longitude: null

    })

  },

  isDetail: {

    type: Boolean,

    default: false

  },

  title: {

    type: String,

    default: '位置'

  }

})



const emit = defineEmits(['update:modelValue'])



const formData = computed({

  get: () => props.modelValue,

  set: (value) => emit('update:modelValue', value)

})



const mapDialogVisible = ref(false)

const mapUrl = ref('')

const tencentKey = ref('')



// 计算属性

const hasLocation = computed(() =>

  formData.value.latitude && formData.value.longitude

)



const dialogTitle = computed(() =>

  props.isDetail ? '查看位置' : '选择位置'

)



// 初始化腾讯地图配置

const initTencentMap = async () => {

  const data = await ConfigApi.getTradeConfig()

  tencentKey.value = data.tencentLbsKey

}



// 打开位置选择器

const openLocationPicker = async () => {

  mapUrl.value = `https://apis.map.qq.com/tools/locpicker?type=1&key=${tencentKey.value}&referer=myapp`

  mapDialogVisible.value = true

}



// 打开位置查看器

const openLocationViewer = async () => {

  if (!hasLocation.value) {

    ElMessage.warning('暂无位置信息')

    return

  }



  const { latitude, longitude, address } = formData.value

  mapUrl.value = `https://apis.map.qq.com/tools/poimarker?type=0&marker=coord:${latitude},${longitude};title:${encodeURIComponent(props.title)};addr:${encodeURIComponent(address)};&key=${tencentKey.value}&referer=myapp`

  mapDialogVisible.value = true

}



// 处理位置选择结果

const selectAddress = (loc) => {

  if (loc.poiname === '我的位置') {

    ElMessage.warning('请移动地图选择位置')

    return

  }



  formData.value = {

    ...formData.value,

    latitude: loc.latlng?.lat,

    longitude: loc.latlng?.lng,

    address: loc.poiname

  }



  mapDialogVisible.value = false

  ElMessage.success('位置选择成功')

}



// 复制坐标

const copyLocation = async () => {

  const locationText = `${formData.value.longitude},${formData.value.latitude}`

  try {

    await navigator.clipboard.writeText(locationText)

    ElMessage.success('坐标已复制到剪贴板')

  } catch (err) {

    // 降级方案

    const textArea = document.createElement('textarea')

    textArea.value = locationText

    document.body.appendChild(textArea)

    textArea.select()

    document.execCommand('copy')

    document.body.removeChild(textArea)

    ElMessage.success('坐标已复制到剪贴板')

  }

}



// 在外部地图中查看

const openExternalMap = () => {

  const { longitude, latitude, address } = formData.value

  const url = `https://uri.amap.com/marker?position=${longitude},${latitude}&name=${encodeURIComponent(address || props.title)}`

  window.open(url, '_blank')

}



// 初始化

onMounted(() => {

  // 设置全局回调函数

  window.selectAddress = selectAddress



  // 监听地图消息

  window.addEventListener('message', (event) => {

    const loc = event.data

    if (loc && loc.module === 'locationPicker') {

      window.parent.selectAddress(loc)

    }

  }, false)



  initTencentMap()

})

</script>



<style scoped>

.location-info {

  display: flex;

  flex-direction: column;

  gap: 8px;

  padding: 12px;

  background-color: #f8f9fa;

  border-radius: 6px;

  border: 1px solid #e4e7ed;

}



.location-item {

  display: flex;

  align-items: center;

  font-size: 14px;

}



.location-item .label {

  font-weight: 500;

  color: #606266;

  min-width: 50px;

}



.location-item .value {

  color: #303133;

  font-family: 'Courier New', monospace;

  background-color: #fff;

  padding: 2px 6px;

  border-radius: 3px;

  border: 1px solid #dcdfe6;

}



.location-actions {

  display: flex;

  gap: 8px;

  margin-top: 4px;

}



.map-wrapper {

  position: relative;

  width: 100%;

  height: 609px;

  overflow: hidden;

}



.map-overlay {

  position: absolute;

  top: 0;

  left: 0;

  right: 0;

  bottom: 0;

  pointer-events: none;

  z-index: 10;

}



.button-mask {

  position: absolute;

  background-color: rgba(255, 255, 255, 0.9);

  pointer-events: auto;

  border-radius: 4px;

  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

}



.button-mask.bottom-right {

  bottom: 20px;

  right: 20px;

  width: 80px;

  height: 40px;

}



.button-mask.top-right {

  top: 20px;

  right: 20px;

  width: 120px;

  height: 40px;

}



.button-mask.bottom-left {

  bottom: 20px;

  left: 20px;

  width: 60px;

  height: 40px;

}

</style>

常见问题

1. 地图无法加载

问题描述: 地图组件显示空白或加载失败

解决方案:

  • 检查API密钥是否正确配置

  • 确认域名是否已添加到白名单

  • 检查网络连接是否正常

  • 验证API密钥是否有足够的调用配额

// 检查API密钥配置

const checkApiKey = async () => {

  try {

    const data = await ConfigApi.getTradeConfig()

    if (!data.tencentLbsKey) {

      console.error('腾讯地图API密钥未配置')

      return false

    }

    return true

  } catch (error) {

    console.error('获取API密钥失败:', error)

    return false

  }

}

2. 位置选择回调不触发

问题描述: 用户选择位置后,回调函数没有执行

解决方案:

  • 确保全局回调函数正确设置

  • 检查消息监听器是否正确配置

  • 验证iframe通信是否被浏览器安全策略阻止

// 正确的回调函数设置

onMounted(() => {

  // 设置全局回调函数

  window.selectAddress = selectAddress



  // 监听消息事件

  window.addEventListener('message', (event) => {

    const loc = event.data

    if (loc && loc.module === 'locationPicker') {

      // 确保调用父窗口的回调函数

      if (window.parent && window.parent.selectAddress) {

        window.parent.selectAddress(loc)

      }

    }

  }, false)

})

3. 地图显示位置不准确

问题描述: 地图显示的位置与实际位置不符

解决方案:

  • 检查经纬度数据是否正确

  • 确认坐标系统是否一致(WGS84、GCJ02等)

  • 验证经纬度的顺序是否正确

// 坐标验证函数

const validateCoordinates = (lat, lng) => {

  // 检查纬度范围 (-90 到 90)

  if (lat < -90 || lat > 90) {

    console.error('纬度超出有效范围:', lat)

    return false

  }



  // 检查经度范围 (-180 到 180)

  if (lng < -180 || lng > 180) {

    console.error('经度超出有效范围:', lng)

    return false

  }



  return true

}

4. 按钮遮罩不生效

问题描述: 地图上的"去这里"等按钮仍然可见

解决方案:

  • 调整遮罩层的位置和大小

  • 确保z-index层级正确

  • 检查CSS样式是否被覆盖

/* 调整遮罩位置 */

.button-mask.bottom-right {

  bottom: 15px;  /* 根据实际按钮位置调整 */

  right: 15px;

  width: 90px;   /* 根据按钮大小调整 */

  height: 45px;

  z-index: 999;  /* 确保层级足够高 */

}

最佳实践

1. 错误处理

始终为地图操作添加错误处理机制:

const handleMapOperation = async () => {

  try {

    // 地图操作代码

    await initTencentMap()

  } catch (error) {

    console.error('地图操作失败:', error)

    ElMessage.error('地图服务暂时不可用,请稍后重试')



    // 提供降级方案

    showManualInput()

  }

}



const showManualInput = () => {

  ElMessageBox.prompt('请手动输入地址', '位置信息', {

    confirmButtonText: '确定',

    cancelButtonText: '取消'

  }).then(({ value }) => {

    formData.value.address = value

  })

}

2. 性能优化

避免频繁的API调用和DOM操作:

// 使用防抖优化地图URL更新

import { debounce } from 'lodash-es'



const updateMapUrl = debounce(async () => {

  // 更新地图URL的逻辑

}, 300)



// 监听数据变化时使用防抖

watch([() => formData.value.latitude, () => formData.value.longitude], () => {

  updateMapUrl()

})

3. 用户体验优化

提供清晰的状态反馈和操作指引:

const openLocationPicker = async () => {

  // 显示加载状态

  const loading = ElLoading.service({

    lock: true,

    text: '正在加载地图...',

    spinner: 'el-icon-loading'

  })



  try {

    await initMapUrl()

    mapDialogVisible.value = true

  } catch (error) {

    ElMessage.error('地图加载失败')

  } finally {

    loading.close()

  }

}

4. 数据验证

对位置数据进行严格验证:

const validateLocationData = (data) => {

  const errors = []



  if (!data.address || data.address.trim() === '') {

    errors.push('地址不能为空')

  }



  if (!data.latitude || !data.longitude) {

    errors.push('经纬度信息不完整')

  }



  if (!validateCoordinates(data.latitude, data.longitude)) {

    errors.push('经纬度格式不正确')

  }



  return {

    isValid: errors.length === 0,

    errors

  }

}

5. 安全考虑

保护API密钥和用户数据:

// 不要在前端直接暴露API密钥

// 通过后端接口获取配置信息

const getMapConfig = async () => {

  try {

    const response = await fetch('/api/map/config', {

      headers: {

        'Authorization': `Bearer ${getToken()}`

      }

    })

    return await response.json()

  } catch (error) {

    console.error('获取地图配置失败:', error)

    throw error

  }

}

总结

腾讯地图组件为应用提供了强大的位置服务功能。通过合理的配置和使用,可以为用户提供优秀的位置选择和查看体验。在实际使用中,请注意:

  1. 正确配置API密钥:确保密钥有效且有足够配额

  2. 处理异常情况:提供完善的错误处理和降级方案

  3. 优化用户体验:提供清晰的操作指引和状态反馈

  4. 保护用户隐私:合理使用位置信息,遵守相关法规

  5. 性能优化:避免不必要的API调用和DOM操作

更多详细信息请参考 腾讯位置服务官方文档

【React Native】自定义跑马灯组件Marquee

简介

这是一个基于 React Native 的通用跑马灯组件,支持以下特性:

  • 泛型数据支持
  • 自定义渲染函数
  • 水平/垂直双方向滚动
  • 动态速度与循环控制

基本用法

          <Marquee
            style={{height: 300, width: 300}}
            speed={1}
            loop={true}
            delay={1000}
            isVertical={false}
            data={['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']}
            renderItem={(item: string, index: number) => (
              <View>
                {index % 2 === 0 ? (
                  <View>
                    <View
                      style={{width: 50, height: 50, backgroundColor: 'red'}}
                    />
                    <Text style={{width: 50, height: 50}}>
                      {item}
                      {index}
                    </Text>
                  </View>
                ) : (
                  <View>
                    <View
                      style={{width: 50, height: 50, backgroundColor: 'blue'}}
                    />
                    <Text style={{width: 50, height: 50}}>
                      {item}
                      {index}
                    </Text>
                  </View>
                )}
              </View>
            )}
          />

属性说明

属性名 类型 默认值 描述
data T[] [] 必填数据数组
renderItem (item: T, index: number) => React.ReactNode - 自定义渲染函数
speed number 30 滚动速度(像素/秒)
loop boolean true 是否循环播放
delay number 0 动画启动延迟(毫秒)
isVertical boolean false 是否垂直滚动(默认水平)
style ViewStyle {} 容器样式

源码

import type {Ref} from 'react';
import React, {
  useCallback,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';
import type {ViewProps} from 'react-native';
import {
  Animated,
  Easing,
  findNodeHandle,
  PixelRatio,
  ScrollView,
  UIManager,
  View,
} from 'react-native';

export interface MarqueeProps<T> extends ViewProps {
  /**
   * 滚动数据
   */
  data: T[];
  /**
   * 渲染函数
   */
  renderItem: (item: T, index: number) => React.ReactNode;
  /**
   * 滚动速度(单位:像素/秒)
   */
  speed?: number;
  /**
   * 是否循环播放滚动动画
   */
  loop?: boolean;
  /**
   * 动画开始前的延迟时间(毫秒)
   */
  delay?: number;
  /**
   * 是否垂直滚动,默认为水平滚动
   */
  isVertical?: boolean;
}

export interface MarqueeHandles {
  start: () => void; // 启动滚动动画方法
  stop: () => void; // 停止滚动动画方法
}

/**
 * 创建动画配置(支持循环和连续动画)
 * @param animValue 动画值对象
 * @param config 动画配置(目标值、持续时间、是否循环、延迟时间)
 * @returns 组合动画对象
 */
const createAnim = (
  animValue: Animated.Value,
  config: {
    toValue: number;
    duration: number;
    loop: boolean;
    delay: number;
  },
): Animated.CompositeAnimation => {
  // 动画(线性缓动,原生驱动)
  const anim = Animated.timing(animValue, {
    easing: Easing.linear,
    useNativeDriver: true,
    ...config,
  });

  if (config.loop) {
    // 循环(动画完成后延迟1秒重复)
    return Animated.loop(Animated.sequence([anim]));
  }

  return anim; // 单次动画
};

// 跑马灯组件实现
const Marquee = <T,>(
  props: MarqueeProps<T>,
  ref: Ref<MarqueeHandles>, // 暴露给父组件的句柄引用
): JSX.Element => {
  // 解构组件属性(带默认值)
  const {
    style,
    data,
    renderItem,
    speed = 1, // 默认速度1像素/秒
    loop = true, // 默认循环播放
    delay = 0, // 默认无延迟
    isVertical = false,
    children, // 子组件内容
    ...restProps // 其他传递属性
  } = props;

  // 状态:是否正在动画中
  const [isRunning, setIsRunning] = useState<boolean>(false);
  // 缓存:容器宽度(初始为null)
  const outWidth = useRef<number | null>(null);
  // 缓存:容器高度(初始为null)
  const outHeight = useRef<number | null>(null);
  // 缓存:跑马灯内容实际宽度(初始为null)
  const innerViewWidth = useRef<number | null>(null);
  // 缓存:跑马灯内容实际高度(初始为null)
  const innerViewHeight = useRef<number | null>(null);
  // 动画值(控制跑马灯内容水平位移)
  const animatedValue = useRef<Animated.Value>(new Animated.Value(0));
  // 跑马灯内容引用(用于测量宽度)
  const innerRef = useRef<typeof Animated.View & View>(null);
  // 滚动容器引用(用于测量容器宽度)
  const outRef = useRef<ScrollView>(null);
  // 动画实例引用(用于控制启动/停止)
  const animRef = useRef<Animated.CompositeAnimation>();
  // 配置缓存(避免重复读取props)
  const conf = useRef<{
    speed: number;
    loop: boolean;
    delay: number;
  }>({
    speed,
    loop,
    delay,
  });

  // 停止动画方法
  const stopAnim = useCallback(() => {
    setIsRunning(false); // 更新状态
    clearSize(); // 清空尺寸缓存(下次需要重新测量)
  }, []);

  // 启动动画方法(核心逻辑)
  const startAnim = useCallback(async (): Promise<void> => {
    setIsRunning(true); // 标记动画开始

    await calSize(); // 计算容器和内容的实际宽度

    // 计算需要滚动的距离(内容宽度的一半,因为内容重复了一次)
    let distance = 0;
    // 计算动画时长(根据速度和距离)
    let animDuration = 0;

    if (!isVertical) {
      if (!outWidth.current || !innerViewWidth.current) {
        // 如果宽度缓存未获取到(测量失败)
        return;
      }
      distance = innerViewWidth.current / 2;
      if (distance < outWidth.current) {
        // 内容宽度小于容器宽度,不需要滚动
        return;
      }
      // 计算动画时长(根据速度和距离)
      animDuration =
        PixelRatio.getPixelSizeForLayoutSize(innerViewWidth.current) /
        conf.current.speed;
    } else {
      if (!outHeight.current || !innerViewHeight.current) {
        // 如果高度缓存未获取到(测量失败)
        return;
      }
      distance = innerViewHeight.current / 2;
      if (distance < outHeight.current) {
        // 内容高度小于容器高度,不需要滚动
        return;
      }
      // 计算动画时长(根据速度和距离)
      animDuration =
        PixelRatio.getPixelSizeForLayoutSize(innerViewHeight.current) /
        conf.current.speed;
    }

    // 创建动画配置(使用循环模式)
    animRef.current = createAnim(animatedValue.current, {
      ...conf.current,
      toValue: -distance, // 目标位移(向左/下滚动内容宽度的一半)
      duration: animDuration, // 动画时长
    });

    // 启动动画(无完成回调)
    animRef.current.start((): void => {});
  }, [isVertical]);

  // 暴露命令式句柄给父组件(start/stop方法)
  useImperativeHandle(ref, () => {
    return {
      start: () => {
        startAnim().then(); // 调用启动方法
      },
      stop: () => {
        stopAnim(); // 调用停止方法
      },
    };
  });

  // 副作用:当isStart变化或子组件更新时触发
  useEffect(() => {
    stopAnim(); // 先停止现有动画
    startAnim().then(); // 重新启动动画
  }, [children, startAnim, stopAnim]); // 依赖子组件和动画方法

  // 测量容器和内容宽度的核心方法(异步)
  const calSize = async (): Promise<void> => {
    try {
      // 如果容器或内容引用不存在则返回
      if (!outRef.current || !innerRef.current) {
        return;
      }

      // 通用测量函数(通过UIManager获取组件宽度)
      const measureWidth = (component: ScrollView | View): Promise<number[]> =>
        new Promise(resolve => {
          UIManager.measure(
            findNodeHandle(component) as number, // 获取组件节点句柄
            (_x: number, _y: number, w: number, h: number) => {
              // 测量回调(返回宽度w和高度h)
              return resolve([w, h]); // 解析宽高
            },
          );
        });

      // 并行测量容器宽度和内容宽度和高度
      const [oWidth, oHeight, iWidth, iHeight] = await Promise.all([
        ...(await measureWidth(outRef.current)), // 容器宽度和高度
        ...(await measureWidth(innerRef.current)), // 内容实际宽度和高度
      ]);

      // 缓存测量结果
      outWidth.current = oWidth;
      outHeight.current = oHeight;
      innerViewWidth.current = iWidth;
      innerViewHeight.current = iHeight;
    } catch (error) {
      console.error(error);
    }
  };

  // 清空尺寸缓存(用于动画停止后重新测量)
  const clearSize = () => {
    outWidth.current = null;
    outHeight.current = null;
    innerViewWidth.current = null;
    innerViewHeight.current = null;
  };

  // 组件渲染结构
  return (
    <View style={[{overflow: 'hidden'}, style]}>
      <ScrollView
        ref={outRef} // 绑定容器引用
        showsHorizontalScrollIndicator={false} // 隐藏水平滚动条
        showsVerticalScrollIndicator={false} // 隐藏垂直滚动条
        horizontal={!isVertical} // 水平滚动
        scrollEnabled={false} // 禁用用户手动滚动
        onContentSizeChange={calSize}>
        <Animated.View
          ref={innerRef} // 绑定内容引用
          {...restProps} // 传递其他属性
          style={[
            style, // 原生样式
            {
              display: 'flex',
              flexDirection: isVertical ? 'column' : 'row', // 子元素横向排列使内容重复显示transform: [
                isVertical
                  ? {translateY: animatedValue.current}
                  : {translateX: animatedValue.current},
              ], // 应用水平位移动画
              opacity: isRunning ? 1 : 0, // 动画时显示停止时隐藏
            },
            isVertical ? {height: '100%'} : {width: '100%'},
          ]}>
          {data.map((item, index) => (
            <View key={index}>{renderItem(item, index)}</View>
          ))}
          {data.map((item, index) => (
            <View key={index + data.length}>{renderItem(item, index)}</View>
          ))}
        </Animated.View>
      </ScrollView>
    </View>
  );
};

// 导出带ref的组件(支持命令式调用)
export default React.forwardRef<MarqueeHandles, MarqueeProps<any>>(Marquee);

Promise(一)极简版demo

Promise(一)极简版demo

前言

在所有的编程语言中,异步处理都是一个绕不开的话题。而在JS的这个话题中,Promise则是当之无愧的主角之一,这应该是我们日常用的最多的异步API了。从今天开始将更新一些有关Promise的笔记。

异步逻辑

先来看一个异步操作:

setTimeout(() => {
    data = 'result';
    return data;
}, 1000);

显然这样是无法获取结果的,那如果我们将代码修改一下:

let result = setTimeout(() => {
    // 异步获取结果
    data = 'result';
    return data;
}, 1000);

JS的主线程是同步运行的,当代码运行到这里的时候,根据setTimeout的机制,返回值result的值是该定时器的ID。所以此处也无法获取值。既然已经联想到JS的机制了,或许我们可以考虑,使用变量赋值:

let data = ''
setTimeout(() => {
    data = 'result';
}, 1000)
console.log(data); // ''
setTimeout(() => {
    console.log(data);
}, 1001); // 1001ms后,打印出来的结果是result

也就是说,变量赋值是有效的,只是如何抓住异步执行完的时机呢?毕竟我们没法估计实际的异步操作时间。

那么最好的办法就是把具体操作也放到异步逻辑中去,这样就可以利用JS的线性执行和任务队列来帮助我们抓住这个Timing:

setTimeout(() => {
    data = 'result';
    console.log(data);
}, 1000)

const handler = (data) => {}
setTimeout(() => {
    data = 'result';
    const handler = (data) => {}
}, 1000)

使用过Promise的我们会发现,这似乎很眼熟:

new Promise((resolve, reject) => {
    try {
        // 执行一个异步操作,成功
        const data = syncFunc();
        // 调用resolve传递返回值
        resolve(data);
    } catch (error) {
        // 失败
        reject(error)
    }
})

在Promise中我们也需要传入一个方法函数,只是这个函数中resolve, reject的实现对于使用者来说是黑盒。但起码我们知道为什么它这么要求了。

代码实现

class Promise {
    constructor(executor) {
        executor(resolve, reject) // 用户传入的executor通常都是异步的,在Promise的内部应该执行它
    }
}

至于resolvereject到底要做什么呢?我们先按下不表.

我们先来看看Promise A+规范的要求:

2.1 Promise state

A promise must be in one of three states: pending, fulfilled, or rejected.

2.1.1 When pengding, a promise may transition to fulfilled or rejected.

2.1.2 When fulfilled, must not transition, must have a value which must not change.

2.1.3 When rejected, must not transiton, must have a reason which must not change.

根据这部分说明,我们进一步更新代码:

const PENDING = 'PENDING';     // 等待态
const FULFILLED = 'DULFILLED'; // 成功态
const REJECTED = 'REJECTED';   // 失败态
class Promise {
    constructor() {
        this.state = PENDING;
        this.value = '';
        this.reason ='';
    }
}

完成这些定义后,我们还缺少一些相关的状态转化的实现,这就是resolve、reject要做的了

class Promise {
    constructor(executor) {
        this.state = PENDING;
        this.value = '';
        this.reason ='';

        const resolve = (value) => { // 成功时传入值
            this.state = FULFILLED;
            this.value = value;
        }
        const reject = (error) => {
            this.state = REJECTED;
            this.reason = error;
        }
        
        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
        
    }
}

这样我们就完成了上述的内容,它既符合异步处理的思想,也符合Promise的实际运行逻辑、Promise A+规范的要求

结语

下一篇我们继续实现Promise

重磅升级丨Mapmost全面兼容3DTiles 1.1,3DGS量测精度跃升至亚米级!

Mapmost SDK

for WebGL 9.8.0-beta 版本正式发布!

✅ 行业前沿标准适配:全面支持3DTiles 1.1,加载性能提升10%!

✅ 三维空间分析进阶:新增三维压平分析

功能,支持倾斜模型与三维通用模型(包括3DTiles、GLB、FBX等格式)局部/全局压平。

3DGS

量测精度提升:基于3DGS数据的拾取精度从米级提升至亚米级,量测结果更精准!

一、全面支持3DTiles 1.1

3DTiles 1.1 是开放的三维数据标准,作为3DTiles 1.0 的下一代,支持更高效的数据压缩、更精细的LOD控制和更逼真的渲染效果。本次支持让 Mapmost 用户能直接对接最新行业数据,提升三维场景加载效率与渲染性能!

Mapmost能力:

兼容Cesium体系的3DTiles 1.1服务,包括基于通用模型、BIM模型与倾斜模型发布的3DTiles 1.1服务。

Mapmost加载3DTiles 1.1服务

二、新增三维压平分析

能力与应用场景:

支持通过设置范围实现对倾斜和三维通用模型的局部/全局压平,压平后支持放置其他三维模型,可用于城区建筑规划改造、精细模型与实景三维模型融合等场景。

动图封面

压平后放置三维模型

三、3DGS量测精度提升

优化点:

3DGS拾取与三维量测算法优化,全面提升交互的精度与性能,解决原射线拾取方式无法对稀疏点云进行拾取的问题,整体精度可控在亚米级

动图封面

电塔高度量测

四、3D热力图效果升级

优化点:

优化3D热力图中热区顶部过平问题,同时支持增长动画,整体效果过渡得更自然!可应用于人口密度分析,酒店、景区、体育馆等POI点位分布分析等。

三维热力图效果对比

动图封面

三维热力图动画

如何体验新功能?

网址:www.mapmost.com/#

❌