webpack

image.png

前端将大型项目分成一个个单独的模块,一般封装好的每个模块都会实现一个目的明确的完成的功能。如何处理这些模块以及模块之间的依赖,将这些模块打包在一起成为一个完整的应用,就是webpack的任务。webpack一般从entry文件开始,逐步的搜索项目的所有依赖,通过各种loader(比如css-loader、url-loader、babel-loader)以及各种插件(如commonChunksPlugin、extractTextPlugin、UglifyJsWebpackPlugin、HtmlWebpackPlugin、InlineManifestWebpackPlugin、DefinePlugin、hashedModulesPlugin、OptimizeCssAssetsPlugin)处理优化资源文件,最终打包出一个或者多个适合在浏览器中运行的js文件

基本知识

  • entry
  • output
  • externals
  • loaders
  • plugins
  • resolve
  • configuration
  • modules
  • modules resolution
  • targets
  • manifest
  • code split
  • Caching

开发和发布环境配置

  • 通用配置
    1. 公共插件
  • 开发环境
    1. 使用source map
    2. 配置一个实现热更新的localhost的服务器
    3. 插件
      • connect-history-api-fallback
      • HotModuleReplacementPlugin
      • friendly-errors-webpack-plugin
  • 发布环境
    1. tree shaking
    2. source map
    3. Plugin
      • ExtractTextPlugin
      • DefinePlugin
      • CommonsChunkPlugin
      • inline-manifest-webpack-plugin
      • HashedModuleIdsPlugin
      • optimize-css-assets-webpack-plugin
      • ModuleConcatenationPlugin

css loaders

基本知识

entry

entry: { [entryChunkName:string]: string | Array(string) },配置入口文件的地址,可以是string或者string组成的array

  entry: {
    pageOne: './src/pageOne/index.js',
    pageTwo: './src/pageTwo/index.js',
    pageThree: './src/pageThree/index.js'
  }

output

  output: {
    filename: string  // 打包输出的文件名
    path: string  // 打包输出文件的路径
    chunkFilename: string // 代码分离时打包输出的块的名字
    publicPath: string // 配置资源的CDN或者hash地址
  }

externals

防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。

<script src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous"></script>

externals : {
  jquery: 'jQuery'
}

import $ from 'jquery'就会在运行时去获取通过 <script src="...">引入的jquery

externals : {
  lodash : {
    commonjs: "lodash",
    amd: "lodash",
    root: "_" // 指定全局变量
  }
}

// or

externals : {
  subtract : {
    root: ["math", "subtract"] //转换为父子结构,其中 math 是父模块,而 bundle 只引用 subtract 变量下的子集。
  }
}

通过window['math']['subtract']访问subtract

loaders

loaders是模块源码的转换器。可以将一些其他类型的语言(如typescript)等转换为javascript,内联图片和一些其他的资源,loaders甚至允许你在js中import css资源

  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          { loader: 'style-loader' },
          {
            loader: 'css-loader',
            options: {
              modules: true
            }
          }
        ]
      }
    ]
  }

在module.rules定义一些列loaders

  1. loaders可以被串联在一起。如sass: 'vue-style-loader!css-loader?sourceMap!sass-loader?indentedSyntax&sourceMap'
  2. loaders可以是同步或者异步的
  3. loaders运行在Nodejs中,所以可以使用Nodejs的资源
  4. loaders可以接受query参数
  5. loaders也可以配置options参数
  6. 插件可以给loaders提供更多特性

使用方法
loader的options:: 修改文件名,放在输出目录下,并返其对应的 url
test: 检测哪些文件需要此loader,是一个正则表达式
exclude: 忽略哪些文件

file-loader

处理资源文件,寻找资源文件并将其放在输出目录里面,为了更好地缓存文件,可以使用版本hash为资源文件命名。

 require("file?name=html-[hash:6].html!./page.html");
 // => html-109fa8.html

资源路径规则:

  1. 相对URLs:如./assets/logo.png会解释成require('./assets/logo.png')
  2. 没有前缀URLs:如assets/logo.png会被当做相对路径./assets/logo.png
  3. ~为前缀:如~assets/logo.png解释成require('assets/logo.png')
  4. 绝对路径: 不会被处理
url-loader

如果图片大小小于一个limit,转化为base64,否则回退到file-loader的功能

html-loader

把Html文件输出成字符串。它默认处理html中的[图片上传失败...(image-58f768-1513065689939)]require("./image.png"),同时需要在你的配置中指定image文件的加载器,比如:url-loader或者file-loader。

css-loader、postcss-loader、style-loader

postcss-loader:对css应用autoprefixer
css-loader:解决css文件中的路径问题,将css中资源作为依赖:如background: url(./logo.png),webpack以获取模块的方式来获取图片
style-loader:将css转化为js模块,注入 <style>标签

loader: 'style!css!postcss'

sass-loader

将sass解析成css,需要node-sass、webpack作为依赖。query parameters:https://github.com/sass/node-sass

Plugins

plugins是webpack的“骨”,webpack自己也是建立在plugins之上的。plugins可以做一些loaders做不了的工作

  plugins: [
    new webpack.optimize.UglifyJsPlugin(), // 丑化代码
    new HtmlWebpackPlugin({template: './src/index.html'})  // 提供html模板
  ]

reslove

resolve.modules

在resolve模块的时候,告诉webpack去哪些文件夹搜索

    modules: [
      resolve('src'),
      resolve('node_modules')
    ]

如果src文件夹下面有个a文件,那么就可以直接 require('a'),webpack就会去src文件夹下面寻找a。
会顺着引用依赖的文件目录逐级向上寻找,直至找到目标文件。例如 文件夹目录为

image.png

那么如果在main.jsimport 'a'会逐级向上搜索,在第一个src下面找到a文件就会返回这个文件对象,停止搜索

resolve.alias
    alias: {
      vue$: 'vue/dist/vue.common.js',
      Utilities: path.resolve(__dirname, 'src/utilities/')
    }

那么import 'src/utilities/a.js'就可以简写成import 'Utilities/a.js'
加一个$用于精确匹配路径。

resolve.extensions

用于搜索指定的扩展名
extensions: [".js", ".json", ".vue", ".ts"]

resolve.mainFields

当引入一个package.json中的包时,mianField字段决定了check这个包的package.json的哪个字段
当target设置为webwebworker或者未指定时,mainFields: ["browser", "module", "main"]
否则mainFields: ["module", "main"]
例如import * as D3 from "d3", D3包的package.json

{
  ...
  main: 'build/d3.Node.js',
  browser: 'build/d3.js',
  module: 'index',
  ...
}

就会从browser字段对应的地址(此处是build/d3.js)去寻找文件(按照mainFileds定义的先后顺序寻找文件)

resolve.mainFiles

当查找到的是一个文件目录,那么mainFiles决定了这个目录下查找的文件名
mainFiles: ["main"]
那么解决到文件目录时,返回main名字的文件

configuration、modules、modules resolution

configuration

webpack的配置文件就是导出对象的js文件。因为他是标准的Nodejs Commonjs模块,所以可以

  1. 通过require(...)引入文件
  2. 使用js的控制流表达式,如?:
  3. 使用constants(常量)和variables(变量)
  4. 编写和使用函数

webpack接受多种语言编写的配置文件。支持的文件后缀可以在node-interpret中找到。webpack能够处理Typescript、coffeeScript、Babel和JSX等编写的配置文件

除了导出一个对象文件,webpack还可以导出一个函数、一个promise和一个数组(数组里面是多种配置)

modules

在编程时,开发人员将程序分割成多个离散的成为功能的模块。编写良好的模块具有封装性和抽象性。应用程序中的每个模块都有一致的设计和清晰的目的,如验证、调试和测试
webpack的模块能够可以通过以下方式声明他们的依赖

  1. ES2015import声明
  2. CommonJSrequire声明
  3. AMDdefinerequire声明
  4. 在css/sass/less 文件@import 声明
  5. 样式表 (url(...)) 或者html (<img src=...>) 文件中的图片url

通过loaders,webpack支持一系列的语言和预处理器。loaders告诉webpack如何处理非js模块以及将这些模块打包到bundles里面。

modules Resolution

一个文件可以作为另外一个文件的依赖,reslover就是定位依赖文件(找到文件的绝对路径)的解决方案。

绝对路径

import "/home/me/file";
import "C:\\Users\\me\\file";

相对路径
找到requireimport发生的上下文目录。上下文目录的路径和相对路径拼接形成文件的绝对路径

modlue 路径
将搜索定义在 resolve.modules中的所有目录。也可以通过resolve.alias设置文件目录的别名

  • 如果最后resolve的路径是一个文件:
    如果路径带有文件名后缀,那么久定位到了文件。否则,定位以resolve.extensions选项中的后缀名为后缀的文件
  • 如果最后resolve路径是一个目录
    1. 如果目录下包含package.json,按照resolve.mainFields的options的先后顺序查找文件
    2. 如果没有package.json字段或者mainFields没有返回有效的路径,就会按照先后顺序在resolve.mainFiles中查找
    3. extension也是在resolve.extensions选项中找

resolve loaders
方法和查找文件的方法一致。只是使用resolveLoader

Targets

由于js可以用在浏览器和服务器端。webpack提供了target,例如

module.exports = {
  target: 'node'
};

webpack会将源代码编译成适用于Node.js类似环境运行的目标代码,
由于不能target不能是一个array,所以可以通过以下形式配置多个target环境

var path = require('path');
var serverConfig = {
  target: 'node',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'lib.node.js'
  }
  //…
};

var clientConfig = {
  target: 'web', // <=== can be omitted as default is 'web'
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'lib.js'
  }
  //…
};

module.exports = [ serverConfig, clientConfig ];

manifest

webpack打包的应用主要包括三部分代码

  1. 你或者你的团队成员编写的源代码
  2. 第三方库或者你的代码依赖的“vendor”
  3. 指挥模块间交互的runtime或者manifest

runtime
runtime的代码是在浏览器中运行的,主要包括loading和resolving逻辑。用于连接模块,控制模块间的交互

manifest
应用程序编译的时候,会保存一份关于所有模块的详细记录,这就是manifest,通过manifest,runtime会知道去哪里提取模块

在实际应用中,会使用CommonsChunkPlugin打包公共模块

new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks: function(module) {
    if(module.resource && (/^.*\.(css|scss)$/).test(module.resource)) {
      return false;
    }
    return module.context && module.context.indexOf("node_modules") !== -1;
  }
})

打包出来的vendor文件会包含应用程序文件的chunkId和chunkHash等信息

  entry: {
    main: './commonJs/c',
    main1: './commonJs/c1',
    main2: './commonJs/c2',
    main3: './commonJs/c3',
    main4: './commonJs/c4',
  }, // 一般entry文件只有一个,这里仅是一个说明例子
  output: {
    path: \_\_dirname + '/dist',
    filename: "[name].[chunkhash:7].js"
  }

打包出来的vendor会包含以下一段代码

var head = document.getElementsByTagName('head')[0];
script.src = __webpack_require.p + "" + chunkId + "." + ({"0":"main","1":"main1","2":"main2","3":"main3","4":"main4","5":"commons"}[chunkId]||chunkId) + "." + {"0":"0c9ed2","1":"9bef64","2":"dfadbc","3":"21a5a9","4":"d8fc6e","5":"7a6ed2"}[chunkId] + ".js";
head.appendChild(script);`

那么入口任意一个文件有些许变化,都会导致chunkId或者chunkHash的变化,从而导致整个vendor文件的重新加载,解决方法就是将这段代码提取出来,这段提取出来的代码即manifest

new webpack.optimize.CommonsChunkPlugin({
  name: 'manifest'
})

查看更多关于manifest信息

code split、懒加载

有时候打包出来的模块太大,可能会影响模块的加载速度,而有些代码不必马上加载,此时可以通过import实现代码分离

import(
  /* webpackChunkName: "my-chunk-name" */
  /* webpackMode: "lazy" */
  'lodash'
);

lodash 将会另外打包成一个js文件,而不会打包至当前的js文件中。

  • webpackChunkName: 打包输出的文件名,webpackChunkName对应webpack配置中output: { chunkFilename: '[name].[chunkhash:6].js' }里面的name

  • webpackMode:

  1. "lazy" (default): 对每一个import()的模块创建一个可以懒加载的chunk
  2. "lazy-once": 对所有的import()模块创建一个可以懒加载的chunk
  3. "eager": 不创建额外的chunk,所以也不会有额外的网络请求。对比静态importimport返回一个resolved的promise,在返回resolved的promise,模块不会执行
  4. "weak"

以下方法可以将a,b模块打包到一个chunk中

() => import(/* webpackChunkName: "a" */ './a')
() => import(/* webpackChunkName: "a" */ './b')

注意:

  1. 如果.babelrc的配置中"comments": false, 注释会失效,webpackChunkName, webpackMode也就失效了
  2. .babelrc配置需要引入"syntax-dynamic-import",即plugins: ["syntax-dynamic-import"]

Caching

通过webpack最后输出了一个可以部署在服务器中的dist目录。浏览器从服务器获取网站和资源,资源的获取可能会耗时,所以浏览器提供了caching技术,即资源没有改变时,浏览器就会使用缓存,不会重新去服务器获取新的代码

webpack通过根据文件内容生成hash,内容变化导致hash变化,进而重新去服务器请求新的code。

  output: {
    filename: "[name].[chunkhash:6].js", // 配置入口打包输出的hash
    chunkFilename: 'js/[name].[chunkhash:6].js' // 配置chunk的hash
  },
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif|svg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10000,
              name: 'assets/img/[name].[hash:7].[ext]'  // 配置图片资源的hash
            }
          }
        ]
      }
    ]
  }
  plugins: [
    new extractTextPlugin({
      filename: "[name].[contenthash:6].css",  // 提取出来的css文件的hash
      allChunks: true  // 有分离文件的样式也会全部压缩到一个css文件上
    })
  ]

开发和发布环境配置

发布环境的配置和开发环境不一样。在开发环境我们需要配置一个支持刷新或者热更新的localhost的服务器,需要source map。在发布环境,我们关注于如何缩小输出的bundle的体积,使用轻便的source map,优化资源来提升加载速度。通常建议对不同的环境写一个不同的配置。

对于两个环境,会有一些相同的配置,比如,入口文件,输出文件地址等。我们一般提取出相同的配置至webpack.common.js中,

通用配置

了解了基础配置以后,可以开始配置部分开发环境和发布环境的配置。
首先,我们有一个webpack.common.js,这个文件包含开发环境和测试环境的公共配置

const path = require('path');

//一个公共方法,用于resolve文件
function resolve (dir) {
  return path.join(__dirname, '..', dir)
}

module.exports = {
  entry: {
    app: [resolve('./src/main.js')]
  },
  output: {
    filename: '[name].js',
    path: resolve('dist'),
    publicPath: '/'
  },
  resolve: {
    extensions: ['.js'],
    modules: [
      resolve('src'),
      resolve('node_modules')
    ],
    alias: {
      'src': resolve('src')
    }
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: resolve('src')
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10000,
              name: 'assets/img/[name].[hash:7].[ext]'
            }
          }
        ]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10000,
              name: 'assets/img/[name].[hash:7].[ext]'
            }
          }
        ]
      }
    ]
  }
}

babel-loader: 对es6代码做一些处理,转化成适合在浏览器中运行的js代码。
url-loader: 如果图片大小小于一个limit,转化为base64,否则回退到file-loader的功能。所以再使用url-loader时候,要按照file-loader依赖

公共插件

webpack-merge
现在有一个公共的webpack配置webpack.common.js,在特定的development或者production环境的webpack配置中,我们需要合并公共配置,就可以使用webpack-merge

var commonConfig = require('./webpack.common.js')
const merge = require('webpack-merge')
const config = merge(commonConfig, {
  module: {
    rules: [
      ...
    ]
  },
  devtool: '#source-map',
  plugins: [
    ...
  ]
})

HtmlWebpackPlugin
此插件用于生产一个html文件。可以用HtmlWebpackPlugin直接生成html文件,也可以根据你提供的html文件,根据HtmlWebpackPlugin生成一个html文件。将这个生成的html文件打包到输出中。
基本用法:

var HtmlWebpackPlugin = require('html-webpack-plugin');
var path = require('path');

var webpackConfig = {
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js'
  },
  plugins: [new HtmlWebpackPlugin()]
};

会生成一个包含以下内容的dist/index.html文件

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>webpack App</title>
  </head>
  <body>
    <script src="index_bundle.js"></script>
  </body>

HtmlWebpackPlugin的配置项
template:html5模板地址,根据该template生成html
inject:true | 'head' | 'body' | false,inject为true或 'body',所有的js文件会注入body元素的最后面。inject为head时,js会注入head中
filename:生成的html文件名
title: html文件title
chunks:只添加指定的chunks
excludeChunks:指定不添加某些chunks
chunksSortMode:控制chunk的排序,有'none' | 'auto' | 'dependency' | {function} ,默认auto

DefinePlugin
创建全局的变量

new webpack.DefinePlugin({
  DEV: JSON.stringify(true)
  PRODUCTION: JSON.stringify(false)
})

如在只在测试环境下打印出一些log的时候,就会很有用,如:

DEV && (console.log('这是一个只会在测试环境下打印出的信息'))

开发环境

打造一个应用的开发环境。在开发环境我们需要source map,需要配置一个支持刷新或者热更新的localhost的服务器,

使用source map

由于浏览器中运行的代码都是打包输出的bundle,当你的代码出现错误或者警告的时候,可以通过source map定位到出错的源代码。此处以inline-source-map为例(更多devtool选择
devtool: 'inline-source-map'

配置一个实现热更新的localhost的服务器

  1. webpack-dev-server:一个简单的web服务器。使用webpack-dev-server后,任意修改一个文件,保存后,编译完成后刷新浏览器,就能看到更新。webpack-dev-server会把编译后的文件存放到内存里面,而不是输出到文件目录。
  2. webpack-dev-middleware:webpack-dev-middleware就是一个中间件,将webpack的修改发射给服务器。webpack-dev-server的内部也使用了webpack-dev-middleware
  3. webpack-hot-middleware:结合webpack-hot-middleware使用,实现无刷新更新(hot reload)

express可以用于构建一个web服务器,webpack-dev-middleware和webpack-hot-middleware都是适用于express的中间件,可以通过express构建一个可以启动一个适用于热更新的应用

development.js

const app = require('express')() 
const webpack = require('webpack')
const config = require('./webpack.dev.js')  // 获取测试环境的webpack配置
const compiler = webpack(config) 
const port = 3000

//告诉webpack-dev-middleware去使用webpack配置
const devMiddle = require('webpack-dev-middleware')(compiler, {
  quiet: true,
  publicPath: config.output.publicPath
})

//告诉webpack-hot-middleware去使用webpack配置
const hotMiddle = require('webpack-hot-middleware')(compiler, {
  log: () => {}
})

//告诉express服务器去使用中间件
app.use(devMiddle)
app.use(hotMiddle)

app.listen(port, function (err) {
  if (err) {
    console.log(err)
  } else {
    console.log('Listening at http://localhost:' + port + '\n')
  }
})

package.json中,通过"scripts": { "start": "node development.js", },就可以启动一个localhost服务
更多参考Express结合Webpack的全栈自动刷新

然后在每个entry后面增加一个hotMiddlewareScript,告诉浏览器在不能hot reload的时候,整页刷新

var hotMiddlewareScript = 'webpack-hot-middleware/client?reload=true'
module.exports = {
  entry: {
    app: ['./src/main.js', hotMiddlewareScript]
  }
}

完成上述配置后,包含HMR(热更新)的模块在代码被修改后就可以实现热更新。通常会全局开启代码热替换,可以通过插件webpack. HotModuleReplacementPlugin()实现

plugins: [
  new webpack. HotModuleReplacementPlugin()
]

适用于开发环境的一些其他插件

connect-history-api-fallback

在使用h5 history的api的时候,获取不到index.html的页面就会返回404错误,这时候就需要使用connect-history-api-fallback,对其他页面的请求也发送index.html给浏览器端

app.use(require('connect-history-api-fallback')())
app.use(devMiddleware)
app.use(hotMiddleware)

注意:connect-history-api-fallback中间件需要在devMiddleware之前使用

friendly-errors-webpack-plugin
友好的输出错误提示

plugins: [ new FriendlyErrorsPlugin({}) ]

开发环境webpack.dev.js的最终配置

const commonConfig = require('./webpack.common.js')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const webpack = require('webpack')

var hotMiddlewareScript = ['webpack-hot-middleware/client?noInfo=true&reload=true']
commonConfig.entry.app = commonConfig.entry.app.concat(hotMiddlewareScript)


var outputConfig = merge(commonConfig, {
  module: {
    rules: [
      // 对css处理的loader
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          }
          {
            loader: 'css-loader'
            option: {
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
  devtool: '#source-map',
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.DefinePlugin({
      __DEV__: true,
      __PRODUCTION__: false
    }),
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'src/index.dev.html',
      inject: true
    }),
    new FriendlyErrorsPlugin({})
  ]
})

module.exports = outputConfig

发布环境

在发布环境,我们关注于如何缩小输出的bundle的体积,使用轻便的source map,优化资源来提升加载速度

tree shaking(UglifyJSPlugin)

export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

上面的代码,如果你只用到了cube函数,打包后会发现square函数也在输出的bundle里面
把你的代码想象成一棵树,绿色的叶子代表有用的代码(如上述的cube),棕色的死了的叶子代表没有用到的代码(如上述的square),那么你用劲摇晃这棵树,希望将死掉的叶子摇晃下来(在output的bundle中剔除无用的代码)。这就是tree shaking

const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
  plugins: [new UglifyJSPlugin(
    compress: {
      warnings: false
    }
  )]
}

uglifyjs-webpack-plugin还会对代码进行压缩丑化,会大大减小打包输出文件的体积。
要实现tree shaking的效果需要注意:

  • 要实现es6模块语法(如: importexport
  • 能够丑化代码并且支持无用代码移除的插件(如UglifyJSPlugin)

source map
在运行基准测试的时候,source map是非常有用的,因此,建议在发布环境也配置source map,建议配置

module.exports = merge(common, {
    devtool: 'source-map', // 使用'source-map'选项
    plugins: [
      new UglifyJSPlugin({
         sourceMap: true 
      })
    ]
  })

ExtractTextPlugin
在打包输出的文件中,css也会被打包至js文件中。我们需要把这一部分css文件从js文件中提取至一个单独的css文件中。就需要用到ExtractTextPlugin,使用方法

const ExtractTextPlugin = require('extract-text-webpack-plugin')
module: {
  rules: [
    {
      test: /\.css$/,
      use: ExtractTextPlugin.extract({
        use: 'css-loader',
        fallback: 'vue-style-loader'
      })
    },
    {
      test: /\.css$/,
      use: ExtractTextPlugin.extract({
        use: 'css-loader',
        fallback: 'style-loader'
      })
    }
  ]
}
plugins: [
  new ExtractTextPlugin({
    filename: '[name].[contenthash:6].css',
    allChunks: true
  })
]

new ExtractTextPlugin(option)
这个option是一个对象,包含一个filename属性,表示打包输出的css文件名。
'[name].[contenthash:6].css',其中name表示打包输出的文件名,contenthash表示根据文件内容生成的哈希,contenthash:6表示长度为6

ExtractTextPlugin.extract(options: loader | object)
options.use:string | Array | object。表示把资源文件转化为css文件需要用到的loaders
options.fallback: css没有提取到的时候使用的loader
关于更多loaders的处理

  module: {
    rules: [
      {
        test: /\.css$/,
        use: extractTextPlugin.extract({
          use: [
            {
              loader: 'css-loader',
              options: {
                sourceMap: false
              }
            }
          ],
          fallback: 'style-loader'
        })
      },
      {
        test: /\.(sass|scss)$/,
        use: extractTextPlugin.extract({
          use: [
            {
              loader: 'css-loader',
              options: {
                sourceMap: false
              }
            },
            {
              loader: 'sass-loader',
              options: {
                sourceMap: false
              }
            }
          ],
          fallback: 'style-loader'
        })
      },
      {
        test: /\.vue$/,
        use: {
          loader: 'vue-loader',
          options: {
            loaders: {
              css: extractTextPlugin.extract({
                use: 'css-loader',
                fallback: 'vue-style-loader'
              }),
              sass:  extractTextPlugin.extract({
                use: 'css-loader!sass-loader?indentedSyntax',
                fallback: 'vue-style-loader'
              }),
              stylus: extractTextPlugin.extract({
                use: 'css-loader!stylus-loader',
                fallback: 'vue-style-loader'
              }),
              styl: extractTextPlugin.extract({
                use: 'css-loader!stylus-loader',
                fallback: 'vue-style-loader'
              }),
            }
          }
        }
      }
    ]
  }

extract-text-webpack-plugin的更多用法

DefinePlugin
定义一些常量,用于判断环境后执行一些特性环境下执行的logging或者testing,易于测试

  plugins: [
    new webpack.DefinePlugin({
        DEV: JSON.stringify(false),
        PRODUCTION: JSON.stringify(true)
    })
  ]

或者

  plugins: [
    new webpack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify('production')
    })
  ]

CommonsChunkPlugin
提取多个入口的公共代码

new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks: function(module) {
    // This prevents stylesheet resources with the .css or .scss extension
    // from being moved from their original chunk to the vendor chunk
    if(module.resource && (/^.*\.(css|scss)$/).test(module.resource)) {
      return false;
    }
    return module.context && module.context.indexOf("node_modules") !== -1;
  }
}),
new webpack.optimize.CommonsChunkPlugin({
  // 将vendor中的运行时(Runtime)提取至manifest中
  name: 'manifest'
})

提取出来的manifest文件非常的小(只有1000多字节),可以将manifest文件内联至index.html中,节省http请求,这时候就可以用到inline-manifest-webpack-plugin插件

inline-manifest-webpack-plugin


const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin')
plugins: [ new InlineManifestWebpackPlugin() ]

然后在html的body中添加<%=htmlWebpackPlugin.files.webpackManifest%>即可

<body>
    <%=htmlWebpackPlugin.files.webpackManifest%>
</body>

执行后,打开dist中的index.html文件,可以看到没有引入manifest文件,而是有一段内联的manifest代码

__ HashedModuleIdsPlugin__
比如,我们使用code split引入a, b, c, d四个文件,打开dist中的vendor文件可以看到

image.png

有一段webpackJsonp([5]表示vendor自身的chunkId

之后进行修改,只引入a,c,d三个文件。再打开dist/vendor.js,可以看到webpackJsonp([5]变成了webpackJsonp([4]

可以看出a,c,d三个文件以及vendor文件可能会因为自身chunkId的改变而变化,导致浏览器重新加载,使用HashedModuleIdsPlugin可以避免该问题

plugins: [ new webpack.HashedModuleIdsPlugin() ]

optimize-css-assets-webpack-plugin

对提取出来的css文件做优化,可以比较使用optimize-css-assets-webpack-plugin前后的css文件体积,会缩小很多

const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin")
plugins: [ new OptimizeCssAssetsPlugin() ]

__ ModuleConcatenationPlugin__
将一些有联系的模块,放到一个闭包函数里面去,通过减少闭包函数数量从而加快JS的执行速度。
更多解释

最后webpack.production.js配置

var commonConfig = require('./webpack.common.js')
const merge = require('webpack-merge')
const extractTextPlugin = require('extract-text-webpack-plugin')
const uglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin')
const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin")

module.exports = merge(commonConfig, {
  output: {
    filename: "[name].[chunkhash:6].js",
    chunkFilename: 'js/[name].[chunkhash:6].js',
    publicPath: '/'  // 此处根据实际情况填写资源存放的publicPath地址
  },
  plugins: [
    new extractTextPlugin({
      filename: "[name].[contenthash:6].css",
      allChunks: true
    }),
    new webpack.DefinePlugin({
      DEV: false,
      PRODUCTION: true,
      'process.env.NODE_ENV': '"production"'
    }),
    new uglifyjsWebpackPlugin({
      compress: {
        warnings: false
      },
      sourceMap: false
    }),
    new webpack.HashedModuleIdsPlugin(),
    new OptimizeCssAssetsPlugin(),
    new webpack.optimize.ModuleConcatenationPlugin(),
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'src/index.html',
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
      },
      // 保证vendor在app前加载
      chunksSortMode: 'dependency'
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function(module) {
        if(module.resource && (/^.*\.(css|scss)$/).test(module.resource)) {
          return false;
        }
        return module.context && module.context.indexOf("node_modules") !== -1;
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest'
    }),
    new InlineManifestWebpackPlugin()
  ],
  devtool: '#source-map'
})

css loaders

建议使用vue-cli封装的utils.js,已经对当前流行的各种类型的css进行了封装处理
例如utils.js

'use strict'
const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')

exports.cssLoaders = function (options) {
  options = options || {}

  const cssLoader = {
    loader: 'css-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }

  const postcssLoader = {
    loader: 'postcss-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }

  // generate loader string to be used with extract text plugin
  function generateLoaders (loader, loaderOptions) {
    const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]

    if (loader) {
      loaders.push({
        loader: loader + '-loader',
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap
        })
      })
    }

    // Extract CSS when that option is specified
    // (which is the case during production build)
    if (options.extract) {
      return ExtractTextPlugin.extract({
        use: loaders,
        fallback: 'vue-style-loader'
      })
    } else {
      return ['vue-style-loader'].concat(loaders)
    }
  }

  // https://vue-loader.vuejs.org/en/configurations/extract-css.html
  return {
    css: generateLoaders(),
    postcss: generateLoaders(),
    less: generateLoaders('less'),
    sass: generateLoaders('sass', { indentedSyntax: true }),
    scss: generateLoaders('sass'),
    stylus: generateLoaders('stylus'),
    styl: generateLoaders('stylus')
  }
}

// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
  const output = []
  const loaders = exports.cssLoaders(options)

  for (const extension in loaders) {
    const loader = loaders[extension]
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }

  return output
}

vue-loader.conf.js

'use strict'
const utils = require('./utils')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
const cacheBustingEnabled = !isProduction

const loaders = {
  loaders: utils.cssLoaders({
    sourceMap: sourceMapEnabled,
    extract: isProduction
  }),
  cssSourceMap: sourceMapEnabled,
  cacheBusting: cacheBustingEnabled,
  transformToRequire: {
    video: ['src', 'poster'],
    source: 'src',
    img: 'src',
    image: 'xlink:href'
  }
}

module.exports = loaders

webpack.common.js中,只需要

  // 对.vue文件中引入的样式做处理
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: require('./vue-loader.conf')
      }
}

webpack.dev.js中,添加代码

merge(commonConfig, {
  module: {
    rules: utils.styleLoaders({
      sourceMap: false,
      extract: false,
      usePostCSS: true
    })
  }
})

webpack.production.js中,添加代码

merge(commonConfig, {
  module: {
    rules: utils.styleLoaders({
      sourceMap: true,
      extract: true,
      usePostCSS: true
    })
  }
})

其他的plugins

CopyWebpackPlugin
用于拷贝文件

portscanner
var portscanner = require('portscanner')
 
// Checks the status of a single port 
portscanner.checkPortStatus(3000, '127.0.0.1', function(error, status) {
  // Status is 'open' if currently in use or 'closed' if available 
  console.log(status)
})
 
// Find the first available port. Asynchronously checks, so first port 
// determined as available is returned. 
portscanner.findAPortNotInUse(3000, 3010, '127.0.0.1', function(error, port) {
  console.log('AVAILABLE PORT AT: ' + port)
})
 
// Find the first port in use or blocked. Asynchronously checks, so first port 
// to respond is returned. 
portscanner.findAPortInUse(3000, 3010, '127.0.0.1', function(error, port) {
  console.log('PORT IN USE AT: ' + port)
})
check-dependencies
require('check-dependencies')(config, callback);
或者
require('check-dependencies')(config)
 .then(function (output) {
        /* handle output */ 
  });  
callback = { 
      status: number, // 0 if successful, 1 otherwise 
      depsWereOk: boolean, // true if dependencies were already satisfied 
      log: array, // array of logged messages 
      error: array, // array of logged errors
}
config = {
  packageManager: string // 'npm'(default) 或者'bower'
  packageDir: package.json或者bower.json目录
   install:安装缺失的package,默认false
  scopeList:去哪些keys寻找package的名字和版本,默认['dependencies', 'devDependencies']
  verbose:打印message
  log:打印dubug信息的function,verbose必须为true
  error:打印error信息的function,verbose必须为true
}

webpack中的资源优化

1 url-loader:将小的图片转为base64,内联在html中,减少资源的http请求
2 将一些不常改变的公共模块通过commonChunksPlugin抽离成一个或几个单独的js文件。缓存在本地,减少http请求
3 通过UglifyPlugin丑化文件,缩小文件体积
4 通过optimizeCssAssetsPlugin处理css文件,可以大幅度减少css文件体积
5 通过code spliting(es6的import()方法可以实现)实现按需加载,加速首屏加载
6 通过inlineManifestPlugin将体积较小的manifest文件内联在html中,避免多余的http请求
7 根据文件内容生成hash,用hash作为文件名。内容变化则hash变化,本地缓存失效,重新请求http。内容不变化情况下,使用本地缓存,减少http请求(有output的filename、chunkFilename的chunkHash、ExtractTextPlugin的contenthash,图片等资源的hash)
8 通过hashedModulesPlugin处理输出的文件,避免因文件名hash变化引起的缓存失效,重新请求资源

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342