内容简介::heart:觉得不错点个赞哟:heart:。原文链接随着web开发变得越来越复杂,我们需要使用工具来帮助我们构建现代化网站。这是一个完整通过复杂webpack4配置的线上生产例子。构建现代化网站已经成为自定义应用程序开发,网站期望能做的更多,具有传统应用的功能,而不仅仅是一个推广网站。
: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的知识,甚至是如何安装它,下面有很多、资料给你选择,你可以选择你认为不错的方式:
-
webpack—the Confusing Parts ——简要概述了webpack的工作原理。
-
webpack documentation —— 建议读webpack官方文档,如果你想学好它的话
-
webpack fundamentals —— 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 容器中构建),我希望尽可能优化每一个点。因此,我们区分
dev
和prod
的配置以及构建。 -
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 —— 目前,图片仍然是大部分网页呈现的主要内容,所以可以通过
mozjpeg
,optipng
,svgo
等自动化工具来压缩优化图片资源是很有必要的。 -
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
设置中,我们将 legacyBrowsers
和 modernBrowsers
传递给 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 Better package.json for the Frontend article 一文中讨论过的类似方法,为了封锁从项目之间配置变为单独的 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.js
, webpack.dev.js
和 webpack.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
配置文件,包含 dev
和 prod
构建类型间共享的所有配置。
// 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.entries
从 webpack.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]' } } ] }; }; 复制代码
dev
和 prod
构建字体加载是相同的,所以我们把它写在这里,对于我们使用的任何本地字体,我们可以通知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
将与 modernConfig
和 legacyConfig
合并:
// 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.js
和 webpack.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
@import
和url()
-
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
。
通过设置 mode
为 development
,告知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-merge
将 webpack.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 仓库。
希望这篇文章对你有所帮助,慢慢消化,将它做的更棒。
以上所述就是小编给大家介绍的《用于前端开发的webpack4配置[带注释]》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- iOS 注释方法大全 代码块加快捷键注释
- 让 MyBatis Generator 用数据库注释作 Java 注释,并支持附加注解
- 请停止代码注释
- 体面编码之代码注释评论
- Spring 注解注入—@Qualifier 注释
- swagger注释API详细说明
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
解决网页设计一定会遇到的210个问题
2006-4 / 42.00元
如何选择适合、简单、方便、快速的方法来解决您的网页设计问题?不会HTML、JavaScript、CSS也可轻易完成许多网页功能与特效。本书包含上百种HTML、JavaScript、CSS使用应用技巧与盲点解说,包含10个常用表单资料判断函数与特殊技巧,不必修改就可用于任何网页。本书现有的多数网页设计书籍相辅相成,让您事半功倍地完成工作。 许多计算机书籍都是从某个语言或者某个软件的......一起来看看 《解决网页设计一定会遇到的210个问题》 这本书的介绍吧!