webpack4.0入門到深刻

webpack4.0

(一) webpack安裝使用

1.簡介

webpack是個打包工具,它默認處理js文件,同時也能借助loaders實現對其餘類型文件的處理,同時還能用插件來簡化咱們的開發流程。

2.配置環境

先要安裝一下準備環境,node,由於webpcak是基於node的打包工具
其次要安裝webpack和webpack-clicss

npm init -y
npm install webpack webpack-cli -D

3.命令行打包

安裝好了之後,咱們能夠經過命令行直接來進行打包,能夠先新建一個index.js文件,而後在命令行:html

npx webpack index.js

打包完成後會有一個默認的打包文件,咱們要想看看效果能夠新建一個index.html來引入默認的打包文件,而後看效果。vue

4.腳本打包

在此以前,咱們先來修改一下目錄,讓結構更加清晰,咱們新建一個src目錄,將index.js放在裏面,而後新建一個dist目錄,將index.html放在裏面node

4.1 配置webpack.config.js

接着在根目錄下新建一個webpack.config.js的文件,而後在裏面配置一些打包的參數,react

const path = require('path');
module.exports = {
    entry:{
        'main':'./src/index.js'
    },
    output:{
        filename:'bundle.js',
        path:path.resolve(__dirname,'dist')
    }
}

4.2 配置package.json

配置好了之後,咱們再來配置一下package.json文件,實現腳本打包的功能,jquery

{
    "scripts": {
        "bundle": "webpack"
      },
}

此時在命令行中直接使用 npm run bundle,等待打包成功後,手動打開咱們的index.html文件看看效果,至此咱們已經完成了webpack的安裝和使用。webpack

(二)webpack打包資源(loader)

前面咱們安裝並使用了webpack,咱們處理的內容是js文件,那它如何處理其餘類型的文件呢?好比說圖片,css,字體等等,這就須要用到loaders,也就是說,webpack能認得js文件,但他不認識其餘文件,因此須要一箇中間人來告訴他該怎麼處理.

1.處理圖片資源

首先先來看一下圖片文件。首先咱們先來截屏一張圖片,而後把它放在src/dog.png,而後要把它掛載在index.html上,在index.js中這樣來寫;css3

import dog from './dog.png';
var img  = new Image();
img.src = dog;
var demo = ducument.getElementById('demo');
demo.append(img);

寫好之後,咱們直接來打包確定會失敗,由於webpack不認識這種文件,因此咱們還須要來寫一下配置文件,在webpack.config,js中這樣來寫;es6

module.exports = {
    module:{
        rules:[
            {
                test:/\.(png|jpg|gif)$/,
                use:{
                    loader:'file-loader'
                }
            }
        ]            
    }
}

1.1 file-loader

寫好了之後咱們先來安裝一下file-loader,npm install file-loader -D,而後再來打包,npm run bundle,打包完成之後,再來看看index.html的效果,就會發現圖片已經可以加載在頁面了,咱們成功的處理了圖片這種資源,此時打包出來的圖片的名字一堆亂碼,太難看,因此咱們能夠修改一下web

module:{
    rules:[
        {
            test:/\.(jpg|png|gif)$/,
            use:{
                loader:"file-loader",
                options:{
                    name:"[name].[ext]"
                }
            }
        }
    ]
    
}

1.2 url-loader

除了可使用file-loader,咱們還可使用另外一種loader來處理圖片,那就是url-loader,咱們來npm install url-loader -D,而後把圖片處理規則改爲url-loader再來看看效果,先把dist裏面的打包文件刪除,而後再打包,完成後,會發現並無像file-loader那樣把圖片單獨打包出來,咱們在瀏覽器看一下它的img的src就會發現它被打包成了base64文件,這是它和file-loader的區別,爲何會有file-loader和url-loader這兩種處理方式呢?這是由於圖片的大小是不肯定的,若是很是大,那就單獨處理,這樣的話頁面的加載就不用等這個圖片加完了之後再顯示,若是很是小,那就直接包含在打包文件內,這樣就會使得減小發送http的請求。
其實url-loader也能夠經過配置來實現分類打包,利用limit來設置,若是小於某kb就打包在bundle.js中,不然的話就單獨打包在img中而後引用,配置以下:

options:{
    name:"[name].[ext]",
    outputPath:"img/",
    limit:20400    
}

就會發現大於20kb的圖片就會直接被分開打包,小於20kb就以base64位的方式打包在bundle.js中。

2.處理css資源

前面咱們已經能處理js和圖片資源了,接下來咱們來處理一下css,scss等文件,
其實思路大概都差很少,都是使用對應的loader來進行處理。
首先咱們先新建style.css和style.scss文件,隨便寫點樣式,

//style.css
body{
    background:green
}
//style.scss
body{
    transform:translate(0,0)
}

而後在index.js中引入並使用

import './style.css';

1.1 css/style loader

引入好了之後,咱們須要去配置一下loader,在webpack.config.js中

{
    test:/\.css$/,
    use:{
        loader:["css-loader","style-loader"]
    }
}

1.2 scss-loader

這些lodaer加載時是從後向前加載的,css-loader 是將多個css文件合併成一個,style-loader是將合併完成的css文件掛載在html的head,首先咱們先來安裝一下,npm install css-loader style-loader -D,而後npm run bundle進行打包,而後打開index,html就會發現頁面已經變成了藍色,說明css這種類型的文件已經能被webpack識別並打包了,咱們再來試試scss文件,一樣在webpack.config.js中進行配置

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

此時咱們再把剛纔寫好的style.scss文件在index.js中引用

import './style.scss';

而後再來安裝一下npm install sass-loader -D,後npm run bundle,會發現有報錯,這是由於咱們缺乏了node—sass這個包,安裝一下,npm install node-sass -D,而後繼續npm run build打包完成後在html中打開,就會發現頁面變成了綠色,說明sass文件也能被webpack進行處理了

1.3 postcss

前面是最基礎的處理css文件的方法,那除了這樣來處理文件咱們有時候還須要給不一樣的css屬性添加廠商前綴,這個咱們經過postcss-loader來實現,首先咱們先來在style.css文件中使用一些css3的屬性

body{
    transform: translate(0,0);
}

此時咱們來打包的時候是不會有廠商前綴的,這就須要咱們來寫一些規則,

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

}

配置好了之後,咱們還須要新建一個postcss.config.js來對postcss來進行配置,

//postcss.config.js
module.exports = {
    plugins:[require('autoprefixer')]
}

先來安裝postcss-loader,npm install postcss-loader autoprefixer -D,而後npm run build此時再來看看,就會發現裏面自動加了前綴

3.處理字體文件

至此咱們已經能夠處理js,css,圖片文件,接下來咱們處理字體文件,咱們日常使用的字體都是默認字體,有時候咱們但願使用一些特殊的字體,那webpack是不認識這些文件的,咱們就來看看它是如何處理這些文件的,首先咱們先下載一種字體,我已經準備好了,你們下載來用就行了,接着咱們在樣式文件中使用一下

//style.css
font-face {
 font-family: 'fontnameRegular';
 src: url('fontname.ttf');
}


div{
    font-family: fontnameRegular
}

而後在js中引用一下

import './style.css';

var demo = document.getElementById('demo');
var div = document.createElement('div');
div.innerHTML = '我要自學網';
demo.append(div);

若是此時進行打包,確定會失敗,試一下,果真失敗了,那怎麼處理呢?

1.1 file-loader

配置一下loader

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

而後打包試試,npm run bundle,在瀏覽器查看效果,字體就變化了,這樣就成功處理了字體文件

(三)webpack實現自動化(plugins)

1.構建流程

上面咱們說的都是關於處理資源的內容,接下來咱們來看一下咱們是如何經過plugins簡化操做流程的,先想一下,咱們到目前爲止對於打包編譯過程裏手工作了什麼?
源碼-清除-新建-打包-瀏覽-刷新-報錯-定位-轉碼-切換

  • 0.新建dist-----------output裏的path會自動新建dist目錄
  • 1.新建index.html---------------html(模板)
  • 2.引入打包後的文件引用到index.html--------html
  • 3.修改src源代碼後清空上次打包的文件-----------clean
  • 4.再次npm run bundle進行打包--------------------npm run start
  • 5.將index.html在瀏覽器打開---------------open:true
  • 6.下次有變動刷新頁面-------------contentBase:'./dist'
  • 7.代碼開發完成後切換到線上環境從新配置相關參數---------------webpack merge
  • 8.配置babel使得支持es6語法,react語法
  • 9.配置eslint使得支持智能報錯

這些均可以自動化實現,

2.新建模板引入資源

咱們首先來處理1和2自動的新建index.html並引入打包資源,這須要藉助插件html-webpack-plugin來實現,先來安裝一下,

npm install html-webpack-plugin -D

安裝好了之後,須要在webpack.config.js裏配一下,

const HtmlWebpackPlugin = require('html-webpack-plugin');

plugins:[new HtmlWebpackPlugin({
    template:'./src/idex.html'
})]
//src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>html模板</title>
</head>
<body>
    <div id="demo"></div>
</body>
</html>

它是一個插件,因此寫在plugins裏面,它的做用是在每次打包文件的時候讀取模板文件而後插入到新生成的index.html而後把打包好的資源引入文件中。此時咱們呢npm run bundle發現它果真實現了新建HTML文件並引入打包資源。

3.清空打包目錄

接着咱們要解決3.自動清空dist目錄,你可能會想說,我之前並無手動的清除過dist目錄,在我從新bundle之後仍是直接能用啊,那爲何還要來清除呢?咱們假想這樣的場景,你在打包的時候,把出口的文件名修改了,在打包的時候會生成不一樣的打包文件,多修改幾回就會使得dist目錄很是的臃腫,因此咱們須要清除。
清除一樣是靠插件clean-webpack-plugin一樣來安裝一下

npm install clean-webpack-plugin -D

而後配一下

plugins:[
    new CleanWebpackPlugin({
        root:path.resolve(__dirname,'dist')
    })
]

這個插件會在打包以前被調用而後去清除dist目錄下的內容.實現了咱們的目的。

4.持續監控(devserver)

接下來咱們4.輸入一次命令持續監控文件變化,而不是每次修改源碼就重複的打包,這個能夠藉助webpackdevserver來實現,首先先來下載

npm install webpack-dev-server -D

接着來配一下,

devServer:{
    contentBase:'./dist',
    open:true
}

修改一下啓動腳本,

{
    "scripts":{
        "dev":"webpack-dev-server"
    }
}

此時再來打包一下,npm run dev,就會發現它打包完成後自動打開了瀏覽器,而後當咱們源碼有修改的時候會自動刷新頁面.
爲何要用這個服務器呢?使用server它的協議就會變成http,這有助於咱們在開發時避免跨域問題。

5.環境切換

那咱們如何來區分這兩種環境呢?難道每次切換環境的時候都來修改對應的配置麼?那太麻煩了,咱們能夠把開發環境和線上環境分別寫在一個文件裏,而後把共有的邏輯寫在另外一個文件,在切換環境的時候直接調用環境配置文件便可,

首先是開發環境

//webpack.dev.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');


module.exports = {
    mode:"development",
    devtool:"source-map",
    entry:{
        "main":"./src/index.js"
    },
    devServer:{
        contentBase:'./dist',
        open: true,
        port:8080,
        hot:true
    },
    module:{
        rules:[
            {
                test:/\.js$/,
                exclude:"/node_modules/",
                loader:"babel-loader"
            },
            {
                test:/\.(jpg|png|gif)$/,
                use:{
                    loader:'url-loader',
                    options:{
                        name:'[name].[ext]',
                        outputPath:'images/',
                        limit:1024
                    }
                }
            },
            {
                test:/\.css$/,
                use:['style-loader','css-loader']
            },
            {
                test:/\.scss$/,
                use:['style-loader',
                {
                    loader:'css-loader',
                    options:{
                        modules:true
                    }
                },

                'sass-loader']
            },
            {
                test:/\.(ttf)$/,
                use:{
                    loader:'file-loader'
                }
            }
        ]

    },
    plugins:[
        new HtmlWebpackPlugin({
            template:'./src/index.html'
        }),
        new CleanWebpackPlugin({
            root:path.resolve(__dirname,'dist')
        }),
        new webpack.HotModuleReplacementPlugin()

    ],
    optimization:{
        usedExports: true 
    },
    output:{
        filename:'bundle.js',
        path:path.resolve(__dirname,'dist')
    }
}

接着是線上環境

//webpack.prod.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');


module.exports = {
    mode:"production",
    devtool:"cheap-module-source-map",
    entry:{
        "main":"./src/index.js"
    },

    module:{
        rules:[
            {
                test:/\.js$/,
                exclude:"/node_modules/",
                loader:"babel-loader"
            },
            {
                test:/\.(jpg|png|gif)$/,
                use:{
                    loader:'url-loader',
                    options:{
                        name:'[name].[ext]',
                        outputPath:'images/',
                        limit:1024
                    }
                }
            },
            {
                test:/\.css$/,
                use:['style-loader','css-loader']
            },
            {
                test:/\.scss$/,
                use:['style-loader',
                {
                    loader:'css-loader',
                    options:{
                        modules:true
                    }
                },

                'sass-loader']
            },
            {
                test:/\.(ttf)$/,
                use:{
                    loader:'file-loader'
                }
            }
        ]
    },

    plugins:[
        new HtmlWebpackPlugin({
            template:'./src/index.html'
        }),
        new CleanWebpackPlugin({
            root:path.resolve(__dirname,'dist')
        })
    ],

    output:{
        filename:'bundle.js',
        path:path.resolve(__dirname,'dist')
    }
}

而後咱們來看,這兩個裏面有太多的重複代碼,咱們能夠把他抽離出來放在單獨的一個文件中,而後在dev和prod中分別來引用common

//wenpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
    entry:{
        "main":"./src/index.js"
    },

    module:{
        rules:[
            {
                test:/\.js$/,
                exclude:"/node_modules/",
                loader:"babel-loader"
            },
            {
                test:/\.(jpg|png|gif)$/,
                use:{
                    loader:'url-loader',
                    options:{
                        name:'[name].[ext]',
                        outputPath:'images/',
                        limit:1024
                    }
                }
            },
            {
                test:/\.css$/,
                use:['style-loader','css-loader']
            },
            {
                test:/\.scss$/,
                use:['style-loader',
                {
                    loader:'css-loader',
                    options:{
                        modules:true
                    }
                },

                'sass-loader']
            },
            {
                test:/\.(ttf)$/,
                use:{
                    loader:'file-loader'
                }
            }
        ]

    },

    plugins:[
        new HtmlWebpackPlugin({
            template:'./src/index.html'
        }),
        new CleanWebpackPlugin({
            root:path.resolve(__dirname,'dist')
        })
    ],

    output:{
        filename:'bundle.js',
        path:path.resolve(__dirname,'dist')
    }

}

抽離好了common,接下來就是引用了,這裏咱們須要使用一個新的插件來完成合並webpack-merge,先來安裝一下,

npm install webpack-merge -D
//dev
const webpack = require('webpack');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');


const devConfig = {
    mode:"development",
    devtool:"source-map",
    devServer:{
        contentBase:'./dist',
        open: true,
        port:8080,
        hot:true
    },
    
    plugins:[
        new webpack.HotModuleReplacementPlugin()
    ],
    optimization:{
        usedExports: true 
    }
}

module.exports = merge(commonConfig,devConfig)
//prod

const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js')

 const prodConfig = {
    mode:"production",
    devtool:"cheap-module-source-map"
}
module.exports = merge(commonConfig,prodConfig)

此時根目錄下有一堆的配置文件,太雜亂了,因此把他們放在build文件夾下,而後修改腳本路徑

"scripts": {
    "bundle": "webpack",
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js"
  },

6.配置babel

6.1支持ES6語法

使用es6代碼書寫業務,這個須要使用babel來進行轉碼,咱們來寫一下
首先,要使用babel就要先來安裝

npm install -D babel-loader @babel/core

而後,配置loader

module.exports = {
    module:{
        rules:[
            {
                test:/\.js$/,
                exclude:"/node_modules/",
                loader:"babel-loader",
                options:{
                    presets:['@babel/preset-env']
                }
            }
        ]
    }
}

配置好了loader之後,咱們還須要安裝一個模塊,由於loader只是把babel和webpack聯繫在一塊兒,具體的轉換工做仍是由下面這個/preset-env 模塊來作的,轉換出來的內容有時候一些低版本的瀏覽器一樣也不支持,就須要對他進行在此的兼容使用/polyfill 這個模塊

npm isntall @babel/preset-env -D
npm install @babel/polyfill --save

寫好了之後,咱們來寫點es6的代碼來試試

//index.js
import '@babel/polyfill'
const arr = [
    new Promise(() => {}),
    new Promise(() => {})
];

arr.map(item => {
    console.log(item);
});

而後就會看到,代碼被打包成了能夠被低版本瀏覽器識別的es5代碼,可是咱們再來考慮一下,咱們只用到了僅有的幾個es6的語法,可是他在識別的時候把全部的特性都轉換了,這根本不必,另外,有的瀏覽器默認就支持es6的代碼,不須要進行轉換就能用 ,根本不用轉換就能用啊,這兩個點怎麼解決呢?咱們能夠給他添加一些配置來實現,

options:{
        presets:[['@babel/preset-env',{
            targets:{
                chrome:"67"
            },
            useBuiltIns:"usage"
        }]]
    }

總結一下:
1.babel-loader @babel/core:使用babel的必要環境,loader負責將babel和webpack聯繫起來
2.@babel/preset-env:實際進行代碼轉換的模塊
3.@babel/polyfill:轉化出來的代碼在一些低版本瀏覽器一樣不能識別,用這個模塊來兼容
4.targets:{chrome:"67"} :瀏覽器支持就不用再轉換了
5.useBuiltIns:"usage":只轉換使用到的es6代碼特性

至此咱們就能夠寫任意的es6代碼了,可是babel自己的配置很是多,若是都寫在webpack.config.js中,那會很是的臃腫,因此咱們能夠把他單獨拿出來寫在.babelrc中

//.babelrc
{
    "presets":[
        [
            "@babel/preset-env",
            {
                "targets":{
                    "chrome":"67"
                },
                "useBuiltIns":"usage"
            }
        ]
    ]
}

咱們再來打包試一下,npm run bundle 發現沒有問題

6.2支持react語法

最後一個內容就是,如何使用框架來寫源碼?
首先,咱們先安裝框架

npm install react react-dom -D

接着寫配置,

//.babelrc
{
    "presets":[
        [
            "@babel/preset-env",
            {
                "targets":{
                    "chrome":"67"
                },
                "useBuiltIns":"usage"
            }
        ],
        "@babel/preset-react"
    ]
}

寫點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('demo'));

再來運行npm run dev就能看到react代碼了。

6.3支持vue語法

一樣先來安裝框架

npm install vue

接着安裝loader

npm install -D vue-loader vue-template-compiler

配製一下loader

module:{
  rules:[
    {
      test: /\.vue$/,
      loader: 'vue-loader'
    }
    ]
},
plugons:[
    new VueLoaderPlugin()
]

寫點vue代碼

//index.vue
<template>
    <div class="rest">{{info}}</div>
</template>


<script>
module.exports = {
    data(){
        return {
            info:'hello'
        }
    }
}

</script>


<style lang="stylus" scoped>
.test
  color:red
</style>

掛載在頁面上

//index.js
import Vue from 'vue';
import App from './index.vue';

const root = document.getElementById('root');

new Vue({
    render:(h) => h(App)

}).$mount(root)

7.配置Eslint

在項目中,爲了統一編碼規範,咱們能夠用一些工具來幫咱們約束,eslint是最經常使用的規範工具,咱們來使用一下

npm instll eslint -D
npm install babel-eslint -D
npm install eslint-loader -D
npx eslint --init

如今已經有了一個配置文件.eslint.js,可是對於一些錯誤無法直接在sublimt中顯示,咱們就能夠用eslint-loader來進行配置,接着咱們來配置一下

//.eslintrc
module.exports = {
    "parser":"babel-eslint"
}
rules:{}
//dev
devServer:{
    overlay:true
}
//common
module:{
    rules:[
        {
            test:/\.js$/,
            use:["babel-loader","eslint-loader"]
        }
    ]
}

此時,只要有錯誤就會在瀏覽器上進行彈出。
至此咱們看一下,全部手工操做的內容咱們如今都變成了自動操做,大大簡化了開發的繁瑣,

(四)webpack提高性能

1.性能分析

接下來咱們要考慮的問題就是性能問題,咱們在上面的開發過程當中有哪些性能能夠提高呢?

  • 1.源碼有修改時不要整頁刷新只刷新修改的部分便可(HML)
  • 2.源碼出錯時快速定位到出錯的位置,沒必要精確到字符,只要精確到哪一行便可。(sourcemap)
  • 3.

2.HMR(熱更替)

2.1 cssHMR

首先是1.局部刷新更替的代碼,這個可使用HML來進行實現,它的名字叫熱替換,當咱們開啓了HML就會只刷新更改的代碼,咱們舉例來看一下

//index.js
import './style.css'
var div = document.createElement('div');
div.innerHTML = "我要自學網";
var p = document.createElement('p');
p.innerHTML = "51zxw";

var demo = document.getElementById('demo');
demo.append(div);
demo.append(p);
//style.css
div{
    color:green;
}
p{
    color:red
}

此時咱們npm run dev開啓服務器,會發現頁面上渲染好了內容,當咱們去修改div的顏色的時候,頁面會整頁刷新,可是實際上p的顏色並無變啊,它也被從新加載了一遍,若是隻有這兩個樣式那還好說,樣式一旦增多,這絕對是極大的資源浪費啊,因此,就須要只刷新div的樣式而不去刷新總體的樣式,咱們須要配一下

const webpack = require('webpack');

devServer:{
    contentBase:'./dist',
    open:true,
    port:8080,
    hot:true
},
plugins:[
    new HtmlWebpackPlugin({
        template:'./src/index.html'
    }),
    new CleanWebpackPlugin({
        root:path.rasolve(__dirnam,'dist')
    }),
    new webpack.HotModuleReplacement()    
]

此時咱們再來重啓服務器,而後在修改div顏色的時候,就會發現它不會再整頁刷新而是隻替換了修改的代碼。

2.2jsHMR

上面是針對css代碼的熱替換,其實js代碼一樣也能夠,咱們舉例說明一下

//counter.js
function counter(){
    var div = document.createElement('div');
    div.setAttribute('id','counter');
    div.innerHTML = 1;
    div.onclick = function(){
        div.innerHTML = parseInt(div.innerHTML,10)+1;
    }
    document.body.appendChild(div)
}
export default counter;
//number.js
function number(){
    var div = document.createElement('div');
    div.setAttribute('id','number');
    div.innerHTML = 3;
    document.body.appendChild(div);
}
export default number;
//index.js
import counter from './counter.js';
import number from './number.js';

counter()
number()

在未開啓HML的時候,頁面第一次加載,顯示p和div,當修改div裏的內容時,p裏的內容沒有改變,但也被刷新了
在開啓HML的時候,咱們能夠指定只刷新a的內容

//index.js
import createDiv from './a.js';
import createP from './b.js';
createDiv();
createP();

if(module.hot){
    module.hot.accept('./a.js',function(){
        var div = document.getElementById('d');
        document.body.remove(div)
        createDiv()
    })
}

至此就實現了js代碼的HML熱更替

3.sourceMap(源碼映射)

接着咱們須要定位出錯代碼的位置,這個要用soucemap來進行定位,可是開發環境下它是默認開啓的,因此咱們先把他關掉

//webpack.config.js
    module.exports = {
        devtool:"none"
    }

而後啓動服務器,這時候咱們故意改錯代碼

console.lo('51zxw')

這是後當咱們點擊錯誤的代碼的連接的時候,咱們直接跳到了打包文件裏去了,而不是定位到源碼裏,因此咱們但願能定位到源碼裏,咱們再來修改一下配置

module.exports = {
    devtool:"source-map"
}

而後重啓服務器,再次點擊的時候就會定位到具體的出錯源碼,此時咱們會發現,dist目錄不見了,實際上是webpack爲了加快打包速度,把dist目錄直接放在了內存中,咱們從新npm run bundle來學習一下,就會發現此時dist目錄又出現了,但也多了一個map文件,這個就是soucre-map的對應文件,他會把打包資源和源碼關係醉成對應放在這個文件裏。知道了怎麼用,咱們接下來講說devtool有哪些參數,各表明什麼意思。
inline-source-map:直接把對應文件map放在打包文件裏
cheap-source-map:只定位到錯誤行而不是具體錯誤字符,同時也不處理引入的第三方包帶來的錯誤
module-source-map:既處理業務代碼又處理第三方包的錯誤
eval-source-map:速度最快但錯誤信息不全面
在開發環境中通常要使用:devtool:cheap-module-eval-source-map
在生產環境中通常要使用:devtool:cheap-module-source-map

4.TreeShaking

咱們在前面已經學會了打包不一樣的資源和自動化開發流程,同時也作了一點性能優化,但還有不少的地方能夠優化,好比,咱們在前面配置過"useBuiltIns":"usage",這個是爲了使得babel在轉碼的時候只轉碼咱們用到的es6特性而不是把全部的es6特性都轉碼,那在咱們本身寫的js代碼中,若是咱們引入了一個模塊,可是隻是用了其中導出的一個內容,那其餘的內容也會被打包,那有沒有辦法使得咱們用哪些東西就打包哪些,不用的就全去除掉呢?這個就是TreeShaking,es模塊未引用的導出內容所有不加載咱們來實現一下,

export const add = function(a,b){
    console.log(a+b)
}

export const minus = function(a,b){
    console.log(a-b)
}
//index.js
import { add } from './math.js';

add(1,2)

此時咱們打包的時候,這兩個方法都會被打包進bundle.js,咱們來配置一下,

optimization:{
    usedExports:true
}
{
    "sideEffects":false
}

在線上環境基本上默認開啓treeshaking,開發環境使用optionzation來進行觸發。

5.CodeSplitting(代碼分割)

接着咱們來看一下代碼分割,有時候咱們的代碼裏既包括本身寫的業務邏輯,又有引入的第三方包,那在用戶加載的時候,若是把兩個文件打包在一個文件中,下載的速度會很是的慢,因此就須要把代碼分割開來,加載兩個較小的文件比加載一個較大的文件速度更快。那如何來實現代碼分割呢?
首先咱們先來使用一個外來的庫loadsh

npm install loadsh -D
//index.js
import _ from 'loadsh';
console.log(_.join(['a','b'],'***'))

而後配置common來實現代碼分割

optimization:{
        splitChunks:{
            chunks:"all"
        }
    },

此時,loadsh和業務代碼就分開打包了

6.LazyLoading(懶加載)

再來看一下懶加載,經過import異步加載模塊,也就是隻有須要使用這個模塊的時候纔來加載,舉例說明一下

function getComponent(){
    return import('loadsh');
    var element = document.createElement('div');
    element.innerHTML = _.join(['a','b'],'***');
    rerun element;
}
document.addEventListener('click',()=>{
    getComponent().then(element => {
        document.body.appendChild(element)
    })
})

7.PreFech(預請求)

再來看一下prefech,異步代碼在網絡空閒的時候自動下載,舉例說明一下

//index.js

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

function handleClick(){
    const ele = document.createElement('div')
    ele.innerHTML = "hello";
    document.body.append(ele)
}

export default handleClick;

8.caching(緩存)

caching,在進行打包的時候,若是有的包內容沒有發生改變,那麼先後兩次打包的名稱就相同,這使得瀏覽器在請求的時候,若是看到相同的文件就不會去從新發請求,因此能夠用contenthash來時的每次修改生成一個新的文件名來避免。

output:{
    filename:'[name].[contenthash].js'
}

9.shimming(墊片)

6.shimming:墊片,一種兼容方案,有不少的行爲都叫墊片,它主要是彌補一些不足,例如將變量全局化

const webpack = require('webpack');

plugins:[
    new webpack.ProvidePlugin({
            $:'jquery'
        })
]

全局變量

module.exports = (env) => {
    if(env && env.production){
        return merge(commonConfig,prodConfig);
    }else{
        return merge(commonConfig,devConfig);
    }
}

10.DllPlugins

在打包的時候,咱們不免會引入第三方的包,重複的打包時這些沒有改變的模塊也會從新進行打包,爲了加快打包的時間,咱們能夠在第一次打包的時候就把它保存下來,在之後打包的時候直接調用就好了,這個要藉助Dllplugin來實現。
首先咱們先來引入一些內容來看一下

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('demo'));

而後新建一個webpack.dll.js

const path = require('path');
const webpack = require('webpack');



module.exports = {
    mode:'development',
    entry:{
        vendors:['react','react-dom']
    },
    output:{
        filename:'[name].dll.js',
        path:path.resolve(__dirname,'../dll'),
        library:'[name]'

    },
    plugins:[
        new webpack.DllPlugin({
            name:'[name]',
            path:path.resolve(__dirname,'../dll/[name].manifest.json'),

        })
    ]

}

先來打包一次,生成模塊對應的打包文件和json對應文件

{
    "scripts":{
        "build:dll": "webpack --config ./build/webpack.dll.js"
    }
}

接下來咱們須要在index.html中引入這個文件,

//common.js
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
plugins:[
    new AddAssetHtmlWebpackPlugin({
            filepath:path.resolve(__dirname,'../dll/vendors.dll.js')
        })
]

引入成功後,當打包時在commonjs中來檢查是否是有vendors.manifest.json中已經打包的內容。若是有就直接在全局變量中使用,沒有就去node_modules尋找後再打包,

//common.js
plugins:[
    new webpack.DllReferencePlugin({
            manifest:path.resolve(__dirname,'../dll/vendors.manifest.json')
        })
]

就會發現打包的速度確實加快了,若果在大型項目裏,咱們須要引入多個模塊,須要重複的配太麻煩,因此坐下自動插入

//common

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
const devConfig = require('./webpack.dev.js');
const prodConfig = require('./webpack.prod.js');
const merge = require('webpack-merge');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const fs = require('fs');



const plugins = [
    new HtmlWebpackPlugin({
            template:"./src/index.html"
        }),
    new CleanWebpackPlugin({
            root:path.resolve(__dirname,'../dist')
        }),
    new webpack.ProvidePlugin({
            $:'jquery'
        })
]

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



// module.exports = {
const commonConfig = {
    entry:{
        "main":"./src/index.js"
    },
    module:{
        rules:[
            {
                test:/\.js$/,
                exclude: /node_modules/,
                use:[
                    "babel-loader",
                    // "eslint-loader",
                    // {
                    //     loader:"imports-loader?this=>window"
                    // }
                ]
            },
            {
                test:/\.(png|jpg|gif)$/,
                use:{
                    loader:"url-loader"
                }
            },

            {
                test:/\.(ttf|eot|svg)$/,
                use:{
                    loader:"file-loader"
                }
            }
        ]
    },
    plugins,

    optimization:{
        usedExports:true,
        splitChunks:false
    },

    output:{
        path:path.resolve(__dirname,'../dist')
    }

}

module.exports = (env) => {
    if(env && env.production){
        return merge(commonConfig,prodConfig);
    }else{
        return merge(commonConfig,devConfig);
    }
}

1.採用最新版本的node
2.loader和plugin做用在有需求的模塊上,像第三方模塊上儘可能就不檢查了
3.resolve

resolve:{
    extends:['js','jsx'],
    alias:{
        child:path.resolve(__diename,'/')
    }
}

4.DllPlugins
5.控制包的大小
6.合理使用sourcemap
7.結合stats.json進行分析
8.開發環境內存編譯
9.開發環境剔除不用的插件

總結一下提高性能:
1.treeshaking:搖掉引入模塊中未使用的內容

//dev
optimization:{
    usedExports:true
}
//package
{
    "sideEffects":false
}

2.codesplitting:分塊打包代碼

optimization:{
        splitChunks:{
            chunks:"all",
            minSize:30000,
            minChunks:1,
            name:true,
            cacheGroup:{
                vendors:{
                    vendors:{
                        test:/[\\/]node_modules[\\/]/,
                        priority:-10,
                        filename:'vendors.js'
                    }
                },
                default:{
                    priority:-20,
                    reuseExistingChunk:true
                    filename:"common.js"
                }
            }
        }
    },
npm install mini-css-extract-plugin -D//拆分
npm install optimize-css-assets-webpack-plugin -D//壓縮
//prod
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module:{
        rules:[
            {
                test:/\.css$/,
                use:[
                    MiniCssExtractPlugin.loader,
                    "css-loader",
                    "postcss-loader"
                    ]
            },

            {
                test:/\.scss$/,
                use:[
                    MiniCssExtractPlugin.loader,
                    "css-loader",
                    "sass-loader"
                       ]
            }

        ]
    },
    optimization:{
        minimizer:[new OptimizeCssAssetsWebpackPlugin({})]
    },
    plugins:[
        new MiniCssExtractPlugin({
            filename:'[name].css',
            chunkFilename:'[name].chunk.css'
        })
    ]
//注意避免和optimization衝突,而且把common中的css處理摘出來後把prod中也配上

3.lazy-loading:懶加載,只有在調用的時候纔去加載模塊

function getComponent(){
    return import('loadsh');
    var element = document.createElement('div');
    element.innerHTML = _.join(['a','b'],'***');
    rerun element;
}
document.addEventListener('click',()=>{
    getComponent().then(element => {
        document.body.appendChild(element)
    })
})

4.preFech:異步代碼在網絡空閒的時候自動下載

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

5.caching:huancun

output:{
    filename:'[name].[contenthash].js'
}

6.shimming:墊片,一種兼容方案,將變量全局化

const webpack = require('webpack');

plugins:[
    new webpack.ProvidePlugin({
            $:'jquery'
        })
]

全局變量

module.exports = (env) => {
    if(env && env.production){
        return merge(commonConfig,prodConfig);
    }else{
        return merge(commonConfig,devConfig);
    }
}

(五)webpack底層探索

1.本身實現loader

loader是用來處理各類資源的,它本質上是一個函數,咱們來寫一下,

//webpack.config.js
const path = require('path');

module.exports = {
    mode:'development',
    entry:{
        main:"./src/index.js"
    },
    resolveLoader:{
        modules:['node_modules','./loaders']
    },
    module:{
        rules:[
            {
            test:/\.js$/,
            use:{
                loader:'my-loader.js',
                options:{
                    name:'kk'
                }
            }

        }

        ]
    },
    output:{
        filename:'[name].js',
        path:path.resolve(__dirname,'dist')

    }

}
//index.js
console.log('hello world');
//my-loader.js
//npm install loader-utils -D

const loaderUtils = require('loader-utils');

module.exports = function(source){
    //1.直接處理參數
    const options = loaderUtils.getOptions(this);
    // return source.replace('lee',options.name);

    //2.callback處理參數
    // const result = source.replace('lee',options.name);
    // this.callback(null,result);

    //3.寫異步代碼
    const callback = this.async();
    setTimeout(()=>{
        const result = source.replace('dell',options.name);
        callback(null,result)
    },1000)

}

2.本身實現plugin

plugin是用來幫助咱們簡化一些操做流程,它實質上是一個類,咱們來寫一下

//webpack.config.js
const path = require('path');
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin.js');




module.exports = {
    mode:'development',
    entry:{
        main:"./src/index.js"
    },
    resolveLoader:{
        modules:['node_modules','./loaders']
    },
    plugins:[
        new CopyrightWebpackPlugin({
            name:'dell'
        })
    ],
    output:{
        filename:'[name].js',
        path:path.resolve(__dirname,'dist')

    }
}
//plugins/copyright-webpack-plugin.js



class CopyrightWebpackPlugin{
    constructor(options){
        cosnole.log(options)
    }
    apply(compiler){
        compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin',(compiltion,cb)=>{
            compiltions.assets['kk.txt'] = {
                source:function(){
                    return 'kk'
                },
                size:function(){
                    return 2;
                }
                
                };
                cb();
        })
    }
    

}
module.exports = CopyrightWebpackPlugin;

3.本身實現bundller

bundller實質上是要尋找層層引用,而後轉化代碼使之執行,咱們來寫一下

const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');



//分析一個模塊
const moduleAnalyser = (filename) => {
    //1.讀取文件
    const content = fs.readFileSync(filename,'utf-8'); 
    //2.得到抽象語法樹
    const ast = parser.parse(content,{
        sourceType:'module'
    });
    //3.找出第一個模塊中引入模塊的相對/絕對地址,轉化後的代碼
    const dependencies = {};
    traverse(ast,{
        ImportDeclaration({ node }){
            const dirname = path.dirname(filename);
            const newFile = './' + path.join(dirname,node.source.value);
            dependencies[node.source.value] = newFile;
        }
    });
    const { code } = babel.transformFromAst(ast,null,{
        presets:['@babel/preset-env']
    });
    return {
        filename,
        dependencies,
        code
    }
    
    
}


//4.遍歷全部的模塊
const makeDenpendencesGraph = (entry) => {
    const entryModule = moduleAnalyser(entry);
    const graphArray = [ entryModule ];
    for(let i = 0; i<graphArray.length; i++){
        const item  = graphArray[i];
        const { dependencies } = item;
        if(dependencies){
            for(let j in dependencies){
                graphArray.push(
                    moduleAnalyser(dependencies[j])
                    );
            }
        }
    }
    const graph = {};
    graphArray.forEach(item => {
        graph[item.filename] = {
            dependencies:item.dependencies,
            code:item.code
        }

    });
    return graph;
}


//5.生成打包後的代碼
const generateCode = (entry) => {
    const graph = JSON.stringify(makeDenpendencesGraph(entry))
    return `
        (function(graph){
            function require(module){
                function localRequire(relativePath){
                    return require(graph[module].dependencies[relativePath]);
                }
                var exports = {};
                (function(require,exports,code){
                    eval(code);
                })(localRequire,exports,graph[module].code)    
                return exports;
            };

            require('${entry}')

        })('${graph}')
    `;
}
const code = generateCode('./src/index.js');

console.log(code)

終於寫完了,參考教程放在下面:
https://coding.imooc.com/clas...

相關文章
相關標籤/搜索