Vue 开发规范(下)
提供组件 API 文档
使用 Vue.js 组件的过程中会创建 Vue 组件实例,这个实例是通过自定义属性配置的。为了便于其他开发者使用该组件,对于这些自定义属性即组件API应该在 README.md 文件中进行说明。
为什么?
良好的文档可以让开发者比较容易的对组件有一个整体的认识,而不用去阅读组件的源码,也更方便开发者使用。
组件配置属性即组件的 API,对于组件的用户来说他们更感兴趣的是 API 而不是实现原理。
正式的文档会告诉开发者组件 API 变更以及向后的兼容性情况
README.md
是标准的我们应该首先阅读的文档文件。代码托管网站(GitHub、Bitbucket、Gitlab
等)会默认在仓库中展示该文件作为仓库的介绍。
怎么做?
在模块目录中添加 README.md 文件:
range-slider/
├── range-slider.vue
├── range-slider.less
└── README.md
在 README 文件中说明模块的功能以及使用场景。对于 vue 组件来说,比较有用的描述是组件的自定义属性即 API 的描述介绍。
提供组件 demo
添加 index.html 文件作为组件的 demo 示例,并提供不同配置情况的效果,说明组件是如何使用的。
为什么?
demo 可以说明组件是独立可使用的。
demo 可以让开发者预览组件的功能效果。
demo 可以展示组件各种配置参数下的功能。
对组件文件进行代码校验
代码校验可以保持代码的统一性以及追踪语法错误。.vue 文件可以通过使用 eslint-plugin-html插件来校验代码。你可以通过 vue-cli 来开始你的项目,vue-cli 默认会开启代码校验功能。
为什么?
保证所有的开发者使用同样的编码规范。
更早的感知到语法错误。
怎么做?
为了校验工具能够校验 *.vue文件,你需要将代码编写在
ESLint
ESLint 需要通过 ESLint HTML 插件来抽取组件中的代码。
通过 .eslintrc 文件来配置 ESlint,这样 IDE 可以更好的理解校验配置项:
{
"extends": "eslint:recommended",
"plugins": ["html"],
"env": {
"browser": true
},
"globals": {
"opts": true,
"vue": true
}
}
运行 ESLint
eslint src/**/*.vue
JSHint
JSHint 可以解析 HTML(使用 --extra-ext命令参数)和抽取代码(使用 --extract=auto命令参数)。
通过 .jshintrc 文件来配置 ESlint,这样 IDE 可以更好的理解校验配置项。
{
"browser": true,
"predef": ["opts", "vue"]
}
运行 JSHint
jshint --config modules/.jshintrc --extra-ext=html --extract=auto modules/
注:JSHint 不接受 vue 扩展名的文件,只支持 html。
只在需要时创建组件
为什么?
Vue.js 是一个基于组件的框架。如果你不知道何时创建组件可能会导致以下问题:
- 如果组件太大, 可能很难重用和维护;
- 如果组件太小,你的项目就会(因为深层次的嵌套而)被淹没,也更难使组件间通信;
怎么做?
- 始终记住为你的项目需求构建你的组件,但是你也应该尝试想到它们能够从中脱颖而出(独立于项目之外)。如果它们能够在你项目之外工作,就像一个库那样,就会使得它们更加健壮和一致。
- 尽可能早地构建你的组件总是更好的,因为这样使得你可以在一个已经存在和稳定的组件上构建你的组件间通信(props & events)。
规则
- 首先,尽可能早地尝试构建出诸如模态框、提示框、工具条、菜单、头部等这些明显的(通用型)组件。总之,你知道的这些组件以后一定会在当前页面或者是全局范围内需要。
- 第二,在每一个新的开发项目中,对于一整个页面或者其中的一部分,在进行开发前先尝试思考一下。如果你认为它有一部分应该是一个组件,那么就创建它吧。
- 最后,如果你不确定,那就不要。避免那些“以后可能会有用”的组件污染你的项目。它们可能会永远的只是(静静地)待在那里,这一点也不聪明。注意,一旦你意识到应该这么做,最好是就把它打破,以避免与项目的其他部分构成兼容性和复杂性。
Vue 组件规范
<!-- iview 等第三方公共组件,推荐大写开头 -->
<Button> from the top</Button>
<Row>
<Col span="24">
</Col>
</Row>
/** * 公共组件 项目内,自己开发的 推荐p开头 * import pLinkpage from 'public/module/linkage' */
<p-linkage v-model="form.pcarea"></p-linkage>
/** * 非公共组件 项目内,自己开发的推荐v开头 * import vSearch from './search' */
<v-search @search="params = $event"></v-search>
自闭合组件
在单文件组件、字符串模板和 JSX 中没有内容的组件应该是自闭合的——但在 DOM 模板里永远不要这样做。
自闭合组件表示它们不仅没有内容,而且刻意没有内容。其不同之处就好像书上的一页白纸对比贴有“本页有意留白”标签的白纸。而且没有了额外的闭合标签,你的代码也更简洁。
不幸的是,HTML 并不支持自闭合的自定义元素——只有官方的“空”元素。所以上述策略仅适用于进入 DOM 之前 Vue 的模板编译器能够触达的地方,然后再产出符合 DOM 规范的 HTML。
// 反例
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent></MyComponent>
<!-- 在 DOM 模板中 -->
<my-component/>
// 好例子
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent/>
<!-- 在 DOM 模板中 -->
<my-component></my-component>
作者:_Battle
链接:https://juejin.cn/post/7023549490372182052/
收起阅读 »
Vue 开发规范(中)
上一篇:https://www.imgeek.org/article/825358938
将 this 赋值给 component 变量
在 Vue.js 组件上下文中,this指向了组件实例。因此当你切换到了不同的上下文时,要确保 this 指向一个可用的 component 变量。
换句话说,如果你正在使用 ES6 的话,就不要再编写 var self = this; 这样的代码了,您可以安全地使用 Vue 组件。
为什么?
- 使用 ES6,就不再需要将 this 保存到一个变量中了。
- 一般来说,当你使用箭头函数时,会保留 this 的作用域。(译者注:箭头函数没有它自己的 this 值,箭头函数内的 this 值继承自外围作用域。)
- 如果你没有使用 ES6,当然也就不会使用 箭头函数 啦,那你必须将 “this” 保存到到某个变量中。这是唯一的例外。
怎么做?
<script type="text/javascript">
export default { methods: { hello() { return 'hello'; }, printHello() { console.log(this.hello()); }, }, };
</script>
<!-- 避免 -->
<script type="text/javascript">
export default { methods: { hello() { return 'hello'; }, printHello() { const self = this; // 没有必要 console.log(self.hello()); }, }, };
</script>
组件结构化
按照一定的结构组织,使得组件便于理解。
为什么?
- 导出一个清晰、组织有序的组件,使得代码易于阅读和理解。同时也便于标准化。
- 按首字母排序 properties、data、computed、watches 和 methods 使得这些对象内的属性便于查找。
- 合理组织,使得组件易于阅读。(name; extends; props, data 和 computed; components; watch 和 methods; lifecycle methods 等)。
- 使用 name 属性。借助于 vue devtools 可以让你更方便的测试。
- 合理的 CSS 结构,如 BEM 或 rscss - 详情?。
- 使用单文件 .vue 文件格式来组件代码。
怎么做?
组件结构化
<template lang="html">
<div class="Ranger__Wrapper">
<!-- ... -->
</div>
</template>
<script type="text/javascript">
export default {
// 不要忘记了 name 属性 name: 'RangeSlider',
// 组合其它组件 extends: {},
// 组件属性、变量 props: { bar: {},
// 按字母顺序 foo: {}, fooBar: {}, },
// 变量 data() {}, computed: {},
// 使用其它组件 components: {},
// 方法 watch: {}, methods: {},
// 生命周期函数 beforeCreate() {}, mounted() {}, };
</script>
<style scoped> .Ranger__Wrapper { /* ... */ } </style>
组件事件命名
Vue.js 提供的处理函数和表达式都是绑定在 ViewModel 上的,组件的每一个事件都应该按照一个好的命名规范来,这样可以避免不少的开发问题,具体可见如下 为什么。
为什么?
- 开发者可以随意给事件命名,即使是原生事件的名字,这样会带来迷惑性。
- 过于宽松的事件命名可能与 DOM 模板不兼容。
怎么做?
- 事件名也使用连字符命名。
- 一个事件的名字对应组件外的一组意义操作,如:upload-success、upload-error 以及 dropzone-upload-success、dropzone-upload-error (如果需要前缀的话)。
- 事件命名应该以动词(如 client-api-load) 或是 形容词(如 drive-upload-success)结尾。(出处)
避免 this.$parent
Vue.js 支持组件嵌套,并且子组件可访问父组件的上下文。访问组件之外的上下文违反了基于模块开发的第一原则。因此你应该尽量避免使用 this.$parent。
为什么?
- 组件必须相互保持独立,Vue 组件也是。如果组件需要访问其父层的上下文就违反了该原则。
- 如果一个组件需要访问其父组件的上下文,那么该组件将不能在其它上下文中复用。
怎么做?
- 通过
props
将值传递给子组件。 - 通过
props
传递回调函数给子组件来达到调用父组件方法的目的。 - 通过在子组件触发事件来通知父组件。
谨慎使用 this.$refs
Vue.js
支持通过 ref
属性来访问其它组件和 HTML
元素。并通过 this.$refs
可以得到组件或 HTML
元素的上下文。在大多数情况下,通过 this.$refs
来访问其它组件的上下文是可以避免的。在使用的的时候你需要注意避免调用了不恰当的组件 API
,所以应该尽量避免使用 this.$refs
。
为什么?
- 组件必须是保持独立的,如果一个组件的
API
不能够提供所需的功能,那么这个组件在设计、实现上是有问题的。 - 组件的属性和事件必须足够的给大多数的组件使用。
怎么做?
- 提供良好的组件
API
。 - 总是关注于组件本身的目的。
- 拒绝定制代码。如果你在一个通用的组件内部编写特定需求的代码,那么代表这个组件的
API
不够通用,或者你可能需要一个新的组件来应对该需求。 - 检查所有的
props
是否有缺失的,如果有提一个issue
或是完善这个组件。 - 检查所有的事件。子组件向父组件通信一般是通过事件来实现的,但是大多数的开发者更多的关注于
props
从忽视了这点。 - Props向下传递,事件向上传递!。以此为目标升级你的组件,提供良好的 API 和 独立性。
- 当遇到
props
和events
难以实现的功能时,通过this.$refs
来实现。 - 当需要操作
DOM
无法通过指令来做的时候可使用this.$ref
而不是JQuery、document.getElement*、document.queryElement
。 - 基础使用准则是,能不用
ParseError: KaTeX parse error: Expected 'EOF', got '就' at position 5: refs就̲
尽量不用,如果用,尽量不要通过refs
操作状态,可以通过$refs
调用methods
。
<!-- 推荐,并未使用 this.$refs -->
<range :max="max" :min="min" @current-value="currentValue" :step="1"></range>
<!-- 使用 this.$refs 的适用情况-->
<modal ref="basicModal">
<h4>Basic Modal</h4>
<button class="primary" @click="$refs.basicModal.hide()">Close</button>
</modal>
<button @click="$refs.basicModal.open()">Open modal</button>
<!-- Modal component -->
<template>
<div v-show="active">
<!-- ... -->
</div>
</template>
<script>
export default { // ... data() { return { active: false, }; }, methods: { open() { this.active = true; }, hide() { this.active = false; }, }, // ... };
</script>
<!-- 这里是应该避免的 -->
<!-- 如果可通过 emited 来做则避免通过 this.$refs 直接访问 -->
<template>
<range :max="max" :min="min" ref="range" :step="1"></range>
</template>
<script>
export default { // ... methods: { getRangeCurrentValue() { return this.$refs.range.currentValue; }, }, // ... };
</script>
使用组件名作为样式作用域空间
Vue.js 的组件是自定义元素,这非常适合用来作为样式的根作用域空间。可以将组件名作为 CSS 类的命名空间。
为什么?
- 给样式加上作用域空间可以避免组件样式影响外部的样式。
- 保持模块名、目录名、样式根作用域名一样,可以很好的将其关联起来,便于开发者理解。
怎么做?
使用组件名作为样式命名的前缀,可基于 BEM 或 OOCSS 范式。同时给 style 标签加上 scoped 属性。加上 scoped 属性编译后会给组件的 class 自动加上唯一的前缀从而避免样式的冲突。
<style scoped> /* 推荐 */
.MyExample { }
.MyExample li { }
.MyExample__item { }
/* 避免 */
.My-Example { }
/* 没有用组件名或模块名限制作用域, 不符合 BEM 规范 */
</style>
作者:_Battle
链接:https://juejin.cn/post/7023548108214648863
收起阅读 »
Vue 开发规范(上)
基于模块开发
始终基于模块的方式来构建你的 app,每一个子模块只做一件事情。
Vue.js 的设计初衷就是帮助开发者更好的开发界面模块。一个模块是应用程序中独立的一个部分。
怎么做?
每一个 Vue 组件(等同于模块)首先必须专注于解决一个单一的问题,独立的、可复用的、微小的 和 可测试的。
如果你的组件做了太多的事或是变得臃肿,请将其拆分成更小的组件并保持单一的原则。一般来说,尽量保证每一个文件的代码行数不要超过 100 行。也请保证组件可独立的运行。比较好的做法是增加一个单独的 demo 示例。
Vue 组件命名
组件的命名需遵从以下原则:
- 有意义的: 不过于具体,也不过于抽象
- 简短: 2 到 3 个单词
- 具有可读性: 以便于沟通交流
同时还需要注意:
必须符合自定义元素规范: 使用连字符分隔单词,切勿使用保留字。
app- 前缀作为命名空间: 如果非常通用的话可使用一个单词来命名,这样可以方便于其它项目里复用。
为什么?
组件是通过组件名来调用的。所以组件名必须简短、富有含义并且具有可读性。
如何做?
<!-- 推荐 -->
<app-header></app-header>
<user-list></user-list>
<range-slider></range-slider>
<!-- 避免 -->
<btn-group></btn-group> <!-- 虽然简短但是可读性差. 使用 `button-group` 替代 -->
<ui-slider></ui-slider> <!-- ui 前缀太过于宽泛,在这里意义不明确 -->
<slider></slider> <!-- 与自定义元素规范不兼容 -->
组件表达式简单化
Vue.js 的表达式是 100% 的 Javascript 表达式。这使得其功能性很强大,但也带来潜在的复杂性。因此,你应该尽量保持表达式的简单化。
为什么?
复杂的行内表达式难以阅读。
行内表达式是不能够通用的,这可能会导致重复编码的问题。
IDE 基本上不能识别行内表达式语法,所以使用行内表达式 IDE 不能提供自动补全和语法校验功能。
怎么做?
如果你发现写了太多复杂并难以阅读的行内表达式,那么可以使用 method 或是 computed 属性来替代其功能。
<!-- 推荐 -->
<template>
<h1>
{{ `${year}-${month}` }}
</h1>
</template>
<script type="text/javascript"> export default { computed: { month() { return this.twoDigits((new Date()).getUTCMonth() + 1); }, year() { return (new Date()).getUTCFullYear(); } }, methods: { twoDigits(num) { return ('0' + num).slice(-2); } }, }; </script>
<!-- 避免 -->
<template>
<h1>
{{ `${(new Date()).getUTCFullYear()}-${('0' + ((new Date()).getUTCMonth()+1)).slice(-2)}` }}
</h1>
</template>
组件 props 原子化
虽然 Vue.js 支持传递复杂的 JavaScript 对象通过 props 属性,但是你应该尽可能的使用原始类型的数据。尽量只使用 JavaScript 原始类型(字符串、数字、布尔值)和函数。尽量避免复杂的对象。
为什么?
- 使得组件 API 清晰直观。
- 只使用原始类型和函数作为 props 使得组件的 API 更接近于 HTML(5) 原生元素。
- 其它开发者更好的理解每一个 prop 的含义、作用。
- 传递过于复杂的对象使得我们不能够清楚的知道哪些属性或方法被自定义组件使用,这使得代码难以重构和维护。
怎么做?
组件的每一个属性单独使用一个 props,并且使用函数或是原始类型的值。
验证组件的 props
在 Vue.js 中,组件的 props 即 API,一个稳定并可预测的 API 会使得你的组件更容易被其他开发者使用。
组件 props 通过自定义标签的属性来传递。属性的值可以是 Vue.js 字符串(:attr="value" 或 v-bind:attr="value")或是不传。你需要保证组件的 props 能应对不同的情况。
为什么?
验证组件 props 可以保证你的组件永远是可用的(防御性编程)。即使其他开发者并未按照你预想的方法使用时也不会出错。
怎么做?
- 提供默认值。
- 使用 type 属性校验类型。
- 使用 props 之前先检查该 prop 是否存在。
<template>
<input type="range" v-model="value" :max="max" :min="min">
</template>
<script type="text/javascript">
export default {
props: {
max: { type: Number, // 这里添加了数字类型的校验 default() { return 10; }, },
min: { type: Number, default() { return 0; }, },
value: { type: Number, default() { return 4; }, },
},
};
</script>
作者:_Battle
链接:https://juejin.cn/post/7023188232368029710
收起阅读 »
带你理解scoped、>>>、/deep/、::v-deep的原理
前言
平时开发项目我们在使用第三方插件时,必须使用element-ui的某些组件需要修改样式时,老是需要加上/deep/深度选择器,以前只是知道这样用,但是还不清楚他的原理。还有平时每个组件的样式都会加上scoped,但是也不知道他具体的原理。今天我就带大家理解理解理解
1. Scoped CSS的原理
1.1 区别
先带大家看一下无设置Scoped
与设置Scoped
的区别在哪
无设置Scoped
<div class="login">登录</div>
<style>
.login {
width: 100px;
height: 100px
}
</style>
打包之后的结果是跟我们的代码一摸一样的,没有区别。
设置Scoped
<div class="login">登录</div>
<style scoped>
.login {
width: 100px;
height: 100px
}
</style>
打包之后的结果是跟我们的代码就有所区别了。如下:
<div data-v-257dda99b class="login">登录</div>
<style scoped>
.login[data-v-257dda99b] {
width: 100px;
height: 100px
}
</style>
我们通过上面的例子,不难发现多了一个data-v-hash属性,也就是说加了scoped,PostCSS给一个组件中的所有dom添加了一个独一无二的动态属性,然后,给CSS选择器额外添加一个对应的属性选择器来选择该组件中dom,这种做法使得样式只作用于含有该属性的dom——组件内部dom,可以使得组件之间的样式不互相污染。
1.2 原理
Vue的作用域样式 Scoped CSS 的实现思路如下:
- 为每个组件实例(注意:是组件的实例,不是组件类)生成一个能唯一标识组件实例的标识符,我称它为组件实例标识,简称实例标识,记作 InstanceID;
- 给组件模板中的每一个标签对应的Dom元素(组件标签对应的Dom元素是该组件的根元素)添加一个标签属性,格式为
data-v-实例标识
,示例:<div data-v-e0f690c0="" >
; - 给组件的作用域样式
<style scoped>
的每一个选择器的最后一个选择器单元增加一个属性选择器原选择器[data-v-实例标识]
,示例:假设原选择器为.cls #id > div
,则更改后的选择器为.cls #id > div[data-v-e0f690c0]
;
1.3 特点
将组件的样式的作用范围限制在了组件自身的标签,即:组件内部,包含子组件的根标签,但不包含子组件的除根标签之外的其它标签;所以 组件的css选择器也不能选择到子组件及后代组件的中的元素(子组件的根元素除外);
因为它给选择器的最后一个选择器单元增加了属性选择器
[data-v-实例标识]
,而该属性选择器只能选中当前组件模板中的标签;而对于子组件,只有根元素 即有 能代表子组件的标签属性data-v-子实例标识
,又有能代表当前组件(父组件)的 签属性data-v-父实例标识
,子组件的其它非根元素,仅有能代表子组件的标签属性data-v-子实例标识
;
如果递归组件有后代选择器,则该选择器会打破特性1中所说的子组件限制,从而选中递归子组件的中元素;
原因:假设递归组件A的作用域样式中有选择器有后代选择器
div p
,则在每次递归中都会为本次递归创建新的组件实例,同时也会为该实例生成对应的选择器div p[data-v-当前递归组件实例的实例标识]
,对于递归组件的除了第一个递归实例之外的所有递归实例来说,虽然div p[data-v-当前递归组件实例的实例标识]
不会选中子组件实例(递归子组件的实例)中的 p 元素(具体原因已在特性1中讲解),但是它会选中当前组件实例中所有的 p 元素,因为 父组件实例(递归父组件的实例)中有匹配的 div 元素;
2. >>>、/deep/、::v-deep深度选择器的原理
2.1 例子
实际开发中遇到的例子:当我们开发一个页面使用了子组件的时候,如果这时候需要改子组件的样式,但是又不影响其他页面使用这个子组件的样式的时候。比如:
父组件:Parent.vue
<template>
<div class="parent" id="app">
<h1>我是父组件</h1>
<div class="gby">
<p>我是一个段落</p>
</div>
<child></child>
</div>
</template>
<style scoped>
.parent {
background-color: green;
}
.gby p {
background-color: red;
}
// 把子组件的背景变成红色,原组件不变
.child .dyx p {
background-color: red;
}
</style>
子组件:Child.vue
<template>
<div class="child">
<h1>我是子组件</h1>
<div class="dyx">
<p>我是子组件的段落</p>
</div>
</div>
</template>
<style scoped>
.child .dyx p {
background-color: blue;
}
</style>
这时候我们就会发现没有效果。但是如果我们使用>>>
、/deep/
、::v-deep
三个深度选择器其中一个就能实现了。看代码:
<template>
<div class="parent" id="app">
<h1>我是父组件</h1>
<div class="gby">
<p>我是一个段落</p>
</div>
<child></child>
</div>
</template>
<style scoped>
.parent {
background-color: green;
}
.gby p {
background-color: red;
}
// 把子组件的背景变成红色,原组件不变
::v-deep .child .dyx p {
background-color: red;
}
</style>
2.2 原理
如果你希望 scoped 样式中的一个选择器能够选择到子组 或 后代组件中的元素,我们可以使用 深度作用选择器
,它有三种写法:
>>>
,示例:.gby div >>> #dyx p
/deep/
,示例:.gby div /deep/ #dyx p
或.gby div/deep/ #dyx p
::v-deep
,示例:.gby div::v-deep #dyx p
或.gby div::v-deep #dyx p
它的原理与 Scoped CSS 的原理基本一样,只是第3步有些不同(前2步一样),具体如下:
- 为每个组件实例(注意:是组件的实例,不是组件类)生成一个能唯一标识组件的标识符,我称它为实例标识,记作 InstanceID;
- 给组件模板中的每一个标签对应的Dom元素(组件标签对应的Dom元素是该组件的根元素)添加一个标签属性,格式为
data-v-实例标识
,示例:<div data-v-e0f690c0="" >
; - 给组件的作用域样式
<style scoped>
的每一个深度作用选择器前面的一个选择器单元增加一个属性选择器[data-v-实例标识]
,示例:假设原选择器为.cls #id >>> div
,则更改后的选择器为.cls #id[data-v-e0f690c0] div
;
因为Vue不会为深度作用选择器后面的选择器单元增加 属性选择器[data-v-实例标识]
,所以,后面的选择器单元能够选择到子组件及后代组件中的元素;
收起阅读 »
手摸手教你用webpack搭建TS开发环境
前言
最近在学习typescript,也就是我们常说的TS,它是JS的超集。具体介绍就不多说了,今天主要是带大家用webpack从零搭建一个TS开发环境。直接用传统的tsc xx.ts文件进行编译的话太繁琐,不利于我们开发,经过这次手动配置,我们也能知道vue3内部对TS的webpack进行了怎样的配置,废话不多说进入正题。
Node 编译TS
先讲讲如何运行ts文件吧,最传统的方式当然是直接输入命令
tsc xxx.ts
当然你必须得先安装过ts,如果没有请执行以下命令
npm install typescript -g
安装后查看下版本
tsc --version
这样我们就能得到编译后的js文件了,然后我们可以通过node指令
node xxx.js
进行查看,当然也可以新建一个HTML页面引入编译后的js文件
我们从上可以发现有点小复杂,那可不可以直接通过Node直接编译TS呢?接来下就是介绍这种方法
使用ts-node 就可以得到我们想要的效果
安装
npm install ts-node -g
另外ts-node需要依赖 tslib 和 @types/node 两个包,也需要下载
npm install tslib @types/node -g
现在,我们可以直接通过 ts-node 来运行TypeScript的代码
ts-node xxx.ts
如果遇到很多ts文件,那我们用这种方法也会觉得繁琐,所以我们最好是用webpack搭建一个支持TS开发环境,这样才是最好的解决方案。
webpack搭建准备工作
先新建一个文件夹
下载 webpack webpack-cli
npm install webpack webpack-cli -D
下载 ts tsloader(编译ts文件)
npm install typescript ts-loader -D
下载 webpack-dev-server(搭建本地服务器)
npm install webpack-dev-server -D
下载 html模板插件
npm install html-webpack-plugin -D
初始化webpack
npm init
初始化ts
tsc --init
新建配置文件 webpack.config.js
初始化后文件结构如下图所示,当然还有一些测试ts和html需要自己手动创建下
webpack 配置
配置之前我们先去package.json中添加两个运行和打包指令
webpack.config.js
代码中有详细说明哦
const path = require('path')//引入内置path方便得到绝对路径
const HtmlWebpackPlugin = require('html-webpack-plugin')//引入模板组件
module.exports = {
mode: 'development',//开发模式
entry: './src/main.ts',//入口文件地址
output: {
path: path.resolve(__dirname, "./dist"),//出口文件,即打包后的文件存放地址
filename: 'bundle.js' //文件名
},
devServer: {
},
resolve: {
extensions:['.ts', '.js', '.cjs', '.json'] //配置文件引入时省略后缀名
},
module: {
rules: [
{
test: /\.ts$/, //匹配规则 以ts结尾的文件
loader: 'ts-loader' //对应文件采用ts-loader进行编译
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html' //使用模板地址
})
]
}
配置完成我们可以进行测试了,执行指令
npm run serve
打包指令
npm run build
End
看完的话点个赞吧~~
收起阅读 »
用 JS 写算法时你应该知道的——数组不能当队列使用!!
在初学 JS 时,发现数组拥有 shift()
、unshift()
、pop()
、push()
这一系列方法,而不像 Java 或 CPP 中分别引用队列、栈等数据结构,还曾偷偷窃喜。现在想想,这都是以高昂的复杂度作为代价的QAQ。
举个例子 - BFS
一般队列的应用是在 BFS 题目中使用到。BFS(Breath First Search)广度优先搜索,作为入门算法,基本原理大家应该都了解,这里不再细说。
给你一个大小为
m x n
的整数矩阵isWater
,它代表了一个由 陆地 和 水域 单元格组成的地图。
如果
isWater[i][j] == 0
,格子(i, j)
是一个 陆地 格子。
如果isWater[i][j] == 1
,格子(i, j)
是一个 水域 格子。
你需要按照如下规则给每个单元格安排高度:
- 每个格子的高度都必须是非负的。
- 如果一个格子是是 水域 ,那么它的高度必须为
0
。
- 任意相邻的格子高度差 至多 为
1
。当两个格子在正东、南、西、北方向上相互紧挨着,就称它们为相邻的格子。(也就是说它们有一条公共边)
找到一种安排高度的方案,使得矩阵中的最高高度值 最大 。
请你返回一个大小为
m x n
的整数矩阵height
,其中height[i][j]
是格子(i, j)
的高度。如果有多种解法,请返回 任意一个 。
常规 BFS 题目,从所有的水域出发进行遍历,找到每个点离水域的最近距离即可。常规写法,三分钟搞定。
/**
* @param {number[][]} isWater
* @return {number[][]}
*/
var highestPeak = function(isWater) {
// 每个水域的高度都必须是0
// 一个格子离最近的水域的距离 就是它的最大高度
let n = isWater.length, m = isWater[0].length;
let height = new Array(n).fill().map(() => new Array(m).fill(-1));
let q = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < m; j++) {
if (isWater[i][j] === 1) {
q.push([i, j]);
height[i][j] = 0;
}
}
}
let dir = [[0, 1], [0, -1], [1, 0], [-1, 0]];
while (q.length) {
for (let i = q.length - 1; i >= 0; i--) {
let [x, y] = q.shift();
for (let [dx, dy] of dir) {
let nx = x + dx, ny = y + dy;
if (nx < n && nx >= 0 && ny < m && ny >= 0 && height[nx][ny] === -1) {
q.push([nx, ny]);
height[nx][ny] = height[x][y] + 1;
}
}
}
}
return height;
};
然后,超时了……
调整一下,
/**
* @param {number[][]} isWater
* @return {number[][]}
*/
var highestPeak = function(isWater) {
// 每个水域的高度都必须是0
// 一个格子离最近的水域的距离 就是它的最大高度
let n = isWater.length, m = isWater[0].length;
let height = new Array(n).fill().map(() => new Array(m).fill(-1));
let q = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < m; j++) {
if (isWater[i][j] === 1) {
q.push([i, j]);
height[i][j] = 0;
}
}
}
let dir = [[0, 1], [0, -1], [1, 0], [-1, 0]];
while (q.length) {
let tmp = [];
for (let i = q.length - 1; i >= 0; i--) {
let [x, y] = q[i];
for (let [dx, dy] of dir) {
let nx = x + dx, ny = y + dy;
if (nx < n && nx >= 0 && ny < m && ny >= 0 && height[nx][ny] === -1) {
tmp.push([nx, ny]);
height[nx][ny] = height[x][y] + 1;
}
}
}
q = tmp;
}
return height;
};
ok,这回过了,而且打败了 90% 的用户。
那么问题出在哪里呢?shift()!!!
探究 JavaScript 中 shift()
的实现
在学习 C++ 的时候,队列作为一个先入先出的数据结构,入队和出队肯定都是O(1)
的时间复杂度,用链表
让我们查看下 V8 中 shift() 的源码
简单实现就是
function shift(arr) {
let len = arr.length;
if (len === 0) {
return;
}
let first = arr[0];
for (let i = 0; i < len - 1; i++) {
arr[i] = arr[i + 1];
}
arr.length = len - 1;
return first;
}
所以,shift()
是 O(N)
的!!! 吐血 QAQ
同理,unshift()
也是 O(N)
的,不过,pop()
和 push()
是 O(1)
,也就是说把数组当做栈是没有问题的。
我就是想用队列怎么办!
没想到作为一个 JSer,想好好地用个队列都这么难……QAQ
找到了一个队列实现,详情见注释。
/*
Queue.js
A function to represent a queue
Created by Kate Morley - http://code.iamkate.com/ - and released under the terms
of the CC0 1.0 Universal legal code:
http://creativecommons.org/publicdomain/zero/1.0/legalcode
*/
/* Creates a new queue. A queue is a first-in-first-out (FIFO) data structure -
* items are added to the end of the queue and removed from the front.
*/
function Queue(){
// initialise the queue and offset
var queue = [];
var offset = 0;
// Returns the length of the queue.
this.getLength = function(){
return (queue.length - offset);
}
// Returns true if the queue is empty, and false otherwise.
this.isEmpty = function(){
return (queue.length == 0);
}
/* Enqueues the specified item. The parameter is:
*
* item - the item to enqueue
*/
this.enqueue = function(item){
queue.push(item);
}
/* Dequeues an item and returns it. If the queue is empty, the value
* 'undefined' is returned.
*/
this.dequeue = function(){
// if the queue is empty, return immediately
if (queue.length == 0) return undefined;
// store the item at the front of the queue
var item = queue[offset];
// increment the offset and remove the free space if necessary
if (++ offset * 2 >= queue.length){
queue = queue.slice(offset);
offset = 0;
}
// return the dequeued item
return item;
}
/* Returns the item at the front of the queue (without dequeuing it). If the
* queue is empty then undefined is returned.
*/
this.peek = function(){
return (queue.length > 0 ? queue[offset] : undefined);
}
}
把最初代码中的数组改为 Queue
,现在终于可以通过了。:)
收起阅读 »
如何“优雅”地修改 node_modules 下的代码?
在实际开发过程中当我们遇到 node_modules
中的 A 包有 bug 时候,通常开发者有几个选择:
方法一:给 A 包提 issue 等待他人修复并发布:做好石沉大海或修复周期很长的准备。
方法二:给 A 包提 mr 自行修复并等待发布:很棒,不过你最好祈祷作者发版积极,并且新版本向下兼容。
方法三:把 A 包的源码拖出来自己维护:有点暴力且事后维护成本较高,不过应急时也能勉强接受。
等等,可如果出问题的包是“幽灵依赖”呢,比如项目的依赖链是: A -> B -> C,此时 C 包有 bug。那么上面三个方法的改动需要同时影响到 A、B、C 三个包,修复周期可能就更长了,可是你今晚就要上线啊,这可怎么办?
上线要紧,直接手动修改 node_modules
下的代码给缺陷包打个临时补丁吧,可问题又来了,改动只能在本地生效,构建却在云端, 积极的同学开始写起了脚本,然后陷入一个个坑里...
上述场景下即可考虑使用 patch-package 这个包,假设我们现在的源码结构如下所示:
├── node_modules
│ └── lodash
│ └── toString.js
├── src
│ └── app.js // 依赖 lodash 的 toString 方法
└── package.json
node_modules/lodash/toString.js
var baseToString = require('./_baseToString')
function toString(value) {
return value == null ? '' : baseToString(value);
}
module.exports = toString;
src/app.js
const toString = require('lodash/toString')
console.log(toString(123));
假设现在需要修改 node_modules/lodash/toString.js
文件,只需要遵循以下几步即可“优雅”完成修改:
第一步:安装依赖
yarn add patch-package postinstall-postinstall -D
第二步:修改 node_modules/lodash/toString.js
文件
function toString(value) {
console.log('it works!!!'); // 这里插入一行代码
return value == null ? '' : baseToString(value);
}
module.exports = toString;
第三步:生成修改文件
npx patch-package lodash
这一步运行后会生成 patches/lodash+4.17.21.patch
,目录结构变成下面这样:
├── node_modules
│ └── lodash
│ └── toString.js
├── patches
│ └── lodash+4.17.21.patch
├── src
│ └── app.js
└── package.json
其中 .patch 文件内容如下:
diff --git a/node_modules/lodash/toString.js b/node_modules/lodash/toString.js
index daaf681..8308e76 100644
--- a/node_modules/lodash/toString.js
+++ b/node_modules/lodash/toString.js
@@ -22,6 +22,7 @@ var baseToString = require('./_baseToString');
* // => '1,2,3'
*/
function toString(value) {
+ console.log('it works!!!');
return value == null ? '' : baseToString(value);
}
第四步:修改 package.json
文件
"scripts": {
+ "postinstall": "patch-package"
}
最后重装一下依赖,测试最终效果:
rm -rf node_modules
yarn
node ./src/app.js
// it works!!!
// 123
可以看到,即便重装依赖,我们对 node_modules
下代码的修改还是被 patch-package
还原并最终生效。
至此我们便完成一次临时打补丁的操作,不过这并非真正优雅的长久之计,长期看还是需要彻底修复第三方包缺陷并逐步移除项目中的 .patch
文件。
作者:王力国
链接:https://juejin.cn/post/7022252841116893215
收起阅读 »
封装一个底部导航
前言
在我们日常项目开发中,我们在做移动端的时候会涉及到地步导航功能,所以封装了这个底部导航组件。
底部导航
BottomNav组件属性
1. value
选中值(即选中BottomNavPane的name值)
值为字符串类型
非必填默认为第一个BottomNavPane的name
2. lazy
未显示的内容面板是否延迟渲染
值为布尔类型
默认为false
样式要求
组件外面需要包裹可以相对定位的元素,增加样式:position: relative
BottomNavPane组件属性
1. name
英文名称
值为字符串类型
必填
2. icon
导航图标名称
值为字符串类型
值需要与src/assets/icon目录下svg文件的名称一致(name值不含“.svg”后缀)
必填
3. label
导航图标下面显示的文字
值为字符串类型
必填
4. scroll
是否有滚动条
值为布尔类型
默认值为:true
示例
<template>
<div class="bottom-nav-wrap">
<BottomNav v-model="curNav" :lazy="true">
<BottomNavPane name="home" label="首页" icon="home">
<h1>首页内容</h1>
</BottomNavPane>
<BottomNavPane name="oa" label="办公" icon="logo">
<h1>办公内容</h1>
</BottomNavPane>
<BottomNavPane name="page2" label="我的" icon="user">
<h1>个人中心</h1>
</BottomNavPane>
</BottomNav>
</div>
</template>
<script>
import { BottomNav, BottomNavPane } from '@/components/m/bottomNav'
export default {
name: 'BottomNavDemo',
components: {
BottomNav,
BottomNavPane
},
data () {
return {
curNav: ''
}
}
}
</script>
<style lang="scss" scoped>
.bottom-nav-wrap {
position: absolute;
top: $app-title-bar-height;
bottom: 0;
left: 0;
right: 0;
}
</style>
BottomNav.vue
<template>
<div class="bottom-nav">
<div class="nav-pane-wrap">
<slot></slot>
</div>
<div class="nav-list">
<div class="nav-item"
v-for="info in navInfos"
:key="info.name"
:class="{active: info.name === curValue}"
@click="handleClickNav(info.name)">
<Icon class="nav-icon" :name="info.icon"></Icon>
<span class="nav-label">{{info.label}}</span>
</div>
</div>
</div>
</template>
<script>
import { generateUUID } from '@/assets/js/utils.js'
export default {
name: 'BottomNav',
props: {
// 选中导航值(导航的英文名)
value: String,
// 未显示的内容面板是否延迟渲染
lazy: {
type: Boolean,
default: false
}
},
data () {
return {
// 组件实例的唯一ID
id: generateUUID(),
// 当前选中的导航值(导航的英文名)
curValue: this.value,
// 导航信息数组
navInfos: [],
// 导航面板vue实例数组
panes: []
}
},
watch: {
value (val) {
this.curValue = val
},
curValue (val) {
this.$eventBus.$emit('CHANGE_NAV' + this.id, val)
this.$emit('cahnge', val)
}
},
mounted () {
this.calcPaneInstances()
},
beforeDestroy () {
this.$eventBus.$off('CHANGE_NAV' + this.id)
},
methods: {
// 计算导航面板实例信息
calcPaneInstances () {
if (this.$slots.default) {
const paneSlots = this.$slots.default.filter(vnode => vnode.tag &&
vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'BottomNavPane')
const panes = paneSlots.map(({ componentInstance }) => componentInstance)
const navInfos = paneSlots.map(({ componentInstance }) => {
// console.log(componentInstance.name, componentInstance)
return {
name: componentInstance.name,
label: componentInstance.label,
icon: componentInstance.icon
}
})
this.navInfos = navInfos
this.panes = panes
if (!this.curValue) {
if (navInfos.length > 0) {
this.curValue = navInfos[0].name
}
} else {
this.$eventBus.$emit('CHANGE_NAV' + this.id, this.curValue)
}
}
},
// 导航点击事件处理方法
handleClickNav (val) {
this.curValue = val
}
}
}
</script>
<style lang="scss" scoped>
.bottom-nav {
display: flex;
flex-direction: column;
height: 100%;
.nav-pane-wrap {
flex: 1;
}
.nav-list {
flex: none;
display: flex;
height: 90px;
background-color: #FFF;
align-items: center;
border-top: 1px solid $base-border-color;
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
line-height: 1;
text-align: center;
color: #666;
.nav-icon {
font-size: 40px;/*yes*/
}
.nav-label {
margin-top: 6px;
font-size: 24px;/*yes*/
}
&.active {
position: relative;
color: $base-color;
}
}
}
}
</style>
BottomNavPane.vue
<template>
<div v-if="canInit" class="bottom-nav-pane" v-show="show">
<Scroll v-if="scroll">
<slot></slot>
</Scroll>
<slot v-else></slot>
</div>
</template>
<script>
import Scroll from '@/components/base/scroll'
export default {
name: 'BottomNavPane',
components: {
Scroll
},
props: {
// 页签英文名称
name: {
type: String,
required: true
},
// 页签显示的标签
label: {
type: String,
required: true
},
// 图标名称
icon: {
type: String,
required: true
},
// 是否有滚动条
scroll: {
type: Boolean,
default: true
}
},
data () {
return {
// 是否显示
show: false,
// 是否已经显示过
hasShowed: false
}
},
computed: {
canInit () {
return (!this.$parent.lazy) || (this.$parent.lazy && this.hasShowed)
}
},
created () {
this.$eventBus.$on('CHANGE_NAV' + this.$parent.id, val => {
if (val === this.name) {
this.show = true
this.hasShowed = true
} else {
this.show = false
}
})
}
}
</script>
<style lang="scss" scoped>
.bottom-nav-pane {
height: 100%;
position: relative;
}
</style>
/**
* 底部图标导航组件
*/
import BaseBottomNav from './BottomNav.vue'
import BaseBottomNavPane from './BottomNavPane.vue'
export const BottomNav = BaseBottomNav
export const BottomNavPane = BaseBottomNavPane
「欢迎在评论区讨论」
收起阅读 »
你知道为何跨域中会发送 options 请求?
同源策略
同源策略是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
简单说,当我们访问一个网站时,浏览器会对源地址的不同部分(协议://域名:端口)做检查。比如防止利用它源的存储信息(Cookies...)做不安全的用途。
跨域 CORS
但凡被浏览器识别为不同源,浏览器都会认为是跨域,默认是不允许的。
比如:试图在 http://127.0.0.1:4000 中,请求 http://127.0.0.1:3000 的资源会出现如下错误:
这也是前端 100% 在接口调试中会遇到的问题。
同源和跨域的判断规则
简单请求和复杂请求
相信都会在浏览器的 Network 中看到两个同样地址的请求,有没有想过这是为什么呢?这是因为在请求中,会分为 简单请求 和 复杂请求 。
简单请求:满足如下条件的,将不会触发跨域检查:
- 请求方法为:GET 、POST 、 HEAD
- 请求头:Accept、Accept-Language、Content-Language、Content-Type
其中 Content-Type 限定为 :text/plain、multipart/form-data、application/x-www-form-urlencoded
我们可以更改同源规则,看下如下示例:
http://127.0.0.1:4000/ 下,请求 http://127.0.0.1:3000 不同端口的地址
域名不同,这已经跨域了。但由于请求方法为 GET,符合 简单请求,请求将正常工作。
复杂请求:不满足简单请求的都为复杂请求。在发送请求前,会使用 options 方法发起一个 预检请求(Preflight) 到服务器,以获知服务器是否允许该实际请求。
模拟一个跨域请求:
// 端口不同,content-type 也非限定值
axios.post(
'http://127.0.0.1:3000/test/cors',
{},
{
headers: {
'content-type': 'application/json',
},
}
);
能看到在请求之前浏览器会事先发起一个 Preflight 预检请求:
这个 预检请求 的请求方法为 options,同时会包含 Access-Control-xxx 的请求头:
当然,此时服务端没有做跨域处理(示例使用 express 起的服务,预检请求默认响应 200),就会出现浏览器 CORS 的错误警告。
如何解决跨域
对于跨域,前端再熟悉不过,百度搜索能找到一堆解决方法,关键词不是 JSONP,或者添加些 Access-Control-XXX 响应头。
本篇将详细说下后一种方式,姑且称为:服务端解决方案。
为 options 添加响应头
以 express 举例,首先对 OPTIONS 方法的请求添加这些响应头,它将根据告诉浏览器根据这些属性进行跨域限制:
app.use(function (req, res, next) {
if (req.method == 'OPTIONS') {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'content-type');
res.status(200).end();
}
});
如果你不对 预检接口 做正确的设置,那么后续一切都是徒劳。
打个比方:如果 Access-Control-Allow-Methods 只设置了 POST,如果客户端请求方法为 PUT,那么最终会出现跨域异常,并会指出 PUT 没有在预检请求中的 Access-Control-Allow-Methods 出现:
所以,以后读懂跨域异常对于正确的添加服务端响应信息非常重要。另外:GET、POST、HEAD 属于简单请求的方法,所以即使不在 Access-Control-Allow-Methods 定义也不碍事(如果不对请指出)
正式的跨域请求
随后对我们代码发出的请求额外添加跨域响应头(这需要和前面的预检接口一致)
if (req.method == 'OPTIONS') {
//...
} else {
// http://127.0.0.1:3000/test/cors
res.setHeader('Access-Control-Allow-Origin', '*');
next();
}
最后能看到我们等请求正常请求到了:
对于跨域请求头的说明
上例出现了我们经常见到的三个:Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers。
参考 cors 库,另外还有其他用于预检请求的响应头:
下面将对上面这些头做个说明。
Access-Control-Allow-Origin
在 预检请求 和 正常请求 告知浏览器被允许的源。支持通配符“*”,但不支持以逗号“,”分割的多源填写方式。
如果尝试些多个域名,则会出现如下错误:
Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header contains multiple values 'aaa,bbb', but only one is allowed.
另外,也不建议 Access-Control-Allow-Origin 以通配符方式定义,这样会增加安全隐患,最好以请求方的 origin 来赋值。
const origin = req.headers.origin;
res.setHeader('Access-Control-Allow-Origin', origin || '*');
// 因为会随着客户端请求的 Origin 变化,所以标识 Vary,让浏览器不要缓存
res.setHeader('Vary', 'Origin');
Access-Control-Allow-Methods
被允许的 Http 方法,按照需要填写,支持多个,例如: GET , HEAD , PUT , PATCH , POST , DELETE 。
由于判断 简单请求 之一的 HTTP 方法默认为 GET , POST , HEAD ,所以这些即使不在 Access-Control-Allow-Methods 约定,浏览器也是支持的。
比如:如果服务端定义 PUT 方法,而客户端发送的方法为 DELETE,则会出现如下错误:
res.setHeader('Access-Control-Allow-Methods', 'PUT');
Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response.
Access-Control-Allow-Headers
在 预检接口 告知客户端允许的请求头。
像 简单请求 约定的请求头默认支持: Accept 、 Accept-Language 、 Content-Language 、 Content-Type (text/plain、multipart/form-data、application/x-www-form-urlencoded)
如果客户端的请求头不在定义范围内,则会报错:
Request header field abc is not allowed by Access-Control-Allow-Headers in preflight response.
需要将此头调整为:
res.setHeader('Access-Control-Allow-Headers', 'content-type, abc');
Access-Control-Max-Age
定义 预检接口 告知客户端允许的请求头可以缓存多久。
默认时间规则:
- 在 Firefox 中,上限是 24 小时 (即 86400 秒)。
- 在 Chromium v76 之前, 上限是 10 分钟(即 600 秒)。
- 从 Chromium v76 开始,上限是 2 小时(即 7200 秒)。
- Chromium 同时规定了一个默认值 5 秒。
- 如果值为 -1,表示禁用缓存,则每次请求前都需要使用 OPTIONS 预检请求。
比如设置为 5 秒后,客户端在第一次会发送 预检接口 后,5 秒内将不再发送 预检接口:
res.setHeader('Access-Control-Max-Age', '5');
Access-Control-Allow-Credentials
跨域的请求,默认浏览器不会将当前地址的 Cookies 信息传给服务器,以确保信息的安全性。如果有需要,服务端需要设置 Access-Control-Allow-Credentials 响应头,另外客户端也需要开启 withCredentials 配置。
// 客户端请求
axios.post(
'http://127.0.0.1:3000/test/cors',
{},
{
headers: {
'content-type': 'application/json',
abc: '123',
},
withCredentials: true,
}
);
// 所有请求
res.setHeader('Access-Control-Allow-Credentials', 'true');
需要注意的是,Access-Control-Allow-Origin 不能设置通配符“*”方式,会出现如下错误:
这个 Access-Control-Allow-Origin 必须是当前页面源的地址。
Access-Control-Expose-Headers
和 Access-Control-Allow-Credentials 类似,如果服务端有自定义设置的请求头,跨域的客户端请求在响应信息中是接收不到该请求头的。
// 服务端
res.setHeader('def', '123');
axios
.post(
'http://127.0.0.1:3000/test/cors',
{},
{
headers: {
'content-type': 'application/json',
abc: '123',
},
withCredentials: true,
}
)
.then((data) => {
console.log(data.headers.def); //undefined
});
需要在服务端设置 Access-Control-Expose-Headers 响应头,并标记哪些头是客户端能获取到的:
res.setHeader('Access-Control-Expose-Headers', 'def');
res.setHeader('def', '123');
Access-Control-Request-Headers
我试了半天没找到 Access-Control-Request-Headers 的使用示例,其实它是根据当前请求的头拼接得到的。
如果客户端的请求头为:
{
"content-type": "application/json",
"abc": "123",
"xyz": "123",
},
那么浏览器最后会在 预检接口 添加一个 Access-Control-Request-Headers 的头,其值为:abc,content-type,xyz。然后服务端再根据 Access-Control-Allow-Headers 告诉浏览器服务端的请求头支持说明,最后浏览器判断是否会有跨域错误。
另外,对于服务端也需要针对 Access-Control-Request-Headers 做 Vary 处理:
res.setHeader('Vary', 'Origin' + ', ' + req.headers['access-control-request-headers']);
如此,对于跨域及其怎么处理头信息会有个基本的概念。希望在遇到类似问题能有章法的解决,而非胡乱尝试。
作者:Eminoda
链接:https://juejin.cn/post/7021077647417409550
收起阅读 »
移动端常见问题汇总,拿来吧你!
1px适配方案
某些时候,设计人员希望 1px
在手机显示的就是1px
,这也是....闲的,但是我们也要满足他们的需求,
这时候我们可以利用缩放来达到目的
.border_1px:before{
content: '';
position: absolute;
top: 0;
height: 1px;
width: 100%;
background-color: #000;
transform-origin: 0% 0%;
}
@media only screen and (-webkit-min-device-pixel-ratio:2){
.border_1px:before{
transform: scaleY(0.5);
}
}
@media only screen and (-webkit-min-device-pixel-ratio:3){
.border_1px:before{
transform: scaleY(0.33);
}
}
设置一个专门的class
来处理1px
的问题,利用伪类给其添加
-webkit-min-device-pixel-ratio
获取像素比transform: scaleY(0.5)
垂直方向缩放,后面的数字是倍数
图片模糊问题
.avatar{
background-image: url(conardLi_1x.png);
}
@media only screen and (-webkit-min-device-pixel-ratio:2){
.avatar{
background-image: url(conardLi_2x.png);
}
}
@media only screen and (-webkit-min-device-pixel-ratio:3){
.avatar{
background-image: url(conardLi_3x.png);
}
}
根据不一样的像素比,准备不一样的图片,正常来说是1px图片像素
对应1px物理像素
,图片的显示就不会模糊啦,但是这样的情况不多,不是非常重要,特殊需求的图,我们不这么做。
滚动穿透问题
移动端的网站,我们是经常会有一些弹出框出现的,这样的弹出框,在上面滑动,会导致我们后面的整个页面发生移动,这个问题怎么解决呢??
body{
position:fixed;
width:100%;
}
给body添加position:fixed
就可以使滚动条失效,这里弹框的显示和隐藏,我们利用JS
进行控制,而且添加上position:fixed
的一瞬间,可以看到页面一下回到0,0
的位置,因为fixed
是根据可视区定位的。
键盘唤起
main{
padding: 2rem 0;
/* height: 2000px; */
position: absolute;
top: 60px;
bottom: 60px;
overflow-y: scroll;
width: 100%;
-webkit-overflow-scrolling: touch;
}
当底部根据页面进行fixed
定位的时候,键盘弹出一瞬间,fixed
会失效,变成类似absoult
,让main
的内容无滚动,就不会连带fixed
一起动了
并且为了保证如丝般顺滑:
-webkit-overflow-scrolling: touch;
移动端的神奇操作
IOS
下的一些设置 和 安卓下的一些设置
添加到主屏幕后的标题
<meta name="apple-mobile-web-app-title" content="标题">
添加到主屏后的APP图标
<link href="short_cut_114x114.png" rel="apple-touch-icon-precomposed">
- 一般我们只需要提供一个
114*114
的图标即可
启用webApp
全屏模式
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-touch-fullscreen" content="yes" />
apple-mobile-web-app-capable
删除默认的苹果工具栏和菜单栏,默认为no
apple-touch-fullscreen
全屏显示
移动端手机号码识别
<meta name="format-detection" content="telephone=no" />
safari
会对一些可能是手机号码的数字,进行识别,我们可以利用上面的方式,禁止识别
手动开启拨打电话功能
<a href="tel:13300000000">13300000000</a>
- 在手机上点击这个链接,可以直接拨打电话
手动开启短信功能
<a href="sms:13300000000">13300000000</a>
- 在手机上点击这个链接,可以跳转去短信页面,给该手机号发送消息
移动端邮箱识别
<meta name="format-detection" content="email=no" />
手动开启邮箱发送功能
<a href="mailto:854121000@qq.com">发送邮件</a>
- 调用邮箱发送功能
优先启用最新版本IE和chrome
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
移动端默认样式
移动端默认字体
- 数字 和 英文字体 可使用
Helvetica
字体,IOS 和 Android都有这个字体 - 手机系统都有自己默认的字体,直接使用默认的
body{
font-family:Helvetica;
}
- 数字 和 英文字体 可使用
字体大小
如果只是适配手机,可以使用px
IOS系统中,链接、按钮等点击会有灰色遮罩
a,button,input,textarea{-webkit-tap-highlight-color: rgba(0,0,0,0)}
去除圆角
button,input{
-webkit-appearance:none;
border-radius: 0;
}
禁止文本缩放
html{
-webkit-text-size-adjust: 100%;
}
你真的了解border-radius吗?
水平半径和垂直半径
现在很多人都不知道我们平常使用的圆角值是一种缩写,例如我们平常写的top圆角10px就是一种缩写:
border-top-left-radius:10px; 等同于 border-top-left-radius:10px 10px;
其中,第一个值表示水平半径,第二个值表示圆角垂直半径;
例如:
<style>
.talk-dialog {
position: relative;
background: deepskyblue;
width: 100px;
height: 100px;
margin: 0 auto;
border-top-left-radius: 30px 80px;
}
</style>
<div class="talk-dialog"></div>
那么border-radius的写法应该怎么去写呢??它的水平半径和垂直半径是通过 斜杠 区分。 例如:
border-radius: 30px / 40px;
表示四个角的圆角水平半径都是30px,垂直半径是40px;
border-radius斜杠前后都支持1-4个值,以下多个值得写法为:
border-radius:10px 20px / 5% 20% 3% 10%;(左上+右下,右上+左下, / 左上,右上,右下,左下)
重叠问题
难道你认为这就完了,border-radius你彻底搞懂了??其实不然!
我们来看看下面一个列子:
<!DOCTYPE html>
<html lang="en">
<head>
<style>
.talk-dialog {
position: relative;
background: red;
width: 100px;//重点关注
height: 100px;//重点关注
border-radius: 30px / 80px; //重点关注
margin: 50px auto;
}
.talk-dialog1 {
position: relative;
background: deepskyblue;
width: 100px;//重点关注
height: 100px;//重点关注
border-top-left-radius: 30px 80px; //重点关注
margin: 10px auto;
}
</style>
</head>
<body>
<div class="talk-dialog"></div>
<div class="talk-dialog1"></div>
</body>
</html>
我们的容器大小宽为100px,高为100px, 问大家一个问题!
border-radius: 30px / 80px; 与 border-top-left-radius: 30px 80px; 两个不同的容器的 top-left的圆角大小一样吗???
大家或许这样看不出来,我们修改为绝对布局,两个元素重叠在一起看看是否左上角可以完美重叠?
答案揭晓: 圆角效果是不一样的,因为我们容器的垂直高度为100px,我们border-radius:30px / 80px设置以后,我们元素的高度不足以放下两个半轴为80px(80+80=160)的椭圆,如果这种场景不做约束,曲线就会发生一定的重叠,因此css 规范对圆角曲线重叠问题做了额外的渲染设定,具体算法如下:
f=min(L宽度/S宽度,L高度/S高度),L为容器宽高,S为半径之和,
这里计算我们的例子:f=min(100/60,100/160)=0.625 , f的值小于1,则所有的圆角值都要乘以f
因此:border-radius: 30px / 80px;
左上角值等同于:
border-top-left-radius:18.75px 50px;
细节
- border-radius 不支持负值
- 圆角以外的区域不可点击
- border-radius没有继承性,因此父元素设置了border-radius,子元素依旧是直角效果,要想达到圆角效果,需要加overflow:hidden。(重要,工作中常用)
- border-radius 也支持transition过渡效果
高级用法案例:
代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title></title>
<link rel="icon" href="data:;base64,=" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" />
<meta name=" theme-color" content="#000000" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<style>
.radius {
width: 150px;
height: 150px;
border-radius: 70% 30% 30% 70% / 60% 40% 60% 40%;
object-fit: cover;
object-position: right;
}
.demo {
position: relative;
width: 150px;
height: 150px;
margin: 10px auto;
}
.radius-1 {
width: 150px;
height: 150px;
object-fit: cover;
object-position: right;
background: deepskyblue;
color: #fff;
font-size: 40px;
text-align: center;
line-height: 120px;
border-bottom-right-radius: 100%;
}
.talk {
padding: 10px;
background: deepskyblue;
border-radius: .5em;
color: #fff;
position: relative;
z-index: 0;
}
.talk::before {
content: "";
position: absolute;
width: 15px;
height: 10px;
color: deepskyblue;
border-top: 10px solid;
border-top-left-radius: 80%;
left: 0;
bottom: 0;
margin-left: -12px;
-ms-transform: skewX(-30deg) scaleY(1.3);
transform: skewX(-30deg) scaleY(1.3);
z-index: -1;
}
</style>
</head>
<body>
<div class="demo demo1">
<img class="radius" src="./1.jpg" />
</div>
<div class="demo demo2">
<div class="radius-1">1</div>
</div>
<div class="demo demo3">
<div class="talk">border-radius圆角效果实现。</div>
</div>
</body>
</html>
结语:
欢迎大家多提宝贵意见,一赞一回,如果本文让你get 到知识,请不要吝啬你的star!
收起阅读 »
写给vue转react的同志们(5)
写给vue转react的同志们(4)
我们知道 React 中使用高阶组件(下面简称HOC)来复用一些组件的逻辑。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。
组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
上面出自 React 官方文档。
那在 Vue 中 复用组件逻辑实际上比较简单,利用 Mixins 混入复用组件逻辑,当 Mixins 中的逻辑过多时(比如方法和属性),在项目当中使用时追述源代码会比较麻烦,因为他在混入后没有明确告诉你哪个方法是被复用的。
//mixins.js(Vue 2 举例)
export defalut {
data() {
return {
text: 'hello'
}
}
}
// a.vue
import mixins from './mixins.js'
export defalut {
mixins: [mixins]
computed: {
acitveText() {
return `来自mixins的数据:${this.text}`
}
}
}
复制代码
可以看到除了在开头引入并挂载混入,并没有看到this.text
是从哪里来的,混入虽然好用,但当逻辑复杂时,其阅读起来是有一定困难的。
那你想在 Vue 中强行使用像 React 那样的高阶组件呢?那当然可以。只是 Vue 官方不怎么推崇 HOC,且 Mixins 本身可以实现 HOC 相关功能。
简单举个例子:
// hoc.js
import Vue from 'Vue'
export default const HOC = (component, text) => {
return Vue.component('HOC', {
render(createElement) {
return createElement(component, {
on: { ...this.$listeners },
props: {
text: this.text
}
})
},
data() {
return {
text: text,
hocText: 'HOC'
}
},
mounted() {
// do something ...
console.log(this.text)
console.log(this.hocText)
}
})
}
使用高阶组件:
// user.vue
<template>
<userInfo/>
</template>
<script>
import HOC from './hoc.js'
// 引入某个组件
import xxx from './xxx'
const userInfo = HOC(xxx, 'hello')
export default {
name: 'user',
components: {
userInfo
}
}
</script>
是不是相比 Mixins 更加复杂一点了?在 Vue 中使用高阶组件所带来的收益相对于 Mixins 并没有质的变化。不过话又说回来,起初 React 也是使用 Mixins 来完成代码复用的,比如为了避免组件的非必要的重复渲染可以在组件中混入 PureRenderMixin
。
const PureRenderMixin = require('react-addons-pure-render-mixin')
const component = React.createClass({
mixins: [PureRenderMixin]
})
后来 React 使用shallowCompare
来 替代 PureRenderMixin
。
const shallowCompare = require('react-addons-shallow-compare')
const component = React.createClass({
shouldComponentUpdate: (nextProps, nextState) => {
return shallowCompare(nextProps, nextState)
}
})
这需要你自己在组件中实现 shouldComponentUpdate
方法,只不过这个方法具体的工作由 shallowCompare
帮你完成,我们只需调用即可。
再后来 React 为了避免总是要重复调用这段代码,React.PureComponent
应运而生,总之 React 在慢慢将 Mixins 脱离开来,这对他们的生态系统并不是特别的契合。当然每种方案都各有千秋,只是是否适合自己的框架。
那我们回归 HOC,在 React 中如何封装 HOC 呢?
实际上我在往期篇幅有提到过:
点击传送
但是我还是简单举个例子:
封装 HOC:
// hoc.js
export default const HOC = (WrappedComponent) => {
return Class newComponent extends WrappedComponent {
constructor(props) {
super(props)
// do something ...
this.state = {
text: 'hello'
}
}
componentDidMount() {
super.componentDidMount()
// do something ...
console.log('this.state.text')
}
render() {
// init render
return super.render()
}
}
}
使用 HOC:
// user.js
import HOC from './hoc.js'
class user extends React.Component {
// do something ...
}
export defalut HOC(user)
装饰器写法更为简洁:
import HOC from './hoc.js'
@HOC
class user extends React.Component {
// do something ...
}
export defalut user
可以看到无论 Vue 还是 React 亦或是 HOC 或 Mixins 他们都是为了解决组件逻辑复用应运而生的,具体使用哪一种方案还要看你的项目契合度等其他因素。
技术本身并无好坏,只是会随着时间推移被其他更适合的方案取代,技术迭代也是必然的,相信作为一个优秀的程序员也不会去讨论一个技术的好或坏,只有适合与否。
作者:饼干_
链接:https://juejin.cn/post/7020215941422137381
收起阅读 »
写给vue转react的同志们(4)
应各位老爷要求,这篇文章开始拥抱hooks
,本文将从vue3
与react 17.x(hooks)
对比来感受两大框架的同工异曲之处。
今天的主题:
vue3与react 定义与修改数据
vue3与react 计算属性
vue3与react 实现监听
vue3与react hooks 定义与修改数据
实际上两者都是偏hooks
的写法,这样的高灵活性的组合,相信大部分人还是觉得香的,无论是以前的vue options
或是react class
的写法都是比较臃肿且复用性较差的(相较于hooks
)。下面举个例子对比一下。
vue3
react
import { useState } from 'react';
function App() {
const [todos, setTodos] = useState({
age: 25,
sex: 'man'
})
const setObj = () => {
setTodos({
...todos,
age: todos.age + 1
})
}
return (
{todos.age}
{todos.sex}
);
}
通过比较上述代码可以看到vue3
和react hooks
基本写法是差不多的,只是vue
提倡template
写法,react
提倡jsx
写法,模板的写法并不影响你js
逻辑的使用,所以不论框架再怎么变化,js
也是我们前端的铁饭碗,请各位务必掌握好!
vue3与react 计算属性
计算属性这一块是为了不让我们在模板处写上太过复杂的运算,这是计算属性存在的意义。vue3
中提供了computed
方法,react hook
提供了useMemo
让我们实现计算属性(没有类写法中可以使用get
来实现计算属性具体可看往期文章)
vue3
react
import { useMemo, useState } from 'react'
function App() {
const [obj, setObj] = useState({
age: 25,
sex: 'man'
})
const people = useMemo(() => {
return `this people age is ${obj.age} and sex is ${obj.sex}`
}, [obj])
return (
age: {obj.age}
sex: {obj.sex}
info: {people}
)
}
可以看到对比两大框架的计算属性,除了模板书写略有不同其他基本神似,都是hooks
写法,通过框架内部暴露的某个方法去实现某个操作,这样一来追述和定位错误时也更加方便,hooks
大概率就是现代框架的趋势,它不仅让开发者的代码可以更加灵活的组合复用,数据和方法来源也更加容易定位清晰。
vue3与react 实现监听
在vue3
里watch
被暴露成一个方法通过传入对应监听的参数以及回调函数实现,react
中也有类似的功能useEffect
,实际上他和componentDidMount
、componentDidUpdate
和 componentWillUnmount
具有相同的用途,只不过被合并成了一个 API
。看例子:
vue3
import { ref, watch } from 'vue'
export default {
setup() {
const count = ref(0)
watch(count,
(val) => {
console.log(val)
},
{ immediate: true, deep: true }
)
function setCount() {
count.value ++
}
return {
count,
setCount
}
}
}
react
import { useState, useEffect } from 'react'
function App() {
const [count, setCount] = useState(0)
const setCount = () => {
setCount(count + 1)
}
useEffect(() => {
console.log(count)
})
return (
count: {count}
)
}
可以看到,vue3
整体趋势是往hooks
靠,不难看出来未来不论哪种框架大概率最终都会往hooks
靠,react hooks
无疑是给了我们巨大的启发,函数式编程会越来越普及,从远古时期的传统三大金刚html、css、script
就能产出一个页面到现在的组件化,一个js
即可是一个页面。
总结
函数式编程是趋势,但其实有很多老项目都是基于vue2.x
的options
写法或者react class
的写法还是居多,把这些项目迁移迭代到最新才是头疼的事情,当然选择适合现有项目的技术体系才是最重要的。
作者:饼干_
链接:https://juejin.cn/post/6991765115150270478
收起阅读 »
写给vue转react的同志们(3)
我们都知道vue上手比较容易是因为他的三标签写法以及对指令的封装,他更像一个做好的包子你直接吃。
相比react他的纯js写法,相对来说自由度更高,这也意味着很多东西你需要自己手动封装,所以对新手没那么友好,所以他更像面粉,但可以制作更多花样的食物。
今天的主题
react 计算属性
react ref
react 计算属性
我们知道vue中有提供computed让我们来实现计算属性,只要依赖改变就会发生变化,那么react中是没有提供的,这里我们需要自己手动实现计算属性。简单举例一下:
vue 计算属性
react 计算属性(类写法)
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
msg: 'hello react'
}
}
get react_computed() {
return this.state.msg
}
componentDidMount() {
setTimeout(() => {
this.setState({
msg: 'hello react change'
})
}, 2000)
}
render() {
return (
{ this.react_computed }
)
}
}
可以看到react中我们手动定义了get来让他获取msg的值,从而实现了计算属性,实际上vue中的computed也是基于get和set实现的,get中收集依赖,在set中派发更新。
react ref
vue中的ref使用起来也是非常简单在对应组件上标记即可获取组件的引用,那么react中呢?
react中当然也可以像vue一样使用,但官方并不推荐字符串的形式来使用ref,并且在react16.x后的版本移除了。
看一段大佬描述:
- 它要求 React 跟踪当前呈现的组件(因为它无法猜测this)。这让 React 变慢了一点。
- 它不像大多数人所期望的那样使用“渲染回调”模式(例如
),因为 ref 会因为DataGrid上述原因而被放置。 - 它不是可组合的,即如果一个库在传递的子组件上放置了一个引用,用户不能在它上面放置另一个引用。回调引用是完全可组合的。
举例:
vue ref
react ref
class App extends React.Component {
myRef = React.createRef()
constructor(props) {
super(props)
}
render() {
return (
// 正常使用
// 回调使用(可组合)
this['' + index]} />
// 调用api(react16.x)
)
}
}
vue中的ref我们不必多言,看看react的,官方更推荐第三种用法(react16.x),第二种用法在更新过程中会被执行两次,通过在外部定义箭头函数使用即可,但是大多情况都是无关紧要。第一种用法在react16.x后的版本被废弃了。
总结
都到这篇了,相信你转型react上手业务基本没问题了,后续将慢慢深入两大框架的对比,重点叙述react,vue辅之。
我是饼干,让我们一起成长。最后别忘记点赞关注收藏三连击🌟
作者:饼干_
链接:https://juejin.cn/post/6979061382415122462
收起阅读 »
写给vue转react的同志们(2)
下一篇
react中想实现类似vue中的插槽
首先,我个人感觉jsx的写法比模板写法要灵活些,虽然没有像vue那样有指令,这就是为啥vue会上手简单点,因为他就像教科书一样教你怎么使用,而react纯靠你手写表达式来实现。
如果你想实现类似插槽的功能,其实大部分UI框架也可以是你自己定义的组件,例如ant desgin的组件,他的某些属性是可以传jsx来实现类似插槽的功能的,比如:
import React from 'react'
import { Popover } from 'antd'
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
content: (
你好,这里是react插槽
)
}
}
render() {
const { content } = this.state
return (
悬浮
)
}
}
上面这样就可以实现类似插槽的功能,这点上确实是比vue灵活些,不需要在结构里在加入特定的插槽占位。
如果是vue的话就可能是这样:
大家可以自己写写demo去体会一下。
单向数据流与双向绑定
我们知道vue中通过发布订阅模式实现了响应式,把input
和change
封装成v-model
实现双向绑定,react则没有,需要你自己通过this.setState
去实现,这点上我还是比较喜欢v-model
能省不少事。
虽说单向数据流更清晰,但实际大部分人在开发中出现bug依旧要逐个去寻找某个属性值在哪些地方使用过,尤其是当表单项很多且校验多的时候,代码会比vue多不少,所以大家自行衡量,挑取合适自己或团队的技术栈才是最关键的,不要盲目追求所谓的新技术。
举个例子(简写):
react
import React from 'react'
import { Form, Input, Button } from 'antd'
const FormItem = Form.Item
class App extends React.Component {
constructor(props) {
super(props)
}
onChange(key, e) {
this.setState({
[key] : e
})
}
onClick = () => {
console.log('拿到了:',this.state.username)
}
render() {
return (
vue
(简写)
其实乍一看也差不了多少,vue
这种options
的写法其实会比较清晰一点,react
则需要你自己去划分功能区域。
css污染
vue
中可以使用scoped
来防止样式污染,react
没有,需要用到.module.css
,原理都是一样的,通过给类名添加一个唯一hash值来标识。
举个例子:
react(简写):
xxx.module.css
.xxx {
background-color: red;
}
xxx.css
.xxx {
background-color: blue;
}
xxx.jsx
import React from 'react'
import style form './xxx.module.css'
import './xxx.css'
class App extends React.Component {
render(){
return (
blue
red
)
}
}
vue
xxx.css
.xxx {
background-color: red;
}
xxx.vue
export default {
methods: {
click() {
this.$router.push('yyy')
}
}
}
yyy.vue
export default {
methods: {
click() {
this.$router.push('xxx')
}
}
}
上面只是简单举个例子,页面之间的样式污染主要是因为css默认是全局生效的,所以无论scoped也好或是module.css也好,都会解析成ast语法树,通过添加hash值生成唯一样式。
总结
无论vue也好,react也好不变的都是js,把js基础打牢才是硬道理。
作者:饼干_
链接:https://juejin.cn/post/6972099403213438984
收起阅读 »
写给vue转react的同志们(1)
学习一个框架最好的办法就是从业务做起。首先我们要弄清做业务需要什么知识点去支持
今天的主题:
react 是怎么样传输数据的
react 怎么封装组件
react 的生命周期
实际上vue熟练的同学们,我觉得转react还是比较好上手的,就是要适应他的纯js的写法以及jsx等,个人认为还是比较好接受的,其实基本上都一样,只要弄清楚数据怎么传输怎么处理,那剩下的jsx大家都会写了吧。
react 组件通讯
这里我们来跟vue对比一下。比如
在vue中父子组件传值(简写):
// 父组件
data: {
testText:'这是父值'
}
methods:{
receive(val) {
console.log('这是子值:', val)
}
}
<child :testText="testText" @childCallBack="receive"/>
// 子组件
props: {
testText: {
type: String,
default: ''
}
}
methods:{
handleOn(){
this.$emit('childCallBack', '我是子组件')
}
}
<template>
<div @click="handleOn">{{testText}}</div>
</template>
在react中父子组件传值:
// 父组件
export default class Father extends React.Component {
constructor(props) {
super(props)
this.state = {
testText: '这是父值'
}
receive = (val) => {
console.log('这是子值:', val)
}
render(){
return(
<div>
<Son childCallBack={this.receive} testText={testText}/>
</div>
)
}
}
}
// 子组件
export default class Son extends React.Component {
constructor(props) {
super(props)
}
render() {
const { testText } = this.props
return (
<div>
父组件传过来的testText:{testText}
<div onClick={this.receiveFather}>
点我从子传父
</div>
</div>
)
}
receiveFather = () => {
this.props.childCallBack('我是子组件')
}
}
可以看到react 和 vue 其实相差不大,都是通过props去进行父传子的通讯,然后通过一个事件把子组件的数据传给父组件。聪明的同学肯定注意到react里我用了箭头函数赋给了一个变量了。如果不这样写,this的指向是不确定的,也可以在标签上这样写this.receiveFather.bind(this)
,不过这样写的坏处就是对性能有影响,可以在constructor中一次绑定即可。但还是推荐箭头函数的写法。(封装组件其实跟这个八九不离十了,就不再叙述)
react 单向数据流
我们都知道vue里直接v-model 然后通过this.属性名
就可以访问和修改属性了,这是vue劫持了get和set做了依赖收集和派发更新,但是react里没有这种东西,你不能直接通过this.state.属性名
去修改值,需要通过this.setState({"属性名":"属性值"}, callback(回调函数))
,你在同一地方修改属性是没办法立刻拿到修改后的属性值,需要通过setState
的回调拿到。我还是比较喜欢vue的双向绑定(手动狗头)。
react 的生命周期
我们都知道vue的生命周期(create、mounted、update、destory),其实react也差不多,他们都是要把某个html的div替换并挂载渲染的。
列举比较常用的:
constructor()
constructor()中完成了React数据的初始化,它接受两个参数:props和context,当想在函数内部使用这两个参数时,需使用super()传入这两个参数。这个就当于定义初始数据,熟悉vue的同学你可以把他当成诸如data、methods等。
注意:只要使用了constructor()就必须写super(),否则会导致this指向错误。
componentWillMount()
componentWillMount()一般用的比较少,它更多的是在服务端渲染时使用。它代表的过程是组件已经经历了constructor()初始化数据后,但是还未渲染DOM时。这个相当于vue的created啦,vue中可以通过在这个阶段用$nextTick去操作dom(不建议),不知道react有没有类似的api呢?
componentDidMount()
组件第一次渲染完成,此时dom节点已经生成,可以在这里调用ajax请求,返回数据setState后组件会重新渲染,这个就相当于vue的mounted阶段啦。
componentWillUnmount ()
在此处完成组件的卸载和数据的销毁。这个相当于vue中的beforeDestory啦,clear你在组件中所有的setTimeout,setInterval,移除所有组建中的监听 removeEventListener。
componentWillUpdate (nextProps,nextState)
组件进入重新渲染的流程,这里可以拿到改变后的数据值(相当于vue中updated)。
componentDidUpdate(prevProps,prevState)
组件更新完毕后,react只会在第一次初始化成功会进入componentDidmount,之后每次重新渲染后都会进入这个生命周期,这里可以拿到prevProps和prevState,即更新前的props和state,(相当于vue中的beforeupdated)。
render()
render函数会插入jsx生成的dom结构,react会生成一份虚拟dom树,在每一次组件更新时,在此react会通过其diff算法比较更新前后的新旧DOM树,比较以后,找到最小的有差异的DOM节点,并重新渲染。这里就是你写页面的地方。
总结
小细节
react 中使用组件第一个字母需大写
react 万物皆可 props
mobx 很香🐶
react中没有指令(如v-if、v-for等)需自己写三目运算符或so on~
收起阅读 »
总结一下,从vue转react还是比较好上手的(react中还有函数式写法我没有说,感兴趣可以看看),个人认为弄清楚数据通讯以及生命周期对应的钩子使用场景等,其实基本就差不多啦。但是还有很多细节需要同学们在实际业务中去发现和解决。react只是框架,大家js基础还是要打好的。祝各位工作顺利,准时发薪。🐶
手把手教你利用XSS攻击
前两天我收到安全部门的一个通知:高风险XSS攻击漏洞。
我们部门首先确定风险来源,并给出了解决方案。前端部分由我解决,并紧急修复上线。
一:那么什么是XSS攻击呢?
人们经常将跨站脚本攻击(Cross Site Scripting)缩写为CSS,但这会与层叠样式表(Cascading Style Sheets,CSS)的缩写混淆。因此,有人将跨站脚本攻击缩写为XSS。恶意攻击者往Web页面里插入恶意html代码,当用户浏览该页之时,嵌入其中Web里面的html代码会被执行,从而达到恶意用户的特殊目的。主要指的自己构造XSS跨站漏洞网页或者寻找非目标机以外的有跨站漏洞的网页。XSS是web安全最为常见的攻击方式,在近年来,常居web安全漏洞榜首。
光看这个定义,很多同学一定不理解是什么意思,下面我会模拟XSS攻击,同学们应该就知道怎么回事了。
在模拟XSS攻击之前,我们先来看看XSS攻击的分类。
二:XSS攻击有几种类型呢?
①反射型XSS攻击(非持久性XSS攻击)
②存储型XSS攻击(持久型XSS攻击)
③DOM-based型XSS攻击
三:接下来我们将模拟这几种XSS攻击
第一种:反射型XSS攻击(非持久性XSS攻击)
反射型XSS攻击一般是攻击者通过特定手法,诱使用户去访问一个包含恶意代码的URL,当受害者点击这些专门设计的链接的时候,恶意代码会直接在受害者主机上的浏览器执行。此类XSS攻击通常出现在网站的搜索栏、用户登录口等地方,常用来窃取客户端Cookies或进行钓鱼欺骗。
下面我们来看一个例子:
这是一个普通的点击事件,当用户点击之后,就执行了js脚本,弹窗了警告。
你会说,这能代表啥,那如果这段脚本是这样的呢?
当浏览器执行这段脚本,就盗用了用户的cookie信息,发送到了自己指定的服务器。你想想他接下来会干什么呢?
第二种:存储型XSS攻击(持久型XSS攻击)
攻击者事先将恶意代码上传或者储存到漏洞服务器中,只要受害者浏览包含此恶意代码的页面就会执行恶意代码。这意味着只要访问了这个页面的访客,都有可能会执行这段恶意脚本,因此存储型XSS攻击的危害会更大。此类攻击一般出现在网站留言、评论、博客日志等交互处,恶意脚本存储到客户端或者服务端的数据库中。
增删改查在web管理系统中中很常见,我们找到一个新增功能页面,这以一个富文本输入框为例,输入以下语句,点击保存,再去查看详情,你觉得会发生什么?
没错,如果是前端的同学或许已经猜到了,h是浏览器的标签,这样传给服务器,服务器再返回给前端,浏览器渲染的时候,会把第二行当成h1标签来渲染,就会出现以下效果,第二行文字被加粗加大了。
这里我只是输入了普通的文本,而近几年随着互联网的发展,出现了很多h5多媒体标签,那要是我利用它们呢?
不清楚的同学,可自行打开W3cschool网站查看:
黑客是怎么攻击我们的呢?黑客会自己写一些脚本,来获取我们的cookies敏感等信息,然后他发送到他自己的服务器,当他拿到我们这些信息后,就能绕过前端,直接调后端的接口,比如提现接口,想想是不是很恐怖!!!
这里我利用一个在线远程网站来模拟XSS攻击。地址如下:
svg.digi.ninja/xss.svg**
目前网站还能访问,同学们可以自己体验一下,如果后期链接失效不可访问了,同学们可以重新找一个,或者自己手写一个脚本,然后伪装成svg上传到自己的服务器。
我们在地址栏输入上面这个地址,来看看实际效果,提示你已经触发了XSS攻击。
当我们点击确定,出现了一个黑人,哈哈哈,恭喜你,你银行卡里的钱已经全被黑客取走了。这就是黑客得逞后的样子,他得逞后还在嘲讽你。
接下来,我们利用多媒体标签和这个脚本来攻击我们实际的的网站。
这里记得在地址前面加上//表示跨越,如图:
当我们点击保存之后,再去查看详情页面发现。
哦豁,刚刚那个网站的场景在我们的web管理系统里面触发了,点击确定,那个小黑人又来嘲讽你了。
这脚本在我们的管理系统成功运行,并获取了我们的敏感信息,就可以直接绕过前端,去直接掉我们后端银行卡提现接口了。并且这类脚本由于保存在服务器中,并存着一些公共区域,网站留言、评论、博客日志等交互处,因此存储型XSS攻击的危害会更大。
第三种:DOM-based型XSS攻击
客户端的脚本程序可以动态地检查和修改页面内容,而不依赖于服务器端的数据。例如客户端如从URL中提取数据并在本地执行,如果用户在客户端输入的数据包含了恶意的JavaScript脚本,而这些脚本没有经过适当的过滤或者消毒,那么应用程序就可能受到DOM-based型XSS攻击。
下面我们来看一个例子
这段代码的意思是点击提交之后,将输入框中的内容渲染到页面。效果如下面两张图。
①在输入框中输入内容
②点击确定,输入框中的内容渲染到页面
那如何我们输内容是不是普通文本,而是恶意的脚本呢?
没错,恶意的脚本在渲染到页面的时候,没有被当成普通的文本,而是被当成脚本执行了。
总结:XSS就是利用浏览器不能识别是普通的文本还是恶意代码,那么我们要做的就是阻止恶意代码执行,比如前端的提交和渲染,后端接口的请求和返回都要对此类特殊标签做转义和过滤处理,防止他执行脚本,泄露敏感的数据。感兴趣的同学可以根据我上面的步骤,自己去模拟一个XSS攻击,让自己也体验一次当黑客的感觉。
收起阅读 »
产品经理又开始为难我了???我。。。。
最近做项目的时候,就是产品经理给的图总是很大,不压缩。每天要处理这些图片真的很累哇。于是一怒之下写下了这个**「vscode 插件」。「插件核心功能是压缩,然后上传图片」。 压缩的网站其实就是「tinypng」** 这个网站然后图片压缩后,然后再上传到cdn上,然后然后这个压缩过的url 直接放到我们的粘贴板上。下面跟着我的步伐一步一步来写实现它。 先看效果图:
演示gif 图
效率对比
开发这个主要是提高团队开发效率, 绝不是为了炫技。 看图:
image-20211017224316386
需求分析
- 可在vscodde的setting中配置上传所需的参数,可以根据个人的需求单独进行配置;
- 2.在开发过程中可在编辑器中直接选择图片并上传到阿里云将图片链接填写到光标位置;
中文文档
❝
一个好的文档可以帮助我们更容易的开发:如果英文比较好的同学可以直接看Vscode英文文档,这里api会比较全,可以找到更简洁的方案实现功能; 不过我的话,还是花很久时间找了这篇比较全的中文文档
❞
搭建项目
vscode 插件的开发需要全局安装脚手架:
npm install -g yo generator-code
安装成功后,直接使用对应命令 「yo code」 来生成一个插件工程:
vscode开始这个页面
这就开始脚手架页面了,可以选择自己习惯的配置。输入对应的配置 然后 就创建了对应的项目了。
我们看下项目结构:
插件结构
插件运行
这时候我们先要去测试下我的这个插件到底是不是能够成功运行。在项目根目录按住F5 然后运行 「vscode extension」 ,这时候会出现一个新的vscode 窗口,但是我这里遇到的一个问题就是这个:
插件
我大概理解了下就是vscode 插件的依赖版本比较低:
目前是:
插件
这上面说的很清楚 vscode扩展指定 与其兼容的 vscode 版本兼容 很显然我这里太高了, 给他降级。然后给他换成1.60.2 完美解决
插件运行——成功演示
ok, 怎么查看自己查看插件有没有成功运行呢, 分为3步
- F5 开始调试 —— 产生一个新的调试窗口
- 在新的窗口—— command + shift + P 找到 hello word
- 点击运行看见弹窗 显示 表示弹窗运行成功
直接看下面的gif 图吧:
gif 演示
插件开发——配置参数
配置插件的属性面板, 这个主要是要在package.json 配置一些参数
配置参数
第一个参数我们稍后再讲其实就是对应你注册的自定义command, 下面的配置 其实就是对应插件属性面板一些参数,然后你可以通过vscode 的一些api 可以获得你配置的这些参数
下面我是我配置的参数,你可以会根据插件自定义去调整
"properties": {
"upload_image.domain": {
"type": "string",
"default": "",
"description": "设置上传域名"
},
"upload_image.accessKey": {
"type": "string",
"default": "",
"description": "设置oss上传accessKey"
},
"upload_image.secretKey": {
"type": "string",
"default": "",
"description": "设置oss上传secretKey"
},
"upload_image.scope": {
"type": "string",
"default": "",
"description": "设置oss上传上传空间"
},
"upload_image.gzip": {
"type": "boolean",
"default": "true",
"description": "是否启用图片压缩"
}
}
大概就是这几个参数, 然后我们测试下同样打开f5 然后在新窗口 找到设置然后找到扩展, 设置项其实就是对应我们的 上面的**「title」**
压缩图片。
我们看下效果:
效果
插件开发——配置右键菜单
这个功能描述大概就是,你在写的时候突然要上传,直接点击鼠标右键,然后直接选择图片。 对就是这个简单的东西,做东西需要从用户的角度考虑,一定要爽,能省一步是一步。呵呵哈哈哈
这个配置其实就是在 还是在刚才的**「package.json」** 上继续配置:
"menus": {
"editor/context": [
{
"when": "editorFocus",
"command": "extension.choosedImage",
"group": "navigation"
}
]
}
when:就是你鼠标在编辑的时候
command: 就是自定义的事件,我叫他选择图片, 这个其实就是在extension.js 注册的事件名字 tips: 就是对应的事件名称
let texteditor = vscode.commands.registerTextEditorCommand(
'extension.choosedImage', ... )
这个其实就是在extension .js 注册对应的事件名,这里的**「事件名」** 一定要和 「package.json」 中文件对应不然会出不来的。 给大家演示下:
图片
重启插件 按下f5 然后按下右键就有我们自定义的右键菜单了。但是问题来了我们按住右键 是不是得弹出一个选择图片的框哇,不然怎么上传对吧?
打开图片上传 弹框
强大的vscode支持了内置的api, 支持打开:
const uri = await vscode.window.showOpenDialog({
canSelectFolders: false,
canSelectMany: false,
filters: {
images: ['png', 'jpg','apng','jpeg','gif','webp'],
},
});
就是这个 api, 你可以过滤出想要的图片, 在filters 里面,然后呢 吐出给我们的是对应图片的路径。
我们看下效果:
读取图片数据
其实这个时候我们我们已经有了图片的路径,这时候就要利用 **「node.js」**的fs 模块 去读取 这个图片的数据 buffer ,这个其实为了方便我们将图片上传到oss 上。 代码如下:
const uri = await vscode.window.showOpenDialog({
canSelectFolders: false,
canSelectMany: false,
filters: {
images: ['png', 'jpg','apng','jpeg','gif','webp'],
},
});
let imgBuffer = await fs.readFile(uri[0].path);
这里还涉及到一个就是说: 本地图片的名字 进行加密, 不能上传到oss 各种中文啥的, 显示的我们很不专业哇
所以这里写了一个MD5的转换
function md5Name(name) {
const index = name.lastIndexOf('.')
const sourceFileName = name.substring(0, index)
const suffix = name.substring(index)
const fileName = md5(sourceFileName + Date.now()) + suffix
return fileName.toLowerCase()
}
就是将名字搞成花里胡哨的样子,呵呵哈哈哈!
图片压缩
我们得到图片的buffer 数据后其实要对图片要支持压缩, 其实社区里面有很多方案, 这里的话我调研的很多还是决定使用tinfiy, 他也有对应的**「node.js」** 使用的他主要理由主要是看下面这张图:
apng
对的这家伙支持**「apng」, 其他的不是很清楚。 但是他不是免费的一个人一个月免费「500」** 次, 思考了下还行,我们也用不到辣么多次最终还是考虑用它去实现。
安装
安装npm包并添加到您应用的依赖中,您就可以使用Node.js客户端:
npm install --save tinify
认证
您必须提供您的API密钥来使用API。您可以通过注册您的姓名和Email地址来获取API密钥。 请秘密保存API密钥。
const tinify = require("tinify");
tinify.key = "YOUR_API_KEY";
这个的话其实就是你的邮箱去注册一下,然后把你对应的**「key」** 去激活其实就可以了
如图
其实就是下面这个你的key 设置激活就好了
tinify压缩图片
您可以上传任何JPEG或PNG图片到Tinify API来进行压缩。我们将自动检测图片类型并相应的使用TinyPNG或TinyJPG引擎进行优化。 只要上传文件或提供图片URL,就会开始压缩。
您可以选择一个本地文件作为源并写入到另一个文件中。
const source = tinify.fromFile("unoptimized.webp");
source.toFile("optimized.webp");
您还可以从缓冲区(buffer)(二进制字符串)上传图片并获取压缩后图片的数据。
const fs = require("fs");
fs.readFile("unoptimized.jpg", function(err, sourceData) {
if (err) throw err;
tinify.fromBuffer(sourceData).toBuffer(function(err, resultData) {
if (err) throw err;
// ...
});
});
代码实现
function compressBuffer(sourceData, key = 'xxx') {
return new Promise((resolve,reject) => {
tinify.key = key;
tinify.fromBuffer(sourceData).toBuffer(function(err, resultData) {
if(resultData) {
resolve(resultData)
}
if (err) {
reject(err);
}
// ...
});
})
}
基于他这个封装了一个promise, 这个**「fromBuffer」** , 到 「toBuffer」 是真的好用。 哈哈哈哈很香,记得一定要设置key 不然promise 直接会报错的, 设置key的方法 就在上面👆🏻, 然后这样其实我们就获得了压缩的图片数据了。
上传图片到oss
这里的话其实有的使用七牛云、 有的使用阿里云。去上传图片,或者是ajax 去上传其实都可以
一般都是要获取token 啥的以及各种签名信息,然后直接上传就好了, 然后呢你就可以获得一张图片地址了。代码我就不展示了, 都是前端应该都懂。这里我说下我遇到的一些问题
- 第一个就是js 跑的 是node js 的环境, 如果使用**「FormData」** 这个类的话 他直接会报找不到, 这个方法是 undefined, 还有**「fetch」**, 所以说要去安装对应node js 包 ,我这里使用的是 「cross-fetch」 和 「form-data」
这里我说一下配置的问题就是你在扩展中如何获得的你配置的参数:
"configuration": [
{
"title": "压缩图片",
"properties": {
"upload_image.secretKey": {
"type": "string",
"default": "",
"description": "设置tinify的ApIKey"
},
"upload_image.secretTokenUrl": {
"type": "string",
"default": "",
"description": "设置得物的tokenUrl"
}
}
}
]
每个属性前面对应的 upload_image 其实你在扩展中你可以通过:
const imageConfig = vscode.workspace.getConfiguration('upload_image')
然后你就可以拿到配置了,upload_image 后面的属性 其实对应的就是对象中的key 然后呢你就可以对吧操作了
这个东西还是具体项目, 具体分析,你们自己 可以针对自己的项目去配置
插件开发——图片链接写入编辑器中
通过上面的方法已经可以获得图片上传后的链接,接下来就是将链接写入编辑器中: 首先判断编辑器选择位置,editor.selection
中可以获得光标位置、光标选择首尾位置。若光标有选中内容则editBuilder.replace
替换选中内容,否则editBuilder.insert
在光标位置插入图片链接:
// 将图片链接写入编辑器
function addImageUrlToEditor(url) {
let editor = vscode.window.activeTextEditor
if (!editor) {
return
}
const { start, end, active } = editor.selection
if (start.line === end.line && start.character === end.character) {
// 在光标位置插入内容
const activePosition = active
editor.edit((editBuilder) => {
editBuilder.insert(activePosition, url)
})
} else {
// 替换内容
const selection = editor.selection
editor.edit((editBuilder) => {
editBuilder.replace(selection, url)
})
}
}
插件发布
到这里,其实一整个vscode插件 其实已经可以开发完成了, 然后我们要把他进行打包发布到vscode 的应用市场
创建账号
我是直接github 登录创建, 首先我们进入文档中提到的主页,完成验证登录后创建一个组织。
创建一个组织
创建发布者
进入下面这个页面 marketplace.visualstudio.com/manage/publ…** 插件发布者, 给大家看下我的:
发布者
打包发布
首先全局 安装脚手架
npm install -g vsce
然后 cd 到当前插件目录 使用下面命令
$ cd myExtension
$ vsce package
# myExtension.vsix generated
这里的打包会报一些error:
第一个就是插件的package.json 增加发布者
"publisher": "Fly",
如果给插件加图标: 其实在项目中创建一个文件夹: image 然后把图片放进去: 同时也要在package.json 中配置
"icon": "images/dewu.jpeg",
这里可能有⚠️,不过没什么关系,继续跑就完事了
warn
最后的话其实就是要写readme ,不然 不让你发布。
打包上传
一切准备就绪: 命令行 输入
vsce package
然后项目中就会出现:
照片
然后可以把这个东西拖到页面这个页面
marketplace.visualstudio.com/manage/publ…
上传
然后点击上传就好了,你就可以在vscode 插件商场可以看到自己写的插件了
作者:Fly
链接:https://juejin.cn/post/7020052159999770632
收起阅读 »
TypeScript 想更深入一层?我推荐自定义 transformer 的 compiler api
现在 JS 的很多库都用 typescript 写了,面试也几乎必问 typescript,可能你对 ts 的各种语法和内置高级类型都挺熟悉了,对 ts 的配置、命令行的使用也没啥问题,但总感觉对 ts 的理解没那么深,苦于没有很好的继续提升的方式。这时候我推荐你研究下 typescript compiler api
。
typescript 会把 ts 源码 parse 成 AST,然后对 AST 进行各种转换,之后生成 js 代码,在这个过程中会对 AST 进行类型检查。typescript 把这整个流程封装到了 tsc 的命令行工具里,平时我们一般也是通过 tsc 来编译 ts 代码和进行类型检查的。
但其实 ts 除了提供 tsc 的命令行工具外,也暴露了很多 api,同时也能自定义 transformer。这就像 babel 可以编译 esnext、ts 语法到 js,可以写 babel 插件来转换代码,也暴露了各种 api 一样。只不过 typescript transformer 的生态远远比不上 babel 插件,知道的人也比较少。
其实 typescript transformer 能做到一些 babel 插件做不到的事情:
babel 是从 ts、exnext 等转 js,生成的 js 代码里会丢失类型信息,不能生成 ts 代码。
babel 只是转换 ts 代码,并不会进行类型检查。
这两个 babel 插件做不到的事情,通过 typescript transformer 都可以做到。
而且,学会 typescript compiler 的 api 能够帮助你深入 typescript 的编译流程,更好的掌握 typescript。
说了这么多,我们通过一个例子来入门下 typescript transformer 吧。
案例描述
这样一段 ts 代码:
type IsString<T> = T extends string ? 'Yes' : 'No';
type res = IsString<true>;
type res2 = IsString<'aaa'>;
我们希望能把 res 和 res2 的类型的值算出来,通过注释加在后面。
像这样:
type IsString<T> = T extends string ? 'Yes' : 'No';
type res = IsString<true> //No;
type res2 = IsString<'aaa'> //Yes;
这个案例既用到了 transformer api,又用到了类型检查的 api。
下面我们来分析下思路:
思路分析
我们首先要把 ts 代码 parse 成 AST,然后通过 AST 找到要转换的节点,这里是 TypeReference 节点。
可以用 astexplorer.net 看一下:
IsString 是一个 TypeReference,也就是引用了别的类型,然后有 typeName 是 IsString 和类型参数 typeArguments,这里的类型参数就是 true。
是不是很像一个函数调用,这就是高级类型的本质,通过把类型参数传到引用的高级类型里求出最终的类型。
然后我们找到 TypeReference 的节点之后就可以通过 type checker 的 api 来求出类型值,之后创建一个注释节点添加到后面就行了。
转换完 AST,再把它打印成 ts 代码字符串。
思路就是这样,接下来我们具体来实现下,也熟悉下 ts 的 api。
代码实现
parse 代码成 AST 需要先指定要编译的文件和编译参数(createProgram 的 api),然后就可以拿到不同文件的 AST 了(getSourceFile 的 api)。
const ts = require("typescript");
const filename = "./input.ts";
const program = ts.createProgram([filename], {}); // 第二个参数是 compiler options,就是配置文件里的那些
const sourceFile = program.getSourceFile(filename);
这里的 sourceFile 就是 AST 的根结点。
接下来我们要对 AST 进行转换,使用 transform 的 api:
const { transformed } = ts.transform(sourceFile, [
function (context) {
return function (node) {
return ts.visitNode(node, visit);
function visit(node) {
if (ts.isTypeReferenceNode(node)) {
// ...
}
return ts.visitEachChild(node, visit, context)
}
};
}
]);
transform 要传入遍历的 AST 以及 transfomerFactory。
AST 就是上面 parse 出的 sourceFile。
transformerFactory 可以拿到 context 中的很多 api 来用,它的返回值就是转换函数 transformer。
transformer 参数是 node,返回值是修改后的 node。
要修改 node 就要遍历 node,使用 visit api 和 vistEachChild 的 api,过程中根据类型过滤出 TypeReference 的节点。
之后对 TypeReference 节点做如下转换:
if (ts.isTypeReferenceNode(node)) {
const type = typeChecker.getTypeFromTypeNode(node);
if (type.value){
ts.addSyntheticTrailingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, type.value);
}
}
也就是通过 typeCheker 来拿到 IsString 这个类型的最终类型值,然后通过 addSyntheticTrailingComment 的 api 在后面加一个注释。
其中用到的 typeChecker 是通过 getTypeChecker 的 api 拿到的:
const typeChecker = program.getTypeChecker();
这样就完成了我们的转换 ts AST 的目的。
然后通过 printer 把 AST 打印成 ts 代码。
const printer =ts.createPrinter();
const code = printer.printNode(false, transformed[0], transformed[0]);
console.log(code);
这样就可以了,我们来测试下。
测试之前,全部代码放这里了:
const ts = require("typescript");
const filename = "./input.ts";
const program = ts.createProgram([filename], {}); // 第二个参数是 compiler options,就是配置文件里的那些
const sourceFile = program.getSourceFile(filename);
const typeChecker = program.getTypeChecker();
const { transformed } = ts.transform(sourceFile, [
function (context) {
return function (node) {
return ts.visitNode(node, visit);
function visit(node) {
if (ts.isTypeReferenceNode(node)) {
const type = typeChecker.getTypeFromTypeNode(node);
if (type.value){
ts.addSyntheticTrailingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, type.value);
}
}
return ts.visitEachChild(node, visit, context)
}
};
}
]);
const printer =ts.createPrinter();
const code = printer.printNode(false, transformed[0], transformed[0]);
console.log(code);
测试效果
经测试,我们达到了求出类型添加到后面的注释里的目的
复盘
激不激动,这是我们第一个 ts transformer 的例子,虽然功能比较简单,但是我们也学会了如何对 ts 代码做 parse、 transform,print,以及 type check。
其实 babel 也有 parse、transform、generate 这 3 步,但没有 type check 的过程,也不能打印成 ts 代码。
用 compiler api 的过程中你会发现原来高级类型就是一个 typeReference,需要传入 typeArguments 来求值的,从而对高级类型的理解更深了。
总结
对 typescript 语法和配置比较熟悉后,想更进一步的话,可以学习下 compiler 的 api 来深入 ts 的编译流程。它包括 transfomer、type checker 等 api,可以达到像 babel 插件一样的转换 ts 代码的目的,而且还能做类型检查。
我们通过一个例子来熟悉了下 typescript 的编译流程和 transformer 的写法。
当你需要修改 ts 代码然后生成 ts 代码的时候,babel 是做不到的,它只能生成 js 代码,这时候可以考虑下 typescript 的自定义 transformer。
而且用 typescript compiler api 能够加深你对 ts 编译流程和类型检查的理解。
ts compiler api 尤其是其中的自定义 transformer 是 typescript 更进一层的不错的方向。
收起阅读 »
JavaScript之彻底理解EventLoop
在正式学习Event Loop
之前,先需要解决几个问题:
什么是同步与异步?
JavaScript
是一门单线程语言,那如何实现异步?
同步任务和异步任务的执行顺序如何?
异步任务是否存在优先级?
同步与异步
计算机领域中的同步与异步和我们现实社会的同步和异步正好相反。现实中的同步,就是同时进行,突出的是"同",比如看足球比赛的时候吃着零食,两件事情同时发生;异步就是不同时。但计算机中与现实存在一定差异。
举个栗子
天气冷了,早上刚醒来想喝点热水暖暖身子,但这每天起早贪黑996,晚上回来太累躺下就睡,没开水啊,没法子,只好急急忙忙去烧水。
现在早上太冷了啊,不由得在被窝里面多躺了一会,收拾的时间紧紧巴巴,不能空等水开,于是我便趁此去洗漱,收拾自己。
洗漱完,水开了,喝到暖暖的热水,舒服啊!
舒服完,开启新的996之日,打工人出发!
烧水和洗漱是在同时间进行的,这就是计算机中的异步。
计算机中的同步是连续性的动作,上一步未完成前,下一步会发生堵塞,直至上一步完成后,下一步才可以继续执行。例如:只有等水开,才能喝到暖暖的热水。
单线程却可以异步?
JavaScript
的确是一门单线程语言,但是浏览器UI
是多线程的,异步任务借助浏览器的线程和JavaScript
的执行机制实现。
例如,setTimeout
就借助浏览器定时器触发线程的计时功能来实现。
浏览器线程
GUI
渲染线程
- 绘制页面,解析HTML、CSS,构建DOM树等
- 页面的重绘和重排
- 与JS引擎互斥(JS引擎阻塞页面刷新)
JS
引擎线程
- js脚本代码执行
- 负责执行准备好的事件,例如定时器计时结束或异步请求成功且正确返回
- 与GUI渲染线程互斥
- 事件触发线程
- 当对应的事件满足触发条件,将事件添加到js的任务队列末尾
- 多个事件加入任务队列需要排队等待
- 定时器触发线程
- 负责执行异步的定时器类事件:setTimeout、setInterval等
- 浏览器定时计时由该线程完成,计时完毕后将事件添加至任务队列队尾
HTTP
请求线程
- 负责异步请求
- 当监听到异步请求状态变更时,如果存在回调函数,该线程会将回调函数加入到任务队列队尾
同步与异步执行顺序
JavaScript
将任务分为同步任务和异步任务,同步任务进入主线中中,异步任务首先到Event Table
进行回调函数注册。- 当异步任务的触发条件满足,将回调函数从
Event Table
压入Event Queue
中。 - 主线程里面的同步任务执行完毕,系统会去
Event Queue
中读取异步的回调函数。 - 只要主线程空了,就会去
Event Queue
读取回调函数,这个过程被称为Event Loop
。
举个栗子
- setTimeout(cb, 1000),当1000ms后,就将cb压入Event Queue。
- ajax(请求条件, cb),当http请求发送成功后,cb压入Event Queue。
EventLoop执行流程
Event Loop执行的流程如下:
下面一起来看一个例子,熟悉一下上述流程。
// 下面代码的打印结果?
// 同步任务 打印 first
console.log("first");
setTimeout(() => {
// 异步任务 压入Event Table 4ms之后cb压入Event Queue
console.log("second");
},0)
// 同步任务 打印last
console.log("last");
// 读取Event Queue 打印second
常见异步任务
DOM
事件
AJAX
请求
定时器setTimeout
和setlnterval
ES6
的Promise
异步任务的优先级
下面继续来看一个案例:
setTimeout(() => {
console.log(1);
}, 1000)
new Promise(function(resolve){
console.log(2);
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log(3)
});
console.log(4)
按照上面的学习:
可以很轻松得出案例的打印结果:2,4,1,3。
Promise定义部分为同步任务,回调部分为异步任务
将案例代码在控制台运行,最终返回结果却有些出人意料:
刚看到如此结果,我的第一感觉是,setTimeout
函数1s触发太慢导致它加入Event Queue
的时间晚于Promise.then
于是我修改了setTimeout
的回调时间为0(浏览器最小触发时间为4ms
),但结果仍为发生改变。
那么也就意味着,JavaScript
的异步任务是存在优先级的。
宏任务和微任务
JavaScript
除了广义上将任务划分为同步任务和异步任务,还对异步任务进行了更精细的划分。异步任务又进一步分为微任务和宏任务。
history traversal
任务(h5
当中的历史操作)
process.nextTick
(nodejs
中的一个异步操作)
MutationObserver
(h5
里面增加的,用来监听DOM
节点变化的)
宏任务和微任务分别有各自的任务队列Event Queue
,即宏任务队列和微任务队列。
Event Loop执行过程
了解到宏任务与微任务过后,我们来学习宏任务与微任务的执行顺序。
代码开始执行,创建一个全局调用栈,script
作为宏任务执行
执行过程过同步任务立即执行,异步任务根据异步任务类型分别注册到微任务队列和宏任务队列
同步任务执行完毕,查看微任务队列
若存在微任务,将微任务队列全部执行(包括执行微任务过程中产生的新微任务)
若无微任务,查看宏任务队列,执行第一个宏任务,宏任务执行完毕,查看微任务队列,重复上述操作,直至宏任务队列为空
更新一下Event Loop
的执行顺序图:
总结
在上面学习的基础上,重新分析当前案例:
setTimeout(() => {
console.log(1);
}, 1000)
new Promise(function(resolve){
console.log(2);
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log(3)
});
console.log(4)
分析过程见下图:
收起阅读 »
优雅的使用注释
代码千万行,注释第一行。
代码不规范,同事泪两行。
前言
注释相信小伙伴们都不陌生,但是就是这个小小的注释就像项目文档一样让许多小伙伴又爱又恨。不喜欢写注释,又讨厌别人不写注释。在此我们将讨论 JavaScript
和 CSS
的注释,希望通过这篇文章,让你重拾对注释的喜爱,让编码的乐趣如星辰大海。
一、语法
1.1 CSS 注释
/* css 注释 */
1.2 JavaScript 注释
// 单行注释
/**
* 多行注释,注意第一行最好用两个 *
* ...
*/
/*
当然,除了两端的 * 必须加以外,其他的 * 不加也行
...
*/
二、基本使用
2.1 单行注释
一般情况下,单行注释会出现在代码的正上方,起到提示的作用:
/* 用注释备注 CSS 类名的功能 */
/* 顶部组件 */
.hd {
position: fixed;
width: 100vw;
}
/* 版心 */
.container {
margin: 16px auto;
width: 1200px;
}
// 用单行注释备注简单的信息
const userName = ""; // 用户名
const userAvatar = ""; // 用户头像
// xxx函数
const myFunction = () => {};
2.2 多行注释
多行注释一般用于需要备注的信息过多的情况,常常出没于 JavaScript
函数的附近。首先提出一个问题:为什么要用到多行注释,用单行注释不香吗?下面就来看看下面的代码:
// xxx函数
const myFunction = ({ id, name, avatar, list, type }) => {
// 此处省略 30 行代码
};
小伙伴们可能看到了,一个传入五个参数,内部数行代码的函数竟然只有短短的一行注释,也许你开发的时候能记住这个函数的用途以及参数的类型以及是否必传等,但是如果你隔了一段时间再回头看之前的代码,那么简短的注释就可能变成你的困扰。 更不用说没有注释,不写注释一时爽,回看代码火葬场。 写注释的目的在于提高代码的可读性。相比之下,下面的注释就清晰的多:
/**
* 调整滚动距离
* 用于显示给定 id 元素
* @param id string 必传 元素 id
* @param distance number 非必传 距离视口最顶部距离(避免被顶部固定定位元素遮挡)
* @returns null
*/
export const scrollToShowElement = (id = "", distance = 0) => {
return () => {
if (!id) {
return;
};
const element = document.getElementById(id);
if (!element) {
return;
};
const top = element?.offsetTop || 0;
window.scroll(0, top - distance);
};
};
对于复杂的函数,函数声明上面要加上统一格式的多行注释,同时内部的复杂逻辑和重要变量也需要加上单行注释,两者相互配合,相辅相成。函数声明的多行注释格式一般为:
/**
* 函数名称
* 函数简介
* @param 参数1 参数1数据类型 是否必传 参数1描述
* @param 参数2 参数2数据类型 是否必传 参数2描述
* @param ...
* @returns 返回值
*/
多行注释的优点是清晰明了,缺点是较为繁琐(可以借助编辑器生成 JavaScript 函数注释模板)。建议逻辑简单的函数使用单行注释,逻辑复杂的函数和公共/工具函数使用多行注释。
当然,一个好的变量/函数名也能降低阅读者的思考成本,可以移步到我的文章:《优雅的命名 🧊🧊》。
三、进阶使用
无论是 css
还是 JavaScript
中,当代码越来越多的时候,也使得寻找要改动的代码时变得越来越麻烦。所以我们有必要对代码按模块进行整理,并在每个模块的顶部用注释,结束时使用空行进行分割。
/* 以下代码仅为示例 */
/* 模块1 */
/* 类名1 */
.class-a {}
/* 类名2 */
.class-b {}
/* 类名3 */
.class-c {}
/* 模块2 */
/* 类名4 */
.class-d {}
/* 类名5 */
.class-e {}
/* ... */
// 以下代码仅为示例
// 模块1
// 变量1
const value1 = "";
// 变量2
const value2 = "";
// 变量3
const value3 = "";
// 函数1
const myFunction1 = () => {};
// 模块2
// 变量4
const value4 = "";
// 变量5
const value5 = "";
// 变量6
const value6 = "";
// 函数2
const myFunction2 = () => {};
// ...
效果有了,但是似乎不太明显,因此我们在注释中增加 -
或者 =
来进行分割试试:
/* ------------------------ 模块1 ------------------------ */
/* 类名1 */
.class-a {}
/* 类名2 */
.class-b {}
/* 类名3 */
.class-c {}
/* ------------------------ 模块2 ------------------------ */
/* 类名4 */
.class-d {}
/* 类名5 */
.class-e {}
/* ... */
// 以下代码仅为示例
/* ======================== 模块1 ======================== */
// 变量1
const value1 = "";
// 变量2
const value2 = "";
// 变量3
const value3 = "";
// 函数1
const myFunction1 = () => {};
/* ======================== 模块2 ======================== */
// 变量4
const value4 = "";
// 变量5
const value5 = "";
// 变量6
const value6 = "";
// 函数2
const myFunction2 = () => {};
// ...
能直观的看出,加长版的注释分割效果更好,区分度更高。高质量的代码往往需要最朴实无华的注释进行分割。其中 JavaScript
的注释“分割线”建议使用多行注释。
“华丽的”分割线:
/* ------------------------ 华丽的分割线 ------------------------ */
/* ======================== 华丽的分割线 ======================== */
四、扩展
工欲善其事,必先利其器。下面我要推荐几款 VSCode
编辑器关于注释的插件。
4.1 Better Comments
插件介绍:可以改变注释的颜色,有四种高亮的颜色(默认为红色、橙色、绿色、蓝色)和一种带删除线的黑色。颜色可以在插件配置里面修改。下图为实例颜色和本人在项目中的用法,一个注释对应一种情况。
喜欢花里胡哨的coder们必备插件,有效提高注释的辨析度和美感,从此爱上注释。其改变注释颜色只需要加上一个或多个字符即可,开箱即用。
// ! 红色的高亮注释,双斜线后加英文叹号 ! 配置
// todo 橙色的高亮注释,双斜线后加 todo 函数
// * 绿色的高亮注释,双斜线后加 * 变量
// ? 蓝色的高亮注释,双斜线后加英文问号 ? 组件
// // 黑色带删除线的注释,双斜线后加双斜线 // 说明
4.2 koroFileHeader
插件介绍:文件头部添加注释,在光标处添加函数注释,一键添加佛祖保佑永无BUG、神兽护体等注释图案。
4.3 JavaScript Comment Snippet
插件介绍:可以快速生成 JavaScript 注释,冷门但是好用。
结语
不得不说注释在编码过程中真的相当重要,为了写出更优雅,更易于维护的代码,我们也应当把最重要的信息写到注释里。一个项目的 README.markdown
和项目中的注释就喜像是项目的 说明书 一样,能让非项目开发者更快的读懂代码的含义以及编码的思想。让代码成就我们,让代码改变世界,让注释,伴我同行!
收起阅读 »
技术总结 | 前端萌新现在上车Docker,还来得及么?
序言
作为一名爱学习的前端攻城狮,在当下疯狂内卷的大环境🐱, 不卷一卷Docker
是不是有点说不过去,再加上现在我司前端部署项目大部分都是Docker
,所以现在赶紧上车, 跟着Up主来look look
,欢迎有big old指正
- Q:你能说一下你怎么看待
Docker
,Docker
能干什么么 - A:
Docker
是一个便携的应用容器, 用来自动化测试和持续集成、发布
大家在面试的时候是不是这么回答的😂,恭喜你答对了,但是不够完整,现在来结合文档和Demo
具体看看,Docker
到底能干啥
概念
什么是Docker
Docker
就好比是一个集装箱,里面装着各式各类的货物。在一艘大船上,可以把货物规整的摆放起来。并且各种各样的货物被集装箱标准化了,集装箱和集装箱之间不会互相影响。
有人觉得Docker
是一台虚拟机,但是这种想法是错误的,直接上图
上图差异,左图虚拟机的
Guest OS
层和Hypervisor
层在Docker
中被Docker Engine
层所替代。虚拟机的Guest OS
即为虚拟机安装的操作系统,它是一个完整操作系统内核;虚拟机的Hypervisor
层可以简单理解为一个硬件虚拟化平台,它在Host OS是以内核态的驱动存在的。
三大核心概念
镜像(image)
镜像是创建docker容器的基础,docker镜像类似于虚拟机镜像,可以将它理解为一个面向docker引擎的只读模块,包含文件系统
创建镜像的方式
- 使用
Dockerfile Build
镜像 - 拉取
Docker
官方镜像
容器(container)
容器是从镜像创建的应用运行实例,容器之间是相互隔离、互不可见的。可以把容器看做一个简易版的linux系统环境(包括roo
t权限、进程空间、用户空间和网络空间等),以及运行在这个环境上的应用打包而成的应用盒子。
可以利用docker create
命令创建一个容器,创建后的的容器处于停止状态,可以使用docker start
命令来启动它。也可以运行docker run
命令来直接从镜像启动运行一个容器。docker run = docker creat + docker start
。
当利用docker run
创建并启动一个容器时,docker
在后台的标准操作包括:
(1)检查本地是否存在指定的镜像,不存在就从公有仓库下载。
(2)利用镜像创建并启动一个容器。
(3)分配一个文件系统,并在只读的镜像层外面挂载一层可读写层。
(4)从宿主机配置的网桥接口中桥接一个虚拟的接口到容器中。
(5)从地址池中配置一个IP地址给容器。
(6)执行用户指定的应用程序。
(7)执行完毕后容器终止。
仓库(Repository)
安装Docker
后,可用通过官方提供的registry
镜像来搭建一套本地私有仓库环境。
下载registry
镜像:
基础操作
安装Docker
linux
安装Docker
windows
安装docker
推荐安装Docker
Desktop
飞机票
拉取镜像
# 拉取镜像
>>> docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
f3ef4ff62e0d: Pull complete
Digest: sha256:a0d9e826ab87bd665cfc640598a871b748b4b70a01a4f3d174d4fb02adad07a9
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest
# 查看本地所有镜像
>>> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 597ce1600cf4 13 days ago 72.8MB
hello latest 8b9d88b05a48 2 weeks ago 231MB
centos latest 5d0da3dc9764 4 weeks ago 231MB
docker/getting-started latest 083d7564d904 4 months ago 28MB
# 删除镜像
>>> docker rmi ubuntu
Untagged: ubuntu:latest
Untagged: ubuntu@sha256:a0d9e826ab87bd665cfc640598a871b748b4b70a01a4f3d174d4fb02adad07a9
Deleted: sha256:597ce1600cf4ac5f449b66e75e840657bb53864434d6bd82f00b172544c32ee2
Deleted: sha256:da55b45d310bb8096103c29ff01038a6d6af74e14e3b67d1cd488c3ab03f5f0d
创建容器
#创建容器
>>> docker create --name my-ubuntu ubuntu
2da5d12e9cbaed77d90d23f5f5436215ec511e20607833a5a674109c13b58f48
#启动容器
>>> docker start 2da5d
#查看所有容器
>>> docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2da5d12e9cba ubuntu "bash" About a minute ago Exited (0) 31 seconds ago my-ubuntu
#删除容器
>>> docker rm 2da5d
#创建并进入容器
>>> docker run --name my-ubuntu2 -it ubuntu
root@552c7c73dcf6:/#
#进入容器后就可以在容器内部执行脚本了
# 进入正在运行的容器
>>> docker exec -it 2703b1 sh
/ #
编排Dockerfile
Dockerfile
是一个创建镜像所有命令的文本文件, 包含了一条条指令和说明, 每条指令构建一层, 通过docker build
命令,根据Dockerfile
的内容构建镜像,因此每一条指令的内容, 就是描述该层如何构建.有了Dockefile
, 就可以制定自己的docker镜像规则,只需要在Dockerfile
上添加或者修改指令, 就可生成docker
镜像.
FROM ubuntu #构造的新镜像是基于哪个镜像
MAINTAINER Up_zhu #维护者信息
RUN yum install nodejs #构建镜像时运行的shell命令
WORKDIR /app/my-app #设置工作路径
EXPOSE 8080 #指定于外界交互的端口,即容器在运行时监听的端口
ENV MYSQL_ROOT_PASSWORD 123456 #设置容器内环境变量
ADD ./config /app/config #拷贝文件或者目录到镜像,如果是URL或者压缩包会自动下载或者自动解压
COPY ./dist /app/my-app
VOLUME /etc/mysql #定义匿名卷
实战
基于
vite
项目打镜像,发布
新建Dockerfile
FROM nginx
COPY ./dist/ /usr/share/nginx/html/
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
新建nginx
配置文件
# nginx/default.conf
server {
listen 80;
server_name localhost;
#charset koi8-r;
access_log /var/log/nginx/host.access.log main;
error_log /var/log/nginx/error.log error;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
打镜像
查看本地镜像
>>> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
my-vite latest cc015756264b About a minute ago 133MB
启动容器
现在可以访问地址来验证是否成功
查看本地正在运行的容器
文末
是不是很Easy
呢?我们从上面可以看出,Docker
的功能是十分强大的,除此之外,我们还可以拉取一些 Ubuntu
,Apache
等镜像, 也可以自己定制一下镜像,发布到Docker Hub
.
当然!本文介绍的只是Docker
的基础功能,小编能力到此,还需继续学习~
收起阅读 »
实现无感刷新token,我是这样做的
前言
最近在做需求的时候,涉及到登录token,产品提出一个问题:能不能让token过期时间长一点,我频繁的要去登录。
前端:后端,你能不能把token 过期时间设置的长一点。
后端:可以,但是那样做不安全,你可以用更好的方法。
前端:什么方法?
后端:给你刷新token的接口,定时去刷新token
前端:好,让我思考一下
需求
当token过期的时候,刷新token,前端需要做到无感刷新token,即刷token时要做到用户无感知,避免频繁登录。实现思路
方法一
后端返回过期时间,前端判断token过期时间,去调用刷新token接口
缺点:需要后端额外提供一个token过期时间的字段;使用了本地时间判断,若本地时间被篡改,特别是本地时间比服务器时间慢时,拦截会失败。
方法二
写个定时器,定时刷新token接口
缺点:浪费资源,消耗性能,不建议采用。
方法三
在响应拦截器中拦截,判断token 返回过期后,调用刷新token接口
实现
axios
的基本骨架,利用service.interceptors.response
进行拦截
import axios from 'axios'
service.interceptors.response.use(
response => {
if (response.data.code === 409) {
return refreshToken({ refreshToken: localStorage.getItem('refreshToken'), token: getToken() }).then(res => {
const { token } = res.data
setToken(token)
response.headers.Authorization = `${token}`
}).catch(err => {
removeToken()
router.push('/login')
return Promise.reject(err)
})
}
return response && response.data
},
(error) => {
Message.error(error.response.data.msg)
return Promise.reject(error)
})
问题解决
问题一:如何防止多次刷新token
我们通过一个变量isRefreshing 去控制是否在刷新token的状态。
import axios from 'axios'
service.interceptors.response.use(
response => {
if (response.data.code === 409) {
if (!isRefreshing) {
isRefreshing = true
return refreshToken({ refreshToken: localStorage.getItem('refreshToken'), token: getToken() }).then(res => {
const { token } = res.data
setToken(token)
response.headers.Authorization = `${token}`
}).catch(err => {
removeToken()
router.push('/login')
return Promise.reject(err)
}).finally(() => {
isRefreshing = false
})
}
}
return response && response.data
},
(error) => {
Message.error(error.response.data.msg)
return Promise.reject(error)
})
问题二:同时发起两个或者两个以上的请求时,其他接口怎么解决
当第二个过期的请求进来,token正在刷新,我们先将这个请求存到一个数组队列中,想办法让这个请求处于等待中,一直等到刷新token后再逐个重试清空请求队列。那么如何做到让这个请求处于等待中呢?为了解决这个问题,我们得借助Promise。将请求存进队列中后,同时返回一个Promise,让这个Promise一直处于Pending状态(即不调用resolve),此时这个请求就会一直等啊等,只要我们不执行resolve,这个请求就会一直在等待。当刷新请求的接口返回来后,我们再调用resolve,逐个重试。最终代码:
import axios from 'axios'
// 是否正在刷新的标记
let isRefreshing = false
//重试队列
let requests = []
service.interceptors.response.use(
response => {
//约定code 409 token 过期
if (response.data.code === 409) {
if (!isRefreshing) {
isRefreshing = true
//调用刷新token的接口
return refreshToken({ refreshToken: localStorage.getItem('refreshToken'), token: getToken() }).then(res => {
const { token } = res.data
// 替换token
setToken(token)
response.headers.Authorization = `${token}`
// token 刷新后将数组的方法重新执行
requests.forEach((cb) => cb(token))
requests = [] // 重新请求完清空
return service(response.config)
}).catch(err => {
//跳到登录页
removeToken()
router.push('/login')
return Promise.reject(err)
}).finally(() => {
isRefreshing = false
})
} else {
// 返回未执行 resolve 的 Promise
return new Promise(resolve => {
// 用函数形式将 resolve 存入,等待刷新后再执行
requests.push(token => {
response.headers.Authorization = `${token}`
resolve(service(response.config))
})
})
}
}
return response && response.data
},
(error) => {
Message.error(error.response.data.msg)
return Promise.reject(error)
}
)
作者:远航_
链接:https://juejin.cn/post/7018439775476514823
收起阅读 »
JavaScript之彻底理解原型与原型链
前言
原型与原型链知识历来都是面试中考察的重点,说难不算太难,但要完全理解还是得下一定的功夫。先来看一道面试题开开胃口吧:
function User() {}
User.prototype.sayHello = function() {}
var u1 = new User();
var u2 = new User();
console.log(u1.sayHello === u2.sayHello);
console.log(User.prototype.constructor);
console.log(User.prototype === Function.prototype);
console.log(User.__proto__ === Function.prototype);
console.log(User.__proto__ === Function.__proto__);
console.log(u1.__proto__ === u2.__proto__);
console.log(u1.__proto__ === User.__proto__);
console.log(Function.__proto__ === Object.__proto__);
console.log(Function.prototype.__proto__ === Object.prototype.__proto__);
console.log(Function.prototype.__proto__ === Object.prototype);
基础铺垫
JavaScript
所有的对象本质上都是通过new 函数
创建的,包括对象字面量的形式定义对象(相当于new Object()
的语法糖)。
所有的函数本质上都是通过
new Function
创建的,包括Object
、Array
等
所有的函数都是对象。
prototype
每个函数都有一个属性prototype
,它就是原型,默认情况下它是一个普通Object
对象,这个对象是调用该构造函数所创建的实例的原型。
contructor属性
JavaScript
同样存在由原型指向构造函数的属性:constructor
,即Func.prototype.constructor --> Func
__proto__
JavaScript
中所有对象(除了null
)都具有一个__proto__
属性,该属性指向该对象的原型。
function User() {}
var u1 = new User();
// u1.__proto__ -> User.prototype
console.log(u1.__proto__ === User.prototype) // true
显而易见,实例的__proto__
属性指向了构造函数的原型,那么多个实例的__proto__
会指向同一个原型吗?
var u2 = new User();
console.log(u1.__proto__ === u2.__proto__) // true
如果多个实例的__proto__
都指向构造函数的原型,那么实例如果能通过一种方式,访问原型上的方法,属性等,就可以在原型进行编程,实现继承的效果。
我们继续更新一下原型与原型链的关系图:
原型链
实例对象在查找属性时,如果查找不到,就会沿着__proto__
去与对象关联的原型上查找,如果还查找不到,就去找原型的原型,直至查到最顶层,这也就是原型链的概念。
就借助面试题,举几个原型链的例子:
举例
u1.sayHello()
:
u1
上是没有sayHello
方法的,因此访问u1.__proto__(User.prototype)
,成功访问到sayHello
方法u2.toString()
u2,User.prototype
都没有toString
方法,User.prototype
也是一个普通对象,因此继续寻找User.prototype.__proto__(Object.prototype)
,成功调用到toString
方法
提高
学完上面那些,大多数面试题都可以做出来了,例如下面这种
function A() {}
function B(a) {
this.a = a;
}
function C(a) {
if (a) {
this.a = a;
}
}
A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;
console.log(new A().a); //1
console.log(new B().a); //undefined
console.log(new C(2).a); //2
但距离解决文章的最初的面试题还欠缺些什么,比如Function.__proto__ === Object.__proto__、Function.prototype.__proto__ === Object.prototype.__proto__
等,接着我们来一一攻克它。
Objcet.__proto__
、 Object.prototype
、Object.prototype.__proto__
Object
是构造函数,在第二部分我们讲过所有的函数都是通过new Function
创建了,因此Object
相当于Function
的实例,即Object.__proto__ --> Function.prototype
。
Object.prototype
是Object
构造函数的原型,处于原型链的顶端,Object.prototype.__proto__
已经没有可以指向的上层原型,因此其值为null
// 总结:
Object.__proto__ --> Function.prototype
Object.prototype.__proto__ --> null
Function.__proto__
、Function.prototype
、Function.prototype.__proto__
Function.prototype
是Function
的原型,是所有函数实例的原型,例如上面讲的Object.__proto__
Function.prototype
是一个普通对象,因此Function.prototype.__proto__ --> Object.prototype
Function.__proto__
:__proto__
指向创造它的构造函数的原型,那谁创造了Function
那?
猜想:函数对象也是对象,那
Function.__proto__
会指向Object.prototype
吗?上文提到,Object.__proto__ --> Function.prototype
。如果Function.__proto__ -> Object.prototype
,感觉总是怪怪的,到底谁创造了谁,于是我去做了一下测试:
实践证明只存在Object.__proto__ --> Function.prototype
苦思冥想没得出结果,难道
Function
函数是猴子不成,从石头缝里面蹦出来的?于是我进行了一同乱七八糟的测试,没想到找出了端倪。
通过上面我们可以得出:Function.__proto__ --> Function.prototype
Function
函数不通过任何东西创建,JS
引擎启动时,添加到内存中
总结
最后将原型与原型链方面的知识凝结成一张图:
- 所有函数(包括
Function
)的__proto__
指向Function.prototype
- 自定义对象实例的
__proto__
指向构造函数的原型 - 函数的
prototype
的__proto__
指向Object.prototype
Object.prototype.__proto__ --> null
后语
知识的海洋往往比想象中还要辽阔,原型与原型链这边也反复的学过多次,我认为应该学的比较全面,比较完善了。但遇到这个面试题后,我才发现我所学的只不过是一根枝干,JS里面真的有很多深层次的宝藏等待挖掘。学海无涯,与君共勉。
最后再附赠个简单的面试题,提高一下自信:
var F = function () {}
Object.prototype.a = function () {}
Function.prototype.b = function () {}
var f = new F();
console.log(f.a, f.b, F.a, F.b);
// 原型链
// f.__proto__ --> F.prototype --> Object.prototype
// F.__proto__ --> Function.prototype --> Object.prototype
收起阅读 »
18 个杀手级 JavaScript 单行代码
1、复制到剪贴板
使用 navigator.clipboard.writeText 轻松将任何文本复制到剪贴板。
const copyToClipboard = (text) => navigator.clipboard.writeText(text);
copyToClipboard("Hello World");
2、检查日期是否有效
使用以下代码段检查给定日期是否有效。
const isDateValid = (...val) => !Number.isNaN(new Date(...val).valueOf());
isDateValid("December 17, 1995 03:24:00");
// Result: true
3、找出一年中的哪一天
查找给定日期的哪一天。
const dayOfYear = (date) => Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);
dayOfYear(new Date());
// Result: 272
4、将首字符串大写
Javascript 没有内置的大写函数,因此我们可以使用以下代码。
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1)capitalize("follow for more")// Result: Follow for more
5、找出两日期之间的天数
使用以下代码段查找给定 2 个日期之间的天数。
const dayDif = (date1, date2) => Math.ceil(Math.abs(date1.getTime() - date2.getTime()) / 86400000)dayDif(new Date("2020-10-21"), new Date("2021-10-22"))// Result: 366
6、清除所有 Cookie
你可以通过使用 document.cookie 访问 cookie 并清除它来轻松清除存储在网页中的所有 cookie。
const clearCookies = document.cookie.split(';').forEach(cookie => document.cookie = cookie.replace(/^ +/, '')
.replace(/=.*/, `=;expires=${new Date(0).toUTCString()};
path=/`));
7、生成随机十六进制
你可以使用 Math.random 和 padEnd 属性生成随机十六进制颜色。
const randomHex = () => `#${Math.floor(Math.random() * 0xffffff).toString(16).padEnd(6, "0")}`
console.log(randomHex());
//Result: #92b008
8、从数组中删除重复项
你可以使用 JavaScript 中的 Set 轻松删除重复项。
const removeDuplicates = (arr) => [...new Set(arr)];
console.log(removeDuplicates([1, 2, 3, 3, 4, 4, 5, 5, 6]));
// Result: [ 1, 2, 3, 4, 5, 6 ]
9、从 URL 获取查询参数
你可以通过传递 window.location 或原始 URL goole.com?search=easy&page=3 从 url 轻松检索查询参数
const getParameters = (URL) => {
URL = JSON.parse('{"' + decodeURI(URL.split("?")[1]).replace(/"/g, '\"').replace(/&/g, '","').replace(
/=/g, '":"') + '"}');
return JSON.stringify(URL);
};
getParameters(window.location) // Result: { search : "easy", page : 3 }
10、从日期记录时间
我们可以从给定日期以小时::分钟::秒的格式记录时间。
const timeFromDate = date => date.toTimeString().slice(0, 8);
console.log(timeFromDate(new Date(2021, 0, 10, 17, 30, 0)));
// Result: "17:30:00"
11、检查数字是偶数还是奇数
const isEven = num => num % 2 === 0;console.log(isEven(2));
// Result: True
12、求数字的平均值
使用 reduce 方法找到多个数字之间的平均值。
const average = (...args) => args.reduce((a, b) => a + b) / args.length;
average(1, 2, 3, 4);
// Result: 2.5
13、反转字符串
你可以使用 split、reverse 和 join 方法轻松反转字符串。
const reverse = str => str.split('').reverse().join('');reverse('hello world');
// Result: 'dlrow olleh'
14、检查数组是否为空
检查数组是否为空的简单单行程序将返回 true 或 false。
const isNotEmpty = arr => Array.isArray(arr) && arr.length > 0;
isNotEmpty([1, 2, 3]);
// Result: true
15、获取选定的文本
使用内置的 getSelectionproperty 获取用户选择的文本。
const getSelectedText = () => window.getSelection().toString();
getSelectedText();
16、打乱数组
使用 sort 和 random 方法打乱数组非常容易。
const shuffleArray = (arr) => arr.sort(() => 0.5 - Math.random());console.log(shuffleArray([1, 2, 3, 4]));// Result: [ 1, 4, 3, 2 ]
17、检测暗模式
使用以下代码检查用户的设备是否处于暗模式。
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matchesconsole.log(isDarkMode) // Result: True or False
18、将 RGB 转换为十六进制
const rgbToHex = (r, g, b) => "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);rgbToHex(0, 51, 255); // Result: #0033ff
作者:耀在掘金73091
链接:https://juejin.cn/post/7018106908129099807
收起阅读 »
抛弃Vue转入React的六个月,我收获了什么?
对不起,我抛弃了Vue,转入React阵营。不因为其它,就因为在我这边使用React的工资比使用Vue的工资高。
在六月前,我硬背了几百道的React面试题,入职一家使用React的公司,薪资增幅120%;
入职就马上进入开发阶段,完全是从零开始,随着时间的推移,发现React入门也不是网传的那么难。难道是自己天生就适合吃这碗饭…………
到今天已经六个月了,在这里想把这段时间的收获跟掘友们分享一下,请掘友们多多指教,一起进步哈。
另外我是用React16.8来入门,故在开发过程中大多使用函数式组件和React Hooks来开发。
重磅提醒,文末有抽奖噢。
一、关于函数式组件的收获
函数式组件可以理解为一个能返回React元素的函数,其接收一个代表组件属性的参数props
。
在React16.8之前,也就是没有React Hooks之前,函数式组件只作为UI组件,其输出完全由参数props
控制,没有自身的状态没有业务逻辑代码,是一个纯函数。函数式组件没有实例,没有生命周期,称为无状态组件。
在React Hooks出现后,可以用Hook赋予函数式组件状态和生命周期,于是函数式组件也可以作为业务组件。
开发过程中,类组件和函数式组件都有使用,经过六个月的开发,感觉还是函数式组件比类组件好用一些,感受最深的是以下两点:
- 不用去学习class,不用去管烦人的this指向问题;
- 复用性高,很容易就把共同的抽取出来,写出自定义Hook,来替代高阶组件。
函数式组件和类组件之间有一个非常重要的区别:函数式组件捕获了渲染所使用的值。
我是遇到一个BUG才知道有这个区别,在解决这个BUG的过程理解了这个区别的含义。
那个BUG的场景是这样的,一个输入框,输入完内容,点击按钮搜索,搜索时先请求一个接口,获取一个类型,再用类型和输入框值去请求搜索接口。用代码简单描述一下。
import React, { Component } from "react";
import * as API from 'api/list';
class SearchComponent extends Component {
constructor() {
super();
this.state = {
inpValue: ""
};
}
getType () {
const param = {
val:this.state.inpValue
}
return API.getType(param);
}
getList(type){
const param = {
val:this.state.inpValue,
type,
}
return API.getList(param);
}
async handleSearch() {
const res = await this.getType();
const type = res?.data?.type;
const res1 = await this.getList(type);
console.log(res1);
}
render() {
return (
<div>
<input
type="text"
value={this.state.inpValue}
onChange={(e) => {
this.setState({ inpValue: e.target.value });
}}
/>
<button
onClick={() => {
this.handleSearch();
}}
>
搜索
</button>
</div>
);
}
}
export default SearchComponent;
以上代码逻辑看上去都没什么毛病,但是QA给我挑了一个BUG,在输入框输入要搜索的内容后,点击搜索按钮开始搜索,然后很快在输入框中又输入内容,结果搜索接口getList
报错。查一下原因,发现是获取类型接口getType
和搜索接口getList
接受的参数val
不一致。
在排查过程中,我非常纳闷,明明两次请求中val
都是读取this.state.inpValue
的值。当时同事指导我改成函数式组件就可解决这个BUG。
import React, { useState } from "react";
import * as API from 'api/list';
export const SearchComponent = () =>{
const [inpValue,setInpValue] = useState('');
const getType = () =>{
const param = {
val:inpValue
}
return API.getType(param);
}
const getList = (type) =>{
const param = {
val:inpValue,
type:type,
}
return API.getList(param);
}
const handleSearch = async ()=>{
const res = await getType();
const type = res?.data?.type;
const res1 = await getList(type);
console.log(res1);
}
return (
<div>
<input
type="text"
value={inpValue}
onChange={(e) => {
setInpValue(e.target.value);
}}
/>
<button
onClick={() => {
handleSearch();
}}
>
搜索
</button>
</div>
);
}
export default SearchComponent;
改成函数式组件后,再试一下,不报错了,BUG修复了。后面我查阅资料后才知道在函数式组件中的事件的state和props所获取的值是事件触发那一刻页面渲染所用的state和props的值。当点击搜索按钮后,val
的值就是那一刻输入框中的值,无论输入框后面的值在怎么改变,不会捕获最新的值。
那为啥类组件中,能获取到最新的state值呢?关键在于类组件中是通过this
去获取state的,而this
永远是最新的组件实例。
其实在类组件中改一下,也可以解决这个BUG,改动的地方如下所示:
getType (val) {
const param = {
val,
}
return API.getType(param);
}
getList(val,type){
const param = {
val,
type,
}
return API.getList(param);
}
async handleSearch() {
const inpValue = this.state.inpValue;
const res = await this.getType(inpValue);
const type = res?.data?.type;
const res1 = await this.getList(inpValue,type);
console.log(res1);
}
在搜索事件handleSearch
触发时,就把输入框的值this.state.inpValue
存在inpValue
变量中,后续执行的事件用到输入框的值都去inpValue
变量取,后续再往输入框输入内容也不会影响到inpValue
变量的值,除非再次触发搜索事件handleSearch
。这样修改也可以解决这个BUG。
二、关于受控和非受控组件的收获
在React中正确理解受控和非受控组件的概念和作用至关重要,因为太多地方用到了。
- 受控组件
在Vue中有v-model
指令可以很轻松把组件和数据关联起来,而在React中没有这种指令,那怎么让组件和数据联系起来,这时候就要用到受控组件的概念。
受控组件,我理解为组件的状态由数据来控制,改变这个数据的方法却不是组件的,这里所说的组件不仅仅是组件,也可以表示一个原生DOM。比如一个input输入框。
input输入框的状态(输入框的值)由数据value
控制,改变数据value
的方法setValue
不是input输入框自身的。
import React, { useState } from "react";
const Input = () =>{
const [value,setValue] = useState('');
return (
<div>
<input
type="text"
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
</div>
);
}
export default Input;
再比如在Ant Design UI中的Form组件自定义表单控件时,要求自定义表单控件接受属性value
和onChange
,其中value
来作为自定义表单控件的值,onChange
事件来改变属性value
。
import React from 'react';
import { Form, Input } from 'antd';
const MyInput = (props) => {
const { value, onChange } = props;
const onNameChange = (e) => {
onChange?.(e.target.value)
}
return (
<Input
type="text"
value={value || ''}
onChange={onNameChange}
/>
)
}
const MyForm = () => {
const onValuesChange = (values) => {
console.log(values);
};
return (
<Form
name="demoFrom"
layout="inline"
onValuesChange={onValuesChange}
>
<Form.Item name="name">
<MyInput />
</Form.Item>
</Form>
)
}
export default MyForm;
我认为受控组件最大的作用是在第三方组件的状态经常莫名奇妙的改变时,用一个父组件将其包裹起来,传入一个要控制的状态和改变状态方法的props,把组件的状态提到父组件来控制,这样当组件的状态改变时就很清晰的知道哪个地方改变了组件的状态。
- 非受控组件
非受控组件就是组件自身状态完全由自身来控制,是相对某个组件而言,比如input输入框受Input组件控制,而Input组件不受Demo组件控制,那么Input相对Demo组件是非受控组件。
import React, { useState } from "react";
const Input = () =>{
const [value,setValue] = useState('');
return (
<div>
<input
type="text"
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
</div>
);
}
const Demo = () =>{
return (
<Input/>
)
}
export default Demo;
也可以把非受控组件理解为组件的值只能由用户设置,而不能通过代码控制。此外要记住一个非常特殊的DOM元素<input type="file" />
,其无法通过代码设置所上传的文件。
三、关于useState的收获
useState
可谓是使用频率最高的一个Hook,下面分享一下使用的3个心得。
useState
可以接收一个函数作为参数,把这个函数称为state初始化函数
一般场景下
useState
只要传入一个值就可以作为这个state的初始值,但是如果这个值要通过一定计算才能得出的呢?那么此时就可以传入一个函数,在函数中计算完成后,返回一个初始值。
import React, { useState } from 'react';
export default (props) => {
const { a } = props;
const [b] = useState(() => {
return a + 1;
})
return (
<div>{b}</div>
)
};
state更新如何使用旧的state
刚开始时,我这么使用的
const [a , setA] = useState(1);
const changeA = () =>{
setA(a + 1);
}
后来遇到一个错误,代码如下
const [a , setA] = useState(1);
const changeA = () =>{
setA(a + 1);
setA(a + 1);
}
在函数中连续调用两次
setA
会发现a
还是等于2,并不是等于3。正确要这么调用
const [a , setA] = useState(1);
const changeA = () =>{
setA( a => a+1 );
setA( a => a+1 );
}
如何拆分state
在函数式组件中,只要一个state改变都会引起组件的重新渲染。而在React中只要父组件重新渲染,其子组件都会被重新渲染。
那么问题就来了,如果把state拆分成多个,当依次改变这些state,则会多次触发组件重新渲染。
若不拆分state,改变state时,就只会触发一次组件重新渲染。但是要注意,函数式组件不像类组件那样,改变其中state中的一个数据,会自动更新state中对应的数据。要这么处理
const [data,setData] = useState({a:1,b:2,c:3});
const changeData = () =>{
setData(data =>{
...data,
a:2,
})
}
当然也不能不拆分state,这样代码复用性会大大降低。我的经验是:
将完全不相关的state拆分成一个个。
如果有些state是相互关联的,或是需要一起改变,那么将其合并为一个state。
四、关于useMemo和usecallback的收获
- 对其定义的理解
useCallback(fn,[a, b]);
useMemo(fn, [a, b]);
如上所示,useMemo
和usecallback
参数中fn
是一个函数,参数中[a,b]
的a
和b
可以是state或props。
useMemo
和usecallback
首次执行时,执行fn
后创建一个缓存并返回这个缓存,监听[a,b]
中的表示state或props的a
和b
,若值未发生变化直接返回缓存,若值发生变化则重新执行fn
再创建一个缓存并返回这个缓存。
useMemo
和usecallback
都会返回一个缓存,但是这个缓存各不相同,useMemo
返回一个值,这个值可以认为是执行fn
返回的。usecallback
返回一个函数,这个函数可以认为是fn
。那么注意了传给useMemo
的fn
必须返回一个值。
- 结合
React.memo
来使用
用React.memo
包裹一个组件,当组件的props发生改变时才会重新渲染。
若包裹的组件是个函数式组件,在其中拥有useState
、useReducer
、useContext
的 Hook,当state 或context发生变化时,它仍会重新渲染,不过这些影响不大,使用React.memo
的主要目的是控制父组件更新迫使子组件也更新的问题。
props值的类型可以为String、Boolean、Null、undefined、Number、Symbol、Object、Array、Function,简单的来说就是基础类型和引用类型。
两个值相等的基础类型的数据用==
比较返回true
,那两个值相等的引用类型的数据用==
比较返回false
,不信试一试以下代码,看是不是都为false
。
console.log({a:1} == {a:1});
console.log([1] == [1]);
const fn1 = () =>{console.log(1)};
const fn2 = () =>{console.log(1)};
console.log(fn1 == fn2);
正因为如此,当props的值为引用类型时,且这个值是通过函数计算出来的,用useMemo
或usecallback
来处理一下,避免计算出来的值相等,但是比较却不相等,导致组件更新。我认为usecallback
是专门为处理props值为函数而创建的Hook。
在下面的例子,List组件是一个渲染开销很大的组件,它有两个属性,其中data
属性是渲染列表的数据源,是一个数组,onClick
属性是一个函数。在Demo组件中引入List组件,用useMome
处理data
属性值,用useCallback
处理onClick
属性值,使得List组件是否重新渲染只受Demo组件的data
这个state控制。
List组件:
import React from 'react';
const List = (props) => {
const { onClick, data } = props;
//...
return (
<>
{data.map(item => {
return (
<div onClick={() =>{onClick(item)}} key={item.id}>{item.content}</div>
)
})}
</>
)
}
export default React.memo(List);
Demo组件:
import
React,
{ useState, useCallback, useMemo }
from 'react';
import List from './List';
const Demo = () => {
//...
const [data, setData] = useState([]);
const listData = useMemo(() => {
return data.filter(item => {
//...
})
}, [data])
const onClick = useCallback((item) => {
console.log(item)
}, []);
return (
<div>
<List onClick={onClick} data={listData} />
</div>
)
}
export default Demo;
可见useMemo
和usecallback
作为一个性能优化手段,可以在一点程度上解决React父组件更新,其子组件也被迫更新的性能问题。
- 单独使用
假设List组件不用React.memo
包裹,还能用useMemo
或usecallback
来优化吗?先来看一段代码,也可以这么使用组件。
import React, { useState } from 'react';
import List from './List';
export default function Demo() {
//...
const [data, setData] = useState([]);
const list = () => {
const listData = data.filter(item => {
//...
})
const onClick = (item) => {
console.log(item)
}
return (
<List onClick={onClick} data={listData} />
)
}
return (
<div>
{list()}
</div>
)
}
其中list
返回一个React元素,是一个值,那么是不是可以用useMemo
来处理一下。
import React, { useState, useMemo } from 'react';
import List from './List';
export default function Demo() {
//...
const [data, setData] = useState([]);
const listMemo = useMemo(() => {
const listData = data.filter(item => {
//...
})
const onClick = (item) => {
console.log(item)
}
return (
<List onClick={onClick} data={listData} />
)
}, [data])
return (
<div>
{listMemo}
</div>
)
}
listMemo
这个值(React元素)是否重新生成只受Demo组件的data
这个state控制。这样不是相当List组件是否重新渲染只受Demo组件的data
这个state控制。
- 不能滥用
不能认为“不管什么情况,只要用
useMemo
或useCallback
处理一下,就能远离性能的问题”
要认识到useMemo
和useCallback
也有一定的计算开销,例如useMemo
会缓存一些值,在后续重新渲染,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回缓存的值。这个过程有一定的计算开销。
所以在使用useMemo
和useCallback
前一定要考虑清楚使用场景,不能滥用。
下面总结一下要使用useMemo
和useCallback
的场景:
一个渲染开销很大的组件的属性是一个函数时,用
useCallback
处理一下这个属性值。
得到一个值,有很高得计算开销,用
useMemo
处理一下。
一个通过计算得到的引用类型的值,且这个值被赋值给一个渲染开销很大的组件的属性,用
useMemo
处理一下。
自定义Hook中暴露出的引用类型的值,用
useMemo
处理一下。
总之一句话使用useMemo
和useCallback
是为了保持引用相等和避免重复成本很高的计算。
五、关于useEffect和useLayoutEffect的收获
useEffect(fn);
useLayoutEffect(fn);
useEffect
可谓是使用频率第二高的一个Hook,useLayoutEffect
使用频率比较低。下面介绍四个使用心得:
- 执行的时机
在使用useEffect
和useLayoutEffect
之前,要搞明白传入这个两个Hook的函数会在什么时候执行。
传入useEffect
的函数是在React完成对DOM的更改,浏览器把更改后的DOM渲染出来后执行的。
传入useLayoutEffect
的函数是在React完成对DOM的更改后,浏览器把更改后的DOM渲染出来之前执行的。
所以useEffect
不会阻碍页面渲染,useLayoutEffect
会阻碍页面渲染,但是如果要在渲染前获取DOM元素的属性做一些修改,useLayoutEffect
是一个很好的选择。
- 只想执行一次
组件初始化和每次更新时都会重新执行传入useEffect
和useLayoutEffect
的函数。那只想在组件初始化时执行一次呢?相当Vue中的mounted
,这样实现:
useEffect(fn,[]);
useLayoutEffect(fn,[]);
- 用来进行事件监听的坑
上面介绍在useEffect
在第二参数传入一个空数组[]
相当Vue中的mounted
,那么在Vue的mounted
中经常会用addEventListener
监听事件,然后在beforeDestory
中用removeEventListener
移除事件监听。那用useEffect
实现一下。
useEffect(() => {
window.addEventListener('keypress', handleKeypress, false);
return () => {
window.removeEventListener('keypress', handleKeypress, false);
};
},[])
上面用来监听键盘回事事件,但是你会发现一个很奇怪的现象,有些页面回车后会执行handleKeypress
方法,有些页面回车后执行几次handleKeypress
方法后,就不再执行了。
这是因为一个useEffect
执行前会执行上一个useEffect
的传入函数的返回函数,这个返回函数可以用来解除绑定,取消请求,防止引起内存泄露。
此外组件卸载时,也会执行useEffect
的传入函数的返回函数。
示意如下代码所示:
window.addEventListener('keypress', handleKeypress, false); // 运行第一个effect
window.removeEventListener('keypress', handleKeypress, false);// 清除上一个effect
window.addEventListener('keypress', handleKeypress, false); // 运行下一个 effect
window.removeEventListener('keypress', handleKeypress, false); // 清除上一个effect
window.addEventListener('keypress', handleKeypress, false); // 运行下一个effect
window.removeEventListener('keypress', handleKeypress, false); // 清除最后一个effect
所以要解决上面的BUG,只要把useEffect
的第二参数去掉即可。这点跟Vue中的mounted
不一样。
- 用来监听某个state或prop
在useEffect
和useLayoutEffect
的第二参数是个数组,称为依赖,当依赖改变时候会执行传入的函数。
比如监听 a
这个state 和 b
这个prop,这样实现:
useEffect(fn,[a,b]);
useLayoutEffect(fn,[a,b]);
但是要注意不要一次性监听过多的state或prop,也就是useEffect
的依赖过多,如果过多要去优化它,否则会导致这个useEffect
难以维护。
我是这样来优化的:
- 考虑该依赖是否必要,不必要去掉它。
- 将
useEffect
拆分为更小的单元,每个useEffect
依赖于各自的依赖数组。 - 通过合并依赖中的相关state,将多个state聚合为一个state。
六、结语
以上五点就是我这六个月中印象最深的收获,有踩过的坑,有写被leader吐槽的代码。不过最大的收获还是薪资涨幅120%,哈哈哈,各位掘友要勇于跳出自己的舒适区,才能有更丰厚的收获。
虽然以上的收获在某些掘友们眼里会觉得比较简单,但是对于刚转入React的掘友们这些知识的使用频率非常高,要多多琢磨。如果有错误欢迎在评论中指出,或者掘友有更好的收获也在评论中分享一下。
作者:红尘炼心
链接:https://juejin.cn/post/7018328359742636039
收起阅读 »
转动的CSS“loading”,全都是技巧!
loader-1
这应该是最简单的CSS加载了。在圆圈上有一个红色的圆弧,仔细观察会发现,这个圆弧正好是1/4.
实现逻辑:
一个宽高相等容器,设定border为白色。然后给底边bottom设置红色,
当设定border-radius是50%,那他正好可以变成一个圆。
给这个圆加上旋转的动画。CSS中旋转角度的动画是rotate()我们只要给他设定从0旋转到360即可。(这个动画会在下面多次使用,下文不再赘述)
@-webkit-keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
完整代码
.loader-1 {
width: 48px;
height: 48px;
border: 5px solid #FFF;
border-bottom-color: #FF3D00;
border-radius: 50%;
display: inline-block;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}
loader-2
观察:外围是一个圈,内部有一个红色的元素在转动。
实现逻辑
一个宽高相等的容器,加上白色的边,50%的圆角。这样就是外围的圈。
里面的红色是如何实现?这里有两个思路。1;新增一个小div,让他在里面,并且跟loader-1一样,设置一个红色的底边。2:使用::after,思路跟方法1 一致。
加上旋转的动画。
完整代码
.loader-2 {
width: 48px;
height: 48px;
border: 3px solid #FFF;
border-radius: 50%;
display: inline-block;
position: relative;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}
.loader-2:after {
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
border-radius: 50%;
border: 3px solid transparent;
border-bottom-color: #FF3D00;
}
loader-3
观察:内部是一个圆,外围是一个红色的圆弧。
实现逻辑
这个加载效果跟loader-2是一致的,区别就是红色圆弧在内外。
完整代码
.loader-3 {
width: 48px;
height: 48px;
border: 3px solid #FFF;
border-radius: 50%;
display: inline-block;
position: relative;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}
.loader-3:after {
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 56px;
height: 56px;
border-radius: 50%;
border: 3px solid transparent;
border-bottom-color: #FF3D00;
}
loader-4
观察:外围是一个圆,内部有两个圆,这两个圆正好是对称的。
实现逻辑
一个宽高相等的容器,加上白色的边,50%的圆角。这样就是外围的圈。
里面的红色是如何实现?这里有两个思路。1;新增两个小div,背景颜色设置为红色,然后设置50%圆角,这样看上去就像是两个小点。2:使用::after和::before,思路跟方法1 一致。
加上旋转的动画。
完整代码
.loader-4 {
width: 48px;
height: 48px;
border: 2px solid #FFF;
border-radius: 50%;
display: inline-block;
position: relative;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}
.loader-4:before {
left: auto;
top: auto;
right: 0;
bottom: 0;
content: "";
position: absolute;
background: #FF3D00;
width: 6px;
height: 6px;
transform: translate(150%, 150%);
border-radius: 50%;
}
.loader-4:after {
content: "";
position: absolute;
left: 0;
top: 0;
background: #FF3D00;
width: 6px;
height: 6px;
transform: translate(150%, 150%);
border-radius: 50%;
}
loader-5
观察:一共是三层,最外层白圈,中间红圈,里边白圈。每一圈都有一个一半圆弧的缺口,外圈和最内圈的旋转方向一致。
实现逻辑
一个宽高相等的容器,加上白色的边,50%的圆角。这样就是外围的圈。
这里的问题是,圈的缺口如何实现,其实很简单,在css中有一个属性值:transparent,利用这个值给边框设置透明,即可实现缺口。
对于内部的红色和白色圆弧,继续使用::after和::before即可。
加上动画,这里有一个反方向旋转的动画(rotationBack)。
这里设置旋转是往负角度,旋转即可反方向旋转。
@keyframes rotationBack {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-360deg);
}
}
完整代码
.loader-5 {
width: 48px;
height: 48px;
border-radius: 50%;
display: inline-block;
position: relative;
border: 3px solid;
border-color: #FFF #FFF transparent transparent;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}
.loader-5:before {
width: 32px;
height: 32px;
border-color: #FFF #FFF transparent transparent;
-webkit-animation: rotation 1.5s linear infinite;
animation: rotation 1.5s linear infinite;
}
.loader-5:after, .loader-5:before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
border: 3px solid;
border-color: transparent transparent #FF3D00 #FF3D00;
width: 40px;
height: 40px;
border-radius: 50%;
-webkit-animation: rotationBack 0.5s linear infinite;
animation: rotationBack 0.5s linear infinite;
transform-origin: center center; *
}
loader-6
观察:看上去像是一个时钟,一个圆里面有一根指针。
实现逻辑
一个宽高相等的容器,加上白色的边,50%的圆角。这样就是外围的圈。
指针是如何实现的:从这里开始不再讨论新增div的情况。
其实红色的指针就是一个单纯的宽高不一致的容器。
完整代码
.loader-6 {
width: 48px;
height: 48px;
border: 2px solid #FFF;
border-radius: 50%;
display: inline-block;
position: relative;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}
.loader-6:after {
content: "";
position: absolute;
left: 50%;
top: 0;
background: #FF3D00;
width: 3px;
height: 24px;
transform: translateX(-50%);
}
loader-7
观察:首先确定几个圈,一共两个。当第一个圈还没消失,第二个圈已经出现。最后出现了类似水波的效果。同时要注意的是,这两个两个圈是一样大的,这是因为他们最终消失的地方是一致的。
实现逻辑
首先确定,这两个圈是否在容器上。上面一直时在容器上添加边框,当然这个例子也可以,但是为了实现的简单,我们把这两个圈放在::after和::before中。
加上动画,这里的圈是逐渐放大的,在CSS中scale用来放大缩小元素。同时为了实现波纹逐渐清晰的效果,我们加上透明度。
@keyframes animloader7 {
0% {
transform: scale(0);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}
完整代码
这里因为两个圈是先后出现的,所以需要一个圈加上delay
.loader-7 {
width: 48px;
height: 48px;
display: inline-block;
position: relative;
}
.loader-7::after, .loader--7::before {
content: "";
width: 48px;
height: 48px;
border-radius: 50%;
border: 2px solid #FFF;
position: absolute;
left: 0;
top: 0;
-webkit-animation: animloader7 2s linear infinite;
animation: animloader7 2s linear infinite;
}
.loader-7::after {
-webkit-animation-delay: 1s;
animation-delay: 1s;
}
.loader-7::after, .loader-7::before {
content: "";
width: 48px;
height: 48px;
border-radius: 50%;
border: 2px solid #FFF;
position: absolute;
left: 0;
top: 0;
-webkit-animation: animloader7 2s linear infinite;
animation: animloader7 2s linear infinite;
}
loader-8
观察:一段圆弧加上一个三角形。
实现逻辑
一个宽高相等的容器,加上白色的边,50%的圆角。这样就是外围的圈。
transparent,利用这个值给边框设置透明,即可实现缺口。
在:after上创建箭头。CSS中我们有多种方法实现三角形,其中最简单是使用border,不需要给元素设置宽高,只需要设置border的大小,并且只有一边设置颜色。
border: 10px solid transparent;
border-right-color: #FFF
加上旋转动画。
完整代码
.loader-8 {
width: 48px;
height: 48px;
border: 3px solid #FFF;
border-bottom-color: transparent;
border-radius: 50%;
display: inline-block;
position: relative;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}
.loader-8:after {
content: "";
position: absolute;
left: 20px;
top: 31px;
border: 10px solid transparent;
border-right-color: #FFF;
transform: rotate(-40deg);
}
收起阅读 »
有了for循环 为什么还要forEach?
js中那么多循环,for
for...in
for...of
forEach
,有些循环感觉上是大同小异今天我们讨论下for
循环和forEach
的差异。
我们从几个维度展开讨论:
for
循环和forEach
的本质区别。
for
循环和forEach
的语法区别。
for
循环和forEach
的性能区别。
本质区别
for
循环是js提出时就有的循环方法。forEach
是ES5提出的,挂载在可迭代对象原型上的方法,例如Array
Set
Map
。
forEach
是一个迭代器,负责遍历可迭代对象。那么遍历,迭代,可迭代对象分别是什么呢。
遍历:指的对数据结构的每一个成员进行有规律的且为一次访问的行为。
迭代:迭代是递归的一种特殊形式,是迭代器提供的一种方法,默认情况下是按照一定顺序逐个访问数据结构成员。迭代也是一种遍历行为。
可迭代对象:ES6中引入了 iterable
类型,Array
Set
Map
String
arguments
NodeList
都属于 iterable
,他们特点就是都拥有 [Symbol.iterator]
方法,包含他的对象被认为是可迭代的 iterable
。
在了解这些后就知道 forEach
其实是一个迭代器,他与 for
循环本质上的区别是 forEach
是负责遍历(Array
Set
Map
)可迭代对象的,而 for
循环是一种循环机制,只是能通过它遍历出数组。
再来聊聊究竟什么是迭代器,还记得之前提到的 Generator 生成器,当它被调用时就会生成一个迭代器对象(Iterator Object),它有一个 .next()
方法,每次调用返回一个对象{value:value,done:Boolean}
,value
返回的是 yield
后的返回值,当 yield
结束,done
变为 true
,通过不断调用并依次的迭代访问内部的值。
迭代器是一种特殊对象。ES6规范中它的标志是返回对象的 next()
方法,迭代行为判断在 done
之中。在不暴露内部表示的情况下,迭代器实现了遍历。看代码
let arr = [1, 2, 3, 4] // 可迭代对象
let iterator = arr[Symbol.iterator]() // 调用 Symbol.iterator 后生成了迭代器对象
console.log(iterator.next()); // {value: 1, done: false} 访问迭代器对象的next方法
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 4, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
我们看到了。只要是可迭代对象,调用内部的 Symbol.iterator
都会提供一个迭代器,并根据迭代器返回的next
方法来访问内部,这也是 for...of
的实现原理。
let arr = [1, 2, 3, 4]
for (const item of arr) {
console.log(item); // 1 2 3 4
}
把调用 next
方法返回对象的 value
值并保存在 item
中,直到 value
为 undefined
跳出循环,所有可迭代对象可供for...of
消费。 再来看看其他可迭代对象:
function num(params) {
console.log(arguments); // Arguments(6) [1, 2, 3, 4, callee: ƒ, Symbol(Symbol.iterator): ƒ]
let iterator = arguments[Symbol.iterator]()
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 4, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
}
num(1, 2, 3, 4)
let set = new Set('1234')
set.forEach(item => {
console.log(item); // 1 2 3 4
})
let iterator = set[Symbol.iterator]()
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 4, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
所以我们也能很直观的看到可迭代对象中的 Symbol.iterator
属性被调用时都能生成迭代器,而 forEach
也是生成一个迭代器,在内部的回调函数中传递出每个元素的值。
(感兴趣的同学可以搜下 forEach
源码, Array
Set
Map
实例上都挂载着 forEach
,但网上的答案大多数是通过 length
判断长度, 利用for
循环机制实现的。但在 Set
Map
上使用会报错,所以我认为是调用的迭代器,不断调用 next
,传参到回调函数。由于网上没查到答案也不妄下断言了,有答案的人可以评论区给我留言)
for
循环和forEach
的语法区别
了解了本质区别,在应用过程中,他们到底有什么语法区别呢?
forEach
的参数。forEach
的中断。forEach
删除自身元素,index不可被重置。for
循环可以控制循环起点。
forEach
的参数
我们真正了解 forEach
的完整传参内容吗?它大概是这样:
arr.forEach((self,index,arr) =>{},this)
self: 数组当前遍历的元素,默认从左往右依次获取数组元素。
index: 数组当前元素的索引,第一个元素索引为0,依次类推。
arr: 当前遍历的数组。
this: 回调函数中this指向。
let arr = [1, 2, 3, 4];
let person = {
name: '技术直男星辰'
};
arr.forEach(function (self, index, arr) {
console.log(`当前元素为${self}索引为${index},属于数组${arr}`);
console.log(this.name+='真帅');
}, person)
我们可以利用 arr
实现数组去重:
let arr1 = [1, 2, 1, 3, 1];
let arr2 = [];
arr1.forEach(function (self, index, arr) {
arr.indexOf(self) === index ? arr2.push(self) : null;
});
console.log(arr2); // [1,2,3]
forEach
的中断
在js中有break
return
continue
对函数进行中断或跳出循环的操作,我们在 for
循环中会用到一些中断行为,对于优化数组遍历查找是很好的,但由于forEach
属于迭代器,只能按序依次遍历完成,所以不支持上述的中断行为。
let arr = [1, 2, 3, 4],
i = 0,
length = arr.length;
for (; i < length; i++) {
console.log(arr[i]); //1,2
if (arr[i] === 2) {
break;
};
};
arr.forEach((self,index) => {
console.log(self);
if (self === 2) {
break; //报错
};
});
arr.forEach((self,index) => {
console.log(self);
if (self === 2) {
continue; //报错
};
});
如果我一定要在 forEach
中跳出循环呢?其实是有办法的,借助try/catch
:
try {
var arr = [1, 2, 3, 4];
arr.forEach(function (item, index) {
//跳出条件
if (item === 3) {
throw new Error("LoopTerminates");
}
//do something
console.log(item);
});
} catch (e) {
if (e.message !== "LoopTerminates") throw e;
};
若遇到 return
并不会报错,但是不会生效
let arr = [1, 2, 3, 4];
function find(array, num) {
array.forEach((self, index) => {
if (self === num) {
return index;
};
});
};
let index = find(arr, 2);// undefined
forEach
删除自身元素,index不可被重置
在 forEach
中我们无法控制 index
的值,它只会无脑的自增直至大于数组的 length
跳出循环。所以也无法删除自身进行index
重置,先看一个简单例子:
let arr = [1,2,3,4]
arr.forEach((item, index) => {
console.log(item); // 1 2 3 4
index++;
});
index
不会随着函数体内部对它的增减而发生变化。在实际开发中,遍历数组同时删除某项的操作十分常见,在使用forEach
删除时要注意。
for
循环可以控制循环起点
如上文提到的 forEach
的循环起点只能为0不能进行人为干预,而for
循环不同:
let arr = [1, 2, 3, 4],
i = 1,
length = arr.length;
for (; i < length; i++) {
console.log(arr[i]) // 2 3 4
};
那之前的数组遍历并删除滋生的操作就可以写成
let arr = [1, 2, 1],
i = 0,
length = arr.length;
for (; i < length; i++) {
// 删除数组中所有的1
if (arr[i] === 1) {
arr.splice(i, 1);
//重置i,否则i会跳一位
i--;
};
};
console.log(arr); // [2]
//等价于
var arr1 = arr.filter(index => index !== 1);
console.log(arr1) // [2]
for
循环和forEach
的性能区别
在性能对比方面我们加入一个 map
迭代器,它与 filter
一样都是生成新数组。我们对比 for
forEach
map
的性能在浏览器环境中都是什么样的:
性能比较:for > forEach > map
在chrome 62 和 Node.js v9.1.0环境下:for
循环比 forEach
快1倍,forEach
比 map
快20%左右。
原因分析
for
:for循环没有额外的函数调用栈和上下文,所以它的实现最为简单。
forEach
:对于forEach来说,它的函数签名中包含了参数和上下文,所以性能会低于 for
循环。
map
:map
最慢的原因是因为 map
会返回一个新的数组,数组的创建和赋值会导致分配内存空间,因此会带来较大的性能开销。如果将map
嵌套在一个循环中,便会带来更多不必要的内存消耗。
当大家使用迭代器遍历一个数组时,如果不需要返回一个新数组却使用 map
是违背设计初衷的。在我前端合作开发时见过很多人只是为了遍历数组而用 map
的:
let data = [];
let data2 = [1,2,3];
data2.map(item=>data.push(item));
写在最后:这是我面试遇到的一个问题,当时只知道语法区别。并没有从可迭代对象,迭代器,生成器和性能方面,多角度进一步区分两者的异同,我也希望我能把一个简单的问题从多角度展开细讲,让大家正在搞懂搞透彻。
如何在你的项目中使用新的ES规范
JavaScript 和 ECMAScript 的关系
JavaScript
是一种高级的、编译型的编程语言。而 ECMAScript
是一种规范。
JavaScript 是基于 ECMAScript 规范的脚本语言。ECMAScript
(以下简称 ES)在 2015 年发布了 ES6(ECMAScript 2015),而且 TC39 委员会决定每年发布一个 ECMAScript 的版本,也就是我们看到的 ES6/7/8/9/11/12
。
ES11 中两个非常有用的特性
空值合并操作符(??)
Nullish coalescing Operator(空值处理)只有 null 和 undefined 的时候才认为真的是空。
以下为使用方式:
let user = {
u1: 0,
u2: false,
u3: null,
u4: undefined
u5: '',
}
let u1 = user.u1 || '用户1' // 用户1
let u2 = user.u2 || '用户2' // 用户2
let u3 = user.u3 || '用户3' // 用户3
let u4 = user.u4 || '用户4' // 用户4
let u5 = user.u5 || '用户5' // 用户5
// es11语法【只有 null 和 undefined 的时候才认为真的是空】
let u1 = user.u1 ?? '用户1' // 0
let u2 = user.u2 ?? '用户2' // false
let u3 = user.u3 ?? '用户3' // 用户3
let u4 = user.u4 ?? '用户4' // 用户4
let u5 = user.u5 ?? '用户5' // ''
应用的场景:后端返回的数据中 null 和 0 表示的意义可能是不一样的,null 表示为空,展示成 /
。0 还是有数值,展示为 0。
let a = 0;
console.log(a ?? '/'); // 0
a = null;
console.log(a ?? '/'); // '/'
Optional chaining(可选链)
可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之处在于,在引用为空(nullish ) (null 或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined。
这个有点类似于 lodash
工具库中的 get
方法
let user = {
age: 18
}
let u1 = user.childer.name // TypeError: Cannot read property 'name' of undefined
// es11 语法
let u1 = user.childer?.name // undefined
浏览器兼容性问题
虽然 ES
新的特性十分好用,但需要注意的是它们的兼容性问题。
比如,可选链目前的兼容性如下:
解决方法就是讲 ES
新特性的语法转换成 ES5
的语法。
使用 Babel 进行转换
Babel
是一个 JavaScript
编译器。它的输入是下一代 JavaScript
语法书写的代码,输出浏览器兼容的 JavaScript
代码。
我们可以通过 Babel
中的转换插件来进行语法转换。比如我们上面两个语法可以通过以下两个插件进行转换。
空值合并操作符。@babel/plugin-proposal-nullish-coalescing-operator
使用:
npm install --save-dev @babel/plugin-proposal-nullish-coalescing-operator
在配置文件中:
{
"plugins": ["@babel/plugin-proposal-nullish-coalescing-operator"]
}
就可以做到以下的转换,输入:
var foo = object.foo ?? "default";
输出:
var _object$foo;
var foo =
(_object$foo = object.foo) !== null && _object$foo !== void 0
? _object$foo
: "default";
同理,可选链操作符可以看 @babel/plugin-proposal-optional-chaining,还有其他的都可以在 @babel/preset-env
目录中找到。
测试
在 Firefox 中,下载比较老的版本。
const foo = null ?? 'default string';
console.log(foo);
// expected output: "default string"
const baz = 0 ?? 42;
console.log(baz);
运行上面的代码,报错:
项目中使用,成功。说明 polyfil
成功了。
总结
JavaScript
是基于 ECMAScript
规范的脚本语言。ECMAScript
规范的发展给前端开发带来了很多的便利,但我们在使用的时候应该使用 Babel
这种 JavaScript
编译器将其转换成浏览器兼容的代码。
作者:Gopal
链接:https://juejin.cn/post/7018174628090609701
收起阅读 »
Vue 3 凉凉了吗 - 10 个灵魂拷问
很多人问我,现在可以用 Vue 3 了吗,Vue 2升级成本高吗,我想借着早早聊的场子把大家经常问的问题,跟大家谈一谈我的看法,我会尽量公平公正,客观正向,但尽然是看法,难免会有一些有争议的地方,或者不认可的地方,你可以留言。我总结了 10 个问题,期望能帮助你在技术选型中起到一定的帮助:
0、升级 Vue 3 成本大吗
可大可小,如果你使用的Vue推荐的 template 语法,成本是非常小的,改个版本号就已经可以 run 起来了,但前提是它的周边库你也已经升级了,不信你试试,其实周边库的升级成本不能算做 Vue 3 的升级成本,因为就算没有 Vue 3,周边也会不断升级,只是刚好撞到了一起,然后这个锅就让 Vue 一起背了。
关于升级这方面我想也可以借鉴 Ant Design Vue 的渐进式升级姿势,Ant Design Vue 2.x 版本是改了少量的代码,让其在 Vue 3 下运行,然后慢慢的使用Composition API 迭代重构,对于简单的没有破坏性更新的继续在 2.x 下迭代小版本,然后将有大量重构代码,复杂度高的,有破坏性更新的在 3.x 版本上迭代升级。
业务代码的更新升级相较于组件库成本会更加可控,因为组件库一般都会用到一些黑科技或非文档API等等,会让升级成本变高,这也是为什么一些组件库迟迟不兼容 Vue3 的部分原因。
1、Vue 3 稳定了吗?
目前 Vue 3 已经相当稳定,除非你会用到各种黑科技,业务项目不应该有黑科技,如果用到了,千万不要写到简历里,相信我,“黑科技”不但不会加分,还会减分,因为所谓的黑科技,大概率是你写法就不对。不服来辩
2、Vue 3 生态不丰富?
你所谓的生态是指哪些?状态管理?路由管理?国际化?组件库?SSR?常用生态库都已经提供了 Vue 3 版本,而且 Vue 2 版本都在逐渐减少维护时间。
退一步讲,如果还不够,那可是造轮子,刷 KPI 的好机会,不是嘛。
3、Vue 3 的写法不习惯?
Composition API 只是可选项,你依然可以用 Option API,没有什么变化。但我们应该跳出舒适圈,拥抱未来,拥抱更好的东西。
4、Vue 3 好找工作吗?
前端技术风口已经不多了,Vue 3 算一个,遥想当年懂个生命周期、虚拟DOM就可以进大厂的时代,甚是想念。
5、Vue 3 不兼容 IE11?
是的,不兼容,如果公司业务需要兼容IE11,我给的方案是:先统计下你们有多少 IE11 用户,是否还值得投入精力兼容,推动去IE化是需要套路的,数据、成本、收益 PPT形式报告给老板,没有想想的那么难。再透漏下,react 版本的 Antd,也会在下一个大版本中不再兼容 IE 11。
6、Vue、React 如何选择?
还在纠结?工具人用哪个,它都只是工具,哪来的优越感?我用 angular,我骄傲了吗?
摸鱼小能手:哪个熟悉用哪个,哪个干活快用哪个
职场新人:公司用哪个就用哪个
KPI 高手:轮着换,使用 Vue(React) 重构 React(Vue) 项目,加载时间减少 30%,秒开率提升,转化率提升10%,带来收益 2千万/年,这TM得跳着升,没毛病吧
学生:都得学,前端框架还没复杂到二选一的地步
7、升级 Vue 3 带来的收益
性能提升,可维护性提升(主要还是看人),刷 KPI,升职加薪。
尤其是性能提升方面,我会在早早聊 Vue 专场给大家分享 Ant Design Vue 使用 Vue 3 重构,总结的一些经验。
8、何时使用 Vue 3?
别问,问就是现在
9、大厂都在用 React ?
其实并没有,我了解的百度、腾讯、京东、字节、快手、美团等等大厂都是 Vue 重度用户,阿里相对特殊些,只有少数部门在使用 Vue、Angular,之所以使用 React,不是说 Vue 不够好,只是最开始选择了React那些部门做的比较好,后来在 React 基建方面也已经做了很多工作,两套共存,有点浪费资源,仅此而已。至于那些个别团队,自带优越感式的招聘,大家可以忽略了,技术和氛围应该都不咋地。
10、硬广,Ant Design Vue 什么时候兼容 Vue 3?
Ant Design Vue 自 2.0 版本开始,已经全面兼容 Vue 3,目前文档站点默认还是 1.x 版本,是因为就像 Vue 3 一样,2.x 版本目前是 next tag,我们会在 Ant Design Vue 3.0 rc 后切回主站,没错 Ant Design Vue 已经 3.0 alpha 了。
所以 Vue 3 凉了吗,说真的,我也不知道,怎么算凉?从 Github Star、npm 下载量来看,都是呈上升趋势,我个人甚至押宝 Vue 3,已经在 6月份辞职,目前全职开源,押宝 Vue 3 了。
但是个别人有这种感觉,也是可以理解的,可以说 Vue 的成功,yyx 个人运营能力起到了至关重要的作用,react 反而低调了很多,因为 React 主要是为公司服务的,其次才是社区,他们没有运营的压力,也没有太大的动力去做运营。在 Vue 3 前期运营的过程中,或许过度强调了 Composition API,导致有部分人产生了不兼容误解,或许这部分人并不在少数,或许 Vue 应该将 Composition API 放在 3.2、3.3 的小版本上去迭代添加。
当然这都是猜测,10月23日,yyx 亲自为大家解读 Vue 3 及生态现状,这应该可以帮助你进一步做出决策。我也会为大家同步 Ant Design Vue 现状及未来规划,如果顺利,我们也会有新产品发布,但大概率要跳票了,哈哈哈,敬请期待吧。
React 中 setState 是一个宏任务还是微任务?
最近有个朋友面试,面试官问了个奇葩的问题,也就是我写在标题上的这个问题。
能问出这个问题,面试官应该对 React 不是很了解,也是可能是看到面试者简历里面有写过自己熟悉 React,面试官想通过这个问题来判断面试者是不是真的熟悉 React 🤣。
面试官的问法是否正确?
面试官的问题是,setState
是一个宏任务还是微任务,那么在他的认知里,setState
肯定是一个异步操作。为了判断 setState
到底是不是异步操作,可以先做一个实验,通过 CRA 新建一个 React 项目,在项目中,编辑如下代码:
import React from 'react';
import logo from './logo.svg';
import './App.css';
class App extends React.Component {
state = {
count: 1000
}
render() {
return (
<div className="App">
<img
src={logo} alt="logo"
className="App-logo"
onClick={this.handleClick}
/>
<p>我的关注人数:{this.state.count}</p>
</div>
);
}
}
export default App;
页面大概长这样:
上面的 React Logo 绑定了一个点击事件,现在需要实现这个点击事件,在点击 Logo 之后,进行一次 setState
操作,在 set 操作完成时打印一个 log,并且在 set 操作之前,分别添加一个宏任务和微任务。代码如下:
handleClick = () => {
const fans = Math.floor(Math.random() * 10)
setTimeout(() => {
console.log('宏任务触发')
})
Promise.resolve().then(() => {
console.log('微任务触发')
})
this.setState({
count: this.state.count + fans
}, () => {
console.log('新增粉丝数:', fans)
})
}
很明显,在点击 Logo 之后,先完成了 setState
操作,然后再是微任务的触发和宏任务的触发。所以,setState
的执行时机是早于微任务与宏任务的,即使这样也只能说它的执行时机早于 Promise.then
,还不能证明它就是同步任务。
handleClick = () => {
const fans = Math.floor(Math.random() * 10)
console.log('开始运行')
this.setState({
count: this.state.count + fans
}, () => {
console.log('新增粉丝数:', fans)
})
console.log('结束运行')
}
这么看,似乎 setState
又是一个异步的操作。主要原因是,在 React 的生命周期以及绑定的事件流中,所有的 setState
操作会先缓存到一个队列中,在整个事件结束后或者 mount 流程结束后,才会取出之前缓存的 setState
队列进行一次计算,触发 state 更新。只要我们跳出 React 的事件流或者生命周期,就能打破 React 对 setState
的掌控。最简单的方法,就是把 setState
放到 setTimeout
的匿名函数中。
handleClick = () => {
setTimeout(() => {
const fans = Math.floor(Math.random() * 10)
console.log('开始运行')
this.setState({
count: this.state.count + fans
}, () => {
console.log('新增粉丝数:', fans)
})
console.log('结束运行')
})
}
由此可见,setState
本质上还是在一个事件循环中,并没有切换到另外宏任务或者微任务中,在运行上是基于同步代码实现,只是行为上看起来像异步。所以,根本不存在面试官的问题。
React 是如何控制 setState 的 ?
前面的案例中,setState
只有在 setTimeout
中才会变得像一个同步方法,这是怎么做到的?
handleClick = () => {
// 正常的操作
this.setState({
count: this.state.count + 1
})
}
handleClick = () => {
// 脱离 React 控制的操作
setTimeout(() => {
this.setState({
count: this.state.count + fans
})
})
}
先回顾之前的代码,在这两个操作中,我们分别在 Performance 中记录一次调用栈,看看两者的调用栈有何区别。
在调用栈中,可以看到 Component.setState
方法最终会调用enqueueSetState
方法 ,而 enqueueSetState
方法内部会调用 scheduleUpdateOnFiber
方法,区别就在于正常调用的时候,scheduleUpdateOnFiber
方法内只会调用 ensureRootIsScheduled
,在事件方法结束后,才会调用 flushSyncCallbackQueue
方法。而脱离 React 事件流的时候,scheduleUpdateOnFiber
在 ensureRootIsScheduled
调用结束后,会直接调用 flushSyncCallbackQueue
方法,这个方法就是用来更新 state 并重新进行 render。
function scheduleUpdateOnFiber(fiber, lane, eventTime) {
if (lane === SyncLane) {
// 同步操作
ensureRootIsScheduled(root, eventTime);
// 判断当前是否还在 React 事件流中
// 如果不在,直接调用 flushSyncCallbackQueue 更新
if (executionContext === NoContext) {
flushSyncCallbackQueue();
}
} else {
// 异步操作
}
}
上述代码可以简单描述这个过程,主要是判断了 executionContext
是否等于 NoContext
来确定当前更新流程是否在 React 事件流中。
众所周知,React 在绑定事件时,会对事件进行合成,统一绑定到 document
上( react@17
有所改变,变成了绑定事件到 render
时指定的那个 DOM 元素),最后由 React 来派发。
所有的事件在触发的时候,都会先调用 batchedEventUpdates$1
这个方法,在这里就会修改 executionContext
的值,React 就知道此时的 setState
在自己的掌控中。
// executionContext 的默认状态
var executionContext = NoContext;
function batchedEventUpdates$1(fn, a) {
var prevExecutionContext = executionContext;
executionContext |= EventContext; // 修改状态
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
// 调用结束后,调用 flushSyncCallbackQueue
if (executionContext === NoContext) {
flushSyncCallbackQueue();
}
}
}
所以,不管是直接调用 flushSyncCallbackQueue
,还是推迟调用,这里本质上都是同步的,只是有个先后顺序的问题。
未来会有异步的 setState
如果你有认真看上面的代码,你会发现在 scheduleUpdateOnFiber
方法内,会判断 lane
是否为同步,那么是不是存在异步的情况?
function scheduleUpdateOnFiber(fiber, lane, eventTime) {
if (lane === SyncLane) {
// 同步操作
ensureRootIsScheduled(root, eventTime);
// 判断当前是否还在 React 事件流中
// 如果不在,直接调用 flushSyncCallbackQueue 更新
if (executionContext === NoContext) {
flushSyncCallbackQueue();
}
} else {
// 异步操作
}
}
React 在两年前,升级 fiber 架构的时候,就是为其异步化做准备的。在 React 18 将会正式发布 Concurrent
模式,关于 Concurrent
模式,官方的介绍如下。
什么是 Concurrent 模式?
Concurrent 模式是一组 React 的新功能,可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整。在 Concurrent 模式中,渲染不是阻塞的。它是可中断的。这改善了用户体验。它同时解锁了以前不可能的新功能。
作者:Shenfq
链接:https://juejin.cn/post/6992006476558499853
收起阅读 »
正确介绍自己的项目经验 再也不为面试发愁了
在面试时,经过简单寒暄后,面试官一般先从让候选人自我介绍开始,紧接着就是问候选人简历中所列的项目,让介绍下项目经验。常见的问法是,说下你最近的(或感觉不错的)一个项目。面试中很多人忽视对这一个环节的准备,不仅回答不了面试官的追问,甚至连自己的项目都讲不清楚,说起来磕磕巴巴,甚至有的人说出的项目经验从时间段或技术等方面和简历上的不匹配,这样无疑会让面试官对面试者的能力产生怀疑。
面试时7份靠能力,3份靠技能,本文将从“前期准备”和“面试技巧”两大层面告诉大家如何准备面试时的项目介绍,当然,这只是一家之言,没有最好的方式,只有更适合的方法,仁者见仁智者见智。
前期分析
知己知彼百战不殆。如果想打动面试官,那么你就必须得了解他到底想要从你口中了解到什么,哪些信息是他所想要的。
在面试前准备项目描述时,别害怕,因为面试官什么都不知道,最了解你项目的还是你自己。
面试官是人,不是神,拿到你的简历的时候,只能根据你所描述的项目去推测你的工作经历,是没法核实你的项目细节的(一般公司会到录用后,通过背景调查的方式来核实)。
更何况,你做的项目是以月或以年为单位算的,而面试官最多用30分钟来从你的简历上了解你的项目经验,所以你对项目的熟悉程度要远远超过面试官,所以你一点也不用紧张。而面试官想了解更多他想知道的你的工作方式及项目中所负责的内容、所用到的技术栈,就不得不从你的介绍中去深挖技术点,以期了解你对项目及技术的了解的深度。
首先从气势上就要告诉面试官,这项目就是你参与的,有你所负责的功能模块,让面试官不容置疑。
心态上准备好了,那么就要分析面试官想要考察什么呢?
- 表达能力。考察表达及逻辑思维能力,看面试者能不能在几分钟就跟一个完全没有参与过项目的人讲清楚这个项目。
- 实际工作经验。你在项目中中承担了什么角色,做了什么工作。这些工作中的技术含量及跟同事合作情况如何。另外可能会针对某个项目,不断深入问一些技术上的问题,或者是从侧面问技术类实现,这是为了深入核实你做项目的细节及对技术的理解运用。
- 解决问题能力。一般都会问项目难点,其实就是想知道当你遇到技术或业务难点,是如何思考并解决问题的。
- 项目复盘及经验总结能力。哪里是自己觉得做的成功的,哪里觉得没做好的,是否还有继续优化的空间。自己所做的项目对个人能力有哪些提升。
熟能生巧,对答自如。
首先是需要有个充足的准备,写项目经验一定要写自己熟悉的,因为面试官就会根据你写的项目经验提问。在面试前,就要在脑子里过一遍这个项目,准备好说辞,面试的时候自信点。讲清楚这个项目是满足什么需求的,开发过程中遇到哪些困难,自己怎么解决这些困难的。如果你经过充分准备,面试中也能答的很好,那面试官好感度就会增加,相反,如果面试中说的磕磕绊绊,那么可信度就会低了。
明确目标,控盘引导。
在面试前,你需要明确自己的面试目的,就是通过面试,争取拿到 Offer 。
最保守的方式就是在自己介绍项目的时候要么就是以面试官为主导,回答很简单,就是面试官问你一句你答一句。这会让面试官失去想了解你的信心,其次也会让自己错失表现自己,凸显重点思想的机会。做好防守虽然也是一种取胜的方式,但并非上策,很容易丢分。
讲自己简历中所列的项目一定要很清晰明了有逻辑,埋下后续可能会提问到的技术点,也给面试官留下一个好印象。如果项目经验介绍的好,有逻辑和引导力,那么就会给自己带来以下两点明显的好处:
- 给面试官带来的第一印象好,会让面试官感觉该候选人表述能力较强。
- 一般来说,面试官会根据候选人介绍的项目背景来提问题,假设面试时会问10个问题,那么至少有5个问题会根据候选人所介绍的项目背景来问,候选人如果说的好,那么就可以很好地引导面试官去问后继问题,这就相当于把提问权完全交由自己控制了。
如果你的工作经验比面试官还丰富的话,甚至还可以控制整个面试流程,甚至遇到 Nice 的面试官的话会以讨论的方式进行沟通。
既然面试官无法了解你的底细,那么他们怎么来验证你的项目经验和技术?下面总结了一些常用的提问方式。
面试技巧
内容上要对项目进行以下拆分,思考并进行总结,并试着口语化讲出来。
- 项目描述。用通俗易懂且简洁的方式简述项目,阐述整个项目及其背景、规模,不要有太多的技术词汇。
- 项目模块。2-3分钟的流程介绍,详细的列出项目实现的功能,各个模块,整个过程,大概思路。
- 项目技术栈。说出项目实现的技术栈和架构,能说出项目的不寻常之处,比如采用了某项新技术,采用了某个架框等,简要说明技术选型。
- 候选人的角色及责任。说出你在项目中的责任,所涉及的功能模块,运用的技术,碰到的难题、代码细节,技术点、应对措施。
- 项目总结,待优化点。
方法上可以使用万能的STAR原则
Situation(背景):做这个项目的背景是什么,比如这是个人项目还是团队项目,为什么需要做这个
项目,你的角色是什么,等等。
Target(目标):该项目要达成的目标是什么?为了完成这个目标有哪些困难?
Action(行动):针对所要完成目标,你做了哪些工作?如何克服了其中一些困难?
Result(结果):项目最终结果如何?有哪些成就?有哪些不足之处可以改进?
除了项目所取得的成绩之外,候选人还可以说明自己做完项目的感受,包括项目中哪些环节做的不错,哪些环节有提高的空间,自己在做这个项目中有何收获等。
无论是介绍自己的IT产品开发经历,还是在其他公司的实习项目经历,候选人都可以运用STAR法则来具体说明,轻松表现出自己分析阐述问题的清晰性、条理性和逻辑性。
但面试前如下的一些情况还是需要多加注意的。
回答很简单。问什么答什么,往往就一句话回答。如果你日常回答别人的问题或者之前面试中出现过类似情况就要有所改善了。这里应该将你知道的说出来,重点突出跟问题相关的思想、框架或技术点等。
扯闲篇,大忌。说少了太过于简短没有互动不好,自来熟,回答问题没有重点,没有逻辑,乱说一通也是大忌。会让面试官感觉你思路混乱,抓不到重点,只是拿其他方面的东西东拼西凑。
说的太过流利,也未必就是好事。虽然面试有所准备在面试官看来是好事,但是机械的准备好答案去背诵,主观上给人一种你并没有理解这个问题,只是靠记忆知道答案,后续面试官的问题也会相应的加大难度。这方面改善建议是适当停顿,做思考状,边思考边说,过程中同面试官有个眼神上的互动。
有的放矢的介绍技术细节。不要一次性过多的介绍技术细节,技术面点到为止,等面试官来问。因为面试官通常都有自己的面试节奏。所以技术点等着问的时候再多聊,可以先事先埋下技术点引导着面试官继续追问。
主动介绍项目亮点。因为面试官没有义务挖掘你的亮点,所以这就需要自己主动提。遇到不会的问题,就如实说这个技术点不会。或者半懂也可以直接说。甚至可以谈谈自己的见解。把自己了解的说说。
项目准备
一般来说,在面试前,大家应当准备项目描述的说辞,自信些,因为这部分你说了算,流利些,因为你经过充分准备后,可以知道你要说些什么。而且这些是你实际的项目经验(不是学习经验,也不是培训经验),那么一旦让面试官感觉你都说不上来,那么可信度就很低了。
不少人是拘泥于“项目里做了什么业务,以及代码实现的细节”,这就相当于把后继提问权直接交给面试官。下表列出了一些不好的回答方式。
不露痕迹地说出面试官爱听的话
在项目介绍的时候(当然包括后继的面试),面试官其实很想要听一些关键点,只要你说出来,而且回答相关问题比较好,这绝对是加分项。我在面试别人的时候,一旦这些关键点得到确认,我是绝对会在评语上加上一笔的。
下面列些面试官爱听的关键点和对应的说辞。
一旦有低级错误,可能会直接出局
面试过程中有些方面你是绝对不能出错,所以你在准备过程中需要尤其注意如下的因素。下面列了些会导致你直接出局的错误回答。
面试场景题
举一个例子,比如考察候选人是否聪明,star 法则会这样询问:
1.在刚才的项目中,你提到了公司业务发展很快,人手不够,你是如何应对的呢?
2.在你的项目里面解决了什么样的难题
3.在你的项目里面如何做的登录
4.前端的项目如何进行优化,移动端呢?
5.图片加载失败要做啥
6.让你带领一个小团队完成一个项目,你会怎么做?
7.项目的同源处理,跨域相关
8.如果再做这个项目,你会在哪些方面进行改善?
面试中,如果面试官让你描述一个自己比较得意的项目的时候,一定记得要遵循 STAR 法则进行回答。比如
为了整合 xxx 业务(S),我承担 xxx 角色,具体负责 xxx (T)。做了 xxx 事情(A),最后产生了 xxx 结果
然后在描述项目亮点的时候也一样,比如
由于项目 xxx 原因(S),我需要进行 xxx 改进(T),然后进行了 xxx 处理(A),最后产出了 xxx 结果,数据对比为 xxx
整体这样下来,会显得你很有思考力,且具有行动力,可以给企业创造出价值,这也是面试官评定候选人最关键的指标之一。
面试官的套路
面试时所问的问题基本分为两种:具象的问题和开放性的问题。
具象的问题基本都会参考工作经验按照 STAR 法则来进行,主要是了解基本的素养,技术深度和潜力。
开放性的问题基本是考察思维发散能力,考察在某个领域的深度和广度,基本上会结合技术问题来问,或者是结合工作内容来问。
比如:实现某种技术的 n 种方法?某种技术的实现原理?和什么什么相比有哪些优缺点?你对这项技术的思考是什么?
面试者的应对
1.就实际情况做回答,提前准备的时候多发散,多思考,多总结。这一块是可以自己准备的加分项。
2.发散性问题主要是看自己平时积累。首先基础知识要牢固,同时也要了解最新技术动态。面对这类问题切记也不能答非所问而跑题了。
注意:
1.避免拿别人的项目直接用
很多初级阶段的同学们,可能并没有实际的商业项目,或者所做过的项目类型有限,就直接从网上找项目当做自己的项目,直接使用是断不可取的,但是如果你仿造别人的项目自己去尝试着将功能实现,有自己的新得体验,这样在做的过程中也可以对项目中的功能点和技术栈有进一步的了解,不至于在面试的时候,磕磕巴巴,甚至将项目时间都搞错。
2.避免低级错误
很多基础相关的低级错误一定要杜绝,如果被问到熟悉知识点就多答,不熟悉就直接说不熟悉。每个人都有自己擅长的点也有不擅长的。
另外就是可以引导一些话题,不要自说自话。很多人会一直很激进的表达自己,反而显得强势。有的面试者被问到数据库相关内容,他不仅回答数据库,还会把大数据处理技术全部都说出来。其实点到为止最好,面试官感兴趣会继续问,但是你一直主导话题,会减分。
这里要说的是,不要把不是自己做的项目说成是自己做的,自己不是核心负责人说成是负责人,即使你对项目很熟悉了解,像我们一线起来的面试官,问几个问题就很清楚你实际参与了多少了,只是大部分不会明说而已,反而起到反效果。
总结
首先我要劝大家,认真对待每一次面试。既然知道自己要参加面试,就在家自己模拟一下面试。自己提前准备一下自己的项目描述,不要到了面试的时候去打磕巴。但是如果你参加面试的时候实在紧张了,磕巴了不要慌。深呼吸尝试让自己放松,一般面试官也会给些提示帮助你回答的。
两句话,第一,面试前一定要准备,第二,本文给出是的方法,不是教条,大家可以按本文给出的方向结合自己的项目背景做准备,而不是死记硬背本文给出的一些说辞。
作者:Gaby
链接:https://juejin.cn/post/7017732278509453348
收起阅读 »
就因为JSON.stringify,我的年终奖差点打水漂了
产品同学在诉苦:线上用户不能提交表单了,带来了好多客诉,估计会是p0故障,希望尽快解决。
测试同学在纳闷:这个场景测试和预发环境明明验过的,怎么线上就不行了。
后端同学在讲原因:接口缺少了value字段,导致出错了。
就是木有人说问题怎么解决!!!
就是木有人说问题怎么解决!!!
就是木有人说问题怎么解决!!!
这样的场景不知道你是不是也似曾相识呢?o(╥﹏╥)o,不管咋说第一要务还是先把线上问题解决掉,减少持续影响,赶紧把交接的代码翻出来,开始了排查过程。
问题原因
如下图:有这样一个动态表单搜集页面,用户选择或者填写了信息之后(
各字段非必填情况下也可以直接提交
),接着前端把数据发送给后端,结束,看起来没有多复杂的逻辑。
直接错误原因
非必填情况下,signInfo字段中经过
JSON.stringify
后的字符串对象缺少value
key,导致后端parse之后无法正确读取value值,进而报接口系统异常,用户无法进行下一步动作。
// 异常入参数据,数组字符串中没有value key
{
signInfo: '[{"fieldId":539},{"fieldId":540},{"fieldId":546,"value":"10:30"}]'
}
// 正常入参数据
{
signInfo: '[{"fieldId":539,"value":"银卡"},{"fieldId":540,"value":"2021-03-01"},{"fieldId":546,"value":"10:30"}]'
}
异常数据是如何产生的
// 默认情况下数据是这样的
let signInfo = [
{
fieldId: 539,
value: undefined
},
{
fieldId: 540,
value: undefined
},
{
fieldId: 546,
value: undefined
},
]
// 经过JSON.stringify之后的数据,少了value key,导致后端无法读取value值进行报错
// 具体原因是`undefined`、`任意的函数`以及`symbol值`,出现在`非数组对象`的属性值中时在序列化过程中会被忽略
console.log(JSON.stringify(signInfo))
// '[{"fieldId":539},{"fieldId":540},{"fieldId":546}]'
解决方案
问题的原因找到了,解决方式 (这里只讲前端的解决方案,当然也可以由后端解决) 也很简单,将value值为undefined的项转化为空字符串再提交即可。
方案一:新开一个对象处理
let signInfo = [
{
fieldId: 539,
value: undefined
},
{
fieldId: 540,
value: undefined
},
{
fieldId: 546,
value: undefined
},
]
let newSignInfo = signInfo.map((it) => {
const value = typeof it.value === 'undefined' ? '' : it.value
return {
...it,
value
}
})
console.log(JSON.stringify(newSignInfo))
// '[{"fieldId":539,"value":""},{"fieldId":540,"value":""},{"fieldId":546,"value":""}]'
方案二:利用JSON.stringify
第二个参数,直接处理
方案一的缺陷是需要新开一个对象进行一顿操作才能解决,
不够优雅
let signInfo = [
{
fieldId: 539,
value: undefined
},
{
fieldId: 540,
value: undefined
},
{
fieldId: 546,
value: undefined
},
]
// 判断到value为undefined,返回空字符串即可
JSON.stringify(signInfo, (key, value) => typeof value === 'undefined' ? '' : value)
// '[{"fieldId":539,"value":""},{"fieldId":540,"value":""},{"fieldId":546,"value":""}]'
故事后续
原本这是一个已经上线有一段时间的页面,为何会突然出现这个问题,之前却没有呢?仔细询问下,原来是中途产品同学提了一个小的优化点,离职的小伙伴感觉点比较小直接就改了代码上线了,未曾想出现了线上问题。
后面针对这件事从产品到测试、到后端、到前端单独做了一个完整的复盘,细节就不再展开说了。
因为从发现问题到解决问题速度较快、影响用户数较少,还未达到问责程度,俺的年终奖可算是保住了o(╥﹏╥)o。
重学JSON.stringify
经过这件事情,我觉得有必要重新审视一下
JSON.stringify
这个方法,彻底搞清楚转换规则,并尝试手写实现一个JSON.stringify
如果你曾遇到和我一样的问题,欢迎一起来重新学习一次,一定会有不一样的收获噢!
学透JSON.stringify
JSON.stringify()
方法将一个JavaScript 对象
或值
转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性。
以下信息来自MDN
语法
JSON.stringify(value[, replacer [, space]])
value
将要序列化成 一个 JSON 字符串的值。
replacer
可选
- 如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;
- 如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;
- 如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。
space
可选
- 指定缩进用的空白字符串,用于美化输出(pretty-print);
- 如果参数是个数字,它代表有多少的空格;上限为10。
- 该值若小于1,则意味着没有空格;
- 如果该参数为字符串(当字符串长度超过10个字母,取其前10个字母),该字符串将被作为空格;
- 如果该参数没有提供(或者为 null),将没有空格。
返回值
一个表示给定值的JSON字符串。
- 当在循环引用时会抛出异常
TypeError
("cyclic object value")(循环对象值) - 当尝试去转换
BigInt
类型的值会抛出TypeError
("BigInt value can't be serialized in JSON")(BigInt值不能JSON序列化).
基本使用
注意
- JSON.stringify可以转换对象或者值(平常用的更多的是转换对象)
- 可以指定
replacer
为函数选择性的地替换 - 也可以指定
replacer
为数组,可转换指定的属性
这里仅仅是NDN上关于JSON.stringify
其中最基础的说明,咱们先打个码试试这几个特性
// 1. 转换对象
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy' })) // '{"name":"前端胖头鱼","sex":"boy"}'
// 2. 转换普通值
console.log(JSON.stringify('前端胖头鱼')) // "前端胖头鱼"
console.log(JSON.stringify(1)) // "1"
console.log(JSON.stringify(true)) // "true"
console.log(JSON.stringify(null)) // "null"
// 3. 指定replacer函数
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }, (key, value) => {
return typeof value === 'number' ? undefined : value
}))
// '{"name":"前端胖头鱼","sex":"boy"}'
// 4. 指定数组
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }, [ 'name' ]))
// '{"name":"前端胖头鱼"}'
// 5. 指定space(美化输出)
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }))
// '{"name":"前端胖头鱼","sex":"boy","age":100}'
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }, null , 2))
/*
{
"name": "前端胖头鱼",
"sex": "boy",
"age": 100
}
*/
9大特性要记住
以前仅仅是使用了这个方法,却没有详细了解他的转换规则,居然有9个之多。
特性一
undefined
、任意的函数
以及symbol值
,出现在非数组对象
的属性值中时在序列化过程中会被忽略undefined
、任意的函数
以及symbol值
出现在数组
中时会被转换成null
。undefined
、任意的函数
以及symbol值
被单独转换
时,会返回 undefined
// 1. 对象中存在这三种值会被忽略
console.log(JSON.stringify({
name: '前端胖头鱼',
sex: 'boy',
// 函数会被忽略
showName () {
console.log('前端胖头鱼')
},
// undefined会被忽略
age: undefined,
// Symbol会被忽略
symbolName: Symbol('前端胖头鱼')
}))
// '{"name":"前端胖头鱼","sex":"boy"}'
// 2. 数组中存在着三种值会被转化为null
console.log(JSON.stringify([
'前端胖头鱼',
'boy',
// 函数会被转化为null
function showName () {
console.log('前端胖头鱼')
},
//undefined会被转化为null
undefined,
//Symbol会被转化为null
Symbol('前端胖头鱼')
]))
// '["前端胖头鱼","boy",null,null,null]'
// 3.单独转换会返回undefined
console.log(JSON.stringify(
function showName () {
console.log('前端胖头鱼')
}
)) // undefined
console.log(JSON.stringify(undefined)) // undefined
console.log(JSON.stringify(Symbol('前端胖头鱼'))) // undefined
特性二
布尔值
、数字
、字符串
的包装对象在序列化过程中会自动转换成对应的原始值。
console.log(JSON.stringify([new Number(1), new String("前端胖头鱼"), new Boolean(false)]))
// '[1,"前端胖头鱼",false]'
特性三
所有以
symbol
为属性键的属性都会被完全忽略掉,即便replacer
参数中强制指定包含了它们。
console.log(JSON.stringify({
name: Symbol('前端胖头鱼'),
}))
// '{}'
console.log(JSON.stringify({
[ Symbol('前端胖头鱼') ]: '前端胖头鱼',
}, (key, value) => {
if (typeof key === 'symbol') {
return value
}
}))
// undefined
特性四
NaN 和 Infinity 格式的数值及 null 都会被当做 null。
console.log(JSON.stringify({
age: NaN,
age2: Infinity,
name: null
}))
// '{"age":null,"age2":null,"name":null}'
特性五
转换值如果有 toJSON() 方法,该方法定义什么值将被序列化。
const toJSONObj = {
name: '前端胖头鱼',
toJSON () {
return 'JSON.stringify'
}
}
console.log(JSON.stringify(toJSONObj))
// "JSON.stringify"
特性六
Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。
const d = new Date()
console.log(d.toJSON()) // 2021-10-05T14:01:23.932Z
console.log(JSON.stringify(d)) // "2021-10-05T14:01:23.932Z"
特性七
对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
let cyclicObj = {
name: '前端胖头鱼',
}
cyclicObj.obj = cyclicObj
console.log(JSON.stringify(cyclicObj))
// Converting circular structure to JSON
特性八
其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性
let enumerableObj = {}
Object.defineProperties(enumerableObj, {
name: {
value: '前端胖头鱼',
enumerable: true
},
sex: {
value: 'boy',
enumerable: false
},
})
console.log(JSON.stringify(enumerableObj))
// '{"name":"前端胖头鱼"}'
特性九
当尝试去转换
BigInt
类型的值会抛出错误
const alsoHuge = BigInt(9007199254740991)
console.log(JSON.stringify(alsoHuge))
// TypeError: Do not know how to serialize a BigInt
手写一个JSON.stringify
终于重新学完
JSON.stringify
的众多特性啦!咱们根据这些特性来手写一个简单版本的吧(无replacer函数和space)
源码实现
const jsonstringify = (data) => {
// 确认一个对象是否存在循环引用
const isCyclic = (obj) => {
// 使用Set数据类型来存储已经检测过的对象
let stackSet = new Set()
let detected = false
const detect = (obj) => {
// 不是对象类型的话,可以直接跳过
if (obj && typeof obj != 'object') {
return
}
// 当要检查的对象已经存在于stackSet中时,表示存在循环引用
if (stackSet.has(obj)) {
return detected = true
}
// 将当前obj存如stackSet
stackSet.add(obj)
for (let key in obj) {
// 对obj下的属性进行挨个检测
if (obj.hasOwnProperty(key)) {
detect(obj[key])
}
}
// 平级检测完成之后,将当前对象删除,防止误判
/*
例如:对象的属性指向同一引用,如果不删除的话,会被认为是循环引用
let tempObj = {
name: '前端胖头鱼'
}
let obj4 = {
obj1: tempObj,
obj2: tempObj
}
*/
stackSet.delete(obj)
}
detect(obj)
return detected
}
// 特性七:
// 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
if (isCyclic(data)) {
throw new TypeError('Converting circular structure to JSON')
}
// 特性九:
// 当尝试去转换 BigInt 类型的值会抛出错误
if (typeof data === 'bigint') {
throw new TypeError('Do not know how to serialize a BigInt')
}
const type = typeof data
const commonKeys1 = ['undefined', 'function', 'symbol']
const getType = (s) => {
return Object.prototype.toString.call(s).replace(/\[object (.*?)\]/, '$1').toLowerCase()
}
// 非对象
if (type !== 'object' || data === null) {
let result = data
// 特性四:
// NaN 和 Infinity 格式的数值及 null 都会被当做 null。
if ([NaN, Infinity, null].includes(data)) {
result = 'null'
// 特性一:
// `undefined`、`任意的函数`以及`symbol值`被`单独转换`时,会返回 undefined
} else if (commonKeys1.includes(type)) {
// 直接得到undefined,并不是一个字符串'undefined'
return undefined
} else if (type === 'string') {
result = '"' + data + '"'
}
return String(result)
} else if (type === 'object') {
// 特性五:
// 转换值如果有 toJSON() 方法,该方法定义什么值将被序列化
// 特性六:
// Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。
if (typeof data.toJSON === 'function') {
return jsonstringify(data.toJSON())
} else if (Array.isArray(data)) {
let result = data.map((it) => {
// 特性一:
// `undefined`、`任意的函数`以及`symbol值`出现在`数组`中时会被转换成 `null`
return commonKeys1.includes(typeof it) ? 'null' : jsonstringify(it)
})
return `[${result}]`.replace(/'/g, '"')
} else {
// 特性二:
// 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
if (['boolean', 'number'].includes(getType(data))) {
return String(data)
} else if (getType(data) === 'string') {
return '"' + data + '"'
} else {
let result = []
// 特性八
// 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性
Object.keys(data).forEach((key) => {
// 特性三:
// 所有以symbol为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。
if (typeof key !== 'symbol') {
const value = data[key]
// 特性一
// `undefined`、`任意的函数`以及`symbol值`,出现在`非数组对象`的属性值中时在序列化过程中会被忽略
if (!commonKeys1.includes(typeof value)) {
result.push(`"${key}":${jsonstringify(value)}`)
}
}
})
return `{${result}}`.replace(/'/, '"')
}
}
}
}
测试一把
// 1. 测试一下基本输出
console.log(jsonstringify(undefined)) // undefined
console.log(jsonstringify(() => { })) // undefined
console.log(jsonstringify(Symbol('前端胖头鱼'))) // undefined
console.log(jsonstringify((NaN))) // null
console.log(jsonstringify((Infinity))) // null
console.log(jsonstringify((null))) // null
console.log(jsonstringify({
name: '前端胖头鱼',
toJSON() {
return {
name: '前端胖头鱼2',
sex: 'boy'
}
}
}))
// {"name":"前端胖头鱼2","sex":"boy"}
// 2. 和原生的JSON.stringify转换进行比较
console.log(jsonstringify(null) === JSON.stringify(null));
// true
console.log(jsonstringify(undefined) === JSON.stringify(undefined));
// true
console.log(jsonstringify(false) === JSON.stringify(false));
// true
console.log(jsonstringify(NaN) === JSON.stringify(NaN));
// true
console.log(jsonstringify(Infinity) === JSON.stringify(Infinity));
// true
let str = "前端胖头鱼";
console.log(jsonstringify(str) === JSON.stringify(str));
// true
let reg = new RegExp("\w");
console.log(jsonstringify(reg) === JSON.stringify(reg));
// true
let date = new Date();
console.log(jsonstringify(date) === JSON.stringify(date));
// true
let sym = Symbol('前端胖头鱼');
console.log(jsonstringify(sym) === JSON.stringify(sym));
// true
let array = [1, 2, 3];
console.log(jsonstringify(array) === JSON.stringify(array));
// true
let obj = {
name: '前端胖头鱼',
age: 18,
attr: ['coding', 123],
date: new Date(),
uni: Symbol(2),
sayHi: function () {
console.log("hello world")
},
info: {
age: 16,
intro: {
money: undefined,
job: null
}
},
pakingObj: {
boolean: new Boolean(false),
string: new String('前端胖头鱼'),
number: new Number(1),
}
}
console.log(jsonstringify(obj) === JSON.stringify(obj))
// true
console.log((jsonstringify(obj)))
// {"name":"前端胖头鱼","age":18,"attr":["coding",123],"date":"2021-10-06T14:59:58.306Z","info":{"age":16,"intro":{"job":null}},"pakingObj":{"boolean":false,"string":"前端胖头鱼","number":1}}
console.log(JSON.stringify(obj))
// {"name":"前端胖头鱼","age":18,"attr":["coding",123],"date":"2021-10-06T14:59:58.306Z","info":{"age":16,"intro":{"job":null}},"pakingObj":{"boolean":false,"string":"前端胖头鱼","number":1}}
// 3. 测试可遍历对象
let enumerableObj = {}
Object.defineProperties(enumerableObj, {
name: {
value: '前端胖头鱼',
enumerable: true
},
sex: {
value: 'boy',
enumerable: false
},
})
console.log(jsonstringify(enumerableObj))
// {"name":"前端胖头鱼"}
// 4. 测试循环引用和Bigint
let obj1 = { a: 'aa' }
let obj2 = { name: '前端胖头鱼', a: obj1, b: obj1 }
obj2.obj = obj2
console.log(jsonstringify(obj2))
// TypeError: Converting circular structure to JSON
console.log(jsonStringify(BigInt(1)))
// TypeError: Do not know how to serialize a BigInt
复制代码
通过上面测试可以看出,jsonstringify
基本和JSON.stringify
表现一致,(也有可能测试用例不够全面,欢迎提出一起学习)
作者:前端胖头鱼
链接:https://juejin.cn/post/7017588385615200270
收起阅读 »
通过命令行玩转Git,需要记住那些命令?
Git 简介
什么是 Git
? Git
是目前世界上最先进的分布式版本控制系统!!!什么?啥意思?不懂,没关系,你只要记住,它很重要,非常重要,程序猿的必备技能即可。
Git
的命令非常非常多,这里强调一下,不要傻傻的去背这些命令,没啥卵用,有些命令可能你这辈子你都未必使得上。
本章的目的是教你如何通过命令行完成 Git
的日常基操,并会适当介绍命令的意义,加深你的理解。
在介绍这些命令之前,我们先来看一张灰常重要的图:
图中有四个空间,是 Git
工作流程的精髓所在,分别是:
- Remote: 远程仓库,即你在
Github
或者Gitee
等平台上创建的项目仓库; - Repository: 本地仓库,你可以认为就是我们拉取项目后生成的
.git
文件夹; - Index: 暂存区,事实上它只是一个文件,即
.git
文件夹里面的index
文件,它保存即将提交到本地仓库的文件列表信息; - workspace: 工作区,即你在
VS code
或者WebStorm
编译器正在编写的代码。
Git 基本命令手册
1.克隆/拉取项目
拉取项目是开始搬砖的第一步,一般创建好项目远程仓库,你就能获取到一个 .git
结尾的地址,或者这个地址可能由公司同事给到你,之后你随便找个目录,通过 Git Bash 输入下面命令即可拉取到项目到本地。
git clone 你的项目地址.git
2.查看远程分支
一般拉取完项目后,我们是处于 master/main
主分支,不要着急就去编写代码,先要查看一下项目的远程分支情况,根据要求看是否需要切换到某个分支进行特定开发。
git branch -r
3.查看本地分支
一般本地创建的分支情况应该是和远程分支一一对应,当然,在未进行发布或者和远程分支建立关联,本地分支并不会影响到远程分支,你可以随意创建、删除和修改。
git branch
4.创建本地分支
git branch dev(分支名称, 可以随便取)
通过上面的命令,你会创建好一个本地分支,当然,该分支是基于你当前所在分支创建的。你可以再次敲命令去查看远程分支和本地分支各自的情况(多敲,很快命令就记住了-^〇^-)。
5.删除本地分支
对远程仓库分支并不会有任何影响
git branch -d dev
6.切换分支
创建好本地分支,我们就可以切换到该分支上。
git checkout dev
创建和切换两个操作也可以一起做:
git checkout -b xxx
(创建并切换到该本地分支)
7.发布本地分支到远程分支
当我们创建好一个本地分支的时候,这个时候还是不要着急去开始编码,我们要把该分支发布到远程仓库去,让远程仓库也拥有该分支,且让它和你本地分支进行关联,方便我们后续直接进行 git pull
或者 git push
操作。
git push origin dev
发布完本地分支后,你可以同样通过 git branch -r
去查看你远程仓库的分支列表是否多了新的分支。
8.建立本地分支与远程分支的关联
本地分支与远程分支关联这步不是必须,但后续就能很方便的直接使用 git pull
或者 git push
获取或提交该分支的代码,而不用去指定分支。
git push --set-upstream origin dev
如果你不关联两者分支的关系,强行去使用,你可能会遇到图中的情况,大致意思就是让你指定目标分支或者去关联分支情况再进行操作。(Git
的提示信息还是很友好的)
9.添加文件进暂存区
完成上面的几步,我们就能开始搬砖了。在对代码更改后,要把提交代码到远程仓库,我们就要先把代码添加到暂存区,之后提交到本地仓库,最后才能提交到远程仓库。
- 把工作区某个文件添加进暂存区,比如
src/main.js
文件,则git add src/main.js
git add xxx(文件路径)
# 多个
git add xxx(文件路径) xxx(文件路径) xxx(文件路径) ...
- 把工作区更改的所有文件都添加进暂存区
git add .
这里你可能会在想,为什么要先添加到暂存区,再本地仓库之后才提交到远程仓库呢? 如果你是从
SVN
转过来,可能会稍微有点不了解暂存区,这里涉及暂存区的意义,网上有很多解释,这里就不做过多的解释了。
但你可以简单的怎么理解,假如你在开发中开发了用户的添加功能和文章的添加功能,两个功能都同时开发完了,因为都互不影响,完全独立,你想分成两次提交分别写上对应的commit
信息说明,这个时候就可以使用到暂存区,先将用户添加功能添加到暂存区,然后commit
到本地仓库,再进行文章添加功能的提交,最后在一起push
到远程仓库即可。
10.删除文件出暂存区
当你误把文件添加进暂存区,也不要慌,有添加,就肯定有删除。
- 把工作区某个文件删除出暂存区
git rm --cached xxx(文件路径)
- 清空暂存区,暂存区实质是
.git
目录下的index
文件,将此文件删除,那么就认为暂存区被清空。
rm .git/index
当然,这只是把暂存区中跟踪的文件移除而已,不会改动原文件的内容,原先更改的内容还在。
11.查看工作区与暂存区状态
这个命令用于查看工作区和暂存区的状态,能看到哪些文件被修改了,它修改后是否被暂存了,又或者还没有暂存。这个暂存的过程,专业的叫法是 Git tracked
,也就是是否被跟踪。
git status
通过下图,我们能查看到所有改动过的文件,绿色的文件是已经添加进暂存区的,红色的文件则是未添加到暂存区的,而且每个文件前都有对应的操作说明,如果是新文件则是 new file
,修改的文件则是 modified
,删除的是 deleted
, 如果是未添加进暂存区的新文件,则没有。
12.提交暂存区文件到本地仓库
git commit -m "说明信息"
通过上面的命令,我们就将暂存区的文件提交到本地仓库了,我们可以通过 git status
再次查看暂存区的情况。
Git
的提示真的是非常友好的。
13.查看提交记录
这个命令可以显示提交过的记录。
git log
进入提交记录日志列表后,可以按 q
退出。
上面的命令会显示所有的
commit
记录,如果你想要显示最近几条记录,你可以通过git log -n(n为数字, 可以随意指定)
命令完成。
14.提交本地仓库文件到远程仓库
git push
执行这条命令的前提是进行过第7步骤才行哦。
把本地仓库文件提交到远程仓库后,我们再次查看提交日志。
15.拉取新代码
git pull
这个命令同样也是建立在第7步骤之前的。
Git 进阶命令手册
记住上面15个命令,Git
的日常基操基本也满足了,当然现在各种编辑器功能强大,基本都集成了 Git
的可视化操作,不用命令行来操作,也完全没有问题。
但是程序猿使用命令行不是一件很酷的事情吗?不为别的,只为装13也是可以搞一搞的,哈哈哈。下面再来讲一些命令,虽然使用频率不高,但是也是很重要的。
1.查看全局的Git配置
git config --global -l
# or
git config --global --list
2.查看全局用户名及邮箱
git config --global user.name
git config --global user.email
3.设置全局用户名及邮箱
git config --global user.name "你自己的用户名"
git config --global user.email "你自己的邮箱"
4.查看局部项目用户名及邮箱
要在当前项目根目录下执行。
git config user.name
git config user.email
5.设置局部项目用户名及邮箱
要在当前项目根目录下执行。
git config user.name "你自己的用户名"
git config user.email "你自己的邮箱"
6.删除远程仓库分支
这个命令对于一些未设有保护的分支来说,是挺危险的操作,要慎重执行。
git push --delete origin dev(分支名称)
7.修改远程仓库分支名称
修改远程仓库分支的名称过程是:先修改本地分支名称 - 删除远程分支 - 重新发布本地分支到远程仓库
# 修改本地分支名称
git branch -m 旧分支名称 新分支名称
# 删除远程分支
git push --delete origin 分支名称
# 发布新分支
git push origin 新分支名称
# 重新建立关联
git push --set-upstream origin 新分支名称
8.合并分支
合并分支是一个比较常见的操作了,当你在一个分支开发完新功能后,很多时候这些分支最终都要合并到 master/main
这个主分支上,这个主分支拥有所有分支的代码,并且它是稳定且在生产环境上跑的,所以在你确定要将分支合并到主分支上之前,一定要确保这个分支代码是没有问题。
# 切换到稳定的目标分支
git checkout master
# 更新最新代码,防止本地仓库对应分支代码不够新而出现问题
git pull
# 合并分支到本地仓库
git merge dev
# 发布合并后的新代码到远程分支
git push
合并后,你可以通过 git log
去查看是否有相关的 commit
记录。
9.将A分支直接合并到B分支
git merge xxx(A分支名称) xxx(B分支名称)
10.合并单个commit
有时候我们只想合并某一个 commit
来满足一些特定需要,这也是可以做到的。
# 切换到稳定的目标分支
git checkout master
# 合并某个commit到本地仓库
git cherry-pick xxx(commitId)
# 发布合并后的新代码到远程分支
git push
commitId
是一个 commit
的唯一标识,你可以通过 git log
来找到它,它是一串很长不重复的字符。
当然,你也可以去到 Github
或者 Gitee
平台对应项目里面找。
合并单个
commit
你可能会遇到冲突的情况,如:在我执行合并某一个commit
给我报了一个错,大致意思就是代码冲突了,需要去调整后,才能提交。
代码冲突是件非常蛋疼的事情,不仅仅合并的时候会发生冲突,比较频繁发生的场景是多人共同开发的时候,因为两人负责同个项目,就有极大的可能会改到相同的文件,相同的代码,这就会造成冲突。一般这个时候
Git
会阻止你的提交,让你先去整改,这时候就需要你非常谨慎细心的去处理,因为一旦粗心,就极有可能干掉同伴的代码或者自己的代码,这会造成很严重的后果。(不要问我怎么知道的,都是血的教训︶︿︶)
解决代码冲突一般我会借助编辑器等工具来完成,就不通过命令行操作了,这样比较方便,不容易出错。我使用的是
VS code
编辑器,在编辑器内冲突的文件一般都会标红,点开文件,会发现里面会有写<<<<
的符号,被符号包围的内容就是冲突的地方。
编辑器一般会提供四个选项帮助你快速操作,当然你也可以手动删除修改。
- Accept Current Change:接收当前更改,也就是冲突部分以旧的为准。
- Accept Incoming Change:接受传入的更改,也就是冲突部分以新的为准。
- Accept Both Changes:接受两个更改,也就是新旧都存在,可能会出现重复。
- Compare Changes:比较变化,会分成两个文件,让你更直观的查看两者的冲突内容。
当你解决完冲突后,你可以保存文件,再执行以下操作,提交代码。
# 添加更改进入暂存区
git add .
# 提交暂存区到本地仓库
git commit -m ""
# 提交本地仓库commt到远程仓库
git push
11.撤销最近的一个commit
git reset --soft HEAD^
这个命令用于撤销本地仓库最近的一个 commit
,撤销后会回到暂存区中,通过 git log
可以查看 commit
记录会减少,但不影响远程仓库。
还有另一个相似的撤销 commit
命令,但它比较危险,撤销后的内容是直接就删除的,不会回到暂存区,要慎重使用!!!
git reset --hard HEAD^
12.查看简洁提交记录
这个命令可以让你更加直观的查看你需要的信息。
git log --pretty=oneline
其实这些信息都存放在
.git
文件夹中,在.git\logs\refs\heads
下记录了所有分支的commit
版本号。
13.备份工作区内容stash
这是一个神奇的命令,特别对项目分支比较多的情况,是非常有用的,他的使用场景大致是:当你正在A分支中开发功能,你临时接到一个紧急的需求,需要优先切换到B分支中去开发,这个时候A分支中已经改动的代码要怎么办呢?这时就能使用 stash
来备份代码了。
git stash
执行命令后,当前工作区会回到最近的一次 commit
状态,并将更改的代码保存在 Git
栈中,这样你就能先切换到B分支中去开发,等B分支功能开发完成,再切换回A分支,通过下面的命令取回原来的更改的代码,继续进行A分支的功能开发了。
git stash pop
是不是非常的 nice!!!
14.查看备份列表或清空备份列表
- 显示Git栈内的所有备份
git stash list
- 清空Git栈
git stash clear
15.查看远程仓库地址
git remote -v
16.更改远程仓库地址
git remote set-url origin xxx(新仓库地址)
更改远程仓库地址有另外的两种方式
# 删除项目的远程仓库地址
git remote rm origin
# 添加项目的远程仓库地址
git remote add origin xxx(新仓库地址)
或者去改配置文件
17.比较工作区和暂存区的所有差异
git diff
这个命令是对比工作区和暂存区的所有差异,但个人感觉不太直观,也可能只是比较少用吧,关于对比我更愿意借助编辑器工具来查看。
对比工作区单个文件和暂存区的区别:
git diff xxx(文件地址)
18.将工作区中的文件还原成与暂存区的一致
git checkout xxx(文件地址)
# 批量还原
git checkout xxx(文件地址) xxx(文件地址) xxx(文件地址) ...
收起阅读 »
优雅的命名
前言
优秀的代码往往是最通俗易懂的代码,在于它的易于维护。在开发过程中,变量/方法优秀的命名往往有助于理解该变量/方法的用途,起到命名即注释的作用。而糟糕的命名往往会让人摸不着头脑。为了提高代码的可维护性,我们需要更优雅的命名方式。
一、通用规则
1. 有意义
起一个有意义的变量名这条相信绝大多数开发者都能做到,即变量名有实际的指代意义,在此不再赘述。
2. 指代具体
命名时需要使其更加具体详尽,可以具体到所在的模块,或者能表达出其逻辑/功能。
/* bad */
.title {}
/* good */
.head-title {}
// bad
const info;
// good
const userInfo;
3. 遵循传统
无论是CSS、JavaScript、还是文件的命名,都有一些约定俗成的惯例和规范,我们只需遵循即可。
4. 约定规范
命名的规则有很多种,没有高低之分,只有相对合适,没有绝对完美的规则。通常为了维持项目的风格统一,通常在一个项目中,相同种类的规则只选取一种。毕竟规范也只是一种工具,运用规范的根本目的是为了更好的开发和维护,太过复杂的规范反而会阻碍正常开发。因之,在项目启动前,在技术栈选取后就应当进行规范的约定,这个过程是团队意见的整合,毕竟规范是要靠团队成员的相互配合。
二、CSS 中的命名
1. 划分原则
CSS中的类名根据其职责可以分为公共类名和自定义类名。其中公共类名又可以分为常见类名(一般是约定俗成)和工具类名。
2. 常见类名
下面整理了一些常见的 css类名
供大家参考:

❤️谈谈grid布局(细读必有收获)
grid布局的理念是把网页划分成一个一个网格组合成不同样式的布局,再通过对网格进行内容填充,组成一个网页。通过一下这个案例了解grid的基本概念👇👇
经典九宫格布局:
🚨关键点🚨:
容器: 需通过display:grid
设置为grid容器,容器中包含所有item
行: 横向为行,对应颜色块123
行距: 上下两个item的间距为行距
列: 纵向为列,对应颜色块147
列距: 左右两个item的间距为列距
item(子元素): 也就是上图对应的123456789颜色块
边: 每个itme共有 上 下 左 右 四条边
1.1 display
display属性规定是否/如何显示元素。我们需要使用grid布局,就要把容器设置为grid或者inline-grid
grid
设置为块级元素的grid布局
inline-grid
设置为行内元素的grid布局
区别如下:
代码案例
在线代码入口:👉👉(点击传送)
.grid_container {
display:grid;
/* display:inline-grid; */
grid-template-columns: 100px 100px 100px;
grid-template-rows: 100px 100px 100px;
}
1.2 grid-template-columns
和 grid-template-rows
grid-template-columns
属性用来定义grid布局的每一列列宽
grid-template-rows
属性用来定义grid布局的每一行行高
代码案例1:在线代码入口👉👉(点击传送)
定义一个三行三列,每列列宽100px,每行行高100px
.grid_container {
display:grid;
grid-template-columns: 100px 100px 100px;
grid-template-rows: 100px 100px 100px;
}
代码案例2:在线代码入口👉👉(点击传送)
当拥有很多行和列的时候,普通的写法根本不实在,所以现在引入一个函数repeat()
repeat()
函数可设置重复的值,或者重复的一个模式,还是以三行三列100px为例:
.grid_container {
display:grid;
/* 重复一个值 */
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
/* 重复一个模式
grid-template-columns: repeat(3,100px 50px);
grid-template-rows: repeat(3,100px 50px);
*/
}
代码案例3:在线代码入口👉👉(点击传送)
这里以圣杯布局为例:左右固定,中间自适应。在这种情况下固定的列宽或行高已经不能满足实现圣杯布局了,所以这个例子引入两个关键字auto
和fr
auto
:自适应属性
fr
:fraction 的缩写,意为"片段",可以看做百分比属性,通过以下例子可以帮助理解该关键字
以auto
为例:
.grid_container {
display:grid;
/* 左右列150px,中间列自适应*/
grid-template-columns: 150px auto 150px;
/* 一行行高 300px*/
grid-template-rows: repeat(1,300px);
}
以fr
为例:
左右列占比 2/10 = 20% ,中间列占比 6/10 = 60%, 注意10 = 2+6+2
#grid_container{
display: grid;
grid-template-columns: 2fr 6fr 2fr;
grid-template-rows: repeat(1,300px);
}
代码案例4:在线代码入口👉👉(点击传送)
当需求是要求每个item子元素的宽高只有100px,但是容器宽度自适应时,我们就无法得知应该设置几行几列的属性了,所以这里再引入一个关键字auto-fill
auto-fill
:自动填充
⚠️注意:grid-template-rows
需要使用关键字时,容器必须要有固定高度⚠️
#grid_container{
display: grid;
height:500px;
grid-template-columns: repeat(auto-fill,100px);
grid-template-rows: repeat(auto-fill,100px);
}
代码案例5:在线代码入口👉👉(点击传送)
如果grid布局的子元素设置为自适应宽度,但宽度缩小到一定程度时就会出现错误,所以避免出现这种错误,我们必须要有一个最小的宽度,所以这里引入一个函数minmax()
minmax()
:设置一个长度范围,参数1:最小值,参数2:最大值
例子:最小值500px,最大值6fr
.grid_container {
display:grid;
width:600px;
grid-template-columns: 2fr minmax(500px,6fr) 2fr;
/* 自行屏蔽查看区别 */
/* grid-template-columns: 2fr 6fr 2fr; */
grid-template-rows: repeat(1,300px);
}
1.3grid-template-areas
1.3 grid-template-areas
grid-template-areas
:用于划分区域,通过以下案例可以帮助理解
代码案例1:在线代码入口👉👉(点击传送)
1、划分出a
到i
九个区域
2、或者每一行划分一个区域,三行就是a b c
三个区域
2、当然可以不划分部分区域,使用(.
)点表示不需要划分的区域
.grid_container {
display:grid;
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
/* 划分九个区域 */
grid-template-areas:
'a b c'
'd e f'
'g h i';
/* 划分三个区域 */
/* grid-template-areas:
'a a a'
'b b b'
'c c c'; */
/* 不划分部分区域 */
/* grid-template-areas:
'a . c'
'a . c'
'a . c'; */
}
划分区域的用途会在后面结合其他的属性进行讲解!!
1.4 grid-row-gap
和 grid-column-gap
和 grid-gap
grid-row-gap
:行间距
grid-column-gap
:列间距
grid-gap
: 行间距 和 列间距 的简写形式,如:grid-gap: <grid-row-gap> <grid-column-gap>;
代码案例1:在线代码入口👉👉(点击传送)
这里以最简单的九宫格为例
.grid_container {
display:grid;
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
grid-row-gap:10px;
grid-column-gap:20px;
/* 下面语句和上面设置的间距效果相同,自行解除注释对比 */
/* grid-gap:10px 20px; */
}
1.5 grid-auto-flow
grid-auto-flow
:设置grid布局的放置顺序,正常情况下是,从左到右放置每一个item子元素,在特殊情况下我们可以重新改变它的放置顺序,比如从上到下。可选值:从左到右 row
、从上到下column
、稠密从左到右row dense
、稠密从上到下column dense
,接下来会一一举例说明;
正常设置grid-auto-flow
属性为 row
和 column
会出现以下两种效果,左边为row
,右边为column
这里还是以九宫格为例,我们将 数字1 和 数字2 和 数字3 方块设置为各占2个单元格时,在grid-auto-flow
属性默认等于row
就会出现以下一幕
当我们把代码设置成 稠密型的从左到右row dense
时,布局就会被尽可能的填满,不会出现上图存在的空格
代码如下:在线代码入口👉👉(点击传送)
.grid_container {
display:grid;
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
/* 默认,从左到右 */
grid-auto-flow:row;
/* 稠密型 从左到右 请自行开启屏蔽 */
/* grid-auto-flow:row dense; */
}
.item-1 {
background-color: #B53471;
grid-column-start: 1;
grid-column-end: 3;
}
.item-2 {
background-color: #ffcccc;
grid-column-start: 1;
grid-column-end: 3;
}
.item-3 {
background-color: #ff4d4d;
grid-column-start: 1;
grid-column-end: 3;
}
通过上面的例子可以清楚稠密型其实就是,尽可能填满容器而已,所以
column dense
例子就不多做解析,在线代码入口👉👉(点击传送)
1.6 justify-items
和 align-items
和 place-items
属性说明
justify-items
:设置item子元素内容水平位置
align-items
:设置item子元素内容垂直位置
place-items
:align-items
和 justify-items
两个属性的简写方式,若省略第二个值,则认为第二个值等于第一个值
place-items: <align-items> <justify-items>
属性可选值(三个属性均有以下可选值)
start案例:在线代码入口👉👉(点击传送)
对齐子元素容器的起始边框,justify-items
对齐水平的起始边框,align-items
对齐垂直的起始边框
end案例:在线代码入口👉👉(点击传送)
对齐子元素容器的结束边框,justify-items
对齐水平的结束边框,align-items
对齐垂直的结束边框
center案例:在线代码入口👉👉(点击传送)
子元素容器内部居中,justify-items
水平居中,align-items
垂直居中
stretch案例:在线代码入口👉👉(点击传送)
默认就是这个属性,只要不设置宽度和高度就会把宽高拉伸铺满
1.7 justify-content
和 align-content
和 place-content
注意这三个属性和1.6描述的区别在于,
justify-items
和align-items
和place-items
是针对子元素内容的,justify-content
和align-content
和place-content
是针对grid容器内容的
属性说明
justify-content
:设置grid布局容器内容水平位置
align-content
:设置grid布局容器内容垂直位置
place-content
:align-content
和 justify-content
两个属性的简写方式,若省略第二个值,则认为第二个值等于第一个值
place-content: <align-content> <justify-content>
属性可选值(三个属性均有以下可选值)
可选值 | 可选值说明 |
---|---|
start | 对齐grid容器的起始边框 |
end | 对齐grid容器的结束边框 |
center | grid容器内部居中 |
stretch | grid容器内容大小没有指定时,拉伸占据整个grid容器 |
space-around | 每个子元素两侧间隔相等,所以子元素之间间隔比容器边框的间隔大一倍 |
space-between | 子元素与子元素之间间隔相等,子元素与容器边框没有间隔 |
space-evenly | 子元素与子元素之间间隔相等,子元素与容器边框之间也是同样长度的间隔 |
start案例:在线代码入口👉👉(点击传送)
对齐容器的水平和垂直起始边框,justify-content
对齐水平的起始边框,align-content
对齐垂直的起始边框
justify-content:start;
align-content:start;
end案例:在线代码入口👉👉(点击传送)
对齐容器的水平和垂直结束边框,justify-content
对齐水平的结束边框,align-content
对齐垂直的结束边框
justify-content:end;
align-content:end;
center案例:在线代码入口👉👉(点击传送)
容器内容水平和垂直居中对齐,justify-content
容器内容水平居中对齐,align-content
容器内容垂直居中对齐
justify-content:center;
align-content:center;
stretch案例:在线代码入口👉👉(点击传送)
自动拉伸铺满grid容器,justify-content
水平铺满容器,align-content
垂直铺满容器
justify-content:stretch;
align-content:stretch;
space-around案例:在线代码入口👉👉(点击传送)
每个子元素两侧间隔相等,所以子元素之间间隔比容器边框的间隔大一倍
justify-content:space-around;
align-content:space-around;
space-between案例:在线代码入口👉👉(点击传送)
子元素与子元素之间间隔相等,子元素与容器边框没有间隔
justify-content:space-between;
align-content:space-between;
space-evenly案例:在线代码入口👉👉(点击传送)
子元素与子元素之间间隔相等,子元素与容器边框之间也是同样长度的间隔
justify-content:space-evenly;
align-content:space-evenly;
1.8 grid-auto-columns
和 grid-auto-rows
grid-auto-columns
:设置多余列的列宽
grid-auto-rows
:设置多余行的行高
在某种情况下,我们设置了9宫格布局可能会出现10个item子元素,那正常的前9个子元素都设置有合适的宽高,但是多余出现的第10个如果不进行设置,就会出现不正常的布局,通过以下案例可以帮助理解
当使用 grid-auto-flow:column;
改变默认的放置顺序会出现以下情况
所以在出现以上情况时,使用grid-auto-columns
和 grid-auto-rows
解决问题
在线代码入口👉👉(点击传送),自行修改案例代码观察变化。
.grid_container {
grid-auto-columns:100px;
grid-auto-rows:100px;
}
1.9 grid-template
和 grid
grid-template
属性是grid-template-columns
、grid-template-rows
和grid-template-areas
这三个属性的合并简写形式。
grid
属性是grid-template-rows
、grid-template-columns
、grid-template-areas
、 grid-auto-rows
、grid-auto-columns
、grid-auto-flow
这六个属性的合并简写形式。
这两个属性用法比较复杂,后期再考虑重新写一篇文章讲解,有需要的请在评论区留言,留言数多的话,会尽快出新文章
2.0(子元素)grid-column-start
和 grid-column-end
和 grid-row-start
和 grid-row-end
和 grid-column
和 grid-row
横纵向网格线始终比横纵向子元素多1,下面通过几个案例帮助理解
案例1:在线代码入口👉👉(点击传送)
🥇当方块一想占满横向两个方格时,将方块一的grid-column-start
和grid-column-end
分别设置成1
和3
,或者设置grid-column: 1/3
🥈当方块一想占满纵向两个方格时,将方块一的grid-row-start
和grid-row-end
分别设置成1
和3
,或者设置grid-row: 1/3
.item-1 {
background-color: #B53471;
/* 横向 */
/* grid-column-start: 1;
grid-column-end: 3; */
grid-column: 1/3; /*效果相同 */
/* 纵向 */
/* grid-row-start: 1;
grid-row-end: 3; */
grid-row: 1/3; /*效果相同 */
}
案例2:在线代码入口👉👉(点击传送)
🥇当遇到多个方格进行属性设置时,需要考虑网格线是否被别的元素包含,如下图所示:
所以在案例1的原有基础上,我们想把方块2的纵向占两个方块,位置放在原方块4和原方块7的位置,那么我们就要考虑方块1已经包含过的网格线不能使用。所以设置上边框网格线的的时候就要避开纵向的第2条网格线,这样我们要设置上边框网格线为3
,下边框网格线为5
.item-2 {
background-color: #ffcccc;
grid-column: 1/2;
grid-row: 3/5;
}
效果如下:
2.1 (子元素)justify-self
和 align-self
和 place-self
其实这一节没啥好讲的,属性
justify-items
和align-items
和place-items
属性效果一样,只不过前者是统一设置grid容器中的子元素内容位置,后者则是在子元素上单独设置,并且会覆盖统一设置的效果。
justify-self
:设置水平位置
align-self
:设置垂直位置
place-self
:align-self
属性和justify-self
属性的合并简写形式。(忽略第二个值,则认为第二个值等于第一个值)
案例1:在线代码入口👉👉(点击传送)
所有子元素内容水平垂直居中,第一个子元素内容对齐垂直方向结束边框align-self: end;
,对齐水平方向结束边框justify-self: end;
代码和效果如下:justify-self
和 align-self
覆盖了justify-items
和 align-items
设置的居中显示
.grid_container {
justify-items: center;
align-items: center;
}
.item-1 {
justify-self:end;
align-self:end;
background-color: #B53471;
}
2.1 (子元素)grid-area
属性名 | 属性说明 |
---|---|
grid-area | 指定子元素防止在哪个区域 |
在上面 1.3 中已经说过如何划分区域了,接下来我们通过 grid-area
属性来了解如何使用区域
案例1:在线代码入口👉👉(点击传送)
将就九宫格中1 2 3 方块替换到 4 5 6方块
.grid_container {
display:grid;
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
/* 划分九个区域 */
grid-template-areas:
'a b c'
'd e f'
'g h i';
}
.item-1 {
background-color: #B53471;
grid-area: d;
}
.item-2 {
background-color: #ffcccc;
grid-area: e;
}
.item-3 {
background-color: #ff4d4d;
grid-area: f;
}
案例2:在线代码入口👉👉(点击传送)
将九宫格中的方块1 2 3 纵向占满两个单元格,方块4 水平占满3个单元格
.grid_container {
display:grid;
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
/* 划分三个区域 */
grid-template-areas:
'a b c'
'a b c'
'd d d';
}
.item-1 {
background-color: #B53471;
grid-area: a;
}
.item-2 {
background-color: #ffcccc;
grid-area: b;
}
.item-3 {
background-color: #ff4d4d;
grid-area: c;
}
.item-4 {
background-color: #ffaf40;
grid-area: d;
}
作者:是舍长
链接:https://juejin.cn/post/7017074528752762911
收起阅读 »
构建大型前端业务项目的一点经验
目前工作中接手的几个项目都是 B端 PC 项目,业务逻辑都比较复杂,并且代码历史较久,在日常的维护中经常会遇到想摊手的技术问题,发现问题、解决问题、避免再次出现同样的问题,既是项目可持续维护的因素之一,也是个人工作经验积累的一个过程
本文可当做 接手前端新项目?这里有些注意点你可能需要留意一下、 编写可维护的现代化前端项目 的补充
具体、连贯的变量名
在前后端分离的现代化 web
开发流程下,相比于以往,前端承担了更多的业务逻辑,尽管存在着 TypeScript
等约束工具,但相比于后端语言,js
仍具备相当大的灵活性,这就导致了代码一旦复杂,前端代码的排查会更加麻烦
单一变量的层层传递与到处使用是很常见的事情,变量的命名对于追踪变量有着相当大的影响
所以变量名称必须是具体且有实际意义的,不提倡为了追求变量名的精确性而使得变量名称冗长,但模糊而宽泛的变量名同样不可取,这就要求变量名称即准确又简短,在某些时候,可能很难做到这一点,个人倾向是,实在无法做好权衡的前提下,宁愿选择冗长的变量名也不要选择一个模糊宽泛的
例如,data
可以当做是一个变量名,这个变量名用于临时的局部变量没啥问题,毕竟你一眼就能看到这个变量所有的使用范围,但如果变量所持有的数据的作用范围较大(例如跨组件)且具备实际业务意义,那么就不太妙了,data
可以用作任何数据的变量名,当需要追踪 data
的时候,在编辑器里搜索 data
,发现到处都是 data
,并不是一件美好的事情
一旦确定好了变量名后,最好不要再对其进行重命名,例如想将 A
组件里的 userData
传递到 B
组件中,B
组件最好原模原样地接收这个变量名,而不是将其命名为其他的什么名称
有些时候可能就必须要在传递变量的时候进行重命名,例如第三方组件就接收 data
,那么你无法传递 userData
,这种情况下当然不得不重命名
避免对变量重命名的目的主要是,为了防止在追踪变量的时候因变量名称改变而产生思维上的重新整理,一个变量被重命名了好几次后,追踪到底再回过头来你可能就忘记了自己当初在追踪什么东西,同时对于搜索功能也不太友好,从一而终的连贯变量名可以让你一次搜索就能从追踪的起始位置跳过中间一大堆逻辑直接到终点
必要的选择器
现代化的前端项目基本都会使用 React
、Vue
等数据驱动的框架,UI
组件一般也都是使用别人封装好的组件库,除非是要写样式,否则html
元素选择器基本上都是可有可无的了,但并不是说就不需要了,最起码的一个好处是,让你想在代码里查找页面上的一个元素时,直接根据选择器名就能精准定位了
页面上有个弹窗展示得不太对,你在浏览器页面里看到这个弹窗元素名叫 ant-modal-wrap
,是个第三方的组件所以你代码里根本搜不到这个选择器名;页面上有一句文案有点问题,你在浏览器页面里看到这个文案所在的元素是个没有选择器的 div
标签,在目前普遍 全站div
的浪潮下,光是定位这个文案到底是哪里的就够花费好一阵子了
所以,这里选择器是起到了一个索引的作用,既然是索引,那么同样应该遵守上面变量名相关的规则,即选择器名称应当 即准确又简短
优化应该从一开始就开始
不要提前优化
相信很多人都听过这句话,我曾经将这句话当做是至理名言,但经历的多了之后,目前已经开始有所质疑了
不要提前优化,那么要在什么时候优化?快要 hold
不住的时候才优化?迭代了七八十版的时候再优化?团队人员换了一茬又一茬的时候再优化?
当然是可以的,但是那个时候谁来优化,谁来做这种在业务看来毫无产出甚至是可能优化出 bug
的吃力不讨好的事情?
一个函数里塞了数十层的 if...else
,函数体的代码量超过千行,看着就应该要被优化的,但是这些代码在这里绵延了数年之久,经过了一批又一批不同程序员的修改,承载了不知多少明面上暗地里的业务逻辑,技术上或许好优化,但谁能保证某处优化不会对业务逻辑造成破坏?
没有提前优化,过程中也没有优化,那就完全是没有任何优化了,因而屎山就诞生了
我认为 不要提前优化
这句话是产生在一个朝九晚五不加班需求少有充足时间做技术优化的语境之下,这种语境下,这句话是没啥问题的,只是大部分情况下,现实情况根本不符合语境,所以这句话就有问题了
该拆的组件、该提取的公共方法、该规划的目录结构、该引入的代码规范……应该从一开始就形成,不要等着需要优化的时候才优化,那个时候已经来不及了
复用(组件、方法)
代码复用是为了提升工作效率,但如果只是为了复用代码而复用,就本末倒置了
通用方法、通用组件鼓励复用,但业务逻辑、业务组件,慎重复用
一个常见的例子是,移动端详情页页面和编辑页可能具有大部分重合的逻辑,但类似这种业务属性很强的组件,除非你确信这个组件将来不出现大的改动,否则不要为了贪图眼前的便利而想当然地进行复用
本来为了区分展示态和编辑态,就已经写了一些条件语句了,日后若是出现了已经复用的逻辑必须要按照业务需求进行拆分,甚至是逻辑完全南辕北辙,初期还好,或许还能抢救一下拆分出来,但到了中后期才发现这个问题很可能已经晚了,掺杂了那么多的业务逻辑,你还敢去做拆分吗?那么这个复用组件的代码量必然要被大量的 if...else
占领,修改任何一个功能点、排查任何问题都要兼顾两套逻辑,对于维护者来说,这会造成相当大的心智负担,对于项目本身来说,维护的代码将会变得更大
业务代码是千变万化的,原本多个场景下相似的逻辑,很可能随着业务的迭代变得毫无关系,在这种场景下,复用不仅不能提高工作效率,反而还会拖后腿
而对于通用方法和通用组件来说,为了更加彻底地解耦,其应当是函数式的,不应当对外部状态产生隐式地修改
通用方法最好是纯函数,相同的输入有相同的输出,入参、出参都应当是明确的,让人一眼就看出来需要哪些入参,又会有哪些出参,而不是直接传入一个大的对象,然后在方法体内去一个个查找所需的对象属性
通用组件不应当自作主张修改外部数据,而应该将产生的变化主动抛出去,让上一层组件来明确决定如何使用这个变化
依据社区而不是从心
为项目选择设计模式、UI组件库、状态管理库等基础功能的时候,应当选取社区内热度更高的而非根据个人的喜好
你所认为很牛x的设计模式、第三方库等,可能是其他人根本就没听过的,或者其他人根本就不认同的,这只会增加团队之间的协作难度
团队合作项目的代码是用来传承的而不是用来炫技的
抛弃惯性思维
待在舒适区,这是人之本能,因为熟悉,所以上一个项目使用的技术栈在下一个项目里也要继续用
但是,真的合适吗?
上一个项目用了 mobx
,这个项目里也必须要用吗?上一个项目里将所有的状态数据都放到了状态管理库里,这个项目也要这样做吗?上一个项目没用 TypeScript
,这一个也不用吗?
可能是不需要的,可能是需要更换的,当然,并不是说就要跟上一个项目反着来,到底怎样最起码要有一个思考的过程,而不是上一个项目就是这样的,所以这一个也要这样
考虑清楚了再写TODO
有意识做优化是个好习惯,但意识得能落到实处
以我的经验看,在多人协作的、业务敏捷迭代的项目中,大多数 todo
是无法完成的
大部分 todo
都是基于当时的情况做出的考量,当时这个方法可能只有几行,todo
要做的时候很简单,但是当时没有做,当过了一段时间再想起来这事的时候,发现那个方法已经变成了几百行了,你还敢动吗?
或者换句话说,你还有完成这个 todo
的心思吗?人都是懒惰的,你愿意将原本可以用在打游戏刷视频的时间用在完成这个 todo
上吗?看到了别人写的 todo
,并且也看明白了,但是你愿意帮别人完成这个 todo
吗?
该做的事情应当立即完成,或许因为某些原因无法立即完成,所以你想延后再来做,但是一般情况下,后续再来完成的成本必然大于当下,现在都完成不了,凭什么认为以后就能完成?
真的需要做的事情,哪怕会让进度延期,只要你理由充分,其他人不可能也没理由去阻止你
小结
很多时候,一些让你能够写出更好的代码建议,实际上对于业务产出是毫无帮助的,哪怕你不遵守这些建议甚至反着来,也不影响你的产出不影响你的绩效,毕竟产出和绩效跟代码写得好不好并没有直接关系,甚至这些所谓的建议有时候还会影响你快速产出,只要我能拿出一个好的产出拿到一个好的绩效,代码写得糙点烂点又有什么关系?以后的事情以后再说呗,搞不好以后维护的人根本不是我
这种心理或许才是常态,毕竟这更加符合现实的利益
但如果你是一位对技术有追求的人,你真的甘心就如此吗?我认为除了现实的考量之外,还应当为自己写下的代码负责
作者:清夜
链接:https://juejin.cn/post/7016948081321050148
收起阅读 »
npm install之后发生了什么
下载项目后,执行的第一个命令行一般都是 npm install
。在这个过程中可能一帆风顺,也可能遇到大大小小的报错,有时候花点时间各种搜索能解决,可下次遇到还是一头雾水的上网找各种方案尝试解决报错。
那么,你清楚当你输入 npm instal
,按下 Enter
键之后,究竟发生了什么吗?
正文
一、npm install之后发生了什么
npm install 大概会经过以下几个流程,下面我们就来简单看一下(原图地址)。
- npm install执行后,会检查并获取npm配置,优先级为
项目级别的.npmrc文件 > 用户级别的.npmrc文件 > 全局的.npmrc文件 > npm内置的.npmrc文件
.npmrc
文件就是npm的配置文件。查看npm的所有配置, 包括默认配置,可以通过下面的命令:
npm config ls -l
- 然后检查项目中是否有
package-lock.json
文件。
从npm 5.x开始,执行npm install时会自动生成一个 package-lock.json
文件。
package-lock.json
文件精确描述了node_modules 目录下所有的包的树状依赖结构,每个包的版本号都是完全精确的。
因此npm会先检查项目中是否有 package-lock.json
文件,分为两种情况:
- 如果有,检查
package-lock.json
和package.json
中声明的依赖是否一致
- 一致:直接使用
package-lock.json
中声明的依赖,从缓存或者网络中加载依赖 - 不一致:各个版本的npm处理方式如上图
- 如果没有,根据
package.json
递归构建依赖树,然后根据依赖树下载完整的依赖资源,在下载时会检查是否有相关的资源缓存
- 存在:将缓存资源解压到
node_modules
中 - 不存在:从远程仓库下载资源包,并校验完整性,并添加到缓存,同时解压到
node_modules
中
- 最终将下载资源包,存放在缓存目录中;解压资源包到当前项目的
node_modules
目录;并生成package-lock.json
文件。
构建依赖树时,不管是直接依赖还是子依赖,都会按照扁平化的原则,优先将其放置在 node_modules
根目录中(最新的npm规范), 在这个过程中,如果遇到相同的模块,会检查已放置在依赖树中的模块是否符合新模块的版本范围,如果符合,则跳过,不符合,则在当前模块的 node_modules
下放置新模块。
二、npm缓存
在执行 npm install
或 npm update
命令下载依赖后,除了将依赖包安装在 node_modules
目录下外,还会在本地的缓存目录缓存一份。我们
可以通过以下命令获取缓存位置:
// 获取缓存位置
npm config get cache
// C:\Users\DB\AppData\Roaming\npm-cache
复制代码
如我的缓存位置在C:\Users\DB\AppData\Roaming\npm-cache下面的_cacache 文件夹中。
再次安装依赖的时候,会根据 package-lock.json
中存储的 integrity、version、name 信息生成一个唯一的 key,然后拿着key去目录中查找对应的缓存记录,如果有缓存资源,就会找到tar包的hash值,根据 hash 再去找缓存的 tar 包,并把对应的二进制文件解压到相应的项目 node_modules
下面,省去了网络下载资源的开销。
因此,如果我们可能因为网络原因导致下载的包不完整,这就可能造成删除node_modules重新下载的依旧是问题包,假如删除 node_modules
重新下载问题依旧,此时就需借助命令行清除缓存。
// 清除缓存
npm cache clean --force
复制代码
不过 _cacache
文件夹中不包含全局安装的包,所以想清除存在问题的包为全局安装包时,需用 npm uninstall -g
解决
三、关于yarn
yarn简介:
yarn是由Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具 ,正如官方文档中写的,Yarn 是为了弥补 npm 的一些缺陷而出现的。
yarn特点:
- 速度快
- yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快。
- 安全
- 在执行代码之前,yarn 会通过算法校验每个安装包的完整性。
- 可靠
- 使用详细、简洁的锁文件格式和明确的安装算法,yarn 能够保证在不同系统上无差异的工作。
四、yarn和npm部分命令对比

总结
无论是使用npm 还是 yarn 来管理你的项目依赖,我们都应该知其然更知其所以然,这样才能在项目中跟海的定位及解决问题,不是吗?
链接:https://juejin.cn/post/7016994983186006024
收起阅读 »
进来聊聊!Vue 和 React 大杂烩!
相信应用层面的知识,大家都比较熟悉了,实际 React 用来实现业务对于熟悉 Vue 的开发人员来说也不是难事,今天我们简单的了解一下 React 和 Vue 。(瞎聊聊)
先来两张源码编译图对比一下:
由于每个步骤能涉及的东西太多,所以本篇就简单聊一下他们的区别以及他在我们项目中实际的应用场景中能够做什么(想到什么聊什么)。
Vue
new Vue
我们知道 Vue 和 React 都是通过替换调指定的 Dom 元素来渲染我们的组件,来看一下:
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h => h(App),
}).$mount('#app')
先说 Vue 的,new Vue 做了什么?相信读过源码的同学都会知道,他执行了一堆初始化操作 initLifecycle、initEvents、initRender、initInjections、initState、initProvide
。
具体包括以下操作:选项合并(用户选项、默认选项)、$children
、$refs
、$slots
、$createElement
等实例属性和方法初始化、自定义事件处理、数据响应式处理、生命周期钩子调用、可能的挂载。
响应式原理
当一个 Vue 实例被创建时,它将 data 对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。
var data = {
a: 1
}
var vm = new Vue({
data
})
vm.a = 1
data.a // 1
data.a = 2
vm.a // 2
Vue 通过劫持 get 和 set 来实现响应式原理,这也是与 React 最大区别所在,React 只能手动调用 this.setState
来将state
改变。
我在往期篇幅有具体谈过 Vue 的响应式原理:
深入浅出Vue响应式原理
模板编译 && 视图渲染
当 data 中的数据实现了响应式之后,就开始在模板上做功夫了。
这里有一个很重要的东西叫虚拟 Dom。
所谓虚拟 DOM 就是用 js 来描述一个 DOM 节点,在 Vue 中通过 Vnode 类来描述各种真实 DOM 节点。
在视图渲染之前,把 template 先编译成虚拟 Dom 缓存下来,等数据发生变化需要重新渲染时,通过 diff 算法找出差异对比新旧节点(patch),之后把最终结果替换到真实 Dom 上,最终完成一次视图更新。
了解更多关于 diff 移步至:diff算法
关于编译原理要细聊就有点多了,大致总结一下:
- 第一步是将
模板字符串
转换成AST语法树
(解析器) - 第二步是对
AST
进行静态节点标记,主要用来做虚拟 DOM 的渲染优化(优化器) - 第三步是 使用
element ASTs
生成render
函数代码字符串(代码生成器)
有兴趣请移步至:
Vue 模板编译原理
生命周期
在这些过程中,Vue 会暴露一些钩子函数供我们在适当时机去执行某些操作,这就是生命周期钩子函数。关于 Vue 的生命周期大家应该都熟记于心了,简单过一下:
beforeCreate (创建实例前的钩子,此时 data 里的数据还不能用。)
created (实例创建完成后的钩子,此时 data 已完成初始化可使用,但 Dom 尚未挂载。)
beforeMount (将编译完成的 HTML 挂载到对应虚拟 Dom,此时页面并无内容。)
mounted (Dom 已完成挂载,此时可以操作 Dom,此阶段也可以调用接口等操作。)
beforeUpdate (更新之前的钩子,当data变化时,会触发beforeUpdate方法。基本上没有什么用处。)
updated (更新之后的钩子,当数据变化导致地虚拟DOM重新渲染时会被调用,被调用时,组件DOM已经更新。建议不要在这个钩子函数中操作数据,可能陷入死循环。)
beforeDestory (实例销毁前的钩子,此时还可以使用 this,通常在这一步会进行清除计时器等操作)
destoryed (实例销毁完成的钩子,调用完成后,Vue实例的所有内容都会解绑定,移出全部事件监听器,同时销毁所有的子实例。)
React
大家可能会比较关心 React 会扯什么(猜的),毕竟 Vue 已经是家喻户晓,加上国内业务使用也是居多,生态圈及各类解决方案也是层出不穷。
ReactDOM.render
ReactDOM.render 是 React 的最基本方法用于将模板转为 HTML 语言,并插入指定的 DOM 节点。
import App from './App.jsx'
import ReactDOM from 'react-dom'
ReactDOM.render(
<App></App>,
document.getElementById('root')
)
render 方法实际是调用了内部的 React.createElement 方法,进而执行 ReactMount._renderSubtreeIntoContainer
。
还有一个方法 ReactDOM.unmountComponentAtNode() 作用和 ReactDOM.render() 正好相反,他是清空一个渲染目标中的 React 部件或 HTML。
React state
state 是 React 中很重要的东西,说到 state 就不得不提到 setState 这个方法,很多人认为 setState 是异步操作,其实并不是。之所以会有一种异步的表现方式是因为 React 本身的性能机制导致的。因为每次调用 setState 都会触发更新,异步操作是为了提高性能,将多个状态合并一起更新,减少 render 调用。
如图,setState 接受一个新状态并不会立即执行,而是存入 pending 队列中进行判断。
如果有阅读过源码的同学就会知道他在其中通过判断 isBatchingUpdates (是否是批量更新模式)来进行区分。
如果是,那就会将状态保存到 dirtyComponents (脏组件)。
如果否,那就遍历所有的脏组件,并调用 updateComponent 更新 pending 队列的 state 或 props。执行完后,将
isBatchingUpdates 设置为 true。
假如有如下代码:
for ( let i = 0; i < 100; i++ ) {
this.setState( { count: this.state.count + 1 } );
}
复制代码
若 setState 是同步机制,那么这个组件会被 render 100次,这无疑对性能是毁灭性的。
当然 React 也想到了这个问题并做了处理:
React 会将 setState 的调用合并为一个执行,所以 setState 执行时我们并没有看到 state 马上更新,而是通过回调获取到更新后的数据(有点类似 Vue 中的 nextTick),也就是刚刚上图所叙。
React 渲染流程
对于首次渲染,React 的主要是通过 React.render 接收到的 VNode 转化为 Fiber 树,并根据树的层级关系构建出 Dom 树并渲染。
而二次渲染(更新),Fiber 树已经存在内存中了,所以 React 会计算 Fiber 树中的各个节点差异(diff),并将变化更新渲染。
实际上 Vue 和 React 的 diff 算法都是同层 diff,复杂度都为O(n),但是他们的不同在于 React 首位节点是固定不动的(除了删除),然后依次遍历对比。
Vue 的 diff 在 compile 阶段的 optimize 标记了 static 点,可以减少 diff 次数,而且是双向遍历方法,并且借鉴了开源库 snabbdom。(不论 Vue 还是 React 两者都是各有秋千)
再说回渲染, React 中也存在着和 Vue 一样的 VNode(虚拟 Dom)。
JSX 会被编译转换成 React.createElement 函数的调用,返回值就是 VNode,其作用和 Vue 中的 VNode 基本一致。
关于 Fiber 是一个比较抽象的概念比较难理解,可以理解为他是用来描述有关组件以及输入输出的信息的一个 JavaScript 对象。
了解更多 Fiber:Fiber传送门
小结一下:
React 渲染流程(浅看):
jsx --> createElement 函数 --> 这个函数帮助我们创建 ReactElement 对象(对象树) --> ReactDOM.render 函数 --> 映射到浏览器的真实DOM
生命周期
在渲染过程中暴露出来的钩子就是生命周期钩子函数了,看图:
我在 Vue 转 React 系列中有提到过 ->传送门
组件的生命周期可分成三个状态:
- Mounting:已插入真实 DOM
- Updating:正在被重新渲染
- Unmounting:已移出真实 DOM
简单过一下生命周期:
componentWillMount 在渲染前调用,在客户端也在服务端。
componentDidMount : 在第一次渲染后调用,只在客户端。之后组件已经生成了对应的DOM结构,可以通过this.getDOMNode()来进行访问。 如果你想和其他JavaScript框架一起使用,可以在这个方法中调用setTimeout, setInterval或者发送AJAX请求等操作(防止异步操作阻塞UI)。
componentWillReceiveProps 在组件接收到一个新的 prop (更新后)时被调用。这个方法在初始化render时不会被调用。
shouldComponentUpdate 返回一个布尔值。在组件接收到新的props或者state时被调用。在初始化时或者使用forceUpdate时不被调用,可以在你确认不需要更新组件时使用。
componentWillUpdate在组件接收到新的props或者state但还没有render时被调用。在初始化时不会被调用。
componentDidUpdate 在组件完成更新后立即调用。在初始化时不会被调用。
componentWillUnmount在组件从 DOM 中移除之前立刻被调用。
小结
本文只是涉及内容众多,难免会有遗漏或不周,还请看官轻喷
作者:饼干_
链接:https://juejin.cn/post/7016530148073668621
收起阅读 »
前端必学的flip动画思想
前言
相信大家在用Vue的时候,一定用过他的transition-group组件。在该组件下方可以看到这么一句话
这个看起来很神奇,内部的实现,Vue 使用了一个叫 FLIP 简单的动画队列,使用 transforms 将元素从之前的位置平滑过渡新的位置。,我们将之前实现的例子和这个技术结合,使我们列表的一切变动都会有动画过渡。
和一个特别特别酷炫的动画效果
下面,跟我一起走进Flip动画的奇妙世界
前置知识
getBoundingClientRect
通过dom.getBoundingClientRect()
,可以得到某个元素在屏幕上的矩形区域
const rect = dom.getBoundingClientRect(); // 获取矩形区域
rect.left; // 获取矩形区域的left值
rect.top; // 获取矩形区域的top值
transform
transform是css3提供的属性,含义为变形或变换
css3提供了多种变换方式,包括平移、旋转、倾斜、缩放,还包括更加具有通用性的矩阵变换
所有变换,均不会影响真实布局位置,只是影响最终的视觉效果
animate api
Element
接口的animate()
方法是一个创建新Animation
的便捷方法,将它应用于元素,然后运行动画。它将返回一个新建的 Animation
对象实例
使用animate api实现动画非常简单,仅需要通过下面的代码即可实现
dom.animate(
[
{ /* 起始css属性 */ },
{ /* 结束css属性 */ },
],
{
duration: 800, // 完成动画的时间
}
);
其他API请看MDN文档
Flip思想
Flip是一种动画思路,专门针对上述场景
它由四个单词组成,分别是:
- First
- Last
- Invert
- Play
具体过程如下
在代码实现上,可以遵循以下结构实现动画效果
// ① Frist
record(container); // 记录容器中每个子元素的起始坐标
// 改变元素顺序
change();
// ② Last + ③ Invert + ④ Play
move(container); // 让元素真正实现移动
实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
* {
margin: 0;
padding: 0;
}
.btns {
text-align: center;
}
.btns button {
margin: 0 1em;
outline: none;
border: none;
background: #579ef8;
color: #fff;
padding: 7px 10px;
border-radius: 5px;
cursor: pointer;
}
.btns button:hover {
opacity: 0.8;
}
.container {
width: 500px;
overflow: hidden;
margin: 20px auto;
display: flex;
flex-wrap: wrap;
}
.item {
width: 50px;
height: 50px;
box-sizing: border-box;
text-align: center;
background: #eef5fe;
border: 1px solid #ddebfd;
line-height: 50px;
margin: 5px;
}
</style>
<body>
<div class="btns">
<button id="sort">随机排序</button>
</div>
<div class="container">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
<div class="item">9</div>
<div class="item">10</div>
<div class="item">11</div>
<div class="item">12</div>
<div class="item">13</div>
<div class="item">14</div>
<div class="item">15</div>
<div class="item">16</div>
<div class="item">17</div>
<div class="item">18</div>
<div class="item">19</div>
<div class="item">20</div>
<div class="item">21</div>
<div class="item">22</div>
<div class="item">23</div>
<div class="item">24</div>
<div class="item">25</div>
<div class="item">26</div>
<div class="item">27</div>
<div class="item">28</div>
<div class="item">29</div>
<div class="item">30</div>
<div class="item">31</div>
<div class="item">32</div>
<div class="item">33</div>
<div class="item">34</div>
<div class="item">35</div>
<div class="item">36</div>
<div class="item">37</div>
<div class="item">38</div>
<div class="item">39</div>
<div class="item">40</div>
<div class="item">41</div>
<div class="item">42</div>
<div class="item">43</div>
<div class="item">44</div>
<div class="item">45</div>
<div class="item">46</div>
<div class="item">47</div>
<div class="item">48</div>
<div class="item">49</div>
<div class="item">50</div>
</div>
<script>
const container = document.querySelector('.container')
function change() {
const childrens = [...container.children]
for(let i = 0, l = childrens.length; i < l; i ++) {
const children = childrens[i]
const j = Math.floor(Math.random() * l)
if (i !== j) {
// 获取当前dom的下一个元素
const inextDom = children.nextElementSibling
// 把i插入j之前
container.insertBefore(children, childrens[j])
// 把下标j的元素插入到i元素之前
container.insertBefore(childrens[j], inextDom)
}
}
}
sort.onclick = () => {
record(container)
change()
move(container)
}
function record(container) {
for(let i = 0, len = container.children.length; i < len; i ++) {
const dom = container.children[i]
const rect = dom.getBoundingClientRect()
dom.startX = rect.left
dom.startY = rect.top
}
}
function move(container) {
for(let i = 0, len = container.children.length; i < len; i ++) {
const dom = container.children[i]
const rect = dom.getBoundingClientRect()
const curX = rect.left, curY = rect.top
dom.animate([
{ transform: `translate(${dom.startX - curX}px, ${dom.startY - curY}px)` },
{ transform: `translate(0px, 0px)` }
], { duration: 600 })
}
}
</script>
</body>
</html>
以上就是所有代码了,可以在控制台看到,不会出现style标签,非常的神奇。
结语
FLIP 不光可以做位置变化的动画,对于透明度,大小等等都可以很轻松的实现。
Flip非常有用,可以实现在任何需要动画的地方
链接:https://juejin.cn/post/7016912165789515783
收起阅读 »
什么是 Promise.allSettled() !新手老手都要会?
Promise.allSettled()
方法返回一个在所有给定的 promise 都已经 fulfilled
或 rejected
后的 promise
,并带有一个对象数组,每个对象表示对应的 promise 结果。
接着,我们来看看 Promise.allSettled()
是如何工作的。
1. Promise.allSettled()
Promise.allSettled()
可用于并行执行独立的异步操作,并收集这些操作的结果。
该函数接受一个 promise
数组(通常是一个可迭代对象)作为参数:
const statusesPromise = Promise.allSettled(promises);
当所有的输入 promises
都被 fulfilled
或 rejected
时,statusesPromise
会解析为一个具有它们状态的数组
{ status: 'fulfilled', value: value }
— 如果对应的 promise 已经fulfilled
或者
{status: 'rejected', reason: reason}
如果相应的 promise 已经被rejected
在解析所有 promises 之后,可以使用 then
语法提取它们的状态:
statusesPromise.then(statuses => {
statuses; // [{ status: '...', value: '...' }, ...]
});
或者使用 async/await
语法:
const statuses = await statusesPromise;
statuses; // [{ status: '...', value: '...' }, ...]
2. 取水果和蔬菜
在深入研究 Promise.allSettle()
之前,我们先定义两个简单的 helper
函数。
首先,resolveTimeout(value, delay)
返回一个 promise ,该 promise 在经过 delay
时间后用 value
来实现
function resolveTimeout(value, delay) {
return new Promise(
resolve => setTimeout(() => resolve(value), delay)
);
}
第二,rejectTimeout(reason, delay)
- 返回一个 promise,在经过 delay
时间后拒绝reason
。
最后,我们使用这些辅助函数来试验 promise.allsettle()
。
2.1 All promises fulfilled
我们同时访问当地杂货店的蔬菜和水果。访问每个列表是一个异步操作:
const statusesPromise = Promise.allSettled([
resolveTimeout(['potatoes', 'tomatoes'], 1000),
resolveTimeout(['oranges', 'apples'], 1000)
]);
// wait...
const statuses = await statusesPromise;
// after 1 second
console.log(statuses);
// [
// { status: 'fulfilled', value: ['potatoes', 'tomatoes'] },
// { status: 'fulfilled', value: ['oranges', 'apples'] }
// ]
线上事例:codesandbox.io/s/all-resol…
Promise.allSettled([...])
返回一个 promise statusesPromise
,该 promise 在1秒内解决,就在蔬菜和水果被解决之后,并行地解决。
statusesPromise
解析为一个包含状态的数组。
- 数组的第一项包含有蔬菜的已完成状态:
status: 'fulfilled', value: ['potatoes', 'tomatoes'] }
- 同样的方式,第二项是水果的完成状态:
{ status: 'fulfilled', value: ['oranges', 'apples'] }
2.2一个 promise 被拒绝
想象一下,在杂货店里已经没有水果了。在这种情况下,我们拒绝水果的 promise。
promise.allsettle()
在这种情况下如何工作?
const statusesPromise = Promise.allSettled([
resolveTimeout(['potatoes', 'tomatoes'], 1000),
rejectTimeout(new Error('Out of fruits!'), 1000)
]);
// wait...
const statuses = await statusesPromise;
// after 1 second
console.log(statuses);
// [
// { status: 'fulfilled', value: ['potatoes', 'tomatoes'] },
// { status: 'rejected', reason: Error('Out of fruits!') }
// ]
线上事例:codesandbox.io/s/one-rejec…
Promise.allSettled([...])
返回的 promise 在 1
秒后解析为一个状态数组:
数组的第一项,蔬菜
promise
成功解析:{ status: 'fulfilled', value: ['potatoes', 'tomatoes'] }
第二项,因为水果 promise 被拒绝,所以是一个拒绝状态:
{ status: 'rejected', reason: Error('Out of fruits') }
即使输入数组中的第二个 promise 被拒绝,statusesPromise
仍然会成功解析一个状态数组。
2.3 所有的 promises 都被 rejected
如果杂货店里的蔬菜和水果都卖光了怎么办?在这种情况下,两个 promise 都会被拒绝。
const statusesPromise = Promise.allSettled([
rejectTimeout(new Error('Out of vegetables!'), 1000),
rejectTimeout(new Error('Out of fruits!'), 1000)
]);
// wait...
const statuses = await statusesPromise;
// after 1 second
console.log(statuses);
// [
// { status: 'rejected', reason: Error('Out of vegetables!') },
// { status: 'rejected', reason: Error('Out of fruits!') }
// ]
线上事例:codesandbox.io/s/all-rejec…
在这种情况下,statusesPromise
仍然成功地解析为一个状态数组。然而,该数组包含被拒绝的promise 的状态。
3.总结
Promise.allSettled(promises)
可以并行地运行 promise,并将状态(fulfilled 或reject)收集到一个聚合数组中。
Promise.allSettled(...)
在你需要执行平行和独立的异步操作并收集所有结果时非常有效,即使某些异步操作可能失败。
链接:https://juejin.cn/post/7016856020395753509
收起阅读 »
说一说Web端侧AI
前言
AI
正在不断拓展前端的技术边界, 算法的加持也为前端研发注入了全新的力量。本文为大家介绍什么是端智能,端智能的应用场景以及 Web
端侧实现 AI
的基本原理概念。
什么是端智能
首先,回顾一个AI
应用的开发流程,具体步骤包括了
数据的采集与预处理
模型的选取与训练
模型的评估
模型服务部署
模型训练的中间产物为一个模型文件,通过加载模型文件,部署为可调用的服务,然后就可以调用进行推理预测了。
在传统流程中,模型服务会部署在高性能的服务器上,由客户端发起请求,由服务器端进行推理,将预测结果返回给客户端,而端智能则是在客户端上完成推理的过程。
端智能的应用场景
端智能现在已经有非常多的应用场景,涵盖视觉类的 AR
、互动游戏,推荐类的信息流推荐,触达类的智能Push
等,语音类的直播、智能降噪等多个领域。算法逐渐从服务端覆盖到用户实时感知更强的移动终端。
典型应用包括了
AR
应用、游戏。由 AI
提供理解视觉信息的能力,由 AR
根据视觉信息来实现虚实结合的交互,带来更沉浸式的购物、互动体验。比如美颜相机、虚拟试妆,即是通过检测人脸面部的关键点,在特定区域使用 AR
增强、渲染妆容。
互动游戏。飞猪双十一的互动游戏"找一找", 即是一个跑在 h5
页面的图片分类应用,通过摄像头实时捕捉图片,调用分类模型进行分类,当出现游戏设定目标时得分。
端侧重排。通过实时的用户意识识别,对服务器推荐算法下发的feeds
流进行重新排列,做出更精准的内容推荐。
智能Push。通过端侧感知用户状态,决策是否需要向用户实施干预,推送Push
,选取合适的时机主动触达用户,而非服务器端定时的批量推送,带来更精准的营销,更好的用户体验。
端智能的优势
从普遍的应用场景,可以看到端智能的明显优势,包括了
低延时
实时的计算节省了网络请求的时间。对于高帧率要求的应用,比如美颜相机每秒都要请求服务器,高延迟绝对是用户所不能接受的。而对于高频交互场景,比如游戏,低延时变得更为重要。
低服务成本
本地的计算节省了服务器资源,现在的新手机发布都会强调手机芯片的 AI
计算能力,越来越强的终端性能让更多的端上 AI
应用成为了可能。
保护隐私
数据隐私的话题在今天变得越来越重要。通过在端侧进行模型的推理,用户数据不需要上传到服务器,保证了用户隐私的安全。
端智能的局限
同时,端智能也有一个最明显的局限,就是低算力,虽然端侧的性能越来越强,但是和服务器相比还是相差甚远。为了在有限的资源里做复杂的算法,就需要对硬件平台进行适配,做指令级的优化,让模型能够在终端设备中跑起来,同时,需要对模型进行压缩,在时间和空间上减少消耗。
现在已经有一些比较成熟的端侧推理引擎了,这些框架、引擎都对终端设备做了优化来充分发挥设备的算力。比如Tensorflow Lite
、Pytorch mobile
、阿里的 MNN
、百度飞桨 PaddlePaddle
。
Web端呢
Web
端同样拥有端侧 AI
的优势与局限,作为在 PC
上用户访问互联网内容和服务的主要手段,在移动端很多APP
也会嵌入 Web
页面,但是浏览器内存和存储配额的有限,让 Web
上运行 AI
应用看上去更不可能。
然而在 2015
年的时候就已经出现了一个 ConvNetJS
的库,可以在浏览器里用卷积神经网络做分类、回归任务,虽然现在已经不维护了,2018
年的时候涌现了非常多的JS
的机器学习、深度学习框架。如 Tensorflow.js
、 Synaptic
、 Brain.js
、 Mind
、 Keras.js
、 WebDNN
等。
受限于浏览器算力,部分框架如 keras.js
、 WebDNN
框架只支持加载模型进行推理,而不能在浏览器中训练。
此外,一些框架不适用于通用的深度学习任务,它们支持的网络类型有所不同。比如 TensorFlow.js
、 Keras.js
和 WebDNN
支持了 DNN
、 CNN
和 RNN
。而 ConvNetJS
主要支持 CNN
任务,不支持 RNN
。Brain.js
和 synaptic
主要支持 RNN
任务,不支持 CNN
网络中使用的卷积和池化操作。Mind
仅支持基本的 DNN
。
在选择框架时需要看下是否支持具体需求。
Web端架构
Web
端是如何利用有限的算力的呢?
一个典型的 JavaScript
机器学习框架如图所示,从底向上分别是驱动硬件,使用硬件的浏览器接口,各种机器学习框架、图形处理库,最后是我们的应用。
CPU vs GPU
在 Web
浏览器中运行机器学习模型的一个先决条件是通过 GPU
加速获得足够的计算能力。
在机器学习中,尤其是深度网络模型,广泛使用的操作是将大矩阵与向量相乘,再与另一个向量做加法。这种类型的典型操作涉及数千或数百万个浮点操作,而是它们通常是可并行化的。
以一个简单的向量相加为例,将两个向量相加可分为许多较小的运算,即每个索引位置相加。这些较小的操作并不相互依赖。尽管 CPU
对每个单独的加法所需的时间通常更少,随着计算量规模的变大,并发会逐渐显示出优势。
WebGPU/WebGL vs WebAssembly
有了硬件之后,需要对硬件进行充分的利用。
WebGL
WebGL
是目前性能最高的GPU
利用方案,WebGL
为在浏览器中加速2D
和3D
图形渲染而设计,但可以用于神经网络的并行计算来加速推理过程,实现速度数量级的提升。
WebGPU
随着
Web
应用对可编程3D
图形、图像处理和GPU
访问需求的持续增强,为了在WEB
中引入GPU
加速科学计算性能,W3C
在2017
年提出了WebGPU
,作为下一代WEB
图形的的API
标准,具有更低的驱动开销,更好的支持多线程、使用GPU
进行计算。
WebAssembly
当终端设备没有
WebGL
支持或者性能较弱的时候,使用CPU
的通用计算方案为WebAssembly
。WebAssembly
是一种新的编码方式,可以在现代的网络浏览器中运行,它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如C / C ++
等语言提供一个编译目标,以便它们可以在Web
上运行。
Tensorflow.js
以 Tensorflow.js
为例,为了在不同的环境下实现运行,tensorflow
支持了不同的后端,根据设备条件自动选择相应的后端 ,当然也支持手动更改。
tf.setBackend('cpu');
console.log(tf.getBackend());
对一些通用模型进行测试,WebGL
速度大于普通 CPU
后端计算的 100
倍,WebAssembly
则比普通的 JS CPU
后端快 10-30
倍。
同时, tensorflow
也提供了 tfjs-node
版本,通过 C++
和 CUDA
代码编译的本机编译库驱动 CPU
、 GPU
进行计算,训练速度与 Python
版本的 Keras
相当。不需要切换常用语言,可以直接在 nodejs
服务上添加 AI
模块,而不是再启动一个 python
的服务。
模型压缩
有了框架对于硬件设备的适配,还需要对模型进行压缩,复杂的模型固然有更好的预测精度,但是高额的存储空间、计算资源的消耗,过长的推理速度在大部分移动端场景中还是难以接受的。
模型的复杂度在于模型结构的复杂以及海量的参数。模型文件中通常存储了两部分信息:结构与参数,如下图中简化的神经网络所示,每个方块对应一个神经元,每个神经元以及神经元中的连线上都是参数。
模型的推理即从左侧输入,通过与神经元进行计算,再通过连线加上权重传到下一层计算,到最终层得到预测输出。节点越多、连接越多,则计算量越大。
模型剪枝
对训练好的模型进行裁剪,是模型压缩的一个常见方式,网络模型中存在着大量冗余的参数,大量神经元激活值趋近于 0
,通过对无效节点或者是不那么重要的节点进行裁剪,可以减少模型的冗余。
最简单粗暴的剪枝即 DropOut
,在训练过程中随机对神经元进行丢弃。
大部分的剪枝方法会计算重要性因子,计算神经元节点对于最终的结果的重要性,剪去不那么重要的节点。
模型剪枝的过程是迭代反复进行的,并非剪枝后直接用来推理,通过剪枝后的训练恢复模型的精度,模型的压缩过程是在精度和压缩比例之间的不断权衡,在可接受的精度损失范围内选择最好的压缩效果。
模型量化
为了保证较高的精度,大部分的科学运算都是采用浮点型进行计算,常见的是 32
位浮点型和 64
位浮点型,即 float32
和 double64
。量化即是把高精度的数值转化为低精度。
如二值量化(1bit
量化)会直接将 Float32/float64
的值映射到 1bit
,存储空间直接压缩 32
倍/ 64
倍,计算时加载所需内存同样也会变小,更小的模型尺寸,带来更低的功耗以及更快的计算速度。除此还有8bit
量化、任意bit
量化。
知识蒸馏
知识蒸馏则是将深度网络中所学到的知识转移到另一个相对简单的网络中,先训练一个 teacher
网络,然后使用这个 teacher
网络的输出和数据的真实标签去训练 student
网络。
工具
模型压缩的实现较为复杂,如果只是面向应用,大概了解其作用原理即可,可以直接用封装好的工具。
比如 Tensorflow Model Optimization Toolkit
提供了量化功能,其官方对于一些通用模型进行了压缩测试,如下表可以看到,对于 mobilenet
模型,模型大小由 10M+
压缩到了 3、4M
,而模型的精度损失很小。
百度的飞桨提供的 PaddleSlim
则提供了上述三种压缩方法。
总结
综上,开发一个 Web
端上的 AI
应用,流程即变成了
针对特定场景设计算法、训练模型
对模型进行压缩
转换为推理引擎所需要的格式
加载模型进行推理预测
对于算法而言,通用的深度学习框架已然提供了若干的通用预训练模型,可以直接用来做推理,也可以在其基础上训练自己的数据集。模型的压缩与推理也可以使用现有的工具。
作者:凹凸实验室
链接:https://juejin.cn/post/7013674501116264484
收起阅读 »
可恶,又学到了一点 CSS
昨天在做笔记整理的时候,看到一个面试题,如何实现水平垂直居中,虽然心里有一点点数,但是看到好几种答案,还是决定亲自动手验证一番,这验证一开始就出现了小问题,接着就像捅了个马蜂窝一样,各种疑惑扑面而来,而我又想弄清楚,折腾大半天,终于把问题锁定到了 line-height
和 vertical-align
身上。
大家现在应该都用 flex
布局,但是毕竟折腾好一会呢,好歹记录一下自己的收获哈哈
1.疑惑代码
<div>
<div>我要水平垂直居中</div>
</div>
.container{
border: 2px solid black;
background-color: chartreuse;
width: 200px;
height: 200px;
text-align: center;
line-height: 200px;
}
.box{
display: inline-block;
line-height: normal;
font-size: 1rem;
vertical-align: middle;
background-color: cornflowerblue;
}
你别说,还真居中了:
2.一些知识点
2.1 水平垂直对齐
有一些疑惑真的只是自己无知哈哈哈
text-align
不仅可以作用在文本,还可以对行内元素和行内块元素有效果,设置水平对齐方式vertical-align
只对行内元素和行内块元素起作用,设置元素的垂直对齐方式
2.2 line-height
说来也蛮搞笑的,我对 line-height
的印象就是,当元素 height
等于 line-height
的时候,元素内部的文本就会垂直居中。但是昨天查资料的时候发现,这里面牵扯到了很多复杂的问题。
简单的和我一起学习一下吧,MDN 上边说的,line-height
可用于多行文本的间距或者是单行文本的高度等
看到这里大家可以去看一下,底下的两篇参考文章,得出以下结论:
- 没给元素设置高度时,元素高度采用的是
line-height
的高度,这个属性具有继承性。也自带默认值,所以当你给一个没有设置高度的元素设置line-height:0;
,即使里面有文本,它也是会塌陷的。 - 可以分为好几种盒子,当你设置
line-height
的时候,行内框是不会变化的,改变的是行距,它只由font-size
的决定。这其实就是上边元素height
等于line-height
的时候,元素内文本会垂直居中的原因。 - 取值为
number
时,line-height
为number
乘以当前元素的font-size
,取normal
时一般就是number
为 1.2
3. 疑惑产生
喜欢东拆拆西拆拆的我发现,上边代码注释掉 vertical-align: middle;
效果并没有变化,依旧垂直居中着,但是将它改成 vertical-align: top;
又起到作用了如下:
4.解决
真的,其实写博客之前我都没有理解为什么会出现这种怪异情况,但是写着写着就来灵感了,原来是这样哈哈哈
其实咱们把文字内容加一点,使它成为多行文本,效果就出来了,没注释掉 vertical-align: middle
的效果如下:
注释掉了,就是这个样子,没有垂直居中:
所以前面的只是巧合,因为是单行文本的原因,平衡上下行距,应该也不叫行距,应该就是为了平衡才会出现垂直居中的效果哦!
链接:https://juejin.cn/post/7015117674422206494
收起阅读 »
Vue中 前端实现生成 PDF 并下载
思路: 通过 html2canvas 将 HTML 页面转换成图片,然后再通过 jspdf 将图片的 base64 生成为 pdf 文件。
1. 安装及引入
// 将页面 html 转换成图片
npm install html2canvas --save
// 将图片生成 pdf
npm install jspdf --save
在项目主文件 main.js 中引入定义好的实现方法并注册
import htmlToPdf from '@/utils/htmlToPdf';
// 使用 Vue.use() 方法就会调用工具方法中的install方法
Vue.use(htmlToPdf);
2. 封装导出 pdf 文件方法
配置详解
let pdf = new jsPDF('p', 'pt', [pdfX, pdfY]);
第一个参数: l:横向 p:纵向
第二个参数:测量单位("pt","mm", "cm", "m", "in" or "px");
第三个参数:可以是下面格式,默认为“a4”。如需自定义格式,只需将大小作为数字数组传递,如:[592.28, 841.89];
a0 - a10
b0 - b10
c0 - c10
dl
letter
government-letter
legal
junior-legal
ledger
tabloid
credit-card
pdf.addPage() 在PDF文档中添加新页面,默认a4。参数如下:
pdf.addImage() 将图像添加到PDF。参数如下:
删除某页 pdf
let targetPage = pdf.internal.getNumberOfPages(); //获取总页
pdf.deletePage(targetPage); // 删除目标页
复制代码
保存 pdf 文档
pdf.save(`测试.pdf`);
复制代码
封装导出 pdf 文件方法(utils/htmlToPdf.js)
// 导出页面为PDF格式
import html2Canvas from 'html2canvas'
import JsPDF from 'jspdf'
export default{
install (Vue, options) {
Vue.prototype.getPdf = function () {
// 当下载pdf时,若不在页面顶部会造成PDF样式不对,所以先回到页面顶部再下载
let top = document.getElementById('pdfDom');
if (top != null) {
top.scrollIntoView();
top = null;
}
let title = this.exportPDFtitle;
html2Canvas(document.querySelector('#pdfDom'), {
allowTaint: true
}).then(function (canvas) {
// 获取canvas画布的宽高
let contentWidth = canvas.width;
let contentHeight = canvas.height;
// 一页pdf显示html页面生成的canvas高度;
let pageHeight = contentWidth / 841.89 * 592.28;
// 未生成pdf的html页面高度
let leftHeight = contentHeight;
// 页面偏移
let position = 0;
// html页面生成的canvas在pdf中图片的宽高(本例为:横向a4纸[841.89,592.28],纵向需调换尺寸)
let imgWidth = 841.89;
let imgHeight = 841.89 / contentWidth * contentHeight;
let pageData = canvas.toDataURL('image/jpeg', 1.0);
let PDF = new JsPDF('l', 'pt', 'a4');
// 两个高度需要区分: 一个是html页面的实际高度,和生成pdf的页面高度
// 当内容未超过pdf一页显示的范围,无需分页
if (leftHeight < pageHeight) {
PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
} else {
while (leftHeight > 0) {
PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
leftHeight -= pageHeight;
position -= 592.28;
// 避免添加空白页
if (leftHeight > 0) {
PDF.addPage();
}
}
}
PDF.save(title + '.pdf')
})
}
}
}
相关组件中应用
<template>
<div class="wrap" >
<div id="pdfDom" style="padding: 10px;">
<el-table
:data="tableData"
border>
<el-table-column prop="date" label="日期" width="250"></el-table-column>
<el-table-column prop="name" label="姓名" width="250"></el-table-column>
<el-table-column prop="address" label="地址"></el-table-column>
</el-table>
</div>
<button type="button" style="margin-top: 20px;" @click="btnClick">导出PDF</button>
</div>
</template>
<script>
export default {
data() {
return {
exportPDFtitle: "页面导出PDF文件名",
tableData: [
{
date: '2016-05-06',
name: '王小虎',
address: '重庆市九龙坡区火炬大道'
}, {
date: '2016-05-07',
name: '王小虎',
address: '重庆市九龙坡区火炬大道'
},{
date: '2016-05-03',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-02',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-04',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-01',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-08',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-06',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-06',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-07',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-01',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-08',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-06',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-07',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-06',
name: '王小虎',
address: '南京市江宁区将军大道'
}, {
date: '2016-05-07',
name: '王小虎',
address: '南京市江宁区将军大道'
},, {
date: '2016-05-04',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-01',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-08',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-06',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-07',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
},{
date: '2016-05-01',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-08',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-06',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-08',
name: '王小虎',
address: '武汉市洪山区文化大道'
}, {
date: '2016-05-06',
name: '王小虎',
address: '武汉市洪山区文化大道'
}, {
date: '2016-05-07',
name: '王小虎',
address: '武汉市洪山区文化大道'
}, {
date: '2016-05-06',
name: '王小虎',
address: '南京市江宁区将军大道'
}, {
date: '2016-05-07',
name: '王小虎',
address: '武汉市洪山区文化大道'
},
]
}
},
methods: {
btnClick(){
this.$nextTick(() => {this.getPdf();})
},
},
}
</script>
效果
待优化部分
分页时,页面内容被截断(欢迎留言讨论交流);
不同内容,另起一页开始;思路:计算超出内容,占最后一页的高度(设定间距 = 页面高度 - 超出部分高度)。
作者:明天也要努力
链接:https://juejin.cn/post/7016249316834557959
收起阅读 »
你还在为pc端适配而烦恼吗?相信我,看了之后就不烦恼了
作为一名前端开发者,你有没有遇到过这种头痛的事情。每次开发pc端的网页时,不管是官网还是管理后台,UI设计师都是按照1920*1080
(16:9)的比例来给你提供设计稿的,导致你画页面的时候。会出现两种情况。
第一种按照设计师提供的设计稿比例画页面的话,导致在不同比例的屏幕上,就会呈现不同的样式效果,有的过大有的过小,这时候用户就会问你,你是不是写的bug。。。很无语
第二种就是按照设计师提供的比例按自适应的方式来画页面,这样导致页面的尺寸与设计稿不同,并且在不同设备也会有差别,这时候设计师跟用户就会跑过来说,这里是不是少了1px的单位,这里是不是写错了。。。很头疼
今天我就介绍一种可以解决pc端网页适配的方法,大家觉得有用的话,就给我个小小点赞,也可以在评论区留言。
PC实现适配也是用了rem
这个css3属性,rem
相对于根元素(即html元素)font-size
计算值的倍数。这里以PC常见的分辨率1920px和1366px(14寸笔记本)为例说明。为了更好的说明,假设设计师给的设计稿是1920px,我们既要做1920px屏幕,也要给1366px的屏幕做适配。
现在我们随便取1920px设计稿一块区域,假设宽度273px
,高度随意。那么在1366px屏幕上宽度应该显示多少呢?
我们将屏幕宽度等比分成100份
//1920分辨率屏幕
avg = 1920 / 100 = 19.20 px
//1366分辨率屏幕
avg = 1366 / 100 = 13.66 px
在1366分辨率屏幕应该显示宽度 = (1366 * 273) / 1920
最后是194.228125
px
//1920分辨率屏幕定义根
font-size = 19.20px //即 1rem = 19.20px
//1366分辨率屏幕
font-size = 13.66px //即 1rem = 13.66px
适配代码
html{
font-size:19.20px; /*默认以设计稿为基准*/
}
@media only screen and (max-width: 1366px) {
html{
font-size:13.66px;
}
}
#test{
width:14.21875rem;
}
id为test
的盒子在1920屏幕宽度= 14.21875 * 19.20
最后是273
id为test
的盒子在1366屏幕宽度= 14.21875 * 13.66
最后是194.228125
这样一来我们就适配了1920px和1366px屏幕。PC一般也就是这两个分辨率占多数,兼容了这两个分辨率屏幕基本就可以了。在说下国内基本没有在兼容IE8的浏览器了。基本都是IE9+,css3属性在IE9+上还是可以使用的。不过建议小伙伴们使用前还是确定下,浏览器兼容
最后在对上面补充点,有的小伙伴可能觉得每次设置宽高前都要手动的转换,实在是太麻烦,不要着急我为大家找了个sass方法。
// PX 转 rem
@function px2Rem($px, $base-font-size: 19.2px) {
@if (unitless($px)) {
//有无单位
@return ($px / 19.2) * 1rem;
} @else if (unit($px) == em) {
@return $px;
}
@return ($px / $base-font-size) * 1rem;
}
测试下上面的方法
#test{
width:px2Rem(273px)
}
//输出
#test{
width:14.21875rem;
}
大家将屏幕分辨率调整为1920px
和1366px
来查看灰色区域宽度。
内容
@function px2Rem($px, $base-font-size: 19.2px) {
@if (unitless($px)) { //有无单位
@return ($px / 19.2) * 1rem;
} @else if (unit($px) == em) {
@return $px;
}
@return ($px / $base-font-size) * 1rem;
}
*{
margin:0;
padding:0;
}
html{
font-size:19.20px;
}
html,body{
height:100%;
}
body{
font-size:px2Rem(16px);
}
#app{
position:relative;
width:100%;
height:100%;
}
.nav{
position:fixed;
left:0;
top:0;
width:px2Rem(273px);
height:100%;
background-color:#E4E4E4;
transition:all .3s;
z-index:11;
}
.content{
position:absolute;
left:px2Rem(273px);
top:0;
right:0;
bottom:0;
width:auto;
height:100%;
background-color:#CBE9CB;
overflow-y:auto;
z-index:10;
}
p{
font-size:px2Rem(20px);
}
@media only screen and (max-width: 1366px) {
html{
font-size:13.66px;
}
}
效果图
小伙伴们如果有更好的PC适配方案也可给我讲讲,欢迎在下方留言
sass下使用变量
height: calc(100% - #{px2rem(200px)});
链接:https://juejin.cn/post/7015257656193449992
收起阅读 »
你会用ES6,那倒是用啊!
不是标题党,这是一位leader在一次代码评审会对小组成员发出的“怒吼”,原因是在代码评审中发现很多地方还是采用ES5的写法,也不是说用ES5写法不行,会有BUG,只是造成代码量增多,可读性变差而已。
恰好,这位leader有代码洁癖,面对3~5年经验的成员,还写这种水平的代码,极为不满,不断对代码进行吐槽。不过对于他的吐槽,我感觉还是有很大收获的,故就把leader的吐槽记录下来,分享给掘友们,觉得有收获点个赞,有错误的或者更好的写法,非常欢迎在评论中留言。
ps:ES5之后的JS语法统称ES6!!!
一、关于取值的吐槽
取值在程序中非常常见,比如从对象obj
中取值。
const obj = {
a:1,
b:2,
c:3,
d:4,
e:5,
}
吐槽:
const a = obj.a;
const b = obj.b;
const c = obj.c;
const d = obj.d;
const e = obj.e;
或者
const f = obj.a + obj.d;
const g = obj.c + obj.e;
吐槽:“不会用ES6的解构赋值来取值吗?5行代码用1行代码搞定不香吗?直接用对象名加属性名去取值,要是对象名短还好,很长呢?搞得代码中到处都是这个对象名。”
改进:
const {a,b,c,d,e} = obj;
const f = a + d;
const g = c + e;
反驳
不是不用ES6的解构赋值,而是服务端返回的数据对象中的属性名不是我想要的,这样取值,不是还得重新创建个遍历赋值。
吐槽
看来你对ES6的解构赋值掌握的还是不够彻底。如果想创建的变量名和对象的属性名不一致,可以这么写:
const {a:a1} = obj;
console.log(a1);// 1
补充
ES6的解构赋值虽然好用。但是要注意解构的对象不能为undefined
、null
。否则会报错,故要给被解构的对象一个默认值。
const {a,b,c,d,e} = obj || {};
二、关于合并数据的吐槽
比如合并两个数组,合并两个对象。
const a = [1,2,3];
const b = [1,5,6];
const c = a.concat(b);//[1,2,3,1,5,6]
const obj1 = {
a:1,
}
const obj1 = {
b:1,
}
const obj = Object.assgin({}, obj1, obj2);//{a:1,b:1}
吐槽
ES6的扩展运算符是不是忘记了,还有数组的合并不考虑去重吗?
改进
const a = [1,2,3];
const b = [1,5,6];
const c = [...new Set([...a,...b])];//[1,2,3,5,6]
const obj1 = {
a:1,
}
const obj2 = {
b:1,
}
const obj = {...obj1,...obj2};//{a:1,b:1}
三、关于拼接字符串的吐槽
const name = '小明';
const score = 59;
const result = '';
if(score > 60){
result = `${name}的考试成绩及格`;
}else{
result = `${name}的考试成绩不及格`;
}
吐槽
像你们这样用ES6字符串模板,还不如不用,你们根本不清楚在${}
中可以做什么操作。在${}
中可以放入任意的JavaScript表达式,可以进行运算,以及引用对象属性。
改进
const name = '小明';
const score = 59;
const result = `${name}${score > 60?'的考试成绩及格':'的考试成绩不及格'}`;
四、关于if中判断条件的吐槽
if(
type == 1 ||
type == 2 ||
type == 3 ||
type == 4 ||
){
//...
}
吐槽
ES6中数组实例方法includes
会不会使用呢?
改进
const condition = [1,2,3,4];
if( condition.includes(type) ){
//...
}
五、关于列表搜索的吐槽
在项目中,一些没分页的列表的搜索功能由前端来实现,搜索一般分为精确搜索和模糊搜索。搜索也要叫过滤,一般用filter
来实现。
const a = [1,2,3,4,5];
const result = a.filter(
item =>{
return item === 3
}
)
吐槽
如果是精确搜索不会用ES6中的find
吗?性能优化懂么,find
方法中找到符合条件的项,就不会继续遍历数组。
改进
const a = [1,2,3,4,5];
const result = a.find(
item =>{
return item === 3
}
)
六、关于扁平化数组的吐槽
一个部门JSON数据中,属性名是部门id,属性值是个部门成员id数组集合,现在要把有部门的成员id都提取到一个数组集合中。
const deps = {
'采购部':[1,2,3],
'人事部':[5,8,12],
'行政部':[5,14,79],
'运输部':[3,64,105],
}
let member = [];
for (let item in deps){
const value = deps[item];
if(Array.isArray(value)){
member = [...member,...value]
}
}
member = [...new Set(member)]
吐槽
获取对象的全部属性值还要遍历吗?Object.values
忘记了吗?还有涉及到数组的扁平化处理,为啥不用ES6提供的flat
方法呢,还好这次的数组的深度最多只到2维,还要是遇到4维、5维深度的数组,是不是得循环嵌套循环来扁平化?
改进
const deps = {
'采购部':[1,2,3],
'人事部':[5,8,12],
'行政部':[5,14,79],
'运输部':[3,64,105],
}
let member = Object.values(deps).flat(Infinity);
其中使用Infinity
作为flat
的参数,使得无需知道被扁平化的数组的维度。
补充
flat
方法不支持IE浏览器。
七、关于获取对象属性值的吐槽
const name = obj && obj.name;
吐槽
ES6中的可选链操作符会使用么?
改进
const name = obj?.name;
八、关于添加对象属性的吐槽
当给对象添加属性时,如果属性名是动态变化的,该怎么处理。
let obj = {};
let index = 1;
let key = `topic${index}`;
obj[key] = '话题内容';
吐槽
为何要额外创建一个变量。不知道ES6中的对象属性名是可以用表达式吗?
改进
let obj = {};
let index = 1;
obj[`topic${index}`] = '话题内容';
九、关于输入框非空的判断
在处理输入框相关业务时,往往会判断输入框未输入值的场景。
if(value !== null && value !== undefined && value !== ''){
//...
}
吐槽
ES6中新出的空值合并运算符了解过吗,要写那么多条件吗?
if(value??'' !== ''){
//...
}
十、关于异步函数的吐槽
异步函数很常见,经常是用 Promise 来实现。
const fn1 = () =>{
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 300);
});
}
const fn2 = () =>{
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2);
}, 600);
});
}
const fn = () =>{
fn1().then(res1 =>{
console.log(res1);// 1
fn2().then(res2 =>{
console.log(res2)
})
})
}
吐槽
如果这样调用异步函数,不怕形成地狱回调啊!
改进
const fn = async () =>{
const res1 = await fn1();
const res2 = await fn2();
console.log(res1);// 1
console.log(res2);// 2
}
补充
但是要做并发请求时,还是要用到Promise.all()
。
const fn = () =>{
Promise.all([fn1(),fn2()]).then(res =>{
console.log(res);// [1,2]
})
}
如果并发请求时,只要其中一个异步函数处理完成,就返回结果,要用到Promise.race()
。
十一、后续
欢迎来对以上十点leader的吐槽进行反驳,你的反驳如果有道理的,下次代码评审会上,我替你反驳。
此外以上的整理内容有误的地方,欢迎在评论中指正,万分感谢。
如果你还有其它想吐槽的,也非常欢迎在评论中留下你的吐槽。
作者:红尘炼心
链接:https://juejin.cn/post/7016520448204603423
收起阅读 »
国庆假期,整整七天,我使用Flutter终于做出了即时通信!!!?
前言:在这个假期,我完成了一个小Demo,Flutter 与 Springboot 进行websocket的通讯,为啥想要去做这个Demo呢,主要是在各大平台以及google搜索后发现,没有一个详细的例子来教大家进行一对一、一对多的通讯,大多数都是教你怎么连接,却没有教你怎么去进行下一步的功能实现,于是我利用了五天的假期,踩了无数的坑,终于是完成了它,所以,点个赞吧,不容易啊,兄弟们😭
源码在文章最后,直接运行就完事,服务端我都帮兄弟们架包打好了,运行一下就行,运行方法在文末简单叙述了😎
服务端分析:Springboot WebSocket 即时通讯
先上效果图(我自己搜索这样功能性的问题时,没有效果图基本上都是不想看的):
即时通讯最重要的功能是完成了(发送文字信息)
阅读本文的注意点:
1.需要一点WebSocket的原理知识
2.Flutter使用WebSocket的方式,本文使用 'dart:io' ,大家也可以使用插件
正文:
1.WebSocket的简单原理
很多同学在第一次碰到这个协议时会发现它与HTTP相似,于是就会问,我们已经有了 HTTP 协议,为什么还需要WebSocket?它有什么特殊的地方呢?
其实是因为 HTTP 协议有一个缺陷:通信只能由客户端发起。
而WebSocket首先是一个持久化的协议,它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,这个协议非常适合即时通讯或者消息的推送。
2.Flutter中怎么使用WebSocket
有两种方式:
1.Flutter自带的 'dart:io'
连接WebSocket服务器
Future<WebSocket> webSocketFuture = WebSocket.connect('ws://192.168.13.32:9090'); //connect中放服务端地址
存放WebSocket.connect返回的对象
static WebSocket _webSocket;
发送消息
_webSocket.add('发送消息内容');
监听接收消息,调用listen方法
void onData(dynamic content) {
print('收到消息:'+content);
}
_webSocket.listen(onData, onDone: () {
print('onDone');
}, onError: () {
print('onError');
}, cancelOnError: true);
例子:
webSocketFuture.then((WebSocket ws) {
_webSocket = ws;
void onData(dynamic content) {
print('收到新的消息');
}
// 调用add方法发送消息
_webSocket.add('message');
// 监听接收消息,调用listen方法
_webSocket.listen(onData, onDone: () {
print('onDone');
}, onError: () {
print('onError');
}, cancelOnError: true);
});
关闭WebSocket连接
_webSocket.close();
2.第三方插件库实现 WebSocket
基本使用步骤也都是:连接 WebSocket 服务器、发送消息、接收消息、关闭 WebSocket 连接。
- 在项目的 pubspec.yaml 里加入引用:
dependencies:
web_socket_channel: 官网最新版本
- 导入包:
import 'package:web_socket_channel/io.dart';
- 连接 WebSocket 服务器:
var channel = IOWebSocketChannel.connect("ws://192.168.13.32:9090");
通过IOWebSocketChannel我们便可以进行各种操作
- 发送消息:
channel.sink.add("connected!");
- 监听接收消息:
channel.stream.listen((message) { print('收到消息:' + message); });
- 关闭 WebSocket 连接:
channel.sink.close();
以上就是 Flutter 通过第三方插件库实现 WebSocket 通信功能的基本步骤。
3.联系人界面以及对话界面的UI实现
我的小部件都进行了封装,源码请看文章最后,分析部分只放重要代码
对话框ui处理
顶部使用appbar,包含一个返回按钮,用户信息以及状态,还有一个设置按钮(没有什么难点就不放代码了)
- 双方信息处理
这里有个ui处理难点,就是分析信息是谁发出的,是自己还是对方呢
这里我选择在每条信息Json格式的末尾加上一个判断符:messageType,”receiver“代表对方发的信息,”sender“代表是自己发的
ui处理:
ListView.builder( itemCount: UserMessage.messages.length, //总发送信息的条数 shrinkWrap: true, padding: const EdgeInsets.only(top: 10, bottom: 10), physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return Container( padding: const EdgeInsets.only( left: 14, right: 14, top: 10, bottom: 10), child: Align( alignment: (UserMessage.messages[index].messageType == "receiver" ? Alignment.topLeft : Alignment.topRight), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), color: (UserMessage.messages[index].messageType == "receiver" ? Colors.grey.shade200 : Colors.blue[200]), ), padding: const EdgeInsets.all(16), child: Text( UserMessage.messages[index].messageContent, style: TextStyle(fontSize: 15), ))), ); },),
单个联系人的ui
一行为一个联系人模块,其内包含用户头像,用户姓名,即时通讯内容,以及上一次对话的时间,点击每行跳转到相对应的聊天框。
封装处理:
class ConversationList extends StatefulWidget { String name; //用户姓名 String messageText; //即时内容 String imageUrl; //用户头像 String time;//上一次对话时间 bool isMessageRead; //用于处理字体大小 ConversationList( {Key? key, required this.name, required this.messageText, required this.imageUrl, required this.time, required this.isMessageRead}) : super(key: key); @override _ConversationListState createState() => _ConversationListState();}
详细布局:
return GestureDetector( onTap: () { Navigator.push(context, MaterialPageRoute(builder: (context){ return ChatDetailPage(name:widget.name,userImageUrl:widget.imageUrl); })); }, child: Container( padding: const EdgeInsets.only(left: 16, right: 16, top: 10, bottom: 10), child: Row( children: <Widget>[ Expanded( child: Row( children: <Widget>[ CircleAvatar( backgroundImage: AssetImage(widget.imageUrl), maxRadius: 30, ), const SizedBox( width: 16, ), Expanded( child: Container( color: Colors.transparent, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( widget.name, style: const TextStyle(fontSize: 16), ), const SizedBox( height: 6, ), Text( widget.messageText, style: TextStyle( fontSize: 13, color: Colors.grey.shade600, fontWeight: widget.isMessageRead ? FontWeight.bold : FontWeight.normal), ), ], ), ), ), ], ), ), Text( widget.time, style: TextStyle( fontSize: 12, fontWeight: widget.isMessageRead ? FontWeight.bold : FontWeight.normal), ), ], ), ), );}
列表实现:
这里简单使用ListView.builder
ListView.builder( itemCount: chatUsers.length, shrinkWrap: true, padding: const EdgeInsets.only(top: 16), physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index){ return ConversationList( name: chatUsers[index].name, messageText: chatUsers[index].messageText, imageUrl: chatUsers[index].imageURL, time: chatUsers[index].time, isMessageRead: (index == 0 || index == 3)?true:false, ); },),
ui的难点部分在这里就分析完成了
4.Flutter WebSocket处理
因为是个Demo,封装的信息比较简单
对发送的信息进行封装
class ChatMessage { String messageContent; String messageType; ChatMessage({required this.messageContent, required this.messageType});}
对基本信息,以及用户信息进行封装,
class UserMessage{ static int userId = 0; static String socketUrl = "ws://192.168.10.104:9090/websocket/"; ///将对话暂存在这里 static List<ChatMessage> messages = [ ];}
初始化时连接服务器并且对其进行监听
当数据返回时,保存到内存中
//连接wensocket并且监听void connect(String userName) { Future<WebSocket> futureWebSocket = WebSocket.connect(UserMessage.socketUrl + "/$userName"); //socket地址 futureWebSocket.then((WebSocket ws) { _webSocket = ws; _webSocket.readyState; // 监听事件 void onData(dynamic content) { print('收到消息:' + content); setState(() { if (content.toString().substring(0, 1) == UserMessage.userId.toString()) { ///自己发送的消息(服务端没有完善) } else { UserMessage.messages.add(ChatMessage( messageContent: content.toString().substring( 2, ), messageType: "receiver")); } }); } _webSocket.listen(onData, onError: (a) => print("error"), onDone: () => print("done")); });}
发送信息处理
对数据需要将json对象转换为json字符串
// 向服务器发送消息void sendMessage(dynamic message) { print(convert.jsonEncode(message)); _webSocket.add(convert.jsonEncode(message);}
onPressed: () { var toUser = "0"; //服务端没有完善,这里固定用户id了 if (UserMessage.userId == 0) { toUser = "1"; } else { toUser = "0"; } var message = { "msg": _controller.text, "toUser": toUser, "type": 1 }; ///传递信息 sendMessage(message); //发送信息 setState(() { //更新ui UserMessage.messages.add( ChatMessage( messageContent: _controller.text, messageType: "sender"), ); _controller.clear(); //清除输入框内文字 });},
在退出页面时,关闭WebSocket连接
@overridevoid dispose() { // TODO: implement dispose super.dispose(); closeSocket();}
5.该文章项目运行步骤
下载代码压缩包,解压后的目录是这样的:
其中images是图片,lib是源代码,jar包是服务端
第一步:创建一个新项目,源代码是使用了空安全的,也可以创建不是空安全的项目,改一下代码即可
第二步:将images与lib复制进去
第三步:在pubspec.yaml中配置静态图片
assets: - images/
第四步:运行.jar包
切换到包存放的地址,服务端的端口为9090,如果与各位的端口冲突可以修改服务端代码,或者停止你在使用9090的这个端口
java -jar 包名.jar
查找端口:
netstat -aon|findstr 9090
查看指定 PID 的进程
tasklist|findstr 10021
结束进程
强制(/F参数)杀死 pid 为 10021的所有进程包括子进程(/T参数):
taskkill /T /F /PID 9088
第五步:在cmd中查找自己的ip,然后在代码的这里修改
第六步:运行后的操作
运行后会先进入登录界面,这里第一只手机输入0 ,第二只手机输入1,因为服务端默认从0开始(给用户分配id),因为没有数据库。
这样进入就可以了,然后就可以像效果图一样开始交流
服务端有问题可以参考这篇文章:Springboot WebSocket 即时通讯
作者:阿Tya
链接:https://juejin.cn/post/7016606314025451557
收起阅读 »
设计模式-工厂方法模式
工厂方法模式(Factory Method)又称为多态性工厂模式,其核心不再像简单工厂模式那样负责所有的子类的创建,而是将具体的创建工作交给子类去完成
在前文已经介绍简单工厂模式 时,写了如下代码:
/**
* type:角色类型 - 管理员、员工
* name:对应角色的名字
*/
const Factory = (type, name) => {
switch (type) {
case "admin": // 创建管理员
return new Admin(name, ["user", "salary", "vacation"]);
case "staff": // 创建员工
return new Staff(name, ["vacation"]);
default: // 健壮性处理
throw new Error("暂不支持该角色的创建");
}
};
虽然其可以让我们在创建实例的时候很爽,不用关心内部具体的实现,通过观察代码,可以发现其存在的问题:
- 不符合设计原则-开放封闭原则
当每一次新增一个权限角色的时候,对需要对上面的代码进行修改,严重破坏了原有的代码和业务逻辑,与开放封闭原则背离
- 容易变成面条代码
虽然角色越来越多,那么内部的case也会随之变得越来越多,简单工厂函数的内容也随着变得冗余
理想情况下,我们是希望在新增新的权限角色时,对于老的代码无任何的修改便可以完成新功能的增加
首先看一下工厂方法模式的UML:
其相比简单工厂模式,会多了一个工厂方法,Admin类对应多了一个AdminFactory类,现在只需要通过AdminFactory类创建实例即可
接下来看看工厂方法模式如何创建:
class Person {
constructor(name, permission) {
this.name = name;
this.permission = permission;
}
}
/**
* 管理员
*/
class Admin extends Person {
constructor(name, permission) {
super(name, permission);
}
}
/**
* 管理员的工厂方法
*/
class AdminFactory {
static create(name) {
return new Admin(name, ["user", "salary", "vacation"]);
}
}
/**
* 员工
*/
class Staff extends Person {
constructor(name, permission) {
super(name, permission);
}
}
/**
* 员工的工厂方法
*/
class StaffFactory {
static create(name) {
return new Staff(name, ["vacation"]);
}
}
const admin = AdminFactory.create("管理员");
const zs = StaffFactory.create("张三");
const ls = StaffFactory.create("李四");
若是需要创建新的权限角色,只需要创建对应的工厂方法即可,完全符合开放封闭原则,也可以避免面条式代码,具体实例创建都是通过对应工厂方法创建
作者:Nordon
链接:https://juejin.cn/post/7016149646334492679
收起阅读 »
设计模式-适配器模式
适配器模式又称为包装器模式,将一个类的接口转化为用户需要的另外一个接口,主要是为了解决对象之间接口不兼容的问题,比如随着业务迭代升级出现了旧的接口与心的接口不兼容,这个时候不可能强制使用旧接口的用户去升级,而是在中间加一个适配器进行转换,让旧接口的使用者无感使用,保证了稳定性,在日常生活中适配器的案例随处可见,比如耳机插口不统一、充电接口不统一等,这个时候就需要一个适配器来解决问题
UML:
接下看一下UML对应的代码实现:
class Target {
constructor(type) {
let result;
switch (type) {
case "adapter":
result = new Adapter();
break;
default:
result = null;
}
return result;
}
Request() {
console.log('Target Request');
}
}
class Adaptee {
constructor() {
console.log("Adaptee created");
}
SpecificRequest() {
console.log("Adaptee request");
}
}
class Adapter extends Adaptee {
constructor() {
super();
console.log("Adapter created");
}
Request() {
return this.SpecificRequest();
}
}
function init_Adapter() {
var f = new Target("adapter");
f.Request();
}
init_Adapter();
应用场景
开发中常用的axios,其支持node端和浏览器端,那么在不同端调用axios需要进行不同的处理,而这些对于使用者而言都是无感的,我们在使用的时候都是使用同一套API直接干就完事了,不会在意内部具体做了些什么,这个时候就需要使用适配器来抹平不同端的差异,让使用者用着开心
接下来可以模拟简单实现这个过程
使用axios请求一个接口:
axios({
url: "xxx",
method: "GET",
})
.then((res) => {
console.log("success:", res);
})
.catch((err) => {
console.log("fail:", err);
});
接下来需要手动实现axios函数:
function axios(config) {
let adaptor = getDefaultAdaptor();
// 无论是node端 还是 浏览器端,在使用的时候都只是转入一个config配置对象,返回一个 Promise 对象
return adaptor(config);
}
上文说到因为axios可以在浏览器端和node端使用,getDefaultAdaptor函数就是起到适配器的作用,根据不同的环境分别创建不同的adaptor
/**
* 适配器
*/
function getDefaultAdaptor() {
let adaptor;
if (typeof XMLHttpRequest !== "undefined") {
// 是浏览器环境
adaptor = xhr;
} else if (typeof process !== "undefined") {
// node 环境
adaptor = http;
}
return adaptor;
}
其中xhr和http为两个函数,用于创建具体的请求,至此适配器的使用已经完成,接下来就看看不同端是如何实现的接口请求
浏览器端:
/**
* 浏览器环境
*/
function xhr(config) {
return new Promise((resolve, reject) => {
const req = new XMLHttpRequest();
req.open(config.method, config.url, true);
req.onreadystatechange = function () {
if (req.readyState === 4) {
if (req.status >= 200 && req.status < 300) {
resolve(req.responseText);
} else {
reject("请求失败");
}
}
};
req.send();
});
}
node端:
/**
* node 环境
*/
function http(config) {
const url = require("url");
const http = require("http");
// 将需要的参数 解析出来
const { hostname, port, path } = url.parse(config.url);
return new Promise((resolve, reject) => {
const options = {
hostname,
port,
path,
method: config.method,
};
const req = http.request(options, function (response) {
let chunks = [];
response.on("data", (chunk) => {
chunks.push(chunk);
});
response.on("end", () => {
const res = Buffer.concat(chunks).toString();
resolve(res);
});
});
// 监听请求异常
req.on("error", (err) => {
reject(err);
});
// 请求发送完毕
ren.end();
});
}
作者:Nordon
链接:https://juejin.cn/post/7016215674322157581
收起阅读 »
两个 Node.js 进程如何通信?
两个 Node.js 进程之间如何进行通信呢?这里要分两种场景:
- 不同电脑上的两个 Node.js 进程间通信
- 同一台电脑上两个 Node.js 进程间通信
对于第一种场景,通常使用 TCP 或 HTTP 进行通信,而对于第二种场景,又分为两种子场景:
- Node.js 进程和自己创建的 Node.js 子进程通信
- Node.js 进程和另外不相关的 Node.js 进程通信
前者可以使用内置的 IPC 通信通道,后者可以使用自定义管道,接下来进行详细介绍:
不同电脑上的两个 Node.js 进程间通信
要想进行通信,首先得搞清楚如何标识网络中的进程?网络层的 ip 地址可以唯一标识网络中的主机,而传输层的协议和端口可以唯一标识主机中的应用程序(进程),这样利用三元组(ip 地址,协议,端口)就可以标识网络的进程了。
使用 TCP 套接字
TCP 套接字(socket)是一种基于 TCP/IP 协议的通信方式,可以让通过网络连接的计算机上的进程进行通信。一个作为 server 另一个作为 client,server.js 代码如下:
const net = require('net')
const server = net.createServer(socket => {
console.log('socket connected')
socket.on('close', () => console.log('socket disconnected'))
socket.on('error', err => console.error(err.message))
socket.on('data', data => {
console.log(`receive: ${data}`)
socket.write(data)
console.log(`send: ${data}`)
})
})
server.listen(8888)
client.js 代码:
const net = require('net')
const client = net.connect(8888, '192.168.10.105')
client.on('connect', () => console.log('connected.'))
client.on('data', data => console.log(`receive: ${data}`))
client.on('end', () => console.log('disconnected.'))
client.on('error', err => console.error(err.message))
setInterval(() => {
const msg = 'hello'
console.log(`send: ${msg}`)
client.write(msg)
}, 3000)
运行效果:
$ node server.js
client connected
receive: hello
send: hello
$ node client.js
connect to server
send: hello
receive: hello
使用 HTTP 协议
因为 HTTP 协议也是基于 TCP 的,所以从通信角度看,这种方式本质上并无区别,只是封装了上层协议。server.js 代码为:
const http = require('http')
http.createServer((req, res) => res.end(req.url)).listen(8888)
client.js 代码:
const http = require('http')
const options = {
hostname: '192.168.10.105',
port: 8888,
path: '/hello',
method: 'GET',
}
const req = http.request(options, res => {
console.log(`statusCode: ${res.statusCode}`)
res.on('data', d => process.stdout.write(d))
})
req.on('error', error => console.error(error))
req.end()
运行效果:
$ node server.js
url /hello
$ node client.js
statusCode: 200
hello
同一台电脑上两个 Node.js 进程间通信
虽然网络 socket 也可用于同一台主机的进程间通讯(通过 loopback 地址 127.0.0.1),但是这种方式需要经过网络协议栈、需要打包拆包、计算校验和、维护序号和应答等,就是为网络通讯设计的,而同一台电脑上的两个进程可以有更高效的通信方式,即 IPC(Inter-Process Communication),在 unix 上具体的实现方式为 unix domain socket,这是服务器端和客户端之间通过本地打开的套接字文件进行通信的一种方法,与 TCP 通信不同,通信时指定本地文件,因此不进行域解析和外部通信,所以比 TCP 快,在同一台主机的传输速度是 TCP 的两倍。
使用内置 IPC 通道
如果是跟自己创建的子进程通信,是非常方便的,child_process 模块中的 fork 方法自带通信机制,无需关注底层细节,例如父进程 parent.js 代码:
const fork = require("child_process").fork
const path = require("path")
const child = fork(path.resolve("child.js"), [], { stdio: "inherit" });
child.on("message", (message) => {
console.log("message from child:", message)
child.send("hi")
})
子进程 child.js 代码:
process.on("message", (message) => {
console.log("message from parent:", message);
})
if (process.send) {
setInterval(() => process.send("hello"), 3000)
}
运行效果如下:
$ node parent.js
message from child: hello
message from parent: hi
message from child: hello
message from parent: hi
使用自定义管道
如果是两个独立的 Node.js 进程,如何建立通信通道呢?在 Windows 上可以使用命名管道(Named PIPE),在 unix 上可以使用 unix domain socket,也是一个作为 server,另外一个作为 client,其中 server.js 代码如下:
const net = require('net')
const fs = require('fs')
const pipeFile = process.platform === 'win32' ? '\\\\.\\pipe\\mypip' : '/tmp/unix.sock'
const server = net.createServer(connection => {
console.log('socket connected.')
connection.on('close', () => console.log('disconnected.'))
connection.on('data', data => {
console.log(`receive: ${data}`)
connection.write(data)
console.log(`send: ${data}`)
})
connection.on('error', err => console.error(err.message))
})
try {
fs.unlinkSync(pipeFile)
} catch (error) {}
server.listen(pipeFile)
client.js 代码如下:
const net = require('net')
const pipeFile = process.platform === 'win32' ? '\\\\.\\pipe\\mypip' : '/tmp/unix.sock'
const client = net.connect(pipeFile)
client.on('connect', () => console.log('connected.'))
client.on('data', data => console.log(`receive: ${data}`))
client.on('end', () => console.log('disconnected.'))
client.on('error', err => console.error(err.message))
setInterval(() => {
const msg = 'hello'
console.log(`send: ${msg}`)
client.write(msg)
}, 3000)
运行效果:
$ node server.js
socket connected.
receive: hello
send: hello
$ node client.js
connected.
send: hello
receive: hello
作者:乔珂力
链接:https://juejin.cn/post/7016233869565231135
收起阅读 »