用于前端开发的webpack4配置[带注释]

综合技术 2018-12-09 阅读原文

:heart:觉得不错点个赞哟:heart:。原文链接

随着web开发变得越来越复杂,我们需要使用工具来帮助我们构建现代化网站。这是一个完整通过复杂webpack4配置的线上生产例子。

构建现代化网站已经成为自定义应用程序开发,网站期望能做的更多,具有传统应用的功能,而不仅仅是一个推广网站。

随着一个流程变得复杂时,我们就会将它分解为可管理的组件,并使用工具进行自动化构建,比如制造汽车、起草法案[法律文件]、建立网站。

使用正确的工具完成工作

像webpack这样的工具一直处于现代web开发的最前沿,正是因为如此,它帮助我们构建复杂的事物。

webpack4拥有一些意想不到的改进,最吸引我的就是它在构建速度上面变得有多快,所以我决定采用它。

hold住,因为这是一篇充满大量信息的长篇文章。

采用webpack

一年多以前,我发表了一篇文章: A Gulp Workflow for Frontend Development Automation [用于前端自动化的gulp工作流],讲解了如何使用 gulp 完成同样的事情。然而在这段时间里,我越来越多地使用像Vue-JS和GraphQL这样的前端框架,如 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": "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) => {
});
复制代码

有两点:

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 中拿到webpackentry,对于单页应用(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加载器,它们的处理顺序与列出顺序相反:

  • postcss-loader —— 将文件加载并处理为PostCSS

  • resolve-url-loader —— 将css中的所有 url() 重写为相对路径

  • css-loader —— 解析我们所有的CSS @importurl()

  • vue-style-loader —— 将.vue 单文件中注入所有的css。

  • style-loader —— 将所有CSS注入到中

我们在本地开发过程中不需要将所有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,
    };
};
复制代码

这将使用HtmlWebpackPlugin与 WebappWebpackPlugin (见下文)插件为我们的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加载器,它们按照列出的相反顺序进行处理:

  • postcss-loader —— 将文件加载并处理为 PostCSS

  • resolve-url-loader —— 将css中的所有url()重写为相对路径

  • css-loader —— 解析我们所有的CSS @import 和 url()

  • MiniCssExtractPlugin.loader —— 将所有css提取到一个文件中

由于这是一个生产环境构建,我们使用 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 仓库。

希望这篇文章对你有所帮助,慢慢消化,将它做的更棒。

稀土掘金

责编内容by:稀土掘金阅读原文】。感谢您的支持!

您可能感兴趣的

CSS Keylogger (and why you shouldn’t worry about i... Leveraging CSS attribute selectors it – in theory – is possible to write a keylogger in pure CSS . The selector be...
Faster Page Load Using Lightweight CSS and SVG Ani... Including animations on a web page is an exciting process. However exciting, it can be also an expensive process for ...
Website Development Options I believe the options for a web developer to create a website are: Use a targeted theme with WordPress (eg a resta...
IE CSS “display:box” alternative using... I'm using Flexie display: box; to be able to use the display box with IE . as shown in the dis...
Web Animation Tutorials Roundup With the holidays and the end of the year coming up, you might just find yourself with some spare time to learn somethin...