webpack原理以及搭建腳手架完整流程


分享也是再次學習的過程,所以即使已經有非常多這類文章,我也還是再寫一遍。一起學習一起進步。

原理

幾個概念

  • Entry:入口,Webpack 執行構建的第一步將從 Entry 開始,可抽象成輸入。
  • Module:模塊,在 Webpack 裏一切皆模塊,一個模塊對應着一個文件。Webpack 會從配置的 Entry 開始遞歸找出所有依賴的模塊。
  • Chunk:代碼塊,一個 Chunk 由多個模塊組合而成,用於代碼合併與分割。
  • Loader:模塊轉換器,用於把模塊原內容按照需求轉換成新內容。
  • Plugin:擴展插件,在 Webpack 構建流程中的特定時機會廣播出對應的事件,插件可以監聽這些事件的發生,在特定時機做對應的事情。

流程

Webpack 的運行流程是一個串行的過程,從啓動到結束會依次執行以下流程:

  • 初始化參數:從配置文件和 Shell 語句中讀取與合併參數,得出最終的參數;
  • 開始編譯:用上一步得到的參數初始化 Compiler 對象,加載所有配置的插件,執行對象的 run 方法開始執行編譯;
  • 確定入口:根據配置中的 entry 找出所有的入口文件;
  • 編譯模塊:從入口文件出發,調用所有配置的 Loader 對模塊進行翻譯,再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經過了本步驟的處理;
  • 完成模塊編譯:在經過第4步使用 Loader 翻譯完所有模塊後,得到了每個模塊被翻譯後的最終內容以及它們之間的依賴關係;
  • 輸出資源:根據入口和模塊之間的依賴關係,組裝成一個個包含多個模塊的 Chunk,再把每個 Chunk 轉換成一個單獨的文件加入到輸出列表,這步是可以修改輸出內容的最後機會;
  • 輸出完成:在確定好輸出內容後,根據配置確定輸出的路徑和文件名,把文件內容寫入到文件系統。
    在以上過程中,Webpack 會在特定的時間點廣播出特定的事件,插件在監聽到感興趣的事件後會執行特定的邏輯,並且插件可以調用 Webpack 提供的 API 改變 Webpack 的運行結果。
    Webpack 是一個龐大的 Node.js 應用,如果你閱讀過它的源碼,你會發現實現一個完整的 Webpack 需要編寫非常多的代碼。 但你無需瞭解所有的細節,只需瞭解其整體架構和部分細節即可。

webpack是一個簡單強大的工具。 對 Webpack 的開發者來說,它是一個擴展性的高系統。

Webpack 把複雜的實現隱藏了起來,給用戶暴露出的只是一個簡單的工具,讓用戶能快速達成目的。 同時整體架構設計合理,擴展性高,開發擴展難度不高,通過社區補足了大量缺失的功能,讓 Webpack 幾乎能勝任任何場景。

初始化項目

mkdir first-webpack
cd first-webpack
mkdir src
npm init -y //初始化

新建index.js和index.html文件

// index.js
console.log('hello world')

// index.html
<!DOVTYPE html>
<html lang='en'>
<head>
	<meta charset ="utf-8">
	<title > xxxx</title>
</head>
 <body>
 	<div id="root"></div>
 </body>
</html>

安裝webpack並配置

安裝webpack

npm add webpack webpack-cli --save-dev

新建一個build文件夾存放webpack配置文件

mkdir build
touch webpack.dev.js

在webpack.dev.js中書寫基本配置

const path = require('path');
module.export = {
	mode: 'development',
	entry: './src/index.js',
	module: {},
	plugins: [ ],
	output: {
		filname: 'bundle.js',
		path: path.resolve(__dirname, '../dist')
	}
}

package.json更改

“script”: {
	"start": "webpack --config ./build/webpack.dev.js"
}

配置babel

使用es6,babel是必不可少的。

npm install @babel/polyfill core-js@2 --save
npm install babel-loader @babel/core @babel/preset-react  --save-dev

// @babel/polyfill: 模擬一個es6+的環境,提供es6方法和函數的墊片
// core-js@2:@babel/preset-env實現按需引入polyfill時,聲明core-js版本
// babel-loader和@babel/core是核心模塊
// @babel/preset-env是一個智能預設,允許您使用最新的JavaScript
// @babel/preset-react 轉換JSX

擴展:
如果是開發工具庫,想要實現按需替換,可以使用下面下面兩個工具來實現
@babel/plugin-transform-runtime避免 polyfill 污染全局變量,減小打包體積,因此更適合作爲開發工具庫
@babel/runtime-corejs2作爲生產環境依賴,約等於@babel/runtime + babel-polyfill,使用了@babel/runtime-corejs2,就無需再使用@babel/runtime了

.babellrc文件

{
    "presets": [ 
        [
          "@babel/preset-env", {  // 將es6的語法翻譯成es5語法
            "targets": {
               "chrome": "67",
            },
            "useBuiltIns": "usage", // 做@babel/polyfill補充時,按需補充,用到什麼才補充什麼,
            "corejs": "2",
          }
        ],
	[ 
		   "@babel/preset-react",
	],
	"plugins": [
		"@babel/plugin-proposal-class-properties"
	  ]
}

更改webpack.dev.js

module: {
	rules: [{
		test: /\.js$/,
		exclude: /node_modules/,
		use: [{
		 		loader: 'babel-loader',
		  }],
	}]
}

安裝react

安裝 react 、react-dom、react-rouder

npm install react react-dom react-router react-router-dom --save

這部分代碼較多,可以在github上查看src的代碼;
https://github.com/LuoShengMen/React-Whole-barrels/commit/55a0a0efdf5f581886a303326259fa9b2ff88444

webpack-dev-server

書寫完成上述代碼運行npm start後,打開index.html你會發現沒有任何內容,此時我們需要配置一個簡單的WEB服務器,指向index.html。

// 安裝webpack-dev-server
npm install webpack-dev-server --save-dev

// 配置webpack.dev.js
...
    devServer: {
        contentBase: path.join(__dirname, '../dist')
    },
...

// 更改npm start 命令
    "start": "webpack-dev-server --config ./build/webpack.dev.js"

運行 npm start 就可以運行代碼

使用html-webpack-plugin和clean-webpack-plugin插件

到目前爲止,我們會發現都需要手動都將index.html放到dist文件夾中,並手動引入bundle.js.這個問題可以通過html-webpack-plugin解決。引入html-webpack-plugin後,在plugins生成一個實例,HtmlWebpackPlugin可以接受一個參數作爲模版文件,打包結束後自動生成一個以參數爲模版的html文件。並把打包生成的js文件自動引入到html文件中。clean-webpack-plugin可以實現在每次打包之前都把上一次的打包文件清空,這樣避免了冗餘文件的存在,用法也是直接在plugins裏面生成一個實例.

// 安裝html-webpack-plugin和clean-webpack-plugin
npm install html-webpack-plugin clean-webpack-plugin --save-dev

更改webpack.dev.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
...
    plugins: [                     // 插件
        new HtmlWebpackPlugin({   // 向dist文件中自動添加模版html
            template: 'src/index.html',
        }),
        new CleanWebpackPlugin(), // 打包後先清除dist文件,先於HtmlWebpackPlugin運行
    ],

less配置

樣式使用less預處理器,那麼就需要使用less,less-loader,css-loader,style-loader等。

npm install less less-loader css-loader style-loader postcss-loader --save-dev
// less-loader 編譯less
// css-loader // 編譯css
// style-loader創建style標籤,並將css添加進去
// postcss-loader提供自動添加廠商前綴的功能,但是需要配合autoprefixer插件來使用
npm install autoprefixer --save-dev

更改webpack.dev.js配置

rules: [
  ...
      {
            test: /\.less$/,
            exclude: /node_modules/,
            use: ['style-loader',
                {
                    loader: 'css-loader',
                    options: {
                        importLoaders: 2
                    }
                }, 'less-loader', 'postcss-loader']
        },
        {
            test: /\.css$/,
            use: ['style-loader', 'css-loader', 'postcss-loader']
        }
]

postcss.config.js

module.exports = {         // 自動添加css廠商前綴
    plugins: [
        require('autoprefixer')
    ]
}

圖標與圖片的處理

npm install file-loader url-loader --save-dev

file-loader幫助我們做兩件事情:
1.當遇到圖片文件時會將其打包移動到dist目錄下
2.接下來會獲得圖片模塊的地址,並將地址返回到引入模塊到變量之中.

url-loader基本上可以實現file-loader的功能,但是有一區別就是經過url-laoder打包後的dist文件下是不存在image文件的,這是因爲url-loader會把圖片轉換成base64的字符串直接放在bundle.js裏面。
好處:直接將圖片打包到js裏,不用額外到請求圖片,省了http請求
壞處:如果遇到打包到文件非常大,那麼加載會加載很長時間,影響體驗因此我們可以這樣配置webpack.dev.js

rules: [
  ...
       {
            test: /\.(png|jpg|gif|jpeg)$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name]_[hash].[ext]', // placeholder 佔位符
                    outputPath: 'images/', // 打包文件名
                    limit: 204800, // 小於200kb則打包到js文件裏,大於則使用file-loader的打包方式打包到imgages裏
                },
            },
        },
        {
            test: /\.(eot|woff2?|ttf|svg)$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name]-[hash:5].min.[ext]', // 和上面同理
                    outputPath: 'fonts/',
                    limit: 5000,
                }
            },
        }
]

模塊熱替HMR

模塊熱替換也稱爲HMR,代碼更新時只會更新被修改部分都顯示。有如下幾點

  • 針對於樣式調試更加方便
  • 只會更新被修改代碼的那部分顯示,提升開發效率
  • 保留在完全重新加載頁面時丟失的應用程序狀態

HMR配置有兩種方式,一種cli方式,一種Node.js API方式,我們這裏採用第二種方式。
我們通過在自定義開發服務下,使用插件webpack-dev-middleware和webpack-hot-middleware配合實現HMR

// 安裝webpack-dev-middleware webpack-Hot-middleware
npm install webpack-dev-middleware webpack-hot-middleware --save-dev
// 不要忘記安裝express,我們是通過express來啓動本地服務
npm install express --save-dev

新建dev-server.js

const path = require('path');
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require("webpack-hot-middleware")
const config = require('./webpack.dev.js');

const complier = webpack(config);   // 編譯器,編譯器執行一次就會重新打包一下代碼
const app = express();  // 生成一個實例
const DIST_DIR = path.resolve(__dirname, '../', 'dist');  // 設置靜態訪問文件路徑

let devMiddleware = webpackDevMiddleware(complier, {
    quiet: true,
    noInfo: true,
    stats: 'minimal'
})

let hotMiddleware = webpackHotMiddleware(complier,{
    log: false,
    heartbeat: 2000
 })

app.use(devMiddleware)

app.use(hotMiddleware)

// 設置訪問靜態文件的路徑
app.use(express.static(DIST_DIR))


app.listen(8080, () => {
    console.log("成功啓動:localhost:"+ 8080)
})  //監聽端口

更改webpack.dev.js

const webpack = require('webpack');
...    
entry: {                  
        //實現刷新瀏覽器webpack-hot-middleware/client?noInfo=true&reload=true 是必填的
        main: ['webpack-hot-middleware/client?noInfo=true&reload=true', './src/index.js']
    },  
    ...
    plugins: [
        ...
        new webpack.NamedModulesPlugin(),  //用於啓動HMR時可以顯示模塊的相對路徑
        new webpack.HotModuleReplacementPlugin(), // 開啓模塊熱更新,熱加載和模塊熱更新不同,熱加載是整個頁面刷新
    ]

修改啓動命令

"start" : "node ./build/dev-server.js"

public path

CDN通過將資源部署到世界各地,使得用戶可以就近訪問資源,加快訪問速度。如果我們把網頁的靜態資源上傳到CDN服務上,在訪問這些資源時,publicPath填寫的就是CDN提供URL
我們當前用/,相對於當前路徑,是因爲我們的資源在同一文件夾下。

webpack.dev.js

    output: {
        ...
        publicPath : '/'
    }

使用souracemap

sourceMap本質上是一種映射關係,打包出來的js文件中的代碼可以映射到代碼文件的具體位置,這種映射關係會幫助我們直接找到在源代碼中的錯誤。可以直接在devtool中使用.合理的使用source-map可以幫助我們提高開發效率,更快的定位到錯誤位置。生產環境和開發環境的devtool配置是不同的。我們可以在webpack.dev.js中添加devtool。

devtool:"cheap-module-eval-source-map",// 開發環境配置最佳實踐
devtool:"cheap-module-source-map",   // 生產配置最佳實踐

生產環境構建

到目前爲止我們配置的都是開發環境的webpack,開發環境(development)和生產環境(production)的構建目標差異很大,而在生產環境中,我們的目標則轉向於關注更小的 bundle,更輕量的 source map,以及更優化的資源,以改善加載時間.新建

webpack.prod.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
    mode: "production",  // 只要在生產模式下, 代碼就會自動壓縮,自動啓用 tree shaking
    devtool:"cheap-module-source-map",
    entry: {                  // 入口文件
        main: './src/index.js'
    },  
    module: {                 
        rules: ...省略 //代碼和dev中相同
    },
    plugins: [                     
        new HtmlWebpackPlugin({   
            template: 'src/index.html',
        }),
        new CleanWebpackPlugin(), 
    ],
    output: {
        publicPath: "/",
        filename: 'bundle.js',  
        path: path.resolve(__dirname, '../dist') 
    }
}

添加打包腳本

    "build": "webpack --config ./build/webpack.prod.js"

執行npm run build後,你會發現dist文件夾下已經生成一系列文件。你會發現生產環境下的配置和開發環境下的配置有很多相同,接下來我們會對webpack配置進行優化。

提取公共配置

webpack.dev.js和webpack.prod.js中有很多相同對配置,我們可以將公共配置提取出來,再使用webpack-merge來將不同環境下的配置合併起來。

npm install webpack-merge --save

複製代碼webpack配置文件更改
webpack.dev.js

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

const devConfig = {
    mode: 'development',     
    devtool:"cheap-module-eval-source-map",
    entry: {                  
        main: ['webpack-hot-middleware/client?noInfo=true&reload=true', './src/index.js']
    },  
    devServer: {
        contentBase: path.join(__dirname, '../dist')
    },
    plugins: [                     
        new webpack.NamedModulesPlugin(),  
        new webpack.HotModuleReplacementPlugin(), 
    ],
    output: {}
}
module.exports = merge.smart(commonConfig, devConfig)

webpack.prod.js

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

const prodConfig = {
    mode: "production",  // 只要在生產模式下, 代碼就會自動壓縮
    devtool:"cheap-module-source-map",
    entry: {
        main: './src/index.js'
    },  
    module: {},
    plugins: [],
    output: {}
}
module.exports = merge.smart(commonConfig, prodConfig)

webpack.common.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

const commonConfig = {
    module: {                 
        ...太多了省略吧
    },
    plugins: [                     
        new HtmlWebpackPlugin({   
            template: 'src/index.html',
        }),
        new CleanWebpackPlugin(), 
    ],
    output: {
        publicPath: "/",
        filename: 'bundle.js',  
        path: path.resolve(__dirname, '../dist') 
    }
}

module.exports = commonConfig;

瀏覽器緩存(Cathing)

爲了解決瀏覽器文件緩存問題,例如:代碼更新後,文件名稱未改變,瀏覽器非強制刷新後,瀏覽器去請求文件時認爲文件名稱未改變而直接從緩存中讀取不去重新請求。我們可以在webpack.prod.js輸出文件名稱中添加hash值.使用HashedModuleIdsPlugin的原因是可以當更改某一個文件時,只改變這一個文件的hash值,而不是所有的文件都改變。

plugins: [
    ...
        new webpack.HashedModuleIdsPlugin(),  //根據模塊的相對路徑生成一個四位數的hash
        new webpack.optimize.CommonsChunkPlugin({ // 配合上面的插件使用
            name: 'runtime'
        })
],
output: {
        filename: '[name].[contenthash].js',  // entry對應的key值
        chunkFilename: '[name].[contenthash].js',  // 間接引用的文件會走這個配置
    },

運行npm run build命令後,會發現dist文件中js文件名中已經有了hash值

記得同步修改 webpack.common.js 和 webpack.dev.js

指定環境

可以通過指定環境,來使webpack進行選擇性編譯,擇性編譯是指根據打包是環境的不同,選擇性地讓特定的語句有效,讓特定的語句無效。這樣可以對具體用戶的環境進行代碼優化,從而刪除或添加一些重要代碼。最簡單的例子,在開發環境中,我們打印日誌,但在生產環境中,我們讓所有打印日誌的語句無效(讓程序不運行打印的語句,甚至讓打包出來的文件根本就不包含打印日誌的語句)我們可以使用 webpack 內置的 DefinePlugin 來實現。

// webpack.dev.js
...
plugins: [
  ...
  new Webpack.DefinePlugin({
     'process.env.NODE_ENV': JSON.stringify('development'),
  }),
]

// webpack.prod.js
plugins: [
  ...
  new Webpack.DefinePlugin({
     'process.env.NODE_ENV': JSON.stringify('production'),
  }),
]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章