用於前端開發的webpack4配置[帶註釋]

❤️以爲不錯點個贊喲❤️。原文連接javascript

隨着web開發變得愈來愈複雜,咱們須要使用工具來幫助咱們構建現代化網站。這是一個完整經過複雜webpack4配置的線上生產例子。php

構建現代化網站已經成爲自定義應用程序開發,網站指望能作的更多,具備傳統應用的功能,而不只僅是一個推廣網站。css

隨着一個流程變得複雜時,咱們就會將它分解爲可管理的組件,並使用工具進行自動化構建,好比製造汽車、起草法案[法律文件]、創建網站。html

使用正確的工具完成工做前端

像webpack這樣的工具一直處於現代web開發的最前沿,正是由於如此,它幫助咱們構建複雜的事物。vue

webpack4擁有一些意想不到的改進,最吸引個人就是它在構建速度上面變得有多快,因此我決定採用它。java

hold住,由於這是一篇充滿大量信息的長篇文章。node

採用webpack

一年多之前,我發表了一篇文章: A Gulp Workflow for Frontend Development Automation[用於前端自動化的gulp工做流],講解了如何使用gulp完成一樣的事情。然而在這段時間裏,我愈來愈多地使用像Vue-JSGraphQL這樣的前端框架,如Using VueJS + GraphQL to make Practical Magic這篇文章。android

我發現webpack讓我更容易的去構建各類類型的網站以及應用程序,並且它也容許我使用最現代化的工具鏈。webpack

還有其餘選擇:

  • Laravel Mix是基於webpack的構建工具層,它十分簡潔,你能夠快速啓動並運行,它能夠在90%的時間內完成你想要的任務,但剩下的10%不管如何都會進入到webpack,目前還不支持webpack4。

  • 若是你只是用VueJS前端框架,那麼使用vue-cli是個不錯的選擇,它也是基於webpack,大部分時間均可以工做,而且爲你作一些意想不到的事情。但一樣的,當它提供的功能已經知足不了你的需求,你仍是須要用到webpack,並且我並非只使用VueJS。

  • Neutrino也是基於webpack,咱們能夠關注博客:Neutrino: How I Learned to Stop Worrying and Love Webpack。神奇的點就是它能夠經過像搭樂高積木同樣去配置webpack,但學習使用讓的成本跟學習webpack其實差不了多少。

若是你選擇上述工具(或者其餘工具),我不會對你提出任:它們都是基於webpack封裝。

理解開發系統中層是如何工做的是有好處的。

最終,你只須要決定你但願站在前端技術金字塔中的哪一個位置。

某些時候,我認爲了解像webpack這樣重要的工具是如何工做是有意義的。不久前,我向Sean Larkin(webpack核心團隊成員之一)抱怨說webpack就像一個「黑匣子」,他的回答簡潔卻很是精闢:

It’s only black if you haven’t opened it.[若是你沒有打開這個所謂的「黑匣子」,它永遠都是未知的。]

他說的對,是時候打開「黑匣子」了。

本文不會教你全部關於webpack的知識,甚至是如何安裝它,下面有不少、資料給你選擇,你能夠選擇你認爲不錯的方式:

這樣的資料還有不少,相反地本文將用webpack4配置一個複雜的完整工做例子,並添加註釋。你可使用完整的示例,也可使用它的部分配置項,但但願你能夠從中學到一些東西。在我學習webpack的過程當中,我發現有不少教程視頻,一堆文章給你將如何安裝它並添加一些基礎配置,但卻大部分沒有實際線上生產環境的webpack配置示例,因此我寫了這篇文章。

WHAT WE GET OUT OF THE BOX

當我開始經過打開「黑匣子」來學習webpack時,我有一份我依賴的技術列表,我想將它成爲構建流程的一部分。我也會花時間四處看看,看看在這個過程當中,我還能採用什麼。

正如在文章 A Pretty Website Isn’t Enough article討論的那樣,網站性能一直都是我關注的重點,因此在配置webpack過程當中關注性能問題也很正常。

因此這是我想用webpack爲我處理的事情,以及我但願在構建過程當中加入的技術:

  • Development / Production —— 在本地開發中,我經過webpack-dev-server進行快速構建,對於生產環境的構建(一般經過buddy.works在Docker容器中構建),我但願儘量優化每個點。所以,咱們區分devprod的配置以及構建。

  • Hot Module Replacement —— 當我修改了js、css或者頁面,我但願網頁可以自動刷新,大幅度提升了開發效率:不須要你去點瀏覽器刷新按鈕。

  • Dynamic Code Splitting —— 我不想手動在配置文件中定義js chunk,因此我讓webpack幫我解決這個問題。

  • Lazy Loading —— 又稱異步動態模塊加載,在須要時加載所需的代碼資源。

  • Modern & Legacy JS Bundles —— 我想將es2015 + JavaScript模塊發佈到可以支持全球75%以上的瀏覽器上,同時爲低版本的瀏覽器提供一個補丁包(包括全部轉碼和polyfills)。

  • Cache Busting via manifest.json —— 可讓咱們爲靜態資源設置緩存,同時保證它們在更改使自動從新緩存。

  • Critical CSS —— 根據文章Implementing Critical CSS on your website,能夠提升首頁面的加載速度。

  • Workbox Service Worker —— 咱們可使用Google的Workbox項目爲咱們建立一個Service Worker ,瞭解咱們項目的全部東西[這句翻譯的有點問題,能夠看原文理解]。PWA,咱們來了!

  • PostCSS —— 我認爲它是「css的babel」,像sass和scss都是基於它來構建,它讓你可使用即將推出的css功能。

  • Image Optimization —— 目前,圖片仍然是大部分網頁呈現的主要內容,因此能夠經過mozjpegoptipngsvgo等自動化工具來壓縮優化圖片資源是頗有必要的。

  • Automatic .webp Creation —— Chrome、Edge和FireFox都支持.webp文件,它比jpeg體積更小,節省資源。

  • VueJS —— VueJs是我此次用的前端框架,我但願可以經過單個文件.vue組件做爲開發過程的一部分。

  • Tailwind CSS —— Tailwind是一個實用程序優先的css,我用它在本地開發中快速進行原型設計,而後經過PurgeCss進行生產,從而減少體積。

哇,看起來至關豐富的清單!

還有不少東西,好比JavaScript自動化、css壓縮以及其餘標準配置,去構建咱們指望的前端系統。

我還但願它能夠給開發團隊使用,開發團隊可使用不一樣的工具應用在他們的本地開發環境,並使配置易於維護以及能夠被其餘項目重用。

The importance of maintainability and reusability can’t be understated [可維護性和複用性是很是重要的。]

你使用的前端框架或者技術棧能夠跟個人不同,但應用的規則實際上是相同的,因此請繼續閱讀其他部分,無論你用的是什麼技術棧!

PROJECT TREE & ORGANIZATION

爲了讓你瞭解程序的總體架構,這裏展現一個簡單的項目樹:

├── example.env
├── package.json
├── postcss.config.js
├── src
│   ├── css
│   │   ├── app.pcss
│   │   ├── components
│   │   │   ├── global.pcss
│   │   │   ├── typography.pcss
│   │   │   └── webfonts.pcss
│   │   ├── pages
│   │   │   └── homepage.pcss
│   │   └── vendor.pcss
│   ├── fonts
│   ├── img
│   │   └── favicon-src.png
│   ├── js
│   │   ├── app.js
│   │   └── workbox-catch-handler.js
│   └── vue
│       └── Confetti.vue
├── tailwind.config.js
├── templates
├── webpack.common.js
├── webpack.dev.js
├── webpack.prod.js
├── webpack.settings.js
└── yarn.lock
複製代碼

完整的代碼能夠查看: annotated-webpack-4-config

在覈心配置文件方法,包括:

  • .env—— webpack-dev-server特定於開發環境的設置,不須要在git中檢查

  • webpack.settings.js —— 一個JSON-ish設置文件,咱們須要在項目之間編輯的惟一文件

  • webpack.common.js —— 相同類型的構建放在統一設置文件

  • webpack.dev.js —— 設置本地開發各個構建

  • webpack.prod.js —— 設置生產環境各個構建

這是一個如何將以上配置組合成的圖表:

目標是你只須要編輯項目之間的金色圓角區域(.env&webpack.settings.js)。

以這種形式分離出來使得配置文件使用變得更加容易,即便你最終修改了我原先提供的各類webpack配置文件,但保持這種方式有助於你長期去對配置文件進行維護。

彆着急,咱們等下會詳細介紹每一個文件。

ANNOTATED PACKAGE.JSON

讓咱們從修改咱們的package.json開始入手:

{
    "name": "example-project",
    "version": "1.0.0",
    "description": "Example Project brand website",
    "keywords": [
        "Example",
        "Keywords"
    ],
    "homepage": "https://github.com/example-developer/example-project",
    "bugs": {
        "email": "someone@example-developer.com",
        "url": "https://github.com/example-developer/example-project/issues"
    },
    "license": "SEE LICENSE IN LICENSE.md",
    "author": {
        "name": "Example Developer",
        "email": "someone@example-developer.com",
        "url": "https://example-developer.com"
    },
    "browser": "/web/index.php",
    "repository": {
        "type": "git",
        "url": "git+https://github.com/example-developer/example-project.git"
    },
    "private": true,
複製代碼

這裏沒什麼有趣的東西,只是包含了咱們網站的元信息,就像package.json規範中所述。

"scripts": {
    "dev": "webpack-dev-server --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js --progress --hide-modules"
},
複製代碼

上述腳本表明了咱們爲項目提供的兩個主要構建步驟:

  • dev —— 只要咱們修改了項目的代碼,啓動該配置後,它會使用webpack-dev-server來實現熱模塊替換(HMR),內存編譯以及其餘細節處理。

  • build —— 當咱們進行生產部署時,它會執行全部花哨以及耗時的事情,例如Critical CSS、JavaScript壓縮等。

咱們只須要在命令行執行如下操做: 若是咱們使用的是yarn,輸入yarn dev或者yarn build;若是使用的是npm,輸入npm run dev或者npm run build。這些是你惟一須要使用的兩個命令。

請注意,不只能夠經過--config配置,咱們還能夠傳入單獨的配置文件進行配置。這樣咱們能夠將webpack配置分解爲單獨的邏輯文件,由於與生產環境構建相比,咱們將爲開發環境的構建作不少不一樣的事情。

接下來咱們的browserslist配置:

"browserslist": {
        "production": [
            "> 1%",
            "last 2 versions",
            "Firefox ESR"
        ],
        "legacyBrowsers": [
            "> 1%",
            "last 2 versions",
            "Firefox ESR"
        ],
        "modernBrowsers": [
            "last 2 Chrome versions",
            "not Chrome < 60",
            "last 2 Safari versions",
            "not Safari < 10.1",
            "last 2 iOS versions",
            "not iOS < 10.3",
            "last 2 Firefox versions",
            "not Firefox < 54",
            "last 2 Edge versions",
            "not Edge < 15"
        ]
    },
複製代碼

這是一個基於人類可讀配置的特定瀏覽器列表PostCSS autoprefixer默認使用在production設置中,咱們將legacyBrowsersmodernBrowsers傳遞給Babel用來處理傳統[過去]和現代js包的構建[處理轉碼問題,兼容es6等寫法],後面會有詳細介紹。

接着是devDependencies,它是構建系統所需的全部npm包:

"devDependencies": {
        "@babel/core": "^7.1.0",
        "@babel/plugin-syntax-dynamic-import": "^7.0.0",
        "@babel/plugin-transform-runtime": "^7.1.0",
        "@babel/preset-env": "^7.1.0",
        "@babel/register": "^7.0.0",
        "@babel/runtime": "^7.0.0",
        "autoprefixer": "^9.1.5",
        "babel-loader": "^8.0.2",
        "clean-webpack-plugin": "^0.1.19",
        "copy-webpack-plugin": "^4.5.2",
        "create-symlink-webpack-plugin": "^1.0.0",
        "critical": "^1.3.4",
        "critical-css-webpack-plugin": "^0.2.0",
        "css-loader": "^1.0.0",
        "cssnano": "^4.1.0",
        "dotenv": "^6.1.0",
        "file-loader": "^2.0.0",
        "git-rev-sync": "^1.12.0",
        "glob-all": "^3.1.0",
        "html-webpack-plugin": "^3.2.0",
        "ignore-loader": "^0.1.2",
        "imagemin": "^6.0.0",
        "imagemin-gifsicle": "^5.2.0",
        "imagemin-mozjpeg": "^7.0.0",
        "imagemin-optipng": "^5.2.1",
        "imagemin-svgo": "^7.0.0",
        "imagemin-webp": "^4.1.0",
        "imagemin-webp-webpack-plugin": "^1.0.2",
        "img-loader": "^3.0.1",
        "mini-css-extract-plugin": "^0.4.3",
        "moment": "^2.22.2",
        "optimize-css-assets-webpack-plugin": "^5.0.1",
        "postcss": "^7.0.2",
        "postcss-extend": "^1.0.5",
        "postcss-hexrgba": "^1.0.1",
        "postcss-import": "^12.0.0",
        "postcss-loader": "^3.0.0",
        "postcss-nested": "^4.1.0",
        "postcss-nested-ancestors": "^2.0.0",
        "postcss-simple-vars": "^5.0.1",
        "purgecss-webpack-plugin": "^1.3.0",
        "purgecss-whitelister": "^2.2.0",
        "resolve-url-loader": "^3.0.0",
        "sane": "^4.0.1",
        "save-remote-file-webpack-plugin": "^1.0.0",
        "style-loader": "^0.23.0",
        "symlink-webpack-plugin": "^0.0.4",
        "terser-webpack-plugin": "^1.1.0",
        "vue-loader": "^15.4.2",
        "vue-style-loader": "^4.1.2",
        "vue-template-compiler": "^2.5.17",
        "webapp-webpack-plugin": "https://github.com/brunocodutra/webapp-webpack-plugin.git",
        "webpack": "^4.19.1",
        "webpack-bundle-analyzer": "^3.0.2",
        "webpack-cli": "^3.1.1",
        "webpack-dashboard": "^2.0.0",
        "webpack-dev-server": "^3.1.9",
        "webpack-manifest-plugin": "^2.0.4",
        "webpack-merge": "^4.1.4",
        "webpack-notifier": "^1.6.0",
        "workbox-webpack-plugin": "^3.6.2"
    },
複製代碼

沒錯,這裏面依賴了不少npm包,但咱們的構建過程確實作的事情須要用到它們。

最後,dependencies的使用:

"dependencies": {
        "@babel/polyfill": "^7.0.0",
        "axios": "^0.18.0",
        "tailwindcss": "^0.6.6",
        "vue": "^2.5.17",
        "vue-confetti": "^0.4.2"
    }
}
複製代碼

顯然,對於一個真實存在的網站或者應用,dependencies中會有更多npm包,但咱們如今專一於構建過程。

ANNOTATED WEBPACK.SETTINGS.JS

我還使用了我在 A Bet­ter package.json for the Fron­tend arti­cle一文中討論過的相似方法,爲了封鎖從項目之間配置變爲單獨的webpack.settings.js,並保持webpack配置自己不變。

The key concept is that the only file we need to edit from project to project is the webpack.settings.js. [關鍵概念是咱們須要在項目之間編輯的惟一文件是webpack.settings.js]

因爲大部分項目都有一些很是類似的事情須要完成,因此咱們能夠建立一個適用於各個項目的webpack配置,咱們只須要更改它所操做的數據。

所以,在咱們的webpack.settings.js配置文件中的內容(從項目到項目的數據)和webpack配置中的內容(如何操做這些數據產生最終結果)之間的關注點分離。

// webpack.settings.js - webpack settings config

// node modules
require('dotenv').config();

// Webpack settings exports
// noinspection WebpackConfigHighlighting
module.exports = {
    name: "Example Project",
    copyright: "Example Company, Inc.",
    paths: {
        src: {
            base: "./src/",
            css: "./src/css/",
            js: "./src/js/"
        },
        dist: {
            base: "./web/dist/",
            clean: [
                "./img",
                "./criticalcss",
                "./css",
                "./js"
            ]
        },
        templates: "./templates/"
    },
    urls: {
        live: "https://example.com/",
        local: "http://example.test/",
        critical: "http://example.test/",
        publicPath: "/dist/"
    },
    vars: {
        cssName: "styles"
    },
    entries: {
        "app": "app.js"
    },
    copyWebpackConfig: [
        {
            from: "./src/js/workbox-catch-handler.js",
            to: "js/[name].[ext]"
        }
    ],
    criticalCssConfig: {
        base: "./web/dist/criticalcss/",
        suffix: "_critical.min.css",
        criticalHeight: 1200,
        criticalWidth: 1200,
        ampPrefix: "amp_",
        ampCriticalHeight: 19200,
        ampCriticalWidth: 600,
        pages: [
            {
                url: "",
                template: "index"
            }
        ]
    },
    devServerConfig: {
        public: () => process.env.DEVSERVER_PUBLIC || "http://localhost:8080",
        host: () => process.env.DEVSERVER_HOST || "localhost",
        poll: () => process.env.DEVSERVER_POLL || false,
        port: () => process.env.DEVSERVER_PORT || 8080,
        https: () => process.env.DEVSERVER_HTTPS || false,
    },
    manifestConfig: {
        basePath: ""
    },
    purgeCssConfig: {
        paths: [
            "./templates/**/*.{twig,html}",
            "./src/vue/**/*.{vue,html}"
        ],
        whitelist: [
            "./src/css/components/**/*.{css,pcss}"
        ],
        whitelistPatterns: [],
        extensions: [
            "html",
            "js",
            "twig",
            "vue"
        ]
    },
    saveRemoteFileConfig: [
        {
            url: "https://www.google-analytics.com/analytics.js",
            filepath: "js/analytics.js"
        }
    ],
    createSymlinkConfig: [
        {
            origin: "img/favicons/favicon.ico",
            symlink: "../favicon.ico"
        }
    ],
    webappConfig: {
        logo: "./src/img/favicon-src.png",
        prefix: "img/favicons/"
    },
    workboxConfig: {
        swDest: "../sw.js",
        precacheManifestFilename: "js/precache-manifest.[manifestHash].js",
        importScripts: [
            "/dist/workbox-catch-handler.js"
        ],
        exclude: [
            /\.(png|jpe?g|gif|svg|webp)$/i,
            /\.map$/,
            /^manifest.*\\.js(?:on)?$/,
        ],
        globDirectory: "./web/",
        globPatterns: [
            "offline.html",
            "offline.svg"
        ],
        offlineGoogleAnalytics: true,
        runtimeCaching: [
            {
                urlPattern: /\.(?:png|jpg|jpeg|svg|webp)$/,
                handler: "cacheFirst",
                options: {
                    cacheName: "images",
                    expiration: {
                        maxEntries: 20
                    }
                }
            }
        ]
    }
};
複製代碼

咱們將在webpack配置部分介紹全部內容,這裏須要注意的重點是,咱們已經採起了從項目到項目的更改,並加其從咱們的webpack配置文件中分離出來,並添加到單獨的webpack.settings.js文件中。

這意味着咱們能夠在webpack.settings.js配置文件中定義每一個項目不一樣的地方,而不須要與webpack自己配置進行摻和在一塊兒。儘管webpack.settings.js文件是一個js文件,但我儘可能將它保持爲JSON-ish,因此咱們只是更改其中的簡單設置,我沒有使用JSON做爲文件格式的靈活性,也容許添加註釋。

COMMON CONVENTIONS FOR WEBPACK CONFIGS

我爲全部webpack配置文件(webpack.common.jswebpack.dev.jswebpack.prod.js)採用了一些約定,讓它們看起來比較一致。

每一個配置文件都有兩個內置配置:

  • legacyConfig —— 適用於舊版ES5構建的配置

  • modernConfig —— 適用於構建現代ES2015+版本的配置

咱們這樣作是由於咱們有單獨的配置來建立兼容舊版本與現代構建,使它們在邏輯獨立。webpack.common.js 也有一個baseConfig,爲了保證組織的純粹。

能夠把它想象成面向對象編程,其中各類配置項目繼承,baseConfig做爲根對象。

爲了保證配置簡潔清晰和具備可讀性,採用的另外一個約定是爲各類webpack插件和須要配置的其餘webpack片斷配置configure()函數,而不是所有混在一塊兒。

這樣作是由於在webpack.settings.js中的一些數據須要在使用webpack以前進行轉換,而且因爲過去/現代構建,咱們須要根據構建類型返回不一樣的配置。

它還使配置文件更具可讀性。

做爲一個通用的webpack概念,要知道webpack自己只知道如何加載JavaScript和JSON。要加載其餘東西,須要使用對應的加載器,咱們將在webpack配置中使用許多不一樣的加載器。

ANNOTATED WEBPACK.COMMON.JS

如今讓咱們看一下webpack.common.js配置文件,包含devprod構建類型間共享的全部配置。

// webpack.common.js - common webpack config
const LEGACY_CONFIG = 'legacy';
const MODERN_CONFIG = 'modern';

// node modules
const path = require('path');
const merge = require('webpack-merge');

// webpack plugins
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const WebpackNotifierPlugin = require('webpack-notifier');

// config files
const pkg = require('./package.json');
const settings = require('./webpack.settings.js');
複製代碼

在一開始,咱們引入了咱們須要的node包,以及須要使用的webpack插件。而後咱們導入webpack.settings.js 做爲settings,以便咱們能夠訪問那裏的設置,並將package.json做爲pkg導入,對其進行訪問。

CONFIGURATION FUNCTIONS

這是configureBabelLoader()的設置:

// Configure Babel loader
const configureBabelLoader = (browserList) => {
   return {
       test: /\.js$/,
       exclude: /node_modules/,
       use: {
           loader: 'babel-loader',
           options: {
               presets: [
                   [
                       '@babel/preset-env', {
                       modules: false,
                       useBuiltIns: 'entry',
                       targets: {
                           browsers: browserList,
                       },
                   }
                   ],
               ],
               plugins: [
                   '@babel/plugin-syntax-dynamic-import',
                   [
                       "@babel/plugin-transform-runtime", {
                       "regenerator": true
                   }
                   ]
               ],
           },
       },
   };
};
複製代碼

函數configureBabelLoader()配置babel-loader來處理全部js後綴文件的加載,它使用@babel/preset-env而不是.babelrc文件,所以咱們能夠把因此內容保留在webpack配置文件中。

Babel能夠將現代ES2015+(以及其餘許多語言,如TypeScript或CoffeeScript)編譯爲針對特定瀏覽器或標準的JavaScript。咱們將browserList做爲參數傳入,這樣咱們能夠爲舊版瀏覽器構建現代ES2015+模塊和用polyfills兼容舊版ES5。

在咱們的HTML中,咱們只作這樣的事情:

<!-- Browsers with ES module support load this file. -->
<script type="module" src="main.js"></script>

<!-- Older browsers load this file (and module-supporting -->
<!-- browsers know *not* to load this file). -->
<script nomodule src="main-legacy.js"></script>
複製代碼

不用polyfills,不用大驚小怪,舊版瀏覽器忽略type="module"腳本,並獲取main-legacy.js,新版瀏覽器加載main.js,忽略nomodule,看起來很棒,真慶幸我想出了這個想法!爲了避免讓你以爲這種方法是極端,vue-cli在版本3中採用了這種策略

@ babel/plugin-syntax-dynamic-import插件甚至能夠在web瀏覽器實現ECMAScripr動態導入以前進行動態導入,這使咱們能夠異步加載咱們的JavaScript模塊,並根據須要動態加載

那麼到底在說啥?這意味着咱們能夠作這樣的事:

// App main
const main = async () => {
   // Async load the vue module
   const Vue = await import(/* webpackChunkName: "vue" */ 'vue');
   // Create our vue instance
   const vm = new Vue.default({
       el: "#app",
       components: {
           'confetti': () => import(/* webpackChunkName: "confetti" */ '../vue/Confetti.vue'),
       },
   });
};
// Execute async function
main().then( (value) => {
});
複製代碼

有兩點:

一、經過/* webpackChunkName: "vue" */,咱們告訴webpack但願這個動態代碼拆分塊被命名。

二、因爲咱們在異步函數(「main」)中使用import(),該函數等待動態加載的JavaScript導入的結果,而其他的代碼以其方式繼續。

咱們已經有效地告訴webpack,咱們但願咱們的塊經過代碼分割,而不是經過配置,經過@babel/plugin-syntax-dynamic-import的自帶魔法,能夠根據須要異步加載此JavaScript塊。

注意,咱們也是使用.vue單文件組件作了一樣的操做,很好。

除了使用await,咱們也能夠在import()Promise返回後執行咱們的代碼:

// Async load the vue module
import(/* webpackChunkName: "vue" */ 'vue').then(Vue => {
   // Vue has loaded, do something with it
   // Create our vue instance
   const vm = new Vue.default({
       el: "#app",
       components: {
           'confetti': () => import(/* webpackChunkName: "confetti" */ '../vue/Confetti.vue'),
       },
   });
});
複製代碼

這裏咱們使用了Promise,而不是await,所以咱們知道動態導入已經成功而且能夠愉快地使用Vue

若是你足夠仔細,你能夠看到咱們經過Promises有效地解決了JavaScript依賴關係,太棒了!

咱們甚至能夠在用戶點擊了某些內容,滾動到某個位置或者知足其餘條件後去加載某些JavaScript快等有趣的事情。

查看更多關於Module Methods import()信息。

若是你有興趣瞭解更多有關Babel的信息,能夠查看Working with Babel 7 and Webpack這篇文章。

接下來咱們有configureEntries()

// Configure Entries
const configureEntries = () => {
   let entries = {};
   for (const [key, value] of Object.entries(settings.entries)) {
       entries[key] = path.resolve(__dirname, settings.paths.src.js + value);
   }

   return entries;
};
複製代碼

這裏咱們經過swttings.entrieswebpack.settings.js中拿到webpack entry,對於單頁應用(SPA),只存在一個entry。對於更傳統的網站,你可能有幾個entry(每頁模版可能有一個entry)。

不管哪一種方式,因爲咱們已經在webpack.settings.js中定義了entry points,因此很容易在文件對其進行配置,entry points實際上只是一個<script src =「app.js」> </ script>標記,你將在HTML中包含該標記以引入JavaScript。

因爲咱們使用的是動態導入模塊,所以咱們一般在頁面上只有一個<script></script>標籤;其他的JavaScript會根據須要動態加載。

接下來咱們有configureFontLoader()函數:

// Configure Font loader
const configureFontLoader = () => {
   return {
       test: /\.(ttf|eot|woff2?)$/i,
       use: [
           {
               loader: 'file-loader',
               options: {
                   name: 'fonts/[name].[ext]'
               }
           }
       ]
   };
};
複製代碼

devprod構建字體加載是相同的,因此咱們把它寫在這裏,對於咱們使用的任何本地字體,咱們能夠通知webpack在JavaScript中加載它們:

import comicsans from '../fonts/ComicSans.woff2';
複製代碼

接下來咱們有configureManifest()函數:

// Configure Manifest
const configureManifest = (fileName) => {
   return {
       fileName: fileName,
       basePath: settings.manifestConfig.basePath,
       map: (file) => {
           file.name = file.name.replace(/(\.[a-f0-9]{32})(\..*)$/, '$2');
           return file;
       },
   };
};
複製代碼

這會爲基於文件名的緩存清除配置webpack-manifest-plugin,簡單來講,webpack知道咱們須要的全部JavaScript、css和其餘資源,因此它能夠生成一個指向帶哈希命名的資源清單,例如:

{
 "vendors~confetti~vue.js": "/dist/js/vendors~confetti~vue.03b9213ce186db5518ea.js",
 "vendors~confetti~vue.js.map": "/dist/js/vendors~confetti~vue.03b9213ce186db5518ea.js.map",
 "app.js": "/dist/js/app.30334b5124fa6e221464.js",
 "app.js.map": "/dist/js/app.30334b5124fa6e221464.js.map",
 "confetti.js": "/dist/js/confetti.1152197f8c58a1b40b34.js",
 "confetti.js.map": "/dist/js/confetti.1152197f8c58a1b40b34.js.map",
 "js/precache-manifest.js": "/dist/js/precache-manifest.f774c437974257fc8026ca1bc693655c.js",
 "../sw.js": "/dist/../sw.js"
}
複製代碼

咱們傳入文件名,由於建立一個現代的monifest.json以及一個用於兼容的manifest-legacy.json,它們分別具備現代ES2015+模塊和兼容舊版ES5模塊的入口點。對於爲現代以及舊版本生成的資源,這兩個json文件中的關鍵點都是一致的。

接下來咱們有一個至關標準的configureVueLoader()配置:

// Configure Vue loader
const configureVueLoader = () => {
   return {
       test: /\.vue$/,
       loader: 'vue-loader'
   };
};
複製代碼

這配置只是讓咱們輕鬆解析Vue單文件組件,webpack負責爲你提取適當的HTML、CSS和Javascript。

BASE CONFIG

baseConfig將與modernConfiglegacyConfig合併:

// The base webpack config
const baseConfig = {
   name: pkg.name,
   entry: configureEntries(),
   output: {
       path: path.resolve(__dirname, settings.paths.dist.base),
       publicPath: settings.urls.publicPath
   },
   resolve: {
       alias: {
           'vue$': 'vue/dist/vue.esm.js'
       }
   },
   module: {
       rules: [
           configureVueLoader(),
       ],
   },
   plugins: [
       new WebpackNotifierPlugin({title: 'Webpack', excludeWarnings: true, alwaysNotify: true}),
       new VueLoaderPlugin(),
   ]
};
複製代碼

這裏全部的配置都是很是標準的webpack配置,但請注意咱們將vue$指向vue/dist/vue.esm.js,以便咱們能夠得到Vue的ES2015模塊版本。

咱們使用WebpackNotifierPlugin插件以直觀的方式告訴咱們構建的狀態。

LEGACY CONFIG

legacyConfig配置用於使用合適的polyfill構建兼容舊版本ES5:

// Legacy webpack config
const legacyConfig = {
   module: {
       rules: [
           configureBabelLoader(Object.values(pkg.browserslist.legacyBrowsers)),
       ],
   },
   plugins: [
       new CopyWebpackPlugin(
           settings.copyWebpackConfig
       ),
       new ManifestPlugin(
           configureManifest('manifest-legacy.json')
       ),
   ]
};
複製代碼

請注意,咱們將pkg.browserslist.legacyBrowsers傳遞給configureBabelLoader(),將manifest-legacy.json傳遞給configureManifest()

咱們還在此配置中加入了CopyWebpackPlugin插件,咱們只須要複製settings.copyWebpackConfig中定義的文件一次。

MODERN CONFIG

modernConfig用於構建現代ES2015 Javascript模塊,不須要藉助其餘東西:

// Modern webpack config
const modernConfig = {
   module: {
       rules: [
           configureBabelLoader(Object.values(pkg.browserslist.modernBrowsers)),
       ],
   },
   plugins: [
       new ManifestPlugin(
           configureManifest('manifest.json')
       ),
   ]
};
複製代碼

請注意,咱們將pkg.browserslist.modernBrowsers傳遞給configureBabelLoader(),將manifest.json傳遞給configureManifest()

MODULE.EXPORTS

最後,module.exports使用webpack-merge插件將以前的配置合併在一塊兒,並返回webpack.dev.jswebpack.prod.js使用的對象。

// Common module exports
// noinspection WebpackConfigHighlighting
module.exports = {
   'legacyConfig': merge(
       legacyConfig,
       baseConfig,
   ),
   'modernConfig': merge(
       modernConfig,
       baseConfig,
   ),
};
複製代碼

ANNOTATED WEBPACK.DEV.JS

如今讓咱們看看webpack.dev.js配置文件,它包含了咱們開發項目時用於構建的全部設置,與webpack.common.js文件中的設置合併,造成一個完整的webpack配置。

// webpack.dev.js - developmental builds
const LEGACY_CONFIG = 'legacy';
const MODERN_CONFIG = 'modern';

// node modules
const merge = require('webpack-merge');
const path = require('path');
const sane = require('sane');
const webpack = require('webpack');

// webpack plugins
const Dashboard = require('webpack-dashboard');
const DashboardPlugin = require('webpack-dashboard/plugin');
const dashboard = new Dashboard();

// config files
const common = require('./webpack.common.js');
const pkg = require('./package.json');
const settings = require('./webpack.settings.js');
複製代碼

在序言中,咱們再次引入了須要用到的node包,以及使用的webpack插件,而後引入webpack.settings.js做爲settings,以便咱們能夠訪問那裏的設置,並導入package.json做爲pkg,以便訪問那裏的一些設置。

咱們同時還導入了webpack.common.js經常使用的webpack配置,並將合併到咱們的開發設置。

CONFIGURATION FUNCTIONS

這是configureDevServer()的配置:

// Configure the webpack-dev-server
const configureDevServer = (buildType) => {
   return {
       public: settings.devServerConfig.public(),
       contentBase: path.resolve(__dirname, settings.paths.templates),
       host: settings.devServerConfig.host(),
       port: settings.devServerConfig.port(),
       https: !!parseInt(settings.devServerConfig.https()),
       quiet: true,
       hot: true,
       hotOnly: true,
       overlay: true,
       stats: 'errors-only',
       watchOptions: {
           poll: !!parseInt(settings.devServerConfig.poll()),
           ignored: /node_modules/,
       },
       headers: {
           'Access-Control-Allow-Origin': '*'
       },
       // Use sane to monitor all of the templates files and sub-directories
       before: (app, server) => {
           const watcher = sane(path.join(__dirname, settings.paths.templates), {
               glob: ['**/*'],
               poll: !!parseInt(settings.devServerConfig.poll()),
           });
           watcher.on('change', function(filePath, root, stat) {
               console.log(' File modified:', filePath);
               server.sockWrite(server.sockets, "content-changed");
           });
       },
   };
};
複製代碼

當咱們進行生產構建時,webpack綁定全部各類資源並保存到文件系統中,相比之下,當咱們在本地項目中開發時,咱們經過webpack-dev-server使用開發構建:

  • 啓動爲咱們的資源提供服務的本地express web服務器。

  • 爲了提高速度,在內存而不是文件系統中構建咱們的資源。

  • 從新構建資源,如JavaScript、css、Vue組件等等,經過使用熱模塊更新(HMR),當咱們修改了這些資源,能夠不須要從新加載界面。

  • 在更改模版時將會從新加載頁面。

這相似於更復雜的Browsersync變體,大大加快了開發速度。

惟一不一樣的是咱們這裏使用了Sane監控不須要經過webpack運行的文件(本例中咱們的模板),當該文件修改時,從新加載頁面。

注意,webpack-dev-server的配置再次引用了webpack.settings.js文件,對於大部分人來講默認值可能沒問題,但我使用Laravel Homestead做爲本地開發,像咱們在文章Local Development with Vagrant / Homestead討論的那樣,意味着我在Homestead VM中運行全部的開發工具。

所以,webpack.settings.js能夠從一個.env文件中讀取擁有特定的devServer配置,而不是在個人weboack.settings.js文件中對本地開發環境進行硬編碼(由於它可能因人而異):

// .env file DEVSERVER settings
# webpack example settings for Homestead/Vagrant
DEVSERVER_PUBLIC="http://192.168.10.10:8080"
DEVSERVER_HOST="0.0.0.0"
DEVSERVER_POLL=1
DEVSERVER_PORT=8080
DEVSERVER_HTTPS=0
複製代碼

你可使用不一樣的配置,所以能夠根據須要在.env文件中更改設置,dotenv背後的想法是咱們在.env文件中定義了一個特定於環境的配置,不會將其簽入git repo。若是.env文件不存在,那很好,使用默認值:

// webpack.settings.js devServerConfig defaults
devServerConfig: {
    public: () => process.env.DEVSERVER_PUBLIC || "http://localhost:8080",
    host: () => process.env.DEVSERVER_HOST || "localhost",
    poll: () => process.env.DEVSERVER_POLL || false,
    port: () => process.env.DEVSERVER_PORT || 8080,
    https: () => process.env.DEVSERVER_HTTPS || false,
},
複製代碼

接下來是configureImageLoader()配置:

// webpack.dev.js configureImageLoader()
// Configure Image loader
const configureImageLoader = (buildType) => {
    if (buildType === LEGACY_CONFIG) {
        return {
            test: /\.(png|jpe?g|gif|svg|webp)$/i,
            use: [
                {
                    loader: 'file-loader',
                    options: {
                        name: 'img/[name].[hash].[ext]'
                    }
                }
            ]
        };
    }
    if (buildType === MODERN_CONFIG) {
        return {
            test: /\.(png|jpe?g|gif|svg|webp)$/i,
            use: [
                {
                    loader: 'file-loader',
                    options: {
                        name: 'img/[name].[hash].[ext]'
                    }
                }
            ]
        };
    }
};
複製代碼

傳入buildType參數,以便返回不一樣的結果,具體取決於它是舊版本仍是新版構建,在該例子中,咱們返回了相同的配置,但能夠想象可能會改變。

值得注意的是,這只是適用於咱們webpack構建中包含的圖片;許多其餘的圖片來自於其餘地方(CMS系統,資產管理系統等等)。

要讓webpack知道這裏有圖像,須要將其導入到你的JavaScript文件中:

import Icon from './icon.png';
複製代碼

有關這方面的更多詳細信息,請查看webpack文檔「加載圖像」部分。

接下來是configurePostcssLoader()配置:

// Configure the Postcss loader
const configurePostcssLoader = (buildType) => {
    // Don't generate CSS for the legacy config in development if (buildType === LEGACY_CONFIG) { return { test: /\.(pcss|css)$/, loader: 'ignore-loader' }; } if (buildType === MODERN_CONFIG) { return { test: /\.(pcss|css)$/, use: [ { loader: 'style-loader', }, { loader: 'vue-style-loader', }, { loader: 'css-loader', options: { importLoaders: 2, sourceMap: true } }, { loader: 'resolve-url-loader' }, { loader: 'postcss-loader', options: { sourceMap: true } } ] }; } }; 複製代碼

咱們使用PostCSS來處理全部的css,包括Tailwind CSS。我以爲PostCSS是css的Babel,它將各類高級css功能編程成瀏覽器能夠解析的普通css。

對於webpack加載器,它們的處理順序與列出順序相反:

咱們在本地開發過程當中不須要將全部css文件提取到最小的文件中,相反咱們只是讓style-loader在咱們的文檔中內聯它。

webpack-dev-server爲css使用熱模塊替換(HMR),每當咱們修改樣式時,它都會從新構建css並自動注入,很神奇(what)。

咱們經過引入它來告知webpack去解析:

import styles from '../css/app.pcss';
複製代碼

在webpack文檔的Loading CSS部分中有詳細討論。

咱們從App.js入口點執行此操做,將此視爲PostCSS的入口點,app.pcss文件@import咱們項目中使用到的全部CSS,後面會對此進行詳細介紹。

MODULE.EXPORTS

最後,module.exports使用webpack-merge包將webpack.common.js中的common.legacyConfig與咱們的開發舊版兼容配置合併,並將common.modernConfig與開發環境現代配置合併:

// Development module exports
module.exports = [
    merge(
        common.legacyConfig,
        {
            output: {
                filename: path.join('./js', '[name]-legacy.[hash].js'),
                publicPath: settings.devServerConfig.public() + '/',
            },
            mode: 'development',
            devtool: 'inline-source-map',
            devServer: configureDevServer(LEGACY_CONFIG),
            module: {
                rules: [
                    configurePostcssLoader(LEGACY_CONFIG),
                    configureImageLoader(LEGACY_CONFIG),
                ],
            },
            plugins: [
                new webpack.HotModuleReplacementPlugin(),
            ],
        }
    ),
    merge(
        common.modernConfig,
        {
            output: {
                filename: path.join('./js', '[name].[hash].js'),
                publicPath: settings.devServerConfig.public() + '/',
            },
            mode: 'development',
            devtool: 'inline-source-map',
            devServer: configureDevServer(MODERN_CONFIG),
            module: {
                rules: [
                    configurePostcssLoader(MODERN_CONFIG),
                    configureImageLoader(MODERN_CONFIG),
                ],
            },
            plugins: [
                new webpack.HotModuleReplacementPlugin(),
                new DashboardPlugin(dashboard.setData),
            ],
        }
    ),
];
複製代碼

經過module.exports中返回一個數組,咱們告知webpack有多個須要執行的編譯:一個用於舊版兼容構建,另外一個用於新版構建。

對於舊版構建,咱們將處理後的JavaScript命名爲[name]-legacy.[hash].js,而新版構建命名爲[name].[hash].js

經過設置modedevelopment,告知webpack這是開發環境構建。

devtool設置爲inline-source-map,咱們要求將CSS/JavsScript的.map內聯到文件中,雖然構建出來的項目會偏大,可是便於開發調試。

經過webpack.HotModuleReplacementPlugin插件,能夠支持Webpack的熱模塊替換(HMR)。

DashboardPlugin插件讓咱們以爲本身是一個宇航員,擁有一個酷炫的面板:

我發現DashboardPlugin插件開發HUD比默認的webpack進度展現更直觀。

到這裏,如今已經爲咱們項目提供了一個很好的開發環境配置,查看熱模塊替換視頻,瞭解該操做的示例

ANNOTATED WEBPACK.PROD.JS

如今咱們看看webpack.prod.js配置文件,它包含咱們正在處理項目時用於生產構建的全部配置。它與webpack.common.js中的設置合併,造成一個完整的webpack配置。

// webpack.prod.js - production builds
const LEGACY_CONFIG = 'legacy';
const MODERN_CONFIG = 'modern';

// node modules
const git = require('git-rev-sync');
const glob = require('glob-all');
const merge = require('webpack-merge');
const moment = require('moment');
const path = require('path');
const webpack = require('webpack');

// webpack plugins
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CreateSymlinkPlugin = require('create-symlink-webpack-plugin');
const CriticalCssPlugin = require('critical-css-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ImageminWebpWebpackPlugin = require('imagemin-webp-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const PurgecssPlugin = require('purgecss-webpack-plugin');
const SaveRemoteFilePlugin = require('save-remote-file-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const WebappWebpackPlugin = require('webapp-webpack-plugin');
const WhitelisterPlugin = require('purgecss-whitelister');
const WorkboxPlugin = require('workbox-webpack-plugin');

// config files
const common = require('./webpack.common.js');
const pkg = require('./package.json');
const settings = require('./webpack.settings.js');
複製代碼

咱們再次引入了在序言中涉及到的node包,以及咱們使用的webpack插件,而後將webpack.settings.js做爲settings導入,並將package.json做爲pkg導入,便於訪問須要用到的配置。

咱們還導入了webpack.common.js中公共的webpack配置,咱們將與開發設置合併。

TAILWIND EXTRACTOR

該類是Tailwind CSS的自定義PurgeCSS提取器,容許在類名中使用特殊字符。

// Custom PurgeCSS extractor for Tailwind that allows special characters in
// class names.
//
// https://github.com/FullHuman/purgecss#extractor
class TailwindExtractor {
    static extract(content) {
        return content.match(/[A-Za-z0-9-_:\/]+/g) || [];
    }
}
複製代碼

這取自Tailwind CSS文檔中Removing unused CSS with PurgeCSS這一部分。有關此提取器如何與 purgcss 配合使用的詳細信息, 請參閱下文, 讓你的css變得更加的整潔。

CONFIGURATION FUNCTIONS

這是configureBanner()函數:

// Configure file banner
const configureBanner = () => {
    return {
        banner: [
            '/*!',
            ' * @project ' + settings.name,
            ' * @name ' + '[filebase]',
            ' * @author ' + pkg.author.name,
            ' * @build ' + moment().format('llll') + ' ET',
            ' * @release ' + git.long() + ' [' + git.branch() + ']',
            ' * @copyright Copyright (c) ' + moment().format('YYYY') + ' ' + settings.copyright,
            ' *',
            ' */',
            ''
        ].join('\n'),
        raw: true
    };
};
複製代碼

這只是爲咱們生成的每一個文件添加了一個帶有項目名稱、文件名、做者和 git 信息的banner。

接着是configureBundleAnalyzer()

// webpack.prod.js configureBundleAnalyzer()
// Configure Bundle Analyzer
const configureBundleAnalyzer = (buildType) => {
    if (buildType === LEGACY_CONFIG) {
        return {
            analyzerMode: 'static',
            reportFilename: 'report-legacy.html',
        };
    }
    if (buildType === MODERN_CONFIG) {
        return {
            analyzerMode: 'static',
            reportFilename: 'report-modern.html',
        };
    }
};
複製代碼

使用 WebpackBundleAnalyzer 插件爲咱們的新版和舊版本構建生成一份報告,而且生成一個獨立可交互的HTML頁面,能夠查看webpack打包後的確切內容。

我發現這個插件挺有用,能夠幫助我縮小最終構建包的大小,並且確切地瞭解了webpack構建了什麼,因此我已經把它做爲項目生產構建過程的一部分。

接着是configureCriticalCss()

// webpack.prod.js configureCriticalCss()
// Configure Critical CSS
const configureCriticalCss = () => {
    return (settings.criticalCssConfig.pages.map((row) => {
            const criticalSrc = settings.urls.critical + row.url;
            const criticalDest = settings.criticalCssConfig.base + row.template + settings.criticalCssConfig.suffix;
            let criticalWidth = settings.criticalCssConfig.criticalWidth;
            let criticalHeight = settings.criticalCssConfig.criticalHeight;
            // Handle Google AMP templates
            if (row.template.indexOf(settings.criticalCssConfig.ampPrefix) !== -1) {
                criticalWidth = settings.criticalCssConfig.ampCriticalWidth;
                criticalHeight = settings.criticalCssConfig.ampCriticalHeight;
            }
            console.log("source: " + criticalSrc + " dest: " + criticalDest);
            return new CriticalCssPlugin({
                base: './',
                src: criticalSrc,
                dest: criticalDest,
                extract: false,
                inline: false,
                minify: true,
                width: criticalWidth,
                height: criticalHeight,
            })
        })
    );
};
複製代碼

使用CriticalCssPlugin插件經過webpack.settings.js中的settings.criticalCssConfig.pages進行分塊,爲咱們的網站生成CriticalCSS。

須要注意的是,若是傳入的頁面在任何位置的名字都包含settings.criticalCssConfig.ampPrefix,則它將經過傳入很是大的高度爲整個網頁(而不只僅是上面的摺疊內容)生成CriticalCSS。

這裏不會詳細介紹CriticalCSS,有關它的更多資料,請查看Implementing Critical CSS on your website這篇文章。

接着是configureCleanWebpack()

// Configure Clean webpack
const configureCleanWebpack = () => {
    return {
        root: path.resolve(__dirname, settings.paths.dist.base),
        verbose: true,
        dry: false
    };
};
複製代碼

這只是使用CleanWebpackPlugin從咱們的webpack.settings.js中刪除 settings.paths.dist.base 中的生成目錄。

接着是configureHtml()

// Configure Html webpack
const configureHtml = () => {
    return {
        templateContent: '',
        filename: 'webapp.html',
        inject: false,
    };
};
複製代碼

這將使用HtmlWebpackPluginWebappWebpackPlugin(見下文)插件爲咱們的favicons生成HTML。注意,咱們在templateContent中傳入一個空字符串,以便輸出只是WebappWebpackPlugin的原始輸出。

接着是configureImageLoader()

// Configure Image loader
const configureImageLoader = (buildType) => {
    if (buildType === LEGACY_CONFIG) {
        return {
            test: /\.(png|jpe?g|gif|svg|webp)$/i,
            use: [
                {
                    loader: 'file-loader',
                    options: {
                        name: 'img/[name].[hash].[ext]'
                    }
                }
            ]
        };
    }
    if (buildType === MODERN_CONFIG) {
        return {
            test: /\.(png|jpe?g|gif|svg|webp)$/i,
            use: [
                {
                    loader: 'file-loader',
                    options: {
                        name: 'img/[name].[hash].[ext]'
                    }
                },
                {
                    loader: 'img-loader',
                    options: {
                        plugins: [
                            require('imagemin-gifsicle')({
                                interlaced: true,
                            }),
                            require('imagemin-mozjpeg')({
                                progressive: true,
                                arithmetic: false,
                            }),
                            require('imagemin-optipng')({
                                optimizationLevel: 5,
                            }),
                            require('imagemin-svgo')({
                                plugins: [
                                    {convertPathData: false},
                                ]
                            }),
                        ]
                    }
                }
            ]
        };
    }
};
複製代碼

咱們傳入buildType參數,以致於咱們能夠返回不一樣的結果,具體取決於它是新版仍是舊版構建。咱們經過優化處理圖像,經過img-loader進行新版構建。

咱們只對新版構建執行此操做,由於花費時間去處理優化新版本和舊版本的圖像沒有意義(圖像對於二者都是同樣的)。

須要注意的是,這隻適用於咱們的webpack構建中包含的圖像,許多其餘圖像資源其實來自來與其餘地方(cms 系統、資產管理系統等)。

要讓webpack優化圖像,請將其導入 JavaScript:

import Icon from './icon.png';
複製代碼

更多Loading Images詳細信息,請查看webpack文檔對應部分。

接着是咱們的configureOptimization()配置:

// Configure optimization
const configureOptimization = (buildType) => {
    if (buildType === LEGACY_CONFIG) {
        return {
            splitChunks: {
                cacheGroups: {
                    default: false,
                    common: false,
                    styles: {
                        name: settings.vars.cssName,
                        test: /\.(pcss|css|vue)$/,
                        chunks: 'all',
                        enforce: true
                    }
                }
            },
            minimizer: [
                new TerserPlugin(
                    configureTerser()
                ),
                new OptimizeCSSAssetsPlugin({
                    cssProcessorOptions: {
                        map: {
                            inline: false,
                            annotation: true,
                        },
                        safe: true,
                        discardComments: true
                    },
                })
            ]
        };
    }
    if (buildType === MODERN_CONFIG) {
        return {
            minimizer: [
                new TerserPlugin(
                    configureTerser()
                ),
            ]
        };
    }
};
複製代碼

這是webpack生產環境優化的配置,對於舊版構建(執行此操做兩次沒有任何意義),咱們使用MiniCssExtractPlugin插件將項目裏使用到的css提取到單個css文件中。若是您之前使用過webpack,那麼之前應該已經使用過ExtractTextPlugin來執行過此操做,然而如今不須要這麼作了。

咱們還使用了OptimizeCSSAssetsPlugin插件經過刪除重複的規則來優化生成的css,並經過cssnano壓縮css。

最後,咱們將Javascript minimizer設置成TerserPlugin,這是由於[UglifyJsPlugin] (github.com/webpack-con…)再也不支持最小化ES2015+JavaScript。因爲咱們正在生成新版es2015+bundles,咱們須要它。

接着是configurePostcssLoader()

// Configure Postcss loader
const configurePostcssLoader = (buildType) => {
    if (buildType === LEGACY_CONFIG) {
        return {
            test: /\.(pcss|css)$/,
            use: [
                MiniCssExtractPlugin.loader,
                {
                    loader: 'css-loader',
                    options: {
                        importLoaders: 2,
                        sourceMap: true
                    }
                },
                {
                    loader: 'resolve-url-loader'
                },
                {
                    loader: 'postcss-loader',
                    options: {
                        sourceMap: true
                    }
                }
            ]
        };
    }
    // Don't generate CSS for the modern config in production if (buildType === MODERN_CONFIG) { return { test: /\.(pcss|css)$/, loader: 'ignore-loader' }; } }; 複製代碼

這個配置看起來十分相似於開發版本的configurePostcssLoader(),除了最終加載器,咱們使用MiniCssExtractPlugin.loader將全部css提取到一個文件中。

咱們只對舊版兼容構建執行此操做,由於對每一個構建執行它沒有意義(css是相同的)。咱們使用ignore-loader進行新版構建,所以咱們的.css和.pcss文件存在一個加載器,但什麼都沒作。

如前面說到,咱們使用PostCSS處理全部的css,包括Tailwind CSS,我認爲它是CSS的babel,由於它將各類高級的css功能編譯成你的瀏覽器能夠解析的普通css。

一樣,對於webpack加載器,它們按照列出的相反順序進行處理:

因爲這是一個生產環境構建,咱們使用MiniCssExtractPlugin.loader提取全部使用到的css,並保存到.css文件中。CSS也被最小化,並針對生產環境進行了優化。

咱們經過引入css文件告知webpack:

import styles from '../css/app.pcss';
複製代碼

這在webpack文檔的Loading CSS有詳細介紹。

咱們從App.js入口點執行此操做,將此視爲postCSS的入口點,app.pcss文件@import咱們項目使用的全部CSS,稍後將詳細介紹。

接着是configurePurgeCss()

// Configure PurgeCSS
const configurePurgeCss = () => {
    let paths = [];
    // Configure whitelist paths
    for (const [key, value] of Object.entries(settings.purgeCssConfig.paths)) {
        paths.push(path.join(__dirname, value));
    }

    return {
        paths: glob.sync(paths),
        whitelist: WhitelisterPlugin(settings.purgeCssConfig.whitelist),
        whitelistPatterns: settings.purgeCssConfig.whitelistPatterns,
        extractors: [
            {
                extractor: TailwindExtractor,
                extensions: settings.purgeCssConfig.extensions
            }
        ]
    };
};
複製代碼

Tailwind CSS是一個出色的實用程序優先的CSS框架,它容許快速原型化,由於在本地開發中,不多須要實際編寫任何css。 相反,你只須要使用提供的實用程序CSS類。

缺點就是生成的CSS可能有點大,這時候就須要用到PurgeCSS,它將解析全部HTML/template/Vue/任何文件,並刪除沒有使用到的CSS。

節省的空間可能很大,Tailwind CSS和PurgeCSS是天做之合。咱們在 Tailwind CSS utility-first CSS with Adam Wathan博客中深刻討論了這個問題。

它遍歷settings.purgeCssConfig.paths中的全部路徑globs,以尋找要保留的CSS規則,任何未找到的CSS規則都會從咱們生成的CSS構建中刪除。

咱們還使用了WhitelisterPlugin,當咱們知道不但願某些CSS 被剝離時,能夠輕鬆地將整個文件或全局列入白名單。與咱們的settings.purgeCssConfig.whitelist匹配的全部文件中的CSS規則都列入白名單,而且永遠不會從生成的構建中刪除。

接下來是configureTerser()

// Configure terser
const configureTerser = () => {
    return {
        cache: true,
        parallel: true,
        sourceMap: true
    };
};
複製代碼

這只是配置了[TerserPlugin] (github.com/webpack-con…)使用的一些設置,最大限度地減小了咱們的舊版和新版JavaScript代碼。

接着是configureWebApp()

// Configure Webapp webpack
const configureWebapp = () => {
    return {
        logo: settings.webappConfig.logo,
        prefix: settings.webappConfig.prefix,
        cache: false,
        inject: 'force',
        favicons: {
            appName: pkg.name,
            appDescription: pkg.description,
            developerName: pkg.author.name,
            developerURL: pkg.author.url,
            path: settings.paths.dist.base,
        }
    };
};
複製代碼

這裏使用webappwebpackepulin以無數種格式生成咱們全部的網站favicon,以及咱們的webapp manifest.json和其餘PWA細節。

它與HtmlWebpackPlugin結合使用,還能夠輸出一個webapp.html文件,它包含全部生成的favicons和相關文件的連接,以包含在咱們的HTML頁面的<head></head>中。

接着是configureWorkbox()

// Configure Workbox service worker
const configureWorkbox = () => {
    let config = settings.workboxConfig;

    return config;
};
複製代碼

咱們使用Google的WorkboxWebpackPlugin爲網站生成一個Service Worker,解釋 Service Worker是什麼超出了本文的內容範圍,但能夠查看Going Offline: Service Workers with Jeremy Keith博客做爲入門。

配置數據所有來自webpack.settings.js中的settings.workboxConfig對象。除了預先緩存新版構建minifest.json中全部的資源外,咱們還包括一個workbox-catch-handler.js來配置它以使用回退響應catch-all路由

// fallback URLs
const FALLBACK_HTML_URL = '/offline.html';
const FALLBACK_IMAGE_URL = '/offline.svg';

// This "catch" handler is triggered when any of the other routes fail to
// generate a response.
// https://developers.google.com/web/tools/workbox/guides/advanced-recipes#provide_a_fallback_response_to_a_route
workbox.routing.setCatchHandler(({event, request, url}) => {
    // Use event, request, and url to figure out how to respond.
    // One approach would be to use request.destination, see
    // https://medium.com/dev-channel/service-worker-caching-strategies-based-on-request-types-57411dd7652c
    switch (request.destination) {
        case 'document':
            return caches.match(FALLBACK_HTML_URL);
            break;

        case 'image':
            return caches.match(FALLBACK_IMAGE_URL);
            break;

        default:
            // If we don't have a fallback, just return an error response. return Response.error(); } }); // Use a stale-while-revalidate strategy for all other requests. workbox.routing.setDefaultHandler( workbox.strategies.staleWhileRevalidate() ); 複製代碼

MODULE.EXPORTS

最後,module.export使用webpack-mergewebpack.commons.js中的common.legacyConfig與咱們的生產環境舊版配置合併,並將common.modernConfig與咱們的生產環境新版配置合併:

// Production module exports
module.exports = [
    merge(
        common.legacyConfig,
        {
            output: {
                filename: path.join('./js', '[name]-legacy.[chunkhash].js'),
            },
            mode: 'production',
            devtool: 'source-map',
            optimization: configureOptimization(LEGACY_CONFIG),
            module: {
                rules: [
                    configurePostcssLoader(LEGACY_CONFIG),
                    configureImageLoader(LEGACY_CONFIG),
                ],
            },
            plugins: [
                new CleanWebpackPlugin(settings.paths.dist.clean,
                    configureCleanWebpack()
                ),
                new MiniCssExtractPlugin({
                    path: path.resolve(__dirname, settings.paths.dist.base),
                    filename: path.join('./css', '[name].[chunkhash].css'),
                }),
                new PurgecssPlugin(
                    configurePurgeCss()
                ),
                new webpack.BannerPlugin(
                    configureBanner()
                ),
                new HtmlWebpackPlugin(
                    configureHtml()
                ),
                new WebappWebpackPlugin(
                    configureWebapp()
                ),
                new CreateSymlinkPlugin(
                    settings.createSymlinkConfig,
                    true
                ),
                new SaveRemoteFilePlugin(
                    settings.saveRemoteFileConfig
                ),
                new BundleAnalyzerPlugin(
                    configureBundleAnalyzer(LEGACY_CONFIG),
                ),
            ].concat(
                configureCriticalCss()
            )
        }
    ),
    merge(
        common.modernConfig,
        {
            output: {
                filename: path.join('./js', '[name].[chunkhash].js'),
            },
            mode: 'production',
            devtool: 'source-map',
            optimization: configureOptimization(MODERN_CONFIG),
            module: {
                rules: [
                    configurePostcssLoader(MODERN_CONFIG),
                    configureImageLoader(MODERN_CONFIG),
                ],
            },
            plugins: [
                new webpack.optimize.ModuleConcatenationPlugin(),
                new webpack.BannerPlugin(
                    configureBanner()
                ),
                new ImageminWebpWebpackPlugin(),
                new WorkboxPlugin.GenerateSW(
                    configureWorkbox()
                ),
                new BundleAnalyzerPlugin(
                    configureBundleAnalyzer(MODERN_CONFIG),
                ),
            ]
        }
    ),
];
複製代碼

經過在咱們的module.exports中返回一個數組,咱們告訴webpack有多個須要完成的編譯:一個用於舊版兼容構建,另外一個用於新版構建。

注意,對於舊版兼容構建,咱們將處理後的JavaScript輸出爲[name]-legacy.[hash].js,而新版構建將其輸出爲[name].[hash].js

經過將mode設置爲production,咱們告知webpack這是一個生產環境構建,這將啓用許多適用於生產環境的設置。

經過將devtool設置爲source-map,咱們要求將CSS/JavaScript生成單獨的.map文件,這是咱們更容易調試實時生產環境網站,而無需添加資源的文件大小。

這裏使用了幾個咱們還沒有涉及的webpack插件:

  • CreateSymlinkPlugin —— 這是我建立的一個插件,容許在構建過程當中建立符號連接,使用它來將生成的favicon.ico符號連接到/favicon.ico,由於許多web瀏覽器在web根目錄中查找。

  • SaveRemoteFilePlugin —— 用於下載遠程文件並將其做爲webpack構建過程的一部分輸出。我用它來下載和提供谷歌的分析。

  • ImageminWebpWebpackPlugin —— 此插件會爲項目導入的全部JPEG和PNG文件建立.webp變體。

直到如今,咱們爲項目提供了一個很好的生產環境構建。

TAILWIND CSS & POSTCSS CONFIG

爲了使webpack正確構建Tailwind CSS和其餘css,咱們須要作一些設置,感謝個人夥伴Jonathan Melville在構建這方面的工做,首先咱們須要一個postcss.config.js文件:

module.exports = {
    plugins: [
        require('postcss-import'),
        require('postcss-extend'),
        require('postcss-simple-vars'),
        require('postcss-nested-ancestors'),
        require('postcss-nested'),
        require('postcss-hexrgba'),
        require('autoprefixer'),
        require('tailwindcss')('./tailwind.config.js')
    ]
};
複製代碼

這能夠存儲在項目根目錄中,PostCSS將在構建過程當中自動查找它,並應用咱們指定的PostCSS插件。請注意,這是咱們引入tailwind.config.js文件的位置,以便其成爲構建過程的一部分。

最後,咱們的CSS入口點app.pcss看起來像這樣:

/**
 * app.css
 *
 * The entry point for the css.
 *
 */

/**
 * This injects Tailwind's base styles, which is a combination of * Normalize.css and some additional base styles. * * You can see the styles here: * https://github.com/tailwindcss/tailwindcss/blob/master/css/preflight.css */ @import "tailwindcss/preflight"; /** * This injects any component classes registered by plugins. * */ @import 'tailwindcss/components'; /** * Here we add custom component classes; stuff we want loaded * *before* the utilities so that the utilities can still * override them. * */ @import './components/global.pcss'; @import './components/typography.pcss'; @import './components/webfonts.pcss'; /** * This injects all of Tailwind's utility classes, generated based on your
 * config file.
 *
 */
@import 'tailwindcss/utilities';

/**
 * Include styles for individual pages
 *
 */
@import './pages/homepage.pcss';

/**
 * Include vendor css.
 *
 */
 @import 'vendor.pcss';
複製代碼

顯然,對其進行定製以包括用於自定義css的任何組件/界面。

POST-BUILD PROJECT TREE

這是咱們項目在構建後的結構:

├── example.env
├── package.json
├── postcss.config.js
├── src
│   ├── css
│   │   ├── app.pcss
│   │   ├── components
│   │   │   ├── global.pcss
│   │   │   ├── typography.pcss
│   │   │   └── webfonts.pcss
│   │   ├── pages
│   │   │   └── homepage.pcss
│   │   └── vendor.pcss
│   ├── fonts
│   ├── img
│   │   └── favicon-src.png
│   ├── js
│   │   ├── app.js
│   │   └── workbox-catch-handler.js
│   └── vue
│       └── Confetti.vue
├── tailwind.config.js
├── templates
├── web
│   ├── dist
│   │   ├── criticalcss
│   │   │   └── index_critical.min.css
│   │   ├── css
│   │   │   ├── styles.d833997e3e3f91af64e7.css
│   │   │   └── styles.d833997e3e3f91af64e7.css.map
│   │   ├── img
│   │   │   └── favicons
│   │   │       ├── android-chrome-144x144.png
│   │   │       ├── android-chrome-192x192.png
│   │   │       ├── android-chrome-256x256.png
│   │   │       ├── android-chrome-36x36.png
│   │   │       ├── android-chrome-384x384.png
│   │   │       ├── android-chrome-48x48.png
│   │   │       ├── android-chrome-512x512.png
│   │   │       ├── android-chrome-72x72.png
│   │   │       ├── android-chrome-96x96.png
│   │   │       ├── apple-touch-icon-114x114.png
│   │   │       ├── apple-touch-icon-120x120.png
│   │   │       ├── apple-touch-icon-144x144.png
│   │   │       ├── apple-touch-icon-152x152.png
│   │   │       ├── apple-touch-icon-167x167.png
│   │   │       ├── apple-touch-icon-180x180.png
│   │   │       ├── apple-touch-icon-57x57.png
│   │   │       ├── apple-touch-icon-60x60.png
│   │   │       ├── apple-touch-icon-72x72.png
│   │   │       ├── apple-touch-icon-76x76.png
│   │   │       ├── apple-touch-icon.png
│   │   │       ├── apple-touch-icon-precomposed.png
│   │   │       ├── apple-touch-startup-image-1182x2208.png
│   │   │       ├── apple-touch-startup-image-1242x2148.png
│   │   │       ├── apple-touch-startup-image-1496x2048.png
│   │   │       ├── apple-touch-startup-image-1536x2008.png
│   │   │       ├── apple-touch-startup-image-320x460.png
│   │   │       ├── apple-touch-startup-image-640x1096.png
│   │   │       ├── apple-touch-startup-image-640x920.png
│   │   │       ├── apple-touch-startup-image-748x1024.png
│   │   │       ├── apple-touch-startup-image-750x1294.png
│   │   │       ├── apple-touch-startup-image-768x1004.png
│   │   │       ├── browserconfig.xml
│   │   │       ├── coast-228x228.png
│   │   │       ├── favicon-16x16.png
│   │   │       ├── favicon-32x32.png
│   │   │       ├── favicon.ico
│   │   │       ├── firefox_app_128x128.png
│   │   │       ├── firefox_app_512x512.png
│   │   │       ├── firefox_app_60x60.png
│   │   │       ├── manifest.json
│   │   │       ├── manifest.webapp
│   │   │       ├── mstile-144x144.png
│   │   │       ├── mstile-150x150.png
│   │   │       ├── mstile-310x150.png
│   │   │       ├── mstile-310x310.png
│   │   │       ├── mstile-70x70.png
│   │   │       ├── yandex-browser-50x50.png
│   │   │       └── yandex-browser-manifest.json
│   │   ├── js
│   │   │   ├── analytics.45eff9ff7d6c7c1e3c3d4184fdbbed90.js
│   │   │   ├── app.30334b5124fa6e221464.js
│   │   │   ├── app.30334b5124fa6e221464.js.map
│   │   │   ├── app-legacy.560ef247e6649c0c24d0.js
│   │   │   ├── app-legacy.560ef247e6649c0c24d0.js.map
│   │   │   ├── confetti.1152197f8c58a1b40b34.js
│   │   │   ├── confetti.1152197f8c58a1b40b34.js.map
│   │   │   ├── confetti-legacy.8e9093b414ea8aed46e5.js
│   │   │   ├── confetti-legacy.8e9093b414ea8aed46e5.js.map
│   │   │   ├── precache-manifest.f774c437974257fc8026ca1bc693655c.js
│   │   │   ├── styles-legacy.d833997e3e3f91af64e7.js
│   │   │   ├── styles-legacy.d833997e3e3f91af64e7.js.map
│   │   │   ├── vendors~confetti~vue.03b9213ce186db5518ea.js
│   │   │   ├── vendors~confetti~vue.03b9213ce186db5518ea.js.map
│   │   │   ├── vendors~confetti~vue-legacy.e31223849ab7fea17bb8.js
│   │   │   ├── vendors~confetti~vue-legacy.e31223849ab7fea17bb8.js.map
│   │   │   └── workbox-catch-handler.js
│   │   ├── manifest.json
│   │   ├── manifest-legacy.json
│   │   ├── report-legacy.html
│   │   ├── report-modern.html
│   │   ├── webapp.html
│   │   └── workbox-catch-handler.js
│   ├── favicon.ico -> dist/img/favicons/favicon.ico
│   ├── index.php
│   ├── offline.html
│   ├── offline.svg
│   └── sw.js
├── webpack.common.js
├── webpack.dev.js
├── webpack.prod.js
├── webpack.settings.js
└── yarn.lock
複製代碼

INJECTING SCRIPT & CSS TAGS IN YOUR HTML

經過這裏顯示的webpack配置,<script><style>不會做爲生產構建的一部分注入到HTML中,該設置使用Craft CMS,它具備模板系統,咱們使用Twigpack插件注入標籤。

若是你沒有使用Craft CMS或具備模板引擎的系統,而且但願將這些標記注入到HTML中,那麼須要使用HtmlWebpackPlugin執行此操做,這個配置已經包含在內,你只須要添加一個配置來告訴它將標籤注入到HTML。

CRAFT CMS 3 INTEGRATION WITH THE TWIGPACK PLUGIN

若是你沒有使用Craft CMS 3,能夠跳過這一部分,它只是提供了一些有用的集成信息。

我寫了一個叫Twigpack的免費插件,能夠很容易地將咱們的webpack構建設置與Craft CMS 3集成。

它處理manifest.json文件以將入口點注入到Twig模板中,甚至用於處理執行舊版/新版模塊注入,異步css加載以及更多的模式。

它將使這裏介紹的webpack4配置很是簡單。

爲了包含CSS,我這樣作:

<!--# if expr="$HTTP_COOKIE=/critical\-css\=1/" -->
    {{ craft.twigpack.includeCssModule("styles.css", false) }}
<!--# else -->
    <script>
        Cookie.set("critical-css", '1', { expires: "7D", secure: true });
    </script>
    {{ craft.twigpack.includeCriticalCssTags() }}

    {{ craft.twigpack.includeCssModule("styles.css", true) }}
    {{ craft.twigpack.includeCssRelPreloadPolyfill() }}
<!--# endif -->
複製代碼

<!--#-->HTML註釋是Nginx Servier Side Includes指令,模式是若是設置了critical-css cookie,用戶已經在過去7天訪問過咱們的網站,那麼他們的瀏覽器應該有網站css緩存,咱們只是正常提供網站css。

若是沒有設置critical-css cookie,咱們經過TinyCookie設置cookie,包括咱們的Critical CSS,並異步加載站點CSS。有關Critical CSS的詳細信息,能夠參考Implementing Critical CSS on your website文章。

爲了提供咱們的javascript,咱們只需執行如下操做:

{{ craft.twigpack.includeSafariNomoduleFix() }}
{{ craft.twigpack.includeJsModule("app.js", true) }}
複製代碼

第二個參數true告訴它將JavaScript異步模塊加載,所以生成的HTML以下所示:

<script>
!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();
</script>
<script type="module" src="http://example.test/dist/js/app.273e88e73566fecf20de.js"></script>
<script nomodule src="http://example.test/dist/js/app-legacy.95d36ead9190c0571578.js"></script>
複製代碼

有關詳細介紹,請查看Twigpack文檔。

這是我使用的完整config/twigpack.php文件,請注意,它具備我在Homestead VM內部運行的本地設置,與你的設置可能不一樣:

return [
    // Global settings
    '*' => [
        // If `devMode` is on, use webpack-dev-server to all for HMR (hot module reloading)
        'useDevServer' => false,
        // The JavaScript entry from the manifest.json to inject on Twig error pages
        'errorEntry' => '',
        // Manifest file names
        'manifest' => [
            'legacy' => 'manifest-legacy.json',
            'modern' => 'manifest.json',
        ],
        // Public server config
        'server' => [
            'manifestPath' => '/dist/',
            'publicPath' => '/',
        ],
        // webpack-dev-server config
        'devServer' => [
            'manifestPath' => 'http://localhost:8080/',
            'publicPath' => 'http://localhost:8080/',
        ],
        // Local files config
        'localFiles' => [
            'basePath' => '@webroot/',
            'criticalPrefix' => 'dist/criticalcss/',
            'criticalSuffix' => '_critical.min.css',
        ],
    ],
    // Live (production) environment
    'live' => [
    ],
    // Staging (pre-production) environment
    'staging' => [
    ],
    // Local (development) environment
    'local' => [
        // If `devMode` is on, use webpack-dev-server to all for HMR (hot module reloading)
        'useDevServer' => true,
        // The JavaScript entry from the manifest.json to inject on Twig error pages
        'errorEntry' => 'app.js',
        // webpack-dev-server config
        'devServer' => [
            'manifestPath' => 'http://localhost:8080/',
            'publicPath' => 'http://192.168.10.10:8080/',
        ],
    ],
];
複製代碼

WRAPPING UP!

哇,這是一個深坑,當我第一次開始研究webpack時,我很快意識到它是一個很是強大的工具,具備很是強大的功能。你走多遠取決於你想要游到多深。

有關本篇文章的完整源代碼,請查看annotated-webpack-4-config倉庫。

但願這篇文章對你有所幫助,慢慢消化,將它作的更棒。

FURTHER READING

若是你想收到有關新文章的通知,請在推特上關注@nystudio107

相關文章
相關標籤/搜索