Webpack 4 配置最棒实行彩民之家高手论坛

2019-10-11 12:57 来源:未知

webpackChunkName

在 webpack2.4.0 版本之后可以自定义异步 chunk 的名字了,例如:

import(/* webpackChunkName: "my-chunk-name" */ "module");

1
import(/* webpackChunkName: "my-chunk-name" */ "module");

我们在结合 vue 的懒加载可以这样写。

{ path: '/test', component: () => import(/* webpackChunkName: "test" */ '@/views/test') },

1
2
3
4
{
    path: '/test',
    component: () => import(/* webpackChunkName: "test" */ '@/views/test')
  },

打包之后就生成了名为 test的 chunk 文件。

彩民之家高手论坛 1

chunk 有了 name 之后就可以解决NamedChunksPlugin没有 name 的情况下的 bug 了。查看打包后的代码我们发现 chunkId 就不再是一个简单的自增 id 了。

不过这种写法还是有弊端的,首先你需要手动编写每一个 chunk 的 name,同时还需要保证它的唯一性,当页面一多,维护起来还是很麻烦的。这就违背了程序员的原则:能偷懒就偷懒。

所以有什么办法可以自动生成一个 name 给 chunk 么 ?查看 webpack 源码我们发现了NamedChunksPlugin其实可以自定义 nameResolver 的。

Webpack 4 配置最佳实践

2018/06/22 · JavaScript · webpack

原文出处: Zindex   

Webpack 4 发布已经有一段时间了。Webpack 的版本号已经来到了 4.12.x。但因为 Webpack 官方还没有完成迁移指南,在文档层面上还有所欠缺,大部分人对升级 Webpack 还是一头雾水。

不过 Webpack 的开发团队已经写了一些零散的文章,官网上也有了新版配置的文档。社区中一些开发者也已经成功试水,升级到了 Webpack 4,并且总结成了博客。所以我也终于去了解了 Webpack 4 的具体情况。以下就是我对迁移到 Webpack 4 的一些经验。

本文的重点在:

  • Webpack 4 在配置上带来了哪些便利?要迁移需要修改配置文件的哪些内容?
  • 之前的 Webpack 配置最佳实践在 Webpack 4 这个版本,还适用吗?

多入口

其实上面的配置信息,对于单页面程序是没有问题的了,但是我们的rails,或者项目变大了,是多页面的。那么怎么处理呢?

  1. 每一个页面一个entry

Each file would look something like this:

var SignupView = require('./views/users/signup');
var view = new SignupView();
view.render({ el: $('[data-view-container]')});

The Rails view just needs to have an element on the page with the data-view-container
attribute, include two bundles, and we’re done. No <script>
tags necessary.
<%= javascript_include_tag 'users-signup-bundle' %>

  1. 一个入口,多个模块暴露到global(window)

利用webpack的exposeloader可以把它暴露到global上。

代码如下:

// entry.js
var $app = require('./app');

$app.views.users.Signup = require('./views/users/signup');
$app.views.users.Login = require('./views/users/login');

// app.js
module.exports = {
  views = {
    users: {}
  }
}

# ./views/users/signup.coffee
module.exports = class SignupView extends Backbone.View
  initialize: ->
    # do awesome things

配置loader

loaders: [
  {
    test: path.join(__dirname, 'app', 'frontend', 'javascripts', 'app.js'),
    loader: 'expose?$app'
  },
]

This will add the module.exports of the app.js module to window.$app, to be used by any <script> tag in a Rails view:

(function() {
  var view = new $app.views.users.Signup({ el: $('#signup-page') });
  view.render();
})();

博弈

其实优化就是一个博弈的过程,是让 a bundle 大一点还是 b? 是让首次加载快一点还是让 cache 的利用率高一点? 但有一点要切记,拆包的时候不要过分的追求颗粒化,什么都单独的打成一个 bundle,不然你一个页面可能需要加载十几个.js文件,如果你还不是HTTP/2的情况下,请求的阻塞还是很明显的(受限于浏览器并发请求数)。所以还是那句话资源的加载策略并没什么完全的方案,都需要结合自己的项目找到最合适的拆包策略。

比如支持HTTP/2的情况下,你可以使用 webpack4.15.0 新增的 maxSize,它能将你的chunkminSize的范围内更加合理的拆分,这样可以更好地利用HTTP/2来进行长缓存(在HTTP/2的情况下,缓存策略就和之前又不太一样了)。


区分开发和生产环境

大致的目录结构是这样的:

build config src

1
2
3
build
config
src

在 build 目录下有四个 webpack 的配置。分别是:

  • webpack.base.conf.js
  • webpack.dev.conf.js
  • webpack.prod.conf.js
  • webpack.test.conf.js

这分别对应开发、生产和测试环境的配置。其中 webpack.base.conf.js 是一些公共的配置项。我们使用 webpack-merge 把这些公共配置项和环境特定的配置项 merge 起来,成为一个完整的配置项。比如 webpack.dev.conf.js 中:

'use strict' const merge = require('webpack-merge') const baseWebpackConfig = require('./webpack.base.conf') const devWebpackConfig = merge(baseWebpackConfig, { ... })

1
2
3
4
5
6
7
'use strict'
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
 
const devWebpackConfig = merge(baseWebpackConfig, {
   ...
})

这三个环境不仅有一部分配置不同,更关键的是,每个配置中用 webpack.DefinePlugin 向代码注入了 NODE_ENV 这个环境变量。

这个变量在不同环境下有不同的值,比如 dev 环境下就是 development。这些环境变量的值是在 config 文件夹下的配置文件中定义的。Webpack 首先从配置文件中读取这个值,然后注入。比如这样:

build/webpack.dev.js

plugins: [ new webpack.DefinePlugin({ 'process.env': require('../config/dev.env.js') }), ]

1
2
3
4
5
plugins: [
  new webpack.DefinePlugin({
    'process.env': require('../config/dev.env.js')
  }),
]

config/dev.env.js

module.exports ={ NODE_ENV: '"development"' }

1
2
3
module.exports ={
  NODE_ENV: '"development"'
}

至于不同环境下环境变量具体的值,比如开发环境是 development,生产环境是 production,其实是大家约定俗成的。

框架、库的作者,或者是我们的业务代码里,都会有一些根据环境做判断,执行不同逻辑的代码,比如这样:

if (process.env.NODE_ENV !== 'production') { console.warn("error!") }

1
2
3
if (process.env.NODE_ENV !== 'production') {
  console.warn("error!")
}

这些代码会在代码压缩的时候被预执行一次,然后如果条件表达式的值是 true,那这个 true 分支里的内容就被移除了。这是一种编译时的死代码优化。这种区分不同的环境,并给环境变量设置不同的值的实践,让我们开启了编译时按环境对代码进行针对性优化的可能。

写一个helper来实现development和production对entry的调用

# app/helpers/application_helper.rb

def webpack_bundle_tag(bundle)
  src =
    if Rails.configuration.webpack[:use_manifest]
      manifest = Rails.configuration.webpack[:asset_manifest]
      filename = manifest[bundle]

      "#{compute_asset_host}/assets/#{filename}"
    else
      "#{compute_asset_host}/assets/#{bundle}-bundle"
    end

  javascript_include_tag(src)
end

其中的webpack-asset-manifest.json, 大概如下:

{
"common":"common-b343fccb2be9bef14648.js",
"ques":"ques-bundle-ad8e6456e397dd8e7144.js",
"activities":"activities-bundle-806617bb69dfc78f4772.js",
"pages":"pages-bundle-77b73a5a1a91cd3b92bd.js",
"pages_front":"pages_front-bundle-3e4ed8bdff3d2fc59a70.js"
}

所以可以利用这个来需找 entry name和bundle的文件的对应关系。

其中webpack-common-manifest.json,大概如下

{
"1":"ques-bundle-ad8e6456e397dd8e7144.js",
"2":"activities-bundle-806617bb69dfc78f4772.js",
"3":"pages-bundle-77b73a5a1a91cd3b92bd.js",
"4":"pages_front-bundle-3e4ed8bdff3d2fc59a70.js"
}

webpack会产生一个id为每一个entrypoint,默认webpack把这些ids存在common bundle中。但是问题是,不论什么时候你改变任何一个entrypoint的代码引发id的变化,那么common的代码都要更新,因此缓存就没有什么意义了。

而 ChunkManifestPlugin的作用就是不写在common中,而是写在外面的一个文件中,我们在rails中把它解析了并且绑到了window上的webpackBundleManifest变量,所以我们的webpack会自己去找这个变量.
所以我们的第二个helper就是来绑定这个变量,代码如下:

def webpack_manifest_script
  return '' unless Rails.configuration.webpack[:use_manifest]
  javascript_tag "window.webpackManifest = #{Rails.configuration.webpack[:common_manifest]}"
end

手摸手,带你用合理的姿势使用webpack4(下)

2018/08/26 · 基础技术 · webpack

原文出处: 华尔街见闻技术团队 - 花裤衩   

推荐先阅读 webpack 入门教程之后再来阅读本文。

  • Webpack 4 和单页应用入门
  • 手摸手,带你用合理的姿势使用 webpack4 (上)

本文为手摸手使用 webpack4(下),主要分为两部分:

  • 怎么合理的运用浏览器缓存
  • 怎么构建可靠的持久化缓存

Webpack 4 之前的 Webpack 最佳实践

这里以 Vue 官方的 Webpack 模板 vuejs-templates/webpack 为例,说说 Webpack 4 之前,社区里比较成熟的 Webpack 配置文件是怎样组织的。

准备工作

1.新建一个rails 项目

2.安装npm install webpack -g

3.安装bower npm install bower -g,由于webpack并没有要求我们必须使用什么包管理器所以可以使用npm bower

webpack records

很多人可能连这个配置项都没有注意过,不过早在 2015 年就已经被设计出来让你更好的利用 cache。官方文档

要使用它配置也很简单:

recordsPath: path.join(__dirname, "records.json");

1
recordsPath: path.join(__dirname, "records.json");

对,只要这一行代码就能开启这个选项,并打包的时候会自动生成一个 JSON 文件。它含有 webpack 的 records 记录 – 即「用于存储跨多次构建(across multiple builds)的模块标识符」的数据片段。可以使用此文件来跟踪在每次构建之间的模块变化。

大白话就是:等于每次构建都是基于上次构建的基础上进行的。它会先读取你上次的 chunk 和 module id 的信息之后再进行打包。所以这时候你再添加或者删除 chunk,并不会导致之前所说的乱序了。

简单看一下构建出来的 JSON 长啥样。

{ "modules": { "byIdentifier": { "demo/vendor.js": 0, "demo/vendor-two.js": 1, "demo/index.js": 2, .... }, "usedIds": { "0": 0, "1": 1, "2": 2, ... } }, "chunks": { "byName": { "vendor-two": 0, "vendor": 1, "entry": 2, "runtime": 3 }, "byBlocks": {}, "usedIds": [ 0, 1, 2 } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
  "modules": {
    "byIdentifier": {
      "demo/vendor.js": 0,
      "demo/vendor-two.js": 1,
      "demo/index.js": 2,
      ....
    },
    "usedIds": {
      "0": 0,
      "1": 1,
      "2": 2,
      ...
    }
  },
  "chunks": {
    "byName": {
      "vendor-two": 0,
      "vendor": 1,
      "entry": 2,
      "runtime": 3
    },
    "byBlocks": {},
    "usedIds": [
      0,
      1,
      2
  }
}

我们和之前一样,在路由里面添加一个懒加载的页面,打包对比后发现 id 并不会像之前那样按照遍历到的顺序插入了,而是基于之前的 id 依次累加了。一般新增页面都会在末尾填写一个新 id,删除 chunk 的话,会将原来代表 chunk 的 id,保留,但不会再使用。

彩民之家高手论坛 2

但这个方案不被大家知晓主要原因就是维护这个records.json比较麻烦。如果你是在本地打包运行webpack的话,你只要将records.json当做普通文件上传到githubgitlab或其它版本控制仓库。

但现在一般公司都会将打包放在 CI里面,用docker打包,这时候这份records.json存在哪里就是一个问题了。它不仅需要每次打包之前先读取你这份 json,打包完之后它还需要再更新这份 json,并且还要找地方存贮,为了下次构建再使用。你可以存在 git 中或者找一个服务器存,但存在什么地其它方都感觉怪怪的。

如果你使用 Circle CI可以使用它的store_artifacts,相关教程。

本人在使用了之后还是放弃了这个方案,使用成本略高。前端打包应该更加的纯粹,不需要依赖太多其它的东西。

开发和生产环境的区分

Webpack 4 引入了 mode 这个选项。这个选项的值可以是 development 或者 production。

设置了 mode 之后会把 process.env.NODE_ENV 也设置为 development 或者 production。然后在 production 模式下,会默认开启 UglifyJsPlugin 等等一堆插件。

Webpack 4 支持零配置使用,可以从命令行指定 entry 的位置,如果不指定,就是 src/index.js。mode 参数也可以从命令行参数传入。这样一些常用的生产环境打包优化都可以直接启用。

我们需要注意,Webpack 4 的零配置是有限度的,如果要加上自己想加的插件,或者要加多个 entry,还是需要一个配置文件。

虽然如此,Webpack 4 在各个方面都做了努力,努力让零配置可以做的事情更多。这种内置优化的方式使得我们在项目起步的时候,可以把主要精力放在业务开发上,等后期业务变复杂之后,才需要关注配置文件的编写。

在 Webpack 4 推出 mode 这个选项之前,如果想要为不同的开发环境打造不同的构建选项,我们只能通过建立多个 Webpack 配置且分别设置不同的环境变量值这种方式。这也是社区里的最佳实践。

Webpack 4 推出的 mode 选项,其实是一种对社区中最佳实践的吸收。这种思路我是很赞同的。开源项目来自于社区,在社区中成长,从社区中吸收养分,然后回报社区,这是一个良性循环。最近我在很多前端项目中都看到了类似的趋势。接下来要讲的其他几个 Webpack 4 的特性也是和社区的反馈离不开的。

那么上文中介绍的使用多个 Webpack 配置,以及手动环境变量注入的方式,是否在 Webpack 4 下就不适用了呢?其实不然。在Webpack 4 下,对于一个正经的项目,我们依然需要多个不同的配置文件。如果我们对为测试环境的打包做一些特殊处理,我们还需要在那个配置文件里用 webpack.DefinePlugin 手动注入 NODE_ENV 的值(比如 test)。

Webpack 4 下如果需要一个 test 环境,那 test 环境的 mode 也是 development。因为 mode 只有开发和生产两种,测试环境应该是属于开发阶段。

创建一个rake,当然用gulp或者grunt也可以

namespace :webpack do
  desc 'compile bundles using webpack'
  task :compile do
    cmd = 'webpack --config config/webpack/production.config.js --json'
    output = `#{cmd}`

    stats = JSON.parse(output)

    File.open('./public/assets/webpack-asset-manifest.json', 'w') do |f|
      f.write stats['assetsByChunkName'].to_json
    end
  end
end

其中的--json是让webpack返回一个json的结果。
其中的stats['assetsByChunkName']是一个entry name -> bundle name的json文件。

如下:

{
  "common": "common-4cdf0a22caf53cdc8e0e.js",
  "authenticated": "authenticated-bundle-2cc1d62d375d4f4ea6a0.js",
  "public":"public-bundle-a010df1e7c55d0fb8116.js"
}

Long term caching

持久化缓存其实是一个老生常谈的问题,前端发展到现在,缓存方案已经很成熟了。简单原理:

  • 针对 html 文件:不开启缓存,把 html 放到自己的服务器上,关闭服务器的缓存
  • 针对静态的 js,css,图片等文件:开启 cdn 和缓存,将静态资源上传到 cdn 服务商,我们可以对资源开启长期缓存,因为每个资源的路径都是独一无二的,所以不会导致资源被覆盖,保证线上用户访问的稳定性。
  • 每次发布更新的时候,先将静态资源(js, css, img) 传到 cdn 服务上,然后再上传 html 文件,这样既保证了老用户能否正常访问,又能让新用户看到新的页面。

相关文章 大公司里怎样开发和部署前端代码?

所以我们现在要做的就是要让 webpack 给静态资源生产一个可靠的 hash,让它能自动在合适的时候更新资源的 hash,
并且保证 hash 值的唯一性,即为每个打包后的资源生成一个独一无二的 hash 值,只要打包内容不一样,那么 hash 值就不一样。

其实 webpack 4 在持久化缓存这一块已经做得非常的不错了,但还是有一些欠缺,下面我们将要从这几个方面讨论这个问题。

  • RuntimeChunk(manifest)
  • Module vs Chunk
  • HashedModuleIdsPlugin
  • NamedChunksPlugin

Long-term caching

Long-term caching 这里,基本的操作和 Webpack 3 是一样的。不过 Webpack 3 的 Long-term caching 在操作的时候,有个小问题,这个问题是关于 chunk 内容和 hash 变化不一致的:

在公共代码 Vendor 内容不变的情况下,添加 entry,或者 external 依赖,或者异步模块的时候,Vendor 的 hash 会改变

之前 Webpack 官方的专栏里面有一篇文章讲这个问题:Predictable long term caching with Webpack。给出了一个解决方案。

这个方案的核心就是,Webpack 内部维护了一个自增的 id,每个 chunk 都有一个 id。所以当增加 entry 或者其他类型 chunk 的时候,id 就会变化,导致内容没有变化的 chunk 的 id 也发生了变化。

对此我们的应对方案是,使用 webpack.NamedChunksPlugin 把 chunk id 变为一个字符串标识符,这个字符包一般就是模块的相对路径。这样模块的 chunk id 就可以稳定下来。

彩民之家高手论坛 3

这里的 vendors1 就是 chunk id

HashedModuleIdsPlugin 的作用和 NamedChunksPlugin 是一样的,只不过 HashedModuleIdsPlugin 把根据模块相对路径生成的 hash 作为 chunk id,这样 chunk id 会更短。因此在生产中更推荐用 HashedModuleIdsPlugin。

这篇文章说还讲到,webpack.NamedChunksPlugin 只能对普通的 Webpack 模块起作用,异步模块,external 模块是不会起作用的。

异步模块可以在 import 的时候加上 chunkName 的注释,比如这样:import(/ webpackChunkName: “lodash” / ‘lodash’).then() 这样就有 Name 了

所以我们需要再使用一个插件:name-all-modules-plugin

这个插件中用到一些老的 API,Webpack 4 会发出警告,这个 pr 有新的版本,不过作者不一定会 merge。我们使用的时候可以直接 copy 这个插件的代码到我们的 Webpack 配置里面。

做了这些工作之后,我们的 Vendor 的 ChunkId 就再也不会发生不该发生的变化了。

source map的作用

上边的配置信息用webpack打包会自动产生一个bundle.map.js,这个文件就是 source map 文件。 这个source map的作用非常的大,我们打包之后下载一个bundle.js文件就好了不用再下载10个或20个文件(这样非常的慢),可是如果发生错误了,怎么办,有了source map我们可以看到这些错误在原来的individual files。

自定义 nameResolver

NamedChunksPlugin支持自己写 nameResolver 的规则的。但目前大部分相关的文章里的自定义函数是不适合 webpack4 ,而且在结合 vue 的情况下还会报错。

社区旧方案:

new webpack.NamedChunksPlugin(chunk => { if (chunk.name) { return chunk.name; } return chunk.modules.map(m => path.relative(m.context, m.request)).join("_"); });

1
2
3
4
5
6
new webpack.NamedChunksPlugin(chunk => {
  if (chunk.name) {
    return chunk.name;
  }
  return chunk.modules.map(m => path.relative(m.context, m.request)).join("_");
});

适配 webpack4 和 vue 的新实现方案:

new webpack.NamedChunksPlugin(chunk => { if (chunk.name) { return chunk.name; } return Array.from(chunk.modulesIterable, m => m.id).join("_"); });

1
2
3
4
5
6
new webpack.NamedChunksPlugin(chunk => {
  if (chunk.name) {
    return chunk.name;
  }
  return Array.from(chunk.modulesIterable, m => m.id).join("_");
});

当然这个方案还是有一些弊端的因为 id 会可能很长,如果一个 chunk 依赖了很多个 module 的话,id 可能有几十位,所以我们还需要缩短一下它的长度。我们首先将拼接起来的 id hash 以下,而且要保证 hash 的结果位数也能太长,浪费字节,但太短又容易发生碰撞,所以最后我们我们选择 4 位长度,并且手动用 Set 做一下碰撞校验,发生碰撞的情况下位数加 1,直到碰撞为止。详细代码如下:

const seen = new Set(); const nameLength = 4; new webpack.NamedChunksPlugin(chunk => { if (chunk.name) { return chunk.name; } const modules = Array.from(chunk.modulesIterable); if (modules.length > 1) { const hash = require("hash-sum"); const joinedHash = hash(modules.map(m => m.id).join("_")); let len = nameLength; while (seen.has(joinedHash.substr(0, len))) len ; seen.add(joinedHash.substr(0, len)); return `chunk-${joinedHash.substr(0, len)}`; } else { return modules[0].id; } });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const seen = new Set();
const nameLength = 4;
 
new webpack.NamedChunksPlugin(chunk => {
  if (chunk.name) {
    return chunk.name;
  }
  const modules = Array.from(chunk.modulesIterable);
  if (modules.length > 1) {
    const hash = require("hash-sum");
    const joinedHash = hash(modules.map(m => m.id).join("_"));
    let len = nameLength;
    while (seen.has(joinedHash.substr(0, len))) len ;
    seen.add(joinedHash.substr(0, len));
    return `chunk-${joinedHash.substr(0, len)}`;
  } else {
    return modules[0].id;
  }
});

我给 vue-cli 官方也提了一个相关
issue尤雨溪最后也采纳了这个方案。
所以如果你现在下载最新 vue-cli@3上面啰嗦了半天的东西,其实都已经默认配置好了(但作者本人为了找到这个 hack 方法整整花了两天时间 o(╥﹏╥)o)。

目前测试了一段时间没发现有什么问题。不过有一点不是很理解,不知道 webpack 出于什么样的原因,官方一直没有修复这个问题?可能是在等 webpack5 的时候放大招吧。

Webpack 4 的变与不变

Webpack 4 这个版本的 API 有一些 breaking change,但不代表说这个版本就发生了翻天覆地的变化。其实变化的点只有几个。而且只要你仔细了解了这些变化,你一定会拍手叫好。

迁移到 Webpack 4 也只需要检查一下 checklist,看看这些点是否都覆盖到了,就可以了。

3.能用commonjs的规范来实现模块依赖

总结

拆包策略:

  • 基础类库 chunk-libs
  • UI 组件库 chunk-elementUI
  • 自定义共用组件/函数 chunk-commons
  • 低频组件 chunk-eachrts/chunk-xlsx
  • 业务代码 lazy-loading xxxx.js

持久化缓存:

  • 使用 runtimeChunk 提取 manifest,使用 script-ext-html-webpack-plugin等插件内联到index.html减少请求
  • 使用 HashedModuleIdsPlugin 固定 moduleId
  • 使用 NamedChunkPlugin结合自定义 nameResolver 来固定 chunkId

上述说的问题大部分在 webpack 官方文档都没明确指出,唯一可以参考的就是这份 cache 文档,在刚更新 webpack4 的时候,我以为官方已经将 id 不能固定的问题解决了,但现实是残酷的,结果并不理想。不过作者也在很多的 issue 中说他正在着手优化 long term caching

We plan to add another way to assign module/chunk ids for long term caching, but this is not ready to be told yet.

在 webpack 的 issue 和源码中也经常见到 Long term caching will be improved in webpack@5TODO webpack 5 xxxx这样的代码注释。这让我对webpack 5很期待。真心希望webpack 5能真正的解决前面几个问题,并且让它更加的out-of-the-box,更加的简单和智能,就像webpack 4optimization.splitChunks,你基本不用做什么,它就能很好的帮你拆分好bundle包,同时又给你非常的自由发挥空间。

Code Splitting

Webpack 4 下还有一个大改动,就是废弃了 CommonsChunkPlugin,引入了 optimization.splitChunks 这个选项。

optimization.splitChunks 默认是不用设置的。如果 mode 是 production,那 Webpack 4 就会开启 Code Splitting。

默认 Webpack 4 只会对按需加载的代码做分割。如果我们需要配置初始加载的代码也加入到代码分割中,可以设置 splitChunks.chunks 为 ‘all’。

Webpack 4 的 Code Splitting 最大的特点就是配置简单(0配置起步),和基于内置规则自动拆分。内置的代码切分的规则是这样的:

  • 新 bundle 被两个及以上模块引用,或者来自 node_modules
  • 新 bundle 大于 30kb (压缩之前)
  • 异步加载并发加载的 bundle 数不能大于 5 个
  • 初始加载的 bundle 数不能大于 3 个

简单的说,Webpack 会把代码中的公共模块自动抽出来,变成一个包,前提是这个包大于 30kb,不然 Webpack 是不会抽出公共代码的,因为增加一次请求的成本是不能忽视的。

具体的业务场景下,具体的拆分逻辑,可以看 SplitChunksPlugin 的文档以及 webpack 4: Code Splitting, chunk graph and the splitChunks optimization 这篇博客。这两篇文章基本罗列了所有可能出现的情况。

如果是普通的应用,Webpack 4 内置的规则就足够了。

如果是特殊的需求,Webpack 4 的 optimization.splitChunks API也可以满足。

splitChunks 有一个参数叫 cacheGroups,这个参数类似之前的 CommonChunks 实例。cacheGroups 里每个对象就是一个用户定义的 chunk。

之前我们讲到,Webpack 4 内置有一套代码分割的规则,那用户也可以自定义 cacheGroups,也就是自定义 chunk。那一个 module 应该被抽到哪个 chunk 呢?这是由 cacheGroups 的抽取范围控制的。每个 cacheGroups 都可以定义自己抽取模块的范围,也就是哪些文件中的公共代码会抽取到自己这个 chunk 中。不同的 cacheGroups 之间的模块范围如果有交集,我们可以用 priority 属性控制优先级。Webpack 4 默认的抽取的优先级是最低的,所以模块会优先被抽取到用户的自定义 chunk 中。

splitChunksPlugin 提供了两种控制 chunk 抽取模块范围的方式。一种是 test 属性。这个属性可以传入字符串、正则或者函数,所有的 module 都会去匹配 test 传入的条件,如果条件符合,就被纳入这个 chunk 的备选模块范围。如果我们传入的条件是字符串或者正则,那匹配的流程是这样的:首先匹配 module 的路径,然后匹配 module 之前所在 chunk 的 name。

比如我们想把所有 node_modules 中引入的模块打包成一个模块:

vendors1: { test: /[/]node_modules[/]/, name: 'vendor', chunks: 'all', }

1
2
3
4
5
  vendors1: {
    test: /[/]node_modules[/]/,
    name: 'vendor',
    chunks: 'all',
  }

因为从 node_modules 中加载的依赖路径中都带有 node_modules,所以这个正则会匹配所有从 node_modules 中加载的依赖。

test 属性可以以 module 为单位控制 chunk 的抽取范围,是一种细粒度比较小的方式。splitChunksPlugin 的第二种控制抽取模块范围的方式就是 chunks 属性。chunks 可以是字符串,比如 ‘all’|’async’|’initial’,分别代表了全部 chunk,按需加载的 chunk 以及初始加载的 chunk。chunks 也可以是一个函数,在这个函数里我们可以拿到 chunk.name。这给了我们通过入口来分割代码的能力。这是一种细粒度比较大的方式,以 chunk 为单位。

举个例子,比如我们有 a, b, c 三个入口。我们希望 a,b 的公共代码单独打包为 common。也就是说 c 的代码不参与公共代码的分割。

我们可以定义一个 cacheGroups,然后设置 chunks 属性为一个函数,这个函数负责过滤这个 cacheGroups 包含的 chunk 是哪些。示例代码如下:

optimization: { splitChunks: { cacheGroups: { common: { chunks(chunk) { return chunk.name !== 'c'; }, name: 'common', minChunks: 2, }, }, }, },

1
2
3
4
5
6
7
8
9
10
11
12
13
  optimization: {
    splitChunks: {
      cacheGroups: {
        common: {
          chunks(chunk) {
            return chunk.name !== 'c';
          },
          name: 'common',
          minChunks: 2,
        },
      },
    },
  },

上面配置的意思就是:我们想把 a,b 入口中的公共代码单独打包为一个名为 common 的 chunk。使用 chunk.name,我们可以轻松的完成这个需求。

在上面的情况中,我们知道 chunks 属性可以用来按入口切分几组公共代码。现在我们来看一个稍微复杂一些的情况:对不同分组入口中引入的 node_modules 中的依赖进行分组。

比如我们有 a, b, c, d 四个入口。我们希望 a,b 的依赖打包为 vendor1,c, d 的依赖打包为 vendor2。

这个需求要求我们对入口和模块都做过滤,所以我们需要使用 test 属性这个细粒度比较小的方式。我们的思路就是,写两个 cacheGroup,一个 cacheGroup 的判断条件是:如果 module 在 a 或者 b chunk 被引入,并且 module 的路径包含 node_modules,那这个 module 就应该被打包到 vendors1 中。 vendors2 同理。

vendors1: { test: module => { for (const chunk of module.chunksIterable) { if (chunk.name && /(a|b)/.test(chunk.name)) { if (module.nameForCondition() && /[/]node_modules[/]/.test(module.nameForCondition())) { return true; } } } return false; }, minChunks: 2, name: 'vendors1', chunks: 'all', }, vendors2: { test: module => { for (const chunk of module.chunksIterable) { if (chunk.name && /(c|d)/.test(chunk.name)) { if (module.nameForCondition() && /[/]node_modules[/]/.test(module.nameForCondition())) { return true; } } } return false; }, minChunks: 2, name: 'vendors2', chunks: 'all', }, };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  vendors1: {
    test: module => {
      for (const chunk of module.chunksIterable) {
            if (chunk.name && /(a|b)/.test(chunk.name)) {
                if (module.nameForCondition() && /[/]node_modules[/]/.test(module.nameForCondition())) {
                 return true;
             }
            }
       }
      return false;
    },
    minChunks: 2,
    name: 'vendors1',
    chunks: 'all',
  },
  vendors2: {
    test: module => {
      for (const chunk of module.chunksIterable) {
            if (chunk.name && /(c|d)/.test(chunk.name)) {
                if (module.nameForCondition() && /[/]node_modules[/]/.test(module.nameForCondition())) {
                 return true;
             }
            }
       }
      return false;
    },
    minChunks: 2,
    name: 'vendors2',
    chunks: 'all',
  },
};

写webpack的基本配置信息

文件名用webpack.config.js

var path = require('path');
var webpack = require('webpack');

var config = {
  entry: './app/frontend/javascript/entry.js',
  context: __dir,
}

context: 指定了the base directory (绝对路径的位置)
entry: 打包的入口(the entry point for the bundle),可以接受一个单独的文件,可以接受数组,例如:

config.entry = ['./entry1.js', './entry2.js']

所有的文件都会启动时加载,最后一个作为输出;如果传入一个对象,那么会有多个bundles被创建,key是片段的名称,value是文件的位置或者数组,例如:

{
  entry: {
    page1: './page1',
    page2: ['./entry1', 'entry2'],
  },
  output: {
    filename: "[name].bundle.js",
    chunkFilename: '[id].bundle.js',
  }
}

后面这个文件会非常的复杂,这里我们只写一些最基本的必须的配置,后面会写入越来越多的配置信息。
现在我们只需要一个入口文件,所以可以直接写一个文件地址,多个入口的时候,entry可以接受数组和对象。(webpack找入口文件来开始打包确定依赖关系,没有在入口文件中确定的文件,是不会被打包的)
下一个属性,我们要写output,确定打包好的文件输出到哪里

config.output = {
  path: path.join(__dirname, 'app', 'asset', 'javascripts'),
  filename: 'bunlde.js',
  publicPath: '/assets',
}

filename: 输出的文件的名称,这里必须是相对的路径
path:输出文件的位置

多个entries时候,filename对每一个entry有一个唯一的name,有下面这几个选项:

[name] 被chunk的name替换
[hash]被compilation的hash替换
[chunkhash]被chunk的hash替换

output的publicPath指定了这些资源在浏览器中被调用的公共url地址。
这里在浏览器中引用我们的资源只需要<script src='/assets/....'/>
output的chunkFilename是非输入点的文件片段(non-entry chunks as relative to the output.path)

[id]
is replaced by the id of the chunk.
[name]
is replaced by the name of the chunk (or with the id when the chunk has no name).
[hash]
is replaced by the hash of the compilation.
[chunkhash]
is replaced by the hash of the chunk.

现在我们要添加resolve属性, resolve属性是告诉webpack怎么寻找打包文件中的依赖关系,具体配置如下:

config.resolve = {

  extensions: ['', 'js'],
  modulesDirectories: ['node_modules', 'bower_components']
}

最后是plugin参数

config.plugin = {
  new webpack.ResolverPlugin([
    new webpack.ResulvePlugin.DiretoryDescriptionFilePlugin('.bower.json', ['main'])
  ])
}

这个配置告诉webpack对于bower的包怎么找entrypoints,因为可能没有package.json

执行命令

webpack -d --display-reasons --colors -- display-chunks --progress

彩民之家高手论坛 4

Paste_Image.png

现在rails的assets/scripts中就已经生成了相关的chunk.
在rails中应用通过<%= javascript_include_tag 'bundle'%>

RuntimeChunk(manifest)

webpack 4 提供了 runtimeChunk 能让我们方便的提取 manifest,以前我们需要这样配置

new webpack.optimize.CommonsChunkPlugin({ name: "manifest", minChunks: Infinity });

1
2
3
4
new webpack.optimize.CommonsChunkPlugin({
  name: "manifest",
  minChunks: Infinity
});

现在只要一行配置就可以了

{ runtimeChunk: true; }

1
2
3
{
  runtimeChunk: true;
}

它的作用是将包含chunks 映射关系的 list单独从 app.js里提取出来,因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以你每次改动都会影响它,如果不将它提取出来的话,等于app.js每次都会改变。缓存就失效了。

单独抽离 runtimeChunk 之后,每次打包都会生成一个runtimeChunk.xxx.js。(默认叫这名字,可自行修改)

彩民之家高手论坛 5

优化

其实我们发现打包生成的 runtime.js非常的小,gzip 之后一般只有几 kb,但这个文件又经常会改变,我们每次都需要重新请求它,它的 http 耗时远大于它的执行时间了,所以建议不要将它单独拆包,而是将它内联到我们的 index.html 之中(index.html 本来每次打包都会变)。

这里我选用了 script-ext-html-webpack-plugin,主要是因为它还支持preloadprefetch,正好需要就不想再多引用一个插件了,你完全可以使用 inline-manifest-webpack-plugin或者 assets-webpack-plugin等来实现相同的效果。

const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin"); // 注意一定要在HtmlWebpackPlugin之后引用 // inline 的name 和你 runtimeChunk 的 name保持一致 new ScriptExtHtmlWebpackPlugin({ //`runtime` must same as runtimeChunk name. default is `runtime` inline: /runtime..*.js$/ });

1
2
3
4
5
6
7
8
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
 
// 注意一定要在HtmlWebpackPlugin之后引用
// inline 的name 和你 runtimeChunk 的 name保持一致
new ScriptExtHtmlWebpackPlugin({
  //`runtime` must same as runtimeChunk name. default is `runtime`
  inline: /runtime..*.js$/
});

Code Splitting && Long-term caching

Code Splitting 一般需要做这些事情:

  • 为 Vendor 单独打包(Vendor 指第三方的库或者公共的基础组件,因为 Vendor 的变化比较少,单独打包利于缓存)
  • 为 Manifest (Webpack 的 Runtime 代码)单独打包
  • 为不同入口的公共业务代码打包(同理,也是为了缓存和加载速度)
  • 为异步加载的代码打一个公共的包

Code Splitting 一般是通过配置 CommonsChunkPlugin 来完成的。一个典型的配置如下,分别为 vendor、manifest 和 vendor-async 配置了 CommonsChunkPlugin。

new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks (module) { return ( module.resource && /.js$/.test(module.resource) && module.resource.indexOf( path.join(__dirname, '../node_modules') ) === 0 ) } }), new webpack.optimize.CommonsChunkPlugin({ name: 'manifest', minChunks: Infinity }), new webpack.optimize.CommonsChunkPlugin({ name: 'app', async: 'vendor-async', children: true, minChunks: 3 }),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks (module) {
        return (
          module.resource &&
          /.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
 
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),
 
    new webpack.optimize.CommonsChunkPlugin({
      name: 'app',
      async: 'vendor-async',
      children: true,
      minChunks: 3
    }),

CommonsChunkPlugin 的特点就是配置比较难懂,大家的配置往往是复制过来的,这些代码基本上成了模板代码(boilerplate)。如果 Code Splitting 的要求简单倒好,如果有比较特殊的要求,比如把不同入口的 vendor 打不同的包,那就很难配置了。总的来说配置 Code Splitting 是一个比较痛苦的事情。

而 Long-term caching 策略是这样的:给静态文件一个很长的缓存过期时间,比如一年。然后在给文件名里加上一个 hash,每次构建时,当文件内容改变时,文件名中的 hash 也会改变。浏览器在根据文件名作为文件的标识,所以当 hash 改变时,浏览器就会重新加载这个文件。

Webpack 的 Output 选项中可以配置文件名的 hash,比如这样:

output: { path: config.build.assetsRoot, filename: utils.assetsPath('js/[name].[chunkhash].js'), chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') },

1
2
3
4
5
output: {
  path: config.build.assetsRoot,
  filename: utils.assetsPath('js/[name].[chunkhash].js'),
  chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},

loading coffeescript 和 其他编译语言

我们可以loader来自动的翻译这些语言。
当然,所有的loader都一样可以通过在require中处理,也可以通过设置config.js来的module.loaders来设置。
首先安装coffee loader
npm install coffee-loader --save-dev
现在我们可以设置resolve.extensions来使用coffeescript不需要后缀。

extensions: ['', '.js', '.coffee']

config中的module.loaders可以添加一个coffeescript的配置信息,如下

config.module = {
  loaders: [
    {
      test: /.coffee$/,
      loader: 'coffee-loader'
    }
  ]
}

Module vs Chunk

我们经常看到xxxModuleIdsPluginxxxChunksPlugin,所以在 webpack 中 modulechunk到底是一个怎么样的关系呢?

  • chunk: 是指代码中引用的文件(如:js、css、图片等)会根据配置合并为一个或多个包,我们称一个包为 chunk。
  • module: 是指将代码按照功能拆分,分解成离散功能块。拆分后的代码块就叫做 module。可以简单的理解为一个 export/import 就是一个 module。

每个 chunk 包可含多个 module。 比如:

//9.xxxxxxxxx.js //chunk id为 9 ,包含了Vc2m和JFUb两个module (window.webpackJsonp = window.webpackJsonp || []).push([ [9], { Vc2m: function(e, t, l) {}, JFUb: function(e, t, l) {} } ]);

1
2
3
4
5
6
7
8
9
10
//9.xxxxxxxxx.js
 
//chunk id为 9 ,包含了Vc2m和JFUb两个module
(window.webpackJsonp = window.webpackJsonp || []).push([
  [9],
  {
    Vc2m: function(e, t, l) {},
    JFUb: function(e, t, l) {}
  }
]);

一个module还能跨chunk引用另一个module,比如我想在app.js里面需要引用 chunkId13的模块2700可以这样引用:

return n.e(13).then(n.bind(null, "27OO"));

1
return n.e(13).then(n.bind(null, "27OO"));

第三方库 build 的选择

在 Webpack 3 时代,我们需要在生产环境的的 Webpack 配置里给第三方库设置 alias,把这个库的路径设置为 production build 文件的路径。以此来引入生产版本的依赖。

比如这样:

resolve: { extensions: [".js", ".vue", ".json"], alias: { vue$: "vue/dist/vue.runtime.min.js" } },

1
2
3
4
5
6
resolve: {
  extensions: [".js", ".vue", ".json"],
  alias: {
    vue$: "vue/dist/vue.runtime.min.js"
  }
},

在 Webpack 4 引入了 mode 之后,对于部分依赖,我们可以不用配置 alias,比如 React。React 的入口文件是这样的:

'use strict'; if (process.env.NODE_ENV === 'production') { module.exports = require('./cjs/react.production.min.js'); } else { module.exports = require('./cjs/react.development.js'); }

1
2
3
4
5
6
7
'use strict';
 
if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}

这样就实现了 0 配置自动选择生产 build。

但大部分的第三库并没有做这个入口的环境判断。所以这种情况下我们还是需要手动配置 alias。

和rails的asset对比,它有很多的优势。

优化分包策略

就拿 vue-element-admin 来说,它是一个基于 Element-UI 的管理后台,所以它会用到如 echartsxlsxdropzone等各种第三方插件,同时又由于是管理后台,所以本身自己也会写很多共用组件,比如各种封装好的搜索查询组件,共用的业务模块等等,如果按照默认的拆包规则,结果就不怎么完美了。

如第一张图所示,由于element-uientry入口文件中被引入并且被大量页面共用,所以它默认会被打包到 app.js 之中。这样做是不合理的,因为app.js里还含有你的router 路由声明store 全局状态utils 公共函数icons 图标等等这些全局共用的东西。

但除了element-ui,其它这些又是平时开发中经常会修改的东西,比如我新增了一个全局功能函数,utils文件就会发生改变,或者我修改一个路由的 path,router文件就变了,这些都会导致app.js的 hash 发生改变:app.1.js => app.2.js。但由于 element-uivue/react等也被打包在其中,虽然你没改变它们,但它们的缓存也会随着app.xxx.js变化而失效了,这就非常不合理的。所以我们需要自己来优化一下缓存策略。

我们现在的策略是按照体积大小、共用率、更新频率重新划分我们的包,使其尽可能的利用浏览器缓存。

彩民之家高手论坛 6

我们根据上表来重新划分我们的代码就变成了这样。

  • 基础类库 chunk-libs

它是构成我们项目必不可少的一些基础类库,比如 vue vue-router vuex axios 这种标准的全家桶,它们的升级频率都不高,但每个页面都需要它们。(一些全局被共用的,体积不大的第三方库也可以放在其中:比如 nprogress、js-cookie、clipboard 等)

  • UI 组件库

理论上 UI 组件库也可以放入 libs 中,但这里单独拿出来的原因是: 它实在是比较大,不管是 Element-UI还是Ant Design gizp 压缩完都可能要 200kb 左右,它可能比 libs 里面所有的库加起来还要大不少,而且 UI 组件库的更新频率也相对的比 libs 要更高一点。我们不时的会升级 UI 组件库来解决一些现有的 bugs 或使用它的一些新功能。所以建议将 UI 组件库也单独拆成一个包。

  • 自定义组件/函数 chunk-commons

这里的 commons 主要分为 必要非必要

必要组件是指那些项目里必须加载它们才能正常运行的组件或者函数。比如你的路由表、全局 state、全局侧边栏/Header/Footer 等组件、自定义 Svg 图标等等。这些其实就是你在入口文件中依赖的东西,它们都会默认打包到app.js中。

非必要组件是指被大部分页面使用,但在入口文件 entry 中未被引入的模块。比如:一个管理后台,你封装了很多 select 或者 table 组件,由于它们的体积不会很大,它们都会被默认打包到到每一个懒加载页面的 chunk 中,这样会造成不少的浪费。你有十个页面引用了它,就会包重复打包十次。所以应该将那些被大量共用的组件单独打包成chunk-commons

不过还是要结合具体情况来看。一般情况下,你也可以将那些非必要组件函数也在入口文件 entry 中引入,和必要组件函数一同打包到app.js之中也是没什么问题的。

  • 低频组件

低频组件和上面的共用组件 chunk-commons 最大的区别是,它们只会在一些特定业务场景下使用,比如富文本编辑器、js-xlsx前端 excel 处理库等。一般这些库都是第三方的且大于 30kb,所以 webpack 4 会默认打包成一个独立的 bundle。也无需特别处理。小于 30kb 的情况下会被打包到具体使用它的页面 bundle 中。

  • 业务代码

这部分就是我们平时经常写的业务代码。一般都是按照页面的划分来打包,比如在 vue 中,使用路由懒加载的方式加载页面 component: () => import('./Foo.vue') webpack 默认会将它打包成一个独立的 bundle。

完整配置代码:

splitChunks: { chunks: "all", cacheGroups: { libs: { name: "chunk-libs", test: /[/]node_modules[/]/, priority: 10, chunks: "initial" // 只打包初始时依赖的第三方 }, elementUI: { name: "chunk-elementUI", // 单独将 elementUI 拆包 priority: 20, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app test: /[/]node_modules[/]element-ui[/]/ }, commons: { name: "chunk-comomns", test: resolve("src/components"), // 可自定义拓展你的规则 minChunks: 2, // 最小共用次数 priority: 5, reuseExistingChunk: true } } };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
splitChunks: {
  chunks: "all",
  cacheGroups: {
    libs: {
      name: "chunk-libs",
      test: /[/]node_modules[/]/,
      priority: 10,
      chunks: "initial" // 只打包初始时依赖的第三方
    },
    elementUI: {
      name: "chunk-elementUI", // 单独将 elementUI 拆包
      priority: 20, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app
      test: /[/]node_modules[/]element-ui[/]/
    },
    commons: {
      name: "chunk-comomns",
      test: resolve("src/components"), // 可自定义拓展你的规则
      minChunks: 2, // 最小共用次数
      priority: 5,
      reuseExistingChunk: true
    }
  }
};

彩民之家高手论坛 7

上图就是最终拆包结果概要,你可以 点我点我点我,在线查看拆包结果。

这样就能尽可能的利用了浏览器缓存。当然这种优化还是需要因项目而异的。比如上图中的共用组件 chunk-commons,可能打包出来发现特别大,包含了很多组件,但又不是每一个页面或者大部分页面需要它。很可能出现这种状况:A 页面只需要 chunk-commons里面的 A 组件,
但却要下载整个chunk-commons.js,这时候就需要考虑一下,目前的拆包策略是否合理,是否还需要chunk-commons?还是将这些组件打包到各自的 bundle 中?还是调整一下 minChunks: 2( 最小共用次数)?或者修改一下它的拆包规则?

// 或者你可以把策略改为只提取那些你注册在全局的组件。 - test: resolve("src/components") test: resolve("src/components/global_components") //你注册全局组件的目录

1
2
3
4
// 或者你可以把策略改为只提取那些你注册在全局的组件。
 
- test: resolve("src/components")
test: resolve("src/components/global_components") //你注册全局组件的目录

Webpack 4 下的最佳实践

怎么把一些模块作为global模块

  1. 在每一个模块都可以引用的到某些模块

现在我们是通过 var jquery = require('jquery')在一个模块中应用。但是在每一个模块中都要应用这个模块,就要在每一个模块中都写。那么有没有global module这个东西呢?
我们通过ProviderPlugin来实现,具体如下:

config.plugins = [
  .....
  new webpack.ProviderPlugin({
    $: 'jquery',
    jQuery: 'jquery',
  })
]

那么在每一个模块中都可以直接通过$或者jQuery来访问到'jquery'这个模块。

2.怎么把一些模块绑定到全局变量上(window)

比如把jquery暴露到全局上。

这里需要一个loader 叫做expose。 这个expose loader把一个module直接绑定到全局context上。

怎么用呢?代码如下:

require('expose?$!expose?jQuery!jquery');

这个语法看起来有一些怪,其实这个语法是应用两次expose用!连接,用?来传入参数。(expose?$

  • ! expose?jQuery ! jquery)。loader的语法就是这样的,比如引用一个css,用 loader可以这样写:
    require('style!css! ./style.css');//载入css文件

HashedModuleIdsPlugin

了解了 modulechunk之后,我们来研究一下 moduleId

首先要确定你的 filename 配置的是chunkhash(它与 hash 的区别可以看上篇文章)。

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

1
2
3
4
output: {
  path: path.join(__dirname, 'dist'),
  filename: '[name].[chunkhash].js',
}

我们在入口文件中随便引入一个新文件test.js

//main.js import "./test"; //test.js console.log("apple");

1
2
3
4
5
//main.js
import "./test";
 
//test.js
console.log("apple");

我们运行npm run build,发现了一件奇怪的事情,我只是多引入了一个文件,但发现有十几个文件发生了变化。这是为什么呢?

我们随便挑一个文件 diff 一下,发现两个文件只有 module id 的不同。

彩民之家高手论坛 8

这是因为:
webpack 内部维护了一个自增的 id,每个 module 都有一个 id。所以当增加或者删除 module 的时候,id 就会变化,导致其它文件虽然没有变化,但由于 id 被强占,只能自增或者自减,导致整个 id 的顺序都错乱了。

虽然我们使用 [chunkhash] 作为输出名,但仍然是不够的。
因为 chunk 内部的每个 module 都有一个 id,webpack 默认使用递增的数字作为 moduleId
如果引入了一个新文件或删掉一个文件,都可能会导致其它文件的 moduleId 发生改变,
那这样缓存失效了。如:

彩民之家高手论坛 9

本来是一个按序的 moduleId list,这时候我插入一个orange模块,插在第三个位置,这样就会导致它之后的所以 module id 都依次加了 1。

这到了原因,解决方案就很简单了。我们就不要使用一个自增的 id 就好了,这里我们使用HashedModuleIdsPlugin

或者使用optimization.moduleIds v4.16.0 新发布,文档还没有。查看 源码发现它有naturalnamedhashedsizetotal-size。这里我们设置为optimization.moduleIds='hash'等于HashedModuleIdsPlugin。源码了也写了webpack5会优化这部分代码。

它的原理是使用文件路径的作为 id,并将它 hash 之后作为 moduleId。

彩民之家高手论坛 10

使用了 HashedModuleIdsPlugin`,我们再对比一下发现 module id 不再是简单的 id 了,而是一个四位 hash 过得字符串(不一定都是四位的,如果重复的情况下会增加位数,保证唯一性 源码)。
这样就固定住了 module id 了。

NamedModulesPlugin 和 HashedModuleIdsPlugin 原理是相同的,将文件路径作为 id,只不过没有把路径 hash 而已,适用于开发环境方便调试。不建议在生产环境配置,因为这样不仅会增加文件的大小(路径一般偶读比较长),更重要的是为暴露你的文件路径。

总结

Webpack 4 的改变主要是对社区中最佳实践的吸收。Webpack 4 通过新的 API 大大提升了 Code Splitting 的体验。但 Long-term caching 中 Vendor hash 的问题还是没有解决,需要手动配置。本文主要介绍的就是 Webpack 配置最佳实践在 Webpack 3.x 和 4.x 背景下的异同。希望对读者的 Webpack 4 项目的配置文件组织有所帮助。

另外,推荐 SURVIVEJS – WEBPACK 这个在线教程。这个教程总结了 Webpack 在实际开发中的实践,并且把材料更新到了最新的 Webpack 4。

1 赞 4 收藏 评论

彩民之家高手论坛 11

同时采用多入口和expose

多个入口的方式,webpack有一个妙招,可以把公共的部分提取出来。

比如entry_1和entry_2都需要react和jquery,那么webpack可以把他们提取出来放到一个公共的chunk中。

这个功能可以通过webpack的CommonsChunkPlugin来实现。

代码如下:

plugins: [
  new webpack.optimize.CommonsChunkPlugin('common-bundle.js')
]

这个将output一个公共的文件common-bundle.js,这个里面包括最少的webpack bootstrap code 和 多个模块公用的modules。你可以直接在html中引用它

<%= javascript_include_tag 'common-bundle' %>
<%= javascript_include_tag 'public-bundle' %>

展望

Whats next? 官方在这篇文章中展望了一下 webpack5 和讲述了一下未来的计划–持续改进用户体验、提升构建速度和性能,降低使用门槛,完善Persistent Caching等等。同时 webpack 也已经支持 Prefetching/Preloading modules,我相信之后也会有更多的网站会使用这一属性。

同时 webpack 的团队已经承诺会通过投票的方式来决定一些功能。比如不久前发起的投票。

彩民之家高手论坛 12

大家可以关注 Tobias Koppers 的 twitter 进行投票。

最后还是期待一下 webpack5 和它之后的发展吧。如果没有 webpack,也就不会有今天的前端。

其实如一开始就讲的,vue 有vue-cli、react 有creat-react-app,现在新建项目基本都是基于脚手架的,很少有人从零开始写 webpack 配置文件的,而且一般开发中,一般程序员也不需要经常去修改 webpack 的配置。webpack 官方本身也在不断完善默认配置项,相信 webpack 的配置门槛也会越来低多。

愿世间再无 webpack 配置工程师。

彩民之家高手论坛 13

  1. 管理所有的包,通过npm或者bower

拓展阅读

  • Webpack 4 和单页应用入门
  • Long term caching using Webpack records
  • Predictable long term caching with Webpack
  • 从 Bundle 文件看 Webpack 模块机制
  • minipack
  • 各种可视化分析 webpack bundle 库

    1 赞 收藏 评论

彩民之家高手论坛 14

4.能够异步加载通过 require.ensure

NamedChunkPlugin

我们在固定了 module id 之后同理也需要固定一下 chunk id,不然我们增加 chunk 或者减少 chunk 的时候会和 module id 一样,都可能会导致 chunk 的顺序发生错乱,从而让 chunk 的缓存都失效。

作者也意识到了这个问题,提供了一个叫NamedChunkPlugin的插件,但在使用路由懒加载的情况下,你会发现NamedChunkPlugin并没什么用。
供了一个线上demo,可以自行测一下。这里提就直接贴一下结果:

彩民之家高手论坛 15

产生的原因前面也讲了,使用自增 id 的情况下是不能保证你新添加或删除 chunk 的位置的,一旦它改变了,这个顺序就错乱了,就需要重排,就会导致它之后的所有 id 都发生改变了。

接着我们 查看源码 还发现它只对有 name 的 chunk 才奏效!所以我们那些异步懒加载的页面都是无效的。这启不是坑爹!我们迭代业务肯定会不断的添加删除页面,这岂不是每新增一个页面都会让之前的缓存都失效?那我们之前还费这么大力优化什么拆包呢?

其实这是一个古老的问题了 相关 issue: Vendor chunkhash changes when app code changes 早在 2015 年就有人提了这个问题,这个问题也一直讨论至今,’网友们’也提供了各种奇淫巧技,不过大部分随着 webpack 的迭代已经不适用或者是修复了。

这里我就结合一下 timse(webpack 第二多贡献)写的持久缓存的文章(在 medium 上需要翻墙)
总结一下目前能解决这个问题的三种方案。

目前解决方案有三种

  • records
  • webpackChunkName
  • 自定义 nameResolver

虚拟资源路径

在chrome中, webpack默认产生的source map会把所有的东西放到webpack://路径下,但是这个不太美观明了,可以通过output参数来设置,如下:

config.output = {
  ...
devtoolModuleFilenameTemplate: '[resourcePath]',
devtoolFallbackModuleFilenameTemplate: '[resourcePath]?[hash]',
}

现在虚拟资源在domain > assets了.
directory in the Sources tab.

默认分包策略

webpack 4 最大的改动就是废除了 CommonsChunkPlugin 引入了 optimization.splitChunks

webpack 4 的Code Splitting 它最大的特点就是配置简单,如果你的 modeproduction,那么 webpack 4 就会自动开启 Code Splitting

彩民之家高手论坛 16

以下内容都会以 vue-element-admin 为例子。 在线
bundle-report

如上图所示,在没配置任何东西的情况下,webpack 4 就智能的帮你做了代码分包。入口文件依赖的文件都被打包进了app.js,那些大于 30kb 的第三方包,如:echartsxlsxdropzone等都被单独打包成了一个个独立 bundle。

它内置的代码分割策略是这样的:

  • 新的 chunk 是否被共享或者是来自 node_modules 的模块
  • 新的 chunk 体积在压缩之前是否大于 30kb
  • 按需加载 chunk 的并发请求数量小于等于 5 个
  • 页面初始加载时的并发请求数量小于等于 3 个

彩民之家高手论坛 17

但有一些小的组件,如上图:vue-count-to 在未压缩的情况下只有 5kb,虽然它被两个页面共用了,但 webpack 4 默认的情况下还是会将它和那些懒加载的页面代码打包到一起,并不会单独将它拆成一个独立的 bundle。(虽然被共用了,但因为体积没有大于 30kb)

你可能会觉得 webpack 默认策略是不是有问题,我一个组件被多个页面,你每个页面都将这个组件打包进去了,岂不是会重复打包很多次这个组件?就拿vue-count-to来举例,你可以把共用两次以上的组件或者代码单独抽出来打包成一个 bundle,但你不要忘了vue-count-to未压缩的情况下就只有 5kb,gizp 压缩完可能只有 1.5kb 左右,你为了共用这 1.5kb 的代码,却要额外花费一次 http 请求的时间损耗,得不偿失。我个人认为 webpack 目前默认的打包规则是一个比较合理的策略了。

但有些场景下这些规则可能就显得不怎么合理了。比如我有一个管理后台,它大部分的页面都是表单和 Table,我使用了一个第三方 table 组件,几乎后台每个页面都需要它,但它的体积也就 15kb,不具备单独拆包的标准,它就这样被打包到每个页面的 bundle 中了,这就很浪费资源了。这种情况下建议把大部分页面能共用的组件单独抽出来,合并成一个component-vendor.js的包(后面会介绍)。

优化没有银弹,不同的业务,优化的侧重点是不同的。个人认为 webpack 4 默认拆包已经做得不错了,对于大部分简单的应用来说已经够用了。但作为一个通用打包工具,它是不可能满足所有的业务形态和场景的,所以接下来就需要我们自己稍微做一些优化了。

添加webpack的配置信息到rails

config/applicaton.rb

config.webpack = {
  :use_manifest => false,
  :asset_manifest => {},
  :common_manifest => {},
}

config/initializers/webpack.rb

if Rails.configuration.webpack[:use_manifest]
  asset_manifest = Rails.root.join('public', 'assets', 'webpack-asset-manifest.json')
  common_manifest = Rails.root.join('public', 'assets', 'webpack-common-manifest.json')

  if File.exist?(asset_manifest)
    Rails.configuration.webpack[:asset_manifest] = JSON.parse(
      File.read(asset_manifest),
    ).with_indifferent_access
  end

  if File.exist?(common_manifest)
    Rails.configuration.webpack[:common_manifest] = JSON.parse(
      File.read(common_manifest),
    ).with_indifferent_access
  end
end

如果要Rails.configuration[:use_manifest]那么就是配置asset_manifest和common_manifest。

config/environments/production.rb中

config.webpack[:use_manifest] = true

webpack是一个强大的模块构建工具,主要用于前端开发,可以和npm和bower很好的配合。

webpack的生产环境

那么我们把原来的webpack.config.js,删掉,新建三个文件common.config.js、development.config.js和production.config.js。其中config/webpack/common.config.js来写入一些基本的配置信息,如下:

var path = require('path');
var webpack = require('webpack');

var config = module.exports = {
  context: path.join(__dirname, '../', '../'),
};

var config.entry = {
  // your entry points
};

var config.output = {
  // your outputs
  // we'll be overriding some of these in the production config, to support
  // writing out bundles with digests in their filename
}

config/webpack/development.config.js如下:

var webpack = require('webpack');
var _ = require('lodash');
var config = module.exports = require('./main.config.js');

config = _.merge(config, {
  debug: true,
  displayErrorDetails: true,
  outputPathinfo: true,
  devtool: 'sourcemap',
});

config.plugins.push(
  new webpack.optimize.CommonsChunkPlugin('common', 'common-bundle.js')
);

config/webpack/production.config.js代码如下:

var webpack = require('webpack');
var ChunkManifestPlugin = require('chunk-manifest-webpack-plugin');
var _ = require('lodash');
var path = require('path');

var config = module.exports = require('./main.config.js');

config.output = _.merge(config.output, {
  path: path.join(config.context, 'public', 'assets'),
  filename: '[name]-bundle-[chunkhash].js',
  chunkFilename: '[id]-bundle-[chunkhash].js',
});

config.plugins.push(
  new webpack.optimize.CommonsChunkPlugin('common', 'common-[chunkhash].js'),
  new ChunkManifestPlugin({
    filename: 'webpack-common-manifest.json',
    manfiestVariable: 'webpackBundleManifest',
  }),
  new webpack.optimize.UglifyJsPlugin(),
  new webpack.optimize.OccurenceOrderPlugin()
);

这里我们把输出目录换成了publice/assets,同时把文件名称添加chunkhash,来标记。
同时添加了ChunkManifestPlugin这个plugin。
UglifyJsPlugin来压缩
OccurenceOrderPlugin,which will shorten the IDs of modules which are included often, to reduce filesize.

2.可以针对多种资源(js png css coffeescript jade es6 jsx)

代码分割和lazy loading module(异步加载模块)

webpack可以通过require.ensure(['ace'], function(require){})来实现异步加载。
比如我们使用ace这个editor, 由于这个editor还是比较重的,所以只有在用的时候才加载,那么webpack可以split来确定的modules在一个自己单独的chunk file 中,只有应用的时候才调用,webpack会通过jsonp来实现,具体应用例子如下:

function Editor() {};
Editor.prototype.open = function() {
  require.ensure(['ace'], function(require) {
    var ace = require('ace');
    var editor = ace.edit('code-editor');
    editor.setTheme('ace/theme/textmate');
    editor.gotoLine(0);
  });
};

var editor = new Editor();
$('a[data-open-editor]').on('click', function(e) {
  e.preventDefault();
  editor.open();
});
版权声明:本文由彩民之家高手论坛发布于前端知识,转载请注明出处:Webpack 4 配置最棒实行彩民之家高手论坛