Thursday, June 9, 2022

SegmentFault 最新的文章

SegmentFault 最新的文章


十年老友记 | @杨成功:没有什么能比写代码更让我快乐

Posted: 31 May 2022 07:56 PM PDT

不要浅尝辄止,做精一件事,总会有回报的。——杨成功

十年前的今天,SegmentFault 思否正式创立,如一颗嫩绿的幼芽开始成长,期间承载过和煦的日光、沐浴过柔和的春风,也挑战过滂沱的暴雨、体会过凛冽的冬雪。所幸,今日呈现在我们面前的 SegmentFault 思否,已经长成了一棵足以抵御一些风雨的大树,这样的成长离不开各位管理员的修剪,更离不开社区每一位用户的栽种。

正如 SegmentFault 思否创始人之一祁宁所言:

"SegmentFault 思否是一个属于大家的社区,因此,在这个特殊的时刻,我们想跟社区的成员一起为它喝彩。"

这十年中,有成千上万的开发者加入了 SegmentFault 社区,我们雀跃于看到每张不同的新面孔,也感动于社区里的那些老朋友们多年如一日的陪伴,见到他们就像见到一位相识多年的老友。或许老友们会在某一段时间里突然消失,但消失并不代表着再也不见,而是重逢后的那句:好久不见。


今天,我们有幸邀请到老朋友 @杨成功 参与我们的十年老友记系列访谈。

杨成功成为程序员的路途比较坎坷,在访谈中他表示自己是在大学时期,感觉对所学的专业并不感兴趣,于是退学后学了编程,为了当一名合格的程序员,他放弃了许多。

我本想问他会不会后悔当年退学的决定,但他却说没有什么能比写代码更让他快乐,听他说完这句话后,我意识到我的想法是多余的,就算他在通往编程的路上遇到更多困难,我相信他的心里也一定觉得所有这些为了接近编程而遭受的磨难都是值得的。

以下为 SegmentFault 思否与杨成功的访谈内容:

1、还记得和 SegmentFault 思否的初识吗?是在什么样的机缘巧合下踏入这个社区的?

我和思否相识很早,大概在我做程序的第二年吧,经常能从 SegmentFault 中搜到有用的信息。有一次我遇到了个难题,看到 SegmentFault 上有提问的板块,我就注册了账号提了问题,结果一个小时就收到了答复,帮我解决了问题。当时我就点头说,嗯这个网站比较靠谱。
 
2、这些年有没有见证思否的改变?其中对你而言触动最大的是哪一部分?与思否一起成长的路上,你觉得自己改变了什么?

其实我对思否最大的感悟就是这么多年一直保持纯粹,问答和专栏依然是最纯粹的技术贴,几乎没有见过水军,我觉得这个是相比其他社区最难得的。我在思否的这几年,从一个提问者和查阅者,变成了一个回答者和输出者,或许也或多或少的帮助了别人,这是我最大的成长。
 
3、为什么会选择做一名程序员?因为梦想和因为现实这两者的占比哪个更重?

对我来说是梦想吧,因为我是大学退学学的技术,成为一名程序员放弃了很多。但在这个过程中让我更加坚定了没什么比写代码更让我快乐的事情,我可以 coding 到 60 岁哈哈。
 
4、如果有一天因为种种因素你决定放弃编程,你想去做什么?

也许会做一名老师吧,继续教小孩写代码,或者成为一名技术博主
 
5、程序员的形象在很多人的心目中离不开格子衬衫、黑框眼镜、双肩背包等物品,你想对这种刻板印象说什么?

多看看 95 后,他们可能比你想像的更时尚。
 
6、编程对你而言只是工作任务吗?如果不是的话,它对你有何种特殊的意义呢?

工作任务是基本,对我而言更多是一项事业。可能多数程序员并不是单纯的喜欢写代码,更重要的是攻克了一个难题所带来的成就感,以及用编写的代码来解决实际的问题,并且带来商业价值,这是非常酷的一件事。
 
7、工作中有没有让你焦虑的事?这种焦虑源于何处?面对焦虑你一般会怎么做?

有啊,焦虑就是技术更迭太快啦,环境越来越卷了,很担心掉队。还有圈内会有各种比较,让你觉得自己不够优秀。我认为有效的方法就是选择一个方向持续深耕下去,保持专注,成为专家。不要浅尝辄止,做精一件事,总会有回报的。
 
8、年龄对程序员这个职业有一定的影响,你认同这个观点吗?有没有想过自己未来的职业规划?

肯定是有的,35 岁危机所有人都有嘛,只不过对程序员来说更突出一些。我觉的抵御危机的方法就是尽早的建设自己的不可替代性,尤其是需要时间积累才能出成果的东西,这会是你手里代表价值的一张王牌。
 
9、请留下你对 SegmentFault 思否社区十岁生日的祝福

首先感谢 SegmentFault 思否十年来坚守初心,深耕中文技术社区,为开发者提供了一片分享交流的乐土。祝 SegmentFault 思否十周岁生日快乐!希望下一个十年思否依然乘风破浪,扬帆远航!


为了梦想努力前进的人,都不会被辜负。

希望杨成功会在下个十年依然如此热爱编程,希望下个十年,我们都能在历尽千帆后仍觉得一路坎坷值得。

Lerna --多包存储管理工具

Posted: 08 Jun 2022 08:25 PM PDT

lerna

最近在看vue-cli的源码部分,注意到这一个仓库下维护了多个package,很好奇他是如何在一个repo中管理这些package的。

我们组现在也在使用组件库的方式维护项目间共用的业务代码。有两个组件库,存在依赖的关系,目前联调是通过npm link的方式,性能并不好,时常出现卡顿的问题。加上前一段时间组内分享vue3也提到了lerna,于是便决定仔细的调研一下这个工具,为接下里的组件库优化助力。

lerna的文档还是很详细的,因为全是英文的,考虑到阅读问题,这里我先是自己跑了几个demo,然后做了中文翻译。后续我会出一篇专门的lerna实战篇

demo

lerna 是干什么的?

Lerna 是一个工具,它优化了使用 git 和 npm 管理多包存储库的工作流。

背景

1.将一个大的 package 分割成一些小的 packcage 便于分享,调试

2.在多个 git 仓库中更改容易变得混乱且难以跟踪

3.在多个 git 仓库中维护测试繁琐

两种工作模式

Fixed/Locked mode (default)

vue,babel 都是用这种,在 publish 的时候,所有的包版本都会更新,并且包的版本都是一致的,版本号维护在 lerna.jon 的 version 中

Independent mode

lerna init --independent

独立模式,每个 package 都可以有自己的版本号。版本号维护在各自 package.json 的 version 中。每次发布前都会提示已经更改的包,以及建议的版本号或者自定义版本号。这种方式相对第一种来说,更灵活

初始化项目

npm install -g lerna // 这里是全局安装,也可以安装为项目开发依赖,使用全局方便后期使用命令行  mkdir lerna-repo  cd lerna-repo  lerna init // 初始化一个lerna项目结构,如果希望各个包使用单独版本号可以加 -i | --independent

lerna init

标准的 lerna 目录结构

  • 每个单独的包下都有一个 package.json 文件
  • 如果包名是带 scope 的,例如@test/lerna,package.json 中,必须配置"publishConfig": {"access": "public"}
my-lerna-repo/      package.json      lerna.json      LICENSE      packages/          package-1/              package.json          package-2/              package.json

启用 yarn Workspaces (强烈建议)

Workspaces can only be enabled in private projects.

默认是 npm, 每个子 package 下都有自己的 node_modules,通过这样设置后,会把所有的依赖提升到顶层的 node_modules 中,并且在 node_modules 中链接本地的 package,便于调试

注意:必须是 private 项目才可以开启 workspaces

// package.json  "private": true,  "workspaces": [      "packages/*"  ],    // lerna.json    "useWorkspaces": true,  "npmClient": "yarn",

hoist: 提取公共的依赖到根目录的node_moduels,可以自定义指定。其余依赖安装的package/node_modeles中,可执行文件必须安装在package/node_modeles

workspaces: 所有依赖全部在跟目录的node_moduels,除了可执行文件

hoist vs workspaces

常用命令

lerna init

初始化 lerna 项目

  • -i, --independent 独立版本模式

[lerna create <name> [loc]](https://github.com/lerna/lern...

创建一个 packcage

  • --access 当使用scope package时(@qinzhiwei/lerna),需要设置此选项 可选值: "public", "restricted"
  • --bin 创建可执行文件 --bin
  • --description 描述 [字符串]
  • --dependencies 依赖,用逗号分隔 [数组]
  • --es-module 初始化一个转化的Es Module [布尔]
  • --homepage 源码地址 [字符串]
  • --keywords 关键字数 [数组]
  • --license 协议 字符串
  • --private 是否私有仓库 [布尔]
  • --registry 源 [字符串]
  • --tag 发布的标签 [字符串]
  • -y, --yes 跳过所有的提示,使用默认配置 [布尔]

lerna add

为匹配的 package 添加本地或者远程依赖,一次只能添加一个依赖

$ lerna add <package>[@version] [--dev] [--exact] [--peer]

运行该命令时做的事情:

  1. 为匹配到的 package 添加依赖
  2. 更改每个 package 下的 package.json 中的依赖项属性
Command Options

以下几个选项的含义和npm install时一致

  • --dev
  • --exact
  • --peer 同级依赖,使用该package需要在项目中同时安装的依赖
  • --registry
  • --no-bootstrap 跳过 lerna bootstrap,只在更改对应的 package 的 package.json 中的属性

所有的过滤选项都支持

Examples

# Adds the module-1 package to the packages in the 'prefix-' prefixed folders  lerna add module-1 packages/prefix-*      # Install module-1 to module-2  lerna add module-1 --scope=module-2      # Install module-1 to module-2 in devDependencies  lerna add module-1 --scope=module-2 --dev      # Install module-1 to module-2 in peerDependencies  lerna add module-1 --scope=module-2 --peer      # Install module-1 in all modules except module-1  lerna add module-1      # Install babel-core in all modules  lerna add babel-core

lerna bootstrap

将本地 package 链接在一起并安装依赖

执行该命令式做了一下四件事:

1.为每个 package 安装依赖

2.链接相互依赖的库到具体的目录,例如:如果 lerna1 依赖 lerna2,且版本刚好为本地版本,那么会在 node_modules 中链接本地项目,如果版本不满足,需按正常依赖安装

3.在 bootstraped packages 中 执行 npm run prepublish

4.在 bootstraped packages 中 执行 npm run prepare

lerna bootstrap

lerna bootstrap

Command Options
  • --hoist 匹配 [glob] 依赖 提升到根目录 [默认值: '**'], 包含可执行二进制文件的依赖项还是必须安装在当前 package 的 node_modules 下,以确保 npm 脚本的运行
  • --nohoist 和上面刚好相反 [字符串]
  • --ignore-prepublish 在 bootstraped packages 中不再运行 prepublish 生命周期中的脚本 [布尔]
  • --ignore-scripts 在 bootstraped packages 中不再运行任何生命周期中的脚本 [布尔]
  • --npm-client 使用的 npm 客户端(npm, yarn, pnpm, ...) [字符串]
  • --registry 源 [字符串]
  • --strict 在 bootstrap 的过程中不允许发出警告,避免花销更长的时间或者导致其他问题 [布尔]
  • --use-workspaces 启用 yarn 的 workspaces 模式 [布尔]
  • --force-local 无论版本范围是否匹配,强制本地同级链接 [布尔]
  • --contents 子目录用作任何链接的源。必须适用于所有包 字符串

lerna link

将本地相互依赖的 package 相互连接。例如 lerna1 依赖 lerna2,且版本号刚好为本地的 lerna2,那么会在 lerna1 下 node_modules 中建立软连指向 lerna2

Command Options
  • --force-local 无论本地 package 是否满足版本需求,都链接本地的
// 指定软链到package的特定目录  "publishConfig": {      "directory": "dist" // bootstrap的时候软链package下的dist目录 package-1/dist => node_modules/package-1  }

lerna list

list 子命令
  • lerna ls: 等同于 lerna list本身,输出项目下所有的 package
  • lerna ll: 输出项目下所有 package 名称、当前版本、所在位置
  • lerna la: 输出项目下所有 package 名称、当前版本、所在位置,包括 private package
Command Options

所有的过滤选项都支持

--json

以 json 形式展示

$ lerna ls --json  [      {          "name": "package-1",          "version": "1.0.0",          "private": false,          "location": "/path/to/packages/pkg-1"      },      {          "name": "package-2",          "version": "1.0.0",          "private": false,          "location": "/path/to/packages/pkg-2"      }  ]
--ndjson

newline-delimited JSON展示信息

$ lerna ls --ndjson  {"name":"package-1","version":"1.0.0","private":false,"location":"/path/to/packages/pkg-1"}  {"name":"package-2","version":"1.0.0","private":false,"location":"/path/to/packages/pkg-2"}
--all

Alias: -a

显示默认隐藏的 private package

$ lerna ls --all  package-1  package-2  package-3 (private)
--long

Alias: -l

显示包的版本、位置、名称

$ lerna ls --long  package-1 v1.0.1 packages/pkg-1  package-2 v1.0.2 packages/pkg-2      $ lerna ls -la  package-1 v1.0.1 packages/pkg-1  package-2 v1.0.2 packages/pkg-2  package-3 v1.0.3 packages/pkg-3 (private)
--parseable

Alias: -p

显示包的绝对路径

In --long output, each line is a :-separated list: ::[:flags..]

$ lerna ls --parseable  /path/to/packages/pkg-1  /path/to/packages/pkg-2      $ lerna ls -pl  /path/to/packages/pkg-1:package-1:1.0.1  /path/to/packages/pkg-2:package-2:1.0.2      $ lerna ls -pla  /path/to/packages/pkg-1:package-1:1.0.1  /path/to/packages/pkg-2:package-2:1.0.2  /path/to/packages/pkg-3:package-3:1.0.3:PRIVATE
--toposort

按照拓扑顺序(dependencies before dependents)对包进行排序,而不是按目录对包进行词法排序。

$ json dependencies <packages/pkg-1/package.json  {      "pkg-2": "file:../pkg-2"  }      $ lerna ls --toposort  package-2  package-1
--graph

将依赖关系图显示为 JSON 格式的邻接表 adjacency list.

$ lerna ls --graph  {      "pkg-1": [          "pkg-2"      ],      "pkg-2": []  }      $ lerna ls --graph --all  {      "pkg-1": [         "pkg-2"      ],      "pkg-2": [          "pkg-3"      ],      "pkg-3": [          "pkg-2"      ]  }

lerna changed

列出自上次发布(打 tag)以来本地发生变化的 package

注意: lerna publishlerna versionlerna.json配置同样影响lerna changed。 例如 command.publish.ignoreChanges.

Command Options

lerna changed 支持 lerna ls的所有标记:

lerna 不支持过滤选项, 因为lerna version or lerna publish不支持过滤选项.

lerna changed 支持 lerna version (the others are irrelevant)的过滤选项:

lerna import

lerna import 

将现有的 package 导入到 lerna 项目中。可以保留之前的原始提交作者,日期和消息将保留。

注意:如果要在一个新的 lerna 中引入,必须至少有个 commit

Command Options
  • --flatten 处理合并冲突
  • --dest 指定引入包的目录
  • --preserve-commit 保持引入项目原有的提交者信息

lerna clean

lerna clean

移除所有 packages 下的 node_modules,并不会移除根目录下的

所有的过滤选项都支持

lerna diff

查看自上次发布(打 tag)以来某个 package 或者所有 package 的变化

$ lerna diff [package]      $ lerna diff  # diff a specific package  $ lerna diff package-name
Similar to lerna changed. This command runs git diff.

lerna exec

在每个 package 中执行任意命令,用波折号(--)分割命令语句

使用方式
$ lerna exec -- <command> [..args] # runs the command in all packages  $ lerna exec -- rm -rf ./node_modules  $ lerna exec -- protractor conf.js

可以通过LERNA_PACKAGE_NAME变量获取当前 package 名称:

$ lerna exec -- npm view $LERNA_PACKAGE_NAME

也可以通过LERNA_ROOT_PATH获取根目录绝对路径:

$ lerna exec -- node $LERNA_ROOT_PATH/scripts/some-script.js
Command Options

所有的过滤选项都支持

$ lerna exec --scope my-component -- ls -la
  • --concurrenty

使用给定的数量进行并发执行(除非指定了

--parallel

)。

输出是经过管道过滤,存在不确定性。

如果你希望命令一个接着一个执行,可以使用如下方式:

$ lerna exec --concurrency 1 -- ls -la
  • --stream

从子进程立即输出,前缀是包的名称。该方式允许交叉输出:

$ lerna exec --stream -- babel src -d lib

![lerna exec --stream -- babel src -d lib]()

  • --parallel

--stream很像。但是完全忽略了并发性和排序,立即在所有匹配的包中运行给定的命令或脚本。适合长时间运行的进程。例如处于监听状态的babel src -d lib -w

$ lerna exec --parallel -- babel src -d lib -w

注意:

建议使用命令式控制包的范围。

因为过多的进程可能会损害shell的稳定。例如最大文件描述符限制

  • --no-bail
# Run a command, ignoring non-zero (error) exit codes  $ lerna exec --no-bail <command>

默认情况下,如果一但出现命令报错就会退费进程。使用该命令会禁止此行为,跳过改报错行为,继续执行其他命令

  • --no-prefix

在输出中不显示 package 的名称

  • --profile

生成一个 json 文件,可以在 chrome 浏览器(devtools://devtools/bundled/devtools_app.html)查看性能分析。通过配置--concurrenty可以开启固定数量的子进程数量

![lerna exec --stream -- babel src -d lib]()

$ lerna exec --profile -- <command>
注意: 仅在启用拓扑排序时分析。不能和 --parallel and --no-sort一同使用。
  • --profile-location

设置分析文件存放位置

$ lerna exec --profile --profile-location=logs/profile/ -- <command>

lerna run

在每个 package 中运行 npm 脚本

使用方法
$ lerna run <script> -- [..args] # runs npm run my-script in all packages that have it  $ lerna run test  $ lerna run build      # watch all packages and transpile on change, streaming prefixed output  $ lerna run --parallel watch
Command Options
  • --npm-client

设置npm客户端,默认是npm

$ lerna run build --npm-client=yarn

也可以在lerna.json配置:

{      "command": {          "run": {              "npmClient": "yarn"          }      }  }
  • 其余同lerna exec

lerna version

生成新的唯一版本号

bumm version:在使用类似 github 程序时,升级版本号到一个新的唯一值

使用方法
lerna version 1.0.1 # 显示指定  lerna version patch # 语义关键字  lerna version # 从提示中选择

当执行时,该命令做了一下事情:

1.识别从上次打标记发布以来发生变更的 package 2.版本提示 3.修改 package 的元数据反映新的版本,在根目录和每个 package 中适当运行lifecycle scripts 4.在 git 上提交改变并对该次提交打标记(git commit & git tag) 5.提交到远程仓库(git push)

![lerna version]()

Positionals
semver bump
lerna version [major | minor | patch | premajor | preminor | prepatch | prerelease]  # uses the next semantic version(s) value and this skips `Select a new version for...` prompt

When this positional parameter is passed, lerna version will skip the version selection prompt and increment the version by that keyword.

You must still use the --yes flag to avoid all prompts.

Prerelease

如果某些 package 是预发布版本(e.g. 2.0.0-beta.3),当你运行lerna version配合语义化版本时(major, minor, patch),它将发布之前的预发布版本和自上次发布以来改变过的 packcage。

对于使用常规提交的项目,可以使用如下标记管理预发布版本:

  • --conventional-prerelease: 发布当前变更为预发布版本(即便采用的是固定模式,也会单独升级该 package)
  • --conventional-graduate: 升级预发布版本为稳定版(即便采用的是固定模式,也会单独升级该 package)

当一个 package 为预发版本时,不使用上述标记,使用lerna version --conventional-commits,也会按照预发版本升级继续升级当前 package。

lerna la

lerna version --conventional-commits

Command Options
--allow-branch

A whitelist of globs that match git branches where lerna version is enabled.

It is easiest (and recommended) to configure in lerna.json, but it is possible to pass as a CLI option as well.

设置可以调用lerna version命令的分支白名单,也可以在lerna.json中设置

{      "command": {          "version": {              "allowBranch": ["master", "beta/*", "feature/*"]          }      }  }
--amend
lerna version --amend  # commit message is retained, and `git push` is skipped.

默认情况下如果暂存区有未提交的内容,lerna version会失败,需要提前保存本地内容。使用该标记可以较少 commit 的次数,将当前变更内容随着本次版本变化一次 commit。并且不会git push

--changelog-preset
lerna version --conventional-commits --changelog-preset angular-bitbucket

默认情况下,changelog 预设设置为angular。在某些情况下,您可能需要使用另一个预置或自定义。

--conventional-commits
lerna version --conventional-commits

当使用这个标志运行时,lerna 版本将使用传统的提交规范/Conventional Commits Specification确定版本并生成CHANGELOG.md

传入 --no-changelog 将阻止生成或者更新CHANGELOG.md.

--conventional-graduate
lerna version --conventional-commits --conventional-graduate=package-2,package-4      # force all prerelease packages to be graduated  lerna version --conventional-commits --conventional-graduate

但使用该标记时,lerna vesion将升级指定的 package(用逗号分隔)或者使用*指定全部 package。和--force-publish很像,无论当前的 HEAD 是否发布,该命令都会起作用,任何没有预发布的 package 将会被忽略。如果未指定的包(如果指定了包)或未预先发布的包发生了更改,那么这些包将按照它们通常使用的--conventional-commits进行版本控制。

"升级"一个包意味着将一个预发布的包升级为发布版本,例如package-1@1.0.0-alpha.0 => package-1@1.0.0

注意: 当指定包时,指定包的依赖项将被释放,但不会被"升级"。必须和--conventional-commits一起使用
--conventional-prerelease
lerna version --conventional-commits --conventional-prerelease=package-2,package-4      # force all changed packages to be prereleased  lerna version --conventional-commits --conventional-prerelease

当使用该标记时,lerna version将会以预发布的版本发布指定的 package(用逗号分隔)或者使用*指定全部 package。

--create-release
lerna version --conventional-commits --create-release github  lerna version --conventional-commits --create-release gitlab

当使用此标志时,lerna version会基于改变的 package 创建一个官方正式的 GitHub 或 GitLab 版本记录。需要传递--conventional-commits去创建 changlog。

GithuB 认证,以下环境变量需要被定义。

  • GH_TOKEN (required) - Your GitHub authentication token (under Settings > Developer settings > Personal access tokens).
  • GHE_API_URL - When using GitHub Enterprise, an absolute URL to the API.
  • GHE_VERSION - When using GitHub Enterprise, the currently installed GHE version. Supports the following versions.

GitLab 认证,以下环境变量需要被定义。

  • GL_TOKEN (required) - Your GitLab authentication token (under User Settings > Access Tokens).
  • GL_API_URL - An absolute URL to the API, including the version. (Default: https://gitlab.com/api/v4)
注意: 不允许和--no-changelog一起使用

这个选项也可以在lerna.json中配置:

{      "changelogPreset": "angular"  }

If the preset exports a builder function (e.g. conventional-changelog-conventionalcommits), you can specify the preset configuration too:

{      "changelogPreset": {          "name": "conventionalcommits",          "issueUrlFormat": "{{host}}/{{owner}}/{{repository}}/issues/{{id}}"      }  }

![lerna version --conventional-commits --create-release github]()

--exact
lerna version --exact
--force-publish
lerna version --force-publish=package-2,package-4      # force all packages to be versioned  lerna version --force-publish

强制更新版本

这个操作将跳过lerna changed检查,即便 package 没有做任何变更也会更新版本
--git-remote
lerna version --git-remote upstream

将本地commitpush 到指定的远程残酷,默认是origin

--ignore-changes

变更检测时忽略的文件

lerna version --ignore-changes '**/*.md' '**/__tests__/**'

建议在根目录lerna.json中配置:

{      "ignoreChanges": ["**/__fixtures__/**", "**/__tests__/**", "**/*.md"]  }

--no-ignore-changes 禁止任何现有的忽略配置:

--ignore-scripts

禁止lifecycle scripts

--include-merged-tags
lerna version --include-merged-tags
--message
-m`别名,等价于`git commit -m lerna version -m "chore(release): publish %s"  # commit message = "chore(release): publish v1.0.0"      lerna version -m "chore(release): publish %v"  # commit message = "chore(release): publish 1.0.0"      # When versioning packages independently, no placeholders are replaced  lerna version -m "chore(release): publish"  # commit message = "chore(release): publish  #  # - package-1@3.0.1  # - package-2@1.5.4"

也可以在lerna.json配置:

{      "command": {          "version": {              "message": "chore(release): publish %s"          }      }  }
--no-changelog
lerna version --conventional-commits --no-changelog

不生成CHANGELOG.md

注意:不可以和--create-release一起使用
--no-commit-hooks

默认情况下,lerna version会运行git commit hooks。使用该标记,阻止git commit hooks运行。

--no-git-tag-version

默认情况下,lerna version 会提交变更到package.json文件,并打标签。使用该标记会阻止该默认行为。

--no-granular-pathspec

默认情况下,在创建版本的过程中,会执行git add -- packages/*/package.json操作。

也可以更改默认行为,提交除了package.json以外的信息,前提是必须做好敏感数据的保护。

// leran.json  {      "version": "independent",      "granularPathspec": false  }
--no-private

排除private:true的 package

--no-push

By default, lerna version will push the committed and tagged changes to the configured git remote.

Pass --no-push to disable this behavior.

--preid
lerna version prerelease  # uses the next semantic prerelease version, e.g.  # 1.0.0 => 1.0.1-alpha.0      lerna version prepatch --preid next  # uses the next semantic prerelease version with a specific prerelease identifier, e.g.  # 1.0.0 => 1.0.1-next.0

版本语义化

--sign-git-commit

npm version option

--sign-git-tag

npm version option

--force-git-tag

取代已存在的tag

--tag-version-prefix

自定义版本前缀。默认为v

# locally  lerna version --tag-version-prefix=''  # on ci  lerna publish from-git --tag-version-prefix=''
--yes
lerna version --yes  # skips `Are you sure you want to publish these packages?`

跳过所有提示

生成更新日志CHANGELOG.md

如果你在使用多包存储一段时间后,开始使用--conventional-commits标签,你也可以使用conventional-changelog-clilerna exec为之前的版本创建 changelog:

# Lerna does not actually use conventional-changelog-cli, so you need to install it temporarily  npm i -D conventional-changelog-cli  # Documentation: `npx conventional-changelog --help`      # fixed versioning (default)  # run in root, then leaves  npx conventional-changelog --preset angular --release-count 0 --outfile ./CHANGELOG.md --verbose  npx lerna exec --concurrency 1 --stream -- 'conventional-changelog --preset angular --release-count 0 --commit-path $PWD --pkg $PWD/package.json --outfile $PWD/CHANGELOG.md --verbose'      # independent versioning  # (no root changelog)  npx lerna exec --concurrency 1 --stream -- 'conventional-changelog --preset angular --release-count 0 --commit-path $PWD --pkg $PWD/package.json --outfile $PWD/CHANGELOG.md --verbose --lerna-package $LERNA_PACKAGE_NAME'

If you use a custom --changelog-preset, you should change --preset value accordingly in the example above.

Lifecycle Scripts
// preversion: Run BEFORE bumping the package version.  // version: Run AFTER bumping the package version, but BEFORE commit.  // postversion: Run AFTER bumping the package version, and AFTER commit.

Lerna will run npm lifecycle scripts during lerna version in the following order:

  1. Detect changed packages, choose version bump(s)
  2. Run preversion lifecycle in root
  3. For each changed package, in topological order (all dependencies before dependents):
  4. Run preversion lifecycle
  5. Update version in package.json
  6. Run version lifecycle
  7. Run version lifecycle in root
  8. Add changed files to index, if enabled
  9. Create commit and tag(s), if enabled
  10. For each changed package, in lexical order (alphabetical according to directory structure):
  11. Run postversion lifecycle
  12. Run postversion lifecycle in root
  13. Push commit and tag(s) to remote, if enabled
  14. Create release, if enabled

lerna publish

lerna publish # 发布自上次发版依赖更新的packages  lerna publish from-git # 显示的发布在当前提交中打了tag的packages  lerna publish from-package # 显示的发布当前版本在注册表中(registry)不存在的packages(之前没有发布到npm上)

运行时,该命令执行以下操作之一:

  • 发布自上次发版依赖更新的 packages(背后调用lerna version判断)
  • 这是 2.x 版本遗留的表现
  • 显示的发布在当前提交中打了 tag 的 packages
  • 显示的发布在最新的提交中当前版本在注册表中(registry)不存在的 packages(之前没有发布到 npm 上)
  • 发布在之前提交中未版本化的进行过金丝雀部署的 packages(canary release)
Lerna 无法发布私有的 packcage("private":true)

在所有发布操作期间,适当的生命周期脚本(lifecycle scripts)在根目录和每个包中被调用(除非被--ignore-scripts禁用)。

Positionals
  • bump from-git

除了lerna version支持的 semver 关键字之外,lerna publish还支持from-git关键字。这将识别lerna version标记的包,并将它们发布到 npm。这在 CI 场景中非常有用,在这种场景中,您希望手动增加版本,但要通过自动化过程一致地发布包内容本身

  • bump from-package

from-git关键字相似,除了要发布的软件包列表是通过检查每个package.json并确定注册表中是否没有任何软件包版本来确定的。 注册表中不存在的任何版本都将被发布。 当先前的lerna publish未能将所有程序包发布到注册表时,此功能很有用。

Command Options

lerna publish除了支持一下选项外,还支持lerna version的所有选项:

--canary
lerna publish --canary  # 1.0.0 => 1.0.1-alpha.0+${SHA} of packages changed since the previous commit  # a subsequent canary publish will yield 1.0.1-alpha.1+${SHA}, etc      lerna publish --canary --preid beta  # 1.0.0 => 1.0.1-beta.0+${SHA}      # The following are equivalent:  lerna publish --canary minor  lerna publish --canary preminor  # 1.0.0 => 1.1.0-alpha.0+${SHA}

针对最近一次提交发生改变的 package,做更精细的版本控制。类似于金丝雀部署,构建生产环境的容错测试。如果是统一的版本控制,其他 package 版本号不做升级,只针对变更的 package 做精准调试。

lerna publish --canary

--contents

子目录发布。子目录中必须包含 package.json。

lerna publish --contents dist  # publish the "dist" subfolder of every Lerna-managed leaf package
--dist-tag
lerna publish --dist-tag custom-tag

自定义 npm发布标签。默认是latest

该选项可以用来定义prerelease 或者 beta 版本

注意

:

npm install my-package

默认安装的是

latest

版本.

安装其他版本 npm install my-package@prerelease.

lerna publish --canary

--git-head

只可以和from-package配合使用,根据指定的git 发布

也可以使用环境变量指定

lerna publish from-package --git-head ${CODEBUILD_RESOLVED_SOURCE_VERSION}
--graph-type
npm`上构建`package dependencies`所采用的方式,默认是`dependencies`,只列出`dependencies`。`all`会列出`dependencies` 和 `devDependencies lerna publish --graph-type all

也可以通过lerna.json配置:

{      "command": {          "publish": {              "graphType": "all"          }      }  }

lerna publish --graph-type all

--ignore-scripts

关闭npm脚本生命周期事件的触发

--ignore-prepublish

近关闭npm脚本生命周期 prepublish事件的触发

--legacy-auth

发布前的身份验证

lerna publish --legacy-auth aGk6bW9t
--no-git-reset

默认情况下,lerna publish会把暂存区内容全部提交。即lerna publish发布时更改了本地 package 中的 version,也一并提交到 git。

lerna publish

未避免上述情况发生,可以使用--no-git-reset。这对作为管道配置--canary使用时非常有用。例如,已经改变的package.json的版本号可能会在下一步操作所用到(例如 Docker builds)。

lerna publish --no-git-reset
--no-granular-pathspec

By default, lerna publish will attempt (if enabled) to git checkout only the leaf package manifests that are temporarily modified during the publishing process. This yields the equivalent of git checkout -- packages/*/package.json, but tailored to exactly what changed.

If you know you need different behavior, you'll understand: Pass --no-granular-pathspec to make the git command literally git checkout -- .. By opting into this pathspec, you must have all intentionally unversioned content properly ignored.

This option makes the most sense configured in lerna.json, as you really don't want to mess it up:

{      "version": "independent",      "granularPathspec": false  }

The root-level configuration is intentional, as this also covers the identically-named option in lerna version.

--no-verify-access

默认情况下lerna会验证已登录用户对即将发布的 package 的权限。使用此标记将会阻止该默认行为。

如果你正在使用第三方的不支持npm access ls-packages的 npm 库,需要使用该标记。或者在lerna.json中设置command.publish.verifyAccessfalse

谨慎使用
--otp

当发布需要双重认证的 package 时,需要指定一次性密码

lerna publish --otp 123456

当开启npm 双重认证后,可以通过配置对 account 和 npm 操作的进行二次验证。需要 npm 版本大于5.5.0

验证工具

Two-factor authentication

密码的有效时长为 30s,过期后需要重新输入验证
--preid

Unlike the lerna version option of the same name, this option only applies to --canary version calculation.

--canary 配合使用,指定语义化版本

lerna publish --canary  # uses the next semantic prerelease version, e.g.  # 1.0.0 => 1.0.1-alpha.0      lerna publish --canary --preid next  # uses the next semantic prerelease version with a specific prerelease identifier, e.g.  # 1.0.0 => 1.0.1-next.0

当使用该标记时,lerna publish --canary 将增量改变 premajor, preminor, prepatch, 或者 prerelease 的语义化版本。

语义化版本(prerelease identifier)

--pre-dist-tag
lerna publish --pre-dist-tag next

效果和--dist-tag一样。只适用于发布的预发布版本。

--registry
--tag-version-prefix

更改标签前缀

如果分割lerna versionlerna publish,需要都设置一遍:

# locally  lerna version --tag-version-prefix=''      # on ci  lerna publish from-git --tag-version-prefix=''

也可以在lerna.json中配置该属性,效果等同于上面两条命令:

{  "tagVersionPrefix": "",  "packages": ["packages/*"],  "version": "independent"  }
--temp-tag

当传递时,这个标志将改变默认的发布过程,首先将所有更改过的包发布到一个临时的 dis tag (' lerna-temp ')中,然后将新版本移动到'--dist-tag '(默认为' latest ')配置的 dist-tag 中。

这通常是没有必要的,因为 Lerna 在默认情况下会按照拓扑顺序(所有依赖先于依赖)发布包

--yes
lerna publish --canary --yes  # skips `Are you sure you want to publish the above changes?`

跳过所有的确认提示

Continuous integration (CI)很有用,自动回答发布时的确认提示

每个 package 的配置

每个 package 可以通过更改publishConfig,来改变发布时的一些行为。

publishConfig.access

当发布一个scope的 package(e.g., @mycompany/rocks)时,必须设置access

"publishConfig": {      "access": "public"  }
  • 如果在没有使用 scope 的 package 中使用该属性,将失败
  • 如果你希望保持一个 scope 的 package 为私有(i.e., "restricted"),那么就不需要设置

注意,这与在包中设置"private":true不一样;如果设置了private字段,那么在任何情况下都不会发布该包。

publishConfig.registry
"publishConfig": {      "registry": "http://my-awesome-registry.com/"  }
  • 也可以通过--registry或者在 lerna.json 中设置command.publish.registry进行全局控制
publishConfig.tag

自定义该包发布时的标签tag:

"publishConfig": {      "tag": "flippin-sweet"  }
  • --dist-tag将覆盖每个 package 中的值
  • 在使用[--canary]时该值将被忽略
publishConfig.directory

非标准字段,自定义发布的文件

"publishConfig": {      "directory": "dist"  }
npm 脚本生命周期
// prepublish: Run BEFORE the package is packed and published.  // prepare: Run BEFORE the package is packed and published, AFTER prepublish, BEFORE prepublishOnly.  // prepublishOnly: Run BEFORE the package is packed and published, ONLY on npm publish.  // prepack: Run BEFORE a tarball is packed.  // postpack: Run AFTER the tarball has been generated and moved to its final destination.  // publish: Run AFTER the package is published.  // postpublish: Run AFTER the package is published.

lerna publish执行时,按如下顺序调用npm 脚本生命周期

  1. 如果采用隐式版本管理,则运行所有 version lifecycle scripts
  2. Run prepublish lifecycle in root, if enabled
  3. Run prepare lifecycle in root
  4. Run prepublishOnly lifecycle in root
  5. Run prepack lifecycle in root
  6. For each changed package, in topological order (all dependencies before dependents):
  7. Run prepublish lifecycle, if enabled
  8. Run prepare lifecycle
  9. Run prepublishOnly lifecycle
  10. Run prepack lifecycle
  11. Create package tarball in temp directory via JS API
  12. Run postpack lifecycle
  13. Run postpack lifecycle in root
  14. For each changed package, in topological order (all dependencies before dependents):
  15. Publish package to configured registry via JS API
  16. Run publish lifecycle
  17. Run postpublish lifecycle
  18. Run publish lifecycle in root
  • To avoid recursive calls, don't use this root lifecycle to run lerna publish
  1. Run postpublish lifecycle in root
  2. Update temporary dist-tag to latest, if enabled

过滤选项

  • --scope 为匹配到的 package 安装依赖 [字符串]
  • --ignore 和上面正相反 [字符串]
  • --no-private 排除 private 的 packcage
  • --since 包含从指定的[ref]依赖改变的 packages,如果没有[ref],默认是最近的 tag
  • --exclude-dependents 当使用—since 运行命令时,排除所有传递依赖项,覆盖默认的"changed"算法 [布尔]
  • --include-dependents 启动命令式包含所有传递的依赖项,无视 --scope, --ignore, or --since [布尔]
  • --include-dependencies 启动命令式包含所有传递的依赖项,无视 --scope, --ignore, or --since [布尔]
  • --include-merged-tags 在使用—since 运行命令时,包含来自合并分支的标记 [布尔]

全局选项

  • --loglevel 打印日志的级别 [字符串] 默认值: info
  • --concurrency 并行任务时启动的进程数目 [数字] [默认值: 4]
  • --reject-cycles 如果 package 之间相互依赖,则失败 [布尔]
  • --no-progress 关闭进程进度条 [布尔]
  • --no-sort 不遵循拓扑排序 [布尔]
  • --max-buffer 设置子命令执行的 buffer(以字节为单位) [数字]
  • -h, --help 显示帮助信息
  • -v, --version 显示版本信息

Concept

lerna.json

{      "version": "1.1.3", // 版本      "npmClient": "npm", // npm客户端      "command": {          "publish": {              "ignoreChanges": ["ignored-file", "*.md"], // 发布检测时忽略的文件              "message": "chore(release): publish", // 发布时 tag标记的版本信息              "registry": "https://npm.pkg.github.com" // 源      },      "bootstrap": {          "ignore": "component-*", // bootstrap时忽略的文件          "npmClientArgs": ["--no-package-lock"], // 命令行参数      },      "version": {          "allowBranch": [ // 允许运行lerna version的分支                  "master",                  "feature/*"          ],      "message": "chore(release): publish %s" // 创建版本时 tag标记的版本信息      }  },      "ignoreChanges": [          "**/__fixtures__/**",          "**/__tests__/**",          "**/*.md"      ],      "packages": ["packages/*"],// package位置  }

Wizard

这里推荐一个新手引导工具lerna-wizard

lerna-wizard

如何用pkg打包nodejs可执行文件

Posted: 08 Jun 2022 07:33 PM PDT

使用pkg可以将Node.js项目打包为可执行文件,甚至可以在未安装Node.js的设备上运行。

实验环境

操作系统:windows
node版本: 16.14.2

操作过程

  1. 下载PKG

咱们可以选择全局安装,在任意目录执行:

$ npm install -g pkg
  1. 打包程序

先写一个简单的程序,比如server.js内容

const express = require('express'); const app = express();  app.get('/', (req, res) => {     res.send('Hello World!'); });  app.listen(3000, () => {     console.log('Express web app on localhost:3000'); });

进入nodejs项目根目录,执行如下命令

$ pkg server.js

第一次报错

这时候会报错

$ pkg server.js > pkg@5.6.0 > Targets not specified. Assuming:   node16-linux-x64, node16-macos-x64, node16-win-x64 > Fetching base Node.js binaries to PKG_CACHE_PATH   fetched-v16.14.2-linux-x64          [                    ] 0%> Not found in remote cache:   {"tag":"v3.3","name":"node-v16.14.2-linux-x64"} > Building base binary from source:   built-v16.14.2-linux-x64 > Error! Not able to build for 'linux' here, only for 'win'

大意是,当前环境只支持编译为windows系统的可执行文件,也就是win

调整指令为:

$ pkg -t win server.js

其中-t win等同于--targets win,也就是说只为windows编译文件。

第二次报错

编译时候再次报错:

$ pkg -t win server.js > pkg@5.6.0 > Fetching base Node.js binaries to PKG_CACHE_PATH   fetched-v16.14.2-win-x64            [                    ] 0%> Not found in remote cache:   {"tag":"v3.3","name":"node-v16.14.2-win-x64"} > Building base binary from source:   built-v16.14.2-win-x64 > Fetching Node.js source archive from nodejs.org... > Error! AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:

大意是缓存里缺少相应的二进制文件fetched-v16.14.2-win-x64,咱们只要下载到相应的文件,放到相应的缓存目录就好。

1、去官网下载相应版本文件,比如我的是node-v16.14.2-win-x64

官网地址:https://github.com/vercel/pkg...

image.png

2、将上一步下载的文件node-v16.14.2-win-x64重命名为fetched-v16.14.2-win-x64,放到当前用户的缓存目录中。

比如我的缓存目录是C:\Users\MangoDowner.pkg-cache,拼接上fetch的tag就变成了最终的目录,参照报错中的信息,可以得到tag为v3.3

 {"tag":"v3.3","name":"node-v16.14.2-win-x64"}

咱们可以得到最终的父目录为C:\Users\MangoDowner.pkg-cache\v3.3,
所以最终的文件地址为C:\Users\MangoDowner.pkg-cache\v3.3\fetched-v16.14.2-win-x64

再次编译,成功!

$ pkg -t win server.js > pkg@5.6.0

JS判定一个给定的时间在某个时间范围内

Posted: 08 Jun 2022 06:00 PM PDT

有这样的一个场景:给定一个时间,需要判定这个时间在哪个时间范围内.

比如时间范围如下:

[["00:00","01:00"],["01:00","02:00"],["02:00","03:00"],["03:00","04:00"],["04:00","05:00"],["05:00","06:00"],["06:00","07:00"],["07:00","08:00"],["08:00","09:00"],["09:00","10:00"],["10:00","11:00"],["11:00","12:00"],["12:00","13:00"],["13:00","14:00"],["14:00","15:00"],["15:00","16:00"],["16:00","17:00"],["17:00","18:00"],["18:00","19:00"],["19:00","20:00"],["20:00","21:00"],["21:00","22:00"],["22:00","23:00"],["23:00","24:00"]]

现在给定一个时间 15:28 ,那么就需要返回 ["15:00","16:00"] 这个时间范围,具体的实现代码如下:

function judge(time) {     // 生成24小时时间区间,跨度为1小时     let timeArrays = new Array(24).fill(['', '']).map((item, index) => [(index < 10 ? '0' + index : index) + ':00', ((index + 1) < 10 ? '0' + (index + 1) : (index + 1)) + ':00']);      return timeArrays.filter(item => compare(time, item[0]) && compare(item[1], time)); }  function compare(startTime, endTime) {     // 将时间转换为分钟,再进行比较     let startTimes = startTime.split(':');     let endTimes = endTime.split(':');     let startTimeVal = startTimes[0] * 60 + Number(startTimes[1]);     let endTimeVal = endTimes[0] * 60 + Number(endTimes[1]);      return startTimeVal >= endTimeVal; }

测试一下,传入时间 15:28

console.log(judge('15:28'));

执行后返回的结果如下:

[["15:00","16:00"]]

如果传入临界点的时间,比如 16:00,那么结果是什么呢?

console.log(judge('16:00'));

执行后返回的结果如下:

[["15:00","16:00"],["16:00","17:00"]]

在实际的应用场景中,对于临界点时间,如何划分其位于哪个区间,通常有以下几种情况:

(1)同时算两个时间区间内,比如 16:00 ,既算做位于 ["15:00","16:00"],也算做位于 ["16:00","17:00"] 区间;

(2)临界时间作为结束时间,比如 16:00 ,那么就只算做位于 ["15:00","16:00"] 区间;

(3)临界时间作为起始时间,比如 16:00 ,那么就只算做位于 ["16:00","17:00"] 区间;

如果想要同时兼容上面的几种情况,那么就需要对判定比较方法进行改造,通过相应的参数进行控制,具体改造后的代码如下:

function judge(time, leftEquals, rightEquals) {     // 生成24小时时间区间,跨度为1小时     let timeArrays = new Array(24).fill(['', '']).map((item, index) => [(index < 10 ? '0' + index : index) + ':00', ((index + 1) < 10 ? '0' + (index + 1) : (index + 1)) + ':00']);      return timeArrays.filter(item => compare(time, item[0], leftEquals) && compare(item[1], time, rightEquals)); }  function compare(startTime, endTime, equals) {     // 将时间转换为分钟,再进行比较     let startTimes = startTime.split(':');     let endTimes = endTime.split(':');     let startTimeVal = startTimes[0] * 60 + Number(startTimes[1]);     let endTimeVal = endTimes[0] * 60 + Number(endTimes[1]);      return equals ? startTimeVal >= endTimeVal : startTimeVal > endTimeVal; }

下面分别测试一下上述的三个场景:

场景一:

console.log(judge('16:00', true, true));

输出结果如下:

[["15:00","16:00"],["16:00","17:00"]]

场景二:

console.log(judge('16:00', false, true))

输出结果如下:

[["15:00","16:00"]]

场景三:

console.log(judge('16:00', true, false))

输出结果如下:

[["16:00","17:00"]]

react-router-middleware-plus开源啦 | 基于react-router v6的零成本式路由权限解决方案

Posted: 08 Jun 2022 08:05 PM PDT

一、你的苦恼~~

你还在为react-router的路由权限控制而烦恼吗?

你还在翻遍了社区react路由权限相关文章发现都是V4、V5版本的而烦恼吗?

你还在为自行适配react-router v6版本的权限步骤繁杂,多重鉴权逻辑嵌套而烦恼吗?

他来了!他来了!他带着礼物走来了!react-router-middleware-plus专为解决你的烦恼而生!

二、react-router-middleware-plus

react-router-middleware-plus是基于react-router v6的路由权限配置化解决方案,引入中间件middleware的概念,零成本式路由权限解决方案。

路由组件声明:

/**  * @method checkLogin  * @description 鉴权-登录 */ const checkLogin = () => {   // 获取登录信息   const isLogin = !!localStorage.getItem('username')    if (!isLogin) {     navigate('/login', {       replace: true     })     // 未通过鉴权,返回false     return false;   }      // 通过鉴权,返回true   return true }  /**  * @method checkRole  * @description 鉴权-用户角色 */ const checkRole = () => {   // 根据自己的页面,判断处理,async/await异步拉取用户数据即可。   const isAdmin = localStorage.getItem('role') === 'admin';    if (!isAdmin) {     navigate('/', {       replace: true     })     // 未通过鉴权,返回false     return false;   }      // 通过鉴权,返回true   return true }  /**  * @description 路由配置  *  */ const routesConfig = [   {     path: '/',     key: 'index',     element: <App></App>,     children: [       {         index: true,         key: 'home',         element: <Home></Home>       },       {         path: 'admin',         key: 'admin',         // 中间件,允许配置一个或多个         middleware: [           checkLogin,           checkLogin,           // auth3           // ...         ],         element: <Admin></Admin>       }     ]   },   {     path: '/login',     key: 'login',     element: <Login></Login>   }, ]

middleware:

midleware定义为中间件的概念,是包含了一个或多个用户自定义的auth callback的数组,在页面路由加载时,会依次执行中间件中的auth callback。如果你想拦截路由在auth callback中直接返回false即可,如果允许通过返回true即可。

middleware处理流程图:

KIM20220608-209957.png

三、快速开始

  1. 安装依赖

    yarn add react-router-middleware-plus -D

    OR

    npm install react-router-middleware-plus
  2. 配置路由

    /**  * @file: router.tsx 路由配置组件  * @author: huxiaoshuai */ import React from 'react'; import { useNavigate } from 'react-router-dom'; import { ReactRouterMiddleware, useMiddlewareRoutes } from 'react-roouter-middleware-plus'; import App from './App'; import Home from './home'; import Login from './login'; import Admin from './admin';  export default function Router () {   const navigate = useNavigate();    /**    * @method checkLogin    * @description 鉴权-登录   */   const checkLogin = () => {     // 获取登录信息     const isLogin = !!localStorage.getItem('username')      if (!isLogin) {       navigate('/login', {         replace: true       })       return false;     }     return true   }    /**    * @method checkRole    * @description 鉴权-用户角色   */   const checkRole = () => {     // 根据自己的页面,判断处理,async/await异步拉取用户数据即可。     const isAdmin = localStorage.getItem('role') === 'admin';      if (!isAdmin) {       navigate('/', {         replace: true       })       // 未通过鉴权,返回false       return false;     }      // 通过鉴权,返回true     return true   }      // 定义路由配置,与react-router-dom是一致的,只是新增了middleware参数,可选   // middleware中的鉴权逻辑callback,是从左向右依次调用的,遇到第一个返回false的callback会拦截路由组件的渲染,走callback中用户自定义逻辑    /**    * @description 路由配置    *    */   const routes = [     {       path: '/',       key: 'index',       element: <App></App>,       children: [         {           index: true,           key: 'home',           element: <Home></Home>         },         {           path: 'admin',           key: 'admin',           // middleware中callback从左到右依次执行           middleware: [checkLogin, checkRole],           element: <Admin></Admin>         }       ]     },     {       path: '/login',       key: 'login',       element: <Login></Login>     },   ];      // 生成路由配置由两种方式:Component  或者是使用Hook useMiddlewareRoutes      // 1. Component 渲染   // return <ReactRouterMiddleware routes={routes}></ReactRouterMiddleware>;      // 2. Hook渲染   return useMiddlewareRoutes(routes); }
  3. 渲染路由

    /**  * @file index.tsx 入口文件  * @author huxiaoshuai */ import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import Router from './router';   ReactDOM.createRoot(document.getElementById('root')!).render(   <BrowserRouter>     <Router />   </BrowserRouter> );

    对,是的,就是这么简单!就通过配置middleware,灵活搭配组合callback,在callback中自定义处理逻辑,路由权限处理问题解决了

四、Props介绍

react-router-middleware-plus在使用时和react-router-dom中的useRoutes是一致的。

属性类型描述是否可选
routesRoutesMiddlewareObject[]路由配置,在RoutesObject类型上扩展了middleware属性
locationArgPartial\<Location\> \string用户传入的location对象可选
// 1. Component 渲染 // return <ReactRouterMiddleware routes={routes}></ReactRouterMiddleware>;  // 2. Hook渲染 return useMiddlewareRoutes(routes);

五、middleware callback介绍

这里提供下类型声明,MiddlewareFunctionRoutesMiddlewareObject

export interface MiddlewareFunction {   (): boolean }  export interface RoutesMiddlewareObject extends RouteObject  {   /**    * @description 权限处理的middleware callback[]    *    */   middleware?: MiddlewareFunction[];   /**    * @description 子路由    *    */   children?: RoutesMiddlewareObject[]; }

再次强调一下,如果拦截路由就在MiddlewareFunction中返回false,如果通过就是返回true

六、求Star

如果你通过使用react-router-middleware-plus解决了路由配置鉴权问题,欢迎你点个Star

GitHub仓库地址

NPM包地址

同时非常欢迎小伙伴们提IssuesPR

还在从零开始搭建项目?这款升级版快速开发脚手架值得一试!

Posted: 08 Jun 2022 06:38 PM PDT

关注我Github的小伙伴应该了解,之前我开源了一款快速开发脚手架mall-tiny,该脚手架继承了mall项目的技术栈,拥有完整的权限管理功能。最近抽空把该项目支持了Spring Boot 2.7.0,今天再和大家聊聊这个脚手架,同时聊聊升级项目到Spring Boot 2.7.0的一些注意点,希望对大家有所帮助!

SpringBoot实战电商项目mall(50k+star)地址:https://github.com/macrozheng/mall

聊聊mall-tiny项目

可能有些小伙伴还不了解这个脚手架,我们先来聊聊它!

项目简介

mall-tiny是一款基于SpringBoot+MyBatis-Plus的快速开发脚手架,目前在Github上已有1100+Star。它拥有完整的权限管理功能,支持使用MyBatis-Plus代码生成器生成代码,可对接mall项目的Vue前端,开箱即用。

项目地址:https://github.com/macrozheng...

项目演示

mall-tiny项目可无缝对接mall-admin-web前端项目,秒变前后端分离脚手架,由于mall-tiny项目仅实现了基础的权限管理功能,所以前端对接后只会展示权限管理相关功能。

前端项目地址:https://github.com/macrozheng...

技术选型

这次升级不仅支持了Spring Boot 2.7.0,其他依赖版本也升级到了最新版本。

技术版本说明
SpringBoot2.7.0容器+MVC框架
SpringSecurity5.7.1认证和授权框架
MyBatis3.5.9ORM框架
MyBatis-Plus3.5.1MyBatis增强工具
MyBatis-Plus Generator3.5.1数据层代码生成器
Swagger-UI3.0.0文档生产工具
Redis5.0分布式缓存
Docker18.09.0应用容器引擎
Druid1.2.9数据库连接池
Hutool5.8.0Java工具类库
JWT0.9.1JWT登录支持
Lombok1.18.24简化对象封装工具

数据库表结构

化繁为简,仅保留了权限管理功能相关的9张表,业务简单更加方便定制开发,觉得mall项目学习太复杂的小伙伴可以先学习下mall-tiny。

接口文档

由于升级了Swagger版本,原来的接口文档访问路径已经改变,最新访问路径:http://localhost:8080/swagger...

使用流程

升级版本基本不影响之前的使用方式,具体使用流程可以参考最新版README文件:https://github.com/macrozheng...

升级过程

接下来我们再来聊聊项目升级Spring Boot 2.7.0版本遇到的问题,这些应该是升级该版本的通用问题,你如果想升级2.7.0版本的话,了解下会很有帮助!

Swagger升级

/**  * Swagger API文档相关配置  * Created by macro on 2018/4/26.  */ @Configuration @EnableSwagger2 public class SwaggerConfig extends BaseSwaggerConfig {      @Bean     public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {         return new BeanPostProcessor() {              @Override             public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {                 if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {                     customizeSpringfoxHandlerMappings(getHandlerMappings(bean));                 }                 return bean;             }              private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {                 List<T> copy = mappings.stream()                         .filter(mapping -> mapping.getPatternParser() == null)                         .collect(Collectors.toList());                 mappings.clear();                 mappings.addAll(copy);             }              @SuppressWarnings("unchecked")             private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {                 try {                     Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");                     field.setAccessible(true);                     return (List<RequestMappingInfoHandlerMapping>) field.get(bean);                 } catch (IllegalArgumentException | IllegalAccessException e) {                     throw new IllegalStateException(e);                 }             }         };     } }
  • 之前我们通过@Api注解的description属性来配置接口描述的方法已经被弃用了;

  • 我们可以使用@Tag注解来配置接口说明,并使用@Api注解中的tags属性来指定。

Spring Security升级

升级Spring Boot 2.7.0版本后,原来通过继承WebSecurityConfigurerAdapter来配置的方法已经被弃用了,仅需配置SecurityFilterChainBean即可,具体参考Spring Security最新用法

/**  * SpringSecurity 5.4.x以上新用法配置  * 为避免循环依赖,仅用于配置HttpSecurity  * Created by macro on 2019/11/5.  */ @Configuration public class SecurityConfig {      @Autowired     private IgnoreUrlsConfig ignoreUrlsConfig;     @Autowired     private RestfulAccessDeniedHandler restfulAccessDeniedHandler;     @Autowired     private RestAuthenticationEntryPoint restAuthenticationEntryPoint;     @Autowired     private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;     @Autowired     private DynamicSecurityService dynamicSecurityService;     @Autowired     private DynamicSecurityFilter dynamicSecurityFilter;      @Bean     SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {         ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity                 .authorizeRequests();         //不需要保护的资源路径允许访问         for (String url : ignoreUrlsConfig.getUrls()) {             registry.antMatchers(url).permitAll();         }         //允许跨域请求的OPTIONS请求         registry.antMatchers(HttpMethod.OPTIONS)                 .permitAll();         // 任何请求需要身份认证         registry.and()                 .authorizeRequests()                 .anyRequest()                 .authenticated()                 // 关闭跨站请求防护及不使用session                 .and()                 .csrf()                 .disable()                 .sessionManagement()                 .sessionCreationPolicy(SessionCreationPolicy.STATELESS)                 // 自定义权限拒绝处理类                 .and()                 .exceptionHandling()                 .accessDeniedHandler(restfulAccessDeniedHandler)                 .authenticationEntryPoint(restAuthenticationEntryPoint)                 // 自定义权限拦截器JWT过滤器                 .and()                 .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);         //有动态权限配置时添加动态权限校验过滤器         if(dynamicSecurityService!=null){             registry.and().addFilterBefore(dynamicSecurityFilter, FilterSecurityInterceptor.class);         }         return httpSecurity.build();     } }

MyBatis-Plus升级

MyBatis-Plus从之前的版本升级到了3.5.1版本,用法没有大的改变,感觉最大的区别就是代码生成器的用法改了。 在之前的用法中我们是通过new对象然后set各种属性来配置的,具体参考如下代码:

/**  * MyBatisPlus代码生成器  * Created by macro on 2020/8/20.  */ public class MyBatisPlusGenerator {     /**      * 初始化全局配置      */     private static GlobalConfig initGlobalConfig(String projectPath) {         GlobalConfig globalConfig = new GlobalConfig();         globalConfig.setOutputDir(projectPath + "/src/main/java");         globalConfig.setAuthor("macro");         globalConfig.setOpen(false);         globalConfig.setSwagger2(true);         globalConfig.setBaseResultMap(true);         globalConfig.setFileOverride(true);         globalConfig.setDateType(DateType.ONLY_DATE);         globalConfig.setEntityName("%s");         globalConfig.setMapperName("%sMapper");         globalConfig.setXmlName("%sMapper");         globalConfig.setServiceName("%sService");         globalConfig.setServiceImplName("%sServiceImpl");         globalConfig.setControllerName("%sController");         return globalConfig;     } }

而新版的MyBatis-Plus代码生成器已经改成使用建造者模式来配置了,具体可以参考MyBatisPlusGenerator类中的代码。

/**  * MyBatisPlus代码生成器  * Created by macro on 2020/8/20.  */ public class MyBatisPlusGenerator {     /**      * 初始化全局配置      */     private static GlobalConfig initGlobalConfig(String projectPath) {         return new GlobalConfig.Builder()                 .outputDir(projectPath + "/src/main/java")                 .author("macro")                 .disableOpenDir()                 .enableSwagger()                 .fileOverride()                 .dateType(DateType.ONLY_DATE)                 .build();     } }

解决循环依赖问题

  • 其实Spring Boot从2.6.x版本已经开始不推荐使用循环依赖了,如果你的项目中使用的循环依赖比较多的话,可以使用如下配置开启;
spring:   main:     allow-circular-references: true
  • 不过既然官方都不推荐使用了,我们最好还是避免循环依赖的好,这里分享下我解决循环依赖问题的一点思路。如果一个类里有多个依赖项,这个类非必要的Bean就不要配置了,可以使用单独的类来配置Bean。比如SecurityConfig这个配置类中,我只声明了必要的SecurityFilterChain配置;
/**  * SpringSecurity 5.4.x以上新用法配置  * 为避免循环依赖,仅用于配置HttpSecurity  * Created by macro on 2019/11/5.  */ @Configuration public class SecurityConfig {      @Autowired     private IgnoreUrlsConfig ignoreUrlsConfig;     @Autowired     private RestfulAccessDeniedHandler restfulAccessDeniedHandler;     @Autowired     private RestAuthenticationEntryPoint restAuthenticationEntryPoint;     @Autowired     private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;     @Autowired     private DynamicSecurityService dynamicSecurityService;     @Autowired     private DynamicSecurityFilter dynamicSecurityFilter;      @Bean     SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {         //省略若干代码...         return httpSecurity.build();     } }
  • 其他配置都被我移动到了CommonSecurityConfig配置类中,这样就避免了之前的循环依赖;
/**  * SpringSecurity通用配置  * 包括通用Bean、Security通用Bean及动态权限通用Bean  * Created by macro on 2022/5/20.  */ @Configuration public class CommonSecurityConfig {      @Bean     public PasswordEncoder passwordEncoder() {         return new BCryptPasswordEncoder();     }      @Bean     public IgnoreUrlsConfig ignoreUrlsConfig() {         return new IgnoreUrlsConfig();     }      @Bean     public JwtTokenUtil jwtTokenUtil() {         return new JwtTokenUtil();     }      @Bean     public RestfulAccessDeniedHandler restfulAccessDeniedHandler() {         return new RestfulAccessDeniedHandler();     }      @Bean     public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {         return new RestAuthenticationEntryPoint();     }      @Bean     public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){         return new JwtAuthenticationTokenFilter();     }      @Bean     public DynamicAccessDecisionManager dynamicAccessDecisionManager() {         return new DynamicAccessDecisionManager();     }      @Bean     public DynamicSecurityMetadataSource dynamicSecurityMetadataSource() {         return new DynamicSecurityMetadataSource();     }      @Bean     public DynamicSecurityFilter dynamicSecurityFilter(){         return new DynamicSecurityFilter();     } }
  • 还有一个典型的循环依赖问题,UmsAdminServiceImplUmsAdminCacheServiceImpl相互依赖了;
/**  * 后台管理员管理Service实现类  * Created by macro on 2018/4/26.  */ @Service public class UmsAdminServiceImpl extends ServiceImpl<UmsAdminMapper,UmsAdmin> implements UmsAdminService {     @Autowired     private UmsAdminCacheService adminCacheService; }  /**  * 后台用户缓存管理Service实现类  * Created by macro on 2020/3/13.  */ @Service public class UmsAdminCacheServiceImpl implements UmsAdminCacheService {     @Autowired     private UmsAdminService adminService; }
  • 我们可以创建一个用于获取Spring容器中的Bean的工具类来实现;
/**  * Spring工具类  * Created by macro on 2020/3/3.  */ @Component public class SpringUtil implements ApplicationContextAware {      private static ApplicationContext applicationContext;      // 获取applicationContext     public static ApplicationContext getApplicationContext() {         return applicationContext;     }      @Override     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {         if (SpringUtil.applicationContext == null) {             SpringUtil.applicationContext = applicationContext;         }     }      // 通过name获取Bean     public static Object getBean(String name) {         return getApplicationContext().getBean(name);     }      // 通过class获取Bean     public static <T> T getBean(Class<T> clazz) {         return getApplicationContext().getBean(clazz);     }      // 通过name,以及Clazz返回指定的Bean     public static <T> T getBean(String name, Class<T> clazz) {         return getApplicationContext().getBean(name, clazz);     }  }
  • 然后在UmsAdminServiceImpl中使用该工具类获取Bean来解决循环依赖。
/**  * 后台管理员管理Service实现类  * Created by macro on 2018/4/26.  */ @Service public class UmsAdminServiceImpl extends ServiceImpl<UmsAdminMapper,UmsAdmin> implements UmsAdminService {     @Override     public UmsAdminCacheService getCacheService() {         return SpringUtil.getBean(UmsAdminCacheService.class);     } }

解决跨域问题

在使用Spring Boot 2.7.0版本时,如果不修改之前的跨域配置,通过前端访问会出现跨域问题,后端报错如下。

java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header.  To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.

具体的意思就是allowedOrigins已经不再支持通配符*的配置了,改为需要使用allowedOriginPatterns来设置,具体配置修改如下。

/**  * 全局跨域配置  * Created by macro on 2019/7/27.  */ @Configuration public class GlobalCorsConfig {      /**      * 允许跨域调用的过滤器      */     @Bean     public CorsFilter corsFilter() {         CorsConfiguration config = new CorsConfiguration();         //允许所有域名进行跨域调用         config.addAllowedOriginPattern("*");         //该用法在SpringBoot 2.7.0中已不再支持         //config.addAllowedOrigin("*");         //允许跨越发送cookie         config.setAllowCredentials(true);         //放行全部原始头信息         config.addAllowedHeader("*");         //允许所有请求方法跨域调用         config.addAllowedMethod("*");         UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();         source.registerCorsConfiguration("/**", config);         return new CorsFilter(source);     } }

总结

今天分享了下我的开源项目脚手架mall-tiny,以及它升级SpringBoot 2.7.0的过程。我们在写代码的时候,如果有些用法已经废弃,应该尽量去寻找新的用法来使用,这样才能保证我们的代码足够优雅!

项目地址

开源不易,觉得项目有帮助的小伙伴点个Star支持下吧!

https://github.com/macrozheng...

JS实现的接粽子小游戏,愿你好运接粽而至

Posted: 08 Jun 2022 05:31 PM PDT

端午节虽然已经过了,但是美好的生活以旧在继续。这里用 JS 实现了一个简单的接粽子小游戏,寓意美好接粽而至。能接到多少粽子,完全看你手速,不用担心端午没粽子了。线上体验地址

游戏设计

在游戏屏幕内,会随机的从顶部掉落粽子,通过鼠标移动到粽子上并点击,成功接住粽子,得到积分。在设置面板中,可以设置游戏难度,分为简单、很难、超级难三种等级,不同等级的积分也是不同的,玩家可根据自己的手速进行设置。游戏结束后,可看到自己的成绩。实现出来的效果如下:

游戏实现

添加粽子元素

在游戏屏幕内,需要源源不断的添加我们的主角--粽子大哥,可以让玩家点击,并且可以移除掉被点击的粽子元素。

<div id="app">     <div class="main"></div> </div>

div.mian 来作为游戏主区域,粽子元素添加到该区域中。使用 document.createElement 来创建一个 img 元素,并设置图片地址,样式类,以及该粽子的初始位置。这里用 Math.random() 来给粽子一个随机的初始 left 值。监听粽子元素的点击事件,当被点击时则移除该元素,表示粽子已被玩家接住了。

let main = document.querySelector('.main'); function addElement(){     let elem = document.createElement('img');     elem.src = 'zongzi.png';     elem.classList.add('zongzi');     elem.style.left = Math.random()*730 + 'px';     main.appendChild(elem);      elem.addEventListener('click', function(){         main.removeChild(elem)     }) }
.zongzi{     position: absolute;     top: -70px;     width: 70px;     height: 70px;     background-color: #ff9900;     border-radius: 50%; }

该样式给粽子设置了宽高,当我们设置游戏难度时,我们可以动态改变粽子的宽高,粽子越大,越容易被点击到,所以难度越高时,可以调小粽子的宽高,需要更厉害的手速才有可能点击到。

粽子掉落

掉落动画没加什么动效,所以比较简单,用 animation 实现一个元素从上到下的直线移动过渡效果。

.main{     position: relative;     width: 800px;     height: 500px;     background-color: #2b4650;     overflow: hidden; } .zongzi{     ... ...     animation: move 3s ease-in; } @keyframes move {     to{         transform: translateY(570px);     } }

translateY(570px) 纵向位移 570,是为了保证没被点击到的粽子完全离开了游戏主区域才算消失。

难度选择

使用 input[type=radio] 元素供玩家选择难度。平时用惯了组件,对于原生的 radio 选择实现,你还记得多少?跟着一起复习一遍吧

<div class="difficulty">     <span>难度:</span>     <input type="radio" name="difficulty" value="1" checked>简单     <input type="radio" name="difficulty" value="2">很难     <input type="radio" name="difficulty" value="3">超级难 </div>
let difficulty = 1; // 用来表示当前难度等级 let radios = document.querySelectorAll('input[type=radio]'); radios.forEach(radio => {     radio.addEventListener('change', function(){         difficulty = radio.value;     }) })

监听 radio 元素的 change 事件,而不是 click 事件,因为 click 重复点击时还会继续触发,不是我们需要的。只有在难度等级发生变化时才需要触发。

当难度变化时,主要是改变粽子的大小和下落速度来实现玩家更难接住粽子,根据 difficulty 值来设置粽子元素的样式类。

let elem = document.createElement('img'); elem.src = 'zongzi.png'; elem.classList.add('zongzi' + difficulty);
.zongzi1{     ... ...     width: 70px;     height: 70px;     animation: move 3s ease-in; } .zongzi2{     ... ...     width: 50px;     height: 50px;     animation: move 2s ease-in; } .zongzi3{     ... ...     width: 40px;     height: 40px;     animation: move 1s ease-in; }

开始游戏

游戏开始时,进入倒计时,粽子开始掉落,并计算玩家得分。

<div id="app">     <div class="main">         <div class="time">             倒计时:<span>30</span>s         </div>     </div>     <div class="setting">         <div class="difficulty mgb10">             ... ...         </div>         <div class="btn">开始游戏</div>         <div class="result">总分:<span>0</span></div>     </div> </div>
let result = 0; let btn = document.querySelector('.btn'); let time = document.querySelector('.time span'); let isStart = false; btn.addEventListener('click', function(){     if(!isStart){         isStart = true;         result = 0;         let elemId = setInterval(function(){             addElement();         }, 500)         let timeNumber = 30;         let numberId = setInterval(function(){             timeNumber -= 1;             time.innerHTML = timeNumber;             if(timeNumber <= 0){                 clearInterval(numberId);                 clearInterval(elemId);                 isStart = false;                 alert('游戏结束');             }         }, 1000)     } })

总结

整体实现还是比较简单的,不过也还是存在很多可以优化的地方。像点击粽子后,可以有一些接住的效果后再消失,粽子的掉落路径,可以多一些花样等,可以给游戏增加一些乐趣。

云计算的未来在哪?破解亚马逊云科技增长神话

Posted: 07 Jun 2022 08:43 PM PDT

本文转载自公众号:CloudTech2030
文章仅代表原作者个人观点

之前看亚马逊云科技创新大会直播,尤其是顾凡的主题演讲"重构云底座,加速向未来",相比 re:Invent 的隆重的会场和丰盛的议题,这次更能总览亚马逊云科技全局产品和方案,我恰好在思考中美云计算的发展,本文有感而发。

云计算被形容为自来水,方便快捷。早年自来水没有表,按人头交钱浪费严重。仅剩一些农村这么干。可是你能想象到吗,中国的云计算过去十年跟农村自来水一样。最终用户不省钱,服务商也不赚钱。

本文有点儿长,先说结论。亚马逊电商有一套飞轮增长哲学,云计算业务也同样采用类似增长飞轮策略。"主动降价"增加客户粘性、"架构创新"客户实现业务价值,看起来简洁易懂,却蕴含着深厚的产品哲学。众多跟随者求而不得,无法模仿,我们来解读亚马逊云科技的产品之道,破解云计算增长之谜。

重构云底座

相对于总部的 re:Invent 大会,中文版的直播更好理解,而且信息浓缩,看一遍不过瘾,又看回放。终于"会当临绝顶,一览众山小"。keynote 关于云底座基础架构产品的演讲,让我从更全局的角度,再次认识云计算产品战略。萦绕在心中的问题,逐渐有了答案。为什么那么多用户选择亚马逊云科技,为什么 620 亿美元的规模,增长率还能达到 37%?

云计算产业的问题和思考

"亚马逊云业务 2021 年总营收 622 亿美元,同比增长 37%。四季度营收 177 亿美元,同比增长 40%。全年营业利润 185 亿美元,营业利润率为 29%。"年初看到这条新闻,感叹这么大的营收规模,如此高的利润率,是否因为绑定客户,产品定价过高?

反观国内的云计算亏钱降价策略,这样难道不应该更能做大市场吗?

先说结论,亚马逊云科技采用智慧的产品定位和不同定价战略,聪明地为客户省钱,同时获得利润投入技术创新和全球化。前文是总体战略的增长飞轮,以下是产品角度的分解。

云产品的增长飞轮:

对比云计算厂商的增长刺轮(部分跟随者),刺轮需要额外推力才能保持运转,比如客户补贴,重投入客户关系,定制化等。这也许就是很多厂商持续亏损,份额却无法增长的原因。

云计算为客户创造价值

到目前为止云计算还没有颠覆性技术发明,还没有人讲清楚未来它的价值在哪,有多大?

顾凡引用了 McKinsey Quarterly 报告,"到 2030 年云计算为 500 强创造价值规模超一万亿美元,节约成本 4300 亿,创造价值 7700 亿"。成本节约大大降低客户自建 IT 软硬件成本,同时,利用云上更加易用的 AI 和大数据技术,为企业业务创造价值。

文章有点长,咱们用麦肯锡的金字塔思维框架,结论先行。

云产品用户粘性和竞争力有以下 3 个方面:

  1. 节约成本:重新定义IT采购模型,帮助客户优化成本,实现双赢
  2. 创造价值:围绕用户价值重构云底座,从芯片到数据全栈构建先机的计算、产品和服务
  3. 全球化:通过全球化网络和资源布局、本地化合规、数据安全,帮助客户全球化

后面主要从前两个方面分享我的思考。

本文先重点讨论 1 和 2。

1. 重新定义IT,实现成本节约

还是结论先行,亚马逊云科技为客户节约成本,不靠低价格,而是靠贴近用户 IT 模型的产品定义。比如按量付费的 Amazon EC2,承诺用量的节省计划,到灵活切分的容器,低价抢占的 Spot,无服务器的 Amazon Lambda,再到自动降级省钱的 Amazon S3。

比如 Amazon EC2 的按秒付费,比常规包年的价格虽然单价高 50%,如果你的应用 1 天内高负载时间低于 16 小时,按量付费就更省钱。聪明的产品不靠低价,而且双赢。

传统 IT 玩法

过去:IT 老兵都知道,这个领域过去的玩法。IT 软硬件产品市场操作复杂,有厂商-渠道-代理-集成商,代理还分总代/一代/二代。厂商还要建团队去管理渠道、管理价格体系、行业/区域/代理级别折扣体系都不一样,还要防窜货,厂商花费大把资金用来做渠道激励和管理。

从客户侧看,甲方企业还要养着一个强大的采购团队,议价、比价、入围、签单,中间环节复杂且不透明,甲方老板也不放心,还需配套后端廉政、合规、审计。

比如我们常见的企业级高端服务器、存储、网络和安全产品定价官网是查不到的,折扣范围巨大,最低可以到 1 折,也可以 5 折。议价能力弱的客户,就需要找多个渠道询价,各种供应商比较,各种手段压价。本来 1 个月可以下单到货,经过多伦博弈要折腾半年,然后多家组合集成,再用半年来交付。

其他玩家捆绑大法

云计算被形容为自来水,方便快捷,按需使用,按量计费。但是最早是没有水表的,按月收费,现在还有农村这么操作。结果喝水的嫌贵,卖水的亏钱,只有浇田的没意见。
可是中国的云计算过去的十几年就是这么干的。主要售卖方式就是包年包月。看起来折扣低,最终用户没有省钱。终于想通了,要改变模式,却发现弹性能力不够、售卖率不行...  增长飞轮别人无法无法模仿。

参见某云产品 3 年价格调价通知:

2. 亚马逊云科技从哪些产品方面为客户考虑?

1)持续降价的产品战略

客户选择云计算,综合评估 TCO、性能、可靠安全,体验和服务能力;

如果服务商用低价来抢单,要小心是否有陷阱,是否会以次充好?是否先进店后宰客?超低折扣能否长期持续?

亚马逊云科技产品数十次降价,当相对其他玩家来讲,不采用低价策略,而是双赢的产品设计策略,值得思考。

我们接下来重点分析营收主力产品。企业的 IT 开支 IaaS 一般占大头,其中最大的产品是计算、存储加起来一般超过 50%。我们来看看云厂商的产品策略。

2)越来越轻的产品架构

从应用架构上节约:

虚拟机 Amazon EC2--> 容器 Amazon ECS/EKS--> Amazon Fargate--> Amazon Lambda

从过去物理服务器的形态,到越来越细粒度的切分,最终实现仅调用时占用资源。资源开销可以更加贴近客户的IT负载曲线。减少浪费,帮客户省钱。

从定价模式上节约:

按量付费-->预留实例-->节省计划--Spot,主打按量付费的同时,给用户多种灵活选择。

3)坚持按量付费模式

云计算提供的是服务与传统 IT 最大的区别,就是按订阅收费。常见的软件订阅一般按照月或者年的方式。但亚马逊云服务更加激进,服务订阅按秒付费(最小 60 秒),不做长时间绑定,恰恰解决了客户 IT 峰值痛点问题。按照当前的定价比例,如果每天使用时间低于 16 小时,按量比包年更划算。

表-多种付费方式的差别

长期坚持按量计费,提供多种弹性方案和定价策略,培养市场对按量使用资源习惯,自己也孵化出 Serverless 产品。从本次演讲也可以看出,客户弹性用的非常普遍。每天新创建 6000 万个实例,按照每实例 4vcpu 估算,代表 300 万台服务器,云上超50% 应用负载采用弹性方式创建。

过去 IT 服务厂商硬件设备无法实现按量订阅,只能按峰值需求采购。但是国内其他云厂商为什么不跟随?为什么还主推这种模式(很多服务商希望用低价包 N 年方式绑定客户)

本质来看,付费包服务商需要预留资源、容量、性能,保证在需要弹性的时候,兑付成功率高。背后需付出巨大的成本来支持,预留充足的服务器资源池;还要建设规模足够大的可用区机房并支付成本。亚马逊云计算在美国本土的 Region 一般 3-4AZ,国内服务商 AZ 数量经常达到 2 位数。节约短期成本,却造成资源池碎片化,影响客户弹性扩容体验。

4)抢占式实例--共享资源新范式

B2B 共享资源的新范式,闲置资源拍卖

使用 Amazon EC2 Spot 实例,可以请求 Amazon EC2 备用计算容量,与按需实例的价格相比,这类实例最多可以节省 90% 的成本。

计算过去 30 天内与按需相比节省的费用

中断频率表示 Spot 在过去一个月间回收容量的比率。

过去按量售卖,客户峰谷会非常多,削峰填谷非常难调度,导致经常有 30-40% 的限制,采用 spot 方式,可以低价抢占剩余资源。

实际上有不少做算力的公司把这种空余资源低价买入,分发给需要计算的客户;比如并行计算类似玩家,二次售卖 HPC 业务。

5)手中无剑--开创无服务器新赛道

一个做云服务器起家的服务商,竟然开启了无服务器新赛道。顶级高手追求手中无剑,心中有剑的层次。

基础设施无论怎么按量、弹性的方式使用资源,让人有 scale-out 水位,一般应用工作水位 30%,启动扩容的水位是 50-70%,缩容更加谨慎,需要等实例内的最后一个客户服务结束。

开创性地发布产品 Amazon Lambda,完全按照业务负载付费,把资源占用和浪费压缩到 0。

Amazon Lambda 费用=计算时间+请求次数+内存+存储+并发。

咱们看一个官方案例,一个具备峰谷特点的外卖订餐系统;每月处理 300 万个请求。函数计算实现执行时间为 120 ms,每次内存 1536 MB。扣掉 100 万免费额度,一个月下来才 20 美元;

当然,Serverless 还不是万能的,仅支持脚本语言、启动慢(50ms+)、有调用限制。

6)自动省钱的 Amazon S3

Amazon S3 是对象存储,是亚马逊云科技营收最大的存储产品。从开发中角度,数据在产生之时,并不知道未来如何使用,未来算冷还是热数据。而 Amazon S3 存储提供了冷热自动分级,超过 90 天不用,可以自动降级;

我想问一个问题,这么主动帮客户省钱,PD 不用承担营收 KPI 吗?

设计一个产品尽量从客户那儿多收钱,这才是职业化的 PD 。

Amazon S3 不只是一个产品,而是一个系列,有多种热度和性能的存续范式。

Amazon S3 的产品价格随着用量增加,单价下降。按照我的期待,容量达到 PB 后应该继续降价。后面应该留给了销售去申请折扣。

美中不足的是,Amazon S3 的收费复杂度,由六大成本组成:存储定价、请求和数据检索定价、数据传输和传输加速定价、数据管理和分析定价、复制定价以及使用Amazon S3 Object Lambda 处理数据的价格。

如何选择趁手兵器--购买方式总结

大胆设想,目前大客户还是有额外折扣的,有额外议价空间,未来的产业互联网,按照采购量自动生成阶梯折扣,更加透明,期待有云厂商早日迈出这一步。

主动帮助客户省钱,这也许是敢于用按量收费,而且客户粘性越来越强的原因。

成本管理和优化工具的七种武器

好的产品设计要对财务友好,企业IT部门每年都有降本 KPI。亚马逊云科技有如此多的工具组合,功能强大,很多账单还可以导出 excel,二次架构处理。

丰富的工具包:

a.Amazon Pricing Calculator 在亚马逊云科技中国区域估算云使用成本
b.产品内置:购买界面内置多种选项建议,比如 Saving Plan,输入需求,自动给出建议。
c.云上财务管理 (CFM) 使组织能够调整其流程以实现最大的业务价值和财务成功,同时优化亚马逊云科技上的成本。
d.Amazon Cost Explorer 查看和分析您的成本和使用情况。该工具提供默认报告,可帮助您直观地查看成本和使用情况(例如账户,服务)或资源级别(例如 Amazon EC2 实例 ID)
e.Amazon Trusted Advisor,这是一种线上工具,可帮助您按照最佳实践在五个方面配置资源:成本优化、性能、安全性、容错和限制。建议定期使用 Amazon Trusted Advisor 以最佳方式维护您的解决方案。
f.Amazon Compute Optimizer 会为您的工作负载推荐最佳实例种类及規格大小,以降低成本并提高性能,并使用机器学习来分析历史利用率指标。过度配置资源会导致不必要的基础设施成本,而资源不足会导致应用程序性能不佳。帮助您根据您的利用率数据为三种类型的资源选择最佳配置:Amazon EC2 、Amazon EBS 、Amazon Lambda。
g.Amazon QuickSight 成本可视化方案

技术创新围绕客户价值

自底向上的基础技术创新-芯片

亚马逊云深耕客户 IT 多年,为云原生场景设计芯片,从减少虚拟化损耗开始,逐步接管客户通用负载、ML 负载:

Amazon Nitro:就是支持虚拟化的智能网卡,带火了 DPU 概念。快速创新,性能让利给客户
Amazon Graviton:基于 ARM 架构的 CPU,如今已经发展到可以在生态、性能方面赶超 x86的水平。
Amazon Trainum 和 Amazon Inferentia:分别是 ML 的训练和推理芯片,对比 NVIDIA GPU 性价比更高。

Amazon Nitro 加速应用性能

亚马逊云科技早在 2006 年就推出了 Amazon EC2 虚拟主机服务,早期可用的虚拟化技术只有 VMware 商业软件和开源的只有 Xen 技术,KVM 刚刚诞生还未成熟。

Xen 的虚拟化采用全虚拟化技术,也就是靠软件虚拟出内存、IO、外设等设备,CPU 性能损耗达 20-30%,主流云服务商都切换到了 KVM,2013 年收购了 Annapurna labs。还在忍受低效的老技术。被 VM 性能吊打两年后,终于在 2017 年发布了Amazon Nitro 和 Amazon Nitro hypervisor,实现优化虚拟化负载。

表-Amazon EC2 Virtualization Types

过去 KVM 软件虚拟化虽然实现了 SRIOV,但是 vNIC、vBlock 设备还是需要大量内核来处理设备 IO,与应用负载争抢 CPU 资源。负载的比例大概 10-20%。采用 Amazon Nitro 硬件虚拟化技术帮客户应用实现性能提升,同时意味着降低资源需求,降低成本:

Memcahed:Amazon Nitro 领先 9-26%
Nginx:领先 11-20%
MySQL:领先 6%

Amazon Nitro SSD 优化存储时延:从 0.08ms 降低到 0.02ms;数据库、ML 需要更低时延提升应用响应速度。过去应该采用商用方案,现在 Amazon Nitro 加速,实现 SSD 虚拟化,统一监控和管理,且降低时延。

Amazon EC2快速推出新实例

亚马逊云科技主要以服务形式提供给客户,后台技术迭代,客户不需要感知,一个产品系列类型可以收敛到 1-3 个形态。但计算产品不同,客户的应用会感知到 CPU 平台的差异,不同应用也有不同的需求,这导致 Amazon EC2 产品实例系列快速爆炸,比如 CPU 分为 Intel、AMD、ARM,内存分为1:2、1:4、1:8 和 1:16,加速器分为推理、训练、FPGA、媒体转码,此外还有 IO 增强,如网络增强、本地盘等形态。几种组合,每一代有数十种硬件架构形态。亚马逊云科技可以快速推出新实例,得益于Amazon Nitro 实现了虚拟化卸载。

自研 Amazon Graviton-高性能低价格

随着 Intel 这几年挤牙膏,服务器处理器的性价比提升放缓,尤其是能耗快速增长。亚马逊云科技推出 ARM 处理器 Amazon Graviton 2 和 3 系列。

最近分析 ARM 技术,推测应该采用的是 Nerverous V1 架构,代号 Zeus,比 N1 核性能提高 50%(实际 SVE 提升更大)

创新的云原生芯片设计理念:

  • 性能:单核内存带宽和时延
  • ML:BF16 和 INT8 方面遥遥领先
  • 能耗:功耗是 ARM 强项,效率比 x86 优 60%
  • 放弃:随着核心增加 NUMA 作为独木桥,越来越影响性能,干脆放弃NUMA
  • 场景:单 core 性能强劲,HPC、媒体转码场景具备优势;

Amazon Graviton 3 比 G2 增加 200 亿晶体管;但是处理器主频不增加,核数没变,推测战略定位如下:

  • 性能战略:选择了 V1 架构,意味着走了性能路线,主打 ML 和 HPC
  • 成本更高:Amazon Graviton 3 仅 64core,相比较而言行业其他处理器的核密度更高,Ampere AltraMax、Yitian、NVIDIA 分别是 128core 和 144core(2chips) 
  • 让利客户:Amazon G3 实例 C7g 定价只比上一代 Amazon G2 提高 5%;5nm 500 亿晶体管,比7nm 300 亿晶体管成本预计翻倍。

ML芯片赋能AI场景

  • 成本降低 70%
  • Alex 都已经转到 Inf1 实例
  • 成本降低 35%
  • 吞吐提升 2.3 倍

EFA 支持 SRD 协议,帮助客户优化 HPC、ML 并行能力,减少 TCP 通信开销占比。

云上创新技术赋能业务

AI 赋能业务

CTO 格言:每一行代码都是业务逻辑。从 OPPO 案例可以看到,采用云上 Amazon EC2-Inf1,快速构建语音助手的案例。

智能运维

亚马逊云科技利用自身电商积累的经验,通过亚马逊云科技云服务帮助客户运维。包括对事件的分析、响应进行的机器学习.比如 trusted advisor,为客户提供超过 5000 万智能运维推荐。根据 Amazon 电商积累的经验,输出给客户,此外还有 IEM 等工具。

数据价值

数据为客户创造这块儿也很精彩,可以让只懂 SQL 的人玩转 AI。先不做详述,我把回放链接放在文末,感兴趣可以了解下。

数据服务提供:

冷热数据快速恢复
数据备份、安全
Amazon S3-redshift,打通湖与仓
数据安全,审计策略
用 SQL 实现 ML 创新

思考和启发

因为价值所以选择

通过为客户节约成本,创造业务价值,让亚马逊云科技受到全球用户欢迎。另外,云计算消耗占半壁江山的互联网企业,告别内卷奔赴全球化是 2022 年主题,全球化需要强大的安全、合规特性产品能力,需要全球网络。因此国内企业出海纷纷选择亚马逊云科技。

根据统计报告,虽然在国内市场亚马逊云科技只占 6%,但加上中国企业出海份额占到了 26%,足以说明亚马逊云科技强大的竞争力和客户粘性。

亚马逊云科技的秘籍你能学会吗

IT 服务商存在的意义就是为客户创造价值。最后我提几个问题,你的企业文化真的是客户第一吗?今年营收 KPI 和客户粘性需要牺牲一个,你怎么选择?

破解了云计算增长之谜,亚马逊云科技的产品之道,你能学会吗?
(完)码字不易,请点赞支持
前文说过,很多地方本文没有探究,爱好技术的喜欢刨根问底,附一个回放入口自己去看。

JS自动生成24小时时间区间,时间跨度为60或30分钟

Posted: 07 Jun 2022 11:41 PM PDT

1、时间跨度为60分钟

(1)时间区间为字符串

有时候可能需要用到24小时的时间区间,跨度为60分钟,比如下面这样的:

['00:00 - 01:00', '01:00 - 02:00', '02:00 - 03:00', '03:00 - 04:00', '04:00 - 05:00', '05:00 - 06:00', '06:00 - 07:00', '07:00 - 08:00', '08:00 - 09:00', '09:00 - 10:00', '10:00 - 11:00', '11:00 - 12:00', '12:00 - 13:00', '13:00 - 14:00', '14:00 - 15:00', '15:00 - 16:00', '16:00 - 17:00', '17:00 - 18:00', '18:00 - 19:00', '19:00 - 20:00', '20:00 - 21:00', '21:00 - 22:00', '22:00 - 23:00', '23:00 - 24:00']

如果手动去写,则有点麻烦,这个时候可以使用一个简单的 JS 函数去自动生成,示例代码如下:

function generateTimes() {     let timeArrays = new Array(24).fill("");      timeArrays.forEach((item, index) => timeArrays[index] = (index < 10 ? '0' + index : index) + ':00' + ' - ' + ((index + 1) < 10 ? '0' + (index + 1) : (index + 1)) + ':00');      return timeArrays; }

当然,上面的方法,也可以简写成下面这样的,只需要一行代码即可

let timeArrays = new Array(24).fill('').map((item, index) => (index < 10 ? '0' + index : index) + ':00' + ' - ' + ((index + 1) < 10 ? '0' + (index + 1) : (index + 1)) + ':00');

(2)时间区间为数组

let timeArrays = new Array(24).fill(['', '']).map((item, index) => [(index < 10 ? '0' + index : index) + ':00', ((index + 1) < 10 ? '0' + (index + 1) : (index + 1)) + ':00']); console.log(JSON.stringify(timeArrays));

生成的时间区间如下:

[["00:00","01:00"],["01:00","02:00"],["02:00","03:00"],["03:00","04:00"],["04:00","05:00"],["05:00","06:00"],["06:00","07:00"],["07:00","08:00"],["08:00","09:00"],["09:00","10:00"],["10:00","11:00"],["11:00","12:00"],["12:00","13:00"],["13:00","14:00"],["14:00","15:00"],["15:00","16:00"],["16:00","17:00"],["17:00","18:00"],["18:00","19:00"],["19:00","20:00"],["20:00","21:00"],["21:00","22:00"],["22:00","23:00"],["23:00","24:00"]]

(3)时间区间为对象

let timeArrays = new Array(24).fill({}).map((item, index) => {     return {         start: (index < 10 ? '0' + index : index) + ':00',         end: ((index + 1) < 10 ? '0' + (index + 1) : (index + 1)) + ':00'     } }); console.log(JSON.stringify(timeArrays));

生成的时间区间如下:

[{"start":"00:00","end":"01:00"},{"start":"01:00","end":"02:00"},{"start":"02:00","end":"03:00"},{"start":"03:00","end":"04:00"},{"start":"04:00","end":"05:00"},{"start":"05:00","end":"06:00"},{"start":"06:00","end":"07:00"},{"start":"07:00","end":"08:00"},{"start":"08:00","end":"09:00"},{"start":"09:00","end":"10:00"},{"start":"10:00","end":"11:00"},{"start":"11:00","end":"12:00"},{"start":"12:00","end":"13:00"},{"start":"13:00","end":"14:00"},{"start":"14:00","end":"15:00"},{"start":"15:00","end":"16:00"},{"start":"16:00","end":"17:00"},{"start":"17:00","end":"18:00"},{"start":"18:00","end":"19:00"},{"start":"19:00","end":"20:00"},{"start":"20:00","end":"21:00"},{"start":"21:00","end":"22:00"},{"start":"22:00","end":"23:00"},{"start":"23:00","end":"24:00"}]

2、时间跨度为30分钟

如果时间跨度为30分钟,也就是说1天24小时,需要分成48个时间区间。

(1)时间区间为字符串

let timeArrays = new Array(48).fill('').map((item, index) => {     let startVal = index * 30;     let endVal = (index + 1) * 30;     let startHour = Math.floor((startVal / 60));     let startMinute = (startVal % 60);     let endHour = Math.floor((endVal / 60));     let endMinute = (endVal % 60);     let startTime = ((startHour < 10) ? ('0' + startHour) : startHour) + ':' + (startMinute === 0 ? '00' : startMinute);     let endTime = ((endHour < 10) ? ('0' + endHour) : endHour) + ':' + (endMinute === 0 ? '00' : endMinute);          return startTime + ' - ' + endTime; }); console.log(timeArrays);

生成的时间区间如下:

['00:00 - 00:30', '00:30 - 01:00', '01:00 - 01:30', '01:30 - 02:00', '02:00 - 02:30', '02:30 - 03:00', '03:00 - 03:30', '03:30 - 04:00', '04:00 - 04:30', '04:30 - 05:00', '05:00 - 05:30', '05:30 - 06:00', '06:00 - 06:30', '06:30 - 07:00', '07:00 - 07:30', '07:30 - 08:00', '08:00 - 08:30', '08:30 - 09:00', '09:00 - 09:30', '09:30 - 10:00', '10:00 - 10:30', '10:30 - 11:00', '11:00 - 11:30', '11:30 - 12:00', '12:00 - 12:30', '12:30 - 13:00', '13:00 - 13:30', '13:30 - 14:00', '14:00 - 14:30', '14:30 - 15:00', '15:00 - 15:30', '15:30 - 16:00', '16:00 - 16:30', '16:30 - 17:00', '17:00 - 17:30', '17:30 - 18:00', '18:00 - 18:30', '18:30 - 19:00', '19:00 - 19:30', '19:30 - 20:00', '20:00 - 20:30', '20:30 - 21:00', '21:00 - 21:30', '21:30 - 22:00', '22:00 - 22:30', '22:30 - 23:00', '23:00 - 23:30', '23:30 - 24:00']

(2)时间区间为数组

let timeArrays = new Array(48).fill(['', '']).map((item, index) => {     let startVal = index * 30;     let endVal = (index + 1) * 30;     let startHour = Math.floor((startVal / 60));     let startMinute = (startVal % 60);     let endHour = Math.floor((endVal / 60));     let endMinute = (endVal % 60);     let startTime = ((startHour < 10) ? ('0' + startHour) : startHour) + ':' + (startMinute === 0 ? '00' : startMinute);     let endTime = ((endHour < 10) ? ('0' + endHour) : endHour) + ':' + (endMinute === 0 ? '00' : endMinute);          return [startTime, endTime]; }); console.log(JSON.stringify(timeArrays));

生成的时间区间如下:

[["00:00","00:30"],["00:30","01:00"],["01:00","01:30"],["01:30","02:00"],["02:00","02:30"],["02:30","03:00"],["03:00","03:30"],["03:30","04:00"],["04:00","04:30"],["04:30","05:00"],["05:00","05:30"],["05:30","06:00"],["06:00","06:30"],["06:30","07:00"],["07:00","07:30"],["07:30","08:00"],["08:00","08:30"],["08:30","09:00"],["09:00","09:30"],["09:30","10:00"],["10:00","10:30"],["10:30","11:00"],["11:00","11:30"],["11:30","12:00"],["12:00","12:30"],["12:30","13:00"],["13:00","13:30"],["13:30","14:00"],["14:00","14:30"],["14:30","15:00"],["15:00","15:30"],["15:30","16:00"],["16:00","16:30"],["16:30","17:00"],["17:00","17:30"],["17:30","18:00"],["18:00","18:30"],["18:30","19:00"],["19:00","19:30"],["19:30","20:00"],["20:00","20:30"],["20:30","21:00"],["21:00","21:30"],["21:30","22:00"],["22:00","22:30"],["22:30","23:00"],["23:00","23:30"],["23:30","24:00"]]

(3)时间区间为对象

let timeArrays = new Array(48).fill(['', '']).map((item, index) => {     let startVal = index * 30;     let endVal = (index + 1) * 30;     let startHour = Math.floor((startVal / 60));     let startMinute = (startVal % 60);     let endHour = Math.floor((endVal / 60));     let endMinute = (endVal % 60);     let startTime = ((startHour < 10) ? ('0' + startHour) : startHour) + ':' + (startMinute === 0 ? '00' : startMinute);     let endTime = ((endHour < 10) ? ('0' + endHour) : endHour) + ':' + (endMinute === 0 ? '00' : endMinute);          return {         start: startTime,         end: endTime     }; }); console.log(JSON.stringify(timeArrays));

生成的时间区间如下:

[{"start":"00:00","end":"00:30"},{"start":"00:30","end":"01:00"},{"start":"01:00","end":"01:30"},{"start":"01:30","end":"02:00"},{"start":"02:00","end":"02:30"},{"start":"02:30","end":"03:00"},{"start":"03:00","end":"03:30"},{"start":"03:30","end":"04:00"},{"start":"04:00","end":"04:30"},{"start":"04:30","end":"05:00"},{"start":"05:00","end":"05:30"},{"start":"05:30","end":"06:00"},{"start":"06:00","end":"06:30"},{"start":"06:30","end":"07:00"},{"start":"07:00","end":"07:30"},{"start":"07:30","end":"08:00"},{"start":"08:00","end":"08:30"},{"start":"08:30","end":"09:00"},{"start":"09:00","end":"09:30"},{"start":"09:30","end":"10:00"},{"start":"10:00","end":"10:30"},{"start":"10:30","end":"11:00"},{"start":"11:00","end":"11:30"},{"start":"11:30","end":"12:00"},{"start":"12:00","end":"12:30"},{"start":"12:30","end":"13:00"},{"start":"13:00","end":"13:30"},{"start":"13:30","end":"14:00"},{"start":"14:00","end":"14:30"},{"start":"14:30","end":"15:00"},{"start":"15:00","end":"15:30"},{"start":"15:30","end":"16:00"},{"start":"16:00","end":"16:30"},{"start":"16:30","end":"17:00"},{"start":"17:00","end":"17:30"},{"start":"17:30","end":"18:00"},{"start":"18:00","end":"18:30"},{"start":"18:30","end":"19:00"},{"start":"19:00","end":"19:30"},{"start":"19:30","end":"20:00"},{"start":"20:00","end":"20:30"},{"start":"20:30","end":"21:00"},{"start":"21:00","end":"21:30"},{"start":"21:30","end":"22:00"},{"start":"22:00","end":"22:30"},{"start":"22:30","end":"23:00"},{"start":"23:00","end":"23:30"},{"start":"23:30","end":"24:00"}]

3、时间跨度任意指定

除了常见的时间跨度为60分钟或者30分钟,有的时候还可能需要其他的时间跨度,那么是否可能写一个相对通用的方法,参数为时间跨度(以分钟为单位),当然是可以的,具体实现代码如下(这里仅生成时间区间为字符串的,其他格式参考上面):

function generateTimes(step) {     let size = Math.floor(24 * 60 / step);     let timeArrays = new Array(size).fill('').map((item, index) => {         let startVal = index * step;         let endVal = (index + 1) * step;         let startHour = Math.floor((startVal / 60));         let startMinute = (startVal % 60);         let endHour = Math.floor((endVal / 60));         let endMinute = (endVal % 60);         let startTime = ((startHour < 10) ? ('0' + startHour) : startHour) + ':' + (startMinute === 0 ? '00' : startMinute);         let endTime = ((endHour < 10) ? ('0' + endHour) : endHour) + ':' + (endMinute === 0 ? '00' : endMinute);              return startTime + ' - ' + endTime;     });      return timeArrays; }

比如想要生成时间跨度为120分钟的时间区间,可以直接传入120即可

console.log(generateTimes(120));

生成的时间区间如下:

['00:00 - 02:00', '02:00 - 04:00', '04:00 - 06:00', '06:00 - 08:00', '08:00 - 10:00', '10:00 - 12:00', '12:00 - 14:00', '14:00 - 16:00', '16:00 - 18:00', '18:00 - 20:00', '20:00 - 22:00', '22:00 - 24:00']
需要注意的是,如果时间跨度无法被整除,那么生成的时间区间可能无法完全覆盖24小时。

SegmentFault 思否技术周刊 -- Node.js 进阶之旅,看看那些还需要学?

Posted: 07 Jun 2022 08:20 PM PDT

简单的说 Node.js 就是运行在服务端的 JavaScript。
Node.js 是一个基于 Chrome JavaScript 运行时建立的一个平台。
Node.js 是一个事件驱动 I/O 服务端 JavaScript 环境,基于 Google 的 V8 引擎,V8 引擎执行 Javascript 的速度非常快,性能非常好。

文章推荐

《Node.js 如何实现异步资源上下文共享?》

本文使用尽量简单的方式介绍 Node.js 如何实现异步资源上下文共享,以及异步资源上下文共享对我们来说有什么用。

《锁定项目 Node 版本和包管理器》

成员机器 Node.js 版本不统一: 守旧派用 12.x、保守用 14.x、激进用 17.x。项目能否正常跑起来全凭天意,在没有 CICD 流水线加持本地 npm run build 的场景下线上风险可想而知。
有人习惯用 npm、有人习惯用 yarn, 代码库里面经常会存在 package-lock.json、yarn.lock 文件同时存在的情况。更痛的点还是各种奇奇怪怪问题排查起来没有头绪。
我们要做的就是将问题掐死在源头:锁定 Node.js 版本和包管理器

《使用 node.js 创建子进程并使用 WebSocket 和 Ipc 实现进程间通信》

本文主要记录、总结:
node.js子进程的创建
fork和spawn的区别与联系
Ipc实现进程间通信
WebSocket实现进程间通信

《node 异常数据响应排查(pm2 Cluster Mode、异步)》

不启动 Cluster 集群模式:
因为本地是非 Cluster 集群模式,所以表现正常。那么第一个解决办法就是生产环境也不开启集群模式,但是一般来说这个方案是不可取的,生产环境的请求比较高,集群模式才是最优解法。

增加单实例的数据服务 | 降为单实例模式:
类似于 redis ,只不过是新建一个单实例的 nodeJs 脚本。获取数据&更新数据都是请求这个脚本服务。
因为不使用集群模式所以也就不存在共享问题了。同时也避免了上一个解法的问题,因为数据服务不对外开放,只给内网的服务开通,所以请求量级不会太大。

《【nodejs进阶之旅(2)】:使用koa2+mysql 实现列表数据分页》

分页主要字段包括 pageSize 每页条数、pageNum 第几页、startRow 当前开始页编号、endRow 当前结束页编号、total 总数量。主要是根据前端分页的参数,进行处理后,返回前端正确的数据,其实是一个很常见且简单的功能,但也是非常重要的。

《node+express 构建 web 服务器部署前端项目》

传统的前端项目部署依赖于tomcat服务做静态资源服务器、随着前后端分离的进程化、前端项目需要单独部署。对于前端部署我们除了采用nginx搭建静态资源服务器外,还可以使用node来搭建web服务器。下面跟大家分享下如何使用node搭建web服务器。

《2022 年用于 Node.js 的顶级 WebSocket 库》

在这篇文章中,我们将讨论2022年你应该考虑的八个Node.js WebSocket库。
SockJS
ws
Socket.IO
Faye WebSocket
SocketCluster
Sockette
Feathers

《使用 node-config 在 Node.js 中创建配置文件》

管理跨不同环境的多个配置文件可能具有挑战性,并且有多种工具正试图用不同的方法解决这个问题。但是,在本文中,我们将学习如何使用 node-config 跨不同部署环境创建和管理 Node.js 配置文件。

《前端架构师破局技能,NodeJS 落地 WebSocket 实践》

本文从网络协议,技术背景,安全和生产应用的方向,详细介绍 WebSocket 在 Node.js 中的落地实践。

本文介绍的内容包括以下方面:
网络协议进化
Socket.IO?
ws 模块实现
Express 集成
WebSocket 实例
消息广播
安全与认证
BFF 应用

《如何安全地执行用户的自定义 nodejs 脚本》

本文将介绍在业务开发过程中,遇到需要执行用户自定义 nodejs 脚本的场景,该如何安全地执行用户的神秘代码。

《前端Node.js面试题》

Node基础概念
Node 全部对象
谈谈对process的理解
谈谈对fs模块的理解
谈谈对Stream的理解
事件循环机制
EventEmitter
中间件
如何设计并实现JWT鉴权
Node性能监控与优化

热门问答

课程推荐

《Node.js 高级实战 · 手把手带你搭建动漫网站》

课程收获:

  • 异步编程和 HTTP 编程的基础
  • 最流行的 Koa2 框架的用法
  • 数据库访问技术,能够让你轻松的,编写各种 SQL 查询
  • 各种常见的 Web 编程技术,让你能快速的开发出,功能完备的 Web 服务
  • 完整的项目流程和最佳工程实践,可以让你跟老司机之间,项目经验的差距大大缩短
  • 大量的、常用的、中间件的用法和第三方组件的用法
  • 大大提高你的开发效率,提升你的代码质量
  • 组件化开发的思想以及高质量编程的工具和方法
  • 向架构工程师的职位发展,打下基础

适用人群:

  • 对于有一些前端开发经验的工程师,通过课程的学习,可以掌握后端开发的技术,这样就可以打通,前端开发和后端开发的任督二脉,让你成为全栈工程师,扩大、扩宽,自己的技能领域。
  • 对于项目经验比较少的工程师,通过课程的学习,可以掌握项目流程和最佳工程实践,让你能更快的、更好的融入,团队协作,让你在项目团队中,发挥重要的作用。

PS:大家想看哪些方面的技术内容,可以在评论区留言喔 ~
如有问题可以添加小姐姐微信~
image.png

十年老友记 | @边城:恰当的编程是会产生幸福感的

Posted: 31 May 2022 07:59 PM PDT

任何一件事情,只要能沉浸进去,去感受其中的酸甜苦辣,总会从好奇到喜欢,从喜欢到厌烦,从厌烦到习惯 …… ——边城

十年前的今天,SegmentFault 思否正式创立,如一颗嫩绿的幼芽开始成长,期间承载过和煦的日光、沐浴过柔和的春风,也挑战过滂沱的暴雨、体会过凛冽的冬雪。所幸,今日呈现在我们面前的 SegmentFault 思否,已经长成了一棵足以抵御一些风雨的大树,这样的成长离不开各位管理员的修剪,更离不开社区每一位用户的栽种。

正如 SegmentFault 思否创始人之一祁宁所言:

"SegmentFault 思否是一个属于大家的社区,因此,在这个特殊的时刻,我们想跟社区的成员一起为它喝彩。"

这十年中,有成千上万的开发者加入了 SegmentFault 社区,我们雀跃于看到每张不同的新面孔,也感动于社区里的那些老朋友们多年如一日的陪伴,见到他们就像见到一位相识多年的老友。或许老友们会在某一段时间里突然消失,但消失并不代表着再也不见,而是重逢后的那句:好久不见。


今天,我们有幸邀请到老朋友 @边城 参与我们的十年老友记系列访谈。

小编有话说:

在我加入思否后,经常能在社区中见到边城老师,他会在一段时间里突然沉默,又在某个时刻突然归来,频繁产出高质量高赞的文章。

采访中印象很深的一段是,他说思否已经不是当年的思否了,而他还是当年那个他。或许他一直觉得自己停留在原地,但他的 2355 篇问答和 107 篇文章,都是他一直在前进着的证明,而社区,就是最好的见证者。

他说,编程会带给他幸福感,同时,也会带给他一些焦虑。但在他的字里行间,都在传达着一个信息:编程带给他的快乐远远大过编程带给他的负面情绪。

我想,能从事自己热爱的职业并为之奋斗,是一件再治愈不过的事情。

以下为 SegmentFault 思否对边城的访谈内容:

1、还记得和 SegmentFault 思否的初识吗?是在什么样的机缘巧合下踏入这个社区的?
 
要说怎么认识 SegmentFault 的,还真不记得了。去查了一下,回答第一个问题的时间早于写第一篇博客的时间,所以肯定是通过问题认识的 SegmentFault。
 
2、这些年有没有见证思否的改变?其中对你而言触动最大的是哪一部分?与思否一起成长的路上,你觉得自己改变了什么?
 
思否一直未变,又一直在变。对于我来说,思否有最重要的两个功能:问答和博客。这两个功能一直是思否的核心功能,从未改变过。而变化其实也很多,比如在某一天加入了"资讯"频道,又在某一天引入了圈子,还在某一天创办了思否讲堂(思否编程)…… 这些年来,思否尝试过很多东西,有的被越来越多的人接受,也有一些逐渐淡出了视野。思否不忘初心,坚持创新,勇于接受社区提出的各种挑战(各种需求),这大概就是触动我的地方。但要说到一起成长,我有些惭愧,思否已经不是当年的思否,而我还是当年那个我。我自己没感觉到自己的进步,但是我觉得思否应该感觉到了 ^_^。
 
3、为什么会选择做一名程序员?因为梦想和因为现实这两者的占比哪个更重?
 
选择做一名程序员,很偶然,因为和梦想和现实都没有太大的关系。简单地说,就是由于机缘巧合接触到这个行业,开始编程,然后就懒得再去学习别的东西,于是就在这条路上一直走了下去。但是任何一件事情,只要能沉浸进去,去感受其中的酸甜苦辣,总会从好奇到喜欢,从喜欢到厌烦,从厌烦到习惯 ……
 
4、如果有一天因为种种因素你决定放弃编程,你想去做什么?
 
如果有一天,我决定放弃编程,大概还会在应用软件行业吧。但确实常有幻想 —— 在大学旁边开一家小小的咖啡馆,听着同学们讨论程序的问题,然后插上两句话,刷下存在感,哈哈!
 
5、程序员的形象在很多人的心目中离不开格子衬衫、黑框眼镜、双肩背包等物品,你想对这种刻板印象说什么?
 
我想对这种刻板的印象说……没毛病。程序员通常具有良好的逻辑思维能力,即使没有学过经济学会也会很容易做到一些符合经济学规律的事情,比如格子衬衫,耐脏;黑框眼镜,便宜;双肩背包,功能性强!但是话说回来,一般环境下确实如此,但这不代表程序员不会生活,程序员改变生活态度,只需要一个能改变其价值观的机缘,一个对的人!
 
6、编程对你而言只是工作任务吗?如果不是的话,它对你有何种特殊的意义呢?
 
如果一件事情,做完之后能够感觉到它的价值,就会产生劳动的喜悦和自豪感;相反,如果事情做完连自己都嫌弃,是不会产生幸福感的。编程对我而言,是工作的一部分,也是我兴趣的一部分。就好像有时候我会在思否上刷一些不太难回答的问题,对我而言是一种休息,但同时对提问者是一种帮助,是一件双赢的事情。
 
7、工作中有没有让你焦虑的事?这种焦虑源于何处?面对焦虑你一般会怎么做?
 
焦虑肯定是有的。就像刚才说的,适当的编程是会产生幸福感的。然而工作有时候需要为成本、时限这些东西让步,要在自己所认为的价值和工作目的产生的价值之间找平衡,做抉择,是非常容易产生焦虑的。而且人到中年,各方面的压力扑面而来,要说不焦虑那是骗人。面对焦虑,有时候就只想放空自己,找点有幸福感的事情来做,哪怕偷得浮生半日闲也是好的。
 
8、年龄对程序员这个职业有一定的影响,你认同这个观点吗?有没有想过自己未来的职业规划?
 
年龄对程序员这个职业肯定有一定的影响。但是这个影响到底有多大,取决于"程序员"的定义。我们通常所说的程序员,其实范围很广,从 Coder 到架构师都可以被称为程序员。通常我们会认为 Coder 只需要高中水平就可以胜任,工作两年就可以做到非常熟练,所以一个 25 岁的 Coder 跟一个 20 岁的 Coder 相比,并没有明显的优势,更不要说现在只需要一句注释 AI 都能写代码。但作为架构师来说,一个 25 岁的架构师甚至可能还没有形成成熟的架构思想。对自己的职业规划,我确实没要考虑太多,因为我自己也还迷茫。
 
9、请留下你对 SegmentFault 思否社区十岁生日的祝福
 
十年不易,祝贺思否社区十岁生日。思否十年,从一个名字,发展成了一个生态。希望思否的生态在接下来的年月中越加枝繁叶茂。


边城老师说他对自己的职业规划还很迷茫,但我想说的是,我们每个人都是在迷茫中慢慢摸索前进的,只要不停在原地,我相信未来的光总会照亮这段暂时昏黑的道路。

希望在下个十年,我们再次相遇的时候,都会感谢现在步履不停的自己。

文字轮播与图片轮播?CSS 不在话下

Posted: 06 Jun 2022 07:30 PM PDT

今天,分享一个实际业务中能够用得上的动画技巧。

巧用逐帧动画,配合补间动画实现一个无限循环的轮播效果,像是这样:

看到上述示意图,有同学不禁会发问,这不是个非常简单的位移动画么?

我们来简单分析分析,从表面上看,确实好像只有元素的 transform: translate() 在位移,但是注意,这里有两个难点:

  1. 这是个无限轮播的效果,我们的动画需要支持任意多个元素的无限轮播切换
  2. 因为是轮播,所以,运行到最后一个的时候,需要动画切到第一个元素

到这里,你可以暂停思考一下,如果有 20 个元素,需要进行类似的无限轮播播报,使用 CSS 实现,你会怎么去做呢?

逐帧动画控制整体切换

首先,我需要利用到逐帧动画效果,也被称为步骤缓动函数,利用的是 animation-timing-function 中,的 steps,语法如下:

{     /* Keyword values */     animation-timing-function: step-start;     animation-timing-function: step-end;     /* Function values */     animation-timing-function: steps(6, start)     animation-timing-function: steps(4, end); }

如果你对 steps 的语法还不是特别了解,强烈建议你先看看我的这篇文章 -- 深入浅出 CSS 动画,它对理解本文起着至关重要的作用。

好的,还是文章以开头的例子,假设我们存在这样 HTML 结构:

<div class="g-container">   <ul>     <li>Lorem ipsum 1111111</li>     <li>Lorem ipsum 2222222</li>     <li>Lorem ipsum 3333333</li>     <li>Lorem ipsum 4444444</li>     <li>Lorem ipsum 5555555</li>     <li>Lorem ipsum 6666666</li>   </ul> </div>

首先,我们实现这样一个简单的布局:

在这里,要实现轮播效果,并且是任意个数,我们可以借助 animation-timing-function: steps()

:root {   // 轮播的个数   --s: 6;   // 单个 li 容器的高度   --h: 36;   // 单次动画的时长   --speed: 1.5s; } .g-container {   width: 300px;   height: calc(var(--h) * 1px); } ul {   display: flex;   flex-direction: column;   animation: move calc(var(--speed) * var(--s)) steps(var(--s)) infinite; } ul li {   width: 100%; } @keyframes move {   0% {     transform: translate(0, 0);   }   100% {     transform: translate(0, calc(var(--s) * var(--h) * -1px));   } }

别看到上述有几个 CSS 变量就慌了,其实很好理解:

  1. calc(var(--speed) * var(--s)):单次动画的耗时 * 轮播的个数,也就是总动画时长
  2. steps(var(--s)) 就是逐帧动画的帧数,这里也就是 steps(6),很好理解
  3. calc(var(--s) * var(--h) * -1px)) 单个 li 容器的高度 * 轮播的个数,其实就是 ul 的总体高度,用于设置逐帧动画的终点值

上述的效果,实际如下:

如果给容器添加上 overflow: hidden,就是这样的效果:

这样,我们就得到了整体的结构,至少,整个效果是循环的。

但是由于只是逐帧动画,所以只能看到切换,但是每一帧之间,没有过渡动画效果。所以,接下来,我们还得引入补间动画。

利用补间动画实现两组数据间的切换

我们需要利用补间动画,实现动态的切换效果。

这一步,其实也非常简单,我们要做的,就是将一组数据,利用 transform,从状态 A 位移到 状态 B。

单独拿出一个来演示的话,大致的代码如下:

<div class="g-container">   <ul style="--s: 6">     <li>Lorem ipsum 1111111</li>     <li>Lorem ipsum 2222222</li>     <li>Lorem ipsum 3333333</li>     <li>Lorem ipsum 4444444</li>     <li>Lorem ipsum 5555555</li>     <li>Lorem ipsum 6666666</li>   </ul> </div>
:root {   --h: 36;   --speed: 1.2s; } ul li {   height: 36px;   animation: liMove calc(var(--speed)) infinite; } @keyframes liMove {   0% {     transform: translate(0, 0);   }   80%,   100%  {     transform: translate(0, -36px);   } }

非常简单的一个动画:

bgg1

基于上述效果,我们如果把一开始提到的 逐帧动画 和这里这个 补间动画 结合一下,ul 的整体移动,和 li 的 单个移动叠在在一起:

:root {   // 轮播的个数   --s: 6;   // 单个 li 容器的高度   --h: 36;   // 单次动画的时长   --speed: 1.5s; } .g-container {   width: 300px;   height: calc(var(--h) * 1px); } ul {   display: flex;   flex-direction: column;   animation: move calc(var(--speed) * var(--s)) steps(var(--s)) infinite; } ul li {   width: 100%;   animation: liMove calc(var(--speed)) infinite; } @keyframes move {   0% {     transform: translate(0, 0);   }   100% {     transform: translate(0, calc(var(--s) * var(--h) * -1px));   } } @keyframes liMove {   0% {     transform: translate(0, 0);   }   80%,   100%  {     transform: translate(0, calc(var(--h) * -1px));   } }

就能得到这样一个效果:

Wow,神奇的化学反应产生了!基于 逐帧动画补间动画 的结合,我们几乎实现了一个轮播效果。

当然,有一点瑕疵,可以看到,最后一组数据,是从第六组数据 transform 移动向了一组空数据:

末尾填充头部第一组数据

实际开发过轮播的同学肯定知道,这里,其实也很好处理,我们只需要在末尾,补一组头部的第一个数据即可:

改造下我们的 HTML:

<div class="g-container">   <ul>     <li>Lorem ipsum 1111111</li>     <li>Lorem ipsum 2222222</li>     <li>Lorem ipsum 3333333</li>     <li>Lorem ipsum 4444444</li>     <li>Lorem ipsum 5555555</li>     <li>Lorem ipsum 6666666</li>     <!--末尾补一个首条数据-->     <li>Lorem ipsum 1111111</li>   </ul> </div>

这样,我们再看看效果:

Beautiful!如果你还有所疑惑,我们给容器加上 overflow: hidden,实际效果如下,通过额外添加的最后一组数据,我们的整个动画刚好完美的衔接上,一个完美的轮播效果:

完整的代码,你可以戳这里:CodePen Demo -- Vertical Infinity Loop

横向无限轮播

当然,实现了竖直方向的轮播,横向的效果也是一样的。

并且,我们可以通过在 HTML 结构中,通过 style 内填写 CSS 变量值,传入实际的 li 个数,以达到根据不同 li 个数适配不同动画:

<div class="g-container">   <ul style="--s: 6">     <li>Lorem ipsum 1111111</li>     <li>Lorem ipsum 2222222</li>     <li>Lorem ipsum 3333333</li>     <li>Lorem ipsum 4444444</li>     <li>Lorem ipsum 5555555</li>     <li>Lorem ipsum 6666666</li>     <!--末尾补一个首尾数据-->     <li>Lorem ipsum 1111111</li>   </ul> </div>

整个动画的 CSS 代码基本是一致的,我们只需要改变两个动画的 transform 值,从竖直位移,改成水平位移即可:

:root {   --w: 300;   --speed: 1.5s; } .g-container {   width: calc(--w * 1px);   overflow: hidden; } ul {   display: flex;   flex-wrap: nowrap;    animation: move calc(var(--speed) * var(--s)) steps(var(--s)) infinite; } ul li {   flex-shrink: 0;   width: 100%;   height: 100%;   animation: liMove calc(var(--speed)) infinite; } @keyframes move {   0% {     transform: translate(0, 0);   }   100% {     transform: translate(calc(var(--s) * var(--w) * -1px), 0);   } } @keyframes liMove {   0% {     transform: translate(0, 0);   }   80%,   100%  {     transform: translate(calc(var(--w) * -1px), 0);   } }

这样,我们就轻松的转化为了横向的效果:

完整的代码,你可以戳这里:CodePen Demo -- Horizontal Infinity Loop

轮播图?不在话下

OK,上面的只是文字版的轮播,那如果是图片呢?

没问题,方法都是一样的。基于上述的代码,我们可以轻松地将它修改一下后得到图片版的轮播效果。

代码都是一样的,就不再列出来,直接看看效果:

完整的代码,你可以戳这里:CodePen Demo -- Horizontal Image Infinity Loop

掌握了这个技巧之后,你可以将它运用在非常多只需要简化版的轮播效果之上。

再简单总结一下,非常有意思的技巧:

  1. 利用 逐帧动画,实现整体的轮播的循环效果
  2. 利用 补间动画,实现具体的 状态A状态B* 的动画效果
  3. 逐帧动画 配合 补间动画 构成整体轮播的效果
  4. 通过向 HTML 结构末尾补充一组头部数据,实现整体动画的衔接
  5. 通过 HTML 元素的 style 标签,利用 CSS 变量,填入实际的参与循环的 DOM 个数,可以实现 JavaScript 与 CSS 的打通

最后

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

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

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

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

2022 前端开发报告:TypeScript 成 84% Web 开发者的“最爱”|无障碍性、边缘渲染成大趋势

Posted: 05 Jun 2022 09:30 PM PDT

近日,The Software House发布了一份"2022 前端开发市场状态调查报告"。

此次报告,共有来自全球 125 个国家 / 地区、超 3700+ 名前端开发专业人士填写了调查,同时也融合了来自前端技术开发领域的 19 位专家的观点与分享,并对前端 2020 年和 2022 年的数据并排呈现作对比后最终得出了一份调查结论。

调查结果显示,高达 56% 的受访者正在进行远程工作,其中仅 5% 在办公室工作。大规模远程工作的概念确实比较新颖,以至于 2020 年的调查甚至都没有对这个数据进行调查。

看得出,大多数工程师显然更喜欢远程工作,因为不需要通勤,不用面对随时随刻有人在肩膀上轻拍你而分散注意力的尴尬等等。然而,远程办公状态下,实时分享信息、复制群组消息及自发讨论等问题仍然是一项挑战。

前端开发较易"入门"

此次调查结果中,发现了一个非常有意思的现象:做前端开发的人员,他自身并非仅仅是个前端工程师。据数据显示,在"其他"选项中共享的一些从事前端开发的职位包括:

  • 一个刚开始学习 frontend 的训练班学生,
  • 一位在非技术大学学习的自学成才的开发人员爱上了 frontend,
  • 有时将代码推向生产的产品经理,
  • 开发人员倡导者,时不时帮助前端团队,
  • 前端开发架构师,
  • 设计系统负责人,
  • 一位同时会编码的设计师,
  • 平面设计师和开发人员,
  • 全局负责人:单人负责的开发者商店,自己包揽一切事情,包括前端开发。

虽然这个发现似乎有点不足为奇,但这可以很好地表明:前端技术领域是一个容易进入的领域,即使你此前并没有太多前端背景,但快速学习之后依然能参与进来。

开发者在更大的前端团队工作成常态

据调查数据显示,有 27% 的受访者表示在一家拥有 50 多名前端工程师的公司工作。与此同时,30% 的开发者分享了 5 个或更少的前端开发者在他们公司的工作方式。50% 的受访者在拥有 10 名或以上前端工程师的公司工作。

这个统计数据也显示了一个有趣的现象:在有着大量前端团队公司工作的前端工程师,与在少数人团队或单独工作的公司的工程师数量几乎一样多。当然,这些公司的开发人员经验和期望大不相同:大公司将更多地拥有开发人员经验和前端平台团队。导师制更为常见;在较小的公司里,每个开发人员的责任更大,获得反馈的选项也会更少。

82% 的前端工程师来自科技型公司

数据还显示,有 82% 的受访者被认定为在软件开发公司、开发机构或技术为主或数字为主公司工作。另外,仅 18% 的受访者表示他们在非科技型公司工作。

其中,来自软件开发公司/开发机构的工程师占比为 41.6%,技术为主/数字为主型公司的工程师占比为 41.2%。非技术型公司工程师占比为 12.3%,另外 2.9% 的工程师则来其他领域,1.9% 的工程师来自政府机构。

63% 的开发者关注前端的"无障碍性"

根据调查数据,前端开发的"无障碍性"是今年受访者们普遍关注的重点:有 63% 的人预测它在未来几年会越来越受欢迎。而框架则正倾向于提供不同的方法来解决这个问题,其中就包括 Next/Nuxt Image、HTML validator 和 WebHint。

同时,组件驱动的开发也受到了大多数开发人员的欢迎,考虑到 React、Vue、Svelte 甚至 Web 组件的流行(如今年的独立成功案例——Wordle),这一点很有意义。

渐进式 Web 应用程序也越来越受欢迎,开发人员渴望使用相同的核心代码库充分利用跨平台开发。另外,Headless CMS (无头 CMS)也在不断进步,采用率越来越高,并更多地集成到框架中。

前端"边缘渲染"方案将成大趋势

通过对 2020 年的调查数据与 2022 年今年的调查数据相比对之后,还发现了一个重要的趋势:前端性能优化的方案 —— 边缘渲染。

边缘渲染最初由 CloudFlare 及其 worker 平台驱动。此次调查的期间,大多数部署目标都发布或实现了自己的无服务器或边缘功能,用户很快就会采用这些功能,因此这一趋势并非偶然。

据悉,Nuxt 3、Remix 或 Sveltekit 等框架正朝着这个方向发展,直接在 CDN 级别支持按需渲染。随着服务器呈现的应用程序在减少延迟和降低成本方面的相应收益,由此可以预测这将是 2023 年的一大焦点。

前端开发者正从"Moment.js"转向"Date-FNS"

此次调查结果还显示,在日期处理类库方面,如今的前端开发者们正在从"Moment.js"转向"Date-FNS"。

同时,超过 40% 的人仍然在他们的项目中使用 Moment,尽管该库已经失去了支持,甚至其官方网站上也有创作者留言说"如果你正在考虑使用 Moment,你可能应该寻找替代品"。仅 5% 的受访者希望继续使用该库,看来 Moment 确实正走向衰落。

调查结果中,Axios 网络请求库以超过 60% 的高"得票率",进入了稳定阶段。该库在前端市场已经有很长一段时间了,人们对此很清楚,它更像是一种"标准"而非"趋势"。

另外,由于 Apollo 用于与 GraphQL 的无缝连接,因此它在"使用过的和喜欢的"类别中得票也较高:40% 的开发人员希望在未来学习 Apollo ,这意味着 Apollo 社区正在稳步增长。

TypeScript 成 84% Web 开发者的"最爱"

据调查数据显示,相比 2030 年, 2022 年也就是今年使用 TypeScript 的人数上升了 7 个百分点以上,已经达到了惊人的 84%!


看来大家都知道,TypeScript 如今已受到了广大开发人员的普遍欢迎,人们经常称赞"TypeScript 如何在 bug 发生之前就阻止了一整类 bug",这反过来又使得开发速度更快,应用程序更可靠。

那么,什么让这么多开发人员喜欢 TypeScript?

在经历了多年的 Web 开发之后,前端开发人员早就不想重复多次在代码编辑器和浏览器之间来回切换的经历,不用再猜测为什么"未定义不是功能"。

所以,TypeScript 不仅赢得了开发人员的心,而且还努力成为前端行业标准,它让 web 开发方式变得不再像以前那么让人沮丧了。

关于本次调查结果的更多详情,可查看完整报告。
参考链接:https://tsh.io/state-of-front...

一文搞懂四种 WebSocket 使用方式,建议收藏!!!

Posted: 06 Jun 2022 01:19 AM PDT

在上家公司做IM消息系统的时候,一直是使用 WebSocket 作为收发消息的基础组件,今天就和大家聊聊在 Java 中,使用 WebSocket 所常见的四种姿势,如果大家以后或者现在碰到有要使用 WebSoocket 的情况可以做个参考。

image.png

上面的思维导图已经给大家列出了三种使用 WebSocket 的方式,下文会对它们的特点进行一一解读,不同的方式具有不同的特点,我们先按下不表。

在这里,我想让大家思考一下我在思维导图中列举的第四种做 WebScoket 支持的方案可能是什么?不知道大家能不能猜对,后文将会给出答案。


本文代码:以下仓库中 spring-websocket 模块,拉整个仓库下来后可在 IDEA Maven 工具栏中单独编译此模块。

  • Github
  • Gitee

    WS简介

    在正式开始之前,我觉得有必要简单介绍一下 WebSocket 协议,引入任何一个东西之前都有必要知道我们为什么需要它?

    在 Web 开发领域,我们最常用的协议是 HTTP,HTTP 协议和 WS 协议都是基于 TCP 所做的封装,但是 HTTP 协议从一开始便被设计成请求 -> 响应的模式,所以在很长一段时间内 HTTP 都是只能从客户端发向服务端,并不具备从服务端主动推送消息的功能,这也导致在浏览器端想要做到服务器主动推送的效果只能用一些轮询和长轮询的方案来做,但因为它们并不是真正的全双工,所以在消耗资源多的同时,实时性也没理想中那么好。

    既然市场有需求,那肯定也会有对应的新技术出现,WebSocket 就是这样的背景下被开发与制定出来的,并且它作为 HTML5 规范的一部分,得到了所有主流浏览器的支持,同时它还兼容了 HTTP 协议,默认使用 HTTP 的80端口和443端口,同时使用 HTTP header 进行协议升级。

    和 HTTP 相比,WS 至少有以下几个优点:

  1. 使用的资源更少:因为它的头更小。
  2. 实时性更强:服务端可以通过连接主动向客户端推送消息。
  3. 有状态:开启链接之后可以不用每次都携带状态信息。

除了这几个优点以外,我觉得对于 WS 我们开发人员起码还要了解它的握手过程和协议帧的意义,这就像学习 TCP 的时候需要了解 TCP 头每个字节帧对应的意义一样。

像握手过程我就不说了,因为它复用了 HTTP 头只需要在维基百科(阮一峰的文章讲的也很明白)上面看一下就明白了,像协议帧的话无非就是:标识符、操作符、数据、数据长度这些协议通用帧,基本都没有深入了解的必要,我认为一般只需要关心 WS 的操作符就可以了。

WS 的操作符代表了 WS 的消息类型,它的消息类型主要有如下六种:

  1. 文本消息
  2. 二进制消息
  3. 分片消息(分片消息代表此消息是一个某个消息中的一部分,想想大文件分片)
  4. 连接关闭消息
  5. PING 消息
  6. PONG 消息(PING的回复就是PONG)

那我们既然知道了 WS 主要有以上六种操作,那么一个正常的 WS 框架应当可以很轻松的处理以上这几种消息,所以接下来就是本文的中心内容,看看以下这几种 WS 框架能不能很方便的处理这几种 WS 消息。

J2EE 方式

先来 J2EE,一般我把 javax 包里面对 JavaWeb 的扩展都叫做 J2EE,这个定义是否完全正确我觉得没必要深究,只是一种个人习惯,而本章节所介绍的 J2EE 方式则是指 Tomcat 为 WS 所做的支持,这套代码的包名前缀叫做:javax.websocket

这套代码中定义了一套适用于 WS 开发的注解和相关支持,我们可以利用它和 Tomcat 进行WS 开发,由于现在更多的都是使用 SpringBoot 的内嵌容器了,所以这次我们就来按照 SpringBoot 内嵌容器的方式来演示。

首先是引入 SpringBoot - Web 的依赖,因为这个依赖中引入了内嵌式容器 Tomcat:

        <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-web</artifactId>         </dependency>

接着就是将一个类定义为 WS 服务器,这一步也很简单,只需要为这个类加上@ServerEndpoint注解就可以了,在这个注解中比较常用的有三个参数:WS路径、序列化处理类、反序列化处理类。

@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) public @interface ServerEndpoint {     String value();      String[] subprotocols() default {};      Class<? extends Decoder>[] decoders() default {};      Class<? extends Encoder>[] encoders() default {};      Class<? extends Configurator> configurator() default Configurator.class; }

接下来我们来看具体的一个 WS 服务器类示例:

@Component @ServerEndpoint("/j2ee-ws/{msg}") public class WebSocketServer {      //建立连接成功调用     @OnOpen     public void onOpen(Session session, @PathParam(value = "msg") String msg){         System.out.println("WebSocketServer 收到连接: " + session.getId() + ", 当前消息:" + msg);     }      //收到客户端信息     @OnMessage     public void onMessage(Session session, String message) throws IOException {         message = "WebSocketServer 收到连接:" + session.getId() +  ",已收到消息:" + message;         System.out.println(message);         session.getBasicRemote().sendText(message);     }      //连接关闭     @OnClose     public void onclose(Session session){         System.out.println("连接关闭");     }  }

在以上代码中,我们着重关心 WS 相关的注解,主要有以下四个:

  1. @ServerEndpoint : 这里就像 RequestMapping 一样,放入一个 WS 服务器监听的 URL。
  2. @OnOpen :这个注解修饰的方法会在 WS 连接开始时执行。
  3. @OnClose :这个注解修饰的方法则会在 WS 关闭时执行。
  4. @OnMessage :这个注解则是修饰消息接受的方法,并且由于消息有文本和二进制两种方式,所以此方法参数上可以使用 String 或者二进制数组的方式,就像下面这样:

     @OnMessage  public void onMessage(Session session, String message) throws IOException {   }   @OnMessage  public void onMessage(Session session, byte[] message) throws IOException {   }

    除了以上这几个以外,常用的功能方面还差一个分片消息、Ping 消息 和 Pong 消息,对于这三个功能我并没有查到相关用法,只在源码的接口列表中看到了一个 PongMessage 接口,有知道的读者朋友们有知道的可以在评论区指出。
    细心的小伙伴们可能发现了,示例中的 WebSocketServer 类还有一个 @Component 注解,这是由于我们使用的是内嵌容器,而内嵌容器需要被 Spring 管理并初始化,所以需要给 WebSocketServer 类加上这么一个注解,所以代码中还需要有这么一个配置:

    @Configuration public class WebSocketConfig {   @Bean  public ServerEndpointExporter serverEndpointExporter() {      return new ServerEndpointExporter();  } }

    Tips:在不使用内嵌容器的时候可以不做以上步骤。
    最后上个简陋的 WS 效果示例图,前端方面直接使用 HTML5 的 WebScoket 标准库,具体可以查看我的仓库代码:
    image.png

    Spring 方式

    第二部分来说 Spring 方式,Spring 作为 Java 开发界的老大哥,几乎封装了一切可以封装的,对于 WS 开发呢 Spring 也提供了一套相关支持,而且从使用方面我觉得要比 J2EE 的更易用。

    使用它的第一步我们先引入 SpringBoot - WS 依赖,这个依赖包也会隐式依赖 SpringBoot - Web 包:

         <dependency>          <groupId>org.springframework.boot</groupId>          <artifactId>spring-boot-starter-websocket</artifactId>      </dependency>

    第二步就是准备一个用来处理 WS 请求的 Handle了,Spring 为此提供了一个接口—— WebSocketHandler,我们可以通过实现此接口重写其接口方法的方式自定义逻辑,我们来看一个例子:

    @Component public class SpringSocketHandle implements WebSocketHandler {   @Override  public void afterConnectionEstablished(WebSocketSession session) throws Exception {      System.out.println("SpringSocketHandle, 收到新的连接: " + session.getId());  }   @Override  public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {      String msg = "SpringSocketHandle, 连接:" + session.getId() +  ",已收到消息。";      System.out.println(msg);      session.sendMessage(new TextMessage(msg));  }   @Override  public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {      System.out.println("WS 连接发生错误");  }   @Override  public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {      System.out.println("WS 关闭连接");  }   // 支持分片消息  @Override  public boolean supportsPartialMessages() {      return false;  } }

    上面这个例子很好的展示了 WebSocketHandler 接口中的五个函数,通过名字我们就应该知道它具有什么功能了:

  5. afterConnectionEstablished:连接成功后调用。
  6. handleMessage:处理发送来的消息。
  7. handleTransportError:WS 连接出错时调用。
  8. afterConnectionClosed:连接关闭后调用。
  9. supportsPartialMessages:是否支持分片消息。

以上这几个方法重点可以来看一下 handleMessage 方法,handleMessage 方法中有一个 WebSocketMessage 参数,这也是一个接口,我们一般不直接使用这个接口而是使用它的实现类,它有以下几个实现类:

  1. BinaryMessage:二进制消息体
  2. TextMessage:文本消息体
  3. PingMessage:Ping 消息体
  4. PongMessage:Pong 消息体

但是由于 handleMessage 这个方法参数是WebSocketMessage,所以我们实际使用中可能需要判断一下当前来的消息具体是它的哪个子类,比如这样:

    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {         if (message instanceof TextMessage) {             this.handleTextMessage(session, (TextMessage)message);         } else if (message instanceof BinaryMessage) {             this.handleBinaryMessage(session, (BinaryMessage)message);         }     }

但是总这样写也不是个事,为了避免这些重复性代码,Spring 给我们定义了一个 AbstractWebSocketHandler,它已经封装了这些重复劳动,我们可以直接继承这个类然后重写我们想要处理的消息类型:

    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {     }      protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {     }      protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {     }

上面这部分都是对于 Handle 的操作,有了 Handle 之后我们还需要将它绑定在某个 URL 上,或者说监听某个 URL,那么必不可少的需要以下配置:

@Configuration @EnableWebSocket public class SpringSocketConfig implements WebSocketConfigurer {      @Autowired     private SpringSocketHandle springSocketHandle;      @Override     public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {         registry.addHandler(springSocketHandle, "/spring-ws").setAllowedOrigins("*");     } }

这里我把我的自定义 Handle 注册到 "/spring-ws" 上面并设置了一下跨域,在整个配置类上还要打上@EnableWebSocket 注解,用于开启 WS 监听。

Spring 的方式也就以上这些内容了,不知道大家是否感觉 Spring 所提供的 WS 封装要比 J2EE 的更方便也更全面一些,起码我只要看 WebSocketHandler 接口就能知道所有常用功能的用法,所以对于 WS 开发来说我是比较推荐 Spring 方式的。

最后上个简陋的 WS 效果示例图,前端方面直接使用 HTML5 的 WebScoket 标准库,具体可以查看我的仓库代码:
image.png

SocketIO 方式

SocketIO 方式和上面两种有点不太一样,因为 SocketIO 诞生初就是为了兼容性作为考量的,前端的读者们应该对它更熟悉,因为它是一个 JS 库,我们先来看一下维基百科对它的定义:

Socket.IO 是一个面向实时 web 应用的 JavaScript 库。它使得服务器和客户端之间实时双向的通信成为可能。他有两个部分:在浏览器中运行的客户端库,和一个面向Node.js的服务端库,两者有着几乎一样的API。
Socket.IO 主要使用WebSocket协议。但是如果需要的话,Socket.io可以回退到几种其它方法,例如Adobe Flash Sockets,JSONP拉取,或是传统的AJAX拉取,并且在同时提供完全相同的接口。

所以我觉得使用它更多是因为兼容性,因为 HTML5 之后原生的 WS 应该也够用了,然而它是一个前端库,所以 Java 语言这块并没有官方支持,好在民间大神已经以 Netty 为基础开发了能与它对接的 Java 库: netty-socketio

不过我要先给大家提个醒,不再建议使用它了,不是因为它很久没更新了,而是因为它支持的 Socket-Client 版本太老了,截止到 2022-04-29 日,SocketIO 已经更新到 4.X 了,但是 NettySocketIO 还只支持 2.X 的 Socket-Client 版本。

说了这么多,该教大家如何使用它了,第一步还是引入最新的依赖:

        <dependency>             <groupId>com.corundumstudio.socketio</groupId>             <artifactId>netty-socketio</artifactId>             <version>1.7.19</version>         </dependency>

第二步就是配置一个 WS 服务:

@Configuration public class SocketIoConfig {      @Bean     public SocketIOServer socketIOServer() {         com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();          config.setHostname("127.0.0.1");         config.setPort(8001);         config.setContext("/socketio-ws");         SocketIOServer server = new SocketIOServer(config);         server.start();         return server;     }      @Bean     public SpringAnnotationScanner springAnnotationScanner() {         return new SpringAnnotationScanner(socketIOServer());     } }

大家在上文的配置中,可以看到设置了一些 Web 服务器参数,比如:端口号和监听的 path,并将这个服务启动起来,服务启动之后日志上会打印这样一句日志:

[ntLoopGroup-2-1] c.c.socketio.SocketIOServer : SocketIO server started at port: 8001

这就代表启动成功了,接下来就是要对 WS 消息做一些处理了:

@Component public class SocketIoHandle {      /**      * 客户端连上socket服务器时执行此事件      * @param client      */     @OnConnect     public void onConnect(SocketIOClient client) {         System.out.println("SocketIoHandle 收到连接:" + client.getSessionId());     }      /**      * 客户端断开socket服务器时执行此事件      * @param client      */     @OnDisconnect     public void onDisconnect(SocketIOClient client) {         System.out.println("当前链接关闭:" + client.getSessionId());     }      @OnEvent( value = "onMsg")     public void onMessage(SocketIOClient client, AckRequest request, Object data) {         System.out.println("SocketIoHandle 收到消息:" + data);         request.isAckRequested();         client.sendEvent("chatMsg", "我是 NettySocketIO 后端服务,已收到连接:" + client.getSessionId());     } }

我相信对于以上代码,前两个方法是很好懂的,但是对于第三个方法如果大家没有接触过 SocketIO 就比较难理解了,为什么@OnEvent( value = "onMsg")里面这个值是自定义的,这就涉及到 SocketIO 里面发消息的机制了,通过 SocketIO 发消息是要发给某个事件的,所以这里的第三个方法就是监听 发给onMsg事件的所有消息,监听到之后我又给客户端发了一条消息,这次发给的事件是:chatMsg,客户端也需要监听此事件才能接收到这条消息。

最后再上一个简陋的效果图:
image.png
由于前端代码不再是标准的 HTML5 的连接方式,所以我这里简要贴一下相关代码,具体更多内容可以看我的代码仓库:

    function changeSocketStatus() {         let element = document.getElementById("socketStatus");         if (socketStatus) {             element.textContent = "关闭WebSocket";             const socketUrl="ws://127.0.0.1:8001";             socket = io.connect(socketUrl, {                 transports: ['websocket'],                 path: "/socketio-ws"             });             //打开事件             socket.on('connect', () => {                 console.log("websocket已打开");             });             //获得消息事件             socket.on('chatMsg', (msg) => {                 const serverMsg = "收到服务端信息:" + msg;                 pushContent(serverMsg, 2);             });             //关闭事件             socket.on('disconnect', () => {                 console.log("websocket已关闭");             });             //发生了错误事件             socket.on('connect_error', () => {                 console.log("websocket发生了错误");             })         }     }

第四种方式?

第四种方式其实就是 Netty 了,Netty 作为 Java 界大名鼎鼎的开发组件,对于常见协议也全部进行了封装,所以我们可以直接在 Netty 中去很方便的使用 WebSocket,接下来我们可以看看 Netty 怎么作为 WS 的服务器进行开发。

注意:以下内容如果没有 Netty 基础可能一脸蒙的进,一脸蒙的出,不过还是建议大家看看,Netty 其实很简单。

第一步需要先引入一个 Netty 开发包,我这里为了方便一般都是 All In:

        <dependency>             <groupId>io.netty</groupId>             <artifactId>netty-all</artifactId>             <version>4.1.75.Final</version>         </dependency>

第二步的话就需要启动一个 Netty 容器了,配置很多,但是比较关键的也就那几个:

public class WebSocketNettServer {     public static void main(String[] args) {          NioEventLoopGroup boss = new NioEventLoopGroup(1);         NioEventLoopGroup work = new NioEventLoopGroup();          try {             ServerBootstrap bootstrap = new ServerBootstrap();             bootstrap                     .group(boss, work)                     .channel(NioServerSocketChannel.class)                     //设置保持活动连接状态                     .childOption(ChannelOption.SO_KEEPALIVE, true)                     .localAddress(8080)                     .handler(new LoggingHandler(LogLevel.DEBUG))                     .childHandler(new ChannelInitializer<SocketChannel>() {                         @Override                         protected void initChannel(SocketChannel ch) throws Exception {                             ch.pipeline()                                     // HTTP 请求解码和响应编码                                     .addLast(new HttpServerCodec())                                     // HTTP 压缩支持                                     .addLast(new HttpContentCompressor())                                     // HTTP 对象聚合完整对象                                     .addLast(new HttpObjectAggregator(65536))                                     // WebSocket支持                                     .addLast(new WebSocketServerProtocolHandler("/ws"))                                     .addLast(WsTextInBoundHandle.INSTANCE);                         }                     });              //绑定端口号,启动服务端             ChannelFuture channelFuture = bootstrap.bind().sync();             System.out.println("WebSocketNettServer启动成功");              //对关闭通道进行监听             channelFuture.channel().closeFuture().sync();          } catch (InterruptedException e) {             e.printStackTrace();         } finally {             boss.shutdownGracefully().syncUninterruptibly();             work.shutdownGracefully().syncUninterruptibly();         }      } }

以上代码我们主要关心端口号和重写的 ChannelInitializer 就行了,里面我们定义了五个过滤器(Netty 使用责任链模式),前面三个都是 HTTP 请求的常用过滤器(毕竟 WS 握手是使用 HTTP 头的所以也要配置 HTTP 支持),第四个则是 WS 的支持,它会拦截 /ws 路径,最关键的就是第五个了过滤器它是我们具体的业务逻辑处理类,效果基本和 Spring 那部门中的 Handle 差不多,我们来看看代码:

@ChannelHandler.Sharable public class WsTextInBoundHandle extends SimpleChannelInboundHandler<TextWebSocketFrame> {      private WsTextInBoundHandle() {         super();         System.out.println("初始化 WsTextInBoundHandle");     }      @Override     public void channelActive(ChannelHandlerContext ctx) throws Exception {         System.out.println("WsTextInBoundHandle 收到了连接");     }      @Override     protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {          String str = "WsTextInBoundHandle 收到了一条消息, 内容为:" + msg.text();          System.out.println(str);          System.out.println("-----------WsTextInBoundHandle 处理业务逻辑-----------");          String responseStr = "{\"status\":200, \"content\":\"收到\"}";          ctx.channel().writeAndFlush(new TextWebSocketFrame(responseStr));         System.out.println("-----------WsTextInBoundHandle 数据回复完毕-----------");     }      @Override     public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {          System.out.println("WsTextInBoundHandle 消息收到完毕");         ctx.flush();     }      @Override     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {         System.out.println("WsTextInBoundHandle 连接逻辑中发生了异常");         cause.printStackTrace();         ctx.close();     } }

这里面的方法我都不说了,看名字就差不多知道了,主要是看一下这个类的泛型:TextWebSocketFrame,很明显这是一个 WS 文本消息的类,我们顺着它的定义去看发现它继承了 WebSocketFrame,接着我们去看它的子类:

image.png

一图胜千言,我想不用多说大家也都知道具体的类是处理什么消息了把,在上文的示例中我们是一定了一个文本 WS 消息的处理类,如果你想处理其他数据类型的消息,可以将泛型中的 TextWebSocketFrame 换成其他 WebSocketFrame 类就可以了
至于为什么没有连接成功后的处理,这个是和 Netty 的相关机制有关,可以在 channelActive 方法中处理,大家有兴趣的可以了解一下 Netty。

最后上个简陋的 WS 效果示例图,前端方面直接使用 HTML5 的 WebScoket 标准库,具体可以查看我的仓库代码:
image.png

总结

洋洋洒洒五千字,有了收获别忘赞。

在上文中,我总共介绍了四种在 Java 中使用 WS 的方式,从我个人使用意向来说我感觉应该是这样的:Spring 方式 > Netty 方式 > J2EE 方式 > SocketIO 方式,当然了,如果你的业务存在浏览器兼容性问题,其实只有一种选择:SocketIO。

最后,我估计某些读者会去具体拉代码看代码,所以我简单说一下代码结构:

├─java │  └─com │      └─example │          └─springwebsocket │              │  SpringWebsocketApplication.java │              │  TestController.java │              │ │              ├─j2ee │              │      WebSocketConfig.java │              │      WebSocketServer.java │              │ │              ├─socketio │              │      SocketIoConfig.java │              │      SocketIoHandle.java │              │ │              └─spring │                      SpringSocketConfig.java │                      SpringSocketHandle.java │ └─resources     └─templates             J2eeIndex.html             SocketIoIndex.html             SpringIndex.html

代码结构如上所示,应用代码分成了三个文件夹,分别放着三种方式的具体示例代码,在资源文件夹下的 templates 文件夹也有三个 HTML 文件,就是对应三种示例的 HTML 页面,里面的链接地址和端口我都预设好了,拉下来直接单独编译此模块运行即可。

我没有往里面放 Netty 的代码,是因为感觉 Netty 部分内容很少,文章示例中的代码直接复制就能用,后面如果写 Netty 的话会再开一个 Netty 模块用来放 Netty 相关的代码。

好了,今天的内容就到这了,希望对大家有帮助的话可以帮我文章点点赞,GitHub 也点点赞,大家的点赞与评论都是我更新的不懈动力,下期见。

如何写出高性能代码之优化数据访问

Posted: 05 Jun 2022 05:48 AM PDT

  同一份逻辑,不同人的实现的代码性能会出现数量级的差异; 同一份代码,你可能微调几个字符或者某行代码的顺序,就会有数倍的性能提升;同一份代码,也可能在不同处理器上运行也会有几倍的性能差异;十倍程序员不是只存在于传说中,可能在我们的周围也比比皆是。十倍体现在程序员的方法面面,而代码性能却是其中最直观的一面。

  本文是《如何写出高性能代码》系列的第四篇,本文将告诉你数据访问会怎么样影响到程序的性能,以及如何通过变更数据访问的方式提升程序的性能。

数据访问速度为什么会影响到程序的性能?

  程序的运行的每一个可以简化为这样一个三步模型:第一步,读数据(当然也有部分数据是别的地方法发过来的);第二步,对数据做处理;第三步,将处理完的结果写入存储器。这里我将这三步骤简称为 读算写。 实际上真实的CPU指令执行过程会稍微复杂有些,但实际上也是这三个步骤。 而一个复杂的程序包含无数个CPU指令,如果读取或者写入数据太慢,必然会影响到程序的性能。
在这里插入图片描述
  为了能更直观一点,我这里将程序执行的流程比作是大厨做菜,大厨的工作流程就是取原始食材,然后对食材进行加工(煎烤烹炸煮),最后出锅上菜。影响大厨出菜速度的因素除了加工过程之前,获取食材的耗时也会影响到大厨出菜速度。有些食材就在手边,可以很快获取到,但有些食材可能在冷库、甚至在菜市场,获取就很不方便了。

  CPU犹如大厨,而数据就是CPU的食材,寄存器里的数据就是CPU手边的食材,内存的数据就是在冷库的食材,固态硬盘(SSD)上的数据是还在菜市场的食材,机械硬盘(HDD)上的数据犹如还在地里生长的菜…… 如果CPU在运行程序时,如果拿不到所需要的数据,它也只能等在那儿浪费时间了。

数据访问速度对程序性能有多大影响?

  不同存储器数据读取和写入的时延相差极大,鉴于大多数场景下,我们都是读取数据,我们就只拿数据读取为例,最快的寄存器和最慢的机械磁盘,随机读写的时延相差百万倍。可能你没有直观概念,我们还是拿厨师做个类比。

   假设厨师要做一道西红柿炒鸡蛋,如果食材都有人备好的话,只需要十来秒食材就能下锅炒制。 我们把这个时间比作是CPU从寄存器里取到数据的时间。然而如果是CPU从磁盘获取数据的话,所耗费的时间相当于厨师自己种出西红柿或者养小鸡下蛋了(3-4个月)。由此可见,从错误的存储设备上获取数据,会极大影响程序的运行速度。

  再说一个我们之前在生产环境遇到的实际案例,我们在生产环境也出过故障。原因是这样的,我们有个服务容器化改造的时候,和上游服务没有部署在同一个机房,跨机房虽然只会增加1ms的时延,但他们服务代码写的有问题,有个接口批量串行调另外一个服务,串行累加导致接口时延增加上百ms。 本来没有性能问题的服务,就因为迁移了机房,导致性能出现了问题……

各存储器性能差异

  实际上在编码的时候,遇到的存储设备多种多样,寄存器、内存、磁盘、网络存储……,每种设备都有自己的特点。只有认识到各种存储器之间的差异,我们才能在正确的场景下使用合适的存储器。以下表格就是各类常见存储设备的随机读时延参考数据……
在这里插入图片描述

备注:以上数据在不同硬件设备会有出入,这里只是为了展示其差异性,不代表准确值,准确信息请参考硬件手册。

  虽然日常我们觉得内存的读取速度已经很非常快了,日常写代码的时候遇到啥数据获取比较慢,加个内存缓存速度简直就起飞了。但内存的访问速度相对于CPU运行速度来说还是太慢,读取一次内存的时间,都够CPU执行几百条指令了,所以现代CPU都对内存加了缓存。

如何减小数据访问时延对性能的影响?

  减少数据访问时延对性能的影响也很简单,那就是把数据尽可能放到最快的存储介质上。然而,存取速度、容量、价格三者之间有着不可调和的矛盾,简单来说就是 速度越快容量越小但价格越贵,反之容量越大速度越慢而价格越便宜
在这里插入图片描述
  世界总是那么巧秒,仿佛一切早被安排好,我们并不需要把所有的数据都放在最快的存储介质上。 还记得我们在第二篇(巧用数据特性)[https://blog.csdn.net/xindoo/article/details/123941141] 提到的数据局部性吗! 局部性分两种,空间局部性和时间局部性。

  • 时间局部性: 如果一份某个时刻数据被访问过,那不久之后这份数据会被再次访问到。
  • 空间局部性: 如果某个存储元被访问过,大概率那不久之后,其附近的存储单元也会被访问。

  总结下这两点就是,程序大部分时间只会集中访问很小的一部分数据。 这意味着我们可以用较小的存储空间覆盖到大部分被访问的数据。 说直接点就是,我们可以加缓存。 实际上,不管是计算机硬件、数据库、还是业务系统,到处都充斥着缓存。甚至你写下的每一行代码,在机器上运行时都用到了缓存,不知道大家有没有关注过CPU,CPU有个参数,就是缓存大小,我们以intel酷睿i7-12650HX 为例,它就有24MB的三级缓存,这个缓存就是CPU到内存之间的缓存。 只不过现代计算机将底层的细节屏蔽掉了而已,我们日常不太可能主要的到。

  在我们自己写代码的时候,也可以加缓存来提升程序性能。举个最近的我们在系统中遇到的例子,我们最新在做数据权限相关的功能,不同的员工在我们系统中有不同的权限,所以他们看到的数据也应该是不同的。我们的实现方式是每个用户请求系统的时候,首先获取到该用户所有的权限列表,然后把所有在权限列表中的数据展示出来。

  因为每个人的权限列表比较大,所以权限接口的性能不怎么样,每次请求耗时也比较长。所以,我们直接给这个接口的数据加了缓存,优先从缓存里取,取不到再调接口,极大提升了程序性能。当然因为权限数据也不会经常变动,所以也不用太考虑数据滞后导致的后果。另外,我们缓存数据只加了几分钟,因为一个用户单次使用我们系统时长也就持续几分钟,过几分钟后数据过期缓存空间也会自动释放,达到节省空间的目的。

  我在上大学那会,笔记本电脑还是标配机械硬盘的年代,那时候电脑永久了会很卡,后来了解到换装SSD会提升电脑性能,那个时候SSD还挺贵的,普通笔本都不会标配SSD后来我攒半个月的生活费给自己笔记本替换了一块120g的SSD,电脑的运行速度就有明显的提升,本质上还是因为SSD的随机访问时延比机械硬盘快上百倍的原因。 之前某大厂号称将mysql性能提升了上百倍,其实也是基于SSD做的很多查询优化。

缓存不是银弹

银弹(英文:Silver Bullet),指由纯银质或镀银的子弹。在欧洲民间传说及19世纪以来哥特小说风潮的影响下,银色子弹往往被描绘成具有驱魔功效的武器,是针对狼人、吸血鬼等超自然怪物的特效武器。后来也被比喻为具有极端有效性的解决方法,作为杀手锏、最强杀招、王牌等的代称。

在这里插入图片描述
  这里特别提醒下,缓存不是万能的,缓存其实是有副作用的,那就是数据的有效性很难得到保证。缓存其实里面放的是旧数据,当前时刻数据是不是还是这样的?不确定,也许数据早就变了,所以使用缓存时必须要关注缓存数据有效性问题。如果缓存时间过久,数据失效的可能性能,数据不一致导致的风险也就越大。 如果缓存时间过短,因为经常需要获取原始数据,缓存存在意义也就越小。所以在使用缓存必须要做出数据不一致和性能之间的权衡(trade-off),你需要正确评估数据的时效性,对缓存设置合理的过期策略。

  上文说到其实我们写下的每一行代码都用到了缓存,现在大家已经都知道这个缓存其实就是CPU的Cache。CPU的Cache也是有明显的副作用的,我们在写多线程代码的时候也不得不关注到,那就是多核CPU之间数据一致性的问题。因为CPU Cache的存在,我们写多线程代码时不得不考虑数据同步的问题,导致多线程的代码很难编写,出了问题也很难排查。

  有个面试八股文题目其实就很容易说明这个问题——多线程计数器,多线程去操作计数器,累加统计数据,如何保证数据统计的准确性。如果只是简单使用cnt++实现,这里就会遇到多核CPU缓存导致的数据不一致性,具体原理这里不再解释,反正结果就是统计出来的数据会比真是数据少。 正确的做法就是,你必须在累加的过程中加多线程同步的机制,保证同一时刻只可能有一个线程在操作,操作完之后也能保证数据能写回内存,在java中必须使用锁或者原子类实现。而这对于编程新手而言又是一道门槛。


总结

  数据访问是任何程序不可或缺的一部分,甚至对大多数程序而言时间都耗费在了数据访问的过程上,所以只要优化了这部分的耗时,程序的性能必然能得到提升。

  本文全部内容就到这了,下一篇,我们将继续探讨下性能优化到极致该怎么做,敬请期待!!另外,有兴趣也可以查阅下之前的几篇文章。

如何写出高性能代码系列文章

No comments:

Post a Comment