下面是一些常见的 Node.js 面试问题 和答案列表:

1) 什么是 Node.js?

Node.js 是一种用于构建可扩展程序的服务器端脚本语言。它是一个基于 Google Chrome 的 JavaScript 引擎构建的 Web 应用程序框架。它在 Mac OS、Windows 和 Linux 上的 Node.js 运行时中运行,而无需进行任何更改。该运行时使您能够在浏览器外的任何计算机上执行 JavaScript 代码。

2) Node.js 是否免费使用?

是的,Node.js 是在 MIT 许可下发布的,免费使用。

3) Node 是否是单线程应用程序?

是的,Node 是一个单线程应用程序,具有事件循环。

4) Node.js 的目的是什么?

Node.js 的主要目的有:

  • 实时 Web 应用程序
  • 网络应用程序
  • 分布式系统
  • 通用应用程序

5) Node.js 的优势有哪些?

以下是 Node.js 的主要优势:

  • Node.js 是异步和事件驱动的。所有 Node.js 库的 API 都是非阻塞的,其服务器在调用 API 后不会等待 API 返回数据。它在调用后转移到下一个 API,并且 Node.js 事件通知机制会从先前的 API 调用中响应服务器。
  • Node.js 非常快,因为它构建在 Google Chrome 的 V8 JavaScript 引擎上。它的库在代码执行方面非常快。
  • Node.js 是单线程的,但高度可扩展。
  • Node.js 不提供缓冲功能。其应用程序从不缓冲任何数据。它以分块方式输出数据。

6) 解释 Node.js Web 应用程序架构是什么?

Web 应用程序分为四个层:

  • 客户端层: 客户端层包含 Web 浏览器、移动浏览器或可以向 Web 服务器发出 HTTP 请求的应用程序。
  • 服务器层: 服务器层包含 Web 服务器,它可以拦截客户端发出的请求并将响应传递给客户端。
  • 业务层: 业务层包含应用程序服务器,Web 服务器使用它来执行所需的处理。此层通过数据库或某些外部程序与数据层进行交互。
  • 数据层: 数据层包含数据库或任何数据源。

1.png

7) 什么是 I/O?

I/O 代表输入和输出。它用于访问应用程序外部的任何内容。I/O 用于描述任何传输数据的程序、操作或设备,该数据可以从一个媒体传输到另一个媒体。这个媒体可以是物理设备、网络或系统内的文件。

一旦应用程序启动,I/O 就会加载到计算机内存中以运行程序。

8) Node.js 中有多少种类型的 API 函数?

Node.js 中有两种类型的 API 函数:

  • 异步、非阻塞函数
  • 同步、阻塞函数

9) 什么是 JavaScript 中的一等函数?

当函数被像任何其他变量一样对待时,那么这些函数被称为一等函数。除了 JavaScript,许多其他编程语言,如 Scala、Haskell 等,都遵循这种模式。一等函数可以作为参数传递给另一个函数(回调函数),或者函数可以返回另一个函数(高阶函数)。一些常用的高阶函数示例包括 map()filter()

10) JavaScript 和 Node.js 之间有什么区别?

JavaScript 和 Node.js 的区别

下表列出了 JavaScript 和 Node.js 之间的关键区别:

比较特点JavaScriptNode.js
类型JavaScript 是一种编程语言。更准确地说,它是一种用于在网站上编写脚本的脚本语言。Node.js 是 JavaScript 的解释器和运行时环境。
功能JavaScript 用于 Web 应用程序的任何客户端活动。Node.js 用于访问或执行任何操作系统的非阻塞操作。
运行引擎JavaScript 的运行引擎有 Spider monkey(Firefox)、JavaScript Core(Safari)、V8(Google Chrome)等。Node.js 的运行引擎是 V8(Google Chrome)。
浏览器兼容性JavaScript 只能在浏览器中运行。Node.js 代码可以在浏览器之外运行。
平台依赖性JavaScript 基本上用于客户端,并用于前端开发。Node.js 主要用于服务器端,并用于服务器端开发。
HTML 兼容性JavaScript 足够强大,可以添加 HTML 并与 DOM 交互。Node.js 不太兼容,无法添加 HTML 标签。
示例JavaScript 框架的一些示例包括 RamdaJS、TypedJS 等。Node.js 模块的一些示例包括 Lodash、express 等。我们必须从 npm 导入这些模块。
编写语言JavaScript 是 ECMA 脚本的升级版本,使用了 Chrome 的 V8 引擎,用 C++ 编写。Node.js 是用 C++ 编写的。
应用JavaScript 用于在浏览器中构建交互式网页和应用程序。Node.js 用于构建服务器端应用程序。
并行性JavaScript 是单线程的,不适合 CPU 密集型操作。Node.js 支持并行性,适用于高性能的分布式应用程序。
I/O 操作JavaScript 通常用于简单的浏览器 I/O 操作。Node.js 用于执行复杂的非阻塞 I/O 操作。

11) 解释 Node.js 的工作原理?

Node.js Web 服务器的工作流程通常如下所示的图表。让我们详细了解操作流程:

2.png

  • 根据上图,客户端发送请求与 Web 服务器进行交互,这些请求可以是非阻塞或阻塞的,用于查询数据、删除数据或更新数据。
  • Node.js 接收传入的请求并将其添加到事件队列。
  • 在此步骤之后,请求通过事件循环逐个传递。它检查请求是否足够简单,不需要任何外部资源。
  • 事件循环然后处理简单请求(非阻塞操作),如 I/O 轮询,并将响应返回给相应的客户端。
  • 从线程池中分配一个线程给单个复杂请求。此线程负责通过访问外部资源(如计算、数据库、文件系统等)来完成特定的阻塞请求。
  • 任务完成后,响应发送到事件循环,然后将响应发送回客户端。

12) 如何管理 Node.js 项目中的软件包?

我们可以通过使用几个软件包安装器及其相应的配置文件来管理 Node.js 项目中的软件包。大多数情况下使用 npm 或 yarn。npm 和 yarn 都提供几乎所有 JavaScript 库,具有控制环境特定配置的扩展功能。我们可以使用 package.json 和 package-lock.json 来维护项目中安装的库的版本。因此,在将应用程序移植到不同环境时不会出现问题。

13) 为什么 Node.js 是单线程的?

Node.js 是一个采用事件循环进行异步处理的单线程应用程序。在典型的 Web 负载下,使用单线程进行异步处理的最大优势是,相较于典型的基于线程的实现,可以实现更好的性能和可扩展性。

14) 什么是 Node.js 中的回调地狱?

回调地狱是一种现象,当 JavaScript 开发者试图依次执行多个异步操作时,会给开发者带来很多问题。当在处理结果之前必须完成某些外部活动时,函数被称为异步函数。之所以称之为异步,是因为在结果可用之前有不可预测的时间。这些函数需要一个回调函数来处理错误并处理结果。

示例:

getData(function(a){ 
  getMoreData(a, function(b){ 
    getMoreData(b, function(c){  
      getMoreData(c, function(d){  
        getMoreData(d, function(e){  
          // ...
        }); 
      }); 
    }); 
  }); 
});

15) Node.js 如何优于其他流行的框架?

根据以下标准,我们可以说 Node.js 优于其他流行的框架:

  • Node.js 通过其非阻塞 I/O 和事件驱动模型使开发变得简单。这种简单性导致更短的响应时间和并发处理,而其他框架需要开发者使用线程管理。
  • Node.js 运行在一个用 C++ 编写的 Chrome V8 引擎上,这极大地提高了性能,并且在不断改进中。
  • 使用 Node.js,我们可以在前端和后端开发中都使用 JavaScript,这将更加快速。
  • Node.js 提供了丰富的库,使我们不需要重新发明轮子。

16) Node.js 最常用于哪些类型的应用程序?

Node.js 最常用于以下类型的应用程序:

  • 物联网
  • 实时协作工具
  • 实时聊天
  • 复杂的单页应用程序(SPAs)
  • 流媒体应用程序
  • 微服务架构等。

17) Node.js 中常用的定时特性有哪些?

以下是 Node.js 中常用的定时特性列表:

  • setTimeout/clearTimeout: 用于在代码执行中实现延迟。
  • setInterval/clearInterval: 用于在应用程序中多次运行代码块。
  • setImmediate/clearImmediate: 用于在事件循环周期结束时设置代码的执行。
  • nextTick: 此定时特性将代码的执行设置为在下一个事件循环周期的开始时。

18) Node.js 中的 fork 是什么意思?

一般来说,fork 用于生成子进程。在 Node.js 中,它用于创建一个新的 V8 引擎实例,以运行多个工作进程来执行代码。

19) 在 Node.js 中,我们可以使用哪个工具来确保一致的代码风格?

ESLint 工具是我们可以与任何集成开发环境(IDE)一起使用的最佳工具之一,以确保一致的代码风格。它还有助于维护代码库。

20) 前端开发和后端开发之间的主要区别是什么?

下表列出了前端开发和后端开发之间的主要区别:

前端开发后端开发
应用程序的前端开发涉及应用程序的客户端部分。应用程序的后端开发涉及应用程序的服务器端部分。
正如名称所示,前端开发是 web 应用程序的用户可以看到和交互的部分。正如名称所示,后端开发包括在幕后发生的一切,用户无法看到和交互。
前端开发包括与 web 应用程序的视觉方面相关的一切内容。后端开发通常包括与数据库通信以为用户提供请求服务的 Web 服务器。
HTML、CSS、Bootstrap、jQuery、JavaScript、AngularJS 和 React.js 是必备的前端开发技术。Java、PHP、Python、C++、Node.js 等是后端开发所需的技术。
一些前端框架的示例包括 AngularJS、React.js、jQuery、Sass 等。一些后端框架的示例包括 Express、Django、Rails、Laravel、Spring 等。

21) 举个例子,演示在 Node.js 中如何使用 async await?

以下是使用 async-await 模式的示例:

function wait(timeout) { 
  return new Promise((resolve) => { 
    setTimeout(() => { 
      resolve(); 
    }, timeout); 
  }); 
} 

async function requestWithRetry(url) { 
  const MAX_RETRIES = 10; 
  for (let i = 0; i <= MAX_RETRIES; i++) { 
    try { 
      return await request(url); 
    } catch (err) { 
      const timeout = Math.pow(2, i); 
      console.log('Waiting', timeout, 'ms'); 
      await wait(timeout); 
      console.log('Retrying', err.message, i); 
    } 
  } 
}

22) Node.js 中的模块是什么?有哪些不同的模块在 Node.js 中使用?

在 Node.js 应用程序中,模块类似于 JavaScript 库,并包含一组函数。要在 Node.js 应用程序中包含模块,必须使用 require() 函数,括号中包含模块的名称。

Node.js 有几个模块,用于为 Web 应用程序提供所需的基本功能。以下是其中一些模块的列表:

核心模块描述
HTTPHTTP 模块包括创建 Node.js HTTP 服务器所需的类、方法和事件。
utilutil 模块包含在应用程序中需要的实用函数,对开发人员非常有用。
urlurl 模块用于包含 URL 解析方法。
fsfs 模块包括处理文件 I/O 操作所需的事件、类和方法。
streamstream 模块用于包含处理流式数据所需的方法。
query stringquery string 模块用于包含处理查询字符串的方法。
zlibzlib 模块用于包含压缩或解压在应用程序中使用的文件的方法。

23) 在 Node.js 中,什么是缓冲区(Buffers)?

通常情况下,缓冲区是由流使用的临时内存,用于保存数据,直到被消耗。缓冲区用于表示在 V8 JavaScript 引擎之外分配的固定大小的内存块。它无法调整大小。它类似于整数数组,每个整数代表一个字节的数据。它由 Node.js 的 Buffer 类实现。缓冲区还支持诸如 ASCII、utf-8 等的传统编码方式。

24) 什么是错误优先的回调(Error-First Callback)?

错误优先的回调用于传递错误和数据。如果出现问题,程序员必须检查第一个参数,因为它始终是一个错误参数。其他参数用于传递数据。

fs.readFile(filePath, function(err, data) {  
  if (err) { 
    // 处理错误
  } 
  // 使用 data 对象
}); 

25) 什么是异步 API(Asynchronous API)?

Node.js 库的所有 API 都是异步的,意味着非阻塞。Node.js 基于事件循环的方式工作,不会等待 API 返回数据。Node.js 服务器在调用 API 后立即进入下一个 API,而 Node.js 的事件机制会响应前一个 API 调用。

26) 如何避免回调地狱?

要避免回调地狱,可以选择以下任一选项:

  • 可以使用模块化。将回调拆分为独立的函数。
  • 可以使用Promise
  • 可以使用生成器和 Promise 的yield

27) Node.js 提供调试器吗?

是的,Node.js 提供了一个简单的基于 TCP 的协议和内置的调试客户端。要调试您的 JavaScript 文件,可以使用调试参数,然后跟上您要调试的 js 文件的名称。

语法:

node debug [script.js | -e "script" | <host>:<port>]

28) 什么是控制流函数?

控制流函数是通用的代码片段,在多个异步函数调用之间运行。

29) 控制流如何控制函数调用?

控制流执行以下任务:

  • 控制执行顺序
  • 收集数据
  • 限制并发
  • 调用程序的下一步

30) 在 Node 中可以访问 DOM 吗?

不可以,在 Node.js 环境中没有 DOM(Document Object Model),因为它是在浏览器中实现的。在 Node.js 中,您主要在服务器端执行 JavaScript 代码,处理 I/O 操作、文件系统等任务,而不涉及浏览器的页面渲染和 DOM 操作。

31) 使用事件循环可以异步执行哪些类型的任务?

Node.js 中的事件循环被设计用于高效地处理异步任务。以下是可以使用事件循环异步执行的一些任务类型:

  • I/O 操作:读写文件,进行网络请求,与数据库交互等。
  • 大量计算:执行消耗 CPU 的任务而不阻塞事件循环。
  • 基于计时器的操作:在特定时间间隔或延迟后调度任务执行。
  • 事件驱动编程:处理用户输入、传入请求和其他异步事件。
  • 并行性:同时执行多个任务以利用可用的 CPU 核心。

32) Node.js 中的 REPL 是什么?

REPL 表示“Read Eval Print Loop”(读取-求值-输出-循环)。它是一个交互式编程环境,允许您执行 JavaScript 代码并立即查看结果。它类似于一个命令行界面,用于尝试代码片段、测试想法和实验 JavaScript 特性。Node.js 内置了一个 REPL 环境,您可以通过运行没有任何参数的 node 命令来访问它。

33) 解释 Node REPL 中使用的术语的任务。

在 Node 的 REPL 环境中,术语具有以下任务:

  • Read(读取):读取用户输入并将其解析为 JavaScript 数据结构。
  • Eval(求值):对解析后的数据结构进行求值。
  • Print(输出):打印求值结果。
  • Loop(循环):循环回到读取下一个用户输入的步骤,创建一个连续的交互循环。

34) 是否可以使用 Node REPL 评估简单表达式?

是的,可以使用 Node 的 REPL 评估简单表达式。您可以输入表达式并立即看到其结果。例如:

> 2 + 3
5
> 'Hello, ' + 'world!'
'Hello, world!'
> Math.sqrt(16)
4

35) 在 REPL 中下划线变量的用途是什么?

在 Node.js 的 REPL 中,下划线变量(_)保存上次评估表达式的结果值。如果您想在后续计算或操作中重用结果,这可能会很有用。例如:

> 2 + 3
5
> _ * 2
10

36) Node.js 支持加密吗?

是的,Node.js 通过其内置的 crypto 模块支持加密功能。该模块提供了加密哈希函数、HMAC、加密、解密、数字签名等功能。它用于保护数据、生成安全令牌以及在网络上实现安全通信。

37) 什么是 npm?它的主要功能是什么?

npm 表示 Node Package Manager(Node 包管理器)。以下是 npm 的两个主要功能:

  • 包管理:npm 允许开发人员轻松安装、管理和更新扩展 Node.js 应用程序功能的第三方包/模块。
  • 依赖管理:npm 处理项目的依赖关系,确保所需的包被安装和维护。
  • 版本管理:npm 允许您指定项目所需的包的版本,确保在不同环境中保持一致性。
  • 脚本执行:npm 允许您使用 package.json 文件定义和运行脚本。
  • 发布包:开发人员可以将自己的包发布到 npm 注册表供他人使用。

这些只是 npm 的核心功能之一。

38) 有哪些工具可以用于确保 Node.js 中的一致的编码风格?

为了确保 Node.js 项目中的一致的编码风格,可以使用代码 linting 和格式化工具。一些流行的工具包括:

  • JSLint
  • JSHint
  • ESLint(最常用)
  • Prettier(用于代码格式化)

这些工具有助于开发人员遵循编码标准,捕获错误,并在整个代码库中保持一致的风格。

39) 操作错误和程序员错误之间有什么区别?

操作错误是在运行时由外部环境或环境造成的问题。这些错误与应用程序的代码不直接相关,但可能影响其运行。示例包括网络故障、数据库不可用和服务器崩溃。

另一方面,程序员错误是开发人员所犯的编码错误。这些错误会导致应用程序出现错误和不正确的行为。示例包括语法错误、逻辑错误和错误的变量赋值。

40) 全局安装依赖和本地安装依赖之间有什么区别?

在 Node.js 中,全局安装和本地安装依赖具有不同的目的:

  • 全局安装: 当您全局安装一个包时,它会在系统范围内安装,并且可以从终端的任何位置访问。全局包通常是您希望在不同项目中使用的命令行工具。您可以在 npm install 命令中使用 -g 标志进行全局安装。
  • 本地安装: 当您本地安装一个包时,它特定于特定项目,并存储在该项目的 node_modules 目录中。本地包是您的项目需要正常运行的依赖项。对于本地安装,不使用 -g 标志。

本地安装是管理项目依赖项的最常见方式,因为它允许版本隔离和在不同项目之间保持可重现性。

41) Node.js 中的缓冲区类有什么用途?

Node.js 提供了缓冲区(Buffer)类,用于存储类似于整数数组的原始数据,但对应于 V8 堆外的原始内存分配。缓冲区是一个全局类,可以在应用程序中直接访问,无需导入缓冲区模块。缓冲区类的作用是因为纯 JavaScript 不兼容二进制数据。因此,在处理 TCP 流或文件系统时,必须处理八位字节流。

42) 在 Node.js 中,assert 的作用是什么?

Node.js 的 assert 模块是一种编写测试的方法。除非测试失败,否则在运行测试时不提供任何反馈。assert 模块提供了一组简单的断言测试,用于测试不变式。该模块主要用于 Node.js 的内部使用,但可以通过 require ('assert') 在应用程序代码中使用。示例如下:

var assert = require('assert');

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

var expected = add(1, 2);
assert(expected === 3, 'one plus two is three');

43) 在 Node.js 中,什么是流(Streams)?

流是一种对象,用于从源读取数据并将数据写入目标。在 Node.js 中有四种类型的流:

  • Readable(可读流): 用于读取操作。
  • Writable(可写流): 用于写操作。
  • Duplex(双工流): 可用于读写操作。
  • Transform(转换流): 是一种特殊类型的双工流,其输出根据输入进行计算。

44) 什么是 Node.js 中的事件驱动编程?

在 Node.js 中,事件驱动编程意味着一旦 Node 启动其服务器,它会初始化其变量,声明函数,然后等待事件发生。这是 Node.js 相对于其他类似技术非常快速的一个原因。

45) 在 Node.js 中,事件(Events)和回调(Callbacks)之间有什么区别?

尽管事件和回调看起来相似,但区别在于回调函数在异步函数返回其结果时被调用,而事件处理基于观察者模式运作。每当事件被触发时,其监听函数开始执行。Node.js 通过 events 模块和 EventEmitter 类提供了多个内置事件和事件监听器。

46) 什么是 Punycode 在 Node.js 中的作用?

在 Node.js 中,Punycode 用于将一种类型的字符串转换为另一种类型。Punycode 是一种编码语法,用于将 Unicode(UTF-8)字符串转换为基本的 ASCII 字符串。由于主机名只能理解 ASCII 字符,因此从 Node.js 版本 0.6.2 开始,Punycode 被捆绑到默认的 Node 包中。对于其他 Node.js 版本,您可以使用 npm 安装 Punycode 模块并通过 require('punycode') 来访问它。

47) Node.js 中的 TTY 模块包含什么?

Node.js 的 TTY 模块包含 tty.ReadStream 和 tty.WriteStream 类。在大多数情况下,没有必要直接使用此模块。您可以使用 require('tty') 来访问这个模块。

48) Angular 和 Node.js 之间的关键区别是什么?

Angular 和 Node.js 之间的关键区别如下:

AngularNode.js
Angular 是用于开发动态 Web 应用程序的结构化前端开发框架。Node.js 是用于编写 JavaScript 语言的应用程序的跨平台、运行时、服务器端环境。
Angular 完全使用 TypeScript 语言编写。Node.js 使用 C、C++ 和 JavaScript 语言编写。
Angular 用于构建单页、客户端端 Web 应用程序。Node.js 用于构建快速、可扩展的客户端和服务器端网络应用程序。
Angular 易于使用。开发人员需要将 Angular 文件添加到其应用程序中才能使用。Node.js 略微复杂。开发人员需要在计算机系统上安装 Node.js。
Angular 将 Web 应用程序拆分为 MVC 组件。其中模型和视图比其他 JavaScript 客户端框架中的简单得多。Node.js 生成数据库查询并利用 JavaScript 的事件驱动特性支持非阻塞操作,使平台更高效。
Angular 基于模型-视图-控制器(MVC)设计模式,并完全遵循该模式。Node.js 是单线程的。这意味着 Web 请求和处理在同一线程上运行。
Angular 是一个 Web 框架。Node.js 提供不同的 Web 框架,如 Socket.io、Hapi.js、Meteor.js、Express.js 和 Sails.js 等。
Angular 适用于创建高度活跃和交互式的 Web 应用程序。Node.js 适用于开发小型项目。
Angular 需要深入了解原型、作用域和其他各种 JavaScript 方面。Node.js 可以使开发人员在客户端和服务器端使用 JavaScript,因此他们可以专注于学习一种语言。

49) 操作错误和程序员错误之间的主要区别是什么?

操作错误和程序员错误之间最重要的区别在于操作错误不是错误,而是系统的问题,例如请求超时或硬件故障。另一方面,程序员错误是应用程序中的实际错误。

50) 你理解 Node.js 中的 EventEmitter 是什么?

在 Node.js 中,EventEmitter 是一个类,包含所有能够触发事件的对象。通过使用 eventEmitter.on() 函数,可以将命名事件附加到对象,并在对象触发事件时同步调用附加的函数。

示例:

const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('an event occurred!');
});
myEmitter.emit('event');

51) 在 Node.js 中,readFile 和 createReadStream 之间有什么区别?

在 Node.js 中,有两种方法可以读取和执行文件:readFile 和 createReadStream。

  • readFile() 过程是一个完全缓冲的过程,仅当完整文件被推入缓冲区并读取时才返回响应。这个过程称为内存密集型过程,在处理大文件时,处理可能非常缓慢。
  • 另一方面,createReadStream() 是一个部分缓冲的过程,将整个过程视为事件序列。整个文件被分成块,然后逐个处理并作为响应发送回来。完成此步骤后,它们最终从缓冲区中移除。与 readFile 过程不同,createReadStream 过程适用于大文件的处理。

52) 在 Node.js 中,Punycode 的概念是什么?

在 Node.js 中,Punycode 的概念用于将一种类型的字符串转换为另一种类型。Punycode 是一种编码语法,用于将 Unicode(UTF-8)字符串转换为基本的 ASCII 字符串。由于主机名只能理解 ASCII 字符,因此从 Node.js 版本 0.6.2 开始,Punycode 被捆绑到默认的 Node 包中。对于其他 Node.js 版本,您可以使用 npm 安装 Punycode 模块并通过 require('punycode') 来访问它。

53) 如何通过集群(clustering)来提高 Node.js 的性能?

由于 Node.js 应用程序在单个处理器上运行,因此默认情况下不能充分利用多核系统。集群用于解决此问题。集群模式用于启动多个 Node.js 进程,从而具有多个事件循环实例。当我们在 Node.js 应用程序中使用集群时,它会创建多个 Node.js 进程。但还有一个名为集群管理器的父进程,负责监视各个应用程序实例的健康情况。

54) Node.js 中的线程池是什么?哪个库处理它?

在 Node.js 中,线程池由 libuv 库处理。libuv 库是一个多平台的 C 库,支持异步 I/O 操作,如文件系统、网络和并发。

3.png

标签: Nodejs, Nodejs安装教程, Nodejs教程, node, nodejs入门, nodejs入门教程, nodejs进阶, nodejs学习教程, nodejs开发, nodejs指南, nodejs学习指南, nodejs环境配置, nodejs框架