构建工具与设计模式
本文是本人整理归纳出来的一篇构建工具方面的面试题及知识点,其中不免有许多漏掉的问题或是答案,发现错误的或者是有什么问题需要补充的朋友们,可以在评论区留言,大家虚心交流,一起进步。
1. 构建工具的作用
构建工具
是前端工程化
中重要的一环,有了构建工具的存在,可以简化
我们的编程过程,在编程的过程中能够使用最新语法
、简单的编写方式
等,然后通过构建工具的处理,使我们最终打包出来的产物变成体积小
、兼容性强
、能够在浏览器运行
的标准语法。
常见的构建工具有webpack
、vite
、rollup
、esbuild
等,目前比较流行的还有Snowpack
、Turbopack
、Bun
等。
2. 说一下webpack的优点
模块化
:webpack支持将项目拆分为多个模块
,最终将多个模块进行打包,编译出最终产物,能够让开发者在开发过程中更便捷的维护
、复用
代码。代码拆分和按需加载
:webpack支持将代码拆分为多个chunk,并且实现按需加载
,降低应用程序的首次加载时间,以及用户在使用过程中的页面加载时间,对spa应用
极为友好。资源优化
:webpack支持代码压缩
、tree-shaking
、静态资源压缩
等功能,能够缩小最终打包产物的体积,去除无用代码。丰富的生态
:webpack经过这么多年的发展,拥有庞大的社区
和完整的生态
,有许多loader以及plugin供开发者使用,这也是为什么webpack久经不衰的原因之一。热更新
:webpack支持热更新
,修改代码时无需再次编译整个文件,只会对改动内容进行重新编译,在开发环境带来了很大的便利。
3. loader是什么,说一下常用的loader以及它们的作用
loader
分为normalLoader
和pitchLoader
,每个loader就是一个函数,该函数可以对源代码
进行转换
,返回值可以被下一个loader
所使用,loader可以是同步
,也可以是异步的
。
(1)loader的配置方式
js
module.exports = {
module: {
rules: [
// 基础使用loader
{
// 匹配需要使用该loader的文件,通常使用正则表达式进行匹配
test: /\.css$/,
use: 'css-loader'
},
{
test: /\.css$/,
// 通过数组可以使用多个loader
use: ['css-loader']
},
{
test: /\.css$/,
use: {
loader: 'css-loader',
// 给loader传递参数
options: {
name: 'lee'
}
}
}
]
}
},
(2)normalLoader
一般的loader都是normalLoader
,也就是我们最终通过module.exports导出的函数
。
js
const myLoader = function (content, map, meta) {
// webpack5内置了getOptions方法
const options = this.getOptions()
console.log(options, '获取webpack配置loder时传入的参数')
console.log('我是normalLoader1')
console.log(this.data, 'pitch阶段传入的参数')
console.log(content, '源文件或上个loader传递的文件')
console.log(map, 'source-map内容')
console.log(meta, '上个loader传递的数据')
return content
}
module.exports = myLoader
(3)pitchLoader
pitchLoader
只是loader的一个属性
,只需要在loader中添加pitch方法
,就可以当作pitchLoader
,可以在该阶段给normalLoader传递一些数据
。
js
// 这个函数就是pitchLoader
// PitchLoader有三个默认参数,
// remainingRequest: 代表未执行pitch阶段的loader数组
// previousRequest: 代表已经执行过pitch阶段的loader数组
// data: 可以传入任意参数, 能够在normal阶段被当前loader通过this.data接收
// tip: currentRequest代表当前正在执行和未执行过pitch阶段的loader数组
myLoader.pitch = function (
remainingRequest,
previousRequest,
data
) {
console.log('我是pitchLoader1')
console.log(remainingRequest)
console.log(previousRequest)
data.a = 123
}
module.exports = myLoader
(4)loader的返回值
loader
的返回值可以被下一个loader
接收,可以通过return
的方式进行返回,也可以通过内置的callback
方法进行返回。
js
// 方式一
const myLoader = function (content) {
return content
}
// 方式二
/**
* @param {*} err 代表是否有错误 null表示没有错误
* @param {*} content 源代码, 也可以是上一个loader处理后的代码
* @param {*} map source-map
* @param {*} meta 给下一个loader传递参数
*/
const myLoader = function (content, map, meta) {
this.callback(null, content, map, meta)
}
module.exports = myLoader
相比直接return
的方式,使用callback函数
进行返回更加的灵活,因为通过这种方式除了返回content之外
还可以返回其它的一些信息,并且可以传递参数。
(5)异步loader
loader
正常情况下是通过同步的方式进行返回的
,但是某些场景下需要异步返回
,这时需要创建一个异步loader
。
js
// 异步loader
const myLoader2 = function (content, map, meta) {
const callback = this.async()
setTimeout(() =>
callback(null, content, map, meta)
}, 1000)
}
module.exports = myLoader2
(6)常用的loader以及作用
babel-loader
:通过babel-loader将代码转换为兼容性更强的代码。css-loader
:可以识别css中的@import以及url()
语句,将多个css文件合并成一个css段落
,还可以进行一些压缩处理;style-loader
:可以将css插入到html文件的style标签中
。less-loader
:对less文件进行处理,处理为正常的css。sass-loader
:对sass文件进行处理,处理为正常的css。postcss-loader
:可以对css属性进行补全浏览器前缀
,使其兼容更多内核的浏览器。
其它常用的loader可以去webpack官网查看,或者有特殊需求的loader可以去npm查找。
4. plugin是什么,说一下常用的plugin以及它们的作用
(1)plugin是什么
plugin
是webpack中的插件
。它的目的在于解决一些loader无法完成
的其他事情,loader一般只工作于模块的加载环节
,而plugin
则可以工作于webpack整个生命周期
,而这一切基于Tapable
这个库,该库是webpack官方维护的一个库,里面提供了多种类型的hook,而webpack又在此基础上提供了许多生命周期hook
,在plugin中,可以访问到这些hook,从而在不同的生命周期执行不同的方法,使得plugin可以贯穿整个webpack的生命周期。
plugin
可以是一个函数
,也可以是一个包含apply方法
的对象。apply
方法会被webpack compiler
调用,并且在整个
编译生命周期都可以访问 compiler 对象。
(2)Tapable中的hooks
js
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
钩子名称 | 执行方式 | 使用要点 |
---|---|---|
SyncHook(同步钩子) | 同步串行 | 回调函数会按照注册的顺序依次执行 |
SyncBailHook(同步熔断钩子) | 同步串行 | 只要监听函数中有一个函数的返回值不为undefined ,则跳过剩下所有的回调函数 |
SyncWaterfallHook(同步瀑布钩子) | 同步串行 | 可以将上一个监听函数的返回值 作为下一个监听函数的参数 |
SyncLoopHook(同步循环钩子) | 同步循环 | 当监听函数被触发的时候,如果该监听函数返回true ,则会反复执行该函数,如果返回undefined 则退出循环 |
AsyncParallelHook(异步并行钩子) | 异步并行 | 钩子支持异步操作,并且并行执行 |
AsyncParallelBailHook(异步并行熔断钩子) | 异步并行 | 在异步并行钩子的基础上增加了熔断机制 ,只要监听函数的返回值不为undefined ,则跳过剩下所有的回调函数 |
AsyncSeriesHook(异步串行钩子) | 异步串行 | 钩子支持异步操作,并且串行执行 |
AsyncSeriesBailHook(异步串行熔断钩子) | 异步串行 | 在异步串行钩子的基础上增加了熔断机制 ,只要监听函数的返回值不为undefined ,则跳过剩下所有的回调函数 |
AsyncSeriesWaterfallHook(异步串行瀑布钩子) | 异步串行 | 上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数 |
(3)webpack的生命周期hook
webpack
一共暴露出了5大类型的hooks
:
Compiler Hooks
:这类hook一般作用于主流程
。Compilation Hooks
:这类hook一般作用于编译流程
。NormalModuleFactory Hooks
:这类hook一般作用于模块创建、解析流程
。ContextModuleFactory Hooks
:这类hook类似于NormalModuleFactory Hooks
,但是它主要用来对使用了webpack独有API require.context模块方法
的文件进行处理。JavascriptParser Hooks
:这类hook一般作用于将模块解析为AST语法树
时。
(4)compiler和compilation的区别
compiler
贯穿了webpack整个生命周期
,compilation
只会在编译模块时产生。compiler
只会在启动webpack
时创建一次,而compilation
则是在每次进行编译时
都会创建一次。- 之所以需要
compilation
是因为在开发环境下,有了热更新的存在,当我们频繁改动模块时
,无需重新创建compiler
了,可以将其复用,只需要重新创建对应模块的compilation对象
。
(5)常见的plugin
HtmlWebpackPlugin
:在单页面开发中,最终只会有一个HTML文件
,该插件可以帮助我们在打包环节
生成一个HTML文件
,并且支持自定义模板
。DefinePlugin
:该插件能够在编译时
将代码中的变量替换为其它值或表达式
。TerserWebpackPlugin
:该插件使用terser
来压缩 JavaScript。
5. loader的执行顺序是什么
配置每一个loader时,可以通过enforce
去设置loader的执行顺序,该属性一共有4个值
,分别为pre(前置)、normal(默认)、inline(行内)、post(后置)
。具体会按照以下顺序执行:
- 先执行
pitchLoader
,顺序为post、inline、normal、pre
。 - 然后执行
normalLoader
,顺序为pre、normal、inline、post
。 - 默认情况下,loader的执行顺序为
从右往左
或者从下往上
。
normalLoader的执行顺序正好是和pitchLoader相反的
,这是因为默认情况下(所有loader都为normal时
),会先去执行每一个loader的pitchLoader,通过一个变量记录当前loader的索引,每执行完一个pitchLoader都会对索引进行++
,然后再进行--
,执行normalLoader,这也是为什么我们的loader执行顺序
和我们书写顺序
是相反的原因。
如果换成代码可以这么理解:
js
const loaders = [loader1, loader2, loader3]
let index = 0
for(let i = 0; i < loaders.length; i++) {
// 执行loaders[i]的pitchLoader
index = i
}
for(let j = index; j >= 0; j--) {
// 执行loaders[j]的normalLoader
}
6. loader和plugin的区别是什么
作用不同
:webpack将所有文件视为模块
,但是无法完成对js以外资源
的解析和打包,loader
的作用就是帮助webpack进行加载、解析其它类型的文件。而plugin
则是完成那些loader无法完成的任务
,它可以使用webpack提供的不同hook
,在不同的生命周期
进行一些处理,比如资源优化
、代码压缩分离
,还可以帮助生成HTML文件等。用法不同
:配置方法不同。影响范围
loader是作用于文件
,而plugin则是作用于整个项目
。
7. 是否手写过loader和plugin
略
8. webpack如何进行分包
webpack
可以通过optimization.splitChunks
进行分包。
(1)splitChunks.chunks
splitChunks.chunks属性
一共有三个值async
、initial
、all
,默认值为async
。
async
当设置为async
时,只会对异步导入
的模块进行分包,同步导入
的模块内容依然会保留在主包内。
js
// webpack配置
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
mode: 'development',
entry: {
a: './src/indexA.js',
b: './src/indexB.js'
},
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].index.js',
chunkFilename: '[name].chunk.js'
},
optimization: {
splitChunks: {
chunks: 'async'
}
},
plugins: [new CleanWebpackPlugin()]
}
// src/indexA.js
// 异步导入utils
import('./js/utils').then(res => {
console.log(res)
})
// 同步导入utils2
import myFun2 from './js/utils2'
myFun2()
// src/indexB.js
// 异步导入utils
import('./js/utils').then(res => {
console.log(res)
})
// 同步导入utils2
import myFun2 from './js/utils2'
myFun2()
因为utils
文件为异步导入
,因此进行了单独分包
,而utils2
文件为同步导入
,因此在a.index.js
和b.index.js
中各存在一份utils2
文件的代码,这无疑是冗余的。
initial
当设置为initial
时,会将同步导入
并且满足条件的文件也进行分包,但是如果同一个文件
,被不同的文件分别异步和同步两种方式导入
,异步导入的会进行拆分,而同步导入的依旧会保留在主包内。
js
// webpack配置
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
mode: 'development',
entry: {
a: './src/indexA.js',
b: './src/indexB.js'
},
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].index.js',
chunkFilename: '[name].chunk.js'
},
optimization: {
splitChunks: {
chunks: 'async'
}
},
plugins: [new CleanWebpackPlugin()]
}
// src/indexA.js
// 异步导入utils
import('./js/utils').then(res => {
console.log(res)
})
// 同步导入utils2
import myFun2 from './js/utils2'
// 同步导入utils3
import myFun3 from './js/utils3'
myFun2()
myFun3()
// src/indexB.js
// 异步导入utils
import('./js/utils').then(res => {
console.log(res)
})
// 同步导入utils2
import myFun2 from './js/utils2'
// 异步导入utils3
import('./js/utils3').then(res => {
console.log(res)
})
myFun2()
此时虽然utils2
是同步导入
,但是也会被进行分包,但是utils3即被同步导入又被异步导入
,虽然进行了分包,但是a.index.js
文件中依然保留了utils3
的代码。
all
使用all
时,会结合async
和initial
的优点,会对满足条件的同步模块和异步模块都进行分包
,并且不会产生冗余代码。因此,一般在开发中,都会将chunks设置为all
。
限制条件
当配置chunks
时,可以给splitChunks
配置一些其它属性配合使用,比如配置splitChunks.minSize(模块体积达到最小值时会分包)、splitChunks.maxSize(模块体积小于最大值时会分包)、splitChunks.minChunks(模块被引用多少次才会进行分包)
等等,还有一些其它配置可参考官方文档。
(2)第三方库的分包(splitChunks.cacheGroups)
有时我们会使用一些第三方库,比如dayjs
、lodash-es
等,当我们多个文件使用这些库时,我们可以把这些库单独打包出来,这时会用到splitChunks.cacheGroups
属性。
js
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
mode: 'development',
entry: {
a: './src/indexA.js',
b: './src/indexB.js'
},
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].index.js',
chunkFilename: '[name].chunk.js'
},
optimization: {
splitChunks: {
chunks: 'all',
// 分包的最小体积单位字节
minSize: 1,
// 将大于maxSize的包拆分于不小于minSize的包
maxSize: 600,
// // 表示最少被使用了几次的包才会分包
minChunks: 1,
cacheGroups: {
// vendor: 包的名字,可以随意命名, test: 匹配规则
vendor: {
test: /[\\/]node_modules[\\/]/,
filename: '[name]_vendor.js' // 包里文件的名字
}
}
}
},
plugins: [new CleanWebpackPlugin()]
}
(3)运行时代码分包(optimization.runtimeChunk)
比如import('xxx').then()
这种异步导入的代码就是一种运行时代码
。
当我们配置了splitChunks.chunks
为all
的时候,异步导入不是可以直接被分包吗,为什么还需要optimization.runtimeChunk
这种方式进行分包呢?
js
// webpack.config.js
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
mode: 'development',
entry: './src/indexA.js',
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].[chunkhash].index.js',
chunkFilename: '[name].[chunkhash].chunk.js'
},
optimization: {
splitChunks: {
chunks: 'all',
// 分包的最小体积单位字节
minSize: 1,
// 将大于maxSize的包拆分于不小于minSize的包
maxSize: 600,
// // 表示最少被使用了几次的包才会分包
minChunks: 1,
}
// runtimeChunk: true
},
plugins: [new CleanWebpackPlugin()]
}
// src/indexA.js
import('./js/utils').then(res => {
console.log(res)
})
// js/utils.js
function myFun() {
console.log('i am 1')
}
export default myFun
没有配置runtimeChunk时
,默认为false
,打包出来的产物如下:
当我们修改js/utils.js
文件的内容,重新进行打包时:
js
// js/utils.js
function myFun() {
console.log('i am 2')
}
export default myFun
打包出来的两个文件,hash值都发生了改变
,这是因为,chunkhash是根据文件内容生成的,当内容改动时,hash值也会变动。如果开启runtimeChunk
之后呢?
js
// 修改js/utils.js
function myFun() {
console.log('i am 3')
}
export default myFun
发现主入口文件的hash值并未改变
。这样有什么好处呢?假如说每次生成的主文件hash值都会改变,我们在浏览器上运行时,每次发布文件都会变化,浏览器每次都要请求新的资源
,那么缓存的作用就几乎没有了,如果说我们开启了runtimeChunk
,即使引入的文件发生了变化,主文件的hash值也不会发生变化,就可以很好的利用浏览器的缓存
。
9. 说一下webpack中热更新的原理
(1)什么是热更新
我们平时在HTML文件中编写代码,想要在浏览器运行时,需要通过浏览器打开我们的HTML文件,如果此时对css、js
一些内容进行了修改,我们需要重新刷新页面
才能够看到最新的效果,如果在开发中也这样的话,是非常影响开发效率的。
而热更新(Hot Module Replacement)
指的就是我们在对一个模块进行修改
、删除
、更新
操作时,不会重启整个项目,只会对修改的内容
进行替换,并不影响当前应用程序的状态,能够提高开发效率。
(2)热更新的原理
webpack
的热更新是基于webpack-dev-server
这个库。
- 当我们以
watch模式
启动webpack,对项目进行编译时,webpack会监听
我们文件的改动; webpack-dev-server
进行启动,这时会帮我们开启两个本地服务
,一个是express服务
,一个是websocket长链接
;- 每一次编译文件之后,会将编译好的文件放在
内存中
,这样是为了减少文件的写入和读取
,性能会更好(之前通过memory-fs库进行实现,现在改为了memfs),第一次
开启服务时,浏览器会访问express服务
,拿到打包后的bundle.js
资源进行加载,在ws中
会有当前的hash值
,浏览器会记录当前的hash值; - 如果某个模块发生了改动,webpack监听到之后会
再次对该模块进行编译
,然后生成一个json文件
和一个js文件
,json文件中存放了哪些模块进行了改动
,js文件中存放了最新变更的代码
。然后通过ws
告知浏览器新的hash值,浏览器发现hash值有变动,于是请求,获取最新资源; - 浏览器使用
上一次的hash值
向服务端请求json文件
,通过json文件得知了哪些模块有改动; - 浏览器再次使用
上一次的hash值
向服务端请求最新的js文件
,然后把相应模块的代码进行替换
,从而达到了热更新的效果
。
10. 说一下你对webpack5中模块联邦的理解
模块联邦(Module Federation)
是webpack5
中的一个新特性,它主要是用于解决公共模块
在多个项目重复引用的问题。
在之前的开发中,如果我们多个项目需要使用同一份配置文件或模块,要么把它复制一份放到另一个项目,要么上传npm包,然后两个项目进行下载使用。但是无论使用哪种方式,当我们想对公共模块进行更新时,使用该模块的项目都需要进行更新,万一漏掉了一个可能会产生许多问题。而模块联邦就可以解决这种问题,当多个项目引用同一个模块时
,只需要更新该模块
,所有引用该模块的项目都会更新为最新的模块。这种情况有利有弊,比如设计有问题时,更新模块可能不兼容旧的使用方式,这时可能会带来更大的麻烦了。
11. webpack性能优化的手段
- 优化loader:可以通过loader的
include/exclude
配置来让loader跳过或精准匹配
某些文件,跳过对那些不必要文件的处理。 - 多进程处理loader:通过
happyPack
插件,可以开启多进程处理
,优化loader处理文件的时间。 - 按需优化:通过
webpack-bundle-analyzer
插件,可以分析打包后文件的依赖关系以及图层结构
,能够让我们更好的分析还有哪些文件资源过大需要优化。 - tree-shaking:利用tree-shaking,移除项目中没有使用到的代码,减少打包体积。
- 按需加载:利用动态导入进行分包,然后实现对不同文件的按需加载。
- gzip压缩:利用
compression-webpack-plugin
插件实现gzip
压缩,对压缩后的文件进行部署(需要服务端配合)。
12. Babel是如何实现编译的
Babel
是一个工具链
,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法
,以便能够运行在当前和旧版本的浏览器或其他环境中。
babel一共有三个阶段
,分别为parse(解析)
、transform(转换)
、generator(生成)
。
- parse:首先babel会将我们的代码解析为
AST抽象语法树
; - transform:然后babel会对AST的内容进行遍历,然后对需要修改的内容进行
修改或补全
,让我们的代码能够满足目标环境的运行条件; - generator:最终生成
字符串形式的代码
,同时还会创建source-map
。
13. tree-shaking的是怎么实现的
tree-shaking
在前端领域被首先使用是rollup
,因为想要实现tree-shaking
需要依赖ES Module
的静态分析,而rollup正是基于ES Module
的一款打包工具。
webpack目前也支持tree-shaking
,并且除了ES Module
模块文件以外,还支持了部分CommonJs
模块的tree-shaking。文档地址
使用tree-shaking的方式
在webpack
中,tree-shaking依赖于terser
,terser是一个插件,全称为terser-webpack-plugin
,在webpack5中已经被内置。同时我们还需要配置webpakc中的optimization.usedExports
以及optimization.minimizer
以及optimization.minimize
属性。
js
// webpack配置
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].index.js',
chunkFilename: '[name].chunk.js'
},
optimization: {
usedExports: true,
// minimize: true,
minimizer: [new TerserPlugin()]
},
module: {},
plugins: [new CleanWebpackPlugin()]
}
// ------------------------------------------------
// utils文件
export function myFun() {
console.log('i am utils')
}
export const a = 1
// ------------------------------------------------
// index文件
import { myFun } from './js/utils'
myFun()
utils文件export
了myFun、a
两个变量,但是在index文件中只import
了myFun,此时变量a并没有用到,因此是可以被删掉的,这时打包看一下:
在打包的文件中,webpack会标注出来export的a是未使用的
,这时我们就可以通过将optimization.minimize
属性设置为true
,从而让terser帮助我们移除掉这些无用代码
。
tree-shaking的副作用
如果说utils文件中的代码变成了以下情况:
js
import { myFun2 } from './utils2'
export function myFun() {
console.log('i am utils')
}
export const a = 1
虽然myFun2
并没有被utils文件所使用,但是因为import
影响了utils文件
,这种影响其它代码的情况称之为副作用
,当有副作用时,terser
不会帮我们消除这部分代码,即使它没有被使用。
消除副作用
为了让最终tree-shaking的代码更加的简洁、准确,我们自身在编写代码时要保证尽量不产生副作用,同时我们可以通过package.json
文件中的sideEffects
属性,来告知terser
哪些文件有副作用,哪些文件没有副作用,从而让terser更好的进行tree-shaking
。
sideEffects
可以为true
、false
、数组
。
- true:代表所有文件
都有副作用
; - false:代表所有文件
都没有副作用
,如果未使用,可以被安全删除; - 数组:可以配置哪些文件或模块
没有副作用
;
修改package.json配置,再次进行打包:
json
{
"name": "webpack-demo",
"version": "1.0.0",
"sideEffects": false
}
再次编译的代码,已经没有utils2文件的内容了。
总结
webpack的tree-shaking是基于ES Module
的静态分析的,但是在webpack5中也支持了部分CommonJS
语法,webpack会通过静态分析,记录export内容被使用的情况,并进行注释记录,最终由terser插件完成对这些无用代码的tree-shaking。
14. webpack的构建流程
15. webpack和vite的区别
- 开发环境下
vite不需要打包
,因为它基于ES Module
,能够按需加载
,而webpack
则需要对所有文件进行打包
,分析依赖,流程较为繁琐,大型项目构建较慢; - 生产环境下
vite是由rollup
完成的打包,而webpack不是; - webpack发展时间较长,
社区生态
也比较完善,而vite起步较晚,在生态方面略逊于webpack
;
16. vite为什么比webpack快
Vite
在开发环境下使用esbuild
进行构建,而生产环境下使用rollup
,esbuild是使用GO语言
编写的属于编译型语言
,而webpack是使用js
编写的,属于解释性语言
,在这一点上天生就落了下风;Vite
利用了最新浏览器支持ESModule
的特性,不会对项目进行打包,而是遇到import语句
时,通过网络请求,请求需要加载的模块,做到了按需请求加载
,而webpack
则不同,webpack通过入口文件逐层分析各个文件的依赖,然后建立依赖关系,打包成最终文件让浏览器直接运行,这就导致了依赖过多或层级过深时,webpack就会打包越慢。
17. git fetch和git pull的区别
git fetch
只会拉取远程的内容分支到本地,但是不会自动合并
,是否合并取决于用户的操作。
而git pull
会拉取远程对应分支的内容到本地,并且与本地分支进行合并
,git pull相当于git fetch + git merge
。
18. git rebase和git merge的区别
git rebase
也可以合并代码,如果把提交记录当作一条线时,当在a分支上使用git rebase b时,会以b分支的最后一个commit为基点
,然后将a分支上与b分支有差异的commit
向上延伸。也可以理解为在b分支最后一个commit之后又提交了n个commit
。
此时有test和test1两个分支,在test分支上使用git rebase feature/test1
:
get merge
则不同,get merge会将两个分支进行合并
,两条分支线在合并处会产生一个交点并新增一条commit记录
。
因此,如果我们想表明某个分支是通过合并进来的,就使用git merge,如果我们想让两个分支进行合并,但是又希望分支线保持一条直线,可以使用git rebase或git cherry-pick的方式进行合并。
git pull --rebase
git pull --rebase
一般用于多人协作时拉取远程代码,如果直接使用git pull,可能远程和本地都有提交,此时代码就会冲突。如果使用git pull --rebase
,就会先将远程的代码拉取到暂存区,然后以本地最新代码为基准,如果远程有改动的代码,就会在当前基准上创建新的commit。
19. 如何回退提交
reset
可以通过git reset [ --soft | --mixed | --hard ] [< commitid >]
的方式进行代码git回退。
- 当参数为
soft
时,会保留回退位置的commit之后的代码,并且为暂存状态; - 当参数为
mixed
时,会保留回退位置的commit之后的代码,但是没有暂存; - 当参数为
hard
时,不会保留回退位置的commit之后的代码;
revert
git revert
不会像git reset
一样,将某个commit之后的commit全都清除,它的作用为修改某一次commit
,然后产生一个新的commit
。
比如有3个commit,分别为a、b、c
,当执行git revert b
时,让用户选择是否保留commit c以及commit c的代码
,最终会生成一个Revert commit
。
20. 谈谈git flow
git flow
是git官方推崇的一种工作流
,在大型团队合作项目中,如果能落实git flow的话,能够给团队合作带来很大提升。
git flow中规定了有五种分支
,分别为master
、develop
、hotfix
、release
、feature
。
master
:该分支是主分支,作为发布分支
,只接收hotfix和release的合并;release
:该分支为预发布分支,作为提测分支
,由develop分支拉取,最终合并进master;hotfix
:该分支为bug修复分支,当有线上bug时,由master分支拉取hotfix进行修改,最终合并给master;develop
:该分支作为开发分支,开发新需求时,以该分支为基准,拉取feature分支,feature合并之后,由develop拉取release进行提测;feature
:该分支作为需求分支,每一个独立的需求单独拉取feature,开发完成之后合并进develop。
在实际工作中应该视情况而定,如果项目较繁杂,人员较多,可以使用五分支模型
,如果较少时,可以变成三分支
等。