2024-08-09



<template>
  <div>
    <button @click="sendPostRequest">发送POST请求</button>
  </div>
</template>
 
<script>
import axios from 'axios';
 
export default {
  methods: {
    async sendPostRequest() {
      try {
        const response = await axios.post('https://your-api-endpoint.com/post', {
          // 你的请求数据
          key1: 'value1',
          key2: 'value2'
        });
        console.log(response.data); // 处理响应数据
      } catch (error) {
        console.error(error); // 处理错误情况
      }
    }
  }
}
</script>

这段代码展示了如何在 Vue 3 应用中使用 axios 发送一个异步的 POST 请求。按钮点击时触发 sendPostRequest 方法,该方法使用 axios.post 发送请求到指定的 API 端点,并携带请求体中的数据。响应处理使用了 try...catch 结构来处理可能发生的错误。

react-native-ios-charts 是一个为 React Native 应用提供的图表库,它是基于 iOS 上的 Charts 库进行构建的。这个库支持多种图表类型,包括线图、柱状图、饼图等。

以下是如何使用 react-native-ios-charts 创建一个简单的线性图表的例子:

首先,你需要安装 react-native-ios-charts 库:




npm install react-native-ios-charts --save

然后,你需要根据 react-native-ios-charts 的文档配置项目。

接下来,你可以在你的 React Native 应用中创建一个图表组件:




import React from 'react';
import { View } from 'react-native';
import { LineChart } from 'react-native-ios-charts';
 
const data = {
  labels: ['January', 'February', 'March', 'April', 'May', 'June'],
  dataSets: [{
    values: [20, 45, 28, 56, 26, 41],
    config: {
      lineWidth: 2, // 线宽
      color: '#ff0000', // 线条颜色
    },
  }],
};
 
const LineChartExample = () => {
  return (
    <View style={{ height: 200 }}>
      <LineChart
        data={data}
        width={300}
        height={200}
        chartDescription={{ text: 'Monthly Sales' }}
        xAxis={{
          valueFormatter: ({ index }) => data.labels[index],
        }}
      />
    </View>
  );
};
 
export default LineChartExample;

在这个例子中,我们创建了一个简单的线性图表,它显示了每个月的销售情况。你可以根据需要调整数据和配置选项来满足你的具体需求。

搭建React Native iOS项目的步骤如下:

  1. 安装Homebrew(如果尚未安装):



/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  1. 安装Node.js和npm:



brew install node
  1. 安装React Native CLI:



npm install -g react-native-cli
  1. 创建一个新的React Native项目:



react-native init AwesomeProject
  1. 进入项目目录:



cd AwesomeProject
  1. 安装CocoaPods(iOS项目的依赖管理工具):



sudo gem install cocoapods
  1. 在项目根目录下设置CocoaPods环境:



npx pod-install
  1. 打开项目工程文件:



open ios/AwesomeProject.xcodeproj
  1. 在Xcode中,选择模拟器并构建项目(⌘+B)。
  2. 在命令行中启动React Native Packager:



yarn start
  1. 在模拟器中,你可以看到应用界面。

注意:确保你的Mac电脑安装了Xcode,并且选择了正确的M2芯片的Mac(如果你有M2芯片的Mac)。如果你遇到任何错误,请检查Xcode的控制台输出以获取详细的错误信息,并根据提示进行修复。

2024-08-09

flutter_cupertino_settings 是一个Flutter包,它提供了一套完整的iOS风格设置页面控件,可以用于构建类似iOS的设置页面。

以下是如何使用 flutter_cupertino_settings 的简单示例:

首先,在你的 pubspec.yaml 文件中添加依赖:




dependencies:
  flutter:
    sdk: flutter
  flutter_cupertino_settings: ^1.0.31

然后,你可以在你的Flutter应用中这样使用它:




import 'package:flutter/material.dart';
import 'package:flutter_cupertino_settings/flutter_cupertino_settings.dart';
 
void main() => runApp(MyApp());
 
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: CupertinoSettingsExample(),
    );
  }
}
 
class CupertinoSettingsExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CupertinoSettings(
      items: [
        CupertinoTextSettingsItem(
          title: 'Reset Location',
          description: 'Use current location as new default',
          placeholder: 'Allow "App Name" to access your location',
        ),
        CupertinoSwitchSettingsItem(
          title: 'Notifications',
          description: 'Allow "App Name" to send you notifications',
          value: true,
        ),
        // You can add more settings items here
      ],
    );
  }
}

这个示例创建了一个简单的iOS风格的设置页面,包含了一个文本设置项和一个开关设置项。你可以根据需要添加更多的设置项,以构建功能丰富的设置页面。

2024-08-09

在Flutter中,如果你在使用Provider.of来获取Provider中的数据,但你不想监听数据的变化,你可以将listen参数设置为false。这样做可以避免每次数据更新时重新调用build方法,从而提高性能。

例如,如果你有一个计数器的Provider,并且你只想在初始化时获取计数器的值,而不关心它之后的变化,你可以这样使用Provider.of:




int counterValue = Provider.of<Counter>(context, listen: false).value;

这段代码获取了CounterProvider中的计数器值,但是由于listen参数被设置为false,所以当计数器值变化时,UI不会自动更新。这样做可以避免不必要的重建,从而提高应用的运行效率。

2024-08-09

在开始之前,确保你已经安装了Node.js和npm/yarn。

  1. 创建项目:



npm create vite@latest my-vue3-app --template vue-ts
cd my-vue3-app
  1. 安装必要的依赖:



npm install ant-design-vue@next axios unocss
  1. 配置vite.config.ts以支持AntDesign和Unocss:



import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import antDesign from 'unplugin-antd-vue/vite'
import unocss from 'unocss/vite'
 
export default defineConfig({
  plugins: [
    vue(),
    antDesign(),
    unocss()
  ]
})
  1. 在main.ts中引入AntDesign和Unocss:



import 'unocss/dist/bundle.css'
import 'ant-design-vue/dist/antd.css'
import { createApp } from 'vue'
import App from './App.vue'
import { setupAntd } from 'ant-design-vue'
 
const app = createApp(App)
setupAntd(app)
app.mount('#app')
  1. 在src/api/http.ts中创建Axios实例:



import axios from 'axios'
 
const http = axios.create({
  baseURL: 'http://your-backend-api.com/api/v1',
  // 其他配置...
})
 
export default http
  1. 在src/api/index.ts中封装API调用:



import http from './http'
 
export const getData = () => {
  return http.get('/data')
}
  1. 在组件中使用API:



<template>
  <div>
    <!-- 组件内容 -->
  </div>
</template>
 
<script setup lang="ts">
import { ref } from 'vue'
import { getData } from '../api'
 
const data = ref([])
 
getData().then(response => {
  data.value = response.data
})
</script>

以上代码提供了一个简单的框架,你可以在此基础上开始开发你的Vue应用。记得替换掉示例中的API基础路径和API端点,以连接到你的实际后端API。

2024-08-09

'# HTML内嵌pdf在ios设备上无法正常显示

一、背景与问题

在移动互联网开发中,网页内嵌PDF文件是常见的需求。但iOS设备(如iPhone、iPad)上的Safari浏览器存在特殊限制,会导致PDF文件无法正常显示。根据Apple官方文档,Safari浏览器对PDF的处理存在以下限制:

  1. 不支持<iframe>标签直接显示PDF
  2. 不支持<object>标签的type="application/pdf"类型
  3. 不支持<embed>标签的type="application/pdf"类型
  4. 不支持PDF的打印功能
  5. 不支持PDF的缩放和旋转操作

这些限制源于iOS系统对安全性和资源管理的特殊处理,导致开发者在移动端开发中常遇到PDF显示异常的问题。

二、基本原理

HTML中嵌入PDF的常见方式包括:

  1. 直接URL嵌入

    <a href="example.pdf" download>下载PDF</a>
  2. iframe嵌入

    <iframe src="example.pdf" width="100%" height="600px"></iframe>
  3. object标签嵌入

    <object data="example.pdf" type="application/pdf" width="100%" height="600px">
      <p>您的浏览器不支持PDF查看器</p>
    </object>
  4. 第三方库渲染

    <div id="pdfViewer"></div>
    <script src="pdf.js"></script>

iOS设备的Safari浏览器对PDF的处理机制如下:

  1. 安全限制:出于安全考虑,iOS系统禁止直接通过HTML标签渲染PDF文件,必须通过特定的App或系统内置的PDF阅读器打开
  2. 资源隔离:iOS系统将PDF文件视为独立资源,不允许通过网页直接渲染
  3. 渲染机制:iOS系统会尝试将PDF文件作为附件处理,而不是直接在网页中渲染

三、环境准备

开发环境要求:

  • 浏览器:Safari 15+(iOS 15+)
  • 操作系统:iOS 13+(iPhone 8+及以上)
  • 开发工具:Chrome DevTools(通过Safari开发者工具)

四、核心实现

1. 基础问题复现

<!DOCTYPE html>
<html>
<head>
  <title>PDF显示问题</title>
</head>
<body>
  <iframe src="example.pdf" width="100%" height="600px"></iframe>
</body>
</html>

在iOS设备上运行时,会显示空白页面或提示"无法加载内容"。

关键代码分析

  • iframe标签的src属性指向PDF文件
  • 缺少allow属性(如allowfullscreen)
  • 缺少sandbox属性(限制权限)

2. 带参数的object标签

<object data="example.pdf" 
        type="application/pdf" 
        width="100%" 
        height="600px"
        allowfullscreen="true"
        sandbox="allow-same-origin allow-forms allow-scripts">
  <p>您的浏览器不支持PDF查看器</p>
</object>

关键代码解释

  • type="application/pdf"指定MIME类型
  • allowfullscreen="true"启用全屏模式
  • sandbox属性限制权限
  • 备用内容提示

3. 使用第三方库渲染

<div id="pdfViewer" style="width:100%; height:600px;"></div>
<script src="https://unpkg.com/pdfjs-dist@3.4.122/build/pdf.min.js"></script>
<script>
  const loadingTask = pdfjsLib.getDocument('example.pdf');
  loadingTask.promise.then(function(pdf) {
    pdf.getPage(1).then(function(page) {
      const canvas = document.createElement('canvas');
      const context = canvas.getContext('2d');
      page.render({
        canvasContext: context,
        intent: 'display'
      }).promise.then(function() {
        document.getElementById('pdfViewer').appendChild(canvas);
      });
    });
  });
</script>

关键代码解释

  • 使用PDF.js库解析PDF文件
  • 逐页渲染PDF内容
  • 使用Canvas进行绘制
  • 支持跨平台渲染

五、完整案例

1. 多方案对比页面

<!DOCTYPE html>
<html>
<head>
  <title>PDF显示方案对比</title>
</head>
<body>
  <h2>方案一:iframe嵌入</h2>
  <iframe src="example.pdf" width="100%" height="600px"></iframe>

  <h2>方案二:object标签</h2>
  <object data="example.pdf" 
          type="application/pdf" 
          width="100%" 
          height="600px"
          allowfullscreen="true"
          sandbox="allow-same-origin allow-forms allow-scripts">
    <p>您的浏览器不支持PDF查看器</p>
  </object>

  <h2>方案三:PDF.js渲染</h2>
  <div id="pdfViewer" style="width:100%; height:600px;"></div>
  <script src="https://unpkg.com/pdfjs-dist@3.4.122/build/pdf.min.js"></script>
  <script>
    const loadingTask = pdfjsLib.getDocument('example.pdf');
    loadingTask.promise.then(function(pdf) {
      pdf.getPage(1).then(function(page) {
        const canvas = document.createElement('canvas');
        const context = canvas.getContext('2d');
        page.render({
          canvasContext: context,
          intent: 'display'
        }).promise.then(function() {
          document.getElementById('pdfViewer').appendChild(canvas);
        });
      });
    });
  </script>
</body>
</html>

2. 实际测试结果

方案iOS设备表现浏览器兼容性开发成本
iframe空白/错误全平台低
object空白/错误全平台中
PDF.js正常显示全平台高

六、源码解析

1. PDF.js核心流程

// 初始化PDF文档
const loadingTask = pdfjsLib.getDocument('example.pdf');

// 获取具体页面
loadingTask.promise.then(function(pdf) {
  pdf.getPage(1).then(function(page) {
    // 创建Canvas元素
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');

    // 渲染页面
    page.render({
      canvasContext: context,
      intent: 'display'
    }).promise.then(function() {
      // 添加到DOM
      document.getElementById('pdfViewer').appendChild(canvas);
    });
  });
});

关键点分析

  • getDocument方法获取PDF文档
  • getPage方法获取具体页面
  • render方法将页面内容绘制到Canvas
  • intent: 'display'表示显示意图

2. Canvas绘制原理

// 创建Canvas元素
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');

// 设置Canvas尺寸
canvas.width = page.getViewport({ scale: 1.5 }).width;
canvas.height = page.getViewport({ scale: 1.5 }).height;

// 绘制页面内容
page.render({
  canvasContext: context,
  intent: 'display'
}).promise.then(function() {
  // 添加到DOM
  document.getElementById('pdfViewer').appendChild(canvas);
});

七、进阶使用

1. 多页渲染

function renderPage(pageNumber) {
  const canvas = document.createElement('canvas');
  const context = canvas.getContext('2d');

  page.getViewport({ scale: 1.5 }).then(function(viewport) {
    page.render({
      canvasContext: context,
      viewport: viewport
    }).promise.then(function() {
      document.getElementById('pdfViewer').appendChild(canvas);
    });
  });
}

2. 交互增强

// 添加翻页按钮
document.getElementById('prevPage').addEventListener('click', function() {
  currentPage--;
  if (currentPage < 1) currentPage = 1;
  renderPage(currentPage);
});

document.getElementById('nextPage').addEventListener('click', function() {
  currentPage++;
  if (currentPage > totalPages) currentPage = totalPages;
  renderPage(currentPage);
});

八、性能与工程实践

1. 性能优化

  • 分页加载:只加载当前显示的页面
  • Canvas缓存:将已渲染页面缓存到内存
  • 压缩处理:对PDF文件进行压缩处理
  • 懒加载:按需加载PDF内容

2. 安全风险

  • XSS攻击:直接渲染用户上传的PDF可能导致恶意代码执行
  • 内存泄漏:大量Canvas元素可能导致内存占用过高
  • 跨域问题:PDF文件需要正确设置CORS头

3. 异常处理

try {
  const loadingTask = pdfjsLib.getDocument('example.pdf');
  loadingTask.promise.then(function(pdf) {
    // 处理成功
  }).catch(function(error) {
    console.error('加载PDF失败:', error);
  });
} catch (error) {
  console.error('初始化PDF解析器失败:', error);
}

九、常见问题与踩坑

1. 常见错误

错误类型描述解决方案
错误1PDF文件过大导致加载缓慢使用PDF.js的分页加载机制
错误2空白页面确保PDF文件路径正确,检查网络请求
错误3跨域问题配置服务器添加CORS头:Access-Control-Allow-Origin: *
错误4内存占用过高定期清理Canvas元素,使用弱引用管理

2. 特殊情况处理

  • iOS系统限制:iOS系统不允许直接渲染PDF文件,必须通过第三方库
  • PDF文件格式:确保PDF文件是标准格式,避免使用加密或损坏的文件
  • 字体问题:某些PDF文件使用特殊字体可能导致渲染异常

十、最佳实践

1. 推荐方案

  • 简单场景:使用<a>标签提供下载链接
  • 中等场景:使用PDF.js进行基础渲染
  • 复杂场景:使用第三方PDF查看器插件(如PDFObject)

2. 使用建议

  • iOS设备:优先使用PDF.js方案
  • 跨平台:考虑使用React PDF等现代框架
  • 安全性:对用户上传的PDF文件进行验证
  • 性能优化:对大PDF文件进行分页处理

十一、总结

HTML内嵌PDF在iOS设备上的显示问题源于Safari浏览器的特殊限制,需要采用替代方案。通过分析不同实现方式的优缺点,我们发现:

  1. 直接使用HTML标签存在兼容性问题
  2. PDF.js库提供了可靠的解决方案
  3. 第三方插件可以提供更丰富的功能
  4. 安全性和性能需要特别关注

在实际开发中,应根据具体需求选择合适的方案。对于需要兼容iOS设备的场景,推荐使用PDF.js进行渲染,同时注意处理好安全性和性能问题。通过合理的设计和优化,可以实现良好的PDF显示效果。

2024-08-09

'# 前端简单的图书增、删、改、查(html+css+javascript+jquery+axios)

一、背景与问题

在实际的Web开发中,图书管理系统是一个典型的CRUD(Create, Read, Update, Delete)应用场景。传统开发中,这类功能常通过后端API与前端交互实现。但对于自学前端的开发者而言,如何从零构建一个完整的CRUD系统,理解前后端交互的底层原理,是提升技术深度的关键一步。

本篇文章将以图书管理系统为切入点,深入探讨:

  • 前端如何通过AJAX与后端API通信
  • 前端状态管理的实现方式
  • 前端与后端的数据交互规范
  • 常见错误及解决办法
  • 性能优化与安全风险

通过完整代码示例,帮助读者理解技术原理,而非简单复制粘贴。


二、基本原理

1. 前端与后端的交互机制

在本案例中,前端通过 Axios 发起HTTP请求,与后端RESTful API进行通信。核心流程如下:

  1. 用户操作(如点击“添加图书”按钮)
  2. 前端收集数据(如表单输入的书名、作者)
  3. 前端发送请求(通过Axios的POST或PUT方法)
  4. 后端处理请求(验证数据、保存到数据库)
  5. 后端返回响应(HTTP状态码和数据)
  6. 前端更新界面(根据响应结果刷新列表)

2. 数据格式规范

前后端交互需要遵循统一的数据格式,通常使用JSON。例如:

{
  "id": 1,
  "title": "JavaScript高级程序设计",
  "author": "Nicholas C. Zakas",
  "created_at": "2023-10-05T14:30:00Z"
}

3. 状态管理与异步处理

前端需要管理本地状态(如图书列表),并处理异步请求的回调。使用Promise和async/await可避免回调地狱,同时通过$.ajax或Axios的拦截器统一处理错误。


三、环境准备

1. 技术栈

  • 前端:HTML、CSS、JavaScript、jQuery、Axios
  • 后端:假设使用Node.js + Express(需自行搭建API)
  • 数据库:可选(如MySQL、MongoDB,此处仅演示前端逻辑)

2. 依赖安装

npm install axios jquery

四、核心实现

1. 基础页面结构

<!-- index.html -->
<div id="app">
  <h1>图书管理系统</h1>
  <div id="add-form">
    <input type="text" id="title" placeholder="书名">
    <input type="text" id="author" placeholder="作者">
    <button id="add-btn">添加</button>
  </div>
  <table id="book-table">
    <thead>
      <tr>
        <th>书名</th>
        <th>作者</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody id="book-list">
    </tbody>
  </table>
</div>

2. 样式设计

/* style.css */
#book-table {
  width: 100%;
  border-collapse: collapse;
}
#book-table th, #book-table td {
  border: 1px solid #ccc;
  padding: 10px;
}
#book-table tr:nth-child(even) {
  background-color: #f9f9f9;
}

3. 核心逻辑实现

3.1 添加图书功能

// script.js
$(document).ready(function () {
  const addBtn = $('#add-btn');
  const titleInput = $('#title');
  const authorInput = $('#author');

  addBtn.click(async function () {
    const title = titleInput.val().trim();
    const author = authorInput.val().trim();

    if (!title || !author) {
      alert('书名和作者不能为空');
      return;
    }

    try {
      const response = await axios.post('https://api.example.com/books', {
        title,
        author
      });

      const book = response.data;
      addBookToTable(book);
      titleInput.val('');
      authorInput.val('');
    } catch (error) {
      console.error('添加图书失败:', error);
      alert('网络错误,请重试');
    }
  });
});

关键点解释:

  • 使用async/await简化异步处理
  • 通过axios.post发送POST请求
  • 若后端返回成功响应(如201 Created),则更新界面

3.2 查询图书列表

// script.js
function fetchBooks() {
  return axios.get('https://api.example.com/books');
}

3.3 动态渲染表格

// script.js
function addBookToTable(book) {
  const row = $('<tr>');
  row.append($('<td>').text(book.title));
  row.append($('<td>').text(book.author));
  row.append(
    $('<td>').append(
      $('<button>').text('删除').click(() => deleteBook(book.id)),
      $('<button>').text('编辑').click(() => editBook(book))
    )
  );
  $('#book-list').append(row);
}

3.4 删除图书功能

// script.js
function deleteBook(id) {
  if (confirm('确定要删除这本书吗?')) {
    axios.delete(`https://api.example.com/books/${id}`)
      .then(() => {
        alert('删除成功');
        fetchBooks().then(res => {
          $('#book-list').empty();
          res.data.forEach(addBookToTable);
        });
      })
      .catch(error => {
        console.error('删除失败:', error);
        alert('删除失败,请重试');
      });
  }
}

五、完整案例

1. 项目结构

/book-system
│
├── index.html
├── style.css
├── script.js
└── package.json

2. 后端API模拟(Node.js + Express)

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

app.use(express.json());

let books = [];

app.get('/books', (req, res) => {
  res.json(books);
});

app.post('/books', (req, res) => {
  const { title, author } = req.body;
  const id = Date.now().toString();
  books.push({ id, title, author });
  res.status(201).json({ id, title, author });
});

app.delete('/books/:id', (req, res) => {
  const { id } = req.params;
  books = books.filter(book => book.id !== id);
  res.status(204).send();
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

3. 完整运行流程

  1. 启动后端服务:node server.js
  2. 打开前端页面,输入书名和作者点击“添加”
  3. 查看页面表格实时更新
  4. 点击“删除”或“编辑”按钮进行操作

六、源码解析

1. 事件绑定与异步处理

addBtn.click(async function () {
  // 异步处理逻辑
});

解释:

  • async/await将异步代码转化为同步写法,提升可读性
  • 若后端返回错误(如400 Bad Request),catch块会捕获异常

2. 数据绑定与界面更新

$('#book-list').empty();
res.data.forEach(addBookToTable);

解释:

  • 清空旧数据后重新渲染
  • 避免DOM操作过于频繁,影响性能

七、进阶使用

1. 前端状态管理优化

使用localStorage持久化数据:

localStorage.setItem('books', JSON.stringify(books));

2. 增加编辑功能

function editBook(book) {
  const title = prompt('请输入新书名', book.title);
  const author = prompt('请输入新作者', book.author);
  if (title && author) {
    axios.put(`https://api.example.com/books/${book.id}`, { title, author })
      .then(() => {
        alert('更新成功');
        fetchBooks().then(res => {
          $('#book-list').empty();
          res.data.forEach(addBookToTable);
        });
      });
  }
}

3. 使用拦截器统一处理错误

axios.interceptors.response.use(
  response => response,
  error => {
    console.error('全局错误:', error);
    alert('网络异常,请重试');
    return Promise.reject(error);
  }
);

八、性能与工程实践

1. 性能优化

  • 防抖/节流:限制频繁请求(如输入时实时搜索)
  • 缓存数据:使用localStorage减少重复请求
  • 减少DOM操作:批量更新界面

2. 安全风险

  • CSRF攻击:需在后端验证请求来源
  • XSS漏洞:对用户输入内容进行过滤
  • 数据泄露:避免在前端暴露敏感信息(如密码)

3. 异常处理

  • 网络错误:axios的catch块处理超时、断开连接等
  • 数据校验:前端校验输入格式(如非空、长度限制)

九、常见问题与踩坑

1. 跨域问题(CORS)

错误示例:

axios.post('http://localhost:3000/books', { title: 'test' })
  .catch(error => console.error(error));

错误原因:前端与后端端口不一致,导致跨域请求被拦截

解决办法:

  • 后端配置CORS头
  • 使用代理服务器(如http-proxy-middleware)
  • 使用createProxyMiddleware创建开发代理

2. 请求未正确处理

错误示例:

axios.get('/books')
  .then(data => console.log(data))
  .catch(error => console.error(error));

错误原因:未处理404或500错误

解决办法:

  • 使用if (error.response)检查响应状态码
  • 根据不同状态码返回不同提示

3. 数据未刷新

错误示例:

$('#book-list').append(...);

错误原因:未清空旧数据直接追加

解决办法:

  • 使用empty()清空旧内容
  • 批量更新界面

十、最佳实践

  1. 统一错误处理:使用Axios拦截器集中管理错误
  2. 分离关注点:将业务逻辑与DOM操作分离
  3. 数据验证:在前端和后端双重校验数据
  4. 状态管理:使用localStorage或Vuex管理全局状态
  5. 安全加固:对用户输入内容进行过滤和编码

十一、总结

本文通过一个图书管理系统的案例,深入探讨了前端CRUD功能的实现原理,涵盖了AJAX通信、异步处理、状态管理、性能优化等多个技术点。

适用场景:

  • 小型项目或原型开发
  • 快速验证业务逻辑
  • 学习前后端交互原理

不适用场景:

  • 复杂业务系统(需使用框架如React/Vue)
  • 高并发场景(需考虑后端限流、缓存等)

通过本文的实践,读者不仅能掌握前端CRUD的实现,更能理解其背后的原理,为后续深入学习前端技术打下坚实基础。


附:完整源码
GitHub仓库地址:https://github.com/your-username/book-system

2024-08-09

'# 使用typescript封装axios

一、背景与问题

在现代前端开发中,HTTP请求是核心功能之一。Axios作为流行的HTTP客户端库,提供了丰富的功能,但其原生的使用方式存在以下问题:

  1. 类型安全不足:原生axios在TypeScript项目中需要手动定义接口,容易出现类型不匹配
  2. 重复代码多:每个请求都需要重复配置baseURL、headers等参数
  3. 错误处理不统一:不同接口的错误处理逻辑差异大
  4. 拦截器管理困难:缺乏统一的拦截器管理机制
  5. 性能优化不足:未考虑请求缓存、重试等机制

在大型项目中,这些问题会逐渐演变成维护成本和潜在的运行时错误。通过封装axios,可以构建统一的HTTP服务层,提升代码质量。

二、基本原理

Axios的封装本质上是构建一个统一的请求处理管道,包含以下核心组件:

  1. 类型定义:使用TypeScript的接口和泛型实现类型安全
  2. 请求拦截器:统一处理请求参数、token、loading状态等
  3. 响应拦截器:统一处理错误、数据转换、身份验证等
  4. 配置管理:集中管理baseURL、headers、超时等配置
  5. 错误处理机制:定义统一的错误类型和处理逻辑

Axios的底层使用Promise实现异步请求,通过拦截器队列处理请求和响应。每个拦截器都是一个函数,按顺序执行。

三、环境准备

# 创建项目结构
mkdir axios-encapsulation
cd axios-encapsulation
npm init -y
npm install axios typescript ts-node @types/axios
npx ts-node -p tsconfig.json
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES6",
    "module": "ESNext",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "rootDir": "."
  },
  "include": ["src"]
}

四、核心实现

1. 基础封装

// src/http/index.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';

// 定义通用响应类型
interface HttpResponse<T> {
  code: number;
  message: string;
  data: T | null;
}

// 创建axios实例
const service: AxiosInstance = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
});

// 请求拦截器
service.interceptors.request.use(
  (config: AxiosRequestConfig): AxiosRequestConfig => {
    // 添加token
    const token = localStorage.getItem('token');
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    return config;
  },
  (error: any) => {
    // 请求异常处理
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse): AxiosResponse<HttpResponse<any>> => {
    // 响应数据转换
    const { data } = response;
    return {
      ...response,
      data: {
        code: data.code,
        message: data.message,
        data: data.data
      }
    };
  },
  (error: any): Promise<HttpResponse<any>> => {
    // 响应错误处理
    const { response } = error;
    if (response) {
      return Promise.resolve({
        code: response.status,
        message: response.statusText,
        data: null
      });
    }
    return Promise.resolve({
      code: -1,
      message: '网络异常',
      data: null
    });
  }
);

export default service;

关键代码解释:

  • 创建axios实例时配置了基础URL和超时时间
  • 请求拦截器中添加了token认证逻辑
  • 响应拦截器统一处理了接口返回的数据结构
  • 响应拦截器返回统一的HttpResponse类型

2. 请求封装

// src/http/request.ts
import service from './index';

// 定义请求方法
export async function get<T>(url: string, params?: any): Promise<T> {
  try {
    const response = await service.get(url, { params });
    if (response.data.code === 200) {
      return response.data.data;
    }
    throw new Error(response.data.message);
  } catch (error) {
    console.error('请求失败:', error);
    throw error;
  }
}

export async function post<T>(url: string, data?: any): Promise<T> {
  try {
    const response = await service.post(url, data);
    if (response.data.code === 200) {
      return response.data.data;
    }
    throw new Error(response.data.message);
  } catch (error) {
    console.error('请求失败:', error);
    throw error;
  }
}

3. 错误处理封装

// src/http/error.ts
export function handleHttpError(error: any): void {
  if (error.response) {
    // 响应错误(4xx/5xx)
    console.error('服务器响应错误:', error.response.status);
  } else if (error.request) {
    // 无响应(网络问题)
    console.error('无响应:', error.request);
  } else {
    // 设置请求错误
    console.error('请求错误:', error.message);
  }
}

五、完整案例

项目结构

axios-encapsulation/
├── src/
│   ├── http/
│   │   ├── index.ts       // axios封装
│   │   ├── request.ts    // 请求方法
│   │   └── error.ts      // 错误处理
│   └── main.ts           // 入口文件
├── tsconfig.json
└── package.json

使用示例

// src/main.ts
import { get, post } from './http/request';

async function main() {
  try {
    // 获取用户列表
    const users = await get('/users', { page: 1, pageSize: 10 });
    console.log('用户列表:', users);
    
    // 创建新用户
    const newUser = await post('/users', {
      name: '张三',
      email: 'zhangsan@example.com'
    });
    console.log('创建用户:', newUser);
  } catch (error) {
    handleHttpError(error);
  }
}

main();

六、源码解析

  1. 拦截器队列机制:

    • Axios拦截器使用链式结构,每个拦截器返回的config或response会传递给下一个拦截器
    • 请求拦截器在发送请求前执行,响应拦截器在接收到响应后执行
  2. 类型转换逻辑:

    • 响应拦截器将原始响应数据转换为统一的HttpResponse类型
    • 通过泛型参数实现数据类型校验
  3. 错误处理机制:

    • 区分了网络错误、服务器错误、客户端错误等不同场景
    • 统一的错误处理函数可以集中处理日志、提示等逻辑

七、进阶使用

1. 请求重试机制

// src/http/retry.ts
import service from './index';

export async function retryGet<T>(url: string, params?: any, retries = 3): Promise<T> {
  let attempt = 0;
  while (attempt < retries) {
    try {
      const response = await service.get(url, { params });
      if (response.data.code === 200) {
        return response.data.data;
      }
      throw new Error(response.data.message);
    } catch (error) {
      console.warn(`尝试 ${attempt + 1} 失败: ${error.message}`);
      attempt++;
      if (attempt < retries) {
        await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
      }
    }
  }
  throw new Error('请求重试失败');
}

2. 请求缓存机制

// src/http/cache.ts
import service from './index';

type CacheConfig = {
  maxAge?: number; // 缓存最大时间(秒)
  cacheKey?: (url: string, params?: any) => string;
};

export async function cachedGet<T>(url: string, params?: any, config?: CacheConfig): Promise<T> {
  const cacheKey = config?.cacheKey?.(url, params) || `${url}?${new URLSearchParams(params).toString()}`;
  
  // 检查缓存
  const cached = localStorage.getItem(cacheKey);
  if (cached) {
    const { timestamp, data } = JSON.parse(cached);
    if (Date.now() - timestamp < (config?.maxAge || 3600) * 1000) {
      return data;
    }
  }
  
  // 执行请求
  const result = await get(url, params);
  
  // 存储缓存
  localStorage.setItem(cacheKey, JSON.stringify({
    timestamp: Date.now(),
    data: result
  }));
  
  return result;
}

八、性能与工程实践

1. 性能优化

  • 减少拦截器数量:避免不必要的中间处理
  • 使用缓存:对不常变化的数据进行缓存
  • 限制并发请求:使用axios的concurrency参数控制并发数量
  • 压缩请求体:对大数据量请求进行压缩处理

2. 异常处理

  • 网络错误重试:对网络不稳定场景进行重试
  • 超时处理:设置合理的超时时间
  • 错误日志记录:记录详细的错误信息用于后续分析

3. 安全考虑

  • HTTPS强制:确保所有请求使用HTTPS
  • CORS配置:合理配置CORS策略,避免安全漏洞
  • 敏感数据加密:对敏感数据进行加密传输
  • 防止CSRF:添加CSRF防护机制

九、常见问题与踩坑

1. 类型定义错误

// 错误示例
const response = await service.get('/users');
console.log(response.data.name); // 编译错误

问题:未定义具体类型,导致类型检查失效

解决:使用泛型明确类型

const response = await get('/users', { page: 1 }); // 明确类型
console.log(response.name);

2. 拦截器顺序问题

// 错误示例
service.interceptors.request.use((config) => {
  // 拦截器1
  return config;
});

service.interceptors.request.use((config) => {
  // 拦截器2
  return config;
});

问题:拦截器顺序错误导致配置覆盖

解决:使用use方法添加拦截器

3. 缓存失效问题

// 错误示例
const data = await cachedGet('/users', { page: 1 });
console.log(data);

问题:缓存键未正确生成

解决:确保cacheKey函数返回唯一标识

十、最佳实践

  1. 统一的错误处理:所有请求都应该经过统一的错误处理流程
  2. 类型安全:使用泛型和接口确保类型安全
  3. 拦截器分层:将公共逻辑放在拦截器中,避免重复代码
  4. 配置集中管理:将baseURL、headers等配置集中管理
  5. 接口文档化:为每个接口定义清晰的接口文档
  6. 性能监控:添加请求耗时监控,优化慢接口
  7. 安全防护:添加必要的安全防护措施

十一、总结

通过TypeScript封装axios,我们构建了一个统一的HTTP服务层,解决了原始axios在类型安全、代码重复、错误处理等方面的痛点。这种封装方式特别适合大型项目,能够显著提升代码质量和可维护性。

什么时候应该使用:

  • 项目规模较大,需要统一的HTTP服务层
  • 需要严格的类型安全保证
  • 有统一的错误处理和日志记录需求
  • 需要添加统一的拦截器逻辑(如token认证、请求日志等)

什么时候不应该使用:

  • 极小的项目,增加封装成本不划算
  • 需要高度定制的请求处理逻辑
  • 临时性的接口调用需求

通过合理的封装和实践,可以显著提升开发效率和代码质量,同时为后续的维护和扩展提供良好的基础。在实际开发中,需要根据项目需求灵活调整封装策略,找到最适合的平衡点。

2024-08-09

'# 使用axios(AJAX)获取地理位置及天气

一、背景与问题

在现代Web开发中,获取用户地理位置和天气信息是常见的需求。例如:导航应用需要定位用户位置,天气应用需要展示实时气象数据。传统做法是通过浏览器的Geolocation API获取位置信息,再通过第三方气象API(如OpenWeatherMap)获取天气数据。

但开发中常遇到以下问题:

  1. 跨域请求限制导致无法直接调用第三方API
  2. 地理位置获取的权限控制问题
  3. 网络请求的异常处理和重试机制
  4. API密钥的安全存储问题
  5. 前后端数据格式的转换和校验

本文将深入探讨如何通过axios实现地理位置和天气信息的获取,结合实际开发场景分析技术原理和工程实践。

二、基本原理

1. 地理位置获取原理

浏览器通过Geolocation API访问设备的定位服务,其核心流程如下:

navigator.geolocation.getCurrentPosition(
  successCallback, 
  errorCallback, 
  { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
)
  • 高精度模式:enableHighAccuracy: true启用高精度定位,可能使用GPS、Wi-Fi和基站数据
  • 超时控制:timeout: 10000设置10秒超时,避免用户长时间等待
  • 缓存控制:maximumAge: 0表示不使用旧数据,强制获取最新位置

2. 天气数据获取原理

通过调用第三方API(如OpenWeatherMap)获取天气数据,其请求流程如下:

GET https://api.openweathermap.org/data/2.5/weather?lat=40.71&lon=-74.01&appid=YOUR_API_KEY
  • 经纬度参数:由Geolocation API获取
  • API密钥:需注册获取,不可硬编码在前端
  • 数据格式:返回JSON数据,包含温度、湿度、风速等信息

三、环境准备

1. 前端环境

npm install axios

2. 后端代理服务器(推荐)

由于前端无法直接访问第三方API,建议搭建Node.js代理服务器:

npm install express axios

四、核心实现

1. 获取地理位置的完整代码

// geolocation.js
async function getUserLocation() {
  try {
    const position = await new Promise((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(
        resolve, 
        error => reject(new Error(error.message)),
        { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
      );
    });
    
    console.log(`纬度: ${position.coords.latitude}, 经度: ${position.coords.longitude}`);
    return {
      lat: position.coords.latitude,
      lon: position.coords.longitude
    };
  } catch (error) {
    console.error('获取地理位置失败:', error.message);
    throw error;
  }
}

关键代码解释:

  • 使用Promise封装原生API,便于异步处理
  • 错误处理包含详细的错误信息
  • 高精度模式确保定位准确性
  • 超时控制防止用户长时间等待

2. 获取天气数据的完整代码

// weather.js
async function getWeather(lat, lon, apiKey) {
  try {
    const response = await axios.get(`https://api.openweathermap.org/data/2.5/weather`, {
      params: {
        lat,
        lon,
        appid: apiKey,
        units: 'metric'
      }
    });
    
    const { temperature, humidity, wind_speed, weather } = response.data;
    return {
      temperature,
      humidity,
      windSpeed: wind_speed,
      description: weather[0].description
    };
  } catch (error) {
    console.error('获取天气数据失败:', error.message);
    throw error;
  }
}

关键代码解释:

  • 使用axios的query参数方式传递请求参数
  • 单位设置为metric表示摄氏度
  • 返回结构化数据便于前端使用
  • 错误处理包含完整的错误信息

3. 安全处理方案

// security.js
function getSafeApiKey() {
  // 推荐使用环境变量存储API密钥
  // 在生产环境应使用服务器端配置
  const apiKeys = {
    production: 'YOUR_PRODUCTION_API_KEY',
    development: 'YOUR_DEVELOPMENT_API_KEY'
  };
  
  const env = process.env.NODE_ENV || 'development';
  return apiKeys[env];
}

关键代码解释:

  • 不在前端硬编码API密钥
  • 使用环境变量管理不同环境的配置
  • 避免API密钥泄露风险

五、完整案例

1. 前端页面实现

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>天气查询</title>
</head>
<body>
  <h1>天气查询</h1>
  <div id="result"></div>
  
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    async function fetchWeather() {
      try {
        const { lat, lon } = await getUserLocation();
        const apiKey = 'YOUR_API_KEY'; // 生产环境应使用安全方案
        
        const weatherData = await getWeather(lat, lon, apiKey);
        document.getElementById('result').innerHTML = `
          <p>当前温度: ${weatherData.temperature}℃</p>
          <p>湿度: ${weatherData.humidity}%</p>
          <p>风速: ${weatherData.windSpeed} m/s</p>
          <p>天气: ${weatherData.description}</p>
        `;
      } catch (error) {
        document.getElementById('result').innerHTML = `
          <p>获取天气信息失败: ${error.message}</p>
        `;
      }
    }
    
    fetchWeather();
  </script>
</body>
</html>

2. 后端代理服务器实现

// server.js
const express = require('express');
const axios = require('axios');
const app = express();
const PORT = 3000;

app.get('/weather', async (req, res) => {
  try {
    const { lat, lon } = req.query;
    const apiKey = 'YOUR_API_KEY'; // 生产环境应使用安全方案
    
    const response = await axios.get('https://api.openweathermap.org/data/2.5/weather', {
      params: {
        lat,
        lon,
        appid: apiKey,
        units: 'metric'
      }
    });
    
    res.json(response.data);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(PORT, () => {
  console.log(`服务器运行在 http://localhost:${PORT}`);
});

3. 前端与后端的配合

// frontend.js
async function fetchWeatherFromServer() {
  try {
    const { lat, lon } = await getUserLocation();
    
    const response = await axios.get('http://localhost:3000/weather', {
      params: {
        lat,
        lon
      }
    });
    
    const weatherData = response.data;
    // 处理并展示天气数据
  } catch (error) {
    console.error('请求天气数据失败:', error.message);
  }
}

六、源码解析

1. axios的内部机制

axios基于Promise/A+规范实现异步请求,其核心流程:

  1. 创建XMLHttpRequest对象
  2. 设置请求头和超时时间
  3. 发送HTTP请求
  4. 监听响应事件
  5. 返回Promise对象

2. 错误处理机制

axios的错误处理分为两种情况:

  • 网络错误(如超时、DNS解析失败):通过onunhandledrejection处理
  • HTTP错误(如404、500):通过.catch()处理

3. 重试机制实现

function retryRequest(config, retries = 3) {
  return new Promise((resolve, reject) => {
    axios(config)
      .then(resolve)
      .catch((error) => {
        if (retries > 0 && error.response?.status === 503) {
          setTimeout(() => retryRequest(config, retries - 1).then(resolve).catch(reject), 1000);
        } else {
          reject(error);
        }
      });
  });
}

七、进阶使用

1. 缓存策略实现

// 使用localStorage缓存天气数据
function getWeatherWithCache(lat, lon, apiKey) {
  const cacheKey = `weather_${lat}_${lon}`;
  const cached = localStorage.getItem(cacheKey);
  
  if (cached) {
    return JSON.parse(cached);
  }
  
  return getWeather(lat, lon, apiKey).then(data => {
    localStorage.setItem(cacheKey, JSON.stringify(data));
    return data;
  });
}

2. 增强的错误处理

function handleNetworkError(error) {
  if (error.message.includes('Network request failed')) {
    console.error('网络连接失败,请检查网络');
    return '网络连接失败';
  }
  
  if (error.message.includes('Timeout')) {
    console.error('请求超时,请重试');
    return '请求超时';
  }
  
  return error.message;
}

3. 数据校验增强

function validateWeatherData(data) {
  if (!data || typeof data !== 'object' || !data.main) {
    throw new Error('无效的天气数据');
  }
  
  const { temp, humidity, wind_speed, weather } = data.main;
  if (typeof temp !== 'number' || typeof humidity !== 'number' || typeof wind_speed !== 'number') {
    throw new Error('数据类型校验失败');
  }
  
  if (!Array.isArray(weather) || weather.length === 0) {
    throw new Error('天气描述信息缺失');
  }
}

八、性能与工程实践

1. 性能优化方案

优化措施说明
压缩数据使用Gzip压缩响应数据
前端缓存使用localStorage缓存最近30分钟数据
服务端缓存使用Redis缓存热点数据
预加载在用户点击前预获取位置信息
资源合并合并多个API请求为单个请求

2. 异常处理机制

function handleAsyncError(fn) {
  return async (...args) => {
    try {
      return await fn(...args);
    } catch (error) {
      console.error('异步操作失败:', error.message);
      return null;
    }
  };
}

3. 安全实践

  • 使用HTTPS协议
  • 对API密钥进行加密存储
  • 设置请求签名验证
  • 限制请求频率(使用令牌桶算法)
  • 防止CSRF攻击

九、常见问题与踩坑

1. 跨域问题解决方案

错误示例:

// 前端直接调用第三方API会报错
axios.get('https://api.openweathermap.org/...').catch(...);

解决方案:

// 使用后端代理
axios.get('http://localhost:3000/weather', {
  params: {
    lat: 40.71,
    lon: -74.01
  }
});

2. 权限被拒绝问题

错误示例:

// 用户未授权地理位置权限
navigator.geolocation.getCurrentPosition(...);

解决方案:

// 添加用户提示
navigator.geolocation.getCurrentPosition(
  () => console.log('定位成功'),
  () => alert('请允许获取地理位置')
);

3. 网络请求失败处理

错误示例:

// 忽略错误处理
axios.get('https://api.example.com/data');

解决方案:

axios.get('https://api.example.com/data')
  .then(response => console.log(response.data))
  .catch(error => {
    if (error.response) {
      console.error('服务器响应错误:', error.response.status);
    } else if (error.request) {
      console.error('请求未收到响应');
    } else {
      console.error('请求配置错误:', error.message);
    }
  });

十、最佳实践

1. 安全最佳实践

  • 使用服务端代理处理所有外部API请求
  • 使用环境变量管理配置信息
  • 对敏感信息进行加密存储
  • 使用HTTPS协议保证传输安全
  • 设置请求频率限制

2. 性能最佳实践

  • 实现合理的缓存策略
  • 使用压缩技术减少传输数据量
  • 避免不必要的请求
  • 使用CDN加速静态资源
  • 优化前端渲染性能

3. 异常处理最佳实践

  • 对所有异步操作进行错误处理
  • 提供用户友好的错误提示
  • 记录关键错误日志
  • 实现重试机制
  • 设置超时机制

十一、总结

通过本文的深入探讨,我们了解了如何使用axios获取地理位置和天气信息的完整流程。从Geolocation API的使用到第三方天气API的调用,再到完整的前后端实现方案,我们涵盖了技术原理、代码实现、安全实践和性能优化等多个方面。

在实际开发中,建议采用服务端代理架构来处理外部API请求,既保证了安全性,又能够更好地控制请求频率和数据格式。同时,要特别注意处理用户授权、网络异常和数据校验等常见问题。

对于需要频繁获取地理位置的场景,建议结合缓存策略和预加载技术提高用户体验。对于涉及敏感数据的场景,务必使用加密存储和严格的访问控制。

在开发过程中,要始终遵循最佳实践,包括完善的错误处理、合理的性能优化和安全防护措施。这些实践将帮助开发者构建更健壮、更可靠的Web应用。