大前端進階-模塊化

概述

模塊化是一種解決問題的方案,一個模塊就是實現某種特定功能的文件,能夠幫助開發者拆分和組織代碼。css

js模塊化

JavaScript語言在設計之初只是爲了完成簡單的功能,所以沒有模塊化的設計。可是隨着前端應用的愈來愈龐大,模塊化成爲了js語言必須解決的問題。html

模塊化發展

js的模塊化發展大體能夠劃分爲四個階段:前端

  • 文件劃分

按照js文件劃分模塊,一個文件能夠認爲是一個模塊,而後將文件經過script標籤的方式引入。
編寫模塊:foo.jsvue

var foo = 'foo'
function sayHello() {
    console.log(foo)
}

使用模塊:node

<html>
<header></header>
<body>
    <!--先引用-->
    <script src="./foo.js"></script>
    <script>
        // 經過全局對象調用
        window.sayHello()
    </script>
</body>
</html>

文件劃分方式沒法管理模塊的依賴關係(不是強制定義模塊依賴),並且模塊內全部變量都掛載在全局對象上,容易污染全局做用域,命名衝突。jquery

  • 命名空間

將文件內全部的變量都添加到一個命名空間下。
編寫模塊:webpack

var FooModule = {
    foo: 'foo',
    sayHello() {
        console.log(FooModule.foo)
    }
}

使用模塊:git

<script>
    // 經過命名空間調用
    FooModule.sayHello()
</script>

使用命名空間的好處是能夠儘可能避免命名衝突,可是因爲命名空間掛載在全局對象下,依然可以在外部修改模塊的變量(沒有實現模塊私有化)。web

  • 當即執行函數

利用函數做用域,將模塊路徑包裹在一個當即執行函數中,能夠指定須要暴露給外部的變量。
編寫模塊:shell

;(function (w) {
    var foo = 'foo'
    w.sayHello = function () {
        console.log(foo)
    }
})(window)

使用模塊:

<script>
    // 經過命名空間調用
    window.sayHello()
</script>

自執行函數利用函數做用域實現了變量私有化。

  • 模塊化規範

ES2015提出了標準模塊化規範,即ES Modules。它包含一個模塊化標準和一個模塊加載器。
編寫模塊

// moduleA.js
export const foo = 'foo'

// moduleB.js
// 會自動從服務器下載moduleA.js文件
import { foo } from './moduleA.js'
console.log(foo)

使用模塊

<html>
<header></header>
<body>
    <!--引入moduleB.js-->
    <script type="module" src="./moduleB.js"></script>
</body>
</html>

注意事項:

  1. 引入模塊js時,必須添加type=module
  2. 因爲模塊會自動下載依賴文件,所以html文件必須掛載到服務器下,直接文件瀏覽會報錯。

模塊化規範

目前,JavaScript語言大體上有三種模塊化規範:CommonJs,AMD,ES Modules

CommonJs

CommonJs是Nodejs中使用的模塊化規範,它規定一個文件就是一個模塊,每一個模塊都有單獨的做用域,模塊中經過require引入模塊,經過module.exports導出模塊。

// moduleA.js
module.exports = {
    foo: 'foo'
}

// moduleB.js
const { foo } = require('./moduleA.js')
console.log(foo)

能夠在命令行中經過node moduleB.js運行。

AMD

AMD是瀏覽器端規定異步模塊定義的規範,一般配合requirejs使用。

//經過數組引入依賴 ,回調函數經過形參傳入依賴
define(['ModuleA', 'ModuleB'], function (ModuleA, ModuleB) {
    function foo() {
        // 使用依賴
        ModuleA.test();
    }
    // 導出模塊內容
    return { foo: foo }
})

ES Modules

ES Modules是ECMAScript提出的標準模塊規範,主要應用在瀏覽器端,目前並非全部瀏覽器均支持該特性。

ES Modules

基本特性

  • script type=module

在html中能夠經過script標籤引用,須要使用type=module告訴瀏覽器加載的js文件是一個模塊,瀏覽器會自動下載模塊中的依賴模塊。

  • 自動採用嚴格模式

若是某個js文件經過模塊的方式被瀏覽器引入,那麼該js文件會自動變成嚴格模式,也就是在js文件中能夠省略use strict

  • 運行在單獨的私有做用域中

運行在單獨的私有做用域中保證了命名不會衝突。
module.js中:
var foo = 'foo'
index.html中:

<html>
<header></header>
<body>
    <script type="module" src="./module.js"></script>
    <script>
        // 即便模塊中的foo變量使用的是var聲明的,此時也不能在全局做用域中找到foo變量
        console.log(foo)
    </script>
</body>
</html>
  • 經過CORS方式請求外部js文件

若是script標籤的src屬性值是一個url地址,那麼這個地址必須容許CORS跨域訪問。
<script type="module" src="https://dss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/static/protocol/https/jquery/jquery-1.10.2.min_65682a2.js"></script>
上例中,因爲dss1.bdstatic.com不容許跨域訪問,所以會報錯。

  • 自動延遲執行

經過模塊方式引入的js代碼會被瀏覽器延遲執行。
module.js中:
console.log('module')
index.html中:

<html>
<header></header>
<body>
    <script type="module" src="./module.js"></script>
    <script>
       console.log('out module')
    </script>
</body>
</html>

此時會先打印out module,再打印模塊內部的module。
效果等同於在script標籤上加上defer屬性:

<html>
<header></header>
<body>
    <script defer src="./module.js"></script>
    <script>
       console.log('out module')
    </script>
</body>
</html>

導入導出

  • export {...} 是一種語法,不是對象字面量。
const foo = 'foo'
// 此處並非對象字面量
export {
    foo
}
// 若是是對象字面量,那麼應該支持以下寫法
// 實際上,這樣寫會報錯
export {
    foo: 'foo'
}
  • import導入以後不能再改變變量
import { foo } from './moduleA.js'
// 不容許改變引用的變量
foo = '123'
  • import能夠導入相對路徑,絕對路徑和url

相對路徑
import { foo } from './moduleA.js'
絕對路徑
import { foo } from '/moduleA.js'
url:

import { foo } from 'http://localhost:8080/moduleA.js'
  • import後面直接根文件路徑,此時是隻導入,不引用。

若是某個模版文件module.js中沒有經過export導出成員,那麼能夠經過import '' 的方式導入模塊。

import './moduleA.js'
  • import動態導入

若是模塊中須要在運行的時候才知道導入模塊地址或者在某個邏輯下才導入某個模塊,那麼import from 的方式就會報錯。
錯誤導入:

// 地址不明確(開發階段)
const moduleA = './moduleA.js'
import { foo } from moduleA
// 在某些邏輯中導入成員
if(true) {
    import { foo } from './moduleA.js'
}

這種狀況下,可使用Modules 提供的import函數,該函數返回一個promise對象,由於是個函數,因此能夠在任何地方使用。

const moduleA = './moduleA.js'
import(moduleA).then(module => {
    // module中包含模塊全部的導出成員
    console.log(module.foo)
})
  • import同時導入默認成員和具名成員

在某個模塊中,若是須要同時導入默認成員和具名成員,能夠以以下方式導入:

import { foo, default as sayHi } from './moduleA.js'
// 或者
import sayHi, { foo } from './moduleA.js'
  • 直接導出導入成員

在某些模塊文件中,可能須要從別的模塊導入某個成員,而後在這個模塊直接導出這個成員。
正常寫法:

import { foo } from './moduleA.js'
export {
    foo
}

簡略寫法:

export { foo } from './moduleA.js'

運行環境兼容

瀏覽器

目前,並非全部瀏覽器都支持ES Modules特性,如IE。利用nomodule能夠實現優雅降級。

<html>
<header></header>
<body>
    <script type="module">
        import module from './module.js'
    </script>
    <script nomodule>
        alert('你的瀏覽器版本不支持ES Modules')
    </script>
</body>
</html>

在支持modules的瀏覽器中,會運行type='module'的腳本,在不支持的瀏覽器中,會忽略模塊文件,並運行nomodule對應的script腳本。

nodejs

nodejs在8.0版本開始支持ES Modules。想要在nodejs中使用,須要知足兩個條件:

  • 文件擴展名爲.mjs
  • 運行時須要加--experimental-modules參數
// moduleA.mjs
export const foo = 'foo'
export default function(){
    console.log(foo)
}
// moduleB.mjs
import { foo } from './moduleA.mjs'
console.log(foo)

此時經過命令行運行node .moduleB.mjs --experimental-modules,能夠正常工做。

commonjs交互

在mjs的文件中,能夠導入commonjs定義的模塊,始終導入一個默認對象。

// moduleA.js
module.exports = {
    foo: 'foo'
}
// moduleB.mjs
import * as moduleA from './moduleA.js'
console.log(moduleA.foo)

反過來,不能在commonjs定義的模塊中導入ESModules定義的對象。

在最新的nodejs中能夠在package.json中添加type:'module'屬性,此時,模塊文件的擴展名就不須要再使用mjs,可是相應的,使用commonjs定義的文件擴展名須要爲cjs。

區別

在commonjs定義的模塊文件中,可使用requie,module, exports, __filename, __dirname全局對象,可是ESModules模塊文件中沒有這些全局對象,可使用import.meta屬性中的某些屬性獲取相應的值。

// 可使用路徑解析獲取filename和dirname
console.log(import.meta.url)

模塊化打包

在瀏覽器環境中直接使用ESM(ES Modules)特性,會出現以下問題:

  • 並非全部瀏覽器都支持ESM特性。
  • 模塊化文件過多會致使網絡請求頻繁。
  • ESM只支持js文件模塊化,css、圖片、字體等文件不支持模塊化。

爲了解決上述問題,就出現了模塊化打包工具。此類工具會讓開發者在開發階段使用模塊組織資源、代碼等,在上線前,經過打包,從新組織模塊化的文件,以解決上述問題。

webpack

目前,最經常使用的模塊化打包工具就是webpack,經過webpack能夠快速實現模塊化打包。

安裝依賴: npm install webpack webpack-cli --save-dev
執行打包: npm run webpack
webpack插件會自動認爲當前目錄下的src/index.js爲打包入口文件,查找全部依賴並打包到dist/main.js中。
固然,webpack也支持配置文件,能夠在項目根目錄下添加webpack.config.js文件:

const path = require('path')
module.exports = {
    // 指定打包入口文件
    entry: './src/index.js',
    output: {
        // 打包輸出文件名
        filename: 'bundle.js',
        // 打包輸入文件夾,必須使用絕對路徑
        path: path.join(__dirname, 'dist')
    }
}

利用配置文件能夠修改webpack的默認配置。

工做模式

webpack的工做模式分爲三種:node, development,production。能夠經過設置工做模式,以應對不一樣的打包需求。webpack默認使用production模式打包,會自動優化打包結果。

在配置文件中設置模式:

const path = require('path')
module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    },
    mode: 'none' // 'development production'
}

development:打包後的代碼和開發代碼同樣,可讀性強,不會自動優化。
none: 刪除webpack打包過程當中生成的註釋代碼,其他和development相同。
production:打包後的代碼會自動優化。

loader

在webpack中萬物皆可模塊,只是webpack內置瞭如何處理js代碼,其餘資源如css,圖片等須要使用相應的loader進行轉換。

因爲webpack默認應用是由js驅動的,所以想要打包其餘資源文件,須要在js代碼中創建與其餘資源文件的聯繫,即導入。

import 'logo.png'
import 'common.css'

css

能夠利用css-loader和style-loader配置處理導入的css文件。

原理是css-loader將css代碼轉換爲js模塊(將css中的內容放到一個數組中並導出)。style-loader獲取轉換後的字符串並轉換爲style節點添加到html文件的header節點中。

安裝依賴:npm install --save-dev css-loader style-loader。
添加配置:

module.exports = {
  // ...
  module: {
    rules: [
      {
        // 告訴webpack,以css結尾的文件須要經過這裏配置的loader進行轉換。
        test: /.css$/,
        // 轉換用的loader,執行順序自後向前
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  }
}

圖片

圖片也是一種資源文件,須要loader進行處理。能夠利用file-loader處理圖片資源,原理是將圖片單獨導出爲一個文件,而後在js模塊中導出轉換後的圖片路徑。
加載依賴:npm install --save-dev file-loader。
添加配置:

const path = require('path')
module.exports = {
  // ...
  module: {
    rules: {
      {
        test: /.png$/,
        use: 'file-loader'
      },
      //...
    ]
  }
}

固然,也能夠利用Data URLs協議,該協議容許在文檔中嵌入小文件。
Data URLs 由四個部分組成:

  • 前綴(data:)
  • 指示數據類型的MIME類型
  • 若是非文本則爲可選的base64標記
  • 數據自己
data: [<mediatype>][;base64], <data>

若是使用該協議,那麼能夠利用url-loader,該loader能夠將圖片資源轉換爲url。針對大文件,能夠設置limit屬性,當超過limit限制的大小後,url-loader將圖片做爲單獨的文件打包。
添加依賴:npm install --save-dev url-loader
添加配置:

const path = require('path')

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            // 添加文件大小限制
            limit: 10 * 1024 // 10 KB
          }
        }
      },
      // ...
    ]
  }
}

觸發時機

既然全部資源均可以經過loader進行模塊化處理,那麼在什麼狀況下,webpack會將資源識別爲一個模塊呢?
以下狀況會被識別:

  • ES Modules import導入
  • commonjs require導入
  • amd模式下的define和require
  • html節點中的src屬性
  • css文件中的import和url

若是想要識別html中的src屬性,須要配合html-loader使用:

{
    test: /.html$/,
    use: {
      loader: 'html-loader',
      options: {
        // 指定哪些attr會被識別爲模塊資源
        attrs: ['img:src', 'a:href']
      }
    }
}

ES特性轉換

若是在js代碼中使用了ES的新特性,webpack自己並不會轉換這些特性,須要使用babel-loader。
加載依賴:npm install --save-dev babel-loader @babel/babel-core @babel/preset-env。
添加配置:

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      // ...
    ]
  }
}

自定義loader

webpack提供了大量的用於轉換的loader,loader的本質是完成資源文件輸入和輸出之間的轉換。在特定狀況下,咱們須要本身定義符合要求的loader,字定義loader文件默認導出一個函數,函數的參數是讀取到的文件內容或者是另外一個loader處理後的內容。
下面是一個轉換md文件的自定義loader:

const marked = require('marked')
module.exports = source => {
  // 利用marked將md文檔轉爲html可識別的字符串
  const html = marked(source)
  // 須要返回js能識別的模塊字符串
  // return `module.exports = "${html}"`
  // return `export default ${JSON.stringify(html)}`
  // 或者返回 html 字符串交給下一個 loader 處理
  return html
}

添加配置:

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.md$/,
        use: [
           // html-loader將markdown-loader返回的html字符串轉換爲一個模塊
          'html-loader',
          './markdown-loader'
        ]
      }
    ]
  }
}

Plugin

loader實現了資源文件的轉換,相比於loader,plugin能夠實現其餘自動化工做,如清空輸出文件夾、自動在html中注入打包後的js文件等。

plugin擁有更寬的能力範圍,經過在webpack打包生命週期中掛在函數實現webpack擴展。

清空輸出文件夾

添加clean-webpack-plugin插件,能夠在每次打包以前自動清空上次的打包結果。
添加依賴:npm install --save-dev clean-webpack-plugin。
添加配置:

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
  // ...
  module: {
    rules: [
        // ...
    ]
  },
  plugins: [
    new CleanWebpackPlugin()
  ]
}

自動生成html

經過html-webpack-plugin插件可自動在打包輸出文件夾下生成html文件,生成的html文件中可實現以下自動化功能:

  • 自動添加打包後的js文件。
  • 添加字定義meta屬性。
  • 可利用模板編譯,自動加入變量。

添加依賴:npm install --save-dev html-webpack-plugin。
添加配置:

const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  // ...
  module: {
    rules: [
      //...
    ]
  },
  plugins: [
    // ...
    // 用於生成 index.html
    new HtmlWebpackPlugin({
      // 模板編譯,替換模板文件中的`<%= htmlWebpackPlugin.options.title %>`
      title: 'Webpack Plugin Sample',
      // 添加meta頭
      // 至關於在html header中添加`<meta name="viewport" content="width=device-width">`
      meta: {
        viewport: 'width=device-width'
      },
      // 指定模板文件
      template: './src/index.html'
    }),
    // 能夠添加多個HtmlWebpackPlugin,用於生成多個html文件
    // 用於生成 about.html
    new HtmlWebpackPlugin({
      filename: 'about.html'
    })
  ]
}

複製靜態資源

在public文件夾下的諸如favicon.ico文件是不須要打包的,能夠直接複製到輸出目錄下。
添加依賴:npm install copy-webpack-plugin --save-dev。
添加配置:

const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  // ...
  module: {
    rules: [
    // ...
    ]
  },
  plugins: [
    // 指定直接複製的文件路徑
    new CopyWebpackPlugin([
      // 'public/**'
      'public'
    ])
  ]
}

自定義plugin

雖然webpack提供了大量的plugin插件用於完成平常開發工做,可是某些狀況下,須要咱們添加字定義plugin。

字定義plugin是一個函數或者是一個包含apply方法的類。

下面的例子是自動刪除打包後js文件中每一行開頭的/******/

class MyPlugin {
  apply (compiler) {
    // 註冊生命週期函數
    // 此例中的emit是在完成打包後,將要輸出到輸出目錄的節點執行。
    // compilation 是這次打包的上下文,包含全部的打包的資源文件。
    compiler.hooks.emit.tap('MyPlugin', compilation => {
      for (const name in compilation.assets) {
        // 經過name屬性能夠獲取文件名稱
        if (name.endsWith('.js')) {
          // 經過source方法獲取相應內容
          const contents = compilation.assets[name].source()
          const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
          // 替換原有的內容,須要實現source方法和size方法。
          compilation.assets[name] = {
            source: () => withoutComments,
            size: () => withoutComments.length
          }
        }
      }
    })
  }
}

添加配置:

module.exports = {
  // ...
  module: {
    rules: [
      // ...
    ]
  },
  plugins: [
    new MyPlugin()
  ]
}

加強開發體驗

經過上述的loader和plugin能夠完成項目的打包工做,可是咱們須要在開發時可以加強開發體驗,如自動編譯,自動刷新,方便調試等。

自動編譯

webpack內置了自動編譯功能,其能夠自動監聽文件變化,自動打包運行。能夠在命令執行的時候添加--watch實現。

自動刷新瀏覽器

當webpack可以自動打包後,咱們但願在開發環境下可以自動刷新瀏覽器。webpack提供了webpack-dev-server插件,能夠實現此功能。

安裝依賴:npm install webpack-dev-server --save-dev。
在命令行中運行: npm run webpack-dev-server --open 便可實現自動打開瀏覽器,自動刷新瀏覽器。

瀏覽器中打開的資源指向的webpack輸出的目錄,可是在開發階段,public中的靜態文件並無被打包進去,此時會形成資源丟失,能夠利用配置文件中的devServer屬性完成此功能配置。

devServer: {
    // 指定其餘靜態資源文件地址
    contentBase: './public'
    // 還能夠利用proxy實現開發階段的服務端代理。
}

自動刷新瀏覽器雖然解決了部分開發優化問題,可是自動刷新會致使頁面狀態所有丟失(在input中輸入測試文字,刷新後測試文字沒有了,須要再次手動輸入),這樣還不是很友好。

爲了解決刷新致使的頁面狀態丟失問題,webpack還提供了HRM熱更新,熱更新能夠保證模塊發生變化後,頁面只會替換相應變化的部分,不會致使狀態丟失。

在webpack中啓動熱更新,能夠添加--hot參數。

測試發現,HRM能夠熱更新css和圖片等資源文件,可是針對js文件,沒法作到自動替換,仍是須要刷新瀏覽器,這種狀況下,須要咱們手動添加熱更新處理代碼。

例如在某個模塊中,當依賴模塊發生變化(頁面中的某個元素髮生變化),能夠經過以下代碼監控代碼變化,並手動完成熱更新功能:

let hotEditor = editor
module.hot.accept('./editor.js', () => {
    // 獲取元素的狀態:即獲取用戶已經輸入的內容
    const value = hotEditor.innerHTML
    // 移除舊有的頁面元素
    document.body.removeChild(hotEditor)
    // 建立一個變化後的元素
    hotEditor = createEditor()
    // 將移除以前存儲的狀態賦值給新的元素
    hotEditor.innerHTML = value 
    // 將新的元素添加到頁面上
    document.body.appendChild(hotEditor)
})

其餘和熱更新相關的:

--hotOnly: 使用這個替代--hot參數能夠屏蔽熱更新代碼中的錯誤,熱更新代碼只是輔助開發用的,若是其中出現錯誤並不須要被控制檯輸出。

module.hot: 在模塊中能夠經過判斷module.hot來獲取當前項目是否開啓了熱更新,若是沒有開啓,那麼代碼打包過程當中會自動刪除熱更新邏輯,不影響生產環境。

source-map 調試

webpack打包後的代碼不利於開發階段調試,所以須要source-map來定位錯誤,解決源代碼和運行代碼不一致致使的問題。

感覺一下source-map的魅力:
在瀏覽器控制檯輸入:eval('console.log("foo")')
image.png
紅色框中顯示的是代碼在哪執行,這個顯示很不友好。
再次輸入:eval('console.log(123) //# sourceURL=foo.js')
image.png
經過添加sourceURL就能夠告訴控制檯這個代碼是在哪一個文件中執行的。

webpack中經過簡單的配置便可實現source-map:
devtool: 'eval',
其中devtool的值是source-map的類型,webpack支持12中source-map類型:
image.png
一般狀況下,打包速度快的,調試效果通常都很差,調試效果好的,通常打包速度比較慢,在項目中具體使用哪一種類型,須要本身去斟酌。

  • eval

模塊中的代碼經過eval去執行,在eval的最後添加sourceURL,並無添加source-map,只能定位哪一個文件中出現錯誤。

  • eval-source-map:

在eval的基礎上添加了source-map,能夠定位錯誤的具體行列信息。

  • cheap-eval-source-map

閹割版的eval-source-map,只能定位到行,沒法定位列信息。

  • cheap-module-eval-source-map

在cheap-eval-source-map的基礎上,能夠保證定位的行信息和源文件的行相對應。

  • inline-source-map

普通的source-map中,map文件是物理文件,而inline-source-map模式下,map文件是以Data URLs的形式打包到文件的末尾。

  • hidden-source-map

生成了map文件,可是打包後的末尾沒有添加該map文件,保證了源代碼不會暴露,同時在調試時,能夠手動將map文件添加到文件末尾進行調試。

  • nosources-source-map

能夠定位錯誤的行列信息,可是沒法在開發工具中看到源代碼。

生產環境優化

生產環境和開發環境的關注點是不同的,開發環境注重開發效率,生產環境注重運行效率。

不一樣環境,不一樣配置文件

webpack鼓勵爲不一樣的環境設置不一樣的配置文件,能夠經過如下兩種方式實現。

  • 在一個配置文件中,經過判斷不一樣環境導出不一樣的配置。
  • 添加多個配置文件,指定webpack運行時的配置文件。

同一個配置文件中,導出一個函數,此函數返回一個配置對象:

const webpack = require('webpack')
module.exports = (env, argv) => {
  const config = {
    // ...
  }
  // 判斷是那種環境
  if (env === 'production') {
    config.mode = 'production'
    config.devtool = false
    config.plugins = [
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin(['public'])
    ]
  }
  return config
}

多個配置文件:
添加公用配置文件: webpack.common.js

module.exports = {
  // ....
}

添加生產環境配置文件

const merge = require('webpack-merge')
const common = require('./webpack.common')
// 使用webpack-merge實現配置文件的合併
module.exports = merge(common, {
  mode: 'production',
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin(['public'])
  ]
})

使用時,經過--config參數指定配置文件。

DefinePlugin

能夠利用webpack內置的DefinePlugin爲代碼注入全局成員,打包時,webpack會自動利用指定的值替換代碼中出現的全局成員。
定義成員:

const webpack = require('webpack')
module.exports = {
  // ...
  plugins: [
    new webpack.DefinePlugin({
      // 值要求的是一個代碼片斷
      API_BASE_URL: JSON.stringify('https://api.example.com')
    })
  ]
}

使用成員:

console.log(API_BASE_URL)

打包後:

console.log('https://api.example.com')

合併

模塊打包會致使最終輸出的文件夾中模塊文件過多,能夠利用模塊合併儘量的將模塊合併到一個函數中,減小模塊數量。
啓用合併:

module.exports = {
  // ...
  optimization: {
    // 儘量合併每個模塊到一個函數中
    concatenateModules: true,
  }
}

Tree-shaking

Tree-shaking指的是去除代碼中未引用的代碼,也就是無用代碼。經過去除無用代碼,能夠減小代碼文件體積,優化加載速度,webpack默認在production模式下啓動Tree-shaking。

webpack中沒有明確的某個配置用於啓動Tree-shaking,它是一系列配置一塊兒完成的功能。

module.exports = {
  // ...
  optimization: {
    // 打包後的模塊只導出被使用的成員
    usedExports: true,
    // 壓縮輸出結果,在壓縮的過程當中後自動刪除未被導出的代碼
    minimize: true
  }
}

有的時候,人們會認爲Tree-shaking和babel轉換相沖突,也就是用了babel轉換會致使Tree-shaking失敗。

其實,兩者是不衝突的,Tree-shaking依賴的是ESM,經過對import的分析達到去除無用代碼的效果。babel轉換的時候會默認將ESM編寫的模塊轉換爲Commonjs規範的模塊,所以會致使Tree-shaking失敗。

經過爲babel-loader的presets添加配置可讓babel轉換的時候不將ESM轉換爲Commonjs:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              // 若是 Babel 加載模塊時已經轉換了 ESM,則會致使 Tree Shaking 失效
              // ['@babel/preset-env', { modules: 'commonjs' }]
              // ['@babel/preset-env', { modules: false }]
              // 也可使用默認配置,也就是 auto,這樣 babel-loader 會自動關閉 ESM 轉換
              ['@babel/preset-env', { modules: 'auto' }]
            ]
          }
        }
      }
    ]
  }
}

反作用

反作用指的是模塊除了導出成員以外,還進行了其餘操做。如在模塊中引入了css文件,這個引入過程並無使用內部成員,所以在Tree-shaking的時候就會被自動去掉。

爲了不由於Tree-shaking去掉致使項目運行失敗,須要進行反作用代碼標記。

添加啓用反作用配置:

module.exports = {
  // ...
  optimization: {
    sideEffects: true,
  }
}

在package.json中指定反作用文件地址:

"sideEffects": [
    "./src/extend.js",
    "*.css"
]

指定位置的文件不會被Tree-shaking看成無用代碼刪除。

代碼分割

若是將全部的資源都打包到一個文件中,那麼這個文件會過大,致使加載時間過長,影響項目體驗,此時,須要根據狀況,對項目打包進行代碼分割,代碼分割一般伴隨多入口打包。

const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  // 提供多個代碼打包入口
  entry: {
    // 將index.js入口的全部文件打包到index的chunk中。
    index: './src/index.js',
    album: './src/album.js'
  },
  plugins: [
    // 針對多個入口生成多個html文件
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      // 指定html文件依賴的chunk
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/album.html',
      filename: 'album.html',
      chunks: ['album']
    })
  ]
}

提取公共模塊

在代碼分割時,若是多個入口文件依賴一些公用代碼,這些公用代碼被打包到每一個文件中,會增長文件體積,此時須要提取公共模塊到一個單獨文件中。
添加配置:

module.exports = {
  optimization: {
    splitChunks: {
      // 自動提取全部公共模塊到單獨 bundle
      chunks: 'all'
    }
  }
}

按需加載

若是在項目啓動時,加載全部模塊,那麼會由於請求過多致使加載時間長,此時能夠利用動態導入模塊的方式實現按需加載,全部動態導入的模塊會自動分包。

import(
// webpackChunkName是一種webpack中的魔法註釋,經過魔法註釋,能夠指定動態導入的模塊打包後生成的文件名,同時,多個動態導入的模塊若是註釋的名字相同,那麼會被打包到一個文件中。
/* webpackChunkName: 'components' 
*/'./posts/posts').then(({ default: posts }) => {
      mainElement.appendChild(posts())
    })

MiniCssExtractPlugin

目前狀況下,css樣式都是包含在html的style標籤中,經過MiniCssExtractPlugin插件能夠將css提取到單個文件中,可是並非每一個項目中的css都是須要單獨提取的,若是提取後的文件中css樣式較少,那麼會致使項目請求過多。
添加配置:

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', 
          // 配合MiniCssExtractPlugin使用
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin()
  ]
}

OptimizeCssAssetsWebpackPlugin

默認狀況下,webpack只針對js文件進行壓縮,若是須要對css文件進行壓縮,那麼須要使用OptimizeCssAssetsWebpackPlugin插件。
添加配置:

const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
 // ....
  plugins: [
    new OptimizeCssAssetsWebpackPlugin()
  ]
}

上面的配置能夠實現css壓縮,可是webpack官方推薦將OptimizeCssAssetsWebpackPlugin配置在opitimization屬性中,這樣,在webpack打包的時候,若是啓用了項目優化,那麼就會進行css壓縮,反之則不會啓用,便於統一管理。

const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

module.exports = {
  optimization: {
    minimizer: [
      // 實現js文件壓縮
      new TerserWebpackPlugin(),
      // 實現css文件壓縮
      new OptimizeCssAssetsWebpackPlugin()
    ]
  }
}

文件名hash

瀏覽器中運行的前端項目避不開的就是緩存,利用緩存能夠加快項目的加載速度,可是有的時候緩存會影響項目更新,此時爲項目中的文件添加hash,因爲文件發生變化,打包後的hash值不一樣,也就是瀏覽器下載文件的地址就不一樣,避開了緩存的影響。

webpack中有三種hash模式:

  • hash

項目級別的hash,項目下全部的打包文件使用同一個hash值。

output: {
    // 8表示生成hash值的位數
    filename: '[name]-[hash:8].bundle.js'
}
  • chunkhash

chunk級別的hash,項目中,屬於同一個chunk的文件的hash值相同,如js文件中導入了css文件,那麼打包後,對應的js文件和css文件的hash值相同。

output: {
    filename: '[name]-[chunkhash:8].bundle.js'
}
  • contenthash

文件級別的hash,也就是每一個文件都有單獨的hash值。

output: {
    filename: '[name]-[contenthash:8].bundle.js'
}

rollup

rollup也是一種打包工具,相比webpack來講,rollup更加小巧,是專一於ESM各項特性的高效打包器。

在使用rollup以前,須要安裝依賴: npm install --save-dev rollup

快速使用

在項目目錄下建立三個測試文件:

  • a.js
import { log } from './b.js'
import foo from './c.js'

// 使用
log(foo)
  • b.js
export function log(msg) {
    console.log(msg)
}

export function error(msg) {
    console.error(msg)
}
  • c.js
export default 'foo'

在命令行中執行: npx rollup ./a.js --format iife --file dist/bundle.js就能夠完成快速打包。

--format 指定打包輸出格式,iife表示自執行函數。
--file 指定輸出文件目錄。

打包後的結果以下:

(function () {
    'use strict';

    function log(msg) {
        console.log(msg);
    }

    var foo = 'foo';

    // 使用
    log(foo);

}());

能夠看到,相比於webpack,rollup打包後代碼更加簡潔,並且其默認執行了Tree-shaking,去除了未使用的代碼。

配置文件

rollup也支持使用配置文件,只不過即便在項目下添加了rollup.config.js文件,在使用的時候依然要用--config參數指定配置文件路徑。

export default {
    input: 'a.js',
    output: {
        file: 'dist/bundle.js',
        format: 'iife'
    }
}

插件

插件是rollup的惟一擴展方式。

rollup-plugin-json

能夠利用此插件在打包的時候讀取項目目錄下的json文件。

安裝依賴: npm install --save-dev rollup-plugin-json

在配置文件中添加該插件:

import json from 'rollup-plugin-json'
export default {
    input: 'a.js',
    output: {
        file: 'dist/bundle.js',
        format: 'iife'
    },
    plugins: [
        json()
    ]
}

此時,咱們修改a.js文件,讓其讀取跟目錄下的package.json文件。

import { name, version } from './package.json'
console.log(name, version)

打包後:

(function () {
  'use strict';

  var name = "rollup-test";
  var version = "1.0.0";

  console.log(name, version);

}());

會發現,rollup將json中的相應數據賦值給變量,而後在代碼中就可使用該變量了。

rollup-plugin-node-resolve

rollup並不能像webpack那樣直接在項目中導入node_modules中的node模塊,須要使用rollup-plugin-node-resolve插件。

安裝依賴: npm install --save-dev rollup-plugin-node-resolve

import json from 'rollup-plugin-json'
import reslove from 'rollup-plugin-node-resolve'
export default {
    input: 'a.js',
    output: {
        file: 'dist/bundle.js',
        format: 'iife'
    },
    plugins: [
        json(),
        reslove()
    ]
}

添加完上述配置後,就能夠像webpack那樣經過import方式引入node模塊,須要注意的是此插件僅支持引入符合ESM規範的模塊。

rollup-plugin-commonjs

因爲rollup在設計時就是專門打包ESM規範的文件,所以其須要配合rollup=plugin-commonjs插件來導入符合commonjs規範的模塊。

安裝依賴: npm install --save-dev rollup=plugin-commonjs

添加配置:

import json from 'rollup-plugin-json'
import reslove from 'rollup-plugin-node-resolve'
import commonjs from 'rollup=plugin-commonjs'
export default {
    input: 'a.js',
    output: {
        file: 'dist/bundle.js',
        format: 'iife'
    },
    plugins: [
        json(),
        reslove(),
        commonjs()
    ]
}

其餘

代碼拆分

rollup也支持動態導入,動態導入的模塊會自動拆分爲一個單獨的文件。

多入口打包

將配置文件的input屬性改成一個數組,也可讓rollup支持多入口打包。

須要注意的是,若是使用動態導入或者多入口打包,由於最終打包結果會是多個文件,此時輸出的格式就不能再使用iife,可使用amd。

優缺點

rollup相比webpack有着其自然的優缺點。

優勢:

  1. 輸出結果更加扁平
  2. 自動移除未引用代碼
  3. 打包結果依然徹底可讀

缺點:

  1. 加載非ESM模塊比較複雜
  2. 模塊最終都被打包到一個函數中,沒法實現hrm
  3. 瀏覽器環境中,代碼拆分依賴amd庫(requirejs)

所以在開發一個應用程序的時候,能夠選用webpack,當開發一個框架或者類庫的時候,可使用rollup(vue中就有rollup的影子)。

Parcel

相比於webpack和rollup,Parcel是最近纔出現的打包工具,它的初衷是經過最少的配置完成前端打包,和它造成鮮明對比的是webpack的配置量比較大,須要開發人員掌握的東西比較多。不過隨着webpack的流行,人們已經熟悉了其配置,所以Parcel的位置有點尷尬。

不過其也有能夠借鑑的地方,如自動安裝模塊等功能。

規範化

隨着前端項目的體量愈來愈大,對開發人員的要求也愈來愈高。尤爲針對團隊協做的項目,規範化要求愈來愈高。

目前,項目的規範化不只體如今代碼上,也體如今文檔編寫和提交日誌等方方面面。

ESLint

eslint是目前最經常使用的代碼規範檢查插件,其不只能夠檢查代碼規範,同時也能夠檢查代碼中的語法錯誤。

eslint使用很是方便,經過npm install --save-dev eslint 安裝相應模塊,經過npx eslint --init安裝配置文件,而後就可使用。

git hooks

雖然項目開發時,能夠約定你們在提交代碼前須要lint一下代碼,可是這樣沒法避免未lint代碼的提交,此時就可使用git hooks。

git hooks指的是git鉤子,每個鉤子對應git的一個命令,命令執行的時候,會觸發相應的鉤子執行,經過將lint添加到git鉤子中,就能夠保證代碼入庫以前經歷過lint檢查。

鉤子體驗

每個git項目中都有一個.git文件夾,在其內部hooks文件夾下默認放置了全部的鉤子腳本(shell 腳本)。

image.png

找到其中的pre-commit開頭的文件,其內部的腳本會在git commit以前執行,手動去掉.sample後綴,再其內部添加一段簡單的腳本:

#!/bin/sh
echo "pre commit"

此時在命令行中執行commit命令,就會發現咱們添加的腳本被執行了。

image.png

Husky

並非全部人都能熟練的寫腳本命令,所以能夠經過Husky模塊,在項目中使用這些git hooks。

安裝依賴: npm install --save-dev husky。

在package.json中添加Husky屬性:

"Husky": {
   "hooks": {
      "pre-commit": "npm run lint"
   }
 }

上述配置的意思就是說,在pre commit鉤子執行的時候執行npm run lint腳本,這樣就更加方便項目使用。

lint-staged

雖然husky解決了編寫腳本的問題,可是husky內部只能爲每一個鉤子註冊一件事,若是咱們想在提交以前利用Prettier自動格式化文件,elint檢查文件,而後git add提交文件,那麼就可使用lint-staged。

安裝依賴:npm install --save-dev lint-staged

修改package.json中的配置:

"scripts": {
    "precommit": "lint-staged"
  },
  "Husky": {
    "hooks": {
      "pre-commit": "npm run precommit"
    }
  },
  "lint-staged": {
    "*.js": [
      "prettier --write",
      "eslint --fix",
      "git add"
    ]
  }

這樣配置的化,就能夠在commit提交以前,首先觸發pre-commit鉤子,該鉤子會執行npm run precommit,執行precommit的時候會找到lint-staged下匹配該文件的配置,而後依次執行prettier,eslint和git add,方便使用。

StyleLint

stylelint和eslint在用法上類似,只不過其是檢查css代碼。

Prettier

prettier是一種前端代碼格式化工具,能夠經過它一鍵格式化項目中的js,css,sass,vue,jsx,json等文件。

相關文章
相關標籤/搜索