从零开始配置webpack(优化篇)

文章目录

  • 从零开始配置webpack(优化篇)
    • 前言
    • 提升开发体验
      • source-map
    • 提升打包构建速度
      • HotModuleReplacement(模块热替换与热加载)
        • 模块热替换
        • 模块热加载
      • OneOf
      • Include/Exclude
      • Cache
    • 减少代码体积
      • Tree Shaking
        • sideEffects
    • 优化代码运行性能
      • Network Cache
      • Core-js
      • PWA

前言

从零开始配置webpack系列(基础篇)

从零开始配置webpack系列(进阶篇)

从零开始配置webpack系列(优化篇)

从零开始配置webpack系列(原理篇)

提升开发体验

source-map

无论是什么开发,要求开发环境最不可少的一点功能就是 ——debug功能。

之前我们通过webpack, 将我们的源码打包成了 bundle.js。 试想:实际上客户端(浏览器)读取的是打包后的 bundle.js ,那么当浏览器执行代码报错的时候,报错的信息自然也是bundle的内容。我们如何将报错信息(bundle错误 的语句及其所在行列)映射到源码上?souce-map

webpack已经内置了sourcemap的功能,我们只需要通过简单的配置,将可以开启它。

module.exports = {// 开启 source map// 开发中推荐使用 'source-map'// 生产环境一般不开启 sourcemapdevtool: 'source-map',}

当我们执行打包命令之后,我们发现bundle的最后一行总是会多出一个注释,指向 打包出的bundle.map.js(sourcemap文件)。 sourcemap文件用来描述 源码文件和 bundle文件的代码位置映射关系。基于它,我们将bundle文件的错误信息映射到源 码文件上。

除开’source-map’外,还可以基于我们的需求设置其他值,webpack——devtool一 共提供了7种SourceMap模式:

模式解释
eval每个module会封装到 eval 里包裹起来执行,并且会在末尾追 加注释 //@ sourceURL.
source-map生成一个SourceMap文件.
hidden-source-map和 source-map 一样,但不会在 bundle 末尾追加注释.
inline-source-map生成一个 DataUrl 形式的 SourceMap 文件
eval-source-map每个module会通过eval()来执行,并且生成一个DataUrl形式的 SourceMap.
cheap-source-map生成一个没有列信息(column-mappings)的SourceMaps文 件,不包含loader的 sourcemap(譬如 babel 的 sourcemap)
cheap-module-source-map生成一个没有列信息(column-mappings)的SourceMaps文 件,同时 loader 的 sourcemap 也被简化为只包含对应行的

要注意的是,生产环境我们一般不会开启sourcemap功能,

主要有两点原因:

  1. 通过bundle和sourcemap文件,可以反编译出源码————也就是说,线上产 物有soucemap文件的话,就意味着有暴漏源码的风险。
  2. 可以观察到,sourcemap文件的体积相对比较巨大,这跟我们生产环境的追 求不同(生产环境追求更小更轻量的bundle)

提升打包构建速度

HotModuleReplacement(模块热替换与热加载)

模块热替换

模块热替换(HMR – hot module replacement)功能会在应用程序运行过程中, 替换、添加或删除模块,而无需重新加载整个页面

启用 webpack 的 热模块替换 特性,需要配置devServer.hot参数

module.exports = {//...devServer: {hot: true,},};

此时我们实现了基本的模块热替换功能。

模块热加载

热加载(文件更新时,自动刷新我们的服务和页面) 新版的webpack-dev-server 默认已经开启了热加载的功能。 它对应的参数是devServer.liveReload,默认为 true。 注意,如果想要关掉它,要将liveReload设置为false的同时,也要关掉 hot

module.exports = {//...devServer: {liveReload: false, //默认为true,即开启热更新功能。},};

OneOf

打包时每个文件都会经过所有 loader 处理,虽然因为 test 正则原因实际没有处理上,但是都要过一遍。比较慢。

OneOf 就是只能匹配上一个 loader, 剩下的就不匹配了。

使用:

const path = require("path");const ESLintWebpackPlugin = require("eslint-webpack-plugin");const HtmlWebpackPlugin = require("html-webpack-plugin");module.exports = {entry: "./src/main.js",output: {path: undefined, // 开发模式没有输出,不需要指定输出目录filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中// clean: true, // 开发模式没有输出,不需要清空输出结果},module: {rules: [{oneOf: [{// 用来匹配 .css 结尾的文件test: /\.css$/,// use 数组里面 Loader 执行顺序是从右到左use: ["style-loader", "css-loader"],},{test: /\.(png|jpe?g|gif|webp)$/,type: "asset",parser: {dataUrlCondition: {maxSize: 10 * 1024, // 小于10kb的图片会被base64处理},},generator: {// 将图片文件输出到 static/imgs 目录中// 将图片文件命名 [hash:8][ext][query]// [hash:8]: hash值取8位// [ext]: 使用之前的文件扩展名// [query]: 添加之前的query参数filename: "static/imgs/[hash:8][ext][query]",},},{test: /\.(ttf|woff2?)$/,type: "asset/resource",generator: {filename: "static/media/[hash:8][ext][query]",},},{test: /\.js$/,exclude: /node_modules/, // 排除node_modules代码不编译loader: "babel-loader",},],},],},plugins: [new ESLintWebpackPlugin({// 指定检查文件的根目录context: path.resolve(__dirname, "../src"),}),new HtmlWebpackPlugin({// 以 public/index.html 为模板创建文件// 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源template: path.resolve(__dirname, "../public/index.html"),}),],// 开发服务器devServer: {host: "localhost", // 启动服务器域名port: "3000", // 启动服务器端口号open: true, // 是否自动打开浏览器hot: true, // 开启HMR功能},mode: "development",devtool: "cheap-module-source-map",};

Include/Exclude

开发时我们需要使用第三方的库或插件,所有文件都下载到 node_modules 中了。而这些文件是不需要编译可以直接使用的。

所以我们在对 js 文件处理时,要排除 node_modules 下面的文件。

  • include

包含,只处理 xxx 文件

  • exclude

排除,除了 xxx 文件以外其他文件都处理

module.exports = {module: {rules: [{oneOf: [{test: /\.js$/,// exclude: /node_modules/, // 排除node_modules代码不编译include: path.resolve(__dirname, "../src"), // 也可以用包含loader: "babel-loader",},],},],},plugins: [new ESLintWebpackPlugin({// 指定检查文件的根目录context: path.resolve(__dirname, "../src"),exclude: "node_modules", // 默认值}),],};

Cache

每次打包时 js 文件都要经过 Eslint 检查 和 Babel 编译,速度比较慢。

我们可以缓存之前的 Eslint 检查 和 Babel 编译结果,这样第二次打包时速度就会更快了

Cache 对 Eslint 检查 和 Babel 编译结果进行缓存。

module.exports = {module: {rules: [{oneOf: [{test: /\.js$/,// exclude: /node_modules/, // 排除node_modules代码不编译include: path.resolve(__dirname, "../src"), // 也可以用包含loader: "babel-loader",options: {cacheDirectory: true, // 开启babel编译缓存cacheCompression: false, // 缓存文件不要压缩},},],},],}, };

减少代码体积

Tree Shaking

开发时我们定义了一些工具函数库,或者引用第三方工具函数库或组件库。

如果没有特殊处理的话我们打包时会引入整个库,但是实际上可能我们可能只用上极小部分的功能。

这样将整个库都打包进来,体积就太大了

Tree Shaking 是一个术语,通常用于描述移除 JavaScript 中的没有使用上的代码。

注意:它依赖 ES Module

Webpack 已经默认开启了这个功能,无需其他配置

sideEffects

Webpack 不能百分百安全地进行 tree-shaking。有些模块导入,只要被引入,就会对应用程序产生重要的影响。一个很好的例子就是全局样式表,或者设置全局配 置的JavaScript 文件。

Webpack 认为这样的文件有“副作用”。具有副作用的文件不应该做 tree-shaking, 因为这将破坏整个应用程序

如何告诉 Webpack 你的代码无副作用,可以通过 package.json 有一个特殊的属性 sideEffects,就是为此而存在的。

它有三个可能的值:

  • true 如果不指定其他值的话。这意味着所有的文件都有副作用,也就是没有一个文件 可以 tree-shaking。
  • false 告诉 Webpack 没有文件有副作用,所有文件都可以 tree-shaking。
  • 数组[…] 是文件路径数组。它告诉 webpack,除了数组中包含的文件外,你的任何文件都没有副作用。因此,除了指定的文件之外,其他文件都可以安全地进行 treeshaking。

优化代码运行性能

Network Cache

将来开发时我们对静态资源会使用缓存来优化,这样浏览器第二次请求资源就能读取缓存了,速度很快。

但是这样的话就会有一个问题, 因为前后输出的文件名是一样的,都叫 main.js,一旦将来发布新版本,因为文件名没有变化导致浏览器会直接读取缓存,不会加载新资源,项目也就没法更新了。

所以我们从文件名入手,确保更新前后文件名不一样,这样就可以做缓存了

  • fullhash(webpack4 是 hash)

每次修改任何一个文件,所有文件名的 hash 至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。

  • chunkhash

根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们 js 和 css 是同一个引入,会共享一个 hash 值。

  • contenthash

根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的

// [contenthash:8]使用contenthash,取8位长度filename: "static/js/[name].[contenthash:8].js", // 入口文件打包输出资源命名方式chunkFilename: "static/js/[name].[contenthash:8].chunk.js", // 动态导入输出资源命名方式

Core-js

过去我们使用 babel 对 js 代码进行了兼容性处理,其中使用@babel/preset-env 智能预设来处理兼容性问题。

它能将 ES6 的一些语法进行编译转换,比如箭头函数、点点点运算符等。但是如果是 async 函数、promise 对象、数组的一些方法(includes)等,它没办法处理。

所以此时我们 js 代码仍然存在兼容性问题,一旦遇到低版本浏览器会直接报错。所以我们想要将 js 兼容性问题彻底解决

core-js 是专门用来做 ES6 以及以上 API 的 polyfill

polyfill翻译过来叫做垫片/补丁。就是用社区上提供的一段代码,让我们在不兼容某些新特性的浏览器上,使用该新特性

首先修改js文件,使用promise:

import "core-js/es/promise"; //手动按需引入, import "core-js";是全部引入,体积较大import count from "./js/count";import sum from "./js/sum";const result1 = count(2, 1);console.log(result1);const result2 = sum(1, 2, 3, 4);console.log(result2);// 添加promise代码const promise = Promise.resolve();promise.then(() => {console.log("hello promise");});
npm i core-js

配置:

module.exports = {// 智能预设:能够编译ES6语法presets: [["@babel/preset-env",// 按需加载core-js的polyfill{ useBuiltIns: "usage", corejs: { version: "3", proposals: true } },],],};

此时就会自动根据我们代码中使用的语法,来按需加载相应的 polyfill

PWA

开发 Web App 项目,项目一旦处于网络离线情况,就没法访问了。我们希望给项目提供离线体验。

渐进式网络应用程序(progressive web application – PWA):是一种可以提供类似于 native app(原生应用程序) 体验的 Web App 的技术。

其中最重要的是,在 离线(offline) 时应用程序能够继续运行功能。

内部通过 Service Workers 技术实现的

npm i workbox-webpack-plugin -D

修改配置文件:

const WorkboxPlugin = require("workbox-webpack-plugin");module.exports = {plugins: [new WorkboxPlugin.GenerateSW({// 这些选项帮助快速启用 ServiceWorkers// 不允许遗留任何“旧的” ServiceWorkersclientsClaim: true,skipWaiting: true,}),]}

修改main.js

import count from "./js/count";import sum from "./js/sum";const result1 = count(2, 1);console.log(result1);const result2 = sum(1, 2, 3, 4);console.log(result2);// 添加promise代码const promise = Promise.resolve();promise.then(() => {console.log("hello promise");});const arr = [1, 2, 3, 4, 5];console.log(arr.includes(5));// 配置 PWAif ("serviceWorker" in navigator) {window.addEventListener("load", () => {navigator.serviceWorker.register("/service-worker.js").then((registration) => {console.log("SW registered: ", registration);}).catch((registrationError) => {console.log("SW registration failed: ", registrationError);});});}