webpack4技術棧學習筆記

一、webpack

  • 如何初始化環境?
  1. 安裝node.js
  2. 創建個項目文件夾
  3. npm init -y(一路yes)
  4. npm install webpack webpack-cli -D(等於–save-dev)
  5. 跳轉package.json,刪除main防止發佈代碼,增加private字段
    {
    "name": "webpack-demo",
    "version": "1.0.0", 
    "description": "",
    "private": true,
    "main": "index.js", 
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "devDependencies": {
      "webpack": "^4.0.1",
      "webpack-cli": "^2.0.9"
    },
    "dependencies": {}
  }
  • 目錄說明?
  1. scr放置項目文件
  2. dist放置打包後的文件和index.html,方便查找
  • webpack.config.js配置文件說明?
  1. 在配置文件中註釋
  • 引入方式
  1. ES module 模塊引入方式
    export defalut Header;
    import Header from ‘./header.js’;
    new Header();
  2. CommonJS 模塊引入方式
    modele.exports = Header
    var Header = require(’./header.js’);
    new Header();
  3. CMD
  4. ADM
  • 爲啥沒有webpack.config.js配置文件,一樣能夠進行打包?
    因爲webpack開發者致力於智能打包,儘量簡化開發者的操作,所以是有個默認的配置文件,如果沒有寫配置文件的話。

  • webpack安裝方式

  1. global:npm install webpack webpack-cli ——> webpack index.js
    劣勢:每個項目的依賴可能不同,可能會有干擾影響。例如安裝過新的webpack,A項目的依賴版本較低項目運行可能會有問題等
  2. local:npm install webpack webpack-cli -D ——> npx webpack index.js
    優勢:項目內安裝,每個項目針對安裝
  3. npm script:npm run bundle ——>webpack(package.json)
    優勢:對命令的的操作一目瞭然,容易使用和他人觀看。且script啓動方式會從項目的node包中尋找,所以不用npx。
    "scripts": {
+     "bundle": "webpack"
    }
  • webpack和webpack-cli區別在哪?爲什麼還需要webpack-cli?
  1. webpack-cli作用是使我們能夠在命令行中使用webpack這個命令,如果沒有安裝webpack-cli包,就不能使用webpack和npx命令。作用是使得我們能夠使用命令行運行。

二、loader

  • loader是什麼?
    一個打包的方案。webpack不知道的模塊(文件)則求助loader。
    是webpack打包時使用的類似做好的配置文件包或者模塊。
    例如webpack默認打包的文件爲.js文件,不包括.jpg文件。那麼可以在webpack.config.js裏面配置打包的各種規則,規則指定打包遇到未知後綴時再規則下使用指定.後綴文件所使用的包。
    module:{
        rules:[{
            test:/\.jpg$/,
            use:{
                loader:'file-loader'
            }
        }]
    },

loader:(1)將靜態文件移動到配置文件指定的目錄和重命名(2)將名字放入打包後的.js文件的靜態文件所指的變量名中。

  • 既然知道loader爲打包專用配置文件模塊或包,那麼怎麼根據自己的需求去找到某個loader以打包某後綴的文件?
    (1)去webpack官網的loader裏面查找
    (2)例如:.vue文件如何找到對應的loader。vue-loader
    (3)查看對應的文檔:https://www.webpackjs.com/loaders/file-loader/
    module:{
        rules:[{
            test:/\.vue$/,
            use:{
                loader:'vue-loader'
            }
        }]
    },
  • loader配置,例如靜態文件的打包好的名字配置等
    module:{
        rules:[{
            /* 如果打包遇到.jpg文件,則去使用某個(例file-loader)loader打包。*/
            /* file-loader處理.jpg打包的流程,將.jpg複製到list文件夾中,名字可自定義或隨機,然後打包後的.js文件直接引用該.jpg文件。所以該文件只是將靜態文件進行移動和重命名 */
            test:/\.jpg|png|gif$/,
            use:{
                loader:'file-loader',
                options:{
                    //placeholder 佔位符:[]
                    //打包後的文件名及後綴不變
                    name:'[name]_[hash].[ext]',
                    //該後綴資源打包目錄所在
                    outputPath:'images/'
                }
            }
        }]
    },
  • url-loader可以完成file-loader的一切功能。但是多了個limit配置項,該項能使某靜態資源小於limit參數值時,直接轉換爲base64字符串打包進js文件,避免過多http請求
    1、同時靜態文件直接轉換成base64的內容,直接將類似圖片打包到.js文件中。省了一次HTTP請求。
    不過這樣不合理,帶來的問題是如果靜態資源很大,則JS文件也很大,也就是說很長的時候裏頁面什麼都顯示不出來。所以應用只在圖片特別小的情況下才使用這種打包方式。
    解決:使用limit參數,分情況打包,如果圖片超過該參數則像file-loader一樣打包到指定目錄下,否則直接加載入.js文件
    module:{
        rules:[{
            /* 如果打包遇到.jpg文件,則去使用某個(例file-loader)loader打包。*/
            /* file-loader處理.jpg打包的流程,將.jpg複製到list文件夾中,名字可自定義或隨機,然後打包後的.js文件直接引用該.jpg文件。所以該文件只是將靜態文件進行移動和重命名 */
            test:/\.jpg|png|gif$/,
            use:{
                loader:'url-loader',
                options:{
                    //placeholder 佔位符:[]
                    //打包後的文件名及後綴不變
                    //name:'[name]_[hash].[ext]',
                    name(mode){
                      if(mode === 'development'){
                          return '[path][name].[ext]'
                      }
                      return '[hash].[ext]'
                    },
                    //該後綴資源打包目錄所在
                    outputPath:'images/',
                    //當靜態改後綴名文件大小不超過2048字節,即2kb,則直接轉換爲base64位字符串打包入js文件
                    limit:2048,
                    //如果靜態資源例如圖片使用了服務端的資源可以在打包時禁止操作靜態資源,而只在打包後的js文件中使用路徑及文件名,默認是true
                    emitFile:true,
                }
            }
        }]
    },

三、使用loader打包靜態資源中樣式CSS文件

  • 如何打包.css文件?
    1、設置規則use使用兩個loader,分別是style-loader和css-loader
    style-loader:在得到css-loader生成的內容後,style-loader會把內容掛載到頁面的head部分

  • 如何打包.sass(包括scss)等文件?
    需要使用scss相關的loader,以解決scss相關的語法做編譯。
    1、根據官網,同樣設置use使用兩個loader,分別是sass-loader和node-sass。use引入時再css的基礎上增加sass-loader
    2、use使用loader是有先後順序的,從下到上,從右到左。
    3、自動添加廠商前綴功能的loader,例如transform會根據瀏覽器添加前綴,避免樣式文件繁重的書寫。該loader爲postcss-loader。該loader需要進行配置postcss.config.js。該配置文件還使用了autoprefixer插件自動配置postcss.config.js

    -webkit-transform: rotate(315deg);
    -moz-transform: rotate(315deg);
    -ms-transform: rotate(315deg);
    -o-transform: rotate(315deg);
    transform: rotate(315deg);
    npm install -D autoprefixer
    //postcss.config.js
    module.exports = {
    plugins: [
        require('autoprefixer')({
            "browsers": [
                "defaults",
                "not ie < 11",
                "last 2 versions",
                "> 1%",
                "iOS 7",
                "last 3 iOS versions"
            ]
        })
    ]
    }

        {
            test:/\.scss$/,
            use:[
                'style-loader',
                'css-loader',
                'sass-loader',
                'postcss-loader'
            ]
        }

四、CSS的模塊化

引入css文件時容易引起樣式衝突問題,引入css模塊化的概念。
即import style from ‘index.scss’,使用變量名style.x的形式使用

use: [
    'style-loader',
    {
        loader: 'css-loader',
        options: {
            //無論js還是scss找那個引入scss文件,都會從下到上依次執行多2個loader
            //importLoaders,例如sass文件中再引用sass文件,那麼爲了讓該文件再次從postcss-loader開始往上執行,需要設置該參數表名引入前需要走x個Loader
            importLoaders: 2,
            //如果不是模塊化CSS則去掉
            //css模塊化參數,使得css文件不是全局作用,不會相互影響,同時css的引用發送也需要做變更。
            //modules:true
        }
    },
    'sass-loader',
    'postcss-loader'
]

五、使用webpack打包字體文件

iconfont字體庫使用流程
https://www.iconfont.cn/collections/index?spm=a313x.7781069.1998910419.da2e3581b&type=1
1、創建項目,添加購物車,下載
2、將字體文件拉到項目src目錄的font文件下
3、將下載的css文件的代碼複製到項目的css文件中
4、引入iconfont + icon-changjingguanli(字體名)
5、webpack安裝file-loader,書寫對應.後綴的規則。或者使用url-loader直接打包入js文件

,{
            test:/\.(eot|ttf|svg|woff)$/,
            use:{
                loader:'file-loader',
            }
        }

6、打包

六、plugins插件

  • 例如html-webpack-plugin插件:
    https://www.webpackjs.com/plugins/html-webpack-plugin/
    每次打包dist目錄下都沒有index.html,都需要創建。而使用該插件將在打包結束時在dist目錄下生成一個HTML5文件,並把打包生成的js自動引入到Html文件。其中包括使用script標籤的body中的所有webpack包。
    1、在webpack配置文件中引入插件
    2、實例化插件,並配置模板html
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    plugins: [new HtmlWebpackPlugin({
        template: "src/index.html"
    })],
  • 例如第三方插件:clean-webpack-plugin插件
    每次打包時都會生成根據webpack配置的名字的文件,那麼之前打包的其他名字文件就會因爲不重合而沒有被覆蓋。則使用該插件再打包前對多餘文件進行刪除。
    1、在webpack配置文件中引入插件
    2、實例化插件
    const {CleanWebpackPlugin} = require('clean-webpack-plugin')
    plugins: [
        //打包完成前刪除多餘文件
        new CleanWebpackPlugin(),
    ],

七、多js文件輸入輸出打包

    //入口文件,打包多文件,需要對應[name]輸出
    entry: {
        main: './src/index.js',
        sub: './src/index.js'
    },
    //輸出目錄及文件名,__dirname當前目錄。[name]佔位符,對應這entry的輸入文件.publicPath爲前面目錄或者鏈接名
    output: {
        //publicPath: "http://cdn.com.cn",
        filename: '[name].[hash].js',
        path: path.resolve(__dirname, 'dist')
    },

八、SourceMap的配置

https://www.webpackjs.com/configuration/devtool/
sourceMap是一個映射關係。例如dist目錄下main.js文件96行出錯,它知道dist目錄下main.js文件96實際對應的是src目錄下index.js文件中的第一行
當前其實是index.js中第一行代碼出錯了
1、默認是sourceMap。devtool:"none"則爲打包後的代碼

    //devtool指定輸出目錄,none指定爲打包後映射目錄,而sourceMap則是打包前的映射目錄,inline-source-map的map文件會變成base64放入js文件中,inline精確到行和列。cheap-inline-source-map,cheap只精確到行/cheap-inline-source-map。cheap-module-inline-source-map,module,還顯示第三方包的錯誤代碼。eval直接在打包後的js文件後輸出錯誤信息。
    //線上推薦
    //devtool: "cheap-module-source-map",
    //線下選擇推薦
    //devtool: "cheap-module-eval-source-map",
    //入口文件,打包多文件,需要對應[name]輸出

九、使用webpackDevServer提高開發效率

webpack --watch模式
webpack會監聽打包的文件,只要打包的文件發生變化則會重新打包。
webpackDevServer:比–watch好在不但會監聽文件的更新打包,還會刷新瀏覽器。同時普通的webpack沒有服務器開啓方式,url爲文件目錄。無法發起ajax請求,webpackDevServer則爲服務器連接,可以發起ajax。同時可以配置代理,防止前後臺交互跨域問題。
類似react的腳手架,大部分情況下腳手架使用的就是該服務器。
1、安裝npm install -D webpack-dev-server
2、配置

    devServer: {
        //webpackDevServer服務器啓在哪個文件夾下
        contentBase:'./dist',
        //自動打開瀏覽器和服務器地址
        open:true,
        //代理
        proxy: {
            //請求到/api/users現在會被代理請求到http://localhost:3000/api/users
            "/api": "http://localhost:3000"
        },
        port:8080,
    },

十、配置個類似webpack-dev-server服務器

  1. 安裝express開發框架 和webpack開發中間件
    npm install express webpack-dev-middleware -D
    2、配置script腳本啓動
    3、服務器編寫,以下功能和webpack --watch一樣
"scripts": {
    "server": "node server.js"
  },

server.js
const express = require('express');
//引入webpack庫
const webpack = require('webpack');
//webpack中間件
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');
//webpack的編譯器,complier使用webpack結合配置文件,可以隨時進行代碼的編譯。complier編譯器
const complier = webpack(config)

//創建一個服務器
const app = express();
//使用編譯器,只要文件發生改變,就會重新運行。文件打包的路徑替換webpack.config.js配置。
app.use(webpackDevMiddleware(complier,{
    publicPath:config.output.publicPath
}))

//指定端口和回調函數,啓動成功時執行
app.listen(3000, () => {
    console.log('server is running');
});

十一、熱模塊替換

webpack-dev-server打包後的文件並不會放到dist目錄下,而是放到電腦內存中。

應用場景:例如當只變更了css樣式時,整個頁面都刷新了。那麼可以通過熱模塊替換隻刷新css文件即可。

1、配置devserver

devServer: {
        //webpackDevServer服務器啓在哪個文件夾下
        contentBase:path.join(__dirname,'dist'),
        //自動打開瀏覽器和服務器地址
        open:true,
        //代理
        proxy: {
            //請求到/api/users現在會被代理請求到http://localhost:3000/api/users
            "/api": "http://localhost:3000"
        },
        port:8080,
        //開啓熱模塊替換功能
        hot:true,
        //即使熱模塊替換功能沒有生效,也不讓頁面刷新
        hotOnly:true
    },

2、引入和啓用webpack的熱模塊替換插件

    const webpack = require('webpack')
    new webpack.HotModuleReplacementPlugin()

3、重啓devserver

應用場景2:js文件只改變了一部分,但是所有js模塊都刷新了,那麼可以通過熱模塊替換隻刷新部分js文件。

在應用場景1的情況下,在index.js加多段文件變更判斷代碼(例如css-loader底層內置了下面這段代碼,所以不用手寫)。

if (module.hot) {
    //開啓了HRM,支持hot加載。如果文件發生了變化,就會執行下面的函數
    //1、依賴文件的名字2、執行後面的函數
    module.hot.accept('./number', () => {
        document.body.removeChild(document.getElementById('number'))
        number()
    })
}

十二、babel

之所以谷歌瀏覽器能識別ES6的語法是因爲谷歌瀏覽器與時俱進。Babel將ES6的語法轉換爲ES5,使得瀏覽器能夠識別。

  • 如果是js文件則使用babel-loader與babel進行通信的橋樑,exclude排除了node_modules庫中的js文件。babel-loader並不會ES6的語法轉換爲ES5,需要使用其他入@babel/preset-env -save-dev進行語法轉換。
  1. 安裝babel-loader通信配置包和核心庫
    npm install -D babel-loader @babel/core
  2. 配置babel-loader與babel通信的橋樑
  3. 安裝preset-env,該模塊包含所有ES6轉換成ES5的翻譯規則。npm install -D @babel/preset-env
  4. 配置babel翻譯工具preset-env
  • 但是有些低版本的瀏覽器,就算轉換到ES5依然不夠。還需要把缺少的變量或者函數進行補充。
  1. 安裝polyfill
    npm install -D @babel/polyfill
    6、在業務代碼的最頂部引入polyfill
    在babel配置中配置了useBuiltIns: ‘usage’,那麼在業務代碼就不需要引入polyfill。
    import “@babel/polyfill”;
  • 但是直接引入該庫會使得最後打包的js文件變的很大,例如原先二十多kb直接變成八百多kb。
{
            //如果是js文件則使用babel-loader與babel進行通信的橋樑,exclude排除了node_modules庫中的js文件。babel-loader並不會ES6的語法轉換爲ES5,需要使用其他入@babel/preset-env -save-dev進行語法轉換
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            options:{
                //配置babel的ES6轉ES5的翻譯工具preset-env
                presets:[["@babel/preset-env",{
                    targets: {
                        //對於大於67谷歌瀏覽器版本則不需要進行ES6的語義轉換和語法補充。
                        chrome: "67",
                    },
                    //當做頁面做babel的polyfill低版本瀏覽器不存在的版本特性時(低於ES5),不把所有特性都加入,而是根據業務代碼加入特性
                    useBuiltIns:'usage'
                }]]
            }
        }
  • 又但是import polyfill爲全局使用,可能會污染到使用的UI組件庫
  1. 安裝plugin-transform-runtime,該庫會以閉包的形式去引入對應的內容,不存在全局污染的狀況。
    可創建放入.babelrc文件中
    npm install -D @babel/plugin-transform-runtime
    npm install -D @babel/runtime
     {
        //如果是js文件則使用babel-loader與babel進行通信的橋樑,exclude排除了node_modules庫中的js文件。babel-loader並不會ES6的語法轉換爲ES5,需要使用其他入@babel/preset-env -save-dev進行語法轉換
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options:{
            // //配置babel的ES6轉ES5的翻譯工具preset-env
            // presets:[["@babel/preset-env",{
            //     targets: {
            //         //對於大於67谷歌瀏覽器版本則不需要進行ES6的語義轉換和語法補充。
            //         chrome: "67",
            //     },
            //     //當做頁面做babel的polyfill低版本瀏覽器不存在的版本特性時(低於ES5),不把所有特性都加入,而是根據業務代碼加入特性
            //     useBuiltIns:'usage'
            // }]]
            "plugins": [["@babel/plugin-transform-runtime",{
                "absoluteRuntime": false,
                "corejs": 2,
                "helpers": true,
                "regenerator": true,
                "useESModules": false
            }]]
        }
  • 如何根據編譯需求場景尋找適合的babel,或流程回顧
  1. 進入https://www.babeljs.cn/setup,選擇使用場景
  2. 按照需求安裝,全局直接注入
  3. 簡化,按需注入,和根據瀏覽器版本注入。適合業務代碼場景
  4. 閉包按需(corejs2)注入,使用plugin-transform-runtime包。適合寫庫場景

十三、配置React代碼的打包

使用 preset-react解析jsx的語法
npm install --save-dev @babel/preset-react
安裝react框架
npm install --save react react-dom

//先將react語法轉換爲ES6,再替換成ES5。自下向上,自右向左
.babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "chrome": "67"
        },
        "useBuiltIns": "usage"
      }
    ],
    "@babel/preset-react"
  ]
}
import React,{Component} from 'react';
import ReactDom from 'react-dom';

class App extends Component{
    render(){
        return <div>Hello World!</div>
    }
}

ReactDom.render(<App/>, document.getElementById('root'));

十四、Tree Shaking

在babel配置中配置了useBuiltIns: ‘usage’,那麼在業務代碼就不需要引入polyfill。
babel 綁定一些對象,如window.Promise。沒有導出任何內容

當使用{add}引入時其他的方法也會被引入,最理想的應該是隻引入要使用的方法。

  • 什麼是Tree Sharking?
    引入的每個方法都爲一個樹節點,顧名思義將不需要的樹節點搖晃(不打包)掉。只支持ESModule的引入!
    ESModule底層是靜態的引入方式,而CommonJS則是靜態引入方式。
    1、查看哪些導出的模塊被使用了再做打包
    webpack.config.js
    optimization: {
    usedExports: true
    }
    2、如果配置了Tree Sharking,則打包模塊時會使用Tree Shaking方式打包。
    package.json
    //“sideEffects”:["@babel/poly-fill"]//不希望對poll-fill文件進行Tree Shaking
    “sideEffects”:false//但polyfill不需要引入import,所以當前沒有不需要Tree Sharking的文件
    “sideEffects:[”*.css"]"//import的css文件沒有導出任何內容,Tree Sharking可能會把該文件忽略掉,這樣代碼也可能有問題
    import ‘./style.css’
    3、當是開發環境模式下只是提醒依然會打包入文件,因爲去除未使用的模塊會影響source.map的行數
    4、當是生產環境時則Tree Shaking則會生效,且不需要配置1、
    5、devtool: “cheap-module-source-map”,不帶eval

十五、develoment和production模式的區分打包

1、development環境的source.map信息非常全,幫助快速定位代碼問題
2、production環境的代碼一般都是壓縮過的

  • 改mode爲production
  • devtool:‘cheap-module-source-map’
  • optimization去掉
    這樣會產生一個問題,開發和生成環境切換麻煩?
    1、使用兩個配置文件
    webpack.dev.js
    webpack.prod.js
    2、pagage.json使用兩個script腳本
    3、簡化代碼,使用webpack.common.js(自定義)取出共同部分
    4、npm install webpack-merge -D,安裝合併配置文件合併插件
    5、使用該插件進行合併
    對應配置文件夾下:
    module.exports = merge(commonConfig,prodConfig)
    module.exports = merge(commonConfig,devConfig)
    6、目錄變更
  • output目錄
  • 插件處理目錄

十六、Webpack和Code Splitting

Code Splitting爲代碼分割,對代碼進行拆分,讓代碼執行的性能更高。一種思想或者說是方法。

手動分割

1、安裝loadsh,爲功能方法集合
npm install loadsh --save
2、import _ from ‘lodash’,分文件存放,一個是庫入口文件,另一個是業務邏輯代碼文件

  • 問題:業務代碼多次引用使用。例如loadsh 的大小是1mb,業務邏輯是1mb。最後打包的代碼是2mb!
    (1)打包文件過大(2)加載時間長(3)如果js文件出現變更又需要加載2mb代碼
  • 解決:將引入庫文件與業務代碼文件分開放。修改業務代碼後,瀏覽器刷新只需要重新加載業務代碼。
lodash.js
import _ from 'loadsh';
window._ = _;

3、入口文件增加庫文件

entry: {
        main: './src/index.js',
        lodash:'./src/lodash.js'
    },

插件代碼分割,splitChunk,該插件已與webpack捆綁。

該打包插件同樣將lodash單獨提取出來另建JS文件

    optimization:{
        splitChunks:{
            chunks:'all'
        }
    }

同步執行順序:先引入庫,再調用庫裏面的方法。splitChunks配置參數同步代碼做代碼分割。
異步執行順序:例如以下,一開始沒有該庫代碼,通過異步的方法加載,引入的庫會放入_變量中,加載完成後then方法會執行。調用時相當於promise直接使用。該方法即使不配置splitChunks也會自動進行代碼分割。

  • 異步加載組件有種語法叫魔法註釋
    (1)安裝官方插件npm install -D @babel/plugin-syntax-dynamic-import
    (2)babel配置 plugins:["@babel/plugin-syntax-dynamic-import"]
    (3)使用魔法註釋,給引入的庫起一個ChunkName,該name爲單獨打包生成的文件
function getComponent() {
    return import(/* webpackChunkName:"lodash" */'lodash').then(({default:_})=>{
        var element = document.createElement('div');
        element.innerHTML = _.join(['Dell','Lee'],'-')
        return element;
    })
}

getComponent().then(element=>{
    document.body.appendChild(element)
})
  • 如果出現實驗性語法錯誤,我測試後發現不使用該插件反而沒錯誤。
    (1)安裝插件
    npm install babel-plugin-dynamic-import
    -webpack
    (2).babelrc配置plugins: [“dynamic-import-webpack”]

splitChunk底層使用的是SplitChunksPlugin插件

無論同步執行的代碼分割還是異步執行的代碼分割,都需要使用該插件進行配置,即使使用魔法註釋,也會被配置所影響。
https://webpack.docschina.org/plugins/split-chunks-plugin/

  • 如果沒有進行任何SplitChunksPlugin配置,實際會有一個默認的配置內容,即以下配置。和{}是一樣的
optimization:{
    minimize: true,
    splitChunks: {
        chunks: 'all',//all分割同步及異步代碼,同時受cacheGroup配置影響。initial同步,async異步
        minSize: 30000,//當文件不小於多少字節時進行代碼分割,30kb。爲0沒反應是因爲cacheGroups的走向配置
        maxSize: 0,//可配可不配。對超過該值的js文件進行二次分割,且代碼可分割纔行
        minChunks: 1,//當一個模塊被用了多少次才進行代碼分割
        maxAsyncRequests: 5,//同時加載的模塊式數最多個數
        maxInitialRequests: 3,//整個網站首頁或者入口文件進行加載的時候,入口文件可能會引入其他的JS文件,入口文件引入的文件如果進行做代碼分割,最多3個
        automaticNameDelimiter: '~',//cacheGroups組和文件進行文件名連接時中間的連接符
        name: true,//cacheGroups 命名的filename有效true
        cacheGroups: {
            vendors: {//同步加載
                test: /[\\/]node_modules[\\/]/,//如果爲該文件目錄下則進行分割
                priority: -10,//緩存組優先級別
                filename: "vendors.js"//更改引入分割部分文件名
            },
            default: {//default爲所模塊都符合該要求,沒有test指定。那麼就看優先級
                priority: -20,
                reuseExistingChunk: true,//A和B在第一層被引用,但同時第二次B模塊也引入了A。則兩個代碼都打包到該組的文件中。但代碼引入存在複用則實際只打包一次。
                filename: 'common.js'
            }
        }
    }
}

十七、Lazy Loading懶加載概念

懶加載:並不是webpack的概念,而是ES裏面的裏的一種實驗性質的語法,webpack則是識別這種語法,一種懶加載的實現。

  • 優點:頁面加載速度更快。例如訪問首頁的時候加載了詳情頁、列表頁等。實際可以做代碼分割,訪問首頁時只加載首頁。
    頁面加載的時候某些模塊不加載,只有進行了操作,例如點擊事件時才加載並執行代碼。
  1. promise寫法
function getComponent() {
    return import(/* webpackChunkName:"lodash" */'lodash').then(({default:_})=>{
        var element = document.createElement('div');
        element.innerHTML = _.join(['Dell','Lee'],'-')
        return element;
    })
}

/* 只有點擊了纔會加載文件,然後掛載內容 */
document.addEventListener('click',()=> {
    getComponent().then(element => {
        document.body.appendChild(element)
    })
})
  1. 異步函數寫法
async function getComponent() {
    const {default: _} = await import(/* webpackChunkName:"lodash" */'lodash')
    const element = document.createElement('div');
    element.innerHTML = _.join(['Dell', 'Lee'], '-')
    return element;
}

/* 只有點擊了纔會加載文件,然後掛載內容 */
document.addEventListener('click', () => {
    getComponent().then(element => {
        document.body.appendChild(element)
    })
})

十八、Chunk

打包進行代碼分割,生成的每個JS文件都叫做一個chunk。

十九、打包分析、Preloding/Prefetching

  • 打包分析
    “build”: “webpack --profile --json > stats.json --config build/webpack.prod.js”
    1、配置輸出描述性內容stats.json
    http://webpack.github.io/analyse/
    2、上傳描述json進行分析,bundle analysis

頁面緩存的意義是在第一次加載很慢,後面分割了代碼緩存了庫文件訪問就比較快。
但webpack想達到的效果是第一次訪問頁面加載就是最快的!
1、打包後打開頁面,F12打開開發者模式。ctrl+shift+P,搜索show coverage可查看頁面利用率
2、例如點擊事件追加元素。頁面加載的時候並不執行,只有點擊時纔會執行利用起來。webpack希望交互的代碼把代碼放到異步加載的模塊中去寫。
例如下面當點擊時纔會加載該代碼,代碼使用率較高

click.js
function handleClick() {
    const element = document.createElement('div');
    element.innerHTML = 'Dell Lee'
    document.body.appendChild(element)

export default handleClick;

index.js
document.addEventListener('click', () => {
    import('./click.js').then((handleClick)=>{
        handleClick()
    })
})

所以同步的代碼打包在一起生成vendor.js意義不大。所以splitChunks的chunks默認選項爲async選項。

應用場景例子

1.網頁登錄的登錄框,點擊登錄按鈕時才加載這個框。但點擊才加載可能會存在反應過慢的問題。

  • Preloding/Prefetching
    在空閒時間下載代碼。
    網頁加載時已經把主要內容加載完成了,這時帶寬是空閒的,可以在此時加載例如登錄框的js的代碼。
    實現:藉助webpack的打包特性實現的,使用magic common語法
網絡空閒時才加載,當點擊頁面的時候還是會加載,但是要加載的文件以及緩存過,1ms加載完。所以說prefetch利用的是瀏覽器的緩存功能。

document.addEventListener('click', () => {
    import(/* webpackPrefetch: true */'./click.js').then(({default:_})=>{
        _()
    })
})

兩者區別:
Prefetching會加載完頁面的核心js文件最後在網絡空閒時才加載頁面
Preloding則是和主JS文件一起加載的
推薦:懶加載+Prefetching。但最重要的是代碼利用率上思考

二十、CSS文件分割

MiniCssExtractPlugin插件
需求:打包時將CSS文件單獨打包到CSS文件下而不是打包到JS文件中。藉助該插件
1、npm install -D mini-css-extract-plugin
2、webpack使用該插件,new一下
3、使用了該插件同時還需使用該插件的loader解析css文件,將styleloader
4、教程是不支持HRM所以只配置了線上CSS文件分割,但現在已經指出HRM了。那麼安裝教程來做則:
(1)將css和scss文件處理的loader配置剪切到dev和prod配置文件,然後將prod的style.loader換成該插件的loader:MiniCssExtractPlugin.loader

二十一、webpack與瀏覽器緩存

項目上線後,更改部分JS代碼重新打包後,用戶瀏覽器存在緩存問題而沒有加載最新的JS文件。
1、將dev和prod環境的output區分打包,dev有熱替換所以不需要配置。prod需要給文件名配置個hash值,當文件發送變更,文件名也跟着變更。而文件不變更則hash值也不變

  • 新版本webpack4
output:{
    publicPath: "./",
    chunkFilename: "[name].[contenthash].js",//非入口文件,爲入口JS文件異步加載的JS文件。
    filename: "[name].[contenthash].js",
}
  • 老闆本可能出現內容不變但是哈希值也變了的情況,則需要配置個runtime
    manifest的原因,該文件內置了包和包之間的關係,可能會發送變化。而配置runtime則將manifest抽離出一個js文件。
 optimization:{
       runtimeChunk:{
           name:'runtime'
       },
   }

二十二、Shimming——墊片

webpack打包過程中常要做代碼或打包過程的兼容,如@babel/polyfill。當低版本不存在promise類似變量的問題,則需要藉助polyfill的這樣的工具,在低版本瀏覽器上構建類似Promise的變量。這種兼容性不僅僅在瀏覽器方面上。還有模塊方式打包,則不同模塊內都需要引入庫。
以下兩個行爲都是Shimming行爲,解決webpack實現不了的功能

  • 不同模塊內部引用庫問題?
    解決:配置webpack內部插件
    plugins: [
        new webpack.ProvidePlugin({
            $:"jquery",
            _join:['lodash','join']
        }),
    ]
  • 藉助loader使得每一個模塊的this都指向window,而不是本身模塊。
  1. npm install -D imports-loader
  2. 配置js使用loader
    module: {
       rules: [
           {
               test: /\.js$/,
               exclude: /node_modules/,
               use: [
                   {
                       loader: 'babel-loader'
                   },
                   {
                       loader: 'imports-loader?this=>window'
                   }
               ],
           },usedExports
       ]
   },

二十三、react腳手架

1、npx create-react-app my-app
2、cd my-app/git init
3、npm run eject暴露webpack配置文件
4、如暴露後npm run start遇到問題刪除yarn.lock和node_module,重新安裝依賴

或者不暴露
npm install -D react-app-rewired
進行配置覆蓋

config-overrides.js
const {override, fixBabelImports, addLessLoader, addDecoratorsLegacy} = require('customize-cra')
const path = require('path')
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer')

const addCustomize = () => config => {
   if (process.env.NODE_ENV === 'production') {
       config.devtool = false;
       if (config.plugins) {
           config.plugins.push(new BundleAnalyzerPlugin({
               openAnalyzer: false,
               generateStatsFile: true,
               statsFilename: path.resolve(__dirname, './stats.json'),
               logLevel: 'error'
           }))
       }
       const optimization = config.optimization;
       if (config.entry && config.entry instanceof Array) {
           config.entry = {
               "uc-web":"./src/index.tsx"
           }
           Object.assign(config.output ,{
               path: path.resolve(__dirname, './build'),
               filename: "./static/js/[name].[contenthash].js",
               chunkFilename: "./static/js/[name].[contenthash].js",
           })
       }
       Object.assign(optimization, {
           splitChunks: {
               chunks: 'all',
               automaticNameDelimiter: '~',
               cacheGroups: {
                   vendors: {
                       test: /[\\/]node_modules[\\/]/,
                       priority: -10,
                       name: 'vendors'
                   },
                   default: {
                       name: 'common',
                       priority: -20,
                       reuseExistingChunk: true,
                   },
                   styles: {
                       name: 'styles',
                       test: '/\.css$/',
                       chunks: 'all',
                       enforce: true
                   }
               }
           },
           usedExports: true
       })
   }
   return config
}

module.exports = override(
   fixBabelImports('import', {
           libraryName: 'antd',
           libraryDirectory: 'es',
           style: true
       }, {
           libraryName: '@shopify/polaris',
           libraryDirectory: 'es',
           style: true
       }
   ),
   addLessLoader({
       javascriptEnabled: true,
       modifyVars: {'@primary-color': '#1DA57A'}
   }),
   addDecoratorsLegacy(),
   addCustomize()
)

二十四、庫打包

const path = require('path')

module.exports = {
    mode: 'production',
    entry: './src/index.js',
    externals: ["lodash"],//打包過程中如果遇到lodash則忽略不打包
    // externals:{
    //     lodash:{
    //         root:'_',//既不用commonjs或者Esmodule或AMD等。爲script標籤,必須在頁面注入一個命名爲_的全局變量
    //         commonjs:'lodash',//如果庫在commonjs環境使用庫則名字必須是lodash
    //     }
    // },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'library.js',
        library: 'library',//可通過script標籤引入,打包生成全局變量
        libraryTarget: "umd",//不管在commonJS還是AMD或ESModule任何形式都可以使用。umd是通用的意思。或者window、this掛載到this下
    },
}

index.js
import * as math from './math'
import * as string from './string'

export default {math, string}
  1. 註冊npm賬號,npm adduser …
  2. npm publish …

二十五、PWA(只有上線的代碼才需要做PWA的處理)

Progressive Web Application,如果第一次訪問服務器訪問成功,而後服務器掛掉後但在用戶本地有份緩存,所以服務器掛掉後還是能看到原先加載的頁面。

  • 模擬後端服務器,http-server。
  • http-server和webpack-dev-server區別?http-server可爲模擬後臺api接口的http服務器,易破解所以只能做爲測試。直接打開打包後的index.html是無法實現路由組件跳轉的。
    而dev-server服務器爲web靜態資源服務器,它將打包好的文件放入內容,實時熱更新代碼再進行渲染顯示,達到快速顯示代碼修改的效果,提高開發效率。1爲靜態文件提供服務2自動刷新和熱替換
    (1)npm install http-server -D
    (2)
  "scripts": {
    "start": "http-server dist -i",
    }
  • PWA的實現,React腳手架自帶了
    (1)安裝npm install workbox-webpack-plugin -D
    (2)生成環境下
    new WorkboxPlugin.GenerateSW({
        clientsClaim:true,
        skipWaiting:true,
    })

(3)打包後的文件會多出precache和service-worker的js文件
(4)在入口js文件寫多段業務代碼

//判斷瀏覽器是否支持serviceWorker
if('serviceWorker' in navigator){
    //支持則使用該功能,service-worker.js由workbox-webpack-plugin實現
    window.addEventListener('load',()=>{
        navigator.serviceWorker.register('/service-worker.js')
            .then(registration=>{
                console.log('service-worker registed');
            }).catch(error =>{
                console.log('service-worker register error');
        })
    })
}

二十六、TypeScript打包配置

(1)安裝npm install -D ts-loader typescript @types/lodash,其中lodash方法庫,@types/lodash爲lodash在ts的類型聲明文件,能進行語法提示
類型聲明文件搜索:https://microsoft.github.io/TypeSearch/
(2)配置

webpack.config.js
module:{
        rules:[{
            test:/\.tsx?$/,
            use:'ts-loader',
            exclude: /node_modules/
        }]
    },

tsconfig.js
{
  //編譯過程中的配置項
  "compilerOptions": {
    "outDir": "./dist",//打包生成的文件放到dist下,不寫也可,webpack已配
    "module": "es6",//使用es的模塊引入方式
    "target": "es5",//打包成es5格式
    "allowJs": true //允許在typescript中引入js模塊
  }
}

(3)使用變化,引入發送變化,同時會typescript類型提醒
import * as _ from ‘lodash’

二十七、使用WebpackDevServer實現請求轉發,方便開發過程使用接口

開發環境請求的接口和線上環境請求的接口可能是不一致的
實現:
由絕對http://www.dell-lee.com/react/api/header.json
變相對/react/api/header.json
進行主域名轉發,已經接口轉發(模擬接口)
https://www.webpackjs.com/configuration/dev-server/#devserver-proxy
https://github.com/chimurai/http-proxy-middleware#options
(1)安裝npm i -S axios
(2)配置webpack.config.js中的dev服務器中的proxy代碼選項,也可使用轉發工具charles fiddler

webpack.config.js
devServer: {
    proxy: {
        //不支持 '/' 根目錄轉發,觸發設置index:'',
        //index:'',
        '/react/api': {
            target: 'http://www.dell-lee.com',
            secure:false,//對https請求轉發需設置爲false
            pathRewrite:{
                'header.json':'demo.json'
            },
            bypass: function(req, res, proxyOptions) {//如果請求的地址爲html地址則直接返回首頁index.html
                if (req.headers.accept.indexOf("html") !== -1) {
                    console.log("Skipping proxy for browser request.");
                    return "/index.html";
                    //或者直接返回請求的html
                    //return false
                }
            },
            changeOrigin: true,//防止外部網站爬蟲,對Origin進行限制。設置changeOrigin能突破限制
            headers:{//請求頭設置
                host:'www.dell-lee.com',
                cookie:'asdf'//模擬登陸等場景
            }
        },
    },
},

(3)使用

import React,{Component} from 'react';
import ReactDom from 'react-dom';
import axios from 'axios'

class App extends Component{

    componentDidMount() {
        axios.get('/react/api/header.json')
            .then((res)=>{
                console.log(res);
            })
    }

    render(){
        return <div>Hello World!</div>
    }
}

ReactDom.render(<App/>, document.getElementById('root'));

二十八、WebpackDevServer解決單頁面應用路由問題,只在dev開發環境有效,上線後續後臺進行服務器配置轉發

(1)安裝路由模塊 npm i -S react-router-dom
(2)使用react-router-dom的BrowserRouter和Route

import React,{Component} from 'react';
import {BrowserRouter,Route} from 'react-router-dom';
import ReactDom from 'react-dom';
import Home from './home';
import List from './list'

class App extends Component{
    render(){
        return (
            <BrowserRouter>
                {/*exact精準路由*/}
                <Route path='/' exact component={Home}/>
                <Route path='/list' component={List}/>
            </BrowserRouter>
        )
    }
}

ReactDom.render(<App/>, document.getElementById('root'));

(3)配置webpack.config.js的proxy增加選項,使之任意訪問的404頁面都會訪問index.html。類似後臺無html則進行前臺查詢,而react的router模塊又根據url加載指定的component,所以有時爲空。
historyApiFallback:true

二十九、EsLint在Webpack中的配置

EsLint是一種代碼規範,爲約束工具。

  • 方法一
    (1)安裝npm i -D eslint
    (2)生成eslint配置文件,npx eslint --lint,根據需求選擇
    (3)安裝babel-eslint,npm i -D babel-eslint。在.eslintrc.js中增加"parser":“babel-eslint”
    (4)npx eslint src。會具體顯示不符合eslint規範的語句error

  • 方法二
    (1)IDE(webstorm、VScode)安裝eslint規範的插件——eslint

  • 方法三
    (1)安裝npm i -D eslint-loader eslint
    (2)配置loader,在webpack配置的js的轉化規則,運行dev服務器後會在控制檯顯示不符合eslint規則的代碼

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

(3)在dev-server增加選項overlay:true,之後運行服務器後會在瀏覽器彈出error項。

三十、提升Webpack的打包速度的方法

  1. 跟上技術的迭代(Node,Npm,Yarn)
  2. 在儘可能少的模塊應用Loader。入轉化JS文件時,打包exclude node_module文件目錄或者include。
  3. Plugin儘可能精簡併確保可靠。如開發環境不需要使用插件對代碼進行壓縮,針對環境進行插件使用。還有一些性能好、官方推薦的等。
  4. resolve參數合理配置。該合理是指不亂配置。child比較常用類似目錄別名,避免過長引用,類似路由概念。
resolve: {//當引入其他目錄下的模塊後會先找js結尾的文件再找jsx目錄下的文件,然後引入時可以省略後綴
    extensions: ['.js','.jsx'],
    mainFiles:['index'],//引入文件夾默認引用文件
    alias:{
        //目錄引入時當名爲key時實爲對應值目錄
        child:path.resolve(__dirname,'./src/a/b/c/child')
    }
},
  1. 使用DllPlugin提高打包速度。第一次打包時已經把一些不會變化的庫文件進行打包,後續理想是直接使用而不是再次打包庫文件。即第三方模塊(react、jquery)只打包一次,二次第三方模塊無變更則再無需打包。
    (1)配置專門打包庫文件的配置文件webpack.dll.js
const path = require('path')
const webpack = require('webpack')

module.exports = {
    mode:'production',
    entry: {
        vendors:['react','react-dom','lodash']
    },
    output:{
        filename: "[name].dll.js",
        path:path.resolve(__dirname,'../dll'),
        library: '[name]'//通過全局變量暴露出來
    },
    plugins:[
        //用此插件分析庫,把庫中第三方模塊的映射關係放到[name].manifest.json文件
        new webpack.DllPlugin({
            name:'[name]',
            path:path.resolve(__dirname,'../dll/[name].manifest.json'),
        })
    ]
}

(2)package.json增加庫文件打包script
“build:dll”: “webpack --config build/webpack.dll.js”
(3)安裝add-asset-html-webpack-plugin插件,作用是index.html中增加dll打包後的文件引用

webpack.common.js的plugins
new AddAssetHtmlWebpackPlugin({
filepath:path.resolve(__dirname,'../dll/vender.dll.js')
})

(4)在打包第三方庫文件時使用webpack自帶的DllPlugin插件生成映射關係的文件,而使用的webpack.common.js使用該映射文件,如果映射文件中存在業務代碼中引用的庫時則直接調用該dll打包後的庫代碼,否則打包node_module中的用到的庫。

webpack.dll.js在第(3)步
webpack.common.js
//一旦使用的內容爲該分析文件映射關係中的內容則不再引入模塊,直接使用了。該分析文件由webpack.dll.js配置插件DllPlugin打包生成。它的底層會到全局變量中去拿,而webpack.dll.js中配置了library選項
new webpack.DllReferencePlugin({
    manifest:path.resolve(__dirname,'../dll/vendors.manifest.json')
})

(5)打包第三方庫文件時對庫文件進行分割,例如lodash和react&react-dom分爲兩個文件。但每次第三方分割又生成映射文件又使用映射文件都需要重新new AddAssetHtmlWebpackPlugin和webpack.DllReferencePlugin。所以使用了nodejs的fs文件系統,把文件作爲數組進行查看再打印。

webpack.dll.js
entry: {
        vendors:['lodash'],
        react:['react','react-dom']
    },
webpack.common.js
plugins取出外放
const webpack = require('webpack')
const fs = require('fs');

const plugins = [
    new HtmlWebpackPlugin({
        template: "src/index.html"
    }),
    new CleanWebpackPlugin()
];

//node的fs文件系統下的readdirSync,該方法會將dll文件下的內容放入files變量作爲數組成員
const files = fs.readdirSync(path.resolve(__dirname,'../dll'))
files.forEach(file =>{
    if(/.*\.dll.js/.test(file)){
        plugins.push(
            new AddAssetHtmlWebpackPlugin({
                filepath:path.resolve(__dirname,'../dll/',file)
            })
        )
    }
    if(/.*\.manifest.json/.test(file)){
        plugins.push(
            new webpack.DllReferencePlugin({
                manifest:path.resolve(__dirname,'../dll/',file)
            })
        )
    }
})
modlue.exports = {
    plugins:plugins
}
  1. 控制包文件大小
    (1)treeshaking防止引入卻沒有使用的模塊進行打包
    (2)splitChunks代碼分割?。。

  2. thread-loader,parallel-webpack,happypack多進程打包
    可以用到node的多進程及多個cpu進行打包
    8.合理適用sourceMap
    sourceMap越詳細打包的業就越慢

  3. 結合stats分析打包結果

  4. 開發環境內存編譯
    dev-server打包會放在內存中

  5. 開發環境無用插件剔除
    如調試階段不需要壓縮代碼

三十一、多頁面打包配置

多頁面:多個.html文件
(1)多個入口js文件
(2)多頁面webpack.config.js配置

將配置放到configs中,再根據通過函數對其中的某項(plugins)進行修改,再返回去。再module.exports = configs
const makePlugins = (configs) => {
    const plugins = [
        new CleanWebpackPlugin()
    ]

    Object.keys(configs.entry).forEach(item => {
        plugins.push(
            new HtmlWebpackPlugin({
                filename: `${item}.html`,//生成文件名
                template: `src/index.html`,//模板
                chunks: ['runtime', 'vendors', item]//引入的js文件
            })
        )
    })

    //node的fs文件系統下的readdirSync,該方法會將dll文件下的內容放入files變量作爲數組成員
    const files = fs.readdirSync(path.resolve(__dirname, '../dll'))
    files.forEach(file => {
        if (/.*\.dll.js/.test(file)) {
            plugins.push(
                new AddAssetHtmlWebpackPlugin({
                    filepath: path.resolve(__dirname, '../dll/', file)
                })
            )
        }
        if (/.*\.manifest.json/.test(file)) {
            plugins.push(
                new webpack.DllReferencePlugin({
                    manifest: path.resolve(__dirname, '../dll/', file)
                })
            )
        }
    })
    return plugins;
}
const configs = {
    entry: {
        index: './src/index.js',
        list: './src/list.js',
        detail: './src/detail.js'
    },
    。。
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章