SegmentFault 不羁的风の前端专栏最新的文章
2022-10-09T08:51:03+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
基于webpack搭建Chrome扩展开发环境
https://segmentfault.com/a/1190000042584163
2022-10-09T08:51:03+08:00
2022-10-09T08:51:03+08:00
不羁的风
https://segmentfault.com/u/wenhaoliu
0
<h2>序言</h2><p>Chrome扩展开发是目前比较火爆的领域,作为市场占有率第一的浏览器,有海量用户支撑。同时,Chrome扩展可以无缝对接Edge浏览器,不可不谓一箭双雕。 <br>本文将介绍如何使用webpack构建一个支持Typescript的开发环境,在此基础上,实现一个可以修改网页背景的扩展应用。</p><h4>Why Typescript ?</h4><p>Typescipt作为JavaScript的超集,主要帮助JavaScript具备了以下能力:</p><ol><li>给JavaScript增加类型系统,大大降低bug的产生</li><li>支持最新的ECMAScript特性</li><li>通过VsCode、WebStorm扩展工具实现类型提示</li></ol><p>使用Typescipt的类型提示系统,可以大大提高开发效率,笔者在使用时,有一个很大的感触,就是用了就回不去了,就像用了iPhone以后,再用Android,总感觉没有那么丝滑。<br>值得注意的是,Chrome浏览器默认仅支持JavaScript语言进行开发,如果使用Typescript开发就需要使用打包工具将开发好的.ts文件打包成浏览器支持的.js文件。</p><h2>开始烹饪</h2><h4>搭建webpack环境环境</h4><ul><li>初始化项目开发目录</li></ul><pre><code class="sh">mkdir chrome-extension
cd chrome-extension
npm init -y</code></pre><ul><li>安装需要用到的npm包</li></ul><pre><code>npm install --save-dev webpack webpack-cli
npm install --save-dev typescript ts-loader
npm install --save-dev copy-webpack-plugin
npm install --save-dev @types/chrome</code></pre><p>首先安装了<code>webpack</code>及<code>webpack-cli</code>,负责将项目打包成目标产物;其次安装了typescript以及帮助webpack处理.ts文件的<code>ts-loader</code>;<code>copy-webpack-plugin</code>插件用来复制配置文件到打包目录,<code>@types/chrome</code>则是Chrome扩展开发用到的Chrome API的类型支持,这样就可以在开发工具中实现Chrome扩展API类型提示。</p><ul><li>增加Typscipt配置文件</li></ul><p>在根目录创建一个名为<code>tsconfig.json</code>的文件,该文件主要是配置Typescript的编译。</p><pre><code>{
"compilerOptions": {
"strict": true,
"module": "commonjs",
"target": "es6",
"esModuleInterop": true,
"sourceMap": true,
"rootDir": "src",
"outDir": "dist/js",
"noEmitOnError": true,
"typeRoots": [ "node_modules/@types" ]
}
}</code></pre><p>执行完以上操作后,还需在根目录下创建<code>src</code>、<code>public</code>、<code>webpack</code>三个目录</p><ul><li>创建webpack配置文件</li></ul><p>在webpack目录下新增:<code>webpack.config.js</code>,输入如下配置内容:</p><pre><code class="js">const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
mode: "production",
entry: {
background: path.resolve(__dirname, "..", "src", "background.ts"),
},
output: {
path: path.join(__dirname, "../dist"),
filename: "[name].js",
},
resolve: {
extensions: [".ts", ".js"],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
exclude: /node_modules/,
},
],
},
plugins: [
new CopyPlugin({
patterns: [{from: '.', to: '.',context: "public"}]
}),
],
};</code></pre><p>首先将打包的模式设置为<code>production</code>,然后指定了输入文件以及输出文件,同时,使用<code>ts-loader</code>处理Typescript文件,最后,使用<code>copy-webpack-plugin</code>将public目录中的文件( public目录存放着Chrome扩展必须要包含的<code>manifest.json</code>配置 ),复制到打包目录。</p><ul><li>创建<code>manifest.json</code>文件 <br>manifest文件包含Chrome扩展的元数据信息,类似于node项目中的package.json文件,开发者可以在manifest文件中指定扩展的名称、版本、权限等一系列信息。<br>在<code>public</code>目录下,创建名为<code>manifest.json</code>的文件,输入如下内容:</li></ul><pre><code>{
"name": "Change Theme Extension",
"description": "This extension is made for demonstration purposes",
"version": "1.0",
"manifest_version": 3,
"permissions": [
"activeTab",
"scripting"
],
"action":{},
"background": {
"service_worker": "background.js"
}
}</code></pre><p>这段配置文件配置了扩展的名称、介绍信息、版本,<code>manifest_version</code>是用来指定配置文件的版本,目前最新的版本是 <code>3</code>,不建议使用即将废弃的<code>2</code>版本。<code>permissions</code>字段配置了扩展需要的权限,<code>background</code>配置了扩展运行时需要在后台运行的js文件。</p><ul><li>实现更换当前浏览网页背景逻辑</li></ul><p>在<code>src</code>目录下,增加<code>background.ts</code>,并将以下代码插入进去</p><pre><code>let active = false;
function changeTheme(color: string): void {
document.body.style.backgroundColor = color;
}
chrome.action.onClicked.addListener((tab) => {
active = !active;
const color = active ? 'black' : 'white';
chrome.scripting.executeScript({
target: {tabId: tab.id ? tab.id : -1},
func: changeTheme,
args: [color]
}).then();
});</code></pre><p>在这段代码中,首先是一个更改网页背景色的<code>changeTheme</code>方法,然后监听用户点击图标事件事件:在用户点击浏览器图标时,将网页背景色在黑色和白色中切换。</p><ul><li>目录结构<br><img src="/img/bVc2Qfc" alt="目录结构" title="目录结构"></li></ul><p>至此,所有的环境搭建及功能实现都已经完毕。 </p><p>回顾一下:<br>首先完成了构建配置,实现webpack支持.ts文件构建,及复制配置文件到产物目录功能。接着创建了Chrome扩展的核心配置文件<code>manifest.json</code>,最后又在<code>background.ts</code>文件中实现了更换当前浏览网页背景逻辑。</p><ul><li>执行构建</li></ul><pre><code>npm run build </code></pre><p>此时,在<code>dist</code>目录就能看到构建好的chrome扩展代码及配置文件。</p><h2>部署 & 测试</h2><ul><li>开启<code>开发者模式</code><br>打开Chrome浏览器,地址栏中输入<code>chrome://extensions/</code>,点击右上角的<code>开发者模式</code>按钮,将其开启。<br><img src="/img/bVc2Qfi" alt="" title=""></li></ul><p>接着点击加载已解压的扩展程序,选择刚刚构建好的<code>dist</code>目录</p><p><img src="/img/bVc2Qfj" alt="" title=""></p><p>此时,浏览器扩展已经成功加载</p><p><img src="/img/bVc2Qfl" alt="" title=""></p><p>点击扩展图标,成功的将搜狗更改为暗黑模式</p><h2>尾声</h2><p>本文主要介绍了如何用webpack搭建Chrome扩展的步骤,并实现了一个修改网页背景的小Demo。 <br>对于webpack配置部分,开发者可以根据自身需求进行扩展,比如增加开发环境配置,增加less文件打包支持,也可以将自己的宝藏webpack插件加入进来。对于Chrome扩展部分,不得不说浏览器给扩展开放了很多能力,开发者可以将脚本注入到浏览的网页,可以批量管理用户打开的标签页,可以管理用户浏览网页的右键菜单等等…… <br>总之,目前浏览器扩展开发是一个比较好的方向,希望这篇文章能够帮助到想要做扩展的朋友。</p><p>附:<a href="https://link.segmentfault.com/?enc=EipluSscGTkQvC9i9CXmVQ%3D%3D.M%2BFo3bvTZ7YL3WbscW7Gs0o19z4ZLeYDXdqKpqpqinB%2F8ofxr28ySVDuak8unNT0" rel="nofollow">github项目源码</a></p>
记录一次有趣的React项目埋点过程
https://segmentfault.com/a/1190000042215589
2022-07-24T19:42:23+08:00
2022-07-24T19:42:23+08:00
不羁的风
https://segmentfault.com/u/wenhaoliu
0
<blockquote>公司产品有个埋点需求,当用户在点击日志记录时,上报日志列表中最新记录和当前记录的信息,由于组件间距离比较远,没办法直接拿到多层之外组件中的数据,因此用了hack的办法解决,记录在此,以飨读者。</blockquote><h3>背景介绍</h3><p><img src="/img/bVc1ilS" alt="通知中心" title="通知中心"><br>笔者正在参与的项目有一个日志记录中心功能,类似于上图所示的通知中心。产品给的需求如下:</p><blockquote><em>用户点击某条记录时埋点 ——> 需要发送当前的记录类型及创建时间,及日志列表中最新记录的创建时间、最新记录与当前记录的间隔数。</em></blockquote><p>在React中,数据主要是通过单向数据流向子组件传递,仔细分析后发现通过传统方式不好拿。<br>首先用户点击的记录没有最新记录的信息,所以也不会有最新记录的创建时间,其次,记录是懒加载,所以最新记录与当前记录之间的间隔数也没法直接拿。</p><p>如果通过重构的方式在组件中传递这些数据肯定是可以的,但是要花费很大的代价,且不说涉及到多层业务组件层层嵌套,就算是把数据放到Redux中存储,也需要花费比较多的计算资源,稍有不慎也会导致组件的非必要渲染。</p><p>这次使用的hack方式,技术原理并不复杂,核心思想是在页面中拿到DOM,并使用DOM节点的原生方法进行计算,得到想要的数据。</p><h3>拿到最新记录的日志类型和时间戳</h3><p>与标准html一致,在React中支持使用 <strong>data-</strong>* 属性来嵌入自定义数据:</p><pre><code class="html"><div className="item" data-time={item.time} onClick={this.handleClick}>
一个DIV节点
<div/></code></pre><p>比如在上面的div标签中,通过使用data-time属性,将当前记录的time字段绑定到了标签上。<br>绑定以后如何获取呢,同样可以使用浏览器的原生方法,如:<strong>document.getElementById()</strong><br><strong>document.querySelectorAll()</strong>,这里使用第二种方式演示:</p><pre><code class="js">//获取到日志记录DOM列表(每一条为一个DOM节点)
const itemList = document.querySelectorAll('.item')
//获取用户点击的DOM节点在list中的位置
const clickedRecord = [].indexOf.call(itemList, e.currentTarget)
//获取第一个DOM节点上绑定的time数据
const firstItemTime = itemList[0].getAttribute('data-time')</code></pre><p>简述一下过程:通过日志item独有的className标签拿到日志记录的DOM列表,然后通过鼠标点击对象的currentTarget属性拿到当前点击的日志DOM对象,进而计算当前日志在列表中所处的位置。最新记录的创建时间通过DOM节点的getAttribute方法拿到绑定的数据。</p><h3>总结</h3><p>React官方并不推荐直接操作DOM,但直接操作DOM的确能解决很多特殊的业务需求(香啊),所以开发者朋友在闲来无事的时候还是要把DOM的方法捡起来,说不定啥时候就用上了。另外需要说明的是这只是一个临时的埋点需求,如果是项目中比较核心的功能点,采用这种方式还需要认真评估。</p>
动手撸组件系列 —— 1. 使用React实现一个Collapse组件
https://segmentfault.com/a/1190000042179336
2022-07-15T23:57:31+08:00
2022-07-15T23:57:31+08:00
不羁的风
https://segmentfault.com/u/wenhaoliu
1
<p>写组件的能力是衡量前端工程师水平的重要指标,不管是基础组件还是业务组件。<br>笔者在空闲时间也喜欢写组件,为了帮助初学者上手写React组件,同时为了分享我在写组件中的经验和想法,决定开设一个系列,即:<strong><code>动手撸组件系列</code></strong>,和大家分享一些公共组件和业务组件的实现方式和实现技巧。</p><p>作为这个系列的第一篇文章,分享下如何从零到一实现一个折叠面板(Collapse)组件</p><h3>Collapse基础UI绘制</h3><p>折叠面板作为一个基础组件,由两部分构成:第一部分是标题区域,第二部分是可折叠区域,点击标题区域可以折叠和展开内容区。为了组件的美观性可以在标题右侧添加一个箭头图标,在展开和折叠的时候使其旋转。</p><p>为了降低环境搭建成本,实践采用<code>create-react-app</code>环境,创建<code>create-react-app</code>开发环境异常简单,只需要在安装<code>node</code>的系统中执行如下命令</p><pre><code class="sh">npx create-react-app 项目名称</code></pre><p>需要注意的是项目名必须为英文,create-react-app会自动为我们创建一个目录。</p><p>项目创建完成后,在<code>src</code>目录下创建名为<code>Collapse.jsx</code>的文件,输入如下代码:<br>(初学者可以选择复制)</p><pre><code>import React, { useState } from "react";
import "./style.css";
const CollapsablePanel = () => {
const [isCollapsed, setIsCollapsed] = useState(true);
const togglePanel = () => {
setIsCollapsed((prevState) => !prevState);
};
return (
<div className="wrapper">
<div className="pannel" onClick={togglePanel}>
<div className="heading">
<span>Flower Collapse</span>
<svg width="20px"
height="25px" viewBox="0 0 1024 1024"
style={{ color: '#6495ed' }}><path d="M64 351c0-8 3-16 9-22.2 12.3-12.7 32.6-13.1
45.3-0.8l394.1 380.5L905.7 328c12.7-12.3 33-12 45.3 0.7s12 33-0.7 45.3L534.7
776c-12.4 12-32.1 12-44.5 0L73.8 374c-6.5-6.3-9.8-14.6-9.8-23z" p-id="1705">
</path></svg>
</div>
<div
className="content"
>
<div className="contentInner" >
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam
erat, sed diam voluptua.
</div>
</div>
</div>
</div>
);
};
export default CollapsablePanel;
</code></pre><p>接着创建名称为<code>style.css</code>的样式文件</p><pre><code class="css">.wrapper {
display: flex;
justify-content: center;
width: 100%;
height: 100vh;
background-color: rgb(228, 239, 239);
padding-top: 40vh;
}
.pannel {
width: 400px;
text-align: left;
}
.heading {
background-color: #bfa;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
color: #000;
font-size: 20px;
line-height: 20px;
border: 1px solid rgb(212, 240, 205);
padding: 10px 20px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.content {
font-size: 20px;
background: #fff;
border: 1px solid #fff;
border-top: none;
padding: 0 20px;
color: #000000;
overflow: hidden;
}
.contentInner {
padding: 20px 0;
}</code></pre><p>创建完以上两个文件后,在<code>index.js</code>中挂载创建好的Collapse组件:<br>(原有代码无关紧要,直接删除即可)</p><pre><code>import React from 'react';
import ReactDOM from 'react-dom/client';
import Collapse from './components/Collapse';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Collapse />
);</code></pre><p>创建完成后,使用<code>yarn start</code>命令启动应用,就能看到绘制好的Collapse组件外观:</p><p><img src="/img/bVc08RG" alt="Collapse组件UI图" title="Collapse组件UI图"></p><p>完成了Collapse基础UI绘制,回顾一下做了哪些操作:<br>首先定义了名为<code>isCollapsed</code>的state,存储组件展开关闭状态,并声明了名为<code>togglePanel</code>方法,在用户点击标题的时候调用此方法即可实现面板的展开关闭。<br>接着分别定义了样式名为pannel、heading、content的div容器及与其相关的子容器,并在<code>style.css</code>中设置了容器的样式。组件中的ICON使用svg标签直接绘制,避免因引入svg包增大组件体积。</p><h3>内容区展开动画</h3><p>实现动画的方式有很多,可以使用css的transition属性实现,也可使用React生态种类繁多的动画库。在React生态中,有个非常流行的动画库叫<code>react-spring</code>,不仅功能强大,而且支持hook方式调用,本文就用这个动画库来实现内容区域展开动画和按钮旋转动画。</p><p>安装<code>react-spring</code>动画库</p><pre><code>yarn add react-spring</code></pre><p>安装完<code>react-spring</code>动画库以后,就可以定义方法让spring帮我们生成动画样式了</p><pre><code class="js">const panelContentAnimatedStyle = useSpring({
height: isCollapsed ? 0 : 200,
});</code></pre><p>接着把内容区域的标签名从<div>改为<animated.div>即可(和useSpring相同,animated也是react-spring具备的一个对象),并在标签中加上刚刚创建的<code>panelContentAnimatedStyle</code>:</p><pre><code>import React, { useState } from "react";
import { useSpring, animated } from "react-spring";
import "./style.css";
const CollapsablePanel = () => {
const [isCollapsed, setIsCollapsed] = useState(true);
const togglePanel = () => {
setIsCollapsed((prevState) => !prevState);
};
const panelContentAnimatedStyle = useSpring({
height: isCollapsed ? 0 : 180,
});
return (
<div className="wrapper">
<div className="pannel" onClick={togglePanel}>
<div className="heading">
<span>Flower Collapse</span>
<svg width="20px"
height="25px" viewBox="0 0 1024 1024"
style={{ color: '#6495ed' }}><path d="M64 351c0-8 3-16 9-22.2 12.3-12.7 32.6-13.1
45.3-0.8l394.1 380.5L905.7 328c12.7-12.3 33-12 45.3 0.7s12 33-0.7 45.3L534.7
776c-12.4 12-32.1 12-44.5 0L73.8 374c-6.5-6.3-9.8-14.6-9.8-23z" p-id="1705">
</path></svg>
</div>
<animated.div
style={panelContentAnimatedStyle}
className="content"
>
<div className="contentInner" >
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam
erat, sed diam voluptua.
</div>
</animated.div>
</div>
</div>
);
};
export default CollapsablePanel;
</code></pre><p>点击标题栏可以看到如下效果:</p><p><img src="/img/bVc08T5" alt="collapse.gif" title="collapse.gif"></p><blockquote><strong> 等等~~ 高度是固定的吗?</strong></blockquote><p>显然不是!用户在使用Collapse组件的时候,传递的内容不单是文字还有可能是图片或者是任意类型的ReactNode,所以在展开的时候是需要获取content对象的实际高度,获取DOM对象高度的操作有一个库可以帮助我们,<a href="https://link.segmentfault.com/?enc=YMqkxCbBvreKCWFEGxBrkA%3D%3D.uFrV7FkXXNwNGuLYuoiF5vZtepJcgf5FiVlesh2CFozHbkWS2XyCXsCZmvsDLHrg" rel="nofollow">react-use-measure</a>,这个库不仅可以测量DOM对象的长度和宽度,还可以测量DOM对象距离浏览器上下左右的位置。<br><code>react-use-measure</code>提供了名为<code>useMeasure</code>的hook,使用方式如下:</p><pre><code>const [ref, bounds] = useMeasure();</code></pre><p>第一个参数是ref对象,将其绑定到需要测量的DOM对象的ref属性上即可,第二个bounds就是位置对象,包含上面提到的所有属性。</p><p>继续改造组件代码<code>Collapse.jsx</code>:</p><pre><code>import React, { useState } from "react";
import { useSpring, animated } from "react-spring";
import useMeasure from 'react-use-measure'
import "./style.css";
const CollapsablePanel = () => {
const [isCollapsed, setIsCollapsed] = useState(true);
const [ref, bounds] = useMeasure();
const togglePanel = () => {
setIsCollapsed((prevState) => !prevState);
};
const panelContentAnimatedStyle = useSpring({
height: isCollapsed ? 0 : bounds.height,
});
return (
<div className="wrapper">
<div className="pannel" onClick={togglePanel}>
<div className="heading">
<span>Flower Collapse</span>
<svg width="20px"
height="25px" viewBox="0 0 1024 1024"
style={{ color: '#6495ed' }}><path d="M64 351c0-8 3-16 9-22.2 12.3-12.7 32.6-13.1
45.3-0.8l394.1 380.5L905.7 328c12.7-12.3 33-12 45.3 0.7s12 33-0.7 45.3L534.7
776c-12.4 12-32.1 12-44.5 0L73.8 374c-6.5-6.3-9.8-14.6-9.8-23z" p-id="1705">
</path></svg>
</div>
<animated.div
style={panelContentAnimatedStyle}
className="content"
>
<div ref={ref} className="contentInner" >
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam
erat, sed diam voluptua.
Lorem ipsum, dolor sit amet consectetur adipisicing elit.
Quasi aperiam dignissimos eaque deserunt expedita sit
accusamus sunt laudantium repellendus nisi! Sit,
consequuntur. Tempora, officiis molestiae
fuga sit quae aliquid maxime.
</div>
</animated.div>
</div>
</div>
);
};
export default CollapsablePanel;
</code></pre><p>现在的效果:</p><p><img src="/img/bVc08UR" alt="collapse2.gif" title="collapse2.gif"><br>使用react-use-measure可以非常方便的获取DOM对象的真实高度和在浏览器中的位置,在项目中灵活运用可以提高开发效率。</p><h3>实现箭头图标旋转动画</h3><p>箭头图标的旋转和内容区域的实现类似,只需要将其标签改成<code>animated.div</code>,并将useSpring生成的样式对象绑定即可。<br>生成箭头ICON旋转动画style对象:</p><pre><code class="js">const toggleWrapperAnimatedStyle = useSpring({
transform: isCollapsed ? "rotate(0deg)" : "rotate(180deg)",
});</code></pre><p>svg对象外套一个div,并绑定动画样式:</p><pre><code>import React, { useState } from "react";
import { useSpring, animated } from "react-spring";
import useMeasure from 'react-use-measure'
import "./style.css";
const CollapsablePanel = () => {
const [isCollapsed, setIsCollapsed] = useState(true);
const [ref, bounds] = useMeasure();
const togglePanel = () => {
setIsCollapsed((prevState) => !prevState);
};
const panelContentAnimatedStyle = useSpring({
height: isCollapsed ? 0 : bounds.height,
});
const toggleWrapperAnimatedStyle = useSpring({
transform: isCollapsed ? "rotate(0deg)" : "rotate(180deg)",
});
return (
<div className="wrapper">
<div className="pannel" onClick={togglePanel}>
<div className="heading">
<span>Flower Collapse</span>
<animated.div style={toggleWrapperAnimatedStyle}>
<svg width="20px"
height="25px" viewBox="0 0 1024 1024"
style={{ color: '#6495ed' }}><path d="M64 351c0-8 3-16 9-22.2 12.3-12.7 32.6-13.1
45.3-0.8l394.1 380.5L905.7 328c12.7-12.3 33-12 45.3 0.7s12 33-0.7 45.3L534.7
776c-12.4 12-32.1 12-44.5 0L73.8 374c-6.5-6.3-9.8-14.6-9.8-23z" p-id="1705">
</path></svg>
</animated.div>
</div>
<animated.div
style={panelContentAnimatedStyle}
className="content"
>
<div ref={ref} className="contentInner" >
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam
erat, sed diam voluptua.
</div>
</animated.div>
</div>
</div>
);
};
export default CollapsablePanel;
</code></pre><p>可以看到如下效果:</p><p><img src="/img/bVc08U5" alt="collapse3.gif" title="collapse3.gif"></p><h3>总结</h3><p><strong><code>动手撸组件系列</code></strong>第一篇文章选择讲Collapse组件的实现,是因为这个组件的实现简单而且富有趣味,读着可以体会到写组件的乐趣。一个简单的组件在实现的时候也有可能遇到问题,像如何获取content区域中的高度,就很有代表性。当前React及Vue的生态都异常繁荣,开发者在实现具体需求的时候要能够灵活运用这些开源库,以开发出简洁且功能强大的组件。</p><p><a href="https://gist.github.com/xliudaxia/db341fd3d7776b507c42b4987e6b669f">Coollapse.jsx源代码</a><br><a href="https://gist.github.com/xliudaxia/3f9fd74fc2e14f68e477181a03007f93">style.css源代码</a></p>
React自定义hook之:useClickOutside——判断是否点击DOM之外区域
https://segmentfault.com/a/1190000042032561
2022-06-24T19:41:56+08:00
2022-06-24T19:41:56+08:00
不羁的风
https://segmentfault.com/u/wenhaoliu
2
<blockquote>最近在开发业务需求的时候,有一个场景是点击弹窗之外的区域后,执行某些操作。比如我们常用的github左上角的搜索框,当点击了搜索框之外的区域以后,搜索框就会自动取消搜索并收缩起来。</blockquote><p><img src="/img/bVc0wKD" alt="github搜索框" title="github搜索框"></p><p>经过调研发现使用<strong>useRef</strong>+<strong>浏览器事件绑定</strong>可以实现这一需求,并且可以将这一功能抽象为自定义hook。</p><p>本文将首先介绍如何用传统方式实现这一需求,然后介绍如何抽象成自定义hook,最后结合typescript类型,完善这一自定义hook。</p><h3>实现检测点击对象外区域</h3><pre><code>import React, { useEffect, useRef } from "react";
const Demo: React.FC = () => {
// 使用useRef绑定DOM对象
const domRef = useRef<HTMLDivElement>(null);
// 组件初始化绑定点击事件
useEffect(() => {
const handleClickOutSide = (e: MouseEvent) => {
// 判断用户点击的对象是否在DOM节点内部
if (domRef.current?.contains(e.target as Node)) {
console.log("点击了DOM里面区域");
return;
}
console.log("点击DOM外面区域");
};
document.addEventListener("mousedown", handleClickOutSide);
return () => {
document.removeEventListener("mousedown", handleClickOutSide);
};
}, []);
return (
<div
ref={domRef}
style={{
height: 300,
width: 300,
background: "#bfa",
}}
></div>
);
};
export default Demo;
</code></pre><p>代码不难理解,首先我们在函数式组件里面写了一个长度和宽度都是300像素的正方形,然后创建了一个名为domRef的对象将其绑定到dom节点上,最后在<strong>useEffect</strong>钩子里面声明handleClickOutSide的方法判断用户是否点击了指定的DOM区域,并使用document.addEventListener方法添加事件监听,组件卸载时清理事件监听。<br>在实现的过程中,最核心的是利用了Ref对象上的contains方法,经过研究发现,Node.contains方法是浏览器的<a href="https://link.segmentfault.com/?enc=PbYvbzpX1SZwxzxLuemrTQ%3D%3D.6rIVJO5g0scVJPiGoNW74m6BfPkZWUwTQmx78S%2B2IwWjz82u4r3X7FWl84zS2VpoEtYJc7b4vE8MOgnQ3esFTg%3D%3D" rel="nofollow">原生方法</a>,其主要的作用是判断传入的DOM节点是否为该节点的后代节点。</p><p>使用基本方式实现后,接着封装自定义hook。</p><h3>封装useClickOutside hook</h3><p>大家在理解自定义hook时不用心生畏惧,无非就是调用了其他hook的普通函数,下面来看代码实现:</p><pre><code>import { RefObject, useEffect } from "react";
const useClickOutside = (ref: RefObject<HTMLElement>, handler: Function) => {
useEffect(() => {
const listener = (event: MouseEvent) => {
if (!ref.current || ref.current.contains(event.target as HTMLElement)) {
return;
}
handler(event);
};
document.addEventListener("click", listener);
return () => {
document.removeEventListener("click", listener);
};
}, [ref, handler]);
};
export default useClickOutside;</code></pre><p>可以看到,代码将判断是否点击了DOM之外区域的逻辑都抽离出来,这样在使用时,只需要把DOM节点传递给useClickOutside即可,自定义hook的第二个参数接收一个回调函数,可以在回调函数里面做各种事情。来看一下使用自定义hook后的代码:</p><pre><code class="js">import React, { useRef } from "react";
import useClickOutside from "../hooks/useOnClickOutside";
const Demo: React.FC = () => {
// 使用useRef绑定DOM对象
const domRef = useRef<HTMLDivElement>(null);
useClickOutside(domRef, () => {
console.log("点击了外部区域");
});
return (
<div
ref={domRef}
style={{
height: 300,
width: 300,
background: "#bfa",
}}
></div>
);
};
export default Demo;</code></pre><p>可以看到,组件的代码得到大大的简化,而且抽离出来的自定义hook可以在其他组件中复用。但到这一步有优化的空间吗?当然有的,那就是在类型定义方面,可以使用泛型进行优化。<br>在刚才编写的useClickOutside自定义hook中,对ref对象的定义是:RefObject<HTMLElement>,这种定义其实是不合理的,因为HTMLElement不够具体,有可能传入的是HTMLDivElement或者是HTMLAnchorElement,也有可能是HTMLSpanElement,这里我们可以使用泛型对其加以限制。</p><h3>使用泛型优化类型定义</h3><pre><code class="js">import { RefObject, useEffect, useRef } from 'react'
export function useOnClickOutside<T extends HTMLAnchorElement>(
node: RefObject<T | undefined>,
handler: undefined | (() => void)
) {
const handlerRef = useRef<undefined | (() => void)>(handler)
useEffect(() => {
handlerRef.current = handler
}, [handler])
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (node.current?.contains(e.target as Node) ?? false) {
return
}
if (handlerRef.current) handlerRef.current()
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [node])
}
</code></pre><p>优化后的hook里,使用泛型T指代传入的类型继承自HTMLElement,这样在声明ref对象的时候就可以用T指代传入的DOM类型,使用泛型可以加强对传入参数的约束,大家在项目开发中可以多加尝试。<br>在handleClickOutside方法中,使用了双问号<strong>??</strong>判断,双问号的意思是如果前面的部分为undefined则返回后面的内容,也就是false。<br>本文最后完整版的useClickOutside hook借鉴了uniswap开源项目<br><a href="https://link.segmentfault.com/?enc=S3nbIhOKMfDJMXcHNsXEVQ%3D%3D.PtSrTgqlzLjDmFnQ3dJakebU%2Bywtkupol8Kn5lhm6neQhnNZa0H1WDYP%2BMbntZCO6lYhOKtJpauCQVvvzrVxD9OtsPQC6pl4aZeSC2lVqK4%3D" rel="nofollow">项目地址</a></p><p>谢谢阅读,如果觉得不错,欢迎点赞o( ̄▽ ̄)d!</p>
记录一次线上数据图源本地化操作的过程
https://segmentfault.com/a/1190000041847215
2022-05-15T17:50:34+08:00
2022-05-15T17:50:34+08:00
不羁的风
https://segmentfault.com/u/wenhaoliu
0
<blockquote>最近学了一个比较赞的电商项目,项目作者提供了完整的示例数据,包括商品信息及配图,但是这些配图是固定的URL,商品详情为html,html中有img标签,img标签中也有url。根据过往经验这种在线CDN很容易挂掉,因此产生了把商品数据中的商品图片提取出来,放在自己的腾讯云服务器中的想法,保证可访问性。</blockquote><h3>演示数据</h3><pre><code class="json">[{
"ID": "b93e59e214fc4478ac72652a2c87fe54",
"GOODS_SERIAL_NUMBER": "2300000059885",
"SHOP_ID": "402880e860166f3c0160167897d60002",
"SUB_ID": "402880e86016d1b5016016dcd7c50004",
"GOOD_TYPE": 1,
"STATE": 0,
"IS_DELETE": 1,
"NAME": "云南红提800g/盒",
"ORI_PRICE": 18,
"PRESENT_PRICE": 15,
"AMOUNT": 10000,
"DETAIL": "<img src=\"http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112029_9395.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112029_3391.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112029_7603.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112029_4718.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112030_778.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112030_2602.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112030_7913.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112030_202.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112030_4296.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112030_6956.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112030_8200.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112031_3967.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112031_5114.jpg\" width=\"100%\" height=\"auto\" alt=\"\" />",
"BRIEF": null,
"SALES_COUNT": 0,
"IMAGE1": "http://images.koow.cc/shopGoodsImg/20171225/20171225112020_561.jpg",
"IMAGE2": null,
"IMAGE3": null,
"IMAGE4": null,
"IMAGE5": null,
"ORIGIN_PLACE": null,
"GOOD_SCENT": null,
"CREATE_TIME": 1514172047397,
"UPDATE_TIME": 1522037064430,
"IS_RECOMMEND": 0,
"PICTURE_COMPERSS_PATH": "http://images.koow.cc/compressedPic/20171225112020_561.jpg"
},
{
"ID": "e0ab2f6e2802443ba117b1146cf85fee",
"GOODS_SERIAL_NUMBER": "4894375014863",
"SHOP_ID": "402880e860166f3c0160167897d60002",
"SUB_ID": "2c9f6c94609a62be0160a02d1dc20021",
"GOOD_TYPE": 1,
"STATE": 0,
"IS_DELETE": 1,
"NAME": "菓子町园道乳酸菌味夹心饼干(抹茶味)540/罐",
"ORI_PRICE": 29.8,
"PRESENT_PRICE": 29.8,
"AMOUNT": 10000,
"DETAIL": "<img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110655_230.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110656_329.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110656_2659.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110656_9521.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110656_8611.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110656_1390.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110656_7291.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110657_3919.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110657_2170.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110657_4402.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110657_1926.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110657_9438.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110657_4361.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110657_2730.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110658_314.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110658_8779.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110658_9878.jpg\" width=\"100%\" height=\"auto\" alt=\"\" /><img src=\"http://images.koow.cc/shopGoodsDetailImg/20180213/20180213110658_3471.jpg\" width=\"100%\" height=\"auto\" alt=\"\" />",
"BRIEF": null,
"SALES_COUNT": 0,
"IMAGE1": "http://images.koow.cc/shopGoodsImg/20180213/20180213110648_2744.jpg",
"IMAGE2": null,
"IMAGE3": null,
"IMAGE4": null,
"IMAGE5": null,
"ORIGIN_PLACE": null,
"GOOD_SCENT": null,
"CREATE_TIME": 1518491222336,
"UPDATE_TIME": 1523174099461,
"IS_RECOMMEND": 0,
"PICTURE_COMPERSS_PATH": "http://images.koow.cc/compressedPic/20180213110648_2744.jpg"
}]</code></pre><p>可以看到,数据比较完整,包括ID、编号、名称、价格、介绍等信息。<br>如果想要提取JSON对象中的图片URL,对于其中的images1-images5对象比较好处理,只需要遍历即可。对于DETAIL中的图片URL,由于URL混在html中,没有办法直接拿到,可通过正则匹配的形式获取。下面分步骤操作:</p><h3>提取IMAGE1-IMAGE5中的图片URL</h3><pre><code class="javascript">const fs = require("fs");
fs.readFile("./goods_demo.json", "utf8", (err, data) => {
// 序列化数据
data = JSON.parse(data);
data.map((value, index) => {
for (let i = 0; i < 5; i++) {
// 遍历数据,并写入到名为result.txt的文件中
if (value[`IMAGE${i + 1}`] !== null) {
const url = value[`IMAGE${i + 1}`]
fs.appendFile("./result.txt",`\r\n${url}`, function(err) {
if (err) console.log("写文件操作失败");
else console.log("写文件操作成功");
});
}
}
});
});</code></pre><p>使用NodeJS运行上面的代码后,就能够正确的读取到IMAGE对象中的URL,并写入到result.txt文件中。</p><h3>提取DETAIL对象中的图片URL</h3><p>对url地址分析可以发现,图片URL包括http开头<strong>(part1)</strong>,CDN的URL<strong>(part2)</strong>,图片所在的目录<strong>(part3)</strong>,图片的名称<strong>(part4)</strong>:</p><pre><code>
"http://(part1)images.koow.cc(part2)/shopGoodsImg(part3)/20171225(part3)/20171225112020_561.jpg(part4)"
</code></pre><p>根据以上正则规则,可以用以下正则进行匹配!</p><pre><code>// \w表示任意字母数字或下划线
// url中的/符号需要转义
// {2,5}表示出现2-5次
// /g表示全局匹配
const urlReg = /http\:\/\/images.koow.cc(\/\w+){2,5}\.jpg/g;</code></pre><p>加上对JSON中DETAIL对象处理的代码以后,整体代码如下:</p><pre><code class="javascript">const fs = require("fs");
fs.readFile("./goods_demo.json", "utf8", (err, data) => {
data = JSON.parse(data);
data.map((value, index) => {
if (value.DETAIL) {
// 匹配图片的正则表达式
const urlReg = /http\:\/\/images.koow.cc(\/\w+){2,5}\.jpg/g;
const arrlist = value.DETAIL.match(urlReg);
// 对匹配到的image list遍历并写入文件
if (arrlist && arrlist.length) {
arrlist.map(item => {
fs.appendFile("./result.txt", `\r\n${item}`, function(err) {
if (err) console.log("写DETAIL记录操作失败");
else console.log("写DETAIL记录操作成功");
});
});
}
}
for (let i = 0; i < 5; i++) {
if (value[`IMAGE${i + 1}`] !== null) {
const url = value[`IMAGE${i + 1}`]
fs.appendFile("./result.txt",`\r\n${url}`, function(err) {
if (err) console.log("写文件操作失败");
else console.log("写文件操作成功");
});
}
}
});
});</code></pre><p>最终提取的url在reuslt.txt中存储,等待后续的处理。</p><pre><code>http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112029_9395.jpg
http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112029_3391.jpg
http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112029_4718.jpg
http://images.koow.cc/shopGoodsDetailImg/20171225/20171225112029_7603.jpg
……</code></pre><h3>批量下载</h3><p>想要做私有的CDN服务器,文件的存储路径是不能变的,不然就匹配不到数据库中存储的路径。如何在批量下载时保持图片的目录不变呢?很简单,只需要使用wget命令:</p><pre><code class="bash">wget -nc -r -i ./result.txt</code></pre><p>-nc, --no-clobber 不要覆盖已经存在的文件<br>-r, –recursive 递归下载,下载所有文件<br>-i, --input-file 下载指定文件中的URL</p><h2>总结</h2><p>对JSON或XML数据执行处理是程序员的必备技能,掌握高效的数据处理方法能让工作事半功倍,避免不必要的时间开销。作者写本文的目的是希望能帮助到有同样需求的小伙伴,也希望电脑旁的你能把自己处理数据的技巧分享出来!</p>
如何在React中优雅的使用Interval(轮询)
https://segmentfault.com/a/1190000041831958
2022-05-12T00:09:17+08:00
2022-05-12T00:09:17+08:00
不羁的风
https://segmentfault.com/u/wenhaoliu
4
<blockquote>在前端开发中,经常会使用轮询(setInterval),比如后端异步数据处理,需要定时查询最新状态。但是在用<strong>React Hook</strong>进行轮询操作时,可能会发现<strong>setInterval</strong>没有那么轻松驾驭,今天笔者就来谈谈在项目开发中是如何解决setInterval调用问题的,以及如何更加优雅的使用<strong>setInterval</strong>。</blockquote><h3>问题的引入</h3><p>先从一个简单的例子开始,为了便于叙述,本文中的案例用一个计数定时器来演示。</p><pre><code class="javascript">import React, { useEffect, useState } from "react";
export default function IntervalExp() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<div>
<p>当前计数:{count}</p>
</div>
);
}</code></pre><p>首先使用useState定义了一个<strong>count</strong>变量,然后在useEffect中,定义了一个名为<strong>timer</strong>的定时器,并在定时器中执行count+1操作,并在组件卸载时清除定时器。</p><p>理想状态下,count会执行+1操作,并不断的递增。但实际并非如此,count在变为1以后,将不再有任何变化。原因很简单,useEffect中由于没有将依赖的count对象添加到依赖对象数组中,所以它每次拿到的都是老的count对象,也就是0。</p><h3>方法一:添加依赖数组</h3><pre><code class="javascript">import React, { useEffect, useState } from "react";
export default function IntervalExp() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
console.log("更新了", timer);
return () => clearInterval(timer);
}, [count]);
return (
<div>
<p>当前计数{count}</p>
</div>
);
}
</code></pre><p>当把count对象加入到依赖数组以后,可以发现定时器现在可以正常工作了。但是注意这里有个坑,在return的时候,即组件卸载的时候,一定要做清理操作,否则你的定时器会执行的越来越快,因为新的定时器会不断生成,但老的定时器却没有<strong>清理</strong>。</p><p>但是这种方式完美吗?并不然,如果定时器操作的数据包含父组件传递的props,或者是其他的state,都需要加到依赖数组中,这样做不仅不美观,而且容易出错。同时,这种方式还有个问题,就是定时器要在每次变化时要<strong>重新生成</strong>,这必然也会有很高的<strong>性能损耗</strong>。</p><h3>方法二:不添加依赖数组的方式(useRef)</h3><p>useRef是官方的hook,使用useRef定义的对象有个current对象,是可以存储数据的,而且存储的数据可以被修改,并在组件的每一次渲染中,都能从current中拿到最新的数据。基于ref的这一特性,实现一个名为useInterval的自定义hook。</p><pre><code class="javascript">import { useEffect, useRef } from "react";
export const useInterval = (cb: Function, time = 1000) => {
const cbRef = useRef<Function>();
useEffect(() => {
cbRef.current = cb;
});
useEffect(() => {
const callback = () => {
cbRef.current?.();
};
const timer = setInterval(() => {
callback();
}, time);
return () => clearInterval(timer);
}, []);
};</code></pre><p>在这个自定义hook中,有回调函数和轮询时间两个参数。使用useEffect把最新的回调函数赋值给ref.current,这样在第二个useEffect中就能从ref.current上拿到最新的callback,然后在定时器中执行它。<br>在项目中如何使用呢?</p><pre><code class="javascript">useInterval(() => {
setCount(count + 1);
}, 1000);</code></pre><p>只需引入自定义hook,并按照上面的格式调用即可。</p><h3>方法三:更高级的办法(useReducer)☆☆☆</h3><p>回头看第一个例子,为什么在useEffect中不添加count就无法实现想要的定时器效果呢,说白了是因为读取了state的数据,而又因为闭包原因拿不到最新的count数据,所以导致interval操作失败。其实借助useReducer就可以在不读取count的情况更新count数据。</p><pre><code class="javascript">import React, { useEffect, useReducer } from "react";
function reducer(state: { count: number }) {
return { count: state.count + 1 };
}
export default function IntervalExp() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
useEffect(() => {
setInterval(() => {
dispatch();
}, 1000);
}, []);
return (
<div>
<p>当前计数{state.count}</p>
</div>
);
}</code></pre><p>在这个案例中,使用useReducer定义了一个简单的count操作方法,在interval中,通过调用dispatch方法,成功更新了count数据。useReducer在需要操作多个state的复杂业务逻辑场景下可以使用,虽然定义起来麻烦,但是可以实现将组件中的业务逻辑抽离出来,写出更加易于维护的代码,而且在目前这个场景中,useReducer比上面两个方式处理的更加优雅,也是本文推荐的方式。</p><h3>总结</h3><p>hook是React中非常有魅力的一个发明,灵活使用hook可以写出更有品质的代码。作者写本文的目的也是因为在实际开发中遇到了这一问题,因此希望本文可以帮助到其他开发者。</p><p>参考文章:<a href="https://link.segmentfault.com/?enc=uNNIngbxk%2F1lPnCJ6zegJg%3D%3D.gxAxepnTZ0sVCqqGpGie6UeOpDHNAeByG48r7nvk8iKvXrRs6K%2FG2M8xGGx2C3%2FHTrOPcj%2BvxUIOt9HFkgMlnA%3D%3D" rel="nofollow">usestate中的回调函数_React Hooks 中使用 setInterval 的若干方法</a></p>
git小技巧之 Git stash
https://segmentfault.com/a/1190000041416938
2022-02-16T20:45:29+08:00
2022-02-16T20:45:29+08:00
不羁的风
https://segmentfault.com/u/wenhaoliu
0
<blockquote>在开发的项目中有很多分支的时候,我们总是需要频繁的切换分支来比对代码,bug定位……<br>但是,每次checkout之前,git总是让我们做commit,这种打乱我们开发节奏的操作,我们是不会接受的,这里我来推荐一个超好用的命令 <strong>git stash</strong></blockquote><h3>命令定义</h3><p><strong>git stash</strong>会把所有未提交的修改(包括暂存的和非暂存的)都保存起来,用于后续恢复当前工作目录。</p><h4>来看一个案例:</h4><p>目前的状况是我修改了两个文件:</p><pre><code class="bash">git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: file1.tsx
modified: file2.tsx
</code></pre><p>然后我执行checkout操作:</p><pre><code class="bash">git checkout develop
error: Your local changes to the following files would be overwritten by checkout:
file1.tsx
file2.tsx
Please commit your changes or stash them before you switch branches. </code></pre><p>可以看到,很恶心心的提示,我们要commit……,but,stash是什么?</p><p>二话不说,直接git stash</p><pre><code>git stash
Saved working directory and index state WIP on……</code></pre><p>这时候我们执行git status</p><pre><code class="bash">git status
On branch master
nothing to commit, working tree clean</code></pre><p>哇哦,一切都清净了~!<br>然后我们就可以开开心心的checkout任何分支了!</p><h4>如何召回暂存的数据</h4><p>很简单,一条命令</p><pre><code>git stash pop </code></pre><p>相当于弹出暂存区中的修改记录。</p><h4>Tips</h4><ol><li>stash命令是本地的,不会被push到远程</li><li><p>建议添加stash的时候指定名称</p><pre><code>git stash save "操作名称"</code></pre></li><li><p>查看stash记录</p><pre><code>git stash list</code></pre></li><li>git stash pop 和git stash apply区别<br>前者会在取出修改后删除list中的stash记录,后者不会删除list中的记录。</li></ol>
正则表达式应用之:身份证号识别
https://segmentfault.com/a/1190000041148423
2021-12-20T15:59:40+08:00
2021-12-20T15:59:40+08:00
不羁的风
https://segmentfault.com/u/wenhaoliu
0
<blockquote>今天项目中遇到了身份证号识别的问题,需要识别15或18位的居民身份证,网上查了一下,感觉很有意思,这里跟大家分享下我的理解过程,希望能帮助大家更好的学习正则表达式。</blockquote><h3>18位身份证号识别</h3><pre><code class="reg">/^([1-6][1-9]|50)\d{4}(18|19|20)\d{2}((0[1-9])|10|11|12)(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/</code></pre><p>这是一个比较完善的识别正则,直接看的话一头雾水,我们先来把他分别成5个部分,首先要说明的是:<strong>开头的^符号表示待识别对象的开头,也就是一个字符串开始的位置,而$表示字符串结束的位置。</strong></p><h4>part1(身份证1-6位):</h4><pre><code class="reg">^([1-6][1-9]|50)\d{4}</code></pre><p>方括号中的1-6表示1-6中的任意数字,1-9表示1-9之间的任意数字,所以[1-6][1-9]组合起来表示的是符合规则的两个数字,紧接着的|表示或的关系,也就是说可能为50,再接着的\d{4}表示四位数字。因此part1规定了18位居民身份证号的前6位。</p><h4>part2(身份证7-10位):</h4><pre><code class="reg">(18|19|20)\d{2}</code></pre><p>这部分比较容易理解,首先是(18|19|20),表示可能是18、19、20,接着后面的\d{2}表示为两位数字。</p><h4>part3(身份证11-12位):</h4><pre><code class="reg">((0[1-9])|10|11|12)</code></pre><p>与part2类似,内部括号的内容:(0[1-9])表示两位数字,其中第一位为0,第二位为0-9中任意数字。竖杠表示或者的关系,表示也有可能是10、11、12,总之这段正则就是对两位数字进行限制。</p><h4>part4(身份证13-14位):</h4><pre><code class="reg">(([0-2][1-9])|10|20|30|31)</code></pre><p>与part3类似,首先限制了一个两位数([0-2][1-9]),第一个数为0-2,第二个数为1-9,后面的为除了这个规则之外可能的两位数(可能是10、20、30、31)</p><h4>part5(身份证15-18位):</h4><pre><code class="reg">\d{3}[0-9Xx]$</code></pre><p>这部分也很好理解,首先是3位数字,然后对最后一位(18位)进行限定,这一位可能是0-9直接的数字,也可能是X或者是小写x,最后的最后用$表示整个字符串的结尾。</p><h3>15位身份证号识别</h3><pre><code class="reg">/^([1-6][1-9]|50)\d{4}\d{2}((0[1-9])|10|11|12)(([0-2][1-9])|10|20|30|31)\d{3}$/</code></pre><p>15位身份证的正则识别和18位的类似,我们也是拆开一块一块的来看。</p><h4>part1(身份证1-6位):</h4><pre><code class="reg">^([1-6][1-9]|50)\d{4}</code></pre><p>这部分与15位的part1一致,不做赘述。</p><h4>part2(身份证7-10位):</h4><pre><code class="reg">\d{2}((0[1-9])|10|11|12)</code></pre><p>首先\d{2}表示两位数字,然后括号里面也是表示对两位数字的规则校验(这里校验的是月份)。</p><h4>part3(身份证11-12位):</h4><pre><code class="reg">(([0-2][1-9])|10|20|30|31)</code></pre><p>这部分也是表示两位数字,不做赘述。</p><h4>part4(身份证13-15位):</h4><pre><code class="reg">\d{3}$</code></pre><p>三位数字及结束符号。</p><h3>一个小小的推荐</h3><blockquote>作为正则表达式的初学者,刚开始一定会觉得拆分正则比较困难,因为有的正则可能写的很长很长,这里给大家推荐一个能够格式化正则的网站,把正则粘上去,网站就会自动的给拆成一块一块的。<a href="https://link.segmentfault.com/?enc=phH1Qm8%2Bb4OnUX4cWpiV1w%3D%3D.nwAK6wwNJ4vQ8lTiVL9Gd8%2BKcvaTH2sKqBzxwBAqlX4%3D" rel="nofollow">RegExr</a></blockquote><h3>总结</h3><p>不管是前端工程师还是后端工程师,正则表达式都是需要掌握的一门技术,深有感触的是在刷一些leetcode的时候,正则总是能出其不意的轻巧化解字符串问题,在工作中,正则也大有用处,不得不说,正则真的是太奇妙了!</p>
某大厂前端工程师一面面试题整理(三年)
https://segmentfault.com/a/1190000041087611
2021-12-08T22:11:22+08:00
2021-12-08T22:11:22+08:00
不羁的风
https://segmentfault.com/u/wenhaoliu
0
<blockquote>昨天参加了某大厂前端工程师一面,整体面下来感觉偏简单。面试官一脸严肃,搞得我也有点紧张, 面试官,你不那么严肃能死啊!😒</blockquote><p>先从问答题开始</p><h3>1、 说一下const和let的区别</h3><table><thead><tr><th>声明方式</th><th>变量提升</th><th>作用域</th><th>初始值</th><th>重复定义</th></tr></thead><tbody><tr><td>const</td><td>否</td><td>块级作用域</td><td>需要</td><td>不允许</td></tr><tr><td>let</td><td>是</td><td>块级作用域</td><td>不需要</td><td>允许</td></tr><tr><td>var</td><td>是</td><td>函数级作用域</td><td>不需要</td><td>允许</td></tr></tbody></table><h4>变量提升</h4><ul><li>const和let不存在变量提升,var存在变量提升</li><li>使用let和const定义全局变量不再设置为顶层对象(window)的属性,有效避免全局变量污染</li></ul><h4>初始值</h4><ul><li>cosnt定义变量必须给初始值,且不能重复赋值</li></ul><h3>2、 一个http请求主要包括哪些部分</h3><blockquote>HTTP请求主要包括:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成</blockquote><p><img src="/img/bVGuNT" alt="http请求" title="http请求"></p><h4>请求行(request line)</h4><p>如图所示,请求行包括请求方法,URL,协议版本。请求方法主要有:GET、POST、PUT、DELETE、OPTIONS、HEAD等。协议版本有1.0,1.1,2.0等。</p><h4>请求头</h4><p>请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。请求头部通知服务器有关于客户端请求的信息,典型的请求头有:</p><ul><li>User-Agent:产生请求的浏览器类型。</li><li>Accept:客户端可识别的内容类型列表。</li><li>Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机。<br>当然除了这些数据之外还有回车符和换行符</li></ul><h3>请求体</h3><p>根据应用场景的不同,HTTP请求的请求体有三种不同的形式</p><ul><li>任意类型</li><li>文件分割</li><li>Encoding:编码</li></ul><h3>HTTPS交互过程</h3><blockquote>由于http无状态的明文协议,所以很容易被人截取后篡改,为了解决http的安全问题,出现了<strong>https</strong>协议。<strong>https</strong>是一种加密的超文本传输协议,http和https都是处于tcp协议之上,https相对于http增加了一层SSL/TLS安全层传输协议以实现加密通道。</blockquote><h4>1. 初始化过程</h4><p>服务端需要先申请证书并保存公私钥,然后需要把公钥、国家、城市、域名、签名算法等信息发给CA机构。<br>CA机构会把传过来的公钥加密,用客户端的私钥加密服务端的公钥,并生成证书存储到浏览器。</p><h3>2. 交互过程</h3><p><strong>客户端发起请求</strong></p><ol><li>三次握手建立TCP连接</li><li>支持的协议版本</li><li>生成随机数用于生成对话秘钥</li><li>客户端支持的算法</li><li>sessionID</li></ol><p><strong>服务端收到请求然后响应</strong></p><ol><li>确认加密通道协议版本</li><li>服务端生成随机数</li><li>确认使用的加密算</li><li>响应服务器证书</li></ol><p><strong>客户端收到证书并进行验证</strong></p><ol><li>验证证书是否为CA颁发</li><li>验证证书是否有效</li><li>验证通过后会生成随机数并发送给服务器</li></ol><p><strong>服务端接收随机数</strong></p><ol><li>服务端收到加密数据后,会用私钥对密文进行解密,并对比是否符合预期</li><li>然后发送加密数据给客户端</li></ol><p><strong>客户端接收消息</strong></p><ol><li>客户端验证加密数据是否与预期一致,一致则没收过程结束</li><li>之后所有通信过程将使用生成的对称秘钥。</li></ol><h3>算法题1: 求最长非重复子串</h3><blockquote>先来一个笨方法,时间复杂度为o(n方)</blockquote><pre><code class="javascript">function longestUniqueSub(str) {
const len = str.length;
let result = "";
for (let i = len - 1; i >= 0; i--) {
for (let j = i; j < len; j++) {
const subLen = [...new Set(str.slice(i, j + 1))].length;
if (subLen === j - i + 1) {
result = subLen > result.length ? str.slice(i, j + 1) : result;
}
}
}
return result;
}</code></pre><blockquote>再来一个比较高端的方法,时间复杂度为o(n)</blockquote><pre><code>function longestSubStr(str) {
const strArr = [];
let result = "";
for (let i = 0; i < str.length; i++) {
if (strArr.indexOf(str[i]) === -1) {
strArr.push(str[i]);
} else {
strArr.splice(0, strArr.indexOf(str[i]) + 1);
}
result = strArr.length > result.length ? strArr.join("") : result;
}
return result;
}</code></pre><h3>算法题2: leetCode第一道题:两数之和</h3><blockquote>这道题实现起来很简单,主要考察的是如何提高代码执行的效率。<br>同样是有o(n方)和o(n)两种时间复杂度的算法,这里仅提供时间复杂度为o(n)的实现算法</blockquote><pre><code>function twoSum(arr, target) {
const map = new Map();
for (let i = 0; i < arr.length; i++) {
const subValue = target - arr[i];
if (map.has(subValue)) {
return [map.get(subValue), i];
} else {
map.set(arr[i], i);
}
}
}</code></pre><h3>参考链接</h3><ol><li><a href="https://link.segmentfault.com/?enc=%2FZUc3ThzvBRVNCv5AgJwIg%3D%3D.D%2BAU362PhKeTc72HRae9SsAoPfthSlj6lnpdDEFkkpVYIOPmPTjP4Nn0NpdcomDI0I8gWvxaa7RXzWRbfHgnZw%3D%3D" rel="nofollow">Https:深入浅出HTTPS的交互过程。</a></li><li><a href="https://link.segmentfault.com/?enc=r%2BdhwE7PYxyS1a1oSX2KDg%3D%3D.2%2BHsc%2FYKlcXt9jpO3wN5l%2BZONR%2FBHU9hmt6y2lcyuFR7s81MwqQ4bv0BgL3UZn%2FN" rel="nofollow">菜鸟教程-http消息结构</a></li><li><a href="https://link.segmentfault.com/?enc=UlcHBE8jiIr5FCbaFlTrrw%3D%3D.MvZPy0AchdeLSyZB2RAD8Xy%2B03%2FSGPeByLx2bJ7jNjGSmJxt7jYEmuYqvpkXUyk3ufotE6cmGDRd82NyvB%2BC%2Fw%3D%3D" rel="nofollow">JavaScript ES6中const、let与var的对比详解</a></li></ol>
大厂面试题:手写数组的Flat方法
https://segmentfault.com/a/1190000040023297
2021-05-18T23:02:05+08:00
2021-05-18T23:02:05+08:00
不羁的风
https://segmentfault.com/u/wenhaoliu
0
<blockquote>前一阵面试大厂的前端岗,让手撸数组的<strong><em>Flat</em></strong>方法,很遗憾当时太菜没有什么思路,面试后仔细分析了一下,发现这道算法题完全没有想象中的那么难,的确是一套纯基础题,使用数组的reduce结合递归可以很轻松的实现,这里简单介绍下实现思路,以飨读者!</blockquote><h2>前置知识</h2><h4>数组的flat方法:</h4><p><strong><em>Flat()</em></strong>俗称把数组拍平的方法。<br>使用flat方法时可以传递一个数字,代表拍平嵌套数组的层数,不传值的话则为默认值1。如果我们传递的参数为<strong><em>Infinity</em></strong>,则会将所有嵌套数组拍平。如果数组中有空元素,则拍平后的数组也不包含空元素</p><pre><code class="js">let arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]
let arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]
let arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// [1, 2, 3, 4, 5, 6]
//使用 Infinity,可展开任意深度的嵌套数组
let arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr4.flat(Infinity);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
//flat会移除数组中的空项
let arr5 =[1,2,3,,5,[6,7]]
console.log(arr5.flat())
//[1, 2, 3, 5, 6, 7]</code></pre><h4>数组的Reduce方法:</h4><p>第一次接触到Reduce是在学习Hadoop的时候,那时候的理解是对Map后的数据各自执行Reduce处理,然后将处理结果汇总返回。JS中的Reduce与这个概念类似,就是通过一个方法对数组中的元素逐一处理,然后将结果逐一叠加,最后将结果返回出去。比如我们想要对数组中的元素执行平方后求和,用Reduce就很容易实现,代码如下:</p><pre><code class="js">let arr6=[1,2,3];
console.log(arr6.reduce((prev,curr)=>prev+curr*curr,0));</code></pre><p>猛一看的确让人有点费解,大可不必。想要搞清楚reduce的使用方式第一步要搞清楚reduce传递的参数,为如下四个:</p><table><thead><tr><th>参数名</th><th>含义</th></tr></thead><tbody><tr><td>previousValue</td><td>上一个元素(initialValue 或者数组第一项)</td></tr><tr><td>currentValue</td><td>当前元素</td></tr><tr><td>currentIndex</td><td>当前元素的index</td></tr><tr><td>array</td><td>执行reduce方法的数组对象(一般不用)</td></tr><tr><td>initialValue</td><td>执行reduce方法的初始值</td></tr></tbody></table><p>在TS中,对Reduce方法定义的类型如下:</p><pre><code class="ts">reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T;
reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T;</code></pre><p>我们可以看到,这里TS定义的reduce数据类型有两种调用方式,一种为包含初始值的调用方式,一种是不提供初始值的调用方式,这两种有什么区别?这里我们不卖关子,直接看例子:</p><pre><code class="js">let arr6=[2,2,3]
console.log(arr6.reduce((prev,curr,index)=>{
console.log(index)
return prev+curr*curr
}))
//输出为:1 2 15
let arr6=[2,2,3]
console.log(arr6.reduce((prev,curr,index)=>{
console.log(index)
return prev+curr*curr
},0))
//输出为: 0 1 2 17</code></pre><p>可以清楚地看到,提供initialValue后,reduce会从index为0的元素开始执行回调方法,而不提供initialValue则会将跳过index为0的元素,直接从index为1的元素开始执行回调方法(即将index为0的元素设置为initialValue)。</p><h2>算法实现</h2><h4>1.使用reduce方法实现提取深度为1的数组</h4><pre><code class="javascript">function MyFlat(arr){
return arr.reduce((prev,curr)=>prev.concat(curr),[]);
}
let arr=[1,2,3,4,5,6,[7,8,9]];
console.log(MyFlat(arr));
//[1, 2, 3, 4, 5, 6, 7, 8, 9]</code></pre><h4>2.使用数组的解构赋值实现提取深度为1的数组</h4><pre><code class="javascript">function MyFlat(arr){
return [].concat(...arr)
}
let arr=[1,2,3,4,5,6,[7,8,9]]
console.log(MyFlat(arr))</code></pre><p>ps:这个方法有点炫^_^</p><h4>3.使用reduce+递归实现多层数组的提取</h4><p>前面两个实现的是不传参的flat方法,也即是对数组执行层级为1的提取。如果我们想指定提取的层级,或者使用Infinity提取任意层数组怎么做呢?看代码:</p><pre><code class="javascript">function MyFlat(arr,deep=1){
return deep>0 ? arr.reduce((prev,curr)=>prev.concat(Array.isArray(curr)?MyFlat(curr,deep-1):curr),[]):arr.slice()
}
console.log(MyFlat([1,2,3,[4,5,[6,7,8,[9,10]]]],Infinity))
//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]</code></pre><p>代码很短,但是很有嚼头。我们在参数里增加了一个变量deep代表默认深度,给他一个默认值1。程序会首先判断deep的深度,如果深度大于零就执行后面的代码,否则返回数组的副本。<br>如果深度大于0,我们会用initialValue也就是空数组去concat数组里面的元素,在concat的时候我们会判断当前元素是否为数组,如果是数组则递归执行MyFlat方法对子元素进行提取。这里不要忘了,我们提供了初始值"[]",所以其会从index为0的元素开始执行回调方法。</p><h4>4.使用生成器函数实现多层数组的提取</h4><pre><code class="javascript">
function* flatten(arr){
for (let item of arr){
if(Array.isArray(item)){
yield* flatten(item);
}else{
yield item;
}
}
}
let result =[...flatten([1,2,3,[4,5],[6,7,[8,9]]])]
console.log(result) //[1, 2, 3, 4, 5, 6, 7, 8, 9]</code></pre><p>实现代码很短,仔细阅读的话就会发现实际上不难理解。代码的核心在于判断每一个遍历的元素是否为数组元素,如果是数组元素,则递归调用flatten方法,否则返回当前遍历的元素,这里需要读者弄清楚两个知识点:1、什么是生成器函数 2、yield是什么意思,yield*又是什么意思?</p><blockquote>Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。 <br>形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。<br> <strong>阮一峰 ES6入门</strong></blockquote><p>让我们来看一个基本的生成器函数:</p><pre><code class="js">function* MyGeneration(){
yield 'first';
yield 'second';
return 'third';
}
let GF = MyGeneration()
GF.next();
//{value: "first", done: false}
GF.next();
//{value: "second", done: false}
GF.next();
//{value: "third", done: true}</code></pre><p>我们可以清楚的看到Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。另外一点需要记住的是生成器函数不会立刻执行,只有执行了next方法以后,才会真正的执行。<br>如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。而yield*就是为了解决这个问题,他让我们可以在一个generator函数里面调用另外一个函数。这样如果另外一个generator函数里面有多个yield我们就不用逐个进行遍历了。</p><h4>5.使用堆栈实现flat函数</h4><pre><code class="javascript">var arr1 = [1,2,3,[1,2,3,4, [2,3,4]]];
function flatten(input) {
const stack = [...input];
const res = [];
while (stack.length) {
//出栈
const next = stack.pop();
if (Array.isArray(next)) {
//解构后入栈
stack.push(...next);
} else {
//非子数组入栈
res.push(next);
}
}
// 反转恢复原数组的顺序
return res.reverse();
}
flatten(arr1);// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]</code></pre><p>使用堆栈的思想也很精妙,建议盆友们把这段代码在编译器里面调试一下,更能理解其中思想的精髓。其核心思想是模拟栈,逐层剥皮,直至完成所有元素的去方括号操作,然后将结果返回,当然返回的结果是倒序的,因为执行完剥皮操作后是将元素从后往前推入结果数组,因此最后需要翻转后返回。</p><h2>总结</h2><p>实现JS的flat方法其实不止笔者所列上述方法,但是我们可以得出结论:JS是一门非常强大的语言,尤其是在ES6-ES10标准出推出以后。作为前端工程师我们在完成开发任务后,要多思考一下如何优化我们的算法,提高我们代码的执行效率,不断的让我们的代码趋于完美。后面笔者还将推出使用JS去重相关的文章,在即将到来的文章里,大家更能感受到JS这门编程语言的美妙。<br>参考资料:<br>1、MDN Array.prototype.flat():<a href="https://link.segmentfault.com/?enc=48%2Bq%2FCJAxdT1ofZhNCafUw%3D%3D.toDtBZo6qamXwkoBpBHUuDERBnm5hmjuzm3B6O8pe2wI0ezcT51t3vO%2BDE1%2Fyz9UxpmIYuz8j0Q7fJsLZ5lNJYqdo0N2N8Bcn4MV6tkbMMjGSdW9261nY%2F%2BSEDNJR25T" rel="nofollow">Array.prototype.flat()</a><br>2、ES6 入门-Generator <a href="https://link.segmentfault.com/?enc=DnBJSZggMrISzAi4A6ZbLA%3D%3D.r2mTQrJfnyYYIz%2FW5H7dfKv2u4A13BrOwmJoJzK6ft%2Bay0rsNJ88mEqWv3ySehBh" rel="nofollow">Generator</a></p>
HTML元素水平垂直居中实现方式(每天一个知识点)
https://segmentfault.com/a/1190000039831333
2021-04-15T18:27:40+08:00
2021-04-15T18:27:40+08:00
不羁的风
https://segmentfault.com/u/wenhaoliu
3
<blockquote>在前端页面开发中,HTML元素水平垂直居中是需要经常处理的问题,今天我们就来系统的学习一下如何在HTML实现水平垂直居中,通过对主流水平垂直居中实现方式的实践,来找到适合特定情况下的布局实现方式,并逐步达到灵活运用的水平。</blockquote><h2>水平居中</h2><h4>方式一: text-aligin:center(仅限行内元素)</h4><p>text-align属性定义行内元素(例如文字)如何相对它的块父元素对齐。当其值为center时可以领行内元素居中对齐。</p><pre><code class="html"><style>
.box1{
width: 200px;
height: 200px;
background-color: orange;
text-align: center;
}
</style>
<div class="box1">
一段用于演示的文字
</div> </code></pre><h4>方式二:margin:0 auto</h4><p>margin用来设置一个块元素的偏移量,其值有四个参数,分别代表:上、右、下、左四个方向的偏移量(顺时针)。其值可以简写为两个,第一个值代表<strong>上下</strong>两侧的偏移量,第二个值代表<strong>左右</strong>两侧的偏移量,当我们给左右两侧的偏移量设置为auto时,代表我们让浏览器自己选择一个合适的偏移量,这样就能实现左右水平居中。</p><pre><code class="html"><style>
.box2{
width: 75%;
background-color: lime;
margin:0 auto;
}
</style>
<div class="box2">
一段用于演示的文字
</div></code></pre><h4>方式三:基于relative布局实现水平居中</h4><p>我们可以通过将一个元素设置为浮动元素,然后将其定位设置为relative,将其子元素的定位也设置为relative。然后将父元素的left值设置为50%,将其向右移动50%的距离,接着我们给子元素设定left值为-50%使其向反向向移动自身位置的50%,这样同样可以实现居中效果。</p><pre><code class="html"><style>
.box3 {
float: left;
position: relative;
left: 50%;
}
.box4 {
position: relative;
left: -50%;
}
</style>
<div class="box3">
<div class="box4">
一段用于演示的文字
</div>
</div></code></pre><h4>方式四:基于Flex实现水平居中(移动端首选)</h4><p>基于flex实现水平居中很简单,只需要将justify-content属性设定为center即可。由于flex是一个响应式布局,是移动端跨端页面开发的首选,因此非常建议移动端开发的小伙伴以这种方式实现水平布局。</p><pre><code class="html">
<div class="box5">
一段用于演示的文字
</div>
.box5{
display: flex;
justify-content: center;
}</code></pre><h4>方式五:父元素开启相对定位,子元素开启绝对定位</h4><p>我们还可以通过给父元素开启相对定位,并给子元素开启绝对定位,来实现水平居中,具体实现方式如下:</p><pre><code class="html">
<div class="box6">
<div class="box7">
一段用于演示的文字
</div>
</div>
.box6{
position: relative;
}
.box7{
width: 80%;
position: absolute;
background-color: pink;
left:0;
right: 0;
margin:0 auto;
} </code></pre><h2>垂直居中</h2><h4>方式一:单行文字实现水平居中</h4><p>单行文字实现水平居中是我们在前端页面开发中常常遇到的一种情况,这种情况比较特殊,这里我们首先介绍。在一个块元素中,如果只有一行文字,我们只需将line-height参数设置为与当前容器的高度一致即可,代码如下:</p><pre><code class="html">
<div class="box1">
一段用于演示的文字
</div>
<style>
.box1{
height: 80px;
line-height: 80px;
}
</style></code></pre><h4>方式二:基于Flex实现垂直居中</h4><p>基于flex实现垂直居中同样很简单的,只需要设置align-items:center即可,代码如下:</p><pre><code class="html"><div class="box1">
<div class="boxinner1">
</div>
</div>
<style>
.box1 {
display: flex;
width: 100%;
height: 800px;
background-color: lightskyblue;
/* 设置元素垂直排列 */
align-items: center;
}
.boxinner1 {
height: 100px;
width: 100px;
background-color: gray;
}
</style></code></pre><h4>方式三:基于Table布局模式</h4><p>我们可以将父元素的display属性设置为table,然后将子元素的display设置为table-cell,然后设置vertical-align:middle;来实现子元素的垂直布局。</p><pre><code class="html"><div class="outer">
<div class="inner">
一段用于演示的文字
</div>
</div>
<style>
.outer {
width: 100%;
height: 100px;
display: table;
}
.inner {
display: table-cell;
vertical-align: middle;
}
</style></code></pre><p>这种垂直方向实现居中的缺陷在于父元素需要指定宽度。</p><h4>方式四:absolute定位+transform</h4><p>我们可以将父元素的position属性设置为relative,然后将子元素的属性设置为absolute,子元素就会相对于父元素进行定位,然后我们将子元素的top属性设置为50%,这时候子元素相对于父元素顶部的距离为父元素高度的50%,然后我们还需要将元素本身向上移动自身高度的50%,我们可以使用transform实现。</p><pre><code class="html"><div class="outer">
<div class="inner">
一段用于演示的文字
</div>
</div>
<style>
.outer {
height: 100px;
position: relative;
background-color: lime;
}
.inner {
position: absolute;
top:50%;
transform: translate(0,-50%) ;
}
</style></code></pre><p>实现水平垂直居中的方式有很多,每个实现的方式都有自己的缺点或有点,比如我们将元素设置为浮动时会遇到高度塌陷的问题,而使用flex布局则会在老的IE浏览器存在兼容性问题。因此我们需要根据我们的使用场景灵活选择,争取找到实现方式中的最优解!</p>
JavaScript常见继承方式
https://segmentfault.com/a/1190000039787155
2021-04-07T16:48:27+08:00
2021-04-07T16:48:27+08:00
不羁的风
https://segmentfault.com/u/wenhaoliu
1
<blockquote>与传统的面向对象语言不同,JavaScript的继承主要是通过原型链和借用构造函数的方式实现。今天我们就来学习下在JavaScript中常见的四种继承实现方式,分别是:原型链继承、借用构造函数继承、组合继承以及Class类继承。</blockquote><h2>原型链继承</h2><p>原型链继承的核心思想是通过将子类的原型设置为父类实例的对象来实现对属性和方法的继承。</p><p><strong>实际案例1:</strong></p><pre><code class="javascript">//父类构造函数
function SuperType(){
this.color=['红','橙','黄'];
this.name="大壮";
}
//子类构造函数
function SubType(){}
//将子类的原型指向父类的实例
SubType.prototype = new SuperType();
let instance1 = new SubType();
instance1.color.push("紫");
instance1.name ="大顺"
console.log(instance1.name); // 大顺
console.log(instance1.color);// ['红','橙','黄',"紫"]
var instance2 = new SubType();
console.log(instance2.color);// ['红','橙','黄',"紫"]
console.log(instance2.name);// 大壮</code></pre><p>这里,我们可以清晰的看到,由于原型属性中引用的类型会被实例共享,而基本基本数据类型不会被实例共享,所以导致我们修改了原型上的name属性不会影响其他实例,而当修改了引用实例(color)却作用到了其他实例,这肯定不是我们想要的。<br>原型属性还有一个问题,就是我们在实例化属性的时候我们不能进行传参,比如我们在实例化属性的时候想要给name传一个值,但由于子类的参数无法共享给父类,所以是无法做到的。原型链继承主要问题总结如下:</p><ol><li>原型属性上的引用类型数据修改后会污染其他的实例;</li><li>实例化对象时无法传参。</li></ol><h2>借用构造函数继承</h2><blockquote>借用构造函数继承的核心思想是通过call()、apply()函数在<strong>将来</strong>实例化的对象上执行构造函数。</blockquote><p><strong>实际案例2:</strong></p><pre><code class="javascript">// 父类构造函数
function SuperType1(){
this.color = ['red','orange','yellow'];
this.name = "DaZhuang";
}
// 子类构造函数
function SubType1(){
SuperType1.call(this);
}
let instance3 = new SubType1();
instance3.color.push("purple");
instance3.name = "DaShun";
console.log(instance3.color); //["red", "orange", "yellow", "purple"]
console.log(instance3.name); // DaShun
let instance4 = new SubType1();
console.log(instance4.color); //['red','orange','yellow']
console.log(instance4.name); // DaZhuang</code></pre><p>我们可以清楚地看到,通过借用构造函数,我们可以在创建子类实例的时候执行SuperType()上写好的初始化代码,这样每个实例都有一个color的副本了,很好的处理了引用类型属性污染其他实例的问题。<br>但是这种借用构造函数方式的继承也存在问题,那就是属性全都在父类中定义因此无法进行函数的复用,而且在父类原型中定义的方法对子类也是不可见的,结果所有类型都只能使用构造函数模式。因此借用构造函数的方式也很少单独使用。</p><h2>组合继承</h2><blockquote>组合继承是指将原型链继承和借用构造函数继承这两种方式结合起来,从而发挥二者各自的长处的一种继承模式。<br>其核心思路是通过原型链实现对原型属性和方法的继承,而借用构造函数实现对实例属性和方法的继承。</blockquote><p><strong>实际案例3:</strong></p><pre><code class="javascript">//父类构造函数
function Super (name) {
this.name = name;
this.colors = ["金", "木", "水"];
this.sayBigName = function () {
console.log('父类的方法');
}
}
//子类构造函数
function Sub (name) {
//实现父类属性和方法的继承
Super.call(this, name);
}
//实现实例属性和方法的继承
Sub.prototype = new Super();
//修复Sub构造函数原型构造函数的指向,从Super变更为Sub
Sub.prototype.constructor = Sub;
Sub.prototype.sayName = function () {
console.log(this.name);
}
var sub1 = new Sub("大壮");
sub1.colors.push("火");
console.log(sub1.colors); //["金", "木", "水", "火"]
sub1.sayName(); //大壮
sub1.sayBigName(); //父类的方法
var sub2 = new Sub("大顺");
console.log(sub2.colors); // ["金", "木", "水"]
sub2.sayName(); //大顺</code></pre><p>这个案例比较复杂,读者需要细细品读。首先读者要明确哪些属性和方法是在父类中声明的(对应案例中Super里面的:name,colors,sayBigName()),这些属性和方法是怎么实例化的呢?是通过13行代码(借用构造函数)实例化的,通过call方法在Sub上下文执行了Super属性和方法的实例化,这样Sub就拿到了Super里面的属性和方法。但是做完这一步是完全不够的,因为我们在子类中也有一些方法需要子类继承,这些方法我们也希望它能在实例化的时候被实例对象共享,这时候就需要采用原型链继承模式(代码17行)。</p><p>我们通过将子类(Sub)的原型指向父类的实例,这样在我们在子类中定义的方法和属性就会被存放到这个实例化的父类对象里面。在后续实例化Sub子类创建实例过程中,实例对象就可以通过原型链继承的形式拿到子类中定义的方法(sub1._ <em>proto_</em> 等于Sub.prototype)。这样我们就实现了实例对象对父类对象属性和方法的继承以及子类对象方法的继承。</p><p>细心的同学可能会发现,我们并没有通过原型链模式让实例对象继承子类的属性,其原因其实我们在原型链继承模式部分已经介绍,就是因为使用原型链继承引用类型数据的时候存在污染其他实例对象的问题,因此我们采用原型链模式仅继承子类的方法而不对子类的属性进行继承(属性可以放到父类构造函数中)。</p><h2>ES6 Class继承</h2><blockquote>在es6中,引入了class函数,通过class语法我们可以很容易的实现继承<br><strong>实际案例4:</strong></blockquote><pre><code class="javascript">//父类
class Super {
getValue () {
console.log(this.val)
}
}
//子类
class Sub extends Super {
constructor(value) {
super(value)
this.val = value
}
}
let sub = new Sub(1)
sub.getValue() // 1
sub instanceof Super // true</code></pre>
高度塌陷和BFC(Block Formatting Context)
https://segmentfault.com/a/1190000039670866
2021-03-19T11:12:34+08:00
2021-03-19T11:12:34+08:00
不羁的风
https://segmentfault.com/u/wenhaoliu
1
<h3>引言</h3><p>在前端面试中,BFC是一个很经常问到的概念。今天我们就来一起了解下什么是BFC,以及它的主要作用。在学习BFC之前,我们先来看看什么是高度塌陷:</p><h4>高度塌陷</h4><p>在文档流中,父元素的高度默认是被子元素撑开的,子元素多高,父元素就会被撑开多高。但是当子元素设置为<strong>浮动</strong>(float:left/right)以后,子元素就会脱离文档流,这就导致子元素无法撑起父元素的高度,产生了高度塌陷。下面我们用一个案例演示一下。</p><p><strong>我们首先创建一个html页面,里面添加3个div容器</strong></p><pre><code class="html"><!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>
<style>
.box1 {
border: 10px solid red;
}
.box2 {
width: 100px;
height: 300px;
background-color: orange;
}
.box3 {
width: 300px;
height: 300px;
background-color: orchid;
}
</style>
</head>
<body>
<div class="box1">
<div class="box2"></div>
</div>
<div class="box3"></div>
</body>
</html></code></pre><p>将看到如下效果:<br><img src="/img/bVcQCau" alt="基本的html页面" title="基本的html页面"></p><p><strong>然后我们给box2添加一个浮动属性</strong></p><pre><code class="html"> <style>
.box1 {
border: 10px solid red;
}
.box2 {
width: 100px;
height: 300px;
background-color: orange;
float:left;
}
.box3 {
width: 300px;
height: 300px;
background-color: orchid;
}
</style></code></pre><p>效果是这样的:<br><img src="/img/bVcQCaJ" alt="image" title="image"><br>我们可以很清晰的看到由于box2设置了向左浮动,所以脱离了文档流,也就导致了box2之外的box1容器感知不到box2的存在,因此高度变成了0。高度塌陷以后,下面的元素必然会向上拱,这样就让布局非常混乱。那么如何解决这个问题呢?</p><ul><li><p>方法一: 给父元素设定一个固定高度</p><blockquote>这种方式不利于后期维护(子元素高度随时会变),因此不可取</blockquote></li><li>方法二:开启父元素的BFC</li></ul><h4>什么是BFC</h4><pre><code>根据W3C的标准,BFC(Block Formatting Context)叫块级格式化上下文,BFC是CSS中隐
含的属性,该属性可以设置打开或者关闭,默认是关闭的,当我们为一个元素开启BFC后,
该元素会变成一个独立的布局区域。总之BFC的目的就是:形成一个完全独立的空间,让空
间里的子元素不会影响到外面的布局,并且触发了BFC的容器具有普通容器所没有的一些特
性。
</code></pre><h5>BFC的特性</h5><ol><li>开启BFC的元素不会被浮动元素所覆盖</li><li>开启BFC的元素父元素的垂直外边距不会和子元素重叠</li><li>开启BFC的元素可以包含浮动的子元素</li></ol><h5>开启BFC的方法</h5><ol><li><p>将元素的overflow设置为非visible(即overflow:hidden/scroll/auto)</p><blockquote>推荐将overflow属性设置为auto。</blockquote></li><li><p>使用<code>display: flow-root</code></p><blockquote>display:flow-root可以将元素转换为块元素并开启BFC,但是存在兼容性问题,不兼容IE。</blockquote></li><li><p>设置元素的浮动(float)值不为none(即:left/right之一)</p><blockquote>不推荐,会导致元素宽度丢失。</blockquote></li><li><p>将元素设置为行内块元素(display:inline-block)</p><blockquote>不推荐,同样会导致元素宽度丢失。</blockquote></li><li><p>设置元素绝对定位(position:absolute;fixed)</p><blockquote>不推荐,同样会导致元素宽度丢失。</blockquote></li><li>特定布局方式 (display: inline-block/flex/table-cells)</li></ol><h4>BFC验证</h4><p>开启BFC能够解决高度塌陷问题的验证很简单,只需要增加以上列举的开启BFC方法中任意一个,我们以设置overflow:auto 为例。</p><pre><code class="html"><style>
.box1 {
border: 10px solid red;
overflow: auto;
}
.box2 {
width: 100px;
height: 300px;
background-color: orange;
float: left;
}
.box3 {
width: 300px;
height: 300px;
background-color: orchid;
}
</style></code></pre><p>刷新网页,可以看到高度塌陷的问题已经被解决:<br><img src="/img/bVcQCkq" alt="image" title="image"></p><h4>总结</h4><p>BFC是前端工程师需要掌握的一个基本概念,希望大家可以掌握处理高度塌陷的多种方法,这样我们才能够在工作中灵活处理遇到的问题。</p>
HTML语义化(每天一个知识点)
https://segmentfault.com/a/1190000039414010
2021-03-15T09:23:22+08:00
2021-03-15T09:23:22+08:00
不羁的风
https://segmentfault.com/u/wenhaoliu
0
<h3>什么是html语义化?</h3><blockquote><strong>HTML语义化就是用合理、正确的标签来展示内容。</strong><br><strong>比如我们在指定一些标签的时候,如div、span,我们并不知道其定义的内容类型,但当我们用h1-h5标签,我们知道这是定义的一级标题,table我们知道定义的是表格,img我们知道定义的是图片。</strong></blockquote><h3>语义化的优点</h3><ol><li>易于用户阅读,样式丢失的时候可以让页面呈现清晰的结构。</li><li>有利于seo,搜索引擎根据标签来确定上下文和各个关键字的权重</li><li>方便其他设备监解析,如盲人阅读器根据语义渲染网页</li><li>有利于开发和维护,语义化更具有可读性,代码更好维护,与CSS3关系更加和谐</li></ol><h3>主题结构</h3><p><img src="/img/bVcPxx7" alt="image" title="image"></p><h3>主要的语义化标签及含义</h3><p><br></p><table><thead><tr><th>标签名</th><th>具体解释</th></tr></thead><tbody><tr><td><header></td><td><header>标签定义文档或者文档部分区域的页眉,主要用作放置介绍性内容的容器。</td></tr><tr><td><nav></td><td><nav>用来定义导航链接块,但并不是所有的链接都应当位于nav块中</td></tr><tr><td><main></td><td><main>用来定义文档的主要内容(一个文档中只能有一个)</td></tr><tr><td><article></td><td><article>定义独立的自包含内容,如文档、博客、用户提交的评论等</td></tr><tr><td><aside></td><td><aside>定义页面主要内容之外的某些内容,如侧边栏或嵌如内容(分类)</td></tr><tr><td><footer></td><td><footer>定义文档的页脚,如版权信息,联系方式等</td></tr><tr><td><section></td><td><section>表示文档的一个区域。</td></tr></tbody></table><p><br><br><br></p>