9102年:手寫一個React腳手架 【優化極致版】

webpack立刻要出5了,徹底手寫一個優化後的腳手架是不可或缺的技能。
  • 本文書寫時間 2019年5月9日 , webpack版本 4.30.0最新版本
  • 本人全部代碼均手寫,親自試驗過能夠運行達到優化效果。
  • 歡迎關注個人專欄 《前端進階》 之後都是高贊高質量文章
  • 要轉載必須聯繫本人通過贊成纔可轉載 謝謝!
  • 杜絕5分鐘的技術,咱們先深刻原理再寫配置,那會簡單不少。
我這套代碼,在開發環境中性能不是完美的,可是構建速度打包生產環境代碼是 極快的,請你必定要去看個人 git倉庫,如今已經加入了 項目實踐,也在裏面,能夠的話給個 star

實現需求:

    • 識別JSX文件
    • tree shaking 搖樹優化 刪除掉無用代碼
    • 識別 async / await 和 箭頭函數
    • 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中文官網的標語是 :讓一切都變得簡單

      圖片描述

    概念:

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

    webpack v4.0.0 開始,能夠不用引入一個配置文件。然而,webpack 仍然仍是高度可配置的。在開始前你須要先理解四個核心概念:

    • 入口(entry)
    • 輸出(output)
    • loader
    • 插件(plugins)

    本文旨在給出這些概念的高度概述,同時提供具體概念的詳盡相關用例。css

    讓咱們一塊兒來複習一下最基礎的 Webpack知識,若是你是高手,那麼請直接忽略這些往下看吧....
    • 入口html

      • 入口起點`(entry point)指示 webpack 應該使用哪一個模塊,來做爲構建其內部依賴圖的開始。進入入口起點後,webpack 會找出有哪些模塊和庫是入口起點(直接和間接)依賴的。
      • 每一個依賴項隨即被處理,最後輸出到稱之爲 bundles 的文件中,咱們將在下一章節詳細討論這個過程。
      • 能夠經過在 webpack 配置中配置 entry 屬性,來指定一個入口起點(或多個入口起點)。默認值爲 ./src
      • 接下來咱們看一個 entry 配置的最簡單例子:前端

        webpack.config.js
        
        module.exports = {
          entry: './path/to/my/entry/file.js'
        };
      • 入口能夠是一個對象,也能夠是一個純數組node

        entry: {
            app: ['./src/index.js', './src/index.html'],
            vendor: ['react'] 
        },
        entry: ['./src/index.js', './src/index.html'],
      • 有人可能會說,入口怎麼放HTML文件,由於開發模式下熱更新若是不設置入口爲HTML,那麼更改了HTML文件內容,是不會刷新頁面的,須要手動刷新,因此這裏給了入口HTML文件,一個細節。
    • 出口(output)react

      • output 屬性告訴 webpack 在哪裏輸出它所建立的 bundles,以及如何命名這些文件,默認值爲 ./dist。基本上,整個應用程序結構,都會被編譯到你指定的輸出路徑的文件夾中。你能夠經過在配置中指定一個 output 字段,來配置這些處理過程:
    webpack.config.js
        
        const path = require('path');
        
        module.exports = {
          entry: './path/to/my/entry/file.js',
          output: {
            path: path.resolve(__dirname, 'dist'),
            filename: 'my-first-webpack.bundle.js'
          }
        };

    在上面的示例中,咱們經過 output.filenameoutput.path 屬性,來告訴 webpack bundle 的名稱,以及咱們想要 bundle 生成(emit)到哪裏。可能你想要了解在代碼最上面導入的 path 模塊是什麼,它是一個 Node.js 核心模塊,用於操做文件路徑。webpack

    • loadergit

      • loader 讓 webpack 可以去處理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 能夠將全部類型的文件轉換爲 webpack 可以處理的有效模塊,而後你就能夠利用 webpack 的打包能力,對它們進行處理。
      • 本質上,webpack loader 將全部類型的文件,轉換爲應用程序的依賴圖(和最終的 bundle)能夠直接引用的模塊。
      • 注意,loader 可以 import 導入任何類型的模塊(例如 .css 文件),這是 webpack 特有的功能,其餘打包程序或任務執行器的可能並不支持。咱們認爲這種語言擴展是有很必要的,由於這可使開發人員建立出更準確的依賴關係圖。
      • 在更高層面,在 webpack 的配置中 loader 有兩個目標:
      • test 屬性,用於標識出應該被對應的 loader 進行轉換的某個或某些文件。
      • use 屬性,表示進行轉換時,應該使用哪一個 loader。es6

        webpack.config.js
            
            const path = require('path');
            
            const config = {
              output: {
                filename: 'my-first-webpack.bundle.js'
              },
              module: {
                rules: [
                  { test: /\.txt$/, use: 'raw-loader' }
                ]
              }
            };
            
            module.exports = config;
      • 以上配置中,對一個單獨的 module 對象定義了 rules 屬性,裏面包含兩個必須屬性:test 和 use。這告訴 webpack 編譯器(compiler) 以下信息:
      • 「嘿,webpack 編譯器,當你碰到「在 require()/import 語句中被解析爲 '.txt' 的路徑」時,在你對它打包以前,先使用 raw-loader轉換一下。」
      • 重要的是要記得,在 webpack 配置中定義 loader 時,要定義在 module.rules 中,而不是 rules。然而,在定義錯誤時 webpack 會給出嚴重的警告。爲了使你受益於此,若是沒有按照正確方式去作,webpack 會「給出嚴重的警告」
      • loader 還有更多咱們還沒有提到的具體配置屬性。
      • 這裏引用這位做者的優質文章內容,手寫一個loaderplugin 手寫一個loader和plugin

    高潮來了 ,webpack的編譯原理 ,爲何要先學學習原理? 由於你起碼得知道你寫的是幹什麼的!

    • webpack打包原理github

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

      • loader是文件加載器,可以加載資源文件,並對這些文件進行一些處理,諸如編譯、壓縮等,最終一塊兒打包到指定的文件中
      • 處理一個文件可使用多個loaderloader的執行順序是和自己的順序是相反的,即最後一個loader最早執行,第一個loader最後執行。
      • 第一個執行的loader接收源文件內容做爲參數,其餘loader接收前一個執行的loader的返回值做爲參數。最後執行的loader會返回此模塊的JavaScript源碼
      • 在使用多個loader處理文件時,若是要修改outputPath輸出目錄,那麼請在最上面的loader中options設置
    • 什麼是plugin?

      • Webpack 運行的生命週期中會廣播出許多事件,Plugin 能夠監聽這些事件,在合適的時機經過 Webpack 提供的 API 改變輸出結果。
      • plugin和loader的區別是什麼?
      • 對於loader,它就是一個轉換器,將A文件進行編譯造成B文件,這裏操做的是文件,好比將A.scss或A.less轉變爲B.css,單純的文件轉換過程
      • plugin是一個擴展器,它豐富了wepack自己,針對是loader結束後,webpack打包的整個過程,它並不直接操做文件,而是基於事件機制工做,會監聽webpack打包過程當中的某些節點,執行普遍的任務。
    • webpack的運行

      • webpack 啓動後,在讀取配置的過程當中會先執行 new MyPlugin(options) 初始化一個 MyPlugin 得到其實例。在初始化compiler 對象後,再調用 myPlugin.apply(compiler) 給插件實例傳入 compiler 對象。插件實例在獲取到 compiler 對象後,就能夠經過 compiler.plugin(事件名稱, 回調函數) 監聽到 Webpack 廣播出來的事件。而且能夠經過 compiler 對象去操做 webpack
      • 看到這裏可能會問compiler是啥,compilation又是啥?
      • Compiler 對象包含了 Webpack 環境全部的的配置信息,包含 options,loaders,plugins 這些信息,這個對象在 Webpack 啓動時候被實例化,它是全局惟一的,能夠簡單地把它理解爲 Webpack 實例;
      • Compilation 對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當 Webpack 以開發模式運行時,每當檢測到一個文件變化,一次新的 Compilation 將被建立。Compilation 對象也提供了不少事件回調供插件作擴展。經過 Compilation 也能讀取到 Compiler 對象。
      • CompilerCompilation 的區別在於:
      • Compiler 表明了整個 Webpack 從啓動到關閉的生命週期,而 Compilation 只是表明了一次新的編譯。
    • 事件流

      • webpack 經過 Tapable 來組織這條複雜的生產線。
      • webpack 的事件流機制保證了插件的有序性,使得整個系統擴展性很好。
      • webpack的事件流機制應用了觀察者模式,和 Node.js 中的 EventEmitter 很是類似。

    下面正式開始開發環境的配置:

    • 入口設置 :

      • 設置APP,幾個入口文件,即會最終分割成幾個chunk
      • 在入口中配置 vendor,能夠code spliting ,將這些公共的複用代碼最終抽取成一個chunk,單獨打包出來
      • 要想在開發模式中HMTL文件也熱更新,須要加入·index.html爲入口文件
    entry: {
                app: ['./src/index.js', './src/index.html'],
                vendor: ['react']  //這裏還能夠加入redux react-redux better-scroll等公共代碼 
            },
    • output出口

      • webpack基於Node.js環境運行,可使用Node.jsAPIpath模塊的resolve方法
      • 對輸出的JS文件,加入contenthash標示,讓瀏覽器緩存文件,區別版本。
    output: {
                filename: '[name].[contenthash:8].js',
                path: resolve(__dirname, '../dist')
            },
    • mode: 'development' 模式選擇,這裏直接設置成開發模式,先從開發模式開始。
    • resolve解析配置,爲了爲了給全部文件後綴省掉 js jsx json,加入配置

      resolve: {
          extensions: [".js", ".json", ".jsx"]
      }
    • 加入插件 熱更新pluginhtml-webpack-plugin

      const HtmlWebpackPlugin = require('html-webpack-plugin')
         const webpack = require('webpack')
         new HtmlWebpackPlugin({
                 template: './src/index.html'
             }),
         new webpack.HotModuleReplacementPlugin(),
    • 加入代碼分割,開發模式也須要代碼分割,性能優化
    optimization: {
            runtimeChunk: true,
            splitChunks: {
                chunks: 'all'
            }
        }
    • 加入 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
                                },
                            }
    • 加入thread-loader,在babel首次編譯後開啓多線程
    const os = require('os')
        {
                loader: 'thread-loader',
                options: {
                    workers: os.cpus().length   
                         }
        }
    • 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'
        }
    • 加入eslint-loader
    {
            enforce:'pre',
            test:/\.js$/,
            exclude:/node_modules/,
            include:resolve(__dirname,'/src/js'),
            loader:'eslint-loader'
            }
    • 開發模式結束 代碼在下面的git倉庫裏

    必須瞭解的webpack熱更新原理 :

    1620

    • webpack的熱更新又稱熱替換(Hot Module Replacement),縮寫爲HMR。 這個機制能夠作到不用刷新瀏覽器而將新變動的模塊替換掉舊的模塊。

    • 首先要知道server端和client端都作了處理工做

      • 第一步,在 webpack 的 watch 模式下,文件系統中某一個文件發生修改,webpack 監聽到文件變化,根據配置文件對模塊從新編譯打包,並將打包後的代碼經過簡單的 JavaScript 對象保存在內存中。
      • 第二步是 webpack-dev-server webpack 之間的接口交互,而在這一步,主要是 dev-server 的中間件 webpack-dev-middleware 和 webpack 之間的交互,webpack-dev-middleware 調用 webpack 暴露的 API對代碼變化進行監控,而且告訴 webpack,將代碼打包到內存中。
      • 第三步是 webpack-dev-server 對文件變化的一個監控,這一步不一樣於第一步,並非監控代碼變化從新打包。當咱們在配置文件中配置了devServer.watchContentBase 爲 true 的時候,Server 會監聽這些配置文件夾中靜態文件的變化,變化後會通知瀏覽器端對應用進行 live reload。注意,這兒是瀏覽器刷新,和 HMR 是兩個概念。
      • 第四步也是 webpack-dev-server 代碼的工做,該步驟主要是經過 sockjs(webpack-dev-server 的依賴)在瀏覽器端和服務端之間創建一個 websocket 長鏈接,將 webpack 編譯打包的各個階段的狀態信息告知瀏覽器端,同時也包括第三步中 Server 監聽靜態文件變化的信息。瀏覽器端根據這些 socket 消息進行不一樣的操做。固然服務端傳遞的最主要信息仍是新模塊的 hash 值,後面的步驟根據這一 hash 值來進行模塊熱替換。
      • webpack-dev-server/client 端並不可以請求更新的代碼,也不會執行熱更模塊操做,而把這些工做又交回給了 webpack,webpack/hot/dev-server 的工做就是根據 webpack-dev-server/client 傳給它的信息以及 dev-server 的配置決定是刷新瀏覽器呢仍是進行模塊熱更新。固然若是僅僅是刷新瀏覽器,也就沒有後面那些步驟了。
      • HotModuleReplacement.runtime 是客戶端 HMR 的中樞,它接收到上一步傳遞給他的新模塊的 hash 值,它經過 JsonpMainTemplate.runtime 向 server 端發送 Ajax 請求,服務端返回一個 json,該 json 包含了全部要更新的模塊的 hash 值,獲取到更新列表後,該模塊再次經過 jsonp 請求,獲取到最新的模塊代碼。這就是上圖中 七、八、9 步驟。
      • 而第 10 步是決定 HMR 成功與否的關鍵步驟,在該步驟中,HotModulePlugin 將會對新舊模塊進行對比,決定是否更新模塊,在決定更新模塊後,檢查模塊之間的依賴關係,更新模塊的同時更新模塊間的依賴引用。
      • 最後一步,當 HMR 失敗後,回退到 live reload 操做,也就是進行瀏覽器刷新來獲取最新打包代碼。
      • 參考文章 webpack面試題-騰訊雲

    正式開始生產環節:

    • 加入 WorkboxPluginPWA的插件

      • 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/],
                }),
    • 加入每次打包輸出文件清空上次打包文件的插件
    const CleanWebpackPlugin = require('clean-webpack-plugin')
        
        new CleanWebpackPlugin()
    • 加入code spliting代碼分割
    optimization: {
                runtimeChunk:true,  //設置爲 true, 一個chunk打包後就是一個文件,一個chunk對應`一些js css 圖片`等
                splitChunks: {
                    chunks: 'all'  // 默認 entry 的 chunk 不會被拆分, 配置成 all, 就能夠了拆分了,一個入口`JS`,
                    //打包後就生成一個單獨的文件
                }
            }
    • 加入單獨抽取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} }]
                    }
                }),
    • 殺掉html一些沒用的代碼
    new HtmlWebpackPlugin({
            template: './src/index.html',
            minify: {
                removeComments: true,  
                collapseWhitespace: true,  
                removeRedundantAttributes: true,
                useShortDoctype: true, 
                removeEmptyAttributes: true,
                removeStyleLinkTypeAttributes: true,
                keepClosingSlash: true, 
                minifyJS: true,
                minifyCSS: true, 
                minifyURLs: true, 
             }
    }),
    • 加入圖片壓縮
    {
                    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 }
                                  ]
                                })
                              ]
                            }
                          }
                    ]
                    
                    
    
                }
    • 加入file-loader 把一些文件打包輸出到固定的目錄下
    {
                    exclude: /\.(js|json|less|css|jsx)$/,
                    loader: 'file-loader',
                    options: {
                        outputPath: 'media/',
                        name: '[name].[contenthash:8].[ext]'
                    }
                }
    裏面有一些註釋可能不詳細,代碼都是本身一點點寫,試過的,確定沒用任何問題
    • 須要的依賴
    {
        "name": "webpack",
        "version": "1.0.0",
        "main": "index.js",
        "license": "MIT",
        "dependencies": {
            "@babel/core": "^7.4.4",
            "@babel/preset-env": "^7.4.4",
            "@babel/preset-react": "^7.0.0",
            "autoprefixer": "^9.5.1",
            "babel-loader": "^8.0.5",
            "clean-webpack-plugin": "^2.0.2",
            "css-loader": "^2.1.1",
            "eslint": "^5.16.0",
            "eslint-loader": "^2.1.2",
            "file-loader": "^3.0.1",
            "html-loader": "^0.5.5",
            "html-webpack-plugin": "^3.2.0",
            "imagemin": "^6.1.0",
            "imagemin-gifsicle": "^6.0.1",
            "imagemin-mozjpeg": "^8.0.0",
            "imagemin-pngquant": "^7.0.0",
            "imagemin-svgo": "^7.0.0",
            "img-loader": "^3.0.1",
            "less": "^3.9.0",
            "less-loader": "^5.0.0",
            "mini-css-extract-plugin": "^0.6.0",
            "optimize-css-assets-webpack-plugin": "^5.0.1",
            "postcss-loader": "^3.0.0",
            "react": "^16.8.6",
            "react-dom": "^16.8.6",
            "react-loadable": "^5.5.0",
            "react-redux": "^7.0.3",
            "style-loader": "^0.23.1",
            "url-loader": "^1.1.2",
            "webpack": "^4.30.0",
            "webpack-cli": "^3.3.2",
            "webpack-dev-server": "^3.3.1",
            "workbox-webpack-plugin": "^4.3.1"
        },
        "scripts": {
            "start": "webpack-dev-server --config ./config/webpack.dev.js",
            "dev": "webpack-dev-server --config ./config/webpack.dev.js",
            "build": "webpack  --config  ./config/webpack.prod.js "
        },
        "devDependencies": {
            "@babel/plugin-syntax-dynamic-import": "^7.2.0"
        }
    }

    整個項目和webpack配置的源碼地址 已經更新 : 源碼地址啊 看得見嗎親

    路過的小夥伴麻煩點個贊給個star,寫得好辛苦啊!!!!

    相關文章
    相關標籤/搜索