webpack 填坑之路--提取独立文件(模块)

前言

最近重新看了一遍 webpack 提取公共文件的配置。原来觉得这东西是个玄学,都是 “凭感觉” 配置。这篇文章将以解决实际开发遇到的问题为核心,悉数利用 webpack 提取独立文件(模块)的应用。

独立文件在实际开发中一般有两种:

  1. 第三方模块 如 Vue React jQuery 等
  2. 项目开发编写的独立模块(模块),对于 MPA 多页面开发来说是封装出的一些方法库比如 utils.getQueryString() 或者是每个页面的共同操作;对于SPA 应用来说没有特别的需要分离出模块,但是针对首屏渲染速度的提升,可以将 某些独立模块分离出来实现按需加载。

分离出独立文件的目的:

  1. 独立文件一般很少更改或者不会更改,webpack 没必要每次打包进一个文件中,独立文件提取出可以长期缓存。
  2. 提升 webpack 打包速度

提取第三方模块

  1. 配置externals
    Webpack 可以配置 externals 来将依赖的库指向全局变量,从而不再打包这个库。
// webpack.config.js 中
module.exports = {
  entry: {
    app: __direname +'/app/index.js'
  }
  externals: {
    jquery: 'window.jQuery'
  }
  ...
}

// 模板 html 中
...
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
...

// 入口文件  index.js
import $ from 'jquery'

其实就是 script 标签引入的jquery 挂载在window下 其他类型 externals 的配置可以去官网查看,这种方法不算是打包提取第三方模块,只是一个变量引入,不是本文讨论的重点。

  1. 利用CommonsChunkPlugin
    CommonsChunkPlugin 插件是专门用来提取独立文件的,它主要是提取多个入口 chunk 的公共模块。他的配置介绍如下:
配置属性配置介绍
name 或者 nameschunk 的名称 如果是names数组 相当于对每个name进行插件实例化
filename这个common chunk 的文件输出名
minChunks通常情况为一个整数,至少有minChunks个chunk使用了该模块,该模块才会被移入[common chunk]里 minChunks 还可以是Infinity意思为没有任何模块被移入,只是创建当前这个 chunk,这通常用来生成 jquery 等第三方代码库。minChunks还可以是一个返回布尔值的函数,返回 true 该模块会被移入 common chunk,否则不会。默认值是 chunks 的长度。
chunks元素为chunk名称的数组,插件将从该数组中提取common chunk 可见 minChunks 应该小予chunks的长度,且大于1。如果没有 所有的入口chunks 会被选中
children默认为false 如果为true 相当于为上一项chunks配置为chunk的子chunk 用于代码分割code split
async默认为false 如果为true 生成的common chunk 为异步加载,这个异步的 common chunk 是 name 这个 chunk 的子 chunk,而且跟 chunks 一起并行加载
minSize如果有指定大小,那么 common chunk 的文件大小至少有 minSize 才会被创建。非必填项。

创建一个如下图的目录

clipboard.png

package.json 如下

{
  "name": "webpacktest",
  "version": "1.0.0",
  "description": "",
  "directories": {
    "doc": "doc"
  },
  "scripts": {
    "start": "webpack"
  },
  "author": "abzerolee",
  "license": "ISC",
  "devDependencies": {
    "html-webpack-plugin": "^2.30.1",
    "webpack": "^3.8.1"
  },
  "dependencies": {
    "underscore": "^1.8.3",
  }
}

a.js 引入了 underscore 需要进行了数组去重操作,现在需要将underscore分离为独立文件。

// webpack.config.js
entry: {
  a: __dirname +'/app/a.js',
  vendor: ['underscore']
},
output: {
  path: __dirname +'/dist',
  filename: '[name].[chunkhash:6].js',       
  chunkFilename: '[name].[id].[chunkhash:6].js'
},
plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
  }),
  new HtmlWebpackPlugin({
    template: __dirname +'/app/index.html'
  })
]
// a.js
let _ = require('underscore');

let arr = _.filter([1,2,3,2,3,3,5,5], (v, i, self) => self.indexOf(v) === i);
console.log('unique:' +arr);

这样underscore就分离进了 vendor 块,注意的是需要在入口定义 要输出的 [ 独立文件名 ]: [ 需要分离的模块数组 ], 然后在CommonsChunkPlugin中配置 name : [独立文件名]。

当然也可以不用在入口定义,如vue-cli 就是在 在CommonsChunk中配置了minChunks。我们的第三方模块都是通过npm 安装在node_modules 目录下,我们可以通过minChunks 判断模块路径是否含有node_module 来返回true 或 false,前文有介绍minChunks的含义。配置如下:

entry: {
    a: __dirname +'/app/a.js', // **注意** 入口没定义vendor 
  },
  output: {
    path: __dirname +'/dist',
    filename: '[name].[chunkhash:6].js',
    chunkFilename: '[name].[id].[chunkhash:6].js'
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function(module) {
        let flag =  module.context && module.context.indexOf('node_modules') !== -1;
        console.log(module.context, flag);
        return flag;
      }
    }),
    new HtmlWebpackPlugin({
      template: __dirname +'/app/index.html'
    })
  ]

上述两种方式,对于多页面还是单页面都是可应用的。但是现在的问题是每次入口文件 a.js 修改之后都会造成 vendor重新打包。那么如何解决这个问题呢。

manifest 处理第三方模块应用

我们将 a.js 做一个简单修改:

// 原来
-  console.log('unique:' +arr);
// 修改后
+   console.log(arr);

clipboard.png

重新打包发现vendor的hash变化了相当于重新打包了underscore,解决的方法是利用一个 manifest 来记录 vendor 的 id ,如果vendor没改变,则不需要重新打包。这就有两种解决方式 :

1. 利用manifest.js

利用CommonsChunkPlugin的chunks特性,提取出 webpack定义的异步加载代码,配置如下:

entry: {
  a: __dirname +'/app/a.js',
},
output: {
  path: __dirname +'/dist',
  filename: '[name].[chunkhash:6].js',
  chunkFilename: '[name].[id].[chunkhash:6].js'
},
plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks: function(module) {
      let flag =  module.context && module.context.indexOf('node_modules') !== -1;
      console.log(module.context, flag);
      return flag;
    }
  }),
  new webpack.optimize.CommonsChunkPlugin({
    name: 'manifest',
    chunks: ['vendor'],
  }),
  new HtmlWebpackPlugin({
    template: __dirname +'/app/index.html'
  })
]

还是修改了 a.js 之后发现 vendor的 hash 值没有变化,如下图:

clipboard.png

这里要注意的是chunks: [ 独立文件名 ]。但是,又有但是,要是这么就配置没问题了,就不能叫做玄学了,修改 a.js 的内部代码没问题,如果修改了 require 的模块引入,vendor的hash又有变化了,当然我们可以尽量避免修改文件的依赖引入,但是终归不是最完美的方式。那么终极解决方法是什么呢?DllReferencePlugin,DllPlugin。

2. 利用DllReferencePlugin,DllPlugin

既然动态打包的时候建立 manifest 不行,那么能不能直接把他打包成一个纯净的依赖库,本身无法运行,只是让我们的app 来引入。

那么我们需要完成两步,先webpack.DllPlugin打包dll(纯净的第三方独立文件),然后用DllReferencePlugin 在我们的应用中引用,这样的好处是如果下一个项目还是使用一样的依赖比如react react-dom react-router,可以直接引入这个dll。

配置文件如下:

  entry: {
    vendor: ['underscore']
  },
  output: {
    path: __dirname +'/dist',
    filename: '[name].js',
    library: '[name]',
  },
  plugins: [
    new webpack.DllPlugin({
      path: __dirname +'/dist/manifest.json',
      name: '[name]',
      context: __dirname,
    }),
  ],

clipboard.png

根据上述配置打包结果如上图,dist目录下现在有一个vender.js 和 manifest.json 注意这里输出的路径配置。DllPlugin配置介绍如下:

配置项介绍
pathpath 是 manifest.json 文件的输出路径,这个文件会用于后续的业务代码打包;
namename 是 dll 暴露的对象名,要跟 output.library 保持一致;
contextcontext 是解析包路径的上下文,这个要跟接下来配置的 webpack.config.js 一致。

之后在我们的应用中引入中,配置如下:

  entry: {
    a: __dirname +'/app/a.js',
  },
  output: {
    path: __dirname +'/dist',
    filename: '[name].[chunkhash:6].js',
    chunkFilename: '[name].[id].[chunkhash:6].js'
  },
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      manifest: require('./dist/manifest.json'),
    }),
    new HtmlWebpackPlugin({
      template: __dirname +'/app/index.html'
    })
  ]

clipboard.png

根据上述配置打包得到a.3e6285.js index.html 如上图,浏览器中打开index.html会显示
Uncaught ReferenceError: vendor is not defined

这里需要在 index.html 中 a.3e6285.js 插入 script 标签

<script type="text/javascript" src="vendor.js" ></script>
<script type="text/javascript" src="a.3e6285.js"></script>

再打开index.html 可以控制台打印出了数组去重的结果。插入标签的这一步可以在打包好独立文件之前,就在模板html 中插入。

到了这里,提取第三方模块的方法,避免重复打包的方法都介绍完毕了。接下来是配置提取自己编写的公共模块方法。

提取项目公共模块

单页面应用的公共模块没有必要提取出单独的文件,因为不必考虑复用的情况。但是对于打包生成的文件过大,我们又想分离出几个模块有需要的时候才加载,其实这并不是提取公共模块,而是代码分割,通过:

require.ensure(dependencies: String[], callback: function(require), chunkName: String)

在callback中定义的 require的模块将会独立打包,并且插入在 html 的head标签,这里就不做更多介绍了。

多页面应用是有必要抽取公共模块的,比如a.js 引用了lib1, b.js 也引用了 lib1 那么lib1,那么我们肯定希望在提取出 lib1 同时还可以提取出第三方库,配置文件如下:

// a.js 
let _ = require('underscore');
let lib1 = require('./lib1');
console.log('this is entry_a import lib1');

let arr = _.filter([1,2,3,2,3,3,5,5], (v, i, self) => self.indexOf(v) === i);
console.log(arr);

// b.js
require('./lib1');
var b = 'b';

console.log('this is entry_b import lib1');

// webpack.config.js
  entry: {
    a: __dirname +'/app/a.js',
    b: __dirname +'/app/b.js',
    vendor: ['underscore'],
  },
  output: {
    path: __dirname +'/dist',
    filename: '[name].[chunkhash:6].js',
    chunkFilename: '[name].[id].[chunkhash:6].js'
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: ['chunk', 'vendor'],
      minChunks: 2,
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      chunks: ['vendor']
    }),
    new HtmlWebpackPlugin({
      template: __dirname +'/app/index.html',
      filename: __dirname +'/dist/a.html',
      chunks: ['a', 'chunk', 'vendor', 'manifest'],
    }),
    new HtmlWebpackPlugin({
      template: __dirname +'/app/index.html',
      filename: __dirname +'/dist/b.html',
      chunks: ['b', 'chunk', 'vendor', 'manifest'],
    }),
  ]
}

通过打包后发现生成了如下文件:

clipboard.png

可以明确看出生成了chunk.d09623.js 而且 其中就是我们的lib1.js 的库的代码。这里要注意的是Commons.ChunkPlugin的配置 当name 给定数组之后从入口文件中选取 共同引用超过 minChunks 次数的模块打包进name 数组的第一个模块,然后name 数组后面的块 'vendor' 依次打包(查找entry里的key,没有找到相关的key就生成一个空的块),最后一个块包含webpack生成的在浏览器上使用各个块的加载代码,所以插入到页面中最后一个块要最先加载,加载顺序由name数组自右向左

这里我们使用manifest 去提取了 webpackJsonp 的加载代码,为了防止重复打包库文件,这在前文已经提到过。所以vendor中的加载代码在mainfest.js 中,修改a.js 的console.log, 重新打包后的文件可以发现chunk.d0962e.js, vendor.98054b.js都没有重新打包

clipboard.png

所以总结来讲就是多入口配置CommonsChunk

    new webpack.optimize.CommonsChunkPlugin({
      name: ['生成的项目公共模块文件名', '第三方模块文件名'],
      minChunks: 2,
    }),
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
webpack 面试题的问题有很多,我将为你提供一些常见的问题和对应的答案供参考: 1. 请解释一下 webpack 的作用是什么? webpack 是一个现代的 JavaScript 应用程序的静态模块打包工具。它将整个应用程序视为一个依赖关系图,通过分析应用程序的模块之间的依赖关系,并根据配置文件中的设置将它们打包成一个或多个静态资源文件。 2. 请解释一下 webpack 中的 loader 和 plugin 的作用是什么? loader 用于处理非 JavaScript 文件,将它们转换为 webpack 可以识别的模块。plugin 则用于扩展 webpack 的功能,它可以在 webpack 构建过程的不同阶段执行一些额外的任务。 3. webpack 的构建流程是怎样的? webpack 的构建流程可以简要概括为以下几个步骤: - 读取配置文件 - 解析入口文件及其依赖 - 根据模块的类型和配置的规则,应用不同的 loader 进行转换 - 将模块组合成 chunk - 根据配置文件中的设置,将 chunk 转换成静态资源文件 4. 请列举一些常见的 loader 和 plugin,并说明它们的作用。 常见的 loader: - babel-loader:将 ES6+ 代码转换为兼容的 JavaScript 代码 - style-loader 和 css-loader:用于处理样式文件,将 CSS 代码转换为可在浏览器中运行的形式 - file-loader 和 url-loader:处理图片和字体文件,将它们转换为 URL 地址 常见的 plugin: - HtmlWebpackPlugin:用于自动生成 HTML 文件,并将打包后的资源自动引入 - MiniCssExtractPlugin:将 CSS 代码从打包后的 JS 文件提取出来,生成独立的 CSS 文件 - CleanWebpackPlugin:在每次构建之前清理输出目录 5. webpack 的热更新是如何实现的? webpack 的热更新是通过 webpack-dev-server 提供的 HMR(Hot Module Replacement)功能实现的。它会在应用程序运行时监测文件的变化,并将变化的部分替换到运行中的应用程序中,从而实现实时更新。 6. 如何优化前端性能和体验? 通过以下几个方面可以优化前端性能和体验: - 使用代码分割和按需加载,减少首次加载时间 - 压缩和混淆代码,减小文件体积 - 使用缓存策略,减少重复加载 - 使用图片压缩和懒加载,减小图片体积和提高加载速度 - 使用 webpack 的优化功能,如 Tree Shaking 和 Scope Hoisting 7. 如何提高 webpack 的构建速度? 可以通过以下方法提高 webpack 的构建速度: - 使用合适的 loader 和 plugin,避免不必要的转换和处理 - 使用缓存,避免重复构建 - 多线程并行构建,使用 ParallelUglifyPlugin 或 HappyPack - 使用 DLLPlugin 和 DllReferencePlugin,将不常变动的代码预先打包,减少构建时间

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值