Skip to content

dllPlugin 动态连接库

本节学习一下webpack中比较重要的一个概念,叫webpack的动态链接库。

  1. 什么是动态链接库呢?
  2. 使用动态链接库能解决什么问题?
  3. 怎么去使用动态链接库?

前置工作

安装一下react和react-dom

shell
yarn add react react-dom

修改index.js

js
// webpack-optimize\src\index.js

import React from 'react'
import { render } from 'react-dom'

// 将 h1标签 渲染到 root 节点下
render(<h1>jsx</h1>, window.root)

修改index.html

html
<!-- webpack-optimize\public\index.html -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="root"></div>
</body>

</html>

修改配置文件

js
// webpack-optimize\webpack.config.js

let path = require('path')
let Webpack = require('webpack')

// 插件 用来复制html入口模板,到打包后的目录,并把打包后的js文件,以script标签的形式塞到模板文件里
let HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    mode: 'development', // 模式,默认两种模式 production 和 development

    entry: './src/index.js', // 入口文件

    devServer: { // 开发服务器的配置
        port: 3000, // 默认端口是8080,这里可以改
        progress: true, // 打包时候的进度条
        contentBase: './build', // 以哪个文件夹作为服务的根目录 ,如果有就直接使用,没有的话就使用内存中的
        open: true, // 服务启动完毕后,直接打开浏览器
        compress: true, // 启动gzip压缩
    },


    // 打包后的输出路径
    output: {
        filename: 'bundle.js', // 打包后的文件名
        path: path.resolve(__dirname, 'dist'), // 打包后输出的路径
    },

    // 配置一些loader
    module: {
        // 打包优化,不去继续解析内部是否还有别的依赖
        noParse: /jquery/, // 如果发现是引用的jquery,就不再继续解析
        rules: [
            {
                test: /\.js$/,
                // 在检索js文件时,排除node_modules目录,默认node_modules也会检索到,所以可以排除一下
                exclude: /node_modules/,
                // 或者指定检索目录
                include: path.resolve('src'),
                use: {
                    loader: 'babel-loader',
                    // 配置一些它的选项
                    options: {
                        // 配置预设,有多个
                        presets: [
                            '@babel/preset-env', // 用来解析es6
                            '@babel/preset-react', // 用来解析react语法,jsx
                        ]
                    }
                }
            },
        ]
    },
    // 注册插件
    plugins: [
        // 如果从moment中引用了 ./local 那么就忽略掉
        new Webpack.IgnorePlugin(/\.\/local/, /moment/),
        new HtmlWebpackPlugin({
            template: './public/index.html', // 模板的位置
        }),
    ]
}

启动服务, npm run dev,成功渲染。

image-20220127163836005

优化的想法

不过这个时候打包出来的文件特别大,虽然就一行代码。这是因为每次都需要先打包一遍 react和react-dom

image-20220127163936933

能不能先把react和react-dom先抽离出去,这两个文件属于第三方库,并不会更改,不用重新打包。

所以可以先把react和react-dom先独立打包一遍,然后再打包项目时,只需要重打包好的第三方库中引用过来。

先打包react和react-dom

测试

先打包一遍,然后开发时,引用我们打包好的文件。这样就不会重新打包了。相当于一个优化。

新建配置文件 webpack.config.react.js

js
// webpack-optimize\webpack.config.react.js

let path = require('path')

module.exports = {
    mode: 'development',

    entry: {
        test: './src/test.js'
    },

    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    }
}

新建测试文件 test.js

js
// webpack-optimize\src\test.js
// 如果直接打包这个文件,这个模块导出的结果是无法拿到的
module.exports = 'cheny hhh'

先来分析一下打包后的结果,看如何能拿到这个结果

打包,看下结果,npx webpack --config webpack.config.react.js

webpack打包出的文件解析 这章节中,我们研究过这个代码,再来看一下,删除没有用的代码

js
(function (modules) { // webpackBootstrap
  // The module cache
  var installedModules = {};

  // The require function
  function __webpack_require__(moduleId) {

    // Check if module is in cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };

    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded
    module.l = true;

    // Return the exports of the module
    return module.exports;
  }

  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./src/test.js");
})
  ({

    "./src/test.js":

      (function (module, exports) {

        eval("// webpack-optimize\\src\\test.js\r\n// 如果直接打包这个文件,这个模块导出的结果是无法拿到的\r\nmodule.exports = 'cheny hhh'\n\n//# sourceURL=webpack:///./src/test.js?");

      })

  });

默认导出的结果,会被赋值到module.exports上,但是最后返回的结果,并没有被外部函数所接受,

其实我们可以手动的接收一下,比如

js
var a = (function (modules){})()
console.log(a)
// 这个 a 就是 返回的结果

image-20220127165906259

我们不能每次都手动改,所以可以在配置文件中配置一下,让最后的结果,使用一个变量接收一下。

添加变量接收打包后的结果

修改配置文件

js
// webpack-optimize\webpack.config.react.js

let path = require('path')

module.exports = {
    mode: 'development',

    entry: {
        test: './src/test.js'
    },

    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist'),
        library: 'ab', // 打包最外部使用变量接收,可以用作动态链接库
        libraryTarget: 'var', // 暴露给外部的方式,默认是var,还可以是 commonjs、var、this ...
    }
}

然后再打包一下,看下结果,发现已经接收到了 npx webpack --config webpack.config.react.js

image-20220127170307978

动态链接库,先打包第三方插件,然后开发时,引用打包好的结果

先打包react和react-dom,生成打包文件

修改配置文件

js
// webpack-optimize\webpack.config.react.js
let path = require('path')
let webpack = require('webpack')

module.exports = {
    mode: 'development',

    entry: {
        react: ['react', 'react-dom'], //会直接打包node_modules 中的react和react-dom
    },

    output: {
        filename: '_dll_[name].js', // 产生的文件名
        path: path.resolve(__dirname, 'dist'),
        library: '_dll_[name]', // 打包最外部使用变量接收,可以用作动态链接库 _dll_react
        // libraryTarget: 'var', // 暴露给外部的方式,默认是var,还可以是 commonjs、var、this ...
    },

    // 动态链接库需要有一个清单文件,可以找到打包好的react和react-dom
    // 使用webpack内置插件 dll
    plugins: [
        new webpack.DllPlugin({
            name: '_dll_[name]', // name === library 好找到对应关系
            path: path.resolve(__dirname, 'dist', 'manifest.json'), // 产生清单文件的目录,通过这个清单,可以找到已经打包的react和react-dom
        })
    ],
}

打包,npx webpack --config webpack.config.react.js

打包成功,这样就先把react和react-dom打包好了

image-20220127171641468

生成的manifest.json文件实际就是一个任务清单,与打包好的 _dll_react.js文件有一一映射的关系,通过manifest.json文件,就知道应该加载哪些模块代码。

image-20220127171848318

修改index.html

这时候就需要在代码里手动引用已经打包好的react和react-dom

html
<!-- webpack-optimize\public\index.html -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="root"></div>
    <!-- 引用打包好的react和react-dom 第三方库-->
    <script src="/_dll_react.js"></script>
</body>

</html>

虽然手动的写好了这个文件,但是在开发的时候,webpack默认情况下并不知道怎么找这些模块。

所以在开发的配置文件中,就得告诉webpack怎么引用这些已经打包好的第三方模块

告诉webpack,代码中的 import React 让它去任务清单中找

我们在index.js中引用的react和react-dom

js
// webpack-optimize\src\index.js

// 这样写还是会直接打包,所以需要再配置文件告诉webpack
// 先从已经打包好的文件,dll动态链接库中加载,如果找不到了,再重新打包
import React from 'react'
import { render } from 'react-dom'

// 将 h1标签 渲染到 root 节点下
render(<h1>jsx</h1>, window.root)

修改配置文件

js
// webpack-optimize\webpack.config.js

let path = require('path')
let Webpack = require('webpack')

// 插件 用来复制html入口模板,到打包后的目录,并把打包后的js文件,以script标签的形式塞到模板文件里
let HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    mode: 'development', // 模式,默认两种模式 production 和 development

    entry: './src/index.js', // 入口文件

    devServer: { // 开发服务器的配置
        port: 3000, // 默认端口是8080,这里可以改
        progress: true, // 打包时候的进度条
        contentBase: './dist', // 以哪个文件夹作为服务的根目录 ,如果有就直接使用,没有的话就使用内存中的
        open: true, // 服务启动完毕后,直接打开浏览器
        compress: true, // 启动gzip压缩
    },


    // 打包后的输出路径
    output: {
        filename: 'bundle.js', // 打包后的文件名
        path: path.resolve(__dirname, 'dist'), // 打包后输出的路径
    },

    // 配置一些loader
    module: {
        // 打包优化,不去继续解析内部是否还有别的依赖
        noParse: /jquery/, // 如果发现是引用的jquery,就不再继续解析
        rules: [
            {
                test: /\.js$/,
                // 在检索js文件时,排除node_modules目录,默认node_modules也会检索到,所以可以排除一下
                exclude: /node_modules/,
                // 或者指定检索目录
                include: path.resolve('src'),
                use: {
                    loader: 'babel-loader',
                    // 配置一些它的选项
                    options: {
                        // 配置预设,有多个
                        presets: [
                            '@babel/preset-env', // 用来解析es6
                            '@babel/preset-react', // 用来解析react语法,jsx
                        ]
                    }
                }
            },
        ]
    },
    // 注册插件
    plugins: [
        // 引用动态链接库
        new Webpack.DllReferencePlugin({
            // 引用manifest,然后通过manifest找到暴露出的变量,通过变量再加已经打包好的模块
            manifest: path.resolve(__dirname, 'dist', 'manifest.json')
            // 这样的话就会先去查找清单,如果清单里找不到的时候,再去真正的打包react和react-dom
            // 在html中必须手动引用一下那个文件,要不就找不到暴露的变量
        }),
        // 如果从moment中引用了 ./local 那么就忽略掉
        new Webpack.IgnorePlugin(/\.\/local/, /moment/),
        new HtmlWebpackPlugin({
            template: './public/index.html', // 模板的位置
        }),
    ]
}

重新打包一下,就发现明显的体积变小了。

image-20220127173150050

总结

  1. 先把打包后的文件存好,手动把第三方库先打包一下,再使用的时候,就直接从这个打包好的文件中获取。

    js
    // webpack-optimize\webpack.config.react.js
    let path = require('path')
    let webpack = require('webpack')
    
    module.exports = {
        mode: 'development',
    
        entry: {
            react: ['react', 'react-dom'], //会直接打包node_modules 中的react和react-dom
        },
    
        output: {
            filename: '_dll_[name].js', // 产生的文件名
            path: path.resolve(__dirname, 'dist'),
            library: '_dll_[name]', // 打包最外部使用变量接收,可以用作动态链接库 _dll_react
            // libraryTarget: 'var', // 暴露给外部的方式,默认是var,还可以是 commonjs、var、this ...
        },
    
        // 动态链接库需要有一个清单文件,可以找到打包好的react和react-dom
        // 使用webpack内置插件 dll
        plugins: [
            new webpack.DllPlugin({
                name: '_dll_[name]', // name === library 好找到对应关系
                path: path.resolve(__dirname, 'dist', 'manifest.json'), // 产生清单文件的目录,通过这个清单,可以找到已经打包的react和react-dom
            })
        ],
    }
  2. 为了能找到暴露出来的变量,需要手动的引用一下这个打包好的文件

    html
    <body>
        <div id="root"></div>
        <!-- 引用打包好的react和react-dom 第三方库-->
        <script src="/_dll_react.js"></script>
    </body>
  3. 为了告诉webpack,再碰到引用动态链接库中的第三方插件时,就优先重动态链接库中获取,如果没有再重新打包

    js
    // webpack-optimize\webpack.config.js
    
    let path = require('path')
    let Webpack = require('webpack')
    
    // 插件 用来复制html入口模板,到打包后的目录,并把打包后的js文件,以script标签的形式塞到模板文件里
    let HtmlWebpackPlugin = require('html-webpack-plugin')
    
    module.exports = {
        mode: 'development', // 模式,默认两种模式 production 和 development
    
        entry: './src/index.js', // 入口文件
    
        devServer: { // 开发服务器的配置
            port: 3000, // 默认端口是8080,这里可以改
            progress: true, // 打包时候的进度条
            contentBase: './dist', // 以哪个文件夹作为服务的根目录 ,如果有就直接使用,没有的话就使用内存中的
            open: true, // 服务启动完毕后,直接打开浏览器
            compress: true, // 启动gzip压缩
        },
    
    
        // 打包后的输出路径
        output: {
            filename: 'bundle.js', // 打包后的文件名
            path: path.resolve(__dirname, 'dist'), // 打包后输出的路径
        },
    
        // 配置一些loader
        module: {
            // 打包优化,不去继续解析内部是否还有别的依赖
            noParse: /jquery/, // 如果发现是引用的jquery,就不再继续解析
            rules: [
                {
                    test: /\.js$/,
                    // 在检索js文件时,排除node_modules目录,默认node_modules也会检索到,所以可以排除一下
                    exclude: /node_modules/,
                    // 或者指定检索目录
                    include: path.resolve('src'),
                    use: {
                        loader: 'babel-loader',
                        // 配置一些它的选项
                        options: {
                            // 配置预设,有多个
                            presets: [
                                '@babel/preset-env', // 用来解析es6
                                '@babel/preset-react', // 用来解析react语法,jsx
                            ]
                        }
                    }
                },
            ]
        },
        // 注册插件
        plugins: [
            // 引用动态链接库
            new Webpack.DllReferencePlugin({
                // 引用manifest,然后通过manifest找到暴露出的变量,通过变量再加已经打包好的模块
                manifest: path.resolve(__dirname, 'dist', 'manifest.json')
                // 这样的话就会先去查找清单,如果清单里找不到的时候,再去真正的打包react和react-dom
                // 在html中必须手动引用一下那个文件,要不就找不到暴露的变量
            }),
        ]
    }

动态链接库的核心就是

  1. 先打包第三方库,通过manifest清单,建立对应关系。
    1. webpack自己实现的require方法没有把闭包最后的返回值暴露出来,所以需要先暴露出来,通过output->library字段来声明,这时默认会在外部使用一个var的变量接收
  2. 在index.html中引用这个打包好的文件,为了能找到这个全局变量。
  3. 真正开发的时候,告诉webpack,先从动态链接库中查找,没有再重新打包,查找也是通过manifest来建立的链接关系。

参考

https://www.bilibili.com/video/BV1a4411e7Bz?p=22&spm_id_from=pageDriver