【翻译向】webpack2 指南(下)

原文发布与抹桥的博客-【翻译向】webpack2 指南(下)javascript

缓存(Caching)

为了可以使 webpack 处理后的静态资源可以长期缓存下来,须要:css

  1. 使用 [chunkhash] 给每个文件建立基于内容变化的缓存标识html

  2. 在 HTML 文件中引入文件时使用编译状态来拿到文件名称java

  3. 在载入资源以前生成 chunk-manifest JSON 文件并写入到 HTML 页面中jquery

  4. 确保包含启动代码的入口代码块的 hash 值不会被修改,当它的依赖没有变化的时候webpack

存在的问题

每当咱们代码中有一些东西须要被更新的时候,它须要在服务上部署而后由客户端从新下载这些文件。当网络状情况不太好的时候,这是意见很是低效的事情。这也是为何浏览器要缓存静态资源的缘由。git

这会致使一个陷阱:当咱们发布一个新的版本的时候不去更新的文件名,这会让浏览器认为文件没有变化,致使客户端拿不到最新的资源。github

一个简单解决问题的方式就是告诉浏览器一个新的文件名。在没有 webpack 的时候咱们会使用构建版原本标识这次的更新:web

application.js?build=1
application.css?build=1

在 webpack 中这样作也很简单:每一次 webpack 的构建都会生成一个能够用来构成文件名的独一无二的 hash 值。下面这个配置文件会生成两个带有 hash 值的文件名:npm

// webpack.config.js
const path = require('path');

module.exports = {
  entry: {
    vendor: './src/vendor.js',
    main: './src/index.js'
  },
  output: {
    path: path.join(__dirname, 'build'),
    filename: '[name].[hash].js'
  }
};

执行 webpack 命令会获得如下输出:

Hash: 55e783391098c2496a8f
Version: webpack 1.10.1
Time: 58ms
Asset Size Chunks Chunk Names
main.55e783391098c2496a8f.js 1.43 kB 0 [emitted] main
vendor.55e783391098c2496a8f.js 1.43 kB 1 [emitted] vendor
[0] ./src/index.js 46 bytes {0} [built]
[0] ./src/vendor.js 40 bytes {1} [built]

但存在的问题是,每次咱们从新编译,全部的文件名都会变化,这会致使客户端每次都从新把整个应用的代码从新下载一遍。那么咱们如何才能时客户端每次只下载有变更的文件。

给每一个文件生成独特的 hash

当一个文件的内容没有变化的时候,如何保证它的文件名不会在每次编译中变化。好比,一个用来放全部的咱们的全部依赖库文件的代码包。

Webpack 容许根据文件的内容生成 hash 值。这是更新后的配置:

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

这个配置一样会生成两个文件,可是每一个文件都拥有本身的 hash 值:

main.155567618f4367cd1cb8.js 1.43 kB 0 [emitted] main
vendor.c2330c22cd2decb5da5a.js 1.43 kB 1 [emitted] vendor

不要在开发环境中使用 [chunkhash],这会致使每次的编译时间边长。把开发环境和生成环境的配置文件分开,在开发环境使用 [name].js ,在生产环境中使用 [name].[chunkhash].js

从 webpack 编译状态(compilation stats)中获取文件名
在开发环境中,你只须要在 HTML 中引入你的文件就能够了。

<script src="main.js"></srcipt>

然而,在生产环境中,咱们每次都会获得一个不一样的文件名:

<script src="main.12312123t234.js"></srcipt>

为了可以在 HTML 中引入正确的文件,咱们须要了解一些构建的信息。这能够经过下面这个插件来从 webpack 编译状态(compliation stats) 中提取出来。

// webpack.config.js
const path = require('path');

module.exports = {
  /*...*/
  plugins: [
    function() {
      this.plugin("done", function(stats) {
        require("fs").writeFileSync(
          path.join(__dirname, "…", "stats.json"),
          JSON.stringify(stats.toJson()));
      });
    }
  ]
};

或者,经过下面这些插件来暴露 JSON 文件:

{
  "main.js": "main.155567618f4367cd1cb8.js",
  "vendor.js": "vendor.c2330c22cd2decb5da5a.js"
}

接下来的事情就由你的服务来决定了。这有一个很不错的例子 [walk through for Rails-based projects](
walk through for Rails-based projects). 使用 Node.js 的服务端渲染的话可使用 webpack-isomorphic-tools. 若是你的应用不须要依赖服务端渲染的话,那彻底能够直接生成一个 index.html. 可使用下面这两个插件来完成:

肯定的 hashes (Deterministic hashes)

为了压缩生成文件的大小,webpack 使用 id 代替名字来识别模块。再编译过程当中,id 已经被生成,映射到代码块的名字而且放到一个 JavaScript 对象中,被叫作代码块清单( chunk manifest)。它会被放到入口文件中,确保打包后的文件可以正常工做。

这会有和以前同样的问题:不管修改任何地方的文件,即便其它地方都没有修改,更新后的入口须要包含清单文件。这会生成一个新的 hash 值,致使问们的入口文件名修改,没法享受长期缓存带来的好处。

为了解决这个问题,咱们应该使用 https://github.com/diurnalist/chunk-manifest-webpack-plugin ,一个能够把清单文件提取出来单独放到一个 JSON 文件中。这是更新后的配置文件,会生成 chunk-manifest.json 并放到咱们打包后的文件夹下面:

// webpack.config.js
var ChunkManifestPlugin = require('chunk-manifest-webpack-plugin');

module.exports = {
  // your config values here
  plugins: [
    new ChunkManifestPlugin({
      filename: "chunk-manifest.json",
      manifestVariable: "webpackManifest"
    })
  ]
};

当咱们从入口文件中移除掉清单文件后,那么咱们就须要手动把这个文件提供给 webapck 使用。在上面的例子中你可能注意到 manifestVariable 这个选项。这是一个全局变量,一个供 webpack 来查找清单 JSON 文件,这也是为何咱们须要在代码包前面引入到 HTML 中。在 HTML 中写入 JSON 文件的内容是很容易,那么咱们的 HTML 文件的 head 部分就会向下面这样:

<html>
<head>
    <script>
      //<![CDATA[
      window.webpackManifest = {"0":"main.3d038f325b02fdee5724.js","1":"1.c4116058de00860e5aa8.js"}
      //]]>
    </script>
</head>
<body>
</body>
</html>

最终的 webpack.config.js 文件以下:

var path = require('path');
var webpack = require('webpack');
var ManifestPlugin = require('webpack-manifest-plugin');
var ChunkManifestPlugin = require('chunk-manifest-webpack-plugin');
var WebpackMd5Hash = require('webpack-md5-hash');

module.exports = {
  entry: {
    vendor: './src/vendor.js',
    main: './src/index.js'
  },
  output: {
    path: path.join(__dirname, 'build'),
    filename: '[name].[chunkhash].js',
    chunkFilename: '[name].[chunkhash].js'
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: "vendor",
      minChunks: Infinity,
    }),
    new WebpackMd5Hash(),
    new ManifestPlugin(),
    new ChunkManifestPlugin({
      filename: "chunk-manifest.json",
      manifestVariable: "webpackManifest"
    })
  ]
};

若是使用 webpack-html-plugin ,那么你可使用 inline-manifest-webpack-plugin 来作这件事。

使用这个配置,那么第三方代码块(vendors chunkd)将不会变化,除非你修改它的代码或者依赖。

垫片(Shimming)

webpack 做为了个模块打包工具,能够支持的模块系统包括 ES2015 modules, CommonJs 和 AMD. 可是不少状况下,当咱们使用第三方库的时候,咱们看到他们会依赖一个全局变量好比 $ 或者说 jquery. 它们也可能建立一些须要暴露出来的新的全局变量。咱们来看几种不一样的方式来使 webpack 可以理解这些非模块(broken modules)的文件。

最好使用在 dist 文件下没有打包压缩过的 CommonJs/AMD 文件(Prefer unminified CommonJS/AMD files over bundled dist versions.)
大多数模块会在 package.jsonmain 字段中指定它们的 dist 版本。这对大多数开发者来讲都是很是有用的,对 webpack 来讲最好设置一个别名到它们的 src 目录下面,这样可以使 webpack 更好的优化依赖。可是,在不少状况下使用 dist 版本也不会有什么大的问题。

// webpack.config.js

module.exports = {
  ...
  resolve: {
  alias: {
    jquery: "jquery/src/jquery"
  }
}
};

provide-plugin

经过使用 [provide-plugin](https://webpack.js.org/plugins/provide-plugin) 使这个模块在全部经过 webpack 引用的模块中做为一个可用的变量。只有当你使用了这个变量后,对应的模块才会被引用进来。不少古老的模块经过使用特定的全局变量,好比 jQuery 的 $ 和或者 jQuery. 在这个场景下,你能够提早在 webpack 中配置为 var $=requrei('jquery') ,在每一次遇到全局 $ 标识符。

module.exports = {
  plugins: [
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery'
    })
  ]
};

imports-loader

[imports-loader](https://webpack.js.org/loaders/imports-loader/) 将必需要的全局变量插入到传统模块中。好比,一些传统模块依赖 this 指向 window 对象。这会致使一个问题,当模块被执行在 CommonJS 上下文的时候, this 指向为 module.exports.在这种状况下,你能够经过 imports-loader重写 this.

webpack.config.js

module.exports = {
  module: {
    rules: [{
      test: require.resolve("some-module"),
      use: 'imports-loader?this=>window'
    }]
  }
};

它支持不一样的模块类型,好比 AMD,CommonJS 同时也支持传统模块。但是,一般状况下它会去检查 define 变量,而后使用一些奇怪(quirky)的代码去暴露这些属性。在这种状况下,经过设置 define = false 来强制CommonJS 路径可能会有一些帮助。

webpack.config.js

module.exports = {
  module: {
    rules: [{
      test: require.resolve("some-module"),
      use: 'imports-loader?define=>false'
    }]
  }
};

exports-loader

假设一个库文件建立了一个全局变量,期待它的消费者去使用。在这种状况下,咱们应该使用 [exports-loader](https://webpack.js.org/loaders/exports-loader/), 来暴露一个 CommonJS 风格的全局变量。好比,为了暴露 filefilehelpers.parseparse:

webpack.config.js

module.exports = {
  module: {
    rules: [{
      test: require.resolve("some-module"),
      use: 'exports-loader?file,parse=helpers.parse'
      // adds below code the the file's source:
      //  exports["file"] = file;
      //  exports["parse"] = helpers.parse;
    }]
  }
};

script-loader

[script-loader](https://webpack.js.org/loaders/script-loader/) 会在全局上下文里面解析代码,就和你在 HTML 中添加了一个 script 标签同样。在这种状况下,理论上全部的模块都应该正常的运行。

这个文件会被做为字符串打包在代码中。不会被 webpack 压缩,因此请使用压缩后的版本。一样这种状况没法使用 webpack 提供的开发工具。

假设你有一个 legacy.js 文件包含:

GLOBAL_CONFIG = {}

使用 script-loader

require('script-loader!legacy.js')

通常会获得这样的结果:

eval('GLOBAL_CONFIG = {}')

noParse 选项

当没有 AMD/CommonJS 风格的模块,同时你须要在 dist 中引入,你能够把这个模块标识为 [noParse](https://webpack.js.org/configuration/module/#module-noparse). 这样 webpack 就只会引入这个模块可是不会去作任何处理,这样也能够减小构建的时间。

任何须要 AST 支持的,好比 ProvidePlugin, 都是不会工做的。

module.exports = {
  module: {
    noParse: /jquery|backbone/
  }
};

编写一个库文件

webpack 是一个工具,能够用来打包应用代码,一样也能够用来打包库代码。若是你是一个 JavaScript 库的做者,正在寻找精简打包代码的流程,那么这个章节的内容会对你颇有帮助。

编写一个库(Author a Library)

咱们这有一个精简的包装库来把数字 1 到 5 转换到对应的单词,反之亦然。 看起来多是这样的:

src/index.js

import _ from 'lodash';
import numRef from './ref.json';

export function numToWord(num) {
  return _.reduce(numRef, (accum, ref) => {
    return ref.num === num ? ref.word : accum;
  }, '');
};

export function wordToNum(word) {
  return _.reduce(numRef, (accum, ref) => {
    return ref.word === word && word.toLowerCase() ? ref.num : accum;
  }, -1);
};

库的使用规范以下:

//ES2015modules

import*aswebpackNumbersfrom'webpack-numbers';

...
webpackNumbers.wordToNum('Two')//outputis2
...

//CommonJSmodules

varwebpackNumbers=require('webpack-numbers');

...
webpackNumbers.numToWord(3);//outputisThree
...

//Asascripttag

<html>
...
<scriptsrc="https://unpkg.com/webpack-numbers"></script>
<script>
...
/*webpackNumbersisavailableasaglobalvariable*/
webpackNumbers.wordToNum('Five')//outputis5
...
</script>
</html>

完整的库配置和代码放在这里 webpack-library-example.

配置 webpack

那么接下来的事情就是打包这个库

• 不打包 lodash,可是会被它的消费者引入
• 命名这个库为 `webpack-numbers`, 而且变量为 `webpackNumbers`
• 库能够经过 `import webapckNumbers from 'webpack-numbers'` 或者 `require('webpack-numbers')` 来引入
• 当经过 `script` 标签引入的时候,能够经过全局变量 `webpackNumbers` 来访问
• 能够在 Node.js 中使用

添加 webpack

添加基础 webpack 配置。

webpack.config.js

module.exports = {
  entry: './src/index.js',
  output: {
    path: './dist',
    filename: 'webpack-numbers.js'
  }
};

这将添加一个基础配置来打包这个库。

添加 Loaders

可是若是没有对应 loaders 去解析代码是没有办法工做的。这里,咱们添加 json-loader 来添加对 json 文件的解析。

webpack.config.js

module.exports = {
  // ...
  module: {
    rules: [{
      test: /.json$/,
      use: 'json-loader'
    }]
  }
};

添加 externals

如今,若是执行 webpack 命令,你会发现一个提交较大的代码包被生成。若是你去检查代码,会发现 ladash 被打包到了代码包中。对于你的库来讲把 lodash 打包在一块儿使彻底没有必要的。

能够经过 externals 配置:

webpack.config.js

module.exports = {
  ...
    externals: {
  "lodash": {
    commonjs: "lodash",
      commonjs2: "lodash",
      amd: "lodash",
      root: "_"
  }
}
...
};

这意味着在使用者的环境下你的库会指望依赖 lodash

添加 libraryTarget

为了这个库可以被普遍的使用,咱们须要让它在不一样的环境下有相同的表现。好比, CommonJS,AMD,Node.js 或者做为一个全局变量。

为了达到这个目的,须要在 webpack 配置中添加 library 属性。

webpack.config.js

module.exports = {
  ...
    output: {
...
  library: 'webpackNumbers'
}
...
};

这可以你的库被引入的时候能够做为一个全局变量被访问。为了可以在其它状况下使用,在配置中继续添加 libraryTarget 的值:

webpack.config.js

module.exports = {
  ...
    output: {
...
  library: 'webpackNumbers',
    libraryTarget:'umd' // Possible value - amd, commonjs, commonjs2, commonjs-module, this, var
}
...
};

若是 library 设置了,可是 libraryTarget没有配置,那么 libraryTarget 默认为 var 就像在 config reference 中指定的同样。

最后一步

调整生产环境下的配置文件

将打包后的文件添加到 package.json 中指定的字段里面。

package.json

{
    ...
    "main": "dist/webpack-numbers.js",
    "module": "src/index.js", // To add as standard module as per https://github.com/dherman/defense-of-dot-js/blob/master/proposal.md#typical-usage
    ...
}

如今你能够把它做为一个 npm 模块发布了,而且在 unpkg.com 里面向你的用户传播了。

相关文章
相关标签/搜索