webpack從入門到進階

前言

在咱們平時的開發中,會常用到webpack,但不少時候咱們都不作任何配置(webpack4)也能夠進行開發,可是默認的配置真的夠咱們使用了嗎?因此本文將帶你開啓webpack的大門。javascript

前置知識篇

學習webpack前先讓咱們一塊兒學習下必要的輔助概念css

路徑知識

在此以前咱們有必要了解下webpack中會使用到的NodeJS路徑知識:NodeJS路徑知識html

核心概念之 Entry

Entry 用來指定 webpack 的打包⼊口html5

Entry 的用法

單⼊⼝:entry 是⼀個字符串java

module.exports = {
    entry: './src/index.js'
};
複製代碼

多⼊⼝:entry 是⼀個對象node

module.exports = {
    entry: {
        index: './src/index.js',
        manager: './src/manager.js'
    }
};
複製代碼

核心概念之 Output

Output ⽤來告訴 webpack 如何將編譯後的⽂件輸出到磁盤的指定路徑react

Output 的用法:單⼊⼝配置

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js’, path: __dirname + '/dist' } }; 複製代碼

Output 的用法:多⼊⼝配置

module.exports = {
    entry: {
        app: './src/app.js',
        search: './src/search.js'
    },
    output: {
        filename: '[name].js',
        path: __dirname + '/dist'
    }
};
複製代碼

經過[name]佔位符確保⽂件名稱的惟⼀jquery

核心概念之 Loader

自己是一個函數,接受源文件做爲參數,返回轉換的結果。webpack

常見的 Loader 有哪些?

babel-loader, 將es6+語法轉換爲es3/es5語法
css-loader,
style-loader,
scss-loader, 
less-loader,
postcss-loader,
file-loader, 進行圖片字體等資源的打包
ts-loader, 將ts轉換爲js
raw-loader 將文件以字符串形式導入
...
複製代碼

Loader 的用法

const path = require('path');
module.exports = {
    output: {
        filename: 'bundle.js'
    },
    module: {
        rules: [
            { test: /\.js$/, use: 'babel-loader' }
        ]
    }
};
複製代碼

test 指定匹配規則ios

use 指定使⽤的 loader 名稱

核心概念之 Plugins

插件⽤於 bundle ⽂件的優化,資源管理和環境變量注⼊ 做⽤於整個構建過程

常見的Plugins

CleanWebpackPlugin 清理構建目錄
MiniCssExtractPlugin 將css從bundle文件裏提取爲一個獨立的css文件
htmlWebpackPlugin 建立html去承載輸出的bundle文件
UglifyWebpackPlgin 壓縮js 去除console等指定語句
...
複製代碼

Plugins 的用法

const path = require('path');
module.exports = {
    output: {
        filename: 'bundle.js'
    },
    plugins: [
        new HtmlWebpackPlugin({template:'./src/index.html'})
    ]
};
複製代碼

全部的插件都應放到plugins數組裏

實戰篇

在此以前咱們先安裝下必須依賴:

npm install webpack webpack-cli -D
複製代碼

轉換 ES6 爲 ES5語法

安裝所需依賴 npm i @babel/core @babel/preset-env babel-loader -D

webpack.config.js配置以下:

module: {
    rules: [
        {
            test: /\.js$/,
            use: 'babel-loader'
        },
    ]
};
複製代碼

再增長ES6的babel preset配置

babel的配置⽂件是:.babelrc[項目根目錄下新建]

{
    "presets": [
        "@babel/preset-env" //能夠根據配置的目標瀏覽器或者運行環境來自動將ES2015+的代碼轉換爲es5。
    ]
}
複製代碼

解析 React JSX

安裝所需依賴

npm i @babel/preset-react -D

配置以下:

babel的配置⽂件是:.babelrc[項目根目錄下新建]

{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react"
    ],
    "plugins": []
}
複製代碼

資源解析:解析 CSS

npm install style-loader css-loader less-loader -D
複製代碼
module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /\.less$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'less-loader'
                ]
            },
        ]
},
複製代碼

css-loader ⽤於加載 .css ⽂件,而且轉換成 commonjs 對象

style-loader 將樣式經過 <style> 標籤插⼊到 head 中

style-loader配置:

options: {
    insertAt: 'top', // 樣式插入到 <head>
    singleton: true, //將全部的style標籤合併成一個
}
複製代碼

less-loader 將less語法轉換爲css語法

注意: loader的執行順序的從下到上,從右到左

資源解析:解析圖⽚和字體文件

npm install url-loader file-loader -D
複製代碼
module: {
        rules: [
            {
                test: /\.(png|jpg|gif|jpeg)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 10240
                        }
                    }
                ]
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                use: 'file-loader'
            }
        ]
},
複製代碼

file-loader ⽤於處理字體⽂件

url-loader 能夠用來處理圖⽚和字體 能夠設置limit將較⼩資源⾃動 base64

url-loader中用到了file-loader

webpack 中的⽂件監聽

⽂件監聽是在發現源碼發⽣變化時,⾃動從新構建出新的輸出⽂件。

webpack 開啓監聽模式,有兩種⽅式:

·啓動 webpack 命令時,帶上 --watch 參數 [推薦]

·在配置 webpack.config.js 中設置 watch: true

webpack 中的⽂件監聽使⽤

在package.json中配置

{
    "name": "hello-webpack",
    "main": "index.js",
    "scripts": {
        "watch": "webpack --watch",
        "build": "webpack --config webpack.prod.js",
    },
}
複製代碼

熱更新:webpack-dev-server

npm install webpack-dev-server -D
複製代碼

package.json中的scripts對象下配置:

{
    "scripts": {
        "dev": "webpack-dev-server --open --mode development"
    },
}
複製代碼

--open表明自動打開瀏覽器

webpack-dev-server 不刷新瀏覽器 不輸出⽂件,⽽是放在內存中

使⽤ HotModuleReplacementPlugin插件(webpack內置插件)可使瀏覽器自動刷新

webpack.config.js中配置:

const webpack = require('webpack');
plugins: [
    new webpack.HotModuleReplacementPlugin(),
],
devServer:{
    hot: true,
    contentBase: [path.join(__dirname, "public"), path.join(__dirname, "assets")], //contentBase 用於配置提供額外靜態文件內容的目錄,以前提到的 publicPath 是配置構建好的結果以什麼樣的路徑去訪問
    proxy: {
        '/api': {
            target: "http://localhost:3000", // 將 URL 中帶有 /api 的請求代理到本地的 3000 端口的服務上
            pathRewrite: { '^/api': '' }, // 把 URL 中 path 部分的 `api` 移除掉
        },
    },
    before(app) {
            app.get('/some/path', function(req, res) { // 當訪問/some/path 路徑時,返回自定義的 json 數據
                res.json({ custom: 'response' })
            })
    }
}
複製代碼

before 在 webpack-dev-server 靜態資源中間件處理以前,能夠用於攔截部分請求返回特定內容,或者實現簡單的數據 mock。

after 在 webpack-dev-server 靜態資源中間件處理以後,比較少用到,能夠用於打印日誌或者作一些額外處理。

熱更新的原理分析

Webpack Compile: 將 JS 編譯成 Bundle

HMR Server: 將熱更新的⽂件輸出給 HMR Rumtime

Bundle server: 提供⽂件在瀏覽器的訪問 (好比localhost:8080/Bundle.js)

HMR Rumtime: 在開發階段的打包階段會被注⼊到瀏覽器端的bundle.js中,瀏覽器端的bundle.js會和瀏覽器創建一個鏈接,一般是一個websocket,這樣就能夠更新文件的變化,當收到文件的一些變化消息時會自動更新文件

bundle.js: 構建輸出的⽂件

⽂件哈希值

⽂件哈希值就是打包後輸出的⽂件名的後綴

⽂件哈希值如何⽣成

Hash:和整個項⽬的構建相關,只要項⽬⽂件有修改,整個項⽬構建的 hash 值就會更改

Chunkhash:和 webpack 打包的 chunk 有關,不一樣的 entry 會⽣成不一樣的 chunkhash 值

Contenthash:根據⽂件內容來定義 hash ,⽂件內容不變,則 contenthash 不變

JS 的⽂件哈希設置

設置 output 的 filename,使⽤ [chunkhash]

output: {
    filename: '[name][chunkhash:8].js',
    path: __dirname + '/dist'
}
複製代碼

注意: chunkhash沒法和熱更新一塊兒使用

CSS 的⽂件哈希設置

設置 MiniCssExtractPlugin 的 filename,

使⽤ [contenthash]

npm install mini-css-extract-plugin -D
複製代碼
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
plugins: [
     new MiniCssExtractPlugin({
         filename: `[name][contenthash:8].css`
     });
]
複製代碼

若是想把css提取出來,那麼style-loader就不能用了,由於兩個是互斥的,因此咱們能夠這樣寫:

module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    - 'style-loader',
                    + MiniCssExtractPlugin.loader
                    'css-loader'
                ]
            },
            {
                test: /\.less$/,
                use: [
                    - 'style-loader',
                    + MiniCssExtractPlugin.loader
                    'css-loader',
                    'less-loader'
                ]
            },
        ]
},
複製代碼

圖片&字體文件哈希設置

module: {
rules: [
    {
        test: /\.(png|svg|jpg|gif)$/,
        use: [{
              loader: 'file-loader’, options: { name: 'img/[name][hash:8].[ext] ' } }] } ] 複製代碼

佔位符介紹

[ext] 資源名後綴

[name] 文件名稱

[path] 文件的相對路徑

[folder] 文件所在的文件夾

[contenthash] 文件的內容hash 默認md5生成

[hash] 文件內容的hash 默認md5生成

代碼壓縮

HTML 壓縮

CSS 壓縮

JS 壓縮

JS ⽂件的壓縮

npm install uglifyjs-webpack-plugin -D

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

plugins: [
     new UglifyJsPlugin()
]
複製代碼

CSS ⽂件的壓縮

npm install optimize-css-assets-webpack-plugin cssnano -D
複製代碼

使⽤ optimize-css-assets-webpack-plugin, 同時使⽤ cssnano[預處理器]

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
plugins: [
     new OptimizeCSSAssetsPlugin({
         assetNameRegExp: /\.css$/g,
         cssProcessor: require('cssnano’) }) ] 複製代碼

html ⽂件的壓縮

npm install html-webpack-plugin -D
複製代碼

使用html-webpack-plugin,設置壓縮參數

webpack.config.js中

const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
     new HtmlWebpackPlugin({
         template: path.join(__dirname, '/dist/index.html’), filename: 'index.html’,
         chunks: ['index’], inject: true, minify: { html5: true, collapseWhitespace: true, //去除空格 preserveLineBreaks: false, //去除換行 minifyCSS: true, minifyJS: true, removeComments: false //去除註釋 } }) ] 複製代碼

自動清理構建⽬錄

避免構建前每次都須要⼿動刪除 dist

使⽤ clean-webpack-plugin

默認會刪除 output 指定的輸出⽬錄

npm install clean-webpack-plugin -D

const CleanWebpackPlugin = require('clean-webpack-plugin');
plugins: [
    new CleanWebpackPlugin(),
]
複製代碼

PostCSS 插件 autoprefixer補充css3前綴

IE: Trident(-ms) 火狐: Geko(-moz) 谷歌: Webkit(-webkit) 歐鵬:Presto(-o)

npm install postcss-loader autoprefixer -D

複製代碼
module: {
        rules: [
            {
                test: /\.less$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'less-loader',
                    {
                        loader: 'postcss-loader',
                        options: {
                            plugins: () => [
                                require('autoprefixer')({
                                    browsers: ['last 2 version', '>1%', 'ios 7']
                                })
                            ]
                        }
                    },
                ]
            },
        ]
    },
複製代碼

移動端 CSS px ⾃動轉換成 rem

rem 是什麼?

W3C 對 rem 的定義: font-size of the root element

rem 和 px 的對⽐:

rem 是相對單位

px 是絕對單位

使⽤ px2rem-loader

⻚⾯渲染時計算根元素的 font-size 值,可使⽤⼿淘的lib-flexible庫

npm install px2rem-loader -D
npm i lib-flexible raw-loader@0.5.1 -S

module: {
        rules: [
            {
                test: /\.less$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'less-loader',
                    {
                        loader: 'postcss-loader',
                        options: {
                            plugins: () => [
                                require('autoprefixer')({
                                    browsers: ['last 2 version', '>1%', 'ios 7']
                                })
                            ]
                        }
                    },
                    {
                        loader: 'px2rem-loader',
                        options: {
                            remUnit: 75,
                            remPrecision: 8 //轉換好rem的小數點位數
                        }
                    }
                ]
            }
        ]
}
複製代碼

index.html中使用raw-loader內聯lib-flexible

<script> ${require('raw-loader!babel-loader!../node_modules/lib-flexible')} </script>
複製代碼

raw-loader 內聯 html

<script> ${require(' raw-loader!babel-loader!. /meta.html')} </script>
複製代碼

多⻚⾯應⽤(MPA)概念

每⼀次⻚⾯跳轉的時候,後臺服務器都會給返回⼀個新的 html ⽂檔, 這種類型的⽹站也就是多⻚⽹站,也叫作多⻚應⽤。

多⻚⾯打包通⽤⽅案: 動態獲取 entry 並設置 html-webpack-plugin

利用glob

webpack.config.js中編寫以下代碼:

npm install glob -D

const glob = require('glob');
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
const setMPA = () => {
    const entry = {};
    const htmlWebpackPlugins = [];
    const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'));

    Object.keys(entryFiles)
        .map((index) => {
            const entryFile = entryFiles[index];

            const match = entryFile.match(/src\/(.*)\/index\.js/);
            const pageName = match && match[1];

            entry[pageName] = entryFile;
            htmlWebpackPlugins.push(
                new HtmlWebpackPlugin({
                    inlineSource: '.css$',
                    template: path.join(__dirname, `src/${pageName}/index.html`),
                    filename: `${pageName}.html`,
                    chunks: ['vendors', pageName],
                    inject: true,
                    minify: {
                        html5: true,
                        collapseWhitespace: true,
                        preserveLineBreaks: false,
                        minifyCSS: true,
                        minifyJS: true,
                        removeComments: false
                    }
                })
            );
        });

    return {
        entry,
        htmlWebpackPlugins
    }
}
const { entry, htmlWebpackPlugins } = setMPA();

module.exports = {
    entry: entry,
    plugins: [].concat(htmlWebpackPlugins)
}
複製代碼

DefinePlugin

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      PRODUCTION: JSON.stringify(true), // const PRODUCTION = true
      VERSION: JSON.stringify('5fa3b9'), // const VERSION = '5fa3b9'
      BROWSER_SUPPORTS_HTML5: true, // const BROWSER_SUPPORTS_HTML5 = 'true'
      TWO: '1+1', // const TWO = 1 + 1,
      CONSTANTS: {
        APP_VERSION: JSON.stringify('1.1.2') // const CONSTANTS = { APP_VERSION: '1.1.2' }
      }
    }),
  ],
}
複製代碼

有了上面的配置,就能夠在應用代碼文件中,訪問配置好的變量了,如:

console.log("Running App version " + VERSION);

if(!BROWSER_SUPPORTS_HTML5) require("html5shiv");
複製代碼

若是配置的值是字符串,那麼整個字符串會被當成代碼片斷來執行,其結果做爲最終變量的值,如上面的 "1+1",最後的結果是 2

若是配置的值不是字符串,也不是一個對象字面量,那麼該值會被轉爲一個字符串,如 true,最後的結果是 'true'

若是配置的是一個對象字面量,那麼該對象的全部 key 會以一樣的方式去定義

這樣咱們就能夠理解爲何要使用 JSON.stringify() 了,由於 JSON.stringify(true) 的結果是 'true',JSON.stringify("5fa3b9") 的結果是 "5fa3b9"。

copy-webpack-plugin

const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  // ...
  plugins: [
    new CopyWebpackPlugin([
      { from: 'src/file.txt', to: 'build/file.txt', }, // 顧名思義,from 配置來源,to 配置目標路徑
      { from: 'src/*.ico', to: 'build/*.ico' }, // 配置項可使用 glob
      // 能夠配置不少項複製規則
    ]),
  ],
}
複製代碼

ProvidePlugin

能夠理解爲更方便的引入,例如jquery,lodash

new webpack.ProvidePlugin({
     _: 'lodash',//import _ from lodash
     $: 'jquery'//import $ from jquery
})
複製代碼

IgnorePlugin

能夠直接使用 webpack.IgnorePlugin 來獲取。

這個插件用於忽略某些特定的模塊,讓webpack不把這些指定的模塊打包進去。例如咱們使用moment.js,直接引用後,裏邊有大量的 i18n的代碼,致使最後打包出來的文件比較大,而實際場景並不須要這些 i18n 的代碼,這時咱們可使用 IgnorePlugin 來忽略掉這些代碼文件,配置以下:

module.exports = {
  // ...
  plugins: [
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
  ]
}
複製代碼

IgnorePlugin 配置的參數有兩個,第一個是匹配引入模塊路徑的正則表達式,第二個是匹配模塊的對應上下文,即所在目錄名。

sourceMap 類型

devtool:

source-map:

開發: cheap-module-eval-source-map

生產: hidden-source-map

基礎庫分離

思路:將 react、react-dom,axios,element-ui 基礎包經過 cdn 引⼊,不打⼊ bundle 中

npm install html-webpack-externals-plugin -D

使⽤ html-webpackexternals-plugin

const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
plugins:[
    new HtmlWebpackExternalsPlugin({
                externals: [
                      {
                        module: 'react',
                        entry: 'cdn.com/react.min.js',
                        global: 'React',
                      },
                      {
                        module: 'axios',
                        entry: 'cdn.com/axios.min.js',
                        global: 'Axios',
                      },
                ]
    }),
]
複製代碼

利⽤ SplitChunksPlugin 進⾏公共腳本分離

SplitChunksPlugin是Webpack4 內置的,替代webpack3的CommonsChunkPlugin插件

async 異步引⼊的庫進⾏分離(默認)

initial 同步引⼊的庫進⾏分離

all 全部引⼊的庫進⾏分離(推薦)

optimization: {
     splitChunks: {
        chunks: 'all',
        minSize: 30000,//抽離的公共包最小的大小
        maxSize: 0, //抽離的公共包最大的大小
        minChunks: 1, //一段代碼多處都使用的次數 若是大於這裏的次數就抽離成公共的文件
        maxAsyncRequests: 5,
        maxInitialRequests: 3,//瀏覽器同時請求的異步js的數量
        name: true,
        cacheGroups: {
            vendors: {
                test: /(axios|react)/,
                priority: -10,
                minChunks: 1
            }
        }
    }
 }
複製代碼

treeShaking(搖樹優化)

概念:1 個模塊可能有多個⽅法,只要其中的某個⽅法使⽤到了,則整個⽂件都會被打到bundle ⾥⾯去,tree shaking 就是隻把⽤到的⽅法打⼊ bundle ,沒⽤到的⽅法會在uglify 階段被擦除掉。

使⽤:webpack4 默認⽀持,在 .babelrc ⾥設置 modules: false 便可 要求:必須是 ES6 的語法,CommonJS 的⽅式不⽀持.

production mode的狀況下默認開啓

treeShaking的狀況:

代碼執⾏的結果不會被⽤到

代碼不會被執⾏,不可到達

代碼只會影響死變量(只寫不讀)
複製代碼

Tree-shaking 原理

利⽤ ES6 模塊的特色:

只能做爲模塊頂層的語句出現

import 的模塊名只能是字符串常量

import binding 是 immutable的

代碼擦除: uglify 階段刪除⽆⽤代碼

若是代碼中有反作用則tree-shaking失效

能夠在package.json中配置sideEffect:[] 好比babel-polyfill

構建後的代碼存在⼤量閉包代碼

⼤量做⽤域包裹代碼,致使體積增⼤(模塊越多越明顯)

運⾏代碼時建立的函數做⽤域變多,內存開銷變⼤

模塊轉換分析

結論:

被 webpack 轉換後的模塊會帶上⼀層包裹

import 會被轉換成 __webpack_require

進⼀步分析 webpack 的模塊機制

分析: 打包出來的是⼀個 IIFE (匿名閉包)

modules 是⼀個數組,每⼀項是⼀個模塊初始化函數

__webpack_require ⽤來加載模塊,返回 module.exports

使用scope hoisting消除大量閉包現象

原理:將全部模塊的代碼按照引⽤順序放在⼀個函數做⽤域⾥,而後適當的重命名⼀些變量以防⽌變量名衝突

必須是 ES6 語法,CJS 不⽀持

plugins: [
 new webpack.optimize.ModuleConcatenationPlugin()
]
複製代碼

模塊懶加載

webpack 有⼀個功能就是將你的代碼庫分割成chunks(語塊),當代碼運⾏到須要它們的時候再進⾏加載。

CommonJS:require.ensure

ES6:動態 import(⽬前尚未原⽣⽀持,須要 babel 轉換)

如何使⽤動態 import?

安裝 babel 插件 ES6:動態 import(⽬前尚未原⽣⽀持,須要 babel 轉換)

在.babelrc中配置:

npm install @babel/plugin-syntax-dynamic-import --save-dev

{
    "plugins": ["@babel/plugin-syntax-dynamic-import"],
}
複製代碼

代碼中的使用:

loadComponent() {
    import('./text.js').then((Text) => {
        this.setState({
            Text: Text.default
        });
    });
}
複製代碼

這樣的話text.js在打包時就會被自動拆分爲一個單獨的文件,當咱們調用這個方法時才進行加載,也算是一個優化手段

webpack 打包庫和組件

webpack 除了能夠⽤來打包應⽤,也能夠⽤來打包 js 庫

實現⼀個加法庫的打包

須要打包壓縮版和⾮壓縮版本

⽀持 AMD/CJS/ESM 模塊引⼊

//src下index.js
export default function add(a, b) {
    return a + b;
}
複製代碼

如何將庫暴露出去?

只對.min 壓縮可使用TerserPlugin插件進行匹配

npm install terser-webpack-plugin -D
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
    entry: {
        'large-number': './src/index.js',
        'large-number.min': './src/index.js'
    },
    output: {
        filename: '[name].js',
        library: 'largeNumber',
        libraryTarget: 'umd',
        libraryExport: 'default'
    },
    mode: 'none',
    optimization: {
        minimize: true,
        minimizer: [
            new TerserPlugin({
                include: /\.min\.js$/,
            })
        ]
    }
}

//index.js中
if (process.env.NODE_ENV === 'production') {
    module.exports = require('./dist/large-number.min.js');
} else {
    module.exports = require('./dist/large-number.js');
}
複製代碼

設置⼊⼝⽂件

package.json 的 main 字段爲 index.js

而後webpack打包就能夠了

SSR

思路:

服務端 使⽤ react-dom/server 的 renderToString ⽅法將 React 組件渲染成字符串,服務端路由返回對應的模板

客戶端

打包出針對服務端的組件

<!--search.html-->
<body>
    <div id="root"><!--HTML_PLACEHOLDER--></div>
</body>
複製代碼

這裏使用註釋進行佔位

const fs = require('fs');
const path = require('path');
const express = require('express');
const { renderToString } = require('react-dom/server');
const SSR = require('../dist/search-server');
const template = fs.readFileSync(path.join(__dirname, 'search.html'), 'utf-8');

const server = (port) => {
    const app = express();

    app.use(express.static('dist'));
    app.get('/search', (req, res) => {
        const html = renderMarkup(renderToString(SSR));
        res.status(200).send(html);
    });

    app.listen(port, () => {
        console.log('Server is running on port:' + port);
    });
};

server(process.env.PORT || 3000);

const renderMarkup = (str) => {
    return template.replace('<!--HTML_PLACEHOLDER-->', str)
}
複製代碼

SSR優化

減小首屏須要的數據量,剔除冗餘數據和請求;

控制好緩存,對數據/頁面進行合理的緩存;

頁面的請求使用流的形式進行傳遞;

如何優化命令⾏的構建⽇志

使⽤ friendly-errors-webpack-plugin

const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
plugins: [
    new FriendlyErrorsWebpackPlugin()
],
stats: 'errors-only'
複製代碼

構建錯誤上報

plugins:[
    new FriendlyErrorsWebpackPlugin(),
    function() {
            this.hooks.done.tap('done', (stats) => {
                if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf('--watch') == -1)
                {
                    console.log('錯誤上報');
                    process.exit(1);
                }
            })
        }  
]
複製代碼

compiler 在每次構建結束後會觸發 done 這 個 hook

webpack4中是this.hooks.done.tap webpack3中是this.plugin

Node.js 中的 process.exit 規範

0 表示成功完成,回調函數中,err 爲 null

⾮ 0 表示執⾏失敗,回調函數中,err 不爲 null,err.code 就是傳給 exit 的數字

發佈到 npm

升級補丁版本號:npm version patch  //通常是修改了bug

升級小版本號:npm version minor //通常是發佈了feture

升級大版本號:npm version major //通常是重大更新
複製代碼

webpack優化篇

webpack-bundle-analyzer 分析體積

npm install webpack-bundle-analyzer -D
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
plugins:[
    new BundleAnalyzerPlugin(),
]
複製代碼

構建完成後會在 8888 端口展現大小

通常推薦使用高版本的node和webpack,由於他們內部本身作了優化

使用 webpack4的緣由

V8 帶來的優化(for of 替代 forEach、Map 和 Set 替代 Object、includes 替代 indexOf)

默認使用更快的 md4 hash 算法

webpack AST 能夠直接從 loader 傳遞給 AST,減小解析時間

使用字符串方法替代正則表達式

多進程/多實例構建:資源並行解析可選方案

npm install cache-loader thread-loader -D
複製代碼

使用 thread-loader 解析資源

原理:每次 webpack 解析一個模塊,thread-loader 會將它及它的依賴分配給 worker 線程

module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                     {
                         loader: 'thread-loader',
                         options: {
                             workers: 3
                         }
                     },
                     'cache-loader',//使用cacheDirectory,能夠緩存編譯結果,避免屢次重複編譯;
                     'babel-loader',
                ]
        },            
    ]
 }
複製代碼

多進程/多實例:並行壓縮

方式一:使用 parallel-uglify-plugin 插件

plugins:[
 new ParallelUglifyPlugin({
    uglifyJS:{
        output:{
            beautify:false,
            comments:false
        },
        compress:{
            warning:false,
            drop_console:true,
            collapse_vars:true,
            reduce_vars:true
        }
    }
 })
]
複製代碼

方式二:uglifyjs-webpack-plugin 開啓 parallel 參數

plugins:[
 new UglifyJsPlugin({
    uglifyOptions:{
        warning:false
    },
    parallel:true
 })
]
複製代碼

方法三:terser-webpack-plugin 開啓 parallel 參數

optimization:{
    minimizer:[
        new TerserPlugin({
            parallel:4
        })
    ]
}
複製代碼

縮小構建目標

目的:儘量的少構建模塊

好比 babel-loader 不解析 node_modules

rules: [
            {
                test: /\.js$/,
                exclude: 'node_modules',//忽略node_moudles,避免編譯第三方庫中已經被編譯過的代碼;
                use: [
                     'babel-loader',
                ]
            }
]
複製代碼

減小文件搜索範圍

優化 resolve.extensions 配置

合理使用 alias

resolve: {
    alias: {
         'components': path.resolve(__dirname, './src/components'),
         'util': path.resolve(__dirname, './src/util'),
    },
    extensions: ['.js']
}

複製代碼

noParse

在 webpack 中,咱們須要使用的 loader 是在 module.rules 下配置的,webpack 配置中的 module 用於控制如何處理項目中不一樣類型的模塊。

除了 module.rules 字段用於配置 loader 以外,還有一個 module.noParse 字段,能夠用於配置哪些模塊文件的內容不須要進行解析。對於一些不須要解析依賴(即無依賴) 的第三方大型類庫等,能夠經過這個字段來配置,以提升總體的構建速度。

使用 noParse 進行忽略的模塊文件中不能使用 import、require、define 等導入機制。

module.exports = {
  // ...
  module: {
    noParse: /jquery|lodash/, // 正則表達式

    // 或者使用 function
    noParse(content) {
      return /jquery|lodash/.test(content)
    },
  }
}
複製代碼

圖片壓縮

使用:配置 image-webpack-loader

{
        test: /\.(png|jpg|gif|jpeg)$/,
        use: [
            {
                loader: 'file-loader',
                options: {
                    name: '[name]_[hash:8].[ext]'
                }
            },
            {
                loader: 'image-webpack-loader',
                options: {
                  mozjpeg: {
                    progressive: true,
                    quality: 65
                  },
                  optipng: {
                    enabled: false,
                  },
                  pngquant: {
                    quality: '65-90',
                    speed: 4
                  },
                  gifsicle: {
                    interlaced: false,
                  },
                  webp: {
                    quality: 75
                  }
        }
}
複製代碼

無用的 CSS 如何刪除掉?

PurifyCSS: 遍歷代碼,識別已經用到的 CSS class 這個須要和 mini-css-extract-plugin 配合使用

也就是說先提取爲css文件而後再使用PurifyCSS

npm install purgecss-webpack-plugin

const PurgecssPlugin = require('purgecss-webpack-plugin');
plugins:[
    new MiniCssExtractPlugin({
            filename: '[name]_[contenthash:8].css'
    }),
    new PurgecssPlugin({
        paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`,  { nodir: true }),
    })
]
複製代碼

構建體積優化

咱們可使用動態 Polyfill -> Polyfill Service

原理:識別 User Agent,下發不一樣的 Polyfill

如何使用動態 Polyfill service

polyfill.io 官方提供的服務:(引入到index.html中)

<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>
複製代碼

體積優化總結

Scope Hoisting

Tree-shaking

公共資源分離

圖片壓縮

動態 Polyfill

原理篇

loader開發

//raw-loader.js
module.exports = function(source) {
    const json = JSON.stringify(source)
        .replace('foo', '')
        .replace(/\u2028/g, '\\u2028')
        .replace(/\u2029/g, '\\u2029');
    return `export default ${json}`;
}
複製代碼

咱們這個loader是把指定文件中的foo字符替換爲空的 使用loader-runner測試loader是否正常工做:

npm install loader-runner -D

loader-runner用法具體能夠去github查看

const { runLoaders } = require('loader-runner');
const fs = require('fs');
const path = require('path');

runLoaders({
    resource: path.join(__dirname, './src/demo.txt'),//資源路徑
    loaders: [
        {
            loader: path.join(__dirname, './src/raw-loader.js'),//指定loader路徑
        }
    ],
    context: {
        minimize:true
    },
    readResource: fs.readFile.bind(fs)
}, (err, result) => {
    err ? console.log(err) : console.log(result);
});
複製代碼

loader 的參數獲取

npm install loader-utils -D
複製代碼

經過 loader-utils 的 getOptions 方法獲取

const loaderUtils = require("loader-utils");
module.exports = function(content) {
    const { name } = loaderUtils.getOptions(this);
};
複製代碼

loader 異常處理

loader 內直接經過 throw 拋出,經過 this.callback 傳遞錯誤

this.callback(
    err: Error | null, content: string | Buffer, sourceMap?: SourceMap, meta?: any
);
複製代碼

loader 的異步處理

經過 this.async 來返回一個異步函數

第一個參數是 Error,第二個參數是處理的結果

module.exports = function(input) {
    const callback = this.async();
    this.cacheable(false)
    // No callback -> return synchronous results
    // if (callback) { ... }
    callback(null, input + input);
};
複製代碼

this.async()第一個參數也是err對象,第二個參數是數據

在 loader 中使用緩存

webpack 中默認開啓 loader 緩存

可使用 this.cacheable(false) 關掉緩存

緩存條件: loader 的結果在相同的輸入下有肯定的輸出

有依賴的 loader 沒法使用緩存

loader 如何進行文件輸出?

經過 this.emitFile 進行文件寫入

const loaderUtils = require("loader-utils");
module.exports = function(content) {
    const url = loaderUtils.interpolateName(this, "[hash].[ext]", {
    content});
    this.emitFile(url, content);
    const path = `__webpack_public_path__ + ${JSON.stringify(url)};`;
    return `export default ${path}`;
};
複製代碼

interpolateName方法是用於替換佔位符,__webpack_public_path__是webpack的全局變量

plugin開發

插件沒有像 loader 那樣的獨立運行環境, 只能在 webpack 裏面運行

插件的運行環境搭建

const path = require('path');
const MyPlugin = require('./plugins/myPlugin');//插件開發階段的路徑

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'main.js'
    },
    mode: 'production',
    plugins: [
        new MyPlugin({
            name: 'myname'
        })
    ]
}
複製代碼

開發最簡單的插件

module.exports = class MyPlugin { //MyPlugin是插件名
    constructor(options){
        this.options = options
    }
    apply(compiler){ //必須是apply
        console.log('個人插件執行了');
        console.log('個人插件配置項',this.options)
    }
}
複製代碼

插件的錯誤處理

參數校驗階段能夠直接 throw 的方式拋出

throw new Error(「 Error Message」)
複製代碼

若是已經進入hooks邏輯,那麼能夠經過 compilation 對象的 warnings 和 errors 接收

compilation.warnings.push("warning");
compilation.errors.push("error");
複製代碼

插件文件寫入

文件生成階段webpack會調用emit這個hooks,因此咱們能夠監聽emit階段進行操做

文件寫入須要使用 [webpack-sources]((www.npmjs.com/package/web…)

const { RawSource } = require("webpack-sources");
module.exports = class DemoPlugin {
    constructor(options) {
    this.options = options;
    }
    apply(compiler) {
        const { name } = this.options;
        compiler.plugin("emit", (compilation, cb) => {
            compilation.assets[name] = new RawSource("demo");//demo爲文件內容,name爲文件的名稱
            cb();
        });
    }
};
複製代碼

總結

相信本文應該已經涵蓋了webpack最常使用的點以及如何進行優化,感興趣的小夥伴能夠在文章下方進行交流哦

相關文章
相關標籤/搜索