2024-08-10

'# 华为OD机试 - 分割均衡字符串(Java & JS & Python & C & C++)

一、背景与问题

在华为OD机试中,"分割均衡字符串"是一个典型的算法题,主要考察字符串处理与平衡条件判断的能力。题目要求将输入的字符串分割成若干子串,使得每个子串满足"均衡条件"。这里的"均衡条件"通常定义为括号平衡,即每个子串中的左括号数等于右括号数。

例如,对于输入字符串 ")()())",正确的分割结果应为 ["()", "()", "()"]。这种题目在实际开发中常用于验证字符串处理逻辑、平衡条件判断以及分割策略的设计。

本篇将深入解析该问题的算法原理,结合多语言实现,探讨其适用场景、性能优化及常见陷阱。


二、基本原理

1. 问题建模

我们需要将字符串分割为若干子串,每个子串满足括号平衡的条件。核心思想是:

  • 遍历字符串,维护一个括号平衡计数器(balance)。
  • 当遇到左括号 ( 时,balance 增加 1;遇到右括号 ) 时,balance 减少 1。
  • 当 balance 为 0 时,表示当前子串已满足平衡条件,进行分割。

2. 关键点

  • 边界处理:输入字符串可能为空,或包含非法字符(如非括号字符),需提前校验。
  • 分割点判定:仅在 balance 为 0 时分割,避免部分子串未平衡。
  • 复杂度分析:时间复杂度为 $O(n)$,空间复杂度为 $O(n)$(存储结果列表)。

三、环境准备

1. 开发环境

  • Java:JDK 17,IDEA
  • JavaScript:Node.js v18,VS Code
  • Python:Python 3.9,PyCharm
  • C/C++:GCC 11,Visual Studio Code

2. 工具库

  • Java:无特殊依赖
  • JS:无特殊依赖
  • Python:无特殊依赖
  • C/C++:手动管理字符串

四、核心实现

1. Java 实现

import java.util.ArrayList;
import java.util.List;

public class BalancedStringSplitter {
    public static List<String> splitBalancedString(String s) {
        List<String> result = new ArrayList<>();
        int balance = 0;
        StringBuilder current = new StringBuilder();

        for (char c : s.toCharArray()) {
            if (c == '(') {
                balance++;
            } else if (c == ')') {
                balance--;
            }

            current.append(c);

            if (balance == 0) {
                result.add(current.toString());
                current = new StringBuilder();
            }
        }

        return result;
    }

    public static void main(String[] args) {
        String input = ")()())";
        List<String> output = splitBalancedString(input);
        System.out.println(output); // 输出: [")()", "()", "()"]
    }
}

关键代码解释:

  • balance 变量用于跟踪括号的平衡状态。
  • current 用于临时存储当前子串。
  • 当 balance == 0 时,将当前子串加入结果列表,并重置 current。

2. JavaScript 实现

function splitBalancedString(s) {
    const result = [];
    let balance = 0;
    let current = '';

    for (let c of s) {
        if (c === '(') {
            balance++;
        } else if (c === ')') {
            balance--;
        }

        current += c;

        if (balance === 0) {
            result.push(current);
            current = '';
        }
    }

    return result;
}

// 测试用例
const input = ")()())";
console.log(splitBalancedString(input)); // 输出: [")()", "()", "()"]

关键代码解释:

  • 使用 balance 跟踪括号平衡状态。
  • current 字符串拼接当前子串。
  • 分割点判定逻辑与 Java 相同。

3. Python 实现

def split_balanced_string(s):
    result = []
    balance = 0
    current = ''

    for c in s:
        if c == '(':
            balance += 1
        elif c == ')':
            balance -= 1

        current += c

        if balance == 0:
            result.append(current)
            current = ''

    return result

# 测试用例
input_str = ")()())"
print(split_balanced_string(input_str))  # 输出: [')()', '()', '()']

关键代码解释:

  • 使用 balance 和 current 变量实现与 Java/JS 相同的逻辑。
  • Python 中字符串拼接效率较高,适合处理中等规模数据。

4. C 实现

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

char** splitBalancedString(char* s, int* returnSize) {
    char** result = (char**)malloc(100 * sizeof(char*));
    int balance = 0;
    char* current = (char*)malloc(100 * sizeof(char));
    int index = 0;

    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] == '(') {
            balance++;
        } else if (s[i] == ')') {
            balance--;
        }

        current[index++] = s[i];
        current[index] = '\0';

        if (balance == 0) {
            result[*returnSize] = strdup(current);
            (*returnSize)++;
            current = (char*)malloc(100 * sizeof(char));
            index = 0;
        }
    }

    return result;
}

int main() {
    char* input = ")()())";
    int size = 0;
    char** output = splitBalancedString(input, &size);
    for (int i = 0; i < size; i++) {
        printf("%s\n", output[i]);
        free(output[i]);
    }
    free(output);
    return 0;
}

关键代码解释:

  • 使用 malloc 动态分配内存,避免固定大小数组限制。
  • current 字符串需手动管理内存,注意避免内存泄漏。

五、完整案例

1. 测试场景

输入字符串:"(()())(()())"

2. 预期输出

["(()())", "(()())"]

3. 实现验证

  • Java/JS/Python 实现均能正确分割。
  • C 实现需注意内存分配和释放。

六、源码解析

1. 分割逻辑

  • balance 变量是核心,确保每次分割时子串处于平衡状态。
  • 分割点判定逻辑 if (balance == 0) 是关键,避免分割未平衡的子串。

2. 内存管理(C 实现)

  • current 字符串需手动分配内存,避免栈溢出。
  • 分割后重新分配 current 的内存,确保连续性。

七、进阶使用

1. 多类型字符处理

若字符串包含非括号字符(如字母),需在判断前过滤:

def split_balanced_string(s):
    result = []
    balance = 0
    current = ''
    valid_chars = {'(', ')'}  # 仅处理括号字符

    for c in s:
        if c in valid_chars:
            if c == '(':
                balance += 1
            elif c == ')':
                balance -= 1
            current += c
        else:
            continue  # 忽略非括号字符

        if balance == 0:
            result.append(current)
            current = ''
    return result

2. 性能优化

  • 提前终止:若字符串中存在非法字符(如非括号),可直接返回空列表。
  • 预分配内存:C/C++ 中预分配足够内存,避免频繁 malloc。

八、性能与工程实践

1. 时间复杂度

  • O(n):每个字符仅处理一次,遍历一次即可完成分割。

2. 空间复杂度

  • O(n):结果列表的大小与输入字符串长度成正比。

3. 异常处理

  • 输入验证:确保输入为字符串,且仅包含括号字符。
  • 边界条件:空字符串返回空列表,单个括号字符返回空列表。

九、常见问题与踩坑

1. 常见错误

  • 未初始化 balance:可能导致错误的分割点判定。
  • 未处理非法字符:如字符串包含字母,导致分割结果错误。
  • C/C++ 中内存泄漏:未释放 current 内存,导致程序崩溃。

2. 解决办法

  • 初始化 balance 为 0,确保初始状态正确。
  • 过滤非法字符,确保仅处理括号。
  • C/C++ 中手动释放内存,避免内存泄漏。

十、最佳实践

1. 推荐方案

  • 优先选择 Java/JS/Python:代码简洁,内存管理自动,适合快速开发。
  • C/C++ 适用场景:需要高性能、内存控制的嵌入式系统或底层开发。

2. 实际应用

  • 括号平衡校验:如 JSON 解析、代码语法校验。
  • 日志分割:将日志按特定条件分割成子串。

3. 避免使用场景

  • 非括号字符串分割:如按字符频率分割,需重新设计算法。
  • 大数据量处理:需考虑内存优化,避免内存溢出。

十一、总结

华为OD机试中的“分割均衡字符串”问题,核心在于通过括号平衡计数器实现字符串分割。本篇从原理、多语言实现、性能优化、常见错误等方面进行了深入解析,提供了完整的代码示例和实际应用场景。通过不同语言的实现,展示了算法的通用性与灵活性,同时强调了内存管理、边界处理等关键点。在实际开发中,该算法可作为字符串处理的基础模块,适用于括号校验、日志分割等场景,但需根据具体需求选择合适的实现方案。

2024-08-10

'# js获取当前年月日 时分秒的方法(new Date()/moment.js)

一、背景与问题

在前端开发中,获取当前日期和时间是常见的需求,例如日志记录、时间戳生成、倒计时功能等。JavaScript 提供了原生的 Date 对象和第三方库 moment.js 来处理这类需求。

然而,开发者在实际使用中常遇到以下问题:

  1. Date 对象的时区处理容易出错
  2. 日期格式化需要大量手动处理
  3. moment.js 的性能损耗问题
  4. 不同时区下的时间差异

本篇文章将深入解析这两种方案的实现原理,结合实际开发场景,提供可复用的解决方案。

二、基本原理

1. JavaScript Date 对象原理

JavaScript 的 Date 对象基于 UTC 时间戳(以1970年1月1日为起点的毫秒数)进行计算。其核心原理如下:

const timestamp = Date.now(); // 获取当前时间戳
const date = new Date(timestamp); // 将时间戳转换为日期对象

Date 对象内部通过 ECMA-262 标准定义的算法,将时间戳转换为本地时间(考虑时区偏移量)。其核心计算包括:

  • 闰年处理
  • 夏令时调整
  • 时区转换(UTC±0~±14)

2. moment.js 的设计原理

moment.js 是一个基于 Date 对象的封装库,其核心思想是通过链式调用和自定义格式化方法,简化日期处理。其核心机制包括:

  • 日期对象的克隆(避免直接修改原对象)
  • 时区处理(通过 moment-timezone 模块)
  • 可扩展的格式化方法(如 moment.format())

三、环境准备

# 安装 moment.js
npm install moment

四、核心实现

1. 原生 Date 对象实现

// 获取当前日期时间
const now = new Date();

// 获取各部分时间
const year = now.getFullYear(); // 年份(四位)
const month = now.getMonth() + 1; // 月份(0-11,需+1)
const day = now.getDate(); // 日(1-31)

const hour = now.getHours(); // 小时(0-23)
const minute = now.getMinutes(); // 分钟(0-59)
const second = now.getSeconds(); // 秒(0-59)

console.log(`${year}-${month}-${day} ${hour}:${minute}:${second}`);

关键点说明:

  • getMonth() 返回 0-11,需手动+1
  • 时区处理依赖浏览器默认设置
  • 毫秒级精度支持(getMilliseconds())

2. moment.js 实现

// 引入 moment
const moment = require('moment');

// 获取当前时间
const now = moment();

// 格式化输出
const formatted = now.format('YYYY-MM-DD HH:mm:ss');
console.log(formatted);

关键点说明:

  • format() 方法支持丰富的格式化选项
  • 默认使用本地时区(moment-timezone 可扩展)
  • 支持时间差计算(moment.duration())

3. 时区处理方案

// 原生 Date 对象处理时区
const utcDate = new Date(Date.UTC(2023, 10, 1, 12, 0, 0));
console.log(utcDate.toString()); // 输出 UTC 时间

// moment.js 处理时区
const utcMoment = moment.utc('2023-11-01T12:00:00');
console.log(utcMoment.format('YYYY-MM-DD HH:mm:ss [UTC]Z')); // 输出 UTC 时间

五、完整案例

1. 日志记录系统(完整案例)

// log.js
const moment = require('moment');

function log(message) {
  const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
  console.log(`[ ${timestamp} ] ${message}`);
}

// 使用示例
log('用户登录成功');
log('系统开始运行');
// main.js
const log = require('./log');

log('启动应用');
log('加载配置');

运行效果:

[ 2023-11-08 14:30:45 ] 启动应用
[ 2023-11-08 14:30:45 ] 加载配置

2. 时间戳转换服务(完整案例)

// timestampConverter.js
const moment = require('moment');

function convertTimestamp(timestamp) {
  if (typeof timestamp === 'number') {
    return moment(timestamp).format('YYYY-MM-DD HH:mm:ss');
  } else if (typeof timestamp === 'string') {
    return moment(timestamp).format('YYYY-MM-DD HH:mm:ss');
  }
  throw new Error('Invalid timestamp format');
}

// 使用示例
console.log(convertTimestamp(Date.now()));
console.log(convertTimestamp('2023-11-08T14:30:45Z'));

六、源码解析

1. Date 对象源码解析(简化版)

function Date() {
  this._date = new Date(); // 原生 Date 对象
}

Date.prototype.getUTCFullYear = function() {
  return this._date.getUTCFullYear(); // 获取 UTC 年份
};

Date.prototype.getUTCMonth = function() {
  return this._date.getUTCMonth(); // 获取 UTC 月份
};

关键点:

  • 原生 Date 对象的 UTC 方法用于避免时区干扰
  • 通过封装实现更清晰的 API

2. moment.js 源码解析(简化版)

function moment(value) {
  this._d = new Date(value); // 原生 Date 对象
}

moment.prototype.format = function(format) {
  const result = [];
  const tokens = format.split(/([a-zA-Z]+)|(\d+)/);
  
  tokens.forEach(token => {
    if (token === 'YYYY') {
      result.push(this._d.getUTCFullYear().toString());
    } else if (token === 'MM') {
      result.push(String(this._d.getUTCMonth() + 1).padStart(2, '0'));
    }
    // 其他格式化逻辑...
  });
  
  return result.join('');
};

关键点:

  • 通过正则表达式解析格式字符串
  • 使用 UTC 方法避免时区干扰
  • 支持链式调用(moment().format())

七、进阶使用

1. 时间差计算

// 原生 Date 对象
const start = new Date();
setTimeout(() => {
  const end = new Date();
  const diff = (end - start) / 1000; // 转换为秒
  console.log(`耗时 ${diff.toFixed(2)} 秒`);
}, 1000);

2. 时间格式化规则

// moment.js 支持的格式化选项
const options = {
  year: 'YYYY', // 四位年份
  month: 'MM',  // 两位月份
  day: 'DD',    // 两位日期
  hour: 'HH',   // 24小时制
  minute: 'mm', // 分钟
  second: 'ss', // 秒
  timezone: 'Z' // 时区偏移
};

3. 时区转换

// 使用 moment-timezone 模块
const moment = require('moment-timezone');
const utcTime = moment.tz('2023-11-08T12:00:00', 'UTC');
const shanghaiTime = utcTime.tz('Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss');
console.log(shanghaiTime); // 输出 2023-11-08 20:00:00

八、性能与工程实践

1. 性能优化

方案性能特点适用场景
原生 Date轻量级,无依赖性能敏感场景
moment.js功能强大但开销大复杂格式化需求
date-fns更轻量的替代方案需要格式化功能但不需时区处理

2. 安全风险

  • 时区处理漏洞:不正确处理时区可能导致显示错误,例如:

    // 错误示例
    const date = new Date('2023-11-08T12:00:00');
    console.log(date.toString()); // 可能显示错误时区
  • 依赖安全风险:使用第三方库需注意版本安全,建议:

    npm audit

3. 异常处理

try {
  const invalidDate = new Date('Invalid Date');
  if (isNaN(invalidDate.getTime())) {
    throw new Error('无效日期');
  }
} catch (e) {
  console.error('日期处理错误:', e.message);
}

九、常见问题与踩坑

1. 常见错误

问题原因解决方案
时区错误未考虑时区偏移使用 UTC 方法或 moment-timezone
格式化错误格式字符串格式不正确使用 moment.format() 验证
性能损耗频繁创建 moment 实例使用缓存或重用实例

2. 错误示例

// 错误:未处理时区
const date = new Date('2023-11-08T12:00:00');
console.log(date.toString()); // 可能显示错误时区

// 正确:使用 UTC 方法
const utcDate = new Date(Date.UTC(2023, 10, 8, 12, 0, 0));
console.log(utcDate.toString()); // 显示 UTC 时间

3. 时区处理陷阱

// 错误:直接使用 Date 对象
const date = new Date();
console.log(date.toString()); // 可能显示本地时区

// 正确:使用 moment-timezone
const moment = require('moment-timezone');
const localTime = moment.tz(Date.now(), 'Asia/Shanghai');
console.log(localTime.format('YYYY-MM-DD HH:mm:ss'));

十、最佳实践

1. 推荐方案

场景推荐方案理由
简单时间戳原生 Date轻量级且无需依赖
复杂格式化moment.js丰富的格式化方法
时区转换moment-timezone官方支持的时区处理
性能敏感date-fns更轻量的替代方案

2. 实践建议

  • 避免直接使用 Date 对象:推荐封装为工具函数
  • 统一日期格式:使用 ISO 8601 标准格式(YYYY-MM-DDTHH:mm:ssZ)
  • 时区处理:在涉及跨时区时务必使用 UTC 或指定时区
  • 性能优化:避免在循环中频繁创建日期对象

十一、总结

JavaScript 获取当前日期时间是基础但关键的能力,本文深入探讨了原生 Date 对象和 moment.js 两种方案的实现原理、使用场景和注意事项。通过分析时区处理、格式化需求、性能优化等关键点,帮助开发者选择合适的方案。

在实际开发中,建议:

  • 对于简单场景优先使用原生 Date 对象
  • 复杂格式化需求使用 moment.js
  • 时区处理使用 moment-timezone
  • 性能敏感场景可考虑 date-fns

同时需要注意:

  • 避免直接使用 Date 对象进行时区转换
  • 正确处理格式化字符串
  • 定期检查依赖库的安全性

通过合理选择和使用这些工具,可以显著提升开发效率和代码可靠性。

2024-08-10

'# 使用three.js在网页里展示3D模型

一、背景与问题

在现代Web开发中,3D可视化已成为提升用户体验的重要手段。three.js作为基于WebGL的3D库,提供了从基础几何体到复杂场景的完整解决方案。但开发者在使用过程中常遇到以下问题:

  • 模型加载失败时的错误处理机制
  • 复杂场景下的性能瓶颈
  • 与UI交互时的同步问题
  • 移动端设备的兼容性问题
  • 安全策略下的跨域限制

这些挑战需要深入理解three.js的底层机制和WebGL的运行原理。

二、基本原理

1. WebGL渲染管线

three.js封装了WebGL的渲染流程,其核心包含:

  1. 顶点处理:顶点着色器计算几何体在3D空间中的位置
  2. 几何变换:通过矩阵运算实现模型变换(平移/旋转/缩放)
  3. 光栅化:将几何体转换为像素
  4. 像素处理:片段着色器计算最终颜色

2. three.js核心组件

  • Scene:3D场景的容器
  • Camera:控制视角的观察点
  • Renderer:将3D场景绘制到DOM元素
  • Geometry:几何体数据
  • Material:材质属性
  • Mesh:几何体+材质的组合
  • Light:光照系统

3. 模型加载机制

three.js通过loader系统支持多种格式:

// GLTF模型加载示例
const loader = new GLTFLoader();
loader.load('model.gltf', (gltf) => {
  scene.add(gltf.scene);
});

三、环境准备

1. 开发环境要求

  • 现代浏览器(支持WebGL)
  • Node.js环境(可选)
  • 基础HTML/CSS/JS知识

2. 项目结构建议

project/
├── index.html
├── main.js
├── assets/
│   ├── models/
│   └── textures/
└── styles/
    └── style.css

四、核心实现

1. 基础3D场景创建

// 初始化场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x888888);

// 创建相机
const camera = new THREE.PerspectiveCamera(
  75, 
  window.innerWidth/window.innerHeight, 
  0.1, 
  1000
);

// 创建渲染器
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 添加光源
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(10, 10, 10);
scene.add(light);

// 添加坐标轴辅助
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

// 渲染循环
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();

关键点:

  • 使用PerspectiveCamera创建透视效果
  • WebGLRenderer的抗锯齿设置
  • 光照对模型可见性的关键作用

2. GLTF模型加载与处理

// 加载GLTF模型
const loader = new THREE.GLTFLoader();
loader.parse(
  new Uint8Array(await fetch('model.gltf').then(res => res.arrayBuffer())),
  '', 
  (gltf) => {
    scene.add(gltf.scene);
    // 自动设置模型位置
    gltf.scene.position.set(0, 0, -5);
  },
  (xhr) => {
    console.log(`加载进度: ${Math.round(xhr.loaded / xhr.total * 100)}%`);
  }
);

关键点:

  • 使用parse方法处理二进制数据
  • 设置模型位置避免穿透
  • 进度回调用于用户体验优化

3. 与Canvas2D叠加

<canvas id="myCanvas" width="500" height="500"></canvas>
<div id="overlay">点击查看细节</div>
// 创建2D上下文
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// 在three.js渲染后叠加2D内容
function render() {
  requestAnimationFrame(render);
  renderer.render(scene, camera);
  
  // 绘制2D元素
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = 'red';
  ctx.fillRect(10, 10, 50, 50);
}
render();

关键点:

  • 需要同步两个渲染循环
  • 注意Canvas尺寸与three.js渲染区域的对齐
  • 使用requestAnimationFrame确保同步

五、完整案例:产品展示页面

1. 项目结构

product-demo/
├── index.html
├── main.js
├── assets/
│   ├── models/
│   │   └── product.gltf
│   └── textures/
│       └── product.jpg
└── styles/
    └── style.css

2. HTML结构

<!DOCTYPE html>
<html>
<head>
  <title>3D Product Demo</title>
  <link rel="stylesheet" href="styles/style.css">
</head>
<body>
  <div id="canvas-container"></div>
  <div id="ui-controls">
    <button id="toggle-rotation">旋转模型</button>
    <button id="toggle-details">查看详情</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/three@0.155.0/build/three.min.js"></script>
  <script src="main.js"></script>
</body>
</html>

3. JavaScript实现

// main.js
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('canvas-container').appendChild(renderer.domElement);

// 加载模型
const loader = new THREE.GLTFLoader();
loader.load('assets/models/product.gltf', (gltf) => {
  gltf.scene.scale.set(0.5, 0.5, 0.5);
  scene.add(gltf.scene);
}, undefined, (error) => {
  console.error('模型加载错误:', error);
});

// 添加光照
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(10, 10, 10);
scene.add(light);

// 控制旋转
let isRotating = false;
document.getElementById('toggle-rotation').addEventListener('click', () => {
  isRotating = !isRotating;
  if (isRotating) {
    gltf.scene.rotation.y += 0.01;
  }
});

// 渲染循环
function animate() {
  requestAnimationFrame(animate);
  if (isRotating) {
    gltf.scene.rotation.y += 0.01;
  }
  renderer.render(scene, camera);
}
animate();

// 响应式调整
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

六、源码解析

1. 渲染循环机制

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
  • 使用requestAnimationFrame确保与浏览器刷新率同步
  • 每帧更新场景状态(如旋转)
  • 避免使用setInterval或setTimeout造成的性能问题

2. 模型加载流程

loader.load('model.gltf', (gltf) => {
  scene.add(gltf.scene);
}, undefined, (error) => {
  console.error('模型加载错误:', error);
});
  • 第一个参数:模型文件路径
  • 第二个参数:加载完成回调
  • 第三个参数:进度回调(可选)
  • 第四个参数:错误回调
  • 使用GLTFLoader处理glTF格式文件

七、进阶使用

1. 动态加载与LOD技术

// 实现细节层次
const loader = new THREE.GLTFLoader();
loader.load('model.gltf', (gltf) => {
  const lowPoly = gltf.scene.clone();
  lowPoly.traverse((object) => {
    if (object.isMesh) {
      object.geometry = new THREE.SphereGeometry(1, 16, 16);
    }
  });
  scene.add(lowPoly);
});

2. 与WebGL2特性结合

const renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);

3. 与WebXR结合

const xr = new THREE.XRManager(renderer);
xr.setXRDevice(window.navigator.xr);

八、性能与工程实践

1. 性能优化策略

优化策略说明
模型压缩使用glTF格式的二进制压缩
动态加载按需加载不同细节层次
纹理优化使用压缩纹理格式(如DDS)
内存管理避免内存泄漏,及时销毁不再使用的对象
渲染优化使用postprocessing进行抗锯齿和特效处理

2. 安全风险防范

  • 跨域问题:确保模型文件服务器配置Access-Control-Allow-Origin
  • 资源注入:避免直接执行用户输入的模型数据
  • 内存安全:使用WeakRef管理大对象引用

3. 异常处理机制

try {
  // 可能抛出异常的代码
} catch (error) {
  console.error('渲染错误:', error);
  // 清理场景
  scene.clear();
}

九、常见问题与踩坑

1. 常见错误及解决办法

问题原因解决方案
模型不显示模型坐标系不匹配检查模型位置和缩放
渲染卡顿模型面数过多使用LOD技术或简化模型
光照异常灯光位置设置错误调整灯光参数
跨域错误服务器未配置CORS配置服务器响应头

2. 典型错误示例

// 错误:未设置相机位置
const camera = new THREE.PerspectiveCamera(75);
// 正确:设置相机位置
const camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
camera.position.set(0, 5, 5);

十、最佳实践

1. 开发规范建议

  • 使用ES6模块组织代码
  • 采用模块化架构(如MVC)
  • 使用TypeScript提高可维护性
  • 建立完善的错误处理机制
  • 使用性能分析工具(如Chrome DevTools)

2. 项目结构推荐

project/
├── assets/
│   ├── models/
│   ├── textures/
│   └── shaders/
├── components/
│   ├── ModelRenderer.js
│   └── UIController.js
├── utils/
│   └── Loader.js
├── styles/
│   └── style.css
└── main.js

3. 性能优化建议

  • 使用MeshStandardMaterial替代MeshBasicMaterial获得更好的光照效果
  • 使用CanvasTexture实现更复杂的材质效果
  • 使用WebGL2的更高级特性(如计算着色器)

十一、总结

three.js作为Web3D开发的基石,提供了从基础几何体到复杂场景的完整解决方案。在实际项目中,应根据需求选择合适的实现方式:

  • 推荐使用场景:

    • 需要交互式3D展示的电商产品页
    • 工业设计的可视化展示
    • 地理信息系统(GIS)地图
    • 游戏开发的原型设计
  • 不推荐使用场景:

    • 简单的静态图片展示
    • 移动端低端设备上的复杂场景
    • 需要大量实时计算的科学可视化

在开发过程中,需要特别注意性能优化、安全策略和跨平台兼容性。通过合理使用LOD技术、纹理压缩、动态加载等策略,可以构建出高效的3D可视化系统。对于复杂项目,建议采用TypeScript+模块化架构,结合WebXR等扩展功能,实现更丰富的交互体验。

'# uniapp的bug解决:11:34:15.673 at Module._resolveFilename (node:internal/modules/cjs/loader:1140:15)

一、背景与问题

在uniapp开发过程中,我们可能遇到如下错误日志:

11:34:15.673 at Module._resolveFilename (node:internal/modules/cjs/loader:1140:15)

这个错误本质上是Node.js模块加载器的报错,表示无法解析某个模块的路径。在uniapp开发中,这个错误通常出现在以下场景:

  1. 使用了Node.js环境的第三方库(如fs、path等)
  2. 项目中存在不规范的模块引用
  3. 使用了微信小程序的API但未正确处理平台差异
  4. 构建配置中存在路径错误

这个错误在开发过程中可能带来严重困扰,特别是当项目中混用多端运行时。我们需要深入理解其原理并掌握正确的解决方法。

二、基本原理

1. Node.js模块加载机制

Node.js的模块加载过程分为以下几个关键步骤:

  1. 检查文件是否是内置模块(如fs、path等)
  2. 检查文件是否是核心模块(如Buffer、Stream等)
  3. 检查文件是否是文件模块(通过路径解析)
  4. 加载模块并执行其导出内容

在uniapp中,由于其运行环境的特殊性,这一机制会发生变化:

  • 在微信小程序中,模块加载完全由微信运行时管理
  • 在H5端,可能使用的是浏览器的ES模块系统
  • 在App端,可能调用原生模块的加载逻辑

2. 路径解析规则

Node.js的路径解析遵循特定规则,例如:

const path = require('path');
console.log(path.resolve(__dirname, 'test.js')); // 当前目录下test.js

但在uniapp中,由于多端运行的特性,路径解析规则会根据平台不同而变化:

  • 微信小程序:使用相对路径,不支持绝对路径
  • H5端:支持标准的URL路径
  • App端:可能使用原生模块的路径规则

三、环境准备

在开始调试之前,请确保:

  1. 安装uniapp开发环境(最新版本)
  2. 配置好项目的基础结构
  3. 安装必要的依赖(如node_modules)
# 安装uniapp
npm install -g @dcloudio/uni-cli

# 创建项目
uni create my-project

四、核心实现

1. 错误场景示例

// 错误代码示例:尝试使用Node.js模块
const fs = require('fs');
fs.readFile('test.txt', 'utf8', (err, data) => {
  console.log(data);
});

这段代码在微信小程序中会报错,因为微信小程序不支持Node.js的fs模块。

2. 正确实现方式

// 正确代码示例:使用uni-app的API
uni.readLocalFile({
  filePath: 'test.txt',
  success: (res) => {
    console.log(res.data);
  }
});

3. 路径解析示例

// 正确的路径处理
const path = require('path');

// 微信小程序中使用相对路径
const filePath = path.join(__dirname, 'test.txt');
console.log(filePath); // 输出: "test.txt"

// H5端使用URL路径
const urlPath = 'https://example.com/test.txt';
console.log(urlPath);

五、完整案例

1. 项目结构

my-project/
├── pages/
│   └── index/
│       ├── index.vue
│       └── utils/
│           └── fileUtil.js
├── common/
│   └── config.js
├── package.json
└── App.vue

2. 文件内容

fileUtil.js

export function readLocalFile(filePath) {
  return new Promise((resolve, reject) => {
    uni.readLocalFile({
      filePath: filePath,
      success: (res) => resolve(res.data),
      fail: (err) => reject(err)
    });
  });
}

index.vue

<template>
  <view>
    <text>{{ content }}</text>
  </view>
</template>

<script>
import { readLocalFile } from '@/utils/fileUtil.js';

export default {
  data() {
    return {
      content: ''
    };
  },
  mounted() {
    readLocalFile('test.txt')
      .then(content => this.content = content)
      .catch(err => console.error(err));
  }
};
</script>

六、源码解析

以uni.readLocalFile为例,其底层实现可能如下:

// 微信小程序的实现
function readLocalFile(filePath) {
  return new Promise((resolve, reject) => {
    wx.getFileSystemManager().readFile({
      filePath: filePath,
      success: resolve,
      fail: reject
    });
  });
}

对于H5端,可能会使用浏览器的File API:

function readLocalFile(filePath) {
  return new Promise((resolve, reject) => {
    fetch(filePath)
      .then(res => res.text())
      .then(resolve)
      .catch(reject);
  });
}

七、进阶使用

1. 多端兼容的封装

function readLocalFile(filePath) {
  return new Promise((resolve, reject) => {
    if (process.env.VUE_APP_PLATFORM === 'mp-weixin') {
      uni.readLocalFile({
        filePath: filePath,
        success: resolve,
        fail: reject
      });
    } else if (process.env.VUE_APP_PLATFORM === 'h5') {
      fetch(filePath)
        .then(res => res.text())
        .then(resolve)
        .catch(reject);
    } else {
      reject(new Error('Unsupported platform'));
    }
  });
}

2. 路径处理策略

function resolveFilePath(filePath) {
  if (process.env.VUE_APP_PLATFORM === 'mp-weixin') {
    return filePath; // 微信小程序直接使用相对路径
  } else if (process.env.VUE_APP_PLATFORM === 'h5') {
    return `https://example.com/${filePath}`; // H5端转换为URL
  }
  return filePath; // 其他平台保持原样
}

八、性能与工程实践

1. 性能优化

  1. 缓存文件路径:避免重复解析路径
  2. 异步处理:避免阻塞主线程
  3. 按需加载:只加载必要的文件
  4. 压缩文件:减小文件体积

2. 异常处理

try {
  await readLocalFile('test.txt');
} catch (err) {
  console.error('读取文件失败:', err);
  // 提示用户检查文件是否存在
}

3. 安全风险

  1. 路径注入:确保文件路径经过校验
  2. 文件权限:避免读取敏感文件
  3. 内容过滤:防止恶意代码注入

九、常见问题与踩坑

1. 常见错误

错误场景原因解决方法
路径解析错误文件路径不正确使用resolveFilePath进行路径转换
模块加载失败使用了Node.js模块替换为uni-app提供的API
跨平台兼容问题不同平台的API差异使用process.env.VUE_APP_PLATFORM判断平台
文件未找到文件不存在或路径错误检查文件是否存在,确认路径是否正确

2. 常见坑点

  1. 忽略平台差异:直接复制Node.js代码到uniapp项目
  2. 未处理异步错误:未正确捕获Promise的异常
  3. 路径拼接错误:未考虑不同平台的路径规则差异
  4. 文件大小限制:未处理大文件读取时的性能问题

十、最佳实践

  1. 使用uni-app提供的API:避免直接使用Node.js模块
  2. 统一路径处理:使用resolveFilePath函数处理路径
  3. 平台差异处理:通过process.env.VUE_APP_PLATFORM判断运行环境
  4. 错误处理机制:为所有异步操作添加异常捕获
  5. 性能优化:对于大文件使用分块读取,避免阻塞主线程

十一、总结

通过分析Module._resolveFilename错误的原理,我们可以发现其本质是模块加载路径的问题。在uniapp开发中,需要特别注意多端运行环境的差异,合理使用uni-app提供的API,避免直接使用Node.js模块。

在实际开发中,建议:

  • 在需要文件操作时,优先使用uni-app的文件API
  • 对于复杂的文件处理逻辑,建议封装成独立的工具模块
  • 在构建配置中,注意不同平台的路径规则差异
  • 对于性能敏感的场景,使用分块读取和异步处理

通过遵循这些最佳实践,我们可以有效避免类似错误,提高uniapp项目的稳定性和可维护性。

'# 【UmiJS】UmiJS基础使用与集成

一、背景与问题

在现代前端开发中,随着项目规模的扩大,传统的React项目逐渐暴露出配置繁琐、代码组织混乱、路由管理复杂等问题。UmiJS作为阿里巴巴开源的下一代前端框架,通过其插件化架构、内置路由和代码生成能力,解决了传统React项目中常见的痛点。

在实际开发中,开发者常遇到以下问题:

  • 路由配置需要手动编写大量重复代码
  • 项目结构缺乏统一规范
  • 多页面应用需要复杂的配置
  • 资源加载性能优化困难

UmiJS通过其核心机制(如插件系统、代码生成、路由热更新)提供了优雅的解决方案,但其复杂性也带来了一些特殊的使用场景和注意事项。

二、基本原理

1. UmiJS架构设计

UmiJS采用插件化架构,核心组件包括:

  • umi:主命令行工具
  • @umijs/plugin-react:React项目的基础支持插件
  • @umijs/plugin-routing:路由插件
  • @umijs/plugin-model:模型管理插件
  • @umijs/plugin-locale:国际化插件
  • @umijs/plugin-antd:Ant Design集成插件

其核心工作原理如下:

  1. 通过 umi create 命令生成项目结构
  2. 根据 config/config.ts 配置文件加载插件
  3. 使用 @umijs/webpack 构建工具进行代码打包
  4. 通过 @umijs/runner 执行构建流程
  5. 利用 @umijs/runner 提供的热更新机制

2. 路由机制

UmiJS的路由系统基于 @umijs/plugin-routing 插件,其核心特点包括:

  • 基于文件结构自动生成路由
  • 支持动态路由和参数匹配
  • 提供路由热更新能力
  • 支持路由权限控制

3. 代码生成机制

UmiJS通过 @umijs/runner 实现的代码生成机制:

  • 根据路由配置文件生成组件代码
  • 自动创建页面组件和布局结构
  • 支持代码分割和懒加载
  • 提供类型推断和TypeScript支持

三、环境准备

1. 安装依赖

# 安装UmiJS CLI
npm install -g umi

# 创建新项目
umi create my-umi-app

# 进入项目目录
cd my-umi-app

2. 项目结构

my-umi-app/
├── config/
│   └── config.ts
├── src/
│   ├── pages/
│   │   ├── index.tsx
│   │   └── user/
│   │       └── index.tsx
│   ├── model/
│   │   └── index.ts
│   └── layout.tsx
├── .umi/
├── package.json
└── tsconfig.json

四、核心实现

1. 路由配置示例

// config/config.ts
export default defineConfig({
  routes: [
    {
      path: '/',
      component: 'index',
      routes: [
        {
          path: '/user',
          component: 'user',
          routes: [
            {
              path: '/user/detail',
              component: 'detail',
            },
          ],
        },
      ],
    },
  ],
});

关键代码解释:

  • defineConfig 是UmiJS的配置函数
  • path 字段定义路由路径
  • component 字段指向对应页面组件
  • 嵌套的 routes 字段支持多层路由结构
  • 使用 :id 可定义动态路由参数

2. 模型管理示例

// src/model/index.ts
export namespace MyModel {
  export namespace User {
    export interface State {
      users: User[];
    }
    
    export interface User {
      id: number;
      name: string;
    }
  }
}

关键代码解释:

  • 使用 namespace 定义模型结构
  • State 接口定义状态结构
  • User 接口定义数据结构
  • 支持类型推断和类型安全

3. 插件集成示例

// config/config.ts
export default defineConfig({
  plugins: [
    '@umijs/plugin-react',
    '@umijs/plugin-routing',
    '@umijs/plugin-model',
    '@umijs/plugin-locale',
  ],
});

关键代码解释:

  • plugins 数组定义启用的插件
  • 每个插件提供特定功能
  • @umijs/plugin-react 提供React支持
  • @umijs/plugin-routing 提供路由功能
  • @umijs/plugin-model 提供模型管理

五、完整案例

1. 电商项目案例

项目结构

ecommerce/
├── config/
│   └── config.ts
├── src/
│   ├── pages/
│   │   ├── product/
│   │   │   ├── index.tsx
│   │   │   └── detail/
│   │   │       └── index.tsx
│   │   ├── cart/
│   │   │   └── index.tsx
│   │   └── layout.tsx
│   ├── model/
│   │   ├── product.ts
│   │   └── cart.ts
│   └── services/
│       └── api.ts
├── .umi/
├── package.json
└── tsconfig.json

配置文件

// config/config.ts
export default defineConfig({
  routes: [
    {
      path: '/',
      component: 'layout',
      routes: [
        {
          path: '/product',
          component: 'product',
          routes: [
            {
              path: '/product/detail/:id',
              component: 'detail',
            },
          ],
        },
        {
          path: '/cart',
          component: 'cart',
        },
      ],
    },
  ],
  plugins: [
    '@umijs/plugin-react',
    '@umijs/plugin-routing',
    '@umijs/plugin-model',
    '@umijs/plugin-locale',
  ],
});

模型定义

// src/model/product.ts
export namespace Product {
  export interface State {
    products: Product[];
  }
  
  export interface Product {
    id: number;
    name: string;
    price: number;
    image: string;
  }
}

页面组件

// src/pages/product/index.tsx
import React from 'react';
import { useModel } from '@umijs/max';

export default function ProductList() {
  const { products } = useModel('Product');

  return (
    <div>
      <h2>商品列表</h2>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            <img src={product.image} alt={product.name} />
            <p>{product.name}</p>
            <p>价格: {product.price}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

六、源码解析

1. 路由生成机制

// @umijs/runner/src/runner.js
function generateRouteCode(routes) {
  return routes.map(route => {
    const { path, component, routes } = route;
    return `
      {
        path: '${path}',
        component: ${component},
        routes: ${routes ? generateRouteCode(routes) : '[]'}
      }
    `;
  }).join(',\n');
}

关键点:

  • 递归生成路由配置
  • 支持动态路由参数
  • 自动处理嵌套路由

2. 模型管理机制

// @umijs/runner/src/model.ts
function generateModelCode(models) {
  return models.map(model => {
    const { namespace, state, effects } = model;
    return `
      namespace ${namespace} {
        ${state ? 'state ' + state : ''}
        ${effects ? 'effects ' + effects : ''}
      }
    `;
  }).join('\n\n');
}

关键点:

  • 支持命名空间隔离
  • 自动类型推断
  • 集成Redux风格的effects

七、进阶使用

1. 自定义插件开发

// plugins/my-plugin/index.js
export default function({ config, plugins }) {
  return {
    name: 'my-plugin',
    configureWebpack: () => ({
      plugins: [
        new webpack.DefinePlugin({
          __MY_PLUGIN__: true,
        }),
      ],
    }),
  };
}

2. 代码分割优化

// config/config.ts
export default defineConfig({
  routes: [
    {
      path: '/',
      component: 'layout',
      routes: [
        {
          path: '/product',
          component: 'product',
          routes: [
            {
              path: '/product/detail/:id',
              component: 'detail',
              // 启用代码分割
              split: true,
            },
          ],
        },
      ],
    },
  ],
});

3. TypeScritp支持

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2021",
    "module": "ESNext",
    "strict": true,
    "jsx": "react",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "rootDir": ".",
    "resolveJsonModule": true
  },
  "include": ["src"]
}

八、性能与工程实践

1. 性能优化策略

优化策略实现方式效果
代码分割配置 split: true减少初始加载体积
资源压缩使用 @umijs/plugin-optimization减少网络传输数据
路由懒加载配置 dynamic: true降低首屏加载时间
缓存策略配置 cache: true减少重复构建时间
前端资源预加载使用 preload: true提升首次访问速度

2. 异常处理方案

// config/config.ts
export default defineConfig({
  plugins: [
    {
      name: 'error-handling',
      configureWebpack: (config) => {
        config.optimization = {
          ...config.optimization,
          providedExports: true,
          usedExports: true,
        };
      },
    },
  ],
});

3. 安全风险控制

  • 路由安全:通过 @umijs/plugin-locale 实现国际化安全
  • 依赖管理:通过 npm audit 检查依赖安全
  • 权限控制:通过 @umijs/plugin-access 实现访问控制
  • 代码安全:通过 @umijs/plugin-coverage 进行代码覆盖测试

九、常见问题与踩坑

1. 常见错误

错误示例

ERROR  Failed to compile.
./src/pages/index.tsx
Module not found: Can't resolve 'antd'

原因分析:

  • 未安装Ant Design依赖
  • 未启用 @umijs/plugin-antd 插件

解决方案:

npm install antd
// config/config.ts
export default defineConfig({
  plugins: [
    '@umijs/plugin-react',
    '@umijs/plugin-routing',
    '@umijs/plugin-model',
    '@umijs/plugin-antd', // 添加此行
  ],
});

2. 性能问题

问题描述:

大型项目中,页面加载时间过长

解决方案:

  1. 启用代码分割
  2. 使用 @umijs/plugin-optimization 插件
  3. 启用资源压缩
  4. 使用 @umijs/plugin-webpack 自定义打包策略

3. 兼容性问题

问题描述:

TypeScript类型错误

解决方案:

  1. 确保 tsconfig.json 配置正确
  2. 使用 @umijs/plugin-typescript 插件
  3. 配置 types 字段
// config/config.ts
export default defineConfig({
  types: [
    '@umijs/umi-types',
    '@umijs/umi',
    '@umijs/plugins',
  ],
});

十、最佳实践

1. 推荐方案

  • 使用 @umijs/plugin-routing 管理复杂路由
  • 遵循 "one page per file" 原则
  • 使用 @umijs/plugin-model 管理业务逻辑
  • 启用 @umijs/plugin-locale 实现国际化
  • 使用 @umijs/plugin-antd 快速集成Ant Design

2. 使用场景

  • 中大型企业级应用
  • 需要复杂路由管理的项目
  • 需要类型安全的TypeScript项目
  • 需要统一代码规范的团队项目
  • 需要快速迭代的敏捷开发项目

3. 不推荐场景

  • 简单的单页应用
  • 需要完全自定义构建流程的项目
  • 不需要路由管理的纯组件库
  • 需要完全控制webpack配置的项目
  • 需要完全独立的构建环境

十一、总结

UmiJS通过其插件化架构和代码生成机制,为现代前端开发提供了强大的工具。其核心优势在于:

  • 简化路由配置
  • 提供类型安全
  • 支持代码分割
  • 促进团队协作
  • 易于扩展插件

但使用时也需要注意:

  • 复杂项目需要良好的插件管理
  • 需要合理配置构建流程
  • 需要关注性能优化
  • 需要处理安全风险

在实际项目中,建议根据项目规模选择合适的工具。对于中大型项目,UmiJS提供了强大的支持;对于小型项目,可以考虑更轻量级的解决方案。通过合理配置和插件管理,可以充分发挥UmiJS的潜力,构建高质量的现代前端应用。

'# React Native | Java和JavaScript互操作

一、背景与问题

在React Native开发中,JavaScript层与原生代码的交互是核心能力之一。对于Android平台,开发者需要在Java层和JavaScript层之间建立双向通信通道,以实现如下场景:

  • 通过JS调用原生代码(如访问设备硬件、执行性能敏感操作)
  • 从原生代码向JS传递数据(如返回结果、触发事件)
  • 保持两层代码的同步性与一致性

传统做法是通过React Native的Bridge机制进行通信,但这种方式存在以下局限:

  1. 跨线程通信导致的性能损耗
  2. 频繁的JSON序列化/反序列化开销
  3. 同步调用时的阻塞问题
  4. 跨平台代码复用的困难

本文将深入探讨React Native Android平台的Java与JavaScript互操作机制,分析其底层原理,并给出可落地的解决方案。

二、基本原理

React Native的Android平台通信机制主要包含以下几个核心组件:

  1. Bridge:负责JS与原生代码的通信通道,包含:

    • 事件队列(EventQueue)
    • 消息分发器(MessageDispatcher)
    • 线程池(ThreadPool)
  2. Module:Java层暴露的接口,包含:

    • 同步方法(同步调用)
    • 异步方法(通过回调机制)
  3. JSI(JavaScript Interface):从React Native 0.60版本开始引入的底层接口,提供更直接的互操作能力
  4. JSContext:JS运行时环境,包含:

    • 作用域(Scope)
    • 垃圾回收机制
    • 调用栈信息

三、环境准备

1. 开发环境要求

  • Android Studio 4.x
  • Java 8
  • Node.js 14+
  • React Native CLI 0.65+

2. 项目结构

my-app/
├── android/
│   └── app/
│       └── src/
│           └── main/
│               └── java/
│                   └── com/
│                       └── myapp/
│                           └── NativeModule.java
├── ios/
├── index.js
└── App.js

四、核心实现

1. Java模块创建

// NativeModule.java
package com.myapp;

import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;

public class NativeModule extends ReactContextBaseJavaModule {
    public NativeModule(ReactContext context) {
        super(context);
    }

    @Override
    public String getName() {
        return "NativeModule";
    }

    @ReactMethod
    public void syncMethod(String input, Callback callback) {
        // 同步方法:直接返回结果
        callback.invoke(input + " from Java");
    }

    @ReactMethod
    public void asyncMethod(String input, Callback callback) {
        // 异步方法:模拟网络请求
        new Thread(() -> {
            try {
                Thread.sleep(1000);
                callback.invoke(input + " async from Java");
            } catch (Exception e) {
                callback.invoke("Error: " + e.getMessage());
            }
        }).start();
    }
}

关键点说明:

  • ReactContextBaseJavaModule 是所有模块的基类
  • @ReactMethod 注解用于标记可被调用的方法
  • Callback 用于异步返回结果

2. JavaScript调用

// App.js
import React, { useEffect } from 'react';
import { NativeModules, NativeEventEmitter, NativeEventSubscription } from 'react-native';

const { NativeModule } = NativeModules;

const App = () => {
  useEffect(() => {
    // 同步调用
    NativeModule.syncMethod("Hello", (result) => {
      console.log("Sync result:", result);
    });

    // 异步调用
    NativeModule.asyncMethod("World", (result) => {
      console.log("Async result:", result);
    });
  }, []);

  return (
    <View>
      <Text>React Native Interop</Text>
    </View>
  );
};

export default App;

3. Java调用JS方法

// NativeModule.java
public class NativeModule extends ReactContextBaseJavaModule {
    ...

    @ReactMethod
    public void callJSFunction(String methodName, ReadableMap args, Callback callback) {
        // 通过JSI调用JS方法
        ReactContext reactContext = getReactContext();
        if (reactContext == null) return;

        ReactJSIInteropModuleRegistry registry = reactContext.getJSIInteropModuleRegistry();
        JSIInteropModule jsModule = registry.getJSIInteropModule();

        // 调用JS函数
        jsModule.callJSFunction(
            "App", 
            "myFunction", 
            args, 
            (result) -> {
                callback.invoke(result);
            }
        );
    }
}

五、完整案例:调用系统相机

1. Java模块实现

public class CameraModule extends ReactContextBaseJavaModule {
    public CameraModule(ReactContext context) {
        super(context);
    }

    @Override
    public String getName() {
        return "CameraModule";
    }

    @ReactMethod
    public void takePhoto(Callback callback) {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (intent.resolveActivity(getReactContext().getPackageManager()) != null) {
            getReactContext().getActivityResultLauncher().launch(intent);
        } else {
            callback.invoke("Camera not available");
        }
    }

    @ReactMethod
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == 1 && resultCode == Activity.RESULT_OK) {
            Uri imageUri = data.getData();
            callback.invoke(imageUri.toString());
        }
    }
}

2. JavaScript调用

// App.js
import React, { useEffect } from 'react';
import { NativeModules, Image, Alert } from 'react-native';

const { CameraModule } = NativeModules;

const App = () => {
  const takePhoto = () => {
    CameraModule.takePhoto((uri) => {
      if (uri.startsWith("Error")) {
        Alert.alert("Error", uri);
      } else {
        <Image source={{ uri }} style={{ width: 200, height: 200 }} />
      }
    });
  };

  return (
    <View>
      <Button title="Take Photo" onPress={takePhoto} />
    </View>
  );
};

六、源码解析

1. Bridge通信机制

React Native的Bridge采用双线程模型:

  • JS线程:负责UI渲染和事件处理
  • 原生线程:负责处理原生代码调用

通信流程如下:

  1. JS调用原生方法 → 通过Bridge发送消息
  2. 消息进入原生线程的队列
  3. 原生方法执行 → 通过Bridge返回结果
  4. 结果返回JS线程

2. 事件处理机制

// 事件注册示例
ReactContext reactContext = getReactContext();
reactContext.addJSModule(NativeModule.class);
// 事件监听示例
NativeModule.addListener('photoTaken', (uri) => {
  console.log("Photo taken:", uri);
});

七、进阶使用

1. 使用JSI提升性能

// JSI模块注册
public class JSIExample extends ReactContextBaseJavaModule {
    public JSIExample(ReactContext context) {
        super(context);
    }

    @Override
    public String getName() {
        return "JSIExample";
    }

    @ReactMethod
    public void callJSFunction(String methodName, ReadableMap args, Callback callback) {
        ReactContext reactContext = getReactContext();
        if (reactContext == null) return;

        ReactJSIInteropModuleRegistry registry = reactContext.getJSIInteropModuleRegistry();
        JSIInteropModule jsModule = registry.getJSIInteropModule();

        jsModule.callJSFunction(
            "App", 
            "myFunction", 
            args, 
            (result) -> {
                callback.invoke(result);
            }
        );
    }
}

2. 复杂数据类型处理

// 处理可变对象
public class DataModel {
    public String name;
    public int id;

    public DataModel(String name, int id) {
        this.name = name;
        this.id = id;
    }
}

@ReactMethod
public void sendData(DataModel data, Callback callback) {
    callback.invoke(data.name + " " + data.id);
}
// JS端接收
NativeModule.sendData({ name: "Test", id: 123 }, (result) => {
    console.log("Received:", result);
});

八、性能与工程实践

1. 性能优化策略

优化点解决方案
同步调用阻塞使用异步方法,避免主线程阻塞
频繁序列化使用JSI直接传递对象
内存泄漏使用WeakReference管理对象引用
线程切换使用JSI直接访问JS上下文

2. 安全注意事项

  • 禁止暴露敏感API(如getDeviceId())
  • 对输入参数进行验证
  • 对返回数据进行脱敏处理
  • 使用权限控制机制

3. 异常处理

@ReactMethod
public void riskyOperation(String input, Callback callback) {
    try {
        // 模拟可能抛出异常的操作
        if (input.equals("error")) {
            throw new Exception("Invalid input");
        }
        callback.invoke("Success");
    } catch (Exception e) {
        callback.invoke("Error: " + e.getMessage());
    }
}

九、常见问题与踩坑

1. 常见错误

问题原因解决方案
无法调用原生方法未正确注册模块检查模块注册代码
乱码/异常编码不一致确保UTF-8编码
内存泄漏未正确释放资源使用ReactContextBaseJavaModule生命周期管理
线程问题在非主线程更新UI使用runOnUiThread

2. 踩坑案例

// 错误示例:直接修改JS对象
NativeModule.someMethod({ key: "value" }, (result) => {
    result.key = "modified"; // 不会生效
});
// 正确示例:创建新对象
NativeModule.someMethod({ key: "value" }, (result) => {
    const newObj = { ...result, key: "modified" };
    console.log(newObj);
});

十、最佳实践

1. 使用场景建议

场景是否使用
访问设备硬件✅
高性能计算✅
重用现有Java库✅
简单UI交互❌
频繁数据更新❌

2. 安全实践

  • 对敏感API添加权限检查
  • 使用加密传输敏感数据
  • 对输入进行严格校验
  • 使用@ReactMethod注解防止未授权调用

3. 性能实践

  • 使用JSI代替Bridge通信
  • 避免频繁的同步调用
  • 使用弱引用管理对象
  • 对复杂数据类型进行序列化优化

十一、总结

React Native的Java和JavaScript互操作是实现混合开发的核心能力。通过理解Bridge机制、合理使用JSI接口、注意线程安全和内存管理,开发者可以构建高性能的混合应用。在实际开发中,应根据具体需求选择合适的互操作方式,避免不必要的性能损耗。同时,要特别注意安全性和可维护性,确保代码的健壮性和可扩展性。掌握这些核心能力,将显著提升React Native开发的效率和质量。

2024-08-10

'# NodeJs下express使用:body-parser和morgan的安装与使用

一、背景与问题

在构建基于Express的Node.js应用时,处理HTTP请求的输入输出是核心环节。body-parser和morgan作为Express生态中最基础的中间件,分别承担着请求体解析和日志记录的核心职责。但它们的使用往往被开发者忽略其底层原理和潜在风险。

在实际开发中,我们常遇到以下问题:

  • 接收POST请求时出现"Cannot read property 'xxx' of undefined"的错误
  • 日志文件体积过大影响系统性能
  • 安全审计时发现敏感信息泄露
  • 跨域请求时出现日志格式异常

这些问题的根本原因往往在于对body-parser和morgan的原理理解不深,或者在配置时未考虑实际场景。

二、基本原理

1. body-parser工作原理

body-parser是Express内置的中间件,其核心功能是将HTTP请求体转换为JavaScript对象。它通过解析Content-Type头信息,使用不同的解析器处理不同格式的数据:

// 基础用法
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

其内部通过parse方法处理请求体,具体流程如下:

  1. 检查请求头Content-Type
  2. 根据Content-Type选择解析器
  3. 创建缓冲区存储原始数据
  4. 调用对应解析器处理数据
  5. 将解析结果附加到req.body

对于JSON数据的处理,其底层使用的是JSON.parse(),但会进行以下优化:

  • 自动处理JSON字符串中的特殊字符
  • 支持流式处理大文件
  • 自动处理多部分表单数据

2. morgan工作原理

morgan通过读取请求的元数据,按照预定义的格式生成日志。其核心处理流程如下:

// 基础用法
app.use(morgan('tiny'));
  1. 从req对象获取请求信息
  2. 按照指定格式格式化日志
  3. 将日志写入指定输出流(默认是console)

其格式字符串支持以下占位符:

  • :method - HTTP方法
  • :url - 请求路径
  • :status - HTTP状态码
  • :res[content-length] - 响应体大小
  • :res[duration] - 响应耗时(毫秒)

三、环境准备

确保已安装Node.js环境,创建项目结构:

mkdir express-demo
cd express-demo
npm init -y
npm install express body-parser morgan

四、核心实现

1. body-parser基本用法

// app.js
const express = require('express');
const app = express();

// 解析JSON格式的请求体
app.use(express.json({
  limit: '10kb' // 限制最大接收大小
}));

// 解析URL编码格式的请求体
app.use(express.urlencoded({ extended: true }));

// 处理POST请求
app.post('/api/data', (req, res) => {
  console.log('Received data:', req.body);
  res.json({ status: 'success' });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

关键代码解释:

  • express.json()会自动处理application/json类型的请求
  • extended: true允许解析复杂对象(如嵌套对象)
  • limit选项用于防止大文件上传导致内存溢出
  • 未配置body-parser时,req.body会是undefined

2. morgan日志格式定制

// custom-morgan.js
const morgan = require('morgan');

// 自定义日志格式
const customFormat = morgan.format('custom', (tokens, req, res) => {
  return [
    `【${tokens.time(req, res)}】`,
    `HTTP方法: ${tokens.method(req, res)}`,
    `请求路径: ${tokens.url(req, res)}`,
    `状态码: ${tokens.status(req, res)}`,
    `响应体大小: ${tokens['res[content-length]'](req, res)} bytes`,
    `耗时: ${tokens['res[duration]'](req, res)} ms`
  ].join(' | ');
});

// 使用自定义格式
const logger = morgan(customFormat, {
  skip: (req, res) => req.url === '/healthcheck', // 排除健康检查接口
  stream: {
    write: (message) => {
      // 将日志写入文件
      require('fs').writeFileSync('access.log', message, { flag: 'a' });
    }
  }
});

module.exports = logger;

关键代码解释:

  • 自定义格式函数接收tokens参数,可以访问所有可用的token
  • skip选项用于排除不需要记录日志的接口
  • stream选项可以自定义日志输出方式
  • 使用writeFileSync写入文件时需注意文件锁和性能问题

3. body-parser与morgan的协同使用

// combined-use.js
const express = require('express');
const morgan = require('morgan');
const { createLogger, transports, format } = require('winston');

const app = express();

// 自定义日志记录器
const logger = createLogger({
  level: 'info',
  transports: [
    new transports.Console(),
    new transports.File({ filename: 'combined.log' })
  ]
});

// 定义自定义日志格式
const customFormat = format.combine(
  format.timestamp(),
  format.printf((info) => {
    return `${info.timestamp} [${info.level}] ${info.message}`;
  })
);

// 使用morgan记录访问日志
app.use(morgan('combined', {
  stream: logger.stream,
  skip: (req, res) => req.url === '/healthcheck'
}));

// 使用body-parser解析请求体
app.use(express.json({
  limit: '500kb'
}));

// 处理POST请求
app.post('/api/data', (req, res) => {
  logger.info(`Received data: ${JSON.stringify(req.body)}`);
  res.json({ status: 'success' });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

关键代码解释:

  • 使用winston作为日志记录器,可以更灵活地控制日志输出
  • morgan的stream选项支持自定义输出流
  • 通过skip选项排除不需要记录的接口
  • body-parser的limit选项控制请求体大小,防止内存溢出

五、完整案例

创建一个完整的RESTful API服务,同时处理日志记录和请求体解析:

// app.js
const express = require('express');
const morgan = require('morgan');
const { createLogger, transports, format } = require('winston');
const fs = require('fs');

const app = express();

// 自定义日志记录器
const logger = createLogger({
  level: 'info',
  format: format.combine(
    format.timestamp(),
    format.printf((info) => {
      return `${info.timestamp} [${info.level}] ${info.message}`;
    })
  ),
  transports: [
    new transports.Console(),
    new transports.File({ filename: 'api.log' })
  ]
]);

// 自定义日志格式
const customFormat = morgan.format('custom', (tokens, req, res) => {
  return [
    `【${tokens.time(req, res)}】`,
    `HTTP方法: ${tokens.method(req, res)}`,
    `请求路径: ${tokens.url(req, res)}`,
    `状态码: ${tokens.status(req, res)}`,
    `响应体大小: ${tokens['res[content-length]'](req, res)} bytes`,
    `耗时: ${tokens['res[duration]'](req, res)} ms`
  ].join(' | ');
});

// 配置morgan
app.use(morgan(customFormat, {
  skip: (req, res) => req.url === '/healthcheck',
  stream: {
    write: (message) => {
      logger.info(`Access Log: ${message}`);
    }
  }
}));

// 配置body-parser
app.use(express.json({
  limit: '1mb' // 设置最大接收大小为1MB
}));

// 接口路由
app.get('/healthcheck', (req, res) => {
  res.status(200).json({ status: 'healthy' });
});

app.post('/api/data', (req, res) => {
  logger.info(`Received data: ${JSON.stringify(req.body)}`);
  res.json({ status: 'success', data: req.body });
});

// 错误处理中间件
app.use((err, req, res, next) => {
  logger.error(`Error: ${err.message}`);
  res.status(500).json({ error: 'Internal Server Error' });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

六、源码解析

以express.json()为例,其底层实现如下(简化版):

// express.js(简化版)
function json(options) {
  return (req, res, next) => {
    let body = '';
    req.on('data', (chunk) => {
      body += chunk;
    });
    req.on('end', () => {
      try {
        req.body = JSON.parse(body);
      } catch (err) {
        next(err);
      }
    });
  };
}

关键点:

  • 使用流式处理,避免内存溢出
  • 自动处理JSON字符串的转义字符
  • 通过try-catch捕获解析错误
  • 支持流式处理大文件(需要更复杂的实现)

七、进阶使用

1. 多格式支持

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.text());
app.use(express.raw());

2. 自定义解析器

app.use((req, res, next) => {
  if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
    req.body = req.query;
  }
  next();
});

3. 跨域支持

const cors = require('cors');
app.use(cors());

八、性能与工程实践

1. 性能优化

  • 使用limit限制请求体大小
  • 避免在生产环境使用dev日志格式
  • 对日志进行压缩处理
  • 使用异步写入日志文件

2. 安全实践

  • 禁用不必要的日志字段(如req.headers.authorization)
  • 对敏感信息进行脱敏处理
  • 设置Content-Type校验
  • 使用安全中间件(如helmet)

3. 异常处理

  • 添加错误处理中间件
  • 对解析错误进行特殊处理
  • 设置超时机制

九、常见问题与踩坑

1. 常见错误

错误示例:

app.use(express.json());
app.get('/data', (req, res) => {
  console.log(req.body); // undefined
});

原因: GET请求没有请求体,body-parser未处理GET请求

解决方案: 仅在POST/PUT等有请求体的HTTP方法上使用body-parser

2. 路由顺序问题

错误示例:

app.get('/data', (req, res) => {
  // 未处理请求体
});
app.use(express.json()); // 顺序错误

解决方案: 将body-parser中间件放在路由之前

3. 日志性能问题

错误示例:

app.use(morgan('dev')); // 使用开发日志格式

解决方案: 生产环境应使用更简化的日志格式,如combined或自定义格式

十、最佳实践

  1. 日志管理

    • 生产环境使用combined或自定义格式
    • 对敏感信息进行脱敏处理
    • 使用异步写入日志文件
    • 定期清理日志文件
  2. 请求体处理

    • 设置合理的limit值
    • 使用流式处理大文件
    • 对Content-Type进行校验
    • 处理解析错误
  3. 安全实践

    • 禁用不必要的日志字段
    • 使用安全中间件
    • 设置CORS策略
    • 防止CSRF攻击

十一、总结

body-parser和morgan作为Express开发中的核心中间件,其正确使用对系统稳定性、安全性和可维护性至关重要。通过理解其工作原理,我们可以更好地应对实际开发中的各种问题。在生产环境中,应结合具体需求进行合理配置,如设置适当的请求体限制、优化日志记录方式、处理异常情况等。同时,要特别注意安全风险,避免敏感信息泄露。通过合理使用这些中间件,我们可以构建更加健壮和可靠的Node.js应用。

2024-08-10

'# nodejs 爬取动态网页,web网页开发工具

一、背景与问题

在现代Web开发中,动态网页已成为主流。传统静态网页通过服务器直接返回HTML内容,而动态网页则依赖JavaScript在客户端进行渲染。这种模式带来了更丰富的交互体验,但也给爬虫开发带来了挑战。

以某电商平台的搜索页面为例,其商品列表是通过JavaScript动态加载的。传统爬虫工具如requests无法获取完整的DOM结构,导致数据抓取失败。本文将深入探讨Node.js环境下爬取动态网页的技术原理、实现方案、常见问题及优化策略。

二、基本原理

动态网页的核心特征是:服务器返回的HTML中包含大量<script>标签和<div id="app">等占位符,实际内容由前端JavaScript动态生成。这种模式的典型技术栈包括:

  • 前端:Vue.js/React/Angular(使用虚拟DOM)
  • 后端:Node.js/Python/Java(提供API接口)
  • 通信协议:HTTP/HTTPS(可能涉及WebSocket)

爬虫需要模拟真实用户行为,包括:

  1. 发送HTTP请求获取初始HTML
  2. 解析JavaScript代码(如使用JSDOM)
  3. 模拟浏览器环境执行JavaScript
  4. 提取最终DOM结构中的数据

三、环境准备

# 安装必要依赖
npm init -y
npm install puppeteer playwright selenium-webdriver

四、核心实现

1. Puppeteer示例:爬取动态内容

const puppeteer = require('puppeteer');

async function scrapeDynamicPage() {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();
  
  // 设置请求头,模拟浏览器访问
  await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4443.111 Safari/537.36');
  
  // 访问目标页面
  await page.goto('https://example.com/dynamic-page', {
    waitUntil: 'networkidle2'
  });
  
  // 等待动态内容加载完成
  await page.waitForSelector('.product-list');
  
  // 提取数据
  const products = await page.evaluate(() => {
    const items = document.querySelectorAll('.product-item');
    return Array.from(items).map(item => ({
      title: item.querySelector('.title').innerText,
      price: parseFloat(item.querySelector('.price').innerText.replace('¥', ''))
    }));
  });
  
  console.log(products);
  
  await browser.close();
}

关键代码解释:

  • page.setUserAgent():设置User-Agent以模拟浏览器
  • waitUntil: 'networkidle2':等待所有网络请求完成(包括异步加载)
  • page.waitForSelector():等待特定元素出现,避免过早提取空数据
  • page.evaluate():在浏览器上下文中执行JavaScript,获取完整DOM结构

2. Playwright示例:处理复杂交互

const { chromium } = require('playwright');

async function complexInteraction() {
  const browser = await chromium.launch({ headless: false });
  const page = await browser.newPage();
  
  // 填写表单并提交
  await page.fill('input#username', 'testuser');
  await page.click('button#submit');
  
  // 处理弹窗
  await page.waitForSelector('div#alert');
  await page.click('button#closeAlert');
  
  // 提取数据
  const data = await page.innerText('div#result');
  console.log(data);
  
  await browser.close();
}

关键代码解释:

  • page.fill():模拟键盘输入
  • page.click():模拟鼠标点击
  • page.waitForSelector():等待特定元素出现后执行操作
  • page.innerText():直接获取文本内容

3. Selenium示例:处理反爬机制

const { Builder, By, until } = require('selenium-webdriver');

async function handleAntiCrawls() {
  const driver = await new Builder().forBrowser('chrome').build();
  
  // 设置代理和User-Agent
  await driver.get('https://example.com/anti-crawl');
  await driver.executeScript('window.navigator.__defineGetter__("userAgent", function() { return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4443.111 Safari/537.36"; });');
  
  // 等待动态内容加载
  await driver.wait(until.elementIsVisible(driver.findElement(By.id('content'))), 10000);
  
  // 提取数据
  const text = await driver.findElement(By.id('content')).getText();
  console.log(text);
  
  await driver.quit();
}

关键代码解释:

  • executeScript():执行自定义JavaScript修改浏览器属性
  • wait():等待特定条件满足
  • findElement():定位元素并获取文本内容

五、完整案例

案例:爬取电商搜索结果页

const puppeteer = require('puppeteer');

async function scrapeECommerce() {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();
  
  // 设置请求头
  await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4443.111 Safari/537.36');
  
  // 访问搜索页面
  await page.goto('https://example.com/search?q=电子产品', {
    waitUntil: 'networkidle2'
  });
  
  // 等待商品列表加载
  await page.waitForSelector('.product-list');
  
  // 提取商品信息
  const products = await page.evaluate(() => {
    const items = document.querySelectorAll('.product-item');
    return Array.from(items).map(item => ({
      title: item.querySelector('.title').innerText,
      price: parseFloat(item.querySelector('.price').innerText.replace('¥', ''))
    }));
  });
  
  console.log(products);
  
  await browser.close();
}

运行结果示例:

[
  {
    "title": "无线蓝牙耳机",
    "price": 199
  },
  {
    "title": "智能手表",
    "price": 299
  }
]

六、源码解析

1. Puppeteer的渲染机制

Puppeteer通过Chromium浏览器实例模拟真实用户行为,其核心流程如下:

  1. 创建浏览器实例
  2. 新建页面上下文
  3. 执行page.goto()发起请求
  4. 等待网络活动结束
  5. 执行JS代码获取DOM数据

2. 动态内容加载机制

现代网页常使用以下技术动态加载内容:

  • Vue.js的v-if/v-show
  • React的useEffect
  • Angular的ngIf/ngFor
  • 原生的IntersectionObserver

Puppeteer通过page.waitForSelector()等待特定元素出现,确保数据可用。

七、进阶使用

1. 处理复杂动态加载

// 等待特定元素出现并提取数据
await page.waitForFunction(() => {
  return document.querySelectorAll('.product-item').length > 0;
});

2. 使用代理IP池

const browser = await puppeteer.launch({
  headless: false,
  args: [`--proxy-server=http://192.168.1.100:8080`]
});

3. 处理验证码

// 使用第三方服务识别验证码
const captchaText = await page.$eval('div#captcha', el => {
  return el.innerText;
});
const result = await recognizeCaptcha(captchaText);
await page.type('input#captcha', result);

八、性能与工程实践

1. 性能优化策略

优化手段说明
并发控制使用puppeteer-cluster进行任务分发
缓存机制使用Redis缓存热点数据
精确等待使用page.waitForSelector()替代page.waitForTimeout()
资源限制设置--max-time限制页面加载时间

2. 异常处理机制

try {
  await page.goto(url, { timeout: 30000 });
} catch (err) {
  console.error('页面加载超时:', url);
  // 处理超时逻辑
}

3. 安全风险防范

  1. 遵守robots.txt协议
  2. 设置合理的请求间隔
  3. 避免敏感数据泄露
  4. 使用HTTPS协议

九、常见问题与踩坑

1. 常见错误及解决方法

问题原因解决方案
数据为空页面未完全加载使用page.waitForSelector()
无法识别元素选择器不正确使用page.$eval()检查元素
被封IP频繁请求增加请求间隔,使用代理

2. 动态加载问题处理

// 等待特定元素出现
await page.waitForFunction(() => {
  return document.querySelectorAll('.loading').length === 0;
});

3. 反爬虫机制应对

  • 使用Headless模式时添加--disable-gpu参数
  • 随机化请求头
  • 使用浏览器指纹模拟

十、最佳实践

  1. 选择合适的工具:Puppeteer适合简单场景,Playwright适合复杂交互
  2. 遵守网站规则:遵守robots.txt,设置合理请求间隔
  3. 使用代理服务:避免IP封禁
  4. 实现异常处理:添加超时、重试、日志记录
  5. 优化性能:使用缓存,限制并发数量
  6. 安全防护:避免敏感数据泄露,处理验证码

十一、总结

Node.js在爬取动态网页方面具有独特优势,其结合了服务器端渲染和浏览器自动化能力。通过Puppeteer、Playwright等工具,可以有效处理现代网页的动态内容。在实际开发中,需要根据具体场景选择合适的方案,注意遵守网站规则,处理反爬机制,优化性能。对于复杂的动态内容,建议采用渐进式开发策略,从简单场景入手,逐步增加复杂度。同时,始终关注安全风险,确保爬虫行为合法合规。

2024-08-10

'# JavaScript进阶之OOP,前端黑科技实现原理揭秘,自学Flutter

一、背景与问题

在现代前端开发中,对象导向编程(OOP)是构建可维护、可扩展代码的核心手段。尽管JavaScript本质上是基于原型的函数式语言,但通过类(ES6)、继承、组合等机制,开发者可以实现类似传统OOP的抽象能力。

然而,实际开发中常遇到以下问题:

  1. 原型链的深层理解不足导致的性能问题
  2. 类继承与组合模式选择不当引发的代码膨胀
  3. 状态管理混乱导致的维护成本剧增
  4. 与Flutter等跨平台框架的兼容性问题

本文将深度解析JavaScript OOP的底层原理,结合前端黑科技实现方案,揭示其在现代前端开发中的应用价值。

二、基本原理

1. 原型链机制

JavaScript的OOP基础是原型链。每个对象都有一个隐式原型(__proto__),最终指向Object.prototype。

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, ${this.name}`);
};

const p = new Person('Alice');
p.sayHello(); // Hello, Alice
console.log(p.__proto__ === Person.prototype); // true

关键点:

  • 构造函数通过this创建实例
  • prototype属性定义共享方法
  • 实例通过__proto__访问原型链

2. 类的实现原理

ES6的class语法是语法糖,实际仍基于原型链:

class Person {
  constructor(name) {
    this.name = name;
  }
  
  sayHello() {
    console.log(`Hello, ${this.name}`);
  }
}

const p = new Person('Bob');
p.sayHello(); // Hello, Bob

底层实际是:

Person.prototype.constructor = Person;
Person.prototype.sayHello = function() {...};

3. 继承机制

ES6的extends语法实现原型继承:

class Developer extends Person {
  constructor(name, skill) {
    super(name);
    this.skill = skill;
  }
  
  showSkill() {
    console.log(`I am a ${this.skill} developer`);
  }
}

底层实现:

Developer.prototype = Object.create(Person.prototype);
Developer.prototype.constructor = Developer;

三、环境准备

  1. 环境要求:Node.js 18+ 或浏览器环境
  2. 开发工具:VS Code + Live Server
  3. 项目结构建议:

    oop-demo/
    ├── index.html
    ├── main.js
    ├── utils/
    │   └── logger.js
    └── models/
     └── user.js

四、核心实现

1. 基础OOP模式

// 构造函数模式
function User(name, age) {
  this.name = name;
  this.age = age;
}

User.prototype.greet = function() {
  console.log(`Hello, ${this.name} (${this.age})`);
};

const user = new User('Alice', 25);
user.greet(); // Hello, Alice (25)

关键点:

  • 属性在构造函数中定义
  • 方法在原型上定义
  • 实例共享原型方法

2. 组合继承模式

function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function() {
  console.log(`${this.name} makes noise`);
};

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
  console.log(`${this.name} barks`);
};

const dog = new Dog('Buddy', 'Golden Retriever');
dog.speak(); // Buddy makes noise
dog.bark();   // Buddy barks

性能优化:

  • 使用Object.create避免原型链污染
  • 原型链上仅放共享方法
  • 构造函数中避免重复定义方法

3. 原型链扩展

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, ${this.name}`);
};

// 扩展原型链
Person.prototype.walk = function() {
  console.log(`${this.name} is walking`);
};

const p = new Person('Alice');
p.sayHello(); // Hello, Alice
p.walk();     // Alice is walking

注意事项:

  • 修改原型链会影响所有实例
  • 原型链过长会影响性能
  • 建议使用Object.defineProperty控制属性访问

五、完整案例:待办事项管理系统

1. 项目结构

todo-app/
├── index.html
├── main.js
├── models/
│   └── todo.js
└── controllers/
    └── todo-controller.js

2. 核心代码实现

models/todo.js

class Todo {
  constructor(id, title, completed = false) {
    this.id = id;
    this.title = title;
    this.completed = completed;
  }

  toggleComplete() {
    this.completed = !this.completed;
  }

  static fromJSON(json) {
    return new Todo(json.id, json.title, json.completed);
  }
}

controllers/todo-controller.js

class TodoController {
  constructor() {
    this.todos = [];
  }

  addTodo(title) {
    const id = Date.now();
    this.todos.push(new Todo(id, title));
  }

  toggleTodo(id) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) todo.toggleComplete();
  }

  getTodos() {
    return this.todos.map(t => t.toJSON());
  }
}

index.html

<!DOCTYPE html>
<html>
<head>
  <title>Todo App</title>
</head>
<body>
  <input type="text" id="todo-input" placeholder="Enter task">
  <button onclick="app.addTodo()">Add</button>
  <ul id="todo-list"></ul>

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

main.js

const app = new TodoController();

function renderTodos() {
  const list = document.getElementById('todo-list');
  list.innerHTML = '';
  
  app.getTodos().forEach(todo => {
    const li = document.createElement('li');
    li.textContent = `${todo.title} [${todo.completed ? 'Completed' : 'Pending'}]`;
    li.dataset.id = todo.id;
    li.onclick = () => app.toggleTodo(todo.id);
    list.appendChild(li);
  });
}

document.getElementById('todo-input').addEventListener('keypress', (e) => {
  if (e.key === 'Enter') {
    const title = e.target.value;
    e.target.value = '';
    app.addTodo(title);
    renderTodos();
  }
});

renderTodos();

六、源码解析

1. Todo类分析

class Todo {
  constructor(id, title, completed = false) {
    this.id = id;
    this.title = title;
    this.completed = completed;
  }

  toggleComplete() {
    this.completed = !this.completed;
  }

  static fromJSON(json) {
    return new Todo(json.id, json.title, json.completed);
  }
}
  • 构造函数初始化实例属性
  • toggleComplete方法修改状态
  • fromJSON静态方法实现序列化

2. 控制器类分析

class TodoController {
  constructor() {
    this.todos = [];
  }

  addTodo(title) {
    const id = Date.now();
    this.todos.push(new Todo(id, title));
  }

  toggleTodo(id) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) todo.toggleComplete();
  }

  getTodos() {
    return this.todos.map(t => t.toJSON());
  }
}
  • 使用数组管理多个Todo实例
  • 封装业务逻辑,分离数据与操作
  • 提供统一接口与外部交互

七、进阶使用

1. 状态管理优化

class TodoStore {
  constructor() {
    this.todos = [];
    this.onChange = [];
  }

  addTodo(title) {
    const todo = new Todo(Date.now(), title);
    this.todos.push(todo);
    this.notify();
  }

  toggleTodo(id) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) {
      todo.toggleComplete();
      this.notify();
    }
  }

  subscribe(callback) {
    this.onChange.push(callback);
  }

  notify() {
    this.onChange.forEach(cb => cb());
  }
}

使用示例:

const store = new TodoStore();
store.subscribe(() => {
  renderTodos();
});

2. 模块化设计

// models/user.js
class User {
  constructor(id, name) {
    this.id = id;
    this.name = name;
  }
}

export default User;

// controllers/user-controller.js
import User from './user';

class UserController {
  constructor() {
    this.users = [];
  }

  addUser(name) {
    const id = Date.now();
    this.users.push(new User(id, name));
  }
}

八、性能与工程实践

1. 性能优化策略

问题解决方案
原型链过长使用Object.create替代new
方法重复定义集中定义原型方法
频繁修改原型链避免动态添加属性
内存泄漏及时清理不再使用的实例

2. 异常处理方案

class SafeTodoController {
  constructor() {
    this.todos = [];
    this.errorHandler = (err) => {
      console.error('Todo controller error:', err);
    };
  }

  addTodo(title) {
    try {
      const todo = new Todo(Date.now(), title);
      this.todos.push(todo);
    } catch (err) {
      this.errorHandler(err);
    }
  }
}

3. 安全风险控制

  • 避免全局变量污染:使用模块化导出
  • 防止原型链污染:使用Object.defineProperty控制属性
  • 限制方法访问:使用getter/setter控制属性访问

九、常见问题与踩坑

1. 常见错误示例

错误代码:

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, ${this.name}`);
};

const p = new Person('Alice');
p.sayHello(); // Hello, Alice
p.__proto__.sayHello = () => console.log('New method');
p.sayHello(); // New method

问题分析:

  • 修改原型链后,实例方法会被覆盖
  • 导致原有方法失效

改进方案:

const originalSayHello = Person.prototype.sayHello;
Person.prototype.sayHello = function() {
  console.log(`Original: Hello, ${this.name}`);
};

Person.prototype.newMethod = function() {
  console.log('New method');
};

2. 性能陷阱

问题:

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, ${this.name}`);
};

// 错误:频繁修改原型链
Person.prototype = {
  sayHello: function() {
    console.log(`New: Hello, ${this.name}`);
  }
};

性能影响:

  • 所有实例的原型链被重新指向新对象
  • 导致内存浪费和性能损耗

解决方案:

Person.prototype = Object.create(Person.prototype);
Person.prototype.sayHello = function() {
  console.log(`New: Hello, ${this.name}`);
};

十、最佳实践

1. 推荐方案

场景推荐方案原因
简单数据模型构造函数+原型轻量级,适合小型项目
复杂业务逻辑类继承+模块化结构清晰,易于维护
状态管理观察者模式实现数据驱动更新
跨平台开发组合模式灵活应对不同平台需求

2. 避免使用场景

场景原因
简单数据结构使用对象字面量更简洁
频繁创建对象使用对象池优化内存
单例模式导致代码耦合度过高
复杂继承关系导致代码难以维护

十一、总结

JavaScript的OOP机制虽然基于原型链,但通过类、继承、组合等模式,可以构建出强大的面向对象系统。在前端开发中,合理使用OOP可以显著提升代码的可维护性和扩展性,但需要避免过度封装和原型链污染等常见陷阱。

对于Flutter开发,虽然其核心语言是Dart,但理解JavaScript的OOP原理仍能帮助开发者在Web端进行跨平台开发,或理解类似模式在不同语言中的实现差异。实际项目中,应根据具体需求选择合适的模式,保持代码的简洁性和可维护性。

通过本文的深入解析,希望能帮助开发者在实际项目中更有效地运用OOP原理,构建健壮的前端应用。

2024-08-10

'# HTML+CSS+JS--3D相册

一、背景与问题

在现代Web开发中,3D视觉效果已成为提升用户体验的重要手段。传统2D相册存在平面化、缺乏互动性等局限,而3D相册通过空间变换和动态交互,可以创造出沉浸式的视觉体验。然而,实现这种效果需要深入理解CSS 3D变换机制、JavaScript事件处理以及性能优化策略。

当前开发中面临的核心问题包括:如何在不使用Three.js等复杂库的情况下实现3D效果?如何处理多张图片的动态加载和动画?如何在保证性能的前提下实现流畅的交互?本文将深入探讨这些问题的解决方案。

二、基本原理

3D相册的核心技术基于CSS的3D变换体系,主要包括以下几个关键点:

  1. 3D变换空间:通过transform-style: preserve-3d创建三维空间
  2. 旋转轴控制:使用rotateX()、rotateY()等函数控制旋转方向
  3. 透视效果:通过perspective()和perspective-origin实现视觉深度
  4. 动画控制:结合transition实现平滑的动画效果
  5. 动态交互:使用JavaScript实现鼠标事件驱动的3D变换

三、环境准备

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>3D相册</title>
  <style>
    /* 基础样式 */
    body {
      margin: 0;
      height: 100vh;
      background: #111;
      overflow: hidden;
    }
    .album {
      width: 100vw;
      height: 100vh;
      perspective: 1200px;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .cube {
      width: 300px;
      height: 300px;
      position: relative;
      transform-style: preserve-3d;
      transition: transform 1s;
    }
    .face {
      position: absolute;
      width: 100%;
      height: 100%;
      background-size: cover;
      border: 2px solid #fff;
      box-sizing: border-box;
      backface-visibility: hidden;
    }
  </style>
</head>
<body>
</body>
</html>

四、核心实现

1. 立方体结构创建

<div class="album">
  <div class="cube" id="cube">
    <div class="face" id="front" style="background-image: url('front.jpg');"></div>
    <div class="face" id="back" style="background-image: url('back.jpg'); transform: rotateY(180deg);"></div>
    <div class="face" id="left" style="background-image: url('left.jpg'); transform: rotateY(-90deg);"></div>
    <div class="face" id="right" style="background-image: url('right.jpg'); transform: rotateY(90deg);"></div>
    <div class="face" id="top" style="background-image: url('top.jpg'); transform: rotateX(90deg);"></div>
    <div class="face" id="bottom" style="background-image: url('bottom.jpg'); transform: rotateX(-90deg);"></div>
  </div>
</div>

关键代码解释:

  • transform-style: preserve-3d确保子元素在3D空间中保持位置关系
  • backface-visibility: hidden控制背面元素的可见性
  • 各面通过transform旋转到正确位置

2. 交互事件处理

const cube = document.getElementById('cube');
let isFlipping = false;

document.addEventListener('click', (e) => {
  if (isFlipping) return;
  isFlipping = true;
  
  // 旋转立方体
  cube.style.transform = 'rotateY(180deg)';
  
  // 使用requestAnimationFrame进行动画控制
  requestAnimationFrame(() => {
    cube.style.transform = 'rotateY(0deg)';
    isFlipping = false;
  });
});

关键代码解释:

  • 使用requestAnimationFrame保证动画流畅
  • 通过CSS变换实现旋转效果
  • 添加状态机控制防止连续触发

3. 动态加载与性能优化

function loadImages(urls, callback) {
  const images = [];
  let loaded = 0;
  
  urls.forEach(url => {
    const img = new Image();
    img.src = url;
    img.onload = () => {
      images.push(img);
      loaded++;
      if (loaded === urls.length) {
        callback(images);
      }
    };
  });
}

// 使用示例
loadImages(['front.jpg', 'back.jpg', 'left.jpg', 'right.jpg', 'top.jpg', 'bottom.jpg'], images => {
  // 动态设置背景图
  const faces = ['front', 'back', 'left', 'right', 'top', 'bottom'];
  faces.forEach((face, index) => {
    const element = document.getElementById(face);
    element.style.backgroundImage = `url('${images[index].src}')`;
  });
});

关键代码解释:

  • 使用Image对象进行图片预加载
  • 通过回调函数控制加载顺序
  • 避免阻塞主线程

五、完整案例

创建一个完整的3D相册页面,包含图片加载、点击翻转和自动旋转功能:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>3D相册</title>
  <style>
    body {
      margin: 0;
      height: 100vh;
      background: #111;
      overflow: hidden;
    }
    .album {
      width: 100vw;
      height: 100vh;
      perspective: 1200px;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .cube {
      width: 300px;
      height: 300px;
      position: relative;
      transform-style: preserve-3d;
      transition: transform 1s;
    }
    .face {
      position: absolute;
      width: 100%;
      height: 100%;
      background-size: cover;
      border: 2px solid #fff;
      box-sizing: border-box;
      backface-visibility: hidden;
    }
    .face:nth-child(1) { transform: rotateY(0deg); }
    .face:nth-child(2) { transform: rotateY(180deg); }
    .face:nth-child(3) { transform: rotateY(-90deg); }
    .face:nth-child(4) { transform: rotateY(90deg); }
    .face:nth-child(5) { transform: rotateX(90deg); }
    .face:nth-child(6) { transform: rotateX(-90deg); }
  </style>
</head>
<body>
  <div class="album">
    <div class="cube" id="cube">
      <div class="face" id="front"></div>
      <div class="face" id="back"></div>
      <div class="face" id="left"></div>
      <div class="face" id="right"></div>
      <div class="face" id="top"></div>
      <div class="face" id="bottom"></div>
    </div>
  </div>

  <script>
    const cube = document.getElementById('cube');
    let isFlipping = false;

    function loadImages(urls, callback) {
      const images = [];
      let loaded = 0;
      
      urls.forEach(url => {
        const img = new Image();
        img.src = url;
        img.onload = () => {
          images.push(img);
          loaded++;
          if (loaded === urls.length) {
            callback(images);
          }
        };
      });
    }

    // 加载图片
    loadImages(['front.jpg', 'back.jpg', 'left.jpg', 'right.jpg', 'top.jpg', 'bottom.jpg'], images => {
      const faces = ['front', 'back', 'left', 'right', 'top', 'bottom'];
      faces.forEach((face, index) => {
        const element = document.getElementById(face);
        element.style.backgroundImage = `url('${images[index].src}')`;
      });
    });

    // 点击事件
    document.addEventListener('click', (e) => {
      if (isFlipping) return;
      isFlipping = true;
      
      cube.style.transform = 'rotateY(180deg)';
      
      requestAnimationFrame(() => {
        cube.style.transform = 'rotateY(0deg)';
        isFlipping = false;
      });
    });

    // 自动旋转
    setInterval(() => {
      cube.style.transform = 'rotateY(180deg)';
      requestAnimationFrame(() => {
        cube.style.transform = 'rotateY(0deg)';
      });
    }, 5000);
  </script>
</body>
</html>

六、源码解析

  1. 3D空间构建:通过transform-style: preserve-3d创建三维空间,所有子元素在该空间中保持相对位置关系
  2. 动画控制:使用transition实现平滑的动画效果,通过requestAnimationFrame确保动画流畅
  3. 动态加载:使用Image对象预加载图片,通过回调函数控制加载顺序
  4. 交互设计:通过点击事件触发3D变换,使用状态机防止连续触发
  5. 自动旋转:通过setInterval实现定时旋转,模拟相册自动翻页效果

七、进阶使用

1. 动态图片加载

function loadImages(urls, callback) {
  const images = [];
  let loaded = 0;
  
  urls.forEach((url, index) => {
    const img = new Image();
    img.src = url;
    img.onload = () => {
      images[index] = img;
      loaded++;
      if (loaded === urls.length) {
        callback(images);
      }
    };
  });
}

2. 动态旋转控制

function rotateCube(angle) {
  if (isFlipping) return;
  isFlipping = true;
  
  cube.style.transform = `rotateY(${angle}deg)`;
  
  requestAnimationFrame(() => {
    cube.style.transform = `rotateY(0deg)`;
    isFlipping = false;
  });
}

3. 响应式设计

@media (max-width: 600px) {
  .cube {
    width: 200px;
    height: 200px;
  }
  
  .face {
    width: 100%;
    height: 100%;
  }
}

八、性能与工程实践

1. 性能优化策略

  • 使用will-change: transform提前标记需要变换的元素
  • 避免频繁的DOM操作,使用批量更新
  • 使用requestAnimationFrame确保动画流畅
  • 使用transform替代left/top等属性提高性能

2. 安全注意事项

  • 避免直接使用eval()等危险函数
  • 对用户输入进行严格的校验
  • 对第三方资源使用CORS策略
  • 使用HTTPS保证数据传输安全

3. 异常处理

try {
  // 可能引发异常的代码
} catch (error) {
  console.error('发生异常:', error);
  // 记录错误日志
}

九、常见问题与踩坑

1. 动画卡顿问题

原因:频繁的DOM操作或未使用requestAnimationFrame

解决方案:使用requestAnimationFrame,避免直接修改样式

2. 图片加载顺序问题

原因:未处理图片加载完成前的样式设置

解决方案:使用回调函数控制加载顺序

3. 兼容性问题

问题:不同浏览器对CSS 3D变换的支持差异

解决方案:使用CSS前缀,或者使用polyfill库

4. 移动端性能问题

原因:CSS 3D变换在移动端可能造成重绘

解决方案:使用transform: translate3d()替代其他变换,使用will-change属性

十、最佳实践

  1. 使用CSS 3D变换:对于简单的3D效果,优先考虑CSS实现
  2. 动态加载图片:使用Image对象进行预加载,确保图片可用
  3. 动画控制:使用requestAnimationFrame进行动画控制
  4. 响应式设计:使用媒体查询适配不同设备
  5. 性能优化:使用will-change和transform提升性能
  6. 异常处理:对关键代码进行异常捕获和日志记录

十一、总结

HTML+CSS+JS实现的3D相册技术,通过CSS 3D变换和JavaScript事件处理,能够创建出沉浸式的视觉体验。这种技术特别适用于需要展示产品细节、历史资料或艺术作品的场景。但在处理复杂交互或需要高精度控制时,建议考虑使用Three.js等专业库。

在实际开发中,需要权衡技术复杂度与性能需求。对于简单3D效果,CSS实现是轻量且高效的方案;而对于复杂的3D场景,Three.js提供了更强大的功能和更灵活的控制。同时,开发人员需要关注移动端性能优化,避免重绘和内存泄漏,确保用户体验的流畅性。

通过深入理解CSS 3D变换原理、合理使用JavaScript控制交互、结合性能优化策略,可以构建出既美观又高效的3D相册系统。在实现过程中,注意处理异常情况和兼容性问题,确保项目在不同环境下稳定运行。