webpack的核心配置有哪些?
- entry:入口文件;
- output: 打包之后输出的路径
- loader: 处理js之外的其他类型的loader配置;
- plugins: 扩展webpack功能的插件配置;
- mode: 打包环境的配置;
webpack的运行环境
webpack是在node环境中运行的,因此它的配置文件都是遵循commonJs规范;
webpack的优化有哪些?
提升打包速度(提高开发效率)方面
- 启动热更新
- 对于vue-loader和react-loader会默认进行热更新
- 对于css会自动进行热更新
- 对于自己配置的非框架项目,js模块中修改是不会开启热更新,需要使用module.hot.accept(模块文件)指定热更新的文件;
- oneOf包裹loader,命中一个loader之后不再继续命中其他loader;
- 通过includes指定或excludes排序不需要处理的文件;
- 缓存babel处理过的文件;
{
test: /\.js$/,
exclude: /(node_modules)/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启缓存
cacheCompression: false, // 关闭缓存的文件进行压缩
}
}
]
}
- 缓存eslint校验过的文件;
new ESLintPlugin({
exclude: "node_modules",
cache: true, // 开启缓存eslint
cacheLocation: path.resolve(__dirname, '../node_modules/eslintcache') // 缓存到指定的文件中
}),
- 针对于比较大的项目开启多线程打包,使用thread-loader插件;开启线程也是比较耗时,因此只针对于较大的项目;
const os = require('os')
{
test: /\.js$/,
exclude: /(node_modules)/,
use: [
{ // 开启多进程打包js文件
loader: "thread-loader",
options: {
works: os.cpus().length,
}
},
{
loader: 'babel-loader',
}
]
}
- 把模块中依赖的文件的hash单独生成到一个文件中;这样做的目的为了防止依赖修改使用到这个依赖的文件也会被重新打包,通过一个文件单独映射文件和文件名,这样依赖改变只改变此文件中的文件名,而不会影响使用到它的其他模块;通过runtimeChunk配置实现;
runtimeChunk: {
name: (entrypoint) => `runtime-${entrypoint.name}.js`
}
- 改用SWC去代替babel处理js,SWC是rust编写的web编译器,速度是babel的20-100倍;
减少包的体积(提升请求速度)
- tree-shaking抖动掉没有使用的代码,只针对esModule有效,生产模式默认开启,代码中使用es module导入和导出即可;
- 禁止babel为每个js文件引入辅助代码,使用@babel/plugin-transform-runtime插件
{
test: /\.js$/,
exclude: /(node_modules)/,
use: [
{
loader: 'babel-loader',
options: {
plugins: [
'@babel/plugin-transform-runtime' // 禁止babel给每个文件添加辅助代码
]
}
}
]
},
- 压缩图片
- 分割代码, 针对于vue项目可以使用异步组件或路由就可以进行分包,针对于非框架的项目可以手动指定哪些文件进行分包;
splitChunks: {
chunks: "all", // 对所有模块进行分割
// 修改配置
cacheGroups: {
// default: {
// // 其他没有写的配置会使用上面的默认值
// minSize: 1000, // 打包的最小体积
// minChunks: 1, // 最小的引用次数
// priority: -1, // 优先级 值越大优先级越高
// },
a: { // 把index3.js文件打成一个单独的包
test: /[\\/]index3\.js$/,
name: 'a',
chunks: 'all',
enforce: true
},
},
},
- 动态导入配合分割代码可以把导入的模块进行单独打包
通过import()动态引入模块,配合代码分割就可以打包成单独的文件
eg: import(xxx.js).then(res => { 逻辑处理 })
给动态引入的模块命名,需要在import中添加模块命注释,在输出中配置chunkFilename
eg: import(/** webpackChunkName: "aa"*/ "./js/aa.js").then(XXX)
- 使用预加载,当引入的资源太大,在需要的时候才去加载可能会影响性能,通过预加载进行提前加载;
preload: 告诉浏览器立即加载此资源,只能加载当前页面中的资源。兼容性比prefetch好,优先级比prefetch高;
prefetch: 告诉浏览器在空闲的时候加载此资源,可以加载当前页和下一页的资源,ssfari不兼容;
通过插件@vue/preload-webpack-plugin在webpack中给每个chunk文件设置预加载
new preloadWebpackPlugin({
rel: 'preload',
as: 'script'
}),
- PWA,使用workbox-webpack-plugin插件
什么是loader?
loader就是模块转换器,将非js模块转换成js可以直接引用的模块;
loader分为pre前置,normal普通,inline内联和post后置loader;
这些loader的执行顺序为:pre > normal > inline > post;同级的从右往左,从下到上执行;
{
enfore: "post", // 指定为后置loader
test: /\.js$/,
loader: "jsLoader",
}
内联的loader通过import语句显示的执行loader;多个loader通过!进行隔开;
import Styles from 'style-loader!css-loader?./styles.css'
使用style-loader和css-loader处理styles.css文件
内联loader可以使用不同的前缀跳过不同类型的loader
! 跳过normal loader的处理
-! 跳过pre和normal loader的处理
!! 跳过pre,normal和post loader的处理
import Styles from '!!style-loader!css-loader?./styles.css'
跳过pre,normal,post loader的处理只使用pitch loader处理
loader分类的作用:可以执行loader的执行顺序;
如何实现一个loader?
每个loader就是一个函数,接收三个参数,分别为处理文件的内容,source-map和额外的参数;
loader分为同步loader和异步loader;
- 同步loader
// 写法1:处理完之后直接返回处理之后的内容,一般用于只有一个处理的loader
module.exports = function (content, map, meta) {
return content
}
// 写法2:通过callback抛出错误,并且可以通过多个loader处理
module.exports = function (content, map, meta) {
this.callback(new Error(), content, map, meta)
}
- 异步loader
module.exports = function (content, map, meta) {
const callback = this.async()
setTime(() => {
callback(new Error(), content, map, meta)
},3000)
}
- raw loader 接收的内容为buffer格式
// 写法1
module.exports = function (content) {
// 也可以使用异步方式的loader
return content
}
module.exports.raw = true
// 写法2
function test3Loader (content) {
return content
}
test3Loader.raw = true
module.exports = test3Loader
- pitch loader 优先执行每个loader下的pitch loader,再执行普通loader;pitch loader如果有返回就会中断后续loader,继续执行上个normal loader
test1
module.exports = function (content) {
console.log('loader1')
return content
}
module.exports.pitch = function (content) {
console.log('pitch loader1')
}
test2
module.exports = function (content) {
console.log('loader2')
return content
}
module.exports.pitch = function (content) {
console.log('pitch loader2')
}
test2
module.exports = function (content) {
console.log('loader3')
return content
}
module.exports.pitch = function (content) {
console.log('pitch loader3')
}
// 执行顺序为 pitch loader1 pitch loader2 pitch loader3 loader3 loader2 loader1
如果在test2的pitch中返回结果,那么就会中断后续的pitch和loader
// 执行顺序为 pitch loader1 pitch loader2 loader1
注意事项:每个loader都遵循单一原则,每个loader只做一种转换,多个转换就写多个loader进行处理;
什么是plugins?
plugins是用来扩展webpack的插件,webpack运行的各个阶段都会广播出对应的事件,插件就是去监听对应的事件做出一些处理;
如何实现一个plugins?
webpack中通过Tapable库实现各个阶段事件的广播,webpack会在run阶段执行插件的apply方法,因此插件是一个类,并且有apply方法,webpack会把compiler对象传递给apply方法;在apply方法中去调用compiler下的hooks下的各个阶段的钩子;不同钩子区分异步还是同步执行,根据webpack文档查看具体的钩子执行时机;
常见的loader有哪些?
- file-loader: 把文件输出到指定的目录中,并且通过url引入文件;
- url-loader: 和file-loader功能一样,但是可以处理小文件,小文件可以转成base64直接注入到代码中;
- img-loader: 加载并压缩图片;
- babel-loader:把es6转成es5;
- css-loader: 处理css;
- style-loader: 把css代码放在style标签中,并且在html中创建style标签;
- vue-loader: 处理.vue文件
常见的plugins?
- define-plugin: 定义环境变量;
- html-webpack-plugin: 根据指定的html文件生成新的html并且引入打包之后的bundler;
- miniCssExtractPlugin: 把css抽离成一个单独的css文件;
- analyzePlugin: 分析打包之后包的依赖和体积;
webpack的构建流程是什么?
- webpack的构建过程是串联的;
- 初始化参数:合并配置文件和shell命令中的参数;
- 开始编译:通过参数初始化Compiler对象,加载配置的插件;
- 执行编译:执行对象的run方法;
- 找到入口:通过配置中的entry属性找到所有的入口文件;
- 编译模块:从入口开始通过loader进行编译模块,找出模块依赖的其他模块通过递归进行处理;直到所有的依赖都处理完毕;
- 依赖图:得到loader转换之后的模块内容和它们之间的依赖图;
- 输出资源:根据依赖图,组装成一个个包含多个module的chunk,再把每个chunk转成bundle放在输出列表中;
- 根据配置的输出信息,把bundle输出到指定文件中;
- 以上整个过程会在各个阶段广播出不同的事件,plugins就可以监听到这些事件执行插件的回调;
如何提高webpack的构建速度?
- 通过speedMeasureWebpackPlugin插件进行分析构建的耗时;
- 通过多进程构建
- 设置缓存
- 对于node_modules不进行处理
webpack热更新(HMR)原理?
- webpack-dev-server在webpack打包的时候会向入口文件注入两个文件路径,这两个文件一个表示用在客户端和服务端链接websocket,接收服务端发送来的请求,一个是用来接收到变化之后进行请求变化的文件进行替换执行;
- 服务端和客户端建立Websocket链接;
- 服务端监听webpack的打包完成的done事件,当打包完成之后就通过Websocket去主动发送hash请求;
- 服务端还通过Watch监听文件的修改,文件修改了就重新打包再次触发第三步;
- 客户端websocket接收到服务端发来的hash请求,之后会去请求一个修改的文件列表,遍历文件列表通过动态的创建script加载对应修改的文件,获取到每个修改的模块的父模块,执行父模块代码中的hot.accept的回调进行更新模块代码。如果没有回调就重新加载整个页面;
chunk和bundle的区别?
chunk: chunk是webpack打包过程中modules的集合;多个入口文件就是多个chunk,也可以通过分包把一个入口文件的依赖的其他模块分包成多个chunk;
bundle: 是webpack构建之后输出的结果文件,一般情况下一个chunk对应一个bundle,开启source-map,一个chunk对应多个bundle;
什么是complier?什么是Compilation?它们的区别?
complier: 是webpack构建过程中创建的一个全局唯一的compiler对象,它包含了webpack配置的所有信息,包含options,loader和plugin等;
Compilation: 是当前编译过程中的一个对象,包含当前模块资源,编译生成的资源;webpack在运行过程中,每当检查到一个模块的变化就会重新生成一个Compilation对象;
webpack3到webpack4有什么优化?
- commonChunkPlugin 改成 splitChunks
- loaders 改成 rules
- css-loader增加了use
- webpack4对打包和编译速度进行了优化
webpack4到webpack5有什么优化?
- webpack5增加了持续优化,将编译的结果存入到磁盘中
- 增加了资源模块配置,统一处理资源文件比如图片和文件等,webpack5集成了url和file-loader;
asset/resource 相当于 file-loader
asset/inline 相当于 url-loader
asset/source 相当于 raw-loader
- tree-shaking:webpack5默认使用Es module模块化对优化更高效,而webpack4使用commonJS;
- splitChunk配置更细致,有minSize和minChunk;
- webpack5进一步优化了打包速度;
谈谈你对Webpack的理解?
webpack是一个打包模块化的js工具,在webpack中一切兼模块,通过loader转换非js文件,通过plugin注入钩子扩展webpack的功能,最后输出由多个模块组合成的bundle文件;
webpack的splitChunk分包原理?
- 抽离公共代码,将多个模块共用的代码进行抽离成公共的模块,比如第三方库,从而避免重复代码;
- 按需加载:对异步路由和组件可以实现代码拆分;
- 最小体积:分离的模块的最小体积,低于这个大小就不进行分离;
- 最少引用次数:分离的模块被其他模块的引用次数,如果小于这个次数就不分离;
webpack中tree-shaking的原理
- 在make阶段收集模块导出的变量,并记录到模块依赖关系图中;
- seal阶段,遍历模块依赖关系图,标记哪些导出的变量没有被使用到;
- 构建阶段,使用terser将没有被使用到的导出语句进行删除;
webapck和grunt,glup的不同?
grunt和glup是基于任务和流的。找到一个或一类文件对其做一系列的链式操作更新流上的数据,整条链式操作构成一个任务,多个任务构成整个构建流程; webpack是基于入口,webpack会从入口文件开始处理,如有依赖的其他文件就会通过递归的方式进行处理,直到所有的依赖文件都处理完毕,并在整个构建流程中的各个阶段注入一些钩子,通过plugin可以执行这些钩子进行扩展; grunt和gulp需要开发者将整个构建流程拆分成多个任务并且合理的控制多个任务之间的调用;而webpack只需要配置入口文件,配置相应的loader和plugin即可;
webpack和vite
- vite优势:
- 冷启动:开发状态下不需要重新编译打包;
- 热更新:修改源文件可以直接更新视图
- 按需更新:不需要刷新所有的节点,只需要更新改动的部分;
- vite构建原理:
开发环境:
- 利用了现代浏览器支持es模块的特性,采用非打包模式,将打包放在生产环境;
- vite的服务器不去提前编译代码,而是让浏览器直接请求解析对应的入口模块;
- 浏览器会根据入口模块去请求依赖的模块,vite服务器会进行拦截并且进行即时的编译,比如将非esm模块编译成Esm模块引入;
- vite在首次启动的时候会进行预构建,将构建的结果缓存到node_modules/.vite下;后续直接使用;
生成环境: 通过rollup进行打包;
- vite特性:
- 支持0配置,不需要安装各种loader和plugin就能直接打包;但也可以通过配置文件进行扩展配置;
- 支持ts
- 支持直接引入Css文件
- 支持直接引入json文件
- vite大概流程:
- 浏览器通过path路径判断是否有缓存,有缓存直接返回缓存;
- 没有缓存判断path路径中是否有@modules,来区别是依赖还是业务代码;
- 如果有表示是依赖直接从node_modules下的.vite中获取依赖;否则就是业务代码;
- 最后通过esbuild进行转换成esmodule;
- webpack和vite区别:
- webpack:
开发模式:首先要编译打包成bundle,存储在内存中,启动Dev-server;热更新的时候,改动的模块和相关依赖会进行重新打包编译;
生产模式编译打包生成bundle到指定目录中; - vite:
开发模式:通过路由劫持加实时编译,启动本地服务,请求所需要的模块并且实时编译(rollup模块打包器将cjs,amd转成es module);热更新的时候浏览器直接重新获取当前的模块,并且浏览器进行了缓存优化,对于依赖进行强缓存,对于业务模块进行协商缓存;
生产模式通过rollup打包;
vite相关的面试题
vite的入口从哪里获取?
vite-app搭建的项目,在index.html中默认引入src/main.js,因此main.js就是入口,也可以通过配置文件指定入口;
vite如何区分本地文件和依赖?
依赖的文件在path中会包含@modules,而本地文件则没有;
vite中如何通过@modules找到对应的依赖?如何进行拼装?
vite会通过正则匹配@modules,然后进行请求拦截,从node_modules下的.vite文件中找到对应的依赖;如果是依赖文件在模块中导入的时候是不会用相对路径导入,直接是文件名的形式,直接进行替换文件名;
vite是如何处理.vue文件
匹配.vue文件,把模块和js,css拆分成三个文件,这样针对于三个文件都可以进行缓存,实现最小化的更新;
热更新是如何实现的?
启动独立的websocket服务,推送更新的数据;
babel相关
babel的作用?
babel主要用来处理js的,把es5以上的版本功能通过Es5实现;
babel的编译过程?
parse转成ast抽象语法树,transform遍历ast进行处理,通过generate把处理之后的ast转成js代码;
怎么实现一个babel插件?
- 先把要转换的js通过在线工具转成ast抽象语法树a1;
- 再把转换之后想要的结果也通过在线工具进行转换成a2;
- 对比两个ast,找到它们之间的不同点;
- 通过babel/parser把要转换的js进行转成ast;
- 通过babel-types把a1转成a2
- 通过babel-core的transform把a2转成js代码;
比如把箭头函数转成普通函数: const a = () => {} 转成 const a = function () {}
先把const a = () => {}通过在线工具转成ast,假设为a1
再把const a = function () {}通过在线工具转成ast,假设为a2
通过bale-types的api把a1转成a2
再通过babel-core的transform转成js代码即可
把js代码转成抽象语法树,处理抽象语法树,生成js代码的babel对应的方法
- babel-core: 把js代码转成ast抽象语法树,并且指定对应的插件进行处理,再把ast转成js代码;
- babel-types: 用来判断ast上的类型和生成ast树上的内容
- babel/parser: 把js代码转成ast树
- babel/traverse: 遍历ast语法树。执行一些钩子函数
- visitors: 访问者,可以访问ast上的某一个属性;如果需要访问import语句就写ast中对应的import属性
npm和npx的区别?
npm是包管理工具;npx是把当前文件中的bin文件作为临时环境变量,npx的时候会从bin文件中查找