阅读视图

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

还在用for循环遍历DOM?试试更优雅的NodeIterator与TreeWalker吧

在前端开发中,DOM操作是绕不开的核心技能。无论是动态渲染页面、处理用户交互,还是实现复杂的动画效果,DOM的遍历与操作始终是关键环节。然而,传统的for循环或递归遍历DOM的方式往往显得笨拙且低效。今天,我们将揭开DOM2 Traversal and Range模块的神秘面纱,深入解析其定义的两个核心工具:NodeIteratorTreeWalker。它们不仅能让你像“走迷宫”一样高效遍历DOM树,还能通过灵活的过滤机制精准定位目标节点。


一、DOM2 Traversal模块的前世今生

DOM2 Traversal and Range模块是W3C在2000年提出的标准之一,旨在为开发者提供更高效、更灵活的DOM操作方式。其中,NodeIteratorTreeWalker是模块的核心工具,它们通过深度优先遍历(Depth-First Traversal)的方式,从指定的根节点出发,系统化地访问DOM树中的每一个节点。

1.1 深度优先遍历的奥秘

深度优先遍历是一种“先深入子树,再回溯”的遍历策略。例如,对于以下HTML结构:

<div id="root">
  <p>段落1</p>
  <ul>
    <li>项目1</li>
    <li>项目2</li>
  </ul>
</div>

<div>为根节点的遍历顺序将是:

  1. <div>
  2. <p>
  3. 文本节点“段落1”
  4. <ul>
  5. <li>(项目1)
  6. 文本节点“项目1”
  7. <li>(项目2)
  8. 文本节点“项目2”

这种遍历方式与人类阅读文档的逻辑高度一致,尤其适合处理嵌套层级复杂的DOM结构。


二、NodeIterator与TreeWalker的对比

DOM2 Traversal模块提供了两种遍历工具,它们各有特点,适用于不同的场景。

2.1 NodeIterator:简单粗暴的“单向通道”

NodeIterator是功能较为简单的遍历器,它通过document.createNodeIterator()方法创建,仅支持单向遍历(从根节点到叶子节点)。

核心参数

  • root:遍历的起点节点。
  • whatToShow:通过位掩码(Bitmask)指定需要访问的节点类型(如元素、文本、注释等)。
  • filter:过滤函数或NodeFilter对象,用于决定是否接受某个节点。
  • entityReferenceExpansion:是否扩展实体引用(HTML中无效,默认设为false)。

常用方法

  • nextNode():返回下一个节点。
  • previousNode():仅在TreeWalker中可用,NodeIterator不支持反向遍历。

代码示例

// 创建一个NodeIterator,仅遍历元素节点
const iterator = document.createNodeIterator(
  document.getElementById("root"),
  NodeFilter.SHOW_ELEMENT,
  null,
  false
);

let node;
while ((node = iterator.nextNode())) {
  console.log(node); // 输出所有元素节点
}

应用场景

  • 快速遍历特定类型的节点(如仅需处理元素节点)。
  • 配合过滤函数实现精准筛选(如提取所有<p>标签)。

注意事项

  • 单向性限制:无法回溯到父节点或兄弟节点。
  • 性能优势:由于逻辑简单,执行效率较高。

2.2 TreeWalker:灵活的“双向探索者”

TreeWalker是NodeIterator的“加强版”,它不仅支持深度优先遍历,还允许双向移动(向上、向下、左右跳转),适合需要动态调整遍历路径的场景。

核心方法

  • parentNode():跳转到当前节点的父节点。
  • firstChild():跳转到第一个子节点。
  • lastChild():跳转到最后一个子节点。
  • nextSibling():跳转到下一个兄弟节点。
  • previousSibling():跳转到上一个兄弟节点。

代码示例

// 创建一个TreeWalker,遍历所有节点
const walker = document.createTreeWalker(
  document.getElementById("root"),
  NodeFilter.SHOW_ALL,
  null,
  false
);

// 向下遍历到第一个子节点
walker.firstChild();
console.log(walker.currentNode); // 输出第一个子节点

// 向上回溯到父节点
walker.parentNode();
console.log(walker.currentNode); // 输出根节点

应用场景

  • 动态调整遍历路径(如导航菜单的展开/折叠)。
  • 复杂DOM结构的深度挖掘(如解析文档大纲)。

注意事项

  • 灵活性代价:双向操作可能增加代码复杂度。
  • 兼容性问题:IE浏览器不支持该模块(需通过polyfill或替代方案解决)。

三、实战技巧:让遍历事半功倍

3.1 过滤器的艺术

通过filter参数,你可以自定义节点的筛选规则。例如,仅遍历包含特定类名的元素:

const filter = {
  acceptNode: (node) => {
    return node.classList?.contains("highlight") 
      ? NodeFilter.FILTER_ACCEPT 
      : NodeFilter.FILTER_SKIP;
  }
};

const walker = document.createTreeWalker(
  document.body,
  NodeFilter.SHOW_ELEMENT,
  filter,
  false
);

3.2 位掩码的妙用

whatToShow参数支持按位或(|)组合多个节点类型。例如,同时遍历元素节点和文本节点:

const whatToShow = NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT;

3.3 性能优化

  • 避免全量遍历:通过过滤器提前剪枝,减少不必要的节点访问。
  • 缓存遍历结果:对静态DOM结构,可将遍历结果存储为数组,避免重复操作。

四、常见误区与解决方案

4.1 遍历范围被意外限制

如果根节点设置不当,遍历可能无法覆盖预期区域。例如,以<body>为根节点时,无法访问<head>中的节点。

解决方案:将根节点设为document,确保覆盖整个DOM树。

4.2 动态DOM的陷阱

如果遍历过程中DOM结构被修改(如添加或删除节点),遍历器的行为可能变得不可预测。

解决方案

  • 在遍历前冻结DOM结构。
  • 使用MutationObserver监听变化并重新初始化遍历器。

4.3 兼容性地狱

IE浏览器不支持DOM2 Traversal模块,可能导致代码失效。

解决方案

  • 使用document.implementation.hasFeature("Traversal", "2.0")检测兼容性。
  • 通过递归或第三方库(如jQuery)实现兼容性方案。

五、总结:选择合适的武器

特性 NodeIterator TreeWalker
遍历方向 单向(仅向下) 双向(上下左右)
方法丰富度 简单(仅nextNode() 丰富(支持跳转操作)
性能 略低(因灵活性)
适用场景 快速筛选特定节点 动态调整遍历路径

NodeIterator适合“一次性任务”,而TreeWalker更适合“探索式任务”。掌握它们,你将能像棋手一样精准操控DOM,用代码书写优雅的前端篇章。

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

在现代 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 总结文本的艺术

在现代 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,就像后台的那位戏精,随时准备为你的数据配上旁白——
甚至会夸张地说你是下一个商业传奇。

Vue SSR原理

当搜索引擎的爬虫访问我们的站点时,如果只看到一句冷冰冰的 <div id="app"></div>,SEO 基本就凉了。Vue SSR(Server-Side Rendering,服务端渲染)正是为了解决这个问题:让首屏 HTML 在服务器上生成,既能被爬虫读懂,又能让用户以最短的时间看到内容。

一、两份入口文件,一个共享的“根”

Vue 项目的传统 SPA 只有一个 main.js,而 SSR 需要两个入口:

  • app.js

    这是“纯粹”的 Vue 根实例工厂,既不挂载 DOM,也不关心运行在哪个环境。它返回一个干净的 new Vue(),被客户端和服务端共同引用。

  • client-entry.js

    拿到 app.js 返回的实例后,直接 mount('#app'),把静态标记激活成可交互的 SPA。

  • server-entry.js

    在 Node 环境里执行,职责有三件:

    1. 调用 app.js 创建根实例;
    2. 根据请求 URL 做路由匹配,找到需要渲染的组件;
    3. 执行组件暴露的 asyncDatafetch,把数据预取到 Vuex store。

这样设计让“渲染”与“激活”解耦,同一份业务代码跑在两端。

二、Webpack 打出两份 Bundle

构建时,Webpack 会跑两次:

  • Client Bundle

    打包所有客户端代码,输出到 dist/client,浏览器下载后负责激活静态 HTML。

  • Server Bundle

    打包所有服务端代码,输出到 dist/server,Node 进程通过 vue-server-renderer 读取这份 Bundle,生成首屏 HTML。

两份 Bundle 都包含业务组件,但前者带浏览器运行时,后者只保留渲染逻辑,体积更小。

三、服务器收到请求,一条流水线干活

当用户或爬虫发来 GET /article/42

  1. Node 进程加载 server-entry.js,创建一个新的 Vue 实例。
  2. 路由匹配到 Article.vue,触发 asyncData 钩子,拉取文章详情并写入 store。
  3. vue-server-renderer 把组件树渲染成字符串,插入到模板中的 <!--vue-ssr-outlet--> 占位符里。
  4. 为了让客户端“无缝续命”,服务器把 store 状态序列化成一段脚本:
   <script>window.__INITIAL_STATE__ = {...}</script>
  1. 最终拼好的 HTML 响应给浏览器,首屏直出完成。

四、浏览器“激活”静态标记

浏览器拿到 HTML 后,做了三件事:

  1. 解析 DOM 并立即渲染,用户瞬间看到文章标题与正文。

  2. 加载 Client Bundle,执行 client-entry.js,创建同构的 Vue 实例。

  3. 通过 __INITIAL_STATE__ 恢复 store 数据,再调用 hydrate 而非 mount

    hydrate 会对比服务端返回的 DOM 与客户端虚拟 DOM,复用已有节点、绑定事件,把“死”的 HTML 激活成“活”的 SPA。

这一步叫 客户端激活(Client Hydration),只有 DOM 结构与数据完全一致才能成功,否则 Vue 会整段替换,带来性能损耗。

五、交互回归 SPA 常态

激活完成后,所有路由跳转、数据更新都由浏览器接管,退化为普通单页应用。

SSR 只在首屏出场一次,之后不再参与。这样既享受了 SEO 与首屏性能,又保留了 SPA 的流畅体验。

六、总结

VUE SSR 的核心思想是让服务器先跑一次组件的渲染函数,把结果 HTML 交给浏览器,浏览器再用同一份代码激活它。

JavaScript 代理(Proxy)与反射(Reflect)详解

在现代 JavaScript 开发中,代理(Proxy)反射(Reflect) 是两个非常强大的特性。
它们可以帮助我们 拦截对象操作、控制属性访问、实现验证逻辑,甚至实现响应式系统
下面我们按照知识点逐一展开。


1. 创建空代理

最基本的代理写法如下:

const target = { name: "Alice" };
const proxy = new Proxy(target, {}); // 空代理,没有拦截行为

proxy.age = 20;

console.log(proxy.name);  // "Alice"
console.log(target.age);  // 20

👉 空代理只是 target 的一个“镜像”,没有任何特殊功能。


2. 定义捕获器(Traps)

捕获器就是代理的“拦截器”。例如 get 捕获属性访问:

const target = { name: "Alice" };

const proxy = new Proxy(target, {
  get(obj, prop) {
    console.log(`访问属性: ${prop}`);
    return obj[prop];
  }
});

console.log(proxy.name); // 输出: 访问属性: name \n "Alice"

3. 捕获器与参数

大多数捕获器会接收 target、prop、receiver 参数。例如:

const target = { age: 25 };

const proxy = new Proxy(target, {
  get(t, prop, receiver) {
    console.log(`读取属性: ${prop}`);
    return Reflect.get(t, prop, receiver); // 推荐用 Reflect 保持一致性
  }
});

console.log(proxy.age); // 输出: 读取属性: age \n 25

4. 捕获器不变式

代理必须遵循 JavaScript 对象的不变式(invariants),否则会抛错。

// 示例一
const obj = Object.freeze({ x: 10 });

const proxy = new Proxy(obj, {
  get() {
    return 42; // ❌ 不能篡改冻结对象的不变式
  }
});

console.log(proxy.x); // 10,而不是 42


// 示例二
const obj = {};
Object.defineProperty(obj, "y", {
  value: 100,
  writable: false,      // ❌ 不可写
  configurable: false   // ❌ 不可重新定义
});

const proxy = new Proxy(obj, {
  get() {
    return 200; // ❌ 尝试违反不变式
  }
});

console.log(proxy.y); 
// 输出: 100
// 说明:代理必须保持与目标对象一致,
// 不允许修改不可写 + 不可配置属性的值

5. 可撤销代理

代理可以通过 Proxy.revocable 创建,并支持 撤销

const { proxy, revoke } = Proxy.revocable({ msg: "Hello" }, {
  get(t, prop) {
    return t[prop];
  }
});

console.log(proxy.msg); // "Hello"
revoke(); // 撤销代理
// console.log(proxy.msg); // ❌ 报错:代理已撤销

6. 实用反射 API

Reflect 提供了一套方法,和代理捕获器一一对应。

const obj = { x: 10 };

console.log(Reflect.get(obj, "x")); // 10
Reflect.set(obj, "y", 20);
console.log(obj.y); // 20

👉 使用 Reflect 操作对象,比直接操作更安全规范。


7. 代理另一个代理

代理本身也能被再次代理:

const target = { value: 1 };

const proxy1 = new Proxy(target, {
  get(t, prop) {
    console.log("proxy1 get");
    return Reflect.get(t, prop);
  }
});

const proxy2 = new Proxy(proxy1, {
  get(t, prop) {
    console.log("proxy2 get");
    return Reflect.get(t, prop);
  }
});

console.log(proxy2.value);
// 输出:proxy2 get \n proxy1 get \n 1

8. 代理的问题与不足

  • 性能开销:拦截操作有额外消耗。
  • 调试困难:过度使用会让逻辑难以理解。
  • 部分内建对象(如 DOM 节点)可能无法完全代理。

9. 代理捕获器与反射方法

每个捕获器都有对应的 Reflect 方法,推荐配合使用。下面给出示例:

9.1 get ↔ Reflect.get

const obj = { name: "Alice" };
const proxy = new Proxy(obj, {
  get(t, prop, receiver) {
    console.log(`读取 ${prop}`);
    return Reflect.get(t, prop, receiver);
  }
});
console.log(proxy.name); // 输出: 读取 name \n Alice

9.2 set ↔ Reflect.set

const obj = {};
const proxy = new Proxy(obj, {
  set(t, prop, value, receiver) {
    console.log(`设置 ${prop} = ${value}`);
    return Reflect.set(t, prop, value, receiver);
  }
});
proxy.age = 30; // 输出: 设置 age = 30

9.3 has ↔ Reflect.has

const obj = { x: 10 };
const proxy = new Proxy(obj, {
  has(t, prop) {
    console.log(`检查是否有 ${prop}`);
    return Reflect.has(t, prop);
  }
});
console.log("x" in proxy); // 输出: 检查是否有 x \n true

9.4 defineProperty ↔ Reflect.defineProperty

const obj = {};
const proxy = new Proxy(obj, {
  defineProperty(t, prop, desc) {
    console.log(`定义属性 ${prop}`);
    return Reflect.defineProperty(t, prop, desc);
  }
});
Object.defineProperty(proxy, "name", { value: "Bob" });
console.log(obj.name); // Bob

9.5 getOwnPropertyDescriptor ↔ Reflect.getOwnPropertyDescriptor

const obj = { a: 1 };
const proxy = new Proxy(obj, {
  getOwnPropertyDescriptor(t, prop) {
    console.log(`获取属性描述符 ${prop}`);
    return Reflect.getOwnPropertyDescriptor(t, prop);
  }
});
console.log(Object.getOwnPropertyDescriptor(proxy, "a"));

9.6 deleteProperty ↔ Reflect.deleteProperty

const obj = { secret: "123" };
const proxy = new Proxy(obj, {
  deleteProperty(t, prop) {
    console.log(`删除属性 ${prop}`);
    return Reflect.deleteProperty(t, prop);
  }
});
delete proxy.secret; // 输出: 删除属性 secret

9.7 ownKeys ↔ Reflect.ownKeys

const obj = { x: 1, y: 2 };
const proxy = new Proxy(obj, {
  ownKeys(t) {
    console.log("获取所有键");
    return Reflect.ownKeys(t);
  }
});
console.log(Object.keys(proxy)); // 输出: 获取所有键 \n ["x","y"]

9.8 getPrototypeOf ↔ Reflect.getPrototypeOf

const obj = {};
const proxy = new Proxy(obj, {
  getPrototypeOf(t) {
    console.log("获取原型");
    return Reflect.getPrototypeOf(t);
  }
});
console.log(Object.getPrototypeOf(proxy));

9.9 setPrototypeOf ↔ Reflect.setPrototypeOf

const obj = {};
const proto = { greet: () => "hi" };
const proxy = new Proxy(obj, {
  setPrototypeOf(t, proto) {
    console.log("设置原型");
    return Reflect.setPrototypeOf(t, proto);
  }
});
Object.setPrototypeOf(proxy, proto); // 输出: 设置原型
console.log(obj.greet()); // hi

9.10 isExtensible ↔ Reflect.isExtensible

const obj = {};
const proxy = new Proxy(obj, {
  isExtensible(t) {
    console.log("检查是否可扩展");
    return Reflect.isExtensible(t);
  }
});
console.log(Object.isExtensible(proxy)); // true

9.11 preventExtensions ↔ Reflect.preventExtensions

const obj = {};
const proxy = new Proxy(obj, {
  preventExtensions(t) {
    console.log("禁止扩展");
    return Reflect.preventExtensions(t);
  }
});
Object.preventExtensions(proxy); // 输出: 禁止扩展

9.12 apply ↔ Reflect.apply

function sum(a, b) { return a + b; }
const proxy = new Proxy(sum, {
  apply(fn, thisArg, args) {
    console.log("调用函数:", args);
    return Reflect.apply(fn, thisArg, args);
  }
});
console.log(proxy(2, 3)); // 输出: 调用函数: [2,3] \n 5

9.13 construct ↔ Reflect.construct

function Person(name) { this.name = name; }
const proxy = new Proxy(Person, {
  construct(t, args, newTarget) {
    console.log("构造函数调用:", args);
    return Reflect.construct(t, args, newTarget);
  }
});
const p = new proxy("Alice"); 
// 输出: 构造函数调用: [ 'Alice' ]
console.log(p.name); // Alice

10. 跟踪属性访问

function track(obj) {
  return new Proxy(obj, {
    get(t, prop) {
      console.log(`访问 ${prop}`);
      return Reflect.get(t, prop);
    }
  });
}
const user = track({ name: "Tom", age: 20 });
console.log(user.name);

11. 隐藏属性

function hide(obj, keys) {
  return new Proxy(obj, {
    get(t, prop) {
      if (keys.includes(prop)) return undefined;
      return Reflect.get(t, prop);
    },
    ownKeys(t) {
      return Reflect.ownKeys(t).filter(k => !keys.includes(k));
    }
  });
}
const user = hide({ name: "Alice", password: "123" }, ["password"]);
console.log(user.password); // undefined
console.log(Object.keys(user)); // ["name"]

12. 属性验证

const person = new Proxy({}, {
  set(t, prop, value) {
    if (prop === "age" && typeof value !== "number") {
      throw new TypeError("年龄必须是数字");
    }
    return Reflect.set(t, prop, value);
  }
});
person.age = 30;   // ✅
person.age = "20"; // ❌ 抛错

13. 函数与构造函数参数验证

function sum(a, b) { return a + b; }

const safeSum = new Proxy(sum, {
  apply(fn, thisArg, args) {
    if (!args.every(n => typeof n === "number")) {
      throw new TypeError("参数必须是数字");
    }
    return Reflect.apply(fn, thisArg, args);
  }
});

console.log(safeSum(2, 3)); // 5
// console.log(safeSum(2, "x")); // 抛错

14. 数据绑定与可观察对象

function observable(obj, callback) {
  return new Proxy(obj, {
    set(t, prop, value) {
      const result = Reflect.set(t, prop, value);
      callback(prop, value);
      return result;
    }
  });
}
const state = observable({ count: 0 }, (k, v) => {
  console.log(`${k} 更新为 ${v}`);
});
state.count++; // 输出: count 更新为 1

总结

  • Proxy 让我们可以拦截并自定义对象操作。
  • Reflect 提供与代理捕获器对应的标准 API,确保操作符合规范。
  • 常见用途包括:属性跟踪、隐藏敏感信息、数据验证、响应式编程。

CSS布局三巨头:浮动、定位与弹性布局的恩怨情仇

各位前端小伙伴们,今天咱们来聊聊CSS世界里的三位"布局大佬"。它们就像《西游记》里的师徒四人(哦不,这里只有三位),各有各的神通,也各有各的小脾气。掌握了它们,你就能在前端布局的世界里"横着走"啦!

一、浮动布局:曾经的王者,如今的"叛逆少年"

想当年,浮动布局那可是响当当的"布局一哥"。它最开始的使命特简单——让文字优雅地环绕图片,就像流水绕着石头走。可谁能想到,前端工程师们居然用它玩出了花,搞出了各种复杂的页面布局。

.box {
  float: left;  /* 往左飘 */
  width: 200px;
  height: 200px;
}

但这小子有个叛逆的毛病——脱离文档流。就像个调皮的孩子,一旦飘起来就不管不顾,亲爹(父容器)都感受不到它的重量(高度塌陷)。这可苦了我们这些当"保姆"的开发者,得想尽办法"清除浮动":

  • 给父容器手动定高度?太low了,不够灵活
  • 加个空div设置clear:both?代码洁癖表示无法接受
  • 最佳方案还是伪元素清除法,既干净又优雅:
.parent::after {
  content: "";
  display: block;
  clear: both;
}

对了,浮动还有个好兄弟叫BFC(块级格式化上下文)。这玩意儿就像给元素加了个"隔离罩",不仅能解决margin重叠问题,还能让父容器重新认识自己的浮动孩子(包含浮动元素高度)。开启BFC的方式有很多,比如overflow:hidden(最简单但小心隐藏内容)、position:absolute(一言不合就脱离文档流)、display:flex(直接叫来了更厉害的角色)。

二、定位布局:元素界的"轻功大师"

如果说浮动是叛逆少年,那定位布局就是身怀绝技的轻功大师。它有五种"轻功心法",咱们一个个来看:

  1. 静态定位(static):最普通的状态,元素规规矩矩按照文档流排队,就像上班族挤地铁,一步都不敢乱走。

  2. 相对定位(relative):有点小个性,可以相对于自己原来的位置偏移,但脚还踩在文档流里,不会影响别人。就像你在地铁上稍微挪了挪身子,但没离开自己的站位。

  3. 绝对定位(absolute):彻底放飞自我,脱离文档流,满世界找"靠山"(有定位属性的父元素)。如果没找到,就认body当爸爸。想让它水平垂直居中?简单:

.box {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);  /* 回退自身一半 */
}
  1. 固定定位(fixed):最任性的主儿,直接把浏览器窗口当自己家,不管页面怎么滚动,它都岿然不动。就像小区门口的保安,你走你的路,他站他的岗。

  2. 粘性定位(sticky):最善变的家伙,平时像relative一样老实,一旦你滚动到某个阈值,它立刻切换成fixed模式,死死黏在你指定的位置。就像你手机里的常用APP,平时藏在文件夹里,用的时候一点就出来。

三、弹性布局(Flexbox):布局界的"全能选手"

如果说前面两位是"老派高手",那弹性布局就是"新生代全能王"。它解决了前两者的诸多痛点,让布局变得简单、优雅、响应式。

弹性布局有两个核心角色:弹性容器弹性子元素。容器就像教练,子元素就是学员,教练说往哪排,学员就往哪排。

子元素的"生存法则"

  • 可以通过order属性改变排列顺序(数字越小越靠前,默认都是0)
  • flex-grow:控制是否放大(0不放大,1放大,数值越大占比越多)
  • flex-shrink:控制是否缩小(0不缩小,1缩小)
  • flex-basis:设置初始尺寸
  • 简写形式flex: 0 0 100px;(不放大、不缩小、初始100px)

容器的"指挥艺术"

  • justify-content: center;:子元素在主轴(默认水平)上居中
  • align-items: center;:子元素在交叉轴(默认垂直)上居中
  • flex-direction: column;:把主轴换成垂直方向
  • flex-wrap: wrap;:允许子元素换行(再也不用担心溢出了)
  • flex-flow: row wrap;flex-directionflex-wrap的简写
  • align-content: center;:有多行子元素时,控制整体在交叉轴上的对齐方式

有了弹性布局,什么两栏布局、三栏布局、居中对齐,都变得so easy!再也不用像以前那样写一堆浮动和清除代码了。

总结:三位大佬怎么选?

  • 如果你需要做文字环绕图片或者简单的水平排列,浮动布局依然是个不错的选择(记得清除浮动哦)。
  • 如果你需要精确定位元素(比如导航栏固定、弹窗居中),定位布局能帮你实现。
  • 如果你想做复杂的响应式布局(比如栅格系统、灵活的多栏布局),弹性布局绝对是首选,它会让你的代码更简洁、更易维护。

最后想说,CSS布局没有绝对的"银弹",每种布局方式都有它的适用场景。作为前端开发者,我们要做的就是了解它们的特性,在合适的场景用合适的布局,让我们的页面既美观又高效!

各位小伙伴,你们在布局时遇到过哪些有趣的问题?又是怎么解决的呢?欢迎在评论区分享你的故事!

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

序言

在当今前端开发中,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

推荐 vue vxe-gantt 最好用的甘特图,功能全面的甘特图组件

推荐 vue vxe-gantt 最好用的甘特图,功能全面的甘特图组件

查看官网:vxeui.com Github:github.com/x-extends/v… Gitee:gitee.com/x-extends/v…

npm install vxe-pc-ui@4.8.19 vxe-table@4.16.0 vxe-gantt@4.0.0
// ...
import VxeUIAll from 'vxe-pc-ui'
 import 'vxe-pc-ui/es/style.css'

 import VxeUITable from 'vxe-table'
 import 'vxe-table/es/style.css'

 import VxeUIGantt from 'vxe-gantt'
 import 'vxe-gantt/lib/style.css'
 // ...

 createApp(App).use(VxeUIAll).use(VxeUITable).use(VxeUIGantt).mount('#app')
 // ...

基础功能

image

<template>
  <div>
    <vxe-gantt v-bind="ganttOptions"></vxe-gantt>
  </div>
</template>

<script setup>
import { reactive } from 'vue'

const ganttOptions = reactive({
  columns: [
    { field: 'name', title: '任务名称' },
    { field: 'start', title: '开始时间', width: 100 },
    { field: 'end', title: '结束时间', width: 100 }
  ],
  data: [
    { id: 10002, name: '城市道路修理进度', start: '2024-03-03', end: '2024-03-08', progress: 10 },
    { id: 10004, name: '超级大工程', start: '2024-03-05', end: '2024-03-11', progress: 15 },
    { id: 10006, name: '一个小目标项目', start: '2024-03-10', end: '2024-03-21', progress: 5 },
    { id: 10007, name: '某某计划', start: '2024-03-15', end: '2024-03-24', progress: 70 },
    { id: 10008, name: '某某科技项目', start: '2024-03-20', end: '2024-03-29', progress: 50 }
  ]
})
</script>

显示任务进度

通过 task-bar-config.showProgress 显示任务进度

image

<template>
  <div>
    <vxe-gantt v-bind="ganttOptions"></vxe-gantt>
  </div>
</template>

<script setup>
import { reactive } from 'vue'

const ganttOptions = reactive({
  border: true,
  columnConfig: {
    resizable: true
  },
  taskBarConfig: {
    showProgress: true
  },
  columns: [
    { field: 'title', title: '任务名称' },
    { field: 'start', title: '开始时间', width: 100 },
    { field: 'end', title: '结束时间', width: 100 }
  ],
  data: [
    { id: 10001, title: 'A项目', start: '2024-03-01', end: '2024-03-04', progress: 3 },
    { id: 10002, title: '城市道路修理进度', start: '2024-03-03', end: '2024-03-08', progress: 10 },
    { id: 10003, title: 'B大工程', start: '2024-03-03', end: '2024-03-11', progress: 90 },
    { id: 10004, title: '超级大工程', start: '2024-03-05', end: '2024-03-11', progress: 15 },
    { id: 10005, title: '地球净化项目', start: '2024-03-08', end: '2024-03-15', progress: 100 },
    { id: 10006, title: '一个小目标项目', start: '2024-03-10', end: '2024-03-21', progress: 5 },
    { id: 10007, title: '某某计划', start: '2024-03-15', end: '2024-03-24', progress: 70 },
    { id: 10008, title: '某某科技项目', start: '2024-03-20', end: '2024-03-29', progress: 50 },
    { id: 10009, title: '地铁建设工程', start: '2024-03-19', end: '2024-03-20', progress: 5 },
    { id: 10010, title: '铁路修建计划', start: '2024-03-12', end: '2024-03-20', progress: 10 },
    { id: 10011, title: '蓝天计划', start: '2024-03-02', end: '2024-03-42', progress: 0 },
    { id: 10012, title: 'C计划', start: '2024-03-05', end: '2024-03-14', progress: 90 }
  ]
})
</script>

设置颜色

image

<template>
  <div>
    <vxe-gantt v-bind="ganttOptions"></vxe-gantt>
  </div>
</template>

<script setup>
import { reactive } from 'vue'

const ganttOptions = reactive({
  border: true,
  taskBarConfig: {
    showProgress: true,
    barStyle: {
      round: true,
      bgColor: '#fca60b',
      completedBgColor: '#65c16f'
    }
  },
  columns: [
    { field: 'name', title: '任务名称' },
    { field: 'start', title: '开始时间', width: 100 },
    { field: 'end', title: '结束时间', width: 100 }
  ],
  data: [
    { id: 10001, name: 'A项目', start: '2024-03-01', end: '2024-03-04', progress: 3 },
    { id: 10002, name: '城市道路修理进度', start: '2024-03-03', end: '2024-03-08', progress: 10 },
    { id: 10003, name: 'B大工程', start: '2024-03-03', end: '2024-03-11', progress: 90 },
    { id: 10004, name: '超级大工程', start: '2024-03-05', end: '2024-03-11', progress: 15 },
    { id: 10005, name: '地球净化项目', start: '2024-03-08', end: '2024-03-15', progress: 100 },
    { id: 10006, name: '一个小目标项目', start: '2024-03-10', end: '2024-03-21', progress: 5 },
    { id: 10007, name: '某某计划', start: '2024-03-15', end: '2024-03-24', progress: 70 },
    { id: 10008, name: '某某科技项目', start: '2024-03-20', end: '2024-03-29', progress: 50 },
    { id: 10009, name: '地铁建设工程', start: '2024-03-19', end: '2024-03-20', progress: 5 },
    { id: 10010, name: '铁路修建计划', start: '2024-03-12', end: '2024-03-20', progress: 10 }
  ]
})
</script>

单选框

image

<template>
  <div>
    <vxe-gantt v-bind="ganttOptions"></vxe-gantt>
  </div>
</template>

<script setup>
import { reactive } from 'vue'

const ganttOptions = reactive({
  border: true,
  height: 300,
  rowConfig: {
    isHover: true
  },
  radioConfig: {
    labelField: 'title',
    highlight: true
  },
  taskBarConfig: {
    showProgress: true,
    showContent: true,
    barStyle: {
      round: true,
      bgColor: '#fca60b',
      completedBgColor: '#65c16f'
    }
  },
  taskViewConfig: {
    tableStyle: {
      width: 480
    }
  },
  columns: [
    { type: 'radio', title: '任务名称' },
    { field: 'start', title: '开始时间', width: 100 },
    { field: 'end', title: '结束时间', width: 100 }
  ],
  data: [
    { id: 10001, title: 'A项目', start: '2024-03-01', end: '2024-03-04', progress: 3 },
    { id: 10002, title: '城市道路修理进度', start: '2024-03-03', end: '2024-03-08', progress: 10 },
    { id: 10003, title: 'B大工程', start: '2024-03-03', end: '2024-03-11', progress: 90 },
    { id: 10004, title: '超级大工程', start: '2024-03-05', end: '2024-03-11', progress: 15 },
    { id: 10005, title: '地球净化项目', start: '2024-03-08', end: '2024-03-15', progress: 100 },
    { id: 10006, title: '一个小目标项目', start: '2024-03-10', end: '2024-03-21', progress: 5 },
    { id: 10007, title: '某某计划', start: '2024-03-15', end: '2024-03-24', progress: 70 },
    { id: 10008, title: '某某科技项目', start: '2024-03-20', end: '2024-03-29', progress: 50 },
    { id: 10009, title: '地铁建设工程', start: '2024-03-19', end: '2024-03-20', progress: 5 },
    { id: 10010, title: '铁路修建计划', start: '2024-03-12', end: '2024-03-20', progress: 10 }
  ]
})
</script>

复选框

image

<template>
  <div>
    <vxe-gantt v-bind="ganttOptions"></vxe-gantt>
  </div>
</template>

<script setup>
import { reactive } from 'vue'

const ganttOptions = reactive({
  border: true,
  height: 500,
  rowConfig: {
    isHover: true
  },
  checkboxConfig: {
    labelField: 'title',
    highlight: true
  },
  taskBarConfig: {
    showProgress: true,
    showContent: true,
    barStyle: {
      round: true,
      bgColor: '#fca60b',
      completedBgColor: '#65c16f'
    }
  },
  taskViewConfig: {
    tableStyle: {
      width: 480
    }
  },
  columns: [
    { type: 'checkbox', title: '任务名称' },
    { field: 'start', title: '开始时间', width: 100 },
    { field: 'end', title: '结束时间', width: 100 }
  ],
  data: [
    { id: 10001, title: 'A项目', start: '2024-03-01', end: '2024-03-04', progress: 3 },
    { id: 10002, title: '城市道路修理进度', start: '2024-03-03', end: '2024-03-08', progress: 10 },
    { id: 10003, title: 'B大工程', start: '2024-03-03', end: '2024-03-11', progress: 90 },
    { id: 10004, title: '超级大工程', start: '2024-03-05', end: '2024-03-11', progress: 15 },
    { id: 10005, title: '地球净化项目', start: '2024-03-08', end: '2024-03-15', progress: 100 },
    { id: 10006, title: '一个小目标项目', start: '2024-03-10', end: '2024-03-21', progress: 5 },
    { id: 10007, title: '某某计划', start: '2024-03-15', end: '2024-03-24', progress: 70 },
    { id: 10008, title: '某某科技项目', start: '2024-03-20', end: '2024-03-29', progress: 50 },
    { id: 10009, title: '地铁建设工程', start: '2024-03-19', end: '2024-03-20', progress: 5 },
    { id: 10010, title: '铁路修建计划', start: '2024-03-12', end: '2024-03-20', progress: 10 }
  ]
})
</script>

子任务

image

<template>
  <div>
    <vxe-gantt v-bind="ganttOptions"></vxe-gantt>
  </div>
</template>

<script setup>
import { reactive } from 'vue'

const ganttOptions = reactive({
  border: true,
  stripe: true,
  treeConfig: {
    transform: true,
    rowField: 'id',
    parentField: 'parentId'
  },
  taskBarConfig: {
    showProgress: true,
    showContent: true
  },
  taskViewConfig: {
    tableStyle: {
      width: 480
    }
  },
  columns: [
    { type: 'seq', width: 70 },
    { field: 'title', title: '任务名称', treeNode: true },
    { field: 'start', title: '开始时间', width: 100 },
    { field: 'end', title: '结束时间', width: 100 }
  ],
  data: [
    { id: 10001, parentId: null, title: 'A项目', start: '2024-03-01', end: '2024-03-04', progress: 3 },
    { id: 10002, parentId: 10001, title: '城市道路修理进度', start: '2024-03-03', end: '2024-03-08', progress: 10 },
    { id: 10003, parentId: null, title: 'B大工程', start: '2024-03-03', end: '2024-03-11', progress: 90 },
    { id: 10004, parentId: 10003, title: '超级大工程', start: '2024-03-05', end: '2024-03-11', progress: 15 },
    { id: 10005, parentId: 10003, title: '地球净化项目', start: '2024-03-08', end: '2024-03-15', progress: 100 },
    { id: 10006, parentId: 10003, title: '一个小目标项目', start: '2024-03-10', end: '2024-03-21', progress: 0 },
    { id: 10007, parentId: 10005, title: '某某计划', start: '2024-03-15', end: '2024-03-24', progress: 70 },
    { id: 10008, parentId: null, title: '某某科技项目', start: '2024-03-20', end: '2024-03-29', progress: 50 },
    { id: 10009, parentId: 10008, title: '地铁建设工程', start: '2024-03-19', end: '2024-03-20', progress: 5 },
    { id: 10010, parentId: 10008, title: '公寓装修计划2', start: '2024-03-12', end: '2024-03-20', progress: 30 },
    { id: 10011, parentId: 10008, title: '两个小目标工程', start: '2024-03-01', end: '2024-03-04', progress: 20 },
    { id: 10012, parentId: null, title: '蓝天计划', start: '2024-03-02', end: '2024-03-08', progress: 50 },
    { id: 10013, parentId: 10010, title: 'C大项目', start: '2024-03-08', end: '2024-03-11', progress: 10 },
    { id: 10014, parentId: 10010, title: 'H计划', start: '2024-03-12', end: '2024-03-16', progress: 100 },
    { id: 10015, parentId: 10011, title: '铁路修建计划', start: '2024-03-05', end: '2024-03-06', progress: 0 },
    { id: 10016, parentId: 10011, title: 'D项目', start: '2024-03-06', end: '2024-03-11', progress: 10 },
    { id: 10017, parentId: 10011, title: '海外改造工程', start: '2024-03-08', end: '2024-03-09', progress: 0 },
    { id: 10018, parentId: null, title: 'Z计划', start: '2024-03-24', end: '2024-03-26', progress: 80 },
    { id: 10019, parentId: 10018, title: 'F工程', start: '2024-03-20', end: '2024-03-28', progress: 10 },
    { id: 10020, parentId: 10018, title: '投资大项目', start: '2024-03-23', end: '2024-03-28', progress: 60 },
    { id: 10021, parentId: 10018, title: 'X计划', start: '2024-03-16', end: '2024-03-25', progress: 10 },
    { id: 10022, parentId: null, title: '上天计划', start: '2024-03-05', end: '2024-03-24', progress: 0 },
    { id: 10023, parentId: null, title: 'G项目', start: '2024-03-08', end: '2024-03-28', progress: 5 },
    { id: 10024, parentId: 10023, title: '下地计划', start: '2024-03-09', end: '2024-03-16', progress: 50 }
  ]
})
</script>

查询表单

image

<template>
  <div>
    <vxe-gantt v-bind="ganttOptions" v-on="ganttEvents"></vxe-gantt>
  </div>
</template>

<script setup>
import { reactive } from 'vue'

const ganttOptions = reactive({
  showOverflow: true,
  border: true,
  height: 500,
  taskBarConfig: {
    showProgress: true,
    showContent: true,
    barStyle: {
      round: true,
      bgColor: '#fca60b',
      completedBgColor: '#65c16f'
    }
  },
  taskViewConfig: {
    tableStyle: {
      width: 480
    }
  },
  formConfig: {
    data: {
      title: '',
      start: '',
      end: ''
    },
    items: [
      { field: 'title', title: '任务名称', itemRender: { name: 'VxeInput' } },
      { field: 'start', title: '开始时间', itemRender: { name: 'VxeDatePicker' } },
      { field: 'end', title: '结束时间', itemRender: { name: 'VxeDatePicker' } },
      {
        itemRender: {
          name: 'VxeButtonGroup',
          options: [
            { type: 'submit', content: '搜索', status: 'primary' },
            { type: 'reset', content: '重置' }
          ]
        }
      }
    ]
  },
  columns: [
    { type: 'seq', width: 70 },
    { field: 'title', title: '任务名称' },
    { field: 'start', title: '开始时间', width: 100 },
    { field: 'end', title: '结束时间', width: 100 }
  ],
  data: [
    { id: 10001, title: 'A项目', start: '2024-03-01', end: '2024-03-04', progress: 3 },
    { id: 10002, title: '城市道路修理进度', start: '2024-03-03', end: '2024-03-08', progress: 10 },
    { id: 10003, title: 'B大工程', start: '2024-03-03', end: '2024-03-11', progress: 90 },
    { id: 10004, title: '超级大工程', start: '2024-03-05', end: '2024-03-11', progress: 15 },
    { id: 10005, title: '地球净化项目', start: '2024-03-08', end: '2024-03-15', progress: 100 },
    { id: 10006, title: '一个小目标项目', start: '2024-03-10', end: '2024-03-21', progress: 5 },
    { id: 10007, title: '某某计划', start: '2024-03-15', end: '2024-03-24', progress: 70 },
    { id: 10008, title: '某某科技项目', start: '2024-03-20', end: '2024-03-29', progress: 50 },
    { id: 10009, title: '地铁建设工程', start: '2024-03-19', end: '2024-03-20', progress: 5 },
    { id: 10010, title: '铁路修建计划', start: '2024-03-12', end: '2024-03-20', progress: 10 }
  ]
})

const ganttEvents = {
  formSubmit () {
    console.log('form submit')
  },
  formReset () {
    console.log('form reset')
  }
}
</script>

数据分页

image

<template>
  <div>
    <vxe-gantt v-bind="ganttOptions" v-on="ganttEvents"></vxe-gantt>
  </div>
</template>

<script setup>
import { reactive } from 'vue'

const allList = [
  { id: 10001, title: 'A项目', start: '2024-03-01', end: '2024-03-04', progress: 3 },
  { id: 10002, title: '城市道路修理进度', start: '2024-03-03', end: '2024-03-08', progress: 10 },
  { id: 10003, title: 'B大工程', start: '2024-03-03', end: '2024-03-11', progress: 90 },
  { id: 10004, title: '超级大工程', start: '2024-03-05', end: '2024-03-11', progress: 15 },
  { id: 10005, title: '地球净化项目', start: '2024-03-08', end: '2024-03-15', progress: 100 },
  { id: 10006, title: '一个小目标项目', start: '2024-03-10', end: '2024-03-21', progress: 0 },
  { id: 10007, title: '某某计划', start: '2024-03-15', end: '2024-03-24', progress: 70 },
  { id: 10008, title: '某某科技项目', start: '2024-03-20', end: '2024-03-29', progress: 50 },
  { id: 10009, title: '地铁建设工程', start: '2024-03-19', end: '2024-03-20', progress: 5 },
  { id: 10010, title: '公寓装修计划2', start: '2024-03-12', end: '2024-03-20', progress: 30 },
  { id: 10011, title: '两个小目标工程', start: '2024-03-01', end: '2024-03-04', progress: 20 },
  { id: 10012, title: '蓝天计划', start: '2024-03-02', end: '2024-03-08', progress: 50 },
  { id: 10013, title: 'C大项目', start: '2024-03-08', end: '2024-03-11', progress: 10 },
  { id: 10014, title: 'H计划', start: '2024-03-12', end: '2024-03-16', progress: 100 },
  { id: 10015, title: '铁路修建计划', start: '2024-03-05', end: '2024-03-06', progress: 0 },
  { id: 10016, title: 'D项目', start: '2024-03-06', end: '2024-03-11', progress: 10 },
  { id: 10017, title: '海外改造工程', start: '2024-03-08', end: '2024-03-09', progress: 0 },
  { id: 10018, title: 'Z计划', start: '2024-03-24', end: '2024-03-26', progress: 80 },
  { id: 10019, title: 'F工程', start: '2024-03-20', end: '2024-03-28', progress: 10 },
  { id: 10020, title: '投资大项目', start: '2024-03-23', end: '2024-03-28', progress: 60 },
  { id: 10021, title: 'X计划', start: '2024-03-16', end: '2024-03-25', progress: 10 },
  { id: 10022, title: '上天计划', start: '2024-03-05', end: '2024-03-24', progress: 0 },
  { id: 10023, title: 'G项目', start: '2024-03-08', end: '2024-03-28', progress: 5 },
  { id: 10024, title: '下地计划', start: '2024-03-09', end: '2024-03-16', progress: 50 }
]

// 模拟前端分页
const handlePageData = () => {
  ganttOptions.loading = true
  setTimeout(() => {
    const { pageSize, currentPage } = pagerVO
    pagerVO.total = allList.length
    ganttOptions.data = allList.slice((currentPage - 1) * pageSize, currentPage * pageSize)
    ganttOptions.loading = false
  }, 100)
}

const pagerVO = reactive({
  total: 0,
  currentPage: 1,
  pageSize: 10
})

const ganttOptions = reactive({
  showOverflow: true,
  border: true,
  loading: false,
  height: 500,
  pagerConfig: pagerVO,
  taskBarConfig: {
    showProgress: true,
    showContent: true
  },
  taskViewConfig: {
    tableStyle: {
      width: 480
    }
  },
  columns: [
    { type: 'seq', width: 70 },
    { field: 'title', title: '任务名称' },
    { field: 'start', title: '开始时间', width: 100 },
    { field: 'end', title: '结束时间', width: 100 }
  ],
  data: []
})

const ganttEvents = {
  pageChange ({ pageSize, currentPage }) {
    pagerVO.currentPage = currentPage
    pagerVO.pageSize = pageSize
    handlePageData()
  }
}

handlePageData()
</script>

gitee.com/x-extends/v…

深入理解CSS Position:从基础到进阶与底层原理(下)

底层原理:渲染过程与定位参照系

理解CSS position属性的底层原理,需要我们对浏览器渲染页面的过程有一个基本的认识。这不仅能帮助我们更好地运用position,也能在遇到布局问题时,更有效地进行调试和优化。

1. 页面的渲染过程简述

浏览器将HTML、CSS和JavaScript代码转换为用户可见的像素,这个过程通常包括以下几个主要阶段:

  1. 解析HTML,生成DOM树(Document Object Model) :浏览器读取HTML文件,将其解析成一个树形结构,每个HTML标签都成为DOM树中的一个节点。DOM树描述了页面的内容和结构。
  2. 解析CSS,生成CSSOM树(CSS Object Model) :浏览器解析CSS文件(包括内联样式、内部样式表和外部样式表),为每个DOM节点计算出最终的样式。CSSOM树描述了页面的样式信息。
  3. 合并DOM树和CSSOM树,生成渲染树(Render Tree) :DOM树和CSSOM树合并后,形成渲染树。渲染树只包含需要渲染的可见元素(例如,display: none的元素不会包含在渲染树中),并且每个节点都包含了其计算后的样式信息。
  4. 布局阶段(Layout / Reflow) :也称为“重排”。在这个阶段,浏览器会根据渲染树计算每个可见元素在屏幕上的精确位置和大小。这个过程是递归的,从根元素开始,计算所有子元素的位置和尺寸。任何导致元素几何属性(如宽度、高度、边距、填充、定位等)变化的操作,都会触发重排。
  5. 绘制阶段(Paint / Repaint) :也称为“重绘”。在这个阶段,浏览器会根据布局阶段计算出的位置和大小,将渲染树的每个节点绘制到屏幕上。绘制涉及将元素的背景、颜色、边框、文本、阴影等视觉属性转换为屏幕上的像素。任何只改变元素样式而不影响其布局(如颜色、背景色、透明度等)的操作,都会触发重绘。
  6. 合成阶段(Compositing) :在现代浏览器中,绘制过程通常会分为多个图层进行。这些图层最终会被合成为一个完整的图像,呈现在屏幕上。某些CSS属性(如transformopacitywill-change等)可以促使元素被提升到独立的合成图层,从而利用GPU进行加速渲染。

2. 定位参照系:position属性的幕后逻辑

position属性的五种类型,其核心区别之一在于它们如何确定元素的“定位参照系”(Containing Block)。

  • static 没有定位参照系。元素在正常的文档流中,其位置由其在HTML中的顺序和周围元素决定。
  • relative 定位参照系是元素自身在正常文档流中的原始位置。toprightbottomleft属性会使元素相对于这个原始位置进行偏移,但它仍然占据原始空间。
  • absolute 定位参照系是其最近的非static祖先元素。如果一个absolute定位的元素的所有祖先元素都是static(默认值),那么它的定位参照系将是初始包含块,通常是<html>元素或<body>元素(取决于浏览器实现)。这意味着absolute元素会相对于整个文档的左上角进行定位。一旦找到非static的祖先,absolute元素就会相对于该祖先的内边距边缘进行定位。
  • fixed 定位参照系是浏览器视口(viewport) 。这意味着无论页面如何滚动,fixed元素都会保持在屏幕上的固定位置。它完全脱离文档流,不占据任何空间。
  • sticky 行为比较特殊。在未达到阈值时,其定位参照系是元素自身在正常文档流中的原始位置(类似于relative)。一旦达到阈值,它的定位参照系会变为其最近的具有滚动机制的祖先容器(如果存在,且该祖先的overflow属性不是visible),或者浏览器视口。它会在这个参照系内“粘”住,直到其父容器的边界超出参照系。

独立图层渲染与GPU硬件加速

为了提高渲染性能,现代浏览器会利用“分层”的概念。某些元素会被提升到独立的“合成图层”(Compositing Layer),这些图层可以独立于其他图层进行绘制和合成,从而利用GPU进行硬件加速。

1. 什么是独立图层?

想象一下Photoshop中的图层概念。网页渲染也类似,浏览器会将页面内容分成多个图层。当某个图层的内容发生变化时,只需要重绘该图层,然后将其与其他图层重新合成,而不需要重绘整个页面。这大大减少了渲染开销,尤其是在动画和复杂交互场景中。

2. 哪些CSS属性会创建独立图层?

以下是一些常见的会触发独立图层创建的CSS属性或条件:

  • transform (非none值,如translateZ(0)translate3d(0,0,0)):这是最常用的触发硬件加速的方式,通过将元素提升到独立图层,利用GPU进行位移、旋转、缩放等操作。
  • opacity (非1的值)
  • will-change:明确告诉浏览器元素将要发生哪些变化,浏览器可以提前进行优化,包括创建独立图层。
  • position: fixedposition: sticky:这些元素通常会被提升到独立图层,因为它们需要独立于页面滚动进行绘制。
  • z-index:在某些情况下,具有较高z-index的定位元素(relative, absolute, fixed, sticky)可能会被提升到独立图层。
  • filter (非none值)
  • perspective
  • mix-blend-mode (非normal值)
  • clip-path (非none值)
  • mask (非none值)
  • border-radius (在某些复杂情况下)
  • box-shadow (在某些复杂情况下)
  • videocanvasiframe等元素。

3. transform: translate3d(0,0,0);will-change

transform: translate3d(0,0,0); 是一种常见的“hack”技巧,用于强制浏览器将元素提升到独立的合成图层,从而启用GPU硬件加速。虽然它没有实际的3D位移效果,但它会告诉浏览器这个元素可能会进行3D变换,从而触发图层提升。这在一些需要高性能动画的场景中非常有用,例如登录弹窗的动画效果。

代码示例(参考 4.html):

<style>
    .container {
        padding: 100px;
    }
    .card {
        width: 200px;
        height: 120px;
        background: linear-gradient(45deg, #007bff, #00d8ff);
        border-radius: 12px;
        box-shadow: 0 4px 12px rgba(0,0,0,0.1);

        /* 关键:创建独立合成图层,启用 GPU 加速 */
        position: relative; /* 确保元素可以被提升 */
        transform: translateZ(0); /* 或使用:transform: translate3d(0, 0, 0); */
        will-change: transform; /* 明确告诉浏览器transform属性将要变化 */

        /* 动画使用 transform + opacity,不会触发重排重绘 */
        transition: transform 0.4s ease-out;
    }
    /* 鼠标悬停时平移并轻微放大 */
    .container:hover .card {
        transform: translateX(100px) scale(1.1);
    }
</style>
<div class="container">
    <div class="card"></div>
</div>

will-change属性则是一个更现代、更明确的优化手段。它允许开发者提前告知浏览器元素将要发生哪些变化,从而让浏览器在元素实际变化之前进行一些优化,例如创建独立的合成图层。这比translate3d(0,0,0)更具语义性,并且通常是更推荐的做法。

然而,需要注意的是,过度使用独立图层并非总是好事。 虽然独立图层可以带来性能提升,但创建和管理过多的图层会增加内存消耗和GPU负担,反而可能导致性能下降。因此,应谨慎使用,只在确实需要优化动画或复杂渲染的元素上使用。

position: fixed 的“失效”问题与解决方案

尽管fixed定位通常表现为相对于视口固定,但在某些特定情况下,它可能会“失效”,即不再相对于视口定位,而是相对于某个祖先元素定位。这通常发生在祖先元素应用了某些CSS属性时,这些属性会创建一个新的“堆叠上下文”(Stacking Context)或“包含块”(Containing Block)。

最常见导致fixed失效的场景是当其祖先元素应用了非nonetransform属性时。

1. 问题描述

当一个position: fixed的元素被放置在一个具有transform属性(例如transform: translateZ(0)transform: scale(1.1)等)的祖先元素内部时,这个fixed元素将不再相对于浏览器视口定位,而是相对于这个具有transform属性的祖先元素进行定位。这会使得fixed元素在滚动时不再保持固定,从而产生“失效”的假象。

代码示例(参考 5.html):

<style>
    body {
        margin: 0;
        height: 200vh; /* 制造滚动条 */
        padding: 20px;
        font-family: Arial;
    }
    .scroll-container {
        width: 300px;
        height: 400px;
        margin: 50px auto;
        border: 2px solid #007bff;
        overflow-y: auto;
        transform: translateZ(0); /* 关键:这个transform导致fixed失效 */
    }
    .content {
        position: fixed;
        top: 20px;
        right: 20px;
        width: 100px;
        height: 50px;
        background: red;
        color:white;
        text-align: center;
        line-height: 50px;
        font-size: 14px;
        border-radius: 8px;
    }
</style>
<h3>fixed 被transform容器限制实例</h3>
<p>滚动蓝色框, 观察红色块是否固定</p>
<div class="scroll-container">
    <div class="content">
        <div class="fixed-box">Fixed</div>
        <p>滚动我...</p>
    </div>
</div>

在这个例子中,fixed定位的.content元素本应固定在视口右上角,但由于其父元素.scroll-container应用了transform: translateZ(0),导致.content相对于.scroll-container定位,并在.scroll-container内部滚动时不再固定。

2. 原因分析

这是CSS规范中的一个特性,而非bug。当一个元素被应用了transformfilterperspective等属性时,它会创建一个新的“堆叠上下文”(Stacking Context)和“包含块”(Containing Block)。在这种情况下,其内部的fixed定位元素将不再相对于视口定位,而是相对于这个新的包含块进行定位。这是因为这些属性会改变元素的渲染方式,使其成为一个独立的渲染层,从而影响其内部元素的定位行为。

3. 解决方案

解决fixed失效问题最直接有效的方法是:

  • position: fixed的元素移出受影响的祖先元素。 确保fixed元素直接位于<body>元素下,或者其所有祖先元素都没有触发新的堆叠上下文或包含块的属性(如transformfilter等)。

在实际开发中,如果确实需要在一个具有transform的容器内部实现固定效果,可能需要考虑使用JavaScript来模拟fixed行为,或者重新评估布局结构,避免这种冲突。

总结与展望

通过本文上下两篇的深入探讨,我们全面解析了CSS position属性的五种基本类型:staticrelativeabsolutefixedsticky。我们不仅学习了它们的精确定义和行为,还结合了丰富的业务场景,如消息提醒徽章、模态框居中、回到顶部按钮和表格表头吸顶等,展示了position属性在实际开发中的强大应用能力。

更重要的是,我们深入到了position属性的底层原理,理解了浏览器渲染页面的基本过程,包括DOM树、CSSOM树、渲染树的构建,以及布局、绘制和合成阶段。我们还探讨了独立图层渲染的概念,以及transform: translate3d(0,0,0)will-change等属性如何利用GPU硬件加速来优化页面性能。同时,我们也揭示了position: fixed在特定transform场景下“失效”的常见问题及其背后的原因,并提供了相应的解决方案。

希望本文能为所有前端学习者和面试者提供有价值的参考,助您在前端开发的道路上更进一步。

深入理解CSS Position:从基础到进阶与底层原理(上)

引言

在前端开发中,CSS(层叠样式表)是构建网页视觉呈现的核心。而position属性,作为CSS布局的关键一环,其重要性不言而喻。它决定了元素在文档流中的定位方式,是实现复杂页面布局、交互效果以及响应式设计的基石。无论是简单的元素对齐,还是复杂的浮层、模态框、吸顶导航等,都离不开对position属性的灵活运用。然而,许多开发者对position的理解可能仅停留在表面,对其底层原理和潜在问题知之甚少。这不仅会限制他们在实际项目中的应用能力,也可能在面试中遇到挑战。

本文旨在深入剖析CSS position属性,从其五种基本类型入手,结合丰富的业务场景示例,逐步揭示其背后的渲染机制、独立图层概念以及常见的“坑点”。通过本文的学习,读者将不仅掌握position属性的用法,更能理解其工作原理,从而在面对各种布局需求时游刃有余,并在面试中展现出对CSS底层知识的深刻理解。

我们将首先详细介绍position的五种属性值,并通过具体代码示例展示它们在不同场景下的表现。随后,我们将探讨这些属性在实际业务中的常见应用模式,帮助读者将理论知识与实践相结合。在文章的下半部分,我们将深入探讨position属性的底层渲染原理,包括定位参照系、独立图层渲染以及GPU硬件加速等高级概念,并分析position: fixed在特定transform场景下失效的常见问题及其解决方案。希望通过这种由浅入深、理论结合实践的方式,为前端开发者,提供一份全面、详尽且具有深度的学习资料。

CSS position属性的五种类型

CSS position属性定义了元素在文档中的定位方式。理解这五种不同的定位类型是掌握CSS布局的关键。每种类型都有其独特的行为和应用场景。

1. static:默认值,不定位,回到文档流

static是所有HTML元素的默认position值。当一个元素的position属性设置为static时,它会按照正常的文档流进行布局。这意味着元素不会受到toprightbottomleftz-index属性的影响。它会紧随其前一个元素之后,并占据其在文档流中的正常位置。

特点:

  • 默认行为: 所有元素在没有明确设置position时,都默认为static
  • 遵循文档流: 元素完全按照HTML的结构顺序排列,不会脱离文档流。
  • 定位属性无效: toprightbottomleftz-index属性对其无效。
  • 取消定位: 它可以用于取消之前设置的定位,使元素回到正常的文档流中。

示例:

<div style="background-color: lightblue; padding: 10px;">这是一个普通块级元素。</div>
<span style="background-color: lightgreen; padding: 5px;">这是一个普通行内元素。</span>
<div style="position: static; background-color: lightcoral; padding: 10px; top: 20px; left: 20px;">这个元素设置了position: static,top和left属性无效。</div>

在上述示例中,即使第三个div设置了top: 20pxleft: 20px,它仍然会按照正常的文档流排列,因为position: static使其忽略了这些定位属性。

2. relative:相对自身原位置偏移,不脱离文档流

relative定位允许元素相对于其在正常文档流中的原始位置进行偏移。通过设置toprightbottomleft属性,可以将元素从其初始位置移动。然而,即使元素被移动了,它仍然占据着其原始位置的空间,不会影响周围元素的布局。这使得relative定位非常适合进行微调或作为absolute定位元素的参照。

特点:

  • 相对自身偏移: 元素相对于其在文档流中的原始位置进行偏移。
  • 不脱离文档流: 元素在文档流中仍然占据其原始空间,不会影响周围元素的布局。
  • 可作为参照: relative定位的元素可以作为其内部absolute定位元素的包含块(containing block)。
  • z-index有效: z-index属性对relative定位的元素有效,可以控制其堆叠顺序。

示例:

<div style="background-color: lightblue; padding: 10px;">这是一个普通元素。</div>
<div style="position: relative; top: 20px; left: 30px; background-color: lightgreen; padding: 10px;">这是一个相对定位的元素,向下偏移20px,向右偏移30px。</div>
<div style="background-color: lightcoral; padding: 10px;">这是另一个普通元素,它会受到上面相对定位元素原始位置的影响。</div>

在这个例子中,绿色div虽然向下和向右移动了,但它在文档流中仍然占据着原来的位置,所以红色div会紧随其原始位置之后。

3. absolute:相对最近的非static祖先定位,脱离文档流

absolute定位的元素会完全脱离正常的文档流。这意味着它不再占据空间,其原始位置会被其他元素填充。absolute定位的元素会相对于其最近的、position属性不是static的祖先元素进行定位。如果找不到这样的祖先元素,它将相对于初始包含块(通常是<body>元素)进行定位。

特点:

  • 脱离文档流: 元素不再占据空间,不影响周围元素的布局。
  • 相对非static祖先定位: 定位参照系是最近的非static祖先元素。如果所有祖先都是static,则相对于<body>
  • toprightbottomleft有效: 这些属性定义了元素相对于其包含块的偏移量。
  • z-index有效: z-index属性对absolute定位的元素有效,可以控制其堆叠顺序。

示例:

<div style="position: relative; width: 300px; height: 150px; border: 2px solid blue; padding: 20px;">
    这是一个相对定位的父容器。
    <div style="position: absolute; top: 20px; left: 20px; background-color: orange; padding: 10px;">这是一个绝对定位的子元素。</div>
</div>
<div style="background-color: lightgray; padding: 10px; margin-top: 20px;">这是一个普通元素,它会忽略上面绝对定位元素所占据的空间。</div>

在这个例子中,橙色div相对于蓝色父容器进行定位,并且它脱离了文档流,所以灰色div会紧随蓝色父容器之后,而不是橙色div之后。

4. fixed:相对浏览器窗口定位,脱离文档流

fixed定位的元素与absolute定位类似,也会脱离正常的文档流。但不同的是,fixed定位的元素是相对于浏览器视口(viewport)进行定位的。这意味着即使页面滚动,fixed定位的元素也会保持在屏幕上的固定位置。这使得它非常适合创建固定头部、底部导航、返回顶部按钮或聊天客服图标等。

特点:

  • 脱离文档流: 元素不再占据空间,不影响周围元素的布局。
  • 相对视口定位: 定位参照系是浏览器视口,不随页面滚动而移动。
  • toprightbottomleft有效: 这些属性定义了元素相对于视口的偏移量。
  • z-index有效: z-index属性对fixed定位的元素有效,可以控制其堆叠顺序。

示例:

<div style="height: 1000px; background-color: #f0f0f0;">页面内容,用于产生滚动条</div>
<button style="position: fixed; bottom: 20px; right: 20px; padding: 10px 20px; background-color: #007bff; color: white; border: none; border-radius: 5px;">返回顶部</button>

当页面滚动时,“返回顶部”按钮会始终保持在浏览器视口的右下角。

5. sticky:粘性定位,结合了relativefixed的特性

sticky定位是一种相对较新的CSS定位方式,它结合了relativefixed的特性。在元素滚动到特定阈值之前,它表现得像relative定位,即在文档流中占据空间并随页面滚动。一旦滚动达到设定的阈值(例如top: 0),它就会“粘”在视口或其最近的滚动祖先的指定位置,表现得像fixed定位,直到其父容器的边界超出视口。这使得sticky非常适合实现吸顶导航、侧边栏滚动跟随或表格表头吸顶等效果。

特点:

  • 混合行为: 在达到阈值前是relative,达到阈值后是fixed
  • 不脱离文档流(初始): 在未达到阈值时,它仍然占据文档流中的空间。
  • 相对滚动祖先或视口定位: 粘性定位的参照系是其最近的具有滚动机制的祖先容器(如果存在)或视口。
  • toprightbottomleft有效: 这些属性定义了元素“粘”住时的偏移量。
  • z-index有效: z-index属性对sticky定位的元素有效。

示例:

<style>
    .table-container {
        height: 300px; /* 模拟滚动区域 */
        overflow-y: auto;
        border: 1px solid #ccc;
    }
    table {
        width: 100%;
        border-collapse: collapse;
    }
    thead th {
        position: sticky;
        top: 0; /* 当滚动到距离其最近的具有滚动机制的祖先容器的顶部0px时,开始吸顶 */
        background-color: #007bff;
        color: white;
        padding: 12px;
        text-align: left;
        z-index: 10;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }
    tbody td {
        padding: 10px;
        border-bottom: 1px solid #ddd;
    }
</style>
<div class="table-container">
    <table>
        <thead>
            <tr><th>姓名</th><th>年龄</th><th>城市</th><th>职业</th></tr>
        </thead>
        <tbody>
            <!-- 更多行数据以产生滚动 -->
            <tr><td>张三</td><td>28</td><td>北京</td><td>工程师</td></tr>
            <tr><td>李四</td><td>32</td><td>上海</td><td>设计师</td></tr>
            <tr><td>王五</td><td>25</td><td>广州</td><td>产品经理</td></tr>
            <tr><td>赵六</td><td>30</td><td>深圳</td><td>前端开发</td></tr>
            <tr><td>钱七</td><td>27</td><td>杭州</td><td>数据分析师</td></tr>
            <tr><td>孙八</td><td>35</td><td>成都</td><td>架构师</td></tr>
            <tr><td>周九</td><td>29</td><td>武汉</td><td>测试工程师</td></tr>
            <tr><td>吴十</td><td>31</td><td>南京</td><td>运维</td></tr>
            <tr><td>郑一</td><td>26</td><td>西安</td><td>UI设计师</td></tr>
            <tr><td>陈二</td><td>33</td><td>重庆</td><td>项目经理</td></tr>
            <tr><td>冯三</td><td>24</td><td>长沙</td><td>实习生</td></tr>
            <tr><td>朱四</td><td>36</td><td>天津</td><td>技术总监</td></tr>
            <tr><td>秦五</td><td>28</td><td>青岛</td><td>后端开发</td></tr>
            <tr><td>何六</td><td>30</td><td>大连</td><td>全栈开发</td></tr>
            <tr><td>许七</td><td>27</td><td>厦门</td><td>移动开发</td></tr>
        </tbody>
    </table>
</div>

在这个表格示例中,当用户滚动.table-container时,表头<th>会在滚动到容器顶部时“粘”住,保持可见,直到整个表格内容滚出视线。

常见业务场景

理解了position的五种基本类型后,我们来看看它们在实际前端开发中是如何被巧妙地组合和应用的。掌握这些常见模式,能够帮助我们更高效地解决布局问题。

1. 结合relative + absolute 实现消息提醒或徽章

这是position属性最经典且常用的组合之一。通过将父元素设置为position: relative,子元素设置为position: absolute,可以实现子元素相对于父元素的精确定位,而不会影响父元素周围的布局。

场景: 在按钮、头像或图标的右上角添加一个消息提醒的小红点(徽章)。

实现原理:

  1. 父元素 position: relative 将需要添加徽章的父容器(如按钮、div)设置为relative。这使得该父容器成为其内部absolute定位元素的参照系。
  2. 子元素 position: absolute 将徽章元素设置为absolute。这样它就会脱离文档流,不再占据空间,并且可以相对于其relative定位的父元素进行定位。
  3. topright定位: 使用top: 0right: 0将徽章定位到父元素的右上角。
  4. transform: translate(50%, -50%)微调: 为了让徽章的中心点恰好位于父元素的右上角边缘,通常会使用transform: translate(50%, -50%)translate(50%, -50%)表示向右移动自身宽度的一半,向上移动自身高度的一半,从而实现精确的居中对齐效果。

代码示例(参考 1.html):

<style>
    .btn-wrapper {
        position: relative;
        display: inline-block; /* 使父容器包裹内容,以便徽章能正确相对定位 */
        margin: 50px;
    }
    .btn {
        padding: 12px 20px;
        font-size: 16px;
        background-color: #007bff;
        color: white;
        border: none;
        border-radius: 6px;
        cursor: pointer;
    }
    .badge {
        position: absolute;
        top: 0;
        right: 0;
        transform: translate(50%, -50%); /* 关键:向右偏移自身宽度50%,向上偏移自身高度50% */
        width: 12px;
        height: 12px;
        background-color: red;
        border-radius: 50%;
        box-shadow: 0 0 4px rgba(0,0,0,0.3);
    }
</style>
<div class="btn-wrapper">
    <button class="btn">消息中心</button>
    <span class="badge"></span>
</div>

这个模式非常灵活,可以应用于各种需要叠加元素的场景,如购物车图标上的商品数量、新消息提示等。

2. absolute + transform 实现水平垂直居中(模态框)

在CSS中实现元素的水平垂直居中是一个常见需求,尤其是在设计模态框(Modal)、弹窗或加载动画时。absolute结合transform是一种非常高效且兼容性良好的居中方案。

场景: 页面中央的模态框、图片预览弹窗、加载动画。

实现原理:

  1. 父元素 position: relative(可选): 如果模态框需要相对于某个特定容器居中,则该容器需要设置为position: relative。如果模态框需要相对于整个视口居中,则无需设置父元素,直接让模态框相对于<body>定位。
  2. 子元素 position: absolute 将需要居中的元素设置为absolute,使其脱离文档流。
  3. top: 50%; left: 50% 将元素的左上角定位到其包含块的中心点。此时,元素会以其左上角为基准点进行定位,导致元素整体偏右下。
  4. transform: translate(-50%, -50%) 这是实现精确居中的关键一步。translate(-50%, -50%)表示将元素向左移动自身宽度的一半,向上移动自身高度的一半。这样,元素的中心点就恰好与包含块的中心点对齐,实现了完美的水平垂直居中。

代码示例:

<style>
    body {
        margin: 0;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
        background-color: rgba(0,0,0,0.5); /* 模拟背景遮罩 */
    }
    .modal {
        position: absolute; /* 相对于body或视口定位 */
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%); /* 核心居中技巧 */
        width: 400px;
        height: 250px;
        background-color: white;
        border-radius: 8px;
        box-shadow: 0 4px 15px rgba(0,0,0,0.2);
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 24px;
        color: #333;
    }
</style>
<div class="modal">这是一个居中的模态框</div>

这种方法不仅代码简洁,而且由于transform属性不会触发重排(reflow)和重绘(repaint),只触发合成(composite),因此具有非常好的性能。

3. fixed 实现回到顶部按钮或聊天客服图标

fixed定位最直观的应用就是创建那些需要始终保持在屏幕特定位置的元素,无论用户如何滚动页面。

场景: 网页右下角的“回到顶部”按钮、悬浮的在线客服图标、固定在顶部的广告条。

实现原理:

  1. position: fixed 将元素设置为fixed,使其脱离文档流并相对于视口定位。
  2. bottomright(或topleft)定位: 根据需求,使用bottomright(或topleft)属性来确定元素在视口中的具体位置。
  3. z-index 通常会设置一个较高的z-index值,以确保这些固定元素能够覆盖页面上的其他内容,避免被遮挡。

代码示例:

<style>
    body {
        margin: 0;
        height: 200vh; /* 制造滚动条 */
        font-family: Arial, sans-serif;
    }
    .content-placeholder {
        height: 150vh;
        background-color: #f0f0f0;
        padding: 20px;
        text-align: center;
        font-size: 20px;
        line-height: 1.5;
    }
    .back-to-top {
        position: fixed;
        bottom: 30px;
        right: 30px;
        width: 50px;
        height: 50px;
        background-color: #28a745;
        color: white;
        border-radius: 50%;
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 14px;
        cursor: pointer;
        box-shadow: 0 2px 10px rgba(0,0,0,0.2);
        z-index: 1000;
    }
</style>
<div class="content-placeholder">向下滚动以查看“回到顶部”按钮的效果。</div>
<div class="back-to-top">顶部</div>

当页面滚动时,“顶部”按钮会始终固定在视口的右下角,为用户提供便捷的导航功能。

4. sticky 实现粘连导航或表格表头吸顶

sticky定位是实现“吸顶”效果的理想选择,它比传统的JavaScript实现方式更具性能优势,并且代码更简洁。

场景: 网站的顶部导航栏在滚动时吸顶、侧边栏的目录在滚动到一定位置时固定、表格的表头在滚动时始终可见。

实现原理:

  1. position: sticky 将需要吸顶的元素设置为sticky
  2. top(或bottomleftright)阈值: 设置一个偏移量(例如top: 0)。当元素滚动到距离其最近的滚动祖先(或视口)的这个偏移量时,它就会“粘”住。
  3. 父容器的overflow属性: sticky元素会受到其最近的拥有overflow属性(如overflow: autoscrollhidden)的祖先容器的影响。如果父容器没有滚动条,或者sticky元素本身的高度超出了父容器,其行为可能会不符合预期。
  4. z-index 同样,为了确保吸顶元素在其他内容之上,通常会设置一个合适的z-index

代码示例(参考 2.html):

<style>
    body {
        font-family: Arial, sans-serif;
        margin: 0;
        padding: 20px;
    }
    .table-container {
        height: 300px; /* 模拟滚动区域 */
        overflow-y: auto; /* 关键:提供滚动机制 */
        border: 1px solid #ccc;
    }
    table {
        width: 100%;
        border-collapse: collapse;
        margin: 0;
    }
    thead th {
        position: sticky;
        top: 0; /* 当滚动到距离视口顶部 0px 时,开始吸顶 */
        background-color: #007bff;
        color: white;
        padding: 12px;
        text-align: left;
        z-index: 10; /* 确保在其他内容之上 */
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }
    tbody td {
        padding: 10px;
        border-bottom: 1px solid #ddd;
    }
    tbody tr:hover {
        background-color: #f5f5f5;
    }
</style>
<h2>滚动时表头吸顶示例</h2>
<div class="table-container">
    <table>
        <thead>
            <tr><th>姓名</th><th>年龄</th><th>城市</th><th>职业</th></tr>
        </thead>
        <tbody>
            <!-- 重复多行数据以产生滚动 -->
            <tr><td>张三</td><td>28</td><td>北京</td><td>工程师</td></tr>
            <tr><td>李四</td><td>32</td><td>上海</td><td>设计师</td></tr>
            <tr><td>王五</td><td>25</td><td>广州</td><td>产品经理</td></tr>
            <tr><td>赵六</td><td>30</td><td>深圳</td><td>前端开发</td></tr>
            <tr><td>钱七</td><td>27</td><td>杭州</td><td>数据分析师</td></tr>
            <tr><td>孙八</td><td>35</td><td>成都</td><td>架构师</td></tr>
            <tr><td>周九</td><td>29</td><td>武汉</td><td>测试工程师</td></tr>
            <tr><td>吴十</td><td>31</td><td>南京</td><td>运维</td></tr>
            <tr><td>郑一</td><td>26</td><td>西安</td><td>UI设计师</td></tr>
            <tr><td>陈二</td><td>33</td><td>重庆</td><td>项目经理</td></tr>
            <tr><td>冯三</td><td>24</td><td>长沙</td><td>实习生</td></tr>
            <tr><td>朱四</td><td>36</td><td>天津</td><td>技术总监</td></tr>
            <tr><td>秦五</td><td>28</td><td>青岛</td><td>后端开发</td></tr>
            <tr><td>何六</td><td>30</td><td>大连</td><td>全栈开发</td></tr>
            <tr><td>许七</td><td>27</td><td>厦门</td><td>移动开发</td></tr>
        </tbody>
    </table>
</div>

这个示例展示了如何在表格中实现表头吸顶。当用户滚动.table-container时,表头会固定在顶部,方便用户查看数据。sticky定位的这种行为与IntersectionObserver API在某些场景下有异曲同工之妙,但sticky是纯CSS实现,性能更优。

通过上述对position五种类型的详细介绍和常见业务场景的分析,我们已经对position属性有了全面的认识。在文章的下半部分,我们将深入探讨position属性的底层原理,包括渲染过程、独立图层以及fixed定位的常见陷阱,帮助读者构建更扎实的CSS知识体系。

---待续---

Flutter 插件工作原理深度解析:从 Dart 到 Native 的完整调用链路

作为 Flutter 开发者,我们经常使用各种插件来访问原生平台功能,但很少深入了解插件是如何工作的。本文将从源码层面深入分析 Flutter 插件的完整工作机制,揭示从 Dart 代码到 Native 代码的完整调用链路。

插件架构概览

Flutter 插件采用了一种基于消息传递的架构,通过 Platform Channel 实现 Dart 层与 Native 层的双向通信。整个架构可以分为以下几个核心组件:

  • Dart 层:插件的 Dart API 接口
  • Platform Channel:消息传递通道
  • Binary Messenger:二进制消息传输器
  • Platform Dispatcher:平台消息分发器
  • Native 层:原生平台实现

插件注册机制深度解析

1. 自动注册流程

当 Flutter 应用启动时,会自动执行插件注册流程。这个过程始于 GeneratedPluginRegistrant.register(with: self) 调用:

// iOS 端自动生成的注册代码
public static func register(with registry: FlutterPluginRegistry) {
    MyFlutterPlugin.register(with: registry.registrar(forPlugin: "MyFlutterPlugin"))
    // 其他插件注册...
}

2. 插件实例注册

每个插件都需要向 Flutter 引擎注册自己的实例和方法通道:

[MyFlutterPlugin registerWithRegistrar:[registry registrarForPlugin:@"MyFlutterPlugin"]];

这里的 registrar 是一个关键对象,它提供了插件与 Flutter 引擎交互的所有必要接口。

3. 方法调用委托绑定

插件注册的核心步骤是建立方法调用委托关系:

[registrar addMethodCallDelegate:myPluginInstance channel:myPluginChannel];

这一步将插件实例与特定的方法通道绑定,使得通过该通道的所有方法调用都会路由到插件实例。

4. 方法调用处理器设置

最关键的一步是设置方法调用处理器:

[channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
    [delegate handleMethodCall:call result:result];
}];

5. 底层消息处理器注册

在 Flutter 引擎的 C++ 层,会将处理器注册到全局的消息处理器映射表中:

message_handlers_[channel] = {
    .handler = fml::ScopedBlock<FlutterBinaryMessageHandler>{
        handler, 
        fml::scoped_policy::OwnershipPolicy::kRetain
    },
};

这个 message_handlers_ 是一个全局的哈希表,以 channel 名称为 key,存储所有注册的消息处理器。

方法调用执行流程深度剖析

1. Dart 层方法调用

当 Dart 代码调用插件方法时:

final result = await myPluginInstance.method1(parameters);

实际上会转换为 Platform Channel 的方法调用:

return await _channel.invokeMethod<bool>('method1', parameters);

2. Binary Messenger 消息发送

invokeMethod 内部会调用 BinaryMessenger.send()

binaryMessenger.send(name, input);

这里的 name 是 channel 名称,input 是序列化后的方法调用数据。

3. Platform Dispatcher 分发

消息最终通过 PlatformDispatcher 发送到原生层:

ui.PlatformDispatcher.instance.sendPlatformMessage(channel, message, callback);

PlatformDispatcher 是 Flutter 引擎与原生平台之间的桥梁,负责所有跨平台消息的分发。

4. 原生层消息路由

在 Flutter 引擎的 C++ 层,会根据 channel 名称查找对应的处理器:

auto it = message_handlers_.find(message->channel());
if (it != message_handlers_.end()) {
    handler_info = it->second;
}

5. 异步处理器执行

找到处理器后,会在主队列中异步执行:

dispatch_block_t run_handler = ^{
    handler(data, ^(NSData* reply) {
        // 处理回调
    });
};
dispatch_async(dispatch_get_main_queue(), run_handler);

6. 插件方法执行

最终调用到插件实现的 handleMethodCall 方法:

- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
    if ([@"method1" isEqualToString:call.method]) {
        // 执行具体的原生功能
        result(@"success");
    } else {
        result(FlutterMethodNotImplemented);
    }
}

核心技术细节分析

1. 消息序列化机制

Flutter 使用 StandardMessageCodec 进行消息序列化,支持以下数据类型:

  • 基本类型(null, bool, int, double, String)
  • 集合类型(List, Map)
  • 字节数组(Uint8List)

序列化过程确保了跨平台数据传输的一致性。

2. 内存管理策略

在消息处理器注册时,使用了 fml::ScopedBlockOwnershipPolicy::kRetain 来管理内存:

fml::ScopedBlock<FlutterBinaryMessageHandler>{
    handler, 
    fml::scoped_policy::OwnershipPolicy::kRetain
}

这确保了处理器在插件生命周期内不会被意外释放。

3. 线程安全保障

Flutter 插件的方法调用涉及多个线程:

  • UI Thread:Dart 代码执行
  • Platform Thread:原生代码执行
  • IO Thread:消息传递

通过 dispatch_async(dispatch_get_main_queue(), run_handler) 确保原生方法在主线程执行,避免线程安全问题。

4. 错误处理机制

插件调用支持完整的错误处理:

  • FlutterMethodNotImplemented:方法未实现
  • FlutterError:自定义错误
  • 异常传播:原生异常可以传播到 Dart 层

性能优化考虑

1. 消息传递开销

每次插件调用都涉及:

  • 消息序列化/反序列化
  • 跨线程消息传递
  • 原生方法查找和执行

对于高频调用场景,应考虑批量处理或缓存策略。

2. 内存使用优化

  • 使用 ScopedBlock 进行自动内存管理
  • 避免在消息处理器中持有大对象引用
  • 及时释放不需要的资源

3. 异步处理优势

Flutter 插件的异步设计避免了 UI 线程阻塞,但也要注意:

  • 合理控制并发调用数量
  • 避免回调地狱
  • 使用 Future/async-await 模式

总结

Flutter 插件的工作原理体现了优秀的跨平台架构设计:

  1. 清晰的分层架构:Dart 层、通道层、原生层职责分明
  2. 高效的消息传递:基于二进制消息的轻量级通信
  3. 完善的生命周期管理:自动注册、内存管理、错误处理
  4. 良好的扩展性:支持自定义编解码器和通道类型

理解这些底层机制不仅有助于更好地使用现有插件,也为开发高质量的自定义插件提供了理论基础。在实际开发中,我们应该充分利用 Flutter 插件架构的优势,同时注意性能优化和错误处理,构建稳定可靠的跨平台应用。

👶 小孩报数问题:当熊孩子遇上“约瑟夫环

👶 小孩报数问题:当熊孩子遇上“约瑟夫环”

哈喽,各位掘友们!你有没有想过,一群天真无邪的小朋友围成一圈,玩着报数游戏,结果报到特定数字的就要“出局”?这听起来是不是有点“残忍”?😂 但别担心,我们今天不是来搞“淘汰赛”的,而是要用代码来揭秘这个经典算法问题——约瑟夫环问题(Josephus Problem)的一个变种!

准备好了吗?系好安全带,咱们这就出发,一起看看这群“熊孩子”是怎么玩转算法的!🚀

❓ 问题来了:谁是最后的幸存者?

场景描述

想象一下,有30个小朋友,编号从1到30,手拉手围成一个大大的圆圈。他们要开始报数了,从1号小朋友开始,依次报1、2、3……报到3的小朋友就要“出局”,然后下一个小朋友(也就是原来报4的小朋友)重新从1开始报数。如此循环往复,直到只剩下最后一个小朋友。那么问题来了,这位“天选之子”的编号是多少呢?

是不是听起来有点像“鱿鱼游戏”?别怕,我们只是用代码模拟,没有真的小朋友会受伤!😉

🔧 揭秘算法:代码是这样玩的!

为了解决这个问题,我们请出了一段神奇的JavaScript代码。别看它其貌不扬,里面可是藏着解决问题的“大智慧”!

function childNum(num, count){
    let allPlayer = [];
    for(let i = 0; i < num; i++){
        allPlayer[i] = i + 1;
    }

    let exitCount = 0; // 离开人数
    let counter = 0;   // 记录报数
    let curIndex = 0;  // 当前下标

    while(exitCount < num - 1){
        if(allPlayer[curIndex] !== 0) counter++;

        if(counter === count){
            allPlayer[curIndex] = 0;
            counter = 0;
            exitCount++;
        }
        curIndex++;
        if(curIndex === num){
            curIndex = 0
        }
    }

    for(let i = 0; i < num; i++){
        if(allPlayer[i] !== 0){
            return allPlayer[i]
        }
    }
}

childNum(30, 3);

💡 代码解读:一步步拆解“报数”过程

1. 📝 初始化:小朋友们,各就各位!

首先,childNum(num, count) 函数接收两个参数:num 代表小朋友的总人数(这里是30),count 代表报到几就“出局”(这里是3)。

    let allPlayer = [];
    for(let i = 0; i < num; i++){
        allPlayer[i] = i + 1;
    }

这段代码创建了一个数组 allPlayer,用来模拟围成一圈的小朋友。数组的每个元素就是小朋友的编号。比如,allPlayer[0] 就是1号小朋友,allPlayer[29] 就是30号小朋友。我们用 0 来表示已经“出局”的小朋友。

接着,我们有三个重要的“计数器”:

    let exitCount = 0; // 离开人数:记录已经有多少小朋友“出局”了
    let counter = 0;   // 记录报数:当前小朋友报的数(1、2、3...)
    let curIndex = 0;  // 当前下标:当前正在报数的小朋友在数组中的位置
2. 🔄 循环报数:谁是下一个“幸运儿”?

核心逻辑都在这个 while 循环里。它会一直运行,直到只剩下最后一个小朋友(exitCount < num - 1)。

    while(exitCount < num - 1){
        if(allPlayer[curIndex] !== 0) counter++;

        if(counter === count){
            allPlayer[curIndex] = 0;
            counter = 0;
            exitCount++;
        }
        curIndex++;
        if(curIndex === num){
            curIndex = 0
        }
    }
  • if(allPlayer[curIndex] !== 0) counter++;

    • 这行代码是关键!它检查当前小朋友是否还在圈内(即 allPlayer[curIndex] 不为0)。如果还在,counter 就加1,表示他报了一个数。
    • 如果小朋友已经“出局”了(allPlayer[curIndex] 是0),那么他就不会报数,counter 也不会增加。这很合理,毕竟“人都没了”,还怎么报数呢?😂
  • if(counter === count){ ... }

    • counter 等于我们设定的 count 值(这里是3)时,说明当前小朋友报到了“出局”的数字。
    • allPlayer[curIndex] = 0;:这位小朋友“光荣出局”,我们把他的编号设为0。
    • counter = 0;:报数重新从1开始,所以 counter 重置为0。
    • exitCount++;:离开人数加1,记录又有一个小朋友“出局”了。
  • curIndex++;if(curIndex === num){ curIndex = 0 }

    • 这两行代码负责让报数的小朋友“轮流上岗”。curIndex 每次循环都会加1,指向下一个小朋友。
    • 如果 curIndex 到了数组的末尾(num),说明已经绕了一圈,需要回到数组的开头(0),形成一个完美的“环”!🔄
3. 🏆 寻找幸存者:谁是最后的赢家?

while 循环结束时,意味着只剩下最后一个小朋友了。那么,怎么找到他呢?

    for(let i = 0; i < num; i++){
        if(allPlayer[i] !== 0){
            return allPlayer[i]
        }
    }

这段代码很简单粗暴:遍历 allPlayer 数组,找到那个唯一一个不为0的元素,它就是我们苦苦寻找的“天选之子”的编号!🎉

🧪 运行一下:答案揭晓!

最后,我们调用 childNum(30, 3) 来运行这个游戏。根据代码逻辑,最终会返回最后一个幸存者的编号。

那么,30个小朋友,报数到3出局,最后剩下的是几号小朋友呢?

答案是29,快去试试吧!

总结与思考

算法小结

这个“小孩报数问题”其实是经典的约瑟夫环问题的一种模拟解法。它的核心思想是:

  1. 模拟过程:用数组模拟环形结构,用特定值(0)标记出局者。
  2. 循环计数:通过 counter 变量实现报数功能,遇到出局者跳过。
  3. 环形遍历:通过 curIndex 的重置实现循环报数。

这种模拟方法虽然直观易懂,但当人数非常多时,效率可能会降低。对于大规模的约瑟夫环问题,通常会有更高效的数学解法(例如通过递推公式)。但对于面试或日常小问题,这种模拟解法已经足够清晰和优雅了!

💖 掘友们,你们怎么看?

你有没有遇到过类似的场景题?或者你有什么更风趣幽默的解释方式?欢迎在评论区留言,一起交流学习!

别忘了点赞👍、收藏⭐、转发↗️,让更多掘友看到这篇“不正经”的算法科普文!我们下期再见!👋

Git 日常使用与面试考点详解:从入门到精通

在现代软件开发中,版本控制系统(Version Control System, VCS) 是不可或缺的工具。它帮助开发者追踪代码的每一次变更、协作开发、回滚错误、管理分支与发布版本。而在众多版本控制工具中,Git 凭借其分布式架构、高效性能、强大功能和广泛生态,已成为全球开发者事实上的标准。

本文将从 Git 的基本概念讲起,深入介绍其在日常开发中的使用流程、协作模式、PR(Pull Request)机制,以及如何在团队中高效使用 Git。最后,我们将系统梳理 Git 在技术面试中的常见考点,并提供高质量的回答示例,帮助开发者不仅“会用”,更能“讲清楚”Git 的核心原理与最佳实践。


一、Git 简介:什么是 Git?

Git 是由 Linux 之父 Linus Torvalds 于 2005 年开发的分布式版本控制系统。它的设计初衷是高效管理 Linux 内核的开发,因此具备极高的性能和灵活性。

1.1 什么是版本控制?

版本控制是一种记录文件(尤其是代码)变化历史的系统。它允许你:

  • 查看文件的历史版本
  • 回滚到任意历史状态
  • 比较不同版本之间的差异
  • 协作开发,避免覆盖他人代码
  • 管理多个功能开发线(分支)

1.2 集中式 vs 分布式

  • 集中式版本控制(如 SVN):所有代码历史存储在中央服务器上,开发者本地只保留当前版本。必须联网才能提交。
  • 分布式版本控制(如 Git):每个开发者都拥有完整的代码仓库副本,包含全部历史记录。可以在本地提交、查看历史、创建分支,无需联网。这大大提升了开发效率和容错能力。

Git 的“分布式”特性意味着:

  • 每个开发者都是一个“备份”
  • 本地操作极快(无需网络)
  • 支持离线开发
  • 更灵活的协作模型(如 Fork + PR)

二、Git 基础概念与核心对象

在深入使用 Git 之前,我们需要理解其核心概念和数据模型。

2.1 Git 的三大区域

Git 将文件的状态分为三个区域:

  1. 工作区(Working Directory):你当前正在编辑的文件。
  2. 暂存区(Staging Area / Index):通过 git add 命令将修改的文件放入暂存区,准备提交。
  3. 本地仓库(Repository):通过 git commit 将暂存区的内容提交到本地仓库,形成一个版本快照。
# 工作流程:
# 修改文件 → git add → git commit → git push

2.2 Git 的核心对象

Git 以对象(Object)的形式存储数据,主要有四种:

  1. Blob(二进制大对象):存储文件内容,不包含文件名或权限。
  2. Tree:相当于目录,存储文件名、权限和指向 Blob 或其他 Tree 的指针。
  3. Commit:表示一次提交,包含作者、时间、提交信息、父提交指针和指向根 Tree 的指针。
  4. Tag:给某个 Commit 打标签,常用于标记版本(如 v1.0.0)。

这些对象通过 SHA-1 哈希值唯一标识,形成一个有向无环图(DAG),记录了项目的完整历史。


三、Git 日常开发流程详解

下面我们结合一个典型的开发场景,详细讲解 Git 的日常使用。

3.1 开发环境搭建

在开始使用 Git 之前,需要安装必要的开发环境:

# 1. 安装 Node.js(可选,根据项目需求)
# 下载地址:https://nodejs.org

# 2. 安装 Git
# 下载地址:https://git-scm.com

# 3. 配置全局用户信息(公司通常会发放 Git 账号)
git config --global user.name "Your Name"
git config --global user.email "your.email@company.com"

# 可选:设置默认编辑器
git config --global core.editor "code --wait"

注意:公司通常使用私有 Git 仓库(如 GitLab、GitHub Enterprise、Bitbucket),你需要使用公司提供的账号进行身份认证(SSH 或 HTTPS)。


3.2 入职第一天:克隆项目

当你加入一个新项目时,第一步是克隆(clone)代码仓库到本地:

# 克隆远程仓库
git clone https://gitlab.company.com/group/project.git

# 进入项目目录
cd project

克隆后,本地会生成一个完整的仓库副本,包含所有分支和历史记录。


3.3 主分支与开发分支

在大多数项目中,存在以下分支约定:

  • main / master:主分支,代表线上稳定版本。通常受保护,不允许直接推送。
  • develop:开发分支,用于集成所有功能。
  • feature/xxx:功能分支,用于开发新功能。
  • hotfix/xxx:紧急修复分支,用于修复线上 bug。

3.4 开始开发:创建功能分支

在开发新功能前,必须从主分支拉取最新代码,并创建自己的功能分支:

# 1. 切换到主分支
git checkout main

# 2. 拉取最新代码(确保本地与远程同步)
git pull origin main

# 3. 创建并切换到新分支(命名规范:feature/功能名)
git checkout -b feature/user-authentication

# 4. 查看当前分支
git branch
# 输出:
#   develop
# * feature/user-authentication
#   main

最佳实践:分支名应清晰描述功能,避免使用 devtest 等模糊名称。


3.5 开发过程中的 Git 操作

在功能开发过程中,你会频繁使用以下命令:

# 查看当前状态(哪些文件被修改、新增、删除)
git status

# 将修改的文件添加到暂存区
git add .

# 或添加特定文件
git add src/auth.js

# 提交到本地仓库
git commit -m "feat: implement user login logic"

# 推送到远程仓库(首次推送需设置上游分支)
git push origin feature/user-authentication

# 如果是首次推送,Git 会提示设置上游分支
# git push --set-upstream origin feature/user-authentication

提交信息规范:建议使用 Conventional Commits 规范,如:

  • feat: 新功能
  • fix: 修复 bug
  • docs: 文档更新
  • style: 代码格式调整
  • refactor: 重构
  • test: 测试相关
  • chore: 构建或辅助工具变动

3.6 暂存区与工作区的撤销操作

开发中难免会误操作,Git 提供了强大的撤销机制:

# 1. 从暂存区取消暂存(但保留工作区修改)
git restore --staged file.txt

# 2. 从工作区丢弃修改(恢复到上次提交状态)
git restore file.txt

# 3. 删除未跟踪的文件或目录
git clean -fd

# 4. 查看修改内容
git diff          # 工作区 vs 暂存区
git diff --staged # 暂存区 vs 最近一次提交

警告git restore 是 Git 2.23+ 引入的新命令,替代了旧的 git checkout -- <file>git reset HEAD <file>。它语义更清晰,推荐使用。


3.7 查看历史与日志

了解项目历史是协作开发的基础:

# 查看提交历史(简洁格式)
git log --oneline

# 查看最近 5 次提交
git log --oneline -5

# 查看某文件的修改历史
git log --oneline path/to/file.js

# 查看分支图
git log --oneline --graph --all

# 查看某次提交的详细改动
git show <commit-hash>

3.8 分支管理

分支是 Git 的核心功能之一,支持并行开发:

# 查看所有本地分支
git branch

# 查看所有远程分支
git branch -r

# 查看所有分支(本地+远程)
git branch -a

# 切换分支
git checkout develop

# 创建新分支(不切换)
git branch feature/new-ui

# 删除本地分支
git branch -d feature/old-ui

# 强制删除(未合并的分支)
git branch -D feature/experiment

# 删除远程分支
git push origin --delete feature/old-ui

四、协作开发:Pull Request(PR)流程

在团队开发中,直接向主分支推送代码是危险的。因此,现代开发普遍采用 Pull Request(PR)Merge Request(MR) 流程。

4.1 什么是 Pull Request?

Pull Request 是一种代码审查(Code Review)机制。开发者在自己的分支上完成开发后,向主分支发起“合并请求”,邀请团队成员审查代码、讨论修改,最终由负责人合并。

4.2 如何给开源项目提交 PR?

以 GitHub 为例,流程如下:

步骤 1:Fork 项目

  1. 访问开源项目仓库(如 https://github.com/owner/project
  2. 点击右上角的 Fork 按钮
  3. GitHub 会将项目复制到你的账户下(如 https://github.com/yourname/project

步骤 2:克隆你的 Fork

git clone https://github.com/yourname/project.git
cd project

步骤 3:添加上游仓库(Upstream)

为了同步原项目的更新,需要添加原仓库为上游:

# 添加上游仓库
git remote add upstream https://github.com/owner/project.git

# 查看远程仓库
git remote -v
# 输出:
# origin    https://github.com/yourname/project.git (fetch)
# origin    https://github.com/yourname/project.git (push)
# upstream  https://github.com/owner/project.git (fetch)
# upstream  https://github.com/owner/project.git (push)

步骤 4:同步上游更新

定期从原项目拉取最新代码,避免你的 Fork 落后太多:

# 切换到 main 分支
git checkout main

# 从 upstream 拉取最新代码
git pull upstream main

# 推送到你的远程仓库
git push origin main

步骤 5:创建功能分支并开发

# 基于最新的 main 创建分支
git checkout -b feature/improve-readme

# 开发、提交
git add .
git commit -m "docs: improve README with usage examples"
git push origin feature/improve-readme

步骤 6:发起 Pull Request

  1. 访问你的 Fork 仓库页面
  2. 点击 Compare & pull request
  3. 选择:
    • base repository: owner/project
    • base: main
    • head repository: yourname/project
    • compare: feature/improve-readme
  4. 填写 PR 标题和描述,说明修改内容
  5. 提交 PR

步骤 7:等待审查与合并

  • 原项目维护者会审查你的代码
  • 可能要求你修改(你可以在原分支继续提交,PR 会自动更新)
  • 审查通过后,维护者会合并你的 PR

4.3 开源项目作者如何合并 PR?

作为项目维护者,你可能会收到其他开发者的 PR。合并流程如下:

方式 1:GitHub 界面一键合并

  1. 进入 PR 页面
  2. 点击 Merge pull request
  3. 选择合并方式:
    • Create a merge commit:保留 PR 历史,生成合并提交
    • Squash and merge:将多个提交压缩为一个,保持主分支整洁
    • Rebase and merge:将 PR 分支变基到主分支,形成线性历史
  4. 点击 Confirm merge

方式 2:命令行合并(更灵活)

# 1. 添加贡献者的远程仓库
git remote add contributor https://github.com/contributor/project.git

# 2. 拉取贡献者的分支
git fetch contributor feature/improve-readme

# 3. 创建本地分支并切换
git checkout -b pr-123 contributor/feature/improve-readme

# 4. 审查代码
git log --oneline --graph
git diff main

# 5. 合并到主分支
git checkout main
git merge pr-123 --no-ff  # --no-ff 保留合并信息

# 6. 推送到远程
git push origin main

注意:合并后记得删除已合并的分支,保持仓库整洁。


五、Git 高级技巧与最佳实践

5.1 .gitignore 文件

用于忽略不需要版本控制的文件(如日志、缓存、依赖包):

# 依赖包
node_modules/
vendor/

# 环境变量
.env
.env.local

# 编译输出
dist/
build/

# IDE 文件
.vscode/
.idea/

# 日志
*.log

5.2 Git Hooks

Git Hooks 是在特定事件(如提交、推送)触发的脚本。常用钩子:

  • pre-commit:提交前运行(如代码格式化、单元测试)
  • pre-push:推送前运行(如 lint 检查)

工具推荐:Husky + lint-staged

5.3 变基(Rebase) vs 合并(Merge)

  • Merge:创建一个合并提交,保留分支历史,适合团队协作。
  • Rebase:将分支的提交“重放”到目标分支上,形成线性历史,适合清理本地分支。
# 将 feature 分支变基到 main
git checkout feature
git rebase main

# 解决冲突后继续
git rebase --continue

警告:不要对已推送的公共分支使用 rebase,否则会导致历史混乱。


六、Git 面试考点

Q1:请解释 Git 的工作流程(三大区域)

回答

Git 的工作流程基于三个核心区域:

  1. 工作区(Working Directory):开发者直接编辑的文件所在目录。这里的修改尚未被 Git 跟踪。
  2. 暂存区(Staging Area / Index):通过 git add 命令将工作区的修改添加到暂存区。暂存区是一个中间状态,用于准备下一次提交的内容。
  3. 本地仓库(Repository):通过 git commit 命令将暂存区的内容提交到本地仓库,生成一个带有唯一哈希值的提交对象(Commit),记录了本次变更的快照。

这个流程确保了开发者可以精确控制哪些修改被包含在本次提交中,避免意外提交无关文件。


Q2:如何撤销一次提交?

回答

撤销提交的方法取决于提交是否已推送到远程仓库:

  1. 未推送的提交

    • 使用 git reset --soft HEAD~1:撤销提交,但保留修改在暂存区。
    • 使用 git reset --mixed HEAD~1(默认):撤销提交和暂存,修改回到工作区。
    • 使用 git reset --hard HEAD~1:彻底删除提交和所有修改(危险操作)。
  2. 已推送的提交

    • 使用 git revert <commit-hash>:创建一个新提交来“反转”指定提交的修改。这是安全且推荐的方式,因为它不会改变历史,适合团队协作。

例如:

git revert abc1234

Q3:Merge 和 Rebase 有什么区别?

回答

  • Merge

    • 创建一个新的“合并提交”(merge commit),将两个分支的历史连接起来。
    • 保留完整的分支历史,适合团队协作。
    • 历史记录可能包含“分叉”,但信息完整。
  • Rebase

    • 将当前分支的提交“重放”到目标分支的最新提交之上,形成线性历史。
    • 不创建合并提交,历史更整洁。
    • 但会重写提交历史,因此不应在公共分支上使用,否则会导致协作者的本地历史与远程不一致。

选择建议:

  • 个人分支整理:使用 rebase
  • 团队分支合并:使用 merge

Q4:如何处理合并冲突?

回答

当两个分支修改了同一文件的同一行时,Git 无法自动合并,会产生冲突。

处理步骤:

  1. 执行合并或变基操作时,Git 会提示冲突文件。
  2. 打开冲突文件,会看到类似以下标记:
<<<<<<< HEAD
当前分支的内容
=======
其他分支的内容
>>>>>>> branch-name
  1. 手动编辑文件,保留正确的代码,删除 <<<<<<<=======>>>>>>> 标记。
  2. 保存文件后,使用 git add <file> 将解决后的文件标记为已解决。
  3. 继续合并操作:
    • 如果是 merge:执行 git commit
    • 如果是 rebase:执行 git rebase --continue

Q5:什么是 Pull Request?为什么使用它?

回答

Pull Request(PR)是一种代码审查和协作机制。开发者在自己的分支上完成开发后,向主分支发起“拉取请求”,邀请团队成员审查代码。

使用 PR 的好处

  1. 代码质量保障:通过同行评审发现潜在 bug、设计问题。
  2. 知识共享:团队成员了解彼此的代码变更。
  3. 自动化测试集成:CI/CD 系统可在 PR 上运行测试、构建、代码扫描。
  4. 变更可追溯:PR 记录了讨论、审查意见和决策过程。
  5. 防止直接推送:保护主分支,避免未经审查的代码上线。

PR 是现代软件开发中保障代码质量和团队协作的核心实践。


Q6:如何将本地分支与远程分支关联?

回答

首次推送分支时,需要设置上游分支(upstream):

# 方法1:推送时设置上游
git push -u origin feature/login

# 方法2:明确设置上游
git push --set-upstream origin feature/login

# 方法3:后续设置
git branch --set-upstream-to=origin/feature/login

设置上游后,后续可以使用 git pullgit push 而无需指定分支名。


Q7:如何查看某次提交修改了哪些文件?

回答

使用 git show 命令:

# 显示提交信息和文件变更
git show <commit-hash>

# 仅显示修改的文件名
git show --name-only <commit-hash>

# 显示文件变更的统计信息(增删行数)
git show --stat <commit-hash>

Q8:如何查找引入某个 bug 的提交?

回答

使用 git bisect 命令进行二分查找:

# 1. 开始 bisect
git bisect start

# 2. 标记当前为坏提交(有 bug)
git bisect bad

# 3. 标记一个已知的好提交(无 bug)
git bisect good abc1234

# 4. Git 会自动检出中间提交,你测试后标记好/坏
git bisect good  # 或 git bisect bad

# 5. 重复步骤4,直到 Git 找到第一个引入 bug 的提交
# 6. 结束 bisect
git bisect reset

git bisect 能高效定位问题,特别适合在长历史中查找回归 bug。


Q9:如何清理本地未跟踪的文件?

回答

使用 git clean 命令:

# 列出将被删除的文件(预览)
git clean -fdn

# 删除未跟踪的文件
git clean -f

# 删除未跟踪的文件和目录
git clean -fd

# 删除忽略的文件(如 node_modules)
git clean -fdx

警告git clean 是不可逆操作,建议先用 -n 参数预览。


Q10:如何将多个提交合并为一个?

回答

使用 git rebase -i(交互式变基):

# 对最近3次提交进行交互式变基
git rebase -i HEAD~3

会打开编辑器,显示类似:

pick abc1234 feat: add login
pick def5678 fix: login bug
pick ghi9012 style: format login

将后面的 pick 改为 squashs

pick abc1234 feat: add login
s def5678 fix: login bug
s ghi9012 style: format login

保存后,Git 会将三个提交合并为一个,并允许你编辑新的提交信息。

注意:此操作会重写历史,仅适用于未推送的提交。


七、总结

Git 不仅仅是一个版本控制工具,更是现代软件开发协作的基石。掌握 Git 的核心概念、日常操作、协作流程和高级技巧,是每一位开发者必备的技能。

本文从环境搭建、分支管理、PR 流程到面试考点,系统梳理了 Git 的知识体系。希望读者不仅能“会用”,更能理解其背后的设计哲学,并在实际项目中应用最佳实践。

最后建议

  1. 多练习:创建测试仓库,尝试各种操作。
  2. 阅读官方文档:git-scm.com/doc
  3. 使用图形化工具辅助:如 GitKraken、SourceTree、VS Code 内置 Git。
  4. 遵循团队规范:不同公司可能有不同的分支策略和提交规范。

掌握 Git,让你的代码开发更加高效、安全、可协作。

【渲染流水线】[几何阶段]-[图元装配]以UnityURP为例

图元装配负责将离散顶点组装成完整几何图元(如点、线、三角形、三角形条带)

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

输入数据

接收‌顶点着色器输出的离散顶点数据‌,包括:

  • 变换后的空间坐标(如裁剪空间位置)
  • 顶点属性(颜色、法线、纹理坐标等

输出数据

生成‌完整几何图元‌(Primitive),例如:

  • 三角形(GL_TRIANGLES
  • 线段(GL_LINES
  • 点(GL_POINTS)‌

在Unity中,图元装配的实现

主要通过‌**网格拓扑(Mesh Topology)‌和‌索引缓冲区(Index Buffer)**‌完成。

顶点分组模式‌

  1. 索引分组模式‌通过索引数组(如Mesh.trianglesMesh.GetIndices())定义顶点连接顺序,每个索引指向顶点缓冲区中的位置,按预设拓扑规则分组‌。例如:

    text
    索引数组 [0,1,2,3,4,5]
    三角形拓扑 → 分组为△(0,1,2)和△(3,4,5)
    
  2. 顺序分组模式‌无索引时直接按顶点提交顺序分组(如连续3顶点构成一个三角形)‌。


‌Unity支持的图元类型‌

图元类型 描述
三角形(Triangles 每3个独立顶点构成一个三角形,默认用于3D模型渲染‌。
三角形条带(TriangleStrip 复用前2顶点与当前顶点生成新三角形,减少顶点重复提交‌。
四边形(Quads 每4顶点构成一个四边形(实际渲染时拆分为2个三角形)。
线段(Lines 每2顶点构成一条线段,用于线框渲染‌。
点(Points 每个顶点独立渲染为屏幕上的点‌。

‌拓扑连接规则‌

  1. 缠绕顺序(Winding Order)‌Unity默认使用‌顺时针顺序‌判定三角形正面,逆时针面会被剔除‌。例如:
    • 顶点顺序(v1,v2,v3)为顺时针 → 可见
    • 顺序(v1,v3,v2)为逆时针 → 剔除‌。
  2. 共享顶点优化‌索引数组可复用顶点(如[0,1,2,1,2,3]生成两个共享边(1,2)的三角形)。

‌关键实现接口‌

  1. 设置拓扑类型‌通过MeshTopology枚举指定图元类型(如MeshTopology.Triangles)‌。
  2. 索引缓冲区操作
    • Mesh.SetIndices():自定义索引分组规则
    • Mesh.triangles:直接设置三角形索引(旧API,效率较低)‌

URP中对图元装配的调用位置与示例

在Unity URP (Universal Render Pipeline) 中,几何阶段的图元装配是由底层渲染管线自动处理的,主要通过ScriptableRenderContextCommandBuffer系统完成。

‌核心类与调用流程‌

  • UniversalRenderPipeline.RenderSingleCamera‌入口点,通过ScriptableRenderContext提交绘制命令
  • ScriptableRenderContext.DrawRenderers‌触发几何处理,最终调用底层图形API (如OpenGL/D3D)
  • CommandBuffer.DrawProcedural‌直接控制图元装配(手动模式)

图元装配示例代码‌

以下是不同图元类型的装配方式示例:

三角形 (Triangles)‌

csharp
// 通过MeshFilter自动装配var meshFilter = GetComponent<MeshFilter>();
Graphics.DrawMesh(meshFilter.sharedMesh, transform.position, transform.rotation, material, 0);

三角形带 (Triangle Strip)‌

csharp
// 手动通过CommandBuffer装配
CommandBuffer cmd = new CommandBuffer();
cmd.DrawProcedural(
    Matrix4x4.identity,
    material,
    0,
    MeshTopology.TriangleStrip,
    vertexCount: 4// 需要至少4个顶点形成2个三角形
);
context.ExecuteCommandBuffer(cmd);

四边形 (Quads)‌

csharp
// URP中四边形会被拆分为三角形处理
Mesh quadMesh = new Mesh();
quadMesh.vertices = new Vector3[] {/* 4个顶点 */ };
quadMesh.SetIndices(new int[] {0,1,2, 0,2,3}, MeshTopology.Triangles, 0);
Graphics.DrawMesh(quadMesh, Matrix4x4.identity, material, 0);

线段 (Lines)‌

csharp
// 使用GL.LINES或LineRenderer组件
CommandBuffer cmd = new CommandBuffer();
cmd.DrawProcedural(
    Matrix4x4.identity,
    lineMaterial,
    0,
    MeshTopology.Lines,
    vertexCount: 2
);

点 (Points)‌

csharp
// 使用MeshTopology.Points
CommandBuffer cmd = new CommandBuffer();
cmd.DrawProcedural(
    Matrix4x4.identity,
    pointMaterial,
    0,
    MeshTopology.Points,
    vertexCount: 1
);

底层实现位置‌

  • URP源码关键文件‌:UniversalRenderPipelineCore.cs → ExecuteRenderPass方法ScriptableRenderer.cs → EnqueuePass提交绘制命令
  • Shader支持‌:在Shader中需声明正确的#pragma target和几何着色器(如需要)

调试

  1. 使用Frame Debugger查看实际提交的图元类型
  2. 在URP设置中启用Native Rendering Debugger
  3. 检查材质的Render QueueShader Pass设置

更深入的管线定制,可继承ScriptableRendererFeature实现自定义几何处理。


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

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

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

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

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

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视觉格式化模型的深刻体现。掌握每种方案的底层原理,方能灵活应对复杂场景。当然也能让你在面试官面前眼前一亮

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

在现代前端框架(如 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。”

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

实战项目:用包管理器构建一个豆瓣电影爬虫

前言

学习包管理器,光看理论是不够的。最好的方式就是动手做一个项目!

今天我们要构建一个豆瓣电影爬虫,这个项目会用到多个第三方包,让你真正体验包管理器在项目开发中的实际作用。

项目目标

我们要做一个豆瓣电影数据爬虫,功能包括:

  • 爬取豆瓣电影排行榜数据
  • 提取电影名称、海报、详情信息
  • 将数据保存为 JSON 文件

技术选型

在开始之前,我们需要选择合适的包:

// 项目需要的包
{
  "网络请求": "axios - 简单易用的请求库",
  "HTML解析": "cheerio - 解析HTML神器",
  "文件操作": "fs - Node.js内置模块"
}

项目初始化

1. 创建项目目录

# 创建项目文件夹
mkdir douban-movie-crawler
cd douban-movie-crawler

# 初始化 npm 项目
npm init -y

执行 npm init -y 后,会生成一个基础的 package.json 文件:

{
  "name": "douban-movie-crawler",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

2. 安装依赖包

现在我们需要安装项目所需的包(一般是在开发过程中发现需要用到哪些包再去安装):

# 安装 axios 用于网络请求
npm install axios

# 安装 cheerio 用于解析HTML
npm install cheerio

执行后,package.json 会自动更新:

{
  "name": "douban-movie-crawler",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^1.11.0",
    "cheerio": "^1.1.2"
  }
}

同时会生成 package-lock.json 文件(用于锁定依赖版本)和node_modules文件(存放第三方库)

3. 项目结构

image.png

编写爬虫代码

由于我们要爬的是豆瓣电影的内容,那么我们可以先在浏览器中打开豆瓣电影的网站,https://movie.douban.com/chart,右键检查(或F12)打开控制台,选中源代码/来源 tab栏,然后我们可以看到这个下面的chat是html文件,我们要拿到的就是这个html文件

image.png

1. 网络请求模块

创建 getMovies.js 文件:

const axios = require("axios");

/**
 * 得到所有电影的html字符串
 */
async function getMoviesHtml() {
  const resp = await axios.get("https://movie.douban.com/chart")
  console.log(resp);
}

getMoviesHtml()

这里我们使用了 axios 包来发送 HTTP 请求, 用于获取豆瓣网站的数据。相比 Node.js 内置的 http 模块,axios 提供了更简洁的 API。

我们可以在终端中运行一下这个文件,看看获取到的resp是什么

// 运行命令

node getMovies.js

可以发现得到的resp是一个对象,里面有很多的属性,而这里的data就是我们需要的html数据

image.png

所以我们需要拿到resp.data,那么在getMoviesHtml函数中返回resp.data

2. 数据解析模块

现在获取到了html,那么我们就需要对这个html进行解析,这里就需要用到另外一个库:cheerio,前面已经安装过了,现在就直接拿来用即可

// getMovies.js
const axios = require("axios");
const cheerio = require("cheerio");

/**
 * 得到所有电影的html字符串
 */
async function getMoviesHtml() {
  const resp = await axios.get("https://movie.douban.com/chart")
  return resp.data;
}
/**
 * 获取所有电影数据
 */
async function getMoviesData() {
  const html = await getMoviesHtml();
  // 将html字符串解析为可操作的dom对象
  const $ = cheerio.load(html);
  // 找到带有item属性的tr标签
  var trs = $("tr.item")
  console.log(trs);
}
getMoviesData()

cheerio.load(html) 方法解析 HTML 字符串,返回的$是一个函数,可以提供类似 jQuery 的语法来操作 DOM

$("tr.item") 使用 CSS 选择器语法,查找所有 <tr> 标签且 class 包含 item 的元素

cheerio 让我们可以在服务端使用类似 jQuery 的语法来解析 HTML,非常方便!

tr中的内容是这部分电影基础内容:

image.png

3. 单部电影数据提取

// getMovies.js
const axios = require("axios");
const cheerio = require("cheerio");
/**
 * 得到所有电影的html字符串
 */
async function getMoviesHtml() {
  const resp = await axios.get("https://movie.douban.com/chart")
  return resp.data;
}

/**
 * 获取所有电影数据
 */
async function getMoviesData() {
  const html = await getMoviesHtml();
  const $ = cheerio.load(html);
  var trs = $("tr.item")
  var movies = [];
  for (let i = 0; i < trs.length; i++) {
    var tr = trs[i];
    //分析每个tr的数据,得到一部电影对象
    var m = getMovie($(tr));
    movies.push(m);
  }
  return movies;
}

/**
 * 分析tr,得到一部电影对象
 * @param {*} tr 
 */
function getMovie(tr) {
  // 获取到电影名称
  var name = tr.find("div.pl2 a").text();
  name = name.replace(/\s/g, "");//去掉空白字符
  name = name.split("/")[0];

  // 电影封面图
  var imgSrc = tr.find("a.nbg img").attr("src");
  // 电影详情
  var detail = tr.find("div.pl2 p").text();
  console.log(name, 'name')
  console.log(imgSrc, 'imgSrc')
  console.log(detail, 'detail')
  console.log('=========================')
  return {
    name,
    imgSrc,
    detail
  }
}

getMoviesData()

我们在拿到所有的tr标签以后,需要遍历每个tr标签,解析电影的名称,电影的封面图和电影的基础信息,这些信息都在对应的标签中可以获取,可以通过在网页中查看具体的元素找到不同内容的存放位置

然后运行一下打印看看:

node getMovies.js

image.png

好的,已经成功拿到了电影信息,下面我们把getMoviesData函数导出,然后在主文件去使用 下面是getMovies.js文件的完整内容:

const axios = require("axios");
const cheerio = require("cheerio");
/**
 * 得到所有电影的html字符串
 */
async function getMoviesHtml() {
  const resp = await axios.get("https://movie.douban.com/chart")
  return resp.data;
}

/**
 * 获取所有电影数据
 */
async function getMoviesData() {
  const html = await getMoviesHtml();
  const $ = cheerio.load(html);
  var trs = $("tr.item")
  var movies = [];
  for (let i = 0; i < trs.length; i++) {
    var tr = trs[i];
    //分析每个tr的数据,得到一部电影对象
    var m = getMovie($(tr));
    movies.push(m);
  }
  return movies;
}

/**
 * 分析tr,得到一部电影对象
 * @param {*} tr 
 */
function getMovie(tr) {
  // 获取到电影名称
  var name = tr.find("div.pl2 a").text();
  name = name.replace(/\s/g, "");//去掉空白字符
  name = name.split("/")[0];

  // 电影封面图
  var imgSrc = tr.find("a.nbg img").attr("src");
  // 电影详情
  var detail = tr.find("div.pl2 p").text();
  return {
    name,
    imgSrc,
    detail
  }
}

module.exports = getMoviesData;

4. 主程序入口

创建 index.js 文件, 引入刚刚导出的getMoviesData函数,执行这个函数得到存放有电影信息的对象,然后转为json字符串存储到json文件中

var getMovies = require("./getMovies")
var fs = require("fs");

async function init() {
  // 获取电影数据
  const movies = await getMovies();
  // fs.writeFile 要求写入的内容必须是字符串或二进制数据,所以需要转换为json字符串
  const moviesJson = JSON.stringify(movies)
  // 将得到的电影数据存到json文件中
  fs.writeFile("movie.json", moviesJson, function () {
    console.log("成功!")
  });
}

init()

这里fs.writeFile是nodejs的语法,用于把数据写入到movie.json文件中,如果没有该文件则会创建这个movie.json文件,并写入内容

运行项目

// 运行命令

node index.js

2. 查看结果

运行成功后,会生成 movie.json 文件,内容如下:

image.png

到此,就成功完成了从豆瓣爬取数据到解析数据,存储数据的全流程。

总结

通过这个实战项目,我们学到了:

🎯 包管理器的核心价值

  • 依赖管理:自动安装和管理第三方包
  • 版本控制:确保团队使用相同的依赖版本
  • 项目配置:统一管理项目信息和脚本
  • 团队协作:简化项目分享和部署

🔧 实际应用场景

  • 选择合适的包解决特定问题
  • 管理项目依赖关系
  • 配置开发和生产环境
  • 团队协作和项目部署

💡 最佳实践

  • 合理选择依赖包
  • 区分生产依赖和开发依赖
  • 使用 package-lock.json 锁定版本
  • 配置合适的脚本命令

这个项目虽然简单,但展示了包管理器在实际开发中的重要作用。通过动手实践,你会发现包管理器不仅仅是安装包的工具,更是现代前端开发的核心基础设施。

下一章预告

在下一章《语义版本控制:掌握版本管理的艺术》中,我们将深入学习:

  • 语义化版本规范:理解主版本号、次版本号、补丁版本号的含义
  • 版本范围指定:掌握 ^~>= 等版本前缀的使用
  • 依赖版本冲突:学会识别和解决版本冲突问题
  • 版本锁定策略:了解 package-lock.json 的作用机制
  • 安全更新:掌握如何安全地更新依赖包版本

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

最近在接入高德地图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

nodejs 概念以及下载

Nodejs 简介

Node.js 是一个开源且跨平台的 JavaScript 运行时环境。它几乎适用于任何类型的项目!

Node.js 在浏览器之外运行 V8 JavaScript 引擎(Google Chrome 的核心)。这使得 Node.js 性能非常出色。

Node.js 应用在单进程中运行,无需为每个请求创建新线程。Node.js 在其标准库中提供了一组异步 I/O 原语,可防止 JavaScript 代码阻塞。此外,Node.js 中的库通常使用非阻塞范例编写。因此,阻塞行为在 Node.js 中只是例外,而非常态。

当 Node.js 执行 I/O 操作(例如从网络读取、访问数据库或文件系统)时,Node.js 不会阻塞线程并浪费 CPU 周期等待,而是会在响应返回时恢复操作。

这使得 Node.js 能够通过单个服务器处理数千个并发连接,而不会引入管理线程并发的负担,而这可能是造成错误的一个重要原因。

Node.js 具有独特的优势,因为数百万为浏览器编写 JavaScript 的前端开发人员现在除了能够编写客户端代码之外,还能编写服务器端代码,而无需学习完全不同的语言。

在 Node.js 中,可以毫无问题地使用新的 ECMAScript 标准,因为您不必等待所有用户更新他们的浏览器 - 您可以通过更改 Node.js 版本来决定使用哪个 ECMAScript 版本,并且您还可以通过使用标志运行 Node.js 来启用特定的实验性功能。

NodeJS架构图

image.png

查找资料过程中在 stack overflow 找到对 node 架构的描述stackoverflow.com/questions/3…

segmentfault.com/a/119000000…

nodejs 知识体系图

nodejs.jpg

❌