likes
comments
collection
share

Webpack浅应用

作者站长头像
站长
· 阅读数 21

Webpack浅应用


Webpack安装

Webpack的安装目前分为两个:webpackwebpack-cli

webpack和webpack-cli之间的关系

  • 执行Webpack命令,会执行node_modules下的.bin目录下的Webpack
  • Webpack在执行时是依赖Webpack-cli的,如果没有安装就会报错
  • Webpack-cli中代码执行时,才是真正利用Webpack进行编译和打包的过程
  • 在安装Webpack时,我们需要同时安装Webpack-cli(第三方的脚手架事实上是没有使用Webpack-cli的,而是类似于vue-service-cli
npm install webpack webpack-cli –D

yarn add webpack webpack-cli –D

Webpack默认打包

在根目录下创建一个webpack.config.js文件,来作为webpack的配置文件

const path = require('path')

// 导出配置信息
module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, './dist')
    }
}

指定配置文件

Webpack默认配置文件名称为webpack.config.js,支持修改

  • 我们可以通过 --config 来指定对应的配置文件
webpack --config custom.config.js
  • 也可以在package.json中增加一个新的脚本
{
    "scripts": {
        "build": "webpack --config custom.config.js"
    }
}

工作模式

Mode配置选项,可以告知Webpack使用响应模式的内置优化:

  • 默认值是production
  • 可选值有:'none' | 'development' | 'production' Webpack浅应用

Loader和Plugin

通过上述配置我们已经可以实现打包了,但是 Webpack 默认支持处理 js 文件,其他类型都处理不了,必须借助 Loader 来对不同类型的文件的进行处理

Loader

Loader从字面的意思理解,是加载的意思。 由于Webpack 本身只能打包commonjs规范的js文件,所以,针对css、图片等格式的文件没法打包,就需要引入第三方的模块进行打包。

配置方式

在webpack.config.js中写明配置信息

  • module.rules中允许我们配置多个loader(因为我们也会继续使用其他的loader,来完成其他文件的加载)
  • 这种方式可以更好的表示loader的配置,也方便后期的维护,同时也让你对各个loader有一个全局的概览

module.rules的配置如下:

  • rules属性对应的值是一个数组:[Rule]
  • 数组中存放的是一个个的RuleRule是一个对象,对象中可以设置多个属性

Rule属性说明:

  • test属性:用于对 resource(资源)进行匹配的,通常会设置成正则表达式
  • use属性:对应的值是一个数组:[UseEntry] UseEntry是一个对象,有以下三个属性
    1. loader:必须有一个 loader属性,对应的值是一个字符串;
    2. options:可选的属性,值是一个字符串或者对象,值会被传入到loader
    3. query:目前已经使用options来替代
  • loader属性:Rule.use: [ { loader } ] 的简写

Plugin

Webpack运行的生命周期中会广播出许多事件,Plugin可以监听这些事件,在合适的时机通过Webpack提供的API来改变构建结果或做你想要的事情,作⽤于整个构建过程

Loader和Plugin的区别

Loader本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。 因为Webpack只认识JavaScript,所以Loader就成了翻译官,对其他类型的资源进行转译的预处理工作

Loadermodule.rules中配置,作为模块的解析规则,类型为数组。每一项都是一个Object,内部包含了test(类型文件)、loaderoptions (参数)等属性

Plugin是一个扩展器,它丰富了Wepack本身,针对是Loader结束后,Webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听Webpack打包过程中的某些节点,执行广泛的任务

Pluginplugins中单独配置,类型为数组,每一项是一个Plugin的实例,参数都通过构造函数传入

Webpack基础配置

样式配置

css-loader
示例

通过JavaScript创建一个元素,并给它添加样式,运行打包命令发现报错 Webpack浅应用 Webpack浅应用 Webpack浅应用

对于加载css文件我们需要一个可以读取css文件的loadercss-loader

css-loader配置
npm install css-loader -D
const path = require('path')

// 导出配置信息
module.exports = {
    mode: 'development',
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, './dist')
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                // loader: 'css-loader' // 写法一
                // use: ['css-loader'] // 写法二
                use: [
                    { loader: 'css-loader' }
                ]
            }
        ]
    }
}
style-loader配置

css-loader配置完成后,页面还是没有效果,这是为什么?

  • 因为css-loader只是负责将.css文件进行解析,并不会将解析之后的css插入到页面中
  • 如果我们希望再完成插入style的操作,那么我们还需要另外一个loader,就是style-loader
npm install style-loader -D

注意:因为loader的执行顺序是从右向左(或者说从下到上,或者说从后到前的),所以我们需要将styleloader写到css-loader的前面

// 注意:style-loader在css-loader前面
use: [
    { loader: 'style-loader' },
    { loader: 'css-loader' }
]

此时执行npm run build,可以发现打包后的css已经生效了

less-loader

在开发中,我们可能会使用lesssassstylus的预处理器来编写css样式,如何处理这些样式呢

less样式:

@fontSize: 30px;
@fontWeight: 700;

.content {
    font-size: @fontSize;
    font-weight: @fontWeight;
}
npm install less-loader -D
{
    test: /\.less&/,
    use: [
        { loader: 'style-loader' },
        { loader: 'css-loader' },
        { loader: 'less-loader' }
    ]
}

执行npm run buildless就可以自动转换成css,并且页面生效

兼容性配置

Browserslist

在不同的前端工具之间,共享目标浏览器和Node.js版本的配置(如:babel、autoprefixer等共享兼容性配置)

在很多的脚手架配置中,都能看到类似于这样的配置信息

// 这里的百分之一,就是指市场占有率
> 1%
last 2 versions
not dead

上述配置信息其实就是Browserslist的配置信息

Browserslist编写规则

加粗部分是比较常用的

Webpack浅应用 Webpack浅应用

Browserslist配置
// package.json配置
"browserslist": [
    "last 2 versions",
    "not dead",
    "> 0.2%"
]
// 新建.browserslistrc文件
> 0.5%
last 2 versions
not dead
默认配置和条件关系
// 如果没有配置,那么也会有一个默认配置
browserslist.defualts = [
    '> 0.5%',
    'last 2 versions',
    'Firefox ESR',
    'not dead'
]

编写多个条件,多个条件之间的关系是什么? Webpack浅应用

PostCSS

PostCSS是一个通过JavaScript来转换样式的工具,这个工具可以帮助我们进行一些CSS的转换和适配,比如自动添加浏览器前缀、css样式的重置

postcss-loader
npm install postcss-loader -D
npm install autoprefixer -D

注意:postcss需要有对应的插件才会起效果,所以我们需要配置它的plugin

// 配置方式一,写在webpack.config.js
{
    loader: 'postcss-loader',
    options: {
        postcssOptions: {
            plugins: [
                require('autoprefixer')
            ]
        }
    }
}
// 配置方式二,根目录新建postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}

Webpack浅应用 Webpack浅应用

postcss-preset-env

事实上,在配置postcss-loader时,我们配置插件并不需要使用autoprefixer。我们可以使用另外一个插件:postcss-preset-env

  • postcss-preset-env也是一个postcss的插件
  • 它可以帮助我们将一些现代的css特性,转成大多数浏览器认识的css,并且会根据目标浏览器或者运行时环境添加所需的polyfill
  • 包括会自动帮助我们添加autoprefixer(所以相当于已经内置了autoprefixer
npm install postcss-preset-env -D
module.exports = {
    plugins: [
        require('postcss-preset-env')
    ]
}
Babel

Babel是一个工具链,主要用于旧浏览器或者环境中将ECMAScript 2015+代码转换为向后兼容版本的JavaScript

命令行使用

Babel本身可以作为一个独立的工具(和postcss一样),不和webpack等构建工具配合使用

npm install @babel/cli @babel/core
// src:是源文件的目录
// --out-dir:指定要输出的文件夹dist
npx babel src --out-dir dist
插件使用

箭头函数转换

npm install @babel/plugin-transform-arrow-functions -D
npx babel src --out-dir dist --plugins=@babel/plugin-transform-arrow-functions

const转换

npm install @babel/plugin-transform-block-scoping -D
npx babel src --out-dir dist --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions
Webpack中配置Babel

babel-loader

npm install babel-loader @babel/core
// 配置
module.exports = {
    module: {
        rules: [
            {
                test: /\.m?js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        plugins: [
                            '@babel/plugin-transform-block-scoping',
                            '@babel/plugin-transform-arrow-functions'
                        ]
                    }
                }
            }
        ]
    }
}

预设preset

如果要转换的内容过多,一个个设置是比较麻烦的,我们可以使用预设(preset)

比如常见的预设有三个:

  • env
  • react
  • TypeScript
npm install @babel/preset-env
module.exports = {
    module: {
        rules: [
            {
                test: /\.m?js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            ['@babel/preset-env']
                        ]
                    }
                }
            }
        ]
    }
}
Stage-X

TC39

  • TC39是指技术委员会(Technical Committee)第39
  • 它是 ECMA 的一部分,ECMA是 “ECMAScript” 规范下的JavaScript语言标准化的机构
  • ECMAScript规范定义了JavaScript如何一步一步的进化、发展

TC39 遵循的原则是:分阶段加入不同的语言特性,新流程涉及四个不同的 Stage

  • Stage 0strawman(稻草人),任何尚未提交作为正式提案的讨论、想法变更或者补充都被认为是第 0 阶段的"稻草人"
  • Stage 1proposal(提议),提案已经被正式化,并期望解决此问题,还需要观察与其他提案的相互影响
  • Stage 2draft(草稿),Stage 2的提案应提供规范初稿、草稿。此时,语言的实现者开始观察 runtime 的具体实现是否合理
  • Stage 3candidate(候补),Stage 3 提案是建议的候选提案。在这个高级阶段,规范的编辑人员和评审人员必须在最终规范上签字。Stage 3的提案不会有太大的改变,在对外发布之前只是修正一些问题
  • Stage 4finished(完成),进入 Stage 4 的提案将包含在ECMAScript的下一个修订版中

Babel的Stage-X设置babel7之前(比如babel6中),我们会经常看到这种设置方式:

module.exports = {
    "presets": ["stage-0"]
}
  • 它表达的含义是使用对应的babel-preset-stage-x预设
  • 但是从babel7开始,已经不建议使用了,建议使用preset-env来设置
Babel配置文件

我们可以将babel的配置信息放到一个独立的文件中,babel给我们提供了两种配置文件的编写:

  • babel.config.json(或者.js.cjs.mjs)文件
  • .babelrc.json(或者.babelrc.js.cjs.mjs)文件

它们有什么区别呢?目前很多的项目都采用了多包管理的方式(babelelement-plus等):

  • .babelrc.json:早期使用较多的配置方式,但是对于配置Monorepos项目是比较麻烦的
  • babel.config.json(babel7):可以直接作用于Monorepos项目的子包,更加推荐
polyfill

就像是一个补丁,可以帮助我们更好的使用JavaScript

什么时候使用polyfill

比如我们使用了一些语法特性(例如:Promise, Generator,Symbol等以及实例方法例如=Array.prototype.includes等),但是某些浏览器压根不认识这些特性,必然会报错,我们可以使用polyfill来填充或者说打一个补丁,那么就会包含该特性了

为什么有了Babel还要polyfill

真实的情况是babel只是提供了一个“平台”,让更多有能力的plugins入驻平台,是这些plugins提供了将ECMAScript 2015+版本的代码转换为向后兼容的JavaScript语法的能力

babelECMAScript 2015+版本的代码分为了两种情况处理:

  • 语法层: letconstclass、箭头函数等,这些需要在构建时进行转译,是指在语法层面上的转译。这种使用preset-env
  • api方法层:Promiseincludesmap等,这些是在全局或者ObjectArray等的原型上新增的方法,它们可以由相应es5的方式重新定义。这种使用polyfill

如何使用polyfill

  • babel7.4.0之前,可以使用 @babel/polyfill的包,但是该包现在已经不推荐使用了
  • babel7.4.0之后,可以通过单独引入core-jsregenerator-runtime来完成polyfill的使用
npm install core-js regenerator-runtime --save
module.exports = {
    module: {
        rules: [
            {
                test: /\.m?js$/,
                exclude: /node_modules/,
                use: 'babel-loader'
            }
        ]
    }
}
配置babel.config.js

我们需要在babel.config.js文件中进行配置,给preset-env配置一些属性:

  • useBuiltIns:设置以什么样的方式来使用polyfill
  • corejs:设置corejs的版本,目前使用较多的是3.x的版本;另外corejs可以设置是否对提议阶段的特性进行支持,设置proposals属性为true即可;

useBuiltIns

useBuiltIns属性有三个常见的值:false、usage、entry

  • false
    1. 打包后的文件不使用polyfill来进行适配
    2. 这个时候是不需要设置corejs属性的
  • usage
    1. 根据源代码中出现的语言特性,自动检测所需要的polyfill
    2. 确保最终包里的polyfill数量的最小化,打包的包相对会小一些
    3. 设置corejs属性来确定使用的corejs的版本
module.exports = {
   module: {
       rules: [
           {
               test: /\.m?js$/,
               use: {
                   loader: 'babel-loader',
                   options: {
                       presets: [
                           ['@babel/preset-env', {
                               useBuiltIns: 'usage',
                               corejs: 3.8
                           }]
                       ]
                   }
               }
           }
       ]
   }
}
  • entry
    1. 如果我们依赖的某一个库本身使用了某些polyfill的特性,但是因为我们使用的是usage,所以之后用户浏览器可能会报错
    2. 如果担心出现这种情况,我们可以使用entry
    3. 在入口文件中添加 import 'core-js/stable'; import 'regenerator-runtime/runtime',这样做会根据browserslist目标导入所有的polyfill,但是对应的包会变大
// 在入口文件添加
import 'core-js/stable'
import 'regenerator-runtime/runtime

module.exports = {
    module: {
        rules: [
            {
                test: /\.m?js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            ['@babel/preset-env', {
                                useBuiltIns: 'entry',
                                corejs: 3.8
                            }]
                        ]
                    }
                }
            }
        ]
    }
}
TypeScript编译

通过TypeScript的compiler来转换成JavaScript

npm install typescript -D
// 生成tsconfig.json
tsc --init
// 编译ts代码
npx tsc

Webpack中使用TypeScript

使用ts-loader或babel-loader来处理ts文件

ts-loader

npm install ts-loader -D
module.exports = {
    module: {
        rules: [
            {
                test: /\.ts$/,
                exclude: /node_modules/,
                use: ['ts-loader']
            }
        ]
    }
}

babel-loader

npm install @babel/preset-typescript -D
module.exports = {
    module: {
        rules: [
            {
                test: /\.m?js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            ['@babel/preset-typescript']
                        ]
                    }
                }
            }
        ]
    }
}

ts-loader和babel-loader选择

  • ts-loader(TypeScript Compiler)

    1. 直接编译TypeScript,那么只能将ts转换成js
    2. 我们还希望在这个过程中添加对应的polyfill,那么ts-loader是无能为力的
    3. 需要借助于babel来完成polyfill的填充功能
  • babel-loader(Babel)

    1. 来直接编译TypeScript,也可以将ts转换成js,并且可以实现polyfill的功能
    2. babel-loader在编译的过程中,不会对类型错误进行检测

综合考虑性能和扩展性,目前比较推荐的是babel + fork-ts-checker-webpack-plugin 方案

  module: {
    rules: [
      {
        test: /\.(t|j)s$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
          },
        ],
      },
    ],
  },
  plugins: [
    // fork-ts-checker-webpack-plugin,创建一个新进程,专门来运行Typescript类型检查。这么做的原因是为了利用多核资源来提升编译的速度
    new ForkTsCheckerWebpackPlugin(),
  ],

加载和处理其他资源

file-loader

file-loader的作用就是帮助我们处理import/require()方式引入的一个文件资源,并且会将它放到我们输出的文件夹中

file-loader使用
npm install file-loader -D
{
    test: /\.(png|jpe?g|gif|svg)$/i,
    use: {
        loader: 'file-loader'
    }
}
文件的名称规则

有时候我们处理后的文件名称按照一定的规则进行显示,比如保留原来的文件名、扩展名,同时为了防止重复,包含一个hash值等,这个时候我们可以使用PlaceHolders

常用的placeholder

  • [ext]: 处理文件的扩展名
  • [name]:处理文件的名称
  • [hash]:文件的内容,使用MD4的散列函数处理,生成的一个128位的hash值(32个十六进制
  • [contentHash]:在file-loader中和[hash]结果是一致的
  • [hash:<length>]:截图hash的长度
  • [path]:文件相对于Webpack配置文件的路径
设置文件名称
{
    test: /\.(png|jpe?g|gif|svg)$/i,
    use: {
        loader: 'file-loader',
        options: {
            name: 'img/[name].[hash:8].[ext]'
        }
    }
}
设置文件存放路径
{
    test: /\.(png|jpe?g|gif|svg)$/i,
    use: {
        loader: 'file-loader',
        options: {
            name: '[name].[hash:8].[ext]',
            outputPath: 'img'
        }
    }
}
url-loader

url-loader和file-loader的工作方式是相似的,但是可以将较小的文件,转成base64的URL

npm install url-loader -D
{
    test: /\.(png|jpe?g|gif|svg)$/i,
    use: {
        loader: 'url-loader',
        options: {
            name: '[name].[hash:8].[ext]',
            outputPath: 'img'
        }
    }
}

图片可以正常展示,但是dist文件夹中会看不到图片文件,因为默认情况下url-loader会将所有的图片文件转成base64编码

limit

url-loader有一个options属性limit,可以用于设置转换的限制

下面的代码100kb以下的图片都会进行base64编码

{
    test: /\.(png|jpe?g|gif|svg)$/i,
    use: {
        loader: 'url-loader',
        options: {
            limit: 100 * 1024,
            name: '[name].[hash:8].[ext]',
            outputPath: 'img'
        }
    }
}
asset module type

在webpack5之前,加载这些资源我们需要使用一些loader,比如raw-loader 、url-loader、file-loader 在webpack5之后,我们可以直接使用资源模块类型(asset module type),来替代上面的这些loader

资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些loader

  • asset/resource 发送一个单独的文件并导出URL。之前通过使用 file-loader 实现
  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现
  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现
  • asset在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现
加载图片
{
    test: /\.(png|svg|jpg|jpeg|gif)$/i,
    type: 'asset/resource'
}
自定义文件的输出路径和文件名
方式一:修改output,添加assetModuleFilename属性
output: {
    filename: 'js/bundle.js',
    path: path.resolve(__dirname, './dist'),
    assetModuleFilename: 'img/[name].[hash:6][ext]'
}

方式二:在Rule中,添加一个generator属性,并且设置filename
{
    test: /\.(png|svg|jpg|jpeg|gif)$/i,
    type: 'asset/resource',
    generator: {
        filename: 'img/[name].[hash:6][ext]'
    }
}
url-loader的limit效果
// 将type修改为asset,添加一个parser属性,并且制定dataUrl的条件,添加maxSize属性
{
    test: /\.(png|svg|jpg|jpeg|gif)$/i,
    type: 'asset',
    generator: {
        filename: 'img/[name].[hash:6][ext]'
    },
    parser: {
        dataUrlCondition: {
            maxSize: 100 * 1024
        }
    }
}
字体文件处理
{
    test: /\.(woff2?|eot|ttf)$/,
    type: 'asset/resource',
    generator: {
        filename: 'font/[name].[hash:6][ext]'
    }
}

Plugin

Plugin可以用于执行更加广泛的任务,比如打包优化、资源管理、环境变量注入等

CleanWebpackPlugin

CleanWebpackPlugin可以帮助我们重新打包时,自动删除dist文件夹

npm install clean-webpack-plugin -D
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
    plugins: [
        new CleanWebpackPlugin()
    ]
}
HtmlWebpackPlugin

我们的HTML文件是编写在根目录下的,而最终打包的dist文件夹中是没有index.html文件的,在进行项目部署时,必然也是需要有对应的入口文件index.html,这时我们可以用到HtmlWebpackPlugin

npm install html-webpack-plugin -D
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    plugins:  [
        new HtmlWebpackPlugin({
            title: '示例'
        })
    ]
}

现在dist文件夹默认生成了一个index.html文件,这个文件是怎么生成的呢?

  • 默认情况下是根据ejs的一个模板来生成的
  • html-webpack-plugin的源码中,有一个default_index.ejs模块
自定义HTML模板

Webpack浅应用

  • template:指定我们要使用的模块所在的路径
  • title:在进行htmlWebpackPlugin.options.title读取时,就会读到该信息
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
    plugins:  [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            title: '示例',
            template: './plubic/index.html',
        })
    ]
}
DefinePlugin

按照上述步骤编译的时候还是会报错,因为在我们的模块中还使用到一个BASE_URL的常量:<link rel="icon" href="<%= BASE_URL %>favicon.ico"> Webpack浅应用

DefinePlugin允许在编译时创建配置的全局常量,是Webpack内置的插件(不需要单独安装)

const { DefinePlugin } = require('webpack')

module.exports = {
    plugins: [
        new DefinePlugin({
            BASE_URL: '"./"'
        })
    ]
}

这时候,编译template就可以正确的编译了,会读取到BASE_URL的值

Webpack搭建本地服务器

我们希望当文件发生变化时,可以自动的完成编译和展示,Webpack提供了几种可选的方式:webpack watch mode、webpack-dev-server、webpack-dev-middleware

webpack watch

在该模式下,webpack依赖图中的所有文件,只要有一个发生了更新,那么代码将被重新编译,我们不需要去手动运行npm run build指令了

如何开启watch

  1. 在导出的配置中,添加 watch: true
  2. 启动Webpack的命令中,添加--watch的标识
"scripts": {
    "watch": "webpack --watch"
}

webpack-dev-server

上面的方式可以监听到文件的变化,但是它本身没有自动刷新浏览器的功能

npm install --save-dev webpack-dev-server
// 添加一个新的script脚本
"serve": "webpack serve --config webpack.config.js"

webpack-dev-server在编译之后不会写入到任何输出文件。而是将bundle文件保留在内存中:webpack-dev-server使用了一个库叫memfsmemory-fs Webpack自己写的)

webpack-dev-middleware

webpack-dev-server已经帮助我们做好了一切,比如通过express启动一个服务,比如HMR(热模块替换),但是如果我们想要更好的自由度,可以使用webpack-dev-middleware

什么是webpack-dev-middleware
  • webpack-dev-middleware 是一个封装器(wrapper),它可以把webpack处理过的文件发送到一个server
  • webpack-dev-server在内部使用了它,然而它也可以作为一个单独的package来使用,以便根据需求进行更多自定义设置
npm install --save-dev express webpack-dev-middleware

Webpack浅应用

HMR(模块热替换)

在应用程序运行过程中,替换、添加、删除模块,无需重新刷新整个页面

如何使用HMR

默认情况下,webpack-dev-server已经支持HMR,我们只需要开启即可。在不开启HMR的情况下,当我们修改了源代码之后,整个页面会自动刷新,使用的是live reloading

devServer: {
    hot: true
}

Webpack浅应用

但是我们会发现,当我们修改了某一个模块的代码时,依然是刷新的整个页面:因为我们需要去指定哪些模块发生更新时,进行HMR

ifmodule.hot) {
    module.hot.accept('./utils.js', () => {
        console.log('HMR')
    })
}
框架的HMR

在Vue和React项目,我们不需要手动写入module.hot.accept相关API,因为社区已经有成熟的解决方案。

vue:使用vue-loader,该loader支持vue组件的HMR,提供开箱即用的体验。 reactReact Hot Loader,实时调整react组件(目前React官方已经弃用了,改成使用reactrefresh

Vue的HMR

Vue的加载我们需要使用vue-loader,而vue-loader加载的组件默认会帮助我们进行HMR的处理

npm install vue-loader vue-template-compiler -D
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
    module: {
        rules: [
            {
                test: /\.vue$/,
                use: 'vue-loader'
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin()
    ]
}

Source Map

source-map可以让我们能够调试经过打包压缩的代码

Source Map文件解析

大致文件结构:

{
  "version": 3,
  "sources": [
    "webpack://webpack5-template/./src/index.js"
  ],
  "names": [],
  "mappings": ";;;;;AAAA;AACA;AACA;AACA;AACA,eAAe;AACf;AACA;AACA,mB",
  "file": "main.bundle.js",
  "sourcesContent": [
    "console.log('Interesting!!!')"
  ],
  "sourceRoot": ""
}
  • version:当前使用的版本,也就是最新的第三版; psources:从哪些文件转换过来的source-map和打包的代码(最初始的文件);
  • names:转换前的变量和属性名称(因为我目前使用的是development模式,所以不需要保留转换前的名 称);
  • mappingssource-map用来和源文件映射的信息(比如位置信息等),一串base64 VLQveriablelength quantity可变长度值)编码;
  • file:打包后的文件(浏览器加载的文件);
  • sourceContent:转换前的具体代码信息(和sources是对应的关系);
  • sourceRoot:所有的sources相对的根目录;
Webpack配置Source Map

Webpack为我们提供了非常多的选项(目前是26个),来处理source-map devtools

简单介绍
  • source-map:最标准的source-map打包形式,生成一个独立的source-map文件,并且在bundle文件中有一个注释,指向source-map文件
  • cheap-source-map:生成sourcemap,但是会更加高效一些(cheap低开销),因为它没有生成列映射(Column Mapping
  • cheap-module-source-map:生成sourcemap,类似于cheap-source-map,但是对源自loadersourcemap处理会更好

官方测试案例

开发最佳实践
  • 开发阶段:推荐使用source-map或者cheap-module-source-map
  • 测试阶段:推荐使用source-map或者cheap-module-source-map
  • 发布阶段:false、缺省值(不写)

Webpack优化

Terser

Terser可以帮助我们压缩、丑化我们的代码,让我们的bundle变得更小

什么是Terser
  • Terser是一个JavaScript的解释(Parser)Mangler(绞肉机)/Compressor(压缩机)的工具集
  • 早期我们会使用 uglify-js来压缩、丑化我们的JavaScript代码,但是目前已经不再维护,并且不支持ES6+的语法
  • Terser是从 uglify-es fork 过来的,并且保留它原来的大部分API以及适配 uglify-esuglify-js@3

Terser是一个独立的工具,所以它可以单独安装

npm install terser -g
// 可以在命令行使用terser
teser [input files] [options]

// 例子
teser js/test.js -c -m

Compress option Mangle option

在Webpack中使用

在webpack中有一个minimizer属性,在production模式下,默认就是使用TerserPlugin来处理我们的代码的 如果我们对默认的配置不满意,也可以自己来创建TerserPlugin的实例,并且覆盖相关的配置

注意:在生产环境下打包默认会开启 js 压缩,但是当我们手动配置 optimization 选项之后,就不再默认对 js 进行压缩,需要我们手动去配置

Webpack5内置了terser-webpack-plugin插件,所以我们不需重复安装,直接引用即可

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true, // 开启最小化
    minimizer: [
      new TerserPlugin()
    ]
  },
}

CSS压缩

CSS压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等 CSS的压缩我们可以使用另外一个插件:css-minimizer-webpack-plugin css-minimizer-webpack-plugin是使用cssnano工具来优化、压缩CSS(也可以单独使用)

cssnano

npm install css-minimizer-webpack-plugin -D

optimization.minimizer中配置

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  optimization: {
    minimize: true, // 开启最小化
    minimizer: [
      new CssMinimizerPlugin({
        parallel: true
      }),
    ]
  },
}

Tree Shaking

什么是Tree Shaking
  • Tree Shaking是一个术语,在计算机中表示消除死代码(dead_code
  • 最早的想法起源于LISP,用于消除未调用的代码(纯函数无副作用,可以放心的消除,这也是为什么要求我们在进行函数式编程时,尽量使用纯函数的原因之一)
JavaScript的Tree Shaking
  • JavaScript进行Tree Shaking是源自打包工具rollup,这是因为Tree Shaking依赖于ES Module的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系)
  • Webpack2正式内置支持了ES2015模块,和检测未使用模块的能力
  • Webpack4正式扩展了这个能力,并且通过package.jsonsideEffects属性作为标记,告知Webpack在编译时,哪里文件可以安全的删除掉
  • Webpack5中,也提供了对部分CommonJSTree shaking的支持commonjs-tree-shaking
Webpack实现Tree Shaking

Webpack实现Tree Shaking采用了两种不同的方案 usedExports:通过标记某些函数是否被使用,之后通过Terser来进行优化的 sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用

usedExports
// 开发环境配置
module.exports = {
  mode: 'development',
  optimization: {
    usedExports: true,
  }
};
// 在生产环境下,`Webpack`默认会添加`Tree Shaking`的配置,因此只需写一行`mode: 'production'`即可
module.exports = {
  mode: 'production',
};
sideEffects

告知webpack compiler哪些模块时有副作用的,副作用的意思是这里面的代码有执行一些特殊的任务,不能仅仅通过export来判断这段代码的意义

sideEffects配置

package.json中设置sideEffects的值:

  • sideEffects默认为true, 告诉Webpack,所有文件都有副作用,他们不能被Tree Shaking
  • sideEffectsfalse时,告诉Webpack,没有文件是有副作用的,他们都可以Tree Shaking
  • sideEffects为一个数组时,告诉Webpack,数组中那些文件不要进行Tree Shaking,其他的可以Tree Shaking
sideEffects 对全局 CSS 的影响

对于那些直接引入到 js 文件的文件,例如全局的 css,它们并不会被转换成一个 CSS 模块

/* reset.css */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html,
body {
  background-color: #eaeaea;
}
// main.js
import "./styles/reset.css"

这些代码打包后,你就会发现样式并没有起作用,原因在于:上面我们将 sideEffects 设置为 false 后,所有的文件都会被 Tree Shaking,通过 import 这样的形式引入的 CSS 就会被当作无用代码处理掉

为了解决这个问题,可以在loader的规则配置中,添加sideEffects: true,告诉Webpack这些文件不要Tree Shaking

{
    test: /\.css/i,
    use: [
        'style-loader',
        'css-loader'
    ],
    sideEffects: true
}
CSS实现Tree Shaking

CSS的Tree Shaking需要借助于其他插件,如:PurgeCSS

安装PurgeCssWebpack插件

npm install purgecss-webpack-plugin -D
module.exports = {
    plugins: [
        new PurgecssPlugin({
            // paths: 表示要检测哪些目录下的内容需要被分析,这里我们可以使用glob
            paths: glob.sync(`${resolveApp('./src')}/**/*`, { nodir: true }),
            // 默认情况下,Purgecss会将我们的html标签的样式移除掉,如果我们希望保留,可以添加一个safelist的属性
            safelist: function() {
                return {
                    standard: ['html']
                }
            }
        })
    ]
}

HTTP压缩

HTTP压缩是一种内置在 服务器 和 客户端 之间的,以改进传输速度和带宽利用率的方式

压缩流程
  • 第一步:HTTP数据在服务器发送前就已经被压缩了;(可以在Webpack中完成)
  • 第二步:兼容的浏览器在向服务器发送请求时,会告知服务器自己支持哪些压缩格式 Webpack浅应用
  • 第三步:服务器在浏览器支持的压缩格式下,直接返回对应的压缩后的文件,并且在响应头中告知浏览器 Webpack浅应用
目前的压缩格式
  • compressUNIX“compress”程序的方法(历史性原因,不推荐大多数应用使用,应该使用gzipdeflate
  • deflate – 基于deflate算法(定义于RFC 1951RFC 1951)的压缩,使用zlib数据格式封装
  • gzipGNU zip格式(定义于RFC 1952),是目前使用比较广泛的压缩算法
  • br – 一种新的开源压缩算法,专为HTTP内容的编码而设计
Webpack对文件压缩

webpack中相当于是实现了HTTP压缩的第一步操作,我们可以使用CompressionPlugin

CompressionPlugin
npm install compression-webpack-plugin -D
module.exports = {
    plugins: [
        new CompressionPlugin({
            test: /\.(css|js)$/, // 匹配哪些文件需要压缩
            threshold: 500, // 设置文件从多大开始压缩
            minRatio: 0.7, // 至少压缩的比例
            algorithm: 'gzip', // 采用的压缩算法
        }),
    ]
}

Scope Hoisting

Scope Hoisting从webpack3开始增加的一个新功能 功能是对作用域进行提升,并且让webpack打包后的代码更小、运行更快

默认情况下webpack打包会有很多的函数作用域,包括一些(比如最外层的)IIFE,无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数,Scope Hoisting可以将函数合并到一个模块中来运行

使用Scope Hoisting非常的简单,Webpack已经内置了对应的模块:

  • production模式下,默认这个模块就会启用
  • development模式下,我们需要自己来打开该模块
new webpack.optimize.ModuleConcatenationPlugin()

DLL

DLL全称是动态链接库(Dynamic Link Library),是为软件在Windows中实现共享函数库的一种实现方式 Webpack中也有内置DLL的功能,它指的是可以将可以共享,并且不经常改变的代码,抽取成一个共享的库 这个库在之后编译的过程中,会被引入到其他项目的代码中,减少的打包的时间

Webpack DLL

webpack内置DllPlugin帮助生成DLL文件

DLL 打包
module.exports = {
    mode: 'production',
    entry: {
        react: ['react', 'react-dom']
    },
    output: {
        path: path.resovle(__dirname, './dll'),
        filename: 'dll_[name].js',
        library: 'dll_[name]'
    },
    plugins: [
        new webpack.DllPlugin({
            name: 'dll_[name]',
            path: path.resolve(__dirname, './dll/[name].manifest.json')
        })
    ]
}
DLL使用

如果我们在我们的代码中使用了react、react-dom,我们有配置splitChunks的情况下,他们会进行分包,打包到一个独立的chunk中,但是我们现在有了dll_react,不再需要单独去打包它们,可以直接去引用dll_react即可

// 通过DllReferencePlugin插件告知要使用的DLL库
new DllReferencePlugin({
    context:path.resolve(__dirname, "../"),
    manifest:path.resolve(__dirname,"../dll/react.manifest.json")
}),

// 通过AddAssetHtmlPlugin插件,将我们打包的DLL库引入到Html模块中
new AddAssetHtmlWebpackPlugin({
    outputPath:"../build/js",
    filepath:path.resolve(__dirname, "../dll/dll_react.js")
})

代码分离

认识代码分离
  • 它主要的目的是将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件
  • 默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度
  • 代码分离可以分出更小的bundle,以及控制资源加载优先级,提供代码的加载性能

常用的代码分离有三种:

  1. 入口起点:使用entry配置手动分离代码
  2. 防止重复:使用Entry Dependencies或者SplitChunksPlugin去重和分离代码
  3. 动态导入:通过模块的内联函数调用来分离代码
入口起点

配置多入口

entry: {
    index: './src/index.js',
    main: './src/main.js'
}
Entry Dependencies(入口依赖)

假如我们的index.jsmain.js都依赖两个库:lodashdayjs: 如果我们单纯的进行入口分离,那么打包后的两个bunlde都有会有一份lodashdayjs,事实上我们可以对他们进行共享

entry: {
    index: { import: './src/index.js', dependOn: 'shared' },
    main: { import: './scr/main.js', dependOn: 'shared' },
    shared: ['lodash', 'axios']
},
output: {
    filename: '[name].bundle.js',
    path: resolveApp('./build'),
    publicPath: ''
}
SplitChunks

使用SplitChunksPlugin来实现,Webpack已经默认安装和集成

Webpack提供了SplitChunksPlugin默认的配置,我们也可以手动来修改它的配置:

// 默认配置中,chunks仅仅针对于异步(async)请求,我们可以设置为initial或者all
optimization: {
    splitChunks: {
        chunks: 'all'
    }
}

关键属性介绍:

参数说明类型可选值默认值
Chunksinitial:对同步的代码进行处理,all:同步异步代码都处理stringall / initial / asyncasync
minSize拆分包的大小,至少为minSizenumber--
maxSize将大于maxSize的包,拆分为不小于minSize的包number--
minChunks至少被引入的次数number-1
name设置拆包的名称, 设置为false后,需要在cacheGroups中设置名称string / false--
cacheGroups用于对拆分的包就行分组,比如一个lodash在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打包object--
动态导入(dynamic import)

Webpack提供了两种实现动态导入的方式:

  1. 使用ECMAScript中的 import() 语法来完成,也是目前推荐的方式
  2. 使用Webpack遗留的 require.ensure,目前已经不推荐使用

比如我们有一个模块bar.js

该模块我们希望在代码运行过程中来加载它(比如判断一个条件成立时加载),因为我们并不确定这个模块中的代码一定会用到,所以最好拆分成一个独立的js文件,这样可以保证不用到该内容时,浏览器不需要加载和处理该文件的js代码,这个时候我们就可以使用动态导入

动态导入的文件命名

因为动态导入通常是一定会打包成独立的文件的,所以并不会在cacheGroups中进行配置,那么它的命名我们通常会在output中,通过 chunkFilename属性来命名

output: {
    filename: '[name].bundle.js',
    path: resolveApp('./build'),
    chunkFilename: 'chunk_[id]_[name].js'
}

但是默认情况下我们获取到的[name]是和id的名称保持一致的:

// 通过magic comments(魔法注释)修改name
import(/* webpackChunkName: 'bar' */"./bar").then({ default: bar }) => {
    bar()
}

Webpack5新特性

大致方向如下:

  • 尝试用持久性缓存来提高构建性能。
  • 尝试用更好的算法和默认值来改进长期缓存。
  • 尝试用更好的Tree Shaking 和代码生成来改善包大小。
  • 尝试改善与网络平台的兼容性。
  • 尝试在不引入任何破坏性变化的情况下,清理那些在实现 v4 功能时处于奇怪状态的内部结构。
  • 试图通过现在引入突破性的变化来为未来的功能做准备,尽可能长时间地保持在v5版本上。

支持崭新的 Web 平台功能

JSON 模块

JSON模块,会与现在的提案保持一致,并且要求进行默认的导出,否则会有警告信息。即使使用默认导出,未使用的属性也会被optimization.usedExports 优化丢弃,属性会被 optimization.mangleExports 优化打乱。 如果想用自定义的JSON 解析器,可以在Rule.parser.parse 中指定一个自定义的JSON 解析器来导入类似JSON 的文件(例如针对 tomlyamljson5 等)

内置静态资源构建能力(Asset Modules)

Webpack5 提供了内置的静态资源构建能力,我们不需要安装额外的loader,仅需要简单的配置就能实现静态资源的打包和分目录存放

原生 Worker 支持

当把资源的new URLnew Worker/new SharedWorker/navigator.serviceWorker.register结合起来时,webpack会自动为web worker 创建一个新的入口点(entrypoint)。new Worker(new URL("./worker.js", import.meta.url))选择这种语法也是为了允许在没有打包工具的情况下运行代码。这种语法在浏览器的原生 ECMAScript 模块中也可以使用

内置FileSystem Cache能力加速二次构建

Webpack5之前,我们会使用cache-loader 缓存一些性能开销较大的 loader ,或者是使用hard-source-webpack-plugin 为模块提供一些中间缓存。在Webpack5之后,默认就为我们集成了一种自带的缓存能力(对 modulechunks 进行缓存)。通过如下配置,即可在二次构建时提速

module.exports = {
    cache: {
        type: 'filesystem',
        // 可选配置
        buildDependencies: {
            config: [__filename],  // 当构建依赖的config文件(通过 require 依赖)内容发生变化时,缓存失效
        },
        name: '',  // 配置以name为隔离,创建不同的缓存文件,如生成PC或mobile不同的配置缓存
    },
}

生产环境下默认的缓存存放目录在 node_modules/.cache/webpack/default-production 中,如果想要修改,可通过配置name,来实现分类存放。

深度Tree Shaking能力支持

Webpack5能够支持深层嵌套的export 的Tree Shaking 官网案例

Webpack浅应用

资源打包策略更优

Prepack 是 Facebook 开源的一个 JavaScript 代码优化工具,运行在 “编译” 阶段,生成优化后的代码。下面是 Prepack 官网上的一个示例,我们可以看到,在对于任何输入,函数都能得到一个固定输出的时候,Prepack 就能在编译时,将结果帮我们计算出来。对于一些复杂且固定的计算逻辑而言,这种“预计算”能力,既能减小我们包的体积,又能加快运行时的速度。 官方案例

Webpack浅应用

Webpack5内置了Prepack的部分能力,能够在极致之上,再度优化你的项目产物体积

Top Level Await

Webpack5还支持Top Level Await。即允许开发者在async函数外部使用await 字段。它就像巨大的async函数,原因是import它们的模块会等待它们开始执行它的代码,因此,这种省略async的方式只有在顶层才能使用

module.exports = {
    experiments: {
        topLevelAwait: true,
    },
}

为了eslint语法检测的支持,我们还需要添加babel插件@babel/plugin-syntax-top-level-await来让我们的babel能够识别top level await语法