前端与Node.js
1. 前端(浏览器环境)
前端通常指的是在浏览器中运行的JavaScript代码。它主要负责用户界面的渲染、交互逻辑和与后端API的通信。
- 核心运行环境:浏览器(如Chrome、Firefox、Safari等)
- 核心引擎:V8(Chrome)、SpiderMonkey(Firefox)、JavaScriptCore(Safari)等
- 宿主对象(Host Objects) :
window:全局对象,提供浏览器窗口相关的APIdocument:DOM操作接口,用于构建和操作网页结构navigator:获取浏览器信息localStorage、sessionStorage:浏览器本地存储fetch、XMLHttpRequest:网络请求API
- 特点 :
- 运行在客户端,直接面向用户
- 受同源策略(Same-Origin Policy)限制
- 需要考虑浏览器兼容性
- 代码通常通过HTML文件加载
2. Node.js(服务器端环境)
Node.js是一个基于Chrome V8引擎 构建的JavaScript运行时,它允许JavaScript在服务器端运行。
- 核心运行环境:Node.js 运行时
- 核心引擎:V8(与Chrome浏览器相同)
- 宿主对象 :
global:全局对象(相当于浏览器中的window)process:提供进程信息和控制Buffer:处理二进制数据require、module.exports:模块系统(CommonJS)fs、path、http等:Node.js内置模块,提供文件系统、网络、路径处理等能力
- 特点 :
- 运行在服务器端,处理业务逻辑、数据存储、API服务等
- 无浏览器安全限制(如同源策略)
- 可直接访问操作系统资源(文件系统、网络等)
- 通常通过命令行启动(
node app.js)
🌟 关键区别总结
| 维度 | 前端(浏览器) | Node.js(服务器) |
|---|---|---|
| 运行环境 | 浏览器 | Node.js 运行时 |
| 全局对象 | window |
global |
| 模块系统 | ES Modules(主流)、CommonJS(打包工具中) | CommonJS(原生)、ES Modules(支持) |
| 网络请求 | fetch、XMLHttpRequest |
http、https 模块,或第三方库 |
| 文件系统 | 无法直接访问(受限) | 可通过 fs 模块直接读写 |
| 主要用途 | UI渲染、用户交互 | 后端服务、API、脚本工具 |
为什么Lodash、Axios能在前端和Node.js中"通用"?
这是本文的核心问题。答案是:它们是"环境无关"的JavaScript库,通过巧妙的设计实现了跨平台兼容。
我们以 Lodash 和 Axios 为例,深入分析其原理。
🔹 案例1:Lodash ------ 纯函数工具库的"环境无关性"
Lodash 是一个提供大量实用函数的JavaScript工具库,如 _.debounce、_.cloneDeep、_.get 等。
为什么它能跨平台?
-
不依赖特定宿主对象
- Lodash 的函数大多是纯函数(Pure Functions) ,只依赖输入参数,不依赖
window、document或fs等环境特定对象。 - 例如
_.cloneDeep(obj)只操作JavaScript原生对象,不涉及DOM或文件系统。
- Lodash 的函数大多是纯函数(Pure Functions) ,只依赖输入参数,不依赖
-
模块化设计
-
Lodash 支持多种模块格式:CommonJS、ES Modules、UMD(Universal Module Definition)。
-
UMD 是关键!它是一种兼容多种环境的模块包装方式:
(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD define([], factory); } else if (typeof module === 'object' && module.exports) { // CommonJS (Node.js) module.exports = factory(); } else { // 浏览器全局变量 root._ = factory(); } }(typeof self !== 'undefined' ? self : this, function () { // Lodash 核心实现 return _; })); -
这段代码会自动检测当前环境,选择合适的模块导出方式。
-
-
构建工具支持
- 在前端项目中,Webpack、Vite等工具会将Lodash打包进最终的bundle.js。
- 在Node.js中,直接通过
require('lodash')加载。
✅ 结论 :Lodash 之所以通用,是因为它不依赖环境API + 使用UMD兼容多模块系统。
🔹 案例2:Axios ------ HTTP客户端的"适配器模式"跨平台方案
Axios 是一个基于Promise的HTTP客户端,用于发送网络请求。
它比Lodash更复杂,因为网络请求在浏览器和Node.js中实现方式完全不同:
- 浏览器 :使用
XMLHttpRequest或fetch - Node.js :使用
http/https模块
那么,Axios是如何做到"一套API,两端运行"的?
核心机制:适配器模式(Adapter Pattern)
Axios 内部采用了适配器模式,根据运行环境自动选择合适的HTTP实现。
-
默认适配器选择逻辑:
// Axios 源码简化示意 function getDefaultAdapter() { let adapter; if (typeof XMLHttpRequest !== 'undefined') { // 浏览器环境 adapter = require('./adapters/xhr'); } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { // Node.js 环境 adapter = require('./adapters/http'); } return adapter; } -
浏览器适配器 :使用
XMLHttpRequest发送请求 -
Node.js适配器 :使用
http模块发送请求 -
统一的API层:
-
无论底层是XHR还是http模块,Axios向上暴露的API完全一致:
axios.get('/api/users') .then(response => console.log(response.data));
-
-
配置化与可替换:
- Axios 允许开发者手动指定适配器,甚至使用自定义适配器。
✅ 结论 :Axios 通过适配器模式,在不同环境中使用不同的底层实现,但对外提供统一的API,从而实现跨平台。
如何判断一个库是否"跨平台通用"?
你可以通过以下几点快速判断:
| 判断标准 | 跨平台库(如Lodash) | 非跨平台库(如jQuery) |
|---|---|---|
| 是否依赖DOM/BOM API | ❌ 不依赖 | ✅ 依赖 document、window |
| 是否依赖Node.js内置模块 | ❌ 不依赖 fs、path |
✅ 如 fs-extra 只能在Node.js用 |
| 模块格式 | 支持 UMD 或 ESM + CJS | 仅支持 CJS 或仅浏览器 |
| 构建方式 | 可通过CDN引入或npm安装 | 通常只能npm安装用于Node.js |
✅ 推荐的跨平台库:Lodash、Axios、Moment.js(已归档)、Day.js、Zod、Yup等
❌ 仅Node.js库:
express、fs-extra、child_process❌ 仅浏览器库:
jquery、three.js(虽可Node运行,但无意义)
| 特性 | CJS (CommonJS) | ESM (ES Modules) | UMD (Universal) |
|---|---|---|---|
| 语法 | require() / module.exports |
import / export |
兼容多种 |
| 加载方式 | 同步 | 异步(支持动态导入) | 取决于环境 |
| 原生支持 | Node.js ✅ | 浏览器 ✅ + Node.js ✅ Node.js 从 v12+ 开始支持 (需 .mjs 扩展名或 package.json 中设置 "type": "module") |
❌(需构建) |
| Tree Shaking | ❌ 不支持 | ✅ 支持 | ❌ 通常不支持 |
| 适用环境 | Node.js | 前端 + 现代 Node.js | 所有环境(兼容性最强) |
| 典型使用 | const _ = require('lodash') |
import _ from 'lodash' |
CDN 引入或老项目兼容 |
实际开发中的常见组合"ESM + CJS"
指的是一个 npm 包同时提供两种格式的构建版本:
dist/index.esm.js→ 供前端构建工具(Vite/Webpack)使用 ESMdist/index.cjs.js→ 供 Node.js 直接require使用
例如 package.json 中可能这样配置:
{
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js"
}
}
}
前端工程项目打包
前端打包就是在 Node.js 环境下进行的。
- 谁在干活? → Node.js 进程中的打包工具(Webpack/Vite)
- 输入是什么? → 你的源码 +
node_modules中的库 - 输出是什么? → 适合浏览器加载的
.html、.js、.css、.png等静态文件 - 为什么必须用 Node.js? → 因为需要文件系统、模块解析、高性能计算等能力,浏览器做不到
1. 代码是如何处理的?
-
✅ 流程 :
入口文件 → 递归解析
import→ 收集所有模块(含node_modules)→ 转换(Babel/TS)→ 优化 → 输出dist/静态文件。 -
✅ 关键机制:
- Tree Shaking:仅打包实际使用的代码(需 ESM 模块)。
- Scope Hoisting:合并模块,减少闭包开销。
- Code Splitting :自动分包(如
vendor.js),提升缓存利用率。
2. 变量(如 process.env)是如何处理的?
-
✅ 机制 :构建时静态替换(不是运行时)。
-
✅ 流程:
- 打包工具读取 Node.js 环境变量(或
.env文件) - 在配置中定义替换规则(如 Vite 的
define,Webpack 的DefinePlugin) - 源码中的
process.env.NODE_ENV被替换为字符串(如'production') - 无用代码被 Tree Shaking 删除
- 打包工具读取 Node.js 环境变量(或
-
⚠️ 注意:
- 浏览器中没有
process,这是"伪变量"。 - 敏感信息(如密钥)不应暴露在前端变量中。
- 浏览器中没有
3. 开发依赖(devDependencies)如何处理?
- ❌ 不会被打包进前端文件。
- ✅ 用途 :仅在 Node.js 构建时使用,如:
- 打包工具(Vite、Webpack)
- 编译器(TypeScript、Babel)
- 代码检查(ESLint、Prettier)
- ✅ 类比:厨师的刀具,不端上餐桌。
4. 生产依赖(dependencies)如何处理?
- ✅ 会被打包,但仅限"被引用"的部分。
- ✅ Tree Shaking 生效前提 :
- 使用 ESM 语法:
import { func } from 'lib' - 库支持 ESM 格式(
package.json有module字段) - 生产模式构建(
mode: 'production')
- 使用 ESM 语法:
- ❌ 不会打包 :
- 未使用的库或函数(如只用
lodash.debounce,其他函数被摇掉) - 全量导入(
import _ from 'lodash')会打包整个库(应避免)
- 未使用的库或函数(如只用
✅ 一句话总结
打包是在 Node.js 中运行工具 ,将代码按需打包,通过 静态替换注入变量 ,开发依赖不进前端 ,生产依赖按需打包,最终生成轻量、高效的浏览器可用文件。