webpack

image.png

前端將大型項(xiàng)目分成一個(gè)個(gè)單獨(dú)的模塊,一般封裝好的每個(gè)模塊都會(huì)實(shí)現(xiàn)一個(gè)目的明確的完成的功能。如何處理這些模塊以及模塊之間的依賴,將這些模塊打包在一起成為一個(gè)完整的應(yīng)用,就是webpack的任務(wù)。webpack一般從entry文件開始,逐步的搜索項(xiàng)目的所有依賴,通過各種loader(比如css-loader、url-loader、babel-loader)以及各種插件(如commonChunksPlugin、extractTextPlugin、UglifyJsWebpackPlugin、HtmlWebpackPlugin、InlineManifestWebpackPlugin、DefinePlugin、hashedModulesPlugin、OptimizeCssAssetsPlugin)處理優(yōu)化資源文件,最終打包出一個(gè)或者多個(gè)適合在瀏覽器中運(yùn)行的js文件

基本知識(shí)

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

開發(fā)和發(fā)布環(huán)境配置

  • 通用配置
    1. 公共插件
  • 開發(fā)環(huán)境
    1. 使用source map
    2. 配置一個(gè)實(shí)現(xiàn)熱更新的localhost的服務(wù)器
    3. 插件
      • connect-history-api-fallback
      • HotModuleReplacementPlugin
      • friendly-errors-webpack-plugin
  • 發(fā)布環(huán)境
    1. tree shaking
    2. source map
    3. Plugin
      • ExtractTextPlugin
      • DefinePlugin
      • CommonsChunkPlugin
      • inline-manifest-webpack-plugin
      • HashedModuleIdsPlugin
      • optimize-css-assets-webpack-plugin
      • ModuleConcatenationPlugin

css loaders

基本知識(shí)

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 // 代碼分離時(shí)打包輸出的塊的名字
    publicPath: string // 配置資源的CDN或者h(yuǎn)ash地址
  }

externals

防止將某些 import 的包(package)打包到 bundle 中,而是在運(yùn)行時(shí)(runtime)再去從外部獲取這些擴(kuò)展依賴(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'就會(huì)在運(yùn)行時(shí)去獲取通過 <script src="...">引入的jquery

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

// or

externals : {
  subtract : {
    root: ["math", "subtract"] //轉(zhuǎn)換為父子結(jié)構(gòu),其中 math 是父模塊,而 bundle 只引用 subtract 變量下的子集。
  }
}

通過window['math']['subtract']訪問subtract

loaders

loaders是模塊源碼的轉(zhuǎn)換器。可以將一些其他類型的語言(如typescript)等轉(zhuǎn)換為javascript,內(nèi)聯(lián)圖片和一些其他的資源,loaders甚至允許你在js中import css資源

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

在module.rules定義一些列l(wèi)oaders

  1. loaders可以被串聯(lián)在一起。如sass: 'vue-style-loader!css-loader?sourceMap!sass-loader?indentedSyntax&sourceMap'
  2. loaders可以是同步或者異步的
  3. loaders運(yùn)行在Nodejs中,所以可以使用Nodejs的資源
  4. loaders可以接受query參數(shù)
  5. loaders也可以配置options參數(shù)
  6. 插件可以給loaders提供更多特性

使用方法
loader的options:: 修改文件名,放在輸出目錄下,并返其對應(yīng)的 url
test: 檢測哪些文件需要此loader,是一個(gè)正則表達(dá)式
exclude: 忽略哪些文件

file-loader

處理資源文件,尋找資源文件并將其放在輸出目錄里面,為了更好地緩存文件,可以使用版本hash為資源文件命名。

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

資源路徑規(guī)則:

  1. 相對URLs:如./assets/logo.png會(huì)解釋成require('./assets/logo.png')
  2. 沒有前綴URLs:如assets/logo.png會(huì)被當(dāng)做相對路徑./assets/logo.png
  3. ~為前綴:如~assets/logo.png解釋成require('assets/logo.png')
  4. 絕對路徑: 不會(huì)被處理
url-loader

如果圖片大小小于一個(gè)limit,轉(zhuǎn)化為base64,否則回退到file-loader的功能

html-loader

把Html文件輸出成字符串。它默認(rèn)處理html中的[圖片上傳失敗...(image-58f768-1513065689939)]require("./image.png"),同時(shí)需要在你的配置中指定image文件的加載器,比如:url-loader或者file-loader。

css-loader、postcss-loader、style-loader

postcss-loader:對css應(yīng)用autoprefixer
css-loader:解決css文件中的路徑問題,將css中資源作為依賴:如background: url(./logo.png),webpack以獲取模塊的方式來獲取圖片
style-loader:將css轉(zhuǎn)化為js模塊,注入 <style>標(biāo)簽

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模塊的時(shí)候,告訴webpack去哪些文件夾搜索

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

如果src文件夾下面有個(gè)a文件,那么就可以直接 require('a'),webpack就會(huì)去src文件夾下面尋找a。
會(huì)順著引用依賴的文件目錄逐級向上尋找,直至找到目標(biāo)文件。例如 文件夾目錄為

image.png

那么如果在main.jsimport 'a'會(huì)逐級向上搜索,在第一個(gè)src下面找到a文件就會(huì)返回這個(gè)文件對象,停止搜索

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

那么import 'src/utilities/a.js'就可以簡寫成import 'Utilities/a.js'
加一個(gè)$用于精確匹配路徑。

resolve.extensions

用于搜索指定的擴(kuò)展名
extensions: [".js", ".json", ".vue", ".ts"]

resolve.mainFields

當(dāng)引入一個(gè)package.json中的包時(shí),mianField字段決定了check這個(gè)包的package.json的哪個(gè)字段
當(dāng)target設(shè)置為webwebworker或者未指定時(shí),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',
  ...
}

就會(huì)從browser字段對應(yīng)的地址(此處是build/d3.js)去尋找文件(按照mainFileds定義的先后順序?qū)ふ椅募?

resolve.mainFiles

當(dāng)查找到的是一個(gè)文件目錄,那么mainFiles決定了這個(gè)目錄下查找的文件名
mainFiles: ["main"]
那么解決到文件目錄時(shí),返回main名字的文件

configuration、modules、modules resolution

configuration

webpack的配置文件就是導(dǎo)出對象的js文件。因?yàn)樗菢?biāo)準(zhǔn)的Nodejs Commonjs模塊,所以可以

  1. 通過require(...)引入文件
  2. 使用js的控制流表達(dá)式,如?:
  3. 使用constants(常量)和variables(變量)
  4. 編寫和使用函數(shù)

webpack接受多種語言編寫的配置文件。支持的文件后綴可以在node-interpret中找到。webpack能夠處理Typescript、coffeeScript、Babel和JSX等編寫的配置文件

除了導(dǎo)出一個(gè)對象文件,webpack還可以導(dǎo)出一個(gè)函數(shù)、一個(gè)promise和一個(gè)數(shù)組(數(shù)組里面是多種配置)

modules

在編程時(shí),開發(fā)人員將程序分割成多個(gè)離散的成為功能的模塊。編寫良好的模塊具有封裝性和抽象性。應(yīng)用程序中的每個(gè)模塊都有一致的設(shè)計(jì)和清晰的目的,如驗(yàn)證、調(diào)試和測試
webpack的模塊能夠可以通過以下方式聲明他們的依賴

  1. ES2015import聲明
  2. CommonJSrequire聲明
  3. AMDdefinerequire聲明
  4. 在css/sass/less 文件@import 聲明
  5. 樣式表 (url(...)) 或者h(yuǎn)tml (<img src=...>) 文件中的圖片url

通過loaders,webpack支持一系列的語言和預(yù)處理器。loaders告訴webpack如何處理非js模塊以及將這些模塊打包到bundles里面。

modules Resolution

一個(gè)文件可以作為另外一個(gè)文件的依賴,reslover就是定位依賴文件(找到文件的絕對路徑)的解決方案。

絕對路徑

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

相對路徑
找到requireimport發(fā)生的上下文目錄。上下文目錄的路徑和相對路徑拼接形成文件的絕對路徑

modlue 路徑
將搜索定義在 resolve.modules中的所有目錄。也可以通過resolve.alias設(shè)置文件目錄的別名

  • 如果最后resolve的路徑是一個(gè)文件:
    如果路徑帶有文件名后綴,那么久定位到了文件。否則,定位以resolve.extensions選項(xiàng)中的后綴名為后綴的文件
  • 如果最后resolve路徑是一個(gè)目錄
    1. 如果目錄下包含package.json,按照resolve.mainFields的options的先后順序查找文件
    2. 如果沒有package.json字段或者mainFields沒有返回有效的路徑,就會(huì)按照先后順序在resolve.mainFiles中查找
    3. extension也是在resolve.extensions選項(xiàng)中找

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

Targets

由于js可以用在瀏覽器和服務(wù)器端。webpack提供了target,例如

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

webpack會(huì)將源代碼編譯成適用于Node.js類似環(huán)境運(yùn)行的目標(biāo)代碼,
由于不能target不能是一個(gè)array,所以可以通過以下形式配置多個(gè)target環(huán)境

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打包的應(yīng)用主要包括三部分代碼

  1. 你或者你的團(tuán)隊(duì)成員編寫的源代碼
  2. 第三方庫或者你的代碼依賴的“vendor”
  3. 指揮模塊間交互的runtime或者manifest

runtime
runtime的代碼是在瀏覽器中運(yùn)行的,主要包括loading和resolving邏輯。用于連接模塊,控制模塊間的交互

manifest
應(yīng)用程序編譯的時(shí)候,會(huì)保存一份關(guān)于所有模塊的詳細(xì)記錄,這就是manifest,通過manifest,runtime會(huì)知道去哪里提取模塊

在實(shí)際應(yīng)用中,會(huì)使用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文件會(huì)包含應(yīng)用程序文件的chunkId和chunkHash等信息

  entry: {
    main: './commonJs/c',
    main1: './commonJs/c1',
    main2: './commonJs/c2',
    main3: './commonJs/c3',
    main4: './commonJs/c4',
  }, // 一般entry文件只有一個(gè),這里僅是一個(gè)說明例子
  output: {
    path: \_\_dirname + '/dist',
    filename: "[name].[chunkhash:7].js"
  }

打包出來的vendor會(huì)包含以下一段代碼

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);`

那么入口任意一個(gè)文件有些許變化,都會(huì)導(dǎo)致chunkId或者chunkHash的變化,從而導(dǎo)致整個(gè)vendor文件的重新加載,解決方法就是將這段代碼提取出來,這段提取出來的代碼即manifest

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

查看更多關(guān)于manifest信息

code split、懶加載

有時(shí)候打包出來的模塊太大,可能會(huì)影響模塊的加載速度,而有些代碼不必馬上加載,此時(shí)可以通過import實(shí)現(xiàn)代碼分離

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

lodash 將會(huì)另外打包成一個(gè)js文件,而不會(huì)打包至當(dāng)前的js文件中。

  • webpackChunkName: 打包輸出的文件名,webpackChunkName對應(yīng)webpack配置中output: { chunkFilename: '[name].[chunkhash:6].js' }里面的name

  • webpackMode:

  1. "lazy" (default): 對每一個(gè)import()的模塊創(chuàng)建一個(gè)可以懶加載的chunk
  2. "lazy-once": 對所有的import()模塊創(chuàng)建一個(gè)可以懶加載的chunk
  3. "eager": 不創(chuàng)建額外的chunk,所以也不會(huì)有額外的網(wǎng)絡(luò)請求。對比靜態(tài)importimport返回一個(gè)resolved的promise,在返回resolved的promise,模塊不會(huì)執(zhí)行
  4. "weak"

以下方法可以將a,b模塊打包到一個(gè)chunk中

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

注意:

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

Caching

通過webpack最后輸出了一個(gè)可以部署在服務(wù)器中的dist目錄。瀏覽器從服務(wù)器獲取網(wǎng)站和資源,資源的獲取可能會(huì)耗時(shí),所以瀏覽器提供了caching技術(shù),即資源沒有改變時(shí),瀏覽器就會(huì)使用緩存,不會(huì)重新去服務(wù)器獲取新的代碼

webpack通過根據(jù)文件內(nèi)容生成hash,內(nèi)容變化導(dǎo)致hash變化,進(jìn)而重新去服務(wù)器請求新的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  // 有分離文件的樣式也會(huì)全部壓縮到一個(gè)css文件上
    })
  ]

開發(fā)和發(fā)布環(huán)境配置

發(fā)布環(huán)境的配置和開發(fā)環(huán)境不一樣。在開發(fā)環(huán)境我們需要配置一個(gè)支持刷新或者熱更新的localhost的服務(wù)器,需要source map。在發(fā)布環(huán)境,我們關(guān)注于如何縮小輸出的bundle的體積,使用輕便的source map,優(yōu)化資源來提升加載速度。通常建議對不同的環(huán)境寫一個(gè)不同的配置。

對于兩個(gè)環(huán)境,會(huì)有一些相同的配置,比如,入口文件,輸出文件地址等。我們一般提取出相同的配置至webpack.common.js中,

通用配置

了解了基礎(chǔ)配置以后,可以開始配置部分開發(fā)環(huán)境和發(fā)布環(huán)境的配置。
首先,我們有一個(gè)webpack.common.js,這個(gè)文件包含開發(fā)環(huán)境和測試環(huán)境的公共配置

const path = require('path');

//一個(gè)公共方法,用于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代碼做一些處理,轉(zhuǎn)化成適合在瀏覽器中運(yùn)行的js代碼。
url-loader: 如果圖片大小小于一個(gè)limit,轉(zhuǎn)化為base64,否則回退到file-loader的功能。所以再使用url-loader時(shí)候,要按照file-loader依賴

公共插件

webpack-merge
現(xiàn)在有一個(gè)公共的webpack配置webpack.common.js,在特定的development或者production環(huán)境的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
此插件用于生產(chǎn)一個(gè)html文件。可以用HtmlWebpackPlugin直接生成html文件,也可以根據(jù)你提供的html文件,根據(jù)HtmlWebpackPlugin生成一個(gè)html文件。將這個(gè)生成的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()]
};

會(huì)生成一個(gè)包含以下內(nèi)容的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的配置項(xiàng)
template:html5模板地址,根據(jù)該template生成html
inject:true | 'head' | 'body' | false,inject為true或 'body',所有的js文件會(huì)注入body元素的最后面。inject為head時(shí),js會(huì)注入head中
filename:生成的html文件名
title: html文件title
chunks:只添加指定的chunks
excludeChunks:指定不添加某些chunks
chunksSortMode:控制chunk的排序,有'none' | 'auto' | 'dependency' | {function} ,默認(rèn)auto

DefinePlugin
創(chuàng)建全局的變量

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

如在只在測試環(huán)境下打印出一些log的時(shí)候,就會(huì)很有用,如:

DEV && (console.log('這是一個(gè)只會(huì)在測試環(huán)境下打印出的信息'))

開發(fā)環(huán)境

打造一個(gè)應(yīng)用的開發(fā)環(huán)境。在開發(fā)環(huán)境我們需要source map,需要配置一個(gè)支持刷新或者熱更新的localhost的服務(wù)器,

使用source map

由于瀏覽器中運(yùn)行的代碼都是打包輸出的bundle,當(dāng)你的代碼出現(xiàn)錯(cuò)誤或者警告的時(shí)候,可以通過source map定位到出錯(cuò)的源代碼。此處以inline-source-map為例(更多devtool選擇
devtool: 'inline-source-map'

配置一個(gè)實(shí)現(xiàn)熱更新的localhost的服務(wù)器

  1. webpack-dev-server:一個(gè)簡單的web服務(wù)器。使用webpack-dev-server后,任意修改一個(gè)文件,保存后,編譯完成后刷新瀏覽器,就能看到更新。webpack-dev-server會(huì)把編譯后的文件存放到內(nèi)存里面,而不是輸出到文件目錄。
  2. webpack-dev-middleware:webpack-dev-middleware就是一個(gè)中間件,將webpack的修改發(fā)射給服務(wù)器。webpack-dev-server的內(nèi)部也使用了webpack-dev-middleware
  3. webpack-hot-middleware:結(jié)合webpack-hot-middleware使用,實(shí)現(xiàn)無刷新更新(hot reload)

express可以用于構(gòu)建一個(gè)web服務(wù)器,webpack-dev-middleware和webpack-hot-middleware都是適用于express的中間件,可以通過express構(gòu)建一個(gè)可以啟動(dòng)一個(gè)適用于熱更新的應(yīng)用

development.js

const app = require('express')() 
const webpack = require('webpack')
const config = require('./webpack.dev.js')  // 獲取測試環(huán)境的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服務(wù)器去使用中間件
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", },就可以啟動(dòng)一個(gè)localhost服務(wù)
更多參考Express結(jié)合Webpack的全棧自動(dòng)刷新

然后在每個(gè)entry后面增加一個(gè)hotMiddlewareScript,告訴瀏覽器在不能hot reload的時(shí)候,整頁刷新

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

完成上述配置后,包含HMR(熱更新)的模塊在代碼被修改后就可以實(shí)現(xiàn)熱更新。通常會(huì)全局開啟代碼熱替換,可以通過插件webpack. HotModuleReplacementPlugin()實(shí)現(xiàn)

plugins: [
  new webpack. HotModuleReplacementPlugin()
]

適用于開發(fā)環(huán)境的一些其他插件

connect-history-api-fallback

在使用h5 history的api的時(shí)候,獲取不到index.html的頁面就會(huì)返回404錯(cuò)誤,這時(shí)候就需要使用connect-history-api-fallback,對其他頁面的請求也發(fā)送index.html給瀏覽器端

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

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

friendly-errors-webpack-plugin
友好的輸出錯(cuò)誤提示

plugins: [ new FriendlyErrorsPlugin({}) ]

開發(fā)環(huán)境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

發(fā)布環(huán)境

在發(fā)布環(huán)境,我們關(guān)注于如何縮小輸出的bundle的體積,使用輕便的source map,優(yōu)化資源來提升加載速度

tree shaking(UglifyJSPlugin)

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

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

上面的代碼,如果你只用到了cube函數(shù),打包后會(huì)發(fā)現(xiàn)square函數(shù)也在輸出的bundle里面
把你的代碼想象成一棵樹,綠色的葉子代表有用的代碼(如上述的cube),棕色的死了的葉子代表沒有用到的代碼(如上述的square),那么你用勁搖晃這棵樹,希望將死掉的葉子搖晃下來(在output的bundle中剔除無用的代碼)。這就是tree shaking

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

uglifyjs-webpack-plugin還會(huì)對代碼進(jìn)行壓縮丑化,會(huì)大大減小打包輸出文件的體積。
要實(shí)現(xiàn)tree shaking的效果需要注意:

  • 要實(shí)現(xiàn)es6模塊語法(如: importexport
  • 能夠丑化代碼并且支持無用代碼移除的插件(如UglifyJSPlugin)

source map
在運(yùn)行基準(zhǔn)測試的時(shí)候,source map是非常有用的,因此,建議在發(fā)布環(huán)境也配置source map,建議配置

module.exports = merge(common, {
    devtool: 'source-map', // 使用'source-map'選項(xiàng)
    plugins: [
      new UglifyJSPlugin({
         sourceMap: true 
      })
    ]
  })

ExtractTextPlugin
在打包輸出的文件中,css也會(huì)被打包至js文件中。我們需要把這一部分css文件從js文件中提取至一個(gè)單獨(dú)的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)
這個(gè)option是一個(gè)對象,包含一個(gè)filename屬性,表示打包輸出的css文件名。
'[name].[contenthash:6].css',其中name表示打包輸出的文件名,contenthash表示根據(jù)文件內(nèi)容生成的哈希,contenthash:6表示長度為6

ExtractTextPlugin.extract(options: loader | object)
options.use:string | Array | object。表示把資源文件轉(zhuǎn)化為css文件需要用到的loaders
options.fallback: css沒有提取到的時(shí)候使用的loader
關(guān)于更多l(xiāng)oaders的處理

  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
定義一些常量,用于判斷環(huán)境后執(zhí)行一些特性環(huán)境下執(zhí)行的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
提取多個(gè)入口的公共代碼

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中的運(yùn)行時(shí)(Runtime)提取至manifest中
  name: 'manifest'
})

提取出來的manifest文件非常的小(只有1000多字節(jié)),可以將manifest文件內(nèi)聯(lián)至index.html中,節(jié)省http請求,這時(shí)候就可以用到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>

執(zhí)行后,打開dist中的index.html文件,可以看到?jīng)]有引入manifest文件,而是有一段內(nèi)聯(lián)的manifest代碼

__ HashedModuleIdsPlugin__
比如,我們使用code split引入a, b, c, d四個(gè)文件,打開dist中的vendor文件可以看到

image.png

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

之后進(jìn)行修改,只引入a,c,d三個(gè)文件。再打開dist/vendor.js,可以看到webpackJsonp([5]變成了webpackJsonp([4]

可以看出a,c,d三個(gè)文件以及vendor文件可能會(huì)因?yàn)樽陨韈hunkId的改變而變化,導(dǎo)致瀏覽器重新加載,使用HashedModuleIdsPlugin可以避免該問題

plugins: [ new webpack.HashedModuleIdsPlugin() ]

optimize-css-assets-webpack-plugin

對提取出來的css文件做優(yōu)化,可以比較使用optimize-css-assets-webpack-plugin前后的css文件體積,會(huì)縮小很多

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

__ ModuleConcatenationPlugin__
將一些有聯(lián)系的模塊,放到一個(gè)閉包函數(shù)里面去,通過減少閉包函數(shù)數(shù)量從而加快JS的執(zhí)行速度。
更多解釋

最后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: '/'  // 此處根據(jù)實(shí)際情況填寫資源存放的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,已經(jīng)對當(dāng)前流行的各種類型的css進(jìn)行了封裝處理
例如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,默認(rèn)false
??scopeList:去哪些keys尋找package的名字和版本,默認(rèn)['dependencies', 'devDependencies']
??verbose:打印message
??log:打印dubug信息的function,verbose必須為true
??error:打印error信息的function,verbose必須為true
}

webpack中的資源優(yōu)化

1 url-loader:將小的圖片轉(zhuǎn)為base64,內(nèi)聯(lián)在html中,減少資源的http請求
2 將一些不常改變的公共模塊通過commonChunksPlugin抽離成一個(gè)或幾個(gè)單獨(dú)的js文件。緩存在本地,減少http請求
3 通過UglifyPlugin丑化文件,縮小文件體積
4 通過optimizeCssAssetsPlugin處理css文件,可以大幅度減少css文件體積
5 通過code spliting(es6的import()方法可以實(shí)現(xiàn))實(shí)現(xiàn)按需加載,加速首屏加載
6 通過inlineManifestPlugin將體積較小的manifest文件內(nèi)聯(lián)在html中,避免多余的http請求
7 根據(jù)文件內(nèi)容生成hash,用hash作為文件名。內(nèi)容變化則hash變化,本地緩存失效,重新請求http。內(nèi)容不變化情況下,使用本地緩存,減少http請求(有output的filename、chunkFilename的chunkHash、ExtractTextPlugin的contenthash,圖片等資源的hash)
8 通過hashedModulesPlugin處理輸出的文件,避免因文件名hash變化引起的緩存失效,重新請求資源

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。