followWinter

followWinter 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

暂时没有

个人动态

followWinter 发布了文章 · 6月13日

活用 Git 撤销,提升工程质量(文末招聘)

0x001 概述

一个优秀的前端工程师,必先是个优秀的工程师;一个优秀的工程师,要为自己写的每一行代码负责,要为自己提交的每一个 commit 负责。话虽这么说,但是不想写一篇十分大而全的文章,就想写一些小东西。

0x002 撤销 git add

我们知道,要提交一个 commit,必须要先 add,那如果把一些不想要 add 的东西 add 进去了,该咋办?下面提供几个场景和解决方案:

场景1:添加某个误创建文件,这个文件是不想要的

  • 环境:误创建了一个 new.txt,并使用 add
$ echo new > new.txt
$ git add .
$ git status

On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   new.txt # 👈
  • 解决方案:直接删除就好了
$ rm new.txt
$ git add .
$ git status

On branch master
nothing to commit, working tree clean # 👈

场景 2:新建了一个组件,但是不希望在这次 commit 中一起提交

  • 环境:创建了一个 Product 组件,但是这个组件不在当前使用,本次的修改只应该包含一个 index.js
$ git status

 On branch master
 Changes to be committed:
     (use "git restore --staged <file>..." to unstage)
    new file:   Product/index.css # 👈 多余
    new file:   Product/index.js # 👈 多余
    new file:   index.js
  • 解决方案 1:使用git rm移除,-r表示文件夹递归,--cache表示从index移除,working tree还保留着
$ git rm -r --cache Product/
rm 'Product/index.css'
rm 'Product/index.js'

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   index.js  # 👈 index 只剩下一个文件了

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    Product/ # 👈但是 working tree 还有
$ git commit -m 'feat: index'
\[master 3bf1398\] feat: index
 1 file changed, 1 insertion(+)
 create mode 100644 index.js
  • 解决方案 2:使用git stashProduct组件先暂存,提交index.js,再放出Product
  1. 暂存Product
$ git stash -- Product
Saved working directory and index state WIP on master: b426e7e feat: initial
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   index.js  # 👈 只剩下一个文件了
  1. 提交 index.js
$ git commit -m 'feat: index'
\[master 3bf1398\] feat: index
 1 file changed, 1 insertion(+)
 create mode 100644 index.js
  1. 取出 stash 中的Product
$ git stash pop
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   Product/index.css  # 👈 又拿出来了
    new file:   Product/index.js  # 👈 又拿出来了

Dropped refs/stash@{0} (28675d4accc329da3e54b0a839967471f21c7102)
  1. 作为独立commit提交
$ git commit -m "feat: product"
\[master de42d60\] feat: product
 2 files changed, 2 insertions(+)
 create mode 100644 Product/index.css
 create mode 100644 Product/index.js

0x003 撤销不想要的修改

场景1:修改了某个文件,但是又不想要其中一些或者全部

  • 环境
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
  modified:   Product/index.js  # 👈 不想要
    modified:   index.js  # 👈 不想要
  • 解决方案1:git stash,然后就不用管它了,适用于全部抛弃或者部分
$ git stash -- Product/index.js
Saved working directory and index state WIP on master: de42d60 feat: product
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   index.js   # 👈 只剩下这个了
no changes added to commit (use "git add" and/or "git commit -a")
  • 解决方案2:git reset,适用于当前全部修改都不要了
$ git reset --hard HEAD
HEAD is now at de42d60 feat: product

$ git status
On branch master
nothing to commit, working tree clean   # 👈 全没了
  • 解决方案3:git checkout,适用于部分抛弃
$ git checkout  -- Product/index.js
$ git status
 On branch master
 Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
    modified:   index.js   # 👈 只剩下这个了
 
 no changes added to commit (use "git add" and/or "git commit -a")

0x004 撤销/修改 git commit

场景1:commit 已经提交,但是 commit message 有错误

  • 场景展示
$ git add .
$ git commit -m 'feat: 错误的 message'
\[master ed31f58\] feat: 错误的 message # 👈 
 2 files changed, 2 insertions(+), 2 deletions(-)
  • 解决方案:git commit --amend
$ git commit --amend -m 'feat: 正确的 message'
\[master 325e290\] feat: 正确的 message # 👈 
 Date: Tue May 26 12:39:07 2020 +0800
 2 files changed, 2 insertions(+), 2 deletions(-)

场景2:commit 已经提交,但是有文件遗漏了

场景展示:

  1. 查看状态,当前有两个文件被修改
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   Product/index.js  # 👈 修改
    modified:   index.js  # 👈 修改
  1. 只提交一个文件
$ git add Product/index.js  # 👈 提交一个
$ git commit -m 'feat: 完成产品功能'
\[master 8f3f605\] feat: 完成产品功能
 1 file changed, 1 insertion(+), 1 deletion(-)
  1. 查看状态,还有一个文件还没提交
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   index.js  # 👈 剩余一个

解决方案1:git commit --amend

$ git add .
$ git commit --amend -m 'feat: 完成 产品功能'   # 👈 提交
\[master 1ae5752\] feat: 完成 产品功能
 Date: Tue May 26 12:49:00 2020 +0800
 2 files changed, 2 insertions(+), 2 deletions(-)
 
$ git status
On branch master
nothing to commit, working tree clean  # 👈 全提交了

解决方案2:git reset --soft HEAD^

$ git reset head^
Unstaged changes after reset:
M   Product/index.js  # 👈 上次提交的又回来了

$ git add .
$ git commit -m 'feat: 完成 产品功能'  # 👈 再次提交
\[master 1ae5752\] feat: 完成 产品功能
 Date: Tue May 26 12:49:00 2020 +0800
 2 files changed, 2 insertions(+), 2 deletions(-)

场景3:commit 已经提交了,但是多了一些文件

  • 场景展示:
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   Product/index.css
    new file:   Product/index.js
    modified:   index.js

# 原本只想提交 Product 相关文件,但是误把 index.js 提交了
$ git add .
$ git commit -m 'feat: 完成 Product 组件'
\[master 6c0f3c8\] feat: 完成 Product 组件
 3 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 Product/index.css
 create mode 100644 Product/index.js

解决方案1: git reset head^ 之后重新提交

$ git reset head^
Unstaged changes after reset:
M   index.js

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   index.js  # 👈 提交的又回来了

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    Product/  # 👈 提交的又回来了

no changes added to commit (use "git add" and/or "git commit -a")

$ git add Product/\*   # 👈 只提交 Product
$ git commit -m 'feat: 完成 Product 组件'
\[master 9b9d085\] feat: 完成 Product 组件
 2 files changed, 2 insertions(+)
 create mode 100644 Product/index.css
 create mode 100644 Product/index.js

0x005 删除一个 commit(最新的一个)

有时候我们不想要一个 commit 了怎么办,这里提供几个解决方案

  • 场景展示:
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    modified:   index.js

$ git commit -m 'feat: 不想要的提交'
\[master aa47ff2\] feat: 不想要的提交
 1 file changed, 1 insertion(+)
 
$ git log --pretty=oneline
aa47ff2541fa64e6d44735e90dedcee60d5fb0a3 (HEAD -> master) feat: 不想要的提交   # 👈 这个提交不想要
0dc03f1970ed670b97bccffc92ab5598b5049bf8 feat: 完成首页
9b9d085b932043a2d9250b864a1cbb1535f82184 feat: 完成 Product 组件
...
  • 解决方案1:reset
$ git reset --hard HEAD^
HEAD is now at 0dc03f1 feat: 完成首页

$ git log --pretty=oneline
0dc03f1970ed670b97bccffc92ab5598b5049bf8 (HEAD -> master) feat: 完成首页 # 👈 没了
9b9d085b932043a2d9250b864a1cbb1535f82184 feat: 完成 Product 组件
  • 执行以下命令
$ git rebase 7064a1652cda00d4a57d24d3661b018771a49387 # 倒二个
  • 此时会进入 vim
pick 5acd6e2 feat(project): 添加 version 脚本

# Rebase 7064a16..5acd6e2 onto 7064a16 (1 command)
  • 修改为以下内容并保存
drop 5acd6e2 feat(project): 添加 version 脚本
  • 查看结果
Successfully rebased and updated refs/heads/feat/A.

$  git log --pretty=oneline
7064a1652cda00d4a57d24d3661b018771a49387 (HEAD -> feat/A) feat(changeLost): 添加 changeLog  # 第一个没啦
c45cbb73e79d460473dc941ab062ff7e11d099fa (tag: v1.0.3) 1.0.3
671757d69b98e3d6cae6e4dce5717fac4f77a3a7 feat(project): 添加 ChangeLog
fd369e89072279a7057e236ba1707f2a19709b03 (tag: v1.0.2) 1.0.2
4e9f32cc30525a70c07f3c544594f105c48d8d0f (tag: v1.0.1) 1.0.1
  • 解决方案2:rebase
$ git log --pretty=oneline
5acd6e27a85b7e728454374b9fe516387e2e7450 (HEAD -> feat/A) feat(project): 添加 version 脚本
7064a1652cda00d4a57d24d3661b018771a49387 feat(changeLost): 添加 changeLog
c45cbb73e79d460473dc941ab062ff7e11d099fa (tag: v1.0.3) 1.0.3
671757d69b98e3d6cae6e4dce5717fac4f77a3a7 feat(project): 添加 ChangeLog
fd369e89072279a7057e236ba1707f2a19709b03 (tag: v1.0.2) 1.0.2
4e9f32cc30525a70c07f3c544594f105c48d8d0f (tag: v1.0.1) 1.0.1
  • 解决方案3: revert
$ git revert aa47ff2541fa64e6d44735e90dedcee60d5fb0a3 --no-edit
\[master ae7eae8\] Revert "feat: 不想要的提交"
 Date: Tue May 26 13:45:07 2020 +0800
 1 file changed, 1 deletion(-)

$ git log --pretty=oneline
ae7eae86691a90e81b68b85e0f3cb7ebaee4312c (HEAD -> master) Revert "feat: 不想要的提交"   # 👈 没了
aa47ff2541fa64e6d44735e90dedcee60d5fb0a3 feat: 不想要的提交
0dc03f1970ed670b97bccffc92ab5598b5049bf8 feat: 完成首页
9b9d085b932043a2d9250b864a1cbb1535f82184 feat: 完成 Product 组件
  • 解决方案4:reflog + reset
$ git reflog
6fa3df5 (HEAD -> master) HEAD@{0}: commit: feat: 不想要的提交
0dc03f1 HEAD@{1}: reset: moving to HEAD^   # 👈 这个
aa47ff2 HEAD@{2}: reset: moving to HEAD
aa47ff2 HEAD@{3}: reset: moving to HEAD@{6}

$ git reset --hard HEAD@{1}
HEAD is now at 0dc03f1 feat: 完成首页

0x006 取消一次 merge

  • 场景展示
有 feat/A 和 feat/B 分支合并到 master,如下图现在想撤销 feat/B 到 master 的合并

![image.png](https://intranetproxy.alipay.com/skylark/lark/0/2020/png/281707/1590472368645-536b45c9-ebc2-4f6e-858a-f906614ba5cb.png)  
  • 解决方案1:reset
$ git log --pretty=oneline
aeca903ffad7140b2d1c854c936290c006542f05 (HEAD -> master) Merge branch 'feat/B'
7c8f46806ffb2ee5961e24b4a435d68f893c78c4 (feat/A) feat: 添加功能a   # 👈 回退到这个就行了
1b2dd75b7193a9b37e20a269f5490971adfe4b50 (feat/B) feat: 添加功能b
0dc03f1970ed670b97bccffc92ab5598b5049bf8 feat: 完成首页
9b9d085b932043a2d9250b864a1cbb1535f82184 feat: 完成 Product 组件
$ git reset --hard 7c8f46806ffb2ee5961e24b4a435d68f893c78c4
HEAD is now at 7c8f468 feat: 添加功能a

看看结果

![image.png](https://intranetproxy.alipay.com/skylark/lark/0/2020/png/281707/1590472911298-e89e3453-14ff-4ad6-bf32-3c28e4e26699.png?x-oss-process=image%2Fresize%2Cw_1500 "image.png")
  • 解决方案2:revert
$ git log --pretty=oneline
aeca903ffad7140b2d1c854c936290c006542f05 (HEAD -> master) Merge branch 'feat/B'
7c8f46806ffb2ee5961e24b4a435d68f893c78c4 (feat/A) feat: 添加功能a  # 👈 回退到这个就行了
1b2dd75b7193a9b37e20a269f5490971adfe4b50 (feat/B) feat: 添加功能b
0dc03f1970ed670b97bccffc92ab5598b5049bf8 feat: 完成首页
9b9d085b932043a2d9250b864a1cbb1535f82184 feat: 完成 Product 组件

$ git revert aeca903ffad7140b2d1c854c936290c006542f05 -m 1   # 👈 1是只分支序号
\[master a26fa95\] Revert "Merge branch 'feat/B'"
 1 file changed, 1 deletion(-)
 delete mode 100644 b.txt
  • 解决方案3:reflog + reset
$ git reflog
aeca903 (HEAD -> master) HEAD@{0}: merge feat/B: Merge made by the 'recursive' strategy.
7c8f468 (feat/A) HEAD@{1}: merge feat/A: Fast-forward   # 👈 回退到这个就行了
0dc03f1 HEAD@{2}: checkout: moving from feat/A to master
7c8f468 (feat/A) HEAD@{3}: commit: feat: 添加功能a
0dc03f1 HEAD@{4}: checkout: moving from feat/B to feat/A
1b2dd75 (feat/B) HEAD@{5}: commit: feat: 添加功能b
0dc03f1 HEAD@{6}: checkout: moving from feat/A to feat/B
0dc03f1 HEAD@{7}: checkout: moving from master to feat/A

$ git reset --hard HEAD@{1}
HEAD is now at 7c8f468 feat: 添加功能a

结果:

image.png

0x007 删除提交历史中的一个大文件/敏感文件

这个功能在我看来有两个用处:

  1. 过去提交了敏感文件,比如密钥,期望把它从提交历史中移除
  2. 提交了大文件,导致整个项目变大,虽然后期把它删除了,但是历史中还是存在的
  • 场景展示
\# 不小心把一个安装包给提交进来了
$ git add .
$ git commit -m 'feat: 完成首页'
\[master aa23c6a\] feat: 完成首页
 2 files changed, 1 insertion(+), 1 deletion(-)
 create mode 100644 XMind-for-macOS-10.1.0-202003221812.dmg    # 👈不小心添加的大文件
 
 # 尝试删除 安装包
 $ rm XMind-for-macOS-10.1.0-202003221812.dmg
 $ git add .
 $ git commit -m 'feat: 删除安装包'
\[master 68a3be8\] feat: 删除安装包
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 XMind-for-macOS-10.1.0-202003221812.dmg  # 👈删除

明面上看是删除了,但是看看提交历史,这个文件依旧是存在的,如果是密钥文件,那不免有泄露风险。

image.png

并且文件特别大,看看这时候我们项目有多大吧:

image.png

解决方案1:filter-branch

$ git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch XMind-for-macOS-10.1.0-202003221812.dmg' --prune-empty --tag-name-filter cat -- --all
Rewrite b426e7e5fe1633d4321403e36be5656a99ba4ad8 (1/5) (0 seconds passed, remainRewrite 3bf1398f9010f2358ddb9bb6c29669c11a2fea01 (2/5) (0 seconds passed, remainRewrite 9b9d085b932043a2d9250b864a1cbb1535f82184 (3/5) (0 seconds passed, remainRewrite aa23c6ad093baa8a90264e5d8c761d8cc7b062f8 (4/5) (0 seconds passed, remaining 0 predicted)    rm 'XMind-for-macOS-10.1.0-202003221812.dmg'
Rewrite 68a3be8e878eaba59af800c63856d488de3f3df3 (5/5) (0 seconds passed, remaining 0 predicted)
Ref 'refs/heads/master' was rewritten

再看看历史:

image.png

再看看大小,还是没变啊,咋回事

image.png

继续往下:

$ git for-each-ref --format='delete %(refname)' refs/original | git update-ref --stdin
$ git reflog expire --expire=now --all
$ git gc --prune=now
Enumerating objects: 16, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (16/16), done.
Total 16 (delta 3), reused 14 (delta 3)

image.png

注意:git filter-branch 有很多陷阱,不再推荐使用它来重写历史。 请考虑使用 git-filter-repo,它是一个 Python 脚本,相比大多数使用 filter-branch 的应用来说,它做得要更好。它的文档和源码可访问https://github.com/newren/git-filter-repo获取。

0x008 招聘

新零售技术事业群-CCO技术部的前端团队,我们致力于Web前端技术的研究,同时也重视全栈的能力、产品的推动。Glue即为胶水,我们粘合了设计与技术、用户和产品的关系,用最酷的技术提升客户服务的体验和小二工作的幸福感。FGT也可看成是Fighting的缩写。我们在听得到炮火的地方战斗,我们要把好阿里用户体验的最后一关!

我们负责的产品线非常丰富,PC、无线、内部CRM、Native等等,无论你是什么技术特质,总能找到合适的应用场景。

我们的业务也很有挑战,整个BU致力于通过产品技术创新驱动服务变革,改变人力密集型的传统服务模式。通过打造一站式服务平台,为企业提供全套客户服务解决方案,提升服务体验的同时极大降低服务成本。 通过输出阿里平台沉淀,构建服务生态圈,为电商服务体系贡献水电煤。

如果你来到我们团队,你将负责: 

  • 各业务前端开发 - 平台易用性与用户体验的持续改进 
  • Web前沿技术研究和新技术调研  

职位要求:  

  • 精通各种前端技术,同时具备 PC/无线 端的开发能力,有复杂产品的开发经验。  
  • 掌握常见性能优化的方法,以及衡量产品性能的方法。  
  • 对 MVC/MVVM 等模式有一定的理解,熟悉 React/Vue 等热门框架,有实际项目经验。  
  • 对前端工程化有一定理解,熟练掌握 Webpack/Grunt/Gulp 等构建工具的使用和配置。 ‘
  • 熟悉 ES6/Node.js 并且具备一定的开发能力。

简历投递地址:

  • cangxiu.lyx@alibaba-inc.com

微信(不投简历交个朋友也行哦):

IMG_2725.JPG

查看原文

赞 0 收藏 0 评论 0

followWinter 发布了文章 · 4月17日

每一场面试都是人生的一次战役(最后有福利)

〇、概述

一个优秀的工程师,必先是个优秀的程序员,一个优秀的程序员,必先是个优秀的人。这篇文章并不讲述如何在短时间内拿到 offer,而是讲述我作为一个普通的技术人,如何将面试当作人生规划的一部分,一步一步达到自己的目标。其时间跨度是一年、三年、五年,而不是一个月、两个月。

一、产品化思维分析自我,建立能力模型

苏格拉底提出“认识你自己”,在这里,我们要做的也是“认识我们自己”。当然不是为了解答“你是谁,你从哪里来,往哪里去?”的哲学问题,而是使用产品化的思维去分析你自己,将自己作为人才市场上竞争的产品,从而建立能力模型。

  1. 要分析什么

    其实就是分析自己的亮点、缺点。作为技术人,分析的就是我们在技术上所擅长的,比如,框架熟悉的是 React、Vue 还是 Angular?其熟悉程度如何?能否解答大部分常见问题?是否有自己的独立见解和实践沉淀?和市场上其他人同质化能力相比,我的能力有何竞争力?

  2. 为何要分析

    分析的目标是为了找出自己的竞争点,明白自己的价值在哪里,从而建立自己的能力模型。在学习过程中加强优点,补足缺点。在面试过程扬长避短,紧抓优点,规避不足和缺点。

  3. 如何分析

    • 列出自己的亮点
    • 找出自己亮点中的不足
    • 列出市场需要的能力你却没有或者不够的(缺点)
    • 针对前面 a、b、c 列出提升方案或者解决方案

亮点和不足的 🌰:我觉得我的亮点是对 React 很熟悉,但是对 React 的实现原理不够了解。那么我就可以列一个关于 React 的提升计划,专攻 React,比如攻读源码,让这个能力更加具备稀缺性。

缺点的 🌰:项目开发中常常需要使用网络相关的知识,但是我除了 axios 调用啥也不会。那么我可以列一个关于网络知识的学习计划,不足这块的不足。

二、工程化方法规划方向,重在扬长补短

其实分析出了我们的能力模型之后,我们就知道下一步该怎么做了,四个字,扬长补短

想要让亮点成为真正在市场上有竞争力的亮点,那就必须提高它的稀缺性。要提高他的稀缺性,首先就是要消灭亮点中的不足。

就像亮点和不足的 🌰 中的亮点是熟悉 React,而不足是对 React 的原理不够熟悉。那么我们可以用工厂化的方法规划,并执行落地(以下是一个 🌰):

熟悉 React.png

注意 3 点:

  1. 树立里程碑,也就是每一根鱼骨,达成之后让自己有成就感

2. 要有沉淀作为规划的支撑,可以是项目,也可以是文章

  1. 规划都在纸上,实践要在脚下

三、重视简历维护,让简历可持续发展

很多人认为简历不过是面试时候的一张纸,但在我看来,简历是你的人生在某个时间点的切面。简历描绘的是你的能力模型,简历是什么样子的,那么你就是什么样子的。那么如何维护简历呢?

  1. 简历中寸土寸金,每一句话都要有意义,有内容,要紧扣你的亮点,重点突出,有实践支撑。 比如亮点是熟悉 React,那么可以有 React 博客、React 项目、React 开源工具作为沉淀输出
  2. 结合规划,跨越一个里程碑,就更新你的简历相关信息。 比如不足是对 React 原理不熟悉,但是当你补足这个不足的时候,就可以更新对 React 的描述了
  3. 宁缺毋滥,宜少不宜多,宜精不宜泛。 以前我喜欢在简历上罗列一大堆技术栈,显示自己经验丰富,但是发现,列得越多,打击面越广。所以“了解 Spring Cloud,MySql,RabbitMQ...”之类的和前端不太相关的技术描述我就变成了“有过后端开发经验,了解常见技术后端技术”,将可能的坑变成了优势。
  4. 面试之后复盘也要更新简历。 当我们面试之后发现简历上的一些描述给自己埋坑或者不符合,面试之后即刻修改。如 3 的 🌰。

四、面试前的准备

  1. 通用题

    通用题其实就是体现个人基本素质的地方。所谓通用题,就是自我介绍,项目介绍,职业规划说明等。看过去像是闲聊,但其实考察已经开始了。每一句话都体现了一个人的知识,素养。所以,推荐打好草稿,并练习,记住,时刻围绕自己建立的能力模型和简历。

  2. 技术题
  • 常见面试题必会
  • 技术要有自己的见解、实践、总结
  • 算法、设计模式之类的属于积硅步致千里,需要始于足下,慢慢沉淀
  1. 询问题
    面试官通常会给你一个机会,让你问他一些问题,这些问题可以准备一下,比如:

    • 团队情况
    • 项目情况
    • 你的职位定位
    • 公司福利
    • ...
  2. 题库

    整理自己的题库,包含上面提到的三种类型的题目

五、面试中的表达

接触过一些面试官,他们说其实有些候选人很优秀,但是不知道如何去表达。我在面试中的表达总结出了以下三条:

  1. 回答前想几秒,打草稿

    得到问题之后先不急于作答,而是先分析面试官所问的问题的目的是什么?然后打个小草稿,如果之前总结过那就更好了

  2. 回答时有条理,可以按总分总

    通常面试官会做一些记录,那么如果你的回答太过泛,而没有中心,则就不知道该记录啥了。举个例子,我喜欢这么说:关于这个问题,我有三个看法,1、xxx,2、xxx,3、xxx,其中 1 说的是 xxx,2 说的是 xxx,3 说的是 xxx。总之 xxx。

    先抛结论,再详细说明,最后再总结。临时发挥其实有点困难,但是如果养成习惯,或者打草稿并刻意练习,还是可以的。

  3. 始终围绕自己的亮点,不要给自己埋坑

    面试中最怕的就是给自己挖坑,比如回答问题的时候引用了自己不熟悉的技术,让整个面试的走向走到了自己不熟悉的领域。所以我们才要分析自己的能力模型,明白自己的优势和劣势,同时对于常见问题需要打个草稿并练习。要将整场面试掌控在自己的领域内,首先先不要给自己挖坑,然后是引领面试官到自己的领域。

六、面试后的复盘

面试时候记录,面试后回顾:

  • 如果是通用题答得不好,就修改好以后添加到自己的题库去
  • 如果是技术题答得不好,就再去深入这个技术之后,总结、沉淀,确保下次能够很好的回答

七、心态

  1. 失败

    失败是常态,成功是运气,或者是趟过了所有的坑积累出来的,就和 bug 一样,重在复盘和解决,让自己不会再一次踏入这个坑

  2. 放弃

    放弃是常态,始终坚持才是不正常的,我基本一周要对人生放弃一次,但是务必找到方法让自己重新振作。比如我用规划来约束自己,达到了目标,那放弃一个周末也没啥大事。

八、总结

  1. 长期有规划
  2. 中期产沉淀
  3. 短期多准备

九、福利

内推海报 copy.png

查看原文

赞 1 收藏 0 评论 0

followWinter 收藏了文章 · 1月6日

前端通用国际化解决方案

文章首发于个人blog,欢迎大家关注。

DI18n

前端通用国际化解决方案

背景

前端技术日新月异,技术栈繁多。以前端框架来说有React, Vue, Angular等等,再配以webpack, gulp, Browserify, fis等等构建工具去满足日常的开发工作。同时在日常的工作当中,不同的项目使用的技术栈也会不一样。当需要对部分项目进行国际化改造时,由于技术栈的差异,这时你需要去寻找和当前项目使用的技术栈相匹配的国际化的插件工具。比如:

  • vue + vue-i18n
  • angular + angular-translate
  • react + react-intl
  • jquery + jquery.i18n.property

等等,同时可能有些页面没有使用框架,或者完全是没有进行工程化的静态前端页面。

为了减少由于不同技术栈所带来的学习相关国际化插件的成本及开发过程中可能遇到的国际化坑,在尝试着分析前端国际化所面临的主要问题及相关的解决方案后,我觉得是可以使用更加通用的技术方案去完成国际化的工作。

国际化所面临的问题

1.语言翻译

  • 静态文案翻译(前端静态模板文案)
  • 动态文案翻译(server端下发的动态数据)

2.样式

  • 不同语言文案长度不一样造成的样式错乱
  • 图片的替换

3.map表维护

4.第三方服务

  • SDK

5.本地化

  • 货币单位
  • 货币汇率
  • 时间格式

6.打包方案

  • 运行时
  • 编译后

解决方案

在日常的开发过程当中,遇到的最多的需要国际化的场景是:语言翻译,样式,map表维护打包方案。接下来针对这几块内容并结合日常的开发流程说明国际化的通用解决方案。

首先来看下当前开发环境可能用的技术栈:

1.使用了构建工具

  • webpack
  • gulp
  • fis
  • browserify
  • ...

基于这些构建工具,使用:

  • Vue
  • Angular
  • React
  • Backbone
  • ...
  • 未使用任何framework

2.未使用构建工具

  • 使用了jqueryzepto等类库
  • 原生js

其中在第一种开发流程当中,可用的国际化的工具可选方案较多:

从框架层面来看,各大框架都会有相对应的国际化插件,例如:vue-i18n, angular-translate, react-intl等,这些插件可以无缝接入当前的开发环节当中。优点是这些框架层面的国际化插件使用灵活,可以进行静态文案的翻译,动态文案的翻译。缺点就是开发过程中使用不同的框架还需要去学习相对应的插件,存在一定的学习成本,同时在业务代码中可能存在不同语言包判断逻辑。

从构建工具层面来看, webpack有相对应的i18n-webpack-plugin, gulpgulp-static-i18n等相应的插件。这些插件的套路一般都是在你自定义map语言映射表,同时根据插件定义好的需要被编译的代码格式,然后在代码的编译阶段,通过字符串匹配的形式去完成静态文案的替换工作。这些插件仅仅解决了静态文案的问题,比如一些样式,图片替换,class属性,以及动态文案的翻译等工作并没有做。
事实上,这些插件在编译过程中对于样式图片替换, class属性等替换工作是非常容易完成的,而动态文案的翻译因为缺少context,所以不会选择使用这些编译插件去完成动态文案的翻译工作。相反,将动态文案的翻译放到运行时去完成应该是更加靠谱的。

但是换个角度,抛开基于这些构建工具进行开发的框架来说,构建工具层面的国际化插件可以很好的抹平使用不同框架的差异,通过将国际化的过程从运行时转到编译时,在编译的过程中就完成大部分的国际化任务,降低学习相对应国际化插件的成本,同时在构建打包环节可实现定制化。不过也存在一定的缺点,就是这些构建工具层面的国际化插件只能完成一些基本的静态文案的翻译,因为缺少context,并不能很好的去完成动态文案的翻译工作,它比较适用于一些纯静态,偏展示性的网页。

在第二种开发流程当中,可使用的国际化工具较少,大多都会搭配jquery这些类库及相对应的jquery.i18ni18next等插件去完成国际化。

综合不同的构建工具,开发框架及类库,针对不同的开发环境似乎是可以找到一个比较通用的国际化的方案的。

这个方案的大致思路就是:通过构建工具去完成样式, 图片替换, class属性等的替换工作,在业务代码中不会出现过多的因国际化而多出的变量名,同时使用一个通用的翻译函数去完成静态文案动态文案的翻译工作,而不用使用不同框架提供的相应的国际化插件。简单点来说就是:

  • 依据你使用的构建工具 + 一个通用的翻译函数去完成前端国际化

首先,这个通用的语言翻译函数: di18n-translate。它所提供的功能就是静态和动态文案的翻译, 不依赖开发框架及构建工具。

  npm install di18n-translate
// 模块化写法
  const LOCALE = 'en'
  const DI18n = require('di18n-translate')
  const di18n = new DI18n({
    locale: LOCALE,     // 语言环境 
    isReplace: false,   // 是否开始运行时(适用于没有使用任何构建工具开发流程) 
    messages: {         // 语言映射表 
      en: {
        你好: 'Hello, {person}'
      },
      zh: {
        你好: '你好, {person}'
      }
    }
  })

  di18n继承于一个翻译类,提供了2个方法`$t`, `$html`:
 
  di18n.$t('你好', {person: 'xl'})   // 输出: Hello, xl
  di18n.$html(htmlTemp)   // 传入字符串拼接的dom, 返回匹配后的字符串,具体示例可见下文

// 外链形式
  <script data-original="./lib/di18n-translate/index.js"></script>
  <script>
    const LOCALE = 'en'
    const di18n = new DI18n({
      locale: LOCALE,
      isReplace: false,
      messages: {
        // 语言包
      }
    })
  </script>

这个时候你只需要将这个通用的翻译函数以适当的方式集成到你的开发框架当中去。

接下来会结合具体的不同场景去说明下相应的解决方案:

使用MVVM类的framework

使用了MVVM类的framework时,可以借助framework帮你完成view层的渲染工作, 那么你可以在代码当中轻松的通过代码去控制class的内容, 以及不同语言环境下的图片替换工作.

例如vue, 示例(1):


main.js文件:

window.LOCALE = 'en'

app.vue文件:
  <template>
    <p class="desc"
      :class="locale"   // locale这个变量去控制class的内容
      :style="{backgroundImage: 'url(' + bgImg + ')'}"  // bgImg去控制背景图片的路径
    ></p>
    <img :data-original="imgSrc"> // imgSrc去控制图片路径
  </template>

  <script>
    export default {
      name: 'page',
      data () {
        return {
          locale: LOCALE,
          imgSrc: require(`./${LOCALE}/img/demo.png`),
          bgImg: require(`./${LOCALE}/img/demo.png`)
        }
      }
    }
  </script>

这个时候你再加入翻译函数,就可以满足大部分的国际化的场景了,现在在main.js中添加对翻译函数di18n-translate的引用:

main.js文件:

import Vue from 'vue'

window.LOCALE = 'en'
const DI18n = require('di18n-translate')
const di18n = new DI18n({
    locale: LOCALE,       // 语言环境
    isReplace: false,   // 是否进行替换(适用于没有使用任何构建工具开发流程)
    messages: {         // 语言映射表
      en: {
        你好: 'Hello, {person}'
      },
      zh: {
        你好: '你好, {person}'
      }
    }
  })

Vue.prototype.d18n = di18n

翻译函数的基本使用, 当然你还可以使用其他的方式集成到你的开发环境当中去:

app.vue文件:
  <template>
    <p class="desc"
      :class="locale"   // locale这个变量去控制class的内容
      :style="{backgroundImage: 'url(' + bgImg + ')'}"  // bgImg去控制背景图片的路径
    ></p>
    <img :data-original="imgSrc"> // imgSrc去控制图片路径
    <p>{{title}}</p>
  </template>

  <script>
    export default {
      name: 'page',
      data () {
        return {
          locale: LOCALE,
          imgSrc: require(`./${LOCALE}/img/demo.png`),
          bgImg: require(`./${LOCALE}/img/demo.png`),
          title: this.di18n.$t('你好')
        }
      }
    }
  </script>

使用mvvm framework进行国际化,上述方式应该是较为合适的,主要是借助了framework帮你完成view层的渲染工作, 然后再引入一个翻译函数去完成一些动态文案的翻译工作

这种国际化的方式算是运行时处理,不管是开发还是最终上线都只需要一份代码。

当然在使用mvvm framework的情况下也是可以不借助framework帮我们完成的view层的这部分的功能,而通过构建工具去完成, 这部分的套路可以参见下午的示例3

未使用mvvm框架,使用了构建工具(如webpack/gulp/browserify/fis)

使用了前端模板

国际化的方式和上面说的使用mvvm框架的方式一致,因为有模板引擎帮你完成了view层的渲染.所以对于样式图片class属性的处理可以和上述方式一致, 动态文案的翻译需引入翻译函数。

这种国际化的方式也算是运行时处理,开发和最终上线都只需要一份代码。

没有使用前端模板

因为没用使用前端模板,便少了对于view层的处理。这个时候你的DOM结构可能是在html文件中一开始就定义好的了,也可能是借助于webpack这样能允许你使用模块化进行开发,通过js动态插入DOM的方式。

接下来我们先说说没有借助webpack这样允许你进行模块化开发的构建工具,DOM结构直接是在html文件中写死的项目。这种情况下你失去了对view层渲染能力。那么这种情况下有2种方式去处理这种情况。

第一种方式就是可以在你自己的代码中添加运行时的代码。大致的思路就是在DOM层面添加属性,这些属性及你需要翻译的map表所对应的key值:

示例(2):

html文件:

  <div class="wrapper" i18n-class="${locale}">
    <img i18n-img="/images/${locale}/test.png">
    <input i18n-placeholder="你好">
    <p i18n-content="你好"></p>
  </div>

运行时:

  <script data-original="[PATH]/di18-translate/index.js"></script>
  <script>
    const LOCALE = 'en'
    const di18n = new DI18n({
      locale: LOCALE,
      isReplace: true,   // 开启运行时
      messages: {
        en: {
          你好: 'Hello'
        },
        zh: {
          你好: '你好'
        }
      }
    })
  </script>

最后html会转化为:

  <div class="wrapper en">
    <img data-original="/images/en/test.png">
    <input placeholder="Hello">
    <p>Hello</p>
  </div>

第二种方式就是借助于构建工具在代码编译的环节就完成国际化的工作,以webpack为例:

示例(3):

html文件:

  <div class="wrapper ${locale}">
    <img data-original="/images/${locale}/test.png">
    <p>$t('你好')</p>
  </div>

这个时候使用了一个webpackpreloader: locale-path-loader,它的作用就是在编译编译前,就通过webpack完成语言环境的配置工作,在你的业务代码中不会出现过多的关于语言环境变量以及很好的解决了运行时作为cssbackground的图片替换工作, 具体的locale-path-loader文档请戳我

使用方法:

  npm install locale-path-loader

webpack 1.x 配置:

  module.exports = {
    ....
    preLoaders: [
      {
        test: /\.*$/,
        exclude: /node_modules/,
        loaders: [
          'eslint',
          'locale-path?outputDir=./src/common&locale=en&inline=true'
        ]
      } 
    ]
    ....
  }

webpack 2 配置:

  module.exports = {
    ....
    module: {
      rules: [{
        test: /\.*$/,
        enforce: 'pre',
        exclude: /node_modules/,
        use: [{
          loader: 'locale-path-loader',
          options: {
            locale: 'en',
            outputDir: './src/common',
            inline: true
          }
        }]
      }]
    }
    ....
  }

经过webpackpreloader处理后,被插入到页面中的DOM最后成为:

  <div class="wrapper en">
    <img data-original="/images/en/test.png">
    <p>Hello</p>
  </div>

但是使用这种方案需要在最后的打包环节做下处理,因为通过preloader的处理,页面已经被翻译成相对应的语言版本了,所以需要通过构建工具以及改变preloader的参数去输出不同的语言版本文件。当然构建工具不止webpack这一种,不过这种方式处理的思路是一致的。
这种方式属于编译时处理,开发时只需要维护一份代码,但是最后输出的时候会输出不同语言包的代码。当然这个方案还需要服务端的支持,根据不同语言环境请求,返回相对应的入口文件。关于这里使用webpack搭配locale-path-loader进行分包的内容可参见vue-demo:

|--deploy
  |   |
  |   |---en
  |   |    |--app.js
  |   |    |--vendor.js
  |   |    |--index.html
  |   |---zh
  |   |    |--app.js
  |   |    |--vendor.js
  |   |    |--index.html
  |   |---jp
  |   |    |--app.js
  |   |    |--vendor.js
  |   |    |--index.html
  |   |----lang.json

接下来继续说下借助构建工具进行模块化开发的项目, 这些项目可能最后页面上的DOM都是通过js去动态插入到页面当中的。那么,很显然,可以在DOM被插入到页面前即可以完成静态文案翻译样式, 图片替换, class属性等替换的工作。

示例(4):
html文件:

  <div class="wrapper ${locale}">
    <img data-original="/images/${locale}/test.png">
    <p>$t('你好')</p>
  </div>

js文件:

  let tpl = require('html!./index.html')
  let wrapper = document.querySelector('.box-wrapper')
  
  // di18n.$html方法即对你所加载的html字符串进行replace,最后相对应的语言版本
  wrapper.innerHTML = di18n.$html(tpl)

最后插入到的页面当中的DOM为:

  <div class="wrapper en">
    <img data-original="/images/en/test.png">
    <p>Hello</p>
  </div>

这个时候动态翻译再借助引入的di18n上的$t方法

  di18n.$t('你好')

这种开发方式也属于运行时处理,开发和上线后只需要维护一份代码。

没有使用任何framework构建工具的纯静态,偏展示性的网页

这类网页的国际化,可以用上面提到的通过在代码中注入运行时来完成基本的国际化的工作, 具体内容可以参见示例(2)以及仓库中的html-demo文件夹。

语言包map表的维护

建议将语言包单独新建文件维护,通过异步加载的方式去获取语言包.

项目地址(如果觉得文章不错,请不要吝啬你的star~~)

请戳我

最后需要感谢 @kenberkeley 同学,之前和他有过几次关于国际化的探讨,同时关于编译时这块的内容,他的有篇文章(请戳我)也给了我一些比较好的思路。

查看原文

followWinter 赞了文章 · 1月6日

前端通用国际化解决方案

文章首发于个人blog,欢迎大家关注。

DI18n

前端通用国际化解决方案

背景

前端技术日新月异,技术栈繁多。以前端框架来说有React, Vue, Angular等等,再配以webpack, gulp, Browserify, fis等等构建工具去满足日常的开发工作。同时在日常的工作当中,不同的项目使用的技术栈也会不一样。当需要对部分项目进行国际化改造时,由于技术栈的差异,这时你需要去寻找和当前项目使用的技术栈相匹配的国际化的插件工具。比如:

  • vue + vue-i18n
  • angular + angular-translate
  • react + react-intl
  • jquery + jquery.i18n.property

等等,同时可能有些页面没有使用框架,或者完全是没有进行工程化的静态前端页面。

为了减少由于不同技术栈所带来的学习相关国际化插件的成本及开发过程中可能遇到的国际化坑,在尝试着分析前端国际化所面临的主要问题及相关的解决方案后,我觉得是可以使用更加通用的技术方案去完成国际化的工作。

国际化所面临的问题

1.语言翻译

  • 静态文案翻译(前端静态模板文案)
  • 动态文案翻译(server端下发的动态数据)

2.样式

  • 不同语言文案长度不一样造成的样式错乱
  • 图片的替换

3.map表维护

4.第三方服务

  • SDK

5.本地化

  • 货币单位
  • 货币汇率
  • 时间格式

6.打包方案

  • 运行时
  • 编译后

解决方案

在日常的开发过程当中,遇到的最多的需要国际化的场景是:语言翻译,样式,map表维护打包方案。接下来针对这几块内容并结合日常的开发流程说明国际化的通用解决方案。

首先来看下当前开发环境可能用的技术栈:

1.使用了构建工具

  • webpack
  • gulp
  • fis
  • browserify
  • ...

基于这些构建工具,使用:

  • Vue
  • Angular
  • React
  • Backbone
  • ...
  • 未使用任何framework

2.未使用构建工具

  • 使用了jqueryzepto等类库
  • 原生js

其中在第一种开发流程当中,可用的国际化的工具可选方案较多:

从框架层面来看,各大框架都会有相对应的国际化插件,例如:vue-i18n, angular-translate, react-intl等,这些插件可以无缝接入当前的开发环节当中。优点是这些框架层面的国际化插件使用灵活,可以进行静态文案的翻译,动态文案的翻译。缺点就是开发过程中使用不同的框架还需要去学习相对应的插件,存在一定的学习成本,同时在业务代码中可能存在不同语言包判断逻辑。

从构建工具层面来看, webpack有相对应的i18n-webpack-plugin, gulpgulp-static-i18n等相应的插件。这些插件的套路一般都是在你自定义map语言映射表,同时根据插件定义好的需要被编译的代码格式,然后在代码的编译阶段,通过字符串匹配的形式去完成静态文案的替换工作。这些插件仅仅解决了静态文案的问题,比如一些样式,图片替换,class属性,以及动态文案的翻译等工作并没有做。
事实上,这些插件在编译过程中对于样式图片替换, class属性等替换工作是非常容易完成的,而动态文案的翻译因为缺少context,所以不会选择使用这些编译插件去完成动态文案的翻译工作。相反,将动态文案的翻译放到运行时去完成应该是更加靠谱的。

但是换个角度,抛开基于这些构建工具进行开发的框架来说,构建工具层面的国际化插件可以很好的抹平使用不同框架的差异,通过将国际化的过程从运行时转到编译时,在编译的过程中就完成大部分的国际化任务,降低学习相对应国际化插件的成本,同时在构建打包环节可实现定制化。不过也存在一定的缺点,就是这些构建工具层面的国际化插件只能完成一些基本的静态文案的翻译,因为缺少context,并不能很好的去完成动态文案的翻译工作,它比较适用于一些纯静态,偏展示性的网页。

在第二种开发流程当中,可使用的国际化工具较少,大多都会搭配jquery这些类库及相对应的jquery.i18ni18next等插件去完成国际化。

综合不同的构建工具,开发框架及类库,针对不同的开发环境似乎是可以找到一个比较通用的国际化的方案的。

这个方案的大致思路就是:通过构建工具去完成样式, 图片替换, class属性等的替换工作,在业务代码中不会出现过多的因国际化而多出的变量名,同时使用一个通用的翻译函数去完成静态文案动态文案的翻译工作,而不用使用不同框架提供的相应的国际化插件。简单点来说就是:

  • 依据你使用的构建工具 + 一个通用的翻译函数去完成前端国际化

首先,这个通用的语言翻译函数: di18n-translate。它所提供的功能就是静态和动态文案的翻译, 不依赖开发框架及构建工具。

  npm install di18n-translate
// 模块化写法
  const LOCALE = 'en'
  const DI18n = require('di18n-translate')
  const di18n = new DI18n({
    locale: LOCALE,     // 语言环境 
    isReplace: false,   // 是否开始运行时(适用于没有使用任何构建工具开发流程) 
    messages: {         // 语言映射表 
      en: {
        你好: 'Hello, {person}'
      },
      zh: {
        你好: '你好, {person}'
      }
    }
  })

  di18n继承于一个翻译类,提供了2个方法`$t`, `$html`:
 
  di18n.$t('你好', {person: 'xl'})   // 输出: Hello, xl
  di18n.$html(htmlTemp)   // 传入字符串拼接的dom, 返回匹配后的字符串,具体示例可见下文

// 外链形式
  <script data-original="./lib/di18n-translate/index.js"></script>
  <script>
    const LOCALE = 'en'
    const di18n = new DI18n({
      locale: LOCALE,
      isReplace: false,
      messages: {
        // 语言包
      }
    })
  </script>

这个时候你只需要将这个通用的翻译函数以适当的方式集成到你的开发框架当中去。

接下来会结合具体的不同场景去说明下相应的解决方案:

使用MVVM类的framework

使用了MVVM类的framework时,可以借助framework帮你完成view层的渲染工作, 那么你可以在代码当中轻松的通过代码去控制class的内容, 以及不同语言环境下的图片替换工作.

例如vue, 示例(1):


main.js文件:

window.LOCALE = 'en'

app.vue文件:
  <template>
    <p class="desc"
      :class="locale"   // locale这个变量去控制class的内容
      :style="{backgroundImage: 'url(' + bgImg + ')'}"  // bgImg去控制背景图片的路径
    ></p>
    <img :data-original="imgSrc"> // imgSrc去控制图片路径
  </template>

  <script>
    export default {
      name: 'page',
      data () {
        return {
          locale: LOCALE,
          imgSrc: require(`./${LOCALE}/img/demo.png`),
          bgImg: require(`./${LOCALE}/img/demo.png`)
        }
      }
    }
  </script>

这个时候你再加入翻译函数,就可以满足大部分的国际化的场景了,现在在main.js中添加对翻译函数di18n-translate的引用:

main.js文件:

import Vue from 'vue'

window.LOCALE = 'en'
const DI18n = require('di18n-translate')
const di18n = new DI18n({
    locale: LOCALE,       // 语言环境
    isReplace: false,   // 是否进行替换(适用于没有使用任何构建工具开发流程)
    messages: {         // 语言映射表
      en: {
        你好: 'Hello, {person}'
      },
      zh: {
        你好: '你好, {person}'
      }
    }
  })

Vue.prototype.d18n = di18n

翻译函数的基本使用, 当然你还可以使用其他的方式集成到你的开发环境当中去:

app.vue文件:
  <template>
    <p class="desc"
      :class="locale"   // locale这个变量去控制class的内容
      :style="{backgroundImage: 'url(' + bgImg + ')'}"  // bgImg去控制背景图片的路径
    ></p>
    <img :data-original="imgSrc"> // imgSrc去控制图片路径
    <p>{{title}}</p>
  </template>

  <script>
    export default {
      name: 'page',
      data () {
        return {
          locale: LOCALE,
          imgSrc: require(`./${LOCALE}/img/demo.png`),
          bgImg: require(`./${LOCALE}/img/demo.png`),
          title: this.di18n.$t('你好')
        }
      }
    }
  </script>

使用mvvm framework进行国际化,上述方式应该是较为合适的,主要是借助了framework帮你完成view层的渲染工作, 然后再引入一个翻译函数去完成一些动态文案的翻译工作

这种国际化的方式算是运行时处理,不管是开发还是最终上线都只需要一份代码。

当然在使用mvvm framework的情况下也是可以不借助framework帮我们完成的view层的这部分的功能,而通过构建工具去完成, 这部分的套路可以参见下午的示例3

未使用mvvm框架,使用了构建工具(如webpack/gulp/browserify/fis)

使用了前端模板

国际化的方式和上面说的使用mvvm框架的方式一致,因为有模板引擎帮你完成了view层的渲染.所以对于样式图片class属性的处理可以和上述方式一致, 动态文案的翻译需引入翻译函数。

这种国际化的方式也算是运行时处理,开发和最终上线都只需要一份代码。

没有使用前端模板

因为没用使用前端模板,便少了对于view层的处理。这个时候你的DOM结构可能是在html文件中一开始就定义好的了,也可能是借助于webpack这样能允许你使用模块化进行开发,通过js动态插入DOM的方式。

接下来我们先说说没有借助webpack这样允许你进行模块化开发的构建工具,DOM结构直接是在html文件中写死的项目。这种情况下你失去了对view层渲染能力。那么这种情况下有2种方式去处理这种情况。

第一种方式就是可以在你自己的代码中添加运行时的代码。大致的思路就是在DOM层面添加属性,这些属性及你需要翻译的map表所对应的key值:

示例(2):

html文件:

  <div class="wrapper" i18n-class="${locale}">
    <img i18n-img="/images/${locale}/test.png">
    <input i18n-placeholder="你好">
    <p i18n-content="你好"></p>
  </div>

运行时:

  <script data-original="[PATH]/di18-translate/index.js"></script>
  <script>
    const LOCALE = 'en'
    const di18n = new DI18n({
      locale: LOCALE,
      isReplace: true,   // 开启运行时
      messages: {
        en: {
          你好: 'Hello'
        },
        zh: {
          你好: '你好'
        }
      }
    })
  </script>

最后html会转化为:

  <div class="wrapper en">
    <img data-original="/images/en/test.png">
    <input placeholder="Hello">
    <p>Hello</p>
  </div>

第二种方式就是借助于构建工具在代码编译的环节就完成国际化的工作,以webpack为例:

示例(3):

html文件:

  <div class="wrapper ${locale}">
    <img data-original="/images/${locale}/test.png">
    <p>$t('你好')</p>
  </div>

这个时候使用了一个webpackpreloader: locale-path-loader,它的作用就是在编译编译前,就通过webpack完成语言环境的配置工作,在你的业务代码中不会出现过多的关于语言环境变量以及很好的解决了运行时作为cssbackground的图片替换工作, 具体的locale-path-loader文档请戳我

使用方法:

  npm install locale-path-loader

webpack 1.x 配置:

  module.exports = {
    ....
    preLoaders: [
      {
        test: /\.*$/,
        exclude: /node_modules/,
        loaders: [
          'eslint',
          'locale-path?outputDir=./src/common&locale=en&inline=true'
        ]
      } 
    ]
    ....
  }

webpack 2 配置:

  module.exports = {
    ....
    module: {
      rules: [{
        test: /\.*$/,
        enforce: 'pre',
        exclude: /node_modules/,
        use: [{
          loader: 'locale-path-loader',
          options: {
            locale: 'en',
            outputDir: './src/common',
            inline: true
          }
        }]
      }]
    }
    ....
  }

经过webpackpreloader处理后,被插入到页面中的DOM最后成为:

  <div class="wrapper en">
    <img data-original="/images/en/test.png">
    <p>Hello</p>
  </div>

但是使用这种方案需要在最后的打包环节做下处理,因为通过preloader的处理,页面已经被翻译成相对应的语言版本了,所以需要通过构建工具以及改变preloader的参数去输出不同的语言版本文件。当然构建工具不止webpack这一种,不过这种方式处理的思路是一致的。
这种方式属于编译时处理,开发时只需要维护一份代码,但是最后输出的时候会输出不同语言包的代码。当然这个方案还需要服务端的支持,根据不同语言环境请求,返回相对应的入口文件。关于这里使用webpack搭配locale-path-loader进行分包的内容可参见vue-demo:

|--deploy
  |   |
  |   |---en
  |   |    |--app.js
  |   |    |--vendor.js
  |   |    |--index.html
  |   |---zh
  |   |    |--app.js
  |   |    |--vendor.js
  |   |    |--index.html
  |   |---jp
  |   |    |--app.js
  |   |    |--vendor.js
  |   |    |--index.html
  |   |----lang.json

接下来继续说下借助构建工具进行模块化开发的项目, 这些项目可能最后页面上的DOM都是通过js去动态插入到页面当中的。那么,很显然,可以在DOM被插入到页面前即可以完成静态文案翻译样式, 图片替换, class属性等替换的工作。

示例(4):
html文件:

  <div class="wrapper ${locale}">
    <img data-original="/images/${locale}/test.png">
    <p>$t('你好')</p>
  </div>

js文件:

  let tpl = require('html!./index.html')
  let wrapper = document.querySelector('.box-wrapper')
  
  // di18n.$html方法即对你所加载的html字符串进行replace,最后相对应的语言版本
  wrapper.innerHTML = di18n.$html(tpl)

最后插入到的页面当中的DOM为:

  <div class="wrapper en">
    <img data-original="/images/en/test.png">
    <p>Hello</p>
  </div>

这个时候动态翻译再借助引入的di18n上的$t方法

  di18n.$t('你好')

这种开发方式也属于运行时处理,开发和上线后只需要维护一份代码。

没有使用任何framework构建工具的纯静态,偏展示性的网页

这类网页的国际化,可以用上面提到的通过在代码中注入运行时来完成基本的国际化的工作, 具体内容可以参见示例(2)以及仓库中的html-demo文件夹。

语言包map表的维护

建议将语言包单独新建文件维护,通过异步加载的方式去获取语言包.

项目地址(如果觉得文章不错,请不要吝啬你的star~~)

请戳我

最后需要感谢 @kenberkeley 同学,之前和他有过几次关于国际化的探讨,同时关于编译时这块的内容,他的有篇文章(请戳我)也给了我一些比较好的思路。

查看原文

赞 88 收藏 143 评论 19

followWinter 收藏了文章 · 2019-12-03

布隆过滤器你值得拥有的开发利器

在程序的世界中,布隆过滤器是程序员的一把利器,利用它可以快速地解决项目中一些比较棘手的问题。如网页 URL 去重、垃圾邮件识别、大集合中重复元素的判断和缓存穿透等问题。

布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

阅读更多关于 Angular、TypeScript、Node.js/Java 、Spring 等技术文章,欢迎访问我的个人博客 ——全栈修仙之路

一、布隆过滤器简介

当你往简单数组或列表中插入新数据时,将不会根据插入项的值来确定该插入项的索引值。这意味着新插入项的索引值与数据值之间没有直接关系。这样的话,当你需要在数组或列表中搜索相应值的时候,你必须遍历已有的集合。若集合中存在大量的数据,就会影响数据查找的效率。

针对这个问题,你可以考虑使用哈希表。利用哈希表你可以通过对 “值” 进行哈希处理来获得该值对应的键或索引值,然后把该值存放到列表中对应的索引位置。这意味着索引值是由插入项的值所确定的,当你需要判断列表中是否存在该值时,只需要对值进行哈希处理并在相应的索引位置进行搜索即可,这时的搜索速度是非常快的。

bf-array-vs-hashtable.jpg

根据定义,布隆过滤器可以检查值是 “可能在集合中” 还是 “绝对不在集合中”。“可能” 表示有一定的概率,也就是说可能存在一定为误判率。那为什么会存在误判呢?下面我们来分析一下具体的原因。

布隆过滤器(Bloom Filter)本质上是由长度为 m 的位向量或位列表(仅包含 0 或 1 位值的列表)组成,最初所有的值均设置为 0,如下图所示。

bf-bit-vector.jpg

为了将数据项添加到布隆过滤器中,我们会提供 K 个不同的哈希函数,并将结果位置上对应位的值置为 “1”。在前面所提到的哈希表中,我们使用的是单个哈希函数,因此只能输出单个索引值。而对于布隆过滤器来说,我们将使用多个哈希函数,这将会产生多个索引值。

bf-input-hash.jpg

如上图所示,当输入 “semlinker” 时,预设的 3 个哈希函数将输出 2、4、6,我们把相应位置 1。假设另一个输入 ”kakuqo“,哈希函数输出 3、4 和 7。你可能已经注意到,索引位 4 已经被先前的 “semlinker” 标记了。此时,我们已经使用 “semlinker” 和 ”kakuqo“ 两个输入值,填充了位向量。当前位向量的标记状态为:

bf-input-hash-1.jpg

当对值进行搜索时,与哈希表类似,我们将使用 3 个哈希函数对 ”搜索的值“ 进行哈希运算,并查看其生成的索引值。假设,当我们搜索 ”fullstack“ 时,3 个哈希函数输出的 3 个索引值分别是 2、3 和 7:

bf-input-hash-2.jpg

从上图可以看出,相应的索引位都被置为 1,这意味着我们可以说 ”fullstack“ 可能已经插入到集合中。事实上这是误报的情形,产生的原因是由于哈希碰撞导致的巧合而将不同的元素存储在相同的比特位上。幸运的是,布隆过滤器有一个可预测的误判率(FPP):

bf-fpp.jpg

  • n 是已经添加元素的数量;
  • k 哈希的次数;
  • m 布隆过滤器的长度(如比特数组的大小)。

极端情况下,当布隆过滤器没有空闲空间时(满),每一次查询都会返回 true 。这也就意味着 m 的选择取决于期望预计添加元素的数量 n ,并且 m 需要远远大于 n

实际情况中,布隆过滤器的长度 m 可以根据给定的误判率(FFP)的和期望添加的元素个数 n 的通过如下公式计算:

bf-bit-vector-length.jpg

对于 m/n 比率表示每一个元素需要分配的比特位的数量,也就是哈希函数 k 的数量可以调整误判率。通过如下公式来选择最佳的 k 可以减少误判率(FPP):

bf-hash-fn-count.jpg

了解完上述的内容之后,我们可以得出一个结论,当我们搜索一个值的时候,若该值经过 K 个哈希函数运算后的任何一个索引位为 ”0“,那么该值肯定不在集合中。但如果所有哈希索引值均为 ”1“,则只能说该搜索的值可能存在集合中。

二、布隆过滤器应用

在实际工作中,布隆过滤器常见的应用场景如下:

  • 网页爬虫对 URL 去重,避免爬取相同的 URL 地址;
  • 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱;
  • Google Chrome 使用布隆过滤器识别恶意 URL;
  • Medium 使用布隆过滤器避免推荐给用户已经读过的文章;
  • Google BigTable,Apache HBbase 和 Apache Cassandra 使用布隆过滤器减少对不存在的行和列的查找。

除了上述的应用场景之外,布隆过滤器还有一个应用场景就是解决缓存穿透的问题。所谓的缓存穿透就是服务调用方每次都是查询不在缓存中的数据,这样每次服务调用都会到数据库中进行查询,如果这类请求比较多的话,就会导致数据库压力增大,这样缓存就失去了意义。

利用布隆过滤器我们可以预先把数据查询的主键,比如用户 ID 或文章 ID 缓存到过滤器中。当根据 ID 进行数据查询的时候,我们先判断该 ID 是否存在,若存在的话,则进行下一步处理。若不存在的话,直接返回,这样就不会触发后续的数据库查询。需要注意的是缓存穿透不能完全解决,我们只能将其控制在一个可以容忍的范围内。

三、布隆过滤器实战

布隆过滤器有很多实现和优化,由 Google 开发著名的 Guava 库就提供了布隆过滤器(Bloom Filter)的实现。在基于 Maven 的 Java 项目中要使用 Guava 提供的布隆过滤器,只需要引入以下坐标:

<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>28.0-jre</version>
</dependency>

在导入 Guava 库后,我们新建一个 BloomFilterDemo 类,在 main 方法中我们通过 BloomFilter.create 方法来创建一个布隆过滤器,接着我们初始化 1 百万条数据到过滤器中,然后在原有的基础上增加 10000 条数据并判断这些数据是否存在布隆过滤器中:

import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class BloomFilterDemo {
    public static void main(String[] args) {
        int total = 1000000; // 总数量
        BloomFilter<CharSequence> bf = 
          BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), total);
        // 初始化 1000000 条数据到过滤器中
        for (int i = 0; i < total; i++) {
            bf.put("" + i);
        }
        // 判断值是否存在过滤器中
        int count = 0;
        for (int i = 0; i < total + 10000; i++) {
            if (bf.mightContain("" + i)) {
                count++;
            }
        }
        System.out.println("已匹配数量 " + count);
    }
}

当以上代码运行后,控制台会输出以下结果:

已匹配数量 1000309

很明显以上的输出结果已经出现了误报,因为相比预期的结果多了 309 个元素,误判率为:

309/(1000000 + 10000) * 100 ≈ 0.030594059405940593

如果要提高匹配精度的话,我们可以在创建布隆过滤器的时候设置误判率 fpp:

BloomFilter<CharSequence> bf = BloomFilter.create(
  Funnels.stringFunnel(Charsets.UTF_8), total, 0.0002
);

在 BloomFilter 内部,误判率 fpp 的默认值是 0.03:

// com/google/common/hash/BloomFilter.class
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
  return create(funnel, expectedInsertions, 0.03D);
}

在重新设置误判率为 0.0002 之后,我们重新运行程序,这时控制台会输出以下结果:

已匹配数量 1000003

通过观察以上的结果,可知误判率 fpp 的值越小,匹配的精度越高。当减少误判率 fpp 的值,需要的存储空间也越大,所以在实际使用过程中需要在误判率和存储空间之间做个权衡。

四、简易版布隆过滤器

为了便于大家理解布隆过滤器,我们来看一下下面简易版布隆过滤器。

package com.semlinker.bloomfilter;

import java.util.BitSet;

public class SimpleBloomFilter {
    private static final int DEFAULT_SIZE = 2 << 24;
    private static final int[] seeds = new int[]{7, 11, 13, 31, 37, 61};

    private BitSet bits = new BitSet(DEFAULT_SIZE);
    private SimpleHash[] func = new SimpleHash[seeds.length];

    public SimpleBloomFilter() {
        // 创建多个哈希函数
        for (int i = 0; i < seeds.length; i++) {
            func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]);
        }
    }

    /**
     * 添加元素到布隆过滤器中
     *
     * @param value
     */
    public void put(String value) {
        for (SimpleHash f : func) {
            bits.set(f.hash(value), true);
        }
    }

    /**
     * 判断布隆过滤器中是否包含指定元素
     *
     * @param value
     * @return
     */
    public boolean mightContain(String value) {
        if (value == null) {
            return false;
        }
        boolean ret = true;
        for (SimpleHash f : func) {
            ret = ret && bits.get(f.hash(value));
        }
        return ret;
    }

    public static void main(String[] args) {
        SimpleBloomFilter bf = new SimpleBloomFilter();
        for (int i = 0; i < 1000000; i++) {
            bf.put("" + i);
        }
        // 判断值是否存在过滤器中
        int count = 0;
        for (int i = 0; i < 1000000 + 10000; i++) {
            if (bf.mightContain("" + i)) {
                count++;
            }
        }
        System.out.println("已匹配数量 " + count);
    }

    /**
     * 简单哈希类
     */
    public static class SimpleHash {
        private int cap;
        private int seed;

        public SimpleHash(int cap, int seed) {
            this.cap = cap;
            this.seed = seed;
        }

        public int hash(String value) {
            int result = 0;
            int len = value.length();
            for (int i = 0; i < len; i++) {
                result = seed * result + value.charAt(i);
            }
            return (cap - 1) & result;
        }
    }
}

在 SimpleBloomFilter 类的实现中,我们使用到了 Java util 包中的 BitSet,BitSet 是位操作的对象,值只有 0 或 1 ,内部维护了一个 long 数组,初始只有一个 long,所以 BitSet 最小的容量是 64 位。当随着存储的元素越来越多,BitSet 内部会动态扩容,最终内部是由 N 个 long 值来存储。默认情况下,BitSet 的所有位都是 0。

五、总结

本文主要介绍的布隆过滤器的概念和常见的应用场合,在实战部分我们演示了 Google 著名的 Guava 库所提供布隆过滤器(Bloom Filter)的基本使用,同时我们也介绍了布隆过滤器出现误报的原因及如何提高判断准确性。最后为了便于大家理解布隆过滤器,我们介绍了一个简易版的布隆过滤器 SimpleBloomFilter。

六、参考资源

本人的全栈修仙之路订阅号,会定期分享 Angular、TypeScript、Node.js/Java 、Spring 相关文章,欢迎感兴趣的小伙伴订阅哈!

full-stack-logo

查看原文

followWinter 赞了文章 · 2019-12-03

布隆过滤器你值得拥有的开发利器

在程序的世界中,布隆过滤器是程序员的一把利器,利用它可以快速地解决项目中一些比较棘手的问题。如网页 URL 去重、垃圾邮件识别、大集合中重复元素的判断和缓存穿透等问题。

布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

阅读更多关于 Angular、TypeScript、Node.js/Java 、Spring 等技术文章,欢迎访问我的个人博客 ——全栈修仙之路

一、布隆过滤器简介

当你往简单数组或列表中插入新数据时,将不会根据插入项的值来确定该插入项的索引值。这意味着新插入项的索引值与数据值之间没有直接关系。这样的话,当你需要在数组或列表中搜索相应值的时候,你必须遍历已有的集合。若集合中存在大量的数据,就会影响数据查找的效率。

针对这个问题,你可以考虑使用哈希表。利用哈希表你可以通过对 “值” 进行哈希处理来获得该值对应的键或索引值,然后把该值存放到列表中对应的索引位置。这意味着索引值是由插入项的值所确定的,当你需要判断列表中是否存在该值时,只需要对值进行哈希处理并在相应的索引位置进行搜索即可,这时的搜索速度是非常快的。

bf-array-vs-hashtable.jpg

根据定义,布隆过滤器可以检查值是 “可能在集合中” 还是 “绝对不在集合中”。“可能” 表示有一定的概率,也就是说可能存在一定为误判率。那为什么会存在误判呢?下面我们来分析一下具体的原因。

布隆过滤器(Bloom Filter)本质上是由长度为 m 的位向量或位列表(仅包含 0 或 1 位值的列表)组成,最初所有的值均设置为 0,如下图所示。

bf-bit-vector.jpg

为了将数据项添加到布隆过滤器中,我们会提供 K 个不同的哈希函数,并将结果位置上对应位的值置为 “1”。在前面所提到的哈希表中,我们使用的是单个哈希函数,因此只能输出单个索引值。而对于布隆过滤器来说,我们将使用多个哈希函数,这将会产生多个索引值。

bf-input-hash.jpg

如上图所示,当输入 “semlinker” 时,预设的 3 个哈希函数将输出 2、4、6,我们把相应位置 1。假设另一个输入 ”kakuqo“,哈希函数输出 3、4 和 7。你可能已经注意到,索引位 4 已经被先前的 “semlinker” 标记了。此时,我们已经使用 “semlinker” 和 ”kakuqo“ 两个输入值,填充了位向量。当前位向量的标记状态为:

bf-input-hash-1.jpg

当对值进行搜索时,与哈希表类似,我们将使用 3 个哈希函数对 ”搜索的值“ 进行哈希运算,并查看其生成的索引值。假设,当我们搜索 ”fullstack“ 时,3 个哈希函数输出的 3 个索引值分别是 2、3 和 7:

bf-input-hash-2.jpg

从上图可以看出,相应的索引位都被置为 1,这意味着我们可以说 ”fullstack“ 可能已经插入到集合中。事实上这是误报的情形,产生的原因是由于哈希碰撞导致的巧合而将不同的元素存储在相同的比特位上。幸运的是,布隆过滤器有一个可预测的误判率(FPP):

bf-fpp.jpg

  • n 是已经添加元素的数量;
  • k 哈希的次数;
  • m 布隆过滤器的长度(如比特数组的大小)。

极端情况下,当布隆过滤器没有空闲空间时(满),每一次查询都会返回 true 。这也就意味着 m 的选择取决于期望预计添加元素的数量 n ,并且 m 需要远远大于 n

实际情况中,布隆过滤器的长度 m 可以根据给定的误判率(FFP)的和期望添加的元素个数 n 的通过如下公式计算:

bf-bit-vector-length.jpg

对于 m/n 比率表示每一个元素需要分配的比特位的数量,也就是哈希函数 k 的数量可以调整误判率。通过如下公式来选择最佳的 k 可以减少误判率(FPP):

bf-hash-fn-count.jpg

了解完上述的内容之后,我们可以得出一个结论,当我们搜索一个值的时候,若该值经过 K 个哈希函数运算后的任何一个索引位为 ”0“,那么该值肯定不在集合中。但如果所有哈希索引值均为 ”1“,则只能说该搜索的值可能存在集合中。

二、布隆过滤器应用

在实际工作中,布隆过滤器常见的应用场景如下:

  • 网页爬虫对 URL 去重,避免爬取相同的 URL 地址;
  • 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱;
  • Google Chrome 使用布隆过滤器识别恶意 URL;
  • Medium 使用布隆过滤器避免推荐给用户已经读过的文章;
  • Google BigTable,Apache HBbase 和 Apache Cassandra 使用布隆过滤器减少对不存在的行和列的查找。

除了上述的应用场景之外,布隆过滤器还有一个应用场景就是解决缓存穿透的问题。所谓的缓存穿透就是服务调用方每次都是查询不在缓存中的数据,这样每次服务调用都会到数据库中进行查询,如果这类请求比较多的话,就会导致数据库压力增大,这样缓存就失去了意义。

利用布隆过滤器我们可以预先把数据查询的主键,比如用户 ID 或文章 ID 缓存到过滤器中。当根据 ID 进行数据查询的时候,我们先判断该 ID 是否存在,若存在的话,则进行下一步处理。若不存在的话,直接返回,这样就不会触发后续的数据库查询。需要注意的是缓存穿透不能完全解决,我们只能将其控制在一个可以容忍的范围内。

三、布隆过滤器实战

布隆过滤器有很多实现和优化,由 Google 开发著名的 Guava 库就提供了布隆过滤器(Bloom Filter)的实现。在基于 Maven 的 Java 项目中要使用 Guava 提供的布隆过滤器,只需要引入以下坐标:

<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>28.0-jre</version>
</dependency>

在导入 Guava 库后,我们新建一个 BloomFilterDemo 类,在 main 方法中我们通过 BloomFilter.create 方法来创建一个布隆过滤器,接着我们初始化 1 百万条数据到过滤器中,然后在原有的基础上增加 10000 条数据并判断这些数据是否存在布隆过滤器中:

import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class BloomFilterDemo {
    public static void main(String[] args) {
        int total = 1000000; // 总数量
        BloomFilter<CharSequence> bf = 
          BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), total);
        // 初始化 1000000 条数据到过滤器中
        for (int i = 0; i < total; i++) {
            bf.put("" + i);
        }
        // 判断值是否存在过滤器中
        int count = 0;
        for (int i = 0; i < total + 10000; i++) {
            if (bf.mightContain("" + i)) {
                count++;
            }
        }
        System.out.println("已匹配数量 " + count);
    }
}

当以上代码运行后,控制台会输出以下结果:

已匹配数量 1000309

很明显以上的输出结果已经出现了误报,因为相比预期的结果多了 309 个元素,误判率为:

309/(1000000 + 10000) * 100 ≈ 0.030594059405940593

如果要提高匹配精度的话,我们可以在创建布隆过滤器的时候设置误判率 fpp:

BloomFilter<CharSequence> bf = BloomFilter.create(
  Funnels.stringFunnel(Charsets.UTF_8), total, 0.0002
);

在 BloomFilter 内部,误判率 fpp 的默认值是 0.03:

// com/google/common/hash/BloomFilter.class
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
  return create(funnel, expectedInsertions, 0.03D);
}

在重新设置误判率为 0.0002 之后,我们重新运行程序,这时控制台会输出以下结果:

已匹配数量 1000003

通过观察以上的结果,可知误判率 fpp 的值越小,匹配的精度越高。当减少误判率 fpp 的值,需要的存储空间也越大,所以在实际使用过程中需要在误判率和存储空间之间做个权衡。

四、简易版布隆过滤器

为了便于大家理解布隆过滤器,我们来看一下下面简易版布隆过滤器。

package com.semlinker.bloomfilter;

import java.util.BitSet;

public class SimpleBloomFilter {
    private static final int DEFAULT_SIZE = 2 << 24;
    private static final int[] seeds = new int[]{7, 11, 13, 31, 37, 61};

    private BitSet bits = new BitSet(DEFAULT_SIZE);
    private SimpleHash[] func = new SimpleHash[seeds.length];

    public SimpleBloomFilter() {
        // 创建多个哈希函数
        for (int i = 0; i < seeds.length; i++) {
            func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]);
        }
    }

    /**
     * 添加元素到布隆过滤器中
     *
     * @param value
     */
    public void put(String value) {
        for (SimpleHash f : func) {
            bits.set(f.hash(value), true);
        }
    }

    /**
     * 判断布隆过滤器中是否包含指定元素
     *
     * @param value
     * @return
     */
    public boolean mightContain(String value) {
        if (value == null) {
            return false;
        }
        boolean ret = true;
        for (SimpleHash f : func) {
            ret = ret && bits.get(f.hash(value));
        }
        return ret;
    }

    public static void main(String[] args) {
        SimpleBloomFilter bf = new SimpleBloomFilter();
        for (int i = 0; i < 1000000; i++) {
            bf.put("" + i);
        }
        // 判断值是否存在过滤器中
        int count = 0;
        for (int i = 0; i < 1000000 + 10000; i++) {
            if (bf.mightContain("" + i)) {
                count++;
            }
        }
        System.out.println("已匹配数量 " + count);
    }

    /**
     * 简单哈希类
     */
    public static class SimpleHash {
        private int cap;
        private int seed;

        public SimpleHash(int cap, int seed) {
            this.cap = cap;
            this.seed = seed;
        }

        public int hash(String value) {
            int result = 0;
            int len = value.length();
            for (int i = 0; i < len; i++) {
                result = seed * result + value.charAt(i);
            }
            return (cap - 1) & result;
        }
    }
}

在 SimpleBloomFilter 类的实现中,我们使用到了 Java util 包中的 BitSet,BitSet 是位操作的对象,值只有 0 或 1 ,内部维护了一个 long 数组,初始只有一个 long,所以 BitSet 最小的容量是 64 位。当随着存储的元素越来越多,BitSet 内部会动态扩容,最终内部是由 N 个 long 值来存储。默认情况下,BitSet 的所有位都是 0。

五、总结

本文主要介绍的布隆过滤器的概念和常见的应用场合,在实战部分我们演示了 Google 著名的 Guava 库所提供布隆过滤器(Bloom Filter)的基本使用,同时我们也介绍了布隆过滤器出现误报的原因及如何提高判断准确性。最后为了便于大家理解布隆过滤器,我们介绍了一个简易版的布隆过滤器 SimpleBloomFilter。

六、参考资源

本人的全栈修仙之路订阅号,会定期分享 Angular、TypeScript、Node.js/Java 、Spring 相关文章,欢迎感兴趣的小伙伴订阅哈!

full-stack-logo

查看原文

赞 49 收藏 29 评论 1

followWinter 收藏了文章 · 2019-11-27

html2image原理简述

前言

看到 TJ 大神 star了dom-to-image,也一直很好奇html怎么转 image

那么就翻下源码,看下是如何实现的,其实一共就不到800行代码,还蛮容易读懂的

工作原理

使用svg的一个特性,允许在<foreignobject>标签中包含任意的html内容。(主要是 XMLSerializer | MDN这个apidom转为svg
所以,为了渲染那个dom节点,你需要采取以下步骤:

  1. 递归 clone 原始的 dom 节点
  2. 获取 节点以及子节点 上的 computed style,并将这些样式添加进新建的style标签中(不要忘记了clone 伪元素的样式)
  3. 嵌入网页字体
  • 找到所有的@font-face
  • 解析URL资源,并下载对应的资源
  • base64编码和内联资源 作为 data: URLS引用
  • 把上面处理完的css rules全部都放进<style>中,并把标签加入到clone的节点中去
  1. 内嵌图片
  • 内联图片src 的url 进 <img>元素
  • 背景图片 使用 background css 属性,类似fonts的使用方式
  1. 序列化 clone 的 dom 节点 为 svg
  2. 将xml包装到<foreignobject>标签中,放入svg中,然后将其作为data: url
  3. 将png内容或原始数据作为uint8array获取,使用svg作为源创建一个img标签,并将其渲染到新创建的canvas上,然后把canvas转为base64
  4. 完成

核心API

import domtoimage from 'dom-to-image'

domtoimage 有如下一些方法:

    * toSvg (`dom` 转 `svg`)
    * toPng (`dom` 转 `png`)
    * toJpeg (`dom` 转 `jpg`)
    * toBlob (`dom` 转 `blob`)
    * toPixelData (`dom` 转 像素数据)

见名知意,名字取得非常好

下面我挑一个toPng来简单解析一下原理,其他的原理也都是类似的


分析 toPng 原理

尽量挑最核心的讲,希望不会显得很繁琐,了解核心思想就好

下面介绍几个核心函数:

  • toPng (包装了draw函数,没啥意义)
  • Draw (dom => canvas)
  • toSvg (dom => svg)
  • cloneNode (clone dom树和css样式)
  • makeSvgDataUri (dom => svg => data:uri)

调用顺序为

toPng 调用 Draw
Draw 调用 toSvg
toSvg 调用 cloneNode

toPng方法:

// 里面其实就是调用了 draw 方法,promise返回的是一个canvas对象
function toPng(node, options) {
    return draw(node, options || {})
        .then(function (canvas) {
            return canvas.toDataURL();
        });
}

Draw方法

function draw(domNode, options) {
    // 将 dom 节点转为 svg(data: url形式的svg)
    return toSvg(domNode, options)    
        // util.makeImage 将 canvas 转为 new Image(uri)
        .then(util.makeImage)
        .then(util.delay(100))
        .then(function (image) {
            var canvas = newCanvas(domNode);
            canvas.getContext('2d').drawImage(image, 0, 0);
            return canvas;
        });

    // 创建一个空的 canvas 节点
    function newCanvas(domNode) {
        var canvas = document.createElement('canvas');
        canvas.width = options.width || util.width(domNode);
        canvas.height = options.height || util.height(domNode);
          ......
        return canvas;
    }
}

toSvg方法

  function toSvg (node, options) {
    options = options || {}
    // 设置一些默认值,如果option是空的话
    copyOptions(options)

    return (
      Promise.resolve(node)
        .then(function (node) {
          // clone dom 树
          return cloneNode(node, options.filter, true)
        })
        // 把字体相关的csstext 全部都新建一个 stylesheet 添加进去
        .then(embedFonts)
        // clone 处理图片啊,background url('')里面的资源,顺便加载好
        .then(inlineImages)
        // 把option 里面的一些 style 放进stylesheet里面
        .then(applyOptions)
        .then(function (clone) {
          // node 节点序列化成 svg
          return makeSvgDataUri(
            clone,
            // util.width 就是 getComputedStyle 获取节点的宽
            options.width || util.width(node),
            options.height || util.height(node)
          )
        })
    )
      // 设置一些默认值
    function applyOptions (clone) {
        ......
      return clone
    }
  }

cloneNode 方法

  function cloneNode (node, filter, root) {
    if (!root && filter && !filter(node)) return Promise.resolve()

    return (
      Promise.resolve(node)
        .then(makeNodeCopy)
        .then(function (clone) {
          return cloneChildren(node, clone, filter)
        })
        .then(function (clone) {
          return processClone(node, clone)
        })
    )
    // makeNodeCopy
    // 如果不是canvas 节点的话,就clone
    // 是的话,就返回 canvas转image的 img 对象
    function makeNodeCopy (node) {
      if (node instanceof HTMLCanvasElement) { return util.makeImage(node.toDataURL()) }
      return node.cloneNode(false)
    }
    // clone 子节点 (如果存在的话)
    function cloneChildren (original, clone, filter) {
      var children = original.childNodes
      if (children.length === 0) return Promise.resolve(clone)

      return cloneChildrenInOrder(clone, util.asArray(children), filter).then(
        function () {
          return clone
        }
      )
      // 递归 clone 节点
      function cloneChildrenInOrder (parent, children, filter) {
        var done = Promise.resolve()
        children.forEach(function (child) {
          done = done
            .then(function () {
              return cloneNode(child, filter)
            })
            .then(function (childClone) {
              if (childClone) parent.appendChild(childClone)
            })
        })
        return done
      }
    }
    
    // 处理添加dom的css,处理svg
    function processClone (original, clone) {
      if (!(clone instanceof Element)) return clone

      return Promise.resolve()
        // 读取节点的getComputedStyle,添加进css中
        .then(cloneStyle)
        // 获取伪类的css,添加进css
        .then(clonePseudoElements)
        // 读取 input textarea 的value
        .then(copyUserInput)
        // 设置svg 的 xmlns
        // 命名空间声明由xmlns属性提供。此属性表示<svg>标记及其子标记属于名称空间为“http://www.w3.org/2000/svg”的XML方言
        .then(fixSvg)
        .then(function () {
          return clone
        })
下面是这篇的重点 把 html 节点序列化成 svg
  // node 节点序列化成 svg
  function makeSvgDataUri (node, width, height) {
    return Promise.resolve(node)
      .then(function (node) {
        node.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')

        // XMLSerializer 对象使你能够把一个 XML 文档或 Node 对象转化或“序列化”为未解析的 XML 标记的一个字符串。
        // 要使用一个 XMLSerializer,使用不带参数的构造函数实例化它,然后调用其 serializeToString() 方法:
        return new XMLSerializer().serializeToString(node)
      })
      // escapeXhtml代码是string.replace(/#/g, '%23').replace(/\n/g, '%0A')
      .then(util.escapeXhtml)
      .then(function (xhtml) {
        return (
          '<foreignObject x="0" y="0" width="100%" height="100%">' +
          xhtml +
          '</foreignObject>'
        )
      })
      // 变成svg
      .then(function (foreignObject) {
        return (
          '<svg xmlns="http://www.w3.org/2000/svg" width="' +
          width +
          '" height="' +
          height +
          '">' +
          foreignObject +
          '</svg>'
        )
      })
      // 变成 data: url
      .then(function (svg) {
        return 'data:image/svg+xml;charset=utf-8,' + svg
      })
  }

参考链接

查看原文

followWinter 赞了文章 · 2019-11-27

html2image原理简述

前言

看到 TJ 大神 star了dom-to-image,也一直很好奇html怎么转 image

那么就翻下源码,看下是如何实现的,其实一共就不到800行代码,还蛮容易读懂的

工作原理

使用svg的一个特性,允许在<foreignobject>标签中包含任意的html内容。(主要是 XMLSerializer | MDN这个apidom转为svg
所以,为了渲染那个dom节点,你需要采取以下步骤:

  1. 递归 clone 原始的 dom 节点
  2. 获取 节点以及子节点 上的 computed style,并将这些样式添加进新建的style标签中(不要忘记了clone 伪元素的样式)
  3. 嵌入网页字体
  • 找到所有的@font-face
  • 解析URL资源,并下载对应的资源
  • base64编码和内联资源 作为 data: URLS引用
  • 把上面处理完的css rules全部都放进<style>中,并把标签加入到clone的节点中去
  1. 内嵌图片
  • 内联图片src 的url 进 <img>元素
  • 背景图片 使用 background css 属性,类似fonts的使用方式
  1. 序列化 clone 的 dom 节点 为 svg
  2. 将xml包装到<foreignobject>标签中,放入svg中,然后将其作为data: url
  3. 将png内容或原始数据作为uint8array获取,使用svg作为源创建一个img标签,并将其渲染到新创建的canvas上,然后把canvas转为base64
  4. 完成

核心API

import domtoimage from 'dom-to-image'

domtoimage 有如下一些方法:

    * toSvg (`dom` 转 `svg`)
    * toPng (`dom` 转 `png`)
    * toJpeg (`dom` 转 `jpg`)
    * toBlob (`dom` 转 `blob`)
    * toPixelData (`dom` 转 像素数据)

见名知意,名字取得非常好

下面我挑一个toPng来简单解析一下原理,其他的原理也都是类似的


分析 toPng 原理

尽量挑最核心的讲,希望不会显得很繁琐,了解核心思想就好

下面介绍几个核心函数:

  • toPng (包装了draw函数,没啥意义)
  • Draw (dom => canvas)
  • toSvg (dom => svg)
  • cloneNode (clone dom树和css样式)
  • makeSvgDataUri (dom => svg => data:uri)

调用顺序为

toPng 调用 Draw
Draw 调用 toSvg
toSvg 调用 cloneNode

toPng方法:

// 里面其实就是调用了 draw 方法,promise返回的是一个canvas对象
function toPng(node, options) {
    return draw(node, options || {})
        .then(function (canvas) {
            return canvas.toDataURL();
        });
}

Draw方法

function draw(domNode, options) {
    // 将 dom 节点转为 svg(data: url形式的svg)
    return toSvg(domNode, options)    
        // util.makeImage 将 canvas 转为 new Image(uri)
        .then(util.makeImage)
        .then(util.delay(100))
        .then(function (image) {
            var canvas = newCanvas(domNode);
            canvas.getContext('2d').drawImage(image, 0, 0);
            return canvas;
        });

    // 创建一个空的 canvas 节点
    function newCanvas(domNode) {
        var canvas = document.createElement('canvas');
        canvas.width = options.width || util.width(domNode);
        canvas.height = options.height || util.height(domNode);
          ......
        return canvas;
    }
}

toSvg方法

  function toSvg (node, options) {
    options = options || {}
    // 设置一些默认值,如果option是空的话
    copyOptions(options)

    return (
      Promise.resolve(node)
        .then(function (node) {
          // clone dom 树
          return cloneNode(node, options.filter, true)
        })
        // 把字体相关的csstext 全部都新建一个 stylesheet 添加进去
        .then(embedFonts)
        // clone 处理图片啊,background url('')里面的资源,顺便加载好
        .then(inlineImages)
        // 把option 里面的一些 style 放进stylesheet里面
        .then(applyOptions)
        .then(function (clone) {
          // node 节点序列化成 svg
          return makeSvgDataUri(
            clone,
            // util.width 就是 getComputedStyle 获取节点的宽
            options.width || util.width(node),
            options.height || util.height(node)
          )
        })
    )
      // 设置一些默认值
    function applyOptions (clone) {
        ......
      return clone
    }
  }

cloneNode 方法

  function cloneNode (node, filter, root) {
    if (!root && filter && !filter(node)) return Promise.resolve()

    return (
      Promise.resolve(node)
        .then(makeNodeCopy)
        .then(function (clone) {
          return cloneChildren(node, clone, filter)
        })
        .then(function (clone) {
          return processClone(node, clone)
        })
    )
    // makeNodeCopy
    // 如果不是canvas 节点的话,就clone
    // 是的话,就返回 canvas转image的 img 对象
    function makeNodeCopy (node) {
      if (node instanceof HTMLCanvasElement) { return util.makeImage(node.toDataURL()) }
      return node.cloneNode(false)
    }
    // clone 子节点 (如果存在的话)
    function cloneChildren (original, clone, filter) {
      var children = original.childNodes
      if (children.length === 0) return Promise.resolve(clone)

      return cloneChildrenInOrder(clone, util.asArray(children), filter).then(
        function () {
          return clone
        }
      )
      // 递归 clone 节点
      function cloneChildrenInOrder (parent, children, filter) {
        var done = Promise.resolve()
        children.forEach(function (child) {
          done = done
            .then(function () {
              return cloneNode(child, filter)
            })
            .then(function (childClone) {
              if (childClone) parent.appendChild(childClone)
            })
        })
        return done
      }
    }
    
    // 处理添加dom的css,处理svg
    function processClone (original, clone) {
      if (!(clone instanceof Element)) return clone

      return Promise.resolve()
        // 读取节点的getComputedStyle,添加进css中
        .then(cloneStyle)
        // 获取伪类的css,添加进css
        .then(clonePseudoElements)
        // 读取 input textarea 的value
        .then(copyUserInput)
        // 设置svg 的 xmlns
        // 命名空间声明由xmlns属性提供。此属性表示<svg>标记及其子标记属于名称空间为“http://www.w3.org/2000/svg”的XML方言
        .then(fixSvg)
        .then(function () {
          return clone
        })
下面是这篇的重点 把 html 节点序列化成 svg
  // node 节点序列化成 svg
  function makeSvgDataUri (node, width, height) {
    return Promise.resolve(node)
      .then(function (node) {
        node.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')

        // XMLSerializer 对象使你能够把一个 XML 文档或 Node 对象转化或“序列化”为未解析的 XML 标记的一个字符串。
        // 要使用一个 XMLSerializer,使用不带参数的构造函数实例化它,然后调用其 serializeToString() 方法:
        return new XMLSerializer().serializeToString(node)
      })
      // escapeXhtml代码是string.replace(/#/g, '%23').replace(/\n/g, '%0A')
      .then(util.escapeXhtml)
      .then(function (xhtml) {
        return (
          '<foreignObject x="0" y="0" width="100%" height="100%">' +
          xhtml +
          '</foreignObject>'
        )
      })
      // 变成svg
      .then(function (foreignObject) {
        return (
          '<svg xmlns="http://www.w3.org/2000/svg" width="' +
          width +
          '" height="' +
          height +
          '">' +
          foreignObject +
          '</svg>'
        )
      })
      // 变成 data: url
      .then(function (svg) {
        return 'data:image/svg+xml;charset=utf-8,' + svg
      })
  }

参考链接

查看原文

赞 9 收藏 20 评论 3

followWinter 发布了文章 · 2019-11-18

[前端漫谈]Git 内部原理 - Git 对象

### 导读
这篇文章是对Git Pro 10.2 Git 内部原理 - Git 对象章节的解读和转化,主要介绍两个东西:1)使用 Git 底层命令完成提交,2)尝试使用 NodeJS 解析 Git 对象(文章中提供的是 Ruby)。

### 0x001 初始化
初始化一个本地仓库:

$ mkdir git-test
$ cd git-test
$ git init
Initialized empty Git repository in ...

查看文件结构:

+ git-test
   + .git
       + branches
       - config
       - description
       - HEAD
       + hooks
       + info
       + objects
           + info
           + pack
       + refs

其他暂时不关注,只关注objects,此时只有infopack两个文件夹,我们也不关注它,我们只关注objects下除了infopack之外的变化。

0x002 hash-object

这个命令用来为一个文件计算对象 ID 并可能创建一个 blob 文件。这里有两层含义:1)计算对象 ID,对象 ID 是什么呢?2)blob 文件是啥?为啥是可能?接下来将给出答案。

执行命令:

$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

-w指示hash-object存储数据对象,如果不指定,则返回计算的 objectId。--stdin指示从标准输入读取内容,也就是将test content作为内容计算。

当我们执行这个这个命令的时候,会返回一个 40 个字符长度的 SHA1 哈希值。d670460b4b4aece5915caf5c68d12f560a9fe3e4,由于制定了-wgit 会存储这次的计算,查看objects下的文件:

+ objects
    + d6
        - 70460b4b4aece5915caf5c68d12f560a9fe3e4

会发现,多了一个文件夹d6,而d6中有一个文件70460b4b4aece5915caf5c68d12f560a9fe3e4,这两者拼接起来正好是刚刚生成的objectID

如果我们多次执行这个命令,会发现,这个文件没有发生变化,因为已经存在,这也就是之前说可能生成的原因了。

如果我们改变内容,则会生成一个新的objectID和一个新的blob文件。

0x003 cat-file

我们已经直到如何存储文件,那如何读取呢?可以使用cat-file

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

0x004 文件存储和版本恢复

接下来我们使用文件,而不直接使用内容

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30

然后更新这个文件并存储

$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

此时的objects

+ objects
    + 1f
        - 7a7a472abf3dd9643fd615f6da379c4acb3e3a
    + 83
        - baae61804e65cc73a7201a7252750c76066a30
    + d6
        - 70460b4b4aece5915caf5c68d12f560a9fe3e4

然后吧文件内容恢复到第一个版本

$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1

或者第二个版本

$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2

0x005 树对象 和 write-tree

将文件加入缓存区

$ git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt

将缓存区内容写入树对象

$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30      test.txt

新建一个新的数对象,包含test.txt的第二个版本和一个新的文件:

$ echo 'new file' > new.txt
$ git update-index --cacheinfo 100644 \
  1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git update-index --add new.txt
$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

0x006 提交 commit 和 commit-tree

有了树对象之后,就可以提交该树对象,生成一个commit

$ echo 'first commit' | git commit-tree d8329f
b51096bf62fa145c0b95ce18dc3020daa1f2556e

查看这个commit

$ git cat-file -p b51096bf62fa145c0b95ce18dc3020daa1f2556e
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <schacon@gmail.com> 1243040974 -0700
committer Scott Chacon <schacon@gmail.com> 1243040974 -0700

first commit

接着提交第二个树对象,并使用-p指定这个提交的上一个commit

$ echo 'second commit' | git commit-tree 0155eb4229851634a0f03eb265b69f5a2d56f341 -p b51096bf62fa145c0b95ce18dc3020daa1f2556e
bf41fa3700a67914b3b45eefced02fffcdaf4464

使用git log查看记录

commit bf41fa3700a67914b3b45eefced02fffcdaf4464
Author: lyxxxx <lyxxxx@yeah.net>
Date:   Sun Nov 17 22:14:36 2019 +0800

    second commit

 new.txt  | 1 +
 test.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

commit b51096bf62fa145c0b95ce18dc3020daa1f2556e
Author: lyxxxx <lyxxxx@yeah.net>
Date:   Sun Nov 17 22:07:01 2019 +0800

    first commit

 test.txt | 1 +
 1 file changed, 1 insertion(+)

以上,便是使用底层命令创建 Git 提交历史的过程,主要涉及 5 个命令:

  • hash-object:计算objectID,创建blob文件
  • cat-file:读取生产的object文件
  • update-index:更新暂存区文件
  • write-tree:将暂存区文件写入tree文件
  • commit-tree:提交tree文件

0x007 object文件类型

object文件类型一共有三种:

  • blobhash-object生成,表示一个文件
  • treewrite-tree生成,表示缓存区的文件列表
  • commitcommit-tree生成,表示本次提交的文件列表

他们的关系是:

  • commit包含一个tree对象
  • tree包含多个blob对象和tree对象

0x008 objectID如何生成

接下来使用NodeJS演示如何生成objectID,

假设我们要存储的内容是what is up, doc?

const content = 'what is up, doc?'
const type = 'blob'

object对象的存储的格式是:

const store = `${type} ${content.length}\0${content}`

然后计算sh1 值:

const crypto = require('crypto');

const hash = crypto.createHash('sha1');
hash.update(store);
const objectID = hash.digest('hex');

最后计算的结果是:

bd9dbf5aae1a3862dd1526723246b20206e5fc37

接着是存储,在存储的时候,会执行压缩之后再存储:

const zlib = require('zlib');

const result = zlib.deflateSync(Buffer.from(store))

然后按照objectID分割存储到objects文件夹下就行了:

+ objects
    + bd
        - 9dbf5aae1a3862dd1526723246b20206e5fc37

完整源码:

const zlib = require('zlib');
const fs = require('fs');
const Buffer = require('buffer').Buffer
const crypto = require('crypto');

const type = 'blob'
const content = process.argv[2]

const store = `${type} ${content.length}\0${content}`

const hash = crypto.createHash('sha1');
hash.update(store)
const objectID = hash.digest('hex')
const result = zlib.deflateSync(Buffer.from(store))

const path = '.git/objects'
const [a, b, ...file] = objectID
const dirPath = `${path}/${a}${b}`
const filePath = `${dirPath}/${file.join('')}`
fs.mkdirSync(dirPath)
fs.writeFileSync(filePath)

0x009 资源

0x010 带货

最近发现一个好玩的库,作者是个大佬啊--基于 React 的现象级微场景编辑器

查看原文

赞 0 收藏 0 评论 0

followWinter 回答了问题 · 2019-11-17

自调用函数里的this都是指向window吗?

不是,和 this 的指向规则一致。
比如这样:

new function(){
    this.name='inner';
    (()=>{console.log(this===window, this.name)})()
}
// 输出
false "inner"

指向函数实例的 this

关注 4 回答 3

认证与成就

  • 获得 136 次点赞
  • 获得 12 枚徽章 获得 1 枚金徽章, 获得 6 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-02-13
个人主页被 2.2k 人浏览