2024-08-12

'# ajax下window.location.href页面不跳转问题

一、背景与问题

在现代Web开发中,AJAX技术被广泛用于实现动态页面交互。但开发者常遇到一个诡异现象:在AJAX请求完成后调用window.location.href时,页面却始终无法跳转。这种问题在单页应用(SPA)、动态表单提交等场景中尤为常见。

例如:

// 错误示例
fetch('/api/data')
  .then(response => response.json())
  .then(data => {
    if (data.success) {
      window.location.href = '/dashboard'; // 页面不跳转
    }
  });

这种现象的本质是AJAX请求的异步特性与页面导航行为的冲突。我们需要深入理解浏览器事件循环、页面状态管理以及URL重定向机制,才能彻底解决这个问题。

二、基本原理

1. 浏览器事件循环机制

AJAX请求是基于事件循环的异步操作。当调用fetch()时,浏览器会立即返回控制权给主线程,而真正的请求在后台线程执行。这意味着:

  • window.location.href的修改不会立即生效
  • 必须在请求完成回调函数中执行跳转
  • 需要确保回调函数在浏览器主线程执行

2. 页面导航的浏览器行为

浏览器对页面导航有严格的限制:

  • 原生location.href会触发页面重新加载
  • 通过AJAX获取的响应内容不会自动刷新页面
  • 必须通过手动刷新或服务器重定向才能生效

3. URL重定向机制

浏览器的重定向机制包含两种模式:

// 1. 原生重定向(服务器响应头)
location.href = '/dashboard'; // 需要服务器返回Location头

// 2. 手动重定向(前端控制)
location.replace('/dashboard'); // 会清除当前页面历史记录

三、环境准备

确保开发环境包含以下要素:

  • 前端框架:React/Vue/原生JS
  • 服务端:Node.js/PHP/Python
  • 浏览器:Chrome/Firefox/Edge

四、核心实现

1. 基础AJAX跳转实现

// 基础AJAX跳转
fetch('/api/validate')
  .then(response => {
    if (!response.ok) throw new Error('Network response was not ok');
    return response.json();
  })
  .then(data => {
    if (data.authenticated) {
      // 确保在主线程执行跳转
      window.location.href = '/dashboard';
    }
  })
  .catch(error => {
    console.error('Error:', error);
    window.location.href = '/login';
  });

关键点解释:

  • 使用.then()确保在请求完成时执行
  • 添加错误处理防止异常阻断跳转
  • 通过location.href触发页面刷新

2. 带状态码的服务器重定向

// 服务端(Node.js示例)
app.get('/api/validate', (req, res) => {
  const user = authenticateUser(req);
  if (user) {
    res.status(302).header('Location', '/dashboard').end();
  } else {
    res.status(401).send('Unauthorized');
  }
});

关键点解释:

  • 使用302状态码触发浏览器重定向
  • 服务器端控制跳转逻辑更可靠
  • 避免前端手动处理跳转逻辑

3. 带参数的AJAX跳转

// 带参数的AJAX跳转
fetch('/api/redirect', {
  method: 'POST',
  body: JSON.stringify({ id: 123 })
})
  .then(response => response.json())
  .then(data => {
    if (data.success) {
      // 使用URLSearchParams构建带参数的URL
      const url = new URL('/dashboard', window.location.origin);
      url.searchParams.append('id', data.id);
      window.location.href = url.toString();
    }
  });

关键点解释:

  • 使用URL对象构建带参数的URL
  • 避免直接拼接字符串防止注入攻击
  • 保持URL标准化处理

五、完整案例

1. 基于React的登录系统

// Login component
function Login() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
      });

      if (!response.ok) throw new Error('Login failed');

      const data = await response.json();
      if (data.success) {
        // 精确控制跳转时机
        setTimeout(() => {
          window.location.href = '/dashboard';
        }, 500);
      }
    } catch (error) {
      console.error('Error:', error);
      alert('登录失败');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={username} onChange={(e) => setUsername(e.target.value)} />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">登录</button>
    </form>
  );
}

关键点解释:

  • 使用setTimeout延迟跳转避免快速刷新
  • 在React组件中控制跳转时机
  • 使用精确的错误处理机制

六、源码解析

1. location.href的浏览器实现

// 简化版浏览器内核实现
function setLocationHref(url) {
  const current = window.location;
  const newUrl = new URL(url, current.origin);
  
  if (newUrl.protocol !== 'http:' && newUrl.protocol !== 'https:') {
    throw new Error('Invalid protocol');
  }
  
  if (newUrl.hash !== current.hash) {
    // 触发页面重新加载
    window.location.reload();
  }
}

关键点解析:

  • URL校验机制防止非法跳转
  • 通过location.reload()触发页面刷新
  • 处理锚点变化时的特殊逻辑

2. AJAX请求的事件循环处理

// 简化版fetch实现
function fetch(url, options) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(options.method || 'GET', url, true);
    
    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(`HTTP error ${xhr.status}`));
      }
    };
    
    xhr.onerror = () => reject(new Error('Network error'));
    
    xhr.send(options.body);
  });
}

关键点解析:

  • 异步请求的回调机制
  • 状态码处理逻辑
  • 异常捕获机制

七、进阶使用

1. 带历史记录的跳转

// 使用pushState实现带历史记录的跳转
fetch('/api/redirect', {
  method: 'POST',
  body: JSON.stringify({ id: 123 })
})
  .then(response => response.json())
  .then(data => {
    if (data.success) {
      const url = new URL('/dashboard', window.location.origin);
      url.searchParams.append('id', data.id);
      window.history.pushState(null, '', url.toString());
    }
  });

关键点解析:

  • 保持浏览器历史记录
  • 避免页面刷新
  • 更适合单页应用场景

2. 带认证的跳转

// 带认证信息的跳转
fetch('/api/redirect', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + localStorage.getItem('token')
  },
  body: JSON.stringify({ id: 123 })
})
  .then(response => response.json())
  .then(data => {
    if (data.success) {
      window.location.href = `/dashboard?id=${data.id}`;
    }
  });

关键点解析:

  • 通过headers传递认证信息
  • 在URL中附加参数
  • 确保认证信息的安全性

八、性能与工程实践

1. 性能优化策略

  • 使用location.replace()代替location.href避免页面历史记录污染
  • 对关键路径进行缓存
  • 使用服务端重定向减少前端处理负担
  • 避免在AJAX回调中进行复杂的页面操作

2. 安全风险分析

  • URL拼接可能导致XSS攻击
  • 未校验的跳转可能导致CSRF攻击
  • 未加密的跳转可能导致信息泄露
  • 未校验的URL可能导致开放重定向漏洞

3. 异常处理建议

try {
  await fetch('/api/redirect', { ... });
  window.location.href = '/dashboard';
} catch (error) {
  console.error('跳转失败:', error);
  window.location.href = '/error';
}

关键点解析:

  • 异常处理要覆盖所有可能的错误场景
  • 保证错误处理逻辑的完整性
  • 保持页面状态的可控性

九、常见问题与踩坑

1. 常见错误场景

场景问题解决方案
1忘记使用.then()处理异步添加回调函数处理
2未处理错误状态码添加错误状态码判断
3使用location.href前未等待请求完成使用await或then()确保顺序
4未校验URL有效性使用URL类进行校验
5未处理浏览器兼容性问题使用location.replace()替代

2. 踩坑案例分析

// 错误示例:未处理异步
fetch('/api/redirect')
  .then(response => response.json())
  .then(data => {
    // 未处理错误情况
    window.location.href = '/dashboard';
  });

问题分析:

  • 未处理网络错误
  • 未处理服务器错误
  • 未处理超时问题

十、最佳实践

1. 推荐方案

场景推荐方案说明
需要页面跳转服务端重定向更可靠、更安全
需要保留历史记录history.pushState()适合SPA场景
需要带参数跳转URLSearchParams避免URL注入
需要带认证跳转headers + URL参数确保安全性

2. 实施建议

  • 使用fetch()代替XMLHttpRequest更现代
  • 使用URL类处理URL拼接
  • 使用async/await提高可读性
  • 使用try/catch确保异常处理完整性

十一、总结

在AJAX场景下使用window.location.href时,需要特别注意异步特性与页面导航行为的交互。通过深入理解浏览器事件循环机制、URL重定向原理以及安全风险,可以有效避免页面不跳转的问题。在实际开发中,应根据具体场景选择合适的跳转方式,优先使用服务端重定向,必要时结合前端控制。同时,要始终注意安全风险,避免潜在的XSS和CSRF攻击。通过合理的异常处理和性能优化,可以构建出既安全又高效的页面跳转机制。

2024-08-12

'# JavaWeb AJAX和JSON详解详解详解

一、背景与问题

在传统的Web开发中,页面请求和响应是同步的:用户点击按钮后,浏览器会向服务器发起请求,等待服务器返回完整的HTML页面后才更新界面。这种模式在早期Web应用中表现良好,但随着用户体验要求的提升,这种模式逐渐暴露出性能瓶颈。

AJAX(Asynchronous JavaScript and XML)技术的出现,彻底改变了这一现状。它通过JavaScript在浏览器端发起异步请求,与服务器进行数据交互,最终通过动态更新页面实现"局部刷新"。JSON(JavaScript Object Notation)作为AJAX请求和响应的主流数据格式,以其轻量、易读、支持复杂结构的特点,成为现代Web开发的基石。

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

  • 如何在不刷新页面的情况下更新数据
  • 如何处理跨域请求的限制
  • 如何保证数据传输的安全性
  • 如何在前后端分离架构中高效使用JSON
  • 如何处理JSON解析错误和异常

二、基本原理

1. AJAX工作原理

AJAX的核心在于浏览器端的XMLHttpRequest对象,它实现了浏览器与服务器之间的异步通信。其工作流程如下:

  1. 前端通过JavaScript创建XMLHttpRequest实例
  2. 设置请求方法(GET/POST)、URL和请求头
  3. 发起异步请求
  4. 在回调函数中处理服务器响应
  5. 通过DOM操作更新页面内容

2. JSON数据交换机制

JSON作为轻量级的数据交换格式,具有以下特点:

  • 与JavaScript对象直接映射
  • 支持嵌套结构和复杂类型
  • 无类型声明,便于序列化/反序列化
  • 体积比XML小30%以上

典型的JSON数据结构:

{
  "status": "success",
  "data": {
    "id": 1001,
    "name": "张三",
    "email": "zhangsan@example.com"
  },
  "timestamp": 1680756800
}

三、环境准备

1. 开发环境配置

  • Java 8+
  • Spring Boot 2.x(推荐使用Spring Web MVC)
  • 前端使用JavaScript/TypeScript
  • 开发工具:IntelliJ IDEA/VS Code
  • 浏览器:Chrome/Firefox(支持开发者工具)

2. 依赖配置(Spring Boot)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

四、核心实现

1. 基础AJAX请求(JavaScript)

function fetchData() {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', '/api/data', true);
    
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4 && xhr.status === 200) {
            const data = JSON.parse(xhr.responseText);
            console.log('Received:', data);
            // 更新页面内容
            document.getElementById('content').innerText = data.message;
        }
    };
    
    xhr.send();
}

关键点解析:

  • async: true 表示异步请求
  • onreadystatechange 事件处理函数
  • JSON.parse() 将响应内容转换为JavaScript对象
  • 使用document.getElementById更新DOM

2. JSON响应处理(Spring Boot)

@RestController
public class DataController {
    @GetMapping("/api/data")
    public ResponseEntity<String> getData() {
        Map<String, Object> response = new HashMap<>();
        response.put("status", "success");
        response.put("data", Map.of(
            "id", 1001,
            "name", "李四",
            "email", "lisi@example.com"
        ));
        response.put("timestamp", System.currentTimeMillis());
        
        return ResponseEntity.ok().body(new Gson().toJson(response));
    }
}

关键点解析:

  • 使用@RestController注解
  • Map.of()创建不可变Map
  • 使用Gson库将对象转换为JSON
  • 返回ResponseEntity控制响应头和状态码

3. 异常处理(Spring Boot)

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception ex) {
        Map<String, Object> error = new HashMap<>();
        error.put("error", ex.getMessage());
        error.put("timestamp", System.currentTimeMillis());
        return ResponseEntity.status(500).body(new Gson().toJson(error));
    }
}

关键点解析:

  • 使用@ControllerAdvice全局异常处理
  • 返回500状态码和错误信息
  • 统一错误格式便于前端处理

五、完整案例:用户登录验证系统

1. 前端界面(HTML + JavaScript)

<!DOCTYPE html>
<html>
<head>
    <title>AJAX登录示例</title>
</head>
<body>
    <h2>用户登录</h2>
    <div id="content">
        <input type="text" id="username" placeholder="用户名">
        <input type="password" id="password" placeholder="密码">
        <button onclick="login()">登录</button>
    </div>
    <div id="message" style="color: red;"></div>

    <script>
        function login() {
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;
            const xhr = new XMLHttpRequest();
            
            xhr.open('POST', '/api/login', true);
            xhr.setRequestHeader('Content-Type', 'application/json');
            
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4) {
                    const response = JSON.parse(xhr.responseText);
                    if (response.status === 'success') {
                        document.getElementById('message').innerText = '登录成功';
                        window.location.href = '/dashboard';
                    } else {
                        document.getElementById('message').innerText = response.message;
                    }
                }
            };
            
            xhr.send(JSON.stringify({ username, password }));
        }
    </script>
</body>
</html>

2. 后端接口(Spring Boot)

@RestController
public class LoginController {
    @PostMapping("/api/login")
    public ResponseEntity<String> login(@RequestBody Map<String, String> credentials) {
        String username = credentials.get("username");
        String password = credentials.get("password");
        
        // 模拟认证逻辑
        if ("admin".equals(username) && "123456".equals(password)) {
            Map<String, Object> response = new HashMap<>();
            response.put("status", "success");
            response.put("message", "登录成功");
            return ResponseEntity.ok().body(new Gson().toJson(response));
        } else {
            Map<String, Object> error = new HashMap<>();
            error.put("status", "fail");
            error.put("message", "用户名或密码错误");
            return ResponseEntity.status(401).body(new Gson().toJson(error));
        }
    }
}

3. 安全性考虑

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/api/login").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .failureUrl("/login?error=true")
                .and()
            .logout()
                .logoutSuccessUrl("/login?logout=true");
    }
}

关键点解析:

  • 使用Spring Security实现认证授权
  • 允许/api/login接口公开访问
  • 其他接口需要认证
  • 处理登录成功和失败的跳转

六、源码解析

1. XMLHttpRequest源码(简略)

function XMLHttpRequest() {
    this.readyState = 0;
    this.status = 0;
    this.responseText = '';
    
    this.open = function(method, url, async) {
        this._method = method;
        this._url = url;
        this._async = async;
    };
    
    this.send = function(data) {
        // 模拟异步请求
        if (this._async) {
            setTimeout(() => {
                this.responseText = JSON.stringify({ status: 200, data: 'test' });
                this.readyState = 4;
            }, 100);
        }
    };
    
    this.onreadystatechange = null;
}

2. Spring Boot JSON序列化源码(简略)

public class Gson {
    public String toJson(Object obj) {
        // 简化后的序列化逻辑
        return "{ \"name\": \"张三\", \"age\": 30 }";
    }
}

七、进阶使用

1. 使用Fetch API替代XMLHttpRequest

async function fetchData() {
    const response = await fetch('/api/data');
    const data = await response.json();
    console.log('Received:', data);
}

2. 使用Axios库封装请求

axios.post('/api/login', { username, password })
    .then(response => {
        if (response.data.status === 'success') {
            window.location.href = '/dashboard';
        }
    })
    .catch(error => {
        console.error('登录失败:', error);
    });

3. 使用Spring Boot的@RestController和@RequestBody

@RestController
public class UserController {
    @PostMapping("/api/user")
    public ResponseEntity<String> createUser(@RequestBody User user) {
        // 处理用户创建逻辑
        return ResponseEntity.ok().body("User created");
    }
}

八、性能与工程实践

1. 性能优化策略

  • 压缩JSON数据:使用Gzip压缩响应内容
  • 缓存策略:对不常变化的数据设置Cache-Control头
  • 异步分页:避免一次性加载大量数据
  • 减少请求次数:合并多次AJAX请求为一次

2. 安全实践

  • CSRF防护:使用Spring Security的CSRF保护
  • XSS防护:对用户输入进行转义处理
  • 数据校验:在服务器端进行严格的JSON格式校验
  • 速率限制:对频繁请求进行限流处理

3. 异常处理机制

@ExceptionHandler(JSONProcessingException.class)
public ResponseEntity<String> handleJsonException(JSONProcessingException ex) {
    Map<String, Object> error = new HashMap<>();
    error.put("error", "JSON解析失败");
    error.put("message", ex.getMessage());
    return ResponseEntity.status(500).body(new Gson().toJson(error));
}

九、常见问题与踩坑

1. 跨域请求问题(CORS)

错误示例:

fetch('http://localhost:8080/api/data')
    .then(response => response.json())
    .then(data => console.log(data));

错误原因:浏览器出于安全考虑阻止跨域请求

解决方案:

  • 后端配置CORS头:

    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
      @Override
      public void addCorsMappings(CorsRegistry registry) {
          registry.addMapping("/api/**")
                  .allowedOrigins("http://localhost:3000")
                  .allowedMethods("GET", "POST")
                  .allowedHeaders("*")
                  .exposedHeaders("Content-Type")
                  .allowCredentials(true);
      }
    }

2. JSON解析错误

错误示例:

const data = JSON.parse('{"name": "张三", "age": 30, "email":}');

错误原因:JSON字符串格式错误

解决方案:严格校验JSON格式,使用JSON Schema校验

3. 跨站脚本攻击(XSS)

错误示例:

document.write(responseData.userInput);

错误原因:直接输出用户输入内容

解决方案:使用textContent或innerHTML时进行转义处理

十、最佳实践

1. 前端最佳实践

  • 使用Fetch API或Axios进行HTTP请求
  • 采用统一的错误处理机制
  • 对用户输入进行HTML转义处理
  • 使用TypeScript进行类型校验
  • 遵循RESTful API设计规范

2. 后端最佳实践

  • 使用Spring Security实现安全控制
  • 对所有输入进行验证
  • 使用Jackson或Gson进行JSON序列化
  • 对敏感数据进行加密处理
  • 使用Spring Boot Actuator监控接口状态

3. 架构设计建议

  • 前后端分离架构
  • 使用Nginx做反向代理和负载均衡
  • 对关键接口进行缓存
  • 使用日志系统记录请求和响应数据

十一、总结

AJAX和JSON技术的结合,彻底改变了Web应用的开发方式。通过异步请求和结构化的数据交换,开发者能够构建出响应更快、交互更自然的Web应用。在实际开发中,我们需要:

  1. 理解AJAX的底层原理,避免盲目使用
  2. 掌握JSON的序列化/反序列化机制
  3. 正确处理各种异常情况
  4. 关注安全性和性能优化
  5. 根据业务场景选择合适的架构方案

在使用AJAX和JSON时,需要特别注意:

  • 避免过度使用AJAX导致页面复杂度增加
  • 对关键数据进行加密传输
  • 合理使用缓存提高性能
  • 保持前后端接口的兼容性

随着Web技术的发展,AJAX和JSON仍然是构建现代Web应用的核心技术。通过深入理解和合理使用这些技术,开发者能够创造出更优秀的Web应用体验。

2024-08-12

'# React18对接wujie碰到的问题与解决

一、背景与问题

随着React 18的正式发布,其引入的并发模式(Concurrent Mode)彻底改变了React应用的渲染机制。这种模式使得React能够更高效地处理用户交互和数据更新,但同时也对微前端架构的实现提出了新的挑战。

wujie作为阿里巴巴开源的微前端框架,其核心原理是通过动态加载子应用的入口文件,并通过自定义的生命周期钩子进行控制。在React 18中,由于Suspense和并发模式的引入,传统微前端方案需要进行深度调整。

典型问题包括:

  1. 子应用在React 18中无法正确渲染
  2. 跨应用状态管理冲突
  3. 动态加载时的性能瓶颈
  4. 跨域请求配置错误
  5. 生命周期钩子不兼容

二、基本原理

1. React 18的并发模式

React 18的核心变化在于引入了Concurrent Mode,其关键特性包括:

  • Suspense组件用于处理数据加载状态
  • useTransition和startTransition API
  • 基于优先级的渲染机制
  • 更高效的内存回收机制

这种模式使得React可以更智能地处理用户交互,但同时也改变了传统的渲染流程。

2. wujie的微前端架构

wujie的核心机制包括:

  • 通过registerMicroApps注册子应用
  • 使用bootstrap、mount、unmount三个生命周期钩子
  • 通过initGlobalState进行全局状态管理
  • 支持动态加载和热更新

在React 18中,需要特别注意:

  • 子应用入口文件需要兼容React 18的concurrent模式
  • 需要处理Suspense的边界
  • 要避免传统React的ref和context滥用

三、环境准备

1. 项目结构

micro-frontends/
├── main-app/
│   ├── package.json
│   ├── src/
│   │   ├── App.jsx
│   │   └── index.js
│   └── webpack.config.js
│
├── sub-app1/
│   ├── package.json
│   ├── src/
│   │   ├── App.jsx
│   │   └── index.js
│   └── webpack.config.js
│
└── sub-app2/
    ├── package.json
    ├── src/
    │   ├── App.jsx
    │   └── index.js
    └── webpack.config.js

2. 依赖安装

# 主应用
npm install wujie-react@2.0.0 react react-dom

# 子应用
npm install wujie-react@2.0.0 react react-dom

四、核心实现

1. 主应用配置(main-app/src/index.js)

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { registerMicroApps, initGlobalState, start } from 'wujie-react';

// 初始化全局状态
const initAppState = initGlobalState({
  user: {
    name: 'React18',
    version: 'v1.0'
  }
});

// 注册子应用
registerMicroApps([
  {
    name: 'sub-app1',
    entry: '//localhost:3001',
    activeRule: '/sub1'
  },
  {
    name: 'sub-app2',
    entry: '//localhost:3002',
    activeRule: '/sub2'
  }
]);

// 主应用入口
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// 启动微前端
start();

关键点说明:

  • 使用ReactDOM.createRoot创建根节点
  • 通过initGlobalState管理全局状态
  • registerMicroApps注册子应用
  • start()方法启动微前端

2. 子应用入口(sub-app1/src/index.js)

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

// 通过window对象暴露子应用
window['subApp1'] = {
  mount() {
    // 挂载子应用
    const root = ReactDOM.createRoot(document.getElementById('sub-app1'));
    root.render(<App />);
  },
  unmount() {
    // 卸载子应用
    ReactDOM.unmountComponentAtNode(document.getElementById('sub-app1'));
  }
};

关键点说明:

  • 子应用必须通过window对象暴露mount/unmount方法
  • 使用ReactDOM.createRoot创建根节点
  • 需要处理动态加载时的渲染问题

3. 主应用组件(main-app/src/App.jsx)

import React from 'react';
import { useLocation } from 'react-router-dom';
import { initGlobalState } from 'wujie-react';

const App = () => {
  const location = useLocation();
  
  // 获取全局状态
  const { user } = initGlobalState();
  
  return (
    <div>
      <h1>React18微前端示例</h1>
      <p>当前用户: {user.name}</p>
      <p>当前路径: {location.pathname}</p>
      
      {/* 微前端容器 */}
      <div id="sub-app1" />
      <div id="sub-app2" />
    </div>
  );
};

export default App;

关键点说明:

  • 使用react-router-dom获取当前路径
  • 通过initGlobalState获取全局状态
  • 为子应用预留容器位置

五、完整案例

1. 主应用配置(main-app/src/index.js)

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { registerMicroApps, initGlobalState, start } from 'wujie-react';

// 初始化全局状态
const initAppState = initGlobalState({
  user: {
    name: 'React18',
    version: 'v1.0'
  }
});

// 注册子应用
registerMicroApps([
  {
    name: 'sub-app1',
    entry: '//localhost:3001',
    activeRule: '/sub1'
  },
  {
    name: 'sub-app2',
    entry: '//localhost:3002',
    activeRule: '/sub2'
  }
});

// 主应用入口
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// 启动微前端
start();

2. 子应用配置(sub-app1/src/index.js)

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

// 子应用入口
window['subApp1'] = {
  mount() {
    const root = ReactDOM.createRoot(document.getElementById('sub-app1'));
    root.render(<App />);
  },
  unmount() {
    ReactDOM.unmountComponentAtNode(document.getElementById('sub-app1'));
  }
};

3. 子应用组件(sub-app1/src/App.jsx)

import React from 'react';

const App = () => {
  return (
    <div>
      <h2>子应用1</h2>
      <p>这是一个React18的子应用</p>
    </div>
  );
};

export default App;

4. 路由配置(main-app/src/App.jsx)

import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';

const App = () => {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/sub1" element={<SubApp1 />} />
        <Route path="/sub2" element={<SubApp2 />} />
      </Routes>
    </Router>
  );
};

const Home = () => (
  <div>
    <h1>主应用首页</h1>
  </div>
);

const SubApp1 = () => (
  <div>
    <h2>子应用1容器</h2>
    <div id="sub-app1" />
  </div>
);

const SubApp2 = () => (
  <div>
    <h2>子应用2容器</h2>
    <div id="sub-app2" />
  </div>
);

export default App;

六、源码解析

1. wujie-react的注册流程

registerMicroApps([
  {
    name: 'sub-app1',
    entry: '//localhost:3001',
    activeRule: '/sub1'
  }
]);
  • 会将子应用信息存储在内存中
  • 创建对应的容器元素
  • 在路由匹配时触发mount方法

2. 路由匹配机制

const location = useLocation();
  • 通过react-router获取当前路径
  • 比对activeRule规则
  • 触发对应子应用的mount方法

3. 全局状态管理

const { user } = initGlobalState();
  • 通过initGlobalState初始化状态
  • 支持跨应用状态共享
  • 提供状态更新的回调机制

七、进阶使用

1. 动态加载子应用

import { loadMicroApp } from 'wujie-react';

loadMicroApp({
  name: 'dynamic-app',
  entry: '//localhost:3003',
  activeRule: '/dynamic'
});

2. 热更新支持

window['subApp1'].hotUpdate = (newCode) => {
  const root = ReactDOM.createRoot(document.getElementById('sub-app1'));
  root.render(<App newCode={newCode} />);
};

3. 跨应用通信

// 发送消息
window['subApp1'].postMessage('hello', 'sub-app1');

// 接收消息
window.addEventListener('message', (e) => {
  if (e.data === 'hello') {
    console.log('收到子应用消息');
  }
});

八、性能与工程实践

1. 性能优化方案

  • 使用代码分割:通过React.lazy和Suspense实现按需加载
  • 优化子应用体积:使用Webpack的splitChunks和tree shaking
  • 避免不必要的重渲染:使用React.memo和useMemo

2. 状态管理优化

  • 避免直接修改全局状态
  • 使用immutable数据结构
  • 建立状态更新的回调机制

3. 安全风险分析

  • 跨域请求配置不当可能导致CSRF攻击
  • 子应用权限控制缺失可能引发数据泄露
  • 动态加载未校验的代码可能存在注入风险

九、常见问题与踩坑

1. 子应用无法渲染

问题现象:子应用容器显示空白

可能原因:

  • 子应用入口文件未正确导出
  • 未使用ReactDOM.createRoot
  • 路由规则未正确匹配

解决办法:

// 子应用入口文件
window['subApp1'] = {
  mount() {
    const root = ReactDOM.createRoot(document.getElementById('sub-app1'));
    root.render(<App />);
  }
};

2. 状态管理冲突

问题现象:多个子应用共享状态时出现不一致

解决办法:

  • 使用initGlobalState初始化状态
  • 建立状态更新的回调机制
  • 避免直接修改状态对象

3. 跨域请求失败

问题现象:子应用请求外部接口时出现403错误

解决办法:

  • 配置CORS头信息
  • 使用代理服务器
  • 验证请求来源

十、最佳实践

1. 推荐使用场景

  • 多团队协作的大型项目
  • 需要独立开发和部署的子系统
  • 需要按需加载的模块化应用
  • 需要动态切换的业务场景

2. 不推荐使用场景

  • 简单的单页应用
  • 对性能要求极高的场景
  • 依赖大量全局状态的项目
  • 需要严格控制权限的系统

十一、总结

React18与wujie的对接需要特别注意并发模式带来的变化。通过合理配置生命周期、处理全局状态、优化性能,可以实现高效的微前端架构。在实际开发中,需要根据项目特点选择合适的方案,避免不必要的复杂性。同时,要时刻注意安全风险,确保系统的稳定性和安全性。通过合理的架构设计和实践,可以充分发挥React18和wujie的优势,构建可维护、可扩展的微前端系统。

2024-08-12

'# JS魔法堂:函数节流(throttle)与函数去抖(debounce)

一、背景与问题

在JavaScript开发中,我们经常会遇到事件触发过于频繁的场景,比如:

  • 用户在输入框中连续敲击键盘时,每100ms触发一次事件
  • 窗口大小调整时,频繁触发resize事件
  • 鼠标移动时,持续触发mousemove事件
  • 通过WebSocket接收高频数据时

这些场景会导致以下问题:

  1. 浏览器性能下降(过多的函数调用)
  2. 系统资源浪费(不必要的计算)
  3. 用户体验变差(频繁的界面更新)

为了解决这些问题,我们需要引入函数节流(throttle)和函数去抖(debounce)技术。这两种技术的核心思想都是控制函数调用的频率,但实现方式和适用场景存在本质差异。

二、基本原理

1. 函数节流(throttle)

核心思想:确保函数在指定时间间隔内只执行一次。

  • 适用于需要持续处理但不能频繁触发的场景(如窗口调整、滚动事件)
  • 保证至少间隔一定时间才执行一次
  • 适合处理持续动作(如持续移动)

关键特征:

  • 每次触发后会等待指定时间
  • 在等待期间再次触发不会执行
  • 最终会执行一次函数

2. 函数去抖(debounce)

核心思想:只有在触发停止后才执行函数。

  • 适用于需要响应最终状态的场景(如搜索框输入、按钮点击)
  • 保证在连续触发后只执行一次
  • 适合处理瞬时动作(如点击、输入)

关键特征:

  • 每次触发后会等待指定时间
  • 在等待期间再次触发会重置等待时间
  • 只有在停止触发后才会执行一次函数

三、环境准备

确保你的开发环境支持ES6+语法,我们使用以下技术栈:

  • JavaScript(ES6)
  • Node.js(可选)
  • 浏览器环境(Chrome/Firefox)

四、核心实现

1. 函数节流实现

// 基础节流函数
function throttle(func, delay) {
    let lastTime = 0;
    return (...args) => {
        const now = Date.now();
        if (now - lastTime >= delay) {
            lastTime = now;
            return func.apply(this, args);
        }
    };
}

// 带防抖的节流函数(确保首次调用时立即执行)
function throttleWithInitial(func, delay) {
    let lastTime = 0;
    let timer = null;
    return (...args) => {
        const now = Date.now();
        if (now - lastTime >= delay) {
            lastTime = now;
            clearTimeout(timer);
            return func.apply(this, args);
        }
        timer = setTimeout(() => {
            lastTime = now;
            func.apply(this, args);
        }, delay);
    };
}

关键代码解释:

  • lastTime 记录最后一次执行的时间
  • now - lastTime >= delay 判断是否达到触发间隔
  • 使用 setTimeout 实现延迟执行(带防抖版本)
  • 在连续触发时,只在最后一次触发时执行函数

2. 函数去抖实现

// 基础去抖函数
function debounce(func, delay) {
    let timer = null;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

// 带立即执行的去抖函数
function debounceWithImmediate(func, delay) {
    let timer = null;
    let isImmediate = false;
    return (...args) => {
        if (isImmediate) {
            func.apply(this, args);
            isImmediate = false;
        } else {
            clearTimeout(timer);
            timer = setTimeout(() => {
                func.apply(this, args);
            }, delay);
        }
    };
}

关键代码解释:

  • clearTimeout(timer) 清除之前的定时器
  • setTimeout 设置新的定时器
  • 带立即执行版本增加 isImmediate 标志位,支持首次立即执行
  • 适用于需要在触发后立即响应的场景

3. 实现差异比较

特性节流(Throttle)去抖(Debounce)
执行频率至少间隔指定时间执行一次只在停止触发后执行一次
适用场景持续动作(如滚动)瞬时动作(如输入)
代码结构使用 setTimeout使用 setTimeout
内存管理需手动清除定时器需手动清除定时器
性能影响保证最低执行频率只在最终状态执行

五、完整案例

1. 搜索框输入建议案例(去抖)

<!DOCTYPE html>
<html>
<head>
    <title>Debounce Example</title>
</head>
<body>
    <input type="text" id="search" placeholder="输入搜索内容">
    <div id="suggestions"></div>

    <script>
        const searchInput = document.getElementById('search');
        const suggestions = document.getElementById('suggestions');

        // 模拟API请求
        function fetchSuggestions(query) {
            return new Promise(resolve => {
                setTimeout(() => {
                    resolve(`搜索结果:${query}`);
                }, 500);
            });
        }

        // 使用去抖处理输入事件
        const debouncedFetch = debounce(async (query) => {
            const result = await fetchSuggestions(query);
            suggestions.innerHTML = result;
        }, 300);

        searchInput.addEventListener('input', (e) => {
            debouncedFetch(e.target.value);
        });
    </script>
</body>
</html>

关键点:

  • 使用 debounce 控制API调用频率
  • 每次输入后等待300ms才触发搜索
  • 确保用户输入完成后再进行搜索
  • 避免频繁调用API导致的性能问题

2. 窗口调整节流案例

// 使用节流处理窗口调整事件
const throttledResize = throttle(() => {
    console.log('窗口调整完成');
}, 200);

window.addEventListener('resize', throttledResize);

关键点:

  • 使用 throttle 控制窗口调整事件
  • 每次调整后等待200ms才执行
  • 确保浏览器有足够时间完成调整
  • 避免频繁重绘导致的性能问题

六、源码解析

1. 节流函数源码逐行分析

function throttle(func, delay) {
    let lastTime = 0;
    return (...args) => {
        const now = Date.now();
        if (now - lastTime >= delay) {
            lastTime = now;
            return func.apply(this, args);
        }
    };
}

逐行解释:

  1. 定义 throttle 函数,接受函数和延迟时间
  2. 定义 lastTime 变量记录上次执行时间
  3. 返回一个函数,接收任意参数
  4. 计算当前时间 now
  5. 判断是否达到指定延迟时间
  6. 如果达到,更新 lastTime 并执行函数

2. 去抖函数源码逐行分析

function debounce(func, delay) {
    let timer = null;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

逐行解释:

  1. 定义 debounce 函数,接受函数和延迟时间
  2. 定义 timer 变量存储定时器ID
  3. 返回一个函数,接收任意参数
  4. 清除之前的定时器
  5. 设置新的定时器,在指定延迟后执行函数
  6. 执行函数时使用 apply 保持上下文

七、进阶使用

1. 动态调整延迟时间

function throttleWithDynamicDelay(func, minDelay) {
    let lastTime = 0;
    let timer = null;
    return (...args) => {
        const now = Date.now();
        if (now - lastTime >= minDelay) {
            lastTime = now;
            clearTimeout(timer);
            return func.apply(this, args);
        } else {
            timer = setTimeout(() => {
                lastTime = now;
                func.apply(this, args);
            }, minDelay);
        }
    };
}

应用场景:

  • 需要根据输入速度动态调整执行频率
  • 确保快速输入时仍能及时响应
  • 适应不同设备的性能差异

2. 使用 requestAnimationFrame 优化节流

function throttleWithAnimationFrame(func, delay) {
    let lastTime = 0;
    let timer = null;
    return (...args) => {
        const now = performance.now();
        if (now - lastTime >= delay) {
            lastTime = now;
            clearTimeout(timer);
            return func.apply(this, args);
        }
        timer = requestAnimationFrame(() => {
            lastTime = now;
            func.apply(this, args);
        });
    };
}

优势:

  • 与浏览器重绘机制同步
  • 更精确的执行时机控制
  • 适用于需要与动画同步的场景

八、性能与工程实践

1. 性能优化策略

节流优化:

  • 使用 requestAnimationFrame 优化动画相关节流
  • 使用 performance.now() 替代 Date.now() 获取更精确的时间
  • 对于高频事件,设置更短的延迟时间

去抖优化:

  • 使用 debounceWithImmediate 处理首次触发的特殊情况
  • 在输入框中使用 input 事件时,优先使用 debounce
  • 在 search 事件中使用 input 事件代替 onchange

2. 异常处理建议

function safeDebounce(func, delay) {
    let timer = null;
    return (...args) => {
        try {
            clearTimeout(timer);
            timer = setTimeout(() => {
                try {
                    func.apply(this, args);
                } catch (e) {
                    console.error('Debounce function error:', e);
                }
            }, delay);
        } catch (e) {
            console.error('Debounce error:', e);
        }
    };
}

关键点:

  • 包裹函数执行在 try/catch 中
  • 捕获潜在的异常
  • 避免因异常导致定时器残留

3. 安全风险分析

潜在风险:

  • 去抖可能导致用户输入延迟反馈
  • 节流可能导致数据更新不及时
  • 高频事件处理不当可能导致内存泄漏

防范措施:

  • 在关键业务逻辑中使用 try/catch 包裹
  • 使用 WeakMap 存储定时器ID,避免内存泄漏
  • 在关闭组件时主动清除定时器

九、常见问题与踩坑

1. 常见错误

错误示例:

function throttle(func, delay) {
    let timer = null;
    return (...args) => {
        if (!timer) {
            timer = setTimeout(() => {
                func.apply(this, args);
                timer = null;
            }, delay);
        }
    };
}

问题分析:

  • 该实现会导致函数在最后一次触发后不会执行
  • 没有处理连续触发的情况
  • 导致函数永远不会执行

改进方案:

function throttle(func, delay) {
    let timer = null;
    let lastTime = 0;
    return (...args) => {
        const now = Date.now();
        if (now - lastTime >= delay) {
            lastTime = now;
            clearTimeout(timer);
            return func.apply(this, args);
        }
        timer = setTimeout(() => {
            lastTime = now;
            func.apply(this, args);
        }, delay);
    };
}

2. 常见坑点

坑点问题描述解决方案
未清除定时器导致内存泄漏在组件卸载时主动清除
延迟时间设置不当无法满足业务需求根据场景动态调整延迟
未处理同步/异步导致逻辑错误使用 Promise 或 async/await 包裹
未处理上下文丢失函数执行上下文错误使用 bind 或 apply 保持上下文

十、最佳实践

1. 选择策略的建议

场景推荐策略原因
搜索输入去抖避免频繁请求
窗口调整节流确保重绘合理
滚动事件节流控制页面性能
按钮点击去抖防止重复提交
高频数据更新节流控制更新频率

2. 代码组织建议

  • 使用模块化方式封装函数
  • 通过 WeakMap 存储定时器ID
  • 在组件卸载时主动清除定时器
  • 使用 Symbol 作为函数标识符

3. 性能监控建议

  • 使用 performance API 监控函数执行时间
  • 使用 console.time() 进行基准测试
  • 对关键路径进行性能优化

十一、总结

函数节流(throttle)和函数去抖(debounce)是JavaScript开发中处理高频事件的两种重要技术。它们通过控制函数调用频率,有效解决了性能瓶颈问题。在实际开发中:

  • 节流适用于需要持续处理但不能频繁触发的场景(如滚动、resize)
  • 去抖适用于需要响应最终状态的场景(如输入、按钮点击)
  • 选择合适的策略可以显著提升应用性能
  • 需要注意内存管理、异常处理和上下文保持等问题

在实际项目中,建议:

  1. 根据业务需求选择合适的策略
  2. 使用模块化方式组织代码
  3. 在组件卸载时主动清除定时器
  4. 使用性能监控工具进行优化
  5. 处理特殊场景时添加额外逻辑

通过合理使用节流和去抖技术,可以显著提升应用的性能和用户体验,同时避免不必要的资源浪费。

2024-08-12

'# 3D数字孪生 - Three.js 项目介绍与基础环境搭建

一、背景与问题

在工业4.0时代,数字孪生技术已成为构建智能制造系统的核心支柱。Three.js 作为开源的3D图形库,为开发者提供了实现数字孪生的底层技术支撑。其核心价值在于通过Web技术实现三维可视化,但实际应用中常面临以下挑战:

  1. 性能瓶颈:复杂模型渲染时容易出现卡顿
  2. 交互复杂度:需要处理多维度交互逻辑
  3. 跨平台兼容性:不同设备和浏览器的差异
  4. 动态更新:实时数据驱动的三维模型更新
  5. 安全风险:Web3D场景中的潜在安全漏洞

这些挑战需要通过深入理解Three.js的工作原理和合理的技术选型来解决。

二、基本原理

Three.js的核心架构基于WebGL API,其工作原理可分为三个主要阶段:

  1. 场景构建:创建三维坐标系,添加几何体、材质和光照
  2. 摄像机控制:定义视角和投影方式
  3. 渲染循环:持续更新场景并绘制到画布

其底层依赖于WebGL的渲染管线,通过顶点着色器(Vertex Shader)和片段着色器(Fragment Shader)实现图形处理。Three.js通过封装这些复杂操作,提供更友好的API接口。

三、环境准备

3.1 项目初始化

使用Vite创建项目结构:

npm create vite@latest threejs-twin -- --template vue
cd threejs-twin
npm install

项目结构建议:

src/
├── assets/              # 资源文件
├── components/          # 3D组件
├── utils/               # 工具函数
├── main.js              # 入口文件
└── App.vue              # 根组件

3.2 依赖安装

npm install three

四、核心实现

4.1 基础场景搭建

// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import * as THREE from 'three'

const app = createApp(App)

// 创建场景
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.body.appendChild(renderer.domElement)

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

// 添加物体
const geometry = new THREE.BoxGeometry()
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 })
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)

// 动画循环
function animate() {
  requestAnimationFrame(animate)
  cube.rotation.x += 0.01
  cube.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)
})

关键点解释:

  • PerspectiveCamera 构造函数参数分别对应视野角度、宽高比、近裁剪面和远裁剪面
  • WebGLRenderer 的 antialias 属性开启抗锯齿
  • DirectionalLight 提供环境光照
  • 动画循环必须使用 requestAnimationFrame 确保流畅性

4.2 光照系统实现

// 添加多种光源
const ambientLight = new THREE.AmbientLight(0x404040, 1) // 环境光
const pointLight = new THREE.PointLight(0xff0000, 2, 10, 2) // 点光源
pointLight.position.set(5, 5, 5)
scene.add(ambientLight, pointLight)

// 添加阴影
renderer.shadowMap.enabled = true
cube.castShadow = true
cube.receiveShadow = true

注意事项:

  • 阴影功能需要开启 shadowMap
  • 光源的 castShadow 和 receiveShadow 属性控制阴影行为
  • 点光源的衰减参数需要合理设置

4.3 纹理映射实现

// 加载纹理
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load('assets/wood.jpg')

// 创建材质
const material = new THREE.MeshPhongMaterial({
  map: texture, 
  specularMap: texture, 
  shininess: 50
})

// 创建网格
const planeGeometry = new THREE.PlaneGeometry(10, 10)
const plane = new THREE.Mesh(planeGeometry, material)
plane.rotation.x = -Math.PI/2
scene.add(plane)

性能优化建议:

  • 使用 TextureLoader 的 crossOrigin 参数处理跨域问题
  • 对复杂纹理使用 repeat 和 offset 实现无缝贴图
  • 使用 WebGLRenderTarget 实现后期处理效果

五、完整案例

5.1 工业设备3D模型展示

完整项目包含以下功能:

  • 三维模型旋转展示
  • 点击交互事件
  • 动态参数更新
  • 阴影和光照控制
<!-- src/App.vue -->
<template>
  <div ref="container" class="three-container"></div>
</template>

<script>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import * as THREE from 'three'

export default {
  setup() {
    const container = ref(null)
    let renderer, scene, camera, controls, clock
    
    const init = () => {
      // 创建场景
      scene = new THREE.Scene()
      
      // 创建相机
      camera = new THREE.PerspectiveCamera(
        75, 
        window.innerWidth/window.innerHeight, 
        0.1, 
        1000
      )
      
      // 创建渲染器
      renderer = new THREE.WebGLRenderer({ antialias: true })
      renderer.setSize(window.innerWidth, window.innerHeight)
      container.value.appendChild(renderer.domElement)
      
      // 添加光源
      const light = new THREE.DirectionalLight(0xffffff, 1)
      light.position.set(10, 10, 10)
      scene.add(light)
      
      // 创建控件
      controls = new THREE.OrbitControls(camera, renderer.domElement)
      
      // 添加模型
      const loader = new THREE.GLTFLoader()
      loader.load('assets/industrial_equipment.gltf', (gltf) => {
        gltf.scene.scale.set(0.1, 0.1, 0.1)
        scene.add(gltf.scene)
      })
      
      // 动画循环
      clock = new THREE.Clock()
      animate()
    }
    
    const animate = () => {
      requestAnimationFrame(animate)
      const delta = clock.getDelta()
      renderer.render(scene, camera)
    }
    
    const resize = () => {
      camera.aspect = window.innerWidth/window.innerHeight
      camera.updateProjectionMatrix()
      renderer.setSize(window.innerWidth, window.innerHeight)
    }
    
    onMounted(() => {
      init()
      window.addEventListener('resize', resize)
    })
    
    onBeforeUnmount(() => {
      window.removeEventListener('resize', resize)
      container.value.removeChild(renderer.domElement)
    })
    
    return { container }
  }
}
</script>

<style>
.three-container {
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}
</style>

关键点说明:

  • 使用 OrbitControls 实现交互控制
  • 使用 GLTFLoader 加载3D模型
  • 模型缩放参数需要根据实际模型调整
  • clock 对象用于计算时间差

六、源码解析

6.1 渲染管线分析

Three.js的渲染流程包含以下关键步骤:

  1. 几何处理:顶点坐标转换
  2. 光照计算:基于材质属性和光源参数
  3. 纹理映射:采样纹理数据
  4. 深度测试:确定可见面
  5. 后期处理:抗锯齿、运动模糊等效果

在WebGL中,这些步骤通过着色器程序实现,Three.js通过MeshStandardMaterial等材质类封装了这些逻辑。

6.2 光照模型详解

Three.js支持多种光照模型:

  • Phong光照模型:适用于静态物体
  • Lambert光照模型:适用于漫反射
  • Blinn-Phong模型:更高效的计算方式

在MeshPhongMaterial中,光照计算公式为:

I = Ia * Ka + Ie * ( (n · l) * (Ka + Ks * (n · h)^n) ) 

其中 n 是法线向量,l 是光源方向,h 是半角向量。

七、进阶使用

7.1 动态数据绑定

// 动态更新模型参数
function updateModelParams(params) {
  cube.scale.set(params.scaleX, params.scaleY, params.scaleZ)
  cube.position.set(params.x, params.y, params.z)
}

7.2 多线程渲染

使用Web Workers处理复杂计算:

// worker.js
self.onmessage = function(event) {
  const data = event.data
  const result = processData(data)
  self.postMessage(result)
}
// main.js
const worker = new Worker('worker.js')
worker.postMessage({ type: 'calculate', data: modelData })

7.3 性能监控

const stats = new THREE.Stats()
stats.domElement.style.position = 'absolute'
stats.domElement.style.zIndex = '1000'
document.body.appendChild(stats.domElement)

八、性能与工程实践

8.1 渲染性能优化

  1. 减少绘制调用:使用Mesh合并几何体
  2. 优化材质参数:避免频繁修改材质属性
  3. 使用缓存:对重复计算进行缓存
  4. 动态LOD:根据距离切换模型精度

8.2 安全风险分析

  1. CORS问题:加载外部模型时需设置crossOrigin属性
  2. XSS风险:避免直接渲染用户输入内容
  3. 数据泄露:避免暴露敏感信息

8.3 兼容性处理

if (THREE.WebGLRenderer) {
  // 支持WebGL
} else if (THREE.CanvasRenderer) {
  // 降级处理
}

九、常见问题与踩坑

9.1 常见错误分析

问题原因解决方案
无显示渲染器未正确挂载检查domElement是否添加
卡顿复杂模型未优化使用LOD技术
阴影缺失未开启shadowMap设置renderer.shadowMap.enabled = true
光照异常光源参数错误检查光源位置和强度

9.2 踩坑案例

// 错误示例
const mesh = new THREE.Mesh(geometry, material)
mesh.position.set(0, 0, 0)
scene.add(mesh)

// 正确做法
mesh.position.set(10, 10, 10)
scene.add(mesh)

问题分析:未考虑模型空间坐标系,导致模型位置不正确。

十、最佳实践

  1. 模型预处理:使用Blender进行优化
  2. 参数化设计:通过配置文件控制模型参数
  3. 分层管理:按功能模块划分场景对象
  4. 版本控制:对3D模型进行版本管理
  5. 测试驱动:编写单元测试验证关键功能

十一、总结

Three.js作为3D数字孪生的实现基础,其核心价值在于通过Web技术构建可交互的三维场景。在实际开发中需要特别注意性能优化、安全风险和跨平台兼容性等问题。通过合理的技术选型和工程实践,可以构建出稳定、高效的3D数字孪生系统。

在工业场景中,Three.js适用于需要快速原型开发和跨平台展示的场景,但不适合对物理精度要求极高的仿真系统。建议在项目初期进行技术选型评估,结合具体需求选择合适的实现方案。

2024-08-12

'# three.js创建基础模型

一、背景与问题

在现代Web开发中,3D图形渲染已成为不可或缺的技术。three.js作为基于WebGL的3D库,提供了比原生WebGL更高级的抽象层。其核心原理是通过封装WebGL的复杂接口,将3D场景构建分解为场景(Scene)、相机(Camera)、渲染器(Renderer)、几何体(Geometry)、材质(Material)、网格(Mesh)等核心元素。

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

  • 如何高效创建3D模型?
  • 如何处理光照与材质交互?
  • 如何优化性能?
  • 如何处理坐标系差异?

本文将深入解析three.js的3D模型创建机制,结合真实项目场景,探讨最佳实践与常见陷阱。

二、基本原理

1. 渲染管线架构

three.js的渲染流程遵循WebGL的渲染管线:

  1. 顶点处理:将顶点坐标转换为屏幕坐标
  2. 光栅化:将3D模型转换为2D像素
  3. 片段处理:计算每个像素的颜色值
  4. 输出:将最终图像绘制到canvas

核心组件包括:

  • Scene:3D场景容器
  • Camera:定义视角和投影
  • Renderer:将3D场景渲染到canvas
  • Geometry:定义模型顶点数据
  • Material:定义表面属性
  • Mesh:几何体+材质的组合体

2. 坐标系差异

three.js使用右手坐标系,与WebGL的左手坐标系不同。需要特别注意:

  • X轴向右
  • Y轴向上
  • Z轴向前

3. 光照模型

three.js支持多种光照类型:

  • AmbientLight:环境光
  • DirectionalLight:平行光
  • PointLight:点光源
  • SpotLight:聚光灯

三、环境准备

1. 依赖引入

<!-- 最新稳定版 -->
<script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>

2. 开发环境配置

建议使用TypeScript+VSCode,配置如下:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "ESNext",
    "strict": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "lib": ["DOM", "ESNext"]
  }
}

四、核心实现

1. 创建几何体(Geometry)

// 创建立方体几何体
const geometry = new THREE.BoxGeometry(1, 1, 1);
console.log(geometry.vertices); // 输出顶点数组

关键解释:

  • BoxGeometry生成一个立方体,包含8个顶点
  • vertices属性返回顶点坐标数组
  • 三维坐标系中,X/Y/Z分别代表左右、上下、前后方向

常见错误:忘记调用geometry.computeVertexNormals()导致光照计算错误

2. 创建材质(Material)

// 创建基础材质
const material = new THREE.MeshBasicMaterial({
  color: 0x00ff00, // 颜色值
  wireframe: true, // 显示线框模式
});

关键解释:

  • MeshBasicMaterial不依赖光照
  • wireframe属性控制是否显示线框
  • 颜色值使用十六进制格式

3. 创建网格(Mesh)

// 创建网格对象
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

关键解释:

  • Mesh是几何体与材质的结合体
  • scene.add()将物体加入场景
  • 未设置position时,默认位于原点(0,0,0)

五、完整案例

1. 简单3D场景

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Three.js基础模型</title>
    <style>body { margin: 0; }</style>
</head>
<body>
    <script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>
    <script>
        // 场景
        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.body.appendChild(renderer.domElement);

        // 几何体
        const geometry = new THREE.BoxGeometry(1, 1, 1);
        const material = new THREE.MeshBasicMaterial({color: 0x00ff00, wireframe: true});
        const cube = new THREE.Mesh(geometry, material);
        scene.add(cube);

        // 光照
        const light = new THREE.PointLight(0xffffff, 1, 100);
        light.position.set(5, 5, 5);
        scene.add(light);

        // 相机位置
        camera.position.z = 5;

        // 动画循环
        function animate() {
            requestAnimationFrame(animate);
            cube.rotation.x += 0.01;
            cube.rotation.y += 0.01;
            renderer.render(scene, camera);
        }
        animate();
    </script>
</body>
</html>

关键点说明:

  1. 使用PerspectiveCamera创建透视相机
  2. WebGLRenderer启用抗锯齿
  3. 添加点光源并设置位置
  4. 动态旋转立方体实现动画效果

六、源码解析

1. 渲染器初始化

const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
  • antialias启用抗锯齿
  • setSize设置画布尺寸
  • 将canvas添加到DOM中

2. 光照计算

const light = new THREE.PointLight(0xffffff, 1, 100);
light.position.set(5, 5, 5);
scene.add(light);
  • PointLight创建点光源
  • 设置发光强度和影响范围
  • 灯光位置影响光照强度分布

3. 动画循环

function animate() {
    requestAnimationFrame(animate);
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;
    renderer.render(scene, camera);
}
animate();
  • 使用requestAnimationFrame实现平滑动画
  • 每帧更新物体旋转角度
  • 调用renderer.render进行渲染

七、进阶使用

1. 纹理映射

// 加载纹理
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load('textures/uv_grid_opengl.jpg');

// 创建材质
const material = new THREE.MeshPhongMaterial({
    map: texture,
    shininess: 50
});

关键点:

  • 使用TextureLoader加载图像
  • MeshPhongMaterial支持光照计算
  • shininess控制反射强度

2. 动态网格

// 创建平面几何体
const geometry = new THREE.PlaneGeometry(1, 1, 10, 10);
const material = new THREE.MeshStandardMaterial({color: 0xff0000});
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);

// 设置法线
geometry.computeVertexNormals();

关键点:

  • PlaneGeometry生成网格
  • computeVertexNormals计算顶点法线
  • 用于光照计算

3. 轨道控制器

const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;

关键点:

  • 实现鼠标控制视角
  • enableDamping启用阻尼效果
  • 提升交互体验

八、性能与工程实践

1. 性能优化策略

  1. 使用BufferGeometry:

    const geometry = new THREE.BufferGeometry().setFromPoints(points);
    • 比Geometry更高效
    • 适合大量顶点数据
  2. 减少绘制调用:

    const material = new THREE.MeshBasicMaterial({vertexColors: true});
    • 使用顶点颜色避免多次材质创建
  3. 使用骨骼动画:

    const skeleton = new THREE.SkeletonHelper(mesh);
    scene.add(skeleton);
    • 优化复杂模型的动画性能

2. 异常处理

try {
    const texture = textureLoader.load('nonexistent.jpg');
} catch (error) {
    console.error('纹理加载失败:', error);
}

关键点:

  • 网络请求可能出现错误
  • 需要异常处理机制
  • 使用onLoad/onProgress回调

3. 安全风险

  • XSS风险:避免直接使用用户输入作为纹理URL
  • 数据验证:对上传的3D模型进行格式校验
  • 资源隔离:使用沙箱加载第三方资源

九、常见问题与踩坑

1. 常见错误分析

错误类型表现解决方案
网格不可见模型未添加到场景检查scene.add()调用
光照异常模型颜色异常确认材质类型与光照兼容
动画卡顿帧率低使用requestAnimationFrame
渲染延迟首屏加载慢预加载关键资源

2. 坐标系陷阱

// 错误示例
cube.position.set(0, 0, 0);

问题:未考虑Z轴方向

// 正确示例
cube.position.set(0, 0, 5);

解释:Z轴正方向是向前的,需要设置合适的距离

3. 光照计算错误

// 错误示例
const material = new THREE.MeshBasicMaterial({color: 0xff0000});

问题:不支持光照计算

// 正确示例
const material = new THREE.MeshStandardMaterial({color: 0xff0000});

解释:需要使用支持光照的材质类型

十、最佳实践

1. 推荐方案

  1. 使用BufferGeometry:对于复杂模型提升性能
  2. 结合OrbitControls:提供交互式查看
  3. 使用THREE.Clock:控制动画速度
  4. 预加载资源:使用loader的onLoad回调
  5. 分层管理:使用Group组织不同部分

2. 推荐目录结构

project/
├── assets/          # 资源文件
├── src/
│   ├── main.ts      # 主程序
│   ├── models/      # 模型定义
│   └── utils/       # 工具函数
├── index.html       # 入口文件
└── package.json

3. 推荐编码规范

  • 使用TypeScript强类型
  • 使用ES6模块
  • 使用@types/three类型定义
  • 使用代码规范工具(如ESLint)

十一、总结

three.js创建基础模型的原理涉及WebGL的渲染管线和3D图形学基础。通过合理使用Scene、Camera、Renderer、Geometry、Material、Mesh等核心组件,可以构建复杂的3D场景。本文深入解析了:

  • 三维坐标系的转换原理
  • 光照计算机制
  • 动画实现方式
  • 性能优化策略
  • 常见错误分析

在实际开发中,建议:

  • 使用BufferGeometry提升性能
  • 结合交互控件增强用户体验
  • 预加载关键资源
  • 注意坐标系转换
  • 正确选择材质类型

避免使用three.js的场景包括:

  • 需要高精度物理模拟的工程计算
  • 需要大规模动态交互的复杂系统
  • 对性能有极端要求的实时渲染场景

通过合理的设计和优化,three.js可以成为构建高质量3D可视化应用的强大工具。

2024-08-12

'# main.js:46 Uncaught ReferenceError: Cannot access ‘router‘ before initialization at render (main

一、背景与问题

在使用 Vue.js 开发单页应用(SPA)时,开发者常遇到一个熟悉的错误:
main.js:46 Uncaught ReferenceError: Cannot access 'router' before initialization at render (main
这个错误通常出现在使用 Vue Router 的场景中,核心原因是:在组件的生命周期中尝试访问尚未初始化的 router 实例。

该错误在 Vue 2 和 Vue 3 中表现形式略有差异,但本质相同。例如在 Vue 2 中,若在 created 钩子中访问 this.$router,而此时 router 实例尚未完成初始化,就会触发该错误。在 Vue 3 中,若在 setup 函数中直接访问 router 变量,而未通过 useRouter 钩子获取,也会导致类似问题。


二、基本原理

1. Vue 生命周期与 Vue Router 初始化顺序

Vue 组件的生命周期钩子与 Vue Router 的初始化存在时间差。以 Vue 2 为例:

  • beforeCreate:组件实例刚创建,this 指向未初始化的组件实例
  • created:this 指向初始化后的组件实例,但 Vue Router 实例尚未注入
  • beforeMount:模板编译阶段
  • mounted:模板渲染完成,this.$router 才能被访问

Vue Router 的初始化依赖于 Vue 实例的创建。在 main.js 中,通过 new Vue({ ... }) 创建实例后,才会调用 Vue.use(VueRouter) 注册插件。因此,router 实例的初始化发生在组件生命周期的 mounted 阶段之后。

2. Vue 3 的响应式系统与 setup 函数

在 Vue 3 中,setup 函数在组件挂载前执行,而 useRouter 钩子需要在 onMounted 钩子中调用。若直接在 setup 中访问 router 变量,会因未初始化而报错。


三、环境准备

1. 安装依赖

npm install vue vue-router@4

2. 项目结构

src/
├── App.vue
├── main.js
└── views/
    ├── Home.vue
    └── About.vue

四、核心实现

1. 错误代码示例(Vue 2)

// App.vue
export default {
  created() {
    console.log(this.$router); // ❌ 错误:此时 router 未初始化
  }
}

错误原因:created 钩子执行时,this.$router 仍为 undefined。

2. 正确代码示例(Vue 2)

// App.vue
export default {
  mounted() {
    console.log(this.$router); // ✅ 正确:此时 router 已初始化
  }
}

关键点:将访问 router 的逻辑移到 mounted 钩子。

3. Vue 3 错误代码示例

// App.vue
<script setup>
import { useRouter } from 'vue-router';

const router = useRouter(); // ❌ 错误:setup 函数中未等待初始化
</script>

错误原因:setup 函数在组件挂载前执行,而 useRouter 需要 onMounted 钩子。

4. Vue 3 正确代码示例

// App.vue
<script setup>
import { useRouter, onMounted } from 'vue-router';

const router = useRouter();

onMounted(() => {
  console.log(router); // ✅ 正确:此时 router 已初始化
});
</script>

关键点:通过 onMounted 钩子确保 router 初始化。


五、完整案例

1. 项目结构

src/
├── App.vue
├── main.js
└── views/
    ├── Home.vue
    └── About.vue

2. main.js 配置

// main.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import Home from './views/Home.vue';
import About from './views/About.vue';

Vue.use(VueRouter);

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About }
];

const router = new VueRouter({
  routes
});

new Vue({
  router,
  render: h => h(App)
}).$mount('#app');

3. App.vue 正确使用

<!-- App.vue -->
<template>
  <div>
    <nav>
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </nav>
    <router-view />
  </div>
</template>

<script>
export default {
  mounted() {
    console.log(this.$router); // ✅ 正确:此时 router 已初始化
  }
}
</script>

4. Home.vue 示例

<!-- Home.vue -->
<template>
  <div>
    <h1>Home Page</h1>
    <p>当前路由: {{ $route.path }}</p>
  </div>
</template>

<script>
export default {
  mounted() {
    console.log(this.$route); // ✅ 正确:此时路由信息已注入
  }
}
</script>

六、源码解析

1. Vue 2 中 this.$router 的注入时机

在 Vue 2 的 new Vue({ router }) 初始化过程中,router 实例会通过 Vue.options 注入到组件实例中。但这一过程发生在 mounted 钩子之前。

// Vue 2 源码片段(简化版)
function initProps(Vue, Ctor) {
  Vue.options = {
    propsData: {},
    ...
    router: null // router 实例在此处注入
  }
}

// 在组件创建时,通过 this.$options.router 获取 router 实例

2. Vue 3 中 useRouter 的初始化流程

在 Vue 3 的 setup 函数中,useRouter 钩子依赖于 onMounted 事件的触发:

// Vue 3 源码片段(简化版)
function useRouter() {
  const router = inject('router');
  if (!router) {
    throw new Error('useRouter must be called inside a component with a router');
  }
  return router;
}

// 在 onMounted 钩子中调用 useRouter() 可确保 router 已初始化

七、进阶使用

1. 动态路由的处理

在动态加载路由时,需确保 router 实例在 onMounted 之后才被访问:

// 动态加载路由示例
onMounted(async () => {
  const { default: Home } = await import('./views/Home.vue');
  router.addRoute({ path: '/dynamic', component: Home });
});

2. 路由守卫的使用

在 beforeEach 路由守卫中,需避免直接访问未初始化的 router 实例:

router.beforeEach((to, from, next) => {
  // 正确:使用 router 实例
  if (to.meta.requiresAuth) {
    if (localStorage.getItem('token')) {
      next();
    } else {
      next('/login');
    }
  } else {
    next();
  }
});

八、性能与工程实践

1. 性能优化

  • 避免不必要的 router 访问:仅在必要时访问 router 实例,减少不必要的计算。
  • 使用 onMounted 钩子:确保在组件挂载后才进行路由相关的操作。

2. 异常处理

  • 在 onMounted 钩子中添加 try...catch 块,防止因未初始化的 router 导致程序崩溃:
onMounted(() => {
  try {
    console.log(router);
  } catch (e) {
    console.error('Router未初始化', e);
  }
});

3. 安全风险

  • 未初始化的 router 可能引发未定义行为,如访问 this.$router 时可能引发 TypeError。
  • 避免在 setup 函数中直接使用 router 变量,应通过 useRouter 钩子获取。

九、常见问题与踩坑

1. 常见错误场景

  • 在 created 钩子中访问 this.$router:Vue 2 中 created 钩子执行时 router 未初始化。
  • 在 setup 函数中直接使用 router 变量:Vue 3 中 setup 函数在 onMounted 之前执行。

2. 解决方案

  • Vue 2:将访问 router 的逻辑移到 mounted 钩子。
  • Vue 3:在 onMounted 钩子中调用 useRouter。

3. 其他陷阱

  • 在异步加载组件时未处理路由变化:需使用 watch 监听 $route 变化。
  • 未正确配置 base 路径:可能导致路由跳转失败。

十、最佳实践

1. 推荐使用场景

  • 在 mounted 钩子中处理路由相关的逻辑:如初始化状态、获取参数等。
  • 在 onMounted 钩子中使用 useRouter:确保 router 实例已初始化。

2. 不推荐使用场景

  • 在 created 或 setup 函数中直接访问 router 实例:可能导致未定义错误。
  • 在未正确初始化的 router 上执行导航操作:如 this.$router.push()。

3. 推荐代码结构

// Vue 2 推荐结构
export default {
  mounted() {
    // 安全访问 router
    console.log(this.$router);
  }
}

// Vue 3 推荐结构
<script setup>
import { useRouter, onMounted } from 'vue-router';

const router = useRouter();

onMounted(() => {
  // 安全访问 router
  console.log(router);
});
</script>

十一、总结

main.js:46 Uncaught ReferenceError: Cannot access 'router' before initialization 是 Vue 应用中常见的错误,其核心原因是组件生命周期与 Vue Router 初始化顺序的不匹配。通过深入理解 Vue 的生命周期钩子和 Vue Router 的初始化机制,可以避免此类错误。

在实际开发中,应始终遵循以下原则:

  • Vue 2:在 mounted 钩子中访问 this.$router。
  • Vue 3:在 onMounted 钩子中使用 useRouter。
  • 避免直接访问未初始化的 router 实例,确保在组件完全初始化后再执行相关操作。

通过合理使用生命周期钩子和响应式 API,可以有效提升代码的健壮性和可维护性,同时避免因未初始化的 router 导致的潜在问题。

2024-08-12

'# 【vuejs】$nextTick的原理分析和使用场景

一、背景与问题

在Vue开发中,开发者常常需要在数据更新后访问DOM元素。例如:

  • 表单验证时需要获取输入框的值
  • 动画结束后需要执行回调
  • 动态计算DOM尺寸

但直接操作DOM会引发问题,因为Vue的响应式系统会延迟更新DOM。此时就需要$nextTick来确保DOM已经更新。

二、基本原理

Vue的响应式系统基于Object.defineProperty实现,当数据变化时会触发视图更新。但更新是异步的,会合并多个更新请求,并在下一个事件循环中执行。

1. 异步更新机制

Vue会将所有数据变更收集到一个队列中,待当前事件循环结束时统一更新DOM。这个过程称为"异步更新"。

2. $nextTick的实现原理

$nextTick是Vue提供的一个方法,用于在DOM更新后执行回调。其核心原理是:

  • 在数据变更后,将回调函数加入队列
  • 当DOM更新完成后,遍历队列执行回调
  • 支持Promise、函数和回调三种形式

三、环境准备

# 创建Vue3项目
npm create vue@latest

四、核心实现

1. 基础用法示例

<template>
  <div>
    <input v-model="inputValue" placeholder="输入内容">
    <p>输入内容长度: {{ length }}</p>
    <button @click="update">更新</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      inputValue: '',
      length: 0
    };
  },
  methods: {
    update() {
      this.inputValue = 'Hello Vue';
      
      this.$nextTick(() => {
        console.log('DOM更新完成', this.inputValue);
        console.log('输入框当前值:', this.$el.querySelector('input').value);
      });
    }
  }
};
</script>

关键代码解释:

  • this.$nextTick确保在DOM更新后执行回调
  • 能够获取到最新的DOM节点值
  • 与直接使用setTimeout相比,更可靠

2. Promise用法示例

update() {
  this.inputValue = 'Hello Vue';
  
  this.$nextTick().then(() => {
    console.log('Promise执行完成');
    console.log('输入框当前值:', this.$el.querySelector('input').value);
  });
}

3. 与setTimeout的对比

update() {
  this.inputValue = 'Hello Vue';
  
  setTimeout(() => {
    console.log('setTimeout执行完成');
    console.log('输入框当前值:', this.$el.querySelector('input').value);
  }, 0);
}

差异分析:

  • setTimeout有最小延迟(约4ms),而$nextTick会立即执行
  • $nextTick能确保DOM更新完成,而setTimeout不能保证
  • $nextTick是Vue内部机制,更高效

五、完整案例

场景:动态计算高度的组件

<template>
  <div>
    <textarea v-model="content" rows="5" cols="30"></textarea>
    <p>文本高度: {{ height }}px</p>
    <button @click="calculateHeight">计算高度</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      content: '',
      height: 0
    };
  },
  methods: {
    calculateHeight() {
      this.content = '这是一个测试文本,用于演示$nextTick的使用。';
      
      this.$nextTick(() => {
        const textarea = this.$el.querySelector('textarea');
        const computedStyle = window.getComputedStyle(textarea);
        this.height = parseInt(computedStyle.height);
        console.log('计算高度:', this.height);
      });
    }
  }
};
</script>

运行效果:

  • 输入内容后,$nextTick确保DOM更新完成
  • 能准确获取到textarea的实际高度
  • 可用于实现动态布局、自动扩展等功能

六、源码解析

Vue3的$nextTick实现位于src/core/util/index.js:

export function nextTick (cb: Function | null = null) {
  const callback = typeof cb === 'function' ? cb : null;
  const promise = new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      resolve();
    }, 0);
  });
  
  return promise.then(() => {
    if (callback) {
      callback();
    }
  });
}

关键点分析:

  • 返回Promise对象,支持async/await
  • 使用setTimeout模拟微任务
  • 兼容Vue2和Vue3的实现方式

七、进阶使用

1. 多个$nextTick回调

this.$nextTick(() => {
  console.log('第一个回调');
}).then(() => {
  console.log('第二个回调');
});

2. 在组件卸载时使用

beforeUnmount() {
  this.$nextTick(() => {
    // 执行清理操作
  });
}

3. 与v-model配合使用

update() {
  this.inputValue = 'New Value';
  
  this.$nextTick(() => {
    // 确保input元素的值已更新
  });
}

八、性能与工程实践

1. 性能优化

  • 避免在$nextTick中执行耗时操作
  • 使用防抖/节流控制更新频率
  • 对于复杂计算,可使用requestAnimationFrame

2. 异常处理

this.$nextTick().catch((err) => {
  console.error('nextTick执行出错:', err);
});

3. 安全风险

  • 避免在$nextTick中直接操作DOM,应通过Vue数据驱动
  • 注意防止XSS攻击,避免直接拼接用户输入内容

九、常见问题与踩坑

1. 常见错误示例

update() {
  this.inputValue = 'New Value';
  console.log(this.$el.querySelector('input').value); // 错误!
}

错误原因:DOM尚未更新,获取的值是旧值

2. 混合使用$nextTick的陷阱

this.$nextTick(() => {
  this.$nextTick(() => {
    // 两次调用可能导致逻辑错误
  });
});

3. 在Vue2中使用$nextTick的注意事项

this.$nextTick(() => {
  // Vue2中$nextTick是方法,Vue3是Promise
});

4. 与Vue3响应式系统的兼容性

import { ref, nextTick } from 'vue';

const content = ref('');
nextTick(() => {
  // Vue3中$nextTick被改为nextTick函数
});

十、最佳实践

  1. 必须使用场景:

    • 需要访问最新DOM状态时
    • 动画结束后执行回调
    • 动态计算DOM尺寸
    • 需要确保DOM更新完成时
  2. 不推荐使用场景:

    • 简单的DOM操作(可直接使用Vue数据绑定)
    • 频繁触发的更新(应使用防抖/节流)
    • 在生命周期钩子中直接操作DOM(应使用数据驱动)
  3. 推荐方案:

    • 使用Vue的响应式数据驱动更新
    • 对于必须操作DOM的情况,优先使用$nextTick
    • 对于复杂计算,可结合requestAnimationFrame
  4. 性能优化建议:

    • 避免在$nextTick中执行大量计算
    • 使用防抖/节流控制更新频率
    • 对于复杂计算,可使用Web Worker

十一、总结

$nextTick是Vue处理DOM更新的重要机制,理解其原理对开发高质量Vue应用至关重要。通过本文的分析,我们深入理解了:

  • Vue的异步更新机制
  • $nextTick的实现原理
  • 不同使用场景下的最佳实践
  • 常见错误和解决方法
  • 性能优化技巧

在实际开发中,应根据具体需求合理使用$nextTick,避免不必要的DOM操作,优先使用Vue的响应式系统。对于必须操作DOM的场景,应确保在DOM更新完成后执行相关逻辑,以保证程序的稳定性和性能。

2024-08-12

'# Web Spider Fiddler - JS Hook 基本使用

一、背景与问题

在现代Web开发中,动态内容加载已成为常态。传统的爬虫技术(如直接解析HTML)无法应对动态渲染的页面,而基于Chrome DevTools的Fiddler工具提供了JS Hook能力,允许开发者在浏览器端注入自定义JavaScript代码,实现对页面的深度控制。

本篇文章将深入解析JS Hook的工作原理,通过三个代码示例展示其核心功能,并构建一个完整的爬虫案例。我们将探讨其适用场景、性能优化、安全风险以及常见陷阱。


二、基本原理

JS Hook本质上是通过浏览器扩展(Extension)或调试工具注入JavaScript代码到目标页面的运行环境中。其核心机制包含三个层面:

  1. 内容脚本(Content Script):通过chrome.content_scripts API注入代码,与页面上下文隔离
  2. 页面注入(Page Injection):通过chrome.webNavigation API在页面加载时插入脚本
  3. 事件监听(Event Listener):通过chrome.runtime.onMessage实现跨域通信

关键原理如下图所示:

浏览器扩展
│
├── 注入代码(Content Script)
│   ├── 拦截XHR请求(chrome.webRequest)
│   ├── 修改DOM(MutationObserver)
│   └── 监听页面事件(window.addEventListener)
│
└── 与页面通信(chrome.runtime.sendMessage)

三、环境准备

1. 开发环境要求

  • Chrome浏览器(建议使用最新稳定版)
  • Node.js 18+
  • VS Code(推荐配置)
  • 浏览器扩展开发工具(Chrome Web Store)

2. 项目结构

web-spider-fiddler/
├── manifest.json
├── background.js
├── content.js
├── popup.html
└── icons/
    └── icon.png

3. 核心依赖

{
  "name": "web-spider-fiddler",
  "version": "1.0.0",
  "manifest_version": 3,
  "permissions": [
    "activeTab",
    "scripting",
    "webRequest",
    "webRequestBlocking"
  ]
}

四、核心实现

1. 拦截XHR请求(代码示例)

// content.js
chrome.webRequest.onBeforeRequest.addListener(
  (details) => {
    // 拦截请求URL
    if (details.url.includes('api.example.com')) {
      // 修改请求头
      const newHeaders = {
        ...details.requestHeaders,
        'X-Debug-Header': 'Fiddler-Intercepted'
      };
      return { requestHeaders: newHeaders };
    }
  },
  { urls: ['*://api.example.com/*'] },
  ['requestHeaders']
);

关键点解析:

  • 使用chrome.webRequest API拦截请求
  • 通过onBeforeRequest修改请求头
  • urls参数支持通配符匹配
  • 需要webRequestBlocking权限

2. 修改DOM内容(代码示例)

// content.js
document.addEventListener('DOMContentLoaded', () => {
  const elements = document.querySelectorAll('.dynamic-content');
  elements.forEach(el => {
    // 修改元素内容
    el.textContent = 'Fiddler Modified Content';
  });
});

关键点解析:

  • 通过DOMContentLoaded事件确保DOM加载完成
  • 使用querySelectorAll选择目标元素
  • 注意避免频繁操作DOM导致性能问题

3. 监听页面事件(代码示例)

// content.js
window.addEventListener('click', (event) => {
  if (event.target.matches('.interactive-button')) {
    // 模拟点击事件
    event.preventDefault();
    console.log('Fiddler intercepted click on:', event.target);
  }
});

关键点解析:

  • 使用event.target获取实际点击元素
  • matches方法用于CSS选择器匹配
  • 需要处理事件冒泡和捕获阶段

五、完整案例

1. 爬取动态加载的电商页面

需求:爬取动态加载的商品列表,包括价格、评分等信息

实现步骤:

  1. 注入脚本到目标页面
  2. 拦截Ajax请求获取商品数据
  3. 修改DOM结构以便解析
  4. 使用chrome.runtime.sendMessage发送数据
// content.js
chrome.webRequest.onBeforeRequest.addListener(
  (details) => {
    if (details.url.includes('products')) {
      // 模拟返回测试数据
      return {
        redirectUrl: 'https://fiddler.example.com/mock-data.json'
      };
    }
  },
  { urls: ['*://example.com/products/*'] },
  ['redirectUrl']
);

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.type === 'parse') {
    // 解析DOM并发送数据
    const data = parseProductData();
    sendResponse({ data });
  }
});

完整案例说明:

  • 使用重定向模拟真实接口调用
  • 通过消息传递机制将数据发送到后台
  • 可扩展为完整的爬虫系统

六、源码解析

1. 浏览器扩展的运行机制

Chrome扩展在加载时会执行background.js和content.js,其中:

  • background.js负责管理扩展生命周期
  • content.js负责注入页面逻辑
// background.js
chrome.runtime.onInstalled.addListener(() => {
  console.log('Fiddler extension installed');
});

2. 跨域通信机制

// popup.html
chrome.runtime.sendMessage({ type: 'request' }, (response) => {
  console.log('Received:', response);
});
// content.js
chrome.runtime.sendMessage({ type: 'parse' });

通信限制:

  • 只能在content.js和popup.html之间通信
  • 需要runtime.sendMessage权限

七、进阶使用

1. 动态内容处理

使用MutationObserver监听DOM变化:

const observer = new MutationObserver((mutations) => {
  mutations.forEach(m => {
    if (m.type === 'childList') {
      console.log('DOM changed:', m);
    }
  });
});

observer.observe(document.body, { childList: true });

2. 自动化测试

// 自动点击按钮
document.querySelector('.test-button').click();

3. 错误处理机制

try {
  // 可能抛出异常的代码
} catch (e) {
  console.error('Error in Fiddler hook:', e);
}

八、性能与工程实践

1. 性能优化

  • 使用requestIdleCallback处理非紧急任务
  • 避免频繁操作DOM
  • 使用debounce处理事件监听
function debounce(func, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => func.apply(this, args), delay);
  };
}

2. 异常处理

  • 使用try/catch包裹关键代码
  • 记录错误日志到后台
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  try {
    sendResponse({ status: 'success' });
  } catch (e) {
    sendResponse({ status: 'error', message: e.message });
  }
});

3. 安全机制

  • 使用chrome.runtime.getBackgroundPage获取后台上下文
  • 验证消息来源
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (sender.id !== chrome.runtime.id) {
    return false; // 拒绝非本扩展的消息
  }
});

九、常见问题与踩坑

1. 常见错误

问题原因解决方案
代码未执行未正确注入检查manifest.json的content_scripts配置
请求被拦截权限不足添加webRequest权限
DOM未更新事件监听时机不对使用DOMContentLoaded或load事件
被CSP阻止安全策略限制在manifest.json中添加"content_security_policy"

2. 常见陷阱

  • 上下文隔离问题:content scripts和页面上下文是隔离的,无法直接访问window对象
  • 资源加载顺序:需要等待资源加载完成再执行注入代码
  • 跨域限制:必须通过chrome.webRequest处理跨域请求

十、最佳实践

1. 推荐方案

  • 使用chrome.webRequest处理API拦截
  • 采用MutationObserver进行DOM变更监控
  • 通过chrome.runtime.sendMessage实现前后端通信

2. 实施建议

  • 使用DOMContentLoaded确保DOM加载完成
  • 对关键操作进行异常捕获
  • 使用requestIdleCallback优化性能

3. 安全建议

  • 严格校验消息来源
  • 对敏感数据进行加密处理
  • 避免暴露敏感信息

十一、总结

JS Hook技术为Web爬虫和调试提供了强大的能力,但其使用需要谨慎。在合法授权的前提下,可以用于处理动态内容、模拟用户行为、数据埋点等场景。但需要注意:

  • 适用场景:动态内容解析、反爬绕过、自动化测试
  • 不适用场景:涉及用户隐私的敏感数据处理、恶意行为

通过合理使用JS Hook,开发者可以构建更智能的爬虫系统,但也需始终遵循安全规范和法律要求。在实际开发中,建议结合其他技术(如Selenium、Playwright)进行综合应用,以达到最佳效果。

2024-08-12

'# js如何实现要是有两位小数保留最多两位,如果没有小数则不展示小数

一、背景与问题

在Web开发中,价格展示、数据可视化等场景经常需要对数字进行格式化处理。一个典型的业务需求是:当数字包含小数时,保留两位小数;当数字是整数时,不显示小数点。例如:

  • 100 → 100
  • 100.1 → 100.1
  • 100.12 → 100.12
  • 100.123 → 100.12
  • 100.00 → 100

这个需求看似简单,但实际开发中需要考虑以下挑战:

  • 浮点数精度问题(如 1.005.toFixed(2) 的结果)
  • 不同数字类型的处理(整数、浮点数、科学计数法)
  • 多语言支持(如千分位分隔符)
  • 性能优化(高频计算场景)

二、基本原理

1. 数字格式化的核心机制

JavaScript 中的数字格式化主要依赖以下机制:

  • Number.prototype.toFixed():将数字转换为字符串并保留指定小数位数
  • String.prototype.replace():正则表达式替换
  • Intl.NumberFormat:国际化格式化 API

2. 浮点数精度问题

由于计算机使用二进制存储浮点数,部分十进制小数无法精确表示。例如:

console.log(0.1 + 0.2); // 输出 0.30000000000000004

3. 小数点的判断逻辑

需要判断数字是否包含小数部分,核心逻辑如下:

function hasDecimalPart(num) {
  return num.toString().includes('.');
}

三、环境准备

# 安装必要的依赖(如需)
npm install lodash

四、核心实现

1. 基础实现(使用 toFixed + replace)

function formatNumber(num) {
  // 判断是否为整数
  if (Number.isInteger(num)) {
    return num.toString();
  }
  
  // 保留两位小数
  const fixedNum = num.toFixed(2);
  
  // 去除末尾的零
  return fixedNum.replace(/\.0+$/, '');
}

关键代码解释:

  • Number.isInteger() 用于判断整数
  • toFixed(2) 将数字转换为两位小数的字符串
  • 正则表达式 \.0+$ 匹配小数点后连续的零,替换后得到规范格式

示例:

console.log(formatNumber(100));       // "100"
console.log(formatNumber(100.1));      // "100.1"
console.log(formatNumber(100.123));    // "100.12"
console.log(formatNumber(100.0004));   // "100"

2. 基于正则表达式的实现

function formatNumberWithRegex(num) {
  const numStr = num.toString();
  
  // 判断是否为整数
  if (/^-?\d+$/.test(numStr)) {
    return numStr;
  }
  
  // 匹配小数点后三位
  const match = numStr.match(/(\d+\.\d{0,2})\d*$/);
  
  if (match) {
    return match[1];
  }
  
  return numStr;
}

关键代码解释:

  • 正则表达式 ^-?\d+$ 判断是否为整数
  • (\d+\.\d{0,2})\d* 匹配小数点后最多两位的数字
  • 使用捕获组提取规范格式

注意: 该实现对科学计数法(如 1.23e+4)处理不完善,需要特殊处理。

3. 基于 Intl.NumberFormat 的实现

function formatNumberWithIntl(num) {
  const formatter = new Intl.NumberFormat('en-US', {
    maximumFractionDigits: 2,
    minimumFractionDigits: 0,
    useGrouping: false
  });
  
  return formatter.format(num);
}

关键代码解释:

  • maximumFractionDigits: 2 设置最大小数位数
  • minimumFractionDigits: 0 允许整数形式
  • useGrouping: false 禁用千分位分隔符

优势:

  • 支持多语言格式化(如 new Intl.NumberFormat('de-DE'))
  • 自动处理科学计数法

五、完整案例

电商价格展示系统

<!DOCTYPE html>
<html>
<head>
  <title>Price Formatter</title>
</head>
<body>
  <input type="number" id="priceInput" placeholder="Enter price">
  <button onclick="formatPrice()">Format</button>
  <p id="result"></p>

  <script>
    function formatPrice() {
      const input = document.getElementById('priceInput').value;
      const price = parseFloat(input);
      const result = formatNumberWithIntl(price);
      document.getElementById('result').innerText = `Formatted Price: ${result}`;
    }
  </script>
</body>
</html>

业务场景:

  • 用户输入价格时自动格式化
  • 支持整数和浮点数输入
  • 避免显示多余的零

特殊处理:

function formatNumberWithIntl(num) {
  const formatter = new Intl.NumberFormat('en-US', {
    maximumFractionDigits: 2,
    minimumFractionDigits: 0,
    useGrouping: false
  });
  
  // 特殊处理科学计数法
  if (/e/i.test(num.toString())) {
    return num.toString();
  }
  
  return formatter.format(num);
}

六、源码解析

以 formatNumberWithIntl 函数为例:

function formatNumberWithIntl(num) {
  const formatter = new Intl.NumberFormat('en-US', {
    maximumFractionDigits: 2,  // 最多保留两位小数
    minimumFractionDigits: 0,  // 允许整数形式
    useGrouping: false         // 禁用千分位分隔符
  });
  
  // 特殊处理科学计数法
  if (/e/i.test(num.toString())) {
    return num.toString();
  }
  
  return formatter.format(num);
}

关键点:

  • maximumFractionDigits 控制小数位数
  • minimumFractionDigits 确定是否显示小数点
  • useGrouping 控制千分位分隔符
  • 科学计数法的特殊处理避免格式化错误

七、进阶使用

1. 多语言支持

function formatNumberWithLocale(num, locale = 'en-US') {
  const formatter = new Intl.NumberFormat(locale, {
    maximumFractionDigits: 2,
    minimumFractionDigits: 0,
    useGrouping: false
  });
  
  if (/e/i.test(num.toString())) {
    return num.toString();
  }
  
  return formatter.format(num);
}

示例:

console.log(formatNumberWithLocale(100, 'de-DE')); // "100"
console.log(formatNumberWithLocale(100.1, 'ja-JP')); // "100.1"

2. 千分位分隔符支持

function formatNumberWithGrouping(num) {
  const formatter = new Intl.NumberFormat('en-US', {
    maximumFractionDigits: 2,
    minimumFractionDigits: 0,
    useGrouping: true
  });
  
  if (/e/i.test(num.toString())) {
    return num.toString();
  }
  
  return formatter.format(num);
}

注意事项:

  • 需要处理不同地区的分隔符(如 1,000.00 vs 1.000,00)
  • 对于非常大的数字,需考虑性能影响

八、性能与工程实践

1. 性能优化

方法时间复杂度适用场景
toFixed() + replace()O(1)简单格式化
正则表达式O(n)复杂格式需求
Intl.NumberFormatO(n)国际化需求

优化建议:

  • 对高频调用的格式化函数进行缓存
  • 使用 Number.isInteger() 预判断整数
  • 对科学计数法进行预处理

2. 异常处理

function safeFormatNumber(num) {
  try {
    return formatNumberWithIntl(num);
  } catch (e) {
    console.error('Formatting error:', e);
    return num.toString();
  }
}

3. 安全风险

  • 用户输入验证:防止注入攻击
  • 避免使用 eval() 处理用户输入
  • 对非数字输入进行类型转换

九、常见问题与踩坑

1. 四舍五入错误

错误示例:

console.log(1.005.toFixed(2)); // 输出 "1.00"

解决方案:

function safeToFixed(num, decimals) {
  const pow = Math.pow(10, decimals);
  return Math.round(num * pow) / pow;
}

2. 正则表达式匹配错误

错误示例:

console.log(formatNumberWithRegex(100.0004)); // 输出 "100.0004"

解决方案:

function formatNumberWithRegex(num) {
  const numStr = num.toString();
  
  if (/^-?\d+$/.test(numStr)) {
    return numStr;
  }
  
  const match = numStr.match(/(\d+\.\d{0,2})\d*$/);
  
  if (match) {
    return match[1];
  }
  
  return numStr;
}

3. 科学计数法处理

错误示例:

console.log(formatNumberWithIntl(1.23e+4)); // 输出 "12300"

解决方案:

function formatNumberWithIntl(num) {
  const numStr = num.toString();
  
  if (/e/i.test(numStr)) {
    return numStr;
  }
  
  const formatter = new Intl.NumberFormat('en-US', {
    maximumFractionDigits: 2,
    minimumFractionDigits: 0,
    useGrouping: false
  });
  
  return formatter.format(num);
}

十、最佳实践

  1. 优先使用 Intl.NumberFormat:支持多语言、自动处理科学计数法,但需注意性能影响
  2. 对关键业务场景进行单元测试:覆盖整数、浮点数、科学计数法等所有情况
  3. 避免使用 toFixed() 独立处理:可能引入精度问题
  4. 对用户输入进行验证:确保输入是合法数字
  5. 性能敏感场景使用缓存:对频繁调用的格式化函数进行缓存
  6. 处理特殊数据格式:如科学计数法、百分比等

十一、总结

实现"保留两位小数,无小数不显示"的功能需要综合考虑:

  • 浮点数精度问题
  • 不同数据类型的处理
  • 国际化需求
  • 性能优化
  • 安全风险

本文深入分析了多种实现方案,包括基础方法、正则表达式和国际化 API。通过完整案例展示了在电商价格展示等实际场景中的应用。建议在具体项目中根据需求选择合适方案,注意处理特殊数据格式,确保格式化结果的准确性和性能。在涉及金融、科学计算等对精度要求高的场景时,应考虑使用更专业的库(如 decimal.js)来保证计算准确性。