美的AI

5 阅读22分钟

1、canvas如何压缩图片?

Canvas通过将图片绘制到画布并调整尺寸或质量参数来压缩图片。具体实现步骤如下:

核心压缩原理
  • 尺寸调整:将图片绘制到指定宽高的canvas画布上,通过等比缩放减少像素量"1""2"。例如设置最大宽度800px,超出时按比例缩小高度。
  • 质量控制:使用canvas.toDataURL()toBlob()方法时,通过第二个参数(0-1范围)降低图片质量。例如JPEG格式设置0.7可显著减小体积"3""4"。
实现步骤
  1. 加载图片

    • 通过FileReader读取文件或new Image()加载图片路径"1""5"。
  2. 绘制到Canvas

    • 创建离屏canvas元素,设置目标宽高。
    • 使用ctx.drawImage(img, 0, 0, targetWidth, targetHeight)重绘图片"3""6"。
  3. 导出压缩结果

    • toDataURL('image/jpeg', 0.8):输出Base64编码字符串(适合小图)。
    • toBlob(callback, 'image/jpeg', 0.7):生成Blob对象(适合上传)

2、canvas如何把png图的透明部分变成渐变色

要在Canvas中将PNG图的透明部分变成渐变色,可以通过以下步骤实现:

  1. 加载PNG图像。
  2. 使用Canvas获取图像的像素数据。
  3. 修改透明区域的像素颜色为所需的渐变色。
  4. 将修改后的图像重新绘制到Canvas上。

3、promise和async awaite区别,错误捕获如何做?

区别总结

  1. 语法:

    • Promise: 使用 .then() 和 .catch() 来处理结果和错误。
    • async/await: 使用 async 和 await 关键字,看起来更像是同步代码。
  2. 可读性:

    • Promise: 可能会导致“回调地狱”,尤其是在多个异步操作嵌套的情况下。
    • async/await: 提高了代码的可读性和可维护性,特别是在需要处理多个异步操作时。
  3. 错误处理:

    • Promise: 使用 .catch() 来捕获错误。
    • async/await: 使用 try/catch 块来捕获错误。

4、promise的catch捕捉的错误是最后一条还是整条链路的错误

Promise 的 .catch() 捕获的是整条链路中所有未被处理的错误,而非仅仅最后一条。

具体来说,当 Promise 链式调用(.then().then().catch())中任何一个环节抛出错误(或状态变为 rejected),且该错误没有被前面的 .then() 第二个参数(错误回调)处理时,会一直向后传递,最终被链路末端的 .catch() 捕获。

示例说明

javascript

运行

Promise.resolve()
  .then(() => {
    console.log("第一步");
    throw new Error("第一步出错"); // 抛出错误
  })
  .then(() => {
    console.log("第二步"); // 不会执行(因为第一步出错)
  })
  .catch((err) => {
    console.error("捕获到错误:", err.message); // 输出:"捕获到错误: 第一步出错"
  });

在这个例子中:

  • 第一步抛出的错误没有被当前 .then() 的第二个参数处理,所以会向后传递。
  • 第二步的 .then() 不会执行,错误直接传递到最后的 .catch() 并被捕获。

关键特性

  1. 错误冒泡:链路中任何位置的错误都会像 “冒泡” 一样向后传递,直到被某个 .catch() 或 .then() 的错误回调捕获。
  2. 就近捕获:如果链路中间有 .catch(),则错误会被最近的 .catch() 捕获,后续链路可继续执行(类似同步代码中的 try/catch 后继续运行)。

javascript

运行

Promise.resolve()
  .then(() => {
    throw new Error("错误A");
  })
  .catch((err) => {
    console.error("捕获A:", err.message); // 捕获错误A,阻止其继续传递
    return "恢复正常"; // catch 中返回正常值会让 Promise 状态变为 resolved
  })
  .then((res) => {
    console.log("后续步骤:", res); // 执行:"后续步骤: 恢复正常"
    throw new Error("错误B");
  })
  .catch((err) => {
    console.error("捕获B:", err.message); // 捕获错误B
  });

结论

.catch() 捕获的是从当前位置向前追溯,整条链路中所有未被处理的错误,并非仅最后一个错误。这使得 .catch() 可以作为整条链路的 “统一错误处理中心”,简化错误管理。

5、大文件上传如何做断点续传

文件上传的断点续传核心是将文件分片传输,并通过 “记录已上传分片” 和 “校验分片完整性” 实现中断后继续上传,避免重复传输整个文件。以下是具体实现方案和关键步骤:

一、核心原理

  1. 文件分片:将大文件(如 1GB)按固定大小(如 5MB)分割成多个小分片(Blob/File 对象),降低单次传输压力。
  2. 唯一标识:为每个文件生成唯一 ID(如通过 MD5 哈希文件内容、结合文件名 + 大小 + 修改时间),确保服务器能识别同一文件的分片。
  3. 状态记录:上传前向服务器查询 “该文件已上传的分片列表”,仅传输未上传的分片。
  4. 分片合并:所有分片上传完成后,通知服务器合并分片为完整文件。

二、完整实现步骤(前端 + 后端)

1. 前端实现(核心步骤)

前端负责文件分片、记录上传状态、断点续传触发,以下是关键代码逻辑:

1.1 生成文件唯一标识(避免同文件重复上传)

常用 spark-md5 库计算文件内容的 MD5 值(大文件需分片读流计算,避免内存溢出)

import SparkMD5 from 'spark-md5';

// 计算文件MD5(分片读流,适合大文件)
function calculateFileMD5(file) {
  return new Promise((resolve) => {
    const chunkSize = 2 * 1024 * 1024; // 2MB/片(读流分片,非上传分片)
    const spark = new SparkMD5.ArrayBuffer();
    const fileReader = new FileReader();
    let offset = 0;

    fileReader.onload = (e) => {
      spark.append(e.target.result);
      offset += chunkSize;
      if (offset < file.size) {
        readNextChunk(); // 继续读下一片
      } else {
        resolve(spark.end()); // 生成最终MD5(文件唯一ID)
      }
    };

    function readNextChunk() {
      const blob = file.slice(offset, offset + chunkSize);
      fileReader.readAsArrayBuffer(blob);
    }

    readNextChunk();
  });
}
1.2 分割文件为上传分片

按固定大小分割文件,生成分片列表(含分片索引、大小等信息)。

javascript

运行

// 分割文件为上传分片
function splitFileIntoChunks(file, chunkSize = 5 * 1024 * 1024) { // 5MB/上传分片
  const chunks = [];
  const totalChunks = Math.ceil(file.size / chunkSize);
  for (let i = 0; i < totalChunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    chunks.push({
      fileChunk: file.slice(start, end), // 分片Blob
      chunkIndex: i, // 分片索引(从0开始)
      totalChunks: totalChunks, // 总分片数
      fileId: '', // 后续赋值为文件MD5
      fileName: file.name
    });
  }
  return chunks;
}
1.3 断点续传核心逻辑(查询已传分片 + 上传未传分片)

javascript

运行

class ResumableUpload {
  constructor(file, chunkSize = 5 * 1024 * 1024) {
    this.file = file;
    this.chunkSize = chunkSize;
    this.fileId = ''; // 文件唯一ID(MD5)
    this.chunks = []; // 分片列表
    this.uploadedChunks = new Set(); // 已上传分片索引(避免重复传)
  }

  // 初始化:计算MD5+查询已传分片
  async init() {
    this.fileId = await calculateFileMD5(this.file); // 生成文件唯一ID
    this.chunks = splitFileIntoChunks(this.file, this.chunkSize);
    // 向服务器查询:该文件已上传的分片索引
    const uploadedIndices = await this.getUploadedChunksFromServer();
    this.uploadedChunks = new Set(uploadedIndices);
    return this;
  }

  // 向服务器查询已上传分片(后端接口需实现)
  async getUploadedChunksFromServer() {
    const response = await fetch(`/api/upload/check?fileId=${this.fileId}`, {
      method: 'GET'
    });
    const data = await response.json();
    return data.uploadedIndices || []; // 后端返回已传分片索引数组(如[0,1,3])
  }

  // 上传单个分片(带进度)
  async uploadChunk(chunk) {
    const formData = new FormData();
    formData.append('fileChunk', chunk.fileChunk);
    formData.append('fileId', this.fileId);
    formData.append('chunkIndex', chunk.chunkIndex);
    formData.append('totalChunks', chunk.totalChunks);
    formData.append('fileName', chunk.fileName);

    return fetch('/api/upload/chunk', {
      method: 'POST',
      body: formData,
      // 可选:监听分片上传进度
      onProgress: (e) => {
        if (e.lengthComputable) {
          const progress = (e.loaded / e.total) * 100;
          console.log(`分片${chunk.chunkIndex}上传进度:${progress.toFixed(2)}%`);
        }
      }
    });
  }

  // 批量上传未传分片(可控制并发数,避免请求过多)
  async startUpload(concurrent = 3) {
    // 筛选未上传的分片
    const unUploadedChunks = this.chunks.filter(
      chunk => !this.uploadedChunks.has(chunk.chunkIndex)
    );

    // 控制并发上传(用Promise.allSettled避免单个失败阻断整体)
    const pool = [];
    for (const chunk of unUploadedChunks) {
      const task = this.uploadChunk(chunk)
        .then(() => {
          this.uploadedChunks.add(chunk.chunkIndex); // 标记为已上传
          console.log(`分片${chunk.chunkIndex}上传成功`);
        })
        .catch((err) => {
          console.error(`分片${chunk.chunkIndex}上传失败:`, err);
          // 可选:失败重试逻辑(如重试3次)
        });

      pool.push(task);
      // 控制并发数:当池内任务数达到concurrent,等待一个完成再继续
      if (pool.length >= concurrent) {
        await Promise.race(pool);
        pool.splice(pool.findIndex(t => t.isSettled), 1); // 移除已完成的任务
      }
    }

    // 等待所有分片上传完成
    await Promise.allSettled(pool);
    // 检查是否所有分片都上传完成
    if (this.uploadedChunks.size === this.chunks.length) {
      await this.requestMerge(); // 通知服务器合并分片
      console.log('所有分片上传完成,已请求服务器合并文件');
    } else {
      throw new Error('部分分片上传失败,请重试');
    }
  }

  // 通知服务器合并分片(后端接口需实现)
  async requestMerge() {
    await fetch('/api/upload/merge', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        fileId: this.fileId,
        totalChunks: this.chunks.length,
        fileName: this.file.name
      })
    });
  }

  // 暂停上传(前端层面:停止发送新的分片请求,已发送的无法中断)
  pauseUpload() {
    // 实际项目中可通过“取消请求”实现(如AbortController)
    this.isPaused = true;
    console.log('上传已暂停');
  }
}
1.4 前端使用示例

javascript

运行

// 监听文件选择
document.getElementById('fileInput').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file || file.size < 1024 * 1024) return; // 仅处理大文件(如>1MB)

  // 初始化断点续传实例
  const uploader = await new ResumableUpload(file, 5 * 1024 * 1024).init();
  console.log(`文件${file.name}初始化完成,共${uploader.chunks.length}个分片,已上传${uploader.uploadedChunks.size}个`);

  // 开始上传(并发3个分片)
  document.getElementById('startBtn').addEventListener('click', () => {
    uploader.startUpload(3);
  });

  // 暂停上传
  document.getElementById('pauseBtn').addEventListener('click', () => {
    uploader.pauseUpload();
  });
});
2. 后端实现(核心逻辑)

后端负责接收分片、存储已传分片、校验分片完整性、合并分片,以 Node.js(Express)为例:

2.1 存储设计
  • 为每个文件创建独立文件夹(以fileId命名),用于存储该文件的所有分片。
  • 记录已传分片的索引(可存在文件夹内的manifest.json,或数据库中)。
2.2 关键接口实现

javascript

运行

const express = require('express');
const multer = require('multer');
const fs = require('fs-extra');
const path = require('path');
const app = express();

// 配置存储:分片临时存储路径(如 ./uploads/chunks/[fileId]/[chunkIndex])
const chunkStorage = multer.diskStorage({
  destination: (req, file, cb) => {
    const fileId = req.body.fileId;
    const chunkDir = path.join(__dirname, 'uploads', 'chunks', fileId);
    fs.ensureDirSync(chunkDir); // 确保文件夹存在
    cb(null, chunkDir);
  },
  filename: (req, file, cb) => {
    const chunkIndex = req.body.chunkIndex;
    cb(null, `${chunkIndex}`); // 分片文件名直接用索引(如 0、1、2)
  }
});
const uploadChunk = multer({ storage: chunkStorage }).single('fileChunk');

// 1. 接口1:查询文件已上传的分片
app.get('/api/upload/check', async (req, res) => {
  const { fileId } = req.query;
  const chunkDir = path.join(__dirname, 'uploads', 'chunks', fileId);
  
  if (!fs.existsSync(chunkDir)) {
    return res.json({ uploadedIndices: [] }); // 文件夹不存在,无已传分片
  }

  // 读取文件夹内的分片文件,获取已传索引
  const chunkFiles = fs.readdirSync(chunkDir);
  const uploadedIndices = chunkFiles.map(file => parseInt(file, 10)).filter(idx => !isNaN(idx));
  res.json({ uploadedIndices });
});

// 2. 接口2:接收分片
app.post('/api/upload/chunk', uploadChunk, (req, res) => {
  // multer已自动保存分片到指定路径
  res.json({ code: 0, msg: '分片上传成功' });
});

// 3. 接口3:合并分片
app.post('/api/upload/merge', async (req, res) => {
  const { fileId, totalChunks, fileName } = req.body;
  const chunkDir = path.join(__dirname, 'uploads', 'chunks', fileId);
  const targetFile = path.join(__dirname, 'uploads', 'completed', fileName); // 最终文件路径

  try {
    // 1. 检查所有分片是否齐全
    const chunkFiles = fs.readdirSync(chunkDir);
    if (chunkFiles.length !== totalChunks) {
      return res.status(400).json({ code: 1, msg: '分片不完整,无法合并' });
    }

    // 2. 按索引顺序合并分片(创建可写流,依次追加分片)
    const writeStream = fs.createWriteStream(targetFile);
    for (let i = 0; i < totalChunks; i++) {
      const chunkPath = path.join(chunkDir, `${i}`);
      const readStream = fs.createReadStream(chunkPath);
      // 同步追加分片(确保顺序)
      await new Promise((resolve) => {
        readStream.pipe(writeStream, { end: false }); // end:false 不关闭写流
        readStream.on('end', resolve);
      });
    }
    // 所有分片追加完成,关闭写流
    writeStream.end();

    // 3. 合并完成后,删除临时分片文件夹
    fs.removeSync(chunkDir);

    res.json({ code: 0, msg: '文件合并成功', fileUrl: `/uploads/completed/${fileName}` });
  } catch (err) {
    res.status(500).json({ code: 2, msg: '合并失败', err: err.message });
  }
});

app.listen(3000, () => console.log('服务器启动:http://localhost:3000'));

三、关键优化点

  1. 分片大小选择:过小(如 1MB)会导致请求数过多,过大(如 50MB)会增加单次传输失败概率,推荐 5MB~20MB(根据业务网络环境调整)。
  2. 并发控制:前端上传分片时控制并发数(如 3~5 个),避免浏览器请求队列阻塞;后端可限制单个文件的并发上传数,防止资源占用过高。
  3. 断点续传持久化:前端可将fileIduploadedChunks存储到localStorage,即使页面刷新,重新初始化时仍能读取已上传状态。
  4. 分片校验:上传分片时可携带分片的 MD5(前端计算分片 MD5),后端接收后校验,避免分片损坏导致合并后文件错误。
  5. 过期清理:后端定期清理长期未完成合并的分片文件夹(如超过 7 天),避免磁盘空间浪费。

四、总结

断点续传的核心是 “分片传输 + 状态记录”:

  • 前端负责文件分片、查询已传状态、并发上传未传分片;
  • 后端负责接收分片、存储状态、合并分片;
  • 通过文件唯一 ID(如 MD5)关联分片,通过已传分片索引实现 “断点”,避免重复传输。

6、如何做懒加载

懒加载(Lazy Loading)是一种优化网页性能的技术,核心思想是只加载用户当前可视区域内的资源(如图片、视频),当用户滚动到对应区域时再加载其他资源,从而减少初始加载时间和带宽消耗。

以下是懒加载的实现方案,主要以图片为例(视频等资源原理类似):

一、核心原理

  1. 占位符:初始时不设置资源的真实地址(如图片的src),而是用占位符(如低质量缩略图、灰色背景)。
  2. 监听滚动:通过监听页面滚动事件,判断元素是否进入可视区域。
  3. 动态加载:当元素进入可视区域时,再设置真实资源地址(如将data-src的值赋给src),触发加载。

二、实现方式

1. 原生 JavaScript 实现(基础版)

适合简单场景,手动监听滚动和判断元素位置:

html

预览

<!-- 图片用data-src存储真实地址,src用占位符 -->
<img class="lazy" data-src="image1.jpg" src="placeholder.png" alt="示例图片">
<img class="lazy" data-src="image2.jpg" src="placeholder.png" alt="示例图片">
<img class="lazy" data-src="image3.jpg" src="placeholder.png" alt="示例图片">
<!-- 更多图片... -->

<script>
  // 获取所有需要懒加载的元素
  const lazyImages = document.querySelectorAll('img.lazy');

  // 判断元素是否在可视区域内
  function isInViewport(el) {
    const rect = el.getBoundingClientRect();
    // 元素顶部 <= 视口高度(可见),且元素底部 >= 0(未完全滚出)
    return (
      rect.top <= (window.innerHeight || document.documentElement.clientHeight) + 100 && // +100提前加载
      rect.bottom >= 0
    );
  }

  // 加载可视区域内的图片
  function loadVisibleImages() {
    lazyImages.forEach(img => {
      if (isInViewport(img) && !img.src.includes(img.dataset.src)) {
        img.src = img.dataset.src; // 赋值真实地址,触发加载
        img.classList.remove('lazy'); // 移除懒加载类,避免重复处理
      }
    });
  }

  // 初始加载一次可视区域图片
  loadVisibleImages();

  // 监听滚动事件,动态加载
  window.addEventListener('scroll', loadVisibleImages);
  // 监听窗口大小变化(如旋转屏幕)
  window.addEventListener('resize', loadVisibleImages);
  // 监听页面刷新/加载完成
  window.addEventListener('load', loadVisibleImages);
</script>

关键点

  • data-src存储真实图片地址,避免初始加载;
  • isInViewport函数通过getBoundingClientRect()判断元素位置;
  • 提前加载(+100):当元素距离可视区域还有 100px 时就开始加载,提升用户体验。
2. 使用 IntersectionObserver API(现代浏览器推荐)

IntersectionObserver是浏览器原生 API,可自动监听元素是否进入可视区域,性能更优(无需频繁计算滚动事件):

html

预览

<img class="lazy" data-src="image1.jpg" src="placeholder.png" alt="示例图片">
<img class="lazy" data-src="image2.jpg" src="placeholder.png" alt="示例图片">

<script>
  // 实例化观察者,配置回调函数
  const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      // 当元素进入可视区域
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src; // 加载真实图片
        observer.unobserve(img); // 停止观察该元素
        img.classList.remove('lazy');
      }
    });
  }, {
    rootMargin: '100px 0px' // 提前100px开始监听(类似提前加载)
  });

  // 观察所有懒加载图片
  document.querySelectorAll('img.lazy').forEach(img => {
    observer.observe(img);
  });
</script>

优势

  • 性能更好:浏览器原生优化,避免滚动事件的高频触发;
  • 配置灵活:通过rootMargin设置提前加载区域,threshold设置可见比例阈值。
  • 兼容性:现代浏览器(Chrome 51+、Firefox 55+、Edge 15+)支持,低版本需 polyfill。
3. 第三方库(成熟方案)

对于复杂场景(如列表、动态加载内容),可使用成熟库简化开发:

  • lazysizes:轻量级(~4KB),支持图片、视频、iframe,自动处理响应式图片(srcset)。

    html

    预览

    <script src="https://cdnjs.cloudflare.com/ajax/libs/lazysizes/5.3.2/lazysizes.min.js"></script>
    <img class="lazyload" data-src="image1.jpg" src="placeholder.png" alt="">
    

    只需添加lazyload类和data-src,库会自动处理懒加载。

  • lozad.js:基于IntersectionObserver,体积更小(~1KB),无依赖。

    html

    预览

    <script src="https://cdn.jsdelivr.net/npm/lozad/dist/lozad.min.js"></script>
    <script>
      const observer = lozad('.lazy'); // 选择器
      observer.observe();
    </script>
    <img class="lazy" data-src="image1.jpg" src="placeholder.png" alt="">
    

三、优化与注意事项

  1. 占位符设计:用与目标图片同尺寸的占位符(如1x1像素的透明图拉伸),避免页面布局抖动(CLS:累积布局偏移)。

  2. 响应式图片:结合data-srcsetsizes实现响应式懒加载:

    html

    预览

    <img class="lazy" 
         data-srcset="small.jpg 400w, large.jpg 800w" 
         data-sizes="(max-width: 600px) 400px, 800px" 
         src="placeholder.png">
    

    加载时将data-srcset赋值给srcset

  3. SEO 友好:对搜索引擎爬虫重要的图片(如首屏关键图)不使用懒加载,避免爬虫无法抓取。

  4. 视频 /iframe 懒加载:原理类似,视频可用data-src存储src,iframe 同理:

    html

    预览

    <video class="lazy" data-src="video.mp4" poster="poster.jpg" controls></video>
    <iframe class="lazy" data-src="page.html"></iframe>
    
  5. 禁用浏览器预加载:对懒加载图片添加loading="lazy"(浏览器原生懒加载属性)作为补充,但仍需兼容处理:

    html

    预览

    <img class="lazy" data-src="image.jpg" loading="lazy" alt="">
    

四、总结

  • 简单场景:用IntersectionObserver实现,性能优、代码简洁。
  • 兼容旧浏览器:用滚动监听 +getBoundingClientRect()作为降级方案。
  • 复杂项目:直接使用lazysizes等成熟库,减少重复开发。

懒加载的核心是 “按需加载”,合理使用可显著提升长列表、图片密集型页面的加载速度。

7、css如何实现等腰三角形,直角三角形

在 CSS 中实现三角形主要利用边框(border)的特性:当元素宽高为 0 时,边框会呈现三角形拼接的效果,通过隐藏部分边框即可得到不同形状的三角形。以下是等腰三角形和直角三角形的具体实现方法:

一、等腰三角形(以向上的等腰三角形为例)

原理:
  • 设置元素宽高为 0,通过调整四个方向的边框宽度和颜色,隐藏其中三个方向的边框(颜色设为透明),剩余一个方向的边框会形成等腰三角形。
代码:

css

/* 向上的等腰三角形 */
.triangle-up {
  width: 0;
  height: 0;
  /* 左右边框透明,下边框为三角形颜色,边框宽度决定三角形大小 */
  border-left: 50px solid transparent;  /* 左边框宽度 = 三角形底边长的一半 */
  border-right: 50px solid transparent; /* 右边框宽度 = 三角形底边长的一半 */
  border-bottom: 100px solid #ff0000;   /* 下边框高度 = 三角形的高 */
}
效果:
  • 底边长度 = 左边框宽度 + 右边框宽度(此处 50px + 50px = 100px)。
  • 三角形高度 = 下边框宽度(此处 100px)。
方向扩展:
  • 向下的等腰三角形:保留上边框,隐藏其他边框:

    css

    .triangle-down {
      width: 0;
      height: 0;
      border-left: 50px solid transparent;
      border-right: 50px solid transparent;
      border-top: 100px solid #00ff00; /* 上边框为颜色 */
    }
    
  • 向左的等腰三角形:保留右边框,隐藏其他边框:

    css

    .triangle-left {
      width: 0;
      height: 0;
      border-top: 50px solid transparent;
      border-bottom: 50px solid transparent;
      border-right: 100px solid #0000ff; /* 右边框为颜色 */
    }
    
  • 向右的等腰三角形:保留左边框,隐藏其他边框:

    css

    .triangle-right {
      width: 0;
      height: 0;
      border-top: 50px solid transparent;
      border-bottom: 50px solid transparent;
      border-left: 100px solid #ffff00; /* 左边框为颜色 */
    }
    

二、直角三角形(以右下角直角为例)

原理:
  • 同样设置宽高为 0,保留两个相邻方向的边框(如底部和右侧),并隐藏另外两个方向的边框,形成直角三角形。
代码:

css

/* 右下角为直角的三角形(向左上方倾斜) */
.triangle-right-angle {
  width: 0;
  height: 0;
  border-bottom: 100px solid #ff0000; /* 下边框:高度 = 三角形的高 */
  border-right: 100px solid transparent; /* 右边框:宽度 = 三角形的底 */
  /* 隐藏上、左边框(默认透明,可省略) */
}
效果:
  • 直角边分别由下边框(高度)和右边框(宽度)决定,此处形成直角边为 100px 的等腰直角三角形。
方向扩展:
  • 左上角为直角:保留上边框和左边框:

    css

    .triangle-top-left {
      width: 0;
      height: 0;
      border-top: 100px solid #00ff00;
      border-left: 100px solid transparent;
    }
    
  • 右上角为直角:保留上边框和右边框:

    css

    .triangle-top-right {
      width: 0;
      height: 0;
      border-top: 100px solid #0000ff;
      border-right: 100px solid transparent;
    }
    
  • 左下角为直角:保留下边框和左边框:

    css

    .triangle-bottom-left {
      width: 0;
      height: 0;
      border-bottom: 100px solid #ffff00;
      border-left: 100px solid transparent;
    }
    

三、关键总结

  1. 核心依赖:利用 border 的梯形拼接特性,宽高为 0 时边框会形成三角形。

  2. 形状控制

    • 等腰三角形:保留对边的边框(如上下或左右),且对称方向的透明边框宽度相等。
    • 直角三角形:保留相邻的边框(如上下 + 左右中的一组),其中一个边框设为颜色,另一个设为透明。
  3. 尺寸调整:通过修改边框的 width 值控制三角形的边长和高度。

  4. 兼容性:所有浏览器均支持,无兼容性问题。

通过调整边框的方向、宽度和颜色,可以灵活实现各种角度和大小的三角形。

8、如何实现3列布局

在 CSS 中实现 3 列布局有多种方案,适用于不同场景(如是否需要固定宽度、是否响应式等)。以下是常见的实现方式,按灵活性和兼容性排序:

一、Flexbox 布局(推荐,简单灵活)

Flexbox 是现代布局首选,代码简洁,支持灵活调整列宽和对齐方式,适合响应式场景。

代码:

html

预览

<div class="container">
  <div class="column">列1</div>
  <div class="column">列2</div>
  <div class="column">列3</div>
</div>

<style>
.container {
  display: flex; /* 启用 Flex 布局 */
  width: 100%; /* 容器宽度(可按需设置) */
  gap: 20px; /* 列之间的间距(可选) */
}

.column {
  flex: 1; /* 三列平均分配宽度 */
  height: 300px; /* 列高度(按需设置) */
  background: #f0f0f0;
  padding: 20px;
}

/* 如需设置固定宽度的列(如中间列固定) */
/* .column:nth-child(2) {
  flex: none;
  width: 300px;
} */
</style>
特点:
  • 三列默认平均分配容器宽度(flex:1 表示占比相同)。
  • 支持设置某列固定宽度(如中间列固定,两侧自适应)。
  • 响应式友好:可通过 @media 查询在小屏幕下改为堆叠布局。

二、Grid 布局(现代方案,功能强大)

Grid 布局专为二维布局设计,适合复杂网格场景,代码更直观。

代码:

html

预览

<div class="container">
  <div class="column">列1</div>
  <div class="column">列2</div>
  <div class="column">列3</div>
</div>

<style>
.container {
  display: grid; /* 启用 Grid 布局 */
  grid-template-columns: 1fr 1fr 1fr; /* 三列平均分配(1fr 表示一份) */
  gap: 20px; /* 列间距 */
  width: 100%;
}

.column {
  height: 300px;
  background: #f0f0f0;
  padding: 20px;
}

/* 如需固定列宽(如左右200px,中间自适应) */
/* .container {
  grid-template-columns: 200px 1fr 200px;
} */
</style>
特点:
  • 通过 grid-template-columns 直接定义列宽,语义清晰。
  • 支持混合固定宽度和自适应宽度(如 200px 1fr 200px)。
  • 兼容性:IE11 部分支持,现代浏览器(Chrome、Firefox 等)完全支持。

三、浮动布局(兼容旧浏览器)

通过 float 属性实现,需处理浮动清除问题,适合兼容 IE 等旧浏览器。

代码:

html

预览

<div class="container">
  <div class="column left">列1</div>
  <div class="column right">列3</div>
  <div class="column center">列2</div> <!-- 注意顺序:中间列放最后 -->
  <div class="clear"></div> <!-- 清除浮动 -->
</div>

<style>
.container {
  width: 100%;
  overflow: hidden; /* 或用 clear 类清除浮动 */
}

.column {
  height: 300px;
  padding: 20px;
  background: #f0f0f0;
}

/* 左右列固定宽度,中间列自适应 */
.left, .right {
  width: 200px;
  float: left;
}

.right {
  float: right; /* 右列靠右浮动 */
}

.center {
  margin: 0 220px; /* 左右margin等于左右列宽度+间距(200+20) */
}

.clear {
  clear: both; /* 清除浮动,避免容器高度塌陷 */
}
</style>
特点:
  • 需注意 HTML 结构顺序(中间列放最后)。
  • 需手动清除浮动(clear:both 或 overflow:hidden)。
  • 适合需要兼容 IE8 及以下的场景,现代项目不推荐。

四、响应式适配(小屏幕堆叠)

在移动设备上,3 列布局通常需要改为单列堆叠,以保证可读性。以 Flexbox 为例:

css

/* 屏幕宽度小于 768px 时改为单列 */
@media (max-width: 768px) {
  .container {
    flex-direction: column; /* 垂直排列 */
    gap: 10px;
  }
  .column {
    height: auto; /* 高度自适应内容 */
  }
}

总结

  • 首选方案:Flexbox(简单灵活,适合大多数场景)或 Grid(复杂布局更直观)。
  • 兼容旧浏览器:浮动布局(需处理清除浮动)。
  • 响应式:结合 @media 查询,在小屏幕下转为单列布局。

9、node如何实现登录功能

Node.js 实现登录功能通常涉及 用户认证(验证账号密码)、会话管理(保持登录状态)和 安全处理(密码加密、防止攻击)。以下是基于 Express 框架的完整实现方案,包含核心流程和关键代码:

一、核心技术栈

  • Web 框架:Express(处理 HTTP 请求)
  • 数据库:MongoDB + Mongoose(存储用户信息,也可替换为 MySQL 等)
  • 密码加密:bcrypt(不可逆加密,安全存储密码)
  • 会话管理:JWT(无状态令牌,适合前后端分离)或 express-session(基于 cookie 的会话)
  • 请求验证:express-validator(验证登录参数)

10、service worker做离线应用

Service Worker 是浏览器在后台运行的独立线程,可实现离线缓存、拦截网络请求等功能,是构建离线应用的核心技术。以下是利用 Service Worker 实现离线应用的完整方案:

一、核心原理

  1. 安装(Install) :Service Worker 注册后,会触发 install 事件,此时可缓存静态资源(HTML、CSS、JS、图片等)。
  2. 激活(Activate) :旧的 Service Worker 被替换时触发,可清理旧缓存。
  3. 拦截请求(Fetch) :页面所有网络请求会经过 Service Worker,可优先返回缓存内容(离线时),或请求网络并更新缓存(在线时)。

二、实现步骤

1. 注册 Service Worker(前端页面)

在主页面(如 index.html)中注册 Service Worker,使其生效:

html

预览

<!-- index.html -->
<script>
// 检查浏览器是否支持 Service Worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      // 注册 Service Worker 文件(sw.js)
      const registration = await navigator.serviceWorker.register('/sw.js');
      console.log('Service Worker 注册成功:', registration.scope);
    } catch (err) {
      console.error('Service Worker 注册失败:', err);
    }
  });
}
</script>
2. 编写 Service Worker 核心逻辑(sw.js)

创建 sw.js 文件,处理安装、激活、请求拦截等事件:

javascript

运行

// 缓存名称(版本号,用于更新缓存)
const CACHE_NAME = 'offline-cache-v1';
// 需要缓存的静态资源列表
const ASSETS_TO_CACHE = [
  '/', // 首页HTML
  '/index.html',
  '/styles.css',
  '/app.js',
  '/logo.png',
  '/fallback.html' // 离线时的备用页面
];

// 1. 安装阶段:缓存静态资源
self.addEventListener('install', (event) => {
  // 等待缓存完成后再完成安装
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => {
        console.log('缓存静态资源:', ASSETS_TO_CACHE);
        return cache.addAll(ASSETS_TO_CACHE); // 缓存所有指定资源
      })
      .then(() => self.skipWaiting()) // 跳过等待,立即激活新的 Service Worker
  );
});

// 2. 激活阶段:清理旧缓存
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((name) => {
          // 删除非当前版本的缓存
          if (name !== CACHE_NAME) {
            console.log('删除旧缓存:', name);
            return caches.delete(name);
          }
        })
      );
    }).then(() => self.clients.claim()) // 立即控制所有打开的页面
  );
});

// 3. 拦截请求:优先返回缓存,无缓存则请求网络
self.addEventListener('fetch', (event) => {
  // 对于HTML页面,采用"网络优先,缓存兜底"策略
  if (event.request.headers.get('Accept').includes('text/html')) {
    event.respondWith(
      fetch(event.request)
        .then((response) => {
          // 网络请求成功,更新缓存
          caches.open(CACHE_NAME).then((cache) => {
            cache.put(event.request, response.clone());
          });
          return response;
        })
        .catch(() => {
          // 网络失败,返回缓存的备用页面
          return caches.match('/fallback.html');
        })
    );
    return;
  }

  // 对于其他资源(CSS、JS、图片等),采用"缓存优先,网络更新"策略
  event.respondWith(
    caches.match(event.request)
      .then((cachedResponse) => {
        // 先返回缓存内容
        const fetchPromise = fetch(event.request)
          .then((networkResponse) => {
            // 网络请求成功,更新缓存
            caches.open(CACHE_NAME).then((cache) => {
              cache.put(event.request, networkResponse.clone());
            });
            return networkResponse;
          });

        // 若有缓存则返回缓存,同时后台更新缓存;若无缓存则等待网络请求
        return cachedResponse || fetchPromise;
      })
  );
});
3. 缓存策略说明
  • 静态资源(CSS/JS/ 图片) :采用「缓存优先」,优先加载缓存内容,同时后台请求网络更新缓存,确保下次访问是最新资源。
  • HTML 页面:采用「网络优先,缓存兜底」,优先请求网络获取最新内容,网络失败时返回缓存的备用页面(避免展示旧内容)。
  • 动态数据:可在 fetch 事件中单独处理 API 请求,例如缓存接口返回数据,离线时返回缓存数据。
4. 处理缓存更新

当静态资源更新时,需通过以下步骤让用户获取最新版本:

  1. 修改 sw.js 中的 CACHE_NAME(如从 offline-cache-v1 改为 offline-cache-v2)。
  2. 浏览器会检测到 sw.js 内容变化,触发重新安装。
  3. 新的 Service Worker 激活时,会清理旧版本缓存(通过 activate 事件中的逻辑)。
5. 离线状态提示(可选)

在前端页面中监听在线 / 离线状态,提示用户:

javascript

运行

// 监听网络状态变化
function updateOnlineStatus() {
  const isOnline = navigator.onLine;
  const statusEl = document.getElementById('status');
  statusEl.textContent = isOnline ? '在线' : '离线模式,部分功能可能受限';
  statusEl.className = isOnline ? 'online' : 'offline';
}

// 初始检查
updateOnlineStatus();
// 监听状态变化
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);

三、关键注意事项

  1. HTTPS 环境:Service Worker 仅在 HTTPS 环境下生效(本地开发 localhost 除外)。
  2. 作用域(Scope) :Service Worker 只能控制其注册路径及其子路径下的页面(如 /sw.js 可控制所有页面,/js/sw.js 只能控制 /js/ 下的页面)。
  3. 缓存体积:浏览器对缓存空间有限制(通常几十 MB),避免缓存过多资源。
  4. 调试工具:在 Chrome DevTools 的「Application > Service Workers」中可调试、更新、注销 Service Worker。
  5. 兼容性:现代浏览器(Chrome、Firefox、Edge、Safari 11.1+)均支持,可通过 caniuse 查看详情。

四、完整离线应用结构

plaintext

offline-app/
├── index.html       # 主页面(注册 Service Worker)
├── sw.js            # Service Worker 逻辑
├── styles.css       # 样式表(被缓存)
├── app.js           # 业务逻辑(被缓存)
├── logo.png         # 图片(被缓存)
└── fallback.html    # 离线备用页面

通过以上步骤,即可实现一个支持离线访问的应用:用户首次在线访问时缓存资源,后续离线状态下仍能加载已缓存的页面和资源,大幅提升弱网或无网环境下的用户体验。