1、webpack与grunt、gulp的不同?

三者都是前端构建工具,grunt和gulp在早期比较流行,现在webpack相对来说比较主流,不过一些轻量化的任务还是会用gulp来处理,比如单独打包CSS文件等。
grunt和gulp是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。
webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能。

总结:
(1)从构建思路来说:
gulp和grunt需要开发者将整个前端构建过程拆分成多个Task,并合理控制所有Task的调用关系 webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工;
(2)对于知识背景:
gulp更像后端开发者的思路,需要对于整个流程了如指掌 webpack更倾向于前端开发者的思路。

2、 与webpack类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用webpack?

同样是基于入口的打包工具还有以下几个主流的:webpack,rollup,parcel。
应用场景上来看:
(1)webpack适合大型复杂的前端站点构建;
(2)rollup适合基础库的打包,比如vue,react;
(3)parcel适用于简单的实验室项目,但是打包出错很难调试。

3、有哪些常见的Loader?他们是解决什么问题的?

(1)babel-loader:把es6转成es5;
(2)css-loader:加载css,支持模块化,压缩,文件导入等特性;
(3)style-loader:把css代码注入到js中,通过dom操作去加载css;
(4)eslint-loader:通过Eslint检查js代码;
(5)image-loader:加载并且压缩图片晚间;
(6)file-loader:文件输出到一个文件夹中,在代码中通过相对url去引用输出的文件;
(7)url-loader:和file-loader类似,文件很小的时候可以base64方式吧文件内容注入到代码中。
(8)source-map-loader:加载额外的source map文件,方便调试。

4、有哪些常见的Plugin?他们是解决什么问题的?

(1)uglifyjs-webpack-plugin:通过UglifyJS去压缩js代码;
(2)commons-chunk-plugin:提取公共代码;
(3)define-plugin:定义环境变量。

5、loader和plugin的不同

作用不同:
(1)loader让webpack有加载和解析非js的能力;
(2)plugin可以扩展webpack功能,在webpack运行周期中会广播很多事件,Plugin可以监听一些事件,通过webpack的api改变结果。

用法不同:
(1)loader在module.rule中配置。类型为数组,每一项都是Object;
(2)plugin是单独配置的,类型为数组,每一项都是plugin实例,参数通过构造函数传入。

6、webpack的构建流程是什么?从读取配置到输出文件这个过程尽量说全

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
(1)初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
(2)开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
(3)确定入口:根据配置中的 entry 找出所有的入口文件;
(4)编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
(5)完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
(6)输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
(7)输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

7、是否写过Loader和Plugin?描述一下编写loader或plugin的思路?

编写Loader时要遵循单一原则,每个Loader只做一种”转义”工作。 每个Loader的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用 this.callback() 方法,将内容返回给webpack。 还可以通过 this.async()生成一个callback函数,再用这个callback将处理后的内容输出出去。

Plugin的编写就灵活了许多。 webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

8、webpack的热更新是如何做到的?说明其原理?

webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块
原理:
Webpack 常见面试题 - 图1
分析:
(1)第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。

(2)第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。

(3)第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。

(4)第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。

(5)webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。

(6)HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。

(7)而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。

(8)最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

9、如何利用webpack来优化前端性能?(提高性能和体验)

用webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运行快速高效。

(1)压缩代码
删除多余的代码、注释、简化代码的写法等等方式。
利用webpack的UglifyJsPlugin和ParallelUglifyPlugin来压缩JS文件。
利用cssnano(css-loader?minimize)来压缩css。
使用webpack4,打包项目使用production模式,会自动开启代码压缩。

(2)利用CDN加速
在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。
可以利用webpack对于output参数和各loader的publicPath参数来修改资源路径。

(3)删除死代码(Tree Shaking)
将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现或者使用es6模块开启删除死代码

(4)优化图片,对于小图可以使用 base64 的方式写入文件中。

(5)按照路由拆分代码,实现按需加载,提取公共代码。

(6)给打包出来的文件名添加哈希,实现浏览器缓存文件。

10、如何提高webpack的构建速度?

(1)多入口的情况下,使用commonsChunkPlugin来提取公共代码;
(2)通过externals配置来提取常用库;
(3)使用happypack实现多线程加速编译;
(4)使用webpack-uglify-parallel来提升uglifyPlugin的压缩速度。原理上webpack-uglify-parallel采用多核并行压缩来提升压缩速度;
(5)使用tree-shaking和scope hoisting来剔除多余代码。

11、怎么配置单页应用?怎么配置多页应用?

单页应用可以理解为webpack的标准模式,直接在entry中指定单页应用的入口即可。
多页应用的话,可以使用webpack的 AutoWebPlugin来完成简单自动化的构建,但是前提是项目的目录结构必须遵守他预设的规范。

12、npm打包时需要注意哪些?如何利用webpack来更好的构建?

NPM模块需要注意以下问题:
(1)要支持CommonJS模块化规范,所以要求打包后的最后结果也遵守该规则
(2)Npm模块使用者的环境是不确定的,很有可能并不支持ES6,所以打包的最后结果应该是采用ES5编写的。并且如果ES5是经过转换的,请最好连同SourceMap一同上传。
(3)Npm包大小应该是尽量小(有些仓库会限制包大小)
(4)发布的模块不能将依赖的模块也一同打包,应该让用户选择性的去自行安装。这样可以避免模块应用者再次打包时出现底层模块被重复打包的情况。
(5)UI组件类的模块应该将依赖的其它资源文件,例如.css文件也需要包含在发布的模块里。

基于以上需要注意的问题,我们可以对于webpack配置做以下扩展和优化

(1)CommonJS模块化规范的解决方案:
设置output.libraryTarget=’commonjs2’使输出的代码符合CommonJS2 模块化规范,以供给其它模块导入使用;

(2)输出ES5代码的解决方案:
使用babel-loader把 ES6 代码转换成 ES5 的代码。再通过开启devtool: ‘source-map’输出SourceMap以发布调试。

(3)Npm包大小尽量小的解决方案:
Babel 在把 ES6 代码转换成 ES5 代码时会注入一些辅助函数,最终导致每个输出的文件中都包含这段辅助函数的代码,造成了代码的冗余。解决方法是修改.babelrc文件,为其加入transform-runtime插件。

(4)不能将依赖模块打包到NPM模块中的解决方案:**
使用externals配置项来告诉webpack哪些模块不需要打包。

(5)对于依赖的资源文件打包的解决方案:
通过css-loader和extract-text-webpack-plugin来实现,配置如下:
Webpack 常见面试题 - 图2

13、如何在vue项目中实现按需加载?

经常会引入现成的UI组件库如ElementUI、iView等,但是他们的体积和他们所提供的功能一样,是很庞大的。

不过很多组件库已经提供了现成的解决方案,如Element出品的babel-plugin-component和AntDesign出品的babel-plugin-import 安装以上插件后,在.babelrc配置中或babel-loader的参数中进行设置,即可实现组件按需加载了
Webpack 常见面试题 - 图3
单页应用的按需加载 现在很多前端项目都是通过单页应用的方式开发的,但是随着业务的不断扩展,会面临一个严峻的问题——首次加载的代码量会越来越多,影响用户的体验。