打包构建
什么是组件化、模块化、工程化?
组件化:把重复的代码提取出来合并成为一个个组件,组件最重要的就是复用,位于框架最底层,其他功能都依赖于组件,可供不同功能使用,独立性强。组件化更多关注UI部分,每个组件有独立的HTML、css、js代码。可以根据需要把它放在页面的任意部位,也可以和其他组件一起形成新的组件。一个页面是各个组件的结合,可以根据需要进行组装。
模块化:分属同一功能/业务的代码进行分装,让它成独立的模块,可以独立运行,以页面、功能或其他不同粒度划分程度不同的模块,位于业务框架层,模块间通过接口调用,目的是降低模块间的耦合,由之前的主应用与模块耦合,变为主应用与接口耦合,接口与模块耦合。侧重功能的封装,主要是针对Js代码,隔离、组织复制的js代码,将它封装成一个个具有特定功能的的模块。模块可以通过传递参数的不同修改这个功能的的相关配置,每个模块都是一个单独的作用域,根据需要调用。一个模块的实现可以依赖其它模块。
工程化:前端工程化不是具体的某项技术和方法,只要我们引入的方法、技术方案、工具可以提升开发效率、提高前端应用质量,那么都属于前端工程化。前端工程化就是通过一系列的工具、方法、工程化的思维,将成千上万个模块、组件或其他静态资源进行有序、规范、标准化、可控、可追踪的组织起来,作为一个整体运行,以便提高前端工程的性能、稳定性、可用性、可维护性等。
组件化、模块化、工程化带来的好处
组件化带来的好处
当应用是以多组件的方式实现, 这个应用就是一个组件化的应用
1、组件
从UI上拆分下来的每个包含模板(HTML)+样式(CSS)+逻辑(JS)功能完备的结构单元,是一个用来实现局部功能效果的代码和资源的集合
2、为什么要组件化?
一个界面的功能更复杂
3、作用
复用编码, 简化项目编码, 提高运行效率
模块化带来的好处
当应用的JS都以模块来编写时, 这个应用就是一个模块化的应用
1、模块
向外提供特定功能的文件, 可以做到按需引入
2、为什么要模块化?
随着业务逻辑增加,代码越来越多且复杂
3、作用
复用代码, 简化代码的编写, 提高代码运行效率
工程化带来的好处
工程化是一种思想而不是某种特定的技术,当然我们在实现项目工程化的过程中,我们也会去使用一些技术。前端工程化是使用软件工程的技术和方法来对前端的开发流程、技术、工具等进行规范化、标准化,其主要目的为了提高效率和降低成本,即提高开发过程中的开发效率,减少不必要的重复工作时间。
1、为什么要以工程化的思想来看待前端项目?
目前,web业务日益复杂化和多元化,单页面应用和webApp风靡。而且前端的生态圈繁荣,各种框架,组件出现,使得前端发展迅速,快速开发已经成为了前端的一个标准。
靠传统的前端三剑客 JavaScript、HTML、 CSS 以及传统的项目结构已经不能满足日益壮大的大型应用的需求,会带来开发效率、维护成本、代码可读性差等问题。
这就要求我们以工程化的思想去看待一个前端项目而不再是直接撸起袖子开写页面,将前端项目当成一项系统工程去进行分析、组织和构建从而达到项目结构清晰、分工明确、团队配合默契、开发效率和开发质量提高以及降低项目生产风险的目的。
2、 那我们又该怎样去做到前端的工程化呢?
- 页面组件化
- 代码模块化
- 代码质量管理 (QA): ESLint StyleLint
- 代码编译: babel、less、sass、scss等
- 代码构建:webpack vite
- 项目国际化
- 代码版本管理:Git、SVN
Webpack是什么?
Webpack是一个前端资源构建工具,是一个现代 JavaScript 应用程序的静态模块打包器
为什么需要Webpack?
1、文件加载问题。假如我们不依赖以打包工具,那么我就需要在首页index.html文件下,书写非常多的script标签,并且必须保证script标签加载顺序正确(注:html页面是从上往下依次执行标签),因为第三方库、业务代码模块以及模块之间是存在依赖关系的,必须确保依赖的文件先加载,否则页面将会报错,一旦编写的代码文件多了,将及其难以维护。
2、浏览器的运行效率问题。如果编写的代码文件比较多,此时我们就需要发送多次http请求,如果其中一个文件没有请求回来,而下面的文件对该文件存在一些依赖,就会导致页面中的部分功能没有办法实现。
3、浏览器对新技术的支持度不够。比如说我们编写一些比较高级的语法,浏览器是不支持的,例如我们编写的TS
和Vue
文件,这些浏览器都不能识别,而Webpack就会使用对应的babel对其进行转化,转化为浏览器可以识别的文件。
注:
1、使用 Webpack 打包项目时,如果需要手动配置 Webpack 的话,可以在项目的根目录下手动创建 webpack.config.js 文件(名字不能随便取,因为是固定的),然后进行 Webpack 配置,Webpack 打包时会默认读取这个文件。
2、为什么在 webpack.config.js 使用 module.exports = { } 导出一个对象 ?
因为这个文件是在 node.js 里面运行的,因此我们去定义模块的时候,得使用 node.js 中的 CommonJS 模块,所以使用 module.exports 去定义,它的值是一个配置对象。
如何实现 tree-shaking
tree-shaking 就是让没有用到的 JS 代码不打包,以减小包的体积
怎么删?
使用 ES Modules 语法(即 ES6 的 import 和 export 关键字) CommonJS 语法无法 tree-shaking(即 require 和 exports 语法) 引入的时候只引用需要的模块 要写 import { cloneDeep } from 'lodash-es' 引入的库也需要用 ES Modules 不要写 import _ from 'lodash' 因为会导致无法 tree-shaking 怎么不删?
在 package.json 中配置 sideEffects,防止某些文件被删掉
比如我 import 了 x.js,而 x.js 只是添加了 window.x 属性,那么 x.js 就要放到 sideEffects 里 比如所有被 import 的 CSS 都要放在 sideEffects 里 怎么开启?
在 webpack config 中将 mode 设置为 production(开发环境没必要 tree-shaking)
在 Webpack5 中,Tree Shaking 在生产环境下默认启动
webpack的理解
定义:Webpack是一个现代JS应用程序的静态模块打包工具,用于处理各种资源,如JS、JSX、ES6、SASS、LESS、图片等。
核心概念:
- **Entry(入口)**:Webpack执行构建的第一步,告诉webpack要使用哪个模块作为构建项目的起点,默认为./src/index.js。
- **Output(输出)**:告诉webpack在哪里输出它打包好的代码以及如何命名,默认为./dist。
- **Module(模块)**:在Webpack里一切皆模块,一个模块对应着一个文件。Webpack会从配置的Entry开始递归找出所有依赖的模块。
- **Chunk(代码块)**:一个Chunk由多个模块组合而成,用于代码合并与分割。
- **Loader(加载器)**:用于把模块原内容按照需求转换成新内容,例如css-loader、style-loader等。
- **Plugin(插件)**:在Webpack构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。
基本功能:
- 模块打包:将项目的不同文件打包成一个或多个文件。
- 资源转换:转换ES6、SASS、LESS等现代前端资源。
- 代码分割:将代码分割成多个小块,实现懒加载和按需加载。
- 插件系统:通过插件扩展Webpack的功能,如打包优化、文件压缩等。
使用场景和优势: Webpack广泛应用于前端开发中,支持Vue、React等框架的项目。其优势包括易用性、强大的插件系统、高效的资源管理和代码分割等。
webpack版本
Webpack 是一个模块打包工具,用于现代 JavaScript 应用程序的静态模块打包器。它将应用中的所有资源,如 JavaScript、样式表、图片等视为模块,并通过依赖图来处理和优化这些资源。随着 Webpack 的发展,它经历了多个版本的迭代,每个新版本都会带来新的特性和改进。
以下是 Webpack 一些主要版本的简介:
- v1.0 (2014): 初始发布版本,引入了模块打包的概念。
- v2.0 (2016): 引入了 Tree Shaking 功能,这是一种去除未使用代码的技术。同时支持 ES2015 模块语法。
- v3.0 (2017): 提升了性能,减少了内存消耗,增加了 Scope Hoisting(作用域提升)特性,进一步减少打包后的文件大小。
- v4.0 (2018): 移除了对 Node.js 4.x 的支持,简化了配置,默认启用了 mode 参数(development 和 production),使得在不同模式下有更合适的默认设置。这个版本还带来了更快的构建速度。
- v5.0 (2019): 继续提升了性能,增强了持久化缓存机制,以加快开发环境下的重新构建速度。此外,还提高了兼容性,例如更好地支持 WebAssembly。
- v6.0 (2023): 这个版本中,Webpack 团队专注于提高开发者体验和稳定性。它引入了更多的自动化配置选项,让配置变得更加简单。同时,对于一些长期存在的问题进行了修复,并且持续地优化了性能。
请注意,上述信息可能不会覆盖到最新的小版本更新或补丁,因为 Webpack 项目经常会有小幅度的更新来修复 bug 或者添加细微的功能改进。为了获取最准确的信息,建议直接访问 Webpack 的官方文档或 GitHub 页面查看最新版本的变更日志。
如果你正在开始一个新的项目或者考虑升级现有的 Webpack 版本,通常推荐使用最新稳定版,因为它包含了最新的功能以及安全性和性能方面的改进。同时,请确保你的项目及其依赖项都与所选的 Webpack 版本兼容。
webpack打包用的版本是什么版本
webpack5
vite和webpack的区别
启动速度:
- Vite:Vite利用现代浏览器对ES模块(ESM)的原生支持,采用“no bundle”策略,即每个模块都作为一个独立的文件提供给浏览器,不需要预先编译所有文件,因此在启动时速度非常快。
- Webpack:Webpack在启动时会编译所有文件,无论模块是否被使用,这导致随着项目规模的增大,启动时间会显著增加。
**热更新(HMR)**:
- Vite:Vite在热更新时只需要重新请求改动的模块,不需要重新编译整个项目,因此热更新速度非常快。
- Webpack:Webpack在热更新时需要重新编译整个项目,这在大型项目中可能会导致延迟。
插件生态:
- Vite:虽然Vite的插件生态在不断发展,但相比Webpack来说还显得较为稀少。
- Webpack:Webpack拥有丰富的插件生态,有大量社区和官方插件可以选择,覆盖了前端开发的各个方面。
配置复杂度:
- Vite:Vite在设计上更注重开箱即用,大部分场景下用户无需自己写配置文件。
- Webpack:Webpack的配置相对复杂,需要处理不同类型的资源和加载器,适合需要高度自定义的项目。
打包效率:
- Vite:Vite利用了浏览器对ES模块的原生支持,只打包和缓存实际改动的模块,极大提高了打包效率。
- Webpack:Webpack将所有模块打包成一个或多个bundle文件,初次加载速度较慢。
底层实现:
- Vite:Vite基于esbuild预构建依赖,esbuild使用Go语言编写,构建速度非常快。
- Webpack:Webpack使用JavaScript编写,构建速度相对较慢。
答案2:
1.开发环境区别
vite 自己实现 server,不对代码打包,充分利用浏览器的的支持
<script type="module">
- 假设 main.js 引入了 vue
- 该 server 会把
import { useEffect } from 'react'
改成import {useEffect } from "/node_modules/.vite/react.js"
这样浏览器就知道去哪里做到 vue.js
webpack-dev-server 常使用 babel-loader 基于内存打包,比 vite 慢很多很多
- 该 server 会把 react.js 的代码(递归地)打包进 main.js
2.生产环境区别
- vite 使用 rollup + esbuild 来打包 JS 代码
- webpack 使用 babel 来打包 JS 代码,比 esbuild 慢很多很多
- webpack 能使用 esbuild 吗?可以,你需要配置(很麻烦)
- esbuild 为什么快
- babel 用 js 写,esbuild 用 go 写的
- webpack 和 rollup 一个层级
- babel 和 esbuild 一个层级的
- vite 相当于比 webpack 再封装了一层
3.文件处理时机
- vite 只会在你请求某个文件的时候处理该文件
- webpack 会提前打包好 main.js,等你请求的时候直接输出打包好的 JS 给你
目前已知 vite 的缺点有
- 热更新常常失败,原因不清楚
- 有些功能 rollup 不支持,需要自己写 rollup 插件
- 不支持非现代浏览器
webpack、rollup、parcel优劣?
- webpack 适⽤于⼤型复杂的前端站点构建: webpack 有强⼤的 loader 和插件⽣态,打包后的⽂件实际上就是⼀个⽴即执⾏函数,这个⽴即执⾏函数接收⼀个参数,这个参数是模块对象,键为各个模块的路径,值为模块内容。⽴即执⾏函数内部则处理模块之间的引⽤,执⾏模块等,这种情况更适合⽂件依赖复杂的应⽤开发。
- rollup 适⽤于基础库的打包,如 vue、d3 等: Rollup 就是将各个模块打包进⼀个⽂件中,并且通过 Tree-shaking 来删除⽆⽤的代码,可以最⼤程度上降低代码体积,但是 rollup 没有 webpack 如此多的的如代码分割、按需加载等⾼级功能,其更聚焦于库的打包,因此更适合库的开发。
- parcel 适⽤于简单的实验性项⽬: 他可以满⾜低⻔槛的快速看到效果,但是⽣态差、报错信息不够全⾯都是他的硬伤,除了⼀些玩具项⽬或者实验项⽬不建议使⽤。
webpack中的参数
Webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。它根据模块的依赖关系,递归地构建一个依赖图(dependency graph),然后将这些模块打包成一个或多个 bundle。Webpack 的配置是通过一个名为 webpack.config.js
的配置文件来进行的,这个文件可以包含多个参数(或称为配置项)来自定义打包过程。以下是一些常见的 Webpack 配置参数:
entry
1. - 类型:
string | [string] | object
- 描述:定义 webpack 应该使用哪个模块来作为构建其内部依赖图的开始。
output
2. - 类型:
object
- 描述:告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件。至少需要指定一个
filename
。
module
3. 类型:
object
描述
:用于配置如何处理项目中的不同类型的模块。
rules
:数组,用于定义一系列的模块匹配规则(如使用 loader 转换文件)。
resolve
4. 类型:
object
描述
:用于配置模块如何被解析。
extensions
:自动解析确定的扩展。alias
:模块别名配置。
plugins
5. - 类型:
array
- 描述:用于执行范围更广的任务,如打包优化、资源管理和环境变量注入等。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。
mode
6. - 类型:
string
- 描述:设置
mode
参数可启用 webpack 内置的在相应环境下的优化。可选值有development
、production
和none
。
devtool
7. - 类型:
string
- 描述:控制是否生成,以及如何生成 source map。
devServer
8. - 类型:
object
- 描述:用于配置 webpack-dev-server,一个小的 Node.js Express 服务器,能够实时重新加载(live reloading)。
optimization
9. 类型:
object
描述
:包含了 webpack 在进行代码优化和打包时的各种选项。
splitChunks
:代码分割配置。minimize
:是否启用最小化(压缩)JS。
performance
10. - 类型:
object
- 描述:设置性能处理的提示/警告。
示例
这是一个基本的 webpack.config.js
示例,展示了如何配置入口、输出、模式和一些插件:
javascript复制代码
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
mode: 'development',
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
}),
],
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
};
这些只是 Webpack 配置中的一小部分参数,Webpack 的功能非常强大,可以通过不同的参数和插件来定制构建过程,以满足各种复杂的应用场景。
webpack的打包过程
初始化参数阶段:
- 从
webpack.config.js
配置文件中读取配置参数。 - 合并命令行传入的参数,得到最终的打包配置参数。
- 从
开始编译准备阶段:
- 创建Compiler对象,注册所有配置的插件。
- 找到入口文件,调用
compiler.run()
方法开始编译。
模块编译阶段:
- 从入口模块开始,调用匹配文件的Loader进行处理。
- 分析模块依赖的模块,递归进行模块编译。
完成编译阶段:
- 使用Loader翻译完所有模块后,得到每个模块被编译后的最终内容及它们之间的依赖关系。
输出文件阶段:
根据入口和模块之间的依赖关系,组装成包含多个模块的Chunk。
将每个Chunk转换成一个单独的文件,加入输出列表中。
根据配置确定输出的路径和文件名,将文件内容写入文件系统中。
vite兼容性
Vite在低版本浏览器中运行遇到的问题
vite+vue3项目开发完以后,运行在新版浏览器可以正常显示,但运行在一些版本比较老的浏览器如 Chrome < 23、Firefox < 21和IE等浏览器上时显示一片空白,并且没有任何的错误提示。
浏览器兼容性 用于生产环境的构建包会假设目标浏览器支持现代 JavaScript 语法。默认情况下,Vite 的目标浏览器是指能够 支持原生 ESM script 标签 和 支持原生 ESM 动态导入 的。作为参考,Vite 使用这个 browserslist 作为查询标准:
Chrome >=87 Firefox >=78 Safari >=13 Edge >=88 也可以通过 build.target 配置项 指定构建目标,最低支持 es2015。 默认情况下 Vite 只处理语法转译,且 默认不包含任何 polyfill。
官方解决方案:使用@vitejs/plugin-legacy插件
传统浏览器可以通过插件 @vitejs/plugin-legacy 来支持
@vitejs/plugin-legacy插件,在打包过程中做了什么 打包过程中使用@babel/preset-env转换浏览器不支持的语法和新API,为包中的每个块生成相应的转化块; 生成一个包含 SystemJS 运行时的 polyfill 块; 在打包文件中生成带有legacy名的文件,每个js脚本文件都有一个与其对应的转化版本; html文件中新增了一些script-nomodule脚本,这些脚本根据浏览器的支持程度来动态的引入正常版本文件还是带
参考网站:https://blog.csdn.net/wang13679201813/article/details/132730192
Vite优缺点
vite优点:
1.vite启动开发服务器比webpack快
由于vite启动的时候不需要打包,也就无需分析模块依赖、编译,所以启动速度非常快。当浏览器请求需要的模块时,再对模块
进行编译,这种按需动态编译的模式,极大缩短了编译时间,当项目越大,文件越多时,vite的开发时优势越明显
2.vite热更新比webpack快
vite在HRM方面,当某个模块内容改变时,让浏览器去重新请求该模块即可,而不是像webpack重新将该模块的所有依赖重新
编译;
3.vite使用esbuild(Go编写)预构建依赖,而webpack基于nodejs,比node快10-100倍
vite缺点:
1.生态不及webpack,加载器、插件不够
2.打包到生产环境时,vite使用传统的rollup进行打包,生产环境esbuild构建对于css和代码分割不够友好。所以,vite的优势是体现在开发阶段
3.没被大规模重度使用,会隐藏一些问题
4.项目的开发浏览器要支持esmodule,而且不能识别commonjs语法
为什么vite的构建速度更快
vite快的原因是因为 vite在开发环境中是使用的 esbuild,esbuild 是 go 写的,go = 编译型语言、多线程,nodejs = 解释型语言、单线程,并且 vite 使用了原生 esm 导入的,所以快一点,当然,这也只是开发环节,build 的时候vite使用的是 rollup ,回归js,打包速度并没什么提升。
- esbuild 的优势:
- esbuild 使用 Go 语言编写,Go 语言天生支持多线程(goroutines)和并发,这使得 esbuild 在编译 JavaScript 和 CSS 时能够充分利用多核 CPU 的性能。
- esbuild 专注于构建时的快速性能,采用了简单而高效的算法,避免了像 Babel 这样的工具在转换代码时可能产生的额外开销。
- Vite 的开发环境:
- Vite 在开发环境中使用原生 ES 模块(ESM)进行热模块替换(HMR),这避免了传统的打包步骤,从而大大加快了启动速度和模块更新速度。
- Vite 利用了浏览器的原生 ESM 支持,将项目中的模块按需提供给浏览器,而不是将所有代码打包成一个或多个 bundle。
- Vite 的构建过程:
- 虽然 Vite 在开发环境中使用了 esbuild,但在生产构建时,它默认使用 Rollup 进行打包。Rollup 是一个专注于打包 ES6 模块的打包器,它本身并不比 webpack 或其他打包器快很多,但在配合 Vite 的优化策略(如预构建、依赖预处理等)时,可以提供不错的构建性能。
- Vite 的构建性能并不是其主打优势,其主要优势在于开发环境的快速反馈和原生 ESM 的支持。然而,通过一些配置和优化,Vite 的生产构建也可以达到相当的性能水平。
- Node.js 的单线程与多线程:
- Node.js 本身是单线程的,但它是基于事件循环和非阻塞 I/O 设计的,这使得它能够在高并发环境下表现出色。此外,Node.js 可以通过 Worker Threads 实现多线程编程,但需要注意的是,这与 Go 语言的 goroutine 在设计和使用上有所不同。
- 尽管 Node.js 在某些情况下可能不如 Go 语言快,但它仍然是一个强大且灵活的平台,特别适合构建 Web 应用程序和工具。
总之,Vite 的快速开发体验主要归功于其使用原生 ESM 和 esbuild 的优势,而在生产构建方面,虽然它默认使用 Rollup,但通过适当的配置和优化也可以实现不错的性能。
补充:
vite主要优化配置
- 基于ES模块的快速热更新:Vite 利用浏览器对 ES 模块的支持,能够在开发环境下实现近乎即时的模块热替换(HMR),无需打包整个项目,仅重新加载变更的模块,极大提高了开发速度。
- 按需编译:在开发模式下,Vite 对每个请求的模块进行即时编译,而不是像传统构建工具那样预先打包所有资源。这种方式减少了初次启动和更改后重新加载的时间。
- 预构建依赖:Vite 会在启动时预编译项目依赖到一个高效的格式,通常是 ES 模块格式,这样在开发过程中这些依赖就可以被浏览器直接加载,进一步加快加载速度。
- HTTP2 和缓存策略:Vite 利用 HTTP2 多路复用的特性,将多个模块请求合并到少量的连接中,减少网络延迟。同时,通过ETag和强缓存策略确保静态资源能够被有效缓存,提升加载速度。
- Rollup 打包:在生产构建时,Vite 使用 Rollup 进行代码打包,Rollup 以其高效的树摇(Tree Shaking)和代码分割能力著称,帮助减小最终输出的文件大小。
- 懒加载和代码分割:Vite 支持自动代码分割,允许将应用程序的不同部分拆分成单独的 chunks,仅在需要时加载,这对于大型应用来说可以显著提升初始加载速度。
- Vue 3 的原生支持:Vite 对 Vue 3 提供了原生支持,利用Vue 3的Composition API和其它性能优化特性,进一步提升Vue应用的运行效率。
- Dev Server 配置优化:Vite 的 dev server 配置简单且强大,提供了很多开箱即用的优化选项,比如对静态资源的处理、代理设置等,便于开发者根据需要进行微调。
webpack 构建流程简单说一下
初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
确定入口:根据配置中的 entry 找出所有的入口文件
编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖文件都经过了本步骤的处理
完成模块编译:在经过第 4 步使用的 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及他们之间的依赖关系
输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这部是可以修改输出内容的最后机会
输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上的过程中, webpack 会在特定的时间点广告处特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 webpack 提供的 API 改变 Webpack 的运行结果
简单的说:
初始化:启动构建,读取与合并配置参数,加载 plugin,实例化 Compiler
编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行便已处理
输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换为文件,输出到文件系统中
source map 是什么?生产环境怎么用?
source map 是将编译、打包、压缩后的 diamante 映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要吊事源码就需要 source map
map 文件只要不打开开发者工具,浏览器是不会加载的
线上环境一般有三种处理方案:
hidden-source-map:借助第三方错误监控平台 Sentry 使用
nosources-source-map:只会显示具体行数以及查看源代码的错误栈。安全性比 sourcemap 高
sourcemap:通过 nginx 设置将 .map 文件只对白名单开发(公司内网)
注意:避免在生产中使用 inline- 和 eval- ,因为它们会增加 bundle 体积大小,并降低整体性能
webpack 的 Loader 是什么?
webpack 自带的打包器只支持 JS 文件,当我们想加载 css/less/scss/ts/md 文件时,就需要用 loader
loader 的原理就是把 文件内容包装成能运行的 JS
比如加载 css 需要用到 style-loader 和 css-loader
css-loader 把代码从 CSS 代码变成 export default str 形式的 JS 代码
style-loader 把代码挂载到 head 里 的 style 标签里
loader 执行顺序
在webpack中,loader的执行顺序是从右到左,从下到上。
如果webpack遇到一个.less文件,它会首先使用less-loader/sass-loader来处理它,然后使用css-loader,最后使用style-loader。
webpack主要分为四块:entry、output、loader、plugin
loader和plugin的区别
Loader 也叫做就是“加载器”,因为 webpack 原生只能解析 js 文件,而对于其他类型文件,则需要借助 loader。所以 loader 的作用就是实现对不同格式文件的解析和处理,例如把 ES6 转换成 ES5,TypeScript 转换为 JavaScript等,使其能够添加到依赖关系中。
Plugin 就是我们常说的"插件"。主要是扩展 webpack 的功能,增加额外的构建任务。例如打包优化、环境变量注入、优化构建速度等,它们会运行在
webpack
的不同阶段(钩子 / 生命周期),贯穿了webpack
整个编译周期。
1.两者都是为了扩展webpack的功能,loader它只专注于转化文件(transform)这一领域,完成压缩,打包,语言编译,而plugin不仅只局限在打包,资源的加载上,还可以打包优化和压缩,重新定义环境变量等。
2.loader运行在打包文件之前,(loader为在模块加载时的预处理文件);plugins在整个编译周期都起作用。
3.一个loader的职责是单一的,只需要完成一种转换,一个loader其实就是一个Nod.js模块。当需要调用多个loader去转换一个文件时,每个loader会链式打的顺序执行
4.在webpack运行的生命周期中会广播出许多事件,piugin会监听这些事件,在核实的时机通过webpack提供的API改变输出结果。
有哪些常⻅的Loader?
raw-loader
: 加载文件原始内容file-loader
: 把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件(处理图片和字体)url-loader
: 与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值时返回其 publicPath,小于阈值时返回文件 base64 形式编码(处理图片和字体)source-map-loader
:加载额外的 source map 文件,以方便断点调试svg-inline-loader
:将压缩后的 svg 内容注入代码中image-loader
:加载并且压缩图片文件json-loader
:加载 json 文件babel-loader
:将 es6 转换成 es5markdown-loader
:把 markdown 变成 htmlmarkdown-loader
:把 markdown 转换成 htmlts-loader
:将 typescript 转换成 javascript,并提示类型错误sass-loader
:将 sass 代码转换为 css 代码css-loader
:把 CSS 变成 JS 字符串style-loader
: 把 JS 字符串变成 style 标签postcss-loader
:扩展 css 语法,使用下一代 css,可以配合 autoprefixer 插件自动补齐 css3 前缀eslint-loader
:通过 eslint 检查 javascript 代码tslint-loader
:通过 tslint 检查 typescript 代码thread-loader
:用于多进程打包
有哪些常⻅的Plugin?
uglifyjs-webpack-plugin
:通过 UglifyES 压缩 ES6 代码webpack-parallel-uglify-plugin
: 多核压缩,提⾼压缩速度webpack-bundle-analyzer
: 可视化webpack输出⽂件的体积html-webpack-plugin
:用于创建 HTML 页面并自动引入JS 和 CSSclean-webpack-plugin
:用于清理之前打包的残余文件mini-css-extract-plugin
:用于将 JS 中的 CSS 抽离成单独的 CSS 文件SplitChunksPlugin
:用于代码分包(Code Split)DllPlugin
+DllReferencePlugin
:用于避免大依赖被频繁重新打包,大幅降低打包时间eslint-webpack-plugin
:用于检查代码中的错误DefinePlugin
:用于在 webpack config 里添加全局变量copy-webpack-plugin
:用于拷贝静态文件到 dist
bundle,chunk,module是什么?
- bundle:是由webpack打包出来的⽂件;
- chunk:代码块,⼀个chunk由多个模块组合⽽成,⽤于代码的合并和分割;
- module:是开发中的单个模块,在webpack的世界,⼀切皆模块,⼀个模块对应⼀个⽂件,webpack会从配置的 entry中递归开始找出所有依赖的模块。
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 的实例,参数都通过构造函数传⼊。
编写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 体积进行监控和和分析?
VSCode
中有个插件 Import Cost
可以帮组我们对引入的模块的大小进行实时监测,还可以和使用 webpack-bundle-analyzer
生成 bundle
的模块组成图,显示所占体积
bundlesize
工具包可以进行自动化资源体积监控。
文件指纹是什么?怎么用?
文件指纹是打包后输出的文件名的后缀
Hash
:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改Chunkhash
:和 webpack 打包的 chunk 有关,不同的 entry 会发出不同的 chunkhasContenthash
:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变
如何编写loader
定义: loader 只是一个导出为函数的 JavaScript 模块。
**一个简单的loader即可以通过编写一个简单的JavaScript模块,并将模块导出即可。**即如下, 接受一个source当前源码,并返回新的源码即可。
module.exports = function(source) {
return source
}
- Loader 支持链式调用,所以开发商需要严格遵循“单一职责”,每个 Loader 只负责自己需要负责的事件。
过程:
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 npm 链接
resolveloader 分解加速器。
loader的执行顺序:由后向前,由下自上
其中,多个loader的执行顺序是由后向前执行,例如:
module.exports = {
module: {
rules:[{
test:/.less$/,
use: [
'style-loader',
'css-loader',
'less-loader'
]
}]
}
}
运行代码:https://github.com/PCAaron/blogCode/tree/master/webpack/loader-order
loader的开发和调试:loader-runner
loader-runner运行在不安装webpack的情况下运行loaders,进行loader的开发和调试。其中,在webpack中, 作为webpack的依赖,webpack使用它执行loader。
- loader-runner的使用:
import { runLoaders } from 'loader-runner'
runLoaders({
// String: 资源的绝对路径(可以增加查询字符串)
resource: "/abs/path/to/file.txt?query",
// String[]: loader 的绝对路径(可以增加查询字符串)
loaders: ["/abs/path/to/loader.js?query"],
// 基础上下文之外的额外 loader 上下文
context: { minimize: true },
// 读取资源的函数
readResource: fs.readFile.bind(fs)
}, function(err, result) {
// err: Error?
// result.result: Buffer | String
// result.resourceBuffer: Buffer
// result.cacheable: Bool 结果是否需要缓存
// result.fileDependencies: String[] 解析结果需要依赖的文件
// result.contextDependencies: String[] 上下文依赖
})
- 调试raw-loader 实现开发一个loader,将文件内容转换为模块:处理特殊字符转义并以模块方式导出。
//src/raw-loader
module.exports = function (source) {
const json = JSON.stringify(source)
json.replace(/\u2028/g,'\\u2028')
.replace(/\u2029/g,'\\u2029')
return `export default ${json}`
}
新建run-loader.js调试raw-loader。
//run-loader.js
const fs = require('fs')
const path = require('path')
const { runLoaders } = require('loader-runner')
runLoaders({
resource: path.join(__dirname, './src/demo.txt'),
loaders: [
path.join(__dirname, './src/raw-loader.js')
],
context: { minimize: true },
readResource: fs.readFile.bind(fs)
},(err, result) => {
err ? console.error(err) : console.log(result)
})
线上代码:https://github.com/PCAaron/blogCode/tree/master/webpack/raw-loader
loader的参数获取loader-utils
通过loader-utils的getOptions方法可以获取到loader传递过来的参数。
//run-loader.js
runLoaders({
loaders: [
{
loader: path.join(__dirname, './src/raw-loader.js'),
options: {
name: 'test'
}
}
],
...
})
//src/raw-loader.js
const loaderUtils = require('loader-utils')
module.exports = function (source) {
const { name } = loaderUtils.getOptions(this)
console.log(name)
}
loader API
loader-runner提供一些API给给定函数调用。
loader的同步处理
实现同步的loader的异常处理:
- 可通过在loader内直接抛出错误throw new Error(err);
- 亦可以通过loader上下文的回调函数this.callback传递错误并返回回调内容
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap, // 必须是一个可以被这个模块解析的 source map
meta?: any // 会被 webpack 忽略,可以是任何东西(例如一些元数据)
);
//src/raw-loader.js
module.exports = function (source) {
const json = JSON.stringify(source)
...
//return `export default ${json}`
// throw new Error('err')
this.callback(null, json)
}
loader的异步处理
通过loader上下文函数this.async来返回一个异步函数,其中,第一个参数为Error,第二个参数为回调内容。
// src/raw-loader.js
const path = require('path')
const fs = require('fs')
module.exports = function (source) {
// 异步方式
const callback = this.async()
...
fs.readFile(path.join(__dirname, './async.txt'), 'utf-8', (err, data) => {
if(err) {
callback(err, '')
return
}
callback(null, data)
})
}
loader中使用缓存
在webpack中,会默认开启loader缓存,如果不需要缓存则可以通过this.cacheable(false)关闭。 另外,缓存使用的条件需要在,loader的结果在相同的输入下有确定的输出,并且在有依赖的loader下是 无法使用缓存的。
this.emitFile:文件输出
通过loader上下文函数this.emitFile可以将内容输出到指定目录。
如何编写plugin
插件结构
插件是伴随webpack构建的初始化到最后文件生成的整个生命周期,插件的目的是在于解决loader无法实现的其他事情。 另外,插件没有像loader那样的独立运行环境,所以插件的开发只能在webpack里面运行。
插件的基本结构:
// 提供一个class类
class Plugin{
// 接受插件传递的参数
constructor(options){
...
}
// 插件上提供apply方法,webpack提供compiler对象
apply(compiler){
// 指定一个挂载到 webpack 自身的事件钩子
// compiler提供hooks钩子方法,作用于构建流程中
compiler.hooks.done.tap('Plugin',
// 功能完成后调用 webpack 提供的回调
// 插件处理逻辑
()=>{
...
})
}
}
module.exports = Plugin
至此,了解到webpack插件的组成部分:
提供一个class类函数
constructor 接受插件传递的参数
constructor(options){
...
}
插件上提供apply方法,接受compiler参数。compiler 对象代表了完整的 webpack 环境配置。 这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。
指定一个挂载到 webpack 自身的事件钩子,使用 compiler 对象时,可以绑定提供了编译 compilation 引用的回调函数, 一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。
功能完成后调用 webpack 提供的回调
插件事件处理
插件中获取传递的参数
通过插件的构造函数进行获取。
class Plugin{
// 接受插件传递的参数
constructor(options){
this.options = options
}
}
异步插件
有一些编译插件中的步骤是异步的,这样就需要额外传入一个 callback 回调函数,并且在插件运行结束时,必须调用这个回调函数。
插件的错误处理
- 参数校验阶段可以直接throw的方式将错误抛出
throw new Error('error options')
- 如果已经进入到hooks钩子阶段,可以通过compilation对象的warnings和errors接收
compilation.warnings.push('warnings')
compilation.errors.push('error')
对资源文件处理:通过Compilation进行文件写入
对于一些插件,本身需要产生一些静态资源。比如生成zip包,之前源码浏览时候有了解到,文件生成阶段 是在emit阶段,则我们可以监听compiler的emit钩子阶段,获取到compilation对象,并将最终的内容 设置到compilation.assets对象上输出到磁盘目录。 另外,文件的写入,一般需要借助webpack-sources
const { RawSource } = require('webpack-sources')
module.exports = class Plugin {
constructor(options){
this.options = options
}
apply(compiler){
compiler.plugin("emit", function(compilation, callback) {
compilation.assets['name'] = new RawSource('demo')
callback()
});
}
}
插件扩展:编写插件的插件
插件自身也可以通过暴露hooks的方式进行自身扩展,例如html-webpack-plugin就暴露了 html-webpack-plugin-after-chunk(Sync)、html-webpack-plugin-before-html-generation(Async)等钩子。
插件实战
编写一个压缩构建资源为zip包的插件。其中,可实现根据参数生成zip包的文件名称。
实现思路:
- 使用jszip实现zip包生成。
- 使用compiler对象的emit钩子,compiler.hooks.emit对tapAsync方法实现监听,在emit 生成文件阶段,将zip包设置到compilation.assets对象上。
//zip-webpack-plugin
const path = require('path')
const JSZip = require('jszip')
const { RawSource } = require('webpack-sources')
const zip = new JSZip()
class MinZipWebpackPlugin{
constructor(options){
this.options = options
}
apply(compiler){
compiler.hooks.emit.tapAsync('MinZipWebpackPlugin', (compilation,callback)=>{
// zip.folder,创建目录名称
const folder = zip.folder(this.options.filename);
// 遍历compilation.assets对象
for(let filename in compilation.assets){
// 获取source
const source = compilation.assets([filename]).source()
// 将source添加到folder中
folder.file(filename, source)
}
// 将内容生成zip
zip.generateAsync({
type: 'nodebuffer'
}).then((content)=>{
// 获取output(绝对路径)
const outputPath = path.join(
compilation.options.output.path,
this.options.filename + '.zip'
)
const outputRelativePath = path.relative(
compilation.options.output.path,
outputPath
)
// 将内容挂载到compilation.assets上,并将buffer转换为source
compilation.assets[outputRelativePath] = new RawSource(content)
callback()
})
})
}
}
module.exports = MinZipWebpackPlugin
线上代码:https://github.com/PCAaron/min-zip-webpack-plugin,
参考网站:https://developer.aliyun.com/article/916716
webpack热更新的实现原理?
webpack的热更新⼜称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不⽤刷新浏览器⽽将新变更的模块替换掉旧的模块。
原理:
答案1:
⾸先要知道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 操作,也就是进⾏浏览器刷新来获取最新打包代码。
答案2:
webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
HMR 的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff(chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket
,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax
请求来获取更新内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp
请求获取该 chunk 的增量更新
后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HocModulePlugin
来完成,提供了相关 API 以供开发者针对自身场景进行处理,像 react-hot-loader
和 vue-loader
都是借助这些 API 实现 HMR。
原理请参考 webpack HMR 原理解析
如何⽤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
- 外部扩展(externals): 将不怎么需要更新的第三⽅库脱离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的构建速度?
可以讲很多,我觉得按重要性来说
- 使用
高版本
的 webpack 和 Node.js 使用高版本
的 webpack 和 Node.js 多进程/多实例构建
: HappyPack(不维护了)、thread-loader多进程/多实例构建
: HappyPack(不维护了)、thread-loader- 压缩代码
- webpack-paralle-uglify-plugin webpack-plugin-uglify-plug
- 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 将不常变化的代码提前打包,并复用,如 vue、react
- 使用 DllPlugin 进行分包,使用 DllReferencePlugin(索引链接)对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间
- HashedModuleIdsPlugin 可以解决模块数字 id 问题
- 充分利用缓存提升二次构建速度
- babel-loader 开启缓存
- terser-webpack-plugin 开启缓存
- 使用 cache-loader 或者 hard-source-webpack-plugin
- Tree shaking
- purgecss-webpack-plugin 和 mini-css-extract-plugin 配合使用(建议)
- 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的 bundle 中去掉(只能对 ES6 Modlue 生效)开发中尽可能使用 ES6 Module 的模块,提高 tree shaking 效率
- 禁用 babel-loader 的模块依赖解析,否则 webpack 接受到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
- 动态 polyfill
- 建议采用 polyfill-service 只给用户返回需要的 polyfill,社区维护
- 处于生产环境时,关闭不必要的环节,比如可以关闭 source map
如何⽤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(hmr) 热更新的实现原理?
webpack的热更新⼜称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不⽤刷新浏览器⽽将新变更的模块替换掉旧的模块。
原理:
当运行的时候,它会向客户端添加两个文件,这两个文件的目的: 1、websocket文件用于与服务端进行通信 2、客户端获取到需要更新的模块,进行重新执行并更新。
然后它会接着开启两个服务: 1、HTTP服务:用于客户端去请求获取编译完成之后的代码块 2、WebSocket服务:当有模块发生改变,并且编译完成,用于通知客户端去主动请求新的模块,进行热更新
⾸先要知道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 操作,也就是进⾏浏览器刷新来获取最新打包代码。
如何减少 Webpack 打包体积
(1)按需加载
在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 loadash
这种大型类库同样可以使用这个功能。
按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise
,当 Promise
成功以后去执行回调。
(2)Scope Hoisting
Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。
比如希望打包两个文件:
// test.js
export const a = 1
// index.js
import { a } from './test.js'
对于这种情况,打包出来的代码会类似这样:
[
/* 0 */
function (module, exports, require) {
//...
},
/* 1 */
function (module, exports, require) {
//...
}
]
但是如果使用 Scope Hoisting ,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码:
[
/* 0 */
function (module, exports, require) {
//...
}
]
这样的打包方式生成的代码明显比之前的少多了。如果在 Webpack4 中你希望开启这个功能,只需要启用 optimization.concatenateModules
就可以了:
module.exports = {
optimization: {
concatenateModules: true
}
}
(3)Tree Shaking
Tree Shaking 可以实现删除项目中未被引用的代码,比如:
// test.js
export const a = 1
export const b = 2
// index.js
import { a } from './test.js'
对于以上情况,test
文件中的变量 b
如果没有在项目中使用到的话,就不会被打包到文件中。
如果使用 Webpack 4 的话,开启生产环境就会自动启动这个优化功能。
webpack如何拆包?
Webpack 拆包主要通过两种方式实现:
- Entry Points:通过多个入口配置多出口,Webpack 会根据依赖关系自动拆分成多个 chunk。
- SplitChunksPlugin:该插件会分析模块间的依赖关系,提取多个模块公共的部分到单独的 chunk 中。
Entry Points 的拆包方式比较简单,直接在 entry 配置多个入口,并在 output 中配置 [name] 占位符,Webpack 会自动将相互依赖的模块分配到同一个 chunk,相互独立的模块会分配到不同chunk。
例如:
```js
module.exports = {
entry: {
index: './src/index.js',
other: './src/other.js'
},
output: {
filename: '[name].bundle.js'
}
}
```
这会生成 index.bundle.js 和 other.bundle.js 两个 chunk 文件。
SplitChunksPlugin 的拆包更加智能,它会检测模块之间的依赖关系,将“公共”模块与其直接依赖的模块拆出到单独的 chunk 中,以实现最优的拆包效果。
要使用该插件,需要在 Webpack 配置中加插件配置:
```js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
```
这会自动从所有 chunk 中提取共用的模块到单独的 chunk。也可以通过设置 cacheGroups 自定义规则来控制拆包策略。
拆包可以使得初始 bundles 更小、加载更快,并且方便了长效缓存利用。但拆包过度会造成过多的请求,可以根据需要设置不同的拆包策略。
怎么配置单⻚应⽤?怎么配置多⻚应⽤?
单⻚应⽤可以理解为webpack的标准模式,直接在 entry 中指定单⻚应⽤的⼊⼝即可,这⾥不再赘述多⻚应⽤的话,可以使⽤webpack的 AutoWebPlugin 来完成简单⾃动化的构建,但是前提是项⽬的⽬录结构必须遵守他预设的规范。 多⻚应⽤中要注意的是:
- 每个⻚⾯都有公共的代码,可以将这些代码抽离出来,避免重复的加载。⽐如,每个⻚⾯都引⽤了同⼀套css样式表
- 随着业务的不断扩展,⻚⾯可能会不断的追加,所以⼀定要让⼊⼝的配置⾜够灵活,避免每次添加新⻚⾯还需要修改构建配置
Babel的原理是什么**?**
babel 的转译过程也分为三个阶段,这三步具体是:
解析 Parse: 将代码解析⽣成抽象语法树(AST),即词法分析与语法分析的过程;
转换 Transform: 对于 AST 进⾏变换⼀系列的操作,babel 接受得到 AST 并通过 babel-traverse 对其进⾏遍历,在此过程中进⾏添加、更新及移除等操作;
⽣成 Generate: 将变换后的 AST 再转换为 JS 代码, 使⽤到的模块是 babel-generator。
处理jsx 使用那个loader
在webpack中,处理JSX文件需要使用babel-loader
配合@babel/preset-react
预设来转换JSX语法。
CDN webpack配置案例
本节目标:
能够对第三方包使用CDN优化
分析说明:通过 craco(ke rua ko) 来修改 webpack 配置,从而实现 CDN 优化
核心代码
craco.config.js
// 添加自定义对于webpack的配置
const path = require('path')
const { whenProd, getPlugin, pluginByName } = require('@craco/craco')
module.exports = {
// webpack 配置
webpack: {
// 配置别名
alias: {
// 约定:使用 @ 表示 src 文件所在路径
'@': path.resolve(__dirname, 'src')
},
// 配置webpack
// 配置CDN
configure: (webpackConfig) => {
// webpackConfig自动注入的webpack配置对象
// 可以在这个函数中对它进行详细的自定义配置
// 只要最后return出去就行
let cdn = {
js: [],
css: []
}
// 只有生产环境才配置
whenProd(() => {
// key:需要不参与打包的具体的包
// value: cdn文件中 挂载于全局的变量名称 为了替换之前在开发环境下
// 通过import 导入的 react / react-dom
//打包时候排除要使用cdn加速的包
webpackConfig.externals = {
react: 'React',
'react-dom': 'ReactDOM'
}
// 配置现成的cdn 资源数组,为了在index.html中使用配置好的cdn资源 (现在是公共为了测试)
// 实际开发的时候 用公司自己花钱买的cdn服务器
cdn = {
js: [
'https://unpkg.com/react@18/umd/react.production.min.js',
'https://unpkg.com/react-dom@18/umd/react-dom.production.min.js',
],
css: []
}
})
// 为了将来配置 htmlWebpackPlugin插件 将来在public/index.html注入
// cdn资源数组时 准备好的一些现成的资源
//HtmlWebpackPlugin 是一个流行的 Webpack 插件,用于生成 HTML 文件。
const { isFound, match } = getPlugin(
webpackConfig,
pluginByName('HtmlWebpackPlugin')
)
if (isFound) {
// 如果找到了HtmlWebpackPlugin的插件 在其中options中增加cdn配置项,这样index.html就能读取到这个cdn参数值
match.options.cdn = cdn
}
return webpackConfig
}
}
}
public/index.html
<body>
<div id="root"></div>
<!-- 加载第三发包的 CDN 链接 -->
<% htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css.forEach(cdnURL => { %>
<link rel="stylesheet" href="<%= cdnURL %>"/>
<% }) %>
<% htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js.forEach(url => { %>
<script src="<%= url %>"></script>
<% }) %>
</body>
查看效果
我们可以在打包之后,通过切换路由,监控network面板资源的请求情况,验证是否分隔成功