解决 CommonsChunkPlugin 的 Hash 变化问题

原文:

Manifest File
But, if we change application code and run webpack again, we see that the hash for the vendor file changes. Even though we achieved separate bundles for vendor and main bundles, we see that the vendor bundle changes when the application code changes. This means that we still don’t reap the benefits of browser caching because the hash for vendor file changes on every build and the browser will have to reload the file.

The issue here is that on every build, webpack generates some webpack runtime code, which helps webpack do it’s job. When there is a single bundle, the runtime code resides in it. But when multiple bundles are generated, the runtime code is extracted into the common module, here the vendor file.

To prevent this, we need extract out the runtime into a separate manifest file. Even though we are creating another bundle, the overhead is offset by the long term caching benefits that we obtain on the vendor file.

非翻译:

每次打包时 vendor 文件的 hash 也会变化的原因在于, webpack 打包时必定会产生 runtime code.

当我们需要打包出 vendor.js 的时候, 尽管公共代码没有变化, 但是 webpack 会生成新的 runtime code 并注入 vendor.js, 导致其 hash 变化.

解决方法:

将 vendor.js 中的 runtime code 单独打包出来, 这样只有 bundle.js 和 runtime code 的 hash 会变化, vendor.js 的代码不会变化.

具体做法:

new webpack.optimize.CommonsChunkPlugin({
  name: ['vendor', 'manifest'],
}

这样就可以把 runtime code 打包进 manifest.js, 此后该类操作不会改变 vendor.js 的 hash 了

Webpack 2 Config 文件入门

webpack 2 config 文件入门

webpack.config.js 的基本结构

var path = require('path')

module.exports = {
  entry:  {
    home: './home.js',
    about: './about.js',
    contact: './contact.js',
  }, // 入口文件
  // string | [string] | object {<key>: string | [string] }
  // 一个页面搭配一个入口文件
  output: {
    // output 包含一系列关于打包后文件的配置
    // output.chunkFilename: string, 这个参数确定了加载 chunk 的名称
    // output.crossOriginLoading: boolean | string, 只在 target 参数指定为 web 的时候使用, 因为此时通过 jsonp 加载模块
    // crossOriginLoading: false(default) , 禁止跨域加载
    // crossOriginLoading: 'anonymous', 允许无 credential 的跨域加载
    // crossOriginLoading: 'use-credentials', 允许有 credential 的跨域加载
    // 
    // output.devtoolFallbackModuleFilenameTemplate: string | function(info)
    // output.hotUpdateChunkFilename: string, 指定热刷新的 chunk 的名称, 仅支持 id 和 hash 占位符
    // hotUpdateChunkFilename: '[id].[hash].hot-update.js'
    // output.sourceMapFilename: string, 仅当 devtool 使用 source map 时生效, 默认为[ file].map, 可选占位符有[name], [id], [hash], [chunkhash]

    path: path.resolve(__dirname, 'dist'), // 打包后文件存储路径, 必须是绝对路径, 可以用 path 模块生成

    publicPath: /assert/, // 解析文件中相对路径时依赖的根目录
    // 该字段设定了输出文件的 public Url
    // 
    // 默认的, 相对路径会基于 html 文件位置或<base>标签进行解析, 添加 publicPath 后, 各个 loaders 会在每个相对路径前添加 publicPath, 即相对路径基于 publicPath 进行解析
    // 该值一般以'/'结尾
    // 默认值为''
    // simple rule: 该参数的值为html 页面到 output.path 的相对值
    // path: path.resolve(__dirname, 'public/assets'),
    // publicPath: 'https://cdn.example.com/assets/',
    // publicPath 也会对 webpack-dev-server 生效
    // publicPath: 'https://cdn.example.com/assets/' cdn, 并且应该使用 https
    // publicPath: '//cdn.example.com/assets/', cdn, 使用相同协议
    // publicPath: '/assets/', 相对于服务器根路径
    // publicPath: 'assets/', 相对于 html 页面
    // publicPath: '../assets/', 相对于 html 页面
    // publicPath: '', 相对于 html 页面(同一路径)

    filename: 'bundle.js', // 打包后的文件名称
    // 对于单一入口文件, filename 可以为静态名称'bundle.js'
    // 对于多入口文件, 可以使用参数生成特定名称
    // [name], [id], [hash], [chunkhash]
    // 可以为文件添加路径: 'js/[name]/bundle.js'

    library: 'MyLibrary', // 输出库的名称

    libraryTarget: 'umd', // 输出库的类型
  },
  module: {
    // module.noParse: RegExp | [RegExp], 符合正则的字段被 import, require, define 后不会被处理, 可以提高打包速度
    // noParse: /jquery|lodash/
    //
    rules: [
      // 模块应用规则(loaders, parser 的设定等)
      // module.rules: array
      // 一条 Rule 可以分为三部分, Conditions, Results, Nested Rules

      // Conditions, 有两个输入值, 
      // 1\. The resource: 文件引用的绝对路径经过 resolve 规则解析后的结果
      // 2\. The issuer: 引用 resource 的模块到文件的绝对路径
      // 比如, 'app.js' 中 'import ./style.css', resource 是 '/path/to/style.css', issuer 是 '/path/to/app.js'
      // 在 rule 中, test, include, exclude 和 resource 作用于resource, 而 issuer 族谱用于 issuer

      // Results
      // Rule Results 仅在 Rule Conditions 满足时起作用
      // Rule Results 有二种输出值
      // Applied Loaders: 一个数组, 其元素是应用于 resource 的 loaders
      // Parser options: 一个 options 对象, 作为当前 module 的 parser
      // loader, options, use 用于 loaders,
      // query, loaders 用于提高兼容性
      // enforce 属性指定 loader 类型, 可选值有: normal, pre, post
      // parser 属性影响 parser options

      // Nested Rules
      // 在 rules 和 oneOf 属性中可以指定 nested rules

      // Rule.enforce: 'pre' | 'post', 指定 loader 的类型, 默认为 normal
      // 还有一种 inline loader 应用于 inline import/require, 
      // 所有 loaders 按照 post, inline, normal, pre 排序并依次执行
      // 所有 normal loaders 可以通过在 request 中添加前缀 '!' 进行忽略
      // 所有 normal 和 pre loaders 可以通过在 request 中添加前缀 '-!' 进行忽略
      // 所有 normal, post 和 pre loaders 可以通过在 request 前添加 '!!' 进行忽略

      // Rule.exclude
      // Rule.exclude 是 Rule.resource.exclude 的缩写
      // 
      // Rule.include
      // Rule.include 是 Rule.resource.include 的缩写
      //
      // Rule.issuer
      //
      // Rule.loader
      // Rule.loader 是 Rule.use: [{loader}] 的别名
      // 
      // Rule.loaders
      // Rule.loaders 是 Rule.use 的别名, 他的存在仅为了兼容 webpack1, 应使用 Rule.use
      // 
      // Rule.oneOf
      // 一个数组, 其元素为一系列 rules, 仅第一个符合条件的 rule 会起作用
      // 
      // Rule.options/ Rule.query
      // Rule.options 和 Rule.query 都是 Rule.use: [{options}] 的缩写
      // Rule.query 的存在仅为了兼容 webpack1, 应使用 Rule.options
      // 
      // Rule.parser
      // 一个 parser options 的对象, 所有要使用的 parser options 都集中在此
      // 每个不同的 parser options 都会创建对应 parser, 并且 plugins 可以根据 parser options 应用.  当 parser options 中没有设置或设置为 true 时, 大部分 plugins 默认只应用他们自己的 parser plugins
      // parser: {
      // amd: false, // disable AMD
      // commonjs: false, // disable CommonJS
      // harmony: false, // disable ES6 Harmony import/exports
      // requireInclude: false, // disable require.include
      // requreEnsure: false, // disable require.ensure
      // }

      // Rule.resource
      // Rule.rules, 一个 Rules 组成 的数组
      // Rule.test, Rule.resource.test 的缩写
      // Rule.use, 一系列应用于 modules 的 UseEntries, 每一个 entry 指定一个 loader
      // use: ['style-loader'] 是 use: [{loader: 'style-loader'}] 的缩写
      // 可以链式传入多个 loaders, 从右向左依次执行
      // use: [
      //   {loader: 'style-loader'},
      //   {loader: 'css-loader', options: { importLoader: 1}},
      //   {loader: 'less-loader', options: { noIeCompat: true}},
      // ]

      // Condition
      // Condition 可以为以下之一:
      // String: 比如 exclude: 'node_modules'
      // RegExp: 比如 test: /\.js$/, exclude: /node_modules/
      // function: 返回 true 或 false
      // Condition Array: 至少有一个满足时刻认为满足 condition
      // Object: 所有属性都满足时可认为满足
      // 
      // {test: Condition}, {include: Condition}, {exclude: Condition}, {and: [Condition]}, {or: [Condition]}, {not: Condition}
      // 
      // UseEntry: object
      // 该属性必须有一个字符串 loader.
      // 该属性相对 context 的值解析 loader
      // 该属性拥有 options 属性, 该属性为 string 或 object, 该属性会作为 loader options 被传给 loader
      // 考虑到兼容性, 目前仍支持 query 作为 options 的别名
      // 

      {
        test: /\.jsx?$/, // 通过正则表达式选择要处理的文件
        include: [ // 限定文件范围
          path.resolve(__dirname, 'app')
        ],
        exclude: [ // 排除文件范围
          path.resolve(__dirname, 'demo')
        ]
        // 最佳实践
        // test 中使用正则表达式
        // include 和 exclude 中使用绝对路径
        // 优先使用 include 而不是 exclude
        issuer: { test, include, exclude },
        // issuer 的应用范围
        enforce: 'pre',
        enforce: 'post',
        loader: 'babel-loader',
        // 使用 babel-loader 处理符合条件的文件
        // webpack2 中 -loader 不再可以省略
        options: {
          presets: ['es2015']
        }
        // babel-loader 的配置
      },
      {
        test: /\.html$/,
        use: [
          // 应用多个 loader 及相关配置
          'htmllint-loader',
          {
            loader: 'html-loader',
            options: {
              // ...
            }
          }
        ]
      },

      {
         oneOf: [ /* rules */ ],
         // 仅应用 rules 数组中的一条规则
      },

      {
        rules: [ /* rules */ ],
        // 应用所有 rules
      },

      {
        resource: { and: [ /* conditions */ ]}
        // 当所有 conditions 都满足时才认为符合条件
      },

      { resource: { or: [ /* conditions */ ]}, },
      { resource: [ /* conditions */] },
      // 当某一 condition 满足时即可认为符合条件

      {
        resource: { not: /* condition */ }
        // 当某一 condition 不满足时可认为符合条件
      }
    ],
    // rules 结束
  },
  resolve: {
    // 解析模块 request 时的配置
    // (不会应用于解析 loader )
    modules: [
      'node_modules',
      path.resolve(__dirname, 'app')
    ],
    // 解析 modules 时的路径
    extensions: ['.js', '.json', '.jsx', '.css'],
    // 可以省略的扩展名
    alias: {
      // 模块别名
      // 为 import 或 require 创建别名
      // Utilities: path.resolve(__dirname, 'src/utilities')
      // import Utility from '../../utilities/utility' 可以缩写为 
      // import Utility from 'Utilities/utility'
      // 末尾添加$可以进行精确匹配, 类似正则
      // 
      'module': 'new-module',
      // 别名 module 可以表示 new-module, 包括文件与路径, 比如 module/path/file 会被解析为 new-module/path/file
      'only-module$': 'new-module',
      // 别名 only-module(处于尾部) 会被解析成 new-module, 因此 module/path/file 不会被解析
    },
    // aliasFields: string
    // 指定一个 field, 比如 browser, 来执行特定的 alias
    // aliasFields: ['browser']
    // 
    // resolve.descriptionFiles: array
    // 用于描述的 JSON 文件, 默认为
    // descriptionFiles: ['package.json']
    // 
    // resolve.enforceExtension: boolean
    // 如果为 true, webpack 不会处理没有扩展名的文件, 默认为 false, 及允许 require('./foo') 这种写法
    //
    //resolve.enforceModuleExtension: boolean
    //默认为 false, 是否允许 modules(loaders) 省略扩展名
    //
    //resolve.extensions: array, 自动解析指定的扩展名
    //默认为
    // extensions: ['.js', '.json']
    // 因此可以使用 import File from ('../path/to/file') 这种写法
    // 
    // resolve.mainFields: array
    // 当 import 一个包时, 比如 import * as D3 from 'd3', 这个设置决定package.json中的哪个 field 对应的包的会被检索, 默认值基于 target 的值
    // 当 target 指定为 webworker, web 或不指定
    // mainFields: ['browser', 'module', 'main']
    // 对于其他 target, 
    // mainFields ['module', 'main']
    // 例如, D3的 package 包括这些 fields
    // {
    // ...
    // main: 'build/d3.Node.js',
    // browser: 'build/d3.js',
    // module: 'index'
    // ...
    // }
    // 
    // resolve.mainFiles: array
    // 这里的 filename 会在解析路径的时候用到, 默认为
    // mainFiles: ['index']
    // 
    // resolve.modules: array
    // 限定 webpack 搜索与解析的范围
    // 可以使用绝对路径偶相对路径, 但是他们的行为有少许不同.
    // 相对路径的检索方式与 Node 检索 module 相近, 首先检索当前路径及父路径
    // 绝对路径则只会检索给定路径
    // 默认值为 modules: ['node_modules']
    //
    // 如果要添加路径, 应当添加在 node_modules 前面
    // modules: [path.resolve(__dirname, 'src'), 'node_modules']
    // 
    // resolve.unsafeCache: regex | array | boolean
    // 默认为 true, 即允许模块缓存, 即使这种行为是不安全的
    // 
  },

  performance: {
    // 此配置决定 webpack 的通知功能
    // 主要用于对文件尺寸的控制
    hints: 'warning', // enum
    // hints: boolean | 'error' | 'warning'
    // 设定 webpack 何时显示通知
    // 默认为 warning

    maxAssetSize: 200000, // int (in bytes)
    // maxAssetSize: int, 这个选项限制了单个 asset 的尺寸, 默认为250000

    maxEntrypointSize: 400000, // int (in bytes)
    // maxEntrypointSize: int, 限制初始加载时单个 entry 的最大请求数, 默认为250000

    assetFilter: function (assetFilename) {
      return assetFilename.endsWith('.css') || assetFilename.endsWith('.js')
    }
    // assetFilter: function, 这个属性允许 webpack 控制使用哪些文件来计算 performance hint, 默认为
    // function (assertFilename) {
    //   return !(/\.map$/.test(assetFilename))
    // }
    //
    // 可以自己设定 filter function
    // assetFilter: function(assetFilename) {
    //   return assetFilename.endsWith('.js')
    // }
  },

  devtool: 'source-map', // enum, string | false
  // 可以提高 debug 的便利性, 但是会影响打包速度
  // 面向开发有: eval, inline-source-map, eval-source-map, cheap-module-eval-source-map
  // 面向生产有: source-map, hidden-source-map, cheap-source-map, nosources-source-map

  context: __dirname, // 使用绝对路径
  // webpack 的根路径, loaders 的配置文件(.babelrc, postcss.config.js)和 entry 都是基于 context 来解析的
  // 该属性值默认为当前目录, 但是强烈建议书写这一字段, 可以使你的配置文件独立于当前工作目录

  target: 'web', // enum
  // 打包文件运行环境
  // webpack 可以针对多个 environment 或 target 编译
  // target: string, (async-node|electron|electron-renderer|node|node-webkit|web|webworker)

  externals: ['react', /^@angular\//],
  // 不必打包的内容
  // externals: string| RegExp | function | array | object
  // 例如要使用 jquery 的 cdn, 则可以这样配置
  // externals: {
  //   jquery: 'jQuery'
  // }
  // import $ from 'jquery'
  // 
  // externals: {
  //   substract: ['./math', 'substract']
  // }
  // 会形成一个负责结构, ./math 是父模块, 而你的 bundle 仅请求其子模块 substract
  // 
  // externals: {
  //   react: 'react',
  //   lodash: {
  //     commonjs: 'lodash',
  //     amd: 'lodash',
  //     root: '_'
  //   }
  // }
  // 

  watch: boolean
  // webpack 可以监控文件并在他们变化时进行重新编译
  // 默认为 false

  watchOptions: object, 
  // 配置 watch 的模式
  // {
  //   aggregateTimeout: number, 从第一次检测到文件变化到进行重新编译的延迟时间, 这个段时间内的其他变化会被整合进此次编译
  //   ignored: string | RegExp, 忽视某些文件的变化, 减轻 CPU 负担
  //   poll: boolean | number, 每隔1000ms 进行一次检测
  // }

  stats: {
    // 待补充
  },

  devServer: {
    // 这一部分的设置会被 webpack-dev-server 采用
    contentBase: path.resolve(__dirname, 'dist'), // 指定 serve 目录为 dist
    compress: true, // 开启 gzip 压缩
    port: 9000, // 指定端口
    // 当 devServer 启动时, 控制台会打印
    // http://localhost:9000
    // webpack result is served from /build/
    // content is served from dist/
    // 这几行指出了 server 的位置和 served 内容
    // 如果你使用了 Node.js API, 那么这部分会被忽略
    // 
    // devServer.filename: string, 这个选项可以减少 lazy mode 下的编译次数
    // 默认的, 在 lazy mode 下, 每一个请求会进行一次新的编译 
    // 添加 filename 后, devServer 仅在请求 filename 时进行编译, 比如
    // lazy: true,
    // filename: bundle.js
    // 此时, devServer 仅在请求/ bundle.js 时进行编译
    // filename 仅在 lazy mode 中起作用
    // devServer.headers: object, 在所有 request 中添加请求头
    // headers: {
    //   'X-Custom-Foo': 'bar'
    // }
    // 
    // devServer.historyApiFallback: boolean, object
    // 当使用 Html5 History Api 时, index.html 可能得到404响应
    // 
    // devServer.hot: boolean, 开启 webpack 模块热替换
    // 
    // devServer.host - CLI only
    // 指定 host
    // 
    // devServer.https: boolean | object, devServer 默认false, 即使用 http
    // 
    // devServer.inline - CLI only
    // 切换 devServer 的模式, 默认使用 inline 模式, 即脚本会直接注入到你的 bundle 以实现重载, 设置 devServer.inline: false 可以启用 iframe 模式
    // 
    // 
  },

  plugins: [
    // 待补充
  ]
}

Webpack 2 起步

虽然和 webpack1 一样, 还是过一遍

创建一个 bundle

mkdir webpack-demo && webpack-demo

npm init -y

yarn add --dev webpack@beta

yarn add lodash
`</pre>

#### 创建 app/index.js

<pre>`// app/index.js
import _ from 'lodash'
function component () {
  let element = document.createElement('div');

// use lodash
  element.innerHtml = _.join(['Hello', 'webpack'], ' ')

  return element
}

document.body.appendChild(component())
`</pre>

#### 创建 index.html

<pre>`&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;webpack 2 demo&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;script src="public/bundle.js"&gt;&gt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
`</pre>

#### 创建打包后文件

<pre>`./node_modules/.bin/webpack app/index.js public/bundle.js
`</pre>

webpack 会自动处理包的依赖关系

### 添加配置文件

<pre>`var path = require('path')

module.exports = {
  entry: './app/index.js',
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'bundle.js'
  }
}
`</pre>

通过 webpack 命令行进行打包

<pre>`webpack --config webpack.config.js
`</pre>

`webpack` 会默认执行名为 `webpack.config.js` 的配置文件, 因此仅输入`webpack` 即可进行打包

### 通过 yarn 执行打包

在 `yarn init` 生成的 `package.json` 中添加

<pre>`{
  ...
  "scripts": {
    "build": "webpack"
  }
  ...
}

即可将webpack指令添加进 yarn 的快捷方式, 此时执行 yarn build 即可打包

Webpack 常用 Plugin 和 Loader

常用Loader

html 相关

html-loader
`</pre>

css 相关

<pre>`style-loader
css-loader
autoprefixer-loader
sass-loader
`</pre>

js 相关

<pre>`babel-loader
babel-core
babel-preset-es2015
babel-preset-react
babel-preset-stage3
eslint-loader
`</pre>

### 常用插件Plugin

#### config 类

<pre>`NormalModuleReplacementPlugin
ContextReplacementPlugin
IgnorePlugin
PrefetchPlugin
`</pre>

#### Optimize

<pre>`DedupePlugin
LimitChunkCountPlugin
OccurenceOrderPlugin
UglifyJsPlugin
CommonsChunkPlugin
`</pre>

#### 其他

<pre>`HotModuleReplacementPlugin
NoErrorPlugin
ProgressPlugin
HtmlWebpackPlugin

CommonChunks 插件

作用就是提取代码中的公共模块, 然后将公共模块打包到一个独立的文件中, 以便在其他的入口和模块中使用.

// in main.js
var a = require('./a');
a.sayHello();

var b = require('./b');
b.sayHello();

var c = require('./c');
c.sayHello()
`</pre>

<pre>`// in main2.js
var a = require('./a');
a.sayHello();

var b = require('./b');
b.sayHello();
`</pre>

a, b, c 和之前一样, 只有一个 sayHello 方法.

打包后看到`bundle.main.js` 和`bundle.main2.js` 中分别包含了 a, b, c 三个模块, 其中 a, b 是要使用`CommonChunksLoader` 提取出来的公共模块.

### 配置 CommonChunksLoader

<pre>`var webpack = require('webpack');
module.exports = {
  entry:{
    main1: './main',
    main2: './main2'
  },
  output:{
    filename: 'bundle.[name].js'
  },
  plugins:[
    new webpack.optimize.CommonsChunkPlugin('common.js',['main1','main2'])
  ]
} ;
`</pre>

第一行中, 添加 webpack 引用, 然后添加`plugins` 选项, 引用`webpack.optimize.CommonsChunkPlugin` 来提取公共模块, 参数`common.js` 表示公共模块的文件名, 后面的数组表示依赖这个模块的入口文件.

重新打包后, 生成三个文件

<pre>`bundle.main1.js,
bundle.main2.js
common.js

其中 bundle.main1.js 只包含了c模块, bundle.main2.js则没包含任何模块.

当然, 生成了common.js 还需要在 html 中进行引用, 而且要先于 main1, main2

Webpack 配置 React/babel

安装 react

npm install --save react react-dom
`</pre>

### 安装 babel-loader

<pre>`npm install --save-dev babel-loader babel-core babel-preset-es2015 babel-preset-react babel-preset-stage-2 
// 支持 ES2015, JSX, ES7
`</pre>

<pre>`npm install --save polyfill
`</pre>

<pre>`npm install --save babel-runtime
npm install --save-dev babel-plugin-transform-runtime
// 减少打包时候的重复代码
`</pre>

### 配置 babel

<pre>`// 在入口添加 polyfill
entry:[
  'babel-polyfill',
  'webpack/hot/dev-server',
  'webpack-dev-server/client?http://localhost:8080'
]
`</pre>

添加`.babalrc`

<pre>`{
  "presets":[
    "es2015",
    "react",
    "stage-2"
  ],
  "plugins":[
    "transform-runtime"
  ]
}

Webpack Dev Server

The webpack-dev-server is a little node.js Express server, which uses the webpack-dev-middleware to serve a webpack bundle. It also has a little runtime which is connected to the server via Socket.IO.

Let’s say you have the following config file(webpack.config.js)

var path = require('path');
module.exports = {
  entry:{
    app:['./app/main.js']
  },
  output:{
    path: path.resolve(__dirname, 'build'),
    publicPath:'/assets/',
    filename: 'bundle.js'
  },
}
`</pre>

You have an `app` folder with your initial entry point that webpack will bundle into a `bundle.js` file in the `build` folder.

### Content Base

The webpack-dev-server will serve the files in the **current directory**, unless you configure a specific content base.

<pre>`webpack-dev-server --content-base build/
`</pre>

it will serve you `build` folder.

This modified bundle is served from memory at the relative path specified in `publicPath`. Namely your bundle will be available as `localhost:8080/assets/bundle.js`

### Automatic Refresh

The webpack-dev-server supports multiple modes to automatic refresh pages:
  • Iframe mode(page is embedded in an iframe and reload on change)
  • Inline mode(as a small webpack-dev-server client entry is added to the bundle which refresh the page on change)

    Inline Mode

    To use the inline mode, specify --inline on the command line(you cannot specify it in configuration).

    配置 webpack-dev-server 自动刷新

    `// webpack.config.js
    entry: [
        'webpack/hot/dev-server',
        'webpack-dev-server/client?http://localhost:8080',
        path.resolve(__dirname, 'app/main.js')
      ],
      devServer:{
        historyApiFallback: true,
        hot:true,
        inline:true,
        progress: true
      }
    `

    `//package.json

“dev”: “webpack-dev-server –progress –colors –inline –hot –content-base build/“,
“build”: “webpack”
`

此时访问 localhost:8080 相当于访问 build/, 所以可以省略 index.html

Webpack 杂

loaders

在 React 里会用到 JSX, ES6, js, 统一使用 .js 作为后缀, 便于 babel 配置

npm install --save babel-loader

//
{
  test: /\.js?$/,
  exclude: /node_modules/,
  loader: 'babel',
  query:{
    presets: ['es2015', 'react']
  }
}
`</pre>

CSS, SCSS, ICONFONT 字体文件

<pre>`npm install --save style-loader css-loader sass-loader node-sass url-loader, file-loader
//
{
  test: /\.scss$/,
  loader: 'style!css!sass'
},
{
  test: /\.(png|jpg)$/,
  loader: 'url?limit-8192'
}

plugins

html-webpack-plugin 会由 entry 得配置将入口文件所属的 html 文件作为模板, 重新生成一个 html 文件, 其中的静态资源都已经根据配置打包好

使用 webpack 后, 对文件的版本 hash 会变得非常简单, 在出口文件 filename 中添加[hash]即可; 就目前而言, 清楚资源缓存的最好方法是文件名 hash, 由于每次改动后生成的文件名都不一样, 上线并不会覆盖之前的版本, 只是多了一个版本的文件, 页面即使同步有时间差, 要么访问旧版页面, 要么访问新版页面, 不会存在冲突. 这样服务器就可以放心的对静态资源开启永久强缓存, 并且有利于回滚, 至于静态资源产生的积压, 可以交由运维处理.

Webpack 开发和部署

Thanks to Webpack 傻瓜指南

启用 Source Map

在 webpack.config.js 中添加字段

devtool: 'eval-source-map',
`</pre>

这样出错后会采用 sourceMap 直接显示出错位置

### 配置 webpack-dev-server

在 webpack.config.js 中添加字段

<pre>`devServer: {
  historyApiFallback: true,
  hot: true,
  inline: true,
  progress: true,
}
`</pre>

### 使用 preLoaders 和 postLoaders

preLoaders 在 loaders 之前执行, 总体顺序是 preLoader -> loaders -> postLoaders

#### 安装 jshint

<pre>`npm install --save-dev jshint-loader
`</pre>

在 config 中配置

<pre>`module:{
  preloaders:[
    {
      test: /\.jsx$/,
      loader:'jshint-loader'
    }
  ]
}
jshint:{
  'esnext':true
}
`</pre>

### 部署上线

项目开发完成后需要部署上线, 此时应该创建一个新的 config, 因为部署上线使用的 webpack 不需要 dev-tools, dev-server, jshint 等

复制现有的 config 文件, 命名为 webpack.production.config.js, 将里面与开发有关的东西删除, 在 package.json 中添加

<pre>`"scripts":{
  "build": "webpack --progress --profile  --colors --config webpack.production.config.js"
},
`</pre>

上线的时候运行

<pre>`npm run build
`</pre>

### 分离第三方库

如果第三方库很多, 会导致最后 bundle 文件很大, 减慢加载速度, 因此需要把第三方库和 app 本身的代码分开

#### 修改 entry 入口文件

<pre>`entry:{
  app: path.resolve(APP_PATH, 'index.js'),
  //添加要打包在 vendors 里面的库
  vendors: ['jquery', [moment]]
},
`</pre>

#### 添加 CommonsChunkPlugin

<pre>`plugins: [
  //使用 uglifyJs 压缩 js 代码
  new webpack.optimize.UglifyJsPlugin({minimize: true}),
  //把入口文件里的数组打包成 vendors.js
  new webpack.optimize.CommonsChunkPlugin('vendors','vendors.js'),
  new HtmlWebpackPlugin({title: 'Welcome'})
]
`</pre>

运行 build 后会生成 vendor.js

### 生成多页面

假设需求是生成两个页面, 一个叫 index.html, 需要引用 app.js 和 vendors.js 两个文件; 另一个是 mobile.html, 需要应用 mobie.js 和 vendor.js 这两个文件

首先新建一个叫 mobile.js 的文件入口, 比 app.js 简单一些

<pre>`import './main.scss';
import $ from 'jquery';
import 'imports?jQuery=jQuery!./plugin.js';

$(document).ready(function(){
  let app = document.createElement('div');
  app.innerHTML = '&lt;h1&gt;Hello World&lt;/h1&gt;';
  document.body.appendChild(app);
  $('h1').greenify();
});
`</pre>

在 config 修改入口文件和输出配置

<pre>`entry:{
  // 三个入口文件: app, mobile, vendors
  app: path.resolve(APP_PATH, 'index.js'),
  mobile: path.resolve(APP_PATH, 'mobile.js'),
  vendors: ['jquery','moment']
},
output:{
   path: DIST_PATH,
   filename: '[name].js'
},
`</pre>

为 html-webpack-plugin 设置模板, 并保存于 templates

<pre>`// index.html
&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;{%= o.htmlWebpackPlugin.options.title %}&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h3&gt;Welcome to Index&lt;/h3&gt;
  &lt;/body&gt;
&lt;/html&gt;

//mobile.html
&lt;!DOCTYPE html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;{%= o.HtmlWebpackPlugin.options.title %}&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h3&gt;Welcome to Mobile&lt;/h3&gt;
  &lt;/body&gt;
&lt;/html&gt;
`</pre>

继续配置 config.js, 让 HtmlWebpackPlugin 可以生成多个文件

<pre>`//Templates 的文件夹路径
var TEM_PATH = path.resolve(ROOT_PATE, 'templates');
...
plugins:[
  //创建两个 HtmlWebpackPlugin 的实例, 生成两个页面
  new HtmlWebpackPlugin({
    title: 'Hello World App',
    template: path.resolve(TEM_PATH, 'index.html'),
    filename: 'index.html'
    //chunks 这个参数告诉插件要引用 chunks 中的那几个入口
    chunks: ['app','vendors'],
    //scripts 要插入的位置
    inject: 'body'
  }),
  new HtmlWebpackPlugin({
    title: 'Hello Mobile App',
    template: path.resolve(TEM_PATH, 'mobile.html'),
    filename: 'mobile.html',
    chunks: ['mobile','vendors'],
    inject: 'body'
  })
]

生成 hash 名称的 script 来防止缓存

webpack 基于 md5 可以生成 hash 名称
`
output:{
path: DIST_Path,
filename: ‘[name].[hash].js’
},

Webpack简明指南

Thanks to Webpack 傻瓜式指南

安装

npm install -g webpack
`</pre>

### 建立项目

<pre>`mkdir webpack
cd webpack
npm init
touch .gitignore
`</pre>

#### 项目结构

<pre>`project
|
|---app
|    |
|    |---index.js
|    |---sub.js
|
|---package.json
|
|---webpack.config.js
`</pre>

<pre>`//sub.js
function generateText(){
  var element = document.createElement('h2');
  element.innerHTML = "Hello, I'm h2";
  return element;
}
module.exports = generateText;
`</pre>

<pre>`//index.js
var sub = require('./sub');
var app = document.createElement('div');
app.innerHTML = "&lt;h1&gt;Hello, I'm h1";
app.appendChild(sub())
`</pre>

### 配置 Webpack

目的是将两个 js 根据依赖关系合并, 然后在 dist 目录创建一个 index.html 并引用合并后的 js.

这里安装一个 plugin, 可以快速生成 HTML

<pre>`npm install --save-dev html-webpack-plugin
`</pre>

修改 webpack.config.js

<pre>`var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
//定义文件夹路径
var ROOT_PATH = path.resolve(__dirname);
var APP_PATH = path.resolve(ROOT_PATH, 'app');
var DIST_PATH = path.resolve(ROOT_PATH, 'dist');

module.exports = {
  //项目文件夹, 可以直接用文件夹名, 默认寻找 index.js
  entry: APP_PATH,
  output: {
    path: DIST_PATH,
    filename: 'bundle.js'
  },
  //添加插件, 自动生成 html
  plugins: [
    new HtmlWebpackPlugin({title: 'Hello World App'})
  ]
};
`</pre>

然后在项目目录运行

<pre>`webpack
`</pre>

可以看到 html 已经引用了 bundle.js

### 配置 webpack-dev-server

用于自动刷新浏览器

安装webpack-dev-server

<pre>`npm install -g webpack-dev-server
`</pre>

修改 webpack.config.js

<pre>`module.exports = {
  ...
  devServer: {
    historyApiFallback: true,
    hot: true,
        inline: true,
    progress: true,
  },
  ...
}

`</pre>

修改 package.json

<pre>`...
"scripts": {
  "start": "webpack-dev-server --hot --inline"
},
...
`</pre>

在项目目录执行

<pre>`npm start
`</pre>

登录 http://localhost:8080

### 添加 CSS 样式

css-loader 会遍历 css 文件, style-loader 会把样式插入到&lt;style&gt;

<pre>`npm install -save-dev css-loader style-laoder
`</pre>

修改 webpack.config.js

<pre>`...
module:{
  loaders: [
    {
      test: /\.css$/,
      loaders: ['style','css'],
      include: APP_PATH
    }
  ]
}
...
`</pre>

注意 loaders 的书写方式, 从右向左执行

添加样式文件 app/main.css

<pre>`h1 {
  color: red;
}
`</pre>

在入口文件"index.js" 中引用

<pre>`require('./main.css')
`</pre>

### 使用 sass

这里需要 sass-loader 和 node-sass 一起解析文件

<pre>`npm install --save-dev sass-loader node-sass
`</pre>

修改 webpack.config.js

<pre>`//先删除 css 规则
{
  test: /\.scss$/,
  loaders: ['style','css','sass'],
  include: APP_PATH
},
`</pre>

添加两个 sass 文件 app/variables.scss 和 app/main.scss

<pre>`//variables.scss
$red: red;
`</pre>

<pre>`//main.scss
@import './variables.scss';
h1{
  color: $red;
}
`</pre>

在入口文件 index.js 中引用

<pre>`require('./main.scss');
`</pre>

### 处理图片

<pre>`npm install --save-dev url-loader
`</pre>

配置webpack.config.js

<pre>`{
  test: /\.(png|jpg)$/,
  loader: 'url?limit=8192'
}
`</pre>

注意, Limit参数制定了小于 8M 的图片会被转为 base64 编码, 可以减轻网络请求.

<pre>`@import './variabels.scss'
h1 {
  color: $red;
  background-image: url('./imgs/avatar.jpg');
}
`</pre>

### 添加第三方库

<pre>`npm install --save-dev jquery moment
`</pre>

修改 index.js

<pre>`var sub = require('./sub');
require('./main.scss');
var $ = require('jquery');
var moment = require('moment');
var app = document.createElement('div');
app.innerHTML = '&lt;h1&gt;Hello, h1';
document.body.append(app);
app.appendChild(sub());
$('body').append('&lt;p&gt;Inserted By Jquery. Now is ' + moment().format() + '&lt;/p&gt;');
`</pre>

### 添加 ES6 支持

安装 loader

<pre>`npm install -save-dev babel-loader babel-preset-es2015
`</pre>

配置 webpack.config.js

<pre>`...
{
  test: /\.jsx$/,
  loaders: babel,
  include: APP_PATH,
  query:{
    presets: ['es2015']
  }
}
...
`</pre>

es2015 这个参数是 babel 的 plugin, 可以支持最新的 ES6 的特性

现在可以 js 文件改为 ES6 风格

<pre>`//sub.js
export default function(){
  var element = document.createElement('h2');
  element.innerHTML = 'This is h2';
  return element;
}
`</pre>

<pre>`//index.js
import './main.scss';
import generateText from './sub';
import $ from 'jquery';
import moment from 'moment';

let app = document.createElement('div');
const myPromise = Promise.resolve(42);
myPromise.then((number) =&gt; {
  $('body').append('&lt;p&gt;Promise result is ' + number + 'now is ' + moment().format() + '&lt;/p&gt;');
});
app.innerHTML = '&lt;h1&gt;This is h1';
document.body.appendChild(app);
app.appendChild(generateText());

Webpack.config.js

entry

The entry point for the bundle.

If you pass a string: The string is resolved to a module which is loaded upon startup.

If you pass an array: All modules are loaded upon startup. The last one is exported.

entry:['./entry1','./entry2']
`</pre>

If you pass an object: Multiple entry bundles are created. The key is the chunk name. The value can be a string or an array

<pre>`{
  entry:{
    page1: './pages1',
    page2: ['./entry1','./entry2']
  },
  output: {
    // Make sure to use [name] or [id] in output.filename
    // when using multiple entry points
    filename: '[name].bundle.js',
    chunkFilename: '[id].bundle.js'
  }
}
`</pre>

### Output

`output` options tell webpack how to write the compiled files to disk.

Note, that while there can be multiple 'entry' points, only 'output' configuration is specified.

If you use hashing(`[hash]` or `[chunkhash]`) make sure to have a consistent ordering of modules. Use the `OccurenceOrderPlugin` or `recordsPath`

#### output.filename

Specifies the name of each output file on disk. You must **not** specify an absolute path here! The `output.path` option determines the location on disk the files are written to, `filename` is used solely for naming the individual files.

##### single entry

<pre>`{
  entry: './src/app.js',
  output: {
    filename: 'bundle.js',
    path: './build'
  }
}
`</pre>

##### multiple entries

If you configuration creates more than a single "chunk"(as with multiple entry points or when using plugins like CommonsChunkPlugin), you should use substitutions below to ensure that each file has a unique name.

`[name]` is replaced by the name of the chunk

`[hash]` is replaced by the hash of the compilation.

`[chunkhash]` is replaced by the hash of the chunk.

<pre>`{
  entry:{
    app: './src/app.js',
    search: './src/search.js'
  },
  output:{
    filename: [name].js,
    path: __dirname + '/built'
  }
}

output.path

The output directory as absolute path(required).

Module.loaders

An array of automatically applied loaders.

Each item can have there properties:

  • test: A condition that must be met
  • exclude: A condition that must not be met
  • include: A condition that must be met
  • loader: A string of ‘!’ separated loaders
  • ‘loaders’: An array of loaders as string

resolve

Webpack

Use webpack in a project

It’s best to have webpack as a dependency in your project, through this you can choose a local webpack version and will not be forced to use the single global one.
Add a package.json configuration file or npm with:

$ npm init
`</pre>

The answers to the question are not so important if you don't want to publish your project to npm.

Install and add `webpack` to the `package.json` with:

<pre>`$ npm install --save-dev webpack
`</pre>

### Create a modular JavaScript Project

Let's create some modules in JavaScript, using the CommonJS syntax:

#### cat.js

<pre>`var cats = ['dave','henry','martha'];
module.exports = cats;
`</pre>

#### app.js(Entry Point)

<pre>`var cats = require('cats.js');
console.log(cats);
`</pre>

The **Entry Point** is where your application will start, and where webpack will start tracking dependencies between modules.

Give webpack the entry point(app.js) and specify an output file(app.bundle.js):

<pre>`webpack ./app.js app.bundle.js
`</pre>

webpack will read and analyze the entry point and its dependencies(including transitive dependencies). Then it will bundle them into `app.bundle.js`.

Now your bundle is ready to run. Run `node app.bundle.js` and marvel in your abundance of cats.

To gain full access to webpack's flexibility, we need to create a "configuration file"

### Project Structure

We will put the source files in **src**, and bundled files in **dist**.
The final project structure will look like this:
![](https://raw.githubusercontent.com/dtinth/webpack-docs-images/2459637650502958669ea6b11bf49dc0b3b083ae/usage/project-structure.png)
  1. Create webpack.config.js

    `module.exports = {
      entry: './src/app.js',
      output: {
        path: './dist',
        filename: '[name].bundle.js'
      }
    };
    `

    A webpack configuration file is a CommonJS-Style module. So you can run any kind of code here, as long as a configuration object is exported out of its module.

  2. Using loaders
    Webpack only supports JavaScript modules natively, but most people will be using a transpiler for ES2015, CoffeeScript, TypeScript, etc. They can be used in webpack by using loaders.
    Loaders are special modules webpack uses to ‘load’ other modules(written in another language) into JavaScript(that webpack understands).

    using babel-loader
    1. Install Babel and the presets:

    `npm install --save-dev babel-loader
    `
  3. Configure Babel to use there presets by add .babelrc

    `{'presets':['es2015']}
    `
  4. Modify webpack.config.js to process all .js files using babel-loader.

    `module.exports = {
      entry: './src/app.js',
      output: {
        path: './dist',
        filename: '[name].bundle.js'
      },
      module: {
        loaders: [
          {test: /\.js$/,exclude: /node_modules/, loader: 'babel-loader',}
        ]
      }
    }
    `

    We are excluding node_modules here because otherwise all external libraries will also go through Babel, slowing down compilation.
    4. Install the libraries you want to use

    `npm install --save jquery babel-polyfill
    `

    We are using --save instead of --save-dev this time, as there libraries will be used in runtime. -dev means the libraries only used during development.

  5. Edit src/app.js

    `import ‘babel-polyfill’;
    import cats from ‘./cats’;
    import $ from ‘jquery’;

    $(‘<h1>Cats</h1>’).appendTo(‘body’);
    const ul = $(‘<ul></ul>’).appendTo(‘body’);
    for (const cat of cats) {

    $('&lt;li&gt;&lt;/li&gt;').text(cat).appendTo(ul);
    

    }

    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
        6\. Using Plugins
    An example would be minifying your file so that the client can load if faster. This can be done with **plugins**. We'll add the uglify plugin to our configuration:

    const webwebpack = require('webpack');
    module.exports = {
    entry: './src/app.js',
    output: {
    path: './dist',
    filename: '[name].bundle.js'
    },
    module: {
    loaders: [
    {test: /&#46;jsx?$/, exclude: /node_modules/,loader: 'babel-loader'}
    ]
    },

    plugins: [
    new webpack.optimize.UglifyJsPlugin({
    compress: {
    warning:false,
    },
    output: {
    comments: false,
    },
    }),
    ]
    }

The Uglify plugin is included with webpack so you don’t need to add additional modules, but this may not always be the case.

Webpack

Webpack 是一款模块加载兼打包工具, 能把各种资源, 例如 JS(X), coffee, css(sass/less), 图片等都作为模块来使用和处理

可以直接使用 require 的形式来引入各模块, 即时他们可能需要经过编译, webpack 上有各种健全的加载器(loader)会处理这些事情.

Webpack 优势

  1. webpack 是一 commonJS 的形式来书写脚本的, 但对 AMD/CMD 的支持也很全面, 方便就项目进行代码迁移
  2. 能被模块化的不仅是 JS 了
  3. 开发便捷, 能替代部分 grunt/gulp 工作, 比如打包, 压缩混淆, 图片转 base64等
  4. 扩展性强, 插件机制完善, 特别支持 React 热拔插(react-hot-loader).

安装与配置

常规使用 npm 安装
$npm install -g webpack
如果是常规项目, 还是把依赖写入 package.json 更好

$npm init
$npm install --save-dev webpack
`</pre>

### 配置

每个项目下都必须配置一个 webpack.config.js, 他的作用如同常规的 gulpfile.js, 就是一个配置项, 告诉 webpack 需要做什么

比如:

<pre>`var webpack = require('webpack');
var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js');

module.exports = {
  //插件项
  plugins: [commonsPlugin],

  //页面入口文件配置
  entry:{
    index: './src/js/page/entry1.js'
  },

  //文件输出配置
  output:{
    path: 'dist/js/page',
    filename: '[name].js'
  },

  module:{
    //加载器配置
    loaders:[
      {test: /\.css$/, loader: 'style-loader!css-loader'},
      {test: /\.js$/, loader: 'jsx-loader?harmony'},
      {test: /\.scss$/, loader: 'style!css!sass?sourceMap'},
      {test: /\.(png|jpg)$/, loader: 'url-loader?limit-8192'}
    ]
  },
};
`</pre>

#### plugins 插件项

这里使用了一个 CommonsChunkPlugin 的插件, 用于提取多个入口文件的公共部分, 然后生成一个 common.js 来访方便多页面之间的复用

#### entry 是页面入口文件配置, output 是输出配置

决定入口文件要生成什么名字的文件, 存放到哪里

<pre>`{
  entry: {
    page1: './page1',
    //支持数组, 将加载数组中的所有模块, 但一最后一个模块为输出
    page2: ['./entry1','./entry2']
  },
  output:{
    path:'dist/js/page',
    filename: '[name].bundle.js'
  }
}
`</pre>

这段代码最终会生成一个page1.bundle.js, 和一个page2.bundle.js, 并存放到./dist/page下

#### module.loaders 是最关键的配置

告诉 webpack 每一种文件都要用什么加载器来处理
多个 loader 之间用'!'连接
所有加载器都要通过 npm 来加载
配置信息的参数'?limit=8192'表示将所有小于8kb的图片都转为 base64格式, 超过8kb 的图片用 url-loader 来处理

### 运行 webpack

`$webpack --display-error-details`
后面的参数'--display-error-details' 是推荐加上的, 方便出错的时候能查阅更详细的信息
其他主要参数有

<pre>`$webpack --config XXX.js //使用另一份配置文件
$webpack --watch //监听变动并自动打包
$webpack -p //压缩混淆脚本, 这个很重要
$webpack -d //生成 map 隐射文件, 告知哪些模块被最终打包到哪里
`</pre>

### 模块引入

#### HTML

直接在页面&lt;body&gt;中引入 webpack 最终生成的页面脚本即可

<pre>`&lt;body&gt;
  &lt;script scr = 'dist/js/page/common.js'&gt;&lt;/script&gt;
  &lt;script scr = 'dist/js/page/index.js'&gt;&lt;/script&gt;
&lt;/body&gt;
`</pre>

可以看到连样式都不需要, 脚本执行的时候会动态生成&lt;style&gt;并注入 head

#### JS

各脚本模块可以直接用 commonJS 来书写, 并可以直接引入未经编译的模块, 比如 JSX, Sass, coffee 等(只要在 webpack.config.js 中配置好加载器)
看一下编译前的页面入口文件(index.js)

<pre>`require('../../css/reset.scss'); //加载 Reset 模块
require('../../css/allComponent.scss'); //加载组件模块
var React = require('react');
var AppWrap = require('redux').createRedux;
var Provider = require('redux/react').Provider;
var stores = require('AppStore');

var redux = createRedux(stores);
var App = React.createClass({
  render: function(){
    return (
      &lt;Provider redux = {redux}&gt;
        {function() {return &lt;AppWrap /&gt;;}}
      &lt;/Provider&gt;
    );
  }
});

ReactDOM.render(
  &lt;App /&gt;,document.body
);

后续的都由 webpack 处理

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×