libinfs

libinfs 查看完整档案

杭州编辑西北大学  |  考古学_金融学 编辑阿里巴巴集团  |  高级前端工程师 编辑 www.cnblogs.com/libinfs/ 编辑
编辑

我希望在 segmentfault 做一个严肃认真的专栏作者,如果我的文章对你有帮助,请点击文章下方“赞”或“赞赏支持”给我支持,十分感谢。

个人动态

libinfs 发布了文章 · 2月3日

All in one:项目级 monorepo 策略最佳实践

0. 🧉 前言

在最近的项目开发中,出现了一个令我困扰的状况。我正在开发的项目 A,依赖了已经线上发布的项目 B,但是随着项目 A 的不断开发,又需要不时修改项目 B 的代码(这些修改暂时不必发布线上),如何能够在修改项目 B 代码后及时将改动后在项目 A 中同步? 在项目 A 发布上线后,如何以一种优雅的方式解决项目 A,B 版本升级后的版本同步问题? 经过一番调研,我发现解决这些问题的最佳方案便是本篇要介绍的 monorepo 策略。

1. 🤔 什么是 monorepo 策略?

monorepo 是一种将多个项目代码存储在一个仓库里的软件开发策略("mono" 来源于希腊语 μόνος 意味单个的,而 "repo",显而易见地,是 repository 的缩写)。将不同的项目的代码放在同一个代码仓库中,这种「把鸡蛋放在同一个篮子里」的做法可能乍看之下有些奇怪,但实际上,这种代码管理方式有很多好处,无论是世界一流的互联网企业 Google,Facebook,还是社区知名的开源项目团队 Babel (如下图)都使用了 monorepo 策略管理他们的代码。

image-20210124225815627

<p style="text-align: center; color: #999;">babel 使用 monorepo 策略管理代码</p>

使用 monorepo 策略究竟会给代码管理者和程序开发者带来哪些好处?我们又该如何在工作中尝试实践 monorepo 策略?这正是本文想要探讨的话题。希望通过我的一番介绍,您能够对 monorepo 策略有更完整的认知,文章中介绍的工具和思想可以切实帮助到您和您所在的团队。

2. 🌗 monorepo 策略的优劣

通过 monorepo 策略组织代码,您代码仓库的目录结构看起来会是这样:

.
├── lerna.json
├── package.json
└── packages/ # 这里将存放所有子 repo 目录
    ├── project_1/
    │   ├── index.js
    │   ├── node_modules/
    │   └── package.json
    ├── project_2/
    │   ├── index.js
    │   ├── node_module/
    │   └── package.json
    ...

乍看起来,所谓的 monorepo 策略就只是将不同项目的目录汇集到一个目录之下,但实际上操作起来所要考虑的事情则远比看起来要复杂得多。通过分析使用 monorepo 策略的优劣,我们可以更直观的感受到这里面所隐晦涉及的知识点。

2.1 monorepo 方案的优势

  1. 代码重用将变得非常容易:由于所有的项目代码都集中于一个代码仓库,我们将很容易抽离出各个项目共用的业务组件或工具,并通过 TypeScript,Lerna 或其他工具进行代码内引用;
  2. 依赖管理将变得非常简单:同理,由于项目之间的引用路径内化在同一个仓库之中,我们很容易追踪当某个项目的代码修改后,会影响到其他哪些项目。通过使用一些工具,我们将很容易地做到版本依赖管理和版本号自动升级;
  3. 代码重构将变得非常便捷:想想究竟是什么在阻止您进行代码重构,很多时候,原因来自于「不确定性」,您不确定对某个项目的修改是否对于其他项目而言是「致命的」,出于对未知的恐惧,您会倾向于不重构代码,这将导致整个项目代码的腐烂度会以惊人的速度增长。而在 monorepo 策略的指导下,您能够明确知道您的代码的影响范围,并且能够对被影响的项目可以进行统一的测试,这会鼓励您不断优化代码;
  4. 它倡导了一种开放,透明,共享的组织文化,这有利于开发者成长,代码质量的提升:在 monorepo 策略下,每个开发者都被鼓励去查看,修改他人的代码(只要有必要),同时,也会激起开发者维护代码,和编写单元测试的责任心(毕竟朋友来访之前,我们从不介意自己的房子究竟有多乱),这将会形成一种良性的技术氛围,从而保障整个组织的代码质量。

2.2 monorepo 方案的劣势

  1. 项目粒度的权限管理变得非常复杂:无论是 Git 还是其他 VCS 系统,在支持 monorepo 策略中项目粒度的权限管理上都没有令人满意的方案,这意味着 A 部门的 a 项目若是不想被 B 部门的开发者看到就很难了。(好在我们可以将 monorepo 策略实践在「项目级」这个层次上,这才是我们这篇文章的主题,我们后面会再次明确它);
  2. 新员工的学习成本变高:不同于一个项目一个代码仓库这种模式下,组织新人只要熟悉特定代码仓库下的代码逻辑,在 monorepo 策略下,新人可能不得不花更多精力来理清各个代码仓库之间的相互逻辑,当然这个成本可以通过新人文档的方式来解决,但维护文档的新鲜又需要消耗额外的人力;
  3. 对于公司级别的 monorepo 策略而言,需要专门的 VFS 系统,自动重构工具的支持:设想一下 Google 这样的企业是如何将十亿行的代码存储在一个仓库之中的?开发人员每次拉取代码需要等待多久?各个项目代码之间又如何实现权限管理,敏捷发布?任何简单的策略乘以足够的规模量级都会产生一个奇迹(不管是好是坏),对于中小企业而言,如果没有像 Google,Facebook 这样雄厚的人力资源,把所有项目代码放在同一个仓库里这个美好的愿望就只能是个空中楼阁。

2.3 小结:如何取舍?

没错,软件开发领域从来没有「银弹」。monorepo 策略也并不完美,并且,我在实践中发现,要想完美在组织中运用 monorepo 策略,所需要的不仅是出色的编程技巧和耐心。团队日程组织文化个人影响力相互碰撞的最终结果才决定了想法最终是否能被实现。

但是请别灰心的太早,因为虽然让组织作出改变,统一施行 monorepo 策略困难重重,但这却并不意味着我们需要彻底跟 monorepo 策略说再见(否则我这篇文章就该到此为止了)。我们还可以把 monorepo 策略实践在「项目」这个级别,即从逻辑上确定项目与项目之间的关联性,然后把相关联的项目整合在同一个仓库下,通常情况下,我们不会有太多相互关联的项目,这意味着我们能够免费得到 monorepo 策略的所有好处,并且可以拒绝支付大型 monorepo 架构的利息。

本文的剩余篇幅就是对「项目级别 monorepo 实践」的一些总结,即使您最终没有选择 monorepo 策略组织您的代码,相信文章中提供的一些工程化工具或思路也一样会对您产生帮助。

3. 🧑🏻‍💻 monorepo 方案实践

3.1 锁定环境:Volta

Volta 是一个 JavaScript 工具管理器,它可以让我们轻松地在项目中锁定 node,npm 和 yarn 的版本。你只需在安装完 Volta 后,在项目的根目录中执行 volta pin 命令,那么无论您当前使用的 node 或 npm(yarn)版本是什么,volta 都会自动切换为您指定的版本。

因此,除了使用 Docker 和显示在文档中声明 node 和 npm(yarn)的版本之外,您就有了另一个锁定环境的强力工具。

而且相较于 nvm,Volta 还具有一个诱人的特性:当您项目的 CLI 工具与全局 CLI 工具不一致时,Volta 可以做到在项目根目录下自动识别,切换到项目指定的版本,这一切都是由 Volta 默默做到的,开发者不必关心任何事情。

3.2 复用 packages:workspace

使用 monorepo 策略后,收益最大的两点是:

  1. 避免重复安装包,因此减少了磁盘空间的占用,并降低了构建时间
  2. 内部代码可以彼此相互引用

这两项好处全部都可以由一个成熟的包管理工具来完成,对前端开发而言,即是 yarn(1.0 以上)或 npm(7.0 以上)通过名为 workspaces 的特性实现的(⚠️ 注意,支持 workspaces 特性的 npm 目前依旧不是 TLS 版本)。

为了实现前面提到的两点收益,您需要在代码中做三件事:

  1. 调整目录结构,将相互关联的项目放置在同一个目录,推荐命名为 packages
  2. 在项目根目录里的 package.json 文件中,设置 workspaces 属性,属性值为之前创建的目录;
  3. 同样,在 package.json 文件中,设置 private 属性为 true(为了避免我们误操作将仓库发布);

经过修改,您的项目目录看起来应该是这样:

.
├── package.json
└── packages/
    ├── @mono/project_1/ # 推荐使用 `@<项目名>/<子项目名>` 的方式命名
    │   ├── index.js
    │   └── package.json
    └── @mono/project_2/
        ├── index.js
        └── package.json

而当您在项目根目录中执行 npm installyarn install 后,您会发现在项目根目录中出现了 node_modules 目录,并且该目录不仅拥有所有子项目共用的 npm 包,还包含了我们的子项目。因此,我们可以在子项目中通过各种模块引入机制,像引入一般的 npm 模块一样引入其他子项目的代码。

请注意我们对子项目的命名,统一以 @<repo_name>/ 开头,这是一种社区最佳实践,不仅可以让用户更容易了解整个应用的架构,也方便您在项目中更快捷的找到所需的子项目。

至此,我们已经完成了 monorepo 策略的核心部分,实在是很容易不是吗?但是老话说「行百里者半九十」,距离优雅的搭建一个 monorepo 项目,我们还有一些路要走。

3.3 统一配置:合并同类项 - Eslint,Typescript 与 Babel

您一定同意,编写代码要遵循 DRY 原则(Don't Repeat Yourself 的缩写)。那么,理所当然地,我们应该尽量避免在多个子项目中放置重复的 eslintrc,tsconfig 等配置文件。幸运的是,Babel,Eslint 和 Typescript 都提供了相应的功能让我们减少自我重复。

3.3.1 TypeScript

我们可以在 packages 目录中放置 tsconfig.settting.json 文件,并在文件中定义通用的 ts 配置,然后,在每个子项目中,我们可以通过 extends 属性,引入通用配置,并设置 compilerOptions.composite 的值为 true,理想情况下,子项目中的 tsconfig 文件应该仅包含下述内容:

{
  "extends": "../tsconfig.setting.json", // 继承 packages 目录下通用配置
  "compilerOptions": {
    "composite": true, // 用于帮助 TypeScript 快速确定引用工程的输出文件位置
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

3.3.2 Eslint

对于 Eslint 配置文件,我们也可以如法炮制,这样定义子项目的 .eslintrc 文件内容:

{
  "extends": "../../.eslintrc", // 注意这里的不同
  "parserOptions": {
    "project": "tsconfig.json"
  }
}

注意到了吗,对于通用的 eslint 配置,我们并没有将其放置在 packages 目录中,而是放在整个项目的根目录下,这样做是因为一些编辑器插件只会在项目根目录寻找 .eslintrc 文件,因此为了我们的项目能够保持良好的「开发环境一致性」,请务必将通用配置文件放置在项目的根目录中。

3.3.3 Babel

Babel 配置文件合并的方式与 TypeScript 如出一辙,甚至更加简单,我们只需在子项目中的 .babelrc 文件中这样声明即可:

{
  "extends": "../.babelrc"
}

当一切准备就绪后,我们的项目目录应该大致呈如下所示的结构:

.
├── package.json
├── .eslintrc
└── packages/
    │   ├── tsconfig.settings.json
    │   ├── .babelrc
    ├── @mono/project_1/
    │   ├── index.js
    │   ├── .eslintrc
    │   ├── .babelrc
    │   ├── tsconfig.json
    │   └── package.json
    └───@mono/project_2/
        ├── index.js
        ├── .eslintrc
        ├── .babelrc
        ├── tsconfig.json
        └── package.json

3.4 统一命令脚本:scripty

在上一步中,我们尽可能的将所有配置文件进行抽象,从而精简了代码,并提高了整个项目的一致性。我们的整个仓库也因此有了「更浓郁的 monorepo 风味 ☕️」。但如果仔细审视我们的整个工程文件,还有一处存在着明显的瑕疵和一些恼人的坏味道,当您仔细审视您的众多 package.json 文件时,您就知道我在说什么了 -- scripts 脚本。

如果您的子项目足够多,您可能会发现,每个 package.json 文件中的 scripts 属性都大同小异,并且一些 scripts 充斥着各种 Linux 语法,例如管道操作符,重定向或目录生成。重复带来低效,复杂则使人难以理解,这都是需要我们解决的问题。

这里给出的解决方案是,使用 scripty 管理您的脚本命令,简单来说,scripty 允许您将脚本命令定义在文件中,并在 package.json 文件中直接通过文件名来引用。这使我们可以实现如下目的:

  1. 子项目间复用脚本命令
  2. 像写代码一样编写脚本命令,无论它有多复杂,而在调用时,像调用函数一样调用

通过使用 scripty 管理我们的 monorepo 应用,目录结构看起来将会是这样:

.
├── package.json
├── .eslintrc
├── scirpts/ # 这里存放所有的脚本
│   │   ├── packages/ # 包级别脚本
│   │   │   ├── build.sh
│   │   │   └── test.sh
│   └───└── workspaces/ # 全局脚本
│           ├── build.sh
│           └── test.sh
└── packages/
    │   ├── tsconfig.settings.json
    │   ├── .babelrc
    ├── @mono/project_1/
    │   ├── index.js
    │   ├── .eslintrc
    │   ├── .babelrc
    │   ├── tsconfig.json
    │   └── package.json
    └── @mono/project_2/
        ├── index.js
        ├── .eslintrc
        ├── .babelrc
        ├── tsconfig.json
        └── package.json

注意,我们脚本分为两类「package 级别」与「workspace 级别」,并且分别放在两个文件夹内。这样做的好处在于,我们既可以在项目根目录执行全局脚本,也可以针对单个项目执行特定的脚本。

通过使用 scripty,子项目的 package.json 文件中的 scripts 属性将变得非常精简:

{
  ...
  "scripts": {
    "test": "scripty",
    "lint": "scripty",
    "build": "scripty"
  },
  "scripty": {
    "path": "../../scripts/packages" // 注意这里我们指定了 scripty 的路径
  },
  ...
}

大功告成!🎉 至此,我们尽己所能地删除了整个项目中的重复代码,让整个项目变得干净,清爽并且有极强的复用性。

🧉 小贴士:

别忘了使用 chmod -R u+x scripts 命令使所有的 shell 脚本具备可执行权限,也千万别忘了把这条贴士写在您的 README.md 文件中!

3.5 统一包管理:Lerna

<p style="text-align: center; color: #999;">图片来源:https://github.com/lerna/lerna</p>

我有时会感慨自己的灵感匮乏,怎么就想不到 Lerna 这样既有神话色彩又能自我释义的好名字。您可以大胆想象,九头龙的每只龙头都在帮您管理着一个子项目,而您只需要骑在龙身上发号施令的场景,这基本上就是我们使用 Lerna 时的直观感受。

这也是为什么当我们提起 monorepo 策略,就几乎不得不提到 Lerna 的原因了,它的确提供了一种非常便捷的方式供我们管理 monorepo 项目。当子项目越多时,Lerna 就越能显示其威力。

当多个子项目放在一个代码仓库,并且子项目之间又相互依赖时,我们面临的棘手问题有两个:

  1. 如果我们需要在多个子目录执行相同的命令,我们需要手动进入各个目录,并执行命令
  2. 当一个子项目更新后,我们只能手动追踪依赖该项目的其他子项目,并升级其版本

通过使用 Lerna,这些棘手的问题都将不复存在。

当在项目根目录使用 npx lerna init 初始化后,我们的根目录会新增一个 lerna.json 文件,默认内容为:

{
  "packages": ["packages/*"],
  "version": "0.0.0"
}

让我们稍稍改动这个文件,使其变为:

{
  "packages": ["packages/*"],
  "npmClient": "yarn",
  "version": "independent",
  "useWorkspaces": true,
}

可以注意到,我们显示声明了我们的包客户端(npmClient)为 yarn,并且让 Lerna 追踪我们 workspaces 设置的目录,这样我们就依旧保留了之前 workspaces 的所有特性(子项目引用通用包提升)。

除此之外一个有趣的改动在于我们将 version 属性指定为一个关键字 independent,这将告诉 lerna 应该将每个子项目的版本号看作是相互独立的。当某个子项目代码更新后,运行 lerna publish 时,Lerna 将监听到代码变化的子项目并以交互式 CLI 方式让开发者决定需要升级的版本号,关联的子项目版本号不会自动升级,反之,当我们填入固定的版本号时,则任一子项目的代码变动,都会导致所有子项目的版本号基于当前指定的版本号升级。

Lerna 提供了很多 CLI 命令以满足我们的各种需求,但根据 2/8 法则,您应该首先关注以下这些命令:

  • lerna bootstrap:等同于 lerna link + yarn install,用于创建符合链接并安装依赖包;
  • lerna run:会像执行一个 for 循环一样,在所有子项目中执行 npm script 脚本,并且,它会非常智能的识别依赖关系,并从根依赖开始执行命令;
  • lerna exec:像 lerna run 一样,会按照依赖顺序执行命令,不同的是,它可以执行任何命令,例如 shell 脚本;
  • lerna publish:发布代码有变动的 package,因此首先您需要在使用 Lerna 前使用 git commit 命令提交代码,好让 Lerna 有一个 baseline;
  • lerna add:将本地或远程的包作为依赖添加至当前的 monorepo 仓库中,该命令让 Lerna 可以识别并追踪包之间的依赖关系,因此非常重要;
# 向 @mono/project2 和 @mono/project3 中添加 @mono/project1
lerna add @mono/project1 '@mono/project{2,3}'

3.5.1 Lerna 高级命令

除了上面介绍到的常用命令外,Lerna 还提供了一些参数满足我们更灵活的需求,例如:

  • --concurrency <number>:参数可以使 Lerna 利用计算机上的多个核心,并发运行,从而提升构建速度;
  • --scope '@mono/{pkg1,pkg2}'--scope 参数可以指定 Lerna 命令的运行环境,通过使用该参数,Lerna 将不再是一把梭的在所有仓库中执行命令,而是可以精准地在我们所指定的仓库中执行命令,并且还支持示例中的模版语法;
  • --stream:该参数可使我们查看 Lerna 运行时的命令执行信息;

3.5.2 npm 包本地发布:Verdaccio

看到这里,您可能想要亲自体验一把使用 Lerna 管理/发布 monorepo 项目的感觉。可是很快您会发现,将示例代码发布到真实世界的 npm 仓库并非一个好主意,这多少有些令人沮丧,但是别担心,您可以使用 Verdaccio 在本地创建一个 npm 仓库作为代理,然后尽情体验 Lerna 的种种强大之处。

安装运行 Verdaccio 非常简单,您只需运行:

npm install --global verdaccio

在全局安装 Verdaccio 应用,然后在 shell 中输入:

verdaccio

即可通过 localhost:4837 访问您的本地代理 npm 仓库,别忘了在您的项目根目录创建 .npmrc 文件,并在文件中将 npm 仓库地址改写为您的本地代理地址:

registry="http://localhost:4873/"

大功告成 🙌!每当您执行 lerna publish 时,子项目所构建成的 package 将会发布在本地 npm 仓库中,而当您执行 lerna bootstrap 时,Verdaccio 将会放行,让您成功从远程 npm 仓库中拉取相应的代码。

3.6 格式化 commit 信息

至此,我们已经掌握了组织一个项目级 monorepo 仓库的所有前沿技巧,最后,让我们看看最后一个可以优化的地方:代码提交时,约束 commit 信息

一个 monorepo 仓库可能被不同的开发者提交不同子项目的代码,如果没有规范化的 commit 信息,在故障排查或版本回滚时毫无意外会遭遇灾难。因此,千万不要小看 commit 信息格式化的重要性(当然,同样重要的还有代码注释!)。

为了我们能够一目了然的追踪每次代码变更的信息,我们使用 commitlint 工具作为格式化 commit 信息的不二之选。

顾名思义,commitlint 可以帮助我们检查提交的 commit 信息,它强制约束我们的 commit 信息必须在开头附加指定类型,用于标示本次提交的大致意图,支持的类型关键字有:

  • feat:表示添加一个新特性;
  • chore:表示做了一些与特性和修复无关的「家务事」;
  • fix:表示修复了一个 Bug;
  • refactor:表示本次提交是因为重构了代码;
  • style:表示代码美化或格式化;
  • ...

我强烈建议您遵循该规范编写您的 commit 信息,不要偷懒,坚持下去,您的 git 日志将会显得整齐,有条理,富有表现力,同时,您也会收到同行的交口称赞,人人都会以和您这样优雅的工程师合作为荣。

除了限定 commit 信息类型外,commitlint 还支持(虽然不是必须的)显示指定我们本次提交所对应的子项目名称。假如我们有一个名为 @mono/project1 的子项目,我们针对该项目提交的 commit 信息可以写为:

git commit -m "feat(project1): add a attractive button" # 注意,我们省略了 @mono 的项目前缀

毫无疑问,这将会使我们的 commit 信息更具表现力。

我们可以通过下面的命令安装 commitlint 以及周边依赖:

npm i -D @commitlint/cli @commitlint/config-conventional @commitlint/config-lerna-scopes commitlint husky lerna-changelog

注意到了吗?我偷偷安装了 husky,它能够帮助我们在提交 commit 信息时自动运行 commitlint 进行检查,但在这之前,我们需要再在根目录下的 package.json 文件里加点料,像这样:

{
 ...
 "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  }
 ...
}

为了能够让 commitlint 感知我们的子项目名称,我们还需在项目根目录中增加 commitlint.config.js 文件,并设置文件内容为:

module.exports = {
  extends: [
    "@commitlint/config-conventional",
    "@commitlint/config-lerna-scopes",
  ],
};

至此,我们统一并规范化了 monorepo 项目的 commit 信息,终于整个 monorepo 工程化的最后一块拼图被我们拼上了!

(顺便一提,您可以通过在命令行执行 echo "build(project1): change something" | npx commitlint 命令即可验证您的 commit 信息是否通过 commitlint 的检查。)

4. 🚚 如何从 multirepo 迁移至使用 monorepo 策略?

至此,我们学会了如何采用 monorepo 策略组织项目代码的最佳实践,或许您已经开始跃跃欲试想要尝试前文提到的种种技巧。从 0 搭建一个 monorepo 项目,当然没问题!可是如果要基于已有的项目,将其转化为一个使用 monorepo 策略的项目呢?

还记得吗?成百里者半九十,您还有一些坑要踩。不过好在您在这里还能够得到我的帮助,不必客气!

或许您注意到了,Lerna 为我们提供了 lerna import 命令,用来将我们已有的包导入到 monorepo 仓库,并且还会保留该仓库的所有 commit 信息。然而实际上,该命令仅支持导入本地项目,并且不支持导入项目的分支和标签 🙃。

那么如果我们想要导入远程仓库,或是要获取某个分支或标签该怎么做呢?答案是使用 tomono,其内容是一个 shell 脚本。

使用 tomono 导入远程仓库,您所需要做的只有两件事:

  1. 创建一个包含所有需要导入 repo 地址的文本文件;
  2. 执行 shell 命令:cat repos.txt | ~/tomono/tomono.sh(这里我们假定您的文本文件名为 repos.txt,且您将 tomono 下载在用户根目录;

repo 文件内容示例如下:

// 1. Git仓库地址  2. 子项目名称  3. 迁移后的路径
git@github.com/backend.git @mono/backend packages/backend
git@github.com/frontend.git @mono/frontend packages/frontend
git@github.com/mobile.git @mono/mobile packages/mobile

至此,我们也掌握了将现有项目迁移至 monorepo 项目的方法。到这时候,您已绝非再是 monorepo 界的门外汉!

恭喜您 !!🎉

5. 🎓 小结

在本篇文章中,我们共同了解了「什么是 monorepo 策略」以及「monorepo 策略的优劣」,并且一起学习实践了 monorepo 策略的一些最佳实践。您一定也意识到,即使您的工作场景暂时无法实践 monorepo 策略,阅读本篇文章所学习到的种种方法,工具和思想也可以运用到您当下的工作之中。

当然,本文所介绍的这些方法和思想总有过时的一天,并且社区也从未停止对更好地实践 monorepo 策略的探索,说不定您过一阵子就会有更好的想法 ,填补某个领域的空白。希望到时候您也能总结出一篇文章,为 JavaScript 社区贡献一份力量。到时候请千万别忘了回到我的评论区留言,让我分享您的成就。

关于 monorepo 这个主题,我就暂且带您探索到这里,后会有期:)

6. 📝 参考文献

  1. 📹 JavaScript and TypeScript Monorepos
  2. 📄 Why you should use a single repository for all your company’s projects
  3. 📄 Advantages of monorepos
  4. 📄 lerna管理前端packages的最佳实践
  5. 📄 基于lerna和yarn workspace的monorepo工作流
  6. 📄 Monorepos in the Wild
  7. 📄 Monorepos: Please don’t!
  8. 📄 Monorepo: please do!
  9. 📄 Introduction to Lerna
  10. 📄 monorepo 迁移实践

7. 👀 扩展阅读

  1. 介绍实践 monorepo 生态:awesome-monorepo
  2. 一篇介绍 Google 如何将数十亿代码通过 monorepo 方式组织的论文:Why Google Stores Billions of Lines of Code in a Single Repository
  3. 一篇针对 Google 的调研报告,详尽地分析了 monorepo 的优劣: Advantages and Disadvantages of a Monolithic Repository

8. 🙌 招聘信息

阿里巴巴淘系用户增长团队正在如饥似渴的寻找志同道合的伙伴,如果您准备好迎接适度的挑战,在让更多人喜欢手淘的同时,也让自己快速成长,欢迎您发送简历至我的邮箱:kongtang.lb@alibaba-inc.com,我十分期待收到您的讯息。

  • 封面图片来源:Photo by Tetiana SHYSHKINA on Unsplash
  • 本文仅支持有偿转载,请联系作者洽谈转载费用
查看原文

赞 18 收藏 9 评论 0

libinfs 发布了文章 · 1月31日

使用 webpack 代码分割和魔术注释提升应用性能

1. Web 应用性能优化的关键

关于 Web 应用性能优化,有一点是毫无疑问的:「页面加载越久,用户体验就越差」。我们几乎可以说 Web 应用性能优化的关键之处就在于:减少页面初载时,所需加载资源的「数量」和「体积」。

那么当所需加载的资源数量到达多少或资源体积小于多少,我们才可以自信地宣称我们的 Web 应用拥有出色的性能呢?下面是给出的一些参考值,该参考值考虑到了移动端与国外等多种访问环境:

  • 页面初载时,所有未压缩的 JavaScript 脚本大小:<=200KB
  • 页面初载时,所有未压缩的 CSS 资源大小:<=100KB
  • HTTP 协议下,请求资源数:<=6 个
  • HTTP/2 协议下,请求资源数:<=20 个
  • 90% 的代码利用率(也就是说,仅允许 10% 的未使用代码);

或许你会觉得这个标准有点过于苛刻了,是有一点点,但为了创建出高性能的 Web 应用,你的实际资源加载情况应该尽可能靠近这个目标。

2. 如何查看代码利用率

也许你注意到了,我们刚刚最后提到的一个指标是「代码利用率」,你可能是第一次听说这个概念,这里我解释一下它的计算方式:

代码利用率 = 你页面中实际被执行的代码 / 你页面中引入的代码 * 100%

你可能会困惑在实际开发中如何得到这个值,别担心,通过使用 Chrome 开发者工具(很遗憾,目前只有 Chrome 支持这一功能),你就可以迅速对你的 Web 应用进行分析,得到当前页面下的代码利用率状态,步骤如下:

  1. 打开 Chrome Dev Tool;
  2. 按下 Cmd + Shift + P or Ctrl + Shift + P ;
  3. 输入 Coverage,并选择第一个出现的选项(图 1);

img

<p style="text-align: center; color: #999;">图 1:直接输入 coverage 即可开启该看板</p>

  1. 点击面板上的 reload 按钮,查看整个应用 JavaScript 的代码利用率(图 2);

img

<p style="text-align: center; color: #999;">图 2:点击 reload 按钮,开始分析 JavaScript 代码利用率</p>

3. 提高代码使用率的关键技术 - 代码分割(code splitting)

3.1 什么是「代码分割」(code splitting)?

代码分割是指,将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程。

在 Webpack 构建时,会避免加载已声明要异步加载的代码 ,异步代码会被单独分离出一个文件,当代码实际调用时被加载至页面。

3.2 代码分割的原理

代码分割技术的核心是「异步加载资源」,可喜的是,浏览器允许我们这么做,W3C stage 3 规范:whatwg/loader 对其进行了定义:你可以通过 import() 关键字让浏览器在程序执行时异步加载相关资源。

img

<p style="text-align: center; color: #999;">图 3:import() 的浏览器支持性</p>

没错,正如你所看到的, IE 浏览器目前并不支持这一特性,但这并不意味着你的异步加载功能在 IE 浏览会失效(那太可怕了 🤦‍♂️),实际上,Webpack 底层帮你将异步加载的代码抽离成一份新的文件,并在你需要时通过 JSONP 的方式去获取文件资源,因此,你可以在任何浏览器上实现代码的异步加载,并且在将来所有浏览器都实现 import() 方法时平滑过渡,cool!👍

3.3 代码分割的类型

代码分割可以分为「静态分割」和「"动态"分割」两种方式,注意这里打了引号的 "动态",因为实际上它并不意味着异步调用的代码是 "动态" 生成的,我们之后会看到 Webpack 是如何做到这一点的,在那之前,让我们先看看「静态代码分割」。

3.3.1 静态代码分割

静态代码分割是指,在代码中明确声明需要异步加载的代码

下面 👇 的代码说明了我们应该如何使用这一技术:

import Listener from './listeners.js'
const getModal = () => import('./src/modal.js') 
Listener.on(
  'didSomethingToWarrentModalBeingLoaded',
  () => {
    // Async fetching modal code from a separate chunk
    getModal().then(
      (module) => {
        const modalTarget = document.getElementById('Modal')    
        module.initModal(modalTarget)  
      })
  }
)

正如你所看到的:

每当你调用一个声明了异步加载代码的变量时,它总是返回一个 Promise 对象。

⚠️ 注意:在 Vue 中,可以直接使用 import() 关键字做到这一点,而在 React 中,你需要使用 react-loadable 去完成同样的事。

最后,让我们谈谈何时使用静态代码分割技术,这一技术适合以下的场景:

  1. 你正在使用一个非常大的库或框架:如果在页面初始化时你不需要使用它,就不要在页面初载时加载它;
  2. 加载任何临时的资源:指不在页面初始化时被使用,被使用后又会立即被销毁的资源,例如模态框,对话框,tooltip 等(任何一开始不显示在页面上的东西都可以有条件的加载);
  3. 页面路由:既然用户不会一下子看到所有页面,那么只把当前页面相关资源给用户就是个明智的做法;

好了,现在你掌握了静态代码分割技术,现在让我们看看什么是「"动态代"代码分割」技术。

3.3.2 动态代码分割

动态代码分割是指:在代码调用时根据当前的状态,「动态地」异步加载对应的代码块

下面 👇 的代码说明了它具体是如何被实现的:

const getTheme = (themeName) => import(`./src/themes/${themeName}`)
// using `import()` 'dynamically'
if (window.feeling.stylish) {
  getTheme('stylish').then((module) => {
    module.applyTheme()
  })
} else if (window.feeling.trendy) {
  getTheme('trendy').then((module) => {
    module.applyTheme()
  })
}

看到了吗,我们 "动态" 的声明了我们要异步加载的代码块,这是怎么做到的?!

答案出乎意料的简单,Webpack 会在构建时将你声明的目录下的所有可能分离的代码都抽象为一个文件(这被称为 contextModule 模块),因此无论你最终声明了调用哪个文件,本质上就和静态代码分割一样,在请求一个早已准备好的,静态的文件。

下面是一些使用 "动态" 代码分割技术的场景:

  1. A/B Test:你不需要在代码中引入不需要的 UI 代码;
  2. 加载主题:根据用户的设置,动态加载相应的主题;
  3. 为了方便 :本质上,你可以用静态代码分割代替「动态」代码分割,但是后者比前者拥有更少的代码量;

4.魔术注释

魔术注释是由 Webpack 提供的,可以为代码分割服务的一种技术。通过在 import 关键字后的括号中使用指定注释,我们可以对代码分割后的 chunk 有更多的控制权,让我们看一个例子:

// index.js
import (
  /* webpackChunkName: "my-chunk-name" */
  './footer'
)
同时,也要在 webpack.config.js 中做一些改动:
// webpack.config.js
{
  output: {
    filename: "bundle.js",
    chunkFilename: "[name].lazy-chunk.js"
  }
}

通过这样的配置,我们可以对分离出的 chunk 进行命名,这对于我们 debug 而言非常方便。

4.1 Webpack Modes

除了上面提到过的 webpackChunkName 注释外,Webpack 还提供了一些其他注释让我们能够对异步加载模块拥有更多控制权,例如下方这个例子:

import (
  /* webpackChunkName: "my-chunk-name" */
  /* webpackMode: lazy */
  './someModule'
)

webpackMode 的默认值为 lazy 它会使所有异步模块都会被单独抽离成单一的 chunk,若设置该值为 lazy-once,Webpack 就会将所有带有标记的异步加载模块放在同一个 chunk 中。

4.2 Prefetch or Preload

通过添加 webpackPrefetch 魔术注释,Webpack 令我们可以使用与 <link rel="prefetch"> 相同的特性。让浏览器会在 Idle 状态时预先帮我们加载所需的资源,善用这个技术可以使我们的应用交互变得更加流畅。

import(
  /* webpackPrefetch: true */
  './someModule'
)

⚠️ 注意:请确保你的代码在未来一定会用到时,再开启该功能。

5. 小结

至此,我们讲解了所有有关 Code Splitting 的知识,并告诉你了一些神奇的「魔法注释」让你对分割后的代码有更多的掌控,希望你能将上面的技术灵活运用在你的项目中,开发出更加激动人心,如丝般顺滑的应用!

Good Luck!🙌

查看原文

赞 14 收藏 11 评论 0

libinfs 发布了文章 · 1月18日

2020 年 JavaScript 状态调研报告小结

一年一度的 Discover the State of JS 2020 results 在前几天新鲜出炉了,每次阅读这份报告都能帮助我快速地了解到 JavaScript 世界在这一年里都发生了哪些事情,同时也给了我一次查漏补缺的机会,让我十分收益。

今年我打算以文字的方式,和大家快速分享一下这份报告在「语法」和「框架」两个部分所释放出的信息,希望能够对大家有所帮助和启发。

今年的调查覆盖率了 137 个国家的 23,765 个人,大多数被调研者来自美国或西欧。报告地址:https://2020.stateofjs.com/en...

1. Features

这一部分我将会针对 ES6 以来,新的语法特性的使用情况进行概括和总结,并偶尔发表一些自己的看法,如果您对某个内容有自己的见解,也欢迎您在文章的评论区下方留言。

注意每个语法特性背后的百分数代表着:被调研的开发者中使用过该特性的人数占比


1.1 语法特性

语法特性方面,像 Destructuring(89.1%) , Spread Operator (92.8%), Arrow Functions (97.9%)这样便宜好用又大碗的语法特性已经被广大开发者运用的滚瓜烂熟。但是像 Nullish Coalescing (45.3%), Optional Chaining (66.7%)这样同样好用的不得了的语法特性看起来并没有被普及开来广泛使用,不想代码里再有丑陋的 a && a.b && a.b.c 判断符,直接上手就来一个 a?.b?.c 实在是既潇洒又酷。

毫不意外 Private Fields (10.9%)这个语法特性不仅使用的人不多,而且 43.9% 的被调研开发者表示听都没听说过。我特地去查了下,这个语法特性是 ES2020 草案提出的,目前 Firefox,IE 和 Safari 还不支持。

但是这个语法特性表示 JavaScript 终终终于要有语法层面的私有类字段了,很高兴看到 JavaScript 这门基于原型链的语言在 OOP 范式上又前进了一小步,不知道 Java 开发者不知道会不会感到非常开心?


1.2 语言特性

Async/Await (95.2%), Promises (96.2%)这样的老牌异步解决方案看来已经是耳熟能详,被开发者广泛使用了。但是像Decorators(47.4%), Dynamic Import (42.8%)语法使用的人却并不多,至于 Proxies (22.3%) Promise.allSettled() (14.7%)这两个语法则更是不仅使用的人不多,连没听说过的人都不少。

如果说平时写业务很难用到像 Proxy 这样的对象代理方案,用的人少还情有可原。像 Dynamic Import 这种动态加载资源的方案配合上 webpack 打包出异步加载的 chunk 一起使用,绝对是页面性能进一步优化的大杀器,还不了解同学可以深度研究一下。

Promise.allSettled 这个方法终于补齐了 Promise 系列的全家桶,原来的 Promise.all 方法只在异步执行的函数集相互依赖时有效,碰上想要了解每个异步函数解决状态的情况,还是 Promise.allsettled 方法更好使。


1.3 数据结构

数据结构方面,Maps (73.4%), Sets (66.9%)这样的数据结构已经比较广泛的被开发者们使用,而像Typed Arrays (34.9%), Array.prototype.flat() (39.6%)这样的数据结构和新语法则较少被用在工作之中,BigInt (13.9%)的使用率最低,但一般开发需求中也的确用不上。

有一说一,Array.prototype.flat() 这个方法其实挺好用的,虽然我们可以通过 Spread Operator 快速将一个 2 层嵌套的数组「拍平」变成一个一维数组,但是当我们需要对一个多于 2 层的数组进行「拍平」时,通过向 flat() 方法中传入参数的方式,显然更加方便。


1.4 浏览器 API

Local Storage (90.6%), Fetch (87.1%)这种今年看来已经不再新鲜的 API 毫无疑问大家都在用,也确实在存储和 HTTP 请求上没有什么更好的原生方案。

WebSocket 62.6% 的使用率,Service Workers 42% 的使用率和 Intl 31.3% 的使用率也算是合情合理,毕竟受使用场景限制。

Shadow DOM (42.1%) ,Custom Elements (33.4%)无疑是今年最令人疯狂的浏览器 API 了,想想不通过使用 React 和 Vue,仅通过浏览器原生提供的功能就能实现高效可复用的组件化,生命周期函数什么的也一应俱全,仿佛好不容易学会的 React 好像明天就要过时,JavaScript 原教旨主义者终于一统天下。

可是别高兴的太早,现实还是很骨感,别说现在还没有像 Fusion,Antd 一样成熟的 UI 组件库可以开箱即用,如何通过这些 API 稳定搭建 SPA 应用,整个社区还没有讨论出一个像 React,Vue 和 AngularJS 一样的成熟方案,所以还是先等等吧,先熟悉一下 API 总是没错的。

至于像 Web Audio (20%), WebGL (17.5%), Web Animations (16.3%),WebRTC (14%),Web Speech API (8.2%), WebVR (3.3%)这些只有特定开发需求才会使用的 API,使用的人少也是十分正常,但是可千万不要因此就忽略了这些 API。

Web AudioWeb SpeechWebRTC 对于影音视频流的传输和交互就非常重要,WebGLWeb AnimationsWebVR 则更是将 Web 世界的表达能力拉高了好几个台阶。我觉的大家真该好好想想如何结合自身的业务场景通过这些浏览器能力寻求更新的突破,说不定下个风口或是交互模式创新就诞生在你的团队。到时候可千万别忘了给我发个红包(笑)。


1.5. 其他

最后我们再看看 WebAssembly (WASM) 的调研情况,真正使用过的开发者占比为 10.5%,73.9% 的开发者听过但是没用过,15.6% 的开发者则是听都没听说过。

我觉得大多数前端开发者应该都处于听过没用过的象限,目前社区关于 WebAssembly 也确实没有很大的音量。用 C++ 和 Rust 编写 Web 应用这种事情对于 Web 开发者而言也的确没有多大吸引力。未来的发展如何,我还是抱着静观其变的态度。


2. 技术框架

技术框架部分我将重点关注技术框架的使用数量以及对框架的满意程度两个方面,它们代表了当前流行的技术选型以及未来可能流行的技术方向。每一种技术我都会附带 🔗 链接,方便您点击了解更多技术细节。


2.1 语言风格

2020 年对于 JavaScript 究竟应该怎么写才对味这个问题, TypeScript 毫无争议地一锤定音,93% 的参与调研者表示十分满意通过 Typescript 约束自己的 Javascript 代码,看来这个年头还不拥抱 Typescript 的开发者绝对是 out 了。

而对于当前的语言风格是否令人满意的调研则表示,在满分 5 分的限定下,无论是 2019 年还是 2020 年,开发者们都只打了 3.6 分这样差强人意的分数来表达 JavaScript 在更优雅的编写方面还有很多探索的空间。


2.2 前端框架

前端框架方面 ReactAngularVue.js 毫无疑问地依然是世界三大框架。但说出来你可能不信,「最令人满意的前端框架」居然不是 React 而是 2019 年才由 Rich Harris 推出的 Svelte。有 66% 的被调研者表示感兴趣这个框架,并且 89% 的被调研者表示使用这个框架令他们感到十分满意,总之一句话,用过的都说好。

Svelte 人如其名,强调在构建时就直接产出最小的完整的代码,从而在使用时可以直接使用构建后的组件,而无需添加框架自身,因此不仅打包后的应用代码体积更小,由于没有 diff 操作,性能也大幅提高。只可惜目前 Svelte 还不支持 Typescript,也没听说过哪些大型项目在使用,否则众多前端开发er 们可就又有的学了。


2.3 数据层

数据层框架上国外火的一塌糊涂,国内却怎么也火不起来的 GraphQL 依旧是数据层框架排行榜的万年老二,使用最多的状态管理框架依旧是耳熟能详的Redux。沾着 GraphQL 和 React 的光,Apollo Client 近三年来也一直稳稳地占据了排行榜第三名的位置。

比较有意思的是 2020 年异军突起的两大框架 VuexXState 迅速的从老牌状态管理框架 ReduxMobX 的身体上越过分别获得了最受开发者满意排场榜上第三名和第四名的好成绩。我 Vuex 倒是没怎么用过,但是 XState 倒是实打实调研了一把,确实是物有所值的好框架,特别是最近流行的逻辑编排,状态编排,各种编排,配上自带的流程图,不仅立刻感觉高大上了很多,而且确实切实解决程序状态复杂后,难以梳理清楚的老问题。


2.4 后端框架

我最近一年几乎没怎么写服务端应用,通过调研报告才发现我用的最熟练的 Koa 的流行度已经连年下跌,到了使用度排名的中部位置。现在 Next.jsExpress 才是开发服务端应用的首选,并且也是用过的都说好。仔细一看 Hulu,Docker,Netflix 都在用 Next.js,和我一样掉队的同学真应该好好补补功课。


2.5 测试框架

说到测试框架,自从 2019 年 JestMocha 手中抢过龙头棍,从此就一直稳坐测试框架届的头把交椅。 在使用度排名上,Mocha 和 Storybook 紧随其后,但是看起来似乎不可能撼动 Jest 的江湖地位。

比较值得注意的是,由 Kent C. Dodds 开发的 Testing Library 测试框架一经发布就引来了很多前端开发者的关注。Testing Library 主打 DOM 测试,全面支持主流的三大框架,提供一堆好用不贵的 API,用起来那叫一个符合用户使用习惯。可惜国内的开发者大多都不重视单元测试这块,更别提是 DOM 元素级别的测试,我大胆预测下 Testing Library 在国内会像 GraphQL 一样一直保持不温不火的状态,确实可惜但也没办法。


2.6 构建工具

说到构建工具,那是真的有的聊了,虽然 webpack 依然以 89% 的使用率独占鳌头,但是要看众多开发者 2020 年感兴趣或是满意度高的构建工具,你会惊讶地发现曾经如日中天的 gulp.jsBrowserify 已经渐渐显露出中年危机的势头,而 webpack 也在今年跌落神坛,在最受用户满意的构建工具排行榜只排到了第四名。

要说第三名被 TypeScript 抢去还能理解,前两名分别是 esbuild Snowpack 我相信很多国内的开发者听到一定一头雾水,但其实分别去官网看看就能清楚这两个构建工具主打的还是构建速度的提升,尤其是 esbuild ,从官网上给的数据来看要比 webpack 构建速度提升了整整 113 倍。

老实说,随着项目越来越大,再加上 monorepo 方案逐渐在国内流行开来,构建时长有时候真是直接影响着开发体验,一个项目 build 十几秒,怎么看也不像是前端开发应该出现的场景,无论是 esbuild 还是 Snowpack,如果有机会,还是鼓励大家多去尝试,总结经验,造福社区。


2.7 应用端 / 桌面端

毫无疑问,要想用 JS 编写桌面端应用,最好的框架绝对是 Electron。但如果要是开发移动端应用的话,2020 年选择就不止有 React Native 了,2020 年新登台的 Capacitor 同样十分亮眼,虽然只有 10% 的被调研者真正在使用,但是其中 84% 的开发者都表示使用起来十分令人满意。

但是从使用体验上看,整体的移动端/桌面端框架的满意度并不高,近几年基本保持在 3 分左右的状态,看来前端想要在各个端上实现反复左右横跳,还需要更加具有突破性的技术创新。


3. 小结

以上就是 2020 年 JavaScript 整体状态的快速一览,总的来说,无论是语言特性还是各种框架和库,在 2020 年,都没有什么突破性的变化,爆发式的增长。但是仔细观察你会发现实际上在各个细分领域,都有些创新和实践在悄悄地发生,比如构建性能的提升,前端测试的完善,Web 表达的丰富等等等等。

一些前端领域老大难的问题,如何更高性能地实践组件化,如何真正实现 JavaScript 的「一次开发,处处运行」依旧没有一个盖棺定论,换句话说还在等待着更聪明的开发者来解决。

如果非要问 2020 年最红的技术是什么的话,我非常不客观地认为本届的奖杯毫无疑问地要颁发给 TypeScript,随着 TypeScript 新版本的更新,编写前端代码真是越来越对味。

以上,如果您喜欢这篇文章,别忘记点赞或是分享,让更多人看到。这些都会被我视为您对我创作的支持。


4. 广告

阿里巴巴淘系用户增长团队正在如饥似渴的寻找志同道合的伙伴,如果您准备好迎接适度的挑战,在让更多人喜欢手淘的同时,让自己快速成长,欢迎您发送简历至我的邮箱:kongtang.lb@alibaba-inc.com,我十分期待收到您的讯息。

查看原文

赞 17 收藏 12 评论 3

libinfs 收藏了文章 · 2019-08-14

服务端高并发分布式架构演进之路

1. 概述

本文以淘宝作为例子,介绍从一百个到千万级并发情况下服务端的架构的演进过程,同时列举出每个演进阶段会遇到的相关技术,让大家对架构的演进有一个整体的认知,文章最后汇总了一些架构设计的原则。

特别说明:本文以淘宝为例仅仅是为了便于说明演进过程可能遇到的问题,并非是淘宝真正的技术演进路径

2. 基本概念

在介绍架构之前,为了避免部分读者对架构设计中的一些概念不了解,下面对几个最基础的概念进行介绍:

  • 分布式
    系统中的多个模块在不同服务器上部署,即可称为分布式系统,如Tomcat和数据库分别部署在不同的服务器上,或两个相同功能的Tomcat分别部署在不同服务器上
  • 高可用
    系统中部分节点失效时,其他节点能够接替它继续提供服务,则可认为系统具有高可用性
  • 集群
    一个特定领域的软件部署在多台服务器上并作为一个整体提供一类服务,这个整体称为集群。如Zookeeper中的Master和Slave分别部署在多台服务器上,共同组成一个整体提供集中配置服务。在常见的集群中,客户端往往能够连接任意一个节点获得服务,并且当集群中一个节点掉线时,其他节点往往能够自动的接替它继续提供服务,这时候说明集群具有高可用性
  • 负载均衡
    请求发送到系统时,通过某些方式把请求均匀分发到多个节点上,使系统中每个节点能够均匀的处理请求负载,则可认为系统是负载均衡的
  • 正向代理和反向代理
    系统内部要访问外部网络时,统一通过一个代理服务器把请求转发出去,在外部网络看来就是代理服务器发起的访问,此时代理服务器实现的是正向代理;当外部请求进入系统时,代理服务器把该请求转发到系统中的某台服务器上,对外部请求来说,与之交互的只有代理服务器,此时代理服务器实现的是反向代理。简单来说,正向代理是代理服务器代替系统内部来访问外部网络的过程,反向代理是外部请求访问系统时通过代理服务器转发到内部服务器的过程。

3. 架构演进

3.1 单机架构

clipboard.png

以淘宝作为例子。在网站最初时,应用数量与用户数都较少,可以把Tomcat和数据库部署在同一台服务器上。浏览器往www.taobao.com发起请求时,首先经过DNS服务器(域名系统)把域名转换为实际IP地址10.102.4.1,浏览器转而访问该IP对应的Tomcat。

随着用户数的增长,Tomcat和数据库之间竞争资源,单机性能不足以支撑业务

3.2 第一次演进:Tomcat与数据库分开部署

clipboard.png

Tomcat和数据库分别独占服务器资源,显著提高两者各自性能。

随着用户数的增长,并发读写数据库成为瓶颈

3.3 第二次演进:引入本地缓存和分布式缓存

clipboard.png

在Tomcat同服务器上或同JVM中增加本地缓存,并在外部增加分布式缓存,缓存热门商品信息或热门商品的html页面等。通过缓存能把绝大多数请求在读写数据库前拦截掉,大大降低数据库压力。其中涉及的技术包括:使用memcached作为本地缓存,使用Redis作为分布式缓存,还会涉及缓存一致性、缓存穿透/击穿、缓存雪崩、热点数据集中失效等问题。

缓存抗住了大部分的访问请求,随着用户数的增长,并发压力主要落在单机的Tomcat上,响应逐渐变慢

3.4 第三次演进:引入反向代理实现负载均衡

clipboard.png

在多台服务器上分别部署Tomcat,使用反向代理软件(Nginx)把请求均匀分发到每个Tomcat中。此处假设Tomcat最多支持100个并发,Nginx最多支持50000个并发,那么理论上Nginx把请求分发到500个Tomcat上,就能抗住50000个并发。其中涉及的技术包括:Nginx、HAProxy,两者都是工作在网络第七层的反向代理软件,主要支持http协议,还会涉及session共享、文件上传下载的问题。

反向代理使应用服务器可支持的并发量大大增加,但并发量的增长也意味着更多请求穿透到数据库,单机的数据库最终成为瓶颈

3.5 第四次演进:数据库读写分离

clipboard.png

把数据库划分为读库和写库,读库可以有多个,通过同步机制把写库的数据同步到读库,对于需要查询最新写入数据场景,可通过在缓存中多写一份,通过缓存获得最新数据。其中涉及的技术包括:Mycat,它是数据库中间件,可通过它来组织数据库的分离读写和分库分表,客户端通过它来访问下层数据库,还会涉及数据同步,数据一致性的问题。

业务逐渐变多,不同业务之间的访问量差距较大,不同业务直接竞争数据库,相互影响性能

3.6 第五次演进:数据库按业务分库

clipboard.png

把不同业务的数据保存到不同的数据库中,使业务之间的资源竞争降低,对于访问量大的业务,可以部署更多的服务器来支撑。这样同时导致跨业务的表无法直接做关联分析,需要通过其他途径来解决,但这不是本文讨论的重点,有兴趣的可以自行搜索解决方案。

随着用户数的增长,单机的写库会逐渐会达到性能瓶颈

3.7 第六次演进:把大表拆分为小表

clipboard.png

比如针对评论数据,可按照商品ID进行hash,路由到对应的表中存储;针对支付记录,可按照小时创建表,每个小时表继续拆分为小表,使用用户ID或记录编号来路由数据。只要实时操作的表数据量足够小,请求能够足够均匀的分发到多台服务器上的小表,那数据库就能通过水平扩展的方式来提高性能。其中前面提到的Mycat也支持在大表拆分为小表情况下的访问控制。

这种做法显著的增加了数据库运维的难度,对DBA的要求较高。数据库设计到这种结构时,已经可以称为分布式数据库,但是这只是一个逻辑的数据库整体,数据库里不同的组成部分是由不同的组件单独来实现的,如分库分表的管理和请求分发,由Mycat实现,SQL的解析由单机的数据库实现,读写分离可能由网关和消息队列来实现,查询结果的汇总可能由数据库接口层来实现等等,这种架构其实是MPP(大规模并行处理)架构的一类实现。

目前开源和商用都已经有不少MPP数据库,开源中比较流行的有Greenplum、TiDB、Postgresql XC、HAWQ等,商用的如南大通用的GBase、睿帆科技的雪球DB、华为的LibrA等等,不同的MPP数据库的侧重点也不一样,如TiDB更侧重于分布式OLTP场景,Greenplum更侧重于分布式OLAP场景,这些MPP数据库基本都提供了类似Postgresql、Oracle、MySQL那样的SQL标准支持能力,能把一个查询解析为分布式的执行计划分发到每台机器上并行执行,最终由数据库本身汇总数据进行返回,也提供了诸如权限管理、分库分表、事务、数据副本等能力,并且大多能够支持100个节点以上的集群,大大降低了数据库运维的成本,并且使数据库也能够实现水平扩展。

数据库和Tomcat都能够水平扩展,可支撑的并发大幅提高,随着用户数的增长,最终单机的Nginx会成为瓶颈

3.8 第七次演进:使用LVS或F5来使多个Nginx负载均衡

clipboard.png

由于瓶颈在Nginx,因此无法通过两层的Nginx来实现多个Nginx的负载均衡。图中的LVS和F5是工作在网络第四层的负载均衡解决方案,其中LVS是软件,运行在操作系统内核态,可对TCP请求或更高层级的网络协议进行转发,因此支持的协议更丰富,并且性能也远高于Nginx,可假设单机的LVS可支持几十万个并发的请求转发;F5是一种负载均衡硬件,与LVS提供的能力类似,性能比LVS更高,但价格昂贵。由于LVS是单机版的软件,若LVS所在服务器宕机则会导致整个后端系统都无法访问,因此需要有备用节点。可使用keepalived软件模拟出虚拟IP,然后把虚拟IP绑定到多台LVS服务器上,浏览器访问虚拟IP时,会被路由器重定向到真实的LVS服务器,当主LVS服务器宕机时,keepalived软件会自动更新路由器中的路由表,把虚拟IP重定向到另外一台正常的LVS服务器,从而达到LVS服务器高可用的效果。

此处需要注意的是,上图中从Nginx层到Tomcat层这样画并不代表全部Nginx都转发请求到全部的Tomcat,在实际使用时,可能会是几个Nginx下面接一部分的Tomcat,这些Nginx之间通过keepalived实现高可用,其他的Nginx接另外的Tomcat,这样可接入的Tomcat数量就能成倍的增加。

由于LVS也是单机的,随着并发数增长到几十万时,LVS服务器最终会达到瓶颈,此时用户数达到千万甚至上亿级别,用户分布在不同的地区,与服务器机房距离不同,导致了访问的延迟会明显不同

3.9 第八次演进:通过DNS轮询实现机房间的负载均衡

clipboard.png

在DNS服务器中可配置一个域名对应多个IP地址,每个IP地址对应到不同的机房里的虚拟IP。当用户访问www.taobao.com时,DNS服务器会使用轮询策略或其他策略,来选择某个IP供用户访问。此方式能实现机房间的负载均衡,至此,系统可做到机房级别的水平扩展,千万级到亿级的并发量都可通过增加机房来解决,系统入口处的请求并发量不再是问题。

随着数据的丰富程度和业务的发展,检索、分析等需求越来越丰富,单单依靠数据库无法解决如此丰富的需求

3.10 第九次演进:引入NoSQL数据库和搜索引擎等技术

clipboard.png

当数据库中的数据多到一定规模时,数据库就不适用于复杂的查询了,往往只能满足普通查询的场景。对于统计报表场景,在数据量大时不一定能跑出结果,而且在跑复杂查询时会导致其他查询变慢,对于全文检索、可变数据结构等场景,数据库天生不适用。因此需要针对特定的场景,引入合适的解决方案。如对于海量文件存储,可通过分布式文件系统HDFS解决,对于key value类型的数据,可通过HBase和Redis等方案解决,对于全文检索场景,可通过搜索引擎如ElasticSearch解决,对于多维分析场景,可通过Kylin或Druid等方案解决。

当然,引入更多组件同时会提高系统的复杂度,不同的组件保存的数据需要同步,需要考虑一致性的问题,需要有更多的运维手段来管理这些组件等。

引入更多组件解决了丰富的需求,业务维度能够极大扩充,随之而来的是一个应用中包含了太多的业务代码,业务的升级迭代变得困难

3.11 第十次演进:大应用拆分为小应用

clipboard.png

按照业务板块来划分应用代码,使单个应用的职责更清晰,相互之间可以做到独立升级迭代。这时候应用之间可能会涉及到一些公共配置,可以通过分布式配置中心Zookeeper来解决。

不同应用之间存在共用的模块,由应用单独管理会导致相同代码存在多份,导致公共功能升级时全部应用代码都要跟着升级

3.12 第十一次演进:复用的功能抽离成微服务

clipboard.png

如用户管理、订单、支付、鉴权等功能在多个应用中都存在,那么可以把这些功能的代码单独抽取出来形成一个单独的服务来管理,这样的服务就是所谓的微服务,应用和服务之间通过HTTP、TCP或RPC请求等多种方式来访问公共服务,每个单独的服务都可以由单独的团队来管理。此外,可以通过Dubbo、SpringCloud等框架实现服务治理、限流、熔断、降级等功能,提高服务的稳定性和可用性。

不同服务的接口访问方式不同,应用代码需要适配多种访问方式才能使用服务,此外,应用访问服务,服务之间也可能相互访问,调用链将会变得非常复杂,逻辑变得混乱

3.13 第十二次演进:引入企业服务总线ESB屏蔽服务接口的访问差异

clipboard.png

通过ESB统一进行访问协议转换,应用统一通过ESB来访问后端服务,服务与服务之间也通过ESB来相互调用,以此降低系统的耦合程度。这种单个应用拆分为多个应用,公共服务单独抽取出来来管理,并使用企业消息总线来解除服务之间耦合问题的架构,就是所谓的SOA(面向服务)架构,这种架构与微服务架构容易混淆,因为表现形式十分相似。个人理解,微服务架构更多是指把系统里的公共服务抽取出来单独运维管理的思想,而SOA架构则是指一种拆分服务并使服务接口访问变得统一的架构思想,SOA架构中包含了微服务的思想。

业务不断发展,应用和服务都会不断变多,应用和服务的部署变得复杂,同一台服务器上部署多个服务还要解决运行环境冲突的问题,此外,对于如大促这类需要动态扩缩容的场景,需要水平扩展服务的性能,就需要在新增的服务上准备运行环境,部署服务等,运维将变得十分困难

3.14 第十三次演进:引入容器化技术实现运行环境隔离与动态服务管理

clipboard.png

目前最流行的容器化技术是Docker,最流行的容器管理服务是Kubernetes(K8S),应用/服务可以打包为Docker镜像,通过K8S来动态分发和部署镜像。Docker镜像可理解为一个能运行你的应用/服务的最小的操作系统,里面放着应用/服务的运行代码,运行环境根据实际的需要设置好。把整个“操作系统”打包为一个镜像后,就可以分发到需要部署相关服务的机器上,直接启动Docker镜像就可以把服务起起来,使服务的部署和运维变得简单。

在大促的之前,可以在现有的机器集群上划分出服务器来启动Docker镜像,增强服务的性能,大促过后就可以关闭镜像,对机器上的其他服务不造成影响(在3.14节之前,服务运行在新增机器上需要修改系统配置来适配服务,这会导致机器上其他服务需要的运行环境被破坏)。

使用容器化技术后服务动态扩缩容问题得以解决,但是机器还是需要公司自身来管理,在非大促的时候,还是需要闲置着大量的机器资源来应对大促,机器自身成本和运维成本都极高,资源利用率低

3.15 第十四次演进:以云平台承载系统

clipboard.png

系统可部署到公有云上,利用公有云的海量机器资源,解决动态硬件资源的问题,在大促的时间段里,在云平台中临时申请更多的资源,结合Docker和K8S来快速部署服务,在大促结束后释放资源,真正做到按需付费,资源利用率大大提高,同时大大降低了运维成本。

所谓的云平台,就是把海量机器资源,通过统一的资源管理,抽象为一个资源整体,在之上可按需动态申请硬件资源(如CPU、内存、网络等),并且之上提供通用的操作系统,提供常用的技术组件(如Hadoop技术栈,MPP数据库等)供用户使用,甚至提供开发好的应用,用户不需要关系应用内部使用了什么技术,就能够解决需求(如音视频转码服务、邮件服务、个人博客等)。在云平台中会涉及如下几个概念:

  • IaaS:基础设施即服务。对应于上面所说的机器资源统一为资源整体,可动态申请硬件资源的层面;
  • PaaS:平台即服务。对应于上面所说的提供常用的技术组件方便系统的开发和维护;
  • SaaS:软件即服务。对应于上面所说的提供开发好的应用或服务,按功能或性能要求付费。
至此,以上所提到的从高并发访问问题,到服务的架构和系统实施的层面都有了各自的解决方案,但同时也应该意识到,在上面的介绍中,其实是有意忽略了诸如跨机房数据同步、分布式事务实现等等的实际问题,这些问题以后有机会再拿出来单独讨论

4. 架构设计总结

  • 架构的调整是否必须按照上述演变路径进行?
    不是的,以上所说的架构演变顺序只是针对某个侧面进行单独的改进,在实际场景中,可能同一时间会有几个问题需要解决,或者可能先达到瓶颈的是另外的方面,这时候就应该按照实际问题实际解决。如在政府类的并发量可能不大,但业务可能很丰富的场景,高并发就不是重点解决的问题,此时优先需要的可能会是丰富需求的解决方案。
  • 对于将要实施的系统,架构应该设计到什么程度?
    对于单次实施并且性能指标明确的系统,架构设计到能够支持系统的性能指标要求就足够了,但要留有扩展架构的接口以便不备之需。对于不断发展的系统,如电商平台,应设计到能满足下一阶段用户量和性能指标要求的程度,并根据业务的增长不断的迭代升级架构,以支持更高的并发和更丰富的业务。
  • 服务端架构和大数据架构有什么区别?
    所谓的“大数据”其实是海量数据采集清洗转换、数据存储、数据分析、数据服务等场景解决方案的一个统称,在每一个场景都包含了多种可选的技术,如数据采集有Flume、Sqoop、Kettle等,数据存储有分布式文件系统HDFS、FastDFS,NoSQL数据库HBase、MongoDB等,数据分析有Spark技术栈、机器学习算法等。总的来说大数据架构就是根据业务的需求,整合各种大数据组件组合而成的架构,一般会提供分布式存储、分布式计算、多维分析、数据仓库、机器学习算法等能力。而服务端架构更多指的是应用组织层面的架构,底层能力往往是由大数据架构来提供。
  • 有没有一些架构设计的原则?

    • N+1设计。系统中的每个组件都应做到没有单点故障;
    • 回滚设计。确保系统可以向前兼容,在系统升级时应能有办法回滚版本;
    • 禁用设计。应该提供控制具体功能是否可用的配置,在系统出现故障时能够快速下线功能;
    • 监控设计。在设计阶段就要考虑监控的手段;
    • 多活数据中心设计。若系统需要极高的高可用,应考虑在多地实施数据中心进行多活,至少在一个机房断电的情况下系统依然可用;
    • 采用成熟的技术。刚开发的或开源的技术往往存在很多隐藏的bug,出了问题没有商业支持可能会是一个灾难;
    • 资源隔离设计。应避免单一业务占用全部资源;
    • 架构应能水平扩展。系统只有做到能水平扩展,才能有效避免瓶颈问题;
    • 非核心则购买。非核心功能若需要占用大量的研发资源才能解决,则考虑购买成熟的产品;
    • 使用商用硬件。商用硬件能有效降低硬件故障的机率;
    • 快速迭代。系统应该快速开发小功能模块,尽快上线进行验证,早日发现问题大大降低系统交付的风险;
    • 无状态设计。服务接口应该做成无状态的,当前接口的访问不依赖于接口上次访问的状态。
查看原文

libinfs 赞了文章 · 2018-12-07

很全很全的前端本地存储讲解

最近一直在搞基础的东西,弄了一个持续更新的github笔记,可以去看看,诚意之作(本来就是写给自己看的……)链接地址:Front-End-Basics

此篇文章的地址:三种本地存储方式

基础笔记的github地址:https://github.com/qiqihaobenben/Front-End-Basics ,可以watch,也可以star。

发完之后,就有同学表示,你这也不全呀,还有评论说:吹牛不交税……,应该是被人举报了,现在看不到那条评论了,但是我邮箱里面有哦……本人水平有限只用过那三种,不过人家说的也是事实,我就有两个想法,第一是把标题改为“不太全的前端本地存储讲解”,第二种是把那不全的尽力补一下,嗯,做对的事情,我选择了第二种,补充的东西在最后。

正文开始……


三种本地存储方式

cookie

前言

网络早期最大的问题之一是如何管理状态。简而言之,服务器无法知道两个请求是否来自同一个浏览器。当时最简单的方法是在请求时,在页面中插入一些参数,并在下一个请求中传回参数。这需要使用包含参数的隐藏的表单,或者作为URL参数的一部分传递。这两个解决方案都手动操作,容易出错。cookie出现来解决这个问题。

作用

cookie是纯文本,没有可执行代码。存储数据,当用户访问了某个网站(网页)的时候,我们就可以通过cookie来向访问者电脑上存储数据,或者某些网站为了辨别用户身份、进行session跟踪而储存在用户本地终端上的数据(通常经过加密)

如何工作

当网页要发http请求时,浏览器会先检查是否有相应的cookie,有则自动添加在request header中的cookie字段中。这些是浏览器自动帮我们做的,而且每一次http请求浏览器都会自动帮我们做。这个特点很重要,因为这关系到“什么样的数据适合存储在cookie中”。

存储在cookie中的数据,每次都会被浏览器自动放在http请求中,如果这些数据并不是每个请求都需要发给服务端的数据,浏览器这设置自动处理无疑增加了网络开销;但如果这些数据是每个请求都需要发给服务端的数据(比如身份认证信息),浏览器这设置自动处理就大大免去了重复添加操作。所以对于那种设置“每次请求都要携带的信息(最典型的就是身份认证信息)”就特别适合放在cookie中,其他类型的数据就不适合了。

特征

  1. 不同的浏览器存放的cookie位置不一样,也是不能通用的。
  2. cookie的存储是以域名形式进行区分的,不同的域下存储的cookie是独立的。
  3. 我们可以设置cookie生效的域(当前设置cookie所在域的子域),也就是说,我们能够操作的cookie是当前域以及当前域下的所有子域
  4. 一个域名下存放的cookie的个数是有限制的,不同的浏览器存放的个数不一样,一般为20个。
  5. 每个cookie存放的内容大小也是有限制的,不同的浏览器存放大小不一样,一般为4KB。
  6. cookie也可以设置过期的时间,默认是会话结束的时候,当时间到期自动销毁

cookie值既可以设置,也可以读取。

设置

客户端设置

document.cookie = '名字=值';
document.cookie = 'username=cfangxu;domain=baike.baidu.com'    并且设置了生效域

注意: 客户端可以设置cookie 的下列选项:expires、domain、path、secure(有条件:只有在https协议的网页中,客户端设置secure类型的 cookie 才能成功),但无法设置HttpOnly选项。

服务器端设置
不管你是请求一个资源文件(如 html/js/css/图片),还是发送一个ajax请求,服务端都会返回response。而response header中有一项叫set-cookie,是服务端专门用来设置cookie的。

Set-Cookie 消息头是一个字符串,其格式如下(中括号中的部分是可选的):
Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]

注意: 一个set-Cookie字段只能设置一个cookie,当你要想设置多个 cookie,需要添加同样多的set-Cookie字段。
服务端可以设置cookie 的所有选项:expires、domain、path、secure、HttpOnly
通过 Set-Cookie 指定的这些可选项只会在浏览器端使用,而不会被发送至服务器端。

读取

我们通过document.cookie来获取当前网站下的cookie的时候,得到的字符串形式的值,它包含了当前网站下所有的cookie(为避免跨域脚本(xss)攻击,这个方法只能获取非 HttpOnly 类型的cookie)。它会把所有的cookie通过一个分号+空格的形式串联起来,例如username=chenfangxu; job=coding

修改 cookie

要想修改一个cookie,只需要重新赋值就行,旧的值会被新的值覆盖。但要注意一点,在设置新cookie时,path/domain这几个选项一定要旧cookie 保持一样。否则不会修改旧值,而是添加了一个新的 cookie。

删除

把要删除的cookie的过期时间设置成已过去的时间,path/domain/这几个选项一定要旧cookie 保持一样。

注意

如果只设置一个值,那么算cookie中的value; 设置的两个cookie,key值如果设置的相同,下面的也会把上面的覆盖。

cookie的属性(可选项)

过期时间

如果我们想长时间存放一个cookie。需要在设置这个cookie的时候同时给他设置一个过期的时间。如果不设置,cookie默认是临时存储的,当浏览器关闭进程的时候自动销毁

注意:document.cookie = '名称=值;expires=' + GMT(格林威治时间)格式的日期型字符串; 

一般设置天数:new Date().setDate( oDate.getDate() + 5 ); 比当前时间多5天

一个设置cookie时效性的例子

function setCookie(c_name, value, expiredays){
    var exdate=new Date();
    exdate.setDate(exdate.getDate() + expiredays);
    document.cookie=c_name+ "=" + escape(value) + ((expiredays==null) ? "" : ";expires="+exdate.toGMTString())
}
使用方法:setCookie('username','cfangxu',30)
expires 是 http/1.0协议中的选项,在新的http/1.1协议中expires已经由 max-age 选项代替,两者的作用都是限制cookie 的有效时间。expires的值是一个时间点(cookie失效时刻= expires),而max-age 的值是一个以秒为单位时间段(cookie失效时刻= 创建时刻+ max-age)。
另外,max-age 的默认值是 -1(即有效期为 session );max-age有三种可能值:负数、0、正数。
负数:有效期session;
0:删除cookie;
正数:有效期为创建时刻+ max-age

cookie的域概念(domain选项)

domain指定了 cookie 将要被发送至哪个或哪些域中。默认情况下,domain 会被设置为创建该 cookie 的页面所在的域名,所以当给相同域名发送请求时该 cookie 会被发送至服务器。

浏览器会把 domain 的值与请求的域名做一个尾部比较(即从字符串的尾部开始比较),并将匹配的 cookie 发送至服务器。

客户端设置

document.cookie = "username=cfangxu;path=/;domain=qq.com"
如上:“www.qq.com" 与 "sports.qq.com" 公用一个关联的域名"qq.com",我们如果想让 "sports.qq.com" 下的cookie被 "www.qq.com" 访问,我们就需要用到 cookie 的domain属性,并且需要把path属性设置为 "/"。

服务端设置

Set-Cookie: username=cfangxu;path=/;domain=qq.com
注:一定的是同域之间的访问,不能把domain的值设置成非主域的域名。

cookie的路径概念(path选项)

cookie 一般都是由于用户访问页面而被创建的,可是并不是只有在创建 cookie 的页面才可以访问这个 cookie。
因为安全方面的考虑,默认情况下,只有与创建 cookie 的页面在同一个目录或子目录下的网页才可以访问。
即path属性可以为服务器特定文档指定cookie,这个属性设置的url且带有这个前缀的url路径都是有效的。

客户端设置

 最常用的例子就是让 cookie 在根目录下,这样不管是哪个子页面创建的 cookie,所有的页面都可以访问到了。

document.cookie = "username=cfangxu; path=/"

服务端设置

Set-Cookie:name=cfangxu; path=/blog

如上设置:path 选项值会与 /blog,/blogrool 等等相匹配;任何以 /blog 开头的选项都是合法的。需要注意的是,只有在 domain 选项核实完毕之后才会对 path 属性进行比较。path 属性的默认值是发送 Set-Cookie 消息头所对应的 URL 中的 path 部分。

domain和path总结:

domain是域名,path是路径,两者加起来就构成了 URL,domain和path一起来限制 cookie 能被哪些 URL 访问。
所以domain和path2个选项共同决定了cookie何时被浏览器自动添加到请求头部中发送出去。如果没有设置这两个选项,则会使用默认值。domain的默认值为设置该cookie的网页所在的域名,path默认值为设置该cookie的网页所在的目录。

cookie的安全性(secure选项)

通常 cookie 信息都是使用HTTP连接传递数据,这种传递方式很容易被查看,所以 cookie 存储的信息容易被窃取。假如 cookie 中所传递的内容比较重要,那么就要求使用加密的数据传输。

secure选项用来设置cookie只在确保安全的请求中才会发送。当请求是HTTPS或者其他安全协议时,包含 secure 选项的 cookie 才能被发送至服务器。

document.cookie = "username=cfangxu; secure"

把cookie设置为secure,只保证 cookie 与服务器之间的数据传输过程加密,而保存在本地的 cookie文件并不加密。就算设置了secure 属性也并不代表他人不能看到你机器本地保存的 cookie 信息。机密且敏感的信息绝不应该在 cookie 中存储或传输,因为 cookie 的整个机制原本都是不安全的

注意:如果想在客户端即网页中通过 js 去设置secure类型的 cookie,必须保证网页是https协议的。在http协议的网页中是无法设置secure类型cookie的。

httpOnly

这个选项用来设置cookie是否能通过 js 去访问。默认情况下,cookie不会带httpOnly选项(即为空),所以默认情况下,客户端是可以通过js代码去访问(包括读取、修改、删除等)这个cookie的。当cookie带httpOnly选项时,客户端则无法通过js代码去访问(包括读取、修改、删除等)这个cookie。

在客户端是不能通过js代码去设置一个httpOnly类型的cookie的,这种类型的cookie只能通过服务端来设置。

cookie的编码

cookie其实是个字符串,但这个字符串中等号、分号、空格被当做了特殊符号。所以当cookie的 key 和 value 中含有这3个特殊字符时,需要对其进行额外编码,一般会用escape进行编码,读取时用unescape进行解码;当然也可以用encodeURIComponent/decodeURIComponent或者encodeURI/decodeURI,查看关于编码的介绍

第三方cookie

通常cookie的域和浏览器地址的域匹配,这被称为第一方cookie。那么第三方cookie就是cookie的域和地址栏中的域不匹配,这种cookie通常被用在第三方广告网站。为了跟踪用户的浏览记录,并且根据收集的用户的浏览习惯,给用户推送相关的广告。
关于第三方cookie和cookie的安全问题可以查看https://mp.weixin.qq.com/s/oOGIuJCplPVW3BuIx9tNQg



localStorage(本地存储)

HTML5新方法,不过IE8及以上浏览器都兼容。

特点

  • 生命周期:持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
  • 存储的信息在同一域中是共享的。
  • 当本页操作(新增、修改、删除)了localStorage的时候,本页面不会触发storage事件,但是别的页面会触发storage事件。
  • 大小:据说是5M(跟浏览器厂商有关系)
  • 在非IE下的浏览中可以本地打开。IE浏览器要在服务器中打开。
  • localStorage本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡
  • localStorage受同源策略的限制

设置

localStorage.setItem('username','cfangxu');

获取

localStorage.getItem('username')
也可以获取键名
localStorage.key(0) #获取第一个键名

删除

localStorage.removeItem('username')
也可以一次性清除所有存储
localStorage.clear()

storage事件

当storage发生改变的时候触发。
注意: 当前页面对storage的操作会触发其他页面的storage事件
事件的回调函数中有一个参数event,是一个StorageEvent对象,提供了一些实用的属性,如下表:

PropertyTypeDescription
keyStringThe named key that was added, removed, or moddified
oldValueAnyThe previous value(now overwritten), or null if a new item was added
newValueAnyThe new value, or null if an item was added
url/uriStringThe page that called the method that triggered this change


sessionStorage

其实跟localStorage差不多,也是本地存储,会话本地存储

特点:

  • 用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。因此sessionStorage不是一种持久化的本地存储,仅仅是会话级别的存储。也就是说只要这个浏览器窗口没有关闭,即使刷新页面或进入同源另一页面,数据仍然存在。关闭窗口后,sessionStorage即被销毁,或者在新窗口打开同源的另一个页面,sessionStorage也是没有的。


cookie、localStorage、sessionStorage区别

  • 相同:在本地(浏览器端)存储数据
  • 不同:

    localStorage、sessionStorage

    localStorage只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份localStorage数据。

    sessionStorage比localStorage更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)下。

    localStorage是永久存储,除非手动删除。

    sessionStorage当会话结束(当前页面关闭的时候,自动销毁)

    cookie的数据会在每一次发送http请求的时候,同时发送给服务器而localStorage、sessionStorage不会。


扩展其他的前端存储方式(不常用)

web SQL database

先说个会被取代的,为什么会被取代,主要有以下几个原因:

  1. W3C舍弃 Web SQL database草案,而且是在2010年年底,规范不支持了,浏览器厂商已经支持的就支持了,没有支持的也不打算支持了,比如IE和Firefox。
  2. 为什么要舍弃?因为 Web SQL database 本质上是一个关系型数据库,后端可能熟悉,但是前端就有很多不熟悉了,虽然SQL的简单操作不难,但是也得需要学习。
  3. SQL熟悉后,真实操作中还得把你要存储的东西,比如对象,转成SQL语句,也挺麻烦的。

indexedDB

来自MDN的解释: indexedDB 是一种低级API,用于客户端存储大量结构化数据(包括, 文件/ blobs)。该API使用索引来实现对该数据的高性能搜索。虽然 Web Storage 对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB提供了一个解决方案。

所以,IndexedDB API是强大的,但对于简单的情况可能看起来太复杂了,所以要看你的业务场景来选择到底是用还是不用。

indexedDB 是一个基于JavaScript的面向对象的数据库。 IndexedDB允许你存储和检索用键索引的对象;

IndexedDB 鼓励使用的基本模式如下所示:

  • 打开数据库并且开始一个事务。
  • 创建一个 object store。
  • 构建一个请求来执行一些数据库操作,像增加或提取数据等。
  • 通过监听正确类型的 DOM 事件以等待操作完成。
  • 在操作结果上进行一些操作(可以在 request 对象中找到)

1、首先打开indexedDB数据库

语法:
window.indexedDB.open(dbName, version)

var db;
// 打开数据库,open还有第二个参数版本号
var request = window.indexedDB.open('myTestDatabase');
// 数据库打开成功后
request.onsuccess = function (event) {
    // 存储数据结果,后面所有的数据库操作都离不开它。
    db = request.result;
}
request.onerror = function (event) {
    alert("Why didn't you allow my web app to use IndexedDB?!");
}

// 数据库首次创建版本,或者window.indexedDB.open传递的新版本(版本数值要比现在的高)
request.onupgradeneeded = function (event) {

}

onupgradeneeded事件: 更新数据库的 schema,也就是创建或者删除对象存储空间,这个事件将会作为一个允许你处理对象存储空间的 versionchange 事务的一部分被调用。在数据库第一次被打开时或者当指定的版本号高于当前被持久化的数据库的版本号时,这个 versionchange 事务将被创建。onupgradeneeded 是我们唯一可以修改数据库结构的地方。在这里面,我们可以创建和删除对象存储空间以及构建和删除索引。

2、构建数据库

IndexedDB 使用对象存储空间而不是表,并且一个单独的数据库可以包含任意数量的对象存储空间。每当一个值被存储进一个对象存储空间时,它会被和一个键相关联。

  // 数据库首次创建版本,或者window.indexedDB.open传递的新版本(版本数值要比现在的高)
  request.onupgradeneeded = function (event) {

      //之前咱们不是在success中得到了db了么,为什么还要在这获取,
      //因为在当前事件函数执行后才会去执行success事件
      var db = event.target.result;

      // 创建一个对象存储空间,keyPath是id,keyGenerator是自增的
      var objectStore = db.createObjectStore('testItem',{keyPath: 'id',autoIncrement: true});
      // 创建一个索引来通过id搜索,id是自增的,不会有重复,所以可以用唯一索引
      objectStore.createIndex('id','id',{unique: true})

      objectStore.createIndex('name','name');
      objectStore.createIndex('age','age');

      //添加一条信息道数据库中
      objectStore.add({name: 'cfangxu', age: '27'});

  }

注意: 执行完后,在调试工具栏Application的indexedDB中也看不到,你得右键刷新一下。

创建索引的语法:

objectStore.createIndex(indexName, keyPath, objectParameters)

indexName:创建的索引名称,可以使用空名称作为索引。
keyPath:索引使用的关键路径,可以使用空的keyPath, 或者keyPath传为数组keyPath也是可以的。
objectParameters:可选参数。常用参数之一是unique,表示该字段值是否唯一,不能重复。例如,本demo中id是不能重复的,于是有设置:

3、添加数据

上面的代码建好了字段,并且添加了一条数据,但是我们如果想在onupgradeneeded事件外面操作,接下来的步骤了。
由于数据库的操作都是基于事务(transaction)来进行,于是,无论是添加编辑还是删除数据库,我们都要先建立一个事务(transaction),然后才能继续下面的操作。
语法: var transaction = db.transaction(dbName, "readwrite");
第一个参数是事务希望跨越的对象存储空间的列表,可以是数组或者字符串。如果你希望事务能够跨越所有的对象存储空间你可以传入一个空数组。如果你没有为第二个参数指定任何内容,你得到的是只读事务。因为这里我们是想要写入所以我们需要传入 "readwrite" 标识。

var timer = setInterval(function () {
    if(db) {
        clearInterval(timer);
        // 新建一个事务
        var transaction = db.transaction(['testItem'], 'readwrite');
        // 打开一个存储对象
        var objectStore = transaction.objectStore('testItem');
        // 添加数据到对象中
        objectStore.add({ name: 'xiaoming', age: '12' });
        objectStore.add({ name: 'xiaolong', age: '20' });
    }
},100)

为什么要用一个间隔定时器? 因为这是一个demo,正常的是要有操作才能进行数据库的写入,在我们的demo中,js执行到transaction会比indexedDB的onsuccess事件回调快,导致会拿到db为undefined,所以写了个间隔定时器等它一会。

4、获取数据

var transaction = db.transaction(['testItem'], 'readwrite');

var objectStore = transaction.objectStore('testItem');

var getRquest = objectStore.get(1);
getRquest.onsuccess = function (event) {
    console.log(getRquest.result);
}
//输出:{name: "cfangxu", age: "27", id: 1}

5、修改数据

var transaction = db.transaction(['testItem'], 'readwrite');

var objectStore = transaction.objectStore('testItem');

var getRquest = objectStore.put({ name: 'chenfangxu', age: '27', id:1 });
// 修改了id为1的那条数据

6、删除数据

var transaction = db.transaction(['testItem'], 'readwrite');

var objectStore = transaction.objectStore('testItem');

var getRquest = objectStore.delete(1);
// 删除了id为1的那条数据
上面的例子执行完后,一定一定要右键刷新indexedDB,它自己是不会变的。
查看原文

赞 144 收藏 566 评论 18

libinfs 赞了文章 · 2018-12-06

聊一聊 cookie

咱们不搞一开始就一大堆理论知识介绍,怕把人讲懵了...... 咱们换一个思维方式——"从现象看本质",先说说我们看到了什么,再从看到的现象中提出问题,最后深入寻找答案。

我们看到的 cookie

我自己创建了一个网站,网址为http://ppsc.sankuai.com。在这个网页中我设置了几个cookieJSSESSIONIDPA_VTIMEskmtutctest

在 chrome 浏览器中打开这个网站,进入开发者模式,点击Resources栏 -> 选择cookies,我们会看到如下图所示的界面:

图片描述

解释一下:左边栏Cookies下方会列举当前网页中设置过cookie的域都有哪些。上图中只有一个域,即“ppsc.sankuai.com”。而右侧区域显示的就是某个域下具体的 cookie 列表,对应上图就是“ppsc.sankuai.com”域下设置的4个cookie

在这个网页中我往http://ppsc.sankuai.com/getList接口发了一个 Ajax 请求,request header如下图所示:

图片描述

从上图中我们会看到request header中自动添加了Cookie字段(我并没有手动添加这个字段哦~),Cookie字段的值其实就是我设置的那4个 cookie。这个请求最终会发送到http://ppsc.sankuai.com这个服务器上,这个服务器就能从接收到的request header中提取那4个cookie

上面两张图展示了cookie的基本通信流程:设置cookie => cookie被自动添加到request header中 => 服务端接收到cookie。这个流程中有几个问题需要好好研究:

  1. 什么样的数据适合放在cookie中?

  2. cookie是怎么设置的?

  3. cookie为什么会自动加到request header中?

  4. cookie怎么增删查改?

我们要带着这几个问题继续往下阅读。

cookie 是怎么工作的?

首先必须明确一点,存储cookie是浏览器提供的功能。cookie 其实是存储在浏览器中的纯文本,浏览器的安装目录下会专门有一个 cookie 文件夹来存放各个域下设置的cookie

当网页要发http请求时,浏览器会先检查是否有相应的cookie,有则自动添加在request header中的cookie字段中。这些是浏览器自动帮我们做的,而且每一次http请求浏览器都会自动帮我们做。这个特点很重要,因为这关系到“什么样的数据适合存储在cookie中”。

存储在cookie中的数据,每次都会被浏览器自动放在http请求中,如果这些数据并不是每个请求都需要发给服务端的数据,浏览器这设置自动处理无疑增加了网络开销;但如果这些数据是每个请求都需要发给服务端的数据(比如身份认证信息),浏览器这设置自动处理就大大免去了重复添加操作。所以对于那设置“每次请求都要携带的信息(最典型的就是身份认证信息)”就特别适合放在cookie中,其他类型的数据就不适合了。

但在 localStorage 出现之前,cookie被滥用当做了存储工具。什么数据都放在cookie中,即使这些数据只在页面中使用而不需要随请求传送到服务端。当然cookie标准还是做了一些限制的:每个域名下的cookie 的大小最大为4KB,每个域名下的cookie数量最多为20个(但很多浏览器厂商在具体实现时支持大于20个)。

cookie 的格式

document.cookie

JS 原生的 API提供了获取cookie的方法:document.cookie(注意,这个方法只能获取非 HttpOnly 类型的cookie)。在 console 中执行这段代码可以看到结果如下图:

图片描述

打印出的结果是一个字符串类型,因为cookie本身就是存储在浏览器中的字符串。但这个字符串是有格式的,由键值对 key=value构成,键值对之间由一个分号和一个空格隔开。

cookie 的属性选项

每个cookie都有一定的属性,如什么时候失效,要发送到哪个域名,哪个路径等等。这些属性是通过cookie选项来设置的,cookie选项包括:expiresdomainpathsecureHttpOnly。在设置任一个cookie时都可以设置相关的这些属性,当然也可以不设置,这时会使用这些属性的默认值。在设置这些属性时,属性之间由一个分号和一个空格隔开。代码示例如下:

"key=name; expires=Thu, 25 Feb 2016 04:18:00 GMT; domain=ppsc.sankuai.com; path=/; secure; HttpOnly"

expires

expires选项用来设置“cookie 什么时间内有效”。expires其实是cookie失效日期,expires必须是 GMT 格式的时间(可以通过 new Date().toGMTString()或者 new Date().toUTCString() 来获得)。

expires=Thu, 25 Feb 2016 04:18:00 GMT表示cookie讲在2016年2月25日4:18分之后失效,对于失效的cookie浏览器会清空。如果没有设置该选项,则默认有效期为session,即会话cookie。这种cookie在浏览器关闭后就没有了。

expires 是 http/1.0协议中的选项,在新的http/1.1协议中expires已经由 max-age 选项代替,两者的作用都是限制cookie 的有效时间。expires的值是一个时间点(cookie失效时刻= expires),而max-age 的值是一个以为单位时间段(cookie失效时刻= 创建时刻+ max-age)。
另外,max-age 的默认值是 -1(即有效期为 session );若max-age有三种可能值:负数、0、正数。负数:有效期session0:删除cookie;正数:有效期为创建时刻+ max-age

domain 和 path

domain是域名,path是路径,两者加起来就构成了 URL,domainpath一起来限制 cookie 能被哪些 URL 访问。

一句话概括:某cookie的 domain为“baidu.com”, path为“/ ”,若请求的URL(URL 可以是js/html/img/css资源请求,但不包括 XHR 请求)的域名是“baidu.com”或其子域如“api.baidu.com”、“dev.api.baidu.com”,且 URL 的路径是“/ ”或子路径“/home”、“/home/login”,则浏览器会将此 cookie 添加到该请求的 cookie 头部中。

所以domainpath2个选项共同决定了cookie何时被浏览器自动添加到请求头部中发送出去。如果没有设置这两个选项,则会使用默认值。domain的默认值为设置该cookie的网页所在的域名,path默认值为设置该cookie的网页所在的目录。

特别说明1:
发生跨域xhr请求时,即使请求URL的域名和路径都满足 cookie 的 domain 和 path,默认情况下cookie也不会自动被添加到请求头部中。若想知道原因请阅读本文最后一节)

特别说明2:
domain是可以设置为页面本身的域名(本域),或页面本身域名的父域,但不能是公共后缀 public suffix。举例说明下:如果页面域名为 www.baidu.com, domain可以设置为“www.baidu.com”,也可以设置为“baidu.com”,但不能设置为“.com”或“com”。

secure

secure选项用来设置cookie只在确保安全的请求中才会发送。当请求是HTTPS或者其他安全协议时,包含 secure 选项的 cookie 才能被发送至服务器。

默认情况下,cookie不会带secure选项(即为空)。所以默认情况下,不管是HTTPS协议还是HTTP协议的请求,cookie 都会被发送至服务端。但要注意一点,secure选项只是限定了在安全情况下才可以传输给服务端,但并不代表你不能看到这个 cookie。

下面我们设置一个 secure类型的 cookie:

document.cookie = "name=huang; secure";

之后你就能在控制台中看到这个 cookie 了,如下图所示:

图片描述

这里有个坑需要注意下:
如果想在客户端即网页中通过 js 去设置secure类型的 cookie,必须保证网页是https协议的。在http协议的网页中是无法设置secure类型cookie的。

httpOnly

这个选项用来设置cookie是否能通过 js 去访问。默认情况下,cookie不会带httpOnly选项(即为空),所以默认情况下,客户端是可以通过js代码去访问(包括读取、修改、删除等)这个cookie的。当cookiehttpOnly选项时,客户端则无法通过js代码去访问(包括读取、修改、删除等)这个cookie

在客户端是不能通过js代码去设置一个httpOnly类型的cookie的,这种类型的cookie只能通过服务端来设置。

那我们在页面中怎么知道哪些cookiehttpOnly类型的呢?看下图:

图片描述

凡是httpOnly类型的cookie,其 HTTP 一列都会打上√,如上图中的PA_VTIME。你通过document.cookie是不能获取的,也不能修改PA_VTIME的。

——httpOnly与安全

从上面介绍中,大家是否会有这样的疑问:为什么我们要限制客户端去访问cookie?其实这样做是为了保障安全。

试想:如果任何 cookie 都能被客户端通过document.cookie获取会发生什么可怕的事情。当我们的网页遭受了 XSS 攻击,有一段恶意的script脚本插到了网页中。这段script脚本做的事情是:通过document.cookie读取了用户身份验证相关的 cookie,并将这些 cookie 发送到了攻击者的服务器。攻击者轻而易举就拿到了用户身份验证信息,于是就可以摇摇大摆地冒充此用户访问你的服务器了(因为攻击者有合法的用户身份验证信息,所以会通过你服务器的验证)。

如何设置 cookie?

知道了cookie的格式,cookie的属性选项,接下来我们就可以设置cookie了。首先得明确一点:cookie既可以由服务端来设置,也可以由客户端来设置。

服务端设置 cookie

不管你是请求一个资源文件(如 html/js/css/图片),还是发送一个ajax请求,服务端都会返回response。而response header中有一项叫set-cookie,是服务端专门用来设置cookie的。如下图所示,服务端返回的response header中有5个set-cookie字段,每个字段对应一个cookie(注意不能将多个cookie放在一个set-cookie字段中),set-cookie字段的值就是普通的字符串,每个cookie还设置了相关属性选项。

图片描述

注意:

  • 一个set-Cookie字段只能设置一个cookie,当你要想设置多个 cookie,需要添加同样多的set-Cookie字段。

  • 服务端可以设置cookie 的所有选项:expiresdomainpathsecureHttpOnly

客户端设置 cookie

在网页即客户端中我们也可以通过js代码来设置cookie。如我当前打开的网址为http://dxw.st.sankuai.com/mp/,在控制台中我们执行了下面代码:

document.cookie = "name=Jonh; ";

查看浏览器 cookie 面板如下图所示,cookie确实设置成功了,而且属性选项 domainpathexpires都用了默认值。

图片描述

再执行下面代码:

document.cookie="age=12; expires=Thu, 26 Feb 2116 11:50:25 GMT; domain=sankuai.com; path=/";

查看浏览器cookie 面板,如下图所示,新的cookie设置成功了,而且属性选项 domainpathexpires都变成了设定的值。

图片描述

注意:

  • 客户端可以设置cookie 的下列选项:expiresdomainpathsecure(有条件:只有在https协议的网页中,客户端设置secure类型的 cookie 才能成功),但无法设置HttpOnly选项。

用 js 如何设置多个 cookie

当要设置多个cookie时, js 代码很自然地我们会这么写:

document.cookie = "name=Jonh; age=12; class=111";

但你会发现这样写只是添加了第一个cookie“name=John”,后面的所有cookie都没有添加成功。所以最简单的设置多个cookie的方法就在重复执行document.cookie = "key=name",如下:

document.cookie = "name=Jonh";
document.cookie = "age=12";
document.cookie = "class=111";

如何修改、删除

修改 cookie

要想修改一个cookie,只需要重新赋值就行,旧的值会被新的值覆盖。但要注意一点,在设置新cookie时,path/domain这几个选项一定要旧cookie 保持一样。否则不会修改旧值,而是添加了一个新的 cookie。

删除 cookie

删除一个cookie 也挺简单,也是重新赋值,只要将这个新cookie的expires 选项设置为一个过去的时间点就行了。但同样要注意,path/domain/这几个选项一定要旧cookie 保持一样。

cookie 编码

cookie其实是个字符串,但这个字符串中逗号、分号、空格被当做了特殊符号。所以当cookie的 key 和 value 中含有这3个特殊字符时,需要对其进行额外编码,一般会用escape进行编码,读取时用unescape进行解码;当然也可以用encodeURIComponent/decodeURIComponent或者encodeURI/decodeURI三者的区别可以参考这篇文章)。

var key = escape("name;value");
var value = escape("this is a value contain , and ;");
document.cookie= key + "=" + value + "; expires=Thu, 26 Feb 2116 11:50:25 GMT; domain=sankuai.com; path=/";

跨域请求中 cookie

之前在介绍 XHR 的一篇文章里面提过:默认情况下,在发生跨域时,cookie 作为一种 credential 信息是不会被传送到服务端的。必须要进行额外设置才可以。具体原因和如何设置可以参考我的这篇文章:你真的会使用XMLHttpRequest吗?

另外,关于跨域资源共享 CORS极力推荐大家阅读阮一峰老师的这篇 跨域资源共享 CORS 详解

其他补充

  1. 什么时候 cookie 会被覆盖:name/domain/path 这3个字段都相同的时候;

  2. 关于domain的补充说明(参考1/参考2):

    1. 如果显式设置了 domain,则设置成什么,浏览器就存成什么;但如果没有显式设置,则浏览器会自动取 url 的 host 作为 domain 值;

    2. 新的规范中,显式设置 domain 时,如果 value 最前面带点,则浏览器处理时会将这个点去掉,所以最后浏览器存的就是没有点的(注意:但目前大多数浏览器并未全部这么实现)

    3. 前面带点‘.’和不带点‘.’有啥区别:

      • 带点:任何 subdomain 都可以访问,包括父 domain

      • 不带点:只有完全一样的域名才能访问,subdomain 不能(但在 IE 下比较特殊,它支持 subdomain 访问)

总结

咱们今天就聊到这里,若有不对之处欢迎各位指正~~
最后附上一些参考资料:

  1. http://www.quirksmode.org/js/...

  2. http://www.tutorialspoint.com...

  3. http://www.allaboutcookies.or...

  4. http://bubkoo.com/2014/04/21/...

查看原文

赞 351 收藏 543 评论 50

libinfs 赞了回答 · 2018-12-06

解决一个注册域名能分配多少子域名?

我其实是顺着题主的另外一个问题爬过来的,一开始没打算写一个新答案,只是在其中一个答案里评论了一下。之后忽然觉得我所知道的貌似出处皆不可考,于是担心自己也错了,遂查阅权威资料寻找答案,最后的结果是:

以上答案都错了(都有错误的部分)——包括我自己之前的评论(捂脸……)

这里就不长篇大论了,细节在 wikiphdia 上都有(别说维基谁都可以改,我也核对了在 ICANN,CNNIC 等权威机构的相关解释,而且 IETF 发表的几个规范也解释得很清楚——这些资源维基都有提供)。在此我只简述我们普遍理解错误的几个概念:

a.net 为例:

  1. 何为一级域名net 是一级域名
  2. 何为二级域名a.net 是二级域名
  3. xxx.a.net为何物:它其实是三级域名

也就是说,我们惯常所说的二级域名实际上应该是三级域名。当然,这些概念和题主的问题并无直接关系,但随后的答案在这点上竟然都是错的,而且还因此和题主产生了很多分歧与矛盾,渐渐地反而离主题越来越远。

这是一个令人惊讶的结果,原本多数人以为题主没有理解的概念在事实上我们都理解错了,当然我也不确定题主理解的是否正确,因为如果假设题主理解的正确,原本的问题应该也很容易得到答案。上面提供的维基地址里就有。

简单回答一下:

  1. 通常我们花钱去注册的是二级域名,因此除非再额外花钱,否则我们暂定只有一个二级域名可用。
  2. 那么这个二级域名能用来解析几个网站?一个。

    1. 通俗地说,域名是用来“解释”物理地址——IP 的,因而一个二级域名当然只能对应一个网站,因为每一个独立的网站都需要至少一个物理地址才能存在于互联网上供人访问。
  3. 当你拥有一个能完全掌控的二级域名时,你可以建立不限数目的三级域名,当然它们的二级域名部分是一样的。

    1. 三级域名也可以用来解析物理 IP 地址,所以你可以将它使用在和二级域名完全不同的网站上。
    2. 因此,你的确可以只购买一个二级域名,但是却可以建立无数个网站,它们分别使用不同的三级域名
  4. 三级域名不是终点,还有四级五级,……更多级可用。

    1. 然而每一级域名到底能设置多少个,理论上没有限制,但实际上域名的提供商是可以做限制的,这一点在购买域名的时候要留心。

说到这里,题主的问题应该已经得以解决了吧?简而言之,你问题里提出的假设是对的。即:“只购买一个二级域名,想做多少网站就做多少网站”,不过域名只是网站的名片而已,具体做网站还要空间,要流量,要 IP 等东东,额外花钱还是少不了的。这些事情我想题主已经清楚了吧?


补充一点,回头看了一下原题的标题……不得不说题主你这是自相矛盾啊!标题本身和问题描述的设想相互打架了你没发现吗?

对于问题,以上回答已经解释了:你购买一个(二级)域名,可以建立无限个网站,因为你可以在此基础上创建更多低级的子域名。

但是你的标题:

一个域名能分配多少主机名/网站名?

这和问题里的描述完全是两回事啊。

域名,是一个通用概念,一个二级域名是一个域名,一个三级域名也是一个域名。具体到每一个特指的域名,它的确只能“分配”(说对应更合适)一个主机名。

怎么说?

a.net,这是一个域名;xxx.a.net,这也是一个域名(虽然它们层级不同)。
a.net,这是一个主机名;xxx.a.net,这也是一个主机名(它们可以是解析同一个主机,也可以是不同的,这取决于你如何设置解析它们的规则)。

不过我可以理解,在你问问题的时候,你心里想的域名是 a.net,也就是要花钱的那个;其他的,比如 xxx.a.netyyy.a.net 等等才是主机名。所以你才会有这样的问题标题和这样的问题内容。

怎样?现在还觉得提问或回答是应该“能多简单就多简单”吗?

关注 19 回答 7

libinfs 收藏了文章 · 2018-11-30

【译】只用 CSS 就能做到的像素画/像素动画

只用 CSS 就能做到的像素画/像素动画

clipboard.png

原文链接:box-shadowを使ってCSSだけでドット絵を描き、アニメーションさせる
作者推特:bc_rikko
作者的推特里面有不少例子,有能力的同学可以看一下
翻译博客地址:https://ssshooter.com/css-pixel/

这篇文章将会介绍只用 CSS 就能制作像素画·像素动画的方法。虽说纯 CSS 就能做到,但是为了更高的可维护性,也会顺便介绍使用 Sass 的制作方法。

clipboard.png

clipboard.png

上面的马里奥和 Minecraft 方块都没有使用 JavaScript,单纯使用 CSS 动画制作。

关于 box-shadow 属性

绘制像素点可以借助 box-shadow 属性。
原本 box-shadow 属性用于制作阴影效果,先介绍一下基本用法。

该属性的写法有几种:

  • box-shadow: offset-x offset-y color
  • box-shadow: offset-x offset-y blur-radius color
  • box-shadow: offset-x offset-y blur-radius spread-radius color
  • box-shadow: inset offset-x offset-y color

offset-xoffset-y 用于指定阴影偏移位置。以元素的左上角为原点,指定 XY 轴移动的位置。
color 字面意思,指定阴影颜色。
blur-radius 指定模糊效果的半径。跟 border-radius 差不多。
spread-raduis 模糊范围的扩大与缩小。
inset 关键字可以使阴影效果显示在元素内则。

文字说明或许不够形象,我们可以直接看效果:

https://jsfiddle.net/bc_rikko...

实际效果如下,每个值会造成什么影响应该能很直观地看懂。

基础:描绘一个像素点

box-shadow 基础都明白了,就可以进入下一步:描绘一个像素点。
对一个边长 100px 的正方形使用 box-shadow

<div class="container">
    <div class="box"></div>
</div>

<style>
* {
  /* 为了方便看到元素而添加的边框(不加也行) */
  box-sizing: border-box;
}
.container {
  /* 长和宽包括 box-shadow */
  width: 200px;
  height: 200px;
}

.box {
  /* 元素属性 */
  width: 100px;
  height: 100px;
  border: 2px solid #777;

  /* 在元素右下角相同大小的方块 */
  box-shadow: 100px 100px rgba(7,7,7,.3);
}
</style>

clipboard.png

如图所示,使用 box-shadow 描绘了一个与元素相同大小的阴影。代码的意思是把一个 100px 的方形的影子放到 (100px, 100px) 的位置。

进阶:用 box-shadow 属性绘制像素画

完成预想图

完成预想图
这两个都是 5✖️5 的像素画,我们先从左边开始:

<div class="container">
  <div class="pixel one"></div>
</div>

<style>
.container {
  /* 像素画的大小 */
  width: 100px;
  height: 100px;
}

.pixel {
  /* 使伪元素的位置可调整 */
  position: relative;
}
.pixel::before {
  content: "";

  /* 一个点的大小(例:20px x 20px) */
  width: 20px;
  height: 20px;
  /* box-shadow 着色,伪元素设为透明 */
  background-color: transparent;

  /* 调整伪元素位置,让左上角成为(0,0) */
  position: absolute;
  top: -20px;
  left: -20px;
}

.pixel.one::before {
  box-shadow:
     /* 列 行 色 */
     /* 第1列 */
     20px   20px #FB0600,
     20px   40px #FC322F,
     20px   60px #FC6663,
     20px   80px #FD9999,
     20px  100px #FECCCB, 
     /* 第2列 */
     40px   20px #60169F,
     40px   40px #7A23B0,
     40px   60px #964DC2,
     40px   80px #B681D9,
     40px  100px #D8BEED, 
     /* 第3列 */
     60px   20px #1388BC,
     60px   40px #269DC9,
     60px   60px #55B3D7,
     60px   80px #88CAE2,
     60px  100px #BFE3EF, 
     /* 第4列 */
     80px   20px #ACD902,
     80px   40px #BDE02D,
     80px   60px #CDEA5E,
     80px   80px #DBEF8E,
     80px  100px #F4FBC8, 
     /* 第5列 */
    100px  20px #FB8F02,
    100px  40px #FDA533,
    100px  60px #FDBB64,
    100px  80px #FED39A,
    100px 100px #FDE8C9;
}
</style>

首先,box-shadow 生产的影子大小不包括本体元素的大小,container 类的大小设为像素画完成后的大小就行。
接着,box-shadow 的影子大小由,pixel 类的大小决定,所以把 widthheight设定为 20px。
实际的点是 before 伪元素绘制的,pixel 的 20px 正方形会在左上角留下空位,为此可以使用 position: absolute 调整。
最后使用 box-shadow 逐格绘制像素画。

接着实现右边的像素画。

.pixel.two::before {
  box-shadow:
    20px   20px #704b16,
    40px   20px #704b16,
    60px   20px #704b16,
    80px   20px #704b16,
    100px  20px #704b16,
    20px   40px #704b16,
    40px   40px #fdb778,
    60px   40px #fdb778,
    80px   40px #fdb778,
    100px  40px #704b16,
    20px   60px #fdb778,
    40px   60px #333333,
    60px   60px #fdb778,
    80px   60px #333333,
    100px  60px #fdb778,
    20px   80px #fdb778,
    40px   80px #fdb778,
    60px   80px #fdb778,
    80px   80px #fdb778,
    100px  80px #fdb778,
    20px  100px #fdb778,
    40px  100px #c70300,
    60px  100px #c70300,
    80px  100px #c70300,
    100px 100px #fdb778;
}

应用:使用 Sass 编写可维护像素画

上面写的几个例子,至少我是没什么信心去维护好他们。5x5 的像素画要写 25 次属性值,一般的 16x16 则是多达 256 个值。
所以,我们可以使用 Sass 编写可维护像素画。
Sass 环境搭建可以参考以下文章(日语)
https://kuroeveryday.blogspot...

Sass 使用 mixin(function 亦可)生成样式的方法:

@mixin pixelize($matrix, $size, $colors) {
  $ret: "";

  @for $i from 1 through length($matrix) {
    $row: nth($matrix, $i);

    @for $j from 1 through length($row) {
      $dot: nth($row, $j);

      @if $dot != 0 {
        @if $ret != "" {
          $ret: $ret + ",";
        }

        $color: nth($colors, $dot);
        $ret: $ret + ($j * $size) + " " + ($i * $size) + " " + $color;
      }
    }
  }

  box-shadow: unquote($ret + ";");
}

$heart-colors: (#333, #f11416, #831200);
$heart: (
  (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),
  (0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0),
  (0,1,2,2,2,1,0,0,0,1,2,2,3,1,0,0),
  (1,2,0,0,2,2,1,0,1,2,2,2,2,3,1,0),
  (1,2,0,2,2,2,2,1,2,2,2,2,2,3,1,0),
  (1,2,2,2,2,2,2,2,2,2,2,2,2,3,1,0),
  (1,2,2,2,2,2,2,2,2,2,2,2,2,3,1,0),
  (1,2,2,2,2,2,2,2,2,2,2,2,2,3,1,0),
  (0,1,2,2,2,2,2,2,2,2,2,2,3,1,0,0),
  (0,0,1,2,2,2,2,2,2,2,2,3,1,0,0,0),
  (0,0,0,1,2,2,2,2,2,2,3,1,0,0,0,0),
  (0,0,0,0,1,2,2,2,2,3,1,0,0,0,0,0),
  (0,0,0,0,0,1,2,2,3,1,0,0,0,0,0,0),
  (0,0,0,0,0,0,1,3,1,0,0,0,0,0,0,0),
  (0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0),
  (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
);

.icon {
  width: 20px;
  height: 20px;
  @include pixelize($heart, 20px, $heart-colors);
}

定义名为 pixelize 的 mixin,把像素画的矩阵($heart)像素点的大小(20px)颜色列表($hearts-colors)传入其中,即可生成 box-shadow 属性。
像素画的矩阵用数字 0~N 表示,0 为透明,1~n 为颜色列表对应颜色。

如果有代码高亮的话,像素画的图案就一目了然啦。

与原生 CSS 相比,这样简单多了吧?

如果这样都觉得麻烦,可以使用 CSS 像素画生成器~

CSSドット絵ジェネレータ

番外篇:制作像素动画

之前 icon 类直接使用 box-shadow 属性绘制像素画,在制作像素动画时,需要使用 CSS animation。

.mario {
  width: 8px;
  height: 8px;

  animation:
    jump 1s infinite,
    sprite 1s infinite;
}

/* 跳跃动作(上下移動) */
@keyframes jump {
  from, 25%, 75%, to {
    transform: translateY(0);
  }
  50% {
    transform: translateY(calc(8px * -8));
  }
}

/* 普通状态和跳跃状态的像素画 */
@keyframes sprite {
  /* 对比 animation-timing-function: steps(n)
   * 使用百分比可以更细致的调整动画时间 
   */
  from, 24%, 76%, to {
    box-shadow: /* 普通状态的像素画 */
  }
  25%, 75% {
    box-shadow: /* 跳跃状态的像素画 */
  }
}

使用 CSS 动画修改 box-shadow 和元素的位置,看起来就像是跳起来一样。
详细代码可以在 github 仓库中了解
https://github.com/BcRikko/cs...

查看原文

libinfs 赞了文章 · 2018-11-30

【译】只用 CSS 就能做到的像素画/像素动画

只用 CSS 就能做到的像素画/像素动画

clipboard.png

原文链接:box-shadowを使ってCSSだけでドット絵を描き、アニメーションさせる
作者推特:bc_rikko
作者的推特里面有不少例子,有能力的同学可以看一下
翻译博客地址:https://ssshooter.com/css-pixel/

这篇文章将会介绍只用 CSS 就能制作像素画·像素动画的方法。虽说纯 CSS 就能做到,但是为了更高的可维护性,也会顺便介绍使用 Sass 的制作方法。

clipboard.png

clipboard.png

上面的马里奥和 Minecraft 方块都没有使用 JavaScript,单纯使用 CSS 动画制作。

关于 box-shadow 属性

绘制像素点可以借助 box-shadow 属性。
原本 box-shadow 属性用于制作阴影效果,先介绍一下基本用法。

该属性的写法有几种:

  • box-shadow: offset-x offset-y color
  • box-shadow: offset-x offset-y blur-radius color
  • box-shadow: offset-x offset-y blur-radius spread-radius color
  • box-shadow: inset offset-x offset-y color

offset-xoffset-y 用于指定阴影偏移位置。以元素的左上角为原点,指定 XY 轴移动的位置。
color 字面意思,指定阴影颜色。
blur-radius 指定模糊效果的半径。跟 border-radius 差不多。
spread-raduis 模糊范围的扩大与缩小。
inset 关键字可以使阴影效果显示在元素内则。

文字说明或许不够形象,我们可以直接看效果:

https://jsfiddle.net/bc_rikko...

实际效果如下,每个值会造成什么影响应该能很直观地看懂。

基础:描绘一个像素点

box-shadow 基础都明白了,就可以进入下一步:描绘一个像素点。
对一个边长 100px 的正方形使用 box-shadow

<div class="container">
    <div class="box"></div>
</div>

<style>
* {
  /* 为了方便看到元素而添加的边框(不加也行) */
  box-sizing: border-box;
}
.container {
  /* 长和宽包括 box-shadow */
  width: 200px;
  height: 200px;
}

.box {
  /* 元素属性 */
  width: 100px;
  height: 100px;
  border: 2px solid #777;

  /* 在元素右下角相同大小的方块 */
  box-shadow: 100px 100px rgba(7,7,7,.3);
}
</style>

clipboard.png

如图所示,使用 box-shadow 描绘了一个与元素相同大小的阴影。代码的意思是把一个 100px 的方形的影子放到 (100px, 100px) 的位置。

进阶:用 box-shadow 属性绘制像素画

完成预想图

完成预想图
这两个都是 5✖️5 的像素画,我们先从左边开始:

<div class="container">
  <div class="pixel one"></div>
</div>

<style>
.container {
  /* 像素画的大小 */
  width: 100px;
  height: 100px;
}

.pixel {
  /* 使伪元素的位置可调整 */
  position: relative;
}
.pixel::before {
  content: "";

  /* 一个点的大小(例:20px x 20px) */
  width: 20px;
  height: 20px;
  /* box-shadow 着色,伪元素设为透明 */
  background-color: transparent;

  /* 调整伪元素位置,让左上角成为(0,0) */
  position: absolute;
  top: -20px;
  left: -20px;
}

.pixel.one::before {
  box-shadow:
     /* 列 行 色 */
     /* 第1列 */
     20px   20px #FB0600,
     20px   40px #FC322F,
     20px   60px #FC6663,
     20px   80px #FD9999,
     20px  100px #FECCCB, 
     /* 第2列 */
     40px   20px #60169F,
     40px   40px #7A23B0,
     40px   60px #964DC2,
     40px   80px #B681D9,
     40px  100px #D8BEED, 
     /* 第3列 */
     60px   20px #1388BC,
     60px   40px #269DC9,
     60px   60px #55B3D7,
     60px   80px #88CAE2,
     60px  100px #BFE3EF, 
     /* 第4列 */
     80px   20px #ACD902,
     80px   40px #BDE02D,
     80px   60px #CDEA5E,
     80px   80px #DBEF8E,
     80px  100px #F4FBC8, 
     /* 第5列 */
    100px  20px #FB8F02,
    100px  40px #FDA533,
    100px  60px #FDBB64,
    100px  80px #FED39A,
    100px 100px #FDE8C9;
}
</style>

首先,box-shadow 生产的影子大小不包括本体元素的大小,container 类的大小设为像素画完成后的大小就行。
接着,box-shadow 的影子大小由,pixel 类的大小决定,所以把 widthheight设定为 20px。
实际的点是 before 伪元素绘制的,pixel 的 20px 正方形会在左上角留下空位,为此可以使用 position: absolute 调整。
最后使用 box-shadow 逐格绘制像素画。

接着实现右边的像素画。

.pixel.two::before {
  box-shadow:
    20px   20px #704b16,
    40px   20px #704b16,
    60px   20px #704b16,
    80px   20px #704b16,
    100px  20px #704b16,
    20px   40px #704b16,
    40px   40px #fdb778,
    60px   40px #fdb778,
    80px   40px #fdb778,
    100px  40px #704b16,
    20px   60px #fdb778,
    40px   60px #333333,
    60px   60px #fdb778,
    80px   60px #333333,
    100px  60px #fdb778,
    20px   80px #fdb778,
    40px   80px #fdb778,
    60px   80px #fdb778,
    80px   80px #fdb778,
    100px  80px #fdb778,
    20px  100px #fdb778,
    40px  100px #c70300,
    60px  100px #c70300,
    80px  100px #c70300,
    100px 100px #fdb778;
}

应用:使用 Sass 编写可维护像素画

上面写的几个例子,至少我是没什么信心去维护好他们。5x5 的像素画要写 25 次属性值,一般的 16x16 则是多达 256 个值。
所以,我们可以使用 Sass 编写可维护像素画。
Sass 环境搭建可以参考以下文章(日语)
https://kuroeveryday.blogspot...

Sass 使用 mixin(function 亦可)生成样式的方法:

@mixin pixelize($matrix, $size, $colors) {
  $ret: "";

  @for $i from 1 through length($matrix) {
    $row: nth($matrix, $i);

    @for $j from 1 through length($row) {
      $dot: nth($row, $j);

      @if $dot != 0 {
        @if $ret != "" {
          $ret: $ret + ",";
        }

        $color: nth($colors, $dot);
        $ret: $ret + ($j * $size) + " " + ($i * $size) + " " + $color;
      }
    }
  }

  box-shadow: unquote($ret + ";");
}

$heart-colors: (#333, #f11416, #831200);
$heart: (
  (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),
  (0,0,1,1,1,0,0,0,0,0,1,1,1,0,0,0),
  (0,1,2,2,2,1,0,0,0,1,2,2,3,1,0,0),
  (1,2,0,0,2,2,1,0,1,2,2,2,2,3,1,0),
  (1,2,0,2,2,2,2,1,2,2,2,2,2,3,1,0),
  (1,2,2,2,2,2,2,2,2,2,2,2,2,3,1,0),
  (1,2,2,2,2,2,2,2,2,2,2,2,2,3,1,0),
  (1,2,2,2,2,2,2,2,2,2,2,2,2,3,1,0),
  (0,1,2,2,2,2,2,2,2,2,2,2,3,1,0,0),
  (0,0,1,2,2,2,2,2,2,2,2,3,1,0,0,0),
  (0,0,0,1,2,2,2,2,2,2,3,1,0,0,0,0),
  (0,0,0,0,1,2,2,2,2,3,1,0,0,0,0,0),
  (0,0,0,0,0,1,2,2,3,1,0,0,0,0,0,0),
  (0,0,0,0,0,0,1,3,1,0,0,0,0,0,0,0),
  (0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0),
  (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
);

.icon {
  width: 20px;
  height: 20px;
  @include pixelize($heart, 20px, $heart-colors);
}

定义名为 pixelize 的 mixin,把像素画的矩阵($heart)像素点的大小(20px)颜色列表($hearts-colors)传入其中,即可生成 box-shadow 属性。
像素画的矩阵用数字 0~N 表示,0 为透明,1~n 为颜色列表对应颜色。

如果有代码高亮的话,像素画的图案就一目了然啦。

与原生 CSS 相比,这样简单多了吧?

如果这样都觉得麻烦,可以使用 CSS 像素画生成器~

CSSドット絵ジェネレータ

番外篇:制作像素动画

之前 icon 类直接使用 box-shadow 属性绘制像素画,在制作像素动画时,需要使用 CSS animation。

.mario {
  width: 8px;
  height: 8px;

  animation:
    jump 1s infinite,
    sprite 1s infinite;
}

/* 跳跃动作(上下移動) */
@keyframes jump {
  from, 25%, 75%, to {
    transform: translateY(0);
  }
  50% {
    transform: translateY(calc(8px * -8));
  }
}

/* 普通状态和跳跃状态的像素画 */
@keyframes sprite {
  /* 对比 animation-timing-function: steps(n)
   * 使用百分比可以更细致的调整动画时间 
   */
  from, 24%, 76%, to {
    box-shadow: /* 普通状态的像素画 */
  }
  25%, 75% {
    box-shadow: /* 跳跃状态的像素画 */
  }
}

使用 CSS 动画修改 box-shadow 和元素的位置,看起来就像是跳起来一样。
详细代码可以在 github 仓库中了解
https://github.com/BcRikko/cs...

查看原文

赞 38 收藏 24 评论 0

libinfs 赞了文章 · 2018-11-29

浏览器缓存原理以及本地存储

作为一名前端工作人员,前端的缓存知识是必须掌握的,因为一个网站打开网页的速度直接关系到用户体验,用户粘度,而提高网页的打开速度有很多方面需要优化,其中比较重要的一点就是利用好缓存,缓存文件可以重复利用,还可以减少带宽,降低网络负荷。

1 缓存

缓存从宏观上分为私有缓存和共享缓存,共享缓存就是那些能被各级代理缓存的缓存。私有缓存就是用户专享的,各级代理不能缓存的缓存。

缓存从微观上可以分为以下几类:

  • 浏览器缓存
  • 代理服务器缓存
  • CDN缓存
  • 数据库缓存
  • 应用层缓存

这里主要对浏览器的缓存进行说明:

clipboard.png

2 http缓存

2.1 强缓存

  • 不会向服务器发送请求,直接从缓存中读取资源
  • 请求返回200的状态码
  • 在chrome控制台的network选项中可以看到size显示from disk cache或from memory cache。
from memory cache代表使用内存中的缓存,from disk cache则代表使用的是硬盘中的缓存,浏览器读取缓存的顺序为memory –> disk。在浏览器中,浏览器会在js和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取(from memory cache);而css文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(from disk cache)。

clipboard.png

Expires和Cache-Control两者对比:其实这两者差别不大,区别就在于 Expires 是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires

2.2 协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程
  • 协商缓存生效,返回304和Not Modified

clipboard.png

2.2.1 Last-Modified和If-Modified-Since

浏览器在第一次访问资源时,服务器返回资源的同时,在response header中添加 Last-Modified的header,值是这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和header;

浏览器下一次请求这个资源,浏览器检测到有 Last-Modified这个header,于是添加If-Modified-Since这个header,值就是Last-Modified中的值;服务器再次收到这个资源请求,会根据 If-Modified-Since 中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回304和空的响应体,直接从缓存读取,如果If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200

缺点:1、某些服务端不能获取精确的修改时间 2、文件修改时间改了,但文件内容却没有变

2.2.2 ETag和If-None-Match

Etag是上一次加载资源时,服务器返回的response header,是对该资源的一种唯一标识,只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里,服务器只需要比较客户端传来的If-None-Match跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。

2.2.3 协商缓存两种方式的对比

  1. 首先在精确度上,Etag要优于Last-Modified,Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。
  2. 性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
  3. 优先级上,服务器校验优先考虑Etag

3 缓存机制

appcache优先于强缓存,强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回304,继续使用缓存。具体流程看下图:

clipboard.png

不管是浏览器缓存,还是代理服务器缓存,CDN缓存都遵循客户端与服务端之间的缓存机制

4、本地存储

本地存储主要有以下几种,localStorage,sessionStorage和cookie,WebSql和IndexDB主要用在前端有大容量存储需求的页面上,例如,在线编辑浏览器或者网页邮箱。他们都可以将数据存储在浏览器,应该根据不同的场景进行使用。

4.1 Cookie

Cookie主要是由服务器生成,且前端也可以设置,保存在客户端本地的一个文件,通过response响应头的set-Cookie字段进行设置,且Cookie的内容自动在请求的时候被传递给服务器。如下:

[HTTP/1.1 200 OK]
Server:[bfe/1.0.8.18]
Etag:["58860415-98b"]
Cache-Control:[private, no-cache, no-store, proxy-revalidate, no-transform]
Connection:[Keep-Alive]
Set-Cookie:[BDORZ=27315; max-age=86400; domain=.baidu.com; path=/]
Pragma:[no-cache]
Last-Modified:[Mon, 23 Jan 2017 13:24:37 GMT]
Content-Length:[2443]
Date:[Mon, 09 Apr 2018 09:59:06 GMT]
Content-Type:[text/html]

Cookie包含的信息:
它可以记录你的用户ID、密码、浏览过的网页、停留的时间等信息。当你再次来到该网站时,网站通过读取Cookies,得知你的相关信息,就可以做出相应的动作,如在页面显示欢迎你的标语,或者让你不用输入ID、密码就直接登录等等。一个网站只能读取它自己放置的信息,不能读取其他网站的Cookie文件。因此,Cookie文件还保存了host属性,即网站的域名或ip。
这些属性以名值对的方式进行保存,为了安全,它的内容大多进行了加密处理。Cookie文件的命名格式是:用户名@网站地址[数字].txt

Cookie的优点:

  • 给用户更人性化的使用体验,如记住“密码功能”、老用户登录欢迎语
  • 弥补了HTTP无连接特性
  • 站点统计访问人数的一个依据

Cookie的缺点:

  • 它无法解决多人共用一台电脑的问题,带来了不安全因素
  • Cookie文件容易被误删除
  • 一人使用多台电脑
  • Cookies欺骗。修改host文件,可以非法访问目标站点的Cookie
  • 容量有限制,不能超过4kb
  • 在请求头上带着数据安全性差

4.2 localStorage

localStorage主要是前端开发人员,在前端设置,一旦数据保存在本地后,就可以避免再向服务器请求数据,因此减少不必要的数据请求,减少数据在浏览器和服务器间不必要地来回传递。

可以长期存储数据,没有时间限制,一天,一年,两年甚至更长,数据都可以使用。
localStorage中一般浏览器支持的是5M大小,这个在不同的浏览器中localStorage会有所不同

优点:

  • localStorage拓展了cookie的4k限制
  • localStorage可以将第一次请求的5M大小数据直接存储到本地,相比于cookie可以节约带宽
  • localStorage的使用也是遵循同源策略的,所以不同的网站直接是不能共用相同的localStorage

缺点:

  • 需要手动删除,否则长期存在
  • 浏览器大小不一,版本的支持也不一样
  • localStorage只支持string类型的存储,JSON对象需要转换
  • localStorage本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡

4.3 sessionStorage

sessionStorage主要是前端开发人员,在前端设置,sessionStorage(会话存储),只有在浏览器被关闭之前使用,创建另一个页面时同意可以使用,关闭浏览器之后数据就会消失

存储上限限制:不同的浏览器存储的上限也不一样,但大多数浏览器把上限限制在5MB以下

4.4 websql

Web SQL 是在浏览器上模拟数据库,可以使用JS来操作SQL完成对数据的读写。它使用 SQL 来操纵客户端数据库的 API,这些 API 是异步的,规范中使用的方言是SQLlite。数据库还是在服务端,不建议使用,已废弃

4.5 indexDB

随着浏览器的功能不断增强,越来越多的网站开始考虑,将大量数据储存在客户端,这样可以减少从服务器获取数据,直接从本地获取数据。

现有的浏览器数据储存方案,都不适合储存大量数据:Cookie 的大小不超过4KB,且每次请求都会发送回服务器;LocalStorage 在 2.5MB 到 10MB 之间(各家浏览器不同),而且不提供搜索功能,不能建立自定义的索引。所以,需要一种新的解决方案,这就是 IndexedDB 诞生的背景。

通俗地说,IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。

关于indexDB的知识可以查看这篇文章http://www.ruanyifeng.com/blo...

这里,我只是根据自己的理解整理了一下关于缓存,存储方面的知识,还有很多不足的地方,更多实践的知识,还请查看其他文章,如有错误,请指出

参考文章:
https://www.jianshu.com/p/54c...
https://segmentfault.com/a/11...
http://www.cnblogs.com/etoah/...
https://blog.csdn.net/zhouche...

查看原文

赞 127 收藏 100 评论 6

认证与成就

  • 获得 300 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-05-28
个人主页被 4.7k 人浏览