Webpack篇/前端工程化
1. 有哪些常见的loader和plugin,你用过哪些
loader
file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
source-map-loader:加载额外的 Source Map 文件,以方便断点调试
image-loader:加载并且压缩图片文件
babel-loader:把 ES6 转换成 ES5
css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
eslint-loader:通过 ESLint 检查 JavaScript 代码
plugin
define-plugin:定义环境变量
commons-chunk-plugin:提取公共代码
uglifyjs-webpack-plugin:通过UglifyES压缩ES6代码
2. loader 和 plugin 的区别是什么?
不同的作用:
- Loader直译为"加载器"。
Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。 所以Loader的作用是让webpack拥有了加载和解析非JavaScript文件的能力。 - Plugin直译为"插件"。
Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
不同的用法:
- Loader在module.rules中配置,也就是说他作为模块的解析规则而存在。类型为数组,每一项都是一个Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)。
- Plugin在plugins中单独配置,类型为数组,每一项是一个plugin的实例,参数都通过构造函数传入。
3. 如何按需加载代码?
Vue UI组件库的按需加载 为了快速开发前端项目,经常会引入现成的UI组件库如ElementUI、iView等,但是他们的体积和他们所提供的功能一样,是很庞大的。
而通常情况下,我们仅仅需要少量的几个组件就足够了,但是我们却将庞大的组件库打包到我们的源码中,造成了不必要的开销。
不过很多组件库已经提供了现成的解决方案,如Element出品的babel-plugin-component和AntDesign出品的babel-plugin-import 安装以上插件后,
在.babelrc配置中或babel-loader的参数中进行设置,即可实现组件按需加载了。
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
单页应用的按需加载 现在很多前端项目都是通过单页应用的方式开发的,但是随着业务的不断扩展,会面临一个严峻的问题——首次加载的代码量会越来越多,影响用户的体验。
通过import(*)语句来控制加载时机,webpack内置了对于import(*)的解析,
会将import(*)中引入的模块作为一个新的入口在生成一个chunk。
当代码执行到import(*)语句时,会去加载Chunk对应生成的文件。
import()会返回一个Promise对象,所以为了让浏览器支持,需要事先注入Promise polyfill
4. 如何提高webpack的构建速度
- 多入口情况下,使用
CommonsChunkPlugin
来提取公共代码 - 通过
externals
配置来提取常用库 - 利用
DllPlugin
和DllReferencePlugin
预编译资源模块 通过DllPlugin
来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过DllReferencePlugin
将预编译的模块加载进来。 - 使用
Happypack
实现多线程加速编译 - 使用
webpack-uglify-parallel
来提升uglifyPlugin
的压缩速度。 原理上webpack-uglify-parallel
采用了多核并行压缩来提升压缩速度 - 使用
Tree-shaking
和Scope Hoisting
来剔除多余代码
-
使用
高版本
的 Webpack 和 Node.js -
多进程/多实例构建
:HappyPack(不维护了)、thread-loader -
压缩代码
-
多进程并行压缩
-
webpack-paralle-uglify-plugin
-
uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)
-
terser-webpack-plugin 开启 parallel 参数
-
-
通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS。
-
-
图片压缩
-
使用基于 Node 库的 imagemin (很多定制选项、可以处理多种图片格式)
-
配置 image-webpack-loader
-
-
缩小打包作用域
:-
exclude/include (确定 loader 规则范围)
-
resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
-
resolve.mainFields 只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)
-
resolve.extensions 尽可能减少后缀尝试的可能性
-
noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
-
IgnorePlugin (完全排除模块)
-
合理使用alias
-
-
提取页面公共资源
:-
基础包分离:
-
使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
-
使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件
-
-
-
DLL
:-
使用 DllPlugin 进行分包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间。
-
HashedModuleIdsPlugin 可以解决模块数字id问题
-
-
充分利用缓存提升二次构建速度
:-
babel-loader 开启缓存
-
terser-webpack-plugin 开启缓存
-
使用 cache-loader 或者 hard-source-webpack-plugin
-
-
Tree shaking
-
打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率
-
禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
-
使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码
- purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)
-
-
Scope hoisting
-
构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突
-
必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法
-
-
动态Polyfill
- 建议采用 polyfill-service 只给用户返回需要的polyfill,社区维护。 (部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill)
更多优化请参考官网-构建性能
5. 前端性能优化
三个方面来说明前端性能优化
一: webapck优化与开启gzip压缩
- babel-loader用 include 或 exclude 来帮我们避免不必要的转译,不转译node_moudules中的js文件 其次在缓存当前转译的js文件,设置loader: 'babel-loader?cacheDirectory=true'
- 文件采用按需加载等等
- 具体的做法非常简单,只需要你在你的 request headers 中加上这么一句:accept-encoding:gzip
- 图片优化,采用svg图片或者字体图标
- 浏览器缓存机制,它又分为强缓存和协商缓存
二:本地存储——从 Cookie 到 Web Storage、IndexedDB
说明一下SessionStorage和localStorage还有cookie的区别和优缺点
三:代码优化
- 事件代理
- 事件的节流和防抖
- 页面的回流和重绘
- EventLoop事件循环机制
- 代码优化等等
6. 如何利用webpack来优化前端性能?(提高性能和体验)
用webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运行快速高效。
- 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用webpack的
UglifyJsPlugin
和ParallelUglifyPlugin
来压缩JS文件, 利用cssnano
(css-loader?minimize)来压缩css - 利用CDN加速。在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用webpack对于
output
参数和各loader的publicPath
参数来修改资源路径 - 删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数
--optimize-minimize
来实现 - 提取公共代码。
7. 网站性能优化
样本一
- 减少 http 请求次数:CSS Sprites, JS、CSS 源码压缩、图片大小控制合适;网页 Gzip,CDN 托管,data 缓存 ,图片服务器。
- 前端模板 JS+数据,减少由于 HTML 标签导致的带宽浪费,前端用变量保存 AJAX 请求结果,每次操作本地变量,不用请求,减少请求次数
- 用 innerHTML 代替 DOM 操作,减少 DOM 操作次数,优化 javascript 性能。
- 当需要设置的样式很多时设置 className 而不是直接操作 style。
- 少用全局变量、缓存 DOM 节点查找的结果。减少 IO 读取操作。
- 避免使用 CSS Expression(css 表达式)又称 Dynamic properties(动态属性)。
- 图片预加载,将样式表放在顶部,将脚本放在底部 加上时间戳。
- 避免在页面的主体布局中使用 table,table 要等其中的内容完全下载之后才会显示出来,显示比 div+css 布局慢。
样本二
- 减少 HTTP 请求
- 减少 DOM 操作
- 避免不必要的重绘与重排
- 优化 CSS 选择器(从右向左匹配)
- CSS/JS minify,减少文件体积
- 开启 Gzip 压缩
- 将 CSS 放到顶部,JavaScript 放到尾部
- 压缩图片以及使用 CSS Sprite
- 使用 CDN 加速,适当进行文件缓存
- 合理控制 cookie 大小(每次请求都会包含 cookie)
8. 说说你对前端工程化的理解
我个人理解主要是从模块化、组件化、规范化、自动化这四个方面来实现前端工程化的:
- 模块化: 简单来说,模块化就是将一个大文件拆分成相互依赖的小文件,再进行统一的拼装和加载。
- 组件化: 组件化有点类似模块化,但模块化主要是在文件层面上的,而组件化则是在平时写代码的时候就有封装组件的意识,让组件能够多次复用
- 规范化: 比如目录结构的制定、代码的规范、接口的规范、命名的规范等,遵循一套规范可以让代码更容易维护,出问题的时候也方便快速定位
- 自动化: 任何简单机械的重复劳动都应该让机器去完成,比如自动化测试、自动化部署等
9. 谈谈你对 webpack 的看法(webpack 的特点)
WebPack 是一个模块打包工具,你可以使用 WebPack 管理你的模块依赖,并编绎输出模块们所需的静态文件。它能够很好地管理、打包 Web 开发中所用到的 HTML、JavaScript、CSS 以及各种静态文件(图片、字体等),让开发过程更加高效。对于不同类型的资源,webpack 有对应的模块加载器。webpack 模块打包器会分析模块间的依赖关系,最后生成了优化且合并后的静态资源。
webpack 的两大特色:
- code splitting(可以自动完成)
- loader 可以处理各种类型的静态文件,并且支持串联操作
webpack 是以 commonJS 的形式来书写脚本滴,但对 AMD/CMD 的支持也很全面,方便旧项目进行代码迁移。
webpack 具有 requireJs 和 browserify 的功能,但仍有很多自己的新特性:
- 对 CommonJS 、 AMD 、ES6 的语法做了兼容
- 对 js、css、图片等资源文件都支持打包
- 串联式模块加载器以及插件机制,让其具有更好的灵活性和扩展性,例如提供对 CoffeeScript、ES6 的支持
- 有独立的配置文件 webpack.config.js
- 可以将代码切割成不同的 chunk,实现按需加载,降低了初始化时间
- 支持 SourceUrls 和 SourceMaps,易于调试
- 具有强大的 Plugin 接口,大多是内部插件,使用起来比较灵活
- webpack 使用异步 IO 并具有多级缓存。这使得 webpack 很快且在增量编译上更加快
10. 平时如何管理你的项目?
a. 先期团队必须确定好全局样式(globe.css),编码模式(utf-8) 等;
b. 编写习惯必须一致(例如都是采用继承式的写法,单样式都写成一行);
c. 标注样式编写人,各模块都及时标注(标注关键样式调用的地方);
d. 页面进行标注(例如 页面 模块 开始和结束);
e. CSS 跟 HTML 分文件夹并行存放,命名都得统一(例如 style.css);
f. JS 分文件夹存放 命名以该 JS 功能为准的英文翻译。
g. 图片采用整合的 images.png png8 格式文件使用 尽量整合在一起使用方便将来的管理
11. 首屏、白屏时间如何计算?
Performance 接口可以获取到当前页面中与性能相关的信息。
该类型的对象可以通过调用只读属性 Window.performance 来获得。
白屏时间:
performance.timing.responseStart - performance.timing.navigationStart
首屏时间
window.onload = () => {
new Date() - performance.timing.responseStart
}
12.项目开发经历了哪几个阶段
- 需求分析及变更管理
- 项目模型及业务流程分析
- 系统分析及建模设计
- 界面设计及代码开发
- 系统测试,部署和文档编写
- 维护
13. 移动端性能优化
- 尽量使用 css3 动画,开启硬件加速。
- 适当使用 touch 事件代替 click 事件。避免使用 css3 渐变阴影效果。
- 尽可能少的使用 box-shadow 与 gradients。box-shadow 与 gradients 往往都是页面的性能杀手
14. 什么是响应式设计?
它是关于网页制作的过程中让不同的设备有不同的尺寸和不同的功能。 响应式设计是让所有的人能在这些设备上让网站运行正常
15. Ascii、GBK、UTF、Unicode
-
Ascii(1 个字节 1 个字符)
-
GBK 是国内的编码标准(汉字 2 个字节)
-
Unicode 是国际编码标准(统一 2 个字节表示一个字符)
-
UTF 是 Unicode 实现的另一个标准
unicode 同样也不完美,这里就有两个的问题,一个是,如何才能区别 unicode 和 ascii?
由于”半角”英文符号只需要用到低 8 位,所以其高 8 位永远是 0,因此这种大气的方案在保存英文文本时会多浪费一倍的空间
unicode 在很长一段时间内无法推广,直到互联网的出现,为解决 unicode 如何在网络上传输的问题,于是面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF-8 就是每次 8 个位传输数据,而 UTF-16 就是每次 16 个位。UTF-8 就是在互联网上使用最广的一种 unicode 的实现方式,这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用 1~4 个字节表示一个符号,根据不同的符号而变化字节长度,当字符在 ASCII 码的范围时,就用一个字节表示,保留了 ASCII 字符一个字节的编码做为它的一部分,注意的是 unicode 一个中文字符占 2 个字节,而 UTF-8 一个中文字符占 3 个字节)。从 unicode 到 utf-8 并不是直接的对应,而是要过一些算法和规则来转换。
16. 页面大量图片,如何优化加载,优化用户体验
图片懒加载,在页面上的未可视区域可以添加一个滚动条事件,判断图片位置与浏览器顶端的距离与页面的距离,如果前者小于后者,优先加载。
如果为幻灯片、相册等,可以使用图片预加载技术,将当前展示图片的前一张和后一张优先下载。
如果图片为 css 图片,可以使用 CSSsprite,SVGsprite,Iconfont、Base64 等技术。
如果图片过大,可以使用特殊编码的图片,加载时会先加载一张压缩的特别厉害的缩略图,以提高用户体验。
如果图片展示区域小于图片的真实大小,则因在服务器端根据业务需要先行进行图片压缩,图片压缩后大小与展示一致。
17. 渲染优化
1、禁止使用 iframe(阻塞父文档 onload 事件);
*iframe 会阻塞主页面的 Onload 事件;
*搜索引擎的检索程序无法解读这种页面,不利于 SEO;
*iframe 和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。
使用 iframe 之前需要考虑这两个缺点。如果需要使用 iframe,最好是通过 javascript
动态给 iframe 添加 src 属性值,这样可以绕开以上两个问题。
2、禁止使用 gif 图片实现 loading 效果(降低 CPU 消耗,提升渲染性能);
3、使用 CSS3 代码代替 JS 动画(尽可能避免重绘重排以及回流)css3 平面动画开启 translateZ(0),
打开浏览器 3d 加速,在一定程度可缓解卡顿。不宜多用;
4、对于一些小图标,可以使用 base64 位编码,以减少网络请求。但不建议大图使用,比较耗费 CPU;
小图标优势在于: 1.减少 HTTP 请求; 2.避免文件跨域; 3.修改及时生效;
5、页面头部的`<style></style>` 会阻塞页面;(因为 Renderer 进程中 JS 线程和渲染线程是互斥的);
6、页面头部`<script</script>` 会阻塞页面;(因为 Renderer 进程中 JS 线程和渲染线程是互斥的);
7、页面中空的 href 和 src 会阻塞页面其他资源的加载 (阻塞下载进程);
8、网页 Gzip,CDN 托管,data 缓存 ,图片服务器;
9、前端模板 JS+数据,减少由于 HTML 标签导致的带宽浪费,前端用变量保存 AJAX
请求结果,每次操作本地变量,不用请求,减少请求次数
10、用 innerHTML 代替 DOM 操作,减少 DOM 操作次数,优化 javascript 性能。
11、当需要设置的样式很多时设置 className 而不是直接操作 style。
12、少用全局变量、缓存 DOM 节点查找的结果。减少 IO 读取操作。
13、避免使用 CSS Expression(css 表达式)又称 Dynamic properties(动态属性)。
14、图片预加载,将样式表放在顶部,将脚本放在底部 加上时间戳。
15、 避免在页面的主体布局中使用 table,table 要等其中的内容完全下载之后才会显示出来,显示比 div+css 布局慢。
对普通的网站有一个统一的思路,就是尽量向前端优化、减少数据库操作、减少磁盘 IO。
向前端优化指的是,在不影响功能和体验的情况下,能在浏览器执行的不要在服务端执行,
能在缓存服务器上直接返回的不要到应用服务器,程序能直接取得的结果不要到外部取得,
本机内能取得的数据不要到远程取,内存能取到的不要到磁盘取,缓存中有的不要去数据库查询。
减少数据库操作指减少更新次数、缓存结果减少查询次数、将数据库执行的操作尽可能的让你的程序完成(例如 join 查询),
减少磁盘 IO 指尽量不使用文件系统作为缓存、减少读写文件次数等。程序优化永远要优化慢的部分,换语言是无法“优化”的。
16、通过改变 src 的情况下\*\*.MP3(不同于 mp3)在移动端有可能不能播放。
18. 什么是“前端路由"?什么时候适合使用“前端路由"? “前端路由"有哪些优点和缺点?
-
什么是前端路由?
路由是根据不同的 url 地址展示不同的内容或页面
前端路由就是把不同路由对应不同的内容或页面的任务交给前端来做,之前是通过服务端根据 url 的不同返回不同的页面实现的。
-
什么时候使用前端路由?
在单页面应用,大部分页面结构不变,只改变部分内容的使用
-
前端路由有什么优点和缺点?
优点
用户体验好,不需要每次都从服务器全部获取,快速展现给用户
缺点
使用浏览器的前进,后退键的时候会重新发送请求,没有合理地利用缓存
单页面无法记住之前滚动的位置,无法在前进,后退的时候记住滚动的位置
19. source map是什么?生产环境怎么用?
source map
是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。
map文件只要不打开开发者工具,浏览器是不会加载的。
线上环境一般有三种处理方案:
-
hidden-source-map
:借助第三方错误监控平台 Sentry 使用 -
nosources-source-map
:只会显示具体行数以及查看源代码的错误栈。安全性比 sourcemap 高 -
sourcemap
:通过 nginx 设置将 .map 文件只对白名单开放(公司内网)
注意:避免在生产中使用 inline-
和 eval-
,因为它们会增加 bundle 体积大小,并降低整体性能。
20. Webpack构建流程简单说一下
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
- 确定入口:根据配置中的 entry 找出所有的入口文件;
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
- 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
在以上过程中,Webpack
会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
简单说
-
初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
-
编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
-
输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中
- 从entry里配置的module开始递归解析entry依赖的所有module
- 每找到一个module,就会根据配置的loader去找对应的转换规则
- 对module进行转换后,再解析出当前module依赖的module
- 这些模块会以entry为单位分组,一个entry和其所有依赖的module被分到一个组Chunk
- 最后webpack会把所有Chunk转换成文件输出
- 在整个流程中webpack会在恰当的时机执行plugin里定义的逻辑
21. 使用webpack开发时,你用过哪些可以提高效率的插件?
-
webpack-dashboard
:可以更友好的展示相关打包信息。 -
webpack-merge
:提取公共配置,减少重复配置代码 -
speed-measure-webpack-plugin
:简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。 -
size-plugin
:监控资源体积变化,尽早发现问题 -
HotModuleReplacementPlugin
:模块热替换
22. 模块打包原理知道吗?
Webpack 实际上为每个模块创造了一个可以导出和导入的环境,本质上并没有修改 代码的执行逻辑,代码执行顺序与模块加载顺序也完全一致。
23. 文件监听原理呢?
在发现源码发生变化时,自动重新构建出新的输出文件。
Webpack开启监听模式,有两种方式:
-
启动 webpack 命令时,带上 --watch 参数
-
在配置 webpack.config.js 中设置 watch:true
缺点:每次需要手动刷新浏览器
原理:轮询判断文件的最后编辑时间是否变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout
后再执行。
module.export = {
// 默认false,也就是不开启
watch: true,
// 只有开启监听模式时,watchOptions才有意义
watchOptions: {
// 默认为空,不监听的文件或者文件夹,支持正则匹配
ignored: /node_modules/,
// 监听到变化发生后会等300ms再去执行,默认300ms
aggregateTimeout:300,
// 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
poll:1000
}
}
24. 说一下 Webpack 的热更新原理吧
(敲黑板,这道题必考)
Webpack
的热更新又称热替换(Hot Module Replacement
),缩写为 HMR
。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket
,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax
请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp
请求获取该chunk的增量更新。
后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin
来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader
和 vue-loader
都是借助这些 API 实现 HMR。
细节请参考Webpack HMR 原理解析
25. webpack的热更新是如何做到的?说明其原理?
webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
原理:

首先要知道server端和client端都做了处理工作
- 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
- 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
- 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
- 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
- webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
- HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
- 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
- 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。
24. 如何对bundle体积进行监控和分析?
VSCode
中有一个插件 Import Cost
可以帮助我们对引入模块的大小进行实时监测,还可以使用 webpack-bundle-analyzer
生成 bundle
的模块组成图,显示所占体积。
bundlesize
工具包可以进行自动化资源体积监控。
26. 文件指纹是什么?怎么用?
文件指纹是打包后输出的文件名的后缀。
-
Hash
:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改 -
Chunkhash
:和 Webpack 打包的 chunk 有关,不同的 entry 会生出不同的 chunkhash -
Contenthash
:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变
27. 在实际工程中,配置文件上百行乃是常事,如何保证各个loader按照预想方式工作?
可以使用 enforce
强制执行 loader
的作用顺序,pre
代表在所有正常 loader 之前执行,post
是所有 loader 之后执行。(inline 官方不推荐使用)
28. 代码分割的本质是什么?有什么意义呢?
代码分割的本质其实就是在源代码直接上线
和打包成唯一脚本main.bundle.js
这两种极端方案之间的一种更适合实际场景的中间状态。
「用可接受的服务器性能压力增加来换取更好的用户体验。」
源代码直接上线:虽然过程可控,但是http请求多,性能开销大。
打包成唯一脚本:一把梭完自己爽,服务器压力小,但是页面空白期长,用户体验不好。
29. 是否写过Loader?简单描述一下编写loader的思路?
Loader 支持链式调用,所以开发上需要严格遵循“单一职责”,每个 Loader 只负责自己需要负责的事情。
Loader的API 可以去官网查阅
-
Loader 运行在 Node.js 中,我们可以调用任意 Node.js 自带的 API 或者安装第三方模块进行调用
-
Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串,当某些场景下 Loader 处理二进制文件时,需要通过 exports.raw = true 告诉 Webpack 该 Loader 是否需要二进制数据
-
尽可能的异步化 Loader,如果计算量很小,同步也可以
-
Loader 是无状态的,我们不应该在 Loader 中保留状态
-
使用 loader-utils 和 schema-utils 为我们提供的实用工具
-
加载本地 Loader 方法
-
Npm link
-
ResolveLoader
-
30. 是否写过Plugin?简单描述一下编写Plugin的思路?
webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在特定的阶段钩入想要添加的自定义功能。Webpack 的 Tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。
Plugin的API 可以去官网查阅
-
compiler 暴露了和 Webpack 整个生命周期相关的钩子
-
compilation 暴露了与模块和依赖有关的粒度更小的事件钩子
-
插件需要在其原型上绑定apply方法,才能访问 compiler 实例
-
传给每个插件的 compiler 和 compilation对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件
-
找出合适的事件点去完成想要的功能
-
emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(emit 事件是修改 Webpack 输出资源的最后时机)
-
watch-run 当依赖的文件发生变化时会触发
-
-
异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住
31. 是否写过Loader和Plugin?描述一下编写loader或plugin的思路?
Loader像一个"翻译官"把读到的源文件内容转义成新的文件内容,并且每个Loader通过链式操作,将源文件一步步翻译成想要的样子。
编写Loader时要遵循单一原则,每个Loader只做一种"转义"工作。 每个Loader的拿到的是源文件内容(source
),可以通过返回值的方式将处理后的内容输出,也可以调用this.callback()
方法,将内容返回给webpack。 还可以通过 this.async()
生成一个callback
函数,再用这个callback将处理后的内容输出出去。 此外webpack
还为开发者准备了开发loader的工具函数集——loader-utils
。
相对于Loader而言,Plugin的编写就灵活了许多。 webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
32. 聊一聊Babel原理吧
大多数JavaScript Parser遵循 estree
规范,Babel 最初基于 acorn
项目(轻量级现代 JavaScript 解析器) Babel大概分为三大部分:
-
解析:将代码转换成 AST
-
词法分析:将代码(字符串)分割为token流,即语法单元成的数组
-
语法分析:分析token流(上面生成的数组)并生成 AST
-
-
转换:访问 AST 的节点进行变换操作生产新的 AST
- Taro就是利用 babel 完成的小程序语法转换
-
生成:以新的 AST 为基础生成代码
想了解如何一步一步实现一个编译器的同学可以移步 Babel 官网曾经推荐的开源项目 the-super-tiny-compiler
33. webpack的基本功能和工作原理?
- 代码转换:TypeScript 编译成 JavaScript、SCSS 编译成 CSS 等等
- 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等
- 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载
- 模块合并:在采用模块化的项目有很多模块和文件,需要构建功能把模块分类合并成一个文件
- 自动刷新:监听本地源代码的变化,自动构建,刷新浏览器
- 代码校验:在代码被提交到仓库前需要检测代码是否符合规范,以及单元测试是否通过
- 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。
34. webpack打包原理
将所有依赖打包成一个bundle.js,通过代码分割成单元片段按需加载
35. 什么是webpack,与gulp,grunt有什么区别
- webpack是一个模块打包工具,可以递归地打包项目中的所有模块,最终生成几个打包后的文件。
- 区别:webpack支持代码分割,模块化(AMD,CommonJ,ES2015),全局分析
三者都是前端构建工具,grunt和gulp在早期比较流行,现在webpack相对来说比较主流,不过一些轻量化的任务还是会用gulp来处理,比如单独打包CSS文件等。
grunt和gulp是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。
webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能。
所以总结一下:
- 从构建思路来说
gulp和grunt需要开发者将整个前端构建过程拆分成多个`Task`,并合理控制所有`Task`的调用关系
webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工
- 对于知识背景来说
gulp更像后端开发者的思路,需要对于整个流程了如指掌 webpack更倾向于前端开发者的思路
36. webpack如何配置单页面和多页面的应用程序?
单页应用可以理解为webpack的标准模式,直接在entry
中指定单页应用的入口即可,这里不再赘述
多页应用的话,可以使用webpack的 AutoWebPlugin
来完成简单自动化的构建,但是前提是项目的目录结构必须遵守他预设的规范。 多页应用中要注意的是:
-
每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载。比如,每个页面都引用了同一套css样式表
-
随着业务的不断扩展,页面可能会不断的追加,所以一定要让入口的配置足够灵活,避免每次添加新页面还需要修改构建配置
-
单个页面
module.exports = {
entry: './path/to/my/entry/file.js'
}
- 多页面应用程序
module.entrys = {
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js'
}
}
37. webpack-dev-server和http服务器如nginx有什么区别?
webpack-dev-server使用内存来存储webpack开发环境下的打包文件,并且可以使用模块热更新,相比传统http服务器开发更加简单高效
38. dev-server是怎么跑起来的
webpack-dev-server支持两种模式来自动刷新页面
-
iframe模式(页面放在iframe中,当发送改变时重载) 无需额外配置,只要以这种格式url访问即可。
http://localhost:8080/webpack-dev-server/index.html
-
inline模式(将webpack-dev-server的客户端入口添加到bundle中) inline模式下url不用发生变化,但启动inline模式分两种情况
- 以命令行启动webpack-dev-server有两种方式
方式1 在命令行中添加--inline命令
方式2 在webpack-config.js添加devServer:{inline: true}
- 以node.js API启动有两种方式
方式1 添加webpack-dev-server/client?http://localhost:8080/
到webpack配置的entry入口点config.entry.app.unshift("webpack-dev-server/client?http://localhost:8080/");
方式2 将<script src="http://localhost:8080/webpack-dev-server.js"></script>
添加到html文件中
- 以命令行启动webpack-dev-server有两种方式
39. 什么是长缓存?在webpack中如何做到长缓存优化?
- 浏览器在用户访问页面的时候,为了加快加载速度会对用户访问的静态资源进行存储,但是每一次代码升级或更新都需要浏览器下载新的代码,最简单方便的方式就是引入新的文件名称。
- webpack中可以在output中指定chunkhash,并且分离经常更新的代码和框架代码。通过NameModulesPlugin或HashedModuleIdsPlugin使再次打包文件名不变。
40. 什么是Tree-shaking?CSS可以Tree-shaking?
Tree-shaking是指在打包中取出那些引入了但在代码中没有被用到的死代码。webpack中通过uglifysPlugin来Tree-shaking JS。CSS需要使用purify-CSS
41. 与webpack类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用webpack?
同样是基于入口的打包工具还有以下几个主流的:
从应用场景上来看:
- webpack适用于大型复杂的前端站点构建
- rollup适用于基础库的打包,如vue、react
- parcel适用于简单的实验性项目,他可以满足低门槛的快速看到效果
由于parcel在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用parcel
Git篇
1. Git和SVN有什么区别?

2. 什么是Git?
我建议你先通过了解 git 的架构再来回答这个问题,如下图所示,试着解释一下这个图:
- Git 是分布式版本控制系统(DVCS)。它可以跟踪文件的更改,并允许你恢复到任何特定版本的更改。
- 与 SVN 等其他版本控制系统(VCS)相比,其分布式架构具有许多优势,一个主要优点是它不依赖于中央服务器来存储项目文件的所有版本。
- 每个开发人员都可以“克隆”我在图中用“Local repository”标注的存储库的副本,并且在他的硬盘驱动器上具有项目的完整历史记录,因此当服务器中断时,你需要的所有恢复数据都在你队友的本地 Git 存储库中。
- 还有一个中央云存储库,开发人员可以向其提交更改,并与其他团队成员进行共享,如图所示,所有协作者都在提交更改“远程存储库”。

3. 在 Git 中提交的命令是什么?
用于写入提交的命令是 git commit -a
。
现在解释一下 -a
标志, 通过在命令行上加 -a
指示 git 提交已修改的所有被跟踪文件的新内容。还要提一下,如果你是第一次需要提交新文件,可以在在 git commit -a
之前先 git add <file>
。
4. 什么是 Git 中的“裸存储库”?
你应该说明 “工作目录” 和 “裸存储库” 之间的区别。
Git 中的 “裸” 存储库只包含版本控制信息而没有工作文件(没有工作树),并且它不包含特殊的 .git
子目录。相反,它直接在主目录本身包含 .git
子目录中的所有内容,其中工作目录包括:
- 一个
.git
子目录,其中包含你的仓库所有相关的 Git 修订历史记录。 - 工作树,或签出的项目文件的副本。
5. Git 是用什么语言编写的?
你需要说明使用它的原因,而不仅仅是说出语言的名称。我建议你这样回答:
Git使用 C 语言编写。 GIT 很快,C 语言通过减少运行时的开销来做到这一点。
6. 在Git中,你如何还原已经 push 并公开的提交?
这个问题可以有两个答案,你回答时也要保包含这两个答案,因为根据具体情况可以使用以下选项:
- 删除或修复新提交中的错误文件,并将其推送到远程存储库。这是修复错误的最自然方式。对文件进行必要的修改后,将其提交到我将使用的远程存储库
git commit -m "commit message"
- 创建一个新的提交,撤消在错误提交中所做的所有更改。可以使用命令:
git revert <name of bad commit>
7. git pull 和 git fetch 有什么区别?
git pull
命令从中央存储库中提取特定分支的新更改或提交,并更新本地存储库中的目标分支。
git fetch
也用于相同的目的,但它的工作方式略有不同。当你执行 git fetch
时,它会从所需的分支中提取所有新提交,并将其存储在本地存储库中的新分支中。如果要在目标分支中反映这些更改,必须在 git fetch
之后执行git merge
。只有在对目标分支和获取的分支进行合并后才会更新目标分支。为了方便起见,请记住以下等式:
git pull = git fetch + git merge
8. git中的“staging area”或“index”是什么?
可以通过下图进行解释:
在完成提交之前,可以在称为“staging area”或“index”的中间区域中对其进行格式化和审查。从图中可以看出,每个更改首先在暂存区域中进行验证,我将其称为“stage file”,然后将更改提交到存储库。

9. 什么是 git stash?
首先应该解释 git stash 的必要性。
通常情况下,当你一直在处理项目的某一部分时,如果你想要在某个时候切换分支去处理其他事情,事情会处于混乱的状态。问题是,你不想把完成了一半的工作的提交,以便你以后就可以回到当前的工作。解决这个问题的答案是 git stash。
再解释什么是git stash。
stash 会将你的工作目录,即修改后的跟踪文件和暂存的更改保存在一堆未完成的更改中,你可以随时重新应用这些更改。
10. 什么是git stash drop?
通过说明我们使用 git stash drop
的目的来回答这个问题。
git stash drop
命令用于删除隐藏的项目。默认情况下,它将删除最后添加的存储项,如果提供参数的话,它还可以删除特定项。
下面举个例子。
如果要从隐藏项目列表中删除特定的存储项目,可以使用以下命令:
git stash list: 它将显示隐藏项目列表,如:
stash@{0}: WIP on master: 049d078 added the index file
stash@{1}: WIP on master: c264051 Revert “added file_size”
stash@{2}: WIP on master: 21d80a5 added number to log
如果要删除名为 stash@{0} 的项目,请使用命令 git stash drop stash@{0}。
11. 如何找到特定提交中已更改的文件列表?
对于这个问题,不能仅仅是提供命令,还要解释这个命令究竟做了些什么。
要获取特定提交中已更改的列表文件,请使用以下命令:
git diff-tree -r {hash}
给定提交哈希,这将列出在该提交中更改或添加的所有文件。 -r
标志使命令列出单个文件,而不是仅将它们折叠到根目录名称中。
你还可以包括下面提到的内容,虽然它是可选的,但有助于给面试官留下深刻印象。
输出还将包含一些额外信息,可以通过包含两个标志把它们轻松的屏蔽掉:
git diff-tree –no-commit-id –name-only -r {hash}
这里 -no-commit-id
将禁止提交哈希值出现在输出中,而 -name-only
只会打印文件名而不是它们的路径。
12. git config 的功能是什么?
首先说明为什么我们需要 git config
。
git 使用你的用户名将提交与身份相关联。 git config
命令可用来更改你的 git 配置,包括你的用户名。
下面用一个例子来解释。
假设你要提供用户名和电子邮件 ID 用来将提交与身份相关联,以便你可以知道是谁进行了特定提交。为此,我将使用:
git config –global user.name "Your Name": 此命令将添加用户名。
git config –global user.email "Your E-mail Address": 此命令将添加电子邮件ID。
13. 提交对象包含什么?
Commit 对象包含以下组件,你应该提到以下这三点:
- 一组文件,表示给定时间点的项目状态
- 引用父提交对象
- SHAI 名称,一个40个字符的字符串,提交对象的唯一标识。
14. 如何在Git中创建存储库?
这可能是最常见的问题,答案很简单。
要创建存储库,先为项目创建一个目录(如果该目录不存在),然后运行命令 git init。通过运行此命令,将在项目的目录中创建 .git 目录。
15. 怎样将 N 次提交压缩成一次提交?
将N个提交压缩到单个提交中有两种方式:
- 如果要从头开始编写新的提交消息,请使用以下命令:
git reset –soft HEAD~N &&
git commit
- 如果你想在新的提交消息中串联现有的提交消息,那么需要提取这些消息并将它们传给 git commit,可以这样:
git reset –soft HEAD~N &&
git commit –edit -m"$(git log –format=%B –reverse .HEAD@{N})"
16. 什么是 Git bisect?如何使用它来确定(回归)错误的来源?
我建议你先给出一个Git bisect 的小定义。
Git bisect 用于查找使用二进制搜索引入错误的提交。 Git bisect的命令是
git bisect <subcommand> <options>
既然你已经提到过上面的命令,那就解释一下这个命令会做什么。
此命令用了二进制搜索算法来查找项目历史记录中的哪个提交引入了错误。你可以通过告诉它已知包含该错误的“错误”提交以及在引入错误之前已知的“良好”提交来使用它。然后 git bisect 在这两个端点之间选择一个提交,并询问你所选的提交是“好”还是“坏”。它继续缩小范围,直到找到引入更改的确切提交。
17. 如果想要在提交之前运行代码性检查工具,并在测试失败时阻止提交,该怎样配置 Git 存储库?
我建议你先介绍一下完整性检查。
完整性或冒烟测试用来确定继续测试是否可行和合理。
下面解释如何实现这一目标。
这可以通过与存储库的 pre-commit hook 相关的简单脚本来完成。git 会在提交之前触发 pre-commit hook。你可以在这个脚本中运行其他工具,例如 linters,并对提交到存储库中的更改执行完整性检查。
最后举个例子,你可以参考下面的脚本:
#!/bin/sh
files=$(git diff –cached –name-only –diff-filter=ACM | grep ‘.go$’)
if [ -z files ]; then
exit 0
fi
unfmtd=$(gofmt -l $files)
if [ -z unfmtd ]; then
exit 0
fi
echo “Some .go files are not fmt’d”
exit 1
这段脚本检查是否需要通过标准 Go 源代码格式化工具 gofmt 传递所有即将提交的 .go 文件。如果脚步以非 0
状态退出,脚本会有效地阻止提交操作。
18. 描述一下你所使用的分支策略?
这个问题被要求用Git来测试你的分支经验,告诉他们你在以前的工作中如何使用分支以及它的用途是什么,你可以参考以下提到的要点:
-
功能分支(Feature branching)
要素分支模型将特定要素的所有更改保留在分支内。当通过自动化测试对功能进行全面测试和验证时,该分支将合并到主服务器中。
-
任务分支(Task branching)
在此模型中,每个任务都在其自己的分支上实现,任务键包含在分支名称中。很容易看出哪个代码实现了哪个任务,只需在分支名称中查找任务键。
-
发布分支(Release branching)
一旦开发分支获得了足够的发布功能,你就可以克隆该分支来形成发布分支。创建该分支将会启动下一个发布周期,所以在此之后不能再添加任何新功能,只有错误修复,文档生成和其他面向发布的任务应该包含在此分支中。一旦准备好发布,该版本将合并到主服务器并标记版本号。此外,它还应该再将自发布以来已经取得的进展合并回开发分支。
最后告诉他们分支策略因团队而异,所以我知道基本的分支操作,如删除、合并、检查分支等。
19. 如果分支是否已合并为master,你可以通过什么手段知道?
要知道某个分支是否已合并为master,你可以使用以下命令:
git branch –merged
它列出了已合并到当前分支的分支。
git branch –no-merged
它列出了尚未合并的分支。
20. 什么是SubGit?
SubGit 是将 SVN 到 Git迁移的工具。它创建了一个可写的本地或远程 Subversion 存储库的 Git 镜像,并且只要你愿意,可以随意使用 Subversion 和 Git。
这样做有很多优点,比如你可以从 Subversion 快速一次性导入到 Git 或者在 Atlassian Bitbucket Server 中使用SubGit。我们可以用 SubGit 创建现有 Subversion 存储库的双向 Git-SVN 镜像。你可以在方便时 push 到 Git 或提交 Subversion。同步由 SubGit 完成。
21. 什么是 Git 复刻(fork)?复刻(fork)、分支(branch)和克隆(clone)之间有什么区别?
主题:Git 难度:⭐⭐
- 复刻(fork) 是对存储仓库(repository)进行的远程的、服务器端的拷贝,从源头上就有所区别。复刻实际上不是 Git 的范畴。它更像是个政治/社会概念。
- 克隆(clone) 不是复刻,克隆是个对某个远程仓库的本地拷贝。克隆时,实际上是拷贝整个源存储仓库,包括所有历史记录和分支。
- 分支(branch) 是一种机制,用于处理单一存储仓库中的变更,并最终目的是用于与其他部分代码合并。
22. “拉取请求(pull request)”和“分支(branch)”之间有什么区别?
主题:Git 难度:⭐⭐
- 分支(branch) 是代码的一个独立版本。
- 拉取请求(pull request) 是当有人用仓库,建立了自己的分支,做了些修改并合并到该分支(把自己修改应用到别人的代码仓库)。
23. “git pull”和“git fetch”之间有什么区别?
主题:Git 难度:⭐⭐
简单来说,git pull
是 git fetch
+ git merge
。
- 当你使用
pull
,Git 会试着自动为你完成工作。它是上下文(工作环境)敏感的,所以 Git 会把所有拉取的提交合并到你当前处理的分支中。pull
则是 自动合并提交而没有让你复查的过程。如果你没有细心管理你的分支,你可能会频繁遇到冲突。 - 当你
fetch
,Git 会收集目标分支中的所有不存在的提交,并将这些提交存储到本地仓库中。但Git 不会把这些提交合并到当前分支中。这种处理逻辑在当你需要保持仓库更新,在更新文件时又希望处理可能中断的事情时,这将非常实用。而将提交合并到主分支中,则该使用merge
。
24. 如在 Git 恢复先前的提交?
主题:Git 难度:⭐⭐⭐
假设你的情形是这样,其中 C 是你的 HEAD,(F) 是你文件的状态。
(F)
A-B-C
↑
master
- 要修改提交中的更改:
git reset --hard HEAD~1
现在 B 是 HEAD,因为你使用了 --hard
,所以你的文件将重置到提交 B 时的状态。
- 要撤销提交但保留更改:
git reset HEAD~1
现在我们告诉 Git 将 HEAD 指针移回(后移)一个提交(B),并保留文件原样,然后你可以 git status
来显示你已经检入 C 的更改。
- 撤销提交但保留文件和索引:
git reset --soft HEAD~1
执行此操作后,git status
,你讲看到索引中的文件跟以前一致。
25. 什么是“git cherry-pick”?
主题:Git 难度:⭐⭐⭐
命令 git cherry-pick
通常用于把特定提交从存储仓库的一个分支引入到其他分支中。常见的用途是从维护的分支到开发分支进行向前或回滚提交。
这与其他操作(例如:合并(merge)、变基(rebase))形成鲜明对比,后者通常是把许多提交应用到其他分支中。
小结:
git cherry-pick <commit-hash>
26. 解释 Forking 工作流程的优点
主题:Git 难度:⭐⭐⭐
Forking 工作流程 与其他流行的 Git 工作流程有着根本的区别。它不是用单个服务端仓库充当“中央”代码库,而是为每个开发者提供自己的服务端仓库。Forking 工作流程最常用于公共开源项目中。
Forking 工作流程的主要优点是可以汇集提交贡献,又无需每个开发者提交到一个中央仓库中,从而实现干净的项目历史记录。开发者可以推送(push)代码到自己的服务端仓库,而只有项目维护人员才能直接推送(push)代码到官方仓库中。
当开发者准备发布本地提交时,他们的提交会推送到自己的公共仓库中,而不是官方仓库。然后他们向主仓库提交请求拉取(pull request),这会告知项目维护人员有可以集成的更新。
27. 告诉我 Git 中 HEAD、工作树和索引之间的区别?
主题:Git 难度:⭐⭐⭐
- 该工作树/工作目录/工作空间是你看到和编辑的(源)文件的目录树。
- 该**索引/中转区(staging area)**是个在
/.git/index
,单一的、庞大的二进制文件,该文件列出了当前分支中所有文件的 SHA1 检验和、时间戳和文件名,它不是个带有文件副本的目录。 - HEAD是当前检出分支的最后一次提交的引用/指针。
28. 你能解释下 Gitflow 工作流程吗?
主题:Git 难度:⭐⭐⭐
Gitflow 工作流程使用两个并行的、长期运行的分支来记录项目的历史记录,分别是 master
和 develop
分支。
- Master,随时准备发布线上版本的分支,其所有内容都是经过全面测试和核准的(生产就绪)。
- Hotfix,维护(maintenance)或修复(hotfix)分支是用于给快速给生产版本修复打补丁的。修复(hotfix)分支很像发布(release)分支和功能(feature)分支,除非它们是基于
master
而不是develop
分支。
- Hotfix,维护(maintenance)或修复(hotfix)分支是用于给快速给生产版本修复打补丁的。修复(hotfix)分支很像发布(release)分支和功能(feature)分支,除非它们是基于
- Develop,是合并所有功能(feature)分支,并执行所有测试的分支。只有当所有内容都经过彻底检查和修复后,才能合并到
master
分支。- Feature,每个功能都应留在自己的分支中开发,可以推送到
develop
分支作为功能(feature)分支的父分支。
- Feature,每个功能都应留在自己的分支中开发,可以推送到

29. 什么时候应使用 “git stash”?
主题:Git 难度:⭐⭐⭐
git stash
命令把你未提交的修改(已暂存(staged)和未暂存的(unstaged))保存以供后续使用,以后就可以从工作副本中进行还原。
回顾:
$ git status
On branch master
Changes to be committed:
new file: style.css
Changes not staged for commit:
modified: index.html
$ git stash
Saved working directory and index state WIP on master: 5002d47 our new homepage
HEAD is now at 5002d47 our new homepage
$ git status
On branch master
nothing to commit, working tree clean
我们可以使用暂存(stash)的一个地方是,如果我们发现在上次提交中忘记了某些内容,并且已经开始在同一分支中处理下一个提交了:
# Assume the latest commit was already done
# start working on the next patch, and discovered I was missing something
# stash away the current mess I made
$ git stash save
# some changes in the working dir
# and now add them to the last commit:
$ git add -u
$ git commit --ammend
# back to work!
$ git stash pop
30. 如何从 git 中删除文件,而不将其从文件系统中删除?
主题:Git 难度:⭐⭐⭐⭐
如果你在 git add
过程中误操作,你最终会添加不想提交的文件。但是,git rm
则会把你的文件从你暂存区(索引)和文件系统(工作树)中删除,这可能不是你想要的。
换成 git reset
操作:
git reset filename # or
echo filename >> .gitingore # add it to .gitignore to avoid re-adding it
上面意思是,git reset <paths>
是 git add <paths>
的逆操作。
31. 什么时候使用“git rebase”代替“git merge”?
主题:Git 难度:⭐⭐⭐⭐⭐
这两个命令都是把修改从一个分支集成到另一个分支上,它们只是以非常不同的方式进行。
考虑一下场景,在合并和变基前:
A <- B <- C [master]
^
\
D <- E [branch]
在 git merge master
之后:
A <- B <- C
^ ^
\ \
D <- E <- F
在 git rebase master
之后:
A <- B <- C <- D <- E
使用变基时,意味着使用另一个分支作为集成修改的新基础。
何时使用:
- 如果你对修改不够果断,请使用合并操作。
- 根据你希望的历史记录的样子,而选择使用变基或合并操作。
更多需要考虑的因素:
- **分支是否与团队外部的开发人员共享修改(如开源、公开项目)?**如果是这样,请不要使用变基操作。变基会破坏分支,除非他们使用
git pull --rebase
,否则这些开发人员将会得到损坏的或不一致的仓库。 - **你的开发团队技术是否足够娴熟?**变基是一种破坏性操作。这意味着,如果你没有正确使用它,你可能会丢失提交,并且/或者会破坏其他开发者仓库的一致性。
- **分支本身是否代表有用的信息?一些团队使用功能分支(branch-per-feature)模式,每个分支代表一个功能(或错误修复,或子功能等)。在此模式中,分支有助于识别相关提交的集合。在每个开发人员分支(branch-per-developer)**模式中,分支本身不会传达任何其他信息(提交信息已有作者)。则在这种模式下,变基不会有任何破坏。
- **是否无论如何都要还原合并?**恢复(如在撤销中)变基,是相当困难的,并且/或者在变基中存在冲突时,是不可能完成的。如果你考虑到日后可能需要恢复,请使用合并操作。
32. 把配置文件推送到了远程仓库,怎样删除远程仓库的该配置文件,本地还要用到这个文件。
这种操作失误,比较常见。一般这样解决:
git rm --cached filename
echo filename >> .gitignore
先解释第二步,本地需要,远程仓库不需要,肯定是要把那个文件写入 .gitignore
文件里面。 否则以后还要删除。
第一步则是把该文件从 git 的暂存区域中删除。暂存区域,就是 index 区域。
见一下亲人:

git 三个区,git rm filename
, 会把文件从工作区 Working Directory 和暂存区域 Staging Area 中删除。本地还要用,就不能这么搞。
git rm --cached filename
, 则把文件从暂存区域 Staging Area 删除,保留工作区的,我们一般编辑见到的。
这种情况就是已经 commit 了,生成快照,文件进了版本库 Commit History,然后 push, 远程库与本地库同步一下。
这个时候,直接 push 到远程,无效。因为没有新的快照,也就是没有新的 commit id. 本地与远程的历史 log 是一致的。 修改文件,add 再 commit, push 提交过去,就会生效。
33. git 如何解决代码冲突
解决冲突三连
git stash
git pull
git stash pop
操作就是把自己修改的代码隐藏,然后把远程仓库的代码拉下来,然后把自己隐藏的修改的代码释放出来,让 git 自动合并。接着找 <<<<<<<
, 哪里冲突哪里改。
如果要代码库的文件完全覆盖本地版本,
git reset --hard commit id
git pull
前面两招挺管用的,场景就是合作的远程仓库上,别人做了一些改动,我没有 commit , 然后把别人的 commit 拉下来。
刚进入公司的时候,没办法,我也经常这么做
加强版就来了
场景就是合作的远程仓库上,别人做了一些改动,我在本地也做了一些 commit , 然后把别人的 commit 拉下来,再把我的更改添加上去。接着找 <<<<<<<
, 哪里冲突哪里改。
这个时候,先检查一下我的本地仓库与合作的远程仓库的最近的一个共同 commit id.
git reset commit id
git stash
git pull
git stash pop
git reset commit id
的作用是取消暂存文件。将 HEAD 的指针指向 commit id,修改了暂存区域 Staging Area 和版本库 Commit History,工作区沙盒 Working Directory 保持原样。
亲人来了,看图加深一下理解:

-
git reset commit id
就是git reset -mixed commit id
,移动 HEAD,更新索引,即更新 staging area。移动 HEAD 分支的指向,使索引看起来像 HEAD。效果上看,就是取消了 commit id 以后的,add 和 commit . -
git reset --soft commit id
,就是移动 HEAD。移动 HEAD 分支的指向,本质上是撤销了上一次 git commit 命令。
当你在运行 git commit 时,Git 会创建一个新的提交,并移动 HEAD 所指向的分支来使其指向该提交。
当你将它 reset 回 HEAD~(HEAD 的父结点)时,其实就是把该分支移动回原来的位置,而不会改变索引和工作目录。
git reset --hard commit id
, 移动 HEAD,更新索引,更新工作目录。三件事情,全做了。前两件事情,已经说了。更新工作目录,让工作目录看起来像索引。从效果上看,就是撤销一切修改,本地文件状态同 commit id 的那时候。
34. 什么时候合并分支用 git rebase, 不用 git merge ?
git rebase
和 git merge
都可以用于合并分支,从 feature 分支上,取得新的提交 commits , 然后运用到 master 分支上(当然运用到其他分支上也行)。
但是路子不同
merge 是合并,rebase 是变基
变基怎么变?

git rebase
有一个移动 base , 改变合并基准的操作
直观的理解: git rebase
做的事情,就是先移指针,再移结点。
- 先移指针:master 分支之前分出的 feature 分支的 commit id, 是 feature 分支的基准 base.
在 feature 分支上 git rebase master
,就把 feature 分支的基准 base 移动到 master 分支最新的 commit id 上。
- 再移结点: 把 feature 分支上新增的提交 commit id ,放到新的 base 结点后面。准确一些,就是把 feature 分支上新做的修改操作,重新应用到 master 分支的 HEAD 结点上。
举个例子:
合并分支前:
A <- B <- C [master]
^
\
D <- E [branch]
根结点是 A, 最初是 A , 在 A 状态,分出去了分支 branch
git merge
是这样合并的:
A <- B <- C
^ ^
\ \
D <- E <- F
提交到 C 的 master 分支和提交到 E 的 branch 分支,直接合并,一般是合并到 master, 有冲突解决冲突。
看图可知: 采用 git merge
,不改变原来的 commit id, 会产生新的提交 commit id
项目协作成员比较多,一般需要使用 git rebase
, 当然也可以这样git merge
, git merge --squash feeature
如果使用 git merge
,很可能这样,

git rebase
修改历史:
git rebase
是这样合并的:
A <- B <- C <- `D` <- `E`
把 feature 分支( 例子中是 branch 分支)的提交 commits ,移动到 master 分支的顶端。
使用 rebase, 看起来更加 nice, 更加直观,历史就是一条直线嘛,没什么枝枝岔岔的。
看图可知: 使用 rebase 后,把 feature 分支上 commit 拿来后,commit id 改掉了。并且没有创建合并的公共节点 commit id

git rebase
工作流,有的采用 git merge
工作流,git rebase
工作流要求对 git 的理解深一些,商用多一些, feature 做好了,用 git rebase
合并。
这样加 feature 怎么加的,看 log 比较明朗。否则看 log, 上一条 feature A, 下一条 feature B , 不是很职业程序员。
git rebase
工作流,一般这么实操, 在 feature 分支上 git rebase master
, 切到 master 分支上后, git merge feature
, 这时候 merge 也没做什么事情,就是把 master 分支的 HEAD 结点,移动到 feature 的 HEAD 结点, 两个分支这时候状态同步了。
git merge
工作流民用挺合适的,他的设计非常符合直觉,玩 github 开源,比较合适。因为多人写作,git flow 工作流,不是很好统一
如果你采用 git rebase
工作流,你团队其他人不知道,你这么搞,commit id 修改了,与他们本地库的对不上,你们对你的意见可能会比较大
毕竟 git 采用分支,就是不冒犯别人代码的意思