webpack4+react16+react-router-dom4從零配置到優化,實現路由按需載入(上)

高山低谷發表於2019-10-28

之前一直用vue+webpack在寫前端專案,最近換了個環境後,專案都是用react來開發的,所以也想搗鼓下框架啥的東西,最近也看了下webpack4,感覺這玩意兒越來越整合化了,有些外掛已經被丟棄了或者整合了,不過在實際專案開發中,也存在一些依賴版本方面的問題,本文將帶你走進webpack的世界,一起看看那些坑吧。

本人寫的例項,已上傳至GitHub,點選專案地址可以檢視詳情,歡迎star哦。

前言

webpack作為時下前端最流行的自動化構建工具,其版本更新也是一路備受關注,目前大多數新專案都會採用webpack4.0+去構建,接下來在瞭解如何用webpack從零開始搭建自己的專案之前,我們先還是熟悉下webpack的基本概念:

webpack: 本質上,webpack 是一個現代 JavaScript 應用程式的靜態模組打包器(module bundler)。當 webpack 處理應用程式時,它會遞迴地構建一個依賴關係圖(dependency graph),其中包含應用程式需要的每個模組,然後將所有這些模組打包成一個或多個 bundle檔案被瀏覽器識別使用。

WebPack和Grunt以及Gulp相比有什麼特性

gulpgrunt是基於流的一種管理工具,通過建立一個個task任務,配置執行使用者需要的功能,如格式檢驗,程式碼壓縮等,值得一提的是,經過這兩者處理的程式碼只是區域性變數名被替換簡化,整體並沒有發生改變,還是你的程式碼。

webpack是基於入口檔案,識別模組依賴關係,分析程式碼,轉換程式碼,編譯程式碼,輸出程式碼,經過打包後的程式碼基本已經是認不出來了,有點像壓縮的jq那樣的感覺,webpack它本身也是一個node的模組,webpack.config.js是以commonjs形式書寫的(node中的模組化是commonjs規範的)。

webpack核心概念:

  • entry: 一個可執行模組或庫的入口檔案。
  • Module:模組,在 Webpack 裡一切皆模組,一個模組對應著一個檔案,Webpack 會從配置的 Entry 開始遞迴找出所有依賴的模組。
  • chunk: 程式碼塊,一個 Chunk 由多個模組組合而成,用於程式碼合併與分割。
  • loader: 模組轉換器,例如把es6轉換為es5,scss轉換為css。
  • plugin: 擴充套件外掛,用於擴充套件webpack的功能,在webpack構建生命週期的節點上加入擴充套件hook為webpack加入功能。
  • Output:輸出結果,在 Webpack 經過一系列處理並得出最終想要的程式碼後輸出結果。

熟悉了webpck的一些基本概念後,接下來,我們從零開始真正的配置下webpack.config.js檔案。

一、webpack.config配置

1、建立專案

mkdir react-webpack4
cd react-webpack4
mkdir src
mkdir dist
npm init -y

// 安裝webpack和相關模組
yarn add webpack webpack-cli webpack-dev-server -D   //webpack4.X 把webpack拆分了
touch webpack.config.js         // mac上建立檔案 
echo test>webpack.config.js     // win上建立檔案
module.exports = {
    entry: ["./src/index.js"],  //  專案檔案入口
    output: {
        // 輸出目錄
        path: path.resolve(__dirname, "../dist")
    },
    module: {},
    plugins:[],
    devServer: {}
} 
複製程式碼

webpack.config.js的檔案結構就是如上,但是在專案開發時, 往往開發環境和生產環境在配置上會有些不同,所以為了區分開來,我們在專案根目錄下建立一個build的資料夾,在該資料夾下面建立以下三個檔案:

webpack.base.config.js  // 基本配置
webpack.dev.config.js   // 開發環境
webpack.prod.config.js  // 生產環境
複製程式碼

接下來先看下webpack.base.config.js基本配置:

const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
    entry: ["./src/index.js"],
    output: {
        // 輸出目錄
        path: path.resolve(__dirname, "../dist")
    },
    resolve: {
        extensions: [".js", ".jsx"], // 擴充套件
        alias: {
            '@': path.resolve(__dirname, 'src'),
            '@pages': path.resolve(__dirname, 'src/pages'),
            '@router': path.resolve(__dirname, 'src/router'),
            '@assets': path.resolve(__dirname, 'src/assets')
        }
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx?)$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: "happypack/loader?id=happyBabel"
                    }
                ]
            },
            {
                test: /\.(le|c)ss$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    "css-loader", // 編譯css
                    "postcss-loader", // 使用 postcss 為 css 加上瀏覽器字首
                    "less-loader", // 編譯less
                ]
            },
            {
                test: /\.(png|jpg|jpeg|gif|svg)/,
                use: {
                    loader: "url-loader",
                    options: {
                        outputPath: "images/", // 圖片輸出的路徑
                        limit: 10 * 1024
                    }
                }
            },
            {
                test: /\.(eot|woff2?|ttf|svg)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name]-[hash:5].min.[ext]',
                            limit: 5000, // 使用base64進行轉換, 大小限制小於5KB, 否則使用svg輸出
                            publicPath: 'fonts/',
                            outputPath: 'fonts/'
                        }
                    }
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            title:'webpack配置',  //  專案標題
            filename: "index.html", // 最終建立的檔名
            template: path.resolve(__dirname, '..', "src/index.html"), // 指定模板路徑
            minify: {
                collapseWhitespace: true // 去除空白
            }
        }),
        // 單獨生成css檔案和js檔案分離開來 加快頁面渲染
        new MiniCssExtractPlugin({
            filename: "[name]-[hash:5].css",
            chunkFilename: "[id]-[hash:5].css"
        }),
        // happypack
        new HappyPack({
            //用id來標識 happypack處理那裡類檔案
            id: 'happyBabel',
            //如何處理  用法和loader 的配置一樣
            loaders: [{
                loader: 'babel-loader?cacheDirectory=true',
            }],
            //共享程式池threadPool: HappyThreadPool 代表共享程式池,即多個 HappyPack 例項都使用同一個共享程式池中的子程式去處理任務,以防止資源佔用過多。
            threadPool: happyThreadPool,
            //允許 HappyPack 輸出日誌
            verbose: true,
        }),

    ],
    performance: false // 關閉效能提示
};

複製程式碼

以上配置,可能你見過,但是不一定知道有些東西用來幹嘛的,做個解釋:

1: HtmlWebpackPlugin()

yarn add  html-webpack-plugin 
複製程式碼
    new HtmlWebpackPlugin({
        title:'webpack配置',  //  網站標題
        filename: "index.html", // 最終建立的檔名
        template: path.resolve(__dirname, '..', "src/index.html"), // 指定模板路徑
        minify: {
            collapseWhitespace: true // 去除空白
        }
    }),
複製程式碼

使用 HtmlWebpackPlugin外掛,來生成 html, 並將每次打包的js自動插入到你的 index.html 裡面去,而且它還可以基於你的某個 html 模板來建立最終的 index.html,也就是說可以指定模板

2、MiniCssExtractPlugin()

yarn add mini-css-extract-plugin 
複製程式碼

如果不做該配置,我們的css是直接打包進js裡面的,我們希望能單獨生成css檔案。 因為單獨生成css,css可以和js並行下載,提高頁面載入效率。 特別說明:目前 extract-text-webpack-plugin 最新版本不支援 Webpack 4.3.0 版本. Webpack 4.2.0 一下可用。 目前從 extract-text-webpack-plugin issues 瞭解, 未來 extract-text-webpack-plugin 將廢棄,作者建議使用 mini-css-extract-plugin

    {
        test: /\.(le|c)ss$/,
        use: [
            MiniCssExtractPlugin.loader,
            "css-loader", // 編譯css
            "postcss-loader", // 使用 postcss 為 css 加上瀏覽器字首
            "less-loader", // 編譯less
        ]
    },
複製程式碼
new MiniCssExtractPlugin({
            filename: "[name]-[contenthash:5].css",
            chunkFilename: "[id]-[contenthash:5].css"
 }),
複製程式碼

這裡補充說明下,webpack中對於輸出檔名可以有三種hash值:

hash: 如果都使用hash的話,因為這是工程級別的,即每次修改任何一個檔案,所有檔名的hash至都將改變。所以一旦修改了任何一個檔案,整個專案的檔案快取都將失效。

chunkhash: 根據不同的入口檔案(Entry)進行依賴檔案解析、構建對應的chunk,生成對應的雜湊值。在生產環境裡把一些公共庫和程式入口檔案區分開,單獨打包構建,接著我們採用chunkhash的方式生成雜湊值,那麼只要我們不改動公共庫的程式碼,就可以保證其雜湊值不會受影響,但是同一個模組,就算將js和css分離,其雜湊值也是相同的,修改一處,js和css雜湊值都會變,同hash,沒有做到快取意義。

contenthash:表示由檔案內容產生的hash值,內容不同產生的contenthash值也不一樣。在專案中,通常做法是把專案中css都抽離出對應的css檔案來加以引用。(只要檔案內容不一樣,產生的雜湊值就不一樣)

所以css檔案最好使用contenthash。

3、使用happypack併發執行任務

yarn  add happypack
複製程式碼

執行在 Node.之上的Webpack是單執行緒模型的,也就是說Webpack需要一個一個地處理任務,不能同時處理多個任務。 Happy Pack 就能讓Webpack做到這一點,它將任務分解給多個子程式去併發執行,子程式處理完後再將結果傳送給主程式。

plugins:[
      new HappyPack({
      //用id來標識 happypack處理那裡類檔案
        id: 'happyBabel',
        //如何處理  用法和loader 的配置一樣
        loaders: [{
            loader: 'babel-loader?cacheDirectory=true',
        }],
        //共享程式池threadPool: HappyThreadPool 代表共享程式池,即多個 HappyPack 例項都使用同一個共享程式池中的子程式去處理任務,以防止資源佔用過多。
        threadPool: happyThreadPool,
        //允許 HappyPack 輸出日誌
        verbose: true,
  })
]
複製程式碼

以上是對webpack.base.config.js基礎配置的一個解釋說明,接下來我們們再看看再開發環境:webpack.dev.config.js

const path = require("path");
const merge = require('webpack-merge')
const commonConfig = require('./webpack.base.config.js')
const webpack = require("webpack");
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
module.exports = merge(commonConfig, {
    mode: "development", //webpack 4.0 要申明這個
    devtool: 'cheap-module-eval-soure-map',
    output: {
        // 輸出目錄
        path: path.resolve(__dirname, "../dist"),
        // 檔名稱
        filename: "bundle.js",
        chunkFilename: '[name].js'
    },
    plugins: [
        //開啟HMR(熱替換功能,替換更新部分,不過載頁面!) 相當於在命令列加 --hot
        new webpack.HotModuleReplacementPlugin(),
        new webpack.DefinePlugin({
            'ENV': JSON.stringify("development"),
            'process.env': {
                VUEP_BASE_URL: '/'
            }
        }),
              //識別某些類別的webpack錯誤
        new FriendlyErrorsPlugin({
            // 執行成功
            compilationSuccessInfo: {
            message: ['你的應用程式在這裡執行http://localhost:8001'],
            notes: ['有些附加說明要在成功編輯時顯示']
            },
            // 執行錯誤
            onErrors: function(severity, errors){
            //您可以收聽外掛轉換和優先順序的錯誤,嚴重性可以是'錯誤''警告'
            },
            //是否每次編譯之間清除控制檯,預設為true
            clearConsole: true,  
            //新增格式化程式和變換器(見下文)
            additionalFormatters: [],
            additionalTransformers: []
        })
    ],
    devServer: {
        contentBase: path.resolve(__dirname, "../dist"), //  指定訪問資源目錄
        historyApiFallback: true, //  該選項的作用所有的404都連線到index.html
        disableHostCheck: true,   //  繞過主機檢查
        inline: true,             //  改動後是否自動重新整理
        host: 'localhost',        //  訪問地址
        port: 8001,               // 訪問埠
        overlay: true,         //  出現編譯器錯誤或警告時在瀏覽器中顯示全屏覆蓋
        stats: "errors-only",     // 顯示捆綁軟體中的錯誤
        compress: true,           // 對所有服務啟用gzip壓縮
        open: true,               // 自動開啟瀏覽器
        progress: true,            // 顯示編譯進度
        proxy: {
            // 代理到後端的服務地址
            "/api": "http://localhost:8000"
        }
    }
});
複製程式碼

4 webpack-dev-server

    devServer: {
        contentBase: path.resolve(__dirname, "../dist"), //  指定訪問資源目錄
        historyApiFallback: true, //  該選項的作用所有的404都連線到index.html
        disableHostCheck: true,   //  繞過主機檢查
        inline: true,             //  改動後是否自動重新整理
        host: 'localhost',        //  訪問地址
        port: 8001,               // 訪問埠
        overlay: true,         //  出現編譯器錯誤或警告時在瀏覽器中顯示全屏覆蓋
        stats: "errors-only",     // 顯示捆綁軟體中的錯誤
        compress: true,           // 對所有服務啟用gzip壓縮
        open: true,               // 自動開啟瀏覽器
        progress: true,            // 顯示編譯進度
        proxy: {
            // 代理到後端的服務地址
            "/api": "http://localhost:8000"
        }
    }
複製程式碼

webpack4已經整合了open-browser-webpack-plugin 和 progress-bar-webpack-plugin這兩個外掛,所以不需要再單獨引入,直接將 devServer.open 和devServer.progress設定為true即可。

5、mode

mode: "development/production", webpack 4.0+ 要申明這個

6、output

output: {
    // 輸出目錄
    path: path.resolve(__dirname, "../dist"),
    // 檔名稱
    filename: "bundle.js",
    chunkFilename: '[name].js'
},
複製程式碼

7、FriendlyErrorsPlugin

yarn add friendly-errors-webpack-plugin
複製程式碼

Friendly-errors-webpack-plugin識別某些類別的webpack錯誤,並清理,聚合和優先順序,以提供更好的開發人員體驗。

    //識別某些類別的webpack錯誤
    new FriendlyErrorsPlugin({
        // 執行成功
        compilationSuccessInfo: {
        message: ['你的應用程式在這裡執行http://localhost:8001'],
        notes: ['有些附加說明要在成功編輯時顯示']
        },
        // 執行錯誤
        onErrors: function(severity, errors){
        //您可以收聽外掛轉換和優先順序的錯誤, 嚴重性可以是'錯誤''警告'
        },
        //是否每次編譯之間清除控制檯,預設為true
        clearConsole: true,  
        //新增格式化程式和變換器(見下文)
        additionalFormatters: [],
        additionalTransformers: []
    })
複製程式碼

最後,我們們看看再實際生產環境中,webpack.dev.config.js的配置:

const path = require("path");
const webpack = require("webpack");
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const commonConfig = require('./webpack.base.config.js')
const PurifyCSS = require('purifycss-webpack')
const glob = require('glob-all')
const WorkboxPlugin = require('workbox-webpack-plugin') // 引入 PWA 外掛

module.exports = merge(commonConfig, {
    mode: "production", //webpack 4.0 要申明這個
    output: {
        // 輸出目錄
        path: path.resolve(__dirname, "../dist"),
        // 檔名稱
        filename: '[name].[contenthash:5].js',
        chunkFilename: '[name].[contenthash:5].js'
    },
    devtool: 'cheap-module-source-map',
    optimization: {
        usedExports: true,   //js Tree Shaking 清除到程式碼中無用的js程式碼,只支援import方式引入,不支援commonjs的方式引入
        splitChunks: {
            chunks: "all", // 所有的 chunks 程式碼公共的部分分離出來成為一個單獨的檔案
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors'
                }
            }
        },
    },
    plugins: [
        new CleanWebpackPlugin(),// 打包時刪除之前的檔案
        // 清除無用 css---生產環境---csstree-shaking
        new PurifyCSS({
            paths: glob.sync([
                // 清除無用 css---生產環境-- 對於 css的tree shaking 使用 purifycss-webpack 配合 glob-all來實現 。
                path.resolve(__dirname, '..', 'src/*.html'),
                path.resolve(__dirname, '..', 'src/*.js'),
                path.resolve(__dirname, '..', 'src/**/*.jsx'),
            ])
        }),
        // PWA配置,生產環境才需要,PWA優化策略,在你第一次訪問一個網站的時候,如果成功,做一個快取,當伺服器掛了之後,你依然能夠訪問這個網頁 ,這就是PWA。那相信你也已經知道了,這個只需要在生產環境,才需要做PWA的處理,以防不測。
        new WorkboxPlugin.GenerateSW({
            clientsClaim: true,
            skipWaiting: true
        }),
    ]
});
複製程式碼

8、外掛 CleanWebpackPlugin

你執行yarn run build的時候,每次都會在dist目錄下邊生成一堆檔案,但是上一次的打包的檔案還在,這個我們需要每次打包時清除 dist 目錄下舊版本檔案

yarn add  clean-webpack-plugin
複製程式碼

注意: 這個引入的坑,最新版的需要這樣引入,而不是直接

//  報錯:const CleanWebpackPlugin =  require('clean-webpack-plugin')
複製程式碼
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
 
plugins: [
    new CleanWebpackPlugin() 
]
複製程式碼

9、使用 source-map,對devtool進行優化

webpack中devtool選項用來控制是否生成,以及如何生成 source map。簡言之,source map就是幫助我們定位到錯誤資訊位置的檔案。正確的配置source map,能夠提高開發效率,更快的定位到錯誤位置。

devtool:"cheap-module-eval-source-map",// 開發環境配置
devtool:"cheap-module-source-map",   // 線上生成配置
複製程式碼

10、css Tree Shaking 以去除專案程式碼中用不到的 CSS 樣式,僅保留被使用的樣式程式碼,達到減輕包的體積。

yarn  add glob-all purify-css purifycss-webpack 
複製程式碼
const PurifyCSS = require('purifycss-webpack')
const glob = require('glob-all')
plugins:[
    // 清除無用 css
    new PurifyCSS({
      paths: glob.sync([
        // 要做 CSS Tree Shaking 的路徑檔案
        path.resolve(__dirname, './src/*.html'), // 請注意,我們同樣需要對 html 檔案進行 tree shaking
        path.resolve(__dirname, './src/*.js')
      ])
    })
]
複製程式碼

11、js Tree Shaking

在webpack4中已經移除了UglifyJsPlugin,只需要配置mode為"production",即可顯式啟用 UglifyjsWebpackPlugin 外掛。

清除到程式碼中無用的js程式碼,只支援import方式引入,不支援commonjs的方式引入

只要mode是production就會生效,develpoment的tree shaking是不生效的,因為webpack為了方便你的除錯

  optimization: {
    usedExports:true,
  }
複製程式碼

12、PWA優化策略

yarn add workbox-webpack-plugin
複製程式碼

PWA的作用就是當你第一次成功訪問網站是,做一個快取,那麼在伺服器掛了的情況下,你依然可以訪問這個網頁,這個只需要在生產環境下處理,以防不測。

const WorkboxPlugin = require('workbox-webpack-plugin')
new WorkboxPlugin.GenerateSW({
    clientsClaim: true,
    skipWaiting: true
}),
複製程式碼

以上是關於本人在webpack在開發環境和生產環境的一些配置和優化,歡迎大家繼續補充和指正,大家對webpack有興趣的也可以先去官網瞭解下webpack的一些知識點。

由於篇幅太長,本文分為兩部分來寫,這一篇主要講解下webpack方面的配置。

下一篇 將寫一些關於基於react-router4.0實現路由的按需載入。

相關文章