1. 源码位置【source-map】
简单来讲: 当前我们的代码都经过压缩、编译、转译后生成的,和实际开发时的代码对比一看面目全非。那么当我们跑代码时,报错都报的转换后的代码,那怎么看到我们的原始代码?这时候就需要 我们的主角
source-map
了。在webpack中,可以通过devtool选项来配置Source Map。
举个例子,这是我业务代码中『故意』写的错误
- 当然有经验的一看就知道是 ok 前面的对象为空值了,直接跑源码中搜索
.ok
,再找到前面的对象,要是逻辑正确就加个?.
完事~ - 但对于很多情况是无法通过经验找到的,当我们想去看到底哪里出错的时候,这个就会让人异常头疼,因此就需要使用
source-map
了
我们可以通过配置 devtool
参数获取不同形式的 source-map
devtool | 打包结果 | 代码行数显示正确 |
---|---|---|
default / eval | 会打包成字符串 并使用 eval执行 | ❌ |
source-map | 会打包文件同时生成对应的 map 文件【会带上行号和列号】 | ✓ |
hidden-source-map | 会打包文件同时生成对应的 map 文件 | ❌ |
inline-source-map | 在打包产出 bundle.js 最后加上 DataURL 形式的 sourcemap 信息 | ✓ |
eval-source-map | 会在eval打包产出的字符串中 补上 sourceURL信息 | ✓ |
cheap-source-map | 会打包文件同时生成对应的 map 文件【只会带上行号】 | ✓ |
cheap-module-source-map | 会打包文件同时生成对应的 map 文件【只会带上行号】+ 显示 babel-loader 转译前的代码 | ✓ |
cheap-module-eval-source-map | 生成字符串打包到bundle中,无多余mapper文件 其他同上 | ✓ |
总结一下:
- 对于前端使用babel转译loader的时候,且基本只需关心当前行数,因此使用最推荐的
cheap-module-eval-source-map
选项 - 对于生成环境
production
不应该配置devtools
选项,因为不应该暴露给别人看到你的源码信息。且得到更小的体积
2. 代码拆分
将可复用代码,拆分出来减少不必要的打包 或 对第三方代码库进行拆分,减少传输资源的浪费。
可能会对后半句话有点费解,那先抛出3个可能想问的问题,后面进行一一解答。
问题一:
当前有个文件引入 lodash
我们只引了其中的 _.isEmpty
方法,那么请问我们只会抽取 lodash
的部分代码进行打包还是全打包呢?
当前虽然只在文件中引入了一个方法,但我们执行打包命令后会发现,最终实际上会把
lodash
都打包进来,这完全是不必要的。
问题二:
由上一个实验得出,若单入口文件中引入 lodash
,则会被完全打包。那要是当前为多入口且文件中都引入了 lodash
,我们同时对其进行打包,请问会打包几份 lodash
代码呢?
我们先进行双入口的引入举例,一般我们的SPA都只需要单入口。
webpack.config.js
// 多入口文件
// 对于多入口形式肯定无法使用字符串形式指定 - 数组 / 对象形式
module.exports = {
// entry: './index.js',
// entry: ['./index.js', './index2.js'], // 数组形式、将两者打包到一个文件下
entry: {
a: "./index.js", // 对象形式、将文件打包到多个文件下,且对应的name就是当前设置的key
b: "./index2.js"
},
output: {
filename: '[name].js', // 对于多文件bundle肯定就不能打包到一个文件中了,因此可以使用通配符形式
},
};
通过观察最终打包出来的文件大小发现:当我们指定多个打包文件,这些文件都会自己打包一份 lodash
,实际上这显然是没有必要的
问题三:
当我将 lodash
进行抽离后,对整体项目有什么好处呢?
抽离肯定是减少包体积啦,有多种减少体积方式。添加
dependon
的形式、自动判别式
1、先来看看 dependon
的形式,通过指名每个文件所依赖的库名【可自定义库或第三方库】
webpack.config.js
module.exports = {
entry: {
a: {
import: "./index.js",
dependOn: "lodash"
},
b: {
import: "./index2.js",
dependOn: "lodash"
},
lodash: "lodash"
}
...
};
发现最后会打包出3个文件 a.js
、b.js
、lodash.js
我们发现 对于我们自定义文件的 a.js
、b.js
实际打包后体积很小!lodash
还是一样那么大,但只有一份。其实此时就很符合我们的要求了
问题四:
这样太麻烦了,需要手动指定文件,要是引入第三方库无数,我全要这样加吗?
SplitChunksPlugin
在webpack5版本已经内置,会自动将文件进行拆分,对于大部分用户来说非常友好。
默认情况下,它只会影响到按需加载的 chunks,因为修改 initial chunks 会影响到项目的 HTML 文件中的脚本标签。
webpack 将根据以下条件自动拆分 chunks:
- 新的 chunk 可以被共享,或者模块来自于
node_modules
文件夹 - 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积)
- 当按需加载 chunks 时,并行请求的最大数量小于或等于 30
- 当加载初始化页面时,并发请求的最大数量小于或等于 30
webpack.config.js
module.exports = {
// entry: ['./index.js', './index2.js'], // 数组形式、将两者打包到一个文件下
entry: {
a: "./index.js", // 对象形式、将文件打包到多个文件下,且对应的name就是当前设置的key
b: "./index2.js"
},
optimization: {
...
splitChunks: {
chunks: 'all'
}
},
...
};
此时我们并没有手动进行配置,但它能够将我们引入的共有文件进行抽离。对于没有特殊要求的分离chunks时,这显然是最优选择。
问题五:
有了这个,为什么还要之前的配置呢?单个包可以抽离出来,多个包能抽离吗?抽离出来什么样的呢?
做个实验,还是使用这个方式,
a.js
、b.js
都同时引入lodash
和moment
我们发现最终会将第三库都打包到一个特定js
文件中,其他无关的业务代码还是放在一个文件中。因此其实对于平时不关心打包结果时,这种将所有第三方库都打包的方式是不错的。但有时可能需要像 问题三 一样需要单独抽离文件叫特定名【即 lodash
放 lodash.xx.js
, moment
放 moment.xx.js
则需要使用 dependOn
这种方法了】
问题六:
发现一直会有大小限制 244KB,显然这是webpack希望我们的包不超过这个限制,明明我们的代码那么少,一引入第三方库就会超标,那是不是我们就不能引入了呢?有什么办法能够规避吗?
第一种方法当然就是关闭警告啦~但这个显然在掩耳盗铃
第二种方式就是进行
tree-shaking
显然很多lodash
的方法是无用的,不应该一起被打包
这就引出我们下一章 Tree-shaking
了
3. Tree-shaking
用于移除 JS 上下文中未引用过的代码,以减小代码体积,间接减少代码在网络请求过程中的耗时。
-
对于
webpack 4
来说,是默认将所有的包/文件 都进行打包【无Tree-shaking】。 -
对于
webpack 5
来说已经默认内置了,得益于optimization.providedExports
被默认启用。需要注意的是 tree shaking 依赖于 ES6 的模块语法 ——import
和export
。简单来说就是对你import
的代码进行静态分析,如果发现没有被用到的部分就不再export
从而实现实现Tree-shaking
由此看来我们当前使用的就是最优解了,为什么上面的例子中还会报错出现问题呢?
那是因为 lodash
的问题。NPM
包提供出去的代码得是 ESM
!因此这并不是我们能判断和操控的,当然你也可以使用 lodash-es
模块。
// ESM[ES Modules]
import foo from 'foo';
export const bar = foo;
export default bar;
// CJS[CommonJS]
const foo = require('foo');
module.exports = foo;
但我们知道不可能所有的包都有特有的 -es
版本【基本都是以 umd
规范 - (commonJS + AMD)打包】,因此对于我们需要压缩打包体积来说只能另辟蹊径 使用代码压缩
4. 代码压缩
当前整体的打包和各种文件的配置方式我们都已经熟悉了,实际上已经可以初步使用了。可往往当我们直接使用
webpack
就要对代码进行打包时,会发现文件相当的大,这时就需要将我们的js产出物进行压缩处理了
optimization.minimizer
顾名思义就是用来压缩代码的啦。
- 在
webpack 5
中已经内置了terser-webpack-plugin
插件【该插件使用 terser 来压缩 JavaScript】其实压缩效率和质量都是想当高的,除非有些特殊定制化的需求,否则使用默认即可。 - 注意
terser-webpack-plugin
只能压缩js
代码,css
还是需要其相应的CssMinimizerPlugin
进行处理,在上面也有提到过。[css族](#8.1 css族)
webpack.config.js
module.exports = {
...
optimization: {
minimizer: [
// 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`)
// 这是一个内置插件,可以极大压缩js文件
'...',
new CssMinimizerPlugin(), // 压缩css文件
],
},
...
};
综上,当我们没有特定情况时,直接使用默认推荐 JS
的压缩效率就是最佳情况,但别忘记了 css
的压缩。至此又简单又高效的压缩代码方式就学会了。
此时我们来看看之前打包 lodash
的库经过压缩后的结果
虽然这个压缩结果很喜人,但都知道其实我只是引入 lodash
一个方法而已,就要全部打包,有没有什么方法能让我完全摆脱这个负重?因为往往做工具库/组件库的都希望小小的,别人引用起来没有负担。此时 Externals
就出现了。
5. 外部扩展(Externals)
官网介绍:externals
配置选项提供了「从输出的 bundle 中排除依赖」的方法。相反,所创建的 bundle 依赖于那些存在于用户环境(consumer's environment)中的依赖。防止将某些 import
的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。
简单来说就是,将当前依赖的
lodash
不默认打包进bundle
而是将其下放到引用你打包文件的环境,要求使用者提供lodash
。实话实说,这个对于工具库library
来说着实是福音了,因为使用者大概率是二次开发,也大概率会使用到你external
出去的库,因此无需多次打包使用从而减少开发者与使用者的打包体积。
webpack.config.js
module.exports = {
//...
externals: {
jquery: 'jQuery',
moment: 'moment',
lodash: {
commonjs: 'lodash',
commonjs2: 'lodash',
amd: 'lodash',
root: '_',
},
},
};
此时我们就将 jQuery
moment
lodash
放在外部依赖了。那肯定有小伙伴问 这写法为什么不一样呢?
那是为了限制我们打包出来的文件运行于不同的环境下,具有外部依赖可以在各种模块上下文(module context)中使用,例如 CommonJS, AMD, 全局变量和 ES2015 模块,外部 library 可能是以下任何一种形式:
- 字符串形式:代表下面所有都需要兼容
- root:可以通过一个全局变量访问 library(例如,通过 script 标签)。
- commonjs:可以将 library 作为一个 CommonJS 模块访问。
- commonjs2:和上面的类似,但导出的是
module.exports.default
. - amd:类似于
commonjs
,但使用 AMD 模块系统。
至此对于我们的优化打包代码就讲这么多,还有什么可以优化的点呢?缓存是我们经常提及的技术...
6. 缓存
对于浏览器缓存来说,一些不是一直变更的大资源则应当被缓存起来,无需向服务器请求获取。浏览器会按照请求的文件名判断是否相同来决定是否使用缓存。
因此针对这个特性,我们需要做的就是将代码进行 [代码拆分](#11. 代码拆分) 并将一直不变的代码名设置为相同的,将变化的文件名设置为不同,即打包出的文件名会按照文件内容进行生成。
webpack.config.js
module.exports = {
//...
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};
也很简单是将 node_modules
下的所有文件打包到 vendor
文件中。这也是 webpack
的默认选项。
对于我们自己的代码来说 可以使用 [代码拆分](#11. 代码拆分) ,再进行输出文件名控制,保证文件名会按照内容进行命名
webpack.config.js
module.exports = {
...
output: {
filename: "[name].[contenthash].js",
...
},
};
7. 打包工具库
当前我们想要导出了一些工具函数给外部使用,通过上面的学习我们知道,对于没有使用的函数会被 tree-shaking
掉,显然工具函数对外暴露的都不会被引用,这时我们应该怎么办呢?
配置
output.library
webpack.config.js
module.exports = {
...
output: {
filename: "[name].js",
path: path.resolve(__dirname, "./dist"),
// library: 'mylibrary', 只支持script方式引入,注入到window对象上
library: {
name: 'mylibrary',
type: 'umd'|'commonjs'|'window',
}
}
...
}
我们先前有介绍过 umd模块 = CommonJS + AMD、对于我们的esm模块咋办呢?
其实目前只有实验性功能 打包 esm,目前对于 umd 除了下面的模式,其他都兼容
<script type="module">
import {add} from 'lodash'
add(1, 2)
</script>
对于commonJS模块来说,我们肯定是可以在node环境下运行的,可往往我们执行时会出现下面这个问题
这是因为 output.globalObject
默认是 self
属性,但为了使 UMD 构建在浏览器和 Node.js 上均可用,应将 output.globalObject
选项设置为 'this'
即可
webpack.config.js
module.exports = {
...
output: {
filename: "[name].js",
path: path.resolve(__dirname, "./dist"),
library: {
name: 'mylibrary',
type: 'umd,
},
globalObject: 'this'
}
...
}