webpack4大結局:加入騰訊IM配置策略,實現前端工程化環境極致優化

圖片描述

webpack,打包全部的資源

不知道不覺,webpack已經偷偷更新到4.34版本了,本人決定,這是今年最後一篇寫webpack的文章,除非它更新到版本5,本人今年剩下的時間都會放在Golang和二進制數據操做以及後端的生態上

在看本文前,假設你對webpack有必定了解,若是不瞭解,能夠看看我以前的手寫ReactVue腳手架的文章

在此對 webpack的性能優化進行幾點聲明:
  • 在部分極度複雜的環境下,須要雙package.json文件,即實行三次打包
  • 在代碼分割時,低於18K的文件不必單獨打包成一個chunk,http請求次數過多反而影響性能
  • prerenderPWA互斥,這個問題暫時沒有解決
  • babel緩存編譯緩存的是索引,即hash值,很是吃內存,每次開發完記得清理內存
  • babel-polyfill按需加載在某些很是複雜的場景下比較適合
  • prefetch,preload對首屏優化提高是明顯
  • 代碼分割無論什麼技術棧,必定要作,否則就是垃圾項目
  • 多線程編譯對構建速度提高也很明顯
  • 代碼分割配合PWA+預渲染+preload是首屏優化的巔峯,可是pwa沒法緩存預渲染的html文件

本文的webpack主要針對React技術棧,實現功能以下:

  • 開發模式熱更新
  • 識別JSX文件
  • 識別class組件
  • 代碼混淆壓縮,防止反編譯代碼,加密代碼
  • 配置alias別名,簡化import的長字段
  • 同構直出,SSR的熱調試(基於Node作中間件)
  • 實現javaScripttree shaking 搖樹優化 刪除掉無用代碼
  • 實現CSStree shaking
  • 識別 async / await 和 箭頭函數
  • react-hot-loader記錄react頁面留存狀態state
  • PWA功能,熱刷新,安裝後當即接管瀏覽器 離線後仍讓能夠訪問網站 還能夠在手機上添加網站到桌面使用
  • preload 預加載資源 prefetch按需請求資源
  • CSS模塊化,不怕命名衝突
  • 小圖片的base64處理
  • 文件後綴省掉jsx js json
  • 實現React懶加載,按需加載 , 代碼分割 而且支持服務端渲染
  • 支持less sass stylus等預處理
  • code spliting 優化首屏加載時間 不讓一個文件體積過大
  • 加入dns-prefetchpreload預請求必要的資源,加快首屏渲染(京東策略)
  • 加入prerender,極大加快首屏渲染速度
  • 提取公共代碼,打包成一個chunk
  • 每一個chunk有對應的chunkhash,每一個文件有對應的contenthash,方便瀏覽器區別緩存
  • 圖片壓縮
  • CSS壓縮
  • 增長CSS前綴 兼容各類瀏覽器
  • 對於各類不一樣文件打包輸出指定文件夾下
  • 緩存babel的編譯結果,加快編譯速度
  • 每一個入口文件,對應一個chunk,打包出來後對應一個文件 也是code spliting
  • 刪除HTML文件的註釋等無用內容
  • 每次編譯刪除舊的打包代碼
  • CSS文件單獨抽取出來
  • 讓babel不只緩存編譯結果,還在第一次編譯後開啓多線程編譯,極大加快構建速度
  • 等等....

本質上,webpack 是一個現代 JavaScript 應用程序的靜態模塊打包器(module bundler)。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖(dependency graph),其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個 bundle

webpack打包原理

  • 識別入口文件
  • 經過逐層識別模塊依賴。(Commonjs、amd或者es6的import,webpack都會對其進行分析。來獲取代碼的依賴)
  • webpack作的就是分析代碼。轉換代碼,編譯代碼,輸出代碼
  • 最終造成打包後的代碼
  • 這些都是webpack的一些基礎知識,對於理解webpack的工做機制頗有幫助。

溫馨的開發體驗,有助於提升咱們的開發效率,優化開發體驗也相當重要

  • 組件熱刷新、CSS熱刷新
  • 自從webpack推出熱刷新後,前端開發者在開環境下體驗大幅提升。
  • 沒有熱刷新能力,咱們修改一個組件後

clipboard.png

  • 加入熱刷新後

clipboard.png

主要看一下React技術棧,如何在構建中接入熱刷新

  • 不管什麼技術棧,都須要在dev模式下加上 webpack.HotModuleReplacementPlugin插件
devServer: {
        contentBase: '../build',
        open: true,
        port: 5000,
        hot: true
    },

注:也可使用react-hot-loader來實現,具體參考官方文檔javascript

在開發模式下也要代碼分割,加快打開頁面速度

optimization: {
        runtimeChunk: true,
        splitChunks: {
            chunks: 'all',
            minSize: 10000, // 提升緩存利用率,這須要在http2/spdy
            maxSize: 0,//沒有限制
            minChunks: 3,// 共享最少的chunk數,使用次數超過這個值纔會被提取
            maxAsyncRequests: 5,//最多的異步chunk數
            maxInitialRequests: 5,// 最多的同步chunks數
            automaticNameDelimiter: '~',// 多頁面共用chunk命名分隔符
            name: true,
            cacheGroups: {// 聲明的公共chunk
            vendor: {
            // 過濾須要打入的模塊
            test: module => {
            if (module.resource) {
            const include = [/[\\/]node_modules[\\/]/].every(reg => {
            return reg.test(module.resource);
            });
            const exclude = [/[\\/]node_modules[\\/](react|redux|antd)/].some(reg => {
            return reg.test(module.resource);
            });
            return include && !exclude;
            }
            return false;
            },
            name: 'vendor',
            priority: 50,// 肯定模塊打入的優先級
            reuseExistingChunk: true,// 使用複用已經存在的模塊
            },
            react: {
            test({ resource }) {
            return /[\\/]node_modules[\\/](react|redux)/.test(resource);
            },
            name: 'react',
            priority: 20,
            reuseExistingChunk: true,
            },
            antd: {
            test: /[\\/]node_modules[\\/]antd/,
            name: 'antd',
            priority: 15,
            reuseExistingChunk: true,
            },
            },
        }
    }

簡要解釋上面這段配置

  • 將node_modules共用部分打入vendor.js bundle中;
  • 將react全家桶打入react.js bundle中;
  • 若是項目依賴了antd,那麼將antd打入單獨的bundle中;(其實不用這樣,能夠看我下面的babel配置,性能更高)
  • 最後剩下的業務模塊超過3次引用的公共模塊,將自動提取公共塊
注意 上面的配置只是爲了給你們看,其實這樣配置代碼分割,性能更高
optimization: {
        runtimeChunk: true,
        splitChunks: {
            chunks: 'all',
                     }
}

react-hot-loader記錄react頁面留存狀態state

yarn add react-hot-loader
// 在入口文件裏這樣寫
 
import React from "react";
import ReactDOM from "react-dom";
import { AppContainer } from "react-hot-loader";-------------------一、首先引入AppContainre
import { BrowserRouter } from "react-router-dom";
import Router from "./router";
 
/*初始化*/
renderWithHotReload(Router);-------------------二、初始化
 
/*熱更新*/
if (module.hot) {-------------------三、熱更新操做
  module.hot.accept("./router/index.js", () => {
    const Router = require("./router/index.js").default;
    renderWithHotReload(Router);
  });
}
 
 
function renderWithHotReload(Router) {-------------------四、定義渲染函數
  ReactDOM.render(
    <AppContainer>
      <BrowserRouter>
        <Router />
      </BrowserRouter>
    </AppContainer>,
    document.getElementById("app")
  );
}
而後你再刷新試試

React的按需加載,附帶代碼分割功能 ,每一個按需加載的組件打包後都會被單獨分割成一個文件

import React from 'react'
        import loadable from 'react-loadable'
        import Loading from '../loading' 
        const LoadableComponent = loadable({
            loader: () => import('../Test/index.jsx'),
            loading: Loading,
        });
        class Assets extends React.Component {
            render() {
                return (
                    <div>
                        <div>這即將按需加載</div>
                        <LoadableComponent />
                    </div>
                )
            }
        }
        
        export default Assets

* 加入html-loader識別html文件

{
    test: /\.(html)$/,
    loader: 'html-loader'
    }

配置別名

resolve: {
    modules: [
    path.resolve(__dirname, 'src'), 
    path.resolve(__dirname,'node_modules'),
    ],
    alias: {
    components: path.resolve(__dirname, '/src/components'),
    },
    }

加入eslint-loader

{
    enforce:'pre',
    test:/\.js$/,
    exclude:/node_modules/,
    include:resolve(__dirname,'/src/js'),
    loader:'eslint-loader'
    }

resolve解析配置,爲了爲了給全部文件後綴省掉 js jsx json,加入配置

resolve: {
    extensions: [".js", ".json", ".jsx"]
}

加入HTML文件壓縮,自動將入門的js文件注入html中,優化HTML文件

new HtmlWebpackPlugin({
            template: './public/index.html',
            minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeRedundantAttributes: true,
                useShortDoctype: true,
                removeEmptyAttributes: true,
                removeStyleLinkTypeAttributes: true,
                keepClosingSlash: true,
                minifyJS: true,
                minifyCSS: true,
                minifyURLs: true,
            }
        }),

SSR同構直出熱調試

  • , 採用 webpack watch+nodemon 結合的模式實現對SSR熱調試的支持。node 服務須要的html/js經過webpack插件動態輸出,當nodemon檢測到變化後將自動重啓,html文件中的靜態資源所有替換爲dev模式下的資源,並保持socket鏈接自動更新頁面。
  • 實現熱調試後,調試流程大幅縮短,和普通非直出模式調試體驗保持一致。下面是SSR熱調試的流程圖:

clipboard.png

加入 babel-loader 還有 解析JSX ES6語法的 babel preset

  • @babel/preset-react解析 jsx語法
  • @babel/preset-env解析es6語法
  • @babel/plugin-syntax-dynamic-import解析react-loadableimport按需加載,附帶code spliting功能
  • ["import", { libraryName: "antd-mobile", style: true }], Antd-mobile的按需加載
{
                            loader: 'babel-loader',
                            options: {   //jsx語法
                                presets: ["@babel/preset-react",
                                    //tree shaking 按需加載babel-polifill
                                    ["@babel/preset-env", { "modules": false, "useBuiltIns": "false", "corejs": 2 }]],
                                plugins: [
                                    //支持import 懶加載 
                                    "@babel/plugin-syntax-dynamic-import",
                                    //andt-mobile按需加載  true是less,若是不用less style的值能夠寫'css' 
                                    ["import", { libraryName: "antd-mobile", style: true }],
                                    //識別class組件
                                    ["@babel/plugin-proposal-class-properties", { "loose": true }],
                                ],
                                cacheDirectory: true
                            },
                        }
特別提示,若是電腦性能不高,不建議開啓 babel緩存索引,很是吃內存,記得每次開發完了清理內存

加入thread-loader,在babel首次編譯後開啓多線程

const os = require('os')
    {
            loader: 'thread-loader',
            options: {
                workers: os.cpus().length   
                     }
    }

加入單獨抽取CSS文件的loader和插件

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

    {
        test: /\.(less)$/,
        use: [
            MiniCssExtractPlugin.loader,
            {
                loader: 'css-loader', options: {
                    modules: true,
                    localIdentName: '[local]--[hash:base64:5]'
                }
            },
            {loader:'postcss-loader'},
            { loader: 'less-loader' }
        ]
    }
    
     new MiniCssExtractPlugin({
            filename:'[name].[contenthash:8].css'
        }),

CSStree shaking

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')
      ])
    })
]

對小圖片進行base64處理,減小http請求數量,並對輸出的文件統一打包處理

{
                    test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,
                    loader: 'url-loader',
                    options: {
                        limit: 8 * 1024,
                        name: '[name].[hash:8].[ext]',

                    }
                }, {
                    exclude: /\.(js|json|less|css|jsx)$/,
                    loader: 'file-loader',
                    options: {
                        outputPath: 'media/',
                        name: '[name].[hash].[ext]'
                    }
                }
                ]
            }]
    },

加入單獨抽取CSS文件的loader和插件

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

    {
        test: /\.(less)$/,
        use: [
            MiniCssExtractPlugin.loader,
            {
                loader: 'css-loader', options: {
                    modules: true,
                    localIdentName: '[local]--[hash:base64:5]'
                }
            },
            {loader:'postcss-loader'},
            { loader: 'less-loader' }
        ]
    }
    
     new MiniCssExtractPlugin({
            filename:'[name].[contenthash:8].css'
        }),

加入壓縮css的插件

const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
    new OptimizeCssAssetsWebpackPlugin({
                cssProcessPluginOptions:{
                    preset:['default',{discardComments: {removeAll:true} }]
                }
            }),

加入每次打包輸出文件清空上次打包文件的插件

const CleanWebpackPlugin = require('clean-webpack-plugin')
    
    new CleanWebpackPlugin()

加入圖片壓縮

{
                test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,
                
                use:[
                    {loader: 'url-loader',
                    options: {
                        limit: 8 * 1024,
                        name: '[name].[hash:8].[ext]',
                        outputPath:'/img'
                    }},
                    {
                        loader: 'img-loader',
                        options: {
                          plugins: [
                            require('imagemin-gifsicle')({
                              interlaced: false
                            }),
                            require('imagemin-mozjpeg')({
                              progressive: true,
                              arithmetic: false
                            }),
                            require('imagemin-pngquant')({
                              floyd: 0.5,
                              speed: 2
                            }),
                            require('imagemin-svgo')({
                              plugins: [
                                { removeTitle: true },
                                { convertPathData: false }
                              ]
                            })
                          ]
                        }
                      }
                ]
                
                

            }

加入代碼混淆,反編譯

var JavaScriptObfuscator = require('webpack-obfuscator');

// ...

// webpack plugins array
plugins: [
    new JavaScriptObfuscator ({
      rotateUnicodeArray: true
  }, ['excluded_bundle_name.js'])
],

加入 PWA的插件 , WorkboxPlugin

  • pwa這個技術其實要想真正用好,仍是須要下點功夫,它有它的生命週期,以及它在瀏覽器中熱更新帶來的反作用等,須要認真研究。能夠參考百度的lavas框架發展歷史~
const WorkboxPlugin = require('workbox-webpack-plugin')


    new WorkboxPlugin.GenerateSW({ 
                clientsClaim: true, //讓瀏覽器當即servece worker被接管
                skipWaiting: true,  // 更新sw文件後,當即插隊到最前面 
                importWorkboxFrom: 'local',
                include: [/\.js$/, /\.css$/, /\.html$/,/\.jpg/,/\.jpeg/,/\.svg/,/\.webp/,/\.png/],
            }),

加入預加載preload

new PreloadWebpackPlugin({
            rel: 'preload',
            as(entry) {
                if (/\.css$/.test(entry)) return 'style';
                if (/\.woff$/.test(entry)) return 'font';
                if (/\.png$/.test(entry)) return 'image';
                return 'script';
            },
            include: 'allChunks'
            //include: ['app']
        }),

加入預渲染

const PrerenderSPAPlugin = require('prerender-spa-plugin')

new PrerenderSPAPlugin({
            routes: ['/','/home','/shop'],
            staticDir: resolve(__dirname, '../dist'),
          }),
我這套 webpack配置,不管多複雜的環境,都是能夠搞定的
  • webpack真的很是很是重要,若是用很差,就永遠是個初級前端
  • 只要webpack不更新到5,之後就不出webpack的文章了
  • webpack4大結局,謝謝
  • 之後會出一些偏向跨平臺技術,原生javascriptTSGolang等內容的文章
相關文章
相關標籤/搜索