Webpack DLL动态链接库的应用和思考

DLL是"动态链接库"的意思,假设项目中用了 react 、lodash,这些静态资源可以被单独打包,且独立于项目。这样每次主项目打包的时候,都可以直接使用过打包好的 react、lodash,节省了性能。

原理:

  • 单独一份ddl config文件单独对react、lodash打包,会将打包产物和一个manifest文件放在某个目录下,manifest文件。
  • 打包的产物是以全局变量的方式导出模块的,manifest文件用来告诉使用方react、lodash打包后生成的全局变量叫什么,
  • 使用方在遇到react、lodash时候,就无需再次打包,将使用react和lodash模块的地方,根据manifest文件的指引去通过全局变量访问。

简单的例子:

单独开一个dll config文件 webpack.dll.config.js

js 复制代码
const path = require('path');
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  mode: 'production',
  entry: {
     // 将react和lodash以及你想要预打包的静态文件打包成一个名为vendor的bundle
     // 这里没有路径,直接就一个react,就是去打包纯node_modules下的文件
    // 当然我们没有loader,webpack默认只能打包js和json文件。如果这里出现一个css入口文件,必然会发生打包错误。
    vendor: ['react', 'react-dom', 'lodash'] 
  },
  output: {
     // 将生成的bundle生成在dll目录下
    path: path.join(__dirname, 'dll'),
    filename: '[name]_[hash].dll.js',
     // 因为dll对打包生成的模块的导出方式是全局变量的方式,需要通过library告诉webpack应该将模块导出维护在哪个全局变量上。这个全局变量叫做 '[name]_[hash]',实际编译后即 vendor_<hash> 
    library: '[name]_[hash]'
  },
  plugins: [
    new CleanWebpackPlugin(),
     // 使用DllPlugin,这是webpack内置的plugin
    new webpack.DllPlugin({
       // 将生成的bundle生成在dll目录下,并且生成的manifest文件应该是[name]-manifest.json的形式
      path: path.join(__dirname, 'dll', '[name]-manifest.json'),
       // 表明这些模块的导出的全局变量名,必须和 output.library一致
      name: '[name]_[hash]'
    })
  ]
};

然后是主体项目的webpack文件 webpack.config.js

js 复制代码
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
     // 应用方必须配合使用 DllReferencePlugin 才能完成工作
    new webpack.DllReferencePlugin({
       // 告诉manifest文件在哪
      manifest: require('./dll/vendor-manifest.json')
    }),
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
     // 需要注意的是,我们需要手动将react、lodash这样被预打包生成的bundle引入在index.html中
     // 可以选择直接手动通过<scripe>标签的方式引入,但这样不如直接使用 AddAssetHtmlPlugin 享受工程化带来的福利,比如 publicPath,以及index.html删除后还能自动引入
    new AddAssetHtmlPlugin({
      filepath: path.resolve(__dirname, 'dll/vendor_*.dll.js'),
      publicPath: './'
    })
  ]
};

随后可以通过往package.json中加一些npm指令。在发布部署(CICD)时先npm run dll,然后npm run build

打包后的运行时状态(伪代码):

vendor.dll.js 伪代码,大概的原理

js 复制代码
var vendor_2e8f1b = (function(modules) {
  // ▼ 模块缓存机制
  var installedModules = {};
  
  // ▼ 模块加载实现 (类似__webpack_require__)
  function __dll_require__(moduleId) {
    if(installedModules[moduleId]) 
      return installedModules[moduleId].exports;
    
    var module = installedModules[moduleId] = {
      id: moduleId,
      loaded: false,
      exports: {}
    };
    modules[moduleId].call(module.exports, module, exports, __dll_require__);
    module.loaded = true;
    return module.exports;
  }
  
  // ▼ 暴露全局变量
  return __dll_require__(0);
})({
  /*! ▼ 预编译的第三方库模块集合 */
  0: function(module, exports) {
    // React核心实现
    module.exports = window.React = ... // 完整React库代码
  },
  1: function(module, exports) {
    // ReactDOM实现
    module.exports = window.ReactDOM = ... 
  },
  2: function(module, exports) {
    // lodash工具库
    module.exports = window._ = ... 
  }
});

index.bundle.js 中访问lodash

js 复制代码
 // 在bundle中动态绑定DLL模块,通过访问直接变量的方式来读取react、lodash。也就是说打包后的react和lodash的源码将不会出现在 index.bundle.js 中。 
__webpack_require__.d(exports, "lodash", () => window.vendor_2e8f1b[2]);

Manifest 文件

js 复制代码
{
  "name": "vendor_2e8f1b",
  "content": {  // 提供给使用方的预打包bundle信息
    "./node_modules/react/index.js": {
      "id": 1,  // react的id为1,使用方的 import { useState } from 'react'; 打包后会通过 window.vendor_2e8f1b[1].useState 访问
      "buildMeta": {"providedExports": true}
    },
    "./node_modules/lodash/lodash.js": {
      "id": 2,
      "buildMeta": {"usedExports": ["default"]}
    }
  }
}

学习Dll时思考的一些问题:

Q1:如果使用dll将react、lodash等第三方包预打包出去了,主项目对react和lodash是不是就无法启用treeshaking特性了?

A1: 是的,因为在主项目中直接绕过了react和lodash的打包,使用了现成的打包产物,并采用全局变量的方式去访问由dll分离出去的包,自然无法对react和lodash走一遍打包编译的过程,也就没有静态分析的过程,也就是无法treeshaking了。要知道treeshaking影响的是主项目打包后的产物,而不是主项目外的打包产物。webpack总不能会根据manifest文件去反向分析dll预打包的产物,然后修改dll下的产物吧。

Q2: dll能否解决Qiankun微前端资源共用问题?

A2:是的,dll可以解决微前端资源共用问题,因为在微前端中,对公共资源的处理有externals方案。externals原理也是将公共依赖的模块抛出打包工程,根据配置使用cdn链接,进而通过全局变量式使用外置模块。而dll原理类似,并且dll无需独立部署外置模块的cdn了,所以也可以。

大体思路流程:

这样的缺点就是子应用没法脱离主应用独立运行。当然如果有子应用需要独立运行的需求,可以有两种办法:

  • 无论主应用还是子应用都通过addAssetHtmlPlugin实现将cdn下的所有js文件引入在html模板中。
  • 或在子应用运行时,率先判断是否为独立运行状态,然后通过动态创建script标签的方式将所有cdn下的js文件引入,待script标签都加载完成后执行后续逻辑。

通过AddAssetHtmlPlugin实现对cdn下的文件

js 复制代码
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

// 准备CDN链接数组
const cdnAssets = [
  'https://cdn.example.com/library/vendor.react.js',
  'https://cdn.example.com/library/vendor.lodash.js
];

module.exports = {
  // ... 其他webpack配置
  plugins: [
    new HtmlWebpackPlugin(),
    // 使用 AddAssetHtmlPlugin 并传入CDN链接数组
    new AddAssetHtmlPlugin({ 
      files: cdnAssets 
    })
  ]
};

Q3:dll能否替代external解决方案?

A3:还是不能完全替代external方案的。虽然两者具备相似的原理。

Q4:我的第三方库包含vue3,我希望转换为es5,但dll中的打包是es6的,那该怎么办?重新用dllplugin打包一个es5版本的么?

A4:是的。思路是需要把webpack.dll.config.js中增加babel-loader相关配置,来使得dll下的产物也是es5的。下面是代码仅描述意思,仅供参考:

js 复制代码
module.exports = {
  module: {
    rules: [
      {
        test: /.js$/, // 匹配所有JavaScript文件
        use: {
          loader: 'babel-loader', // 必须使用loader而非plugin
          options: {
            presets: ['@babel/preset-env'] // 指定ES5转换规则
          }
        }
      }
    ]
  },
  plugins: [
    new webpack.DllPlugin({ /* DLL配置 */ }) // Plugin负责库生成,不处理文件转换
  ]
};
相关推荐
那我掉的头发算什么2 小时前
【javaEE】多线程--认识线程、多线程
java·jvm·redis·性能优化·java-ee·intellij-idea
益达是我2 小时前
【element-plus】element-plus升级到v2.11.7,el-tree文字不显示问题
前端·javascript·vue.js·element-plus
社恐的下水道蟑螂2 小时前
从 JS 单线程到 Promise:彻底搞懂异步编程的 "同步化" 魔法
前端·javascript
晴殇i3 小时前
《效率翻倍!12个被90%前端忽视的CSS神技》
前端·css·面试
NiKo_W3 小时前
Linux 重定向与Cookie
linux·运维·服务器·前端·网络·线程·协议
Mr_汪3 小时前
离线工程集成其他推送
前端
惜茶3 小时前
使用前端框架vue做一个小游戏
前端·vue.js·前端框架
普通码农3 小时前
Vue 3 接入谷歌登录 (小白版)
前端·vue.js
Ric9703 小时前
Object.fromEntries 实操
前端