一、Webpack初探
webpack is a module bundler
模块打包工具,能够识别任何模块引入的语法
npm npx
npm全称:node.js package manager,是一款node.js的包管理工具。
npx全称: node.js package execute,是一个node.js的包执行工具。
包管理工具
npm
在这里我们把java和node.js来做一下等价,那么npm在node.js作用就相当于maven在java中作用。npm会读取package.json中列出来的依赖包,然后自动安装这些依赖包。
包执行工具
npx
当我们在开发项目使用到gulp时,可以打开终端并cd到当前项目路径下并执行以下命令来运行gulp。
./node_modules/.bin/gulp
每次运行gulp都要输入这么多东西好麻烦,所以我们可以把上面这行代码写到package.json中,如下所示。
1 | "scripts": { |
之后在终端里只需要运行以下代码就可以执行gulp了。 npm run gulp
但是如果定义很多条script命令的话,如下所示。
1 | "scripts": { |
我们要写很多重复的代码”./node_modules/.bin/“。
这个时候npx的作用就体现出来了,它会自动帮我们搜索程序所在的路径,此时就不需要去手动定义程序的路径了,效果如下图。 ./node_modules/.bin/gulp可以替换为``npx gulp`
所以package.json就可以变成:
1 | "scripts": { |
此时npx自动帮我们找到了gulp所在的路径。当然这只是npx一个最基础的功能。
webpack.config.js
webpack.config.js自定义打包配置,运行npx webpack。
1 | const path = require('path'); |
entry打包入口文件,output打包输出文件。mode为development时,bundle.js是不被压缩的JS代码,如果mode为production时,bundle.js是一段压缩的JS代码。
如果不是webpack.config.js命名的话,需要运行npx webpack —config xxx,xxx为自定义的配置文件名。
我们可以在package.json下的scripts中简化命令:
1 | "scripts": { |
这样可以直接运行npm run bundle进行打包,上面语句的webpack和npx webpack是一样的原理,都是先去工程目录下的node_modules去查找webpack。
综上:
webpack有三种方式运行
- 全局安装情况下,直接
webpack index.js - 局部安装情况下
npx webpack index.js- package.json的scripts下配置,
npm run bundle
webpack-cli帮助我们可以在命令行内使用webpack命令。
chunk的含义:打包生成的每个文件都是一个chunk,打包生成很多个js文件,就有很多个chunk。
二、Webpack核心功能
1.loader
webpack enables use of loaders to preprocess files. This allows you to bundle any static resource way beyond JavaScript. You can easily write your own loaders using Node.js.
由于webpack无法识别非js结尾的模块,所以需要loader来让webpack识别。其实就是一个打包方案。比如vue中经常有模块化的操作,import Header from './header.vue'
那么webpack如何识别以vue结尾的模块呢?该去如何打包呢?这时就需要用到loader
1 | module: { |
file-loader
Emits the file into the output folder and returns the (relative) URL
1 | module: { |
上面的配置就是将以jpg、png、gif结尾的文件进行打包,输出的文件名字通过占位符拼接,放到了输出文件夹的images目录下。
url-loader
A loader for webpack which transforms files into base64 URIs.
Works like the file loader, but can return a data URL if the file is smaller than a limit
当文件的大小比较小的时候推荐使用url-loader,这样的话可以节约http请求,但是如果你的文件比较大,就会造成bundle.js文件也会变大,页面无法快速的显示出来。
1 | module: { |
limit用来限制文件的大小,如果小于limit的值,直接以base64的形式打包到js文件中。
css-loader
分析各css文件的关系,合并成一个css文件
1 | module.exports = { |
当执行到css-loader的时候,发现import了scss,此时loader是重新从下往上执行一遍呢,还是直接执行style-loader呢?我们可以通过importLoaders设置,因为import的scss也要处理,所以还要执行一遍sass-loader和postcss-loader。
modules:true 开启css模块化,可以使用import style from './index.scss';这样的语法,可以让当前模块里的样式和其他模块里的样式不会有任何的耦合。
1 | import avatar from './avatar.jpg'; |
如上例子,如果modules:false ,此时createAvatar里的样式和当前模块里的样式都共享了同一个样式index.scss。
1 | { |
打包字体文件
style-loader
把打包好的css挂载到html的
1 | module: { |
loader是从下到上,从右到左的顺序执行。比如上面的配置文件,执行顺序sass-loader -> css-loader -> style-loader。
postcss-loader
自动加浏览器内核前缀,提高css兼容性。
2.plugin
可以在webpack运行到某个时刻的时候,帮你做一些事情
html-webpack-plugin
htmlWebpackPlugin会在打包结束后,自动生成一个html文件,并把打包生成的js自动引入到这个html文件中。
1 | const path = require('path'); |
template是生成html文件的模版。
clean-webpack-plugin
By default, this plugin will remove all files inside webpack’s
output.pathdirectory, as well as all unused webpack assets after every successful rebuild.
1 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); |
clean-webpack-plugin在打包之前先会被运行的,html-webpack-plugin在打包之后运行。
3.Entry和Output基础配置
1 | module.exports = { |
上述的entry的写法,相当于entry: './src/index.js',意思是打包这个指定的文件,默认的文件名是main.js,如果你不想用默认的文件名,直接将entry写成对象的形式就可以了,其中键名是默认的输出文件名。如果下面的output中指定了filename,则以output中的为准。
1 | module.exports = { |
报错:Conflict: Multiple chunks emit assets to the same filename bundle.js (chunks main and sub)
可以看出,如果entry有两个及以上的chunk,不能输出到一个文件名中,会报错,或者可以干脆不在output里面指定filename。
1 | module.exports = { |
用占位符可以指定文件名,此时占位符上的name是上面entry对象的键名。
通过在output中配置publicPath,这样最终被html文件引用的js文件名前面都会加上这个地址。

所以如果我们项目后台用html,而静态资源放到了cdn上,那么这时就要用到output里面的publicPath
path:用来存放打包后文件的输出目录
publicPath:生成最终的styles时,webpack默认会把publicPath的配置添加到图片URL,字体的路径上
4.SourceMap配置
mode是development模式下,默认配置了sourcemap。devtool: 'source-map'
打过过后的文件夹中有一个main.js.map,内容是文件之间的映射关系。
SourceMap是一个映射关系,它知道你打包过后文件里的代码和源文件里的代码之间的映射关系,找bug。
eval: 使用eval包裹模块代码
source-map: 产生.map文件
cheap:不包含列信息,也不包含loader的sourcemap
module: 包含loader的sourcemap(比如jsx to js ,babel的sourcemap)
inline:将.map作为DataURI嵌入,不单独生成.map文件。
inline-source-map- A SourceMap is added as a DataUrl to the bundle.包含inline关键字的配置项也会产生.map文件,但是这个map文件是经过base64编码作为DataURI嵌入,不单独生成.map文件
cheap-source-map- A SourceMap without column-mappings ignoring loader Source Maps.当使用
cheap-inline-source-map的时候,性能会提升。- 因为这里的映射关系只精确到哪一行,具体在哪一列不做映射,所以性能会比较好。
- 映射只针对业务代码,而不会管引入的第三方模块的代码映射,比如loader这些
inline-cheap-source-map- Similar tocheap-source-mapbut SourceMap is added as a DataUrl to the bundle.cheap-module-source-map- A SourceMap without column-mappings that simplifies loader Source Maps to a single mapping per line.不仅要映射业务代码,第三方的模块也要映射。
inline-cheap-module-source-map- Similar tocheap-module-source-mapbut SourceMap is added as a DataUrl to the bundle.eval,打包速度最快的一种方式,通过eval这种js执行形式来生成sourcemap的对应关系。
最佳实践:
- mode:’development’
cheap-module-eval-source-map,提示出来的错误比较全面,同时打包速度也比较快 - mode:’production’
cheap-module-source-map带inline与eval的不能用于生产环境是因为这两者生成的SourceMap是内嵌在构建完成的js代码中的,会在生产环境直接暴露源代码。
5.WebpackDevServer
为什么要使用webpack-dev-server?
要发ajax请求文件必须是以http协议打开,file协议打开的文件不能发送ajax请求
webpack中有几个不同的选项,可以帮助你在代码发生变化后自动编译代码
1 | "scripts": { |
“watch”: “webpack –watch”
添加watch,这样webpack会监控所打包文件的变化,一旦发生变化,那么会自动帮我们执行打包。每次打包完都要重新刷新浏览器,比较麻烦。
“start”: “webpack-dev-server”
1
2
3
4devServer: {
contentBase: './dist',
open: true
}服务器起在contentBase指定的那个文件夹下,那么想要访问代码其实就是在起的那个服务器下面去访问代码。打包文件发生变化,浏览器上的变化无需刷新便可以展示。
open: true 在启动webpackdevserver的时候,会帮我们自动打开浏览器访问服务器的地址。
如何自己实现webpack-dev-server?
“server”: “node server.js”
可以通过webpackDevMiddleware配合express自己写这样的server(简单版,需要手动刷新浏览器变化)
在node中使用webpack
1 | const express = require('express'); |
6.HotModuleReplacement
一个带有热更新功能的webpack.config.js文件的配置如下:
- 引入webpack库
- 使用
new webpack.HotModuleReplacementPlugin() - 设置
devServer选项中的hot字段为true
1 | const path = require('path'); |
Hot Module Replacement (HMR) exchanges, adds, or removes modules while an application is running, without a full reload. This can significantly speed up development in a few ways:
- Retain application state which is lost during a full reload.
- Save valuable development time by only updating what’s changed.
- Instantly update the browser when modifications are made to CSS/JS in the source code, which is almost comparable to changing styles directly in the browser’s dev tools.
HMR能会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。
hot和hotOnly的区别是在某些模块不支持热更新的情况下,前者会自动刷新页面,后者不会刷新页面,而是在控制台输出热更新失败。
本质上如果想实现HMR功能,页面上就一定要写下面的代码,但是由于一些loader等文件帮我们实现了这个代码,所以我们不用自己手动写。
1 | import number from './number' |
7.babel
Babel是一个JavaScript编译器,Babel是一个工具链,主要用于将ECMAScript 2015+版本的代码转换为向后兼容的JavaScript语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
npm install –save-dev babel-loader @babel/core
npm install @babel/preset-env –save-dev
包含了所有ES6转换为ES5的翻译规则which enables transforms for ES2015+
npm install –save @babel/polyfill
Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如
Object.assign)都不会转码。举例来说,ES6在
Array对象上新增了Array.from方法。Babel就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。如果设置了
useBuiltIns,那么文件中就不用import '@babel/polyfill';
webpack.config.js
1 | { |
使用useBuiltIns配置参数,使得打包过程中根据业务代码的需求注入polyfill里面对应的内容,代码体积明显变小。targets配置参数,是告诉babel,我的代码需要运行在什么版本浏览器上,你可以根据需要注入所需要的内容。
三、Webpack高级功能
1.Tree Shaking
我们打包的源文件中引用了某个模块的一部分的时候,打包的时候却会把整个模块都打包进去,会造成打包后文件体积过大。此时就可以用tree shaking仅把我们需要的那部分代码打包进去,而把模块中我们用不到的部分“shaking”掉。正式一点的说法就是:移除上下文中的未引用代码。
由于webpack中的tree shaking是依赖于ES5中的静态结构特性,所以仅适用于模块是用ES Module引入方式的模块(即用import)。
实现
开发模式
在webpack配置文件中新增一个配置项:
1 | optimization: { |
在package.json中新增一个配置项:”sideEffects”。可以把不需要tree shaking的文件用数组的写法写在这个配置项中,如"sideEffects": ["@babel/polly-fill"];,因为在@babel/polly-fill中没有任何的导出,所以也不会被tree shaking掉,但是我们不希望这样,所以为了取消tree shaking带来的影响,需要在sideEffects中设置。如果没有则写为false,意思是对所有模块都tree shaking。
一般会在sideEffects配置*.css,意思是对导入的css模块不做treeshaking。
在开发环境中,tree shaking并打包之后并不会把模块多余的代码从打包完成的文件中删除,而只是会注释哪些是我们用到的代码,哪些是不用的代码。
生产环境
只需要在package.json中新增一个配置项:”sideEffects”。打包过程会默认进行tree shaking。并且会把多余的代码从打包完成的文件中删除。
development模式下,不管设置”sideEffects”: false 还是 “sideEffects”: [“.css”],style.css都不会被tree shaking,页面样式还是会生效。
开发模式下,对于样式文件tree shaking是不生效的
production模式下,“sideEffects”: false页面样式不生效,说明样式文件被tree shaking了;然后设置”sideEffects”: [“.css”]样式生效,说明样式文件没有被tree shaking。
生产模式下,对于样式文件tree shaking是生效的
2.Production和Development模式的区分打包
由于开发环境和生产环境的区别,配置也会不同,所以我们需要将开发环境配置和生产环境配置进行拆分,公共的配置提取到一个文件,通过webpack-merge合并公共配置到webpack.prod.js、webpack.dev.js文件中,打包生产和开发环境安装包
webpack.common.js
1 | const path = require('path'); |
webpack.dev.js
1 | const webpack = require('webpack'); |
webpack.prod.js
1 | const merge = require('webpack-merge'); |
package.json
1 | "scripts": { |
那么我们就可以通过npm run dev和npm run build打包相应的开发环境和生产环境。
3.Code Splitting
当我们的业务逻辑很大的时候(index.js很大),我们打包生成的文件会很大:用户打开页面要一次性加载这个很大的文件,需要很长的加载时间;当文件有一点点修改,用户又要重新加载这个很大的文件,又需要很长加载时间。所以可以用code splitting(代码分割)把代码分离到不同的bundle中,可以实现按需加载或并行加载这些文件,优化加载时间。
比如说,你打包到了一个文件main.js,那么首次访问页面时,需要加载main.js,当页面业务逻辑发生变化时,又要加载main.js。那么通过code splitting,main.js被拆分成若干文件a.js、b.js,首次访问页面时,可以并行加载这些文件,当页面逻辑发生变化时,只需要加载需要变更的文件即可。
实现
- 同步代码:只需要在webpack配置文件中作optimization的配置。
1 | optimization: { |
- 异步代码(指import语法引入的异步组件):异步代码,无需作任何配置,会自动进行代码分割。
1 | //第一种 同步代码 |
借用SplitChunksPlugin,可以实现对同步加载的代码或者异步加载的代码实现代码分割。把spliChunk写在webpack配置文件的optimization配置项中。代码如下
1 | optimization: { |
4.Lazy Loading
懒加载就是将不关键的资源延后加载,是一种很好的优化网页或应用的方式。
这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
实现
对于图片来说,先设置图片标签的src属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为src属性,这样图片就会去下载资源,实现了图片懒加载。
懒加载不仅可以用于图片,也可以使用在别的资源上。例如下面这段代码实现click之后再执行异步函数在页面生成文字。只有在click以后,才会去import,加载loadsh.js文件。
1 | // 利用异步加载的代码,可以实现懒加载 |
babel-plugin-dynamic-import-webpackimport懒加载
打包生成的每个文件都是一个chunk,打包生成很多个js文件,就有很多个chunk,比如上述代码中就生成了两个chunk,main.js、vendors-loadsh.js
5.打包分析 Preloading Prefetching
- 在package.json文件中添加
webpack --profile --json > stats.json命令 - 用分析工具进行分析webpack-chart
为什么webpack默认异步代码才使用代码分割?
同步代码进行代码分割只能增加缓存,对性能提升是有限的。
异步代码通过页面的一些交互操作时进行加载文件,这样可以提高网站首页渲染时间。(可以在浏览器上通过ctrl+shift+p使用Coverage查看加载的js文件的代码覆盖率。)
比如一个网站的弹出登陆界面,一般首屏的时候不需要加载,而是在你点击登陆的时候才需要。如果使用懒加载,可能会影响用户体验,因为只有用户点击的时候才会去请求登陆框的资源,耗时。那么我们就可以用prefetch,在浏览器空闲的时候提前加载资源。
dynamicImport
可以使用magic comment来修改import动态导出的chunkname,语法规则如下:
1 | import(/* webpackChunkName: "lodash" */ 'lodash').then(//...) |
如果需要使用这种注释的写法,应该安装模块 @babel/plugin-syntax-dynamic-import,并在 babelrc 中引入这个插件。
Preload: A preloaded chunk starts loading in parallel to the parent chunk. 所以Proload应该是和核心代码同时(in parallel)加载。
两者的区别
第一: 下载的时间点不同
对于prefetch,是在带宽空闲的时候下载。
对于preload,是立刻下载。(is instantly downloaded)。
第二: preload chunk 和 prefetch chunk 被核心代码请求(call)的时间点不同
对于prefetch,在未来的某一个时刻会被请求,例如登录框这个例子。
对于preload,是立刻被请求。
1
2import(/* webpackPrefetch: true */ 'LoginModal');
import(/* webpackPreload: true */ 'ChartingLibrary');1
2<link rel="prefetch" href="login-modal-chunk.js">
<link rel="preload">
什么情况下用Preload
一个主页上有一个组件,该组件需要引入一个比较大的库。当主页被加载完成之后会立刻请求这个组件,那么这个组件在引入这个库的时候需要使用proload
1 | import(/* webpackPreload: true */ 'biglibrary'); |
当打包完成,主页生成一个page-chunk,这个比较大的library也会生成一个big-library-chunk。假设page-chunk文件大小远远小于big-library-chunk。因为在引入biglibrary的时候,使用了preload。所以主页的page-chunk和biglibrary-chunk会被同时加载。刚在前面已经假设page-chunk文件大小远远小于big-library-chunk,所以结果是主页会被先加载完成,等待big-library-chunk加载完成。最后等big-library-chunk加载完成,主页会立刻请求(call)这个组件。因为是同时加载,会比先加载page-chunk再加载biglibrary-chunk快一些。
webpack希望我们多写异步加载的代码(多用懒加载),能提高页面的代码使用率,eg,把具体代码写在一个js文件中,然后在index.js中写。(所以代码分割默认是async)
1 | document.addEventListener('click', () => { |
但是等到用户点击的时候再加载代码,加载时间可能也比较长,这时就可用preloading、prefetching:实现在页面加加载完之后,在接下来的空闲时间偷偷地把异步操作的代码下载好,这样真正用的时候只需要加载缓存即可(这个其实是prefetching;preloading是会和主代码一起加载)。
使用方法是在import语句中写入魔法注释即可 /* webpackPrefetch: true */
6.CSS文件代码分割
chunkFilename
1 | output: { |
对样式进行code splitting
MiniCssExtractPlugin
1 | 1. 在plugins中引入MiniCssExtractPlugin插件 |
如果你想把多个css文件打包成一个,或者根据entry来进行分类打包的话,可以用splitChunks的cacheGroups。
7.浏览器缓存Caching
performance: false阻止webpack打包时提示性能问题。- 在打包文件内容有变化时,
contenthash值会自动变化。
生产环境下:
1 | output: { |
当项目重新打包上线的时候,用户只需要更新有变化的代码(通过contenthash实现),而没有变化的代码用户直接用本地的缓存。
8.Shimming
改变webpack打包的默认行为,或者实现webpack打包实现不了的效果。
应用场景:由于webpack具有模块化开发的理念,所以当index.js中有引入jQuery,而有个模块用了$变量却没import jQuery的时候,打包的时候这个模块的还是会翻译不出,手动往每个用了$的模块去import却很麻烦。故可用shimming。
1 | new webpack.ProvidePlugin({ |
9.环境变量的使用
1 | 1. package.json文件传递 |