Webpack 最佳实践总结(二)

in Tutorials with 3 comments

上一篇介绍了 Webpack 优化项目的四种技巧,分别是通过 UglifyJS 插件实现对 JavaScript 文件的压缩,css-loader 提供的压缩功能,配置NODE_ENV可以进一步去掉无用代码,tree-shaking帮助找到更多无用代码

这一篇主要讲 Webpack 的改进缓存(hash)、切割代码

使用 hash

开发过程经常需要一边预览代码运行结果一边修改代码,这个时候文件版本控制就显得尤为重要。默认做法是告诉浏览器这个文件的缓存时间,然后当文件内容被修改,则需要重命名该文件告诉浏览器需要重新下载和缓存,例如:

<!-- Before the change -->
<script src="./index.js?version=15">

<!-- After the change -->
<script src="./index.js?version=16">

Webpack 也能做类似的工作。但是不是采用上面的版本方式去控制文件,它会计算文件的hash值去标识打包后文件。每次你修改了代码,文件名也跟着变化,这样浏览器就会加载新的新文件,不再使用缓存过的文件,例如:

// webpack.config.js
module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.[chunkhash].js'
       // → bundle.8e0d62a03.js
  }
};

更多关于 [hash], [chunkhash], [name], [id] 和 [query] 可以查看:here,这里列一个表方便查看:

TemplateDescription
[hash]module 的 hash 标识
[chunkhash]chunk 的 hash 标识
[name]module 的名称
[id]module 的唯一标识
[query]module 的查询(文件名带有?)

现在还剩下的问题是如何将会变化的文件传输到客户端,这里列出两种解决方法,分别是:HtmlWebpackPluginWebpackManifestPlugin

HtmlWebpackPlugin 是一个更自动化的解决方法,在编译打包的期间,它会生成包含打包资源的 HTML 文件。如果你的业务逻辑相对简单的情况下,这个插件是绝对够用的:

<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>

WebpackManifestPlugin 相比HtmlWebpackPlugin是一个更灵活的解决方案,尤其是面对复杂的服务端的部分。它能生成JSON文件,包含文件名以及与其对应(映射)的 hash 过的文件名

{
  "bundle.js": "bundle.8e0d62a03.js"
}

提示:Webpack的 hash 函数是不稳定的,这意味着在不同开发环境等条件下下,即便是同一个文件,webpack 依然会计算出不同的 hash 去标识这个相同的文件。正因为此,如果你有跨平台开发的需求,可以使用webpack-chunk-hash去代替webpack的原生的hash函数算法。更多关于 webpack hash 的问题可以查看:here

切割代码

想象一下你要开发一个大型网站,有首页和文章页,文章页里有文章内容和评论系统,但是你要将网站正常工作的代码都打包到一个文件里,这显然是不科学的。每次你修改其中一个模块,整改打包文件都要重新编译重新打包和生成,这意味着你仅仅只是修改一下评论模块,但当用户只访问首页,他们依然会下载这些暂时无用的代码,从而影响首页访问的加载速度

这个时候就需要切割打包文件,把它切割为首页和文章页所需的两个打包文件,当用用户访问首页时,只加载首页的打包文件,当访问文章页的时候,只加载文章页的打包文件,配置如下:

module.exports = {
  // 设置多个入口点,webpack给每个入口文件生成对应的打包文件
  // 从而实现不同页面加载不同的打包文件
  entry: {
    homepage: './index.js',
    article: './article.js'
  },
  output: {
    // [name]对应的是入口点的 name,如 homepage、article
    filename: '[name].[chunkhash].js'
  },
  plugins: [
    // 生成打包资源列表 json 文件
    new WebpackManifestPlugin(),
    // 取代 webpack 原生的 hash 函数
    new WebpackChunkHash(),
    // 生成依赖包的块文件,转移所有的`node_modules`依赖到一个特别的该文件中
    // 这允许你更新你的代码时,无需更新依赖
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: m => m.context &&
        m.context.includes('node_modules'),
    }),
    // 生成 `webpack’s runtime` 自身的代码文件
    // 这允许你更新你的代码时,无需更新其他无关代码
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime',
      chunks: ['vendor'],
      minChunks: Infinity,
    }),
    // 标识每个模块 hash 值,当你添加新的模块时,如果该模块的依赖影响到别的模块
    // 就可以更新这些受影响的模块从而区分旧的模块
    new webpack.HashedModuleIdsPlugin(),
    // 生成资源映射文件,包含文件名以及与其对应的hash过的文件名,用于其他插件或者服务
    new ChunkManifestPlugin({
      filename: 'chunk-manifest.json',
      manifestVariable: 'webpackManifest'
    })
  ]
};

上面这个配置将会生成6个文件:

// 两个打包入口文文件,当你修改了业务代码,它们也会跟着变化
homepage.a68cd93e1a43281ecaf0.js
article.d07a1a5e55dbd86d572b.js

// 通用依赖文件与 webpack’s runtime 的文件,前者当你依赖包变化也跟着变化,后者极少变化,除非使用的 webpack 版本这类情况变化了才会跟着变化
vendor.1ebfd76d9dbc95deaed0.js
runtime.d41d8cd98f00b204e980.js

// 两个 manifest 文件,用于其他插件或服务,如 DllReferencePlugin
manifest.json
chunk-manifest.json

按需切割代码

除了按照不同页面的切割为不同的入口文件外的切割代码外,还可以按照构成页面的组件的顺序做到按需加载的去切割代码

想象一下,有一个页面布局方式如下所所示:

+-------------------------------+
| logo                    menu  |
+-------+----------------+------+
|       |                |      |
| left  |                | right|
| bar   |                | bar  |
|       |    article     |      |
|       |    content     |      |
|       |                |      |
|       |                |      |
|       +----------------+      |
|       |                |      |
|       |    comments    |      |
|       |                |      |
+-------+----------------+------+
|           copyright           |
+-------------------------------+

当访问这个页面时,用户希望首先能阅读到文章的内容,诸如其他评论、侧边栏等其他页面组成部分是可以被延后查看的。可惜的是,如果你将这些页面组成部分都打包到一个文件里,用户就需要等到整改打包文件加载后才能访问到他想要访问的内容

如何解决页面中各部分的按需加载?webpack 允许你做这方面的优化去实现代码的按需加载。首先你需要自己识别哪些代码是要首先加载的,然后 webpack 会移动需要延迟加载的代码到单独的块中,只有在当需要这些被延迟加载的代码时,才会下载

假设有一个article-page.js业务代码文件,当你打包加载其会全部加载,包括文章内容、评论和侧边栏,如下:

// article-page.js
import { renderArticle } from './components/article';
import { renderComments } from './components/comments';
import { renderSidebar } from './components/sidebar';

renderArticle();
renderComments();
renderSidebar();

做到按需加载则需要你将静态的import修改为动态的import(),webpack 会自带转移这些代码到单独的块中,只有当被需要时才会加载,如下:

// article-page.js
import { renderArticle } from './components/article';
renderArticle();

import('./comments.js')
  .then((module) => { module.renderComments(); });
import('./sidebar.js')
  .then((module) => { module.renderSidebar(); });

将静态的import修改为动态的import()的操作会带来提升首次访问时加载性能,同事也会优化缓存,当你更改这些业务代码时,只会修改对应的块文件,从而不影响其他块文件

当然,还需要重新设置 output ,如下:

// webpack.config.js
module.exports = {
  output: {
    filename: '[name].[chunkhash].js',
    chunkFilename: '[name].[chunkhash].js',
  }
};

output.chunkFilename可以标识按需加载的块文件

提示:当你使用默认的 presets 的 babel 去编译这些代码,你会得到一个语法错误提示:Babel don’t understand import() out of the box. 要避免这个错误,你需要给babel安装syntax-dynamic-import插件

externals

在一个大型项目中,如果有两段业务代码有共同的依赖,通过 webpack 的externals,你在两段代码间可以共享这些依赖,如下:

// webpack.config.js
module.exports = {
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM',
  }
};

上面的做法是将这些框架或库的对象挂靠在全局对象中,然后通过另外一个对象存储对象名以及映射到对应模块名的变量,webpack 就会替换所有所有模块下的相关的引用,然后你需要手动引入这些被externals的框架或库到网站入口文件中,如HtmlWebpackPlugin定义的template文件

总结

这次介绍如何通过 webpack 克服浏览器缓存打包文件,不去更新新的打包文件的问题,以及讲解了从不同的页面的角度去切割代码和从不同的页面组成部分去切割代码的过程,还有通过externals去分离去共有的框架和库,从而实现对这些框架或库的CDN资源加载

内容较多,大概就这样~

Responses
  1. Linus Liu

    感谢分享,学习中

    Reply
  2. 过来学习一下

    Reply
  3. co

    666
    1、在大部分情况下我们在使用github进行管理项目的时候 我会把hash 改成github的hash,两者进行比对再去选择是否要去下载最新的bundle文件.
    2、在react项目中,正式因为webpack有了多入口,所以在 react-router 才会有了路由依赖的优化方案
    大兄弟 点个赞

    Reply