# webpack 与 grunt、gulp 的不同
Grunt、Gulp 是基于任务运行的工具:
它们会自动执行指定的任务,就像流水线,把资源放上去然后通过不同插件进行加工,有活跃的社区,丰富的插件,能方便地打造各种工作流。
Webpack 是基于模块化打包的工具:
自动化处理模块,webpack 把一切当成模块,当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些打包成一个或多个 bundle。
二者是完全不同的两类工具,而现在主流的方式是用 npm script 代替 Grunt、Gulp,npm script 同样可以打造任务流。
# webpack、rollup、parcel 优劣
- webpack
- 适用于大型复杂的前端站点构建
- webpack 有强大的 loader 和 插件生态,打包后的文件实际上就是一个立即执行函数,这个立即执行函数接收一个参数,该参数是模块对象,键为各个模块的路径,值为模块内容
- 立即执行函数内部则处理模块之间的引用,执行模块等,适合文件依赖复杂的应用开发
- rollup
- 适用于基础库的打包,如 vue、d3 等
- Rollup 是将各个模块打包进一个文件中,并且通过 Tree-Shaking 来删除无用的代码,可以最大程度上降低代码体积
- 但是 rollup 没有 webpack 如此多的如代码分割,按需加载等高级功能,更聚焦于库的打包,因此更适合库的开发
- parcel
- 适用于简单的实验性项目
- 可以满足低门槛的快速看到效果
- 但生态差、报错信息不全,仅推荐在实验项目中使用
# 有哪些常见的 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 定义环境变量
- html-webpack-plugin 简化 html 文件创建
- uglifyjs-webpack-plugin 通过 uglifyjs 压缩 ES6 代码
- webpack-parallel-uglify-plugin 多核压缩,提高压缩速度
- webpack-bundle-analyzer 可视化 webpack 输出文件的体积
- mini-css-extract-plugin CSS 提取到单独的文件中,支持按需加载
# Loader 和 Plugin 的不同
- 作用不同:
- Loader 为加载器。
- Webpack 将一切文件视为模块,但是 webpack 原生是只能解析 js 文件,如果想将其他文件也打包的话,就会用到
loader
- Loader 的作用是让 webpack 拥有了加载和解析非 JavaScript 文件的能力
- Webpack 将一切文件视为模块,但是 webpack 原生是只能解析 js 文件,如果想将其他文件也打包的话,就会用到
- Plugin 为插件
- Plugin 可以扩展 webpack 的功能,让 webpack 具有更多的灵活性
- 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果
- Loader 为加载器。
- 用法不同:
- Loader 在
module.rules
中配置,也就是说作为模块的解析规则而存在。类型为数组,每一项都是一个 Object,里面描述了对于生命类型的文件(test
),使用什么加载(loader
)和使用的参数(options
) - Plugin 找
plugins
中单独配置。类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入。
- Loader 在
# 编写 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 改变输出结果。
# bundle、chunk、module 是什么
- bundle:是由 webpack 打包出来的文件
- chunk:代码块,一个 chunk 由多个模块组合而成,用于代码的合并和分割
- module:是开发中的单个模块,在 webpack 的世界,一切皆模块,一个模块对应一个文件,webpack 会从配置的 entry 中递归开始找出所有依赖的模块
# Webpack 的构建流程是什么
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
- 确定入口:根据配置中的 entry 找出所有的入口文件;
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
- 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统 在以上过程中, Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
# Webpack 的热更新是如何实现的
热更新又称热替换(Hot Module Replacement)HMR。该机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
原理:
- 在 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 操作,也就是进行浏览器刷新来获取最新打包代码
# 如何用 webpack 来优化前端性能
用 webpack 优化前端性能是指优化 webpack 的输出结果,让打包的最终结果再浏览器运行快速高效。
- 压缩代码:
- 删除多余代码、注释、简化代码的写法等等
- 可以利用
webpack
的UglifyJsPlugin
和ParallelUglifyPlugin
来压缩 JS 文件,利用cssnano
(css-loader 中 minimize 配置)来压缩 css
- 利用 CDN 加速
- 在构建过程中,将引用的静态资源路径修改为 CDN 上对应的路径
- 可以利用 webpack 对于
output
参数和各 loader 的publicPath
参数来修改资源路径
- Tree Shaking
- 将代码中永远不会走到的片段删除掉
- 可以通过在启动 webpack 时追加参数
--optimize-minimize
来实现
- Code Splitting:
- 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利用浏览器缓存
- 提取公共第三方库
- SplitChunksPlugin 插件来进行公共模块抽取,利用浏览器缓存可以长期缓存这些无需频繁变动的公共代码
# 如何提高 webpack 的打包速度
- happypack:利用进程并行编译 loader,利用缓存来使得 rebuild 更快(以停止维护,可以用 thread-loader 替代)
- 外部扩展:将不怎么需要更新的第三方库脱离 webpack 打包,不被打入 bundle 中,从而减少打包时间,比如 jQuery 用 script 标签引入
- dll:采用 webpack 的 DllPlugin 和 DllReferencePlugin 引入 dll,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间
- 利用缓存:webpack.cache、babel-loader.cacheDirectory、HappyPack.cache 都可以利用缓存提高 rebuild 效率
- 缩小文件搜索范围:比如 babel-loader 插件,如果你的文件仅存于 src 中,那么可以
include: path.resolve(__dirname, 'src')
,当然绝大多数情况下这种操作的提升有限,除非不小心 build 了 node_modules 文件
# 如何提高 webpack 的构建速度
- 多入口情况下,使用
CommonsChunkPlugin
来提取公共代码 - 通过
externals
配置来提取常用库 - 利用
Dllplugin
和DllReferencePlugin
预编译资源模块 通过DllPlugin
来对那些我们引用但是绝对不会修改的 npm 包来进行预编译,再通过DllReferencePlugin
将预编译的模块加载进来 - 使用
HappyPack
实现多线程加速编译 - 使用
webpack-uglify-parallel
来提升uglifyPlugin
的压缩速度。原理上webpack-uglify-parallel
采用了多核并行压缩来提升压缩速度 - 使用
Tree-shaking
和Scope Hoisting
来剔除多余代码
# 怎么配置单页应用和多页应用
单页应用可以理解为 webpack 的标准模式,直接在 entry
中指定单页应用的入口即可
多页面应用的话,可以使用 webpack 的 AutoWebPlugin
来完成简单自动化的构建,但是前提是项目的目录结构必须遵守预设的规范。
多页面应用要注意的是:
- 每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载。比如,每个页面都引用了同一套 css 样式表
- 随着业务的不断扩展,页面可能会不断的追加,所以一定要让入口的配置足够灵活,避免每次添加新页面还需要修改构建配置