1. 程式人生 > >用於前端開發的webpack4配置[帶註釋]

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

❤️覺得不錯點個贊喲❤️。原文連結

隨著web開發變得越來越複雜,我們需要使用工具來幫助我們構建現代化網站。這是一個完整通過複雜webpack4配置的線上生產例子。

構建現代化網站已經成為自定義應用程式開發,網站期望能做的更多,具有傳統應用的功能,而不僅僅是一個推廣網站。

隨著一個流程變得複雜時,我們就會將它分解為可管理的元件,並使用工具進行自動化構建,比如製造汽車、起草法案[法律檔案]、建立網站。

使用正確的工具完成工作

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

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

hold住,因為這是一篇充滿大量資訊的長篇文章。

採用webpack

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

我發現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": "[email protected]",
        "url": "https://github.com/example-developer/example-project/issues"
    },
    "license": "SEE LICENSE IN LICENSE.md",
    "author": {
        "name": "Example Developer",
        "email": "[email protected]",
        "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) => {
});
複製程式碼

有兩點:

1、通過/* webpackChunkName: "vue" */,我們告訴webpack希望這個動態程式碼拆分塊被命名。

2、由於我們在非同步函式(“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: {