Thursday, April 14, 2022

SegmentFault 最新的文章

SegmentFault 最新的文章


现代 CSS 解决方案:Modern CSS Reset

Posted: 13 Apr 2022 07:30 PM PDT

在早年间(其实也不是很早),写过几篇关于 CSS Reset 的文章 - reset.css 知多少

详细描述了当时业界比较常用的,两个 CSS reset 方案:reset.css 与 Normalize.css。

以更为推荐的 Normalize.css 为例,它的核心思想是:

  1. 统一了一些元素在所有浏览器下的表现,保护有用的浏览器默认样式而不是完全清零它们,让它们在各个浏览器下表现一致;
  2. 为大部分元素提供一般化的表现;
  3. 修复了一些浏览器的 Bug ,并且让它们在所有浏览器下保持一致性;
  4. 通过一些巧妙的细节提升了 CSS 的可用性;
  5. 提供了详尽的文档让开发者知道,不同元素在不同浏览器下的渲染规则;

如今,Normalize 已经出到了第八版 -- normalize.css V8.0.1,而随之而变的是浏览器市场环境的巨大变化。

IE 已经逐渐退出历史舞台,处理各个浏览器之间巨大差异、不同兼容性问题的日子像是一去不复返了。虽然今天不同厂商在对待标准仍然存在差异,一些细节上仍旧有出入,但是我们已经不需要再像过去般大肆地对浏览器默认样式进行重置。

到今天,我们更多听到现代 CSS 解决方案一词。它除去页面样式最基本的呈现外,同时也关注用户体验可访问性。这也可能是过去,我们在写 CSS 的时候比较容易忽略的环节。

Modern CSS Reset

我最近比较喜欢的一个 CSS Reset 方案,源自于 -- Modern-CSS-Reset

它的核心观点是:

  1. 重置合理的默认值
  2. 关注用户体验
  3. 关注可访问性

整个 Reset 的源码比较简单:

/* Box sizing rules */ *, *::before, *::after {   box-sizing: border-box; }  /* Remove default margin */ body, h1, h2, h3, h4, p, figure, blockquote, dl, dd {   margin: 0; }  /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ ul[role='list'], ol[role='list'] {   list-style: none; }  /* Set core root defaults */ html:focus-within {   scroll-behavior: smooth; }  /* Set core body defaults */ body {   min-height: 100vh;   text-rendering: optimizeSpeed;   line-height: 1.5; }  /* A elements that don't have a class get default styles */ a:not([class]) {   text-decoration-skip-ink: auto; }  /* Make images easier to work with */ img, picture {   max-width: 100%;   display: block; }  /* Inherit fonts for inputs and buttons */ input, button, textarea, select {   font: inherit; }  /* Remove all animations, transitions and smooth scroll for people that prefer not to see them */ @media (prefers-reduced-motion: reduce) {   html:focus-within {    scroll-behavior: auto;   }      *,   *::before,   *::after {     animation-duration: 0.01ms !important;     animation-iteration-count: 1 !important;     transition-duration: 0.01ms !important;     scroll-behavior: auto !important;   } }

其中一些比较有意思的点,单看盒子模型:

*, *::before, *::after {   box-sizing: border-box; }

Normalize.css 是不推荐这么做的,大部分元素的 box-sizing 其实都是 content-box,但是,对于实际开发,全部元素都设置为 border-box 其实是更便于操作的一种方式。

再看看在用户体验可访问性方面的一些做法:

html:focus-within {   scroll-behavior: smooth; }

scroll-behavior: smooth 意为平滑滚动,当然这里是设置给了 html:focus-within 伪类,而不是直接给 html 赋予平滑滚动,这样做的目的是只对使用键盘 tab 键切换焦点页面时,让页面进行平滑滚动切换,带来更好的使用体验。

如果我们设置了如下 CSS:

html {   scroll-behavior: smooth; }

可能会起到一起副作用,譬如,当我们在页面查找元素时候(使用 Ctrl + F、或者 Mac 的 Commond + F),这段 CSS 代码可能会严重延缓我们的查找速度:

再看看这段代码:

@media (prefers-reduced-motion: reduce) {   html:focus-within {    scroll-behavior: auto;   }      *,   *::before,   *::after {     animation-duration: 0.01ms !important;     animation-iteration-count: 1 !important;     transition-duration: 0.01ms !important;     scroll-behavior: auto !important;   } }

我曾经在 使用 CSS prefers-* 规范,提升网站的可访问性与健壮性 介绍过 prefers-reduced-motion

prefers-reduced-motion 规则查询用于减弱动画效果,除了默认规则,只有一种语法取值 prefers-reduced-motion: reduce,开启了该规则后,相当于告诉用户代理,希望他看到的页面,可以删除或替换掉一些会让部分视觉运动障碍者不适的动画类型。

规范原文:Indicates that user has notified the system that they prefer an interface that removes or replaces the types of motion-based animation that trigger discomfort for those with vestibular motion disorders.

vestibular motion disorders 是一种视觉运动障碍患者,翻译出来是前庭运动障碍,是一种会导致眩晕的一类病症,譬如一个动画一秒闪烁多次,就会导致患者的不适。

使用方法,还是上面那段代码:

.ele {     animation: aniName 5s infinite linear; }  @media (prefers-reduced-motion: reduce) {     .ele {         animation: none;     } }

如果我们有一些类似这样的动画:

在用户开启了 prefers-reduced-motion: reduce 时,就应该把这个动画去掉。

而上述 Reset 中的那段代码,正是用于当用户开启对应选项后,减弱页面上的所有动画效果。属于对可访问性的考虑。

结合实际环境

当然,结合实际环境,目前国内整体不太注重可访问性相关的内容。

而且,许多业务根本无法抛弃一些老旧浏览器,仍然需要兼容 IE 系列。

因此,对于现阶段的 Reset 方案,可以灵活搭配:

  1. 如果你的业务场景仍然需要考虑一些老旧浏览器,依旧需要兼容 IE 系列,Normalize.css 的大部分功能都还是非常好的选择
  2. 如果你的业务场景只专注于 Chrome 或者是 Chromium 内核,Normalize.css 内的许多内容其实可能是一些实际中根本不会遇到或者用上的兼容适配,可以进行必要的精简
  3. 如果你的业务是全球化,面向的用户不仅仅在国内,你应该开始考虑更多可访问性相关的内容,上述的 Modern CSS Reset 可以借鉴一下

因此,更应该的情况是,根据实际的业务需要,吸收多个业界比较常见/知名的 Reset 方案形成自己业务适用的。

这里再罗列一些常见及现代 CSS Reset 方案:

Reset 方案简介Github Stars 数
normalize.cssCSS Reset 的现代替代方案47.1K
sanitize.css提供一致的、跨浏览器的 HTML 元素默认样式以及有用的默认样式4.8K
reseter.cssNormalize.css 和 CSS Reset 的未来替代方案981
Modern-CSS-Reset小而美,重置合理的默认值的现代 CSS Reset 方案2.4K

你会看到,其实大家都号称自己是现代 CSS Reset 解决方案,但其实其内部做的 Reset 工作很多是我们根本用不上的。有人喜欢小而美,有人喜欢大而全,实际使用的时候需要具体取舍,魔改合并成适合自己的才是最好的。

最后

好了,本文到此结束,希望对你有帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

轻量迅捷时代,Vite 与Webpack 谁赢谁输

Posted: 12 Apr 2022 11:00 PM PDT

你知道Vite和Webpack吗?也许有不少"程序猿"对它们十分熟悉。

Webpack

Webpack是一个JavaScript应用程序的静态模块打包工具,它会对整个应用程序进行依赖关系图构建。而这也会导致一个不可避免的情况,使用Webpack启动应用程序的服务器,会花费比较长的时间——一些大型应用程序可能需要10分钟以上。

此时你心里可能已经在抓狂了,为什么会这么费时间?那就让我们一起看看基于 Webpack 包的整个工作流。

基于Webpack包的工作流

当我们保存文件时,整个JavaScript包将由Webpack重建,即使启用了HMR,我们进行修改可能也需要10秒钟才能在浏览器呈现。在比较复杂的环境下,Webpack的反馈却如此之慢,会为开发人员带来极大不便。

说完了Webpack我们再为大家介绍一下Vite。

Vite

Vite是新一代JavaScript构建工具,旨在提高开发人员在构建JavaScript应用程序时对Webpack的体验。

Vite根据JavaScript生态系统中最近所做的两项改进——浏览器中ES模块的可用性,以及esbuild等本机捆绑工具的编译功能,为开发者提供更加强大的支持。

Vite的核心理念是非捆绑式开发建设。

浏览器中ES模块的可用性允许您在浏览器上运行JavaScript应用程序,而无需将它们捆绑在一起。

Vite的核心思想很简单:当浏览器请求时,使用ES模块进行转换并提供一段应用程序代码。

开始开发后,Vite将首先将JavaScript模块分为两类:依赖模块和应用程序模块。

依赖模块是从node\_modules文件夹导入的JavaScript模块。这些模块将使用esbuild进行处理和绑定,esbuild是用Go编写的JavaScript绑定器,执行速度比Webpack快10到100倍。

应用程序模块是为应用程序编写的模块,通常涉及特定于库的扩展,如:jsx / vue 或 scss文件。

虽然基于捆绑程序的工作流(如Webpack)必须在单个浏览器请求之前处理整个JavaScript模块,但Vite仅在单个浏览器请求之前处理依赖模块。

在必要时,Vite会对我们的整个应用模块进行转换。

为了方便大家的理解,下面为大家介绍基于Vite的完整工作流程。

基于 Vite的工作流程

这张图可以清晰的让大家理解,为什么Vite能够比Webpack更快地处理我们的开发构建。

如果用一个通俗的说法比较二者,就好像我们去一家餐厅吃饭,Webpack的后厨一口气做完所有的饭,然后一道道为你上菜;而Vite的厨子手脚麻利,很快做完一道菜就上一道菜。

使用Vite,处理开发构建的时间会随着应用程序的增长缓慢增加。

Vite 的捆绑构建

虽然现在所有主流浏览器都支持原生ES模块,但发布一个包含诸如tree-shaking、延迟加载和通用块拆分等性能优化技术的捆绑应用程序,仍然比非捆绑应用程序会为开发者带来更好的使用体验,整体性能更高。

由于这个原因,Vite内置了一个build的配置的命令,该命令使用Rollup捆绑应用程序;我们可以根据自己的具体需求对Rollup进行自由配置。

Vite 入门基础

使用Vite构建一个应用很简单,vite build创建一个预配置的应用程序,主流前端框架React、Vue和Svelte等都可以支持。

创建应用程序所需的命令:

 $ npm init @vitejs/app

或者

$ yarn create @vitejs/app 

终端中的提示将指导您创建正确的应用程序,然后运行 npm install

启动开发服务器

 npm run dev 

除此之外,还可以创建Vite+React的应用程序。React 的起始页与Create React App的默认模板非常相似,只是稍作修改:

此外Vite preview用于在本地对应用进行预览,还支持许多官方模板,包括vanilla JavaScript和TypeScript。

结论

从目前的的使用状况来看,Vite毫无疑问是新一代JavaScript构建工具中最快捷的,但是面对竞争,Webpack也对一些内容进行优化,作为经典老牌工具Webpack用户基数本身就很大, 实力依旧不容小觑,现在依旧是许多流行应用程序(如Next)的默认JavaScript构建工具。

但随着时间不断推移,Vite周边的生态力量逐渐跟上,结合它本身有的快速的优势,后来居上,也是显而易见的事情。

作为开发者,当然也不会独断专言必须使用哪一个,根据不同得场景选不同的工具,期待在未来这些工具可以进一步为开发助力,让开发变得更加方便。

拓展阅读

大家如果对Vue感兴趣,不妨跟着实战教程,亲自搭建一个基于Vue3的表格编辑系统

原文链接:https://blog.bitsrc.io/vite-i...

🔥前端脚手架辣么多,那我也要写一个玩玩

Posted: 12 Apr 2022 03:38 AM PDT

前言

  2022年已经过了四分之一还多了,之前说好的每个月一片文章好像也没有让自己兑现。最近公司在做一些前端工程化相关的东西,虽然准备做组件库的事情被领导给毙了,不过在这之前写了一个脚手架的工具,毕竟现在这个环境下,脚手架工具泛滥,所以当然也要写一写玩玩。

最终效果

cliInit

支持功能

  • 自主选择web端或移动端;
  • 自主选择项目框架react或vue;
  • 自主选择是否设置远程git地址;
  • 支持在项目模板中自定义变量替换;
  • 自主选择是否自动安装依赖,可选择npm、cnpm、yarn;
  • 支持使用update命令在线更新;
  • 自主选择已存在文件目录是否覆盖;

开发

初始化项目

  那么接下来就开始开发了,首先我们来新建一个项目文件夹就叫new-cli吧,在项目文件夹中新建package.json文件,设置常用的字段,设置完成后如下:

{   "name": "new-cli",   "version": "1.0.0",   "description": "a react project cli, help you create a react project quickly",   "bin": {     "new-cli": "bin/www.js"   },   "dependencies": {     "boxen": "^5.1.2",     "chalk": "^4.1.2",     "commander": "^9.1.0",     "consolidate": "^0.16.0",     "cross-spawn": "^7.0.3",     "download-git-repo": "^3.0.2",     "ejs": "^3.1.6",     "fs-extra": "^10.0.1",     "inquirer": "^8.2.1",     "metalsmith": "^2.4.2",     "ora": "^5.4.1",     "figlet": "^1.5.2",     "semver": "^7.3.5",     "shelljs": "^0.8.5"   },   "repository": {     "type": "git",     "url": "https://github.com/BoWang816/new-cli.git"   },   "keywords": [     "cli",     "react"   ],   "author": "恪晨",   "publishConfig": {     "registry": "私有仓库地址"   },   "engines": {     "node":"^12.20.0 || >=14"    } }

  通过以上设置以后,我们的脚手架名字就叫new-cli,也就是说到时候安装的时候就是通过npm install -g new-cli进行安装。bin下面设置的名称就是为了设置脚手架执行的命令,并且是从bin/www.js文件作为了入口文件;dependencies中为我们需要的项目依赖,值得注意的是像boxen、chalk、figlet这一类的依赖包在最新版本中已经不支持requier方式引入了所以这里我们需要安装低版本的包;publishConfig中可以设置到时候需要发布的npm地址,如果你搭建了npm私服则通过设置registry就可以发布到你的私服了。

设置项目入口

  建好package.json以后我们就开始建入口文件,也就是bin下面的www.js,事实上你的入口文件放置在根目录也是可以的,可以根据自己的喜好,当然如果放置在了根目录,则bin下面就要改为new-cli: './www.js'。www.js中主要是引入commander、inquirer等工具包,进行脚手架工具的初始化。因为www.js将要作为一个node脚本来运行,因此需要在最上方声明环境:#! /usr/bin/env node,我写的这个脚手架中涉及到了init、update、help这三个命令,并且help是commander本身就支持的,这里只是做了一点定制化。

  • 初始化init命令、update命令、help命令

    首先需要引入commander,使用它的program,const {program} = require("commander");,脚手架工具的主体就是它了,我们初始化相关的命令:

      #! /usr/bin/env node   // 引入commander   const {program} = require("commander");      // 初始化init命令, project-name就是你的项目名称与项目文件夹名称   program.command("init <project-name>")           // init命令描述          .description("create a new project name is <project-name>")          // init命令参数项,因为后续会设置支持覆盖文件夹,所以这里提供一个-f参数          .option("-f, --force", "overwrite target directory if it exists")          // init命名执行后做的事情          .action(() => {                console.log('doSomething');           });             program.command("update")           .description("update the cli to latest version")           // update命令执行后做的事情,自动检测更新           .action(async () => {               // await checkUpdate();               console.log('update');           });      program.on("--help", () => {       // 监听--help命令,输出一个提示       console.log(figlet.textSync("new-cli", {           font: "Standard",           horizontalLayout: 'full',           verticalLayout: 'fitted',           width: 120,           whitespaceBreak: true       }));   });         // 这个一定不能忘,且必须在最后!!!   program.parse(process.argv);      

  通过设置以上内容,其实我们就可以使用基本的命令了。本地调试的方式有两种,一种是通过npm link命令将我们写的脚手架工具直接链接到本地的全局npm中,一种则是直接通过node bin/www.js直接执行这个js文件,这里我们使用后者就可以了。
initCommand

  • 扩展init命令

      接下来我们就需要扩展init命名,也就是在action做一些事情了。首先,我们提供了-f的参数选项,目的是为了在初始化项目的时候检测到有同名文件夹则进行覆盖,因此在初始化项目的第一步我们就需要检测当前路径下是否存在同名的文件夹,并且在没有设置-f的时候给出提示信息,同时在设置了-f后给出二次提示,同意覆盖则开始初始化项目。因此action函数中将要执行的以下内容,这里我们就需要引入chalk,paht,fs-extray以及后续我们自己写的create。

const chalk = require("chalk"); const path = require("path"); const fs = require('fs-extra'); const figlet = require('figlet'); const create = require('../utils/create');      program     .command("init <project-name>")     .description("create a new project name is <project-name>")     .option("-f, --force", "overwrite target directory if it exists")     .action(async (projectName, options) => {         const cwd = process.cwd();         // 拼接到目标文件夹         const targetDirectory = path.join(cwd, projectName);         // 如果目标文件夹已存在         if (fs.existsSync(targetDirectory)) {             if (!options.force) {             // 如果没有设置-f则提示,并退出                 console.error(chalk.red(`Project already exist! Please change your project name or use ${chalk.greenBright(`new-cli create ${projectName} -f`)} to create`))                 return;             }             // 如果设置了-f则二次询问是否覆盖原文件夹             const {isOverWrite} = await inquirer.prompt([{                 name: "isOverWrite",                 type: "confirm",                 message: "Target directory already exists, Would you like to overwrite it?",                 choices: [                     {name: "Yes", value: true},                     {name: "No", value: false}                 ]             }]);             // 如需覆盖则开始执行删除原文件夹的操作             if (isOverWrite) {                 const spinner = ora(chalk.blackBright('The project is Deleting, wait a moment...'));                 spinner.start();                 await fs.removeSync(targetDirectory);                 spinner.succeed();                 console.info(chalk.green("✨ Deleted Successfully, start init project..."));                 console.log();                 // 删除成功后,开始初始化项目                 // await create(projectName);                 console.log('init project overwrite');                 return;             }             console.error(chalk.green("You cancel to create project"));             return;         }         // 如果当前路径中不存在同名文件夹,则直接初始化项目         // await create(projectName);         console.log('init project');     });

我们再来查看现在的效果:
initProject

  • 创建create方法

      在上一步操作中,我们覆盖同名文件后,使用了await create(projectName)方法开始初始化项目,接下来我们开始开发create方法。在根目录新建一个文件夹叫utils,当然你可以随意叫lib或者✨点赞都行,在utils下面新建一个文件叫create.js,在这个文件中,我们将设置下载初始化项目中一些问题询问的执行。内容主要有以下:

      const inquirer = require("inquirer");   const chalk = require("chalk");   const path = require("path");   const fs = require("fs");   const boxen = require("boxen");   const renderTemplate = require("./renderTemplate");   const downloadTemplate = require('./download');   const install = require('./install');   const setRegistry = require('./setRegistry');   const {baseUrl, promptList} = require('./constants');         const go = (downloadPath, projectRoot) => {       return downloadTemplate(downloadPath, projectRoot).then(target => {       //下载模版           return {               downloadTemp: target           }       })   }   module.exports = async function create(projectName) {       // 校验项目名称合法性,项目名称仅支持字符串、数字,因为后续这个名称会用到项目中的package.json以及其他很多地方,所以不能存在特殊字符       const pattern = /^[a-zA-Z0-9]*$/;       if (!pattern.test(projectName.trim())) {           console.log(`\n${chalk.redBright('You need to provide a projectName, and projectName type must be string or number!\n')}`);           return;       }       // 询问       inquirer.prompt(promptList).then(async answers => {           // 目标文件夹           const destDir = path.join(process.cwd(), projectName);           // 下载地址           const downloadPath = `direct:${baseUrl}/${answers.type}-${answers.frame}-template.git#master`           // 创建文件夹           fs.mkdir(destDir, {recursive: true}, (err) => {               if (err) throw err;           });              console.log(`\nYou select project template url is ${downloadPath} \n`);           // 开始下载           const data = await go(downloadPath, destDir);           // 开始渲染           await renderTemplate(data.downloadTemp, projectName);           // 是否需要自动安装依赖,默认否           const {isInstall, installTool} = await inquirer.prompt([               {                   name: "isInstall",                   type: "confirm",                   default: "No",                   message: "Would you like to help you install dependencies?",                   choices: [                       {name: "Yes", value: true},                       {name: "No", value: false}                   ]               },               // 选择了安装依赖,则使用哪一个包管理工具               {                   name: "installTool",                   type: "list",                   default: "npm",                   message: 'Which package manager you want to use for the project?',                   choices: ["npm", "cnpm", "yarn"],                   when: function (answers) {                       return answers.isInstall;                   }               }           ]);                      // 开始安装依赖           if (isInstall) {               await install({projectName, installTool});           }              // 是否设置了仓库地址           if (answers.setRegistry) {               setRegistry(projectName, answers.gitRemote);           }            // 项目下载成功           downloadSuccessfully(projectName);       });   }
    • create.js文件中,我们首先判断了初始化的项目名称是否包含特殊字符,如果包含则给出错误提示,并终止项目初始化。如果项目名称合法,则开始询问用户需要的项目模板:
      askTpl
        我们将这些询问的list抽离为常量,同时也将模板的地址抽离为常量,因此需要在utils文件夹下建立一个constants.js的文件,里面的内容如下:

      /**  * constants.js  * @author kechen  * @since 2022/3/25  */  const { version } = require('../package.json');  const baseUrl = 'https://github.com/BoWangBlog'; const promptList = [     {         name: 'type',         message: 'Which build tool to use for the project?',         type: 'list',         default: 'webpack',         choices: ['webpack', 'vite'],     },     {         name: 'frame',         message: 'Which framework to use for the project?',         type: 'list',         default: 'react',         choices: ['react', 'vue'],     },     {         name: 'setRegistry',         message: "Would you like to help you set registry remote?",         type: 'confirm',         default: false,         choices: [             {name: "Yes", value: true},             {name: "No", value: false}         ]     },     {         name: 'gitRemote',         message: 'Input git registry for the project: ',         type: 'input',         when: (answers) => {             return answers.setRegistry;         },         validate: function (input) {             const done = this.async();             setTimeout(function () {                 // 校验是否为空,是否是字符串                 if (!input.trim()) {                     done('You should provide a git remote url');                     return;                 }                 const pattern = /^(http(s)?:\/\/([^\/]+?\/){2}|git@[^:]+:[^\/]+?\/).*?.git$/;                 if (!pattern.test(input.trim())) {                     done(                         'The git remote url is validate',                     );                     return;                 }                 done(null, true);             }, 500);         },     } ];  module.exports = {     version,     baseUrl,     promptList }

        其中version为我们的脚手架版本号,baseUrl为项目模板下载的基础地址,promptList为询问用户的问题列表,promptList的具体写法是根据inquirer.prompt()方法来写的,具体的怎么写后面我都会将官方文档地址附上,大家可以自己发挥。

    • 通过inquirer.prompt()获取到用户反馈的结果以后,我们会拿到相关的字段值,然后去拼接出下载的项目模板地址,接下来就是开始下载项目模板了。这里我们写了go函数和renderTemplate俩个函数,一个用于下载项目模板一个用于渲染项目模板(因为涉及到变量的替换)。go函数中其实是使用了从外部引入的downloadTemplate方法,因此我们需要去关注downloadTemplaterenderTemplate方法,也就是接下来要讲的重点了。
  • 创建download方法

    utils文件夹下,新建一个名称为download.js的文件,文件内容如下:

    /**  * 下载  * download.js  * @author kechen  * @since 2022/3/25  */  const download = require('download-git-repo') const path = require("path") const ora = require('ora') const chalk = require("chalk"); const fs = require("fs-extra");  module.exports = function (downloadPath, target) {     target = path.join(target);     return new Promise(function (resolve, reject) {         const spinner = ora(chalk.greenBright('Downloading template, wait a moment...\r\n'));         spinner.start();          download(downloadPath, target, {clone: true}, async function (err) {             if (err) {                 spinner.fail();                 reject(err);                 console.error(chalk.red(`${err}download template failed, please check your network connection and try again`));                 await fs.removeSync(target);                 process.exit(1);             } else {                 spinner.succeed(chalk.greenBright('✨ Download template successfully, start to config it: \n'));                 resolve(target);             }         })     }) }

      该文件中,我们使用了download-git-repo这个第三方的工具库,用于下载项目模板,因为download-git-repo的返回结果是下载成功或者失败,我们在使用异步的方式的时候如果直接使用会存在问题,因此这里封装为promise,当err的时候给用户抛出异常提示,成功则将目标文件夹路径返回用于后续使用。在create.js中我们使用了go函数,在go函数执行成功后会返回一个data,里面拿到了项目要下载到具体的文件夹的路径,其实主要是为了获取在download中的promise的resolve结果,拿到目标文件夹的路径后,其实项目模板已经下载到了该文件夹中,就可以开始renderTemplate了。

  • 创建renderTemplate方法

  在utils文件夹下,新建一个文件叫renderTemplate.js,该函数的主要目的是为了将初始化的项目中设置的变量进行替换,主要使用了metalSmithconsolidate这两个第三方的包,通过遍历初始化项目中的文件,将其转换为ejs模板,并替换相关的变量。这个方法是参考了vww-cli的方式,通过读取项目模板中的ask.ts文件,获取项目模板中自定义的询问列表,然后再进行文件模板引擎渲染替换相关设置好的变量,主要内容如下:

   /**     * 渲染模板     * renderTemplate.js     * @author kechen     * @since 2022/3/24     */    const MetalSmith = require('metalsmith');     const {render} = require('consolidate').ejs;    const {promisify} = require('util');    const path = require("path");    const inquirer = require('inquirer');    const renderPro = promisify(render);    const fs = require('fs-extra');        module.exports = async function renderTemplate(result, projectName) {        if (!result) {            return Promise.reject(new Error(`无效的目录:${result}`))        }            await new Promise((resolve, reject) => {            MetalSmith(__dirname)                .clean(false)                .source(result)                .destination(path.resolve(projectName))                .use(async (files, metal, done) => {                    const a = require(path.join(result, 'ask.ts'));                    // 读取ask.ts文件中设置好的询问列表                    let r = await inquirer.prompt(a);                    Object.keys(r).forEach(key => {                        // 将输入内容前后空格清除,不然安装依赖时package.json读取会报错                        r[key] = r[key]?.trim() || '';                    })                    const m = metal.metadata();                    const tmp = {                        ...r,                        // 将使用到的name全部转换为小写字母                        name: projectName.trim().toLocaleLowerCase()                    }                    Object.assign(m, tmp);                    // 完成后删除模板中的文件                    if (files['ask.ts']) {                        delete files['ask.ts'];                        await fs.removeSync(result);                    }                    done()                })                .use((files, metal, done) => {                    const meta = metal.metadata();                    // 需要替换的文件的后缀名集合                    const fileTypeList = ['.ts', '.json', '.conf', '.xml', 'Dockerfile', '.json'];                    Object.keys(files).forEach(async (file) => {                        let c = files[file].contents.toString();                        // 找到项目模板中设置好的变量进行替换                        for (const type of fileTypeList) {                            if (file.includes(type) && c.includes('<%')) {                                c = await renderPro(c, meta);                                files[file].contents = Buffer.from(c);                            }                        }                    });                    done()                })                .build((err) => {                    err ? reject(err) : resolve({resolve, projectName});                })        });    };    

通过renderTemplate方法,我们基本就完成我们脚手架的主要功能了。我们就可以实现使用init命令创建项目了。这里我遇到一个问题,就是在删除ask.ts文件的时候,如果后面不加await fs.removeSync(result);这个文件就无法删除,但是加上按理说又不合理,具体原因没有找到,有知道的朋友可以留言解释一下,十分感谢。至此,我们初始化项目的功能已经完成,接下来就是一些扩展了。

  • 创建setRegistry方法

      在utils文件夹下,新建一个文件叫setRegistry.js,主要是为了帮助用户初始化项目的git地址,在用户创建是选择是否需要自动设置项目仓库地址,如果设置了项目地址,则这里会自动初始化git,并设置项目地址,具体内容如下:

      /**    * 设置仓库地址    * setRegistry.js    * @author kechen    * @since 2022/3/28    */      const shell = require("shelljs");   const chalk = require("chalk");      module.exports = function setRegistry(projectName, gitRemote) {       shell.cd(projectName);       if (shell.exec('git init').code === 0) {           if (shell.exec(`git remote add origin ${gitRemote}`).code === 0) {               console.log(chalk.green(`✨ \n Set registry Successfully, now your local gitRemote is ${gitRemote} \n`));               return;           }           console.log(chalk.red('Failed to set.'));           shell.exit(1);       }   };  
  • 创建install方法

      在utils文件夹下,新建一个文件叫install.js,主要是为了帮助用户自动安装依赖,主要内容如下:

     /**    * 安装依赖    * install.js    * @author kechen    * @since 2022/3/22    */   const spawn = require("cross-spawn");      module.exports = function install(options) {       const cwd = options.projectName || process.cwd();       return new Promise((resolve, reject) => {           const command = options.installTool;           const args = ["install", "--save", "--save-exact", "--loglevel", "error"];           const child = spawn(command, args, {cwd, stdio: ["pipe", process.stdout, process.stderr]});              child.once("close", code => {               if (code !== 0) {                   reject({                       command: `${command} ${args.join(" ")}`                   });                   return;               }               resolve();           });           child.once("error", reject);       });   };
  • 创建checkUpdate方法

      在utils文件夹下,新建一个文件叫checkUpdate.js,主要是为了帮助用户自动检测并进行脚手架更新,主要内容如下:

      /**    * 检查更新    * checkUpdate.js    * @author kechen    * @since 2022/3/23    */   const pkg = require('../package.json');   const shell = require('shelljs');   const semver = require('semver');   const chalk = require('chalk');   const inquirer = require("inquirer");   const ora = require("ora");      const updateNewVersion = (remoteVersionStr) => {       const spinner = ora(chalk.blackBright('The cli is updating, wait a moment...'));       spinner.start();       const shellScript = shell.exec("npm -g install new-cli");       if (!shellScript.code) {           spinner.succeed(chalk.green(`Update Successfully, now your local version is latestVersion: ${remoteVersionStr}`));           return;       }       spinner.stop();       console.log(chalk.red('\n\r Failed to install the cli latest version, Please check your network or vpn'));   };      module.exports = async function checkUpdate() {       const localVersion = pkg.version;       const pkgName = pkg.name;       const remoteVersionStr = shell.exec(           `npm info ${pkgName}@latest version`,           {               silent: true,           }       ).stdout;          if (!remoteVersionStr) {           console.log(chalk.red('Failed to get the cli version, Please check your network'));           process.exit(1);       }       const remoteVersion = semver.clean(remoteVersionStr, null);          if (remoteVersion !== localVersion) {           // 检测本地安装版本是否是最新版本,如果不是则询问是否自动更新           console.log(`Latest version is ${chalk.greenBright(remoteVersion)}, Local version is ${chalk.blackBright(localVersion)} \n\r`)              const {isUpdate} = await inquirer.prompt([               {                   name: "isUpdate",                   type: "confirm",                   message: "Would you like to update it?",                   choices: [                       {name: "Yes", value: true},                       {name: "No", value: false}                   ]               }           ]);           if (isUpdate) {               updateNewVersion(remoteVersionStr);           } else {               console.log();               console.log(`Ok, you can run ${chalk.greenBright('wb-cli update')} command to update latest version in the feature`);           }           return;       }       console.info(chalk.green("Great! Your local version is latest!"));   };

    这里需要注意的是,因为脚手架是全局安装的,涉及到权限的问题,因此在mac下需要使用sudo new-cli update进行更新,而在windows中需要以管理员身份打开命令行工具执行new-cli update进行更新。到这里,我们的脚手架基本就完成啦。

其他花里胡哨的东东

  主要功能基本就是上面这些啦,另外我们需要加一个项目创建成功之后的提示,在上文的create.js中最后面有一个downloadSuccessfully的方法,其实就是创建成功后的提示,主要内容如下:

const downloadSuccessfully = (projectName) => {     const END_MSG = `${chalk.blue("🎉 created project " + chalk.greenBright(projectName) + " Successfully")}\n\n 🙏 Thanks for using wb-cli !`;     const BOXEN_CONFIG = {         padding: 1,         margin: {top: 1, bottom: 1},         borderColor: 'cyan',         align: 'center',         borderStyle: 'double',         title: '🚀 Congratulations',         titleAlignment: 'center'     }      const showEndMessage = () => process.stdout.write(boxen(END_MSG, BOXEN_CONFIG))     showEndMessage();      console.log('👉 Get started with the following commands:');     console.log(`\n\r\r cd ${chalk.cyan(projectName)}`);     console.log("\r\r npm install");     console.log("\r\r npm run start \r\n"); }

具体的实现效果就是这样的,这里我是截了之前做好的图。
createSuccess

项目模板

  我们需要创建一个项目模板,里面需要在根目录下包含一个ask.ts文件,其他的就和正常项目一样就好了,aks.ts的文件内容示例如下,

/**  * demo  * aks.ts  * @author kechen  * @since 2022/3/24  */  module.exports = [   {     name: 'description',     message: 'Please enter project description:',   },   {     name: 'author',     message: 'Please enter project author:',   },   {     name: 'apiPrefix',     message: 'Please enter project apiPrefix:',     default: 'api/1.0',     // @ts-ignore     validate: function (input) {       const done = this.async();       setTimeout(function () {         // 校验是否为空,是否是字符串         if (!input.trim()) {           done(             'You can provide a apiPrefix, or not it will be default【api/1.0】',           );           return;         }         const pattern = /[a-zA-Z0-9]$/;         if (!pattern.test(input.trim())) {           done(             'The apiPrefix is must end with letter or number, like default 【api/1.0】',           );           return;         }         done(null, true);       }, 300);     },   },   {     name: 'proxy',     message: 'Please enter project proxy:',     default: 'https://www.test.com',     // @ts-ignore     validate: function (input) {       const done = this.async();       setTimeout(function () {         // 校验是否为空,是否是字符串         if (!input.trim()) {           done(             'You can provide a proxy, or not it will be default【https://www.test.com】',           );           return;         }         const pattern =           /(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?/;         if (!pattern.test(input.trim())) {           done(             'The proxy is must end with letter or number, like default 【https://www.test.com】',           );           return;         }         done(null, true);       }, 300);     },   }, ];

  这里我设置了四个变量分别是description、author、apiPrefix、proxy,在使用时只需要通过<%= var %>这种方式就可以了,var可以是你在ask.ts中设置的任何变量,具体使用demo如下,当然要替换的文件类型必须是在上面我们提到的renderTemplate函数中设置了后缀名的文件才可以。使用这种方式,你就可以在项目模板中自由添加变量,且不需要更新脚手架工具。

{   "name": "xasrd-fe-mobile",   "description": "<%= description %>",   "private": true,   "author": "<%= author %>" } 

  至此,我们的脚手架就全部开发完成啦,接下来就是怎么发布到npm或者npm私服了。

发布

  在上面我们讲过,如果需要发布的npm私服,则需要在package.json中配置publishConfig并指向npm私服的地址,发布的时候则需要通过以下命令进行发布:

当然需要注意的是,发布的时候,package.json中的version版本号不能重复哈!!!

总结

  到这里,我们就完整的开发了一个比较简单前端脚手架工具,并可以发布使用了。其实具体的做法并不是很难,有很多第三方的工具包可以用,当然因为这个工具的交互相对来说比较简单,各位也可以自己奇思妙想,做一些更加花里胡哨的功能进行扩展。示例的demo就不放啦,基本所有的内容都在上面提到了,大家可以自由发挥。当然基于这套我自己也写了一个地址是https://www.npmjs.com/package/wb-fe-cli,不过因为最近实在没时间,所以项目模板还没有,暂时还不能完整的跑起来,后续会慢慢更新的。

参考

结语

  最后希望看完本篇文章后,对你有所帮助,勤快的话可以自己动手手写一写啦。另外希望大家能够关注一下我的Github,哈哈哈哈,带你们看贪吃蛇!

  也可以关注一下GridManager这个好用的表格插件,支持React、Vue,非常好用哦!

  下期,我将会给大家带来一些我常用的Mac软件的介绍,能够帮助你在日常开发与工作中大大提升工作效率!!!可以先预览一下 恪晨的Mac软件推荐

感谢大家的阅读!!

Redis HyperLogLog 是什么?这些场景使用它,让我枪出如龙,一笑破苍穹

Posted: 12 Apr 2022 08:38 PM PDT

在移动互联网的业务场景中,数据量很大,我们需要保存这样的信息:一个 key 关联了一个数据集合,同时对这个数据集合做统计。

比如:

  • 统计一个 APP 的日活、月活数;
  • 统计一个页面的每天被多少个不同账户访问量(Unique Visitor,UV));
  • 统计用户每天搜索不同词条的个数;
  • 统计注册 IP 数。

通常情况下,我们面临的用户数量以及访问量都是巨大的,比如百万、千万级别的用户数量,或者千万级别、甚至亿级别的访问信息。

今天「码哥」分别使用不同的数据类型来实现:统计一个页面的每天被多少个不同账户访问量这个功能,循序渐进的引出 HyperLogLog的原理与 Java 中整合 Redission 实战。

告诉大家一个技巧,Redis 官方网站现在能在线运行 Redis 指令了:https://redis.io/。如图:

Redis 在线运行

使用 Set 实现

一个用户一天内多次访问一个网站只能算作一次,所以很容易就想到通过 Redis 的 Set 集合来实现。

比如微信 ID为「肖菜鸡」访问 「Redis为什么这么快」这篇文章时,我们把这个信息存到 Set 中。

SADD Redis为什么这么快:uv 肖菜鸡 谢霸哥 肖菜鸡 (integer) 1

「肖菜鸡」多次访问「Redis为什么这么快」页面,Set 的去重功能保证不会重复记录同一个「微信 ID」。

通过 SCARD 命令,统计「Redis 为什么这么快」页面 UV。指令返回一个集合的元素个数(也就是用户 ID)。

SCARD Redis为什么这么快:uv (integer) 2

使用 Hash 实现

码老湿,还可以利用 Hash 类型实现,将用户 ID 作为 Hash 集合的 key,访问页面则执行 HSET 命令将 value 设置成 1。

即使「肖菜鸡」重复访问页面,重复执行命令,也只会把 key 等于「肖菜鸡」的 value 设置成 1。

最后,利用 HLEN 命令统计 Hash 集合中的元素个数就是 UV。

如下:

HSET Redis为什么这么快 肖菜鸡 1 // 统计 UV HLEN Redis为什么这么快

使用 Bitmap 实现

Bitmap 的底层数据结构用的是 String 类型的 SDS 数据结构来保存位数组,Redis 把每个字节数组的 8 个 bit 位利用起来,每个 bit 位 表示一个元素的二值状态(不是 0 就是 1)。

Bitmap 提供了 GETBIT、SETBIT 操作,通过一个偏移值 offset 对 bit 数组的 offset 位置的 bit 位进行读写操作,需要注意的是 offset 从 0 开始。

可以将 Bitmap 看成是一个 bit 为单位的数组,数组的每个单元只能存储 0 或者 1,数组的下标在 Bitmap 中叫做 offset 偏移量。

为了直观展示,我们可以理解成 buf 数组的每个字节用一行表示,每一行有 8 个 bit 位,8 个格子分别表示这个字节中的 8 个 bit 位,如下图所示:

Bitmap

8 个 bit 组成一个 Byte,所以 Bitmap 会极大地节省存储空间。 这就是 Bitmap 的优势。

如何使用 Bitmap 来统计页面的独立用户访问量呢?

Bitmap 提供了 SETBIT 和 BITCOUNT 操作,前者通过一个偏移值 offset 对 bit 数组的 offset 位置的 bit 位进行写操作,需要注意的是 offset 从 0 开始。

后者统计给定指定的 bit 数组中,值 = 1 的 bit 位的数量。

需要注意的事,我们需要把「微信 ID」转换成数字,因为offset 是下标。

假设我们将「肖菜鸡」转换成编码6

第一步,执行下面指令表示「肖菜鸡」的编码为 6 并 访问「巧用Redis 数据类型实现亿级数据统计」这篇文章。

SETBIT 巧用Redis数据类型实现亿级数据统计 6 1

第二步,统计页面访问次数,使用 BITCOUNT 指令。该指令用于统计给定的 bit 数组中,值 = 1 的 bit 位的数量。

BITCOUNT 巧用Redis数据类型实现亿级数据统计

HyperLogLog 王者方案

Set 虽好,如果文章非常火爆达到千万级别,一个 Set 就保存了千万个用户的 ID,页面多了消耗的内存也太大了。

同理,Hash数据类型也是如此。

至于 Bitmap,它更适合于「二值状态统计」的使用场景,统计精度高,虽然内存占用要比HashMap少,但是对于大量数据还是会占用较大内存。

咋办呢?

这些就是典型的「基数统计」应用场景,基数统计:统计一个集合中不重复元素的个数。

HyperLogLog 的优点在于它所需的内存并不会因为集合的大小而改变,无论集合包含的元素有多少个,HyperLogLog进行计算所需的内存总是固定的,并且是非常少的

每个 HyperLogLog 最多只需要花费 12KB 内存,在标准误差 0.81%的前提下,就可以计算 2 的 64 次方个元素的基数。

Redis 实战

HyperLogLog 使用太简单了。PFADD、PFCOUNT、PFMERGE三个指令打天下。

PFADD

将访问页面的每个用户 ID 添加到 HyperLogLog 中。

PFADD Redis主从同步原理:uv userID1 userID 2 useID3

PFCOUNT

利用 PFCOUNT 获取 「Redis主从同步原理」文章的 UV值。

PFCOUNT Redis主从同步原理:uv

PFMERGE 使用场景

HyperLogLog` 除了上面的 `PFADD` 和 `PFCOIUNT` 外,还提供了 `PFMERGE

语法

PFMERGE destkey sourcekey [sourcekey ...]

比如在网站中我们有两个内容差不多的页面,运营说需要这两个页面的数据进行合并。

其中页面的 UV 访问量也需要合并,那这个时候 PFMERGE 就可以派上用场了,也就是同样的用户访问这两个页面则只算做一次

如下所示:Redis、MySQL 两个 HyperLogLog 集合分别保存了两个页面用户访问数据。

PFADD Redis数据 user1 user2 user3 PFADD MySQL数据 user1 user2 user4 PFMERGE 数据库 Redis数据 MySQL数据 PFCOUNT 数据库 // 返回值 = 4

将多个 HyperLogLog 合并(merge)为一个 HyperLogLog , 合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的可见集合(observed set)的并集。

user1、user2 都访问了 Redis 和 MySQL,只算访问了一次。

Redission 实战

详细源码「码哥」上传到 GitHub 了:https://github.com/MageByte-Z...

pom 依赖

<dependency>   <groupId>org.redisson</groupId>   <artifactId>redisson-spring-boot-starter</artifactId>   <version>3.16.7</version> </dependency>

添加数据到 Log

// 添加单个元素 public <T> void add(String logName, T item) {   RHyperLogLog<T> hyperLogLog = redissonClient.getHyperLogLog(logName);   hyperLogLog.add(item); }  // 将集合数据添加到 HyperLogLog public <T> void addAll(String logName, List<T> items) {   RHyperLogLog<T> hyperLogLog = redissonClient.getHyperLogLog(logName);   hyperLogLog.addAll(items); }

合并

/**  * 将 otherLogNames 的 log 合并到 logName  *  * @param logName       当前 log  * @param otherLogNames 需要合并到当前 log 的其他 logs  * @param <T>  */ public <T> void merge(String logName, String... otherLogNames) {   RHyperLogLog<T> hyperLogLog = redissonClient.getHyperLogLog(logName);   hyperLogLog.mergeWith(otherLogNames); }

统计基数

public <T> long count(String logName) {   RHyperLogLog<T> hyperLogLog = redissonClient.getHyperLogLog(logName);   return hyperLogLog.count(); }

单元测试

@Slf4j @RunWith(SpringRunner.class) @SpringBootTest(classes = RedissionApplication.class) public class HyperLogLogTest {      @Autowired     private HyperLogLogService hyperLogLogService;      @Test     public void testAdd() {         String logName = "码哥字节:Redis为什么这么快:uv";         String item = "肖菜鸡";         hyperLogLogService.add(logName, item);         log.info("添加元素My7OulCbyNEuDXbm6UMSjDVbRak到 log My7OulCbyNEuDXbm6UMSjDVbRak 中。", item, logName);     }      @Test     public void testCount() {         String logName = "码哥字节:Redis为什么这么快:uv";         long count = hyperLogLogService.count(logName);         log.info("logName = {} count = {}.", logName, count);     }      @Test     public void testMerge() {         ArrayList<String> items = new ArrayList<>();         items.add("肖菜鸡");         items.add("谢霸哥");         items.add("陈小白");          String otherLogName = "码哥字节:Redis多线程模型原理与实战:uv";         hyperLogLogService.addAll(otherLogName, items);         log.info("添加 {} 个元素到 log My7OulCbyNEuDXbm6UMSjDVbRak 中。", items.size(), otherLogName);          String logName = "码哥字节:Redis为什么这么快:uv";         hyperLogLogService.merge(logName, otherLogName);         log.info("将 {} 合并到 {}.", otherLogName, logName);          long count = hyperLogLogService.count(logName);         log.info("合并后的 count = {}.", count);     } } 

基本原理

HyperLogLog 是一种概率数据结构,它使用概率算法来统计集合的近似基数。而它算法的最本源则是伯努利过程。

伯努利过程就是一个抛硬币实验的过程。抛一枚正常硬币,落地可能是正面,也可能是反面,二者的概率都是 1/2

伯努利过程就是一直抛硬币,直到落地时出现正面位置,并记录下抛掷次数k

比如说,抛一次硬币就出现正面了,此时 k1; 第一次抛硬币是反面,则继续抛,直到第三次才出现正面,此时 k 为 3。

对于 n 次伯努利过程,我们会得到 n 个出现正面的投掷次数值 k1, k2 ... kn , 其中这里的最大值是 k_max

根据一顿数学推导,我们可以得出一个结论: 2^{k_ max} 来作为n的估计值。

也就是说你可以根据最大投掷次数近似的推算出进行了几次伯努利过程。

所以 HyperLogLog 的基本思想是利用集合中数字的比特串第一个 1 出现位置的最大值来预估整体基数,但是这种预估方法存在较大误差,为了改善误差情况,HyperLogLog中引入分桶平均的概念,计算 m 个桶的调和平均值。

Redis 中 HyperLogLog 一共分了 2^14 个桶,也就是 16384 个桶。每个桶中是一个 6 bit 的数组,如下图所示。

图片来源:程序员历小冰

关于 HyperLogLog 的原理过于复杂,如果想要了解的请移步:

Redis 对 HyperLogLog 的存储进行了优化,在计数比较小的时候,存储空间采用系数矩阵,占用空间很小。

只有在计数很大,稀疏矩阵占用的空间超过了阈值才会转变成稠密矩阵,占用 12KB 空间。

为何只需要 12 KB 呀?

HyperLogLog 实现中用到的是 16384 个桶,也就是 2^14,每个桶的 maxbits 需要 6 个 bits 来存储,最大可以表示 maxbits=63,于是总共占用内存就是2^14 * 6 / 8 = 12k字节。

总结

分别使用了 HashBitmapHyperLogLog 来实现:

  • Hash:算法简单,统计精度高,少量数据下使用,对于海量数据会占据大量内存;
  • Bitmap:位图算法,适合用于「二值统计场景」,具体可参考我这篇文章,对于大量不同页面数据统计还是会占用较大内存。
  • Set:利用去重特性实现,一个 Set 就保存了千万个用户的 ID,页面多了消耗的内存也太大了。在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
  • HyperLogLog是一种算法,并非 Redis 独有
  • 目的是做基数统计,故不是集合,不会保存元数据,只记录数量而不是数值
  • 耗空间极小,支持输入非常体积的数据量
  • 核心是基数估算算法,主要表现为计算时内存的使用和数据合并的处理。最终数值存在一定误差
  • Redis中每个Hyperloglog key占用了12K的内存用于标记基数(官方文档)
  • pfadd 命令并不会一次性分配12k内存,而是随着基数的增加而逐渐增加内存分配;而pfmerge操作则会将sourcekey合并后存储在12k大小的key中,由hyperloglog合并操作的原理(两个Hyperloglog合并时需要单独比较每个桶的值)可以很容易理解。
  • 误差说明:基数估计的结果是一个带有 0.81% 标准错误(standard error)的近似值。是可接受的范围
  • RedisHyperLogLog 的存储进行优化,在计数比较小时,存储空间采用稀疏矩阵存储,空间占用很小,仅仅在计数慢慢变大,稀疏矩阵占用空间渐渐超过了阈值时才会一次性转变成稠密矩阵,才会占用 12k 的空间

好文推荐

参考资料

No comments:

Post a Comment