vlambda博客
学习文章列表

「一劳永逸」由浅入深配置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 -vnpm -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 = acatorimg.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 = acatorimg.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.jsimport acator from './头像.jpg'import create from './create'
import style from './index.scss' // 通过modules:true 避免了css作用域create中的模块const img = new Image()img.src = acatorimg.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性能优化实践 - 原理篇

developmentproduction环境构建

在开发环境和生成环境中,我们依赖的功能是不一样的,举个例子👇

  • 开发环境中,我们需要具有强大的、具有实时重新加载(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.jsasync 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值。

在生产环境中,我们会把第三方或者公用类库进行单独打包,所以不改动公共库的代码,该chunkhash就不会变,可以合理的使用浏览器缓存了。

但是这个中hash的方法其实是存在问题的,生产环境中我们会用webpack的插件,将css代码打单独提取出来打包。这时候chunkhash的方式就不够灵活,因为只要同一个chunk里面的js修改后,csschunkhash也会跟随着改动。因此我们需要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.jsexport 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面试题


「一劳永逸」由浅入深配置webpack4


「一劳永逸」由浅入深配置webpack4
在看点这里