帶你走進webpack世界,成爲webpack頭號玩家。

最近朋友圈被《頭號玩家》刷爆了,斯皮爾伯格一個資深電影導演,把對過去經典的致敬,對將來的憧憬濃縮在這一部電影中,能夠說讓觀衆燃了起來。javascript

觀望整個前端開發,不斷的演化,發展迅速。前端開發從最開始切頁面, 前端自動化構建工具突飛猛進,從最初的Grunt,Gulp到如今前端項目能夠說標配的webpack。css

咱們先來致敬經典:html

 

 

 

 

1. 什麼是webpack?

能夠看作一個模塊化打包機,分析項目結構,處理模塊化依賴,轉換成爲瀏覽器可運行的代碼。前端

  • 代碼轉換: TypeScript 編譯成 JavaScript、SCSS,LESS 編譯成 CSS.
  • 文件優化:壓縮 JavaScript、CSS、HTML 代碼,壓縮合並圖片。
  • 代碼分割:提取多個頁面的公共代碼、提取首屏不須要執行部分的代碼讓其異步加載。
  • 模塊合併:在採用模塊化的項目裏會有不少個模塊和文件,須要構建功能把模塊分類合併成一個文件。
  • 自動刷新:監聽本地源代碼的變化,自動從新構建、刷新瀏覽器。

構建把一系列前端代碼自動化去處理複雜的流程,解放生產力。java

2. 進入webpack世界

初始化項目

    npm install webpack webpack-cli -D

 

webpack4抽離出了webpack-cli,因此咱們須要下載2個依賴。node

Webpack 啓動後會從Entry裏配置的Module開始遞歸解析 Entry 依賴的全部 Module。 每找到一個 Module, 就會根據配置的Loader去找出對應的轉換規則,對 Module 進行轉換後,再解析出當前 Module 依賴的 Module。 這些模塊會以 Entry 爲單位進行分組,一個 Entry 和其全部依賴的 Module 被分到一個組也就是一個 Chunk。最後 Webpack 會把全部 Chunk 轉換成文件輸出。 在整個流程中 Webpack 會在恰當的時機執行 Plugin 裏定義的邏輯。react

webpack須要在項目根目錄下建立一個webpack.config.js來導出webpack的配置,配置多樣化,能夠自行定製,下面講講最基礎的配置。webpack

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'main.js',
    }
}

 

  • entry表明入口,webpack會找到該文件進行解析
  • output表明輸入文件配置
  • path把最終輸出的文件放在哪裏
  • filename輸出文件的名字

有時候咱們的項目並非spa,須要生成多個js html,那麼咱們就須要配置多入口。git

module.exports = {
    entry: {
        pageA: './src/pageA.js',
        pageB: './src/pageB.js'
    },
    output: {
        path: path.join(__dirname, './dist'),
        filename: '[name].[hash:8].js',
    },
}

 

entry配置一個對象,key值就是chunk: 代碼塊,一個 Chunk 由多個模塊組合而成,用於代碼合併與分割。看看filename[name]: 這個name指的就是chunk的名字,咱們配置的key值pageA pageB,這樣打包出來的文件名是不一樣的,再來看看[hash],這個是給輸出文件一個hash值,避免緩存,那麼:8是取前8位。github

這裏有人會有疑問了,項目是多頁面的,應該有pageA.html``pageA.js``pageA.css, 那麼我應該生成多個html,這個只是作了JS的入口區分,我不想每個頁面都去複製粘貼一個html,而且html是大部分重複的,可能不一樣頁面只須要修改title,下面來看看這個問題怎麼解決:

須要引入一個webpack的plugin:

npm install html-webpack-plugin -D

 

該插件能夠給每個chunk生成html,指定一個template,能夠接收參數,在模板裏面使用,下面來看看如何使用:

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

module.exports = {
    entry: {
        pageA: './src/pageA.js',
        pageB: './src/pageB.js'
    },
    output: {
        path: path.join(__dirname, './dist'),
        filename: '[name].[hash:8].js',
    },
    plugins: [
         new HtmlWebpackPlugin({
            template: './src/templet.html',
            filename: 'pageA.html',
            title: 'pageA',
            chunks: ['pageA'],
            hash: true,
            minify: {
                removeAttributeQuotes: true
            }
        }),
        new HtmlWebpackPlugin({
            template: './src/templet.html',
            filename: 'pageB.html',
            title: 'pageB',
            chunks: ['pageB'],
            hash: true,
            minify: {
                removeAttributeQuotes: true
            }
        }),
    ]
}

 

在webpack中,插件的引入順序沒有規定,這個在後面在繼續詳說。

  • template: html模板的路徑地址
  • filename: 生成的文件名
  • title: 傳入的參數
  • chunks: 須要引入的chunk
  • hash: 在引入JS裏面加入hash值 好比: <script src='index.js?2f373be992fc073e2ef5'></script>
  • removeAttributeQuotes: 去掉引號,減小文件大小<script src=index.js></script>
  • 具體文檔

這樣在dist目錄下就生成了pageA.html和pageB.html而且經過配置chunks,讓pageA.html里加上了script標籤去引入pageA.js。那麼如今還剩下css沒有導入,css須要藉助loader去作,因此如今要下載幾個依賴,以scss爲例,less同理

npm install css-loader style-loader sass-loader node-sass -D
  • css-loader: 支持css中的import
  • style-loader: 把css寫入style內嵌標籤
  • sass-loader: scss轉換爲css
  • node-sass: scss轉換依賴

來看看如何配置loader

module.exports = {
    module: {
        rules: [
                {
                    test: /\.scss$/,
                    use: ['style-loader', 'css-loader', 'sass-loader'],
                    exclude: /node_modules/
                }
        ]
    }
}

 

  • test: 一個正則表達式,匹配文件名
  • use: 一個數組,裏面放須要執行的loader,倒序執行,從右至左。
  • exclude: 取消匹配node_modules裏面的文件

若是想把css做爲一個單獨的文件,須要用到一個插件來作(webpack4.0.0以上版本須要next版本):

 npm i extract-text-webpack-plugin@next -D

 

const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'main.js',
    },
    module: {
        rules: [
            {
                test: /\.scss$/,
                use: ExtractTextPlugin.extract({
                    // style-loader 把css直接寫入html中style標籤
                    fallback: 'style-loader',
                    // css-loader css中import支持
                    // loader執行順序 從右往左執行
                    use: ['css-loader', 'sass-loader']
                }),
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new ExtractTextPlugin('[name].[contenthash:8].css'),
    ]
}

 

  • 須要在plugins里加入插件name: chunk名字 contenthash:8: 根據內容生成hash值取前8位
  • 修改loader配置下的use: fallback: 兼容方案

這樣就實現了js,html,css的打包,那麼再來看看一些經常使用的loader:

  • babel-loader: 用babel轉換代碼
  • url-loader: 依賴於file-loader,把圖片轉換成base64嵌入html,若是超出必定閾值則交給file-loader
rules: [
         // 處理js
         {
            test: /\.js?$/,
            exclude: /node_modules/,
            use: ['babel-loader']
        },
        // 處理圖片
        {
            test: /\.(png|jpg|gif|ttf|eot|woff(2)?)(\?[=a-z0-9]+)?$/,
            use: [{
                loader: 'url-loader',
                options: {
                    query: {
                        // 閾值 單位byte
                        limit: '8192',
                        name: 'images/[name]_[hash:7].[ext]',
                    }
                }
            }]
        },
    ]

 

babel的配置建議在根目錄下新建一個.babelrc文件

{
    "presets": [
        "env",
        "stage-0", 
        "react"
    ],
    "plugins": [
        "transform-runtime",
        "transform-decorators-legacy",
        "add-module-exports"
    ]
}

 

  • presets: 預設, 一個預設包含多個插件 起到方便做用 不用引用多個插件
  • env: 只轉換新的句法,例如const let => ..等 不轉換 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise、Object.assign。
  • stage-0: es7提案轉碼規則 有 0 1 2 3 階段 0包含 1 2 3裏面的全部
  • react: 轉換react jsx語法
  • plugins: 插件 能夠本身開發插件 轉換代碼(依賴於ast抽象語法數)
  • transform-runtime: 轉換新語法,自動引入polyfill插件,另外能夠避免污染全局變量
  • transform-decorators-legacy: 支持裝飾器
  • add-module-exports: 轉譯export default {}; 添加上module.exports = exports.default 支持commonjs

由於咱們在文件名中加入hash值,打包屢次後dist目錄變得很是多文件,沒有刪除或覆蓋,這裏能夠引入一個插件,在打包前自動刪除dist目錄,保證dist目錄下是當前打包後的文件:

plugins: [
    new CleanWebpackPlugin(
        // 須要刪除的文件夾或文件
        [path.join(__dirname, './dist/*.*')],
        {
            // root目錄
            root: path.join(__dirname, './')
        }
    ),
]

 

指定extension以後能夠不用在require或是import的時候加文件擴展名,會依次嘗試添加擴展名進行匹配:

resolve: {
    extensions: ['.js', '.jsx', '.scss', '.json'],
},

 

3. 優化實戰 高級裝備

天下武功惟快不破,優化方案千千萬萬,各取所需吧。

提出公共的JS文件

webpack4中廢棄了webpack.optimize.CommonsChunkPlugin插件,用新的配置項替代

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'main.js',
    },
    optimization: {
        splitChunks: {
            cacheGroups: {
                commons: {
                    chunks: 'initial',
                    minChunks: 2,
                    maxInitialRequests: 5,
                    minSize: 2,
                    name: 'common'
                }
            }
        }
    },
}

 

把屢次import的文件打包成一個單獨的common.js

HappyPack

在webpack運行在node中打包的時候是單線程去一件一件事情的作,HappyPack能夠開啓多個子進程去併發執行,子進程處理完後把結果交給主進程

 npm i happypack -D

 

須要改造一下loader配置,此loader用子進程去處理

const HappyPack = require('happypack');
module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'main.js',
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                exclude: /node_modules/,
                use: 'happypack/loader?id=babel',
            },
        ]
    },
    plugins: [
        new HappyPack({
            id: 'babel',
            threads: 4,
            loaders: ['babel-loader']
        }),
    ]
}

 

  • id: id值,與loader配置項對應
  • threads: 配置多少個子進程
  • loaders: 用什麼loader處理
  • 具體文檔

做用域提高

若是你的項目是用ES2015的模塊語法,而且webpack3+,那麼建議啓用這一插件,把全部的模塊放到一個函數裏,減小了函數聲明,文件體積變小,函數做用域變少。

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'main.js',
    },
    plugins: [
        new webpack.optimize.ModuleConcatenationPlugin(),
    ]
}

 

提取第三方庫

方便長期緩存第三方的庫,新建一個入口,把第三方庫做爲一個chunk,生成vendor.js

module.exports = {
    entry: {
        main: './src/index.js',
        vendor: ['react', 'react-dom'],
    },
}

 

DLL動態連接

第三庫不是常常更新,打包的時候但願分開打包,來提高打包速度。打包dll須要新建一個webpack配置文件,在打包dll的時候,webpack作一個索引,寫在manifest文件中。而後打包項目文件時只須要讀取manifest文件。

webpack.vendor.js

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

module.exports = {
    entry: {
        vendor: ['react', 'react-dom'],
    },
    output: {
        path: path.join(__dirname, './dist'),
        filename: 'dll/[name]_dll.js',
        library: '_dll_[name]',
    },
    plugins: [
        new webpack.DllPlugin({
            path: path.join(__dirname, './dist/dll', 'manifest.json'),
            name: '_dll_[name]',
        }),
    ]
};

 

path: manifest文件的輸出路徑 name: dll暴露的對象名,要跟output.library保持一致 context: 解析包路徑的上下文,這個要跟接下來配置的dll user一致

webpack.config.js

module.exports = {
    entry: {
        main: './src/index.js',
        vendor: ['react', 'react-dom'],
    },
    plugins: [
        new webpack.DllReferencePlugin({
            manifest: path.join(__dirname, './dist/dll', 'manifest.json')
        })
    ]
}

 

html

<script src="vendor_dll.js"></script> 

4. 線上和線下

在生產環境和開發環境其實咱們的配置是存在相同點,和不一樣點的,爲了處理這個問題,會建立3個文件:

  • webpack.base.js: 共同的配置
  • webpack.dev.js: 在開發環境下的配置
  • webpack.prod.js: 在生產環境的配置

經過webpack-merge去作配置的合併,好比:

開發環境

const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const base = require('./webpack.base');

const dev = {
    devServer: {
        contentBase: path.join(__dirname, '../dist'),
        port: 8080,
        host: 'localhost',
        overlay: true,
        compress: true,
        open:true,
        hot: true,
        inline: true,
        progress: true,
    },
    devtool: 'inline-source-map',
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NamedModulesPlugin(),
    ]
}
module.exports = merge(base, dev);

 

開發環境中咱們能夠啓動一個devServer靜態文件服務器,預覽咱們的項目,引入base配置文件,用merge去合併配置。

  • contentBase: 靜態文件地址
  • port: 端口號
  • host: 主機
  • overlay: 若是出錯,則在瀏覽器中顯示出錯誤
  • compress: 服務器返回瀏覽器的時候是否啓動gzip壓縮
  • open: 打包完成自動打開瀏覽器
  • hot: 模塊熱替換 須要webpack.HotModuleReplacementPlugin插件
  • inline: 實時構建
  • progress: 顯示打包進度
  • devtool: 生成代碼映射,查看編譯前代碼,利於找bug
  • webpack.NamedModulesPlugin: 顯示模塊的相對路徑

生產環境

再來看看生產環境最重要的代碼壓縮,混淆:

const path = require('path');
const merge = require('webpack-merge');
const WebpackParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
const base = require('./webpack.base');

const prod = {
    plugins: [
        // 文檔: https://github.com/gdborton/webpack-parallel-uglify-plugin
        new WebpackParallelUglifyPlugin(
            {
                uglifyJS: {
                    mangle: false,
                    output: {
                        beautify: false,
                        comments: false
                    },
                    compress: {
                        warnings: false,
                        drop_console: true,
                        collapse_vars: true,
                        reduce_vars: true
                    }
                }
            }
        ),
    ]
}
module.exports = merge(base, prod);

 

webpack-parallel-uglify-plugin能夠並行壓縮代碼,提高打包效率

uglifyJS配置:

  • mangle: 是否混淆代碼
  • output.beautify: 代碼壓縮成一行 true爲不壓縮 false壓縮
  • output.comments: 去掉註釋
  • compress.warnings: 在刪除沒用到代碼時 不輸出警告
  • compress.drop_console: 刪除console
  • compress.collapse_vars: 把定義一次的變量,直接使用,取消定義變量
  • compress.reduce_vars: 合併屢次用到的值,定義成變量
  • 具體文檔

5. 成爲頭號玩家

想要成爲頭號玩家,玩轉配置可不行,固然還要作一些loader和plugin的開發,去爲項目作一些優化,解決痛點。

loader

loader是一個模塊導出函數,在正則匹配成功的時候調用,webpack把文件數組傳入進來,在this上下文能夠訪問loader API

  • this.context: 當前處理文件的所在目錄,假如當前 Loader 處理的文件是 /src/main.js,則 this.context 就等於 /src。
  • this.resource: 當前處理文件的完整請求路徑,包括 querystring,例如 /src/main.js?name=1。
  • this.resourcePath: 當前處理文件的路徑,例如 /src/main.js。
  • this.resourceQuery: 當前處理文件的 querystring。
  • this.target: 等於 Webpack 配置中的 Target
  • this.loadModule: 但 Loader 在處理一個文件時,若是依賴其它文件的處理結果才能得出當前文件的結果時, 就能夠經過 - - - this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去得到 request 對應文件的處理結果。
  • this.resolve: 像 require 語句同樣得到指定文件的完整路徑,使用方法爲 resolve(context: string, request: string, callback: function(err, result: string))。
  • this.addDependency: 給當前處理文件添加其依賴的文件,以便再其依賴的文件發生變化時,會從新調用 Loader 處理該文件。使用方法爲 addDependency(file: string)。
  • this.addContextDependency: 和 addDependency 相似,但 addContextDependency 是把整個目錄加入到當前正在處理文件的依賴中。使用方法爲 addContextDependency(directory: string)。
  • this.clearDependencies: 清除當前正在處理文件的全部依賴,使用方法爲 clearDependencies()。
  • this.emitFile: 輸出一個文件,使用方法爲 emitFile(name: string, content: Buffer|string, sourceMap: {...})。
  • this.async: 返回一個回調函數,用於異步執行。

下面來看看less-loaderstyle-loader如何實現:

let less = require('less');
module.exports = function (source) {
    const callback = this.async();
    less.render(source, (err, result) => {
        callback(err, result.css);
    });
}
module.exports = function (source) {
    let script = (`
      let style = document.createElement("style");
      style.innerText = ${JSON.stringify(source)};
      document.head.appendChild(style);
   `);
    return script;
}

 

plugin

webpack整個構建流程有許多鉤子,開發者能夠在指定的階段加入本身的行爲到webpack構建流程中。插件由如下構成:

  • 一個 JavaScript 命名函數。
  • 在插件函數的 prototype 上定義一個 apply 方法。
  • 指定一個綁定到 webpack 自身的事件鉤子。
  • 處理 webpack 內部實例的特定數據。
  • 功能完成後調用 webpack 提供的回調。

整個webpack流程由compiler和compilation構成,compiler只會建立一次,compilation若是開起了watch文件變化,那麼會屢次生成compilation. 那麼這2個類下面生成了須要事件鉤子

compiler hooks 文檔 compilation hooks 文檔

寫一個小插件,生成全部打包的文件列表(webpack4不推薦使用compiler.plugin來註冊插件,webpack5將不支持):

class FileListPlugin{
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
        compiler.hooks.emit.tap('FileListPlugin',function (compilation) {
            let fileList = 'filelist:\n\n';
            for (let filename in compilation.assets) {
                fileList += ('- '+filename+'\n');
            }
            compilation.assets['filelist.md']={
                source() {
                    return fileList;
                },
                size() {
                    return fileList.length
                }
            }
        });
    }
}
module.exports = FileListPlugin;

 

6. 最後

都讀在這裏了,還不點個贊嗎。

做者:lihuanji 連接:https://juejin.im/post/5ac9dc9af265da23884d5543 來源:掘金 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索