就在前两天,一篇反对Vue2升级到Vue3的文章在vue官方社区引起了热议。该文章从实际应用角度出发,分析了Vue2到Vue3在真实项目中实操升级的痛点,提出了一个反对的声音:Vue3的升级是一个错误的选择。
在一片热议中,甚至vue作者尤雨溪都出来亲自解释并承认了一些问题。原作者首先声明了他并没有贬低Vue3的意思。他认为Vue3是非常非常棒的框架,解决了Vue2中很多潜在问题,技术层面改善了开发人员的开发体验,并显著提高了性能。原作者主要的问题,是从Vue3突破性的改变以及周边生态圈未能及时跟上的角度,重点强调了迁移升级成本+风险较大。
关于升级成本问题:尤大也承认了Vue3升级体验并没有想象中的那么流畅,Vue4会吸取经验,做好平稳迭代。这一点本文会在下面详细说明。
Events API的弃用让这个问题首当其冲。(straightforward like the depreciation of the Events API)。Vue实例再也不能作为事件总线做事件通信,$on,$off,$once的彻底移除意味着之前所有有关代码都必须重新推翻重写,虽然有很好的插件工具让这件事变得没那么复杂,但是仍然会带来不小的迁移成本。
代码构建问题。 你会经常遇到用Vue2写法写出来的代码在构建(build) 失败或抛出警告。因为这些api写法在Vue3中已经被废弃。这问题在已存在的大型项目中的尤为突出(In an existing large-scale application built with Vue 2, you would probably use some of the deprecated or changed APIs)。下图展示了一部分Breaking changes,可以看到破坏性的api变更数确实很多:
颠覆式的composition-api慢慢向面向函数思想转变,导致很多原有习惯于options-api的开发者反感Vue正在像react靠拢,没有坚持住Vue特色。它提出了一种新的基于函数的 Vue 组件编写方式,引起了Vue社区大量的争议和分裂,甚至将社区分隔为两种观点阵营针锋相对,最终导致了Vue 最黑暗的一天事件。这很令人沮丧。
生态系统和框架本身一样重要。因为没有责任机制,在有争议的决定和在弃用功能的时候,很多框架周边的生态系统的许多贡献者会被迫离开,并导致许多库被放弃或者延迟更新。很多时候,我们没有办法做版本兼容时,我们往往只能把责任归咎于,开源库缺乏同理心和对大局的理解。
在我们的日常开发中,尤其是在使用框架时,我们会遇到各种各样的问题,这时我们时常需要google或者问答社区作为帮手,但是目前关于Vue搜索出来的结果几乎全是Vue2的结果,这也很难不令人难过。
Vue2到Vue3的升级,有一点像angular1到angular2的升级
过渡到 Vue 3 看起来很像从AngularJS过渡到Angular(版本 1⇒ 2)。大量的延迟和重大更改导致了挫败感,最终 Angular 失去了对 React 和 Vue 的吸引力。
看起来前进的道路其实是倒退。
开发满意度看起来并不好。
上图可以看到Vue已经有被svelte超越的趋势。
这根本不是真的。
当我们进行版本切换时,所有核心库和工具都与这两个版本兼容(或为 Vue 2/3 支持提供单独的版本)。
实际上阻碍升级的依赖都是第三方,主要是 Nuxt 和 Vuetify。
实际使用过 Composition API + < script setup> 的用户在真是开发中的反馈非常积极,证明这是一个有价值的补充,现在他们中的许多人更喜欢它而不是 Options API。
我们当然可以更好地处理新 API 的引入,但仅仅因为存在争议,并不意味着它是错误的或者不必要的。实际上,引入大的、新的想法的行为,势必会让那些喜欢呆在舒适区的人感到不安,但如果我们迎合这种心态,就永远不会取得真正的进展。
虽然我们确实创造了 Vue CLI、Vuex、Vetur 和 VuePress 的新替代品,但它们本身都有适用于 Vue 3 的版本。这是夸大事实,不尊重团队为提供这些工具的 Vue 3 兼容版本所做的努力。
没有可比性,不能拿Vue升级和angularjs -> angular做对比。
Angular 和 AngularJS 是根本不同的框架。几乎没有共享交集,除了完全重写之外没有实际的迁移路径。
另一方面,有许多生产 Vue 2 应用程序成功迁移到 Vue 3 的案例。很容易吗,确实不是,但是他们都迁移成功了。
我们同意,Vue3升级体验并没有想象中的那么流畅。Vue 将随着吸取的经验不断发展,我们绝对不打算在未来的Vue4中,进行这样的破坏性重大升级。
在我看来,这篇文章整体上描绘的画面比实际要黑暗(dark)得多,有不必要的夸张,在少数情况下是完全不正确的信息。我希望至少能纠正我在其他评论中指出的一些事实错误。
上述就是原文提出来的问题以及尤大的回复,应该可以给各位正在考虑升级的小伙伴一点真实的参考意见,原有Vue2项目是否能安全平稳升级到Vue3,我还是持有一定保留意见,如果是时间充裕的项目可以升,如果是时间比较赶的项目,建议不升级。
]]>平时在Vue项目中,最常用的状态管理工具就是Vuex了,而最新的Pinia是尤雨溪强烈推荐的一款Vue状态管理工具,也被认为是下一代Vuex的替代产品。
其优点如下:
1 | npm i pinia |
1 | import { createPinia } from 'pinia' |
1 | import { createApp } from 'vue' |
在src/store文件夹下创建一个js文件,命名按照需求即可,我这边定义为main.js,代码如下:
1 | import { defineStore } from 'pinia' |
其中defineStore的第一个参数为该store的名称,第二个参数为一个对象,包含了该store的state,getters和actions,state改为了函数形式,目的应该是像Vue2 options API中的data类似,避免多个store中定义的属性相互受到影响。
此处使用Vue3的SFC语法,主要是Pinia更适合Vue3这种组合式API风格,方便演示
1 | <script lang="ts" setup> |
使用patch的方式对数据进行修改,可以加快修改速度,性能更好。patch 方法可以接受两种类型的参数,对象型和回调函数型。
patch + 对象
patch + 函数 注:使用回调函数型时,回调接收一个state参数,state指代的就是对应store中的state
使用方式如下:
1 | <script lang="ts" setup> |
1 | import { defineStore } from 'pinia' |
1 | <script lang="ts" setup> |
Pinia中的getter和Vue中的计算属性类似,在获取state之前会进行处理,具有缓存性,如果值没有改变,即使多次调用,实际上也只会调用一次。
1 | import { defineStore } from 'pinia' |
1 | <script lang="ts" setup> |
我们可以看到,即使执行了三遍一样的代码,但最终还是只调用了一次。
在Pinia中,可以在一个store中import引入另外一个store,然后通过调用引入的store方法的形式,获取引入的store的状态。
1 | import { defineStore } from 'pinia' |
1 | import { defineStore } from 'pinia' |
Pinia与Vuex一样,刷新页面后,数据就会重置,有时候我们需要将数据进行持久化存储,我们可以使用pinia-plugin-persist这个插件
1 | npm i pinia-plugin-persist --save |
1 | import { createPinia } from 'pinia' |
1 | import { defineStore } from 'pinia' |
更新数据以后,我们就能在浏览器控制台中看到已经将数据存储到了sessionStorage中
数据默认是存在sessionStorage中的,还会以store的名称作为key。但是我们可以对其修改,并且还可以只持久化部分state中的属性,代码如下:
1 | import { defineStore } from 'pinia' |
Pinia就是Vuex的替代产品,相比于Vuex,Pinia更好地兼容了Vue本身,代码更加简洁,开发起来也更加便捷。
]]>pnpm是什么?
它是Node.js 的替代包管理器。它是 npm 的直接替代品,但速度更快、效率更高。
相对于npm、yarn,它的知名度并不高,但是它的性能及实用性却非常高,本次将学习到pnpm的以下知识:
节约磁盘空间并提升安装速度
pnpm代表performant npm(高性能的npm),同npm和Yarn,都属于Javascript包管理安装工具,它较npm和Yarn在性能上得到很大提升,被称为快速的,节省磁盘空间的包管理工具。
当使用 npm 或 Yarn 时,如果你有 100 个项目使用了某个依赖(dependency),就会有 100 份该依赖的副本保存在硬盘上,而在使用 pnpm 时,依赖会被存储在内容可寻址的存储中,所以:
因此,您在磁盘上节省了大量空间,这与项目和依赖项的数量成正比,并且安装速度要快得多!
摘自官网:pnpm.io/zh/motivati…
通过npm安装,也可以在官网查看其他安装方式
1 | npm install -g pnpm |
通过下述命令查看已安装的pnpm的版本
1 | pnpm -v |
1 | pnpm init |
1 | pnpm install xxx |
1 | pnpm run xxx |
示例:创建一个vue3项目
通过pnpm create使用vite套件新建一个vue3的项目,直达vue官方链接
1 | # 使用pnpm create 启动套件(vite,只有存在的套件才可以)创建模板项目 |
通过上述操作,我们学到了pnpm项目的初始化、安装依赖、启动服务等,可以运行起来,感受一下它和npm运行速度的差异。
注意:pnpm init只能一键快速生成package.json文件,如果要一步一步填写每个属性的值生成package.json文件,则需要通过npm init生成,如果要一键快速生成,需要增加-y参数npm init -y来生成。
可实现nvm、n等node版本管理工具,安装并切换node.js版本的功能。
pnpm基于符号链接来创建非扁平化的node_modules
对比npm和pnpm安装的node_modules:
npm: 所有依赖包平铺在node_modules目录,包括直接依赖包以及其他次级依赖包,没有符号链接
pnpm: node_modules目录下只有.pnpm和直接依赖包(vue、vite、…),没有其他次级依赖包,直接依赖包的后面有符号链接的标识
那pnpm怎么管理这些依赖包的呢?带着这一问题,我们继续探究。
详细看一下pnpm生成的node_modules目录如下:
1 | ▾ node_modules |
node_modules 中只有一个 .pnpm 的文件夹以及三个符号链接@vitejs/plugin-vue、 vite 和 vue。
这是因为我们的项目只安装了@vitejs/plugin-vue、 vite 和 vue三个依赖,pnpm使用符号链接的方式将项目的直接依赖添加到node_modules的根目录下,也就是说node_modules目录下只有我们项目中依赖的dependencies、devDependencies和一个.pnpm目录。
以vite依赖包举例,看一下vite依赖包和.pnpm目录里都有些什么:
展开vite依赖包,我们会有两个疑问:
vite的实际位置
.pnpm称为虚拟存储目录,以平铺的形式储存着所有的项目依赖包,每个依赖包都可以通过.pnpm/
即直接依赖的vite包 符号链接到路径:.pnpm/vite@2.9.12/node_modules/vite,vite包中的每个文件都是来自内容可寻址存储的硬链接。
.pnpm/vite@2.9.12/node_modules/vite
1 | ▾ node_modules |
vite的次级依赖
观察上面的目录结构,发现/node_modules/.pnpm/vite@2.9.12/node_modules/vite目录下还是没有次级依赖的node_modules。
pnpm 的 node_modules设计 ,包的依赖项与依赖包的实际位置位于同一目录级别。
所以 vite 的次级依赖包不在 .pnpm/vite@2.9.12/node_modules/vite/node_modules/, 而是在 .pnpm/vite@2.9.12/node_modules/,与vite实际位置位于同一目录级别。
1 | ▾ node_modules |
这里的esbuild等次级依赖包又是一个符号链接,仍符合刚才的逻辑,实际位置在.pnpm/esbuild@0.14.43/node_modules/esbuild,包内的每个文件再硬链接到pnpm store中的对应文件。
我们再通过官网提供的依赖图,再辅助理解一下node_modules依赖包之间的关系。
项目依赖了bar@1.0.0版本,bar依赖了foo@1.0.0版本,node_modules下只有直接依赖包bar@1.0.0符号链接和.pnpm目录。
Node.js解析时,bar@1.0.0就会符号链接到实际位置.pnpm/bar@1.0.0/node_modules/bar,包中的文件(并非包文件夹)都硬链接到.pnpm store中的对应文件,foo@1.0.0做为bar的依赖,与bar的实际位置处于同一层级,符号链接指向实际位置.pnpm/foo@1.0.0/node_modules/foo,包中的文件再硬链接至.pnpm store。
关于peerDependencies是怎么处理依赖的,可以看官网这篇文章
总结:pnpm使用符号链接Symbolic link(软链接)来创建依赖项的嵌套结构,将项目的直接依赖符号链接到node_modules的根目录,直接依赖的实际位置在.pnpm/
pnpm store:pnpm资源在磁盘上的存储位置
一般store在Mac/Linux系统中,默认会设置到{home dir}>/.pnpm-store/v3;windows下会设置到当前盘的根目录下,比如C(C:.pnpm-store\v3)、D盘(D:.pnpm-store\v3)。
可以通过执行pnpm store path命令查看store存储目录的路径
进入store存储路径,查看存储的内容如下:
files/xx/xxx以文件夹进行分类,每个文件夹内包含重新编码命名后的文件,依赖包硬链接到此处对应的文件。
在项目中执行pnpm install的时候,依赖包存在于store中,直接创建依赖包硬链接到store中,如果不存在,则从远程下载后存储在store中,并从项目的node_modules依赖包中创建硬链接到store中。
上图中提示包从Content-addressable store硬链接到Virtual store,以及Content-addressable store和Virtual store的作用位置。
它是一种存储信息的方式,根据内容而不是位置进行检索信息的存储方式,被用于高速存储和检索的固定内容,如存储。这里的CAS作用于/Users/
指向存储的链接的目录,所有直接和间接依赖项都链接到此目录中,项目当中的.pnpm目录node_modules/.pnpm。
因为这样的处理机制,每次安装依赖的时候,如果是相同的依赖,有好多项目都用到这个依赖,那么这个依赖实际上最优情况(即版本相同)只用安装一次。如果依赖包存在于pnpm store中,则从store目录里面去hard-link,避免了二次安装带来的时间消耗,如果不存在的话,就会去下载并存储在store中。
如果是 npm 或 Yarn,那么这个依赖在多个项目中使用,在每次安装的时候都会被重新下载一次。
对比发现pnpm install安装速度相当之快!必须给个大大的赞!
紧接着会有人问,那一直往store里存储依赖包,store会不会越来越大?
官方提供了一个命令:pnpm store prune,从存储中删除未引用的包。
未引用的包是系统上的任何项目中都未使用的包。 在大多数安装操作之后,包有可能会变为未引用状态。
官方举例:在 pnpm install 期间,包 foo@1.0.0 被更新为 foo@1.0.1。 pnpm 将在存储中保留 foo@1.0.0 ,因为它不会自动除去包。 如果包 foo@1.0.0 没有被其他任何项目使用,它将变为未引用。 运行 pnpm store prune 将会把 foo@1.0.0 从存储中删除 。
运行 pnpm store prune 是不会影响项目的。 如果以后需要安装已经被删除的包,pnpm 将重新下载他们。建议清理不要太频繁,以防在切换分支等时pnpm需要重新下载所有删除的包,减慢安装过程。
pnpm store的其他命令
pnpm store status:查看store中已修改的包,如果包的内容与拆包时时相同的话,返回退出代码0。
pnpm store add:只把包加入存储中,且没有修改存储外的任何项目或文件
pnpm store prune:删除存储中未被引用的包
pnpm跟npm和Yarn一样,内置了对单一存储库monorepo的支持,只需要在项目根目录下创建 pnpm-workspace.yaml 文件,定义workspace的根目录。
例如:
pnpm-workspace.yaml
1 | packages: |
workspace:工作空间
默认情况下,如果可用的 packages 与已声明的可用范围相匹配,pnpm 将从工作空间链接这些 packages。
例如,如果 bar 中有 “foo”:”^1.0.0” 的这个依赖项,则 foo@1.0.0 链接到 bar。 但是,如果 bar 的依赖项中有 “foo”: “2.0.0”,而 foo@2.0.0 在工作空间中并不存在,则将从 npm registry 安装 foo@2.0.0 ,这种行为带来了一些不确定性。
pnpm 支持 workspace 协议(写法:workspace:<版本号> )。 当使用此协议时,pnpm 将拒绝解析除本地 workspace 包含的 package 之外的任何内容。 因此,如果您设置为 “foo”: “workspace:2.0.0” 时,安装将会失败,因为 “foo@2.0.0“ 不存在于此 workspace 中。
接下来,我们使用vue代码库来理解一下Workspace协议:
代码库地址:github.com/vuejs/core
根目录可以看到有这两个文件pnpm-lock.yaml、pnpm-workspace.yaml,
其中lock文件为pnpm install时生成的lock文件,space文件则为monorepo仓库中必须需要的定义工作空间目录的文件。
我们看到文件内容为:
1 | packages: |
也就表示core/packages/*这个目录下面所有的文件为workspace的内容。
我们看到用到本地workspace包的都标注了workspace:*协议,这样依赖的就一直是本地的包,而不是从npm registry安装的包。
clone代码库到本地,pnpm install安装依赖
查看core/node_modules/文件夹,发现package.json文件中依赖的@vue/xxx、vue包都已符号链接的形式存在,如下图:
按workspace:*协议,打开packages/reactivity文件夹,做一个测试,在index.js文件中加入console.log(‘test’),如下图:
这时候再打开node_modules/@vue/reactivity/index.js文件,可以发现刚才在packages里面改的内容,显示在了node_modules目录下的包里。
打开磁盘上存储(pnpm store path)的依赖包,并没有上面新增的console.log,上面的改动只影响了本地依赖包,而不是远程install下载后存储在磁盘上的包,也就是说符合workspace:协议引入的依赖包就是本地的workspace目录(即core/packages)下的包。
假如工作区有一个名为 foo 的包,可以通过这样引用 “foo”: “workspace:”,如果是其它别名,可以这么引用:”bar”: “workspace:foo@*”。
假如packages下有同层级的foo、bar,其中bar依赖于foo,则可以写作”bar”: “workspace:../foo”。
当workspace包打包发布时,将会动态替换这些workspace:依赖。
假设我们的 workspace 中有 a、 b、 c、 d 并且它们的版本都是 1.5.0,如下:
1 | { |
将会被转化为:
1 | { |
现在很多很受欢迎的开源项目都适用了pnpm的工作空间功能,感兴趣的可以前往官网查看哦!
性能对比
在pnpm官网上,提供了一个benchmarks基准测试图表,它展示了npm、pnpm、Yarn、Yarn pnp在install、update等场景下的耗时:
通过上图,可以看出pnpm的运行速度基本上是npm的两倍,运行速度排名pnpm > Yarn > npm。
通过上图可以看出pnpm独有的两个功能:
Yarn 在 v3.1 添加了 pnpm 链接器。 因此 Yarn 可以创建一个类似于 pnpm 创建的 node_modules 目录结构。
此外,Yarn 团队计划实现内容可寻址存储,以提高磁盘空间效率。
npm 团队决定也采用 pnpm 使用的符号链接的 node_modules 目录结构(相关 RFC)。
可参考vue代码库的这一次升级commit log
操作步骤:
1 | npm install -g pnpm |
1 | # 项目目录下运行或手动物理删除 |
1 | # 生成`pnpm-lock.yaml` |
1 | # 删除package-lock.json |
通过pnpm ls –g查看全局安装的包,只有通过pnpm install/add xxx –global安装的包才为全局包哦!
rm -rf $(pnpm store path)
如果您不在主磁盘中使用 pnpm ,您必须在使用 pnpm 的每一个磁盘中运行上述命令。 因为 pnpm 会为每一个磁盘创建一个专用的存储空间。
还有一些问题,需要进一步的验证和考究:
pnpm是高性能的npm,通过内容可寻址存储(CAS)、符号链接(Symbolic Link)、硬链接(Hard Link)等管理依赖包,达到多项目之间依赖共享,减少安装时间,也非常的好上手,通过npm install -g pnpm安装,pnpm install安装依赖即可。
]]>你是否会遇到提交代码时,没有改到同事业务模块的任何一行代码,却被提示冲突?提交注释凌乱看不懂的情况?等等。
那么为了项目代码风格统一,代码格式化规范统一,避免代码冲突,提高代码的规范性,提高CodeReview效率等等。因此结合Eslint + Prettier + Husky + Commitlint+ Lint-staged的前端工程化规范应运而生,最终提升了我们开发效率、项目质量。
1 | npm add eslint -D |
1 | npm eslint --init |
1 | (1) How would you like to use ESLint? |
1 | module.exports = { |
此时打开.eslintrc.js配置文件会出现一个报错,需要再env字段中增加node: true配置以解决eslint找不到module的报错
1 | module.exports = { |
1 | { |
1 | npm lint |
这时候命令行中会出现报错,意思就是在解析.vue后缀的文件时候出现解析错误parsing error。
查阅资料后发现,eslint默认是不会解析.vue后缀文件的。因此,需要一个额外的解析器来解析.vue后缀文件。
但是我们查看.eslintrc.js文件中的extends会发现已经有继承”plugin:vue/vue3-essential”的配置。然后在node_modules中可以找到eslint-plugin-vue/lib/cinfigs/essential,里面配置了extends是继承于同级目录下的base.js,在里面会发现parser: require.resolve(‘vue-eslint-parser’)这个配置。因此,按道理来说应该是会解析.vue后缀文件的。
继续往下看.eslintrc.js文件中的extends会发现,extends中还有一个”plugin:@typescript-eslint/recommended”,它是来自于/node_modules/@typescript-eslint/eslint-plugin/dist/configs/recommended.js,查看该文件会发现最终继承于同级目录下的base.js文件。从该文件中可以发现parser: ‘@typescript-eslint/parser’,配置。
按照.eslintrc.js文件中的extends配置的顺序可知,最终导致报错的原因就是@typescript-eslint/parser把vue-eslint-parser覆盖了。
1 | { |
查看eslint-plugin-vue官方文档可知。如果已经使用了另外的解析器(例如”parser”: “@typescript-eslint/parser”),则需要将其移至parseOptions,这样才不会与vue-eslint-parser冲突。
修改.eslintrc.js文件
1 | module.exports = { |
两个parser的区别在于,外面的parser用来解析.vue后缀文件,使得eslint能解析template标签中的内容,而parserOptions中的parser,即@typescript-eslint/parser用来解析vue文件中script标签中的代码。
此时,再执行npm lint,就会发现校验通过了。
如果写一行代码就要执行一遍lint命令,这效率就太低了。所以我们可以配合vscode的ESLint插件,实现每次保存代码时,自动执行lint命令来修复代码的错误。
在项目中新建.vscode/settings.json文件,然后在其中加入以下配置。
1 | { |
1 | npm add prettier -D |
添加以下配置,更多配置可查看官方文档
1 | module.exports = { |
1 | { |
运行该命令,会将我们项目中的文件都格式化一遍,后续如果添加其他格式的文件,可在该命令中添加,例如:.less后缀的文件
安装该插件的目的是,让该插件在我们保存的时候自动完成格式化
在.vscode/settings.json中添加一下规则
1 | { |
在理想的状态下,eslint与prettier应该各司其职。eslint负责我们的代码质量,prettier负责我们的代码格式。但是在使用的过程中会发现,由于我们开启了自动化的eslint修复与自动化的根据prettier来格式化代码。所以我们已保存代码,会出现屏幕闪一起后又恢复到了报错的状态。
这其中的根本原因就是eslint有部分规则与prettier冲突了,所以保存的时候显示运行了eslint的修复命令,然后再运行prettier格式化,所以就会出现屏幕闪一下然后又恢复到报错的现象。这时候你可以检查一下是否存在冲突的规则。
查阅资料会发现,社区已经为我们提供了一个非常成熟的方案,即eslint-config-prettier + eslint-plugin-prettier。
1 | npm add eslint-config-prettier eslint-plugin-prettier -D |
1 | { |
最后重启vscode,你会发现冲突消失了,eslint与prettier也按照我们预想的各司其职了。
stylelint为css的lint工具。可格式化css代码,检查css语法错误与不合理的写法,指定css书写顺序等…
由于我的项目使用的less预处理器,因此配置的为less相关的,项目中使用其他预处理器的可以按照该配置方法改一下就好
stylelint v13版本将css, parse CSS(如SCSS,SASS),html内的css(如*.vue中的style)等编译工具都包含在内。但是v14版本没有包含在内,所以需要安装需要的工具
1 | npm add stylelint postcss postcss-less postcss-html stylelint-config-prettier stylelint-config-recommended-less stylelint-config-standard stylelint-config-standard-vue stylelint-less stylelint-order -D |
依赖说明:
1 | module.exports = { |
1 | "scripts": { |
安装该插件可在我们保存代码时自动执行stylelint
在.vscode/settings.json中添加一下规则
1 | { |
虽然上面已经配置好了eslint、preitter与stylelint,但是还是存在以下问题。
对于不使用vscode的,或者没有安装eslint、preitter与stylelint插件的同学来说,就不能实现在保存的时候自动的去修复与和格式化代码。
这样提交到git仓库的代码还是不符合要求的。因此需要引入强制的手段来保证提交到git仓库的代码时符合我们的要求的。
husky是一个用来管理git hook的工具,git hook即在我们使用git提交代码的过程中会触发的钩子。
1 | npm add husky -D |
1 | { |
该命令会在npm install之后运行,这样其他克隆该项目的同学就在装包的时候就会自动执行该命令来安装husky。这里我们就不重新执行npm install了,直接执行npm prepare,这个时候你会发现多了一个.husky目录。
然后使用husky命令添加pre-commit钩子,运行
1 | npm husky add .husky/pre-commit "npm lint && npm format && npm lint:style" |
执行完上面的命令后,会在.husky目录下生成一个pre-commit文件
1 | #!/usr/bin/env sh |
现在当我们执行git commit的时候就会执行npm lint与npm format,当这两条命令出现报错,就不会提交成功。以此来保证提交代码的质量和格式。
在使用Git提交代码时,通常都需要填写提交说明,也就是Commit Message。在前面的文章中,已经介绍了如何使用Commitizen或可视化工具编写符合规范的Commit Message。然而有些同学可能还是会使用git commit方式提交一些不符合规范的Commit Message。为了禁止不符合规范的Commit Message的提交,我们就需要采用一些工具,只有当开发者编写了符合规范的Commit Message才能够进行commit。而 Commitlint就是这样一种工具,通过结合husky一起使用,可以在开发者进行commit前就对Commit Message进行检查,只有符合规范,才能够进行commit。
使用npm安装Commitlint相关依赖包。
1 | npm install @commitlint/cli @commitlint/config-conventional --save-dev |
安装好Commitlint之后,就需要配置Commitlint,可以在根目录创建commitlint.config.js文件进行配置。
在comminlint.config.js中加入以下代码,表示使用config-conventional规范对提交说明进行检查。具体的规范配置可以查看: https://github.com/conventional-changelog/commitlint
1 | module.exports = { extends: ['@commitlint/config-conventional'] }; |
接下来,需要在package.json中加入commit-msg钩子。
1 | "husky": { |
配置好了之后,当我们进行git commit时,就会触发commit-msg钩子,执行commintlint命令,并且读取commitlint.config.js中的规则对我们的提交说明进行检查,如果校验不通过,将不能提交。
Lint-staged可以在git staged阶段的文件上执行Linters,简单说就是当我们运行ESlint或Stylelint命令时,可以通过设置指定只检查我们通过git add添加到暂存区的文件,可以避免我们每次检查都把整个项目的代码都检查一遍,从而提高效率。
其次,Lint-staged允许指定不同类型后缀文件执行不同指令的操作,并且可以按步骤再额外执行一些其它shell指令。
安装Lint-staged,可以使用npm进行安装。
1 | npm install lint-staged --save-dev |
安装好了Lint-staged之后,就需要配置Lint-staged。我们可以在package.json中加入以下代码,这里需要先安装配置好husky,ESLint和Stylelint。
1 | "husky": { |
接下来,我们就可以将这几个工具结合起来,打造完整的Git检查工作流。下面给出了一份示例代码,其中,该项目采用了Vue-cli进行构建,下面是该项目对应的package.json文件。
1 | { |
配置好package.json之后,当我们进行git commit提交时,首先将会触发pre-commit钩子,调用lint-staged命令,并且会对不同后缀的文件执行不同的检查。接着,还将会触发commit-msg钩子,调用commitlint对我们的提交说明进行检查。如果其中一个无法通过检查,将无法提交。
当校验通过时,就可以放心的将代码提交到代码仓库里。
]]>在我们的日常任务中,我们会编写诸如排序、搜索、查找唯一值、传递参数、交换值等功能,所以在这里我列出了我的速记技巧列表!
JavaScript 真的是一门很棒的语言,值得学习和使用。对于给定的问题,可以有不止一种方法来达到相同的解决方案。在本文中,我们将只讨论最快的。
这些方法会帮助到我们:
大多数这些 JavaScript Hacks 使用 ECMAScript6(ES2015) 以后的技术,尽管最新版本是 ECMAScript11(ES2020)。
我们可以使用默认值(如””、null或 )初始化特定大小的数组0。您可能已经将这些用于一维数组,但如何初始化二维数组/矩阵呢?
1 | const array = Array(5).fill(''); |
我们应该利用reduce方法来快速找到基本的数学运算。
1 | const array = [5,4,7,8,9,2]; |
1 | array.reduce((a,b) => a+b); |
1 | array.reduce((a,b) => a>b?a:b); |
1 | array.reduce((a,b) => a<b?a:b); |
我们有内置的方法sort()和reverse()用于对字符串进行排序,但是数字或对象数组呢?
让我们看看数字和对象的升序和降序排序技巧。
1 | const stringArr = ["Joe", "Kapil", "Steve", "Musk"] |
1 | const array = [40, 100, 1, 5, 25, 10]; |
1 | const objectArr = [ { first_name: 'Lazslo', last_name: 'Jamf' }, { first_name: 'Pig', last_name: 'Bodine' }, { first_name: 'Pirate', last_name: 'Prentice' }]; |
Falsy值喜欢0,undefined,null,false,””,’’可以很容易地通过以下方法省略
1 | const array = [3, 0, 6, 7, '', false]; |
如果你想减少嵌套 if…else 或 switch case,你可以简单地使用基本的逻辑运算符AND/OR。
1 | function doSomething(arg1){ |
您可能已经将 indexOf() 与 for 循环一起使用,该循环返回第一个找到的索引或较新的 includes() 从数组中返回布尔值 true/false 以找出/删除重复项。 这是我们有两种更快的方法。
1 | const array = [5,4,7,8,9,2,7,5]; |
大多数情况下,需要通过创建计数器对象或映射来解决问题,该对象或映射将变量作为键进行跟踪,并将其频率/出现次数作为值进行跟踪。
1 | let string = 'kapilalipak'; |
和
1 | const countMap = new Map(); |
您可以避免使用三元运算符嵌套条件 if…elseif…elseif。
1 | function Fever(temp) { |
通常我们需要在日常任务中合并多个对象。
1 | const user = { |
箭头函数表达式是传统函数表达式的紧凑替代品,但有局限性,不能在所有情况下使用。由于它们具有词法范围(父范围)并且没有自己的范围this,arguments因此它们指的是定义它们的环境。
1 | const person = { |
但
1 | const person = { |
可选的链接 ?.如果值在 ? 之前,则停止评估。为 undefined 或 null 并返回
1 | undefined。 |
利用内置Math.random()方法。
1 | const list = [1, 2, 3, 4, 5, 6, 7, 8, 9]; |
空合并运算符 (??) 是一个逻辑运算符,当其左侧操作数为空或未定义时返回其右侧操作数,否则返回其左侧操作数。
1 | const foo = null ?? 'my school'; |
那些神秘的3点…可以休息或传播!🤓
1 | function myFun(a, b, ...manyMoreArgs) { |
和
1 | const parts = ['shoulders', 'knees']; |
1 | const search = (arr, low=0,high=arr.length-1) => { |
在解决问题的同时,我们可以使用一些内置的方法,例如.toPrecision()或.toFixed()来实现许多帮助功能。
1 | const num = 10; |
1 | let a = 5; |
嗯,这不是一个整体的速记技巧,但它会让你清楚地了解如何使用弦乐。
1 | function checkPalindrome(str) { |
1 | 使用Object.entries(),Object.keys()和Object.values() |
掌握这20个JS方法,在平时项目中还是有帮助的,本文记录Mark下。
]]>什么是双向数据绑定,这里就不做赘述了,vue的双向数据绑定是什么大家都很了解了,这里主要讲vue2和vue3中双向绑定的区别。
vue2中使用“Object.defineProperty”对象以及对象属性的劫持实现双向绑定;而vue3中的响应式采用了ES6中的“Proxy”方法实现双向绑定。
关于对象: Vue 无法检测 property 的添加或移除。
关于数组:不能利用索引直接设置一个数组项,也不能修改数组的长度。
总结来说Object.defineProperty方法存在一定的局限性
1 | push() |
Proxy 是ES6中新增的一个特性,可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
基本用法:
1 | //ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。 |
参数解释
例子
1 | // #注:Proxy 实例也可以作为其他对象的原型对象。 |
1 | observe(data) { |
一般前端开发工程有以前要求:
为了要处理代码压缩混淆, 处理浏览器JavaScript的兼容性, 性能优化等问题,webpack的使用则非常适合。
1 | const path = require('path') |
1 | devServer: { |
1 | module: { |
1 | module.exports = { |
1 | resolve: { |
1 | const path = require('path') |
通过webpack简单的配置就完成了,webpack性能优化其实也是实际项目中需要完成,这里不做赘述了。
]]>假如后台返回一个扁平的数据结构,转成树,应该怎么做呢?
打平的数据内容如下:
1 | let arr = [ |
期望输出的结果:
1 | [ |
假如先不用考虑性能问题,实现功能即可。
可能10%的人没思路,没碰到过这种结构;60%的人说用过递归,有思路,给他个笔记本,但就是写不出来;20%的人在引导下,磕磕绊绊能写出来;剩下10%的人能写出来,但性能不是最佳。
接下来,我们用几种方法来实现这个小算法
判断一个算法的好坏,一般从执行时间和占用空间来看,执行时间越短,占用的内存空间越小,那么它就是好的算法。对应的,我们常常用时间复杂度代表执行时间,空间复杂度代表占用的内存空间。
时间复杂度的计算并不是计算程序具体运行的时间,而是算法执行语句的次数。 随着n的不断增大,时间复杂度不断增大,算法花费时间越多。
常见的时间复杂度有:
通常我们计算时间复杂度都是计算最坏情况。计算时间复杂度的要注意的几个点
1 | let x = 1; |
1 | for (i = 0; i < n; i++){ |
1 | for(var i = 0; i<n && arr[i] !=1; i++) { |
空间复杂度是对一个算法在运行过程中临时占用存储空间的大小。
1 | let a = 1; |
1 | function fun(n) { |
主要思路是提供一个递getChildren的方法,该方法递归去查找子集。 就这样,不用考虑性能,无脑去查,大多数人只知道递归,就是写不出来。。。
1 | /** |
从上面的代码我们分析,该实现的时间复杂度为O(2^n)。
主要思路是先把数据转成Map去存储,之后遍历的同时借助对象的引用,直接从Map找对应的数据做存储
1 | function arrayToTree(items) { |
从上面的代码我们分析,有两次循环,该实现的时间复杂度为O(2n),需要一个Map把数据存储起来,空间复杂度O(n)
主要思路也是先把数据转成Map去存储,之后遍历的同时借助对象的引用,直接从Map找对应的数据做存储。不同点在遍历的时候即做Map存储,有找对应关系。性能会更好。
1 | function arrayToTree(items) { |
从上面的代码我们分析,一次循环就搞定了,该实现的时间复杂度为O(n),需要一个Map把数据存储起来,空间复杂度O(n)
方法 1000(条) 10000(条) 20000(条) 50000(条)
递归实现 154.596ms 1.678s 7.152s 75.412s
不用递归,两次遍历 0.793ms 16.499ms 45.581ms 97.373ms
不用递归,一次遍历 0.639ms 6.397ms 25.436ms 44.719ms
从我们的测试结果来看,随着数量的增大,递归的实现会越来越慢,基本成指数的增长方式。
实践出真理,大家共勉进步。
]]>客户端存储的概念
而把数据保存到磁盘中,就是缓存技术。
平时项目中比较常用的存储方式,有cookie、localStorage、sessionStorage。
存储cookie是浏览器提供的功能,cookie其实是存储在浏览器中的纯文本,浏览器的安装目录下会专门有一个 cookie 文件夹来存放各个域下设置的cookie。
创建cookie:
document.cookie=””;值是字符串,字符串是有格式的,由键值对 key=value构成,键值对之间由一个分号和一个空格隔开。
获取cookie:这个方法只能获取非 HttpOnly 类型的cookie
let myCookie = document.cookie;
左边栏Cookies下方会列举当前网页中设置过cookie的域都有哪些。上图中只有一个域。而右侧区域显示的就是某个域下具体的 cookie 列表,对应上图就是该域下设置的cookie。
当网页要发http请求时,浏览器会先检查是否有相应的cookie,有则自动添加在request header中的cookie字段中。这些是浏览器自动帮我们做的,而且每一次http请求浏览器都会自动帮我们做。
每个cookie都有一定的属性,如什么时候失效,要发送到哪个域名,哪个路径等。这些属性是通过cookie选项来设置的,expires、domain、path、secure、HttpOnly。在设置任一个cookie时都可以设置相关的这些属性也可以不设置,这时会使用这些属性的默认值。在设置这些属性时,属性之间由一个分号和一个空格隔开。
1 | "key=name; expires=Thu, 25 Feb 2016 04:18:00 GMT; domain=ppsc.sankuai.com; path=/; secure; HttpOnly" |
expires=Thu, 25 Feb 2016 04:18:00 GMT表示cookie将在这个时间之后失效,对于失效的cookie浏览器会清空。如果没有设置该选项,则默认有效期为session,即会话cookie,这种cookie在浏览器关闭后就没有了。
domain 和 path,domain是域名,path是路径,两者加起来就构成了 URL,domain和path一起来限制 cookie 能被哪些 URL 访问。domain的默认值为设置该cookie的网页所在的域名,path默认值为设置该cookie的网页所在的目录。
secure选项用来设置cookie只在确保安全的请求中才会发送。当请求是HTTPS或者其他安全协议时,包含 secure 选项的 cookie才能被发送至服务器。默认情况下,cookie不会带secure选项(即为空)。所以默认情况下,不管是HTTPS协议还是HTTP协议的请求,cookie 都会被发送至服务端。但要注意一点,secure选项只是限定了在安全情况下才可以传输给服务端,但并不代表你不能看到这个 cookie。如果想在客户端即网页中通过 js 去设置secure类型的 cookie,必须保证网页是https协议的。在http协议的网页中是无法设置secure类型cookie的。
httpOnly选项用来设置cookie是否能通过 js 去访问。默认情况下,cookie不会带httpOnly选项(即为空),所以默认情况下,客户端是可以通过js代码去访问(包括读取、修改、删除等)这个cookie的。当cookie带httpOnly选项时,客户端则无法通过js代码去访问(包括读取、修改、删除等)这个cookie。在客户端是不能通过js代码去设置一个httpOnly类型的cookie的,这种类型的cookie只能通过服务端来设置。
size选项是cookie对象的值字符串的字符个数。
LocalStorage:永久性保存
window.localStorage.setItem(string key, value); //保存
window.localStorage.getItem(string key);//获取
window.localStorage.clear();//清空
window.localStorage.removeItem(sring key)//根据键得到值
window.localStorage.length :返回一个整数,表示存储在
LocalStorage 对象中的数据项(键值对)数量。
window.localStorage.key(int index) :返回当前LocalStorage 对象的第index序号的key名称。若没有返null。
SessionStorage:关闭浏览器数据就自动全部删除
window.sessionStorage.length :返回一个整数,表示存储在 sessionStorage 对象中的数据项(键值对)数量。
window.sessionStorage.key(int index) :返回当前 sessionStorage 对象的第index序号的key名称。若没有返null。
window.sessionStorage.getItem(string key) :返回键名(key)对应的值(value)。若没有返回null。
window.sessionStorage.setItem(string key, string value) :该方法接受一个键名(key)和值(value)作为参数,将键值对添加到存储中;如果键名存在,则更新其对应的值。
window.sessionStorage.removeItem(string key) :将指定的键名(key)从 sessionStorage 对象中移除。
window.sessionStorage.clear() :清除 sessionStorage 对象所有的项。
localStorage和sessionStorage存储数据的有效期和作用域不同:
localStorage的作用域限制在文档源; 文档源由协议、域名、端口三者来确定;
下面的URL就拥有不同的文档源:
www.hqyj.com //协议http,域名www.hqyj.com
www.hqyj.com //协议https,和上面不同
www.farsight.com.cn/ //和上面域名不同
www.farsight.com.cn:81/ //和上面端口不同,默认端口是80
这种情况下,localStorage存储的数据是不能相互访问的; 即便他们来自同一台服务器;
localStorage同源的文档之间可以相互访问和修改相同名称的数据;
localStorage受浏览器厂商的限制,chrome下存储的数据,360浏览器下不可访问; 会得到‘Invalid Date’;
他还被限制在窗口中,意思是同一个窗口或标签页的不同页面之间可以共享sessionStorage;
但是不同的窗口或标签页之间不能共享sessionStorage,即便他们是同一个页面地址;
窗口是指顶级窗口,如果是多个iframe,他们之间共享sessionStorage;
cookie数据大小不能超过4k;
sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大;
localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据;
sessionStorage 数据在当前浏览器窗口关闭后自动删除;
cookie 设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭;
cookie的数据会自动的传递到服务器,服务器端也可以写cookie到客户端;
sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存;
localStorage的作用域限制在文档源的;
localStorage同源的文档之间可以相互访问和修改相同名称的数据;
localStorage受浏览器厂商的限制,chrome下存储的数据,360浏览器下不可访问; 会得到‘Invalid Date’;
sessionStorage在localStorage的同源策略基础之上,还有更严格的限制:
他还被限制在窗口中,意思是同一个窗口或标签页的不同页面之间可以共享sessionStorage;
但是不同的窗口或标签页之间不能共享sessionStorage,即便他们是同一个页面地址;
这里的窗口是顶级窗口,如果里面有多个iframe,他们之间共享sessionStorage;
]]>众所周知, hash 和 history 在前端面试中是很常考的一道题目。从表面上看,hash 和 history 的区别可能就在 hash 的 url 里面多了个 # ,而 history 就不会。
对于前端路由来说, hash 和 history 都可以用于前后端分离项目,且两者有各自的特点和各自的使用场景,在使用过程中主要要了解当前项目所处的场景,以便于更好地判断使用哪一种路由模式更佳,我们一起来探讨下。
SPA,即单页面应用(Single Page Application)。所谓单页 Web 应用,就是只有一张 Web 页面的应用。单页应用程序 (SPA) 是加载单个 HTML 页面并在用户与应用程序交互时动态更新该页面的 Web 应用程序。浏览器一开始会加载必需的 HTML 、 CSS 和 JavaScript ,所有的操作都在这张页面上完成,都由 JavaScript 来控制。
现如今,为了配合单页面 Web 应用快速发展的节奏,各类前端组件化技术栈层出不穷。近几年来,通过不断的版本迭代, vue 和 react 两大技术栈脱颖而出,成为当下最受欢迎的两大技术栈。
对于现代开发的项目来说,稍微复杂一点的 SPA ,都需要用到路由。而 vue-router 正是 vue 的路由标配,且 vue-router 有两种模式: hash 和 history 。
hash 模式是一种把前端路由的路径用井号 # 拼接在真实 url 后面的模式。当井号 # 后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发 onhashchange 事件。
location.protocal 协议
location.hostname 主机名
location.host 主机
location.port 端口号
location.patchname 访问页面
location.search 搜索内容
location.hash 哈希值
下面用一个网址来演示以上属性:
1 | //http://127.0.0.1:8001/01-hash.html?a=100&b=20#/aaa/bbb |
hash变化会触发网页跳转,即浏览器的前进和后退。
hash 可以改变 url ,但是不会触发页面重新加载(hash的改变是记录在 window.history 中),即不会刷新页面。也就是说,所有页面的跳转都是在客户端进行操作。因此,这并不算是一次 http 请求,所以这种模式不利于 SEO 优化。hash 只能修改 # 后面的部分,所以只能跳转到与当前 url 同文档的 url 。
hash 通过 window.onhashchange 的方式,来监听 hash 的改变,借此实现无刷新跳转的功能。
hash 永远不会提交到 server 端(可以理解为只在前端自生自灭)。
history API 是 H5 提供的新特性,允许开发者直接更改前端路由,即更新浏览器 URL 地址而不重新发起请求。
我们用一个例子来演示, hash 与 history 在浏览器下刷新时的区别。具体如下:
正常页面浏览
1 | https://github.com/xxx 刷新页面 |
改造H5 history模式
1 | https://github.com/xxx 刷新页面 |
下面阐述几种 HTML5 新增的 history API 。具体如下表:
API | 定义 |
---|---|
history.pushState(data, title [, url]) | pushState主要用于往历史记录堆栈顶部添加一条记录。各参数解析如下:①data会在onpopstate事件触发时作为参数传递过去;②title为页面标题,当前所有浏览器都会忽略此参数;③url为页面地址,可选,缺少时表示为当前页地址 |
history.replaceState(data, title [, url]) | 更改当前的历史记录,参数同上; 上面的pushState是添加,这个更改 |
history.state | 用于存储以上方法的data数据,不同浏览器的读写权限不一样 |
window.onpopstate | 响应pushState或者replaceState的调用 |
对于 history 来说,主要有以下特点:
对于 history 来说,确实解决了不少 hash 存在的问题,但是也带来了新的问题。具体如下:
那实际项目中,如何对这两者进行选择。具体如下:
对于 hash 和 history 来讲,要清楚两者的区别以及两者各自的使用场景,还有各自的使用特点和优缺点。
]]>工作中手写CSS,是信手拈来,但是也免不了有些样式忘记了,去百度或google,同时是不是还在手写按钮、文本字体等简单样式,这些不得不费时却又不得不做得工作?
本文的推荐工具就可以帮你解决以上种种难题,从此下班你将快人一步,回家撸猫撸游戏。
本文做Mark,赶快用起来吧!
推荐:★★★★★
简介:CSS Button Generator是一个免费的在线工具,可让您创建跨浏览器的 HTML 和CSS 按钮样式,您不必学习任何复杂的CSS规则。只需单击并滑动即可制作CSS 3按钮。很多漂亮的按钮样本。
地址:9elements.github.io/fancy-borde…
推荐:★★★★★
简介:通过拖拽的形式生成需要的border-radius!
推荐:★★★★★
简介:可以生成多个分层阴影,提供非常酷的效果,你也可以自定义颜色。
推荐:★★★★★
简介:此网站通过 选择颜色:或大小:半径:距离:强度:模糊:形状:复制边框半径:50px;背景,生成非常nice的阴影,让你的界面更加的美观自然。
地址:tool.lu/css/
推荐:★★★★
简介:美化:格式化代码,使之容易阅读。净化:将代码单行化,并去除注释。整理:按照一定的顺序,重新排列css的属性。优化:将css的长属性值优化为简写的形式。压缩:将代码最小化,加快加载速度!
推荐:★★★★
简介:CSS Gradient 是一个快乐的小网站和免费工具,可让您为网站创建渐变背景。
地址:cssgrid-generator.netlify.app/
推荐:★★★★★
简介:您可以设置行和列的数字还有单位,我将为您生成一个 CSS Grid 网格布局!在方框中拖动来创建 div 放置在网格内。
推荐:★★★★
简介:各种各样的css动画合集
地址:loading.io/
推荐:★★★★★
简介:在这里你可以生成多个加载动画并将其下载为SVG、GIF、PNG和其他格式,但它最棒的特点是你可以将这些动画自定义到一个新的水平。值得尝试。
地址:getwaves.io/
推荐:★★★★★
简介:通过一些自定义生成简单的波纹。
推荐:★★★★★
简介:不要问我,得问它!css浏览器兼容性查询工具
推荐:★★★★★
简介:各种前端实用的好工具,杜绝造轮子,每款工具都经过精心打磨,帮助所有程序员提高前端开发效率!
推荐:★★★★★
简介:css小技巧合集,一直在更新!
记个笔记Mark下,以后有用好翻阅。
]]>相信很多人都会被面试到一道比较综合的面试题,答案也不是固定的,从浏览器输入地址到页面渲染经过了很多的过程,且每个过程都可以深挖出很多知识点,面试官可以用这一道题区分出不同面试者的水平。下面我们就来具体学习下。
构建请求
1 | GET/HTTP/1.1; |
浏览器会先检查是否存在缓存,如果存在缓存就直接从缓存里面拿数据,给到浏览器进行渲染
由于我们输入的是域名,而数据包是通过 IP 地址传给对方的。因此我们需要得到域名对应的 IP 地址。这个过程需要依赖一个服务系统,这个系统将域名和 IP 一一映射,我们将这个系统就叫做 DNS(域名系统)。得到具体 IP 的过程就是 DNS 解析。 当然,值得注意的是,浏览器提供了 DNS 数据缓存功能。即如果一个域名已经解析过,那会把解析的结果缓存下来,下次处理直接走缓存,不需要经过 DNS 解析
建立 TCP 连接经历了下面三个阶段
TCP 就是通过三次握手确认连接,数据包校验保证数据到达接收方,然后通过四次挥手断开连接保证数据传输的可靠性
现在 TCP 连接建立完毕,浏览器可以和服务器开始通信,即开始发送 HTTP 请求。浏览器发 HTTP 请求要携带三样东西:请求行、请求头和请求体
HTTP 请求到达服务器,服务器进行对应的处理。最后要把数据传给浏览器,也就是返回网络响应。
响应头包含了服务器及其返回数据的一些信息, 服务器生成数据的时间、返回的数据类型以及对即将写入的 Cookie 信息。如果请求头或响应头中包含 Connection: Keep-Alive,表示建立了持久连接,这样 TCP 连接会一直保持,之后请求统一站点的资源会复用这个连接。
完成以上过程后,数据已经达到浏览器端,接下来就是浏览器解析并渲染数据了
由于浏览器无法直接理解 HTML 字符串,因此将这一系列的字节流转换为一种有意义并且方便操作的数据结构,这种数据结构就是 DOM 树。DOM 树本质上是一个以 document 为根节点的多叉树
首先,浏览器是无法直接识别 CSS 样式文本的,因此渲染引擎接收到 CSS 文本之后第一件事情就是将其转化为一个结构化的对象,即 styleSheets。 这个格式化的过程过于复杂,而且对于不同的浏览器会有不同的优化策略,这里就不展开了。 在浏览器控制台能够通过 document.styleSheets 来查看这个最终的结构。当然,这个结构包含了以上三种 CSS 来源,为后面的样式操作提供了基础。
现在已经生成了 DOM 树和 DOM 样式,接下来要做的就是通过浏览器的布局系统确定元素的位置,也就是要生成一棵布局树(Layout Tree)。 布局树生成的大致工作如下:
值得注意的是,布局树只包含可见元素,对于 head 标签和设置了 display: none 的元素,将不会被放入其中。
浏览器将 HTML 解析成树形结构的 DOM 树,一般来说,这个过程发生在页面初次加载,或页面 JavaScript 修改了节点结构的时候
浏览器将 CSS 解析成树形结构的 CSSOM 树,再和 DOM 树合并成渲染树
浏览器根据渲染树所体现的节点、各个节点的 CSS 定义以及它们的从属关系,计算出每个节点在屏幕中的位置。Web 页面中元素的布局是相对的,在页面元素位置、大小发生变化,往往会导致其他节点联动,需要重新计算布局,这时候的布局过程一般被称为回流(Reflow)。
遍历渲染树,调用渲染器的 paint() 方法在屏幕上绘制出节点内容,本质上是一个像素填充的过程。这个过程也出现于回流或一些不影响布局的 CSS 修改引起的屏幕局部重画,这时候它被称为重绘(Repaint)。实际上,绘制过程是在多个层上完成的,这些层我们称为 渲染层(RenderLayer)
多个绘制后的渲染层按照恰当的重叠顺序进行合并,而后生成位图,最终通过显卡展示到屏幕上。
那什么是渲染层合成呢?
在 DOM 树中每个节点都会对应一个渲染对象(RenderObject),当它们的渲染对象处于相同的坐标空间(z 轴空间)时,就会形成一个 RenderLayers,也就是渲染层。渲染层将保证页面元素以正确的顺序堆叠,这时候就会出现层合成(composite),从而正确处理透明元素和重叠元素的显示。 这个模型类似于 Photoshop 的图层模型,在 Photoshop 中,每个设计元素都是一个独立的图层,多个图层以恰当的顺序在 z 轴空间上叠加,最终构成一个完整的设计图。 对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。
栅格化操作完成后,合成线程会生成一个绘制命令,即”DrawQuad”,并发送给浏览器进程。 浏览器进程中的 viz 组件接收到这个命令,根据这个命令把页面内容绘制到内存,也就是生成了页面,然后把这部分内存发送给显卡,从而展示在屏幕上。
从浏览器的渲染过程中我们知道,页面 HTML 会被解析成 DOM 树,每个 HTML 元素对应了树结构上的一个 node 节点。而从 DOM 树转化到一个个的渲染层,并最终执行合并、绘制的过程,中间其实还存在一些过渡的数据结构,它们记录了 DOM 树到屏幕图形的转化原理,其本质也就是树结构到层结构的演化。
]]>通常情况下,声明在一个函数中的函数,叫做闭包函数,在Javascript语言中,只有函数内部的子函数才能读取局部变量:
1 | // 闭包函数 |
让外部访问函数内部变量成为可能
可以避免使用全局变量,防止全局变量污染
局部变量会常驻在内存中,会造成内存泄漏(有一块内存空间被长期占用,而不被释放)
设想有此场景:输入框中内容变化需要实时请求接口以获取最新搜索结果,如果在输入完成前输入框内容每变化一下都去请求接口,会造成很多不必要的请求,大大增加服务器压力。
解决思路:有变化时延迟一段时间再执行function,若在这段延迟时间内又有新变化,则重新开始延迟
1 | // 定时器期间,有新操作时,清空旧定时器,重设新定时器 |
代码进一步优化:周期内有新事件触发时,重置定时器开始时间戳,定时器执行时,判断开始时间戳,若开始时间戳被推后,重新设定延时定时器;加入是否立即执行参数。
1 | // 增加前缘触发功能 |
设想有此场景:有‘搜索’按钮,每点击一次都会重新请求接口,获取并渲染页面表格最新数据,假如短时间内连续点击按钮,依然会造成很多不必要的请求
解决思路:在一段时间内只执行最后一次function
1 | // 定时器期间,只执行最后一次操作 |
防抖及节流都是使用闭包函数来应用的实际场景,平时也应注意合理使用闭包函数,避免性能消耗过多。
]]>最近更新到Mac OS最新系统Catalina,重装了Flutter,出现了些问题,在网上也搜索了,可能是才出的新版本问题,也没找到解决办法,最终自己捣鼓解决了此问题,随笔记录下此问题。
正常搭建Flutter的过程就不说了,到最后一步执行flutter doctor
时报错如下:
1 | TangYanQiong-MacbookPro:~ TangDan$ flutter doctor |
网上查了,大多答案都是让执行flutter doctor --android-licenses
,结果又报错如下:
1 | TangYanQiong-MacbookPro:~ TangDan$ flutter doctor --android-licenses |
这个意思是说,Android sdkmanager tool没找到,然后按照提示的目录信息,去找了一下,发现我的sdk目录下,根本就没有tools这个文件夹,后来调查发现,这个tools文件实际上是android studio安装了Android SDK Tools才会有生成那个文件夹,在这里勾选安装,如果你本地有这个的话,安装了应该就好了。
但是我本地并没有这个选项,我这边的Android SDK配置是这样的,根本没有tools这个安装包,只有Command-line Tools这个,有点类似,就也勾选安装了:
安装成功后的目录如下:
还是没有tools这个文件夹,但是sdkmanager有了,后面自己想了下,不是缺tools吗?只是路径不对的问题了,就自己新建了个tools,里面在创建了个bin文件夹,再把sdkmanager拷进去。
猜想是,Android Studio最新版本Tools版本、名称及路径修改了,但是flutter最新版本并未更新,还是使用老路径导致找不到。
现在再来执行flutter doctor --android-licenses
,一路y
下去,设置成功。1
2
3
4TangYanQiong-MacbookPro:~ TangDan$ flutter doctor --android-licenses
5 of 7 SDK package licenses not accepted. 100% Computing updates...
Review licenses that have not been accepted (y/N)? y
...
最后,再执行flutter doctor
,大功告成,不报错了。
1 | TangYanQiong-MacbookPro:~ TangDan$ flutter doctor |
前两天更新了Mac OS Catalina系统,总体来说还是可以,虽然有些只支持32位的应用,比如2015版的Office、WireShark等不能用了,但塞翁失马焉知非福呢,卸掉了Office相关的Word、Excel、PPT,瞬间清了几大个G,然后去下载了WPS多方文档格式支持软件,简直不要太好用,而且才几百兆,扯远了。。。
今天随笔记一个小事件,对于强迫症人士有用,比如我。
终端Shell报警告:
1 | The default interactive shell is now zsh. |
从报的警告就可以看出,提示原来使用的是bash风格,然而Catalina系统的shell已更改为zsh,请用 chsh -s /bin/zsh
更新替换,好吧,就run了,结果变成了这个风格
1 | TangDan@TangYanQiongdeMacBook-Pro ~ % |
对这种爱不起来,然后查看系统支持的shell风格列表:
1 | TangYanQiongdeMacBook-Pro:~ TangDan$ cat /etc/shells |
然后每个都试了,还是/bin/bash
这种风格TangYanQiongdeMacBook-Pro:~ TangDan$
最喜欢,但是系统会报警告,好,那把这个警告怎么消除?
1 | vim ~/.bash_profile |
i
进入编辑模式1 | # macOS Catalina |
esc
退出编辑模式,再输入:wq
保存退出,现在终端就不会报警告了。1 | Last login: Fri Apr 10 15:40:06 on ttys006 |
看到我这边shell用户名TangYanQiongdeMacBook-Pro
我想改它很久(中英混合。。。),这会顺便改了,打开系统偏好设置-共享-直接修改电脑名称就可以修改了
1 | Last login: Fri Apr 10 15:46:05 on ttys006 |
舒服了,😃
]]>React 是一个用于构建用户界面的 JavaScript 库。
声明式UI
Props
State
生命周期 图例
react 的核心卖点之一
setState (只要调用了 setState 就会调用 render 无论你 setState 修改的是什么,哪怕是页面里没有的一个数据,render 都会被触发,并且父组件渲染中会嵌套渲染自、子组件。)
render
diff | reconciliation
官方一点的定义应该称为 reconciliation,也就是 React 用来比较两棵节点树的算法,它确定树中的哪些部分需要被更新。
在确定两棵树的区别后,会根据不同的地方对实际节点进行操作,这样你看到的界面终于在这一步得到了改变。当年 React 也就因为这个高效的 dom 操作方法得到追捧。
之前做移动端,对网络进行测试抓包一直用的Charles抓包工具,很实用的一款工具,那怎么抓微信小程序的包呢,实际跟移动端是一样的,下面统一记录下,避免换电脑遗忘。
其实要想抓取到微信小程序的数据首先要解决的第一个问题件就是如何通过charles抓取手机上的数据(HTTP)。
charles上通过proxy->proxy setting进入代理设置,入口如下图所示:
点击后进入如下图所示:
记住此处的port,默认为8888,也可以进行修改,只要不冲突就可以,勾选上Enable transparent HTTP proxying,到此为止完成charles上的初步设置。
到此为止,完成了电脑端的设置。
设置手机代理,注意要保证手机所连接的wifi跟电脑在一个局域网内(就是连接同一个wifi或者通过电脑分享出的wifi进行连接)
首先,需要知道电脑的ip地址,手机上进入wifi设置,电脑与手机共连同一个网络,修改手机上wifi代理
点击代理后进入如下界面,服务器主机名处填写刚才查到的电脑的ip地址即可,服务器端口填写第一步中charles处设置的端口,默认是8888,如果做了修改,填写设置charles时修改的端口值。
点击保存,此时charles上会弹出一个对话框,点击allow(允许)即可。此时就可以抓取手机上的http数据包了(注意不是https)
如下图所示,点入一个应用后,抓取到的http包。
完成以上步骤,charles会同时抓取手机以及电脑上的数据包,如果针对手机抓包可以通过取消勾选下图所示的选项屏蔽掉,这样更清楚。
手机连接不上Charles的几种原因及解决方案:
确认手机跟电脑是不是在一个wifi环境下,我在使用过程中又一次手机由于wifi信号强弱问题自动切换过wifi导致抓了一般的数据包抓不到了。
可以尝试更换一下端口号(8888可能已经被占用)。
可以通过电脑手动添加手机的ip。
试试关掉电脑防火墙,在重新连接。
最后大招,万能重启。重新打开charles,重新设置手机连接。
到此为止,完成了一大步骤的设置,可以通过电脑抓取手机的HTTP数据包了,但对于HTTPS数据,到此步为止,抓包工具上的列表部分会显示一堆unknown的地址。
那怎么来解决呢?
首先是电脑端的配置,进入Charles的Help->SSL Proxying->Install Charles Root Certificate
点击Install Charles Root Certificate之后,会弹出mac的钥匙串访问页面,点击允许并安装证书,加入成功后会显示如下:
右键点击该证书,选择菜单中的“显示简介选项”,接着进入信任栏目,将其全部置为“始终信任”
接着点击Proxy->SSL Proxy Settings,弹出如下页面
弹出的对话框中,勾选Enable SSL Proxying,然后点击add添加Host为和Port为443,点击ok(此处将host设置为的意思是主抓取全部的http是数据包,如果想针对某个域名抓取可以在此设置)
到此为止,完成了电脑端的设置
接着需要在手机端安装证书,点击Charles上的Help->SSL Proxying->Install Charles Root Certificate on a Mobile Device or Remote Browser
点击之后弹出如下对话框:
接着在手机浏览器上访问charlesproxy.com/getssl这个地址,然后会弹出如下界面:
输入一个名字比如charlesproxy之后点击确定,会有一个一闪而过的提示,就ok了
此时进入小程序,可以看到charles上能够看到https的接口的地址和数据了
到此,就可以愉快的抓包了。
苹果设备iOS10以后证书可以正常安装,可正常使用HTTPS抓包,但是安卓设备呢,需注意:
由于在Android7.0及以上的系统中,每个应用可以定义自己的可信CA集,默认情况下,应用只会信任系统预装的CA证书,而不会信任用户安装的CA证书
简单来说,就是安卓系统7.0及以下的设备可以正常安装证书进行HTTPS抓包,7.0以上则需要曲线救国,这里就不多说了,我后面在电脑上用网易MuMu模拟器(该模拟器系统是6.0)安装微信,在wifi设置里长按默认网络进行网络修改,设置代理等,也可以愉快的抓小程序的数据包了。
注意,有一些应用使用的网络框架是不允许通过代理访问的,此时通过charles抓包显示的地址仍是unknown,或者手机上访问该应用会提示网络连接错误等信息,此时取消勾选charles的SSL Proxying settings中的勾选框就可以正常访问了。
抓取工作完成后,记得把手机上的代理设置恢复原样,否则当电脑上的charles关闭时,手机将无法正常访问网络。
]]>关于iOS及Web的使用语言Object-C、Swift、JavaScript,大家都耳熟能详,那它们到底是什么类型的静态语言?以及什么强弱类型语言?很多同学只是简单的背出它们是什么语言及类型,但并没有理解到,现在来举例说明和分享一下。
我用的最多的当然是Object-C,毕竟是做iOS开发出身,对这个语言了解最深。首先,因为黑魔法RunTime机制,Object-C是一个运行时的动态类型的强类型语言,举例如下:
1 | NSString *test = @"1231231"; |
1 | NSString *certsPath = [[NSBundle mainBundle] pathForResource:@"server" ofType:@"crt"]; |
iOS的这两种开发语言,Swift和OC,Swift更简洁了,减少了代码的冗余性,最开始接触它的时候,我还以为它是一门弱语言类型的语言,后来发现我错了,下面会娓娓道来,它实际是一门静态类型的强类型语言。
1 | var tmp = "asdfas" |
Swift 变量不强制的指定类型,而是用 var 和 let 表示可变与不可变。所以,误Swift是一门弱类型的语言。
像下面的代码块,变量赋值时并没声明类型
1 | let num = 1 |
基于以上,从自身理解,误以为Swift是弱类型语言
但是,实际上,这里没有强制声明类型,看似一个弱类型的语言,其实不然。Swift 编译器能够推断出 num 是 Int 类型。那么 num 就不能再被赋值为 String 类型。更不能写成如下的形式:
1 | let num = 1 |
编译器将会报如下的错误:
1 | error: binary operator '+' cannot be applied to operands of type 'Int' and 'String' |
这说明了,Swift 是一门强类型语言。Swift 的类型声明,你可以看成是在定义变量的时候,隐式声明的(由编译器推断出),当然也可以显式的声明。如下:
1 | let num: Int = 1 |
Web中的常用语言JavaScript,如果有语言基础的话,是一门很快上手的语言,我也很喜欢JavaScript,当然也有使用中比较头疼的事,因为它是一门动态类型的弱类型语言,往往项目中在项目运行成功后,控制台意想不到的报错,不像强语言类型那样,直接编译不过,而我又有Swift的编写思想,想着它类型编译通过了,运行肯定就没问题,往往会出现比如编译没问题,类型错误的问题,而且它没有iOS中nil可以调用任何方法的容错机制,如果这个对象为nil再调用其某个方法也会报错,所以写的时候必须要判断是否为空,增加代码冗余性,但是,好处大于弊端,比如代码简洁性,可以动态写很多业务逻辑,我还是很喜欢它。
1 | let tmpFlag = false |
1 | let tmpFlag = false |
TypeScript是JavaScript的一个超集,TypeScript可以使用JavaScript中的所有代码和编码概念,TypeScript是为了使 JavaScript的开发变得更加容易而创建的,它是关于尽早捕获错误并使您成为更高效的开发人员,它是什么类型的语言?我们把在JavaScript上实验的代码,放在ts里一试便知道。
1 | let tmpFlag = false |
报错如下:
1 | 43:9 Type 'false' is not assignable to type 'string'. |
1 | let tmpFlag = false |
以下代码也未报错:
1 | let num = 1 |
综上可知,TypeScript是一门静态类型的弱类型语言。
]]>最近想学跨平台开发,在RN和Flutter中纠结学哪个,看了资料RN对Web前端比较友好,都是JS开发语言,比较容易上手,但是有个以前的iOS同事,他现在已经用Flutter着手在开发iOS、Android了,据他说很好用。
跨平台,首先考虑的就是性能问题,谁的性能好,未来必将是谁的天下。
RN的效率由于是将View编译成了原生View,所以效率上要比基于Cordova的HTML5高很多,但是它也有效率问题,RN的渲染机制是基于前端框架的考虑,复杂的UI渲染是需要依赖多个view叠加。比如我们渲染一个复杂的ListView,每一个小的控件,都是一个native的view,然后相互组合叠加,想想此时如果我们的list再需要滑动刷新,会有多少个对象需要渲染,所以也就有了前面所说的RN的列表方案不友好。
而Flutter则吸收了前两者的教训之后,在渲染技术上,选择了自己实现(GDI),有自己的一套UI系统,由于有更好的可控性,使用了新的语言Dart,避免了RN的那种通过桥接器与Javascript通讯导致效率低下的问题,所以在性能方面无可厚非比RN更高一筹,有经验的开发者可以打开Android手机开发者选项里面的显示边界布局,发现Flutter的布局是一个整体,说明Flutter的渲染没用使用原生控件进行渲染。
Flutter性能会更好无线接近原生的体验,Dart是AOT编译的,编译成快速、可预测的本地代码
RN采用JS语言开发,基于React,对前端工程师更友好。Dart语言受众小
Flutter自己实现了一套UI框架,丢弃了原生的UI框架。而RN还是可以自己利用原生框架,两个各有好处。Flutter的兼容性高,RN可以利用原生已有的优秀UI
RN的布局更像css,而Flutter的布局更像native布局,但是去掉xml通过代码直接写需要适应下
对比了RN/Flutter后,我觉得对于我来说,RN应该很容易上手,因为有前端基础,RN学习起来应该很快,但是我也不想放弃Flutter,Flutter布局像原生布局,有iOS开发经验的我也不想放弃,因为有iOS同事成功上车的前车之鉴,所以我也很有信心能学好它,从性能来说我也更偏向Flutter,但是也不想浪费RN这个学习机会,所以就我自己的实际情况而言,我会两个都学。
总之,一句话吧,基于自身实际情况,没有更好只有最适合你的框架。
]]>由于前段时间工作忙,快一个月没更新博客了,貌似有些忘了MarkDown的语法,这里记录下MarkDown语法作备忘录,以后方便查找。
1 | #一级标题 |
1
2
3
1
2
3
4
引用需要使用>和空格
这就是粗体
这就是斜体
这就是斜粗体
这个是删除线
~~~
@IBAction func showMessage(){
let alertController = UIAlertController(title: “welcome to my first app”, message: “hello world”, preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: “OK”, style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alertController, animated: true, completion: nil)
}`