1. webpack
一文带你梳理Webpack面试题:
1.1. webpack
Webpack 是一种用于构建 JavaScript 应用程序的静态模块打包器,它能够以一种相对一致且开放的处理方式,加载应用中的所有资源文件(图片、CSS、视频、字体文件等),并将其合并打包成浏览器兼容的 Web 资源文件。
- 模块的打包:通过打包整合不同的模块文件保证各模块之间的引用和执行
- 代码编译:通过丰富的
loader
可以将不同格式文件如.sass/.vue/.jsx
转译为浏览器可以执行的文件 - 扩展功能:通过社区丰富的
plugin
可以实现多种强大的功能,例如代码分割、代码混淆、代码压缩、按需加载.....等等
1.2. Loader 和 Plugin有什么区别(高频)
什么是loader
loader是文件加载器,能够加载资源文件如css\img等,并对这些文件进行一些处理,诸如编译、压缩等。最终一起打包到指定的文件中
-
处理一个文件可以使用多个loader,loader的执行顺序和配置中的顺序是相反的,即最后一个loader最先执行,第一个loader最后执行
-
第一个执行的loader接收源文件内容作为参数,其它loader接收前一个执行的loader的返回值作为参数,最后执行的loader会返回此模块的JavaScript源码
常用的loader
babel-loader
:将es6转译为es5image-webpack-loader
: 加载并压缩图片资源sass-loader
: 将SCSS/SASS代码转换为CSScss-loader
: 加载CSS代码 支持模块化、压缩、文件导入等功能特性style-loader
: 把CSS代码注入到js中,通过DOM
操作去加载CSS代码 (style-loader,cssloader,sassloader)thread-loader
: 多线程打包,加快打包速度- postcss-loader + autoprefixer:处理css时自动加前缀,( 决定添加哪些浏览器前缀到css中)
2)、什么是plugin
在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。
使用场景
- 压缩输出的 JavaScript 文件(如使用 TerserPlugin)。
- 提取 CSS 到单独的文件(如使用 MiniCssExtractPlugin)。
- 生成 HTML 文件并自动注入打包后的资源(如使用 HtmlWebpackPlugin)。
- 清理输出目录(如使用 CleanWebpackPlugin)。
ignore-plugin
: 忽略指定的文件,可以加快构建速度uglifyjs-webpack-plugin
: 压缩js代码copy-webpack-plugin
: 在构建的时候,复制静态资源到打包目录。
3)、loader和plugin的区别
对于loader,它是一个转换器,,本质上是一个函数,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程 . Loader主要负责将代码转译为webpack 可以处理的JavaScript代码。 配置Loader通过在 modules.rules
中以数组的形式配置
plugin是一个扩展器,它丰富了webpack本身的能力,针对webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务。Plugin通过plugins
以数组的形式配置
1.3. SourceMap 原理(高频)
配置 devtool: 'cheap-module-eval-source-map'
- 开发环境使用:cheap-module-eval-source-map
-
- cheap: 表示不包含列信息,只包含行信息,这使得构建速度更快,但定位错误时只能到行级别,不能到列级别。
- module: 表示包含loader的sourcemap,这对于像Babel这样的工具转换代码时非常重要,因为它允许源文件被正确地映射到转换后的代码。
- eval: 每个模块都被eval执行,并且sourcemap作为eval的一个dataURL内联在代码中,这允许源代码在浏览器开发者工具中被正确显示。
- source-map: 生成sourcemap文件,使得开发时能够调试到原始代码,而不是编译后的代码。
- 生产环境使用:hidden-source-map (会在外部生成sourcemap文件,但是在目标文件里没有建立关联,不能提示错误代码的准确原始位置 )
(有很多种配置方式)后,在编译过程中,会生成一个 .map
文件,一般用于代码调试和错误监控。
- 包含了源代码、编译后的代码、以及它们之间的映射关系。
- 编译后的文件通常会在文件末尾添加一个注释,指向 SourceMap文件的位置。
-
// # sourceMappingURL=example.js.map
- 当在浏览器开发者工具调试时,浏览器会读取这行注释并加载对应的 SourceMap 文件
报错时,点击跳转。即使运行的是编译后的代码,也能够追溯到原始源代码的具体位置,而不是处理经过转换或压缩后的代码,从而提高了调试效率。
1.4. Webpack 热替换 HMR 原理(高频)
没热替换之前,每次改代码之后要重新刷新页面才能看到新的页面。
热替换可以让我们不用刷新浏览器,通过增删改模块来实时更新页面。HMR 的实现依赖于 Webpack Dev Server 启动一个 WebSocket 服务器,跟浏览器进行全双工通信。
- 服务器和客户端的通信:Webpack Dev Server 在服务端启动一个 WebSocket 服务器,浏览器通过 WebSocket 连接此服务器。
- 监听文件变化: Webpack 使用
watch
模式监听项目中的文件变化。一旦文件发生变化,Webpack 就会重新编译改变的模块,并生成更新后的模块代码及一个更新清单(Mainfest)
javascript 代码解读复制代码watch: true,
// 精细配置
watchOptions: {
//不监听的文件或者文件夹 忽略一些大型的不经常变化的文件可以提高构建速度
ignored: /node_modules/,
//监听到变化会等多少时间再执行
aggregateTimeout: 300,
//判断文件是否发生变化是通过不断轮询指定文件有没有变化实现的
poll: 1000
}
3. 通知客户端:更新清单会通过已建立的 WebSocket 连接发送给客户端(浏览器) 。
4. 热替换:浏览器接收到更新信息后,通过 HMR API
获取更新的模块,并替换旧的模块。
-
- 如果模块热替换失败,会触发整页刷新。
Webpack 的 HMR 功能极大地提高了开发效率,使得开发者可以即时看到代码变更的效果,而无需进行完整的页面刷新。这不仅加快了开发流程,也使得调试更加方便。
用法
- 通过配置项
devServer.hot: true
,启用 HMR 功能。 - 或者使用
HotModuleReplacementPlugin
HMR 插件。
1.5. Webpack 性能优化方法(高频)
性能优化方案总的来说,无非就是追求几点:减少打包体积、多线程并行打包、利用缓存提效
1.5.1. 减小打包体积
1、压缩资源
- JavaScript:使用 TerserPlugin 等工具压缩 JS 代码。(去除注释、未使用内容、空格、)
- CSS:使用 css-minimizer-webpack-plugin 等工具压缩 CSS 代码。
- HTML:使用 html-webpack-plugin 时配置压缩选项。
- 图片:使用 image-webpack-loader 等工具减小图片体积。
2、引入外部库的 CDN
- 对于 React、Vue、Lodash 这种库不会经常变化,所以就没必要打包。这种方式可以减少打包体积,并利用 CDN 的缓存优势加快页面加载速度。
lua 代码解读复制代码
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
// 配置 externals,说明哪些模块是外部引入的,不打包到 bundle 中
externals: {
react: 'React',
'react-dom': 'ReactDOM',
},
};
3、 代码分割
- 用
SplitChunks
自动提取公共模块和第三方库,可以减少代码重复和减少编译时间。 - 提取公共模块:将多个 chunk 共享的模块提取到一个单独 chunk 中,减少代码重复和生成文件的大小。
- 分割大模块:将大的模块拆分成更小的块,提高加载速度和并行下载的效率。
- 按需加载:创建按需加载的代码块,提高应用的启动速度。
1.5.2. 多进程打包
使用 thread-loader
或 parallel-webpack
可以将打包任务分配到多个进程,提高打包速度。
1.5.3. 利用缓存提效
- 使用 babel-loader 的
cacheDirectory
选项开启缓存,减少重复编译时间。
yaml 代码解读复制代码{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
- 开启持久化缓存:提高二次构建速度。
构建结果持久化缓存到本地的磁盘,二次构建(非 watch 模块)直接利用磁盘缓存的结果从而跳过构建过程当中的 resolve、build 等耗时的流程,从而大大提升编译构建的效率。
Webpack 5 引入了持久化缓存,通过配置cache.type
属性缓存生成的 chunk
css 代码解读复制代码cache: {
type: 'filesystem', // 使用文件系统级别的缓存
}
1.6. 按需加载如何实现,原理是什么(高频)
按需加载是基于动态导入和代码分割实现的,允许应用将代码分割成多个 chunk,并在运行时按需动态加载这些chunk。按需加载可以减少应用的初始加载时间,提升用户体验。具体实现方式如下:
- 使用
import()
动态导入模块
-
import
将模块内容转换为 ESM 标准的数据结构后,通过 Promise 形式返回,加载完成后获取 Module 并在then
中注册回调函数。
- Webpack 自动代码分割
-
- 当 webpack 检测到
import()
存在时,将会自动进行代码分割,将动态import
的模块打到一个新 bundle 中 - 此时这部分代码不包含在初始包中,而是在需要的时候动态加载。
- 当 webpack 检测到
- 网络请求
-
- 当
import()
被执行时,浏览器会发起一个网络请求来加载对应的 chunk 文件。 - 加载完成后,模块中的代码就可以被执行了。
- 当
1.7. webpack手写一个plugin 和loader
complier
是 webpack
构建启动时产生的,只有一个,它可以访问构建的各种配置等等。 compilation
是对资源的一次构建,可以有多个,它可以访问构建过程中的资源。下面以 complier
为例,介绍钩子事件时如何注册的。
监听一些具有特定意义的hook
来影响构建
compiler.hooks.compilation
:webpack刚启动完并创建compilation
对象后触发compiler.hooks.make
:webpack开始构建时触发compiler.hooks.done
:webpack 完成编译时触发,此时可以通过stats
对象得知编译过程中的各种信息
javascript 代码解读复制代码class DemoWebpackPlugin {
constructor () {
console.log('plugin init')
}
// compiler是webpack实例
apply (compiler) {
// 一个新的编译(compilation)创建之后(同步)
// compilation代表每一次执行打包,独立的编译
compiler.hooks.compile.tap('DemoWebpackPlugin', compilation => {
console.log(compilation)
})
// emit: 生成资源到 output 目录之前(异步)
compiler.hooks.emit.tapAsync('DemoWebpackPlugin', (compilation, fn) => {
console.log(compilation)
// 生成一个md文件
compilation.assets['index.md'] = {
// 文件内容
source: function () {
return 'this is a demo for plugin'
},
// 文件尺寸
size: function () {
return 25
}
}
fn()
})
}
}
module.exports = DemoWebpackPlugin
// 使用
const DemoWebpackPlugin = require("./path/to/DemoWebpackPlugin");
module.exports = {
// ...其他配置...
plugins: [
new DemoWebpackPlugin(),
// ...其他插件...
],
};
loader
自定义 loader 本质上是一个函数,该函数接收源码作为输入,对源码进行处理后返回新的源码。
- this.resourcePath 和 this.resourceQuery: 这两个属性提供了当前正在处理的资源文件的路径和查询字符串。
javascript 代码解读复制代码export function myloader(resource,sourcemap,data) {
// 使用此 loader 处理的文件的路径
const filePath = this.resourcePath;
// 定义一个正则表达式匹配特定的方法调用,比如 targetMethod()
const methodCallRegex = /targetMethod()/g;
// 替换匹配到的方法调用
const modifiedSource = resource.replace(methodCallRegex, function (match) {
// 插入上报函数调用,传入文件路径
return `reportFunction('${filePath}'); ${match}`;
});
return modifiedSource;
}
//
异步loader
// 这里的this.async(),也就是让webpack知道这个loader是异步运行,
//返回的是和同步使用时一致的this.callback
const loaderUtils = require('loader-utils')
module.exports = function (source) {
const options = loaderUtils.getOptions(this)
const asyncfunc = this.async()
setTimeout(() => {
source += '走上人生颠覆'
asyncfunc(null, res)
}, 200)
}
// 在你的 webpack.config.js 文件中,添加一个 module.rules 条目,
// 以确定哪些文件应该通过你的 loader 处理:
module.exports = {
module: {
rules: [
{
test: /.js$/, // 匹配 JavaScript 文件
use: [
{
loader: "path/to/your/report-loader.js", // 使用自定义 loader 的路径
},
],
},
],
},
};
1.8. 为什么 Vite 速度比 Webpack 快?
一、开发模式的差异
- 当使用 Webpack 时,所有的模块都需要在运行前进行打包 , 会增加启动时间和构建时间。
- Vite 则是直接启动,它会在请求模块时再进行实时编译, Vite利用浏览器原生的ES模块支持来处理模块导入。在开发环境中,Vite不需要将所有模块捆绑在一起,而是直接通过HTTP请求加载所需的模块。下次再请求同一模块时,Vite可以直接返回缓存的编译结果。这种按需加载大大减少了初始构建时间。特别是在大型项目中,文件数量众多,Vite 的优势更为明显。
二、底层语言的差异
- Webpack 是基于 Node.js 构建的,毫秒级别的
- Vite 则是基于 esbuild 进行预构建依赖。esbuild 是采用 Go 语言编写的,纳秒级别的
因此,Vite 在打包速度上相比Webpack 有 10-100
倍的提升。
三、热更新的处理
- Webpack 中,当一个模块或其依赖的模块内容改变时,需要重新编译这些模块。
- Vite 中,当某个模块内容改变时,只需要让浏览器重新请求该模块即可,这大大减少了热更新的时间。
特性 | Webpack | Vite |
---|---|---|
开发环境构建 | 全量打包(Bundle-Based) | 基于原生 ES Module(无打包) |
生产环境构建 | 使用 Webpack 自身打包引擎 | 使用 Rollup(默认)或自定义配置 |
打包产物 | 包含大量 Runtime 胶水代码(如 __webpack_require__ ) | 模块被包裹在函数闭包 |
模块解析 | 运行时解析依赖(动态分析) | 预构建依赖(开发阶段) + 按需编译(生产阶段) |
启动速度 | 较慢(需构建完整 Bundle) | 极快(开发环境无需打包) |
适用场景 | 传统大型项目、需要深度自定义配置 | 现代浏览器项目、追求极速开发体验 |
Tree-Shaking | 支持,但依赖配置(如 optimization.usedExports ) | 默认更高效(Rollup 的静态分析更精准) |
vite 使用Rollup,而不是webpack或者esbuild作为打包工具的原因:
- Rollup使用新的ESM,而Webpack用的是旧的CommonJS。
- Rollup 的打包文件体积很小。
- Rollup支持相对路径,webpack需要使用path模块。
- 尽管esbuild速度更快,但Vite采用了Rollup灵活的插件API和基础建设,这对Vite在生态中的成功起到了重要作用。
2. tree shaking
- Tree-Shaking 它会在运行过程中静态分析模块之间的导入导出,确定 ESM 模块中哪些导出值未曾被其它模块使用,并将其删除,以此实现打包产物的优化。
配置方式:
- 使用 ESM 规范编写模块代码
- 配置
optimization.usedExports
为true
,启动标记功能 - 启动代码优化功能,可以通过如下方式实现:
-
- 配置
mode = production
- 配置
optimization.minimize = true
- 提供
optimization.minimizer
数组
- 配置
例如:
java 代码解读复制代码// webpack.config.js
module.exports = {
entry: "./src/index",
mode: "production",
devtool: false,
optimization: {
usedExports: true,
},
};
对于使用了babel-loader
loader或者根据对代码进行转译的时候,注意应该关闭对于导入/导出语句的转译,因为这会影响到后续的 tree shaking 比如应该将 babel-loader
的 babel-preset-env
的modules
配置为false
Tree shaking的工作流程可以分为
1.标记哪些导出值没有被使用; 2. 使用Terser将这些没用到的导出语句删除
标记的流程如下:
- make阶段:收集模块导出变量并记录到模块依赖关系图中
- seal阶段:遍历模块依赖关系图并标记那些导出变量有没有被使用
- 构建阶段:利用Terser将没有被用到的导出语句删除
3. babel
Babel 是 JS 的转换器,主要用来将新 JS 语法转为向后兼容版本
原理很简单,就三部分:解析/转换/生成。
- 解析:通过词法分析把代码变 token、语法分析把 token 解析成抽象语法树(AST)
- 转换:接收到 AST 并遍历,对树上节点增删改实现兼容性转换
- 生成:被转换的新 AST 被生成为新 JS 代码字符串
4. 微前端
随着公司业务的不断发展,应用开始变得庞大臃肿,逐渐成为一个巨石应用,难以维护不说,每次开发、上线新需求时还需要花费不少的时间来构建项目,对开发人员的开发效率和体验都造成了不好的影响。因此将一个巨石应用拆分为多个子应用势在必行。
就是将一个单体应用,按照一定的规则拆分为一组服务。这些服务,各自拥有自己的仓库,可以独立开发、独立部署,有独立的边界,可以由不同的团队来管理,甚至可以使用不同的编程语言来编写。但对前端来说,仍然是一个完整的服务。
single-spa
single-spa 方案中,应用被分为两类:基座应用和子应用。其中,子应用就是文章上面描述的需要聚合的子应用;而基座应用,是另外的一个单独的应用,用于聚合子应用。
和单页应用的实现原理类似,single-spa 会在基座应用中维护一个路由注册表,每个路由对应一个子应用。基座应用启动以后,当我们切换路由时,如果是一个新的子应用,会动态获取子应用的 js 脚本,然后执行脚本并渲染出相应的页面;如果是一个已经访问过的子应用,那么就会从缓存中获取已经缓存的子应用,激活子应用并渲染出对应的页面。
在这里,本文仅对 single-spa 做初步的介绍,如需深入了解,详见 官网:single-spa 和 微前端学习系列(二):single-spa。
优点:
- 切换应用时,浏览器不用重载页面,提供和单页应用一样的用户体验;
- 完全技术栈无关;
- 多个子应用可并存;
- 生态丰富;
缺点:
-
需要对原有应用进行改造,应用要兼容接入 sing-spa 和独立使用;
-
有额外的学习成本;
-
使用复杂,关于子应用加载、应用隔离、子应用通信等问题,需要框架使用者自己实现;
-
子应用间相同资源重复加载;
-
启动应用时,要先启动基座应用;
qiankun
qiankun 也能给我们提供类似单页应用的用户体验。qiankun 是在 single-spa 的基础上做了二次开发,在框架层面解决了使用 single-spa 时需要开发人员自己编写子应用加载、通信、隔离等逻辑的问题,是一种比 single-spa 更优秀的微前端方案。
在这里,本文同样仅对 qiankun 做简单的说明,如需深入了解,详见 官网:qiankun 和微前端学习系列(三):qiankun。
优点:
- 切换应用时,浏览器不用重载页面,提供和单页应用一样的用户体验;
- 相比 single-spa,解决了子应用加载、应用隔离、子应用通信等问题,使用起来相对简单;
- 完全和技术栈无关;
- 多个子应用可并存;
缺点:
-
需要对原有应用进行改造,应用要兼容接入 qiankun 和独立使用;
-
有额外的学习成本;
-
相同资源重复加载;
-
启动应用时,要先启动基座应用;
5. node
6. 包管理工具
6.1. npm
npm v2
此时期主要是采用简单的递归依赖方法,最后形成高度嵌套的依赖树。然后就会造成如下问题:重复依赖嵌套地狱,空间资源浪费,安装速度过慢,文件路径过长等问题。大家都很熟悉,这里不再详细解释
npm v3
v3 版本作了较大的更新,开始采取扁平化的依赖结构。这样的依赖结构可以很好的解决重复依赖的嵌套地狱问题,但是却出现扁平化依赖算法耗时长这样新的问题
npm 的扁平化依赖算法通过将所有依赖尽可能放在顶层 node_modules
目录下,减少了路径深度和重复依赖,从而避免“依赖地狱”。然而,由于需要解析大量依赖、解决版本冲突、处理网络请求和磁盘 I/O 操作,这个过程可能会耗时较长。
npm v5
为了解决上面出现的扁平化依赖算法耗时长问题,npm 引入 package-lock.json 机制,package-lock.json 的作用是锁定项目的依赖结构,保证依赖的稳定性
npm 是 node.js 官方内置的包管理工具,是 必须要掌握的工具
less 代码解读复制代码// 初始化,生成package.json文件
npm init
// 安装所有依赖
npm i
// 生产时依赖,保存在package.json 的dependences里面(默认)
npm i --save
npm i -S
// 开发时依赖, 保存在package.json 的devDependencies 里面
npm i -D
npm i --save-dev
// 全局依赖
npm i -g
// 指定版本
npm i jquery@1.11.2
// 最新版本
npm i juqery@latest
//
package-lock.json
保障安装一致性
场景 | 无 lock 文件 | 有 lock 文件 |
---|---|---|
新成员克隆项目后安装 | 可能安装最新兼容版本(风险高) | 完全按 lock 文件版本安装 |
CI/CD 流水线构建 | 不同时间构建可能产生差异 | 确保每次构建依赖完全一致 |
多环境部署(Dev/Prod) | 可能因环境差异导致问题 | 全环境依赖版本强制同步 |
package-lock.json
是保障 JavaScript 项目依赖稳定性的基石,它通过 版本锁定、依赖树固化 和 安装优化 三大机制,解决了传统依赖管理的核心痛点。开发中应始终将其纳入版本控制,确保团队协作和生产部署的可靠性。
6.2. yarn
- 速度快:yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快
- 安全:在执行代码之前,yarn 会通过算法校验每个安装包的完整性
- 可靠:使用详细、简洁的锁文件yarn.lock格式和明确的安装算法,yarn 能够保证在不同系统上无差异的工作
功能 | 命令 |
---|---|
初始化 | yarn init / yarn init -y |
安装包 | yarn add uniq 生产依赖 yarn add less --dev 开发依赖 yarn global add nodemon 全局安装 |
删除包 | yarn remove uniq 删除项目依赖包 yarn global remove nodemon 全局删除包 |
安装项目依赖 | yarn |
运行命令别名 | yarn <别名> # 不需要添加 run |
6.3. pnpm
pnpm
是一个更现代化的包管理工具,旨在解决npm
和yarn
的一些效率和资源管理问题。- 采用内容寻址存储系统:
pnpm
使用内容寻址(content-addressable storage)来存储依赖包。每个依赖包都会被哈希处理,并根据其内容生成唯一的存储地址。这样,即使多个项目依赖于相同版本的包,pnpm
也只需要存储一份,不会重复存储同样内容的文件。
- 使用硬链接和符号链接共享依赖:
pnpm
通过在 node_modules
中创建硬链接或符号链接(symlink),指向内容寻址存储中实际的依赖包。这样每个项目可以“共享”依赖,而不必为每个项目单独存储依赖包内容。
- 硬链接(Hard Link) :将文件内容链接到项目文件夹下,不占用额外磁盘空间。
- 符号链接(Symlink) :为特定版本的包创建路径映射,使项目代码能够准确找到每个依赖包版本的地址。
npm 命令 | pnpm 等效 |
---|---|
npm install | pnpm install |
npm i | pnpm add |
npm run | pnpm |
6.4. nvm
nvm全称 Node Version Manager
,顾名思义它是用来管理node版本的工具,方便切换不同版本的Node.js. 同时在一个环境中安装多个node.js版本(和配套的npm)
命令 | 说明 |
---|---|
nvm list available | 显示所有可以下载的 Node.js 版本 |
nvm list | 显示已安装的版本 |
nvm install 18.12.1 | 安装 18.12.1 版本的 Node.js |
nvm install latest | 安装最新版的 Node.js |
nvm uninstall 18.12.1 | 删除某个版本的 Node.js |
nvm use 18.12.1 | 切换 18.12.1 的 Node.js |
6.5. 幽灵依赖
指的是某个包在项目中使用但并未在 package.json
中声明,可能是通过其他依赖的间接依赖引入。这种隐式依赖会导致项目依赖关系难以维护,如果间接依赖被移除,可能会导致项目出错。
6.6. 硬链接和软链接
6.7. 硬链接(hard link)
硬链接实际上是一个指针,指向源文件的inode,系统并不为它重新分配inode。硬连接不会建产新的inode,硬连接不管有多少个,都指向的是同一个inode节点,只是新建一个hard link会把结点连接数增加,只要结点的连接数不是0,文件就一直存在,不管你删除的是源文件还是连接的文件。只要有一个存在,文件就存在(其实就是引用计数的概念)。当你修改源文件或者连接文件任何一个的时候,其他的文件都会做同步的修改。
硬链接文件有两个限制
1)不允许给目录创建硬链接
2)只允许在同一文件系统中的文件之间才能创建链接
硬链接的概念来自于 Unix 操作系统,它是指将一个文件A指针复制到另一个文件B指针中,文件B就是文件A的硬链接。
通过硬链接,不会产生额外的磁盘占用,并且两个文件都能找到相同的磁盘内容。硬链接的数量没有限制,可以为同一个文件产生多个硬链接。
6.8. 符号链接(symbol link)
符号接最直观的解释:相当于Windows系统的快捷方式,是一个独立文件(拥有独立的inode,与源文件inode无关),实际上是特殊文件的一种, 该文件的内容是源文件的路径指针(另一个文件的位置信息),通过该链接可以访问到源文件。所以删除软链接文件对源文件无影响,但是删除源文件,软链接文件就会找不到要指向的文件(可以类比Windows上快捷方式,你点击快捷方式可以访问某个文件,但是删除快捷方式,对源文件无任何影响)。
符号链接又称为软连接,如果为某个文件或文件夹A创建符号连接B,则B指向A。
npm link使用软链接
6.9. 对比
特性 | npm | yarn | pnpm |
---|---|---|---|
依赖管理方式 | 扁平化管理,嵌套依赖树,可能重复安装 | 扁平化管理和符号链接,同版本只安装一次 | 基于硬链接和符号链接的内容寻址存储 |
安装速度 | 最慢 | 中等(并行安装) | 最快(得益于硬链接复用) |
磁盘空间占用 | 最大 | 中 | 最小 |
依赖管理严格性 | 低(可能存在幽灵依赖) | 中(可能存在幽灵依赖) | 高(严格的依赖树结构) |
锁文件格式 | package-lock.json | yarn.lock | pnpm-lock.yaml |
缓存机制 | 基础缓存 | 高效缓存 | 基于内容寻址的全局存储 |
并行安装能力 | 不支持 (npm5-) /支持 (npm5+) | 支持 | 支持 |
依赖提升策略 | 部分提升 | 全量提升 | 不提升(严格按照依赖声明) |
workspace 支持 | 有限支持 | 完整支持 | 完整支持 |
7. commonjs 和 es module
7.1. CommonJS 规范
7.1.1. CommonJS 简介
CommonJS 是 Node.js 采用的模块化规范,主要用于服务端的 JavaScript 环境。
CommonJS 通过 require()
函数同步加载依赖模块,并使用 module.exports
导出模块成员。
CommonJS 的特性
- 同步加载:模块在代码运行时同步加载,适用于服务端,但不适用于浏览器环境,因为浏览器环境中同步加载会阻塞渲染进程。
- 缓存机制:同一个模块在多次加载时会被缓存,除非明确清除缓存。
- 简单易用:通过
require
和module.exports
实现模块的导入和导出,简单直观。
7.1.2. CommonJS 的使用示例
ini 代码解读复制代码// math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
module.exports = {
add,
subtract
};
// main.js
const math = require('./math.js');
console.log(math.add(1, 2)); // 输出: 3
console.log(math.subtract(5, 3)); // 输出: 2
7.1.3. CommonJS 可能出现的问题
尽管 CommonJS 在服务端开发中被广泛使用,但在前端环境或大型项目中,它也存在一些潜在的问题和局限性:
- 同步加载的限制:CommonJS 模块是同步加载的,这意味着在模块加载完成之前,代码的执行会被阻塞。在服务端环境中(例如 Node.js),这种行为是可行的,因为文件系统读取速度相对较快。然而,在前端浏览器环境中,网络延迟可能导致较长的加载时间,进而阻塞页面渲染并降低用户体验。
- 循环依赖问题:CommonJS 规范中,模块被加载时执行(运行时加载),如果两个模块互相引用(循环依赖),这可能会导致未定义的行为或部分代码无法执行。虽然大多数情况下,Node.js 可以处理这种情况,但会引起意料之外的结果,尤其是当模块依赖链较复杂时。
- 缺乏静态分析能力:由于 CommonJS 使用动态
require()
语句来引入模块,这使得工具很难在编译时进行静态分析。这种动态依赖关系的管理方式,使得打包工具(如 Webpack、Rollup)难以进行代码优化(如 Tree Shaking),从而影响性能和代码体积。 - 跨平台兼容性:CommonJS 规范设计之初是为了满足服务端 JavaScript(Node.js)环境的需求,它不适合直接在浏览器环境中使用。虽然可以通过 Browserify 等工具将 CommonJS 模块转换为浏览器可用的格式,但这增加了开发和构建的复杂性。
尽管 CommonJS 规范在 Node.js 服务端开发中取得了巨大成功,但在前端开发和大型项目中,它也暴露了自身的一些局限性。
现代 JavaScript 开发逐渐转向 ES6 Module 标准,这一标准通过静态分析、异步加载和浏览器原生支持,解决了 CommonJS 规范中的许多问题,为开发者提供了更强大和灵活的模块化支持。
7.2. ES6 Module 简介
ES6 Module(ESM)是由 ECMAScript 官方在 ES6(ECMAScript 2015)中引入的模块化规范。它是 JavaScript 语言级别的模块系统,支持静态分析,能够在编译时确定模块的依赖关系。
相较于 CommonJS 和 AMD,ESM 具有更灵活和更高效的模块管理能力。
7.2.1. ES6 Module 的特性
- 静态依赖分析: ES6 Module 在编译时就可以确定模块的依赖关系,从而实现静态分析和树摇(Tree Shaking)优化。这意味着模块中没有被使用的代码可以在打包阶段被移除,从而减小最终的文件大小。
- 严格模式(Strict Mode) : ES6 Module 自动采用 JavaScript 严格模式。这意味着模块中不能使用某些不安全的语法(如
with
语句),提高了代码的安全性和性能。 - 独立的模块作用域: 每个模块都有独立的作用域,模块内部的变量、函数不会污染全局作用域,避免了变量命名冲突问题。
- 导入和导出语句(Import 和 Export) : ES6 Module 使用
import
和export
关键字来导入和导出模块成员。导出可以是命名导出(Named Export)或默认导出(Default Export)。 - 异步加载支持: ES6 Module 可以异步加载模块,避免了阻塞浏览器的渲染进程,从而提升了页面加载性能。
- 浏览器原生支持: 现代浏览器原生支持 ES6 Module,无需额外的加载器(如 RequireJS)或打包工具(如 Webpack)即可直接使用。
7.2.2. ES6 Module 的使用方法
ES6 Module 主要通过 export
和 import
语法来管理模块。
导出模块(Export)
ES6 Module 提供了两种导出方式:命名导出 和 默认导出。
- 命名导出(Named Export):允许导出多个成员,导出时需要使用
{}
包裹。
javascript 代码解读复制代码// module-a.js
export const data = "moduleA data";
export function methodA() {
console.log("This is methodA");
}
export class MyClass {
constructor() {
console.log("This is MyClass");
}
}
- 默认导出(Default Export):每个模块只有一个默认导出,使用
export default
关键字。
javascript 代码解读复制代码// module-b.js
export default function () {
console.log("This is the default exported function");
}
导入模块(Import)
- 导入命名导出:需要使用花括号
{}
指定导入的成员。
javascript 代码解读复制代码// main.js
import { data, methodA, MyClass } from "./module-a.js";
console.log(data); // 输出:moduleA data
- 导入默认导出:直接指定导入的变量名称。
javascript 代码解读复制代码// main.js
import defaultFunction from "./module-b.js";
defaultFunction(); // 输出:This is the default exported function
- 同时导入命名导出和默认导出:
scss 代码解读复制代码// main.js
import defaultFunction, { data, methodA } from "./module-b.js";
defaultFunction();
console.log(data);
methodA();
动态导入(Dynamic Import)
ES6 Module 还支持动态导入模块,这种导入方式适用于需要按需加载的场景。动态导入返回一个 Promise 对象。
javascript 代码解读复制代码// main.js
import("./module-a.js").then((module) => {
module.methodA(); // 输出:This is methodA
});
7.2.3. ES6 Module 与其他模块规范的比较
ES6 Module 相较于 CommonJS 和 AMD 有显著的优势:
- 加载方式: CommonJS 使用同步加载,这在服务器端是可行的,但在浏览器中会导致阻塞。而 ES6 Module 支持异步加载,不会阻塞浏览器的渲染进程。
- 模块依赖分析: CommonJS 模块的依赖关系在运行时解析,这可能导致加载时的性能开销。ES6 Module 在编译阶段就能确定依赖关系,优化了加载效率和性能。
- 代码优化: 由于 ES6 Module 支持静态分析工具,构建工具能够对代码进行更有效的优化(如 Tree Shaking),减少最终产物的大小。
- 兼容性: ES6 Module 是现代浏览器和 Node.js 官方推荐和支持的模块化标准,未来的兼容性和更新都更有保障。
7.2.4. 3.5 ES6 Module 的局限性
虽然 ES6 Module 在现代开发中具有广泛应用,但它也有一些局限性:
- 浏览器兼容性:早期版本的浏览器不支持 ES6 Module,不过随着浏览器的更新,这个问题正逐渐消失。
- 服务端使用限制:在服务端(如 Node.js)环境中,使用 ES6 Module 可能需要一些配置和额外的工具支持(如 Babel、Webpack)。
- 性能影响:在非常大量模块导入的场景下,可能会有性能瓶颈。
7.3. 四、总结
JavaScript 的模块化演进经历了从无到有、从简单到复杂的过程。随着前端应用的复杂性和需求的增加,模块化的重要性愈发凸显。CommonJS、AMD 和 ES6 Module 各有其应用场景和特点。
- CommonJS:适用于 Node.js 服务端开发,使用同步加载机制。
- AMD:适用于浏览器环境,使用异步加载机制,解决了前端模块依赖问题。
- ES6 Module:现代浏览器和 JavaScript 语言级别的模块化标准,支持静态分析、异步加载和 Tree Shaking,是当前前端开发的主流选择。
未来的 JavaScript 开发中,ES6 Module 将继续发挥重要作用,为开发者提供更强大和灵活的模块化支持。
评论记录:
回复评论: