「一劳永逸」由浅入深配置webpack4
前言
webpack在前端化过程中十分重要,所以花了一段时间学习webpack,以及webpack4新特性,本文是按照从易到难的过程,梳理部分webpack概念,常见的loader,plugins,webpack4新特新,还有部分高级概念。
webpack需要掌握的核心概念👇
-
Entry
:webpack开始构建的入口模块 -
Output
: 如何命名输出文件,以及输出目录,比如常见的dist目录。 -
Loaders
:作用在于解析文件,将无法处理的非js文件,处理成webpack能够处理的模块。 -
Plugins
:更多的是优化,提取精华(公共模块去重),压缩处理(css/js/html)等,对webpack功能的扩展。 -
Chunk
: 个人觉得这个是webpack 4 的Code Splitting
产物,抛弃了webpack3的CommonsChunkPlugin
,它最大的特点就是配置简单,当你设置mode
是production
,那么 webpack 4 就会自动开启Code Splitting
,可以完成将某些公共模块去重,打包成一个单独的chunk
。
此次学习webpack4新特性,基本上按照官网来配置的。
如果你跟我一样,在配置自己的webpack中遇到问题,可以来这里找一找思路,「希望本文对你更多的是抛砖引玉」,对你学习webpack4新特性有所帮助。
Webpack基础知识
什么是webpack
webpack其实我理解就是「模块打包工具」,将多个模块打包到生成一个最终的bundle.js问题。
目前来看,webpack4.0已经可以打包任何形式的模块。
如何安装Webpack
首先确保你有node环境,在终端运行下面指令
node -v
npm -v
出现了两个版本号后,接下来就可以继续学习webpack了,npm包管理工具是必须的。
初始化项目
npm init
这个意思就是为了使项目符合规范,「初始化一个项目」,这样子使项目符合规范。
接下来就发现,在该根目录下,会「生成一个package.json文件」,这个文件描述了node项目,node包的一些信息。也就是说,「npm init 生成的就是一个package.json文件。」
「package.json属性说明」
name - 包名.
version - 包的版本号。
description - 包的描述。
homepage - 包的官网URL。
author - 包的作者,它的值是你在https://npmjs.org网站的有效账户名,遵循“账户名<邮件>”的规则, 例如:zhangsan <[email protected]>。
contributors - 包的其他贡献者。
dependencies / devDependencies - 生产/开发环境依赖包列表。它们将会被安装在 node_module 目录下。
main - main 字段指定了程序的主入口文件,require('moduleName') 就会加载这个文件。这个字段的默认值是模块根目录下面的 index.js。
keywords - 关键字
接下来就是安装webpack
npm install webpack webpack-cli -g // 全局安装webpack
不过建议还是不要这样子安装,当你有多个项目的时候,你其中一个webpack依赖版本是3.x本版,当前版本是4.0版本的话,那么这个项目是运行不起来的。
那么先卸载全局安装的webpack,然后在当前你要运行的项目再单独安装。
npm uninstall webpack webpack-cli -g //卸载全局webpack
怎么全局安装呢👇
npm install webpack webpack-cli -D // 局部安装
要想查看版本的话,「webpack -v」这个命令此时不行,因为node此时会去全局查找,发现找不到webpack包,因为我们之前已经卸载全局的webpack,所以我们得用一个新命令。
npx webpack -v
这个时候,就可以看到对于版本号。
如何查看包的版本呢
npm info webpack // 查看webpack包版本
webpack配置文件
webpack.config.js就是webpack的默认配置文件,我们可以自定义配置文件,比如文件的入口,出口。
const path = require('path')
module.exports = {
entry : './index.js',
output : {
filename : 'bundle.js',
path : path.join(__dirname, 'dist')
}
}
这个就是最基本的配置,打包一个index.js文件,也就是entry,output打包的入口,出口配置信息。
那么这个时候,在命令行中运行npx webpack
,就会去找webpack.config.js文件中的配置信息。
「默认的配置文件必须是webpack.config.js这个名称」,但是你自己写了一个webpack配置文件信息,那行不行呢?当然是可以的,那你得运行以下命令👇
npx webpack --config webpack.config.js
// --config 后面就是你自己配置的webpack文件信息
npm scripts
「npm scripts」 有时候,你用过vue,React的话, 经常使用的都是npm run dev的形式,那么我们是不是也能配置这样子的信息呢?我们只需要在package.json文件中配置scripts命令就行👇
"scripts": {
"dev": "webpack --config webpack.config.js"
},
这个时候,你在运行npm run dev,它会被翻译成对于的指令,也会打包对应的文件。
「webpack打包三种命令」
-
webpack index.js (全局) -
npx webpack index.js -
npm run dev
webpack-cli
这个时候,你也许就会发现这个webpack-cli作用了吧,不下载这个包的话,你在命令行运行webpack指令是不生效的,也就是说,「webpack-cli作用就是可以在命令行运行webpack命令并且生效。」
不下载的话,在命令行中使用webpack命令是不允许的。
webpack配置环境
主要分为「development」和「production」两种环境,默认情况下是production环境,**两者的区别就是,后者会对打包后的文件压缩。**那么我们去配置一下👇
const path = require('path')
module.exports = {
mode : 'development',
entry : './index.js',
output : {
filename : 'bundle.js',
path : path.join(__dirname, 'bundle')
}
}
这个时候,再去看的话,就会发现,「bundle.js文件没有压缩代码」。
webpack核心概念loader
什么是loader
「loader就是一个打包的方案,它知道对于某个特定的文件该如何去打包。」 本身webpack不清楚对于一些文件如何处理,loader知道怎么处理,所以webpack就会去求助于loader。
webpack是默认知道如何打包js文件的,但是对于一些,比如图片,字体图标的模块,webpack就不知道如何打包了,那么我们如何让webpack识别图片等其他格式的模块呢?
那么就去配置文件webpack.config.js配置相应的信息,配置module👇
const path = require('path')
module.exports = {
mode: 'production',
entry: './src/index.js',
module: {
rules: [{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'file-loader'
}
}]
},
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
}
}
我们需要file-loader的话,也就是依赖于它,所以先下载它
npm install file-loader -D
然后我们看看index.js是如何写的👇
import acator from './头像.jpg'
console.log(acator)
通过这个我们发现,在控制台,打印的结果是
3f16daf5233d30f46509b1bf2c4e08a5.jpg
说明file-loader帮我们图片模块打包到了dist目录下,并且index.js中,这个acator变量,结果是一个名称,这样子的话,就可以完成打包,后续需要该图片也轻松搞定。
「总结」
webpack无法识别非js结尾的模块,所以需要loader让webpack识别出来,这样子就可以完成打包。
-
遇到非js结尾的模块,webpack会去module中找相应的规则,匹配到了对于的规则,然后去求助于对应的loader -
对应的loader就会将该模块打包到相应的目录下,上面的例子就是dist目录,并且呢, 「返回的是该模块的路径」,拿上面的例子来说,就是 acator
变量的值就是路径。
如何配置file-loader
当然就是看webpack官网了,这里面文档很详细,点这里
举个例子,比如,你想将文件打包名称不改变,并且加个后缀的话,可以这么来配置options👇
{
loader: 'file-loader',
options: {
// name就是原始名称,hash使用的是MD5算法,ext就是后缀
name: '[name]_[hash].[ext]'
}
}
我们引入照片的是下面👇
import acator from './头像.jpg'
那么最后打包完的名称是说明呢👇
头像_3f16daf5233d30f46509b1bf2c4e08a5.jpg
在举个例子,比如你想将图片这些模块都打包到dist目录下的images下,是不是也是可以配置下
{
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/'
}
}
这样子的话,就会在dist/images/ 目录下找到对应的打包好图片。
比如不同的环境下,打包的图片位置也可以不一样,👇
if (env === 'development') {
return '[path][name].[ext]'
}
剩下的就去官网,自己配置吧。
如何配置url-loader
上面对于图片的模块打包,我们同样可以去使用url-loader,那么它与file-loader区别是什么呢?
{
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit : 102400 //100KB
}
}
唯一的区别就在于,要打包的图片是否会打包到images目录下,还是以Base64格式打包到bundle.js文件中,这个就看limit配置项了。
-
当你打包的图片大小比limit配置的参数大,那么跟file-loader一样。 -
当图片较小时,那么就会以Base64打包到bundle.js文件中。
更多的url-loader看官网
如何配置css-loader
比如你引入了一个css模块,这个时候,就需要去下载相应的模块loader。
cnpm install css-loader style-loader -D // 下载对应的模块
然后就是配置module👇
{
test: /\.css$/,
use: ['style-loader','css-loader']
}
这样子的话,你在index.js 导入样式就可以生效啦,我们看看是如何导入的👇
import acator from './头像.jpg'
import './index.css'
const img = new Image()
img.src = acator
img.classList.add('imgtitle')
document.body.appendChild(img)
这个imgtitle就是样式,如下👇
.imgtitle{
width: 100px;
height: 100px;
}
通过两个loader,就实现了webpack打包css文件,那我们分析以下两个loader功能。
-
css-loader主要作用就是将多个css文件整合到一起,形成一个css文件。 -
style-loader会把整合的css部分挂载到head标签中。
那么如果你使用scss预编译css的话,webpack是无法打包该文件的,所以又需要安装新的loader👇
如何配置sass-loader
我们看官网scss-loader需要下载哪些,点这里
npm install sass-loader node-sass --save-dev
上面是安装sass-loader,需要同时安装node-sass,然后就去配置对应的module
{
test: /\.scss$/,
use: ['style-loader','css-loader','sass-loader']
}
这样子的话,你像下面去导入scss样式文件,是可以打包完成的👇
// index.js
import acator from './头像.jpg'
// console.log(acator)
import './index.scss' // 导入scss文件
const img = new Image()
img.src = acator
img.classList.add('imgtitle')
document.body.appendChild(img)
模块的加载就是从右像左来的,所以先加载sass-loader翻译成css文件,然后使用css-loader打包成一个css文件,在通过style-loader挂载到页面上去。
接下来又有新的问题了,如果在scss文件中使用css3新特新的话,是不是需要加上厂商前缀呢?这个时候,我们需要怎么去呢?应该加上一个什么loader呢?看下面
如何配置postcss-loader
这个loader解决的就是加上厂商前缀,我们看webpack官网是怎么做的👉点这里
npm i -D postcss-loader autoprefixer
然后呢,还需要建一个「postcss.config.js」,这个配置文件(「位置跟webpack.config.js一个位置」)配置如下信息👇
// postcss.config.js
// 需要配置这个插件信息
module.exports = {
plugins: [
require('autoprefixer')({
overrideBrowserslist: [
"Android 4.1",
"iOS 7.1",
"Chrome > 31",
"ff > 31",
"ie >= 8"
]
})
]
};
一开始我设置的话,是不生效的,原因就是「没有设置支持的浏览器」,然后看看下面👇
{
test: /\.scss$/,
use: ['style-loader','css-loader','sass-loader','postcss-loader']
}
最后就可以看见比如css3会加上厂商前缀了👇
-webkit-transform: translate(100px, 100px);
-ms-transform: translate(100px, 100px);
transform: translate(100px, 100px);
一些其他问题,有时候,你会遇到这样子的一个问题,你不在某个scss文件中又导入新的scss文件,这个时候,打包的话,它就不会帮你重新安装postcss-loader开始打包,这个时候,我们应该如何去设置呢,我们先来看例子👇
// index.scss
@import './creare.scss';
body {
.imgtitle {
width: 100px;
height: 100px;
transform: translate(100px, 100px);
}
}
-
我们知道,我们配置的loader规则中,是符合这样子的预期 -
当js代码中引入scss模块的话,会按照这样子的规则去做 -
那么如何在scss文件中引入scss文件,那么规则肯定不会从postcss-loader开始打包,所以我们需要配置一些信息。
{
test: /\.scss$/,
use: ['style-loader',
{
loader: 'css-loader',
options:{
importLoaders:2,
modules : true
}
},
'sass-loader',
'postcss-loader'
]
}
我们需要在css-loader中配置options,加入「importLoaders :2」, 这样子就会走postcss-loader,和sass-loader,这样子的语法,「无论你是在js中引入scss文件,还是在scss中引入scss文件,都会重新依次从下往上执行所以loader。」
那么modules:true
这个配置是什么作用呢?有时候,你希望你的css样式作用的是当前的模块中,而不是全局的话,就需要加上这个配置了,看下案例👇
// index.js
import acator from './头像.jpg'
import create from './create'
import style from './index.scss' // 通过modules:true 避免了css作用域create中的模块
const img = new Image()
img.src = acator
img.classList.add(style.imgtitle)
document.body.appendChild(img)
create()
那么create模块是什么呢👇
import acator from './头像.jpg'
import style from './index.scss'
function create() {
const img = new Image()
img.src = acator
img.classList.add(style.imgtitle)
document.body.appendChild(img)
}
export default create;
可以看出来,这个create模块,就是创建一个img标签,并且设置单独的样式。给modules : true
后,我们需要接着往下做的就是import语法上有些改变。
import style from './index.scss'
然后通过style这个对象变量中去找,找到scss中设置的名称即可。
「总结」
-
importLoaders:2
该配置信息解决的就是在scss文件中又引入scss文件,会重新从postcss-loader开始打包 -
modules:true
会作用域当前的css环境中,样式不会全局引入,语法上也需要使用如下引入 -
import style from './index.scss'
比如字体图标怎么配置信息呢?对于字体图标大打包,可以使用file-loader完成👇
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'file-loader'
]
}
更多的静态资源的打包配置,可以看官网是如何使用的,👉(静态loader管理资源)
webpack核心概念plugins
如何使用plugins让打包更加便捷呢,plugins意思就是插件意思,很大程度上方便了我们,那我们来看看吧。
plugins: 「可以在webpack运行到某个时刻的时候,帮你做一些事情.」
如何使用HtmlWebpackPlugin
「这个插件的作用,就是为你生成一个HTML文件,然后将打包好的js文件自动引入到这个html文件中。」
如何配置呢?可以看webpack官网
首先第一步下载HtmlWebpackPlugin
cnpm install --save-dev html-webpack-plugin
然后在webpack.config.js中配置如下信息👇
var HtmlWebpackPlugin = require('html-webpack-plugin');
var path = require('path');
var webpackConfig = {
entry: 'index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'index_bundle.js'
},
plugins: [new HtmlWebpackPlugin({
template: 'src/index.html' // 以src/目录下的index.html为模板打包
}
)],
};
然后运行npm run dev,就会发现在dist目录下,自动帮你生成一个HTML模块,并且引入bundle.js文件。
template: 'src/index.html'
这个配置信息的作用就是告诉你,以具体哪个index.html为模板去打包
如何使用CleanWebpackPlugin
这个插件的作用就是会帮你删除某个目录的文件,是在打包前删除所有上一次打包好的文件。
cnpm i clean-webpack-plugin -D
//"clean-webpack-plugin": "^3.0.0",我的是这个版本
然后配置clean-webpack-plugin的话,需要去对于网站上查看如何配置的,可以点这里👉 npm上
配置信息如下👇,这个是最新的clean-webpack-plugin配置
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
// plugins新增加这一项,webpack4版本不需要配置路径
plugins: [ new CleanWebpackPlugin()]
最新的webpack4版本是不需要去配置路径的,自动帮我们清除打包好的dist目录下文件。
webpack核心概念
entry和output基本配置
有时候,你需要多个入口文件,那么我们又是怎么去完成的呢?这个时候,就需要来看一看entry和output配置项
当然了,webpack官网也是有文档的,out点这里以及entry点这里
entry: {
index :'./src/index.js',
bundle : './src/create.js',
},
output: {
filename: '[name].js',
publicPath: "https://cdn.example.com/assets/",
path: path.join(__dirname, 'dist')
}
「总结」
-
entry这样子配置就可以接受多个打包的文件入口,同时的话,output输出文件的filename需要使用占位符name -
这样子就会生成两个文件,不会报错,对于的名字就是entry名称对应 -
如果后台已经将资源挂载到了cdn上,那么你的publicPath就会把路径前做修改加上publicPath值
如何使用devtool配置source-map
devtool配置source-map,解决的问题就是,当你代码出现问题时,会映射到你的原文件目录下的错误,并非是打包好的错误,这点也很显然,如果不设置的话,只会显示打包后bundle.js文件中报错,对应查找错误而言,是很不利的
devtool:'inline-cheap-source-map'
对应不同的环境,设置devtool是很有必要的,开发环境中,我们需要看我们代码是哪里报错误,所以需要配置
webpack官网有文档介绍
那我们给出结论👇
-
development环境下,配置 devtool:'cheap-module-eval-source-map'
-
production环境下,配置 devtool:'cheap-module-source-map'
// development devtool:'cheap-module-eval-source-map'
// production devtool:'cheap-module-source-map'
如何使用webpack-dev-server
webpack-dev-server 能够用于快速开发应用程序。很多的配置都在webpack官网有,点击这里
首先先下载它
cnpm i clean-webpack-plugin -D
它的作用很多,可以开启一个服务器,而且可以实时去监听打包文件是否改变,改变的话,就会出现去更新.
devServer: {
contentBase: path.join(__dirname, "dist"), // dist目录开启服务器
compress: true, // 是否使用gzip压缩
port: 9000, // 端口号
open : true // 自动打开网页
},
很多的配置项,可以去官方文档查看,比如proxy代理等配置项,更多文档点击这里
然后在package.json中scripts配置项如下
"start": "webpack-dev-server"
「这个devServer可以实时检测文件是否发生变化」
同时你需要注意的内容就是使用webpack-dev-server打包的话,不会生成dist目录,而是将「你的文件打包到内存中」
「总结」
-
devServer可以开启一个本地服务器,同时帮我们更新加载最新资源 -
打包的文件会放在内存中,不会生成dist目录
模块热替换(hot module replacement)
模块热替换(HMR - Hot Module Replacement)功能会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。
顾名思义它说的就是,多个模块之前,当你修改一个模块,而不想重新加载整个页面时,就可以使用hot module replacement
举个例子,当你修改了css代码中一些样式,不配置HMR模块热替换的话,整个页面都会重新去渲染,这样子是没有必要的,那么我们接下来就去配置👇
devServer: {
contentBase: path.join(__dirname, "dist"),
compress: true,
port: 9000,
open: true,
hot: true, // 开启热更新
// hotOnly: true,
},
这个hotOnly可以设置,最主要的是设置hot:true
然后加入两个插件,这个插件时webpack自带的,所以不需要下载👇
const webpack = require('webpack')
plugins: [
new webpack.NamedModulesPlugin(), // 可配置也可不配置
new webpack.HotModuleReplacementPlugin() // 这个是必须配置的插件
],
添加了 NamedModulesPlugin
,以便更容易查看要修补(patch)的依赖。
配置完上述的信息之后,重新去运行命令的话,就会发现启动了模块热替换
,不同模块的文件更新,只会下载当前模块文件
唯一需要注意的内容是,对于css的内容修改,css-loader底层会帮我们做好实时热更新,对于JS模块的话,我们需要手动的去配置👇
if(module.hot){
module.hot.accept('./print',()=>{
print()
})
}
这个官方也给出了语法,module.hot.accept(module1,callback) 表示的就是接受一个需要实时热更新的模块,当内容发生变化时,会帮你检测到,然后执行回调函数
「总结」
-
HMR模块热替换解决的问题就是,它允许在运行时更新各种模块,而无需进行完全刷新。 -
意思就是不需要重新去本地服务器重新去加载其他为修改的资源 -
需要注意的就是,对于js文件的热更新的话,需要手动的去检测更新内容,也就是module.hot.accept语法
更多的配置信息去webpack官网查看,点这里查看HMR
Babel处理ES6语法
接下来我们就来配置它👇
npm install --save-dev babel-loader @babel/core
// @babel/core 是babel中的一个核心库
npm install --save-dev @babel/preset-env
// preset-env 这个模块就是将语法翻译成es5语法,这个模块包括了所有翻译成es5语法规则
npm install --save @babel/polyfill
// 将Promise,map等低版本中没有实现的语法,用polyfill来实现.
配置module👇
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage"
}
]
]
}
}
]
}
// exclude参数: node_modules目录下的js文件不需要做转es5语法,也就是排除一些目录
// "useBuiltIns"参数:
-
有了 preset-env
这个模块后,我们就会发现我们写的 「const语法被翻译成成var」 -
但是细心的会发现,对于Promise以及map这些语法,低版本浏览器是不支持的, -
所以我们需要 @babel/polyfill
模块,对Promise,map进行补充,完成该功能,也就是前面说的polyfill
然后我们怎么使用呢?就是在js文件最开始导入👇
import "@babel/polyfill";
但是细心的同学,会发现问题,用完这个以后,打包的文件体积瞬间增加了10多倍之多,这是为什么呢?
这是因为,@babel/polyfill
为了弥补Promise,map等语法的功能,该模块就需要「自己去实现Promise,map等语法」的功能,这也就是为什么打包后的文件很大的原因.
那我们需要对@babel/polyfill
参数做一些配置即可,如下👇
"useBuiltIns": "usage"
这个语法作用就是: 只会对我们index.js当前要打包的文件中使用过的语法,比如Promise,map做polyfill,其他es6未出现的语法,我们暂时不去做polyfill,这样子,打包后的文件就减少体积了
「总结」
-
需要按照babel-loader @babel/core这些库,@babel/core是它的核心库 -
@babel/preset-env 它包含了es6翻译成es5的语法规则 -
@babel/polyfill 解决了低版本浏览器无法实现的一些es6语法,使用polyfill自己来实现 -
通过 import "@babel/polyfill";
在js文件开头引入,完成对es6语法的polyfill -
以上的场景都是解决的问题是业务中遇到babel的问题
更多的配置看官方文档,点这里
当你生成第三方模块时,或者是生成UI组件库时,使用polyfill解决问题,就会出现问题了,上面的场景使用babel会污染环境,这个时候,我们需要换一种方案来解决👇
「@babel/plugin-transform-runtime」这个库就能解决我们的问题,那我们先安装需要的库
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
我们这个时候可以在根目录下建一个.babelrc
文件,将原本要在options中的配置信息写在.babelrc
文件👇
{
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
// 当你的 "corejs": 2,需要安装下面这个
npm install --save @babel/runtime-corejs2
这样子的话,在使用语法是,就不需要去通过import "@babel/polyfill";
这样子的语法去完成了,直接正常写就行了,而且从打包的体积来看,其实可以接受的
「总结」
-
从业务场景来看,可以使用 @babel/preset-env
-
从自己生成第三方库或者时UI时,使用 @babel/plugin-transform-runtime
,它作用是将 helper 和 polyfill 都改为从一个统一的地方引入,并且引入的对象和全局变量是完全隔离的,避免了全局的污染
webpack高级概念
如何使用tree shaking
tree树,shaking摇动,那么你可以把程序想成一颗树。绿色表示实际用到的源码和 library,是树上活的树叶。灰色表示无用的代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。
通俗意义而言,当你引入一个模块时,你可能用到的只是其中的某些功能,这个时候,我们不希望这些无用
的代码打包到项目中去。通过tree-shaking,就能将没有使用的模块摇掉,这样达到了删除无用代码的目的。
需要注意的时webpack4默认的production下是会进行tree-shaking的,
optimization.usedExports
使webpack确定每个模块导出项(exports)的使用情况。依赖于optimization.providedExports
的配置。optimization.usedExports
收集到的信息会被其他优化项或产出代码使用到(模块未用到的导出项不会被导出,在语法完全兼容的情况下会把导出名称混淆为单个char)。为了最小化代码体积,未用到的的导出项目(exports)会被删除。生产环境(production)默认开启。
module.exports = {
//...
optimization: {
usedExports: true
}
};
这个时候,再去看看自己的打包bundle.js文件,就会发现,它会有相应的提升功能。
「将文件标记为无副作用(side-effect-free)」
有时候,当我们的模块不是达到很纯粹,这个时候,webpack就无法识别出哪些代码需要删除,所以,此时有必要向 webpack 的 compiler 提供提示哪些代码是“纯粹部分”。
这种方式是通过 package.json 的 "sideEffects"
属性来实现的。
{
"name": "webpack-demo",
"sideEffects": false
}
如同上面提到的,如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false
,来告知 webpack,它可以安全地删除未用到的 export 导出。
注意,任何导入的文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似
css-loader
并导入 CSS 文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:
{
"name": "webpack-demo",
"sideEffects": [
"*.css"
]
}
「压缩输出」
通过如上方式,我们已经可以通过
import
和export
语法,找出那些需要删除的“未使用代码(dead code)”,然而,我们不只是要找出,还需要在 bundle 中删除它们。为此,我们将使用-p
(production) 这个 webpack 编译标记,来启用 uglifyjs 压缩插件。
「从 webpack 4 开始,也可以通过 "mode"
配置选项轻松切换到压缩输出,只需设置为 "production"
。」
「总结」
-
为了使用tree-shaking的话,需要使用ES Module语法,也就是使用 ES2015 模块语法(即 import
和export
)。 -
在项目 package.json
文件中,添加一个 "sideEffects" 入口。 -
引入一个能够删除未引用代码(dead code)的压缩工具(minifier)(例如 UglifyJSPlugin
),当然了,webpack4开始,以及支持压缩输出了。
对于原理篇,可以看看这篇Tree-Shaking性能优化实践 - 原理篇
development和production环境构建
在开发环境和生成环境中,我们依赖的功能是不一样的,举个例子👇
-
开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。 -
生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。
基于以上两点的话,我们需要为每个环境搭建彼此独立的webpack配置。
其实,写过vue,React都会发现,有一个webpack.common.js
的配置文件,它的作用就是不必在配置中配置重复的代码。
webpack-merge安装
那么首先需要安装的就是webpack-merge
,之后再整合一起。
cnpm install --save-dev webpack-merge
那么我们的目录就是这样子的👇
webpack-demo
|- build
|- webpack.common.js //三个新webpack配置文件
|- webpack.dev.js //三个新webpack配置文件
|- webpack.prod.js //三个新webpack配置文件
|- package.json
|-postcss.config.js
|-.babelrc
|- /dist
|- /src
|- index.js
|- math.js
|- /node_modules
那么学到现在,看看配置了哪些信息👇
「webpack.common.js」
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
const commonConfig = {
entry: {
main: './src/index.js',
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
}, {
test: /\.(jpg|gif|png)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 1024 //100KB
}
}
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
}, {
test: /\.scss$/,
use: ['style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
modules: true
}
},
'sass-loader',
'postcss-loader'
]
}, {
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'file-loader'
]
}]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html' // 以src/目录下的index.html为模板打包
}),
new CleanWebpackPlugin({
// 不需要做任何的配置
}),
],
output: {
filename: '[name].js',
// publicPath: "https://cdn.example.com/assets/",
path: path.join(__dirname, '../dist')
}
}
module.exports = commonConfig
「webpack.dev.js」
const path = require('path')
const webpack = require('webpack')
const {merge} = require('webpack-merge')
const commonConfig = require('./webpack.common')
const devConfig = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: path.join(__dirname, "dist"),
compress: true,
port: 9000,
open: true,
hot: true,
// hotOnly: true,
},
plugins: [
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin(),
],
optimization:{
usedExports: true
}
}
module.exports = merge(commonConfig, devConfig)
「webpack.prod.js」
const {merge} = require('webpack-merge')
const commomConfig = require('./webpack.common')
const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map',
}
module.exports = merge(commomConfig, prodConfig)
注意,在环境特定的配置中使用 merge()
很容易地包含我们在 dev
和 prod
中的常见配置。webpack-merge
工具提供了多种合并(merge)的高级功能,但是在我们的用例中,无需用到这些功能。
NPM Scripts
现在,我们把 scripts
重新指向到新配置。我们将 npm run dev
定义为开发环境脚本,并在其中使用 webpack-dev-server
,将 npm run build
定义为生产环境脚本:
{
"name": "webpack-demo",
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.dev.js",
"build": "webpack --config ./build/webpack.prod.js",
"start": "npx webpack --config ./build/webpack.dev.js"
},
}
需要注意的是,我将三个文件放在了build目录下,当然了,在根目录情况下,我们就把--config
后面的指令路径修改即可。
还有一个需要注意的就是clean-webpack-plugin
这个插件的配置,当你把它都放进build目录下,此时的相对该插件的根目录就是build,所以我们需要做修改👇
new CleanWebpackPlugin({
// 不需要做任何的配置
}),
最新的clean-webpack-plugin
,不需要设置清除目录,自动清除打包路径,也就是dist目录。
SplitChunksPlugin代码分隔
当你有多个入口文件,或者是打包文件需要做一个划分,举个例子,比如第三方库lodash,jquery等库需要打包到一个目录下,自己的业务逻辑代码需要打包到一个文件下,这个时候,就需要提取公共模块了,也就需要SplitChunksPlugin这个插件登场了。
这个是webpack4新增加的插件,我们需要手动去配置optimization.splitChunks。接下来,我们就来看看它的基本配置吧👇
module.exports = {
//...
optimization: {
splitChunks: {
chunks: "async",
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/, //匹配node_modules中的模块
priority: -10 //优先级,当模块同时命中多个缓存组的规则时,分配到优先级高的缓存组
},
default: {
minChunks: 2, //覆盖外层的全局属性
priority: -20,
reuseExistingChunk: true //是否复用已经从原代码块中分割出来的模块
}
}
}
},
};
那我们从每个参数开始说起👇
-
在cacheGroups外层的属性设置适用于所有的缓存组,不过每个缓存组内部都可以 「重新」设置它们的值 -
chunks: "async"
这个属性设置的是以 「什么类型」的代码经行分隔,有三个值 -
initial
入口代码块 -
all
全部 -
async
按需加载的代码块 -
minSize: 30000
模块大小超过30kb的模块才会提取 -
minChunks: 1
, 当某个模块至少被多少个模块引用时,才会被提取成新的chunk -
maxAsyncRequests: 5
,分割后,按需加载的代码块最多允许的并行请求数 -
maxInitialRequests: 3·
分割后,入口代码块最多允许的并行请求数 -
automaticNameDelimiter: "~"
代码块命名分割符 -
name: true,
每个缓存组打包得到的代码块的名称 -
cacheGroups
缓存组,定制相应的规则。
自己根据实际情况去设置相应的规则,每个缓存组根据规则将匹配的模块会分配到代码块(chunk)中,每个缓存组的打包结果可以是单一 chunk,也可以是多个 chunk。
这里有篇实际项目中如何代码分隔的,有兴趣的可以看看SplitChunk代码实例
Lazy-loding懒加载和Chunk
import异步加载模块
在webpack中,什么是懒加载,举个例子,当我需要按需引入某个模块时,这个时候,我们就可以使用懒加载,其实实现的方案就是import语法,在达到某个条件时,我们才会去请求资源。
那么我们来看看,如何实现懒加载👇
在讲这个之前,我们的先借助一个插件,完成对import语法的识别。
cnpm install --save-dev @babel/plugin-syntax-dynamic-import
然后再.babelrc
文件下配置,增加一个插件
{
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}
这样子的话,我们就可以项目中自由的使用import按需加载模块了。
// create.js
async function create() {
const {
default: _
} = await import(/*webpackChunkName:"lodash"*/'lodash')
let element = document.createElement('div')
element.innerHTML = _.join(['TianTian', 'lee'], '-')
return element
}
function demo() {
document.addEventListener('click', function () {
create().then(element => {
document.body.appendChild(element)
})
})
}
export default demo;
我这个模块的功能,就是当你点击页面后,会触发create函数,然后加载loadsh库,最后再页面中懒加载lodash,打包是正常打包,但是呢,有些资源,可以当你触发某些条件,再去加载,这也算是优化手段吧。
Chunk
Chunk在Webpack里指一个代码块,那具体是指什么样的代码块呢?👇
Chunk是Webpack打包过程中,一堆module的集合。Webpack通过引用关系逐个打包模块,这些module就形成了一个Chunk。
「产生Chunk的三种途径」
-
entry入口 -
异步加载模块 -
代码分割(code spliting)
Chunk只是一个概念,理解了Chunk概念,更有利于对webpack有一定的认识。
CSS代码压缩提取
在线上的环境中,我们需要去将我们的CSS文件单独的打包到一个Chunk下,所以需要借助一定的插件,完成这个工作。
mini-css-extract-plugin css代码提取
将css提取为独立的文件插件,支持按需加载的css和sourceMap,我们可以查看GitHub官方,来看看它的文档
**目前缺失功能,HMR。**所以,我们可以把它运用到生成环境中去,开始安装👇
npm install --save-dev mini-css-extract-plugin
对着这个插件的使用,还是建议在webpack.prod.js中(生产环境)配置,这个插件暂时暂时不支持HMR,而且在开发环境中development,是需要用到HMR的,所以我们这次配置只在webpack.prod.js配置。
需要注意的一点是,当你的webpack版本是4版本的时候,需要去package.json中配置sideEffects
属性,这样子就「避免了把css文件作为Tree-shaking」。
{
"name": "webpack-demo",
"sideEffects": [
"*.css"
]
}
然后的话,我们看看webpack.prod.js是如何配置参数的。
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const {
merge
} = require('webpack-merge')
const commomConfig = require('./webpack.common')
const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map',
plugins: [
new MiniCssExtractPlugin({
filename:'[name].[hash].css',
chunkFilename: '[id].[hash].css',
})
],
module: {
rules: [{
test: /\.(sa|sc|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader',
],
}]
}
}
module.exports = merge(commomConfig, prodConfig)
当你在js中引入css模块时,最后在dist目录下,看到了css单独的Chunk的话,说明css代码提取成功了,接下来就是对「css代码的压缩」。
webpack4默认在生产环境下,是不会去压缩css代码的,所以我们需要下载对于的plugin
optimize-css-assets-webpack-plugin css代码压缩
optimize-css-assets-webpack-plugin GitHub官方文档
这个会对打包后的css代码经行代码压缩,我们下载这个包👇
npm install --save-dev optimize-css-assets-webpack-plugin
接下来就是设置 「optimization.minimizer」 ,这里需要注意的就是,此时设置optimization.minimizer会覆盖webpack默认提供的规则,比如「JS代码就不会再去压缩了」。
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const {
merge
} = require('webpack-merge')
const commomConfig = require('./webpack.common')
const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map',
optimization: {
minimizer: [
new UglifyJsPlugin({
sourceMap: true,
parallel: true, // 启用多线程并行运行提高编译速度
}),
new OptimizeCSSAssetsPlugin({}),
]
},
plugins: [
new MiniCssExtractPlugin({
// 类似 webpackOptions.output里面的配置 可以忽略
filename: '[name].[hash].css',
chunkFilename: '[id].[hash].css'
})
],
module: {
rules: [{
test: /\.(sa|sc|c)ss$/,
use: [{
loader: MiniCssExtractPlugin.loader,
options: {
// 这里可以指定一个 publicPath
// 默认使用 webpackOptions.output中的publicPathcss
// 举个例子,后台支持把css代码块放入cdn
publicPath: "https://cdn.example.com/css/"
},
},
'css-loader',
'postcss-loader',
'sass-loader',
],
}]
},
}
module.exports = merge(commomConfig, prodConfig)
但是呢,此时就会发现在生产环境下,JS压缩也会存在问题,所以为了解决问题,我们统一在下面梳理👇
uglifyjs-webpack-plugin代码压缩
这个插件解决的问题,就是当你需要去optimization.minimizer中设置,这样子会覆盖「webpack基本配置」,原本JS代码压缩的功能就会被覆盖,所以我们需要下载它。
npm install -D uglifyjs-webpack-plugin
然后在webpack.prod.js配置如上信息即可,它的更多配置看官网文档
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const {
merge
} = require('webpack-merge')
const commonConfig = require('./webpack.common')
const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map',
optimization: {
minimizer: [
new UglifyJsPlugin({
sourceMap: true,
parallel: true, // 启用多线程并行运行提高编译速度
}),
new OptimizeCSSAssetsPlugin({}),
]
},
plugins: [
new MiniCssExtractPlugin({
// 类似 webpackOptions.output里面的配置 可以忽略
filename: '[name].[hash].css',
chunkFilename: '[id].[hash].css'
})
],
module: {
rules: [{
test: /\.(sa|sc|c)ss$/,
use: [{
loader: MiniCssExtractPlugin.loader,
options: {
// 这里可以指定一个 publicPath
// 默认使用 webpackOptions.output中的publicPathcss
// 举个例子,后台支持把css代码块放入cdn
publicPath: "https://cdn.example.com/css/"
},
},
'css-loader',
'postcss-loader',
'sass-loader',
],
}]
},
}
module.exports = merge(commonConfig, prodConfig)
对于开发者环境而言,对css代码提取,以及打包是没有意义的,统一对于js代码压缩,也会降低效率,也是不推荐这么去做的,所以我们就跳过在开发环境中对它们的配置。
contenthash解决浏览器缓存
当你打包一个项目即将上线时,有一个需求,你只是修改了部分的文件,只希望用户对于其他的文件,依旧去采用浏览器缓存中的文件,所以这个时候,我们需要用到contenthash
。
webpack中关于hash,有三种,分别是👇
hash
hash,主要用于开发环境中,在构建的过程中,当你的项目有一个文件发现了改变,整个项目的hash值就会做修改(整个项目的hash值是一样的),这样子,每次更新,文件都不会让浏览器缓存文件,保证了文件的更新率,提高开发效率。
chunkhash
跟打包的chunk有关,具体来说webpack
是根据入口entry
配置文件来分析其依赖项并由此来构建该entry的chunk
,并生成对应的hash
值。不同的chunk
会有不同的hash
值。
在生产环境中,我们会把第三方或者公用类库进行单独打包,所以不改动公共库的代码,该chunk
的hash
就不会变,可以合理的使用浏览器缓存了。
但是这个中hash的方法其实是存在问题的,生产环境中我们会用webpack
的插件,将css
代码打单独提取出来打包。这时候chunkhash
的方式就不够灵活,因为只要同一个chunk
里面的js修改后,css
的chunk
的hash
也会跟随着改动。因此我们需要contenthash
。
contenthash
contenthash
表示由文件内容产生的hash
值,内容不同产生的contenthash
值也不一样。生产环境中,通常做法是把项目中css
都抽离出对应的css
文件来加以引用。
对于webpack,旧版本而言,即便每次你npm run build,「内容不做修改的话,contenthash值还是会有所改变」,这个是因为,当你在模块之间存在相互之间的引用关系,有一个「manifest文件」。
manifest文件是用来引导所以模块的交互,manifest文件包含了加载和处理模块的逻辑,举个例子,你的第三方库打包后的文件,我们称之为vendors,你的逻辑代码称为main,当你webpack生成一个bundle时,它同时会去维护一个manifest文件,你可以理解成每个bundle文件存在这里信息,所以每个bundle之间的manifest信息有不同,这样子我们就需要将manifest文件给提取出来。
这个时候,需要在「optimization」中增加一个配置👇
module.exports = {
optimization: {
splitChunks: {
// ...
},
runtimeChunk: {// 解决的问题是老版本中内容不发生改变的话,contenthash依旧会发生改变
name: 'manifest'
}
}
}
当然了,要是还没来理解的话,可以去webpack官方网站,看看manifest定义以及它的含义。
说完了这个,我们看看我们应该如何去配置output吧,我们先看下webpack.prod.js配置
output: {
filename: '[name].[contenthash].js',
chunkFilename:'[vendors].[contenthash].js',
// publicPath: "https://cdn.example.com/assets/",
path: path.join(__dirname, '../dist')
}
对于的webpack.dev.js中只需要将contenthash改为hash就行,这样子开发的时候,提高开发效率。
shimming 全局变量
简单翻译就是垫片
,它解决的场景有哪些呢,举个例子,当你再使用第三方库,此时需要引入它,又或者是你有很多的第三方库或者是自己写的库,每个js文件都需要依赖它,让人很繁琐,这个时候,shimming就派上用场了。
我们需要使用 ProvidePlugin
插件,这个webpack是内置的,shimming依赖的就是这个插件。
使用 ProvidePlugin
后,能够在通过 webpack 编译的每个模块中,通过访问一个变量来获取到 package 包。
增加一个Plugin配置👇
new webpack.ProvidePlugin({
// 这里设置的就是你相应的规则了
// 等价于在你使用lodash模块中语句👇
// import _ from 'lodash'
_: 'lodash'
})
举个例子👇
// array_add.js
export const Arr_add = arr=>{
let str = _.join(arr,'++');
return str;
}
这样子没有正常导入lodash库的话,是会报错的,但是我们使用了ProvidePlugin插件,使得它会提供相应的lodash包,注意到的就是,避免多个lodash包被打包多次,可以使用CommonsChunkPlugin
插件,webpack4已经抛弃它了,使用的是splitChunksPlugin
插件取代它,我在之前的地方已经梳理过了。
更多的用法可以查看shimming垫片
梳理总结有限,反而更多的是自己配置过程中遇到的一些问题梳理总结,webpack4新特性值得你去学习,下面是一些加餐文章👇
加餐
-
Webpack揭秘——走向高阶前端的必经之路 -
import、require、export、module.exports 混合使用详解 -
webpack详解 -
webpack4配置详解之慢嚼细咽 -
「吐血整理」再来一打Webpack面试题