Webpack5基础

663 阅读20分钟

前言

前端开发时一般都会使用框架(React、Vue)、ES6模块化语法、Less或者Sass等 css预处现器等语法。这样的代码要想在浏览器运行,必须要编译成浏览器能够识别的 JS、Css等语法。而打包工具的作用就是压缩代码、兼容性处理、提升代码性能、代码编译等。

常见的打包工具包括Grunt、Gulp、Parcel、Webpack、Rollup、Vite等等,目前市面上最常用的是webpack。

第一章 Webpack的基本配置

1. 基本使用

Webpack 是一个静态资源打包工具。它会以一个或多个文件作为打包的入口,将我们整个项目所有文件编译组合成一个或多个文件输出。输出的文件就是编译好的文件bundle,可以在浏览器端运行。

Webpack本身的功能是有限的:

  • 开发模式:只能编译JS中的ES Module语法
  • 生产模式:不仅可以编译JS中的ES Module语法,还可以压缩JS代码
  1. 直接使用JS代码

index.html主文件 image.png

未打包的main.js文件 image.png

控制台报错 image.png

  1. 使用打包后的JS代码

第一步:初始化一个package.json文件npm init -y

image.png

第二步:下载webpack npm i webpack webpack-cli -D

第三步:打包指定目录文件npx webpack ./src/main.js --mode=development

development模式 image.png

production模式 image.png

控制台正常输出 image.png

2. 核心概念

  1. Entry:入口文件,webpack编译的起点,即从哪个文件开始打包
  2. output:输出,webpack打包完的文件从哪里输出。其中output.filename对应initial chunk文件名称,output.chunkFilename对应non-initial chunk文件名称
  3. Loader:加载器,webpack本身只能处理JS、json资源文件,其他资源需要借助loader处理
  4. Plugin:插件,扩展webpack功能
  5. mode:模式,分为development开发模式和production生产模式

3. 其他概念

  1. Compiler:编译管理器,webpack启动后会创建compiler对象,该对象一直存活到编译结束
  2. Compilation:单次编译过程的管理器,每次触发重新编译时,都会创建一个新的compilation对象
  3. Dependence:依赖对象,webpack基于该类型记录模块间依赖关系
  4. Module:webpack内部所有资源都会以“module”对象形式存在,所有关于资源的操作、转译、合并都是以 “module” 为基本单位进行的
  5. Chunk:编译完成准备输出时,webpack会将module按特定的规则组织成一个一个的chunk,这些chunk某种程度上跟最终输出一一对应

4. Webpack基本配置

在根目录下创建一个Webpack.config.js文件并完成基础配置

const path = require("path");

module.exports = {
    // 入口,相对路径
    entry: "./src/main.js",
    // 输出,绝对路径
    output: {
        path: path.resolve(__dirname,"dist"), // 路径
        filename: "main.js",// 文件名
    },
    // 加载器
    module: {rules: []},
    // 插件
    plugins:[],
    // 模式
    mode: "development"
}

可以在output中添加clean配置,自动清除上一次的打包资源。

output: {
    path: path.resolve(__dirname,"dist"), // 路径
    filename: "main.js",// 文件名
    clean: true, // 在生成文件之前清空output目录
},

5. create-react-app中webpack的默认配置

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

module.exports = {
    optimization:{
        splitChunks:{ // 拆分模块
            chunks: 'all'
        },
        minimizer:[
            new TerserPlugin({
                terserOptions:{
                    compress:{
                        pure_funcs:['console.log'], // 移除console.log代码
                    }
                }
            })
        ]
    },
    mode: 'development', // 开发模式
    entry: { // 多入口打包
        case:{
            import:'./src/case.tsx',
            dependOn:'vendor' // 共享模块的chunk名称
        },
        list:{
            import:'./src/list.tsx',
            dependOn:'vendor'
        }
    },
    devServer:{ 
        hot:true // HMR
    },
    output: {
        clean: true, // 自动清除上次打包内容
        path: path.join(__dirname, 'dist'),
        filename: '[name]-[contenthash].js',
    },
    resolve: {
        extensions: ['.tsx', '.ts']
    },
    plugins:[
        new HtmlWebpackPlugin({
            title:'管理输出', // html文件标题
            filename: 'index.html', // html文件名字
        }),
    ],
    module: {
        rules: [
            {
                test: /\.(js|ts|jsx|tsx)$/, // 解析.tsx文件
                include: path.appSrc, // 只包含app/src/文件夹下面的文件
                use: [{
                    loader: "esbuild-loader",
                    options: {
                        loader: "tsx",
                        target: "es2015"
                    },
                }]
            },
            {
                test: /\.css$/,
                use: ['style-loader',{
                    loader:'css-loader',
                    options:{
                        sourceMap:true
                    }
                }]
            },
            {
                test: /\.html$/,
                use: 'html-loader'
            }
        ],
    },
};

第二章 资源文件的处理

1. 处理样式资源

1.1 style-loader

作用:把CSS插入到DOM中,推荐将style-loadercss-loader一起使用

// 下载
npm install --save-dev style-loader

// 使用加载器
module: {
    rules: [
        {
            test: /\.css$/i, // 正则表达式匹配文件
            use: ["style-loader", "css-loader"], // 从右到左依次使用loader处理
        }
    ]
},

1.2 css-loader

css-loader会对@importurl()进行处理,就像js解析import/require()一样。

// 下载
npm install --save-dev css-loader

// 使用加载器
module: {
    rules: [
        {
            test: /\.css$/i, // 正则表达式匹配文件
            use: ["style-loader", "css-loader"], // 从右到左依次使用loader处理
        }
    ]
},

image.png

1.3 less-loader

作用:将Less编译为CSS的loader

// 下载 
npm install less less-loader --save-dev

// 使用加载器
module: {
    rules: [
        {
            test: /\.less$/i,
            use: ['style-loader','css-loader','less-loader'],
        },
    ]
},

image.png

1.4 sass-loader

作用:加载Sass/SCSS文件并将他们编译为CSS。

// 下载 
npm install sass-loader sass --save-dev

// 使用加载器
module: {
    rules: [
        {
            test: /\.s[ac]ss$/i,
            use: [
                // 将JS字符串生成为style节点
                'style-loader',
                // 将CSS转化成CommonJS模块
                'css-loader',
                // 将Sass编译成CSS
                'sass-loader',
            ],
        },
    ]
},

image.png

1.5 stylus-loader

作用:将Stylus文件编译为CSS

// 下载 
npm install stylus stylus-loader --save-dev

// 使用加载器
module: {
    rules: [
        {
            test: /.styl$/,
            loader: "stylus-loader",
        },
    ]
},

2. 处理图片资源

2.1 简单配置

Webpack4处理图片资源时需要使用file-loader和url-loader两个加载器,而Webpack5已经内置了两个Loader,使用时不需要单独下载安装,只需要简单配置即可。

module: {
    rules: [
        {
            test: /\.(png|jpe?g|gif|webp)$/i,
            type: "asset",
        }
    ]
},

图片作为背景图片,通过url引入 image.png

image.png

2.2 资源模块

资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外loader。包括以下内容:

  • raw-loader:将文件导入为字符串
  • url-loader:将文件作为data URI内联到bundle中
  • file-loader:将文件发送到输出目录

一般情况下webpack将按照默认条件,自动地在resource和inline之间进行选择:小于8kb的文件,将会视为inline模块类型,否则会被视为resource模块类型。可以通过在webpack配置的module rule层级中,设置Rule.parser.dataUrlCondition.maxSize选项来修改此条件。

  • resource资源:直接发送到输出目录,其路径会被被注入到bundle中
  • inline资源:文件会作为data URI注入到bundle中,格式为base64,可以减少网络请求
module: {
    rules: [
        {
            test: /\.(png|jpe?g|gif|webp)$/i,
            type: "asset",
            parser: {
                dataUrlCondition: {
                    maxSize: 100 * 1024 // 小于100kb转化为inline资源
                }
            }
        }
    ]
},

image.png

🤔:url-loader和file-loader的区别是什么?

🙋:大致总结如下

首先概念不同:file-loader可以指定要复制和放置资源文件的位置,以及如何使用版本哈希命名以获得更好的缓存。url-loader允许你有条件地将文件转换为内联的base-64 URL (当文件小于给定的阈值),这会减少小文件的HTTP请求数。如果文件大于该阈值,会自动的交给file-loader处理。

处理图片资源方式不同:file-loader将文件上的importrequire解析为url,并将该文件发射到输出目录中。url-loader可以识别图片的大小,并把图片转换成base64,从而减少代码的体积,如果图片超过设定的限制,就会继续用file-loader处理。

2.3 自定义输出文件

默认情况下,asset/resource模块以[hash][ext][query]文件名发送到输出目录中,可以通过在webpack配置中设置output.assetModuleFilename来修改此模板字符串。

特点:不能对asset/resource模块下的内容进行区分

output: {
    // 所有输出文件的路径
    path: path.resolve(__dirname,"dist"),
    // 入口文件对应的输出文件名称
    filename: "main.js",
    // asset/resource模块的输出路径和名称配置
    assetModuleFilename: 'images/[hash][ext][query]'
},

也可以通过generator.filename单独设置某个resource模块,输出到指定目录下。

{
    test: /\.(png|jpe?g|gif|webp)$/i,
    type: "asset",
    parser: {
        dataUrlCondition: {
            maxSize: 100 * 1024 // 100kb
        }
    },
    // 将图片资源输出到dist/static/images目录中
    // 文件名为前8位hash值 + 文件扩展名 + 其他扩展字段
    generator: {
        filename: 'static/images/[hash:8][ext][query]'
    }
}

image.png

3. 处理字体图标资源

字体图标资源也属于资源模块,但是不需要转化为base-64格式,所以需要使用Resource。

{
    test: /\.(ttf|Woff2?)$/i,
    type:"asset/resource",
    generator: {
        filename: 'static/media/[hash:8][ext][query]'
    }
}

4. 处理其他资源

例如音频、视频等标资源也属于资源模块,同样也不需要转化为base-64格式,所以需要使用Resource。

{
    test: /\.(map3|map4|avi)$/i,
    type:"asset/resource",
    generator: {
        filename: 'static/media/[hash:8][ext][query]'
    }
}

5. 处理JS资源

Webpack对JS的处理是有限的,只能编译JS中ES模块化语法,不能编译其他语法,导致JS不能在IE等浏览器中运行,所以需要做一些兼容性处理。

  • Babel:JS兼容性处理
  • Eslint:代码格式校验

需要先完成Eslint检测代码格式无误后,再由Babel做代码兼容性处理。

5.1 Eslint

作用:用来检测js和jsx语法的工具,可以扩展各种功能

  1. 配置文件
  • .eslintrc.*:新建位于项目根目录的文件,可以是.js或者.json格式
  • package.json中eslintConfig:直接在package文件中添加配置,Eslint会自动查找和读取对应配置规则
  1. 使用
// 下载
npm install eslint-webpack-plugin eslint --save-dev
// 添加配置文件.eslintrc.js
module.exports = {
    // 继承eslint规则
    extends:["eslint:recommended"],
    env:{
        node:true, // 启用node中的全局变量
        browser:true, // 启用浏览器中的全局变量
    },
    parserOptions:{
        ecmaVersion: 6,
        sourceType: "module"
    },
    rules:{
        "no-var": 2,
    }
}
// 添加eslint忽略文件.eslintignore,忽略打包后的dist文件夹
dist

image.png

5.2 babel

作用:将ES6语法转换为向后兼容的JS代码,以便能够运行在当前和旧版本的浏览器中

  1. 配置文件
  • babel.config.*:新建位于根目录的文件,格式为.js或者.json
  • .babelrc.*:新建位于根目录的文件,格式为.js或者.json
  • package.json的babel:直接在package文件中添加配置
  1. 使用
// 下载
npm install -D babel-loader @babel/core @babel/preset-env

//  使用加载器
{
    test: /\.m?js$/,
    exclude: /(node_modules)/, // 忽略node包
    loader: 'babel-loader',
}
// 添加外部的babel.config.js文件,配置预设规则
module.exports = {
    presets: ['@babel/preset-env'] // 智能预设
}
  1. 处理JS语法

@babel-loader

特点:只能转换基本的语法,如let、const、箭头函数等。对于一些高级语法如 Promise、async、数组新API等,不能进行转换

module.export = {
    module: {
        rules: [
            {
                test: /\.js$/, // js文件
                exclude: /node_modules/, // 排除node包
                loader: 'babel-loader',
                options: {
                    preset: ['@babel/preset-env'] // 智能预设
                }
            }
        ]
    }
}

@babel/polyfill

特点:解决所有js语法的兼容问题,不需要在webpack中进行配置,下载完依赖后,直接在对应的js文件中import引入即可。

import '@babel/polyfill'

const promise = new Promise(resolve => {
    setTimeout(() => {
        console.log('定时器执行完成')
    }, 1000)
})

缺点:可以转换所有ES6语法的新特征,即使没有使用到,导致打包后文件体积过大。

core-js

特点:core-js是目前最常用的ES语法兼容问题的解决方案,按需加载,灵活使用,可以指定兼容的浏览器版本。

rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
          options: {
            preset: [
               [
                 '@babel/preset-env',
                 {
                   useBuiltIns: 'usage', //按需加载
                   corejs: { version: 3 }, //指定 core-js 版本
                   targets: { //指定兼容性做到哪几个版本的浏览器
                        chrome: '60',
                        firefox: '60',
                        ie: '9',
                        safari: '10',
                        edge: '17'
                  }
                }
             ]
          ]
       }
    }
]

以promise为例剖析core-js对于ES6高级语法的处理。

// 引入 core-js 中的 Promise 多态
import 'core-js/es/promise';
 
// 使用 Promise
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('异步操作完成');
  }, 1000);
});
 
myPromise.then((message) => {
  console.log(message); // 将在1秒后输出 "异步操作完成"
});

6. 处理html资源

作用:自动生成一个HTML5文件, 在body中使用script标签引入所有webpack生成的bundle

// 下载
npm install --save-dev html-webpack-plugin

// 使用插件
plugins:[
    new ESLintPlugin({
        context: path.resolve(__dirname,"src")
    }),
    new HtmlWebpackPlugin({
        // 配置模版,生成的html文件中会自动保留模板格式
        template: path.resolve(__dirname,"public/index.html")
    })
],

image.png image.png

第三章 搭建开发服务器

1. 自动化打包

作用:自动化编译代码,取消手动输入npx webpack指令操作

// 下载
npm install --save-dev webpack-dev-server

// 添加配置项
module.esports = {
    // 开发服务器配置
    devServer:{
        host: "localhost", // 启动服务器域名
        port: "3000", // 启动服务器端口号
        open: true // 是否自动打开浏览器
    },
}
// 使用
npx webpack server

浏览器自动弹出3000窗口 image.png

2. 生产模式

生产模式即开发完部署上线,生产模式需要对代码进行优化,让其运行性能更好。优化主要从两个角度出发:

  • 优化代码运行性能
  • 优化代码打包迪度

一般在项目中会拆分生产模式和开发模式的配置文件,并在package.json中通过不同的指令分别启动。

image.png

3. 生产模式优化配置

3.1 提取输出CSS文件

插件:MiniCssExtractPlugin

作用:将CSS提取到单独的文件中,为每个包含CSS的JS文件创建一个CSS文件,并且支持CSS和SourceMaps的按需加载。

  • MiniCssExtractPlugin.loader:将css文件以link方式引入
  • style-loader:将css样式放到style内联样式中
// 下载
npm install --save-dev mini-css-extract-plugin
// 使用 MiniCssExtractPlugin.loader代替style-loader,并在plugin中引入
module: {
    rules: [
        {
            test: /\.css$/i, // 正则表达式匹配文件
            use: [MiniCssExtractPlugin.loader, "css-loader"], 
        },
    ]
},

plugins:[
    new MiniCssExtractPlugin({
        filename: "css/main.css"
    })
],

MiniCssExtractPlugin-loader image.png style-loader

image.png

3.2 压缩CSS文件

生产模式默认开启了js和html压缩,针对css,需要使用插件对其进行压缩。

插件:ss-minimizer-webpack-plugin

// 下载
npm install css-minimizer-webpack-plugin --save-dev

// 使用
plugins:[
    new CssMinimizerPlugin()
]

第四章 Webpack优化设置

  • 优化构建速度
    • 升级版本
    • noParse忽略部分文件解析
    • DllPlugin和DllReferencePlugin避免重复编译第三方库
    • 开启多进程loader转换、压缩文件
    • oneOf控制loader作用
    • include/exclude排除打包文件
  • 优化开发体验
    • sourceMap映射
    • HMR热模块替换
  • 减少代码体积
    • 区分环境
    • 压缩资源,JS、CSS、图片
    • Tree Shaking
    • splitChunk分离代码
    • gzip压缩
    • cache缓存
  • 运行性能
    • 按需加载,动态引入
    • preload/prefetch
    • core-js
    • PWA

1. SourceMap

SourceMap是一个源代码映射的系统,可以生成源代码与构建后代码一一映射的文件。SourceMap会生成一个xxx.map文件,里面包含源代码和构建后代码在每一行、每一列的映射关系,当构建后代码出错了,会通过xxx.map文件,从构建后代码出错位置找到映射后源代码出错位置,从而让浏览器提示源代码文件出错位置,帮助我们更快的找到错误根源。

  • 开发模式:cheap-module-source-map

    优点: 打包编译速度快,只包含行映射

    缺点: 没有列映射

mode: "production",
devtool: "cheap-module-source-map"
  • 生产模式:source-map

    优点:既包含行映射,又包含列映射

    缺点:打包编译速度慢

mode: "production",
devtool: "source-map"

2. 模块热替换

模块热替换(HMR)功能会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:

  • 保留在完全重新加载页面期间丢失的应用程序状态
  • 只更新变更内容,从而节省开发时间
  • 在源代码中CSS/JS产生修改时,会立刻在浏览器中进行更新,相当于在浏览器devtools中直接更改样式
devServer:{
    host: "localhost", // 启动服务器域名
    port: "3000", // 启动服务器端口号
    open: true, // 是否自动打开浏览器
    hot: true, // 是否开启HMR热模块替换功能,Webpack5默认开启
},

HRM的原理实际上是 webpack-dev-server(WDS)和浏览器之间维护了一个websocket服务。当本地资源发生变化后,webpack会先将打包生成新的模块代码放入内存中,然后WDS向浏览器推送更新,并附带上构建时的hash,让客户端和上一次资源进行对比。客户端对比出差异后会向WDS发起Ajax请求获取到更改后的内容(文件列表、hash),通过这些信息再向WDS发起jsonp请求获取到最新的模块代码。

3. OneOf

作用:规定一个文件只能被一个loader处理,提升打包速度

module: {
    rules: [{
        oneOf: [
            {
                test: /\.css$/i,
                use: [MiniCssExtractPlugin.loader, "css-loader"],
            },
            {
                test: /\.less$/i,
                use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
            },

        ]
    }]
}

4. Include/Exclude

针对JS文件做处理,提升打包编译速度。

  • include:包含,只处理xx文件
  • exclude:排除,除了xxx文件以外其他文件都需要处理
{
    test: /\.m?js$/,
    exclude: /(node_modules)/, // 忽略node包
    loader: 'babel-loader',
}
{
    test: /\.m?js$/,
    include: path.resolve(__dirname,"./src"),
    loader: 'babel-loader',
}
new ESLintPlugin({
    context: path.resolve(__dirname, "src"),
    exclude: "node_modules", // exclude的默认值
}),

5. Cache

每次打包时js文件都要经过Eslint检查和Babel编译,速度比较慢。我们可以缓存之前的Eslint检查和Babel编译的结果,这样第二次打包时速度就会更快了。

// 缓存babel编译
{
    test: /\.m?js$/,
    include: path.resolve(__dirname,"./src"),
    loader: 'babel-loader',
    options:{
        cacheDirectory: true, // 开启babel缓存
        cacheCompression: false // 关闭缓存文件压缩
    }
}

// 缓存eslint检查
plugins: [
    new ESLintPlugin({
        context: path.resolve(__dirname, "src"),
        exclude: "node_modules", // exclude的默认值
        cache: true, // 开启缓存
        cacheLocation: path.resolve(__dirname, "./node_modules/.cache/eslintcache") // 缓存路径
    }),
]

6. Thead多进程

当项目越来越庞大时,打包速度就会越来越慢,影响最严重的就是JS的打包速度。而对js文件处理主要就是eslint、babel、Terser三个工具,所以要想提升js文件的运行速度,可以开启多进程同时处理js文件,从而提升打包速度。

多进程打包指的是开启电脑的多个进程同时干一件事,速度更快。

⚠️:请在特别耗时的操作中使用,因为每个进程启动就有大约600ms左右开销。而启动进程的数量不得大于CPU的核数

1)获取CPU核数

const os = require("os");
const threads = os.cpus().length;

2)babel解析:开启多进程,设置进程数量

{
    test: /\.m?js$/,
    include: path.resolve(__dirname, "./src"),
    use: [
        {
            loader: 'thread-loader', // 开启多进程
            options: {
                works: threads // 进程数量
            }
        },
        {
            loader: 'babel-loader',
            options: {
                cacheDirectory: true, // 开启babel缓存
                cacheCompression: false // 关闭缓存文件压缩
            }
        },
    ],
}

3)eslint校验:开启多进程,设置进程数量

optimization: {
    minimizer: [
        new CssMinimizerPlugin(), // 压缩css
        new TerserWebpackPlugin({ // 开启多进程、设置进程数量
            parallel: threads
        })
    ]
}

7. Tree Shaking

用于描述移除JavaScript中的没有使用的代码,前提是必须依赖ES Hodule模块化。

Webpack5目前已经内置了Tree Shaking,无需过多的配置。

Tree Shaking的工作原理如下:

  • ES6模块系统:Tree Shaking的基础是ES6模块系统,它具有静态特性,意味着模块的导入和导出关系在编译时就已经确定,不会受到程序运行时的影响

  • 静态分析:在Webpack的构建过程中,会通过静态分析依赖图,从入口文件开始,逐级追踪每个模块的依赖关系,以及模块之间的导入和导出关系

  • 标记未使用代码:在分析模块依赖时,Webpack会标记每个变量、函数、类和导入,以确定它们是否被实际使用。如果一个导入的模块只是被导入而没有被使用,或者某个模块的部分代码没有被使用,Webpack会将这些未使用的部分标记为"unused"

  • 删除未使用代码:在代码标记为未使用后,Webpack会在最终的代码生成阶段,通过工具(如UglifyJS等)删除这些未使用的代码,包括未使用的模块、函数、变量和导入等

8. Babel文件体积优化

Babel为编译的每个文件都插入了辅助代码,如公共方法的辅助代码_extend。但是有一些辅助代码会被重复添加到每一个需要它的文件中,造成文件体积过大。通过将捕助代码作为一个独立模块,从而避免重复引入问题。

@babel/plugin-transform-runtime:禁用了Babel自动对每个文件的 runtime注入,改为引入@babel/plugin-transform-runtin内的所有辅助代码。通过减少定义和重复引入,从而减少文件体积。

// 下载
npm i @babel/plugin-transform-runtime -D

// 使用
{
    loader: 'babel-loader',
    options: {
        cacheDirectory: true, // 开启babel缓存
        cacheCompression: false, // 关闭缓存文件压缩
        plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
    }
}

9. 图片压缩

image-minimizer-webpack-plugin插件,可以对本地静态图片进行压缩处理,从而减少代码体积。

压缩图片的模式分为有损压缩和无损压缩两种:

  • 无损压缩:imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo
  • 有损压缩:imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo
// 下载
npm i image-minimizer-webpack-plugin imagemin -D

// 使用-无损压缩
new ImageMinimizerPlugin({
    minimizer: {
        implementation: ImageMinimizerPlugin.imageminGenerate,
        options: {
            plugins: [
                ['gifsicle',{interlaced: true}],
                ['jpegtran',{progressive: true}],
                ['optipng',{optimizationLevel: 5}],
                [                    'svgo',                    {                        plugins: [                            'preset-default',                            'prefixIds',                            {                                name: 'sortAttrs',                                params: {xmlnsOrder: 'alphabetical'}                            }                        ]
                    }
                ]
            ]
        }
    }
})

10. Code Split

Code Split,通过将代码分割打包,从而可以按需加载,优化加载速度。

代码分调的作用:

  • 分剩文件:将打包生成的文件进行分割,生成多个js文件
  • 按需加载:需要哪个文件就加载哪个文件

1)多入口打包

entry:{
    app: "./src/app.js",
    main: "./src/main.js"
}

2)多入口提取公共模块

optimization: {
    splitChunks: {
        chunks: "all"
    }
}

3)多入口按需加载

// import()动态加载语法,返回值为promise对象
import(./count.js).then((res)=>{
    ...
}).catch((err)=>{
    ...
})

4)模块命名

// 对某个引入模块命名
import(/* webpackChunkName:"math" */'./math.js')

// 打包输出文件名称使用
module.exorts = {
    output:{
        chunkFilename:"static/js/[name].js"
    }
}

11. Preload/Prefetch

Webpack 4.6.0提供了预先拉取(prefetching)和预先加载(preloading)的功能,使用这些声明可以修改浏览器处理异步chunk的方式。

1)含义

  • Preload:预先加载,浏览器会立即加载资源,异步chunk和父级chunk并行加载。父级chunk下载完成后页面就可以显示,同时不影响异步chunk的下载。
import(
  `./utilities/divide`
  /* webpackPreload: true */
  /* webpackChunkName: "utilities" */
)
  • Prefetch:预先拉取,浏览器会在空闲时间下载该模块,且下载是发生在父级chunk加载完成之后。
import(
  `./utilities/divide`
  /* webpackPrefetch: true */
  /* webpackChunkName: "utilities" */
)

2)共同点

  • 只加载资源,不会执行资源
  • 可以缓存资源

3)区别

  • Preload加载优先级高,Prefetch加载优先级低
  • Preload只能加载当前页面需要使用的资源,Prefetch可以加载当前页面使用到的资源,也可以加载下一个页面使用到的资源

4)适用场景

  • 当前页面优先级高的资源用Preload加载
  • 下一个页面需要使用的资源用Prefetch加载

Webpack中使用这两个属性需要通过中间件,不能直接在入口文件index.js中使用

12. Core-js

core-js是用来做ES6以及以上API的polyfill的工具。polyfill翻译过来叫做垫片/补丁,就是用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上使用该新特性。

core-js一般会和babel一起使用,为ES6及以上语法生成对应的兼容性实现方案,并且会在dist文件夹下面生成一个新的打包文件。

// 下载
npm i core-js

// 配合babel.config.js使用
module.exports = {
    presets: [
        [
            '@babel/preset-env',
            {
                useBuiltIns: "usage", // 按需加载自动引入
                coreJs: 3
            }
        ]
    ]
}

13. PWA

浙进式网络应用程序(progressive web application PWA)是一种可以提供类似于native app(原生应用程序)体验的Web App的技术。其中最重要的是在离线(offline) 时,应用程序能够继续运行功能。

其内部是通过Service Workers技术实现的。

// 下载
npm i workbox-webpack-plugin --save-dev

// 添加配置
plugins: [
    new WorkboxPlugin({
        clientsClaim: true,
        skipWaiting: true
    })
]

// 使用-main.js
if("servicelorker" in navigator{
    window.addEventListener("load", ()=>{
        navigator.serviceWorker
            .register("/service-worker.js")
            .then((registration)=>{console.lor("sW registered: ",registration)})
            .catch((registrationError)=>{console.log("sW registration failed: ", reristrationError)})

性能优化概述

  • source Map:开发或上线时代码报错能有更加准确的错误提示

  • HMR:开发时只重新编译打包更新变化了的代码,不变的代码使用缓存,从而使更新速度更快

  • oneOf:资源文件一旦被某个loader处理了,就不会继续遍历其他loader,打包速度更快

  • Include/Exclude:排除或只检测某些文件,处理的文件更少,速度更快

  • Cache:对eslint和babel处理的结果进行缓存,让第二次打包速度更快

  • Thead:多进程处理esint和babel任务,速度更快(需要注意的是,进程启动通信都有开销的,要在比较多代码处理时使用才有效)

  • Tree shaking:移除没有使用的多余代码,让代码体积更小

  • babel/plugin-transform-runtime:对babel进行处理,让辅助代码从中引入,而不是每个文件都生成辅助代码,从而体积更小

  • Image hininizer:对项目中图片进行压缩,体积更小,请求速度更快。(本地项目静态图片才需要进行压缩)

  • code Split:对代码进行分割成多个js文件,从而使单个文件体积更小,并行加载速度更快

  • import():动态导入语法,按需加载

  • Preload/Prefetch:对代码进行提前加载,提升用户体验

  • Network cache:对输出资源文件进行更好的命名,利于缓存处理,提升用户体验

  • core-js:对js进行兼容性处理,使代码能运行在低版本浏览器中

  • PWA:实现代码离线访问功能,提升用户体验

第五章 webpack核心模块

1. Loader

1.1 loader工作原理

  1. 作用

Loader是帮助webpack将不同类型的文件转换为webpack可识别的模块。

  1. 原理

loader在底层就是一个函数,当webpack解祈资源时,会调用相应的loader方法处理。loader()接收三个参数:

  • content:文件内容
  • map:与SourceMap相关数据
  • meta:其他loader传递的数据
module.exports = function(content, map, meta){
    ... ....
    return content;
}

1.2 分类

  1. 优先级分类
  • pre:前置loader
  • normal:普通loader
  • inline:内联loader
  • post:后置loader
  1. 不同优先级的执行顺序
  • 不同优先级loader:pre > normal >inline > post
  • 相同优先级loader:从右到左,从下到上
  1. 配置优先级
  • 配置方式:在webpack.config.js文件中指定loader为pre、normal、post loader中一种,不添加任何指定时,默认为normal loader
  • 内联方式:在每个import语句中显式指定loader为inline loader
// pre loader
{
    enforce: "pre"
    test: /\.js$/,
    loader: "loader1"
},
// normal loader
{
    test: /\.js$/,
    loader: "loader2"
}
// post loader
{
    enforce: "post"
    test: /\.js$/,
    loader: "loader3"
}
// inline loader
// 使用style-loader和css-loader处理styles.css文件
import Styles from 'style-loader!css-loader?modules!./styles.css";

// 前面添加一个“!”,跳过normal loader
import Styles from '!style-loader!css-loader?modules!./styles.css";

// 前面添加一个“-!”,跳过 pre、normal loader
import Styles from '-!style-loader!css-loader?modules!./styles.css";

// 前面添加一个“!!”,跳过pre、normal、postloader
import Styles from '!!style-loader!css-loader?modules!./styles.css";
  1. Loader分类
  • 同步loader
// 只有一个loader时
module.exports = function(content, map, meta){
    // 不需要向下传递参数和source-map
    return content;
}

// 有多个loader连用时
module.exports = function(content, map,meta){
    // err:代表是否有错误
    // content:处理后的内容
    // source-map:继续向下传递source-map
    // meta:给下一个loader传递的参数
    this.callback(null,content,map,meta);
}
  • 异步loader
module.exports = function(content , map, meta){
    // 获取异步的回调函数
    const callback = this.async();
    setTimeout(() => {
        // 参数和同步回调函数一致
        callback(null, content, map, meta);
    }, 1000);
}
  • Raw loader raw loader表示具有raw属性的loader,属性值为布尔值。可以是同步也可以是异步的loader,区别是其接收到的content是Buffer格式的流数据
function mayLoader(content){
    // Buffer流,一般用于操作图片、图标等资源文件
    return content;
}

mayLoader.raw = true;
module.exports = mayLoader;
  • pitch loader

pitch loader表示具有pitch属性的loader,属性值为函数。可以是同步也可以是异步的loader,特点是执行顺序会早于其他loader。

如连用三个loader处理文件资源时,会从左到右依次执行每个loader的pitch方法,然后在从右到左依次执行每个loader方法。

正常的pitch函数无返回值,如果在执行过程中某个pitch有返回值,则会中断执行顺序,转而执行前一个pitch所在的loader方法。

module.exports = function(content){
    console.log('normal loader');
    return content;
}

module.exports.pitch = function(){
    consel.log("pitch loader");
}

pitch无返回值时的执行顺序 image.png

pitch2有返回值时的执行顺序 image.png

1.3 Loader常用API

方法名含义使用
this.async异步回调loader,返回this.callbackconst callback = this.async()
this.callback同步或异步调用的、可以返回多个结果的函数this.callback(err,content,sourceMap?,meta?)
this.getOptions(schema)获取loader的options配置,schema为验证规则this.getOptions(schema)
this.emitFile生成一个文件thisemitFile(name,content,sourceMap)
this.utils.contextify返回一个相对路径this.utils.contextify(context,request)
this.utils.absolutify返回一个绝对路径this.utils.absolutify(context,request)

1.4 自定义Loader

  1. 清除console.log代码的loader
module.exports = function (content){
    return content.replace(/consolel.log\(.* );?/g,"");
}
  1. 低版本浏览器适配loader
const babel = require( @babel/core");
const schema = require("./schema.json");

module.exports = function (content) {
    // 异步loader
    const callback = this.async();
    const options = this.getOptions(schema);
    // 使用babe1对代码进行编译
    babel.transform(content, options, function(err, result){
        if (err){
            callback(err);
        }else{
            callback(null, result.code);
        }
   });
}

2. Plugin

2.1 Plugin原理分析

  1. 作用

Plugin 可以扩展 webpack,加入一些自定义的构建行为,使 webpack 可以执行更广泛的任务,拥有更强的构建能力。

  1. 工作原理

webpack 在编译的过程中,会触发一系列 Tapable 钩子事件,插件的作用就是找到对应的钩子,往钩子中挂载任务。当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行。

第一步:获取Webpack编译器中的数据;

第二步:在合适的时机,监听Webpack的事件,如compile、emit、done等;

第三步:响应Webpack事件,并对编译期间的资源进行处理,例如对JavaScript、CSS等进行压缩、合并、优化等;

第四步:处理完成后,向Webpack编译器返回相应的处理结果,以便Webpack最终生成相应的文件;

常用的插件工作原理:

  • html-webpack-plugin:通过在Compilation对象中的html-webpack-plugin-before-html-processing钩子函数中添加生成的html文件内容来实现自动输出html文件
  • clean-webpack-plugin:在编译之前清除目标目录中的文件,就是通过在Compiler对象中监听webpack工作流程的钩子函数来实现的
  1. 工作流程

1️⃣:webpack 中的 plugin 是一个类(构造函数),通过在 plugins 配置中实例化进行调用

plugins:[new HTMLWebpackPlugin()],

2️⃣:在 plugin 的原型对象上指定了一个 apply 方法,入参是 compiler 对象

apply(compiler){}

3️⃣:指定一个事件钩子,并调用内部提供的API

4️⃣:完成内部操作后,调用 webpack 提供的 callback 方法

// 自定义plugin
class TestPlugin{
    constructor(){}
    // webpack将compiler对象传递给apply方法
    apply(compiler){ 
        // 挂载钩子
        compiler.hooks.environmenttap("TestPlugin",()=>{})
        compiler.hooks.emit.tap("TestPlugin",(compilation)=>{}
        compiler.hooks.emit.tapAsync("Testplugin",(compilation, callback) => {
            setTimeout(()=>{
                // 调用webpack的回调函数
                callback();
            }, 2000);
        })
    }
}
module.exports = TestPlugin;

2.2 webpack钩子

钩子的本质就是事件,为了方便开发者直接介入和控制编译过程,webpack把编译过程中触发的各类关键事件封装成事件接口暴露了出来,这些接口被称为 hooks(钩子)。

Tapable为webpack提供了统一的插件接口(钩子)类型定义,它是webpack的核心功能库。webpack中目前有十种hooks:

image.png

Tapable提供了三个方法给插件,用于注入不同类型的自定义构建行为:

  • top:即可以注册同步钩子和异步钩子
  • topAsync:回调函数方式注册异步钩子
  • tapPromise:Promise方式注册异步钩子

plugin通过Tapable注册事件。对于监听事件的触发,同步钩子通过call方法,异步钩子通过callAsync方法和promise方法。

2.3 编译管理器

plugin功能的实现主要依赖于compiler和complation对象,两者都继承自Tapable对象

  1. Compiler对象

compiler对象中保存着完整的Webpack环境配置,每次启动webpack构建时,都会创建一个独一无二的compiler,它有以下主要属性:

  • compiler.options:访问本次启动webpack时所有的配置文件,包括但不限于 loaders、entry、output、plugin等等完整配置信息。
  • compiler.inputFileSystem、compiler.outputFileSysten:进行文件操作,相当于Nodejs中fs
  • compiler.hooks:注册Tapable的不同种类Hook,从而可以在compiler生命周期中植入不同的逻辑
  1. Compilation对象

compilation对象代表一次资源的构建,compilation实例能够访问所有的模块和它们的依赖。一个compilation对象会对构建依赖图中所有横块进行编译,在编译阶段横块会被加载(load)、封存(seal)、优化(optimize)、分块(chunk)、哈希(hash)和重新创建(restore)。

它有以下主要属性:

  • compilation.modules:访问所有横块,打包的每一个文件都是一个横块
  • compilation.chunks:chunk即是多个modules组成而来的一个代码块,入口文件引入的资源组成一个chunk,通过代码分割的模块又是另外的chunk
  • compilation.assets:访问本次打包生成所有文件的结果
  • compilation.hooks:注册tapable的不同种类Hook,用于在compilation编译模块阶段进行逻辑添加以及修改

compiler对象结构 image.png

compilation对象结构 image.png

2.4 自定义plugin

创建一个自定义的打包zip插件

const JsZip = require('jszip');

class ZipPlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {
    compiler.hooks.emit.tapPromise('1', (compilation) => {
      const assets = compilation.assets;
      const zip = new JsZip();
      for(let filename in assets) {
        zip.file(filename, assets[filename].source())
      }
      // nodebuffer是node环境中的二进制形式;blob是浏览器环境
      return zip.generateAsync({type: 'nodebuffer'}).then((content) =>{
        assets[this.options.name] = {
          source(){return content},
        }
        return new Promise((resolve, reject) => {
          resolve(compilation);
        })   
      })
    })
  }
}

module.exports = ZipPlugin;

webpack.config.js中使用插件:

module.exports = {
  plugins: [
    new ZipPlugin({
      name: 'my.zip'
    })
  ]
}

3. Loader和Plugin的区别

1)功能不同

  • Loader本质是一个函数,它是一个转换器。webpack只能解析原生js文件,对于其他类型文件就需要用loader进行转换。
  • Plugin是一个插件,用于增强webpack功能。webpack在运行的生命周期中会广播出许多事件,Plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。

2)用法不同

  • Loader的配置是在module.rules中,类型为数组,每⼀项都是⼀个 Object,⾥⾯描述了对于什么类型的⽂件(test),使⽤什么加载(loader)和使⽤的参数(options)
  • Plugin的配置在plugins中,类型为数组,每一项是一个Plugin的实例,参数都通过构造函数传入的。

第六章 Webpack工作原理

1. 核心功能

webpack最核心的功能:内容转化+资源合并

  1. 初始化
  • 初始化参数:参数 = 配置文件、配置对象、Shell 参数 + 默认配置
  • 创建编译器对象:通过参数创建Compiler对象
  • 初始化编译环境:注入内置插件、注册模块工厂、初始化RuleSet集合、加载配置的插件
  • 开始编译:执行compiler对象的run方法
  • 确定入口:根据entry找出所有入口文件
  • 转换入口:调用compilition.addEntry将入口文件转换为dependence对象
  1. 构建
  • 编译模块(make):根据dependence对象创建module对象,调用loader将模块转译为标准JS内容,调用JS解释器将内容转换为AST对象,从中找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译:上一步递归处理所有能触达到的模块后,得到module 集合以及 module之间的依赖关系图
  1. 生成
  • 输出资源(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

image.png

2. 初始化阶段

  • 整合参数:process.args + webpack.config.js
  • 校验参数:validateSchema
  • 合并参数:getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults
  • 基于参数创建compiler对象:new Compiler
  • 插入外部plugin插件:遍历plugins集合,执行插件的apply方法
  • 加载内置plugin插件:new WebpackOptionsApply().process

3. 构建阶段

  • 构建module子类:根据文件类型调用handleModuleCreate 构建 module 子类
  • 转义module内容:调用runLoaders将各类资源转译为 JavaScript 文本
  • 解析JS文本:调用 acorn 将 JS 文本解析为AST
  • 遍历AST,处理依赖

module => AST => dependences => module

🤔:webpack与babel区别?

🙋:相同点:webpack构建阶段会读取源码,解析为AST集合,babel解析阶段会读取源码解析为AST集合。不同点:Webpack只遍历AST集合,babel会对AST做等价转换

🤔:webpack如何识别资源依赖?

🙋:遍历AST集合,通过识别require/import之类的导入语句,确定依赖关系

4. 生成阶段

  • 构建chunkGroup对象
  • 将module分配给chunk:遍历compilation.modules集合,将module按entry/动态引入的规则分配给不同的Chunk对象
  • 记录assets输出规则:遍历module/chunk ,调用compilation.emitAssets方法将assets信息记录到 compilation.assets对象中
  • 将assets写入文件系统
  • 控制流回转到compiler对象

entry及entry触达到的模块,组合成一个chunk;

使用动态引入语句引入的模块,各自组合成一个chunk;

5. 资源形态流转

1)compiler.make

  • entry文件以dependence对象形式加入compilation的依赖列表,dependence对象记录entry的类型、路径等信息
  • 根据dependence调用对应的工厂函数创建module对象,之后读入 module 对应的文件内容,调用 loader-runner 对内容做转化,转化结果若有其它依赖则继续读入依赖资源,重复此过程直到所有依赖均被转化为module

2)compilation.seal

  • 遍历module集合,根据entry配置及引入资源的方式,将module分配到不同的chunk
  • 遍历 chunk 集合,调用compilation.emitAsset方法标记chunk的输出规则,即转化为assets集合

3)compiler.emitAssets:将assets写入文件系统

第七章 模块联邦

Webpack模块联邦(Webpack Module Federation)Webpack 5 中引入的一项新功能,它允许不同的 Webpack构建 之间共享代码并动态加载依赖项。具体来说,它允许将应用程序拆分成多个独立的 Webpack构建(或称为远程应用程序),这些构建可以在运行时共享代码和依赖项。

通过使用 Webpack模块联邦,不同的团队可以独立地构建和部署其应用程序的各个部分,而不必将所有代码都打包到一个大的 JavaScript 文件中。这可以提高应用程序的性能和可维护性,同时使得不同团队之间的合作更加容易。

模块 一

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
    mode: "development",
    devServer: {
        port: 8081,
    },
    plugins: [
        new ModuleFederationPlugin({
            name: "microFrontEnd1",
            filename: "remoteEntry.js",
            exposes: {
                "./MicroFrontEnd1Index": "./src/index",
            },
        }),
        new HtmlWebpackPlugin({
            template: "./public/index.html",
        }),
    ],
};

模块 二

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
    mode: "development",
    devServer: {
        port: 8082,
    },
    plugins: [
        new ModuleFederationPlugin({
            name: "microFrontEnd2",
            filename: "remoteEntry.js",
            exposes: {
                "./MicroFrontEnd2Index": "./src/index",
            },
        }),
        new HtmlWebpackPlugin({
            template: "./public/index.html",
        }),
    ],
};

主应用

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
    mode: "development",
    devServer: {
        port: 8080,
    },
    plugins: [
        new ModuleFederationPlugin({
            name: "container",
            filename: "remoteEntry.js",
            remotes: {
                microFrontEnd1: "microFrontEnd1@http://localhost:8081/remoteEntry.js",
                microFrontEnd2: "microFrontEnd2@http://localhost:8082/remoteEntry.js",
            },
        }),
        new HtmlWebpackPlugin({
            template: "./public/index.html",
        }),
    ],
};

image.png

参考文档:模块联邦实现一个微前端

补充

1. SplitChunks原理

optimization: { 
    splitChunks: { 
        chunks: "all" 
    } 
}

splitChunks的默认配置如下所示:

splitChunks: { 
    chunks: "async", //initial表示直接引入的模块,async表示按需引入的模块,all表示都包括
    minSize: 30000, //最小包体积,这里的单位是byte,超过这个大小的包会被splitChunks优化
    minChunks: 1, //模块的最小引用次数,如果引用次数低于这个值,将不会被优化
    maxAsyncRequests: 5, //设置async chunks的最大并行请求数
    maxInitialRequests: 3, //设置initial chunks的最大并行请求数
    automaticNameDelimiter: '~', //产出chunks的文件名分割符
    name: true, //true:根据提取chunk的名字自动生成,false:根据缓存组IdHint生成,string:生成文件名即为这个string
    cacheGroups: { //缓存组,自定义拆包规则在此定义
        vendors: { //默认配置,node_modules的chunk
            test: /[\/]node_modules[\/]/, 
            priority: -10 
      }, 
            default: { //业务代码的chunk
            minChunks: 2, 
            priority: -20, 
            reuseExistingChunk: true //复用已存在的chunks
      } 
  } 
} 

SplitChunks工作流程:

  1. 分析模块间的依赖关系

SplitChunksPlugin 会分析模块之间的依赖关系,并根据这些关系确定哪些模块可以组成一个共享块。这样可以确保代码被正确地分离,而不会出现意外的行为。

  1. 生成共享块

SplitChunksPlugin 根据配置项生成共享块。配置项包括 minSize(指定共享块大小的最小值)、maxSize(指定共享块大小的最大值)、minChunks(指定一个模块至少被使用的次数才会被拆分成共享块)等。

  1. 提取共享块

在分析和生成共享块后,SplitChunksPlugin 会将共享块提取出来,并创建新的 chunk(即打包后的文件),将这些共享块放入新的 chunk 中。这样,每个共享块只需被下载一次,而不必重复下载多次,从而提高了应用程序的加载速度。

  1. 缓存共享块

为了进一步提高性能,SplitChunksPlugin 会将共享块缓存起来,并在后续的构建中重复使用它们。这样,如果某个共享块已经存在于缓存中,就不必再重新生成它,从而节省了构建时间。