9102年:手寫一個React腳手架 【已優化至完美版】

Jerry譚金傑發表於2019-05-10
webpack馬上要出5了,完全手寫一個優化後的腳手架是不可或缺的技能。
  • 本文書寫時間 2019年5月9日 , webpack版本 4.30.0最新版本
  • 本人所有程式碼均手寫,親自試驗過可以執行達到優化效果。
  • 歡迎關注我的專欄 《前端進階》 以後都是高贊高質量文章
  • 要轉載必須聯絡本人經過同意才可轉載 謝謝!
  • 杜絕5分鐘的技術,我們先深入原理再寫配置,那會簡單很多。

實現需求:

    • 識別JSX檔案
    • tree shaking 搖樹優化 刪除掉無用程式碼
    • PWA功能,熱重新整理,安裝後立即接管瀏覽器 離線後仍讓可以訪問網站 還可以在手機上新增網站到桌面使用
    • CSS模組化,不怕命名衝突
    • 小圖片的base64處理
    • 檔案字尾省掉jsx js json
    • 實現React懶載入,按需載入 , 程式碼分割 並且支援服務端渲染
    • 支援less sass stylus等預處理
    • code spliting 優化首屏載入時間 不讓一個檔案體積過大
    • 提取公共程式碼,打包成一個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)

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

    讓我們一起來複習一下最基礎的Webpack知識,如果你是高手,那麼請直接忽略這些往下看吧....
    • 入口

      • 入口起點`(entry point)指示 webpack 應該使用哪個模組,來作為構建其內部依賴圖的開始。進入入口起點後,webpack 會找出有哪些模組和庫是入口起點(直接和間接)依賴的。
      • 每個依賴項隨即被處理,最後輸出到稱之為 bundles 的檔案中,我們將在下一章節詳細討論這個過程。
      • 可以通過在 webpack 配置中配置 entry 屬性,來指定一個入口起點(或多個入口起點)。預設值為 ./src
      • 接下來我們看一個 entry 配置的最簡單例子:

        webpack.config.js
        
        module.exports = {
          entry: './path/to/my/entry/file.js'
        };
      • 入口可以是一個物件,也可以是一個純陣列

        entry: {
            app: ['./src/index.js', './src/index.html'],
            vendor: ['react'] 
        },
        entry: ['./src/index.js', './src/index.html'],
      • 有人可能會說,入口怎麼放HTML檔案,因為開發模式下熱更新如果不設定入口為HTML,那麼更改了HTML檔案內容,是不會重新整理頁面的,需要手動重新整理,所以這裡給了入口HTML檔案,一個細節。
    • 出口(output)

      • 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 核心模組,用於操作檔案路徑。

    • loader

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

            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打包原理

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

      • 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(),
    • 加入 babel-loader 還有 解析JSX ES6語法的 babel preset

      • @babel/preset-react解析 jsx語法
      • @babel/preset-env解析es6語法
      • @babel/plugin-syntax-dynamic-import解析react-loadableimport按需載入,附帶code spliting功能
        {
            test: /\.(js|jsx)$/,
            use:
            {
                loader: 'babel-loader',
                options: {
                    presets: ["@babel/preset-react", ["@babel/preset-env", { "modules": false }]],
                    plugins: ["@babel/plugin-syntax-dynamic-import"]
                         },
                    cacheDirectory: true//開啟babel編譯快取
            }
       },
      
    • 加入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 請求,獲取到最新的模組程式碼。這就是上圖中 7、8、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,寫得好辛苦啊!!!!

    相關文章