前端模塊化開(kāi)發(fā)—webpack詳細(xì)介紹

一、模塊化開(kāi)發(fā)

common.js規(guī)范

  1. 一個(gè)文件就是一個(gè)模塊
  2. 每個(gè)模塊都有單獨(dú)的作用域
  3. 通過(guò)module.exports導(dǎo)出成員
  4. 通過(guò)require函數(shù)載入模塊

commonJS是以同步模式加載模塊

AMD(異步的模塊定義規(guī)范)

Require.js

ES Modules

基本特性
  1. 自動(dòng)采用嚴(yán)格模式
  2. 每個(gè)ESM模塊都是單獨(dú)的私有作用域
  3. ESM是通過(guò)CORS去請(qǐng)求外部JS模塊的
  4. ESM的script標(biāo)簽會(huì)延遲執(zhí)行腳本
導(dǎo)入和導(dǎo)出
 <!--加載模塊不需要提取成員-->
import {} from './modules'
<!--提取模塊的所有成員-->
import * as mod from './modules.js'
<!--動(dòng)態(tài)導(dǎo)入模塊-->
import ('./modules.js').then(modules=>{
    console.log(modules)
})
<!--同時(shí)導(dǎo)出默認(rèn)成員和具名成員-->
import title, { name, age } from './modules.js'
直接導(dǎo)出導(dǎo)入成員

1、將多個(gè)模塊統(tǒng)一在一個(gè)文件導(dǎo)出,在從統(tǒng)一入口進(jìn)行引用
2、使用polifill解決瀏覽器不兼容ESmodules的問(wèn)題(只適用于本地測(cè)試開(kāi)發(fā))

<!--在不支持ESModules的文件中使用該polifill文件-->
<script nomodule src="..."><script>
在node環(huán)境下使用ES Modules編寫(xiě)代碼

1、將文件擴(kuò)展名從js改為mjs

<!--index.mjs-->
import { foo, bar} from './modules.mjs'

2、使用node --experimental-module執(zhí)行mjs文件

node --experimental-modules index.mjs

注意事項(xiàng)
1、系統(tǒng)內(nèi)置成員可以通過(guò)ES module的提取成員方式導(dǎo)入,也可以默認(rèn)導(dǎo)入

import fs from 'fs' 
import { writeFileSync } from 'fs'

2、第三方模塊不支持直接提取成員,因?yàn)榈谌侥K都是默認(rèn)導(dǎo)出

<!--不支持-->
import { _cameCase } from 'lodash'
<!--僅支持-->
import _ from 'lodash'
console.log(_comCase('ES Module'))
ES modulees 與Common JS模塊交互
  1. 可以在ES Module中導(dǎo)入commonJS模塊
<!--common.js文件-->
<!--Comonjs模塊始終只會(huì)導(dǎo)出一個(gè)默認(rèn)成員-->
modules.exports = {
    foo: 1111
}
====>
exports.foo = 111;

<!--ES Module.mjs文件-->
 <!--導(dǎo)入默認(rèn)成員,不能直接提取成員,注意import不是解構(gòu)出對(duì)象-->
import mod from './commonjs.js'  
console.log(mod) // { foo :111}

<!--不支持以下寫(xiě)法-->
import { foo } from './commonjs.js'
  1. CommonJs中不能導(dǎo)入ES Module模塊

  2. CommonJs始終只會(huì)導(dǎo)出一個(gè)默認(rèn)成員

  3. 注意import不是解構(gòu)導(dǎo)出對(duì)象

  4. 在node的最新版本中,在package.json中添加type字段,就表示該工程默認(rèn)使用ES Module編寫(xiě)代碼,這意味著可以不用將js文件改為mjs,不過(guò)此時(shí)如果還需要使用commonJs,需要將CommonJS模塊文件改為cjs后綴名

<!--package.json-->
{
    type: "module"
}
<!--運(yùn)行-->
node --exprimental-modules index.js
node --exprimental-modules common.cjs

二、模塊化打包工具

模塊化打包工具的由來(lái)

  • 新特性代碼編譯
  • 模塊化JavaScript打包
  • 執(zhí)行不同類(lèi)型的資源文件

模塊化打包工具概要

打包工具解決的是前端整體的模塊化,并不是單指JavaScript模塊化

webpack

資源文件加載

樣式文件加載

const path = require('path');
module.exports = {
    mode:'none',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            }
        ]
    }
}

文件資源加載

const path = require('path');
module.exports = {
    mode:'none',
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            }
        ]
    }
}

常用加載器分類(lèi)

  • 編譯轉(zhuǎn)換類(lèi),轉(zhuǎn)換為JS代碼,如css-loader
  • 文件操作類(lèi),將資源文件拷貝到輸出目錄,將文件訪(fǎng)問(wèn)路徑向外導(dǎo)出,如:file-loader
  • 代碼檢查器,統(tǒng)一代碼風(fēng)格,提高代碼質(zhì)量,如:eslint-loader

webpack 處理ES2015

因?yàn)槟K打包需要,所以webpack可以處理import和export,除此之外,并不能轉(zhuǎn)換其他的ES6特性。如果想要處理ES6,需要安裝轉(zhuǎn)化ES6的編譯型loader,最常用的就是babel-loader,babel-loader依賴(lài)于babel的核心模塊,@babel/core和@babel/preset-env

{
   test: /.js$/,
   use: {
     loader: 'babel-loader',
     options: {
       presets: ['@babel/preset-env']
     }
   },
    exclude: /(node_modules)/, // 這個(gè)必須配置
 }

注意:Webpack只是打包工具,加載器可以用來(lái)編譯轉(zhuǎn)化代碼

加載資源的方式

  • 遵循ES Modules標(biāo)準(zhǔn)的import聲明

  • 遵循CommonJS標(biāo)準(zhǔn)的require函數(shù)。對(duì)于ES的默認(rèn)導(dǎo)出,要通過(guò)require('./XXX').default的形式獲取

  • 遵循AMD標(biāo)準(zhǔn)的define函數(shù)和require函數(shù)

  • Loader加載的非JavaScript也會(huì)觸發(fā)資源加載

    css-loader在處理css代碼時(shí),遇到url函數(shù),會(huì)將這個(gè)資源文件 交給url-loader處理

webpack的核心工作原理

核心工作原理:

  1. 根據(jù)配置找到打包入口文件
  2. 順著入口文件代碼里的 import 和 require之類(lèi)的語(yǔ)句
    解析推斷文件所依賴(lài)的資源模塊
  3. 分別去解析每個(gè)資源模塊對(duì)應(yīng)的依賴(lài),最后形成一顆依賴(lài)樹(shù)
  4. 遞歸依賴(lài)樹(shù),找到每個(gè)節(jié)點(diǎn)對(duì)應(yīng)的資源文件
  5. 根據(jù)配置文件 rules 屬性,找到資源模塊所對(duì)應(yīng)的加載器,交給對(duì)應(yīng)的加載器加載對(duì)應(yīng)的資源模塊
  6. 最后將加載以后的結(jié)果放入到bundle.js打包結(jié)果里
    實(shí)現(xiàn)整個(gè)項(xiàng)目的打包。

webpack Loader的工作原理

loader機(jī)制是webpack的核心特性之一。每個(gè) Webpack 的 Loader 都需要導(dǎo)出一個(gè)函數(shù),這個(gè)函數(shù)就是我們這個(gè) Loader 對(duì)資源的處理過(guò)程,它的輸入就是加載到的資源文件內(nèi)容,輸出就是我們加工后的結(jié)果。我們通過(guò) source 參數(shù)接收輸入,通過(guò)返回值輸出。

對(duì)于返回的輸出,有兩種思路:

  • 直接在這個(gè) Loader 的最后返回一段 JS 代碼字符串

  • 再找一個(gè)合適的加載器,在后面接著處理我們這里得到的結(jié)果

Webpack 加載資源文件的過(guò)程類(lèi)似于一個(gè)工作管道,你可以在這個(gè)過(guò)程中依次使用多個(gè) Loader,但是最終這個(gè)管道結(jié)束過(guò)后的結(jié)果必須是一段標(biāo)準(zhǔn)的 JS 代碼字符串。

// ./markdown-loader.js
const marked = require('marked')

module.exports = source => {
  const html = marked(source)
  // const code = `module.exports = ${JSON.stringify(html)}`
  const code = `export default ${JSON.stringify(html)}`
  return code 
}

插件機(jī)制

插件機(jī)制的是webpack的另一個(gè)核心特性,目的是為了增強(qiáng)webpack自動(dòng)化方面的能力。

常見(jiàn)的插件介紹

CleanWebpackPlugin、HtmlWebpackPlugin、CopyWebpackPlugin

插件使用總結(jié)

webpack開(kāi)發(fā)體驗(yàn)問(wèn)題

自動(dòng)進(jìn)行編譯:npx webpack --watch會(huì)監(jiān)視文件的變化自動(dòng)進(jìn)行打包

自動(dòng)打開(kāi)瀏覽器: npx webpack-dev-server --open

source map

Source Map解決了源代碼與運(yùn)行代碼不一致所產(chǎn)生的問(wèn)題.Webpack 支持sourceMap 12種不同的方式,每種方式的效率和效果各不相同。效果最好的速度最慢,速度最快的效果最差.下面是幾種常用方式介紹:

  • eval- 是否使用eval執(zhí)行代碼模塊
  • cheap- Source map是否包含行信息
  • module-是否能夠得到Loader處理之前的源代碼
  • inline- SourceMap 不是物理文件,而是以URL形式嵌入到代碼中
  • hidden- 看不到SourceMap文件,但確實(shí)是生成了該文件
  • nosources- 沒(méi)有源代碼,但是有行列信息。為了在生產(chǎn)模式下保護(hù)源代碼不被暴露

開(kāi)發(fā)模式推薦使用:eval-cheap-module-source-map,原因:

  • 代碼每行不會(huì)太長(zhǎng),沒(méi)有列也沒(méi)問(wèn)題
  • 代碼經(jīng)過(guò)Loader轉(zhuǎn)換后的差異較大
  • 首次打包速度慢無(wú)所謂,重新打包相對(duì)較快

生產(chǎn)模式推薦使用:none,原因:

  • Source Map會(huì)暴露源代碼
  • 調(diào)試是開(kāi)發(fā)階段的事情
  • 對(duì)代碼實(shí)在沒(méi)有信心可以使用nosources-source-map

devtool

201802100830451.png

webpack HRM

HMR(Hot Module Replacement) 模塊熱替換,應(yīng)用運(yùn)行過(guò)程中,實(shí)時(shí)替換某個(gè)模塊,應(yīng)用運(yùn)行狀態(tài)不受影響。

webpack-dev-server自動(dòng)刷新導(dǎo)致的頁(yè)面狀態(tài)丟失。我們希望在頁(yè)面不刷新的前提下,模塊也可以即使更新。熱替換只將修改的模塊實(shí)時(shí)替換至應(yīng)用中。

HMR是webpack中最強(qiáng)大的功能之一,極大程度的提高了開(kāi)發(fā)者的工作效率。

HMR已經(jīng)集成在了webpack-dev-server中,運(yùn)行webpack-dev-server --hot,也可以通過(guò)配置文件開(kāi)啟.

Webpack中的HMR并不是對(duì)所有文件開(kāi)箱即用,樣式文件支持熱更新,腳本文件需要手動(dòng)處理模塊熱替換邏輯。而通過(guò)腳手架創(chuàng)建的項(xiàng)目?jī)?nèi)部都集成了HMR方案。

HMR注意事項(xiàng):

  • 處理HMR的代碼報(bào)錯(cuò)會(huì)導(dǎo)致自動(dòng)刷新
  • 沒(méi)啟動(dòng)HMR的情況下,HMR API報(bào)錯(cuò)
  • 代碼中多了很多與業(yè)務(wù)無(wú)關(guān)的代碼

生產(chǎn)環(huán)境優(yōu)化

我們?cè)谏a(chǎn)環(huán)境中,更注重開(kāi)發(fā)效率,而在生產(chǎn)環(huán)境中,更注重開(kāi)發(fā)效率。

模式(mode)

webpack建議我們?yōu)椴煌沫h(huán)境創(chuàng)建不同的配置,兩種方案:

  • 配置文件根據(jù)環(huán)境不同導(dǎo)出不同配置
const path = require('path')
const webpack = require('webpack')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = (env, argv) => {
  const config = {
    mode: 'none',
    entry: './src/main.js',
    output: {
      filename: 'bundle.js',
      path: path.join(__dirname, 'dist'),
      // publicPath: 'dist/'
    },
    module: {
      rules: [
        {
          test: /.md$/,
          use: ['html-loader', './markdown-loader.js']
        }
      ]
    },
    plugins: [
      new CleanWebpackPlugin(),
      // 用于生成index.html 
      new HtmlWebpackPlugin({
        title: 'Webpack Plugin Sample',
        meta: {
          viewport: 'width=device-width'
        },
        template: './src/index.html'
      }),
      // 用于生成about.html 
      new HtmlWebpackPlugin({
        filename: 'about.html'
      }),
      // 開(kāi)發(fā)過(guò)程最好不要使用這個(gè)插件
      // new CopyWebpackPlugin({
      //   patterns: ['public']
      // }),
      // new MyPlugin(),
      new webpack.HotModuleReplacementPlugin()
    ],
    devServer: {
      contentBase: './public',
      proxy: {
        '/api': {// 以/api開(kāi)頭的地址都會(huì)被代理到接口當(dāng)中
          // http://localhost:8080/api/users -> https://api.github.com/api/users
          target: 'https://api.github.com',
          // http://localhost:8080/api/users -> https://api.github.com/users
          pathRewrite: {
            '^/api': ''
          },
          // 不能使用localhost:8080作為請(qǐng)求GitHub的主機(jī)名
          changeOrigin: true, // 以實(shí)際代理的主機(jī)名去請(qǐng)求
        }
      },
      // hot: true
      hotOnly: true, // 如果熱替換代碼報(bào)錯(cuò)了,則不刷新
    },
    devtool: 'eval-cheap-module-source-map'
  }
  
  if (env === 'production') {
    config.mode = 'production'
    config.devtool = false
    config.plugins = [
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin({
        patterns: ['public']
      })
    ]
  }
  return config
}

  • 一個(gè)環(huán)境對(duì)應(yīng)一個(gè)配置文件

Webpack.common.js

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: `bundle.js`
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: `index.html`
    })
  ]
}

Webpack.dev.js

const common = require('./webpack.common')
const merge = require('webpack-merge')

module.export = merge(common, {
  mode: 'development',
})

Webpack.prod.js

const common = require('./webpack.common')
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = merge(common, {
  mode: 'production',
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({
        patterns: ['public']
    })
  ]
})

Package.json

  "scripts": {
    "server": "npx webpack serve --config webpack.dev.js --open",
    "build": "webpack --config webpack.prod.js"
  },

webpack的優(yōu)化配置

  • definePlugin
    DefinePlugin 為代碼注入全局成員,這個(gè)內(nèi)置插件默認(rèn)就會(huì)啟動(dòng),往每個(gè)代碼中注入一個(gè)全局變量process.env.NODE_ENV
const webpack = require('webpack')

plugins: [
    new HtmlWebpackPlugin({
      filename: `index.html`
    }),
    new webpack.DefinePlugin({
      API_BASE_URL: JSON.stringify('http://api.example.com')
    })
  ]`
  • Tree-Shaking 搖掉代碼中未引用到的代碼(dead-code),這個(gè)功能在生產(chǎn)模式下自動(dòng)被開(kāi)啟。Tree-Shaking并不是webpack中的某一個(gè)配置選項(xiàng),而是一組功能搭配使用后的效果。因?yàn)門(mén)ree-Shaking前提是ES Modules,由Webpack打包的代碼必須使用ESM,為了轉(zhuǎn)化ES中的新特性,會(huì)使用babel處理新特性,就有可能將ESM轉(zhuǎn)化CommonJS,而我們使用的@babel/preset-env這個(gè)插件集合就會(huì)轉(zhuǎn)化ESM為CommonJS,所以Tree-Shaking會(huì)不生效。但是在最新版babel-loader關(guān)閉了轉(zhuǎn)換ESM的插件,所以使用babel-loader不會(huì)導(dǎo)致Tree-Shaking失效
optimization: {
    usedExports: true,
    minimize: true
  }
  • 合并模塊函數(shù) concatenateModules, 又被成為Scope Hoisting,作用域提升
optimization: {
   usedExports: true,
   minimize: true,
   concatenateModules: true
 }
  • sideEffects 副作用,指的是模塊執(zhí)行時(shí)除了導(dǎo)出成員之外所做的事情,sideEffects一般用于npm包標(biāo)記是否有副作用。如果沒(méi)有副作用,則沒(méi)有用到的模塊則不會(huì)被打包。
optimization: {
    usedExports: true,
    minimize: true,
    concatenateModules: true,
    sideEffects: true
  }

在package.json里面增加一個(gè)屬性sideEffects,值為false,表示沒(méi)有副作用,沒(méi)有用到的代碼則不進(jìn)行打包。確保你的代碼真的沒(méi)有副作用,否則在webpack打包時(shí)就會(huì)誤刪掉有副作用的代碼,比如說(shuō)在原型上添加方法,則是副作用代碼;還有CSS代碼也屬于副作用代碼。

代碼分割

webpack的一個(gè)弊端:所有的代碼都會(huì)被打包到一起,如果應(yīng)用復(fù)雜,bundle會(huì)非常大。而并不是每個(gè)模塊在啟動(dòng)時(shí)都是必要的,所以需要分包、按需加載。物極必反,資源太大了不行,太碎了也不行。太大了會(huì)影響加載速度;太碎了會(huì)導(dǎo)致請(qǐng)求次數(shù)過(guò)多,因?yàn)樵谀壳爸髁鞯腍TTP1.1有很多缺陷,如同域并行請(qǐng)求限制、每次請(qǐng)求都會(huì)有一定的延遲,請(qǐng)求的Header浪費(fèi)帶寬流量。所以模塊打包時(shí)有必要的。

目前的webpack分包方式有兩種:

  • 多入口打包:適用于多頁(yè)應(yīng)用程序,一個(gè)頁(yè)面對(duì)應(yīng)一個(gè)打包入口,公共部分單獨(dú)抽取。
entry: {
  index: './src/index.js',
    album: './src/album.js'
},
output: {
    filename: '[name].bundle.js'
},
// 每個(gè)打包入口形成一個(gè)獨(dú)立的chunk
plugins: [
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'Nulti Entry',
      template: './src/album.html',
      filename: 'album.html',
      chunks: ['album']
    })
  ],
// 不同的打包入口肯定會(huì)有公共模塊,我們需要提取公共模塊:
    optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }

  • 動(dòng)態(tài)導(dǎo)入:需要用到某個(gè)模塊時(shí),再加載這個(gè)模塊,動(dòng)態(tài)導(dǎo)入的模塊會(huì)被自動(dòng)分包。通過(guò)動(dòng)態(tài)導(dǎo)入生成的文件只是一個(gè)序號(hào),可以使用魔法注釋指定分包產(chǎn)生bundle的名稱(chēng)。相同的chunk名會(huì)被打包到一起。
import(/* webpackChunkName: 'posts' */'./post/posts').then({default: posts}) => {
  mainElement.appendChild(posts())
}

MiniCssExtractPlugin可以提取CSS到單個(gè)文件

當(dāng)css代碼超過(guò)150kb左右才建議使用。

const MiniCssExtracPlugin = require('mini-css-extract-plugin')

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

OptimizeCssAssetsWebpackPlugin 壓縮輸出的CSS文件

webpack僅支持對(duì)js的壓縮,其他文件的壓縮需要使用插件。

可以使用 optimize-css-assets-webpack-plugin壓縮CSS代碼。放到minimizer中,在生產(chǎn)模式下就會(huì)自動(dòng)壓縮

optimization: {
  minimizer: [
    new TerseWebpackPlugin(), // 指定了minimizer說(shuō)明要自定義壓縮器,所以要把JS的壓縮器指指明,否則無(wú)法壓縮
    new OptimizeCssAssetWebpackPlugin()
  ]
}

輸出文件名hash

生產(chǎn)模式下,文件名使用Hash

項(xiàng)目級(jí)別的hash

output: {
      filename: '[name]-[hash].bundle.js'
  },

chunk級(jí)別的hash

output: {
      filename: '[name]-[chunkhash].bundle.js'
  },

文件級(jí)別的hash,:8是指定hash長(zhǎng)度 (推薦)

output: {
      filename: '[name]-[contenthash:8].bundle.js'
  },
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容