webpack原理與實戰

webpack是一個js打包工具,不一個完整的前端構建工具。它的流行得益於模塊化和單頁應用的流行。webpack提供擴展機制,在龐大的社區支持下各類場景基本它均可找到解決方案。本文的目的是教會你用webpack解決實戰中常見的問題。css

webpack原理

在深刻實戰前先要知道webpack的運行原理html

webpack核心概念

  • entry 一個可執行模塊或庫的入口文件。
  • chunk 多個文件組成的一個代碼塊,例如把一個可執行模塊和它全部依賴的模塊組合和一個 chunk 這體現了webpack的打包機制。
  • loader 文件轉換器,例如把es6轉換爲es5,scss轉換爲css。
  • plugin 插件,用於擴展webpack的功能,在webpack構建生命週期的節點上加入擴展hook爲webpack加入功能。

webpack構建流程

從啓動webpack構建到輸出結果經歷了一系列過程,它們是:前端

  1. 解析webpack配置參數,合併從shell傳入和webpack.config.js文件裏配置的參數,生產最後的配置結果。
  2. 註冊全部配置的插件,好讓插件監聽webpack構建生命週期的事件節點,以作出對應的反應。
  3. 從配置的entry入口文件開始解析文件構建AST語法樹,找出每一個文件所依賴的文件,遞歸下去。
  4. 在解析文件遞歸的過程當中根據文件類型和loader配置找出合適的loader用來對文件進行轉換。
  5. 遞歸完後獲得每一個文件的最終結果,根據entry配置生成代碼塊chunk
  6. 輸出全部chunk到文件系統。

須要注意的是,在構建生命週期中有一系列插件在合適的時機作了合適的事情,好比UglifyJsPlugin會在loader轉換遞歸完後對結果再使用UglifyJs壓縮覆蓋以前的結果。node

場景和方案

經過各類場景和對應的解決方案讓你深刻掌握webpackreact

單頁應用

demo redemo
一個單頁應用須要配置一個entry指明執行入口,webpack會爲entry生成一個包含這個入口全部依賴文件的chunk,但要讓它在瀏覽器裏跑起來還須要一個HTML文件來加載chunk生成的js文件,若是提取出了css還須要讓HTML文件引入提取出的css。web-webpack-plugin裏的WebPlugin能夠自動的完成這些工做。webpack

webpack配置文件git

const { WebPlugin } = require('web-webpack-plugin');
module.exports = {
  entry: {
    app: './src/doc/index.js',
  },
  plugins: [
    // 一個WebPlugin對應生成一個html文件
    new WebPlugin({
      //輸出的html文件名稱
      filename: 'index.html',
      //這個html依賴的`entry`
      requires: ['app'],
    }),
  ],
};

requires: ['doc']指明這個HTML依賴哪些entryentry生成的js和css會自動注入到HTML裏。
你還能夠配置這些資源的注入方式,支持以下屬性:es6

  • _dist 只有在生產環境下才引入該資源
  • _dev 只有在開發環境下才引入該資源
  • _inline 把該資源的內容潛入到html裏
  • _ie 只有IE瀏覽器才須要引入的資源

要設置這些屬性能夠經過在js裏配置github

new WebPlugin({
    filename: 'index.html',
    requires: {
         app:{
              _dist:true,
              _inline:false,
         }
    },
}),

或者在模版裏設置,使用模版的好處是靈活的控制資源注入點。web

new WebPlugin({
      filename: 'index.html',
      template: './template.html',
}),
<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <link rel="stylesheet" href="app?_inline">
    <script src="ie-polyfill?_ie"></script>
</head>
<body>
<div id="react-body"></div>
<script src="app"></script>
</body>
</html>

WebPlugin插件借鑑了fis3的思想,補足了webpack缺失的以HTML爲入口的功能。想了解WebPlugin的更多功能,見文檔

一個項目裏管理多個單頁應用

通常項目裏會包含多個單頁應用,雖然多個單頁應用也能夠合併成一個可是這樣作會致使用戶沒訪問的部分也加載了。若是項目裏有不少個單頁應用,爲每一個單頁應用配置一個entryWebPlugin ?若是項目又新增了一個單頁應用,又去新增webpack配置?這樣作太麻煩了,web-webpack-plugin裏的AutoWebPlugin能夠方便的解決這些問題。

module.exports = {
    plugins: [
        // 全部頁面的入口目錄
        new AutoWebPlugin('./src/'),
    ]
};

AutoWebPlugin會把./src/目錄下全部每一個文件夾做爲一個單頁頁面的入口,自動爲全部的頁面入口配置一個WebPlugin輸出對應的html。要新增一個頁面就在./src/下新建一個文件夾包含這個單頁應用所依賴的代碼,AutoWebPlugin自動生成一個名叫文件夾名稱的html文件。AutoWebPlugin的更多功能見文檔

代碼分割優化

一個好的代碼分割對瀏覽器首屏效果提高很大。好比對於最多見的react體系你能夠

  1. 先抽出基礎庫react react-dom redux react-redux到一個單獨的文件而不是和其它文件放在一塊兒打包爲一個文件,這樣作的好處是隻要你不升級他們的版本這個文件永遠不會被刷新。若是你把這些基礎庫和業務代碼打包在一個文件裏每次改動業務代碼都會致使文件hash值變化從而致使緩存失效瀏覽器重複下載這些包含基礎庫的代碼。以上的配置爲:
// vender.js 文件抽離基礎庫到單獨的一個文件裏防止跟隨業務代碼被刷新
// 全部頁面都依賴的第三方庫
// react基礎
import 'react';
import 'react-dom';
import 'react-redux';
// redux基礎
import 'redux';
import 'redux-thunk';
// webpack配置
{
  entry: {
    vendor: './path/to/vendor.js',
  },
}
  1. 再經過CommonsChunkPlugin能夠提取出多個代碼塊都依賴的代碼造成一個單獨的chunk。在應用有多個頁面的場景下提取出全部頁面公共的代碼減小單個頁面的代碼,在不一樣頁面之間切換時全部頁面公共的代碼以前被加載過而沒必要從新加載。

構建npm包

demo remd
除了構建可運行的web應用,webpack也可用來構建發佈到npm上去的給別人調用的js庫。

const nodeExternals = require('webpack-node-externals');
module.exports = {
  entry: {
    index: './src/index.js',
  },
  externals: [nodeExternals()],
  target: 'node',
  output: {
    path: path.resolve(__dirname, '.npm'),
    filename: '[name].js',
    libraryTarget: 'commonjs2',
  },
};

這裏有幾個區別於web應用不一樣的地方:

  • externals: [nodeExternals()]用於排除node_modules目錄下的代碼被打包進去,由於放在node_modules目錄下的代碼應該經過npm安裝。
  • libraryTarget: 'commonjs2'指出entry是一個可供別人調用的庫而不是可執行的,輸出的js文件按照commonjs規範。

構建服務端渲染

服務端渲染的代碼要運行在nodejs環境,和瀏覽器不一樣的是,服務端渲染代碼須要採用commonjs規範同時不該該包含除js以外的文件好比css。webpack配置以下:

module.exports = {
  target: 'node',
  entry: {
    'server_render': './src/server_render',
  },
  output: {
    filename: './dist/server/[name].js',
    libraryTarget: 'commonjs2',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
      },
      {
        test: /\.(scss|css|pdf)$/,
        loader: 'ignore-loader',
      },
    ]
  },
};

其中幾個關鍵的地方在於:

  • target: 'node' 指明構建出的代碼是要運行在node環境裏
  • libraryTarget: 'commonjs2' 指明輸出的代碼要是commonjs規範
  • {test: /\.(scss|css|pdf)$/,loader: 'ignore-loader'} 是爲了防止不能在node裏執行服務端渲染也用不上的文件被打包進去。

從fis3遷移到webpack

fis3和webpack有類似的地方也有不一樣的地方。類似在於他們都採用commonjs規範,不一樣在於導入css這些非js資源的方式。fis3經過// @require './index.scss'而webpack經過require('./index.scss')。若是想從fis3平滑遷移到webpack可使用comment-require-loader。好比你想在webpack構建是使用採用了fis3方式的imui模塊,配置以下:

loaders:[{
     test: /\.js$/,
     loaders: ['comment-require-loader'],
     include: [path.resolve(__dirname, 'node_modules/imui'),]
}]

自定義webpack擴展

若是你在社區找不到你的應用場景的解決方案,那就須要本身動手了寫loader或者plugin了。
在你編寫自定義webpack擴展前你須要想明白究竟是要作一個loader仍是plugin呢?能夠這樣判斷:

若是你的擴展是想對一個個單獨的文件進行轉換那麼就編寫 loader剩下的都是 plugin

其中對文件進行轉換能夠是像:

  • babel-loader把es6轉換成es5
  • file-loader把文件替換成對應的URL
  • raw-loader注入文本文件內容到代碼裏去

編寫 webpack loader

demo comment-require-loader
編寫loader很是簡單,以comment-require-loader爲例:

module.exports = function (content) {
    return replace(content);
};

loader的入口須要導出一個函數,這個函數要乾的事情就是轉換一個文件的內容。
函數接收的參數content是一個文件在轉換前的字符串形式內容,須要返回一個新的字符串形式內容做爲轉換後的結果,全部經過模塊化倒入的文件都會通過loader。從這裏能夠看出loader只能處理一個個單獨的文件而不能處理代碼塊。想編寫更復雜的loader可參考官方文檔

編寫 webpack plugin

demo end-webpack-plugin
plugin應用場景普遍,因此稍微複雜點。以end-webpack-plugin爲例:

class EndWebpackPlugin {

    constructor(doneCallback, failCallback) {
        this.doneCallback = doneCallback;
        this.failCallback = failCallback;
    }

    apply(compiler) {
        // 監聽webpack生命週期裏的事件,作相應的處理
        compiler.plugin('done', (stats) => {
            this.doneCallback(stats);
        });
        compiler.plugin('failed', (err) => {
            this.failCallback(err);
        });
    }
}

module.exports = EndWebpackPlugin;

loader的入口須要導出一個class, 在new EndWebpackPlugin()的時候經過構造函數傳入這個插件須要的參數,在webpack啓動的時候會先實例化plugin再調用pluginapply方法,插件須要在apply函數裏監聽webpack生命週期裏的事件,作相應的處理。
webpack plugin 裏有2個核心概念:

  • Compiler: 從webpack啓動到推出只存在一個CompilerCompiler存放着webpack配置
  • Compilation: 因爲webpack的監聽文件變化自動編譯機制,Compilation表明一次編譯。

CompilerCompilation 都會廣播一系列事件。
webpack生命週期裏有很是多的事件能夠在event-hooksCompilation裏查到。以上只是一個最簡單的demo,更復雜的能夠查看 how to write a plugin或參考web-webpack-plugin

總結

webpack其實很簡單,能夠用一句話涵蓋它的本質:

webpack是一個打包模塊化js的工具,能夠經過loader轉換文件,經過plugin擴展功能。

若是webpack讓你感到複雜,必定是各類loader和plugin的緣由。
但願本文能讓你明白webpack的原理與本質讓你能夠在實戰中靈活應用webpack。

閱讀原文

相關文章
相關標籤/搜索