webpack

webpack_第1张图片
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. 公共插件
      • webpack-merge
      • HtmlWebpackPlugin
      • DefinePlugin
  • 开发环境
    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)。



externals : {
  jquery: 'jQuery'
}

import $ from 'jquery'就会在运行时去获取通过

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%>即可


    <%=htmlWebpackPlugin.files.webpackManifest%>

执行后,打开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变化引起的缓存失效,重新请求资源

你可能感兴趣的:(webpack)