1.什么是webpack?
webpack是JS的模块打包工具 (module bundler)。通过分析模块之间的依赖,最终将所有模块打包成一份或者多份代码包 (bundler),供 HTML 直接引用。实质上,Webpack 仅仅提供了打包功能 和一套文件处理机制,然后通过生态中的各种 Loader 和 Plugin 对代码进行编译和打包。因此 Webpack 具有高度的可拓展性,能更好的发挥社区生态的力量。
webpack有几个核心概念:
- Entry: 入口文件,Webpack会从该文件开始进行分析与编译;
- Output: 出口路径,打包后创建 bundler的文件路径以及文件名;
- Module: 模块,在 Webpack 中任何文件都可以作为一个模块,会根据配置的不同的 Loader 进行加载和打包;
- Chunk: 代码块,可以根据配置,将所有模块代码合并成一个或多个代码块,以便按需加载,提高性能;
- Loader: 模块加载器,进行各种文件类型的加载与转换;
- Plugin: 拓展插件,可以通过 Webpack 相应的事件钩子,介入到打包过程中的任意环节,从而对代码按需修改;
2.webpack打包原理
根据文件间的依赖关系对其进行静态分析,然后将这些模块按指定规则生成静态资源。当 webpack处理程序时,会递归地构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将所有这些模块打包成bundle
3.webpack打包流程:
1.初始化流程:从配置文件和 Shell 语句中读取并合并参数,得到最终的配置参数。根据配置参数初始化 Compiler 对象,然后调用Plugin的 apply 方法挂载插件。
2.编译流程:执行Compiler对象的 run 方法开始编译。从入口文件出发,针对每个 Module 串行调用对应的 Loader 去编译文件内容,并在合适的时机调用对应的 Plugin。编译过程中找到该模块的依赖模块,并递归编译依赖模块,直到所有模块都完成编译。
3.输出流程:翻译完所有模块,会得到被编译后的最终内容以及它们之间的依赖关系。然后根据编译结果,组装成一个个包含多个模块的 chunk,再把每个 chunk 转换成一个单独的文件加入到输出列表(输出前仍可通过 Plugin 进行文件的修改)。最后根据 Output 把文件内容写入到指定的文件夹,完成整个过程。
4.webpack几个常见的loader?
file-loader:把图片、字体文件等打包到指定文件,可将小文件以base64的方式注入到代码中。
style-loader:把CSS代码注入到js中
css-loader:加载CSS,支持模块化
less-loader:把less转化为css
css-hot-loader:实现css热加载
html-withimg-loader:纠正打包后html里img标签src路径错误的问题
babel-loader:把ES6/jsx转换成ES5
ts-loader:将ts转换成js
vue-loader:转化.vue文件
vue-style-loader:配置.vue文件css里的fonts, 把字体图标输出到指定目录
eslint-loader:通过ESLint检查js代码
5.webpack几个常见的plugin?
html-webpack-plugin:配置入口html,支持多个html
mini-css-extract-plugin:分离css文件
optimize-css-assets-webpack-plugin:压缩css
uglifyjs-webpack-plugin:压缩js
CommonsChunkPlugin:将chunks相同的模块代码提取成公共js
ZipWebpackPlugin:将打包的资源生产zip包
6.webpack有哪些优点
1.webpack只针对打包不预设场景,不局限于 web 打包;可根据不同配置打包不同项目,配置很灵活。
2.可通过 loader 和 plugin扩展功能
3.社区庞大
7.webpack的缺点
1.配置复杂。因为配置极度灵活,导致配置项极其复杂,针对常见的 web 项目,也需要大量的配置。
2.开发阶段热更新慢。热更新时,会将该模块的所有依赖模块都重新编译打包,所以会很慢。虽然 Webpack5实现了缓存,但尚未在所有框架和插件中获得支持。
3.打包体积大。webpack打包的结果中有很多代码注入。因为webpack出现在ESM规范出来之前,于是自己实现require和module.exports方法、用于兼容了commonjs,后来又兼容ESM规范,才会有很多代码注入。
8.分别介绍bundle,chunk,module是什么
bundle:是webpack最终打包出来的文件,bundle可以和chunk长得一模一样,但大多数情况下都是多个chunk的集合。
chunk:打包过程中(loader处理后、还没最终完成打包)的文件叫做chunk,一个chunk由一个或多个模块(module)组成。
module:开发中的单个模块,在webpack中,一切皆模块,一个模块对应一个文件。
9.什么是loader?什么是plugin?
loader:是一个转换器,将A文件编译成B文件,比如:将A.less转换为A.css。接收原始资源数据作为参数,最终输出js代码,传给webpack做进一步的编译。
plugin:是一个扩展器,丰富了webpack本身,针对的是webpack打包的整个过程。它并不直接操作文件,而是基于事件机制,往webpack打包的生命周期函数里注入逻辑,执行广泛的任务。
10.什么是模块热更新?
模块热更新 HMR
全称为 Hot Module Replacement
,也叫做模块热替换。它允许在运行时替换、添加、删除各种模块,而无需刷新整个页面。
Webpack 模块热更新核心原理:
- 浏览器加载页面后,与
webpack-dev-server
静态资源服务器建立 WebSocket 连接- Webpack 监听到文件变化后,构建发生变更的模块,并通过 WebSocket 发送
hash
事件给浏览器- 浏览器接收到
hash
事件后,加载发生变更的模块
11.什么是Tree-shaking
Tree-shaking可以用来剔除js中不用的死代码,它依赖静态的es6模块化语法,例如通过import 和export 导入导出,Tree-shaking最先在rollup中出现,webpack在2.0中将其引入,css中使用Tree-shaking需要引入Purify-CSS。
12.通过webpack处理缓存
浏览器在用户访问页面的时候,为了加快加载速度,会对用户访问的静态资源进行存储,但是每一次代码升级或是更新,都需要浏览器去下载新的代码,这时就需要处理浏览器缓存。webpack中可以在output输出的文件名上指定hash, 或者通过问号传参设置版本号。
13.webpack-dev-server的服务原理
基于nodejs的express框架,搭建了一个http静态文件服务器,它根据路由的不同返回不同的内容。使用webpack-dev-middleware改变webpack打包的输出地址,使用memory-fs模块将打包资源输入到内存中;基于内存中的文件,根据路径,express搭建了静态文件服务器;
14.抽象语法树AST是什么
抽象语法树(Abstract Syntax Tree,AST)是源代码语法结构的一种抽象表示方法。它以树状的形式表示编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
15.Babel的运行原理
1)解析代码并输出AST(抽象语法树)。
Babel使用@babel/parser,根据ESTree规范将读取到的js代码转化成AST抽象语法树。
2)转化AST
Babel使用@babel/traverse方法遍历AST树并对其进行修改、删除或增加节点,返回转换后的AST。
3)生成代码
Babel使用@babel/generator通过深度优先遍历转化后的AST,构建成js代码字符串。
16.webpack常见性能优化
开发阶段的构建性能:
1.限制loader的构建范围。配置loader时,test属性配合include、exclude来精确匹配文件夹,避免不必要文件夹的转换。
2.缓存loader的结果。给loader添加cache-loader,将结果缓存到磁盘里,显著提升二次构建速度。
{
test: /\.(css|less)$/,
include: /node_modules/,
use: [
{
loader: 'cache-loader',
cacheDirectory: './cache',
},
'css-loader',
'less-loader',
],
},
3.多线程打包。通过thread-loader开启一个线程,会把对应loader放进其它线程运行以提高效率。
{
test:/\.js$/,
use:[
{
loader:'cache-loader'.
cacheDirectory:'./cache'
},
'babel-loader',
'thread-loader',
]
}
4.开启热更新
5.在optimization里配置插件,可确保在production模式下执行,在development模式下不执行。
生产环境的打包体积优化:
1.分包打包的三个方案:
1)将entry配置成一个对象,来设置多个打包入口
entry: { // 将entry配置成一个对象,来设置多个打包入口
index:"./src/index.js",
album:"./src/album.js"
},
output:{
// 通过[name]这种占位符的方式动态输出文件名,[name]最终就会替换成打包入口名称
filename:"[name].bundle.js",
},
plugins: [
new HtmlWebpackPlugin({
title:"Multi Entry",
template:"./src/index.html",
filename:"index.html",
chunks:['index']
}),
new HtmlWebpackPlugin({
title:"Multi Entry",
template:"./src/album.html",
filename:"album.html",
chunks:['album']
}),
],
2)动态导入模块:动态导入就是开发项目代码时,采用ES Module的动态导入,整个过程我们无需配置任何地方,只需要按照ES Module动态导入的方式去导入模块就可以了,webpack内部会自动处理分包和按需加载。
// 动态导入模块时,这样注释/* webpackChunkName: 'common1' */,可以导入到common1.js文件中,命名相同的导入到同一个文件。
if (hash === '#posts') {
import(/* webpackChunkName: 'common1' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
});
} else if (hash === '#album') {
import(/* webpackChunkName: 'common1' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
});
}
3)提取公共模块
// 提取公告模块命名为common。只需添加这个配置,webpack就能自动把多个入口文件公共的模块提取出来。
optimization: {
splitChunks: {
chunks: 'all',
name: 'common'
}
}
2.安装依赖包时区分开发环境和生产环境
3.不常改变的第三方依赖包,可以通过cdn引入
4.开发时用development模式,会自动优化打包速度、添加调试插件等。生产阶段用production模式,会启动内置优化插件,自动优化打包结果。
17.loader转化文件的原理
webpack只能直接处理js代码,任何非 js文件都必须被预先处理转换为js代码。loader(加载器)就是这样一个代码转换器,它由webpack的loader runner执行调用。loader是一个node模块,它是一个函数,接收原始资源数据作为参数,最终输出js代码,传给webpack做进一步的编译。
// 删除所有的console.log的loader
module.exports = function(context) { // 由于nodejs是遵从commonjs规范的所以导出格式
// context是被处理文件里面的内容
context = context.replace(/console\.log\(.*\)/g, ''); // 替换内容
return context; // 返回想要的结果
}
// webpack.config.js里使用该loader
const path = require('path');
module.exports = {
mode: 'development',
entry: './main.js',
output: {
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
// 自定义loader
{
test: /\.(jsx|js)$/,
use: ['./loader/delconsole-loader'],
}
]
}
}
18.webpack怎么编写插件
webpack插件就是一个类,里面要有一个apply成员方法,apply方法接收一个compiler对象作为参数。compiler对象就是webpack编译对象,它里面有一些事件钩子函数,就是通过这些事件勾子函数编写一些插件。一般在emit钩子里编写插件,因为这时源文件的转换和组装已经完成了,可以在这里读取并改造最终将要输出的资源、代码块、模块及对应的依赖文件。
class MyPlugin {
apply(compiler) {
compiler.plugin('emit', function(compilation, callback) {
// compilation.chunks 存放了所有的代码块,是一个数组,需要遍历
compilation.chunks.forEach(function(chunk) {
// 通过chunk.forEachModule 能读取组成代码块的每个模块
chunk.forEachModule(function(module) {
// module 代表一个模块。
// module.fileDependencies 存放当前模块的所有依赖的文件路径,它是一个数组
module.fileDependencies.forEach(function(filepath) {
console.log(filepath);
});
});
/*
webpack会根据chunk去输出文件资源,一个chunk对应一个以上的输出文件。
比如一个Chunk中包含了css 模块并且使用了ExtractTextPlugin时,
那么该Chunk就会生成 .js 和 .css 两个文件
*/
chunk.files.forEach(function(filename) {
// compilation.assets 存放当前所有即将输出的资源。
// 调用输出资源的source()方法,能获取输出资源的内容
const source = compilation.assets[filename].source();
});
});
/*
该事件是异步事件,因此要调用 callback 来通知本次的 webpack事件监听结束。
如果我们没有调用callback(); 那么webpack就会一直卡在这里不会往后执行。
*/
callback();
})
}
}
19.vite是什么?
1.首先vite不是打包工具,可以理解为一个项目开发启动工具
和项目打包启动工具,它封装了esbuild和rollup两个工具,开发阶段用esbuild编译,生产环境用rollup打包
。由于现代浏览器原生支持esm,vite提倡开发阶段不打包的思想;
同时利用了浏览器缓存策略,将源码模块的请求进行协商缓存(304 not Modified),将依赖模块请求进行强缓存(200 from cache);还在冷启动时用esbuild
对第三方依赖做了预构建,并将预构建的依赖缓存到 node_modules/.vite
;通过这几点,大大提升了开发阶段的编译和热更新速度。
2.开发环境做预构建的原因:
1.将非 ESM 规范的代码(如cjs)转换为符合 ESM 规范的代码
2.将第三方依赖内部的多个文件合并为一个,减少 http 请求数量
3.vite在热更新时,只会(用Esbuild)编译当前更改的模块,而第三方依赖模块也都在冷启动时预构建完毕,也没有打包过程,所以编译速度很快。
4.开发环境用Esbuild做预构建和代码编译,因为Esbuild是基于 Go 语言开发的 js 打包工具,最大的特征就是快,而开发阶段的关注点就是编译速度。
5.vite在线上生产环境使用rollup打包原因:
1.rollup的打包体积比webpack小的多,而线上环境的关注点就是打包体积
2.esbuild的打包功能还不完善,没有代码分割、打包内容优化等功能,而rollup在这方面做到很好,并且生态很成熟。
20.webpack与vite之间怎么取舍?
-
打包js类库:不用考虑,rollup会更好,建议使用vite
rollup本身也支持很多插件,生态也成熟,各种场景几乎都能照顾到;而且像vue、react这些常见的类库,都是用rollup打包的。 -
打包应用程序:很多人推荐使用webpack,因为webpack功能强大,生态更成熟,社区更活跃。其实rollup的功能和生态也可以,不然尤大也不会放弃webpack
21.npm install 的执行过程
① 发出 npm install 命令
② 查询node_modules目录之中是否已经存在指定模块,若存在,不再重新安装
③ 若不存在,npm 向 registry 查询模块压缩包的网址
④ 下载压缩包,存放在根目录下的.npm目录里
⑤ 解压压缩包到当前项目的node_modules目录
注意: 一个模块安装以后,本地其实保存了两份。一份是.npm目录下的压缩包,另一份是nodemodules目录下解压后的代码。但是,运行npm install 的时候,只会检查nodemodules目录,而不会检查.npm目录。也就是说,如果一个模块在.npm下有压缩包,但是没有安装在node_modules目录中,npm 依然会从远程仓库下载一次新的压缩包。为了解决这些问题,npm提供了一个--cache-min参数,用于从缓存目录安装模块。--cache-min参数指定一个时间(单位为分钟),只有超过这个时间的模块,才会从registry下载。npm install --cache-min 99999 <packageName>
上面命令指定,只有超过99999分钟的模块,才会registry下载。
package-lock.json 建议
开发系统应用时,建议把 package-lock.json 文件提交到代码版本仓库,从而保证所有团队开发者以及 CI 环节可以执行 npm install 时安装的依赖版本都是一致的。
开发一个 npm 包时,你的 npm 包是需要被其他仓库依赖的,由于扁平安装机制,如果锁定了依赖包版本,那你的依赖包就不能和其它依赖包共享语义化版本范围内的依赖包,这样会造成不必要的冗余。所以我们不应该把 package-lock.json 文件发布出去(npm 默认不会把 package-lock.json 文件发布出去)