Webpack
前言
现如今的开发当中,webpack的使用越来越频繁,而我们也经常需要使用诸如vue/cli create-react-app等脚手架来搭建我们的项目,所以对webpack的学习必不可少,下面我们一起进入到webpack的学习当中。
1 入门(一起来用这些小例子让你熟悉webpack的配置)
1.1初始化项目
新建一个目录,初始化npm
1 | npm init |
webpack是运行在node环境中的,我们需要安装以下两个npm包
1 | npm i -D webpack webpack-cli |
- npm i -D 为npm install –save-dev的缩写
- npm i -S 为npm install –save的缩写
新建一个文件夹src
,然后新建一个文件main.js
,写一点代码测试一下
1 | console.log('calle me ') |
配置package.json命令
执行
1 | npm run build |
此时如果生成了一个dist文件夹,并且内部含有main.js说明已经打包成功了
1.2开始我们自己的配置
上面一个简单的例子只是webpack自己默认的配置,下面我们要实现更加丰富的自定义配置,新建一个build
文件夹,里面新建一个webpack.config.js
1 | // webpack.config.js |
更改我们的打包命令
1 | 执行 npm run build 会发现生成了以下目录(图片) |
1.3配置HTML模板
js文件打包好了,但是我们不可能每次在html
文件中手动引入打包好的js。
这里可能有的朋友会认为我们打包js文件名称不是一直是固定的嘛(output.js)?这样每次就不用改动引入文件名称了呀?实际上我们日常开发中往往会这样配置:
1 | module.export={ |
这时候生成的dist目录文件 如下
为了缓存,你会发现打包好的js文件的名称每次都不一样。webpack打包出来的js文件我们需要引入到html中,但是每次我们都手动修改js文件名显得很麻烦,因此我们需要一个插件来帮我们完成这件事情:
1 | npm i -D html-webpack-plugin |
新建一个build同级的文件夹public,里面新建一个index.html,具体配置文件如下
1 | // webpack.config.js |
生成目录如下:
可以发现打包生成的js文件已经被自动引入html文件中
1.3.1多入口文件如何开发
生成多个html-webpack-plugin实例来解决这个问题
1 | const path = require('path'); |
此时会发现生成以下目录
1.3.2clean-webpack-plugin
每次执行npm run build 会发现dist文件夹里会残留上次打包的文件,这里我们推荐一个plugin来帮我们在打包输出前清空文件夹clean-webpack-plugin
1 | const {CleanWebpackPlugin} = require('clean-webpack-plugin') |
1.4引用CSS
我们的入口文件是js,所以我们在入口js中引入我们的css文件
如果我们使用less来构建样式,则需要多安装两个
1 | npm i -D less less-loader |
配置文件如下
1 | // webpack.config.js |
浏览器打开html如下
1.4.1为css添加浏览器前缀
1 | npm i -D postcss-loader autoprefixer |
配置如下
1 | // webpack.config.js |
接下来,我们还需要引入autoprefixer
使其生效,这里有两种方式
1、在项目根目录下创建一个postcss.config.js文件,配置如下:
1 | module.exports={ |
2、直接在webpack.config.js里配置
1 | // webpack.config.js |
这时候我们发现css通过style标签的方式添加到了html文件中,但是如果样式文件很多,全部添加到html中,难免显得混乱。这时候我们想用把css拆分出来用外链的形式引入css文件怎么做呢?这时候我们就需要借助插件来帮助我们
1.4.2拆分css
1 | npm i -D mini-css-extract-plugin |
webpack 4.0以前,我们通过
extract-text-webpack-plugin
插件,把css样式从js文件中提取到单独的css文件中。webpack4.0以后,官方推荐使用mini-css-extract-plugin
插件来打包css文件
配合文件如下
1 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); |
1.4.3拆分多个css
这里需要说的细一点,上面我们所用到的
mini-css-extract-plugin
会将所有的css样式合并为一个css文件。如果你想拆分为一一对应的多个css文件,我们需要使用到extract-text-webpack-plugin
,而目前mini-css-extract-plugin
还不支持此功能。我们需要安装@next版本的extract-text-webpack-plugin
1 | npm i -D extract-text-webpack-plugin@next |
1 | // webpack.config.js |
1.5打包 图片、字体、媒体、等文件
file-loader
就是将文件在进行一些处理后(主要是处理文件名和路径、解析文件url),并将文件移动到输出的目录中
url-loader
一般与file-loader
搭配使用,功能与 file-loader 类似,如果文件小于限制的大小。则会返回 base64 编码,否则使用 file-loader 将文件移动到输出的目录中
1 | // webpack.config.js |
1.6用babel转义js文件
为了使我们的js代码兼容更多的环境我们需要安装依赖
1 | npm i babel-loader @babel/preset-env @babel/core |
- 注意babel-loader与babel-core的版本对应关系
1、bbael-loader 8.x对应babel-core 7.x
2、babel-loader 7.x对应babel-core 6.x
配置如下
1 | // webpack.config.js |
上面的babel-loader
只会将 ES6/7/8语法转换为ES5语法,但是对新api并不会转换 例如(promise、Generator、Set、Maps、Proxy等),此时我们需要借助babel-polyfill来帮助我们转换
1 | npm i @babel/polyfill |
1 | // webpack.config.js |
搭建vue开发环境
上面的小例子已经帮助而我们实现了打包css、图片、js、html等文件。但是我们还需要以下几种配置:
2.1解析.vue文件
1 | npm i -D vue-loader vue-template-compiler vue-style-loader |
vue-loader 用于解析.vue文件
vue-template-compiler用于编译模板 配置如下
1 | const vueLoaderPlugin = require('vue-loader/lib/plugin') |
2.2配置webpack-dev-server进行热更新
1 | npm i -D webpack-dev-server |
配置如下
1 | const Webpack = require('webpack') |
完整配置如下
1 | // webpack.config.js |
2.3配置打包命令
打包文件已经配置完毕,接下来让我们测试一下
首先在src新建一个main.js
新建一个App.vue
新建一个public文件夹,里面新建一个index.html
执行npm run dev
这时候如果浏览器出现Vue开发环境运行成功,那么恭喜你,已经成功迈出了第一步!
2.4区分开发环境与生产环境
实际应用到项目中,我们需要区分开发环境与生产环境,我们在原来webpack.config.js的基础上再新增两个文件
webpack.dev.js 开发环境配置文件
1
开发环境主要实现的是热更新,不要压缩代码,完整的sourceMap
webpack.prod.js生产环境配置文件
1
2
3生产环境主要实现的是压缩代码、提取css文件、合理的sourceMap、分割代码
需要安装以下模块:
npm i -D webpack-merge copy-webpack-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-pluginwebpack-merge
合并配置copy-webpack-plugin
拷贝静态资源optimize-css-assets-webpack-plugin
压缩cssuglifyjs-webpack-plugin
压缩js
webpack mode
设置production
的时候会自动压缩js代码。原则上不需要引入uglifyjs-webpack-plugin
进行重复工作。但是optimize-css-assets-webpack-plugin
压缩css的同时会破坏原有的js压缩,所以这里我们引入uglifyjs
进行压缩
2.4.1webpack.config.js
1 | const path = require('path') |
2.4.2webpack.dev.js
1 | const Webpack = require('webpack') |
2.4.3webpack.prod.js
1 | const path = require('path') |
2.5优化webpack配置
优化配置对我们来说非常有实际意义,这实际关系到你打包出来文件的大小,打包的速度等。 具体优化可以分为以下几点:
2.5.1优化打包速度
构建速度指的是我们每次修改代码后热更新的速度以及发布前打包文件的速度。
2.5.1.1合理的配置mode参数与devtool参数
mode
可设置development`` production
两个参数
如果没有设置,webpack4
会将 mode
的默认值设置为 production
production
模式下会进行tree shaking
(去除无用代码)和uglifyjs
(代码压缩混淆)
2.5.1.2缩小文件的搜索范围(配置include exclude alias noParse extensions)
alias
: 当我们代码中出现import 'vue'
时, webpack会采用向上递归搜索的方式去node_modules
目录下找。为了减少搜索范围我们可以直接告诉webpack去哪个路径下查找。也就是别名(alias
)的配置。include exclude
同样配置include exclude
也可以减少webpack loader
的搜索转换时间。noParse
当我们代码中使用到import jq from 'jquery'
时,webpack
会去解析jq这个库是否有依赖其他的包。但是我们对类似jquery
这类依赖库,一般会认为不会引用其他的包(特殊除外,自行判断)。增加noParse
属性,告诉webpack
不必解析,以此增加打包速度。extensions ``webpack
会根据extensions
定义的后缀查找文件(频率较高的文件类型优先写在前面)
2.5.1.3使用HappyPack开启多进程Loader转换
在webpack构建过程中,实际上耗费时间大多数用在loader解析转换以及代码的压缩中。日常开发中我们需要使用Loader对js,css,图片,字体等文件做转换操作,并且转换的文件数据量也是非常大。由于js单线程的特性使得这些转换操作不能并发处理文件,而是需要一个个文件进行处理。HappyPack的基本原理是将这部分任务分解到多个子进程中去并行处理,子进程处理完成后把结果发送到主进程中,从而减少总的构建时间
1 | npm i -D happypack |
2.5.1.4使用webpack-parallel-uglify-plugin增强代码压缩
上面对于loader转换已经做优化,那么下面还有另一个难点就是优化代码的压缩时间。
1 | npm i -D webpack-parallel-uglify-plugin |
2.5.1.5抽离第三方模块
对于开发项目中不经常会变更的静态依赖文件。类似于我们的
elementUi、vue
全家桶等等。因为很少会变更,所以我们不希望这些依赖要被集成到每一次的构建逻辑中去。 这样做的好处是每次更改我本地代码的文件的时候,webpack
只需要打包我项目本身的文件代码,而不会再去编译第三方库。以后只要我们不升级第三方包的时候,那么webpack
就不会对这些库去打包,这样可以快速的提高打包的速度。
这里我们使用webpack
内置的DllPlugin DllReferencePlugin
进行抽离在与webpack
配置文件同级目录下新建webpack.dll.config.js
代码如下
1 | // webpack.dll.config.js |
在package.json中配置如下命令
1 | "dll":"webpack --config build/webpack.dll.config.js" |
接下来在我们的webpack.config.js中增加以下代码
1 | module.exports = { |
执行
1 | npm run dll |
会发现生成了我们需要的集合第三地方 代码的vendor.dll.js
我们需要在html
文件中手动引入这个js
文件
1 |
|
这样如果我们没有更新第三方依赖包,就不必npm run dll
。直接执行npm run dev npm run build
的时候会发现我们的打包速度明显有所提升。因为我们已经通过dllPlugin
将第三方依赖包抽离出来了
2.5.1.6配置缓存
我们每次执行构建都会把所有的文件都重复编译一遍,这样的重复工作是否可以被缓存下来呢,答案是可以的,目前大部分
loader
都提供了cache
配置项。比如在babel-loader
中,可以通过设置cacheDirectory
来开启缓存,babel-loader?cacheDirectory=true
就会将每次的编译结果写进硬盘文件(默认是在项目根目录下的node_modules/.cache/babel-loader
目录内,当然你也可以自定义)
但如果 loader
不支持缓存呢?我们也有方法,我们可以通过cache-loader
,它所做的事情很简单,就是 babel-loader
开启 cache
后做的事情,将 loader
的编译结果写入硬盘缓存。再次构建会先比较一下,如果文件较之前的没有发生变化则会直接使用缓存。使用方法如官方 demo 所示,在一些性能开销较大的 loader 之前添加此 loader即可
1 | npm i -D cache-loader |
2.5.2优化打包文件体积
打包的速度我们是进行优化,但是打包后的文件体积确实十分大,造成了页面加载缓慢,浪费流量等,接下来我们从文件体积上继续优化
2.5.2.1引入webpack-bundle-analyzer分析打包后的文件
webpack-bundle-analyzer将打包后的内容束展示为方便交互的直观树状图,让我们知道我们所构建包中真正引入的内容
1 | npm i -D webpack-bunble-analyzer |
接下来在package.json里配置启动命令
1 | "analyz": "NODE_ENV=production npm_config_report=true npm run build" |
windows请安装npm i -D cross-env
1 | "analyz": "cross-env NODE_ENV=production npm_config_report=true npm run build" |
接下来npm run analyz浏览器会自动打开文件依赖图的网页
2.5.2.3externals
按照官方文档的解释,如果我们想引用一个库,但是又不想让
webpack
打包,并且又不影响我们在程序中以CMD、AMD
或者window/global
全局等方式进行使用,那就可以通过配置Externals
。这个功能主要是用在创建一个库的时候用的,但是也可以在我们项目开发中充分使用Externals
的方式,我们将这些不需要打包的静态资源从构建逻辑中剔除出去,而使用CDN
的方式,去引用它们。
有时我们希望我们通过script
引入的库,如用CDN的方式引入的jquery
,我们在使用时,依旧用require
的方式来使用,但是却不希望webpack
将它又编译进文件中。这里官网案例已经足够清晰明了,大家有兴趣可以点击了解
1 | <script |
1 | module.exports = { |
1 | import $ from 'jquery'; |
2.5.2.3 Tree-shaking
这里单独提一下
tree-shaking
,是因为这里有个坑。tree-shaking
的主要作用是用来清除代码中无用的部分。目前在webpack4
我们设置mode
为production
的时候已经自动开启了tree-shaking
。但是要想使其生效,生成的代码必须是ES6模块。不能使用其它类型的模块如CommonJS
之流。如果使用Babel
的话,这里有一个小问题,因为Babel
的预案(preset)默认会将任何模块类型都转译成CommonJS
类型。修正这个问题也很简单,在.babelrc
文件或在webpack.config.js
文件中设置modules: false
就好了
1 | // .babelrc |
或者
1 | // webpack.config.js |
3手写webpack系列
经历过上面两个部分,我们已经可以熟练的运用相关的loader和plugin对我们的代码进行转换、解析。接下来我们自己手动实现loader与plugin,使其在平时的开发中获得更多的乐趣。
3.1手写webpack loader
loader
从本质上来说其实就是一个node
模块。相当于一台榨汁机(loader)
将相关类型的文件代码(code)
给它。根据我们设置的规则,经过它的一系列加工后还给我们加工好的果汁(code)
。
loader编写原则
- 单一原则: 每个
Loader
只做一件事; - 链式调用:
Webpack
会按顺序链式调用每个Loader
; - 统一原则: 遵循
Webpack
制定的设计规则和结构,输入与输出均为字符串,各个Loader
完全独立,即插即用;
在日常开发环境中,为了方便调试我们往往会加入许多console
打印。但是我们不希望在生产环境中存在打印的值。那么这里我们自己实现一个loader
去除代码中的console
1 | 知识点普及之AST。AST通俗的来说,假设我们有一个文件a.js,我们对a.js里面的1000行进行一些操作处理,比如为所有的await 增加try catch,以及其他操作,但是a.js里面的代码本质上来说就是一堆字符串。那我们怎么办呢,那就是转换为带标记信息的对象(抽象语法树)我们方便进行增删改查。这个带标记的对象(抽象语法树)就是AST。这里推荐一篇不错的AST文章 AST快速入门 |
1 | npm i -D @babel/parser @babel/traverse @babel/generator @babel/types |
1 | @babel/parser` 将源代码解析成 `AST |
@babel/traverse
对AST
节点进行递归遍历,生成一个便于操作、转换的path
对象@babel/generator
将AST
解码生成js
代码@babel/types
通过该模块对具体的AST
节点进行进行增、删、改、查
新建drop-console.js
1 | const parser = require('@babel/parser') |
如何使用
1 | const path = require('path') |
实际上在
webpack4
中已经集成了去除console
功能,在minimizer
中可配置 去除console
附上官网 如何编写一个loader
3.2手写webpack plugin
在
Webpack
运行的生命周期中会广播出许多事件,Plugin
可以监听这些事件,在合适的时机通过Webpack
提供的API
改变输出结果。通俗来说:一盘美味的 盐豆炒鸡蛋 需要经历烧油 炒制 调味到最后的装盘等过程,而plugin
相当于可以监控每个环节并进行操作,比如可以写一个少放胡椒粉plugin
,监控webpack
暴露出的生命周期事件(调味),在调味的时候执行少放胡椒粉操作。那么它与loader
的区别是什么呢?上面我们也提到了loader
的单一原则,loader
只能一件事,比如说less-loader
,只能解析less
文件,plugin
则是针对整个流程执行广泛的任务。
一个基本的plugin插件结构如下
1 | class firstPlugin { |
compiler/compilation是什么?
compiler
对象包含了Webpack
环境所有的的配置信息。这个对象在启动webpack
时被一次性建立,并配置好所有可操作的设置,包括options
,loader
和plugin
。当在webpack
环境中应用一个插件时,插件将收到此compiler
对象的引用。可以使用它来访问webpack
的主环境。compilation
对象包含了当前的模块资源、编译生成资源、变化的文件等。当运行webpack
开发环境中间件时,每当检测到一个文件变化,就会创建一个新的compilation
,从而生成一组新的编译资源。compilation
对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
compiler和 compilation的区别在于
- compiler代表了整个webpack从启动到关闭的生命周期,而compilation 只是代表了一次新的编译过程
- compiler和compilation暴露出许多钩子,我们可以根据实际需求的场景进行自定义处理
compiler钩子文档
compilation钩子文档
下面我们手动开发一个简单的需求,在生成打包文件之前自动生成一个关于打包出文件的大小信息
新建一个webpack-firstPlugin.js
1 | class firstPlugin{ |
如何使用
1 | const path = require('path') |
1 | 执行 npm run build即可看到在dist文件夹中生成了一个包含打包文件信息的fileSize.md |
上面两个
loader
与plugin
案例只是一个引导,实际开发需求中的loader
与plugin
要考虑的方面很多,建议大家自己多动手尝试一下。