Webpack+实现原理

什么是webpack?

1.webpack是一套基于NodeJS的"模块打包工具",
在webpack刚推出的时候就是一个单纯的JS模块打包工具,可以将多个模块的JS文件合并打包到一个文件中
但是随着时间的推移、众多开发者的追捧和众多开发者的贡献
现在webpack不仅仅能够打包JS模块, 还可以打包CSS/LESS/SCSS/图片等其它文件

2.为什么要分模块?
如果将所有的JS代码都写到一个文件中, 十分不利于代码的维护和复用
所以我们可以将不同的功能写到不同的模块中, 这样就提升了代码的维护性和复用性
但是当将代码写到不同模块时新的问题又出现了,
例如: 导入资源变多了, 请求次数变多了, 网页性能也就差了
例如: 不同功能都放到了不同模块中了, 那么如何维护模块之间的关系也变成一个难题了
<script src="./header.js"></script>
<script src="./content.js"></script>
<script src="./index.js"></script>
<script src="./footer.js"></script> // 如果index.js中用到了footer,就会报错
例如: ... ...

3.如何解决上述问题
3.1项目上线时将用到的所有模块都合并到一个文件中
3.2在index.html中只导入主文件, 再主文件中再导入依赖模块

4.如何通过webpack来打包JS模块
4.1安装webpack
npm init -y
npm install --save-dev webpack
npm install --save-dev webpack-cli
4.2在终端中输入打包的指令
npx webpack index.js
注意点:
index.js就是需要打包的文件
打包之后的文件会放到dist目录中, 名称叫做main.js
-->



什么是webpack配置文件?

1.我们在打包JS文件的时候需要输入: npx webpack index.js
这句指令的含义是: 利用webpack将index.js和它依赖的模块打包到一个文件中
其实在webpack指令中除了可以通过命令行的方式告诉webpack需要打包哪个文件以外,
还可以通过配置文件的方式告诉webpack需要打包哪个文件

2.webpack常见配置
entry: 需要打包的文件
output: 打包之后输出路径和文件名称
mode: 打包模式 development/production
development: 不会压缩打包后的JS代码
production: 会自动压缩打包后的JS代码
-->

3.webpack配置注意事项
配置文件的名称必须叫做: webpack.config.js, 否则直接输入 npx webpack打包会出错
如果要使用其它名称, 那么在输入打包命令时候必须通过 --config 指定配置文件名称
npx webpack --config xxx

4.打包命令简化
每次输入npx webpack --config xxx来打包文件会有一点蛋疼, 所以我们可以通过npm script来简化这个操作

什么是sourcemap?

1.webpack打包后的文件会自动添加很多代码, 在开发过程中非常不利于我们去调试
因为如果运行webpack打包后的代码,错误提示的内容也是打包后文件的内容
所以为了降低调试的难度, 提高错误代码的阅读性, 我们就需要知道打包后代码和打包之前代码的映射关系
只要有了这个映射关系我们就能很好的显示错误提示的内容, 存储这个映射关系的文件我们就称之为sourcemap

2.如何开启sourcemap
https://www.webpackjs.com/configuration/devtool/
2.1在webpack.config.js中添加
devtool: "xxx",
2.2各配置项说明:
eval:
不会单独生成sourcemap文件, 会将映射关系存储到打包的文件中, 并且通过eval存储
优势: 性能最好
缺点: 业务逻辑比较复杂时候提示信息可能不全面不正确

source-map:
会单独生成sourcemap文件, 通过单独文件来存储映射关系
优势: 提示信息全面,可以直接定位到错误代码的行和列
缺点: 打包速度慢

inline:
不会单独生成sourcemap文件, 会将映射关系存储到打包的文件中, 并且通过base64字符串形式存储

cheap:
生成的映射信息只能定位到错误行不能定位到错误列

module:
不仅希望存储我们代码的映射关系, 还希望存储第三方模块映射关系, 以便于第三方模块出错时也能更好的排错

2.3企业开发配置:
development: cheap-module-eval-source-map
只需要行错误信息, 并且包含第三方模块错误信息, 并且不会生成单独sourcemap文件

production: cheap-module-source-map
只需要行错误信息, 并且包含第三方模块错误信息, 并且会生成单独sourcemap文件

什么是loader?

1.webapck的本质是一个模块打包工具, 所以webpack默认只能处理JS文件,不能处理其他文件,
因为其他文件中没有模块的概念, 但是在企业开发中我们除了需要对JS进行打包以外,
还有可能需要对图片/CSS等进行打包, 所以为了能够让webpack能够对其它的文件类型进行打包,
在打包之前就必须将其它类型文件转换为webpack能够识别处理的模块,
用于将其它类型文件转换为webpack能够识别处理模块的工具我们就称之为loader

2.如何使用loader
webpack中的loader都是用NodeJS编写的, 但是在企业开发中我们完全没有必要自己编写,
因为已经有众多大神帮我们编写好了企业中常用的loader, 我们只需要安装、配置、使用即可

2.1通过npm安装对应的loader
2.2按照loader作者的要求在webpack进行相关配置
2.3使用配置好的loader

3.fileloader使用
https://www.webpackjs.com/loaders/file-loader/
3.1安装file-loader
npm install --save-dev file-loader
3.2在webpack.config.js中配置file-loader
module: {
rules: [
{
test: /.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {}
}
]
}
]
}

注意点:
默认情况下fileloader生成的图片名就是文件内容的 MD5 哈希值
如何想打包后不修改图片的名称, 那么可以新增配置 name: "[name].[ext]"
其它命名规则详见: placeholders

注意点:
默认情况下fileloader会将生成的图片放到dist根目录下面
如果想打包之后放到指定目录下面, 那么可以新增配置 outputPath: "images/"

注意点:
如果需要将图片托管到其它服务器, 那么只需在打包之前配置 publicPath: "托管服务器地址"即可

urlloader

1.url-loader 功能类似于 file-loader,
但是在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL

2.urlloader使用
https://www.webpackjs.com/loaders/url-loader/
2.1安装urlloader
npm install --save-dev url-loader
2.2配置urlloader
{
test: /.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
name: "[name].[ext]",
outputPath: "/images",
limit: 1024
}
}
]
}

优势:
图片比较小的时候直接转换成base64字符串图片, 减少请求次数
图片比较大的时候由于生成的base64字符串图片也比较大, 就保持原有的图片

css-loader

1.和图片一样webpack默认能不能处理CSS文件, 所以也需要借助loader将CSS文件转换为webpack能够处理的类型

2.css-loader使用:
2.1安装cssloader
npm install --save-dev css-loader
2.2安装styleloader
npm install style-loader --save-dev
2.3配置css-loader
{
test: /.css$/,
use: [ 'style-loader', 'css-loader' ]
}

3.css-loader和style-loader作用
css-loader: 解析css文件中的@import依赖关系
style-loader: 将webpack处理之后的内容插入到HTML的HEAD代码中

4.loader特点:
4.1单一原则, 一个loader只做一件事情
4.2多个loader会按照从右至左, 从下至上的顺序执行
例如: 从右至左
[ 'style-loader', 'css-loader' ]
先执行css-loader解析css文件关系拿到所有内容,
再执行style-loader将内容插入到HTML的HEAD代码中
例如: 从下至上
[{
loader: "style-loader"
},{
loader: "css-loader"
}]
先执行css-loader解析css文件关系拿到所有内容,
再执行style-loader将内容插入到HTML的HEAD代码中

ES6模块化

1.在ES6出现之前,JS不像其他语言拥有“模块化”这一概念,于是为了支持JS模块化
我们使用类、立即执行函数或者第三方插件(RequireJS、seaJS)来实现模块化
但是在ES6出现之后, 上述解决方案都已经被废弃, 因为ES6中正式引入了模块化的概念

ES6模块化模块和NodeJS中一样, 一个文件就是一个模块, 模块中的数据都是私有的
ES6模块化模块和NodeJS中一样, 可以通过对应的关键字暴露模块中的数据,
可以通过对应的关键字导入模块, 使用模块中暴露的数据

2.ES6模块化使用
2.1常规导出
2.1.1分开导入导出
export xxx;
import {xxx} from "path";

2.1.2一次性导入导出
export {xxx, yyy, zzz};
import {xxx, yyy, zzz} from "path";

注意点:
接收导入变量名必须和导出变量名一致
如果想修改接收变量名可以通过 xxx as newName方式
变量名被修改后原有变量名自动失效

2.2默认导入导出
export default xxx;
import xxx from "path";

注意点:
一个模块只能使用一次默认导出, 多次无效
默认导出时, 导入的名称可以和导出的名称不一致

什么是Tree-Shaking?

1.过滤掉无用的JS代码和CSS代码, 我们称之为Tree-Shaking
例如: 在a.js中引入了b模块, b模块中有2个方法, 但是我只用到了1个方法
默认情况下会将b模块中所有代码都打包到a.js中,
为了提升网页性能降低打包体积, 我们可以只将用到的方法打包到a.js中

2.webpack中如何开启Tree-Shaking?
https://www.webpackjs.com/guides/tree-shaking/
2.1开发环境
webpack.config.js配置, 告诉webpack只打包导入模块中用到的内容
optimization: {
usedExports: true
},
package.json配置, 告诉webpack哪些文件不做Tree-Shaking
"sideEffects": [".css", ".less", "*.scss"],

2.2生产环境
无需进行任何配置, webpack默认已经实现了Tree-Shaking

注意点:

  • 只有ES Modle导入才支持Tree-Shaking
  • 任何导入的文件都会受到 tree shaking 的影响。
    这意味着,如果在项目中使用类似 css-loader 并导入 CSS 文件,
    则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:
CSS模块Tree-Shaking

1.不光JS模块可以进行Tree-Shaking, CSS模块也可以进行Tree-Shaking

2.如何开启CSS模块Tree-Shaking
https://github.com/webpack-contrib/purifycss-webpack
2.1安装相关插件
npm i -D purifycss-webpack purify-css glob-all
2.2配置插件
const PurifyCSS = require("purifycss-webpack");
const glob = require("glob-all");

new PurifyCSS({
paths: glob.sync([
// 要做CSS Tree Shaking的路径文件
path.resolve(__dirname, "./.html"),
path.resolve(__dirname, "./src/js/
.js"),
])
}),

什么是Code-Splitting(代码分割)?

1.默认情况下webpack会将所有引入的模块都打包到一个文件中,
这样就导致了打包后的文件比较大, 以及修改文件后用户需要重新下载所有打包内容问题
例如: 在a.js中引入了b.js, 那么a.js和b.js都会被打包到bundle.js中
如果a.js有1MB, b.js也有1MB, 那么打包之后的文件就有2MB
那么用户第一次打开网页的时候就需要下载2MB的文件
问题的关键在于, 如果我们修改了a.js, 但没有修改b.js
重新打包后用户需要重新下载新打包的文件(因为用户本地缓存的是a和b的合体)
这样就导致了每次修改了其中一个文件用户都要重新下载所有内容
解决方案: 将不经常修改的内容打包到另一个文件中, 这样每次修改后用户就只用下载修改后的文件
没有被修改的文件由于用户上一次打开已经缓存在了本地就不用下载了, 这样性能也提升了
Code-Splitting就是将不经常修改的模块打包到单独的文件中, 避免每次修改用户都需要重新下载所有内容

2.如何开启Code-Splitting
2.1手动分割(了解)

  • 在单独文件中引入模块, 将模块中的内容添加到window上
  • 修改配置文件同时打包多个文件
    entry: {
    calculate: "./src/js/calculate.js", // 先打包会被先引入
    main: "./src/js/index.js",
    },
    output: {
    filename: "js/[name].js",
    path: path.resolve(__dirname, "bundle")
    },

2.2自动分割
webpack会自动判断是否需要分割, 如果需要会自动帮助我们风格
optimization: {
splitChunks: {
chunks: "all"
}
},

浏览器缓存问题

1.浏览器会自动缓存网页上的资源, 以便于提升下次访问的速度
但正式因为浏览器的缓存机制, 导致文件内容被修改之后只要文件名称没有发生变化
就不会重新去加载修改之后的资源, 所以刷新网页后显示的还是修改之前的内容
为了解决这个问题, 我们就需要在打包文件的时候给"文件名称加上内容的hash值"
一旦内容发生了变化, 内容的hash值就会发生变化, 文件的名称也会发生变化
一旦文件的名称发生了变化, 浏览器就会自动去加载新打包的文件

2.hash/chunkhash/contenthash
hash:
根据每次编译打包的内容生成的哈希值, 每次打包都不一样, 不能很好利用缓存, 不推荐
chunkhash:
根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。
在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,
接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响。
注意点: 只支持css和js, 不支持img等其它资源
contenthash(推荐):
根据某个文件内容生成的哈希值, 只要某个文件内容发生改变,该文件的contenthash就会发生变化

注意点: 在webpack4中contenthash和热更新有冲突, 所以在开发模式想使用contenthash需要关闭热更新
但是一般情况下我们需要通过hash解决的是线上代码的内容更新问题, 所以开发模式无关紧要

3.manifest
webpack在打包时,会把库和业务代码之间的关系做manifest,
它既存在于业务代码(main.js),也存在于库中(vendor.js),
在旧版webpack中(webpack4之前),mainfest在每次打包的时候的时候可能会变化,
所以contenthash值也会跟着变化。配置runtimeChunk后,会把manifest提取到runtime中,
这样打包就不会影响到其他js了。
optimization: {
runtimeChunk: "single",
splitChunks: {
chunks: "all",
},
}

什么是Split-Chunks-Plugin?

1.webpack在代码分割的时候底层使用的其实是Split-Chunks-Plugin来实现代码分割的
所以这个插件的作用就是进行代码分割

2.Split-Chunks-Plugin相关配置
https://www.webpackjs.com/plugins/split-chunks-plugin/
{
chunks: "async", // 对那些代码进行分割 async(只分割异步加载模块)、all(所有导入模块)
minSize: 30000, // 表示被分割的代码体积至少有多大才分割(单位是字节)
minChunks: 1, // 表示至少被引用多少次数才分割,默认为1
maxAsyncRequests: 5, // 异步加载并发最大请求数(保持默认即可)
maxInitialRequests: 3, // 最大的初始请求数(保持默认即可)
automaticNameDelimiter: '~', // 命名连接符
name: true, // 拆分出来块的名字使用0/1/2... 还是指定名称
cacheGroups: { // 缓存组, 将当前文件中导入的所有模块缓存起来统一处理
vendors: { // 分割从node_modules目录中导入的模块
test: /[\/]node_modules[\/]/,
priority: -10 // 优先级, 值越小越优先
},
default: { // 分割从其它地方导入的模块
minChunks: 2,
priority: -20,
reuseExistingChunk: true // 如果当前代码块包含的模块已经有了,就不在产生一个新的代码块
}
}
}

模块各种引入方式

1.1在HTML中全局引入
<script src="https://code.jquery.com/jquery-3.4.1.js"></script>
特点: 什么地方都可以使用
1.2通过npm安装通过import局部引入
特点: 只能在import导入的模块中使用

2.什么是Provide-Plugin?
https://www.webpackjs.com/plugins/provide-plugin/
自动加载模块,而不必到处 import 或 require
默认情况下模块中的数据都是私有的, 所以想要使用模块必须先导入模块
如果说在a.js中想要使用jQuery, 那么就必须在a.js中导入jQuery模块
如果说在b.js中想要使用jQuery, 那么就必须在b.js中导入jQuery模块
new Webpack.ProvidePlugin({
$: "jquery"
})

什么是imports-loader?

1.https://www.webpackjs.com/loaders/imports-loader/
https://www.npmjs.com/package/imports-loader
imports-loader和Provide-Plugin功能一样可以实现全局导入,
但是imports-loader的功能比Provide-Plugin更强大,
imports-loader除了可以实现全局导入以外, 还可以修改全局this指向
默认情况下模块中的this指向一个空对象, 我们可以通过imports-loader实现让this指向window

2.imports-loader注意点:
2.1在企业开发中如何需要实现全局导入, 更推荐使用ProvidePlugin来实现
因为ProvidePlugin是webpack内置的官方插件更靠谱
2.2使用imports-loader修改this指向, 系统会自动将我们的代码放到一个立即执行函数中
这就导致了在打包时候import不在第一行, 会报错
2.3如何解决?
无需修改this指向, 直接在模块中使用window

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容