「前端」看懂前端腳手架你需要這篇webpack

尚妝產品技術刊讀發表於2017-03-14

本文來自尚妝前端團隊南洋

發表於尚妝github部落格,歡迎訂閱。

分割webpack配置檔案的多種方法

(一)

將你的配置資訊寫到多個分散的檔案中去,然後在執行webpack的時候利用--config引數指定要載入的配置檔案,配置檔案利用moduleimports匯出。你可以在webpack/react-starter 看到是使用這種發方法的。

// webpack 配置檔案

|-- webpack-dev-server.config.js
|-- webpack-hot-dev-server.config.js
|-- webpack-production.config.js
|-- webpack.config.js複製程式碼
// npm 命令

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev-server": "webpack-dev-server --config webpack-dev-server.config.js --progress --colors --port 2992 --inline",
    "hot-dev-server": "webpack-dev-server --config webpack-hot-dev-server.config.js --hot --progress --colors --port 2992 --inline",
    "build": "webpack --config webpack-production.config.js --progress --profile --colors"
  },複製程式碼

(二)

呼叫第三方的webpack工具,使用其整合的api,方便進行webpack配置。HenrikJoreteg/hjs-webpack 這個repo就是這麼做的。

var getConfig = require('hjs-webpack')


module.exports = getConfig({
  // entry point for the app
  in: 'src/app.js',

  // Name or full path of output directory
  // commonly named `www` or `public`. This
  // is where your fully static site should
  // end up for simple deployment.
  out: 'public',

  // This will destroy and re-create your
  // `out` folder before building so you always
  // get a fresh folder. Usually you want this
  // but since it's destructive we make it
  // false by default
  clearBeforeBuild: true
})複製程式碼

(三) Scalable webpack configurations

ones that can be reused and combined with other partial configurations

在單個配置檔案中維護配置,但是區分好條件分支。呼叫不同的npm命令時候設定不同的環境變數,然後在分支中匹配,返回我們需要的配置檔案。

這樣做的好處可以在一個檔案中管理不同npm操作的邏輯,並且可以共用相同的配置。webpack-merge這個模組可以起到合併配置的作用。


const parts = require('./webpack-config/parts');

switch(process.env.npm_lifecycle_event) {
  case 'build': 
    config = merge(common, 
      parts.clean(PATHS.build),
      parts.setupSourceMapForBuild(),
      parts.setupCSS(PATHS.app),
      parts.extractBundle({
        name: 'vendor',
        entries: ['react', 'vue', 'vuex']
      }),
      parts.setFreeVariable('process.env.NODE_ENV', 'production'),
      parts.minify()
      );
    break;
  default: 
    config = merge(common, 
      parts.setupSourceMapForDev(),
      parts.devServer(), 
      parts.setupCSS(PATHS.app));
}複製程式碼
// minify example
exports.minify = function () {
  return {
    plugins: [
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false,
          drop_console: true
        },
        comments: false,
        beautify: false
      })
    ]
  }
}複製程式碼

開發環境下的自動重新整理

webpack-dev-server

webpack-dev-server在webpack的watch基礎上開啟伺服器。

webpack-dev-server是執行在記憶體中的開發伺服器,支援高階webpack特性hot module replacement。這對於react vue這種元件化開發是很方便的。

使用webpack-dev-server命令開啟伺服器,配合HMR及可以實現程式碼更改瀏覽器區域性重新整理的能力。

hot module replacement

Hot Module Replacement (HMR) exchanges, adds, or removes modules while an application is running without a page reload.
當應用在執行期間hmr機制能夠修改、新增、或者移除相應的模組,而不使整個頁面重新整理。

hmr機制適用於單頁應用。

要實現hmr機制,需要配合webpack-dev-server伺服器,這個伺服器本身就實現了監察watch檔案改動的能力,再開啟HMR選項,就新增了watch模組變化的能力。這是HMR機制能生效的基礎。

從webpack編譯器角度

每次修改一個模組的時候,webpack會生成兩部分,一個是manifest.json,另一部分是關於這次模組更新編譯完成的chunks。manifest.json中存著的是chunk更改前後的hash值。

從編譯器webpack的角度來講提供了hmr的原材料。供後續使用。

從模組的角度

模組發生變化時,webpack會生成之前講過的兩部分基礎檔案,但是何時將變化後的模組應用到app中去?這裡就需要在應用程式碼中編寫handler去接受到模組變化資訊。但是不能在所有模組中編寫handler吧?這裡就用到了訊息冒泡機制。

「前端」看懂前端腳手架你需要這篇webpack

如圖A.js、C.js沒有相關hmr程式碼,B.js有相關hmr程式碼,如果c模組發生了變化,c模組沒有hmr,那麼就會冒泡到a、b模組。b模組捕捉到了訊息,hmr執行時會相應的執行一些操作,而a.js捕捉不到資訊,會冒泡到entry.js,而一旦有訊息冒泡的入口塊,這就代表本次hmr失敗了,hmr會降級進行整個頁面的reload。

從HMR執行時的角度

HMR執行時是一些相關的操作api,執行時支援兩個方法: checkapply

check發起 HTTP 請求去獲取更新的 manifest,以及一些更新過後的chunk。

「前端」看懂前端腳手架你需要這篇webpack

環境變數的設定

var env = {
  'process.env.NODE_ENV': '"production"'
}
new webpack.DefinePlugin(env)複製程式碼

注意這裡單引號間多了個雙引號 why?

以及webpack.DefinePlugin外掛的原理?

開發的時候會想寫很多隻在開發環境出現的程式碼,比如介面mock等,在build命令後這些程式碼不會存在。

這對框架或者外掛、元件的開發是很有幫助的。vue,react等都會這麼做。可以在這些框架的dev模式提供很多有用的提示資訊。

打包檔案分割

為何要進行打包檔案分割?

對於一個單頁應用專案來說,有分為業務程式碼和第三方程式碼,業務程式碼會頻繁改動,而第三方程式碼一般來講變動的次數較少,如果每次修改業務程式碼都需要使用者將整個js檔案都重新下載一遍,對於載入效能來講是不可取的,所以一般而言我們會將程式碼分為業務程式碼和第三方程式碼分別進行打包,雖然多了一個請求的檔案,增加了一些網路開銷,但是相比於瀏覽器能將檔案進行快取而言,這些開銷是微不足道的。

我們在entry中定義了app入口,相應的業務邏輯都封裝在這個入口檔案裡,如果我們想要第三方程式碼獨立出來,就要再增加一個入口,我們習慣使用vendor這個命名。

// app.js

require('vue');
require('vuex');複製程式碼
// webpack.config.js


entry: {
    app: 'app/app.js',
    vendor: ['vue', 'vuex'],
  },複製程式碼

vendor入口的傳參是以一個陣列的形式傳遞的,這是一種非常方便的注入多個依賴的方式,並且能把多個依賴一起打包到一個chunk中。而且不用手動的建立真實存在的入口檔案。

這相當於:

// vendor.js

require('vue');
require('vuex');

// app.js

require('vue');
require('vuex');複製程式碼
// webpack.config.js


entry: {
    app: 'app/app.js',
    vendor: 'app/vendor.js',
  },複製程式碼

但是這樣做只是宣告瞭一個vendor入口而已,對於app這個入口來說,打包完成的檔案還是會有vue和vuex依賴,而新增的入口vendor打包完成的檔案也有了vue和vuex兩個依賴。模組依賴關係如下圖所示。

「前端」看懂前端腳手架你需要這篇webpack

這裡的A可以代表vue依賴,最後生成的打包檔案是兩個平行關係的檔案,且都包含vue的依賴。

此時需要引入CommonsChunkPlugin外掛

This is a pretty complex plugin. It fundamentally allows us to extract all the common modules from different bundles and add them to the common bundle. If a common bundle does not exist, then it creates a new one.

這是個相當複雜的外掛,他的基礎功能是允許我們從不同的打包檔案中抽離出相同的模組,然後將這些模組加到公共打包檔案中。如果公共打包檔案不存在,則新增一個。同時這個外掛也會將執行時(runtime)轉移到公共chunk打包檔案中。

「前端」看懂前端腳手架你需要這篇webpack

plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    names: ['vendor', 'manifest']
  })
]複製程式碼

這裡的name可以選擇已經存在的塊,這裡就選擇了vendor塊,因為我們本來就是將vendor塊當做管理第三方程式碼的入口的。

而names傳入一個陣列,陣列裡包含兩個trunk name,表示CommonsChunkPlugin外掛會執行兩次這個方法,第一次將公共的第三方程式碼抽離移到vendor的塊中,這個過程之前也講過會將執行時runtime也轉移到vendor塊中,第二次執行則是將執行時runtime抽離出來轉移到manifest塊中。這步操作解決了快取問題。

這樣處理,最後會生成3個打包檔案chunk,app.js是業務程式碼,vendor則是公共的第三方程式碼,manifest.js則是執行時。

chunk type 塊的型別大揭祕

webpack1.0官網介紹中的chunk型別讀起來及其拗口chunk type, 所以我這裡解讀一下。

chunk是webpack中最基本的概念之一,且chunk常常會和entry弄混淆。在「打包檔案分割部分」我們定義了兩個入口entry point -- app和vendor,而通過一些配置,webpack會生成最後的一些打包檔案,在這個例子中最後生成的檔案有app.js 、 vendor.js 、 manifest.js。這些檔案便被稱為塊chunk

entry & chunk 可以簡單的理解為一個入口、一個出口

在官方1.0文件中webpack的chunk型別分為三種:

  1. entry chunk 入口塊
  2. normal chunk 普通塊
  3. initial chunk 初始塊

entry chunk 入口塊

entry chunk 入口塊不能由字面意思理解為由入口檔案編譯得到的檔案,由官網介紹

An entry chunk contains the runtime plus a bunch of modules

可以理解為包含runtime執行時的塊可以稱為entry chunk,一旦原本存在執行時(runtime)的entry chunk失去了執行時,這個塊就會轉而變成initial chunk

normal chunk 普通塊

A normal chunk contains no runtime. It only contains a bunch of modules.

普通塊不包含執行時runtime,只包含一系列模組。但是在應用執行時,普通塊可以動態的進行載入。通常會以jsonp的包裝方式進行載入。而code splitting主要使用的就是普通塊。

initial chunk 初始塊

An initial chunk is a normal chunk.

官方對initial chunk的定義非常簡單,初始塊就是普通塊,跟普通塊相同的是同樣不包含執行時runtime,不同的是初始塊是計算在初始載入過程時間內的。在介紹入口塊entry chunk的時候也介紹過,一旦入口塊失去了執行時,就會變成初始塊。這個轉變經常由CommonsChunkPlugin外掛實現。

例子解釋

還是拿「打包檔案分割」的程式碼做例子,

// app.js

require('vue');
require('vuex');複製程式碼
// webpack.config.js


entry: {
    app: 'app/app.js',
    vendor: ['vue', 'vuex'],
  },複製程式碼

沒有使用CommonsChunkPlugin外掛之前,兩個entry分別被打包成兩個chunk,而這兩個chunk每個都包含了執行時,此時被稱為entry chunk入口塊。

而一旦使用了CommonsChunkPlugin外掛,執行時runtime最終被轉移到了manifest.js檔案,此時最終打包生成的三個chunkapp.js 、 vendor.js 、 manifest.js,app.js、vendor.js失去了runtime就由入口塊變成初始塊。

code splitting

前文有講到將依賴分割開來有助於瀏覽器快取,提高使用者載入速度,但是當業務複雜度增加,程式碼量大始終是一個問題。這時候就需要normal chunk普通塊的動態載入能力了。

It allows you to split your code into various bundles which you can then load on demand — like when a user navigates to a matching route, or on an event from the user.
code splitting 允許我們將程式碼分割到可以按需載入的不同的打包檔案中,當使用者導航到對應的路由上時,或者是使用者觸發一個事件時,非同步載入相應的程式碼。

我們需要在業務邏輯中手動新增一些分割點,標明此處事件邏輯之後進行程式碼塊的非同步載入。


// test
window.addEventListener('click', function () {
  require.ensure(['vue', 'vuex'], function (require) {

  })  
})複製程式碼

這段程式碼表明當使用者點選時,非同步請求一個js檔案,這個檔案中包含該有vue vuex的依賴。

「前端」看懂前端腳手架你需要這篇webpack

打包後會根據手動分割點的資訊生成一個打包檔案,就是圖中第一行0開頭的檔案。這個檔案也就是非同步載入的檔案。

下面是之前的一個vue專案,採用code splitting將幾個路由抽離出來非同步載入之後,檔案由212kb減少到了137kb,同樣樣式檔案也由58kb減少到了7kb。對於首屏渲染來說,效能是會增加不少的。

「前端」看懂前端腳手架你需要這篇webpack

「前端」看懂前端腳手架你需要這篇webpack

參考:

相關文章