【webpack5】一些常见优化配置及原理介绍(二)

news2024/9/24 17:13:30

这里写目录标题

  • 介绍
  • sourcemap定位报错
  • 热模块替换(或热替换,HMR)
  • oneOf精准解析
  • 指定或排除编译
  • 开启缓存
  • 多进程打包
  • 移除未引用代码
  • 配置babel,减小代码体积
  • 代码分割(Code Split)
    • 介绍
  • 预获取/预加载(prefetch/preload)
  • 静态资源缓存(Network Cache)
  • JS兼容
  • 渐进式网络应用程序(PWA)
  • 资料


介绍

对于webpack配置优化,目的包括但不限于:

  • 提升开发体验
  • 提升打包速度
  • 减少代码体积
  • 优化代码运行性能

sourcemap定位报错

实际开发中,会遇到这样一个问题,比如我写一串代码如下:

/*index.js*/

import sum from './sum';
import './index.css';

console.log(sum(1, 2, 3, 4));
console.log(1111)(); // 注意这行,多写了一对(),是错误写法

现在启动开发服务器,在浏览器中查看报错:
在这里插入图片描述
它提示,在index.js的第30中出现了错误,点击这行报错,在source

查看。可以看到,它给提示的第30行,是开发模式下打包后的行数,

并非我们实际代码中的行数。不便于我们快速锁定错误代码和处理bug。
在这里插入图片描述

解决这个问题,就需要用到devtool配置中的sourcemap配置项。

它可以在打包代码和源代码之间建立一种映射关系,便于排查问题。

缺点是会在一定程度上拖慢打包速度。

文档:

https://www.webpackjs.com/configuration/devtool/

webpack配置文件中,配置devtool字段,它有多个值,

其中,有两个是最常用的:

  • 开发模式:cheap-module-source-map
  • 生产模式:source-map

配置:

module.exports = {
  mode: "development",
  // ...
  devtool: "cheap-module-source-map"
}
// 注:生产模式下会多生成一份.map文件,增加了包的体积
// 另外,建立映射后会增加暴露源码的风险

module.exports = {
  mode: "production",
  // ...
  devtool: "source-map"
}

配置后,重新启动开发服务器,查看报错内容:
在这里插入图片描述
在这里插入图片描述

此时就很容易看到这一行的报错,和源码完全一致。


热模块替换(或热替换,HMR)

在开发中,每次修改内容后,想查看效果,需要重新打包并刷新页面,

这大大增加了开发耗时。

HMR就解决了这个问题。它能够在修改内容后,只重新打包更新的模块,

无需刷新页面,即可实现页面内容更新。

在配置了devServer后,会自动开启HMR。

文档:

https://www.webpackjs.com/configuration/dev-server/

但是,这个配置并不支持js代码的热替换,在更新js代码后,会实现

热更新,即页面内容会实时变化,但是页面仍然会刷新。

这需要额外配置。比如:

// index.js

import sum from './sum';
import count from './sum';

if(module.hot) {
  module.hot.accept('./sum.js'); // 对sum.js实现热替换
  module.hot.accept('./count.js'); // 对count.js实现热替换
}

文档:

https://www.webpackjs.com/api/hot-module-replacement/

很明显,这个写法一点都不美妙,每个文件都要写一次accept。

除此之外,还有其他一些框架的loader可以实现js代码的热替换功能。

比如Reactreact-hot-loader

文档:

https://github.com/gaearon/react-hot-loader

还有Vuevue-loader

文档:

https://github.com/vuejs/vue-loader

这些loader配置在脚手架中已经内置了。


oneOf精准解析

在使用很多loader时,都要一个一个的挨个去检查,这个loader是否可以

解析目标文件,会把全部loader都检查一遍。而使用oneOf之后,找到某

个匹配的loader后,就不继续往后找了,降低耗时。类似于switch语句中

的break语句。

文档:

https://www.webpackjs.com/configuration/module/#ruleoneof

配置:

module.exports = {
  // ...
  module: {
    rules: [
      {
        oneOf: [
          {
            test: /\.css$/,
            use: getStyleLoader()
          },
          {
            test: /\.less$/,
            use: getStyleLoader('less-loader')
          },
          {
            test: /\.s[ac]ss$/,
            use: getStyleLoader('sass-loader')
          },
          {
            test: /\.(png|jpe?g|gif|svg|webp)$/,
            type: 'asset',
            parser: {
              dataUrlCondition: {
                maxSize: 10 * 1024 // 10kb
              }
            }
          },
          {
            test: /\.(ttf|woff2?)$/,
            type: 'asset/resource'
          },
          {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader'
          }
        ]
      }
    ]
  }
}

指定或排除编译

针对js代码,指定处理某些文件或者把某些文件排除,能减少编译文件

数量,加快编译速度。这里需要用到:

  • include: 指定编译某些文件
  • excluede: 排除某些文件

用其中一个即可。

配置:

module.exports = {
  // ...
  module: {
	rules: [
	  // ...
	  {
		test: /\.js$/,
		exclude: /node_modules/, // 不编译node_modules中的文件
		loader: 'babel-loader'
	  }
	]
  }
}

或者:

const path = require('path');

module.exports = {
  // ...
  module: {
	rules: [
	  // ...
	  {
		test: /\.js$/,
		// 只编译src目录下的文件
		include: path.resolve(__dirname, 'src'), 
		loader: 'babel-loader'
	  }
	]
  }
}

开启缓存

babel和eslint处理js,开启二者的缓存,可以减少js的编译时间。

文档:

https://www.webpackjs.com/loaders/babel-loader/

配置:

const path = require('path');
const EslintPlugin = require('eslint-webpack-plugin');

module.exports = {
  mode: 'development',
  // ...
  module: {
	rules: [
	  // ...
	  {
		test: /\.js$/,
		loader: 'babel-loader',
		include: path.resolve(__dirname, 'src'),
		options: {
		  cacheDirectory: true, // 开启缓存
		  cacheCompression: false, // 关闭压缩
		}
	  }
	]
  },
  plugins: [
	new EslintPlugin({
	  context: path.resolve(__dirname, 'src'),
	  cache: true, // 开启缓存功能
	  // 缓存的eslint文件存放路径
	  cacheLocation: path.resolve(__dirname, 'node_modules/.cache/eslintcash')
	})
  ]
}

多进程打包

需要使用的包:

  • thread-loader 开启多进程,一般放在js相关loader的前面或上面
    一般用来处理js或者较大的项目的打包构建。
  • eslint-webpack-plugin 开启eslint多进程检查
  • terser-webpack-plugin 开启多进程压缩,提升打包速度。
    这是webpack内置的一个压缩js代码的工具,需要自定义时,仍然需要先安装它。

文档:

# 开启loader的多进程
https://www.webpackjs.com/loaders/thread-loader/

# 开启eslint的多进程
https://www.webpackjs.com/plugins/eslint-webpack-plugin/

# 开启打包的多进程
https://www.webpackjs.com/plugins/terser-webpack-plugin/

现在的电脑都是多核处理器,可以启动多进程打包,在打包一些大型文件时,会节省时间。

需要注意的是,如果项目本身不大,则不必开启多进程打包,因为开启一个进程要耗费600ms.

安装:

npm i thread-loader -D

npm i eslint-webpack-plugin -D

npm i terser-webpack-plugin -D

配置:

const EslintPlugin = require('eslint-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  // ...
  module: {
	rules: [
	  {
		test: /\.js$/,
		exclude: /node_nodules/,
		use: [
		  {
			loader: 'thread-loader',
			options: {
			  workerParallelJobs: 50,
			  workerNodeArgs: ['--max-old-space-size=4096'],
			  poolRespawn: false,
			  poolTimeout: 2000,
			  poolParallelJobs: 50,
			  name: "js-pool"
			}
		  },
		  {
		    loader: 'babel-loader'
		  }
		]
	  }
	]
  },
  plugins: [
	new EslintPlugin({
	  context: path.resolve(__dirname, 'src'),
	  threads: true // 开启多进程
	})
  ],
  // 和压缩相关的plugin现在一般写在optimization的minimizer数组中
  optimization: {
	minimize: true,
	minimizer: [
	  new TerserPlugin({
		parallel: true // 开启多进程压缩
	  })
	]
  }
}

移除未引用代码

这是用Tree Shaking概念来处理的。

它依赖于ES6模块,如importexport,检测出未被引用的代码,

在打包时,这些代码不会被打包进来。

这是webpack内置的功能,无需我们配置。

文档:

https://www.webpackjs.com/guides/tree-shaking/

配置babel,减小代码体积

babel在做语法转换时,会在js代码中加入一些辅助函数,随着js文件的增多,

辅助函数的数量也会增大,当这些辅助函数随着一起被打包进来的时候,

打包后的体积就会跟着增加。

要解决这个问题,就需要用到两个插件:

  • @babel/runtime 用来提供更优雅的辅助函数
  • @babel/plugin-tranform-runtime 它会把所有用到的辅助函数集中到一起,辅助函数数量就大大缩减

文档:

https://www.babeljs.cn/docs/babel-plugin-transform-runtime

https://zhuanlan.zhihu.com/p/394783228

安装:

npm i @babel/plugin-transform-runtime -D

npm i @babel/runtime

配置:

// .babelrc.js

module.exports = {
 // ...
 plugins: [
   "@babel/plugin-transform-runtime"
 ]
}

或者:

// webpack.config.js

// ...
{
  loader: 'babel-loader',
  // ...
  options: {
	plugins: ['@babel/plugin-transform-runtime']
  }
}

代码分割(Code Split)

介绍

比如有这样几个文件:

// sum.js

export const str = 'hello';
// index.js

import { str } from './sum';

console.log('index str: ', str);
// main.js
import { str } from './sum';

console.log('main str: ', str);

打包index.jsmain.js文件,查看打包结果:

// index.js
(()=>{"use strict";console.log("index str: ","hello")})();

// main.js
(()=>{"use strict";console.log("main str: ","hello")})();

可以看到,从sum.js中引入的str被分别打包进index.jsmain.js,相当于sum.js被打包了两次,

有没有什么方法,可以把sum.js单独打包,然后让其他文件分别引用,以减小打包体积呢?

另外,当一个文件从sum.js中引入了str,是否可以先不调用,只等发生了点击

或者进入了某个页面后再调用呢?

这就引出了代码分割的作用:

  • 将打包生成的文件进行分割,生成多个js文件
  • 按需加载:需要哪个文件,就加载哪个文件

文档:

https://www.webpackjs.com/guides/code-splitting/

配置:

https://www.webpackjs.com/plugins/split-chunks-plugin/

SplitChunksPlugin是webpack内置的代码分割插件,开箱即用,无需安装。

默认实现了代码分割功能,也可以自定义配置。

自定义配置:

// webpack.config.js
const path = require("path");

module.exports = {
  // 单入口
  // entry: './src/index.js',
  // 多入口
  entry: {
    index: "./src/index.js",
    main: "./src/main.js",
  },
  output: {
    path: path.resolve(__dirname, "./dist"),
    // [name]是webpack命名规则,使用chunk的name作为输出的文件名。
    // 什么是chunk?打包的资源就是chunk,输出出去叫bundle。
    // chunk的name是啥呢? 比如: entry中xxx: "./src/aaa.js", name就是xxx。
    // 注意是前面的xxx,和文件名无关。
    // 为什么需要这样命名呢?如果还是之前写法main.js,那么打包生成两个js文件
    // 都会叫做main.js会发生覆盖。(实际上会直接报错的)
    filename: "js/[name].js",
    clean: true,
  },
  mode: "production",
  optimization: {
    // 代码分割配置
    splitChunks: {
      chunks: "all", // 对所有模块都进行分割
      // 以下是默认值
      // 分割代码最小的大小,单位是byte。
      // 如果小于这个值,就不会去分割
      // minSize: 20000, 
      // minRemainingSize: 0, // 类似于minSize,最后确保提取的文件大小不能为0
      // minChunks: 1, // 至少被引用的次数,满足条件才会代码分割
      // maxAsyncRequests: 30, // 按需加载时并行加载的文件的最大数量
      // maxInitialRequests: 30, // 入口js文件最大并行请求数量
      // 超过50kb一定会单独打包
      //(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
      // enforceSizeThreshold: 50000, 
      // cacheGroups: { // 组,哪些模块要打包到一个组
      //   defaultVendors: { // 组名
      //     test: /[\\/]node_modules[\\/]/, // 需要打包到一起的模块
      //     priority: -10, // 权重(越大越高)
      // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,
      // 则它将被重用,而不是生成新的模块
      //     reuseExistingChunk: true, 
      //   },
      //   default: { // 其他没有写的配置会使用上面的默认值
      //     minChunks: 2, // 这里的minChunks权重更大
      //     priority: -20,
      //     reuseExistingChunk: true,
      //   },
      // },
      // 修改配置 要自定义配置,就在这里面修改参数的值
      cacheGroups: {
        default: {
          // 其他没有写的配置会使用上面的默认值
          minSize: 20 * 1024, // 小于20kb的文件,不会被分割
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

按需引入要怎么做呢?

需要使用动态引入语法import

比如:

// clickbtn.js

document.querySelector('.btn').onclick = function () {
  import('./sum')
    .then(res => { console.log('click str: ', res.str) })
    .catch(err => { console.log(err) });
}

这样就实现了sum.js的按需引入,点击才会调用。

再说说chunk.js文件的命名。

打包后的chunk.js的名字默认是chunk的id命名的,比如52.js 456.js

这里的52和456都是生成的chunk文件的id,我们很难识别它都是谁的打包文件。

因此,当我们需要知道它是谁的打包文件时,就需要给它起个名字。

方法如下:

// 首先是在使用import按需引入时
// 这里的webpackChunkName就是打包sum.js后的chunk名字
// 使用的内联注释激活方法
import(/* webpackChunkName: 'sum' */ './sum.js').then(...)

// 然后在出口里配置:
module.exports = {
  // ...
  output: {
	path: ..
	filename: ..
	chunkFilename: '[name].chunk.js'//这里的name就是webpackChunkName
  }
}

打包后生成的chunk名字就是sum.chunk.js了。

当然,这种配置也只是在我们想知道是哪个chunk文件打包得到的时候才去配置,

毕竟我们不想每次做import动态引入的时候加上/* webpackChunkName: 'xxx' */


预获取/预加载(prefetch/preload)

文档:

https://www.webpackjs.com/guides/code-splitting/#prefetchingpreloading-modules

https://www.webpackjs.com/plugins/prefetch-plugin/

它们也是代码分割策略的一部分。

prefetch是在浏览器空闲的时候进行加载,有三种配置实现方式:

第一种:

// 在import动态引入时通过内联注释来实现
import(/* webpackPrefetch: true */ './sum.js').then(...)

第二种:

// 通过配置plugin的方式,内置plugin,无需安装
plugins: [
  new webpack.PrefetchPlugin([context], request);
]

第三种:

// 先安装第三方的plugin插件
npm i @vue/preload-webpack-plugin -D

// 引入插件
const PreloadPlugin = require('@vue/preload-webpack-plugin');

// 在webpack里配置插件
module.exports = {
  // ...
  plugins: [
	new PreloadPlugin({
	  rel: 'prefetch'
	})
  ]
}

静态资源缓存(Network Cache)

作用是当第一次加载过静态资源后,这些静态资源就会被缓存,以后再获取的时候,

就会直接从缓存中获取。

这里需要用到contenthashruntime相关配置。

配置:

module.exports = {
  output: {
	// ...
	filename: '[name].[contenthash:8].js,
	chunkFilename: '[name].[contenthash:8].chunk.js
  },
  plugins: [
	new MiniCssExtractPlugin({
      filename: '[name],[contenthash:8].css',
      chunkFilename: '[name].[contenthash:8].css'
	})
  ],
  // ...
  runtimeChunk: {
    // runtime文件命名
	name: (entrypoint) => `runtime~${entrypoint.name}`
  }
}

JS兼容

之前配置了@babel/preset-env用来做做兼容,但是它无法编译es6+语法,需要打补丁polyfill

文档:

https://www.babeljs.cn/docs/babel-preset-env

安装:

// 首先,默认之前已经安装了@babel/preset-env
npm i core-js

配置:

// .babelrc.js

module.exports = {
  "presets": [
    [
      "@babel/preset-env",
      {
        useBuiltIns: "usage", // 用到的依赖打包进来
        corejs: {
		  version: "3.8", // core-js的版本
		  proposals: true
		}
      }
    ]
  ]
}

渐进式网络应用程序(PWA)

网络离线时也能让应用程序继续运行。

比如你打开了一个网页,突然断网了,这时候如果有pwa功能,依然可以访问该网页。

文档:

https://www.webpackjs.com/guides/progressive-web-application/

安装:

npm i workbox-webpack-plugin -D

配置:

// 1.配置 webpack.config.js

module.exports = {
  // ...
  plugins: [
	new WorkboxPlugin.GenerateSW({
	  clientsClaim: true, 
	  skipWaiting: true
	})
  ]
}

// 2.注册 Service Worker
// 在主文件里(比如index.js)加上下面这段

if ('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);
     });
   });
 }

如果打开报错,可以继续如下操作:

安装:

npm i serve -g

启动指令:

serve dist // dist是打包生成的目录

资料

https://yk2012.github.io/sgg_webpack5/intro/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/355072.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HydroD 实用教程(四)水动力模型

目 录一、前言二、Hydro Properties2.1 Compartment Properties2.2 Rudder and Thruster2.3 Wind Properties三、Hydro Structure3.1 Load Cross Sections四、Loading Conditions4.1 Mass Model4.2 Second Order Surface Model4.3 Wadam Offbody Points4.4 Additional Matrices…

Redis的常见操作和Session的持久化

安装Redis使用yum命令,直接将redis安装到linux服务器:yum -y install redis启动redis使用以下命令,以后台运行方式启动redis:redis -server /etc/redis.conf &操作redis使用以下命令启动redis客户端:redis-cli设置…

vscode连接服务器(腾讯云)

文章目录1. vscode远程总是报错2. vscode能连上腾讯云但密码不对或者登录后不能打开文件或文件夹1. vscode远程总是报错 报错如图所示 Could not establish connection to *** 过程试图写入的管道不存在。 在百度、csdn找了好久都是说删掉.ssh文件下的某个文件但我压根没有&a…

使用 nutjs实现前端RPA需求

nutjs 相关概念 nut.js 是 Node.js 的桌面自动化框架,我们可以使用 js / ts 来控制鼠标和键盘,来模拟人的操作完成一系列动作。它主要分为以下三块内容: KeyboardMouseScreen Keyboard 常用 API type: 它允许我们输入 键 或 字符串&#…

linux013之文件和目录的权限管理

用户、组、文件目录的关系: 简介:用户和组关联,组合文件目录关联,这样就实现了用户对文件的权限管理。首先来看一下,一个文件或目录的权限是怎么查看的,ls -l, 如下,这个信息怎么看呢…

71. Python 库与模块

71. 库与模块 文章目录71. 库与模块1. Python 就是一个解释器2. 什么模块3. 什么是包5. 什么是库6. 形象理解模块与库7. 库的分类1. 标准库2. 第三方库8. 总结1. Python 就是一个解释器 我们把程序员编写的代码形象理解为写了一个纯文本文件。 我们希望这个文本文件能被计算机…

锁相环(1)

PLL代表相位锁定环。顾名思义,如下图所示,PLL是一种具有反馈循环的电路,可将反馈信号的相/频率保持与参考输入信号的相/频率相同(锁定)。 如下图所示,如果参考输入和反馈输入之间存在相位差,则…

是德Keysight E4991A/e4991B射频阻抗/材料分析仪

Keysight E4991A 射频阻抗/材料分析仪提供终极阻抗测量性能和强大的内置分析功能。它将为评估 3 GHz 范围内组件的组件和电路设计人员的研发提供创新。E4991A 使用 RF-IV 技术,而不是反射测量技术,可在宽阻抗范围内进行更精确的阻抗测量。基本阻抗精度为…

在屎山代码中快速找到想要的代码法-锁表法(C#)

由于本人水平有限,文中有写得不对的地方请指正,本文的方法有些投机取巧,实在是没招的情况下可以酌情使用,如有侵权,请联系删除。 前几天接到一个需求,要在医嘱签署时对检验项目进行分方操作,分…

Linux Socket Buffer介绍

一. 前言 Linux内核网络子系统的实现之所以灵活高效,主要是在于管理网络数据包的缓冲器-socket buffer设计得高效合理。在Linux网络子系统中,socket buffer是一个关键的数据结构,它代表一个数据包在内核中处理的整个生命周期。 二. Socket Bu…

2.5|iot|第1章嵌入式系统概论|操作系统概述|嵌入式操作系统

目录 第1章: 嵌入式系统概论 1.嵌入式系统发展史 2.嵌入式系统定义* 3.嵌入式系统特点* 4.嵌入式处理器的特点 5.嵌入式处理分类 6.嵌入式系统的应用领域及嵌入式系统的发展趋势 第8章:Linux内核配置 1.内核概述 2.内核代码结构 第1章&#xf…

[安装之3] 笔记本加装固态和内存条教程(超详细)

由于笔记本是几年前买的了,当时是4000,现在用起来感到卡顿,启动、运行速度特别慢,就决定换个固态硬盘,加个内存条,再给笔记本续命几年。先说一下加固态硬盘SSD的好处:1.启动快 2.读取延迟小 3.写…

Java每日一练——Java简介与基础练习

系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 例如:第一章 Python 机器学习入门之pandas的使用 文章目录 目录 系列文章目录 文章目录 前言 一、简述解释型语言与编译型语言 二、Java语言的执行流程 2.1、…

电子技术——CS和CE放大器的高频响应

电子技术——CS和CE放大器的高频响应 在绘制出MOS和BJT的高频响应模型之后,我们对MOS和BJT的高频响应有了进一步的认识。现在我们想知道的是在高频响应中 fHf_HfH​ 的关系。 高频响应分析对电容耦合还是直接耦合都是适用的,因为在电容耦合中高频模式下…

智能小车红外避障原理

红外避障电路红外避障电路由电位器R17,R28;发光二极管D8,D9;红外发射管 D2,D4和红外接收管D3,D5和芯片LM324等组成,LM234用于信号的比较,并产生比较结果输出给单片机进行处理。智能小车红外避障…

作为一名开发工程师,我对 ChatGPT 的一些看法

ChatGPT 又又火了。 ChatGPT 第一次爆火是2022年12月的时候,我从一些球友的讨论中知道了这个 AI 程序。 今年2月,ChatGPT 的热火更加猛烈,这时我才意识到,原来上次的热火只是我们互联网圈子内部火了,这次是真真正正的破圈了,为大众所熟悉了。 这个 AI 程序是一个智能问…

(考研湖科大教书匠计算机网络)第四章网络层-第八节:网际控制报文协议ICMP

获取pdf:密码7281专栏目录首页:【专栏必读】考研湖科大教书匠计算机网络笔记导航 文章目录一:网际控制报文协议ICMP(1)ICMP差错报告报文A:终点不可达B:源点抑制C:时间超过D&#xff…

C#的委托原理刨析and事件原理刨析和两者的比较

什么是委托委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。 在实例化委托时,你可以将其实例与任何具有兼容参数和返回类型的方法进行绑定。 你可以通过委托实例调用方法。简单的理解,委托是方法的抽象类,它定…

L2-033 简单计算器

本题要求你为初学数据结构的小伙伴设计一款简单的利用堆栈执行的计算器。如上图所示,计算器由两个堆栈组成,一个堆栈 S1​ 存放数字,另一个堆栈 S2​ 存放运算符。计算器的最下方有一个等号键,每次按下这个键,计算器就…

RabbitMQ相关问题

文章目录避免重复消费(保证消息幂等性)消息积压上线更多的消费者,进行正常消费惰性队列消息缓存延时队列RabbitMQ如何保证消息的有序性?RabbitMQ消息的可靠性、延时队列如何实现数据库与缓存数据一致?开启消费者多线程消费避免重复消费(保证消…