注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

神奇的交叉观察器 - IntersectionObserver

1. 背景网页开发时,不管是在移动端,还是PC端,都有个很重要的概念,叫做动态懒加载,适用于一些图片资源(或者数据)特别多的场景中,这个时候,我们常常需要了解某个元素是否进入了“视口”(viewport),即用户能不能看到它。 传统的实现方法是,监听到scro...
继续阅读 »

1. 背景

网页开发时,不管是在移动端,还是PC端,都有个很重要的概念,叫做动态懒加载,适用于一些图片资源(或者数据)特别多的场景中,这个时候,我们常常需要了解某个元素是否进入了“视口”(viewport),即用户能不能看到它。


传统的实现方法是,监听到scroll事件或者使用setInterval来判断,调用目标元素的getBoundingClientRect()方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于scroll事件触发频率高,计算量很大,如果不做防抖节流的话,很容易造成性能问题,而setInterval由于其有间歇期,也会出现体验问题。


所以在几年前,Chrome率先提供了一个新的API,就是IntersectionObserver,它可以用来自动监听元素是否进入了设备的可视区域之内,而不需要频繁的计算来做这个判断。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做”交叉观察器”。


2. 兼容性

由于这个api问世已经很多年了,所以对浏览器的支持性还是不错的,完全可以上生产环境,点击这里可以看看当前浏览器对于IntersectionObserver的支持性:


111111.png

3. 用法

API的调用非常简单:


const io = new IntersectionObserver(callback, option);

上面代码中,IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数:



  • callback:可见性发现变化时的回调函数
  • option:配置对象(可选)。

构造函数的返回值是一个观察器实例。实例一共有4个方法:



  • observe:开始监听特定元素
  • unobserve:停止监听特定元素
  • disconnect:关闭监听工作
  • takeRecords:返回所有观察目标的对象数组

3.1 observe

该方法需要接收一个target参数,值是Element类型,用来指定被监听的目标元素


// 获取元素
const target = document.getElementById("dom");

// 开始观察
io.observe(target);

3.2 unobserve

该方法需要接收一个target参数,值是Element类型,用来指定停止监听的目标元素


// 获取元素
const target = document.getElementById("dom");

// 停止观察
io.unobserve(target);

3.3 disconnect

该方法不需要接收参数,用来关闭观察器


// 关闭观察器
io.disconnect();

3.4 takeRecords

该方法不需要接收参数,返回所有被观察的对象,返回值是一个数组


// 获取被观察元素
const observerList = io.takeRecords();

注意:

observe方法的参数是一个 DOM 节点,如果需要观察多个节点,就要多次调用这个方法:


// 开始观察多个元素
io.observe(domA);
io.observe(domB);
io.observe(domC);

4. callback 参数

目标元素的可见性变化时,就会调用观察器的回调函数callback


callback一般会触发两次。一次是目标元素刚刚进入视口,另一次是完全离开视口。


const io = new IntersectionObserver((changes, observer) => {
console.log(changes);
console.log(observer);
});

上面代码中,callback函数的参数接收两个参数changesobserver



  • changes:这是一个数组,每个成员都是一个被观察对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,那么changes数组里面就会打印出两个元素,如果只观察一个元素,我们打印changes[0]就能获取到被观察对象
  • observer: 这是一个对象,返回我们在实例中传入的第二个参数option(如果没传,则返回默认值)

5. IntersectionObserverEntry 对象

上面提到的changes数组中的每一项都是一个IntersectionObserverEntry 对象(下文简称io对象),对象提供目标元素的信息,一共有八个属性,我们打印这个对象:


// 创建实例
const io = new IntersectionObserver(changes => {
changes.forEach(change => {
console.log(change);
});
});

// 获取元素
const target = document.getElementById("dom");

// 开始监听
io.observe(target);

运行上面代码,并且改变dom的可见性,这时控制台可以看到一个对象:


555.png

每个属性的含义如下:



  • boundingClientRect:目标元素的矩形区域的信息
  • intersectionRatio:目标元素的可见比例,即intersectionRectboundingClientRect的比例,完全可见时为1,完全不可见时小于等于0
  • intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
  • isIntersecting: 布尔值,目标元素与交集观察者的根节点是否相交
  • isVisible: 布尔值,目标元素是否可见(该属性还在试验阶段,不建议在生产环境中使用)
  • rootBounds:根元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
  • target:被观察的目标元素,是一个 DOM 节点对象
  • time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒

6. 应用


  1. 预加载(滚动加载,翻页加载,无限加载)
  2. 懒加载(后加载、惰性加载)
  3. 其它

7. 注意点

IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发。


规格写明,IntersectionObserver的实现,应该采用requestIdleCallback(),即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。


8. 参考链接


作者:三年没洗澡
来源:https://juejin.cn/post/7035490578015977480

收起阅读 »

js打包时间缩短90%,bundleless生产环境实践总结

最近尝试将bundleless的构建结果直接用到了线上生产环境,因为bundleless只会编译代码,不会打包,因此构建速度极快,同比bundle模式时间缩短了90%以上。得益于大部分浏览器都已经支持了http2和浏览器的es module,对于我们没有强兼容...
继续阅读 »




最近尝试将bundleless的构建结果直接用到了线上生产环境,因为bundleless只会编译代码,不会打包,因此构建速度极快,同比bundle模式时间缩短了90%以上。得益于大部分浏览器都已经支持了http2和浏览器的es module,对于我们没有强兼容场景的中后台系统,将bundleless构建结果应用于线上是有可能的。本文主要介绍一下,本人在使用bundleless构建工具实践中遇到的问题。

  • 起源

  • 结合snowpack实践

  • snowpack的Streaming Imports

  • 性能比较

  • 总结

  • 附录snowpack和vite的对比


本文原文来自我的博客: github.com/fortheallli…

一、起源

1.1 从http2谈起

以前因为http1.x不支持多路服用, HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8个的TCP链接请求限制.因此我们需要做的就是将同域的一些静态资源比如js等,做一个资源合并,将多次请求不同的js文件,合并成单次请求一个合并后的大js文件。这就是webpack等bundle工具的由来。

而从http2开始,实现了TCP链接的多路复用,因此同域名下不再有请求并发数的限制,我们可以同时请求同域名的多个资源,这个并发数可以很大,比如并发10,50,100个请求同时去请求同一个服务下的多个资源。

因为http2实现了多路复用,因此一定程度上,将多个静态文件打包到一起,从而减少请求次数,就不是必须的

主流浏览器对http2的支持情况如下:

Lark20210825-203949

除了IE以外,大部分浏览器对http2的支持性都很好,因为我的项目不需要兼容IE,同时也不需要兼容低版本浏览器,不需要考虑不支持http2的场景。(这也是我们能将不bundle的代码用于线上生产环境的前提之一)

1.2 浏览器esm

对于es modules,我们并不陌生,什么是es modules也不是本文的重点,一些流行的打包构建工具比如babel、webpack等早就支持es modules。

我们来看一个最简单的es modules的写法:

//main.js
import a from 'a.js'
console.log(a)

//a.js
export let  a = 1

上述的es modules就是我们经常在项目中使用的es modules,这种es modules,在支持es6的浏览器中是可以直接使用的。

我们来举一个例子,直接在浏览器中使用es modules

<html  lang="en">
   <body>
       <div id="container">my name is {name}</div>
       <script type="module">
          import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js'
          new Vue({
            el: '#container',
            data:{
               name: 'Bob'
            }
          })
       </script>
   </body>
</html>

上述的代码中我们直接可以运行,我们根据script的type="module"可以判断浏览器支不支持es modules,如果不支持,该script里面的内容就不会运行。

首先我们来看主流浏览器对于ES modules的支持情况:

Lark20201119-151747

从上图可以看出来,主流的Edge, Chrome, Safari, and Firefox (+60)等浏览器都已经开始支持es modules。

同样的因为我们的中后台项目不需要强兼容,因此不需要兼容不支持esm的浏览器(这也是我们能将不bundle的代码用于线上生产环境的前提之二)。

1.3 小结

浏览器对于http2和esm的支持,使得我们可以减少模块的合并,以及减少对于js模块化的处理。

  • 如果浏览器支持http2,那么一定程度上,我们不需要合并静态资源

  • 如果浏览器支持esm,那么我们就不需要通过构建工具去维护复杂的模块依赖和加载关系。

这两点正是webpack等打包工具在bundle的时候所做的事情。浏览器对于http2和esm的支持使得我们减少bundle代码的场景。

二、结合snowpack实践

我们比较了snowpack和vite,最后选择采用了snowpack(选型的原因以及snowpack和vite的对比看最后附录),本章节讲讲如何结合snowpack构建工具,构建出不打包形式的线上代码。

2.1 snowpack的基础用法

我们的中后台项目是react和typescript编写的,我们可以直接使用snowpack相应的模版:

npx create-snowpack-app myproject --template @snowpack/app-template-react-typescript

snowpack构建工具内置了tsc,可以处理tsx等后缀的文件。上述就完成了项目初始化。

2.2 前端路由处理

前端路由我们直接使用react-router或者vue-router等,需要注意的时,如果是在开发环境,那么必须要指定在snowpack.config.mjs配置文件,在刷新时让匹配到前端路由:

snowpack.config.mjs
...
 routes: [{ match: 'routes', src: '.*', dest: '/index.html' }],
...

类似的配置跟webpack devserver等一样,使其在后端路由404的时候,获取前端静态文件,从而执行前端路由匹配。

2.3 css、jpg等模块的处理

在snowpack中同样也自带了对css和image等文件的处理。

  • css

以sass为例,

snowpack.config.mjs

plugins: [
    '@snowpack/plugin-sass',
    {
      /* see options below */
    },
  ],

只需要在配置中增加一个sass插件就能让snowpack支持sass文件,此外,snowpack也同样支持css module。.module.css或者.module.scss命名的文件就默认开启了css module。此外,css最后的结果都是通过编译成js模块,通过动态创建style标签,插入到body中的。

//index.module.css文件
.container{
   padding: 20px;
}

snowpack构建处理后的css.proxy.js文件为:

export let code = "._container_24xje_1 {\n  padding: 20px;\n}";
let json = {"container":"_container_24xje_1"};
export default json;

// [snowpack] add styles to the page (skip if no document exists)
if (typeof document !== 'undefined') {
 const styleEl = document.createElement("style");
 const codeEl = document.createTextNode(code);
 styleEl.type = 'text/css';

 styleEl.appendChild(codeEl);
 document.head.appendChild(styleEl);
}

上述的例子中我们可以看到。最后css的构建结果是一段js代码。在body中动态插入了style标签,就可以让原始的css样式在系统中生效。

  • jpg,png,svg等

如果处理的是图片类型,那么snowpack同样会将图片编译成js.

//logo.svg.proxy.js
export default "../dist/assets/logo.svg";

snowpack没有对图片做任何的处理,只是把图片的地址,包含到了一个js模块文件导出地址中。值得注意的是在浏览器es module中,import 动作类似一个get请求,import from可以是一个图片地址,浏览器es module自身可以处理图片等形式。因此在.js文件结尾的模块中,export 的可以是一个图片。

snowpack3.5.0以下的版本在使用css module的时候会丢失hash,需要升级到最新版本。

2.4 按需加载处理

snowpack默认是不打包的。只对每一个文件都做一些简单的模块处理(将非js模块转化成js模块)和语法处理,因此天然支持按需加载,snowpack支持React.lazy的写法,在react的项目中,只要正常使用React.Lazy就能实现按需加载。

2.5 文件hash处理

在最后构建完成后,在发布构建结果的时候,为了处理缓存,常见的就是跟静态文件增加hash,snowpack也提供了插件机制,插件会处理snowpack构建前的所有文件的内容,做为content转入到插件中,经过插件的处理转换后得到新的content.

可以通过snowpack-files-hash插件来实现给文件增加hash。

2.6 公用esm模块托管

snowpack对于项目构建的bundleless的代码可以直接跑在线上,在bundless的构建结果中,我们想进一步减少构建结果文件大小。以bundleless的方式构建的代码,默认在处理三方npm包依赖的时候,虽然不会打包,snowpack对项目中node_modules中的依赖重新编译成esm形式,然后放在一个新的静态目录下。因此最后构建的代码包含了两个部分:

项目本身的代码,将node_modules中的依赖处理成esm后的静态文件

其中node_modules中的依赖处理成esm后的静态文件,可以以cdn或者其他服务形式来托管。这样我们每次都不需要在构建的时候处理node_modules中的依赖。在项目本身的代码中,如果引用了npm包,只需要将其指向一个cdn地址即可。这样处理后的,构建的代码就变成:

只有项目本身的代码(项目中对于三方插件的引入,直接使用三方插件的cdn地址)

进一步想,如果我们使用了托管所有npm包(es module形式)的cdn地址之后,那么在本地开发或者线上构建的过程中,我们甚至不需要去维护本地的node_modules目录,以及yarn-lock或者package-lock文件。我们需要做的,仅仅是一个map文件进行版本管理。保存项目中的npm包名和该包相对应的cdn地址。

比如:

//config.map.json
{
 "react": "https://cdn.skypack.dev/react@17.0.2",
 "react-dom": "https://cdn.skypack.dev/react-dom@17.0.2",
}

通过这个map文件,不管是在开发还是线上,只要把:

import React from 'react'

替换成

import React from "https://cdn.skypack.dev/react@17.0.2"

就能让代码在开发环境或者生产环境中跑起来。如此简化之后,我们不论在开发环境还是生产环境都不需要在本地维护node_modules相关的文件,进一步可以减少打包时间。同时包管理也更加清晰,仅仅是一个简单的json文件,一对固定意义的key/value,简单纯粹

我们提到了一个托管了的npm包的有es module形式的cdn服务,上述以skypack为例,这对比托管了npm包cjs形式的cdn服务unpkg,两者的区别就是,unpkg所托管的npm包,大部分是cjs形式的,cjs形式的npm包,是不能直接用于浏览器的esm中的。skypack所做的事情就是将大部分npm包从cjs形式转化成esm的形式,然后存储和托管esm形式的结果。

三、snowpack的Streaming Imports

在2.7中我们提到了在dev开发环境使用了skypack,那么本地不需要node_modules,甚至不需要yarn-lock和package-lock等文件,只需要一个json文件,简单的、纯粹的,只有一对固定意义的key/value。在snowpack3.x就提供了这么一个功能,称之为Streaming Imports。

3.1 snowpack和skypack

在snowpack3.x在dev环境支持skypack:

// snowpack.config.mjs
export default {
 packageOptions: {
   source: 'remote',
},
};

如此,在dev的webserver过程中,就是直接下载skypack中相应的esm形式的npm包,放在最后的结果中,而不需要在本地做一个cjs到esm的转换。这样做有几点好处:

  • 速度快: 不需要npm install一个npm包,然后在对其进行build转化成esm,Streaming Imports可以直接从一个cdn地址直接下载esm形式的依赖

  • 安全:业务代码中不需要处理公共npm包cjs到esm的转化,业务代码和三方依赖分离,三方依赖交给skypack处理

3.2 依赖控制

Streaming Imports自身也实现了一套简单的依赖管理,有点类似go mod。是通过一个叫snowpack.deps.json文件来实现的。跟我们在2.7中提到的一样,如果使用托管cdn,那么本地的pack-lock和yarn-lock,甚至node_modules是不需要存在的,只需要一个简单纯粹的json文件,而snowpack中就是通过snowpack.deps.json来实现包的依赖管理的。

我们安装一个npm包时,我们以安装ramda为例:

npx snowpack ramda

在snowpack.deps.json中会生成:

{
 "dependencies": {
   "ramda": "^0.27.1",
},
 "lock": {
   "ramda#^0.27.1": "ramda@v0.27.1-3ePaNsppsnXYRcaNcaWn",
}
}

安装过程的命令行如下所示:

飞书20210831-211844

从上图可以看出来,通过npx snowpack安装的依赖是从skypack cdn直接请求的。

特别的,如果项目需要支持typescript,那么我们需要将相应的npm包的声明文件types下载到本地,skypack同样也支持声明文件的下载,只需要在snowpack的配置文件中增加:

// snowpack.config.mjs
export default {
 packageOptions: {
   source: 'remote',
   types:true //增加type=true
},
};

snowpack会把types文件下载到本地的.snowpack目录下,因此在tsc编译的时候需要指定types的查找路径,在tsconfig.json中增加:

//tsconfig.json
"paths": {
     "*":[".snowpack/types/*"]
  },

3.3 build环境

snowpack的Streaming Imports,在dev可以正常工作,dev的webserver中在请求npm包的时候会将请求代理到skypack,但是在build环境的时候,还是需要其他处理的,在我们的项目中,在build的时候可以用一个插件snowpack-plugin-skypack-replacer,将build后的代码引入npm包的时候,指向skypack。

build后的线上代码举例如下:

import * as __SNOWPACK_ENV__ from '../_snowpack/env.271340c8a413.js';
import.meta.env = __SNOWPACK_ENV__;

import ReactDOM from "https://cdn.skypack.dev/react-dom@^17.0.2";
import App from "./App.e1841499eb35.js";
import React from "https://cdn.skypack.dev/react@^17.0.2";
import "./index.css.proxy.9c7da16f4b6e.js";

const start = async () => {
 await ReactDOM.render(/* @__PURE__ */ React.createElement(App, null), document.getElementById("root"));
};
start();
if (undefined /* [snowpack] import.meta.hot */ ) {
 undefined /* [snowpack] import.meta.hot */ .accept();
}

从上述可以看出,build之后的代码,通过插件将:

import React from 'react'
//替换成了
import React from "https://cdn.skypack.dev/react@^17.0.2";

四、性能比较

4.1 lighthouse对比

简单的使用lighthouse来对比bundleless和bundle两种不同构建方式网页的性能。

  • bundleless的前端简单性能测试:

img

  • bundle的前端性能测试:

img

对比发现,这里两个网站都是同一套代码,相同的部署环境,一套是构建的时候是bundleless,利用浏览器的esm,另一个是传统的bundle模式,发现性能上并没有明显的区别,至少bundleless简单的性能测试方面没有明显差距。

4.2构建时间对比

bundleless构建用于线上,主要是减少了构建的时间,我们传统的bundle的代码,一次编译打包等可能需要几分钟甚至十几分钟。在我的项目中,bundleless的构建只需要4秒。

飞书20210901-165311

同一个项目,用webpack构建bundle的情况下需要60秒左右。

4.3构建产物体积对比

bundleless构建出的产物,一般来说也只有bundle情况下的1/10.这里不一一举例。

五、总结

在没有强兼容性的场景,特别是中后台系统,bundleless的代码直接跑在线上,是一种可以尝试的方案,上线的时间会缩短90%,不过也有一些问题需要解决,首先需要保证托管esm资源的CDN服务的稳定性,且要保障被托管的esm资源在浏览器运行不会出现异常。我们运行了一些常见的npm包,发现并没有异常情况,不过后续需要更多的测试。

六、附录:snowpack和vite的对比

6.1 相同点

snowpack和vite都是bundleless的构建工具,都利用了浏览器的es module来减少对静态文件的打包,从而减少热更新的时间,从而提高开发体验。原理都是将本地安装的依赖重新编译成esm形式,然后放在本地服务的静态目录下。snowpack和vite有很多相似点

  • 在dev环境都将本地的依赖进行二次处理,对于本地node_module目录下的npm包,通过其他构建工具转换成esm。然后将所有转换后的esm文件放在本地服务的静态目录下

  • 都支持css、png等等静态文件,不需要安装其他插件。特别对于css,都默认支持css module

  • 默认都支持jsx,tsx,ts等扩展名的文件

  • 框架无关,都支持react、vue等主流前端框架,不过vite对于vue的支持性是最好的。

6.2 不同点

dev构建: snowpack和vite其实大同小异,在dev环境都可以将本地node_modules中npm包,通过esinstall等编译到本地server的静态目录。不同的是在dev环境

  • snowpack是通过rollup来将node_modules的包,重新进行esm形式的编译

  • vite则是通过esbuild来将node_modules的包,重新进行esm形式的编译

因此dev开发环境来看,vite的速度要相对快一些,因为一个npm包只会重新编译一次,因此dev环境速度影响不大,只是在初始化项目冷启动的时候时间有一些误差,此外snowpack支持Streaming Imports,可以在dev环境直接用托管在cdn上的esm形式的npm包,因此dev环境性能差别不大。

build构建:

在生产环境build的时候,vite是不支持unbundle的,在bundle模式下,vite选择采用的是rollup,通过rollup来打包出线上环境运行的静态文件。vite官方支持且仅支持rollup,这样一定程度上可以保持一致性,但是不容易解耦,从而结合非rollup构建工具来打包。而snowpack默认就是unbundle的,这种unbundle的默认形式,对构建工具就没有要求,对于线上环境,即可以使用rollup,也可以使用webpack,甚至可以选择不打包,直接使用unbundle。

可以用两个表格来总结如上的结论:

dev开发环境:

产品dev环境构建工具
snowpackrollup(或者使用Streaming imports)
viteesbuild

build生产环境:

产品build构建工具
snowpack1.unbundle(esbuild) 2.rollup 3.webpack...
viterollup(且不支持unbundle)

6.3 snowpack支持Streaming Imports

Streaming Imports是一个新特性,他允许用户,不管是生产环境还是开发环境,都不需要在本地使用npm/yarn来维护一个lock文件,从而下载应用中所使用的npm包到本地的node_module目录下。通过使用Streaming Imports,可以维护一个map文件,该map文件中的key是包名,value直接指向托管该npm包esm文件形式的cdn服务器的地址。

6.4 vite的一些优点

vite相对于snowpack有一下几个优点,不过个人以为都不算特别有用的一些优点。

  • 多页面支持,除了根目录的root/index.html外,还支持其他根目录以外的页面,比如nest/index.html

  • 对于css预处理器支持更好(这点个人没发现)

  • 支持css代码的code-splitting

  • 优化了异步请求多个chunk文件(不分场景可以同步进行,从而一定程度下减少请求总时间)

6.5 总结

如果想在生产环境使用unbundle,那么vite是不行的,vite对于线上环境的build,是必须要打包的。vite优化的只是开发环境。而snowpack默认就是unbundle的,因此可以作为前提在生产环境使用unbundle.此外,snowpack的Streaming Imports提供了一套完整的本地map的包管理,不需要将npm包安装到本地,很方便我们在线上和线下使用cdn托管公共库。


作者:yuxiaoliang
来源:https://juejin.cn/post/7034484346874986533

收起阅读 »

重新审视前端模块的调用, 执行和加载之间的关系

在进入正题之前, 让我们先回顾下前端模块从无到有的一个简短历史 如果你有一定的工作经验, 并且经历过 jQuery 那样的年代, 应该了解早期的前端模块, 只是 window 上的一个局部变量. 在最初的时候前端工程师为了分享自己的代码, 往往会通过 wind...
继续阅读 »

在进入正题之前, 让我们先回顾下前端模块从无到有的一个简短历史


如果你有一定的工作经验, 并且经历过 jQuery 那样的年代, 应该了解早期的前端模块, 只是 window 上的一个局部变量.


在最初的时候前端工程师为了分享自己的代码, 往往会通过 window 来建立联系, 这种古老的做法至今还被很多人使用, 因为简单. 例如我们编写了一个脚本, 通常我们并不认为这是个模块, 但我们会习惯于将这个脚本包装成一个对象. 例如


window.myModule = {
getName(name){
return `hello ${name}`
}
}

当其他人加载这个脚本后, 就可以便捷的通过 window.myModule 来调用 getName 方法.


早期的 JavaScript 脚本主要用于开发一些简单的表单和网页的交互功能, 那个年代的前端工程师数量也极少, 通过 window 来实现模块化并没有什么太大的问题.


直到 ajax 的出现, 将 web 逐步推动到了富客户端的阶段, 随着 spa 的兴起, 前端工程师发现使用 window 模块化代码越来越难以维护, 主要原因有 2 个



  1. 大量的模块加载污染了 window, 导致各种命名冲突和意外覆盖, 这些问题还很难定位.

  2. 模块和模块之间的交互越来越多, 为了保证调用顺序, 需要人为保障 script 标签的加载顺序


为了解决这个问题, 类似 require seajs 这样的模块 loader 被创造出来, 通过模块 loader, 大大缓解了上述的两个问题.


但前端技术和互联网发展的速度远超我们的想象, 随着网页越来越像一个真实的客户端, 这对前端的工程能力提出了极大的挑战, 仅靠单纯的脚本开发已经难以满足项目的需要, 于是 gulp 等用于前端工程管理的脚手架开始进入我们的视野, 不过在这个阶段, 模块 loader 和前端工程流之间尚未有机的结合.


直到 nodejs 问世, 前端拥有了自己的包管理工具 npm, 在此基础上 Webpack 进一步推动了前端工程流和模块之间的整合, 随后前端模块化的进程开始稳固下来, 一直保持至今.


从这个历史上去回顾, 前端模块化的整个进程包括 es6 关于 module 的标准都是一直围绕这个一个核心命题存在的.


无论是 require 还是 Webpack 在这个核心命题上并没有区别, 即前端模块遵循


加载 → 调用 → 执行 这样的一个逻辑关系. 因为模块必须先加载才能调用并执行, 模块加载器和构建工具就必须管理和分析应用中所有模块的依赖关系, 从而确定哪些模块可以拆分哪些可以合并, 以及模块的加载顺序.


但是随着时间的推移, 前端应用的模块越来越多, 应用越来越庞大, 我们的本地的 node_modules 几百兆起步, Webpack 虽然做了很多优化, 但是 rebuild 的时间在大型应用面前依然显得很慢.


今年 2 月份, Webpack 5 发布了他们的模块拆解方案, 模块联邦, 这个插件解决了 Webpack 构建的模块无法在多个工程中复用的问题.


早些时间 yarn 2.0 采用共享 node_moudles 的方法来解决本地模块大量冗余导致的性能问题.
包括 nodejs 作者在 deno 中放弃了 npm 改用网络化加载模块的方式等等.


可以看到社区已经意识到了先行的前端模块化机制再次面临瓶颈, 无论是性能还是维护成本都面临诸多挑战, 各个团队都在想办法开辟一个新的方向.


不过这些努力依然没有超越先行模块化机制中的核心命题, 即模块必须先加载, 后调用执行.


只要这个核心命题不变, 模块的依赖问题依然是无解的. 为此我们尝试提出了一种新的思路


模块为什么不能先调用, 后加载执行呢?


如果 A 模块调用 B 模块, 但并不需要 B 模块立即就绪, 这就意味着, 模块加载器可以不关心模块的依赖关系, 而致力于只解决模块加载的效率和性能问题.


同时对于构建工具来说, 如果 A 模块的执行并不基于 B 模块立即就绪这件事, 那么构建工具可以放心的将 A 和 B 模块拆成两个文件, 如果模块有很多, 就可以利用 http2 的并行加载能力, 大大提升模块的加载性能.


在我们的设想中, 一种新的模块加载方式是这样的


// remoteModule.js 这是一个发布到 cdn 的远程模块, 内部代码是这样

widnow.rdeco.create({
name:'remote-module',
exports:{
getName(name, next){
next(`hello ${name}`)
}
}
})


让我们先不加载这个模块, 而是直接先执行调用端的代码例如这样



window.rdeco 可以理解成类似 Webpack runtime 一样的存在, 不过 rdeco 是一个独立的库, 其功能远不止于此



// localModule.js 这个是本地的模块
window.rdeco.inject('remote-module').getName('world').then(fullName=>{
console.log(fullName)
})

然后我们在 html 中先加载 localModule.js 后加载 remoteModule.js


<scirpt src="localModule.js"></script>
<scirpt src="remoteModule.js"></script>


正常理解, localModule.js 加载完之后会试图去调用 remote-module 的 getName 方法, 但此时 remoteModule 尚未加载, 按照先行的模块化机制, 这种调用会抛出异常. 为了避免这个问题


模块构建工具需要分析两个文件的代码, 从而发现 localModule.js 依赖 remoteModule.js, 然后保存这个依赖顺序, 同时通知模块加载器, 为了让代码正常执行, 必须先加载 remoteModule.js.


但如果模块可以先调用后加载, 那么这个复杂的过程就可以完全避免. 目前我们实现了这一机制, 可以看下这个 demo: codesandbox.io/s/tender-ar…


你可试着先点击 Call remote module's getName method 按钮,


此时文案不会变化只是显示 hello, 但代码并不会抛出异常, 然后你再点击 Load remote module 按钮, 开始加载 remoteModule, 等待加载完成, getName 才会真实执行, 此时文案变成了 hello world



作者:掘金泥石流
链接:https://juejin.cn/post/7034412398261993479

收起阅读 »

CSS实现随机不规则圆角头像

 前言 最近真是彻底爱上了 CSS ,我又又又被 CSS 惊艳到了,明明是简单的属性,为啥大佬们稍微一组合,就能形成如此好看的效果啊。本文 给大家带来的是随机不规则圆角头像效果,我们可以把这个效果用于一些人物的展示页面 学习本文章,你可以学到:bor...
继续阅读 »

 前言


最近真是彻底爱上了 CSS ,我又又又被 CSS 惊艳到了,明明是简单的属性,为啥大佬们稍微一组合,就能形成如此好看的效果啊。本文


给大家带来的是随机不规则圆角头像效果,我们可以把这个效果用于一些人物的展示页面


学习本文章,你可以学到:

  • border-radius 实现椭圆效果
  • border-radius 实现不规则圆角头像
  • animation-delay 设置负值
  • 实现随机不规则圆角

📃 预备知识


🎨 border-radius


border-radius 可以设置外边框的圆角。比如我们经常使用的 border-radius: 50% 可以得到一个圆形头像。


radius50.png


border-radius 就只能实现圆形效果吗?当然不是,当使用一个半径是确定圆形,两个半径时则会确定椭圆形。


光说不练假把式,接下来一起试试



  1. 设置 border-radius: 30% 70%,就可以得到椭圆效果


radius3070.png


上面的设置都是针对于四个方向的,也可以只设置一个方向的圆角



  1. 设置 border-top-left-radius: 30% 70%


radius3070top.png


从上图其实可以得出,两个值分别设置水平半径和垂直半径的半径,为了更准确我们验证一下


radiusopa.png


但为啥设置的圆角与 border-radius: 30% 70% 设置有这么大的差距。别急,下面慢慢道来。



  1. 设置 border-radius: 30%/70%,/ 前后的值分别为水平半径和垂直半径



border-radius: 30%/70% 相当于给四个方向都设置 30%/70%,而 border-radius: 30% 70% 是给左上右下设置 30% ,左下右上设置 70%



radius30-70.png



  1. 设置四个方向为四种椭圆角: border-radius: 40% 60% 60% 40% / 60% 30% 70% 40% ,就可以实现简单的不规则圆角效果,小改改的头像是不是看起来舒服了好多。


radiusdisorder.png


💞 animation-delay


animation-delay: 可以定义动画播放的延迟时间。


但如果给 animation-delay 设置负值会发生什么那?



MDN 中指出: 定义一个负值会让动画立即开始。但是动画会从它的动画序列中某位置开始。例如,如果设定值为 -1s ,动画会从它的动画序列的第 1 秒位置处立即开始。



那个,乍看上去,我好像懂了,又好像没懂,咱们还是来自己试一下吧。



  • 创建 div 块,宽高都为 0 ,背景设置为 #000

  • 添加 keyframe 动画,100% 状态宽高都扩展为 1000px


@keyframes extend {
0% {
width: 0;
height: 0;
}
100% {
width: 1000px;
height: 1000px;
}
}


  • div 添加 animationanimation-delay


/* 设置 paused 可以使动画暂停 */
animation: extend 10s linear paused;
animation-delay: -3s;

当我打开浏览器时,浏览器出现 300*300 的黑色块,修改 animation-delay-4s ,浏览器出现 400*400 的黑块。我们使用 linear 匀速作为动画播放函数,10s 后 div 会变为 1000px,设置 -3s 起始为 300px-4s 起始为 400px


这样一对比,我们来把 MDN 的描述翻译一下:
+ animation-delay 设置负值的动画会立即执行
+ 动画起始位置是动画中的一阶段,比如上述案例,定义 10s 的动画,设置 -3s 动画就从 3s 开始执行


🌊 radius 配合 delay 实现


有了上面基础知识的配合,不规则圆角的实现就变得很简单了。


设置 keyframekeyframe 的开始与结束为两种不规则圆角,再使用 :nth-child 进行自然随机设置 animation-delay 的负值延迟时间,就可以得到一组风格各异的不规则圆角效果



自然随机的算法非常有意思,效果开创者为了更好、更自然的随机性,选取序列为 2n+1 3n+2 5n+3 7n+4 11+5 ...




  1. 设置 keyframe 动画


@keyframes morph {
0% {
border-radius: 40% 60% 60% 40% / 60% 30% 70% 40%;
transform: rotate(-5deg);
}
100% {
border-radius: 40% 60%;
transform: rotate(5deg);
}
}


  1. 自然随机设置每个头像的 delay


.avatar:nth-child(n) {
animation-delay: -3.5s;
}
.avatar:nth-child(2n + 1) {
animation-delay: -1s;
}
.avatar:nth-child(3n + 2) {
animation-delay: -2s;
}
.avatar:nth-child(5n + 3) {
animation-delay: -3s;
}
.avatar:nth-child(7n + 5) {
animation-delay: -4s;
}
.avatar:nth-child(11n + 7) {
animation-delay: -5s;
}

当当当当~~~ 效果就实现了! 看着下面这些风格各异的小改改,瞬间心情舒畅了好多。


avater.png


不规则圆角头像的功能实现了,但总感觉缺点什么?如果头像能有点动态效果就更好了。


例如 hover 时,头像圆角会发生变化,用户的体验会更好。


我首先的想法还是在上面的代码基础上面更改,但由于 @keyframe 定义好了终点时的状态,能变化的效果并不多,而且看起来很单调,显得很呆 🤣。


那有没有好的实现方案那?有,最终我找到了张鑫旭大佬的实现方案,大佬还是大佬啊。


🌟 radius 配合 transition 实现


参考博客: “蝉原则”与CSS3随机多背景随机圆角等效果



  1. 按照自然随机给每个头像赋予不同的不规则圆角


/* 举两个例子 */
.list:hover {
border-radius: 95% 70% 100% 80%;
transform: rotate(-2deg);
}
.list:nth-child(2n+1) {
border-radius: 59% 52% 56% 59%;
transform: rotate(-6deg);
}


  1. 设置 hover 时新的不规则圆角


.list:nth-child(2n+1):hover {
border-radius: 51% 67% 56% 64%;
transform: rotate(-4deg);
}

.list:nth-child(3n+2):hover {
border-radius: 69% 64% 53% 70%;
transform: rotate(0deg);
}


  1. list 元素配置 transition


avatar.gif


完成上面的步骤,我们就可以得到更灵动的小改改头像了。



但这种实现方法相比较于 radius 配合 animation-delay 实现具备一定的难点,需要设计多种好看的不规则圆角效果



🛕 源码仓库


传送门: 随机不规则圆角




作者:战场小包
链接:https://juejin.cn/post/7034396555738251301

收起阅读 »

使用 Promise 时的5个常见错误,你占了几个!

Promise 提供了一种优雅的方法来处理 JS 中的异步操作。这也是避免“回调地狱”的解决方案。然而,并没有多少开发人员了解其中的内容。因此,许多人在实践中往往会犯错误。 在本文中,介绍一下使用 promise 时的五个常见错误,希望大家能够避免这些错误。 ...
继续阅读 »

Promise 提供了一种优雅的方法来处理 JS 中的异步操作。这也是避免“回调地狱”的解决方案。然而,并没有多少开发人员了解其中的内容。因此,许多人在实践中往往会犯错误。


在本文中,介绍一下使用 promise 时的五个常见错误,希望大家能够避免这些错误。


1.避免 Promise 地狱


通常,Promise是用来避免回调地狱。但滥用它们也会导致 Promise是地狱。


userLogin('user').then(function(user){
getArticle(user).then(function(articles){
showArticle(articles).then(function(){
//Your code goes here...
});
});
});

在上面的例子中,我们对 userLogingetararticleshowararticle 嵌套了三个promise。这样复杂性将按代码行比例增长,它可能变得不可读。


为了避免这种情况,我们需要解除代码的嵌套,从第一个 then 中返回 getArticle,然后在第二个 then 中处理它。


userLogin('user')
.then(getArticle)
.then(showArticle)
.then(function(){
//Your code goes here...
});

2. 在 Promise 中使用 try/catch


通常情况下,我们使用 try/catch 块来处理错误。然而,不建议在 Promise 对象中使用try/catch


这是因为如果有任何错误,Promise对象会在 catch 内自动处理。


ew Promise((resolve, reject) => {
try {
const data = doThis();
// do something
resolve();
} catch (e) {
reject(e);
}
})
.then(data => console.log(data))
.catch(error => console.log(error));

在上面的例子中,我们在Promise 内使用了 try/catch 块。


但是,Promise本身会在其作用域内捕捉所有的错误(甚至是打字错误),而不需要 try/catch块。它确保在执行过程中抛出的所有异常都被获取并转换为被拒绝的 Promise。


new Promise((resolve, reject) => {
const data = doThis();
// do something
resolve()
})
.then(data => console.log(data))
.catch(error => console.log(error));

**注意:**在 Promise 块中使用 .catch() 块是至关重要的。否则,你的测试案例可能会失败,而且应用程序在生产阶段可能会崩溃。


3. 在 Promise 块内使用异步函数


Async/Await 是一种更高级的语法,用于处理同步代码中的多个Promise。当我们在一个函数声明前使用 async 关键字时,它会返回一个 Promise,我们可以使用 await 关键字来停止代码,直到我们正在等待的Promise解决或拒绝。


但是,当你把一个 Async 函数放在一个 Promise 块里面时,会有一些副作用。


假设我们想在Promise 块中做一个异步操作,所以使用了 async 关键字,但,不巧的是我们的代码抛出了一个错误。


这样,即使使用 catch() 块或在 try/catch 块内等待你的Promise,我们也不能立即处理这个错误。请看下面的例子。


// 此代码无法处理错误
new Promise(async () => {
throw new Error('message');
}).catch(e => console.log(e.message));

(async () => {
try {
await new Promise(async () => {
throw new Error('message');
});
} catch (e) {
console.log(e.message);
}
})();

当我在Promise块内遇到 async 函数时,我试图将 async 逻辑保持在 Promise 块之外,以保持其同步性。10次中有9次都能成功。


然而,在某些情况下,可能需要一个 async 函数。在这种情况下,也别无选择,只能用try/catch 块来手动管理。


new Promise(async (resolve, reject) => {
try {
throw new Error('message');
} catch (error) {
reject(error);
}
}).catch(e => console.log(e.message));


//using async/await
(async () => {
try {
await new Promise(async (resolve, reject) => {
try {
throw new Error('message');
} catch (error) {
reject(error);
}
});
} catch (e) {
console.log(e.message);
}
})();

4.在创建 Promise 后立即执行 Promise 块


至于下面的代码片断,如果我们把代码片断放在调用HTTP请求的地方,它就会被立即执行。


const myPromise = new Promise(resolve => {
// code to make HTTP request
resolve(result);
});

原因是这段代码被包裹在一个Promise构造函数中。然而,有些人可能会认为只有在执行myPromisethen方法之后才被触发。


然而,真相并非如此。相反,当一个Promise被创建时,回调被立即执行。


这意味着在建立 myPromise 之后到达下面一行时,HTTP请求很可能已经在运行,或者至少处于调度状态。


Promises 总是急于执行过程。


但是,如果希望以后再执行 Promises,应该怎么做?如果现在不想发出HTTP请求怎么办?是否有什么神奇的机制内置于 Promises 中,使我们能够做到这一点?


答案就是使用函数。函数是一种耗时的机制。只有当开发者明确地用 () 来调用它们时,它们才会执行。简单地定义一个函数还不能让我们得到什么。所以,让 Promise 变得懒惰的最有效方法是将其包裹在一个函数中!


const createMyPromise = () => new Promise(resolve => {
// HTTP request
resolve(result);
});

对于HTTP请求,Promise 构造函数和回调函数只有在函数被执行时才会被调用。所以现在我们有一个懒惰的Promise,只有在我们需要的时候才会执行。


5. 不一定使用 Promise.all() 方法


如果你已经工作多年,应该已经知道我在说什么了。如果有许多彼此不相关的 Promise,我们可以同时处理它们。


Promise 是并发的,但如你一个一个地等待它们,会太费时间,Promise.all()可以节省很多时间。



记住,Promise.all() 是我们的朋友



const { promisify } = require('util');
const sleep = promisify(setTimeout);

async function f1() {
await sleep(1000);
}

async function f2() {
await sleep(2000);
}

async function f3() {
await sleep(3000);
}


(async () => {
console.time('sequential');
await f1();
await f2();
await f3();
console.timeEnd('sequential');
})();

上述代码的执行时间约为 6 秒。但如果我们用 Promise.all() 代替它,将减少执行时间。


(async () => {
console.time('concurrent');
await Promise.all([f1(), f2(), f3()]);
console.timeEnd('concurrent');
})();

总结


在这篇文章中,我们讨论了使用 Promise 时常犯的五个错误。然而,可能还有很多简单的问题需要仔细解决。



作者:前端小智
链接:https://juejin.cn/post/7034661345148534815

收起阅读 »

没想到吧!这个可可爱爱的游戏居然是用 ECharts 实现的!

前言 echarts是一个很强大的图表库,除了我们常见的图表功能,echarts有一个自定义图形的功能,这个功能可以让我们很简单地在画布上绘制一些非常规的图形,基于此,我们来玩一些花哨的。 下面我们来一步步实现他。 1 在坐标系中画一只会动的小鸟 首先实例化一...
继续阅读 »

前言


echarts是一个很强大的图表库,除了我们常见的图表功能,echarts有一个自定义图形的功能,这个功能可以让我们很简单地在画布上绘制一些非常规的图形,基于此,我们来玩一些花哨的。


下面我们来一步步实现他。


1 在坐标系中画一只会动的小鸟


首先实例化一个echart容器,再从网上找一个像素小鸟的图片,将散点图的散点形状,用自定义图片的方式改为小鸟。


const myChart = echarts.init(document.getElementById('main'));
option = {
series: [
{
name: 'bird',
type: 'scatter',
symbolSize: 50,
symbol: 'image://bird.png',
data: [
[50, 80]
],
animation: false
},
]
};

myChart.setOption(option);

要让小鸟动起来,就需要给一个向右的速度和向下的加速度,并在每一帧的场景中刷新小鸟的位置。而小鸟向上飞的动作,则可以靠角度的旋转来实现,向上飞的触发条件设置为空格事件。


option = {
series: [
{
xAxis: {
show: false,
type: 'value',
min: 0,
max: 200,
},
yAxis: {
show: false,
min: 0,
max: 100
},
name: 'bird',
type: 'scatter',
symbolSize: 50,
symbol: 'image://bird.png',
data: [
[50, 80]
],
animation: false
},
]
};

// 设置速度和加速度
let a = 0.05;
let vh = 0;
let vw = 0.5

timer = setInterval(() => {
// 小鸟位置和仰角调整
vh = vh - a;
option.series[0].data[0][1] += vh;
option.series[0].data[0][0] += vw;
option.series[0].symbolRotate = option.series[0].symbolRotate ? option.series[0].symbolRotate - 5 : 0;

// 坐标系范围调整
option.xAxis.min += vw;
option.xAxis.max += vw;

myChart.setOption(option);
}, 25);

效果如下


GIF1.gif


2 用自定义图形绘制障碍物


echarts自定义系列,渲染逻辑由开发者通过renderItem函数实现。该函数接收两个参数params和api,params包含了当前数据信息和坐标系的信息,api是一些开发者可调用的方法集合,常用的方法有:




  • api.value(...),意思是取出 dataItem 中的数值。例如 api.value(0) 表示取出当前 dataItem 中第一个维度的数值。




  • api.coord(...),意思是进行坐标转换计算。例如 var point = api.coord([api.value(0), api.value(1)]) 表示 dataItem 中的数值转换成坐标系上的点。




  • api.size(...), 可以得到坐标系上一段数值范围对应的长度。




  • api.style(...),可以获取到series.itemStyle 中定义的样式信息。




灵活使用上述api,就可以将用户传入的Data数据转换为自己想要的坐标系上的像素位置。


renderItem函数返回一个echarts中的graphic类,可以多种图形组合成你需要的形状,graphic类型。对于我们游戏中的障碍物只需要使用矩形即可绘制出来,我们使用到下面两个类。




  • type: group, 组合类,可以将多个图形类组合成一个图形,子类放在children中。




  • type: rect, 矩形类,通过定义矩形左上角坐标点,和矩形宽高确定图形。




// 数据项定义为[x坐标,下方水管上侧y坐标, 上方水管下侧y坐标]
data: [
[150, 50, 80],
...
]

renderItem: function (params, api) {
// 获取每个水管主体矩形的起始坐标点
let start1 = api.coord([api.value(0) - 10, api.value(1)]);
let start2 = api.coord([api.value(0) - 10, 100]);
// 获取两个水管头矩形的起始坐标点
let startHead1 = api.coord([api.value(0) - 12, api.value(1)]);
let startHead2 = api.coord([api.value(0) - 12, api.value(2) + 8])
// 水管头矩形的宽高
let headSize = api.size([24, 8])
// 水管头矩形的宽高
let rect = api.size([20, api.value(1)]);
let rect2 = api.size([20, 100 - api.value(2)]);
// 坐标系配置
const common = {
x: params.coordSys.x,
y: params.coordSys.y,
width: params.coordSys.width,
height: params.coordSys.height
}
// 水管形状
const rectShape = echarts.graphic.clipRectByRect(
{
x: start1[0],
y: start1[1],
width: rect[0],
height: rect[1]
},common
);
const rectShape2 = echarts.graphic.clipRectByRect(
{
x: start2[0],
y: start2[1],
width: rect2[0],
height: rect2[1]
},
common
)

// 水管头形状
const rectHeadShape = echarts.graphic.clipRectByRect(
{
x: startHead1[0],
y: startHead1[1],
width: headSize[0],
height: headSize[1]
},common
);

const rectHeadShape2 = echarts.graphic.clipRectByRect(
{
x: startHead2[0],
y: startHead2[1],
width: headSize[0],
height: headSize[1]
},common
);

// 返回一个group类,由四个矩形组成
return {
type: 'group',
children: [{
type: 'rect',
shape: rectShape,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
}, {
type: 'rect',
shape: rectShape2,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
},
{
type: 'rect',
shape: rectHeadShape,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
},
{
type: 'rect',
shape: rectHeadShape2,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
}]
};
},

颜色定义, 我们为了让水管具有光泽使用了echarts的线性渐变色对象。


itemStyle: {
// 渐变色对象
color: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [{
offset: 0, color: '#ddf38c' // 0% 处的颜色
}, {
offset: 1, color: '#587d2a' // 100% 处的颜色
}],
global: false // 缺省为 false
},
borderWidth: 3
},

另外,用一个for循环一次性随机出多个柱子的数据


function initObstacleData() {
// 添加minHeight防止空隙太小
let minHeight = 20;
let start = 150;
obstacleData = [];
for (let index = 0; index < 50; index++) {
const height = Math.random() * 30 + minHeight;
const obstacleStart = Math.random() * (90 - minHeight);
obstacleData.push(
[
start + 50 * index,
obstacleStart,
obstacleStart + height > 100 ? 100 : obstacleStart + height
]
)
}
}

再将背景用游戏图片填充,我们就将整个游戏场景,绘制完成:



3 进行碰撞检测


由于飞行轨迹和障碍物数据都很简单,所以我们可以将碰撞逻辑简化为小鸟图片的正方形中,我们判断右上和右下角是否进入了自定义图形的范围内。


对于特定坐标下的碰撞范围,因为柱子固定每格50坐标值一个,宽度也是固定的,所以,可碰撞的横坐标范围就可以简化为 (x / 50 % 1) < 0.6


在特定范围内,依据Math.floor(x / 50)获取到对应的数据,即可判断出两个边角坐标是否和柱子区域有重叠了。在动画帧中判断,如果重叠了,就停止动画播放,游戏结束。


// centerCoord为散点坐标点
function judgeCollision(centerCoord) {
if (centerCoord[1] < 0 || centerCoord[1] > 100) {
return false;
}
let coordList = [
[centerCoord[0] + 15, centerCoord[1] + 1],
[centerCoord[0] + 15, centerCoord[1] - 1],
]

for (let i = 0; i < 2; i++) {
const coord = coordList[i];
const index = coord[0] / 50;
if (index % 1 < 0.6 && obstacleData[Math.floor(index) - 3]) {
if (obstacleData[Math.floor(index) - 3][1] > coord[1] || obstacleData[Math.floor(index) - 3][2] < coord[1]) {
return false;
}
}
}
return false
}

function initAnimation() {
// 动画设置
timer = setInterval(() => {
// 小鸟速度和仰角调整
vh = vh - a;
option.series[0].data[0][1] += vh;
option.series[0].data[0][0] += vw;
option.series[0].symbolRotate = option.series[0].symbolRotate ? option.series[0].symbolRotate - 5 : 0;

// 坐标系范围调整
option.xAxis.min += vw;
option.xAxis.max += vw;

// 碰撞判断
const result = judgeCollision(option.series[0].data[0])

if(result) { // 产生碰撞后结束动画
endAnimation();
}

myChart.setOption(option);
}, 25);
}

总结


echarts提供了强大的图形绘制自定义能力,要使用好这种能力,一定要理解好数据坐标点和像素坐标点之间的转换逻辑,这是将数据具象到画布上的重要一步。


运用好这个功能,再也不怕产品提出奇奇怪怪的图表需求。


作者:DevUI团队
链接:https://juejin.cn/post/7034290086111871007

收起阅读 »

学会了axios封装,世界都是你的

项目中对axios进行二次封装随着前端技术的发展,网络请求这一块,越来越多的程序猿选择使用axios来实现网络请求。但是单纯的axios插件并不能满足我们日常的使用,因此我们使用时,需要根据项目实际的情况来对axios进行二次封装。接下来就我对axios的二次...
继续阅读 »



项目中对axios进行二次封装

随着前端技术的发展,网络请求这一块,越来越多的程序猿选择使用axios来实现网络请求。但是单纯的axios插件并不能满足我们日常的使用,因此我们使用时,需要根据项目实际的情况来对axios进行二次封装。

接下来就我对axios的二次封装详细的说说,主要包括请求之前、返回响应以及使用等。

「1、请求之前」

一般的接口都会有鉴权认证(token)之类的,因此在接口的请求头里面,我们需要带上token值以通过服务器的鉴权认证。但是如果每次请求的时候再去添加,不仅会大大的加大工作量,而且很容易出错。好在axios提供了拦截器机制,我们在请求的拦截器中可以添加token。

// 请求拦截
axios.interceptors.request.use((config) => {
//....省略代码
config.headers.x_access_token = token
return config
}, function (error) {
return Promise.reject(error)
})

当然请求拦截器中,除了处理添加token以外,还可以进行一些其他的处理,具体的根据实际需求进行处理。


「2、响应之后」

请求接口,并不是每一次请求都会成功。那么当接口请求失败的时候,我们又怎么处理呢?每次请求的时候处理?封装axios统一处理?我想一个稍微追求代码质量的码农,应该都会选择封装axios进行统一处理吧。axios不仅提供了请求的拦截器,其也提供了响应的拦截器。在此处,可以获取到服务器返回的状态码,然后根据状态码进行相对应的操作。

// 响应拦截
axios.interceptors.response.use(function (response) {
if (response.data.code === 401 ) {//用户token失效
  //清空用户信息
  sessionStorage.user = ''
  sessionStorage.token = ''
  window.location.href = '/';//返回登录页
  return Promise.reject(msg)//接口Promise返回错误状态,错误信息msg可有后端返回,也可以我们自己定义一个码--信息的关系。
}
if(response.status!==200||response.data.code!==200){//接口请求失败,具体根据实际情况判断
  message.error(msg);//提示错误信息
  return Promise.reject(msg)//接口Promise返回错误状态
}
return response
}, function (error) {
if (axios.isCancel(error)) {
  requestList.length = 0
  // store.dispatch('changeGlobalState', {loading: false})
  throw new axios.Cancel('cancel request')
} else {
  message.error('网络请求失败,请重试')
}
return Promise.reject(error)
})

当然响应拦截器同请求拦截器一样,还可以进行一些其他的处理,具体的根据实际需求进行处理。


「3、使用axios」

axios使用的时候一般有三种方式:

  • 执行get请求

axios.get('url',{
params:{},//接口参数
}).then(function(res){
console.log(res);//处理成功的函数 相当于success
}).catch(function(error){
console.log(error)//错误处理 相当于error
})
  • 执行post请求

axios.post('url',{
data:xxx//参数
},{
headers:xxxx,//请求头信息
}).then(function(res){
console.log(res);//处理成功的函数 相当于success
}).catch(function(error){
console.log(error)//错误处理 相当于error
})
  • axios API 通过相关配置传递给axios完成请求

axios({
method:'delete',
url:'xxx',
cache:false,
params:{id:123},
headers:xxx,
})
//------------------------------------------//
axios({
method: 'post',
url: '/user/12345',
data: {
  firstName: 'monkey',
  lastName: 'soft'
}
});

直接使用api的方式虽然简单,但是不同请求参数的名字不一样,在实际开发过程中很容易写错或者忽略,容易为开发造成不必要的时间损失。

前面两种方式虽然没有参数不一致的问题,但是使用的时候,太过于麻烦。那么怎么办呢?

前面两种虽然使用过于麻烦,但是仔细观察,是可以发现有一定的相似点,我们便可以基于这些相似点二次封装,形成适合我们使用的一个请求函数。直接上代码:

/*
*url:请求的url
*params:请求的参数
*config:请求时的header信息
*method:请求方法
*/
const request = function ({ url, params, config, method }) {
// 如果是get请求 需要拼接参数
let str = ''
if (method === 'get' && params) {
  Object.keys(params).forEach(item => {
    str += `${item}=${params[item]}&`
  })
}
return new Promise((resolve, reject) => {
  axios[method](str ? (url + '?' + str.substring(0, str.length - 1)) : url, params, Object.assign({}, config)).then(response => {
    resolve(response.data)
  }, err => {
    if (err.Cancel) {
    } else {
      reject(err)
    }
  }).catch(err => {
    reject(err)
  })
})
}

这样我们需要接口请求的时候,直接调用该函数就好了。不管什么方式请求,传参方式都一样。


作者:monkeysoft
来源:https://juejin.cn/post/6847009771606769677

收起阅读 »

大话WEB前端性能优化基本套路

前言前端性能优化这是一个老生常谈的话题,但是还是有很多人没有真正的重视起来,或者说还没有产生这种意识。当用户打开页面,首屏加载速度越慢,流失用户的概率就越大,在体验产品的时候性能和交互对用户的影响是最直接的,推广拉新是一门艺术,用户的留存是一门技术,拉进来留住...
继续阅读 »

前言

前端性能优化这是一个老生常谈的话题,但是还是有很多人没有真正的重视起来,或者说还没有产生这种意识。

当用户打开页面,首屏加载速度越慢,流失用户的概率就越大,在体验产品的时候性能和交互对用户的影响是最直接的,推广拉新是一门艺术,用户的留存是一门技术,拉进来留住用户,产品体验很关键,这里我以 美柚的页面为例子,用实例展开说明前端优化的基本套路(适合新手上车)。

WEB性能优化套路

基础套路1:减少资源体积

css

  • 压缩

  • 响应头GZIP

大话WEB前端性能优化基本套路_前端_02

js

  • 压缩

  • 响应头GZIP

大话WEB前端性能优化基本套路_优化_03html

  • 输出压缩

  • 响应头GZIP

    hhh

大话WEB前端性能优化基本套路_前端_04

图片

  • 压缩

  • 使用Webp格式

大话WEB前端性能优化基本套路_前端_05

cookie

  • 注意cookie体积,合理设置过期时间

基础套路2:控制请求数

js

  • 合并

css

  • 合并

图片

  • 合并

    事实上

大话WEB前端性能优化基本套路_前端_06

  • base64(常用图标:如logo等)

    hhh

大话WEB前端性能优化基本套路_前端_07

接口

  • 数量控制

  • 异步ajax

合理使用缓存机制

  • 浏览器缓存

js编码

  • Require.JS 按需加载

  • 异步加载js

  • lazyload图片

基础套路3:静态资源CDN

请求走CDN

  • html

  • p_w_picpath

  • js

  • css

综合套路

图片地址独立域名

  • 与业务不同域名可以减少请求头里不必要的cookie传输

提高渲染速度

  • js放到页面底部,body标签底部

  • css放到页面顶部,head标签里

代码

  • 代码优化:css/js/html

  • 预加载,如:分页预加载,快滚动到底部的时候以前加载下一页数据

拓展资料

性能辅助工具


看完上面的套路介绍

可能有人会说:我在前端界混了这么多年,这些我都知道,只不过我不想去做
我答: 知道做不到,等于不知道

也可能有人会说:压缩合并等这些操作好繁琐,因为懒,所以不做
我答: 现在前端构建工具都很强大,如:grunt、gulp、webpack,支持各种插件操作,还不知道就说明你OUT了


因为我主要负责后端相关工作,前端并不是我擅长的,但是平时也喜欢关注前端前沿技术,这里以我的视角和开发经验梳理出基本套路。

套路点到为止,具体实施可以通过拓展资料进行深入了解,如有疑义或者补充请留言怼。

感谢你的支持,我会继续努力!~

作者: SFLYQ

来源:https://blog.51cto.com/sflyq/1947541

收起阅读 »

WEB加载动画之彩条起伏动画

介绍 本期将带给大家一个简单的创意加载效果——彩条起伏加载。顾名思义,我们会通过scss来完成,将会制作做7个不同颜色的矩形,按不同的延迟不断的递减然后再反弹,循环往复。寓意是希望各位同学像这个加载动画一样,生活过的多姿多彩。 接下来,我们先来一睹为快吧: ...
继续阅读 »

介绍


本期将带给大家一个简单的创意加载效果——彩条起伏加载。顾名思义,我们会通过scss来完成,将会制作做7个不同颜色的矩形,按不同的延迟不断的递减然后再反弹,循环往复。寓意是希望各位同学像这个加载动画一样,生活过的多姿多彩。


接下来,我们先来一睹为快吧:


VID_20211124_211507.gif


感觉如何,其实这个动画的实现方案有很多,今天就用障眼法去实现它,希望给你打开书写css的新思路。


正文


1.彩条绘制


<div id="app">
<div class="loading">
<span>l</span>
<span>o</span>
<span>a</span>
<span>d</span>
<span>i</span>
<span>n</span>
<span>g</span>
</div>
</div>

结构非常的简单,我们将会在div#app让div.loading居中显示,然后在loading中平分各个距离,渲染不同的颜色。


@import url("https://fonts.googleapis.com/css?family=Baloo+Bhaijaan&display=swap");

#app{
width: 100%;
height: 100vh;
background-color: #fff;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}

.loading{
width: 350px;
height: 120px;
display: flex;
overflow: hidden;
span{
flex:1;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-family: cursive;
font-weight: bold;
text-transform: uppercase;
font-family: "Baloo Bhaijaan", cursive;
color: white;
font-size: 48px;
position: relative;
box-sizing: border-box;
padding-top: 50px;
@for $i from 1 through 7 {
&:nth-child(#{$i}) {
background: linear-gradient(180deg, hsl($i * 20 , 60%, 50%) 0, hsl($i * 20 , 60%, 90%) 100%);
box-shadow: inset 0 15px 30px hsl($i * 20 , 60%, 50%);
text-shadow: 12px 12px 12px hsl($i * 20 , 60%, 30%);
border-left: 1px solid hsl($i * 20 , 60%, 80%);;
border-right: 1px solid hsl($i * 20 , 60%, 60%);;
}
}
}
}

为了,美观我们还引入了谷歌的一个字体,居中显示是在div#app用了弹性布局。


#app{
display: flex;
justify-content: center;
align-items: center;
}

这三句话目的就是完成元素在上下左右居中。


另外,我们用scss的一大好处就是体现了出来,遍历十分的方便,即**@for ifrom1through7这一句就是遍历了七遍,通过i from 1 through 7** 这一句就是遍历了七遍,通过i就可以拿到下标,还可以参与运算,我们的颜色值就是通过他配合hsl色盘(HSL即色相、饱和度、亮度)去完成的。当然,色盘有360度,我们只取一部分形成清新的渐变,如果整个色盘都平分的话这几个色值出入还是太大会感觉很脏。


微信截图_20211124221851.png


我们发现文字被设置了padding-top: 50px,原因就是一会要完成起伏的动画,上面的部分最先消失,我们为了保证这些字母能显示时间更长一些,就往下移了一些距离。


2.起伏动画


一开始我们说过这个要用障眼法去实现,所以我们这里不改变span的高度或者裁切他。


.loading{
span{
//...
&::after{
content: "";
display: block;
box-sizing: border-box;
position: absolute;
height: 100%;
top: 0;
left: -1px;
right: -1px;
background: linear-gradient(180deg, white 0, rgb(249, 249, 249) 100%);
animation: shorten 2.1s infinite ease-out;
}
@for $i from 1 through 7 {
// ...
&::after{
animation-delay: #{ $i * 0.08s};
}
}
}
}
}
@keyframes shorten {
12% { height: 10px; }
}

微信截图_20211124222854.png


看了刚才的scss代码可以发现,我们其实是通过一个绝对定位的伪类去遮挡了他,做了一个障眼法让人感觉他高度改变了,其实不然。


至于动画,就更容易了就只有一句,就是在初期某个阶段让他变化高度到10px,也就是遮挡块变小了,显示的高度就就多了,然后缓缓增大至整块,来完成起伏效果。另外,我依然通过遍历在其伪类中,给他们不同的延迟显得更有层次感。


讲到这里,我们的这个案例就书写完成了


结语


本次通过一个做加载创意动画的案例,向各位同学讲到了css如何弹性居中,scss的遍历,hsl色盘改变色值的方便之处以及障眼法的一种方式,希望大家会喜欢,多多支持哦~


作者:jsmask
链接:https://juejin.cn/post/7034304330878418980
收起阅读 »

学会这招,轻松优化webpack构建性能

webpack webpack 本质上是一个静态资源打包工具,静态资源打包是指 webpack 会将文件及其通过 import 、require 等方式引入的各项资源,处理成一个资源依赖关系图,也称为 chunk ,这些资源包括 js,css,jpg, 等等。...
继续阅读 »

webpack


webpack 本质上是一个静态资源打包工具,静态资源打包是指 webpack 会将文件及其通过 importrequire 等方式引入的各项资源,处理成一个资源依赖关系图,也称为 chunk ,这些资源包括 jscssjpg, 等等。


然后将这个 chunk 内的资源分别进行处理 ,如 less 编译成 csses6 编译成 es5 ,等等。这个处理过程就是打包,最终将这些处理后的文件输出,输出的文件集合便称为 bundle


bundle 分析


学会优化 webpack 构建性能等优化,我们需要先学会如何分析 bundle,通过对产出的分析,才能有针对性的对过程进行优化。


webpack 官方提供了一个非常好用的 bundle 可视化分析工具:webpack-bundle-analyzer。这个工具会将 bundle 处理一个可视化页面,呈现出资源的依赖关系和体积大小等信息。


这个工具的使用方式也很简单,这需要在通过 npm install webpack-bundle-analyzer yarn install webpack-bundle-analyzer 安装这个插件,然后在 webpack 配置文件的 plugins 配置项中加上这一行代码:


plugins: [
new BundleAnalyzerPlugin(),
]
复制代码

运行 webpack 打包后,会自动在 http://127.0.0.1:8888/ 打开一个可视化页面:


image.png


优化小妙招


接下来我们将会结合对 bundle 的分析,进行一些优化操作。


在讲解如何优化之前,我们需要明确 chunkbundle 的关系:chunk 是一组依赖关系的集合,它不单单指一个文件,可以包含一个或多个文件。而 bundlewebpack 打包的输出结果,它可以包含一个或多个 chunk。而 webpack 打包执行时会以一个个 chunk 进行处理,前端在加载 webpack 打包的资源时,也往往是以一个 chunk 为单位加载的(无论它是一个或多个文件)。


splitChunks


从可视化界面中我们可以看到,经过 webpack 打包后我们得到一个 app.bundle.js,这是个 bundle 中包含了我们项目的所有代码以及从 node_modules 中引入的依赖,而这个 bundle 中包含了项目内的所以依赖关系,因此这个 bundle 也是我们项目中唯一一个 chunk


那么我们在加载页面时,便是加载这一整个 chunk,即需要在页面初始时加载全部的代码。


splitChunks,是由 webpack 提供的插件,通过它能够允许我们自由的配置 chunk 的生成策略,当然也包括允许我们将一个巨大的 chunk 拆分成多个 chunk


在使用 splitChunks 之前我们先介绍一个重要的配置属性 cacheGroups(如果需要,可以在官方文档 splitChunks 中了解更多):


cacheGroups 配置提取 chunk 的方案。里面每一项代表一个提取 chunk 的方案。下面是 cacheGroups 每项中特有的选项:

  • test选项:用来匹配要提取的 chunk 的资源路径或名称,值是正则或函数。

  • name选项:生成的 chunk 名称。

  • chunks选项,决定要提取那些内容。

  • priority选项:方案的优先级,值越大表示提取 chunk 时优先采用此方案,默认值为0。

  • enforce选项:true/false。为true时,chunk 的大小和数量限制。
  • 接下来我们便通过实际的配置操作,将 node_modules 的内容提取成单独的 chunk,下面是我们的配置代码:


      optimization: {
    splitChunks: {
    cacheGroups: {
    vendors: {
    test: /[\\/]node_modules[\\/]/,
    name: 'vendors',
    chunks: 'all',
    enforce: true,
    priority: -3,
    },
    },
    },
    },

    配置完成后,重新运行 webpack ,可以看到,node_modules 相关的依赖关系被提取成一个单独的 chunk vendors.bundle.js,最终我们得到了两个 chunkvendors.bundle.jsapp.bundle.js


    image.png


    那么通过这样的chunk 提取,有什么好处呢?



    1. node_modules 下往往是在项目中不会的变化的第三方依赖,我们将这些固定不变的提取成单独的 chunk 处理,webpack 便可以将这个 chunk 进行一定的缓存策略,而不需要每次都做过多的处理,减少了性能消耗。

    2. 网页加载资源时不需要一次性加载太多的资源,可以通过不同 chunk 分批次加载,从而减少首屏加载的时间。


    除了这里介绍的对 node_modules 的处理外,在实际的项目中也可以根据需要对更多的资源采取这样的提取chunk 策略。



    externals + CDN


    通过对 bundle 的分析,我们不难发现:在我们输出的 bundlereact.development.jsreact-dom.development.js 以及 react-router.js 这三个文件特别的显眼。这表示这几个文件的体积在我们总的输出文件中占的比例特别大,那么有什么方法可以解决这些碍眼的家伙呢?


    当然有! 下面将要介绍的 external + CDN 策略,便可以很好的帮助我们做到这点。


    externalwebpack 的一个重要的配置项,顾名思义,它可以帮助我们将某些资源在 webpack 打包时将其剔除,不参与到资源的打包中。


    external 是一个有多项 key-value 组成的对象,它的的每一项属性表示不需要经过 webpack 打包的资源,key 表示的是我们需要排除在外的依赖名称,value 则告诉 webpack,需要从 window 对象的哪个属性获取到这些被排除在外的依赖。


    下面的代码就是将reactreact-domreact-router-dom 三个依赖不进行 webpack 打包的配置,它告诉 webpack,不将 reactreact-domreact-router-dom 打包进最终的输出中,需要用到这些依赖时从 window对象下的 ReactReactDOMReactRouterDOM 属性获取。


      externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
    'react-router-dom': 'ReactRouterDOM',
    },
    复制代码

    那么这些被剔除的依赖,为什么可以从 window 对象获取到呢?答案就是 CDN !


    我们将这些剔除的依赖,通过 script 标签引入对应的 CDN 资源( CDN内容分发网络,我们可以将这些静态资源存储到 CDN 网络中,以便更快的获取资源)。


    这需要我们将引入这些资源的script 标签加在入口 HTML 文件中,这些加载进来的js文件,会将资源挂载在对应的 window 属性 ReactReactDOMReactRouterDOM上。


        <script src="https://cdn.staticfile.org/react/0.0.0-0c756fb-f7f79fd/cjs/react.development.js"></script>
    <script src="https://cdn.staticfile.org/react-dom/0.0.0-0c756fb-f7f79fd/cjs/react-dom.development.js"></script>
    <script src="https://cdn.staticfile.org/react-router-dom/0.0.0-experimental-ffd8c7d0/react-router-dom.development.js"></script>

    接下来看下通过 external + CDN 策略处理后,我们最终输出的bundle:


    image.png


    react.development.jsreact-dom.development.js 以及 react-router.js 这三个文件消失了!



    作者:Promise
    链接:https://juejin.cn/post/7034181462106570759

    收起阅读 »

    前端面试js高频手写大全(下)

    8. 手写call, apply, bind手写callFunction.prototype.myCall=function(context=window){  // 函数的方法,所以写在Fuction原型对象上 if(typeof this !==...
    继续阅读 »



    8. 手写call, apply, bind

    手写call

    Function.prototype.myCall=function(context=window){  // 函数的方法,所以写在Fuction原型对象上
    if(typeof this !=="function"){   // 这里if其实没必要,会自动抛出错误
       throw new Error("不是函数")
    }
    const obj=context||window   //这里可用ES6方法,为参数添加默认值,js严格模式全局作用域this为undefined
    obj.fn=this      //this为调用的上下文,this此处为函数,将这个函数作为obj的方法
    const arg=[...arguments].slice(1)   //第一个为obj所以删除,伪数组转为数组
    res=obj.fn(...arg)
    delete obj.fn   // 不删除会导致context属性越来越多
    return res
    }
    //用法:f.call(obj,arg1)
    function f(a,b){
    console.log(a+b)
    console.log(this.name)
    }
    let obj={
    name:1
    }
    f.myCall(obj,1,2) //否则this指向window

    obj.greet.call({name: 'Spike'}) //打出来的是 Spike

    手写apply(arguments[this, [参数1,参数2.....] ])

    Function.prototype.myApply=function(context){  // 箭头函数从不具有参数对象!!!!!这里不能写成箭头函数
    let obj=context||window
    obj.fn=this
    const arg=arguments[1]||[]    //若有参数,得到的是数组
    let res=obj.fn(...arg)
    delete obj.fn
    return res
    }
    function f(a,b){
    console.log(a,b)
    console.log(this.name)
    }
    let obj={
    name:'张三'
    }
    f.myApply(obj,[1,2])  //arguments[1]

    手写bind

    this.value = 2
    var foo = {
    value: 1
    };
    var bar = function(name, age, school){
    console.log(name) // 'An'
    console.log(age) // 22
    console.log(school) // '家里蹲大学'
    }
    var result = bar.bind(foo, 'An') //预置了部分参数'An'
    result(22, '家里蹲大学') //这个参数会和预置的参数合并到一起放入bar中

    简单版本

    Function.prototype.bind = function(context, ...outerArgs) {
    var fn = this;
    return function(...innerArgs) {   //返回了一个函数,...rest为实际调用时传入的参数
    return fn.apply(context,[...outerArgs, ...innerArgs]); //返回改变了this的函数,
    //参数合并
    }
    }

    new失败的原因:

    例:

    // 声明一个上下文
    let thovino = {
    name: 'thovino'
    }

    // 声明一个构造函数
    let eat = function (food) {
    this.food = food
    console.log(`${this.name} eat ${this.food}`)
    }
    eat.prototype.sayFuncName = function () {
    console.log('func name : eat')
    }

    // bind一下
    let thovinoEat = eat.bind(thovino)
    let instance = new thovinoEat('orange')  //实际上orange放到了thovino里面
    console.log('instance:', instance) // {}

    生成的实例是个空对象

    new操作符执行时,我们的thovinoEat函数可以看作是这样:

    function thovinoEat (...innerArgs) {
    eat.call(thovino, ...outerArgs, ...innerArgs)
    }

    在new操作符进行到第三步的操作thovinoEat.call(obj, ...args)时,这里的obj是new操作符自己创建的那个简单空对象{},但它其实并没有替换掉thovinoEat函数内部的那个上下文对象thovino。这已经超出了call的能力范围,因为这个时候要替换的已经不是thovinoEat函数内部的this指向,而应该是thovino对象。

    换句话说,我们希望的是new操作符将eat内的this指向操作符自己创建的那个空对象。但是实际上指向了thovinonew操作符的第三步动作并没有成功

    可new可继承版本

    Function.prototype.bind = function (context, ...outerArgs) {
    let that = this;

    function res (...innerArgs) {
        if (this instanceof res) {
            // new操作符执行时
            // 这里的this在new操作符第三步操作时,会指向new自身创建的那个简单空对象{}
            that.call(this, ...outerArgs, ...innerArgs)
        } else {
            // 普通bind
            that.call(context, ...outerArgs, ...innerArgs)
        }
        }
        res.prototype = this.prototype //!!!
        return res
    }

    9. 手动实现new

    new的过程文字描述:

    1. 创建一个空对象 obj;

    2. 将空对象的隐式原型(proto)指向构造函数的prototype。

    3. 使用 call 改变 this 的指向

    4. 如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。

    function Person(name,age){
    this.name=name
    this.age=age
    }
    Person.prototype.sayHi=function(){
    console.log('Hi!我是'+this.name)
    }
    let p1=new Person('张三',18)

    ////手动实现new
    function create(){
    let obj={}
    //获取构造函数
    let fn=[].shift.call(arguments)  //将arguments对象提出来转化为数组,arguments并不是数组而是对象   !!!这种方法删除了arguments数组的第一个元素,!!这里的空数组里面填不填元素都没关系,不影响arguments的结果     或者let arg = [].slice.call(arguments,1)
    obj.__proto__=fn.prototype
    let res=fn.apply(obj,arguments)    //改变this指向,为实例添加方法和属性
    //确保返回的是一个对象(万一fn不是构造函数)
    return typeof res==='object'?res:obj
    }

    let p2=create(Person,'李四',19)
    p2.sayHi()

    细节:

    [].shift.call(arguments)  也可写成:
    let arg=[...arguments]
    let fn=arg.shift()  //使得arguments能调用数组方法,第一个参数为构造函数
    obj.__proto__=fn.prototype
    //改变this指向,为实例添加方法和属性
    let res=fn.apply(obj,arg)

    10. 手写promise(常考promise.all, promise.race)

    // Promise/A+ 规范规定的三种状态
    const STATUS = {
    PENDING: 'pending',
    FULFILLED: 'fulfilled',
    REJECTED: 'rejected'
    }

    class MyPromise {
    // 构造函数接收一个执行回调
    constructor(executor) {
        this._status = STATUS.PENDING // Promise初始状态
        this._value = undefined // then回调的值
        this._resolveQueue = [] // resolve时触发的成功队列
        this._rejectQueue = [] // reject时触发的失败队列
       
    // 使用箭头函数固定this(resolve函数在executor中触发,不然找不到this)
    const resolve = value => {
        const run = () => {
            // Promise/A+ 规范规定的Promise状态只能从pending触发,变成fulfilled
            if (this._status === STATUS.PENDING) {
                this._status = STATUS.FULFILLED // 更改状态
                this._value = value // 储存当前值,用于then回调
               
                // 执行resolve回调
                while (this._resolveQueue.length) {
                    const callback = this._resolveQueue.shift()
                    callback(value)
                }
            }
        }
        //把resolve执行回调的操作封装成一个函数,放进setTimeout里,以实现promise异步调用的特性(规范上是微任务,这里是宏任务)
        setTimeout(run)
    }

    // 同 resolve
    const reject = value => {
        const run = () => {
            if (this._status === STATUS.PENDING) {
            this._status = STATUS.REJECTED
            this._value = value
           
            while (this._rejectQueue.length) {
                const callback = this._rejectQueue.shift()
                callback(value)
            }
        }
    }
        setTimeout(run)
    }

        // new Promise()时立即执行executor,并传入resolve和reject
        executor(resolve, reject)
    }

    // then方法,接收一个成功的回调和一个失败的回调
    function then(onFulfilled, onRejected) {
     // 根据规范,如果then的参数不是function,则忽略它, 让值继续往下传递,链式调用继续往下执行
     typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
     typeof onRejected !== 'function' ? onRejected = error => error : null

     // then 返回一个新的promise
     return new MyPromise((resolve, reject) => {
       const resolveFn = value => {
         try {
           const x = onFulfilled(value)
           // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
           x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
           reject(error)
        }
      }
    }
    }

     const rejectFn = error => {
         try {
           const x = onRejected(error)
           x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
           reject(error)
        }
      }

       switch (this._status) {
         case STATUS.PENDING:
           this._resolveQueue.push(resolveFn)
           this._rejectQueue.push(rejectFn)
           break;
         case STATUS.FULFILLED:
           resolveFn(this._value)
           break;
         case STATUS.REJECTED:
           rejectFn(this._value)
           break;
      }
    })
    }
    catch (rejectFn) {
     return this.then(undefined, rejectFn)
    }
    // promise.finally方法
    finally(callback) {
     return this.then(value => MyPromise.resolve(callback()).then(() => value), error => {
       MyPromise.resolve(callback()).then(() => error)
    })
    }

    // 静态resolve方法
    static resolve(value) {
         return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value))
    }

    // 静态reject方法
    static reject(error) {
         return new MyPromise((resolve, reject) => reject(error))
      }

    // 静态all方法
    static all(promiseArr) {
         let count = 0
         let result = []
         return new MyPromise((resolve, reject) =>       {
           if (!promiseArr.length) {
             return resolve(result)
          }
           promiseArr.forEach((p, i) => {
             MyPromise.resolve(p).then(value => {
               count++
               result[i] = value
               if (count === promiseArr.length) {
                 resolve(result)
              }
            }, error => {
               reject(error)
            })
          })
        })
      }

    // 静态race方法
    static race(promiseArr) {
         return new MyPromise((resolve, reject) => {
           promiseArr.forEach(p => {
             MyPromise.resolve(p).then(value => {
               resolve(value)
            }, error => {
               reject(error)
            })
          })
        })
      }
    }

    11. 手写原生AJAX

    步骤

    1. 创建 XMLHttpRequest 实例

    2. 发出 HTTP 请求

    3. 服务器返回 XML 格式的字符串

    4. JS 解析 XML,并更新局部页面

    不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON

    了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。

    version 1.0:

    myButton.addEventListener('click', function () {
    ajax()
    })

    function ajax() {
    let xhr = new XMLHttpRequest() //实例化,以调用方法
    xhr.open('get', 'https://www.google.com') //参数2,url。参数三:异步
    xhr.onreadystatechange = () => { //每当 readyState 属性改变时,就会调用该函数。
      if (xhr.readyState === 4) { //XMLHttpRequest 代理当前所处状态。
        if (xhr.status >= 200 && xhr.status < 300) { //200-300请求成功
          let string = request.responseText
          //JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象
          let object = JSON.parse(string)
        }
      }
    }
    request.send() //用于实际发出 HTTP 请求。不带参数为GET请求
    }

    promise实现

    function ajax(url) {
     const p = new Promise((resolve, reject) => {
       let xhr = new XMLHttpRequest()
       xhr.open('get', url)
       xhr.onreadystatechange = () => {
         if (xhr.readyState == 4) {
           if (xhr.status >= 200 && xhr.status <= 300) {
             resolve(JSON.parse(xhr.responseText))
          } else {
             reject('请求出错')
          }
        }
      }
       xhr.send()  //发送hppt请求
    })
     return p
    }
    let url = '/data.json'
    ajax(url).then(res => console.log(res))
    .catch(reason => console.log(reason))

    12. 手写节流防抖函数

    函数节流与函数防抖都是为了限制函数的执行频次,是一种性能优化的方案,比如应用于window对象的resize、scroll事件,拖拽时的mousemove事件,文字输入、自动完成的keyup事件。

    节流:连续触发事件但是在 n 秒中只执行一次函数

    例:(连续不断动都需要调用时用,设一时间间隔),像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多。

    防抖:指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

    例:(连续不断触发时不调用,触发完后过一段时间调用),像仿百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。

    防抖的实现:

    function debounce(fn, delay) {
        if(typeof fn!=='function') {
           throw new TypeError('fn不是函数')
        }
        let timer; // 维护一个 timer
        return function () {
            var _this = this; // 取debounce执行作用域的this(原函数挂载到的对象)
            var args = arguments;
            if (timer) {
               clearTimeout(timer);
            }
            timer = setTimeout(function () {
               fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args);
            }, delay);
        };
    }

    // 调用
    input1.addEventListener('keyup', debounce(() => {
    console.log(input1.value)
    }), 600)

    节流的实现:

    function throttle(fn, delay) {
     let timer;
     return function () {
       var _this = this;
       var args = arguments;
       if (timer) {
         return;
      }
       timer = setTimeout(function () {
         fn.apply(_this, args); // 这里args接收的是外边返回的函数的参数,不能用arguments
         // fn.apply(_this, arguments); 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。
         timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
      }, delay)
    }
    }

    div1.addEventListener('drag', throttle((e) => {
     console.log(e.offsetX, e.offsetY)
    }, 100))

    13. 手写Promise加载图片

    function getData(url) {
     return new Promise((resolve, reject) => {
       $.ajax({
         url,
         success(data) {
           resolve(data)
        },
         error(err) {
           reject(err)
        }
      })
    })
    }
    const url1 = './data1.json'
    const url2 = './data2.json'
    const url3 = './data3.json'
    getData(url1).then(data1 => {
     console.log(data1)
     return getData(url2)
    }).then(data2 => {
     console.log(data2)
     return getData(url3)
    }).then(data3 =>
     console.log(data3)
    ).catch(err =>
     console.error(err)
    )

    14. 函数实现一秒钟输出一个数

    (!!!这个题这两天字节校招面试被问到了,问var打印的什么,改为let为什么可以?
    有没有其他方法实现?我自己博客里都写了不用let的写法第二种方法,居然给忘了~~~白学了)

    ES6:用let块级作用域的原理实现

    for(let i=0;i<=10;i++){   //用var打印的都是11
    setTimeout(()=>{
       console.log(i);
    },1000*i)
    }

    不用let的写法: 原理是用立即执行函数创造一个块级作用域

    for(var i = 1; i <= 10; i++){
      (function (i) {
           setTimeout(function () {
               console.log(i);
          }, 1000 * i)
      })(i);
    }

    15. 创建10个标签,点击的时候弹出来对应的序号?

    var a
    for(let i=0;i<10;i++){
    a=document.createElement('a')
    a.innerHTML=i+'<br>'
    a.addEventListener('click',function(e){
        console.log(this)  //this为当前点击的<a>
        e.preventDefault()  //如果调用这个方法,默认事件行为将不再触发。
        //例如,在执行这个方法后,如果点击一个链接(a标签),浏览器不会跳转到新的 URL 去了。我们可以用 event.isDefaultPrevented() 来确定这个方法是否(在那个事件对象上)被调用过了。
        alert(i)
    })
    const d=document.querySelector('div')
    d.appendChild(a)  //append向一个已存在的元素追加该元素。
    }

    16. 实现事件订阅发布(eventBus)

    实现EventBus类,有 on off once trigger功能,分别对应绑定事件监听器,解绑,执行一次后解除事件绑定,触发事件监听器。 这个题目面字节和快手都问到了,最近忙,答案会在后续更新

    class EventBus {
       on(eventName, listener) {}
       off(eventName, listener) {}
       once(eventName, listener) {}
       trigger(eventName) {}
    }

    const e = new EventBus();
    // fn1 fn2
    e.on('e1', fn1)
    e.once('e1', fn2)
    e.trigger('e1') // fn1() fn2()
    e.trigger('e1') // fn1()
    e.off('e1', fn1)
    e.trigger('e1') // null

    实现:

          //声明类
         class EventBus {
           constructor() {
             this.eventList = {} //创建对象收集事件
          }
           //发布事件
           $on(eventName, fn) {
             //判断是否发布过事件名称? 添加发布 : 创建并添加发布
             this.eventList[eventName]
               ? this.eventList[eventName].push(fn)
              : (this.eventList[eventName] = [fn])
          }
           //订阅事件
           $emit(eventName) {
             if (!eventName) throw new Error('请传入事件名')
             //获取订阅传参
             const data = [...arguments].slice(1)
             if (this.eventList[eventName]) {
               this.eventList[eventName].forEach((i) => {
                 try {
                   i(...data) //轮询事件
                } catch (e) {
                   console.error(e + 'eventName:' + eventName) //收集执行时的报错
                }
              })
            }
          }
           //执行一次
           $once(eventName, fn) {
             const _this = this
             function onceHandle() {
               fn.apply(null, arguments)
               _this.$off(eventName, onceHandle) //执行成功后取消监听
            }
             this.$on(eventName, onceHandle)
          }
           //取消订阅
           $off(eventName, fn) {
             //不传入参数时取消全部订阅
             if (!arguments.length) {
               return (this.eventList = {})
            }
             //eventName传入的是数组时,取消多个订阅
             if (Array.isArray(eventName)) {
               return eventName.forEach((event) => {
                 this.$off(event, fn)
              })
            }
             //不传入fn时取消事件名下的所有队列
             if (arguments.length === 1 || !fn) {
               this.eventList[eventName] = []
            }
             //取消事件名下的fn
             this.eventList[eventName] = this.eventList[eventName].filter(
              (f) => f !== fn
            )
          }
        }
         const event = new EventBus()

         let b = function (v1, v2, v3) {
           console.log('b', v1, v2, v3)
        }
         let a = function () {
           console.log('a')
        }
         event.$once('test', a)
         event.$on('test', b)
         event.$emit('test', 1, 2, 3, 45, 123)
         event.$off(['test'], b)
         event.$emit('test', 1, 2, 3, 45, 123)

    参考:

    数组扁平化 https://juejin.im/post/5c971ee16fb9a070ce31b64e#heading-3

    函数柯里化 https://juejin.im/post/6844903882208837645

    节流防抖 https://www.jianshu.com/p/c8b...

    事件订阅发布实现 https://heznb.com/archives/js...

    浅拷贝深拷贝 https://segmentfault.com/a/11...

    作者:晚起的虫儿

    来源:https://segmentfault.com/a/1190000038910420

    收起阅读 »

    如何写 CSS 重置(RESET)样式?

    很长一段时间,我都使用Eric Meyer著名的CSS Reset。这是CSS的一个坚实的块,但是在这一点上它有点长。它已经十多年没有更新了,从那时起发生了很多变化! 最近,我一直在使用我自己的自定义CSS重置。它包括我发现的所有小技巧,以改善用户体验和CSS...
    继续阅读 »

    很长一段时间,我都使用Eric Meyer著名的CSS Reset。这是CSS的一个坚实的块,但是在这一点上它有点长。它已经十多年没有更新了,从那时起发生了很多变化!


    最近,我一直在使用我自己的自定义CSS重置。它包括我发现的所有小技巧,以改善用户体验和CSS创作体验。


    像其他CSS重置一样,在设计/化妆品方面,它是不赞成的。您可以将此重置用于任何项目,无论您想要哪种美学。


    在本教程中,我们将介绍我的自定义 CSS 重置。我们将深入研究每个规则,您将了解它的作用以及您可能想要使用它的原因!


    CSS 重置


    事不宜迟,这里是:


    /*
    1. Use a more-intuitive box-sizing model.
    */
    *, *::before, *::after {
    box-sizing: border-box;
    }
    /*
    2. Remove default margin
    */
    * {
    margin: 0;
    }
    /*
    3. Allow percentage-based heights in the application
    */
    html, body {
    height: 100%;
    }
    /*
    Typographic tweaks!
    4. Add accessible line-height
    5. Improve text rendering
    */
    body {
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    }
    /*
    6. Improve media defaults
    */
    img, picture, video, canvas, svg {
    display: block;
    max-width: 100%;
    }
    /*
    7. Remove built-in form typography styles
    */
    input, button, textarea, select {
    font: inherit;
    }
    /*
    8. Avoid text overflows
    */
    p, h1, h2, h3, h4, h5, h6 {
    overflow-wrap: break-word;
    }
    /*
    9. Create a root stacking context
    */
    #root, #__next {
    isolation: isolate;
    }


    它相对较短,但是这个小样式表中包含了很多东西。让我们开始吧!



    从历史上看,CSS重置的主要目标是确保浏览器之间的一致性,并撤消所有默认样式,从而创建一个空白的石板。我的CSS重置并没有真正做这些事情。




    如今,浏览器在布局或间距方面没有巨大的差异。总的来说,浏览器忠实地实现了CSS规范,并且事情的行为符合您的预期。因此,它不再是必要的了。




    我也不认为有必要剥离所有浏览器默认值。例如,我可能确实想要设置标签!我总是可以在各个项目风格中做出不同的设计决策,但我认为剥离常识性默认值是没有意义的。<em>``font-style: italic




    我的CSS重置可能不符合"CSS重置"的经典定义,但我正在采取这种创造性的自由。



    CSS盒子模型


    测验!通过可见的粉红色边框进行测量,假设未应用其他 CSS,则在以下方案中元素的宽度是多少?.box


    <style>
    .parent {
    width: 200px;
    }
    .box {
    width: 100%;
    border: 2px solid hotpink;
    padding: 20px;
    }
    </style>
    <div>
    <div></div>
    </div>

    我们的元素有.因为它的父级是200px宽,所以100%将解析为200px。.box``width: 100%


    但是它在哪里应用200px宽度? 默认情况下,它会将该大小应用于内容框


    如果您不熟悉,"内容框"是框模型中实际保存内容的矩形,位于边框和填充内:


    一个粉红色的盒子,里面有一个绿色的盒子。粉红色代表边框,绿色代表填充。在内部,一个黑色矩形被标记为"内容框"


    该声明会将 的内容框设置为 200px。填充将添加额外的40px(每侧20px)。边框添加最后一个 4px(每侧 2px)。当我们进行数学计算时,可见的粉红色矩形将是244px宽。width: 100%``.box


    当我们尝试将一个 244px 的框塞入一个 200px 宽的父级中时,它会溢出:


    image.png


    这种行为很奇怪,对吧?幸运的是,我们可以通过设置以下规则来更改它:


    *, *::before, *::after {
    box-sizing: border-box;
    }

    应用此规则后,百分比将基于边框进行解析。在上面的示例中,我们的粉红色框将为 200px,内部内容框将缩小到 156px(200px - 40px - 4px)。


    在我看来,这是一个必须的规则。 它使CSS更适合使用。


    我们使用通配符选择器 () 将其应用于所有元素和伪元素。与普遍的看法相反,这对性能来说并不坏*


    我在网上看到了一些建议,可以代替这样做:


    html {
    box-sizing: border-box;
    }
    *, *:before, *:after {
    box-sizing: inherit;
    }


     删除默认间距


    * {
    margin: 0;
    }

    浏览器围绕保证金做出常识性的假设。例如,默认情况下,将包含比段落更多的边距。h1


    这些假设在文字处理文档的上下文中是合理的,但对于现代 Web 应用程序而言,它们可能不准确。


    Margin是一个棘手的魔鬼,而且我经常发现自己希望元素默认情况下没有任何元素。所以我决定全部删除它。🔥


    如果/当我确实想为特定标签添加一些边距时,我可以在我的自定义项目样式中执行此操作。通配符选择器 () 具有极低的特异性,因此很容易覆盖此规则。*


    基于百分比的高度


    html, body {
    height: 100%;
    }

    你有没有试过在CSS中使用基于百分比的高度,却发现它似乎没有效果?


    下面是一个示例:


    image.png


    元素有,但元素根本不会增长!main``height: 100%


    这不起作用,因为在 Flow 布局(CSS 中的主要布局模式)中,并且操作的原则根本不同。元素的宽度是根据其父级计算的,但元素的高度是根据其子元素计算的。height``width


    这是一个复杂的主题,远远超出了本文的范围。我计划写一篇关于它的博客文章,但与此同时,你可以在我的CSS课程中了解它,CSS for JavaScript Developers。


    作为一个快速演示,在这里我们看到,当我们应用此规则时,我们的元素可以增长:main


    image.png


    如果你使用的是像 React 这样的 JS 框架,你可能还希望向这个规则添加第三个选择器:框架使用的根级元素。


    例如,在我的 Next.js 项目中,我按如下方式更新规则:


    html, body, #__next {
    height: 100%;
    }


    为什么不使用vh?


    您可能想知道:为什么要在基于百分比的高度上大惊小怪?为什么不改用该装置呢?vh


    问题是该单元在移动设备上无法正常工作; 将占用超过100%的屏幕空间,因为移动浏览器在浏览器UI来来去去的地方做那件事。vh``100vh


    将来,新的CSS单元将解决这个问题。在此之前,我继续使用基于百分比的高度。



    调整行高


    body {
    line-height: 1.5;
    }

    line-height控制段落中每行文本之间的垂直间距。默认值因浏览器而异,但往往在 1.2 左右。


    此无单位数字是基于字体大小的比率。它的功能就像设备一样。如果为 1.2,则每行将比元素的字体大小大 20%。em``line-height


    问题是:对于那些有阅读障碍的人来说,这些行挤得太紧,使得阅读起来更加困难。WCAG标准规定行高应至少为1.5


    现在,这个数字确实倾向于在标题和其他具有大类型的元素上产生相当大的行:


    image.png


    您可能希望在标题上覆盖此值。我的理解是,WCAG标准适用于"正文"文本,而不是标题。



    使用"计算"实现更智能的线高


    我一直在尝试一种管理行高的替代方法。在这里:


    * {
    line-height: calc(1em + 0.5rem);
    }

    这是一个非常高级的小片段,它超出了这篇博客文章的范围,但这里有一个快速的解释。



    字体平滑,抗锯齿


    body {
    -webkit-font-smoothing: antialiased;
    }

    好吧,所以这个有点争议。


    在 MacOS 电脑上,浏览器将默认使用"子像素抗锯齿"。这是一种旨在通过利用每个像素内的 R/G/B 灯使文本更易于阅读的技术。


    过去,这被视为可访问性的胜利,因为它提高了文本对比度。您可能已经读过一篇流行的博客文章停止"修复"字体平滑,该帖子主张反对切换到"抗锯齿"。


    问题是:那篇文章写于2012年,在高DPI"视网膜"显示时代之前。今天的像素要小得多,肉眼看不见。


    像素 LED 的物理排列也发生了变化。如果你在显微镜下看一台现代显示器,你不会再看到R/G/B线的有序网格了。


    在2018年发布的MacOS Mojave中 ,Apple在整个操作系统中禁用了子像素抗锯齿。我猜他们意识到它在现代硬件上弊大于利。


    令人困惑的是,像Chrome和Safari这样的MacOS浏览器仍然默认使用子像素抗锯齿。我们需要通过设置为 来显式关闭它。-webkit-font-smoothing``antialiased


    区别如下:


    image.png


    MacOS 是唯一使用子像素抗锯齿的操作系统,因此此规则对 Windows、Linux 或移动设备没有影响。如果您使用的是 MacOS 电脑,则可以尝试实时渲染:


    合理的媒体默认值


    img, picture, video, canvas, svg {
    display: block;
    max-width: 100%;
    }

    所以这里有一件奇怪的事情:图像被认为是"内联"元素。这意味着它们应该在段落的中间使用,例如 或 。<em>``<strong>


    这与我大多数时候使用图像的方式不符。通常,我对待图像的方式与处理段落或标题或侧边栏的方式相同;它们是布局元素。


    但是,如果我们尝试在布局中使用内联元素,则会发生奇怪的事情。如果您曾经有过一个神秘的4px间隙,不是边距,填充或边框,那么它可能是浏览器添加的"内联魔术空间"。line-height


    通过默认设置所有图像,我们回避了整个类别的时髦问题。display: block


    我也设置了.这样做是为了防止大图像溢出,如果它们放置在不够宽而无法容纳它们的容器中。max-width: 100%


    大多数块级元素会自动增大/缩小以适应其父元素,但媒体元素是特殊的:它们被称为替换元素,并且它们不遵循相同的规则。<img>


    如果图像的"本机"大小为 800×600,则该元素的宽度也将为 800px,即使我们将其放入 500px 宽的父级中也是如此。<img>


    此规则将防止该图像超出其容器,这对我来说更像是更明智的默认行为。


    继承窗体控件的字体


    input, button, textarea, select {
    font: inherit;
    }

    如果我们想避免这种自动缩放行为,输入的字体大小需要至少为1rem / 16px。以下是解决此问题的一种方法:


    CSS
    input, button, textarea, select {
    font-size: 1rem;
    }

    这解决了自动变焦问题,但它是创可贴。让我们解决根本原因:表单输入不应该有自己的印刷风格!


    CSS
    input, button, textarea, select {
    font: inherit;
    }

    font是一种很少使用的速记,它设置了一堆与字体相关的属性,如 、 和 。通过将其设置为 ,我们指示这些元素与其周围环境中的排版相匹配。font-size``font-weight``font-family``inherit


    只要我们不为正文文本选择令人讨厌的小字体大小,就可以同时解决我们所有的问题。🎉


    自动换行


    CSS
    p, h1, h2, h3, h4, h5, h6 {
    overflow-wrap: break-word;
    }

    在 CSS 中,如果没有足够的空间来容纳一行上的所有字符,文本将自动换行。


    默认情况下,该算法将寻找"软包装"机会;这些是算法可以拆分的字符。在英语中,唯一的软包装机会是空格和连字符,但这因语言而异。


    如果某行没有任何软换行机会,并且它不合适,则会导致文本溢出:


    image.png


    这可能会导致一些令人讨厌的布局问题。在这里,它添加了一个水平滚动条。在其他情况下,它可能会导致文本与其他元素重叠,或滑到图像/视频后面。


    该属性允许我们调整换行算法,并允许它在找不到软换行机会时使用硬换行:overflow-wrap


    image.png


    这两种解决方案都不完美,但至少硬包装不会弄乱布局!


    感谢Sophie Alpert提出类似的规则!她建议将其应用于所有元素,这可能是一个好主意,但不是我个人测试过的东西。


    您也可以尝试添加属性:hyphens


    p {
    overflow-wrap: break-word;
    hyphens: auto;
    }

    hyphens: auto使用连字符(在支持连字符的语言中)来指示硬换行。这也使得硬包装更加普遍。


    如果您有非常窄的文本列,这可能是值得的,但它也可能有点分散注意力。我选择不将其包含在重置中,但值得尝试!


    根堆叠上下文


    #root, #__next {
    isolation: isolate;
    }

    最后一个是可选的。通常只有当你使用像 React 这样的 JS 框架时才需要它。


    正如我们在"到底是什么,z-index??"中看到的那样,该属性允许我们创建新的堆叠上下文,而无需设置 .isolation``z-index


    这是有益的,因为它允许我们保证某些高优先级元素(模式,下拉列表,工具提示)将始终显示在应用程序中的其他元素之上。没有奇怪的堆叠上下文错误,没有z指数军备竞赛。


    您应该调整选择器以匹配您的框架。我们希望选择在其中呈现应用程序的顶级元素。例如,create-react-app 使用 一个 ,因此正确的选择器是 。<div id="root">``#root


    最终成品


    下面再次以精简的复制友好格式进行 CSS 重置:


    /*
    Josh's Custom CSS Reset
    https://www.joshwcomeau.com/css/custom-css-reset/
    */
    *, *::before, *::after {
    box-sizing: border-box;
    }
    * {
    margin: 0;
    }
    html, body {
    height: 100%;
    }
    body {
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    }
    img, picture, video, canvas, svg {
    display: block;
    max-width: 100%;
    }
    input, button, textarea, select {
    font: inherit;
    }
    p, h1, h2, h3, h4, h5, h6 {
    overflow-wrap: break-word;
    }
    #root, #__next {
    isolation: isolate;
    }
    ```
    ```
    /*
    Josh's Custom CSS Reset
    https://www.joshwcomeau.com/css/custom-css-reset/
    */
    *, *::before, *::after {
    box-sizing: border-box;
    }
    * {
    margin: 0;
    }
    html, body {
    height: 100%;
    }
    body {
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    }
    img, picture, video, canvas, svg {
    display: block;
    max-width: 100%;
    }
    input, button, textarea, select {
    font: inherit;
    }
    p, h1, h2, h3, h4, h5, h6 {
    overflow-wrap: break-word;
    }
    #root, #__next {
    isolation: isolate;
    }
    ```
    `




















    作者:非优秀程序员
    链接:https://juejin.cn/post/7034308682825351176

    收起阅读 »

    前端面试js高频手写大全(上)

    在前端面试中,手撕代码显然是不可避免的,并且占很大的一部分比重。编程题主要分为这几种类型:* 算法题* 涉及js原理的题以及ajax请求* 业务场景题: 实现一个具有某种功能的组件* 其他(进阶,对计算机综合知识的考察,考的相对较少):实现订阅发布者模式;分别...
    继续阅读 »



    介绍

    在前端面试中,手撕代码显然是不可避免的,并且占很大的一部分比重。

    一般来说,如果代码写的好,即使理论知识答得不够清楚,也能有大概率通过面试。并且其实很多手写往往背后就考察了你对相关理论的认识。

    编程题主要分为这几种类型:

    * 算法题
    * 涉及js原理的题以及ajax请求
    * 业务场景题: 实现一个具有某种功能的组件
    * 其他(进阶,对计算机综合知识的考察,考的相对较少):实现订阅发布者模式;分别用面向对象编程,面向过程编程,函数式编程实现把大象放进冰箱等等

    其中前两种类型所占比重最大。
    算法题建议养成每天刷一道leetcode的习惯,重点刷数据结构(栈,链表,队列,树),动态规划,DFS,BFS

    本文主要涵盖了第二种类型的各种重点手写。

    建议优先掌握

    • instanceof (考察对原型链的理解)

    • new (对创建对象实例过程的理解)

    • call&apply&bind (对this指向的理解)

    • 手写promise (对异步的理解)

    • 手写原生ajax (对ajax原理和http请求方式的理解,重点是get和post请求的实现)

    • 事件订阅发布 (高频考点)

    • 其他:数组,字符串的api的实现,难度相对较低。只要了解数组,字符串的常用方法的用法,现场就能写出来个大概。(ps:笔者认为数组的reduce方法比较难,这块有余力可以单独看一些,即使面试没让你实现reduce,写其他题时用上它也是很加分的)


    话不多说,直接开始

    1. 手写instanceof

    instanceof作用:

    判断一个实例是否是其父类或者祖先类型的实例。

    instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype查找失败,返回 false

     let myInstanceof = (target,origin) => {
        while(target) {
            if(target.__proto__===origin.prototype) {
               return true
            }
            target = target.__proto__
        }
        return false
    }
    let a = [1,2,3]
    console.log(myInstanceof(a,Array));  // true
    console.log(myInstanceof(a,Object));  // true

    2. 实现数组的map方法

    数组的map() 方法会返回一个新的数组,这个新数组中的每个元素对应原数组中的对应位置元素调用一次提供的函数后的返回值。

    用法:

    const a = [1, 2, 3, 4];
    const b = array1.map(x => x * 2);
    console.log(b);   // Array [2, 4, 6, 8]

    实现前,我们先看一下map方法的参数有哪些
    image.png
    map方法有两个参数,一个是操作数组元素的方法fn,一个是this指向(可选),其中使用fn时可以获取三个参数,实现时记得不要漏掉,这样才算完整实现嘛

    原生实现:

        // 实现
        Array.prototype.myMap = function(fn, thisValue) {
               let res = []
               thisValue = thisValue||[]
               let arr = this
               for(let i=0; i<arr.length; i++) {
                   res.push(fn.call(thisValue, arr[i],i,arr))   // 参数分别为this指向,当前数组项,当前索引,当前数组
              }
               return res
          }
           // 使用
           const a = [1,2,3];
           const b = a.myMap((a,index)=> {
                   return a+1;
              }
          )
           console.log(b)   // 输出 [2, 3, 4]

    3. reduce实现数组的map方法

    利用数组内置的reduce方法实现map方法,考察对reduce原理的掌握

    Array.prototype.myMap = function(fn,thisValue){
        var res = [];
        thisValue = thisValue||[];
        this.reduce(function(pre,cur,index,arr){
            return res.push(fn.call(thisValue,cur,index,arr));
        },[]);
        return res;
    }

    var arr = [2,3,1,5];
    arr.myMap(function(item,index,arr){
    console.log(item,index,arr);
    })

    4. 手写数组的reduce方法

    reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终为一个值,是ES5中新增的又一个数组逐项处理方法

    参数:

    • callback(一个在数组中每一项上调用的函数,接受四个函数:)

      • previousValue(上一次调用回调函数时的返回值,或者初始值)

      • currentValue(当前正在处理的数组元素)

      • currentIndex(当前正在处理的数组元素下标)

      • array(调用reduce()方法的数组)

    • initialValue(可选的初始值。作为第一次调用回调函数时传给previousValue的值)

     function reduce(arr, cb, initialValue){
        var num = initValue == undefined? num = arr[0]: initValue;
        var i = initValue == undefined? 1: 0
        for (i; i< arr.length; i++){
           num = cb(num,arr[i],i)
        }
        return num
    }

    function fn(result, currentValue, index){
        return result + currentValue
    }

    var arr = [2,3,4,5]
    var b = reduce(arr, fn,10)
    var c = reduce(arr, fn)
    console.log(b)   // 24

    5. 数组扁平化

    数组扁平化就是把多维数组转化成一维数组

    1. es6提供的新方法 flat(depth)

    let a = [1,[2,3]]; 
    a.flat(); // [1,2,3]
    a.flat(1); //[1,2,3]

    其实还有一种更简单的办法,无需知道数组的维度,直接将目标数组变成1维数组。 depth的值设置为Infinity。

    let a = [1,[2,3,[4,[5]]]]; 
    a.flat(Infinity); // [1,2,3,4,5] a是4维数组

    2. 利用cancat

    function flatten(arr) {
        var res = [];
        for (let i = 0, length = arr.length; i < length; i++) {
        if (Array.isArray(arr[i])) {
        res = res.concat(flatten(arr[i])); //concat 并不会改变原数组
        //res.push(...flatten(arr[i])); //或者用扩展运算符
        } else {
            res.push(arr[i]);
          }
        }
        return res;
    }
    let arr1 = [1, 2,[3,1],[2,3,4,[2,3,4]]]
    flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

    补充:指定deep的flat

    只需每次递归时将当前deep-1,若大于0,则可以继续展开

         function flat(arr, deep) {
           let res = []
           for(let i in arr) {
               if(Array.isArray(arr[i])&&deep) {
                   res = res.concat(flat(arr[i],deep-1))
              } else {
                   res.push(arr[i])
              }
          }
           return res
      }
       console.log(flat([12,[1,2,3],3,[2,4,[4,[3,4],2]]],1));

    6. 函数柯里化

    用的这里的方法 https://juejin.im/post/684490...

    柯里化的定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数。

    当柯里化函数接收到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?

    有两种思路:

    1. 通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数

    2. 在调用柯里化工具函数时,手动指定所需的参数个数

    将这两点结合一下,实现一个简单 curry 函数:

    /**
    * 将函数柯里化
    * @param fn   待柯里化的原函数
    * @param len   所需的参数个数,默认为原函数的形参个数
    */
    function curry(fn,len = fn.length) {
    return _curry.call(this,fn,len)
    }

    /**
    * 中转函数
    * @param fn   待柯里化的原函数
    * @param len   所需的参数个数
    * @param args 已接收的参数列表
    */
    function _curry(fn,len,...args) {
       return function (...params) {
            let _args = [...args,...params];
            if(_args.length >= len){
                return fn.apply(this,_args);
            }else{
             return _curry.call(this,fn,len,..._args)
            }
      }
    }

    我们来验证一下:

    let _fn = curry(function(a,b,c,d,e){
    console.log(a,b,c,d,e)
    });

    _fn(1,2,3,4,5);     // print: 1,2,3,4,5
    _fn(1)(2)(3,4,5);   // print: 1,2,3,4,5
    _fn(1,2)(3,4)(5);   // print: 1,2,3,4,5
    _fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5

    我们常用的工具库 lodash 也提供了 curry 方法,并且增加了非常好玩的 placeholder 功能,通过占位符的方式来改变传入参数的顺序。

    比如说,我们传入一个占位符,本次调用传递的参数略过占位符, 占位符所在的位置由下次调用的参数来填充,比如这样:

    直接看一下官网的例子:

    image.png

    接下来我们来思考,如何实现占位符的功能。

    对于 lodash 的 curry 函数来说,curry 函数挂载在 lodash 对象上,所以将 lodash 对象当做默认占位符来使用。

    而我们的自己实现的 curry 函数,本身并没有挂载在任何对象上,所以将 curry 函数当做默认占位符

    使用占位符,目的是改变参数传递的顺序,所以在 curry 函数实现中,每次需要记录是否使用了占位符,并且记录占位符所代表的参数位置。

    直接上代码:

    /**
    * @param fn           待柯里化的函数
    * @param length       需要的参数个数,默认为函数的形参个数
    * @param holder       占位符,默认当前柯里化函数
    * @return {Function}   柯里化后的函数
    */
    function curry(fn,length = fn.length,holder = curry){
    return _curry.call(this,fn,length,holder,[],[])
    }
    /**
    * 中转函数
    * @param fn           柯里化的原函数
    * @param length       原函数需要的参数个数
    * @param holder       接收的占位符
    * @param args         已接收的参数列表
    * @param holders       已接收的占位符位置列表
    * @return {Function}   继续柯里化的函数 或 最终结果
    */
    function _curry(fn,length,holder,args,holders){
    return function(..._args){
    //将参数复制一份,避免多次操作同一函数导致参数混乱
    let params = args.slice();
    //将占位符位置列表复制一份,新增加的占位符增加至此
    let _holders = holders.slice();
    //循环入参,追加参数 或 替换占位符
    _args.forEach((arg,i)=>{
    //真实参数 之前存在占位符 将占位符替换为真实参数
    if (arg !== holder && holders.length) {
        let index = holders.shift();
        _holders.splice(_holders.indexOf(index),1);
        params[index] = arg;
    }
    //真实参数 之前不存在占位符 将参数追加到参数列表中
    else if(arg !== holder && !holders.length){
        params.push(arg);
    }
    //传入的是占位符,之前不存在占位符 记录占位符的位置
    else if(arg === holder && !holders.length){
        params.push(arg);
        _holders.push(params.length - 1);
    }
    //传入的是占位符,之前存在占位符 删除原占位符位置
    else if(arg === holder && holders.length){
       holders.shift();
    }
    });
    // params 中前 length 条记录中不包含占位符,执行函数
    if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){
    return fn.apply(this,params);
    }else{
    return _curry.call(this,fn,length,holder,params,_holders)
    }
    }
    }

    验证一下:;

    let fn = function(a, b, c, d, e) {
    console.log([a, b, c, d, e]);
    }

    let _ = {}; // 定义占位符
    let _fn = curry(fn,5,_); // 将函数柯里化,指定所需的参数个数,指定所需的占位符

    _fn(1, 2, 3, 4, 5);                 // print: 1,2,3,4,5
    _fn(_, 2, 3, 4, 5)(1);             // print: 1,2,3,4,5
    _fn(1, _, 3, 4, 5)(2);             // print: 1,2,3,4,5
    _fn(1, _, 3)(_, 4,_)(2)(5);         // print: 1,2,3,4,5
    _fn(1, _, _, 4)(_, 3)(2)(5);       // print: 1,2,3,4,5
    _fn(_, 2)(_, _, 4)(1)(3)(5);       // print: 1,2,3,4,5

    至此,我们已经完整实现了一个 curry 函数~~

    7. 浅拷贝和深拷贝的实现

    深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。

    浅拷贝和深拷贝的区别:

    浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,如果其中一个对象改变了引用类型的属性,就会影响到另一个对象。

    深拷贝:将一个对象从内存中完整的复制一份出来,从堆内存中开辟一个新区域存放。这样更改拷贝值就不影响旧的对象

    浅拷贝实现:

    方法一:

    function shallowCopy(target, origin){
       for(let item in origin) target[item] = origin[item];
       return target;
    }

    其他方法(内置api):

    1. Object.assign

    var obj={a:1,b:[1,2,3],c:function(){console.log('i am c')}}
    var tar={};
    Object.assign(tar,obj);

    当然这个方法只适合于对象类型,如果是数组可以使用slice和concat方法

    1. Array.prototype.slice

    var arr=[1,2,[3,4]];
    var newArr=arr.slice(0);
    1. Array.prototype.concat

    var arr=[1,2,[3,4]];
    var newArr=arr.concat();

    测试同上(assign用对象测试、slice concat用数组测试),结合浅拷贝深拷贝的概念来理解效果更佳

    深拷贝实现:

    方法一:

    转为json格式再解析
    const a = JSON.parse(JSON.stringify(b))

    方法二:

    // 实现深拷贝  递归
    function deepCopy(newObj,oldObj){
        for(var k in oldObj){
            let item=oldObj[k]
            // 判断是数组、对象、简单类型?
            if(item instanceof Array){
                newObj[k]=[]
                deepCopy(newObj[k],item)
            }else if(item instanceof Object){
                newObj[k]={}
                deepCopy(newObj[k],item)
            }else{  //简单数据类型,直接赋值
                newObj[k]=item
            }
        }
    }
    (未完待续……)

    作者:晚起的虫儿

    来源:https://segmentfault.com/a/1190000038910420







    收起阅读 »

    太震撼了!我把七大JS排序算法做成了可视化!!!太好玩了!

    前言大家好,我是林三心。写这篇文章是有原因的,偶然我看到了一个Java的50种排序算法的可视化的视频,但是此视频却没给出具体的实现教程,于是我心里就想着,我可以用JavaScript + canvas去实现这个酷炫的效果。每种排序算法的动画效果基本都不一样哦。...
    继续阅读 »

    前言

    大家好,我是林三心。写这篇文章是有原因的,偶然我看到了一个Java的50种排序算法的可视化的视频,但是此视频却没给出具体的实现教程,于是我心里就想着,我可以用JavaScript + canvas去实现这个酷炫的效果。每种排序算法的动画效果基本都不一样哦。例如冒泡排序是这样的


    冒泡排序2.gif

    实现思路

    想实现的效果

    从封面可以看到,无论是哪种算法,一开始都是第一张图,而最终目的是要变成第二张图的效果


    截屏2021-09-05 下午6.05.45.png

    截屏2021-09-05 下午6.06.03.png

    极坐标

    讲实现思路之前,我先给大家复习一下高中的一个知识——极坐标。哈哈,不知道还有几个人记得他呢?



    • O:极点,也就是原点
    • ρ:极径
    • θ:极径与X轴夹角
    • x = ρ * cosθ,因为x / ρ = cosθ
    • y = ρ * sinθ,因为y / ρ = sinθ

    截屏2021-09-05 下午6.26.31.png

    那我们想实现的结果,又跟极坐标有何关系呢?其实是有关系的,比如我现在有一个排序好的数组,他具有37个元素,那我们可以把这37个元素转化为极坐标中的37个点,怎么转呢?


    const arr = [
    0, 1, 2, 3, 4, 5, 6, 7, 8,
    9, 10, 11, 12, 13, 14, 15, 16, 17,
    18, 19, 20, 21, 22, 23, 24, 25, 26,
    27, 28, 29, 30, 31, 32, 33, 34, 35, 36
    ]

    我们可以这么转:



    • 元素对应的索引index * 10 -> 角度θ(为什么要乘10呢,因为要凑够360°嘛)
    • 元素对应的值arr[index] -> 极径ρ

    按照上面的规则来转的话,那我们就可以在极坐标上得到这37个点(在canvas中Y轴是由上往下的,下面这个图也是按canvas的,但是Y轴我还是画成正常方向,所以这个图其实是反的,但是是有原因的哈):


    (0 -> θ = 00°,ρ = 0) (1 -> θ = 10°,ρ = 1) (2 -> θ = 20°,ρ = 2) (3 -> θ = 30°,ρ = 3)
    (4 -> θ = 40°,ρ = 4) (5 -> θ = 50°,ρ = 5) (6 -> θ = 60°,ρ = 6) (7 -> θ = 70°,ρ = 7)
    (8 -> θ = 80°,ρ = 8) (9 -> θ = 90°,ρ = 9) (10 -> θ = 100°,ρ = 10) (11 -> θ = 110°,ρ = 11)
    (12 -> θ = 120°,ρ = 12) (13 -> θ = 130°,ρ = 13) (14 -> θ = 140°,ρ = 14) (15 -> θ = 150°,ρ = 15)
    (16 -> θ = 160°,ρ = 16) (17 -> θ = 170°,ρ = 17) (18 -> θ = 180°,ρ = 18) (19 -> θ = 190°,ρ = 19)
    (20 -> θ = 200°,ρ = 20) (21 -> θ = 210°,ρ = 21) (22 -> θ = 220°,ρ = 22) (23 -> θ = 230°,ρ = 23)
    (24 -> θ = 240°,ρ = 24) (25 -> θ = 250°,ρ = 25) (26 -> θ = 260°,ρ = 26) (27 -> θ = 270°,ρ = 27)
    (28 -> θ = 280°,ρ = 28) (29 -> θ = 290°,ρ = 29) (30 -> θ = 300°,ρ = 30) (31 -> θ = 310°,ρ = 31)
    (32 -> θ = 320°,ρ = 32) (33 -> θ = 330°,ρ = 33) (34 -> θ = 340°,ρ = 34) (35 -> θ = 350°,ρ = 35)
    (36 -> θ = 360°,ρ = 36)

    截屏2021-09-05 下午7.11.07.png

    有没有发现,跟咱们想实现的最终效果的轨迹很像呢?


    截屏2021-09-05 下午6.06.03.png

    随机打散

    那说完最终的效果,咱们来下想想如何一开始先把数组的各个元素打散在极坐标上呢?其实很简单,咱们可以先把生成一个乱序的数组,比如


    const arr = [
    25, 8, 32, 1, 19, 14, 0, 29, 17,
    6, 7, 26, 3, 30, 31, 16, 28, 15,
    24, 10, 21, 2, 9, 4, 35, 5, 36,
    33, 11, 27, 34, 22, 13, 18, 23, 12, 20
    ]

    然后还是用上面那个规则,去转换极坐标



    • 元素对应的索引index * 10 -> 角度θ(为什么要乘10呢,因为要凑够360°嘛)
    • 元素对应的值arr[index] -> 极径ρ

    那么我们可以的到这37个点,自然就可以实现打散的效果


    (25 -> θ = 00°,ρ = 25) (8 -> θ = 10°,ρ = 8) (32 -> θ = 20°,ρ = 32) (1 -> θ = 30°,ρ = 1)
    (19 -> θ = 40°,ρ = 19) (14 -> θ = 50°,ρ = 14) (0 -> θ = 60°,ρ = 0) (29 -> θ = 70°,ρ = 29)
    (17 -> θ = 80°,ρ = 17) (6 -> θ = 90°,ρ = 6) (7 -> θ = 100°,ρ = 7) (26 -> θ = 110°,ρ = 26)
    (3 -> θ = 120°,ρ = 3) (30 -> θ = 130°,ρ = 30) (31 -> θ = 140°,ρ = 31) (16 -> θ = 150°,ρ = 16)
    (28 -> θ = 160°,ρ = 28) (15 -> θ = 170°,ρ = 15) (24 -> θ = 180°,ρ = 24) (10 -> θ = 190°,ρ = 10)
    (21 -> θ = 200°,ρ = 21) (2 -> θ = 210°,ρ = 2) (9 -> θ = 220°,ρ = 9) (4 -> θ = 230°,ρ = 4)
    (35 -> θ = 240°,ρ = 35) (5 -> θ = 250°,ρ = 5) (36 -> θ = 260°,ρ = 36) (33 -> θ = 270°,ρ = 33)
    (11 -> θ = 280°,ρ = 11) (27 -> θ = 290°,ρ = 27) (34 -> θ = 300°,ρ = 34) (22 -> θ = 310°,ρ = 22)
    (13 -> θ = 320°,ρ = 13) (18 -> θ = 330°,ρ = 18) (23 -> θ = 340°,ρ = 23) (12 -> θ = 350°,ρ = 12)
    (20 -> θ = 360°,ρ = 20)

    截屏2021-09-05 下午7.32.17.png

    实现效果

    综上所述,咱们想实现效果,也就有了思路



    • 1、先生成一个乱序数组
    • 2、用canvas画布画出此乱序数组所有元素对应的极坐标对应的点
    • 3、对乱序数组进行排序
    • 4、排序过程中不断清空画布,并重画数组所有元素对应的极坐标对应的点
    • 5、直到排序完成,终止画布操作

    截屏2021-09-05 下午7.41.54.png

    开搞!!!

    咱们,做事情一定要有条有理才行,还记得上面说的步骤吗?



    • 1、先生成一个乱序数组
    • 2、用canvas画布画出此乱序数组所有元素对应的极坐标对应的点
    • 3、对乱序数组进行排序
    • 4、排序过程中不断清空画布,并重画数组所有元素对应的极坐标对应的点
    • 5、直到排序完成,终止画布操作

    咱们就按照这个步骤,来一步一步实现效果,兄弟们,冲啊!!!


    生成乱序数组

    咱们上面举的例子是37个元素,但是37个肯定是太少了,咱们搞多点吧,我搞了这么一个数组nums:我先生成一个0 - 179的有序数组,然后打乱,并塞进数组nums中,此操作我执行4次。为什么是0 - 179,因为0 - 179刚好有180个数字


    身位一个程序员,我肯定不可能自己手打这么多元素的啦。。来。。上代码


    let nums = []
    for (let i = 0; i < 4; i++) {
    // 生成一个 0 - 179的有序数组
    const arr = [...Array(180).keys()] // Array.keys()可以学一下,很有用
    const res = []
    while (arr.length) {
    // 打乱
    const randomIndex = Math.random() * arr.length - 1
    res.push(arr.splice(randomIndex, 1)[0])
    }
    nums = [...nums, ...res]
    }

    经过上面操作,也就是我的nums中拥有4 * 180 = 720个元素,nums中的元素都是0 - 179范围内的


    canvas画乱序数组

    画canvas之前,肯定要现在html页面上,编写一个canvas的节点,这里我宽度设置1000,高度也是1000,并且背景颜色是黑色


    <canvas id="canvas" width="1000" height="1000" style="background: #000;"></canvas>

    上面看到了,极点(原点)是在坐标正中间的,但是canvas的初始原点是在画布的左上角,我们需要把canvas的原点移动到画布的正中间,那正中间的坐标是多少呢?还记得咱们宽高都是1000吗?那画布中心点坐标不就是(500, 500),咱们可以使用canvas的ctx.translate(500, 500)来移动中心点位置。因为咱们画的点都是白色的,所以咱们顺便把ctx.fillStyle设置为white



    有一点注意了哈,canvas里的Y轴是自上向下的,与常规的Y轴的相反的。



    截屏2021-09-05 下午8.55.39.png

    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    ctx.fillStyle = 'white' // 设置画画的颜色
    ctx.translate(500, 500) // 移动中心点到(500, 500)

    那到底该怎么画点呢?按照之前的,其实光计算出角度θ极径ρ是不够的,因为canvas画板不认这两个东西啊。。那canvas认啥呢,他只认(x, y),所以咱们只要通过角度θ极径ρ去算出(x, y),就好了,还记得前面极坐标的公式吗



    • x = ρ * cosθ,因为x / ρ = cosθ
    • y = ρ * sinθ,因为y / ρ = sinθ

    由于咱们是要铺散点是要铺出一个圆形来,那么一个圆形的角度是0° - 360°,但是我们不要360°,咱们只要0° - 359°,因为0°和360°是同一个直线。咱们一个直线上有一个度数就够了。所以咱们要求出0° - 359°每个角度所对应的cosθ和sinθ(这里咱们只算整数角度,不算小数角度)


    const CosandSin = []
    for (let i = 0; i < 360; i++) {
    const jiaodu = i / 180 * Math.PI
    CosandSin.push({ cos: Math.cos(jiaodu), sin: Math.sin(jiaodu) })
    }

    这时候又有新问题了,咱们一个圆上的整数角度只有0° - 359°360个整数角,但是nums中有720个元素啊,那怎么分配画布呢?很简单啊,一个角度上画2个元素,那不就刚好 2 * 360 = 720


    行,咱们废话不多说,开始画初始散点吧。咱们也知道咱们需要画720个点,对于这种多个相同的东西,咱们要多多使用面向对象这种编程思想


    // 单个长方形构造函数
    function Rect(x, y, width, height) {
    this.x = x // 坐标x
    this.y = y // 坐标y
    this.width = width // 长方形的宽
    this.height = height // 长方形的高
    }

    // 单个长方形的渲染函数
    Rect.prototype.draw = function () {
    ctx.beginPath() // 开始画一个
    ctx.fillRect(this.x, this.y, this.width, this.height) // 画一个
    ctx.closePath() // 结束画一个
    }

    const CosandSin = []
    for (let i = 0; i < 360; i++) {
    const jiaodu = i / 180 * Math.PI
    CosandSin.push({ cos: Math.cos(jiaodu), sin: Math.sin(jiaodu) })
    }

    function drawAll(arr) {
    const rects = [] // 用来存储720个长方形
    for (let i = 0; i < arr.length; i++) {
    const num = arr[i]
    const { cos, sin } = CosandSin[Math.floor(i / 2)] // 一个角画两个
    const x = num * cos // x = ρ * cosθ
    const y = num * sin // y = ρ * sinθ
    rects.push(new Rect(x, y, 5, 3)) // 收集所有长方形
    }
    rects.forEach(rect => rect.draw()) // 遍历渲染
    }
    drawAll(nums) // 执行渲染函数

    来页面中看看效果吧。此时就完成了初始的散点渲染


    截屏2021-09-05 下午6.05.45.png

    边排序边重画

    其实很简单,就是排序一次,就清空画布,然后重新执行上面的渲染函数drawAll就行了。由于性能原因,我先把drawAll封装成一个Promise函数


    function drawAll(arr) {
    return new Promise((resolve) => {
    setTimeout(() => {
    ctx.clearRect(-500, -500, 1000, 1000) // 清空画布
    const rects = [] // 用来存储720个长方形
    for (let i = 0; i < arr.length; i++) {
    const num = arr[i]
    const { cos, sin } = CosandSin[Math.floor(i / 2)] // 一个角画两个
    const x = num * cos // x = ρ * cosθ
    const y = num * sin // y = ρ * sinθ
    rects.push(new Rect(x, y, 5, 3)) // 收集所有长方形
    }
    rects.forEach(rect => rect.draw()) // 遍历渲染
    resolve('draw success')
    }, 10)
    })
    }

    然后咱们拿一个排序算法例子来讲一讲,就拿个冒泡排序来讲吧


    async function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len; i++) {
    for (var j = 0; j < len - 1 - i; j++) {
    if (arr[j] > arr[j + 1]) { //相邻元素两两对比
    var temp = arr[j + 1]; //元素交换
    arr[j + 1] = arr[j];
    arr[j] = temp;
    }
    }
    await drawAll(arr) // 一边排序一边重新画
    }
    return arr;
    }

    然后在页面里放一个按钮,用来执行开始排序


    <button id="btn">开始排序</button>

    document.getElementById('btn').onclick = function () {
    bubbleSort(nums)
    }

    效果如下,是不是很开心哈哈哈!!!


    冒泡排序gift.gif

    完整代码

    这是完整代码


    <canvas id="canvas" width="1000" height="1000" style="background: #000;"></canvas>
    <button id="btn">开始排序</button>
    复制代码
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    ctx.fillStyle = 'white' // 设置画画的颜色
    ctx.translate(500, 500) // 移动中心点到(500, 500)

    let nums = []
    for (let i = 0; i < 4; i++) {
    // 生成一个 0 - 180的有序数组
    const arr = [...Array(180).keys()]
    const res = []
    while (arr.length) {
    // 打乱
    const randomIndex = Math.random() * arr.length - 1
    res.push(arr.splice(randomIndex, 1)[0])
    }
    nums = [...nums, ...res]
    }

    // 单个长方形构造函数
    function Rect(x, y, width, height) {
    this.x = x // 坐标x
    this.y = y // 坐标y
    this.width = width // 长方形的宽
    this.height = height // 长方形的高
    }

    // 单个长方形的渲染函数
    Rect.prototype.draw = function () {
    ctx.beginPath() // 开始画一个
    ctx.fillRect(this.x, this.y, this.width, this.height) // 画一个
    ctx.closePath() // 结束画一个
    }

    const CosandSin = []
    for (let i = 0; i < 360; i++) {
    const jiaodu = i / 180 * Math.PI
    CosandSin.push({ cos: Math.cos(jiaodu), sin: Math.sin(jiaodu) })
    }

    function drawAll(arr) {
    return new Promise((resolve) => {
    setTimeout(() => {
    ctx.clearRect(-500, -500, 1000, 1000) // 清空画布
    const rects = [] // 用来存储720个长方形
    for (let i = 0; i < arr.length; i++) {
    const num = arr[i]
    const { cos, sin } = CosandSin[Math.floor(i / 2)] // 一个角画两个
    const x = num * cos // x = ρ * cosθ
    const y = num * sin // y = ρ * sinθ
    rects.push(new Rect(x, y, 5, 3)) // 收集所有长方形
    }
    rects.forEach(rect => rect.draw()) // 遍历渲染
    resolve('draw success')
    }, 10)
    })
    }
    drawAll(nums) // 执行渲染函数

    async function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len; i++) {
    for (var j = 0; j < len - 1 - i; j++) {
    if (arr[j] > arr[j + 1]) { //相邻元素两两对比
    var temp = arr[j + 1]; //元素交换
    arr[j + 1] = arr[j];
    arr[j] = temp;
    }
    }
    await drawAll(arr) // 一边排序一边重新画
    }
    return arr;
    }

    document.getElementById('btn').onclick = function () {
    bubbleSort(nums) // 点击执行
    }

    正片开始!!!

    首先说明,哈哈



    • 我是算法渣渣
    • 每种算法排序,动画都不一样
    • drawAll放在不同地方也可能有不同效果

    冒泡排序

    async function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len; i++) {
    for (var j = 0; j < len - 1 - i; j++) {
    if (arr[j] > arr[j + 1]) { //相邻元素两两对比
    var temp = arr[j + 1]; //元素交换
    arr[j + 1] = arr[j];
    arr[j] = temp;
    }
    }
    await drawAll(arr) // 一边排序一边重新画
    }
    return arr;
    }

    document.getElementById('btn').onclick = function () {
    bubbleSort(nums) // 点击执行
    }

    冒泡排序gift.gif

    选择排序

    async function selectionSort(arr) {
    var len = arr.length;
    var minIndex, temp;
    for (var i = 0; i < len - 1; i++) {
    minIndex = i;
    for (var j = i + 1; j < len; j++) {
    if (arr[j] < arr[minIndex]) { //寻找最小的数
    minIndex = j; //将最小数的索引保存
    }
    }
    temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
    await drawAll(arr)
    }
    return arr;
    }
    document.getElementById('btn').onclick = function () {
    selectionSort(nums)
    }

    选择排序gif.gif

    插入排序

    async function insertionSort(arr) {
    if (Object.prototype.toString.call(arr).slice(8, -1) === 'Array') {
    for (var i = 1; i < arr.length; i++) {
    var key = arr[i];
    var j = i - 1;
    while (j >= 0 && arr[j] > key) {
    arr[j + 1] = arr[j];
    j--;
    }
    arr[j + 1] = key;
    await drawAll(arr)
    }
    return arr;
    } else {
    return 'arr is not an Array!';
    }
    }
    document.getElementById('btn').onclick = function () {
    insertionSort(nums)
    }

    插入排序gif.gif

    堆排序

    async function heapSort(array) {
    if (Object.prototype.toString.call(array).slice(8, -1) === 'Array') {
    //建堆
    var heapSize = array.length, temp;
    for (var i = Math.floor(heapSize / 2) - 1; i >= 0; i--) {
    heapify(array, i, heapSize);
    await drawAll(array)
    }

    //堆排序
    for (var j = heapSize - 1; j >= 1; j--) {
    temp = array[0];
    array[0] = array[j];
    array[j] = temp;
    heapify(array, 0, --heapSize);
    await drawAll(array)
    }
    return array;
    } else {
    return 'array is not an Array!';
    }
    }
    function heapify(arr, x, len) {
    if (Object.prototype.toString.call(arr).slice(8, -1) === 'Array' && typeof x === 'number') {
    var l = 2 * x + 1, r = 2 * x + 2, largest = x, temp;
    if (l < len && arr[l] > arr[largest]) {
    largest = l;
    }
    if (r < len && arr[r] > arr[largest]) {
    largest = r;
    }
    if (largest != x) {
    temp = arr[x];
    arr[x] = arr[largest];
    arr[largest] = temp;
    heapify(arr, largest, len);
    }
    } else {
    return 'arr is not an Array or x is not a number!';
    }
    }
    document.getElementById('btn').onclick = function () {
    heapSort(nums)
    }

    堆排序gif.gif

    快速排序

    async function quickSort(array, left, right) {
    drawAll(nums)
    if (Object.prototype.toString.call(array).slice(8, -1) === 'Array' && typeof left === 'number' && typeof right === 'number') {
    if (left < right) {
    var x = array[right], i = left - 1, temp;
    for (var j = left; j <= right; j++) {
    if (array[j] <= x) {
    i++;
    temp = array[i];
    array[i] = array[j];
    array[j] = temp;
    }
    }
    await drawAll(nums)
    await quickSort(array, left, i - 1);
    await quickSort(array, i + 1, right);
    await drawAll(nums)
    }
    return array;
    } else {
    return 'array is not an Array or left or right is not a number!';
    }
    }
    document.getElementById('btn').onclick = function () {
    quickSort(nums, 0, nums.length - 1)
    }

    快排gif.gif

    基数排序

    async function radixSort(arr, maxDigit) {
    var mod = 10;
    var dev = 1;
    var counter = [];
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
    for (var j = 0; j < arr.length; j++) {
    var bucket = parseInt((arr[j] % mod) / dev);
    if (counter[bucket] == null) {
    counter[bucket] = [];
    }
    counter[bucket].push(arr[j]);
    }
    var pos = 0;
    for (var j = 0; j < counter.length; j++) {
    var value = null;
    if (counter[j] != null) {
    while ((value = counter[j].shift()) != null) {
    arr[pos++] = value;
    await drawAll(arr)
    }
    }
    }
    }
    return arr;
    }
    document.getElementById('btn').onclick = function () {
    radixSort(nums, 3)
    }

    基数排序gif.gif

    希尔排序

    async function shellSort(arr) {
    var len = arr.length,
    temp,
    gap = 1;
    while (gap < len / 5) { //动态定义间隔序列
    gap = gap * 5 + 1;
    }
    for (gap; gap > 0; gap = Math.floor(gap / 5)) {
    for (var i = gap; i < len; i++) {
    temp = arr[i];
    for (var j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
    arr[j + gap] = arr[j];
    }
    arr[j + gap] = temp;
    await drawAll(arr)
    }
    }
    return arr;
    }
    document.getElementById('btn').onclick = function () {
    shellSort(nums)
    }

    基数排序gif.gif

    参考


    总结

    如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群 想进学习群,摸鱼群,请点击这里摸鱼


    image.png

    作者:Sunshine_Lin
    来源:https://juejin.cn/post/7004454008634998821

    收起阅读 »

    JavaScript复制内容到剪贴板

    最近一个活动页面中有一个小需求,用户点击或者长按就可以复制内容到剪贴板,记录一下实现过程和遇到的坑。 常见方法 查了一下万能的Google,现在常见的方法主要是以下两种:第三方库:clipboard.js原生方法:document.execCommand()分...
    继续阅读 »

    最近一个活动页面中有一个小需求,用户点击或者长按就可以复制内容到剪贴板,记录一下实现过程和遇到的坑。


    常见方法


    查了一下万能的Google,现在常见的方法主要是以下两种:

    • 第三方库:clipboard.js
    • 原生方法:document.execCommand()

    分别来看看这两种方法是如何使用的。


    clipboard.js


    这是clipboard的官网:clipboardjs.com/,看起来就是这么的简单。


    引用


    直接引用: <script src="dist/clipboard.min.js"></script>


    包: npm install clipboard --save ,然后 import Clipboard from 'clipboard';


    使用


    从输入框复制


    现在页面上有一个 <input> 标签,我们需要复制其中的内容,我们可以这样做:


    <input id="demoInput" value="hello world">
    <button class="btn" data-clipboard-target="#demoInput">点我复制</button>
    import Clipboard from 'clipboard';
    const btnCopy = new Clipboard('btn');

    注意到,在 <button> 标签中添加了一个 data-clipboard-target 属性,它的值是需要复制的 <input>id,顾名思义是从整个标签中复制内容。


    直接复制


    有的时候,我们并不希望从 <input> 中复制内容,仅仅是直接从变量中取值。如果在 Vue 中我们可以这样做:


    <button class="btn" :data-clipboard-text="copyValue">点我复制</button>
    import Clipboard from 'clipboard';
    const btnCopy = new Clipboard('btn');
    this.copyValue = 'hello world';

    事件


    有的时候我们需要在复制后做一些事情,这时候就需要回调函数的支持。


    在处理函数中加入以下代码:


    // 复制成功后执行的回调函数
    clipboard.on('success', function(e) {
    console.info('Action:', e.action); // 动作名称,比如:Action: copy
    console.info('Text:', e.text); // 内容,比如:Text:hello word
    console.info('Trigger:', e.trigger); // 触发元素:比如:<button :data-clipboard-text="copyValue">点我复制</button>
    e.clearSelection(); // 清除选中内容
    });

    // 复制失败后执行的回调函数
    clipboard.on('error', function(e) {
    console.error('Action:', e.action);
    console.error('Trigger:', e.trigger);
    });

    小结


    文档中还提到,如果在单页面中使用 clipboard ,为了使得生命周期管理更加的优雅,在使用完之后记得 btn.destroy() 销毁一下。


    clipboard 使用起来是不是很简单。但是,就为了一个 copy 功能就使用额外的第三方库是不是不够优雅,这时候该怎么办?那就用原生方法实现呗。


    document.execCommand()方法


    先看看这个方法在 MDN 上是怎么定义的:



    which allows one to run commands to manipulate the contents of the editable region.



    意思就是可以允许运行命令来操作可编辑区域的内容,注意,是可编辑区域


    定义



    bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)



    方法返回一个 Boolean 值,表示操作是否成功。

    • aCommandName :表示命令名称,比如: copycut 等(更多命令见命令);
    • aShowDefaultUI:是否展示用户界面,一般情况下都是 false
    • aValueArgument:有些命令需要额外的参数,一般用不到;

    兼容性


    这个方法在之前的兼容性其实是不太好的,但是好在现在已经基本兼容所有主流浏览器了,在移动端也可以使用。


    兼容性


    使用


    从输入框复制


    现在页面上有一个 <input> 标签,我们想要复制其中的内容,我们可以这样做:


    <input id="demoInput" value="hello world">
    <button id="btn">点我复制</button>
    const btn = document.querySelector('#btn');
    btn.addEventListener('click', () => {
    const input = document.querySelector('#demoInput');
    input.select();
    if (document.execCommand('copy')) {
    document.execCommand('copy');
    console.log('复制成功');
    }
    })

    其它地方复制


    有的时候页面上并没有 <input> 标签,我们可能需要从一个 <div> 中复制内容,或者直接复制变量。


    还记得在 execCommand() 方法的定义中提到,它只能操作可编辑区域,也就是意味着除了 <input><textarea> 这样的输入域以外,是无法使用这个方法的。


    这时候我们需要曲线救国。


    <button id="btn">点我复制</button>
    const btn = document.querySelector('#btn');
    btn.addEventListener('click',() => {
    const input = document.createElement('input');
    document.body.appendChild(input);
    input.setAttribute('value', '听说你想复制我');
    input.select();
    if (document.execCommand('copy')) {
    document.execCommand('copy');
    console.log('复制成功');
    }
    document.body.removeChild(input);
    })

    算是曲线救国成功了吧。在使用这个方法时,遇到了几个坑。


    遇到的坑


    在Chrome下调试的时候,这个方法时完美运行的。然后到了移动端调试的时候,坑就出来了。


    对,没错,就是你,ios。。。




    1. 点击复制时屏幕下方会出现白屏抖动,仔细看是拉起键盘又瞬间收起


      知道了抖动是由于什么产生的就比较好解决了。既然是拉起键盘,那就是聚焦到了输入域,那只要让输入域不可输入就好了,在代码中添加 input.setAttribute('readonly', 'readonly'); 使这个 <input> 是只读的,就不会拉起键盘了。




    2. 无法复制


      这个问题是由于 input.select() 在ios下并没有选中全部内容,我们需要使用另一个方法来选中内容,这个方法就是 input.setSelectionRange(0, input.value.length);




    完整代码如下:


    const btn = document.querySelector('#btn');
    btn.addEventListener('click',() => {
    const input = document.createElement('input');
    input.setAttribute('readonly', 'readonly');
    input.setAttribute('value', 'hello world');
    document.body.appendChild(input);
    input.setSelectionRange(0, 9999);
    if (document.execCommand('copy')) {
    document.execCommand('copy');
    console.log('复制成功');
    }
    document.body.removeChild(input);
    })


    作者:axuebin
    链接:https://juejin.cn/post/6844903567480848391

    收起阅读 »

    前端vue面霸修炼手册!!

    一、对MVVM的理解MVVM全称是Model-View-ViewModelModel 代表数据模型,数据和业务逻辑都在Model层中定义;泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。View 代表UI视图,负责数据的展示...
    继续阅读 »



    一、对MVVM的理解

    MVVM全称是Model-View-ViewModel

    Model 代表数据模型,数据和业务逻辑都在Model层中定义;泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。View 代表UI视图,负责数据的展示;视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。ViewModel 负责监听 Model 中数据的改变并且控制视图的更新,处理用户交互操作;

    Vue是以数据为驱动的,Vue自身将DOM和数据进行绑定,一旦创建绑定,DOM和数据将保持同步,每当数据发生变化,DOM会跟着变化。 ViewModel是Vue的核心,它是Vue的一个实例。Vue实例时作用域某个HTML元素上的这个HTML元素可以是body,也可以是某个id所指代的元素。

    二、vue常见指令

    1. v-textv-text 主要用来更新 textContent,可以等同于 JS 的 text 属性。

    <span v-text="name"></span>

    <span插值表达式{{name}}</span>
    1. v-html等同于 JS 的 innerHtml 属性

    <div v-html="content"></div>
    1. v-cloak用来保持在元素上直到关联实例结束时进行编译 解决闪烁问题

    <div id="app" v-cloak>
       <div>
          {{msg}}
       </div>
    </div>
    <script type="text/javascript">
       new Vue({
         el:'#app',
         data:{
           msg:'hello world'
        }
      })
    </script>

    正常在页面加载时会闪烁,先显示:

    <div>
      {{msg}}
    </div>

    编译后才显示:

    <div>
      hello world!
    </div>

    可以用 v-cloak 指令解决插值表达式闪烁问题,v-cloak 在 css 中用属性选择器设置为 display: none;

    1. v-oncev-once 关联的实例,只会渲染一次。之后的重新渲染,实例极其所有的子节点将被视为静态内容跳过,这可以用于优化更新性能

    <span v-once>This will never change:{{msg}}</span>  //单个元素
    <div v-once>//有子元素
       <h1>comment</h1>
       <p>{{msg}}</p>
    </div>
    <my-component v-once:comment="msg"></my-component> //组件
    <ul>
       <li v-for="i in list">{{i}}</li>
    </ul>

    上面的例子中,msg,list 即使产生改变,也不会重新渲染。

    1. v-ifv-if 可以实现条件渲染,Vue 会根据表达式的值的真假条件来渲染元素

    <a v-if="true">show</a>
    1. v-elsev-else 是搭配 v-if 使用的,它必须紧跟在 v-if 或者 v-else-if 后面,否则不起作用

    <a v-if="true">show</a>
    <a v-else>hide</a>
    1. v-else-ifv-else-if 充当 v-if 的 else-if 块, 可以链式的使用多次。可以更加方便的实现 switch 语句。

    <div v-if="type==='A'">
      A
    </div>
    <div v-else-if="type==='B'">
      B
    </div>
    <div v-else-if="type==='C'">
      C
    </div>
    <div v-else>
      Not A,B,C
    </div>
    1. v-show也是用于根据条件展示元素。和 v-if 不同的是,如果 v-if 的值是 false,则这个元素被销毁,不在 dom 中。但是 v-show 的元素会始终被渲染并保存在 dom 中,它只是简单的切换 css 的 dispaly 属性。

    <span v-show="true">hello world</span >

    注意:v-if 有更高的切换开销 v-show 有更高的初始渲染开销。因此,如果要非常频繁的切换, 则使用 v-show 较好;如果在运行时条件不太可能改变,则 v-if 较好

    1. v-for用 v-for 指令根据遍历数组来进行渲染

    <div v-for="(item,index) in items"></div>   //使用in,index是一个可选参数,表示当前项的索引
    1. v-bindv-bind 用来动态的绑定一个或者多个特性。没有参数时,可以绑定到一个包含键值对的对象。常用于动态绑定 class 和 style。以及 href 等。简写为一个冒号【 :】

    <div id="app">
       <div :class="{'is-active':isActive, 'text-danger':hasError}"></div>
    </div>
    <script>
       var app = new Vue({
           el: '#app',
           data: {
               isActive: true,  
               hasError: false    
          }
      })
    </script>

    编译后

    <div class = "is-active"></div>
    1. v-model用于在表单上创建双向数据绑定

    <div id="app">
       <input v-model="name">
       <p>hello {{name}}</p>
    </div>
    <script>
       var app = new Vue({
           el: '#app',
           data: {
               name:'小明'
          }
      })
    </script>

    model 修饰符有

    .lazy(在 change 事件再同步) > v-model.lazy .number(自动将用户的输入值转化为数值类型) > v-model.number .trim(自动过滤用户输入的首尾空格) > v-model.trim

    1. v-onv-on 主要用来监听 dom 事件,以便执行一些代码块。表达式可以是一个方法名。 简写为:【 @ 】

    <div id="app">
      <button @click="consoleLog"></button>
    </div>
    <script>
      var app = new Vue({
          el: '#app',
          methods:{
              consoleLog:function (event) {
                  console.log(1)
              }
          }
      })
    </script>

    事件修饰符

    .stop 阻止事件继续传播 .prevent 事件不再重载页面 .capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理 .self 只当在 event.target 是当前元素自身时触发处理函数 .once 事件将只会触发一次 .passive 告诉浏览器你不想阻止事件的默认行为

    三 、v-if 和 v-show 有什么区别?

    共同点:v-if 和 v-show 都能实现元素的显示隐藏

    区别:

    v-show 只是简单的控制元素的 display 属性 而 v-if 才是条件渲染(条件为真,元素将会被渲染,条件为假,元素会被销毁) 2. v-show 有更高的首次渲染开销,而 v-if 的首次渲染开销要小的多 3. v-if 有更高的切换开销,v-show 切换开销小 4. v-if 有配套的 v-else-if 和 v-else,而 v-show 没有 5. v-if 可以搭配 template 使用,而 v-show 不行

    四、如何让CSS只在当前组件中起作用?

    将组件样式加上 scoped

    <style scoped>
    ...
    </style>

    五、 keep-alive的作用是什么?

    keep-alive包裹动态组件时,会缓存不活动的组件实例, 主要用于保留组件状态或避免重新渲染。

    六、在Vue中使用插件的步骤

    采用ES6的 import … from … 语法 或 CommonJSd的 require() 方法引入插件 2、使用全局方法 Vue.use( plugin ) 使用插件,可以传入一个选项对象 Vue.use(MyPlugin, { someOption: true })

    七、Vue 生命周期

    八、Vue 组件间通信有哪几种方式

    Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信

    九、computed 和 watch 的区别和运用的场景

    computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值

    watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作

    运用场景:

    • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed

    • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch

    十、vue-router 路由模式有几种

    1. Hash: 使用 URL 的 hash 值来作为路由。支持所有浏览器。

    2. History: 以来 HTML5 History API 和服务器配置。参考官网中 HTML5 History 模式

    3. Abstract: 支持所有 javascript 运行模式。如果发现没有浏览器的 API,路由会自动强制进入这个模式。

    十一、SPA 单页面的理解,它的优缺点分别是什么

    SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS 一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转 取而代之的是利用路由机制实现 HTML 内容的变换, UI 与用户的交互,避免页面的重新加载。

    优点:

    1、用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染
    2、基于上面一点,SPA 相对对服务器压力小
    3、前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理

    缺点:

    1、初次加载耗时多:为实现单页 Web 应用功能及显示效果, 需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载
    2、前进后退路由管理:由于单页应用在一个页面中显示所有的内容, 所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理
    3、SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势
    作者:不要搞偷袭
    来源:https://blog.51cto.com/u_15115139/2675806


    收起阅读 »

    不想加班,你就背会这 10 条 JS 技巧

    为了让自己写的代码更优雅且高效,特意向大佬请教了这 10 条 JS 技巧1. 数组分割const listChunk = (list = [], chunkSize = 1) => {const result = [];const tmp = [...l...
    继续阅读 »



    为了让自己写的代码更优雅且高效,特意向大佬请教了这 10 条 JS 技巧

    1. 数组分割

    const listChunk = (list = [], chunkSize = 1) => {
    const result = [];
    const tmp = [...list];
    if (!Array.isArray(list) || !Number.isInteger(chunkSize) || chunkSize <= 0) {
    return result;
      };
    while (tmp.length) {
    result.push(tmp.splice(0, chunkSize));
      };
    return result;
    };
    listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
    // [['a'], ['b'], ['c'], ['d'], ['e'], ['f'], ['g']]

    listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], 3);
    // [['a', 'b', 'c'], ['d', 'e', 'f'], ['g']]

    listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], 0);
    // []

    listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], -1);
    // []

    2. 求数组元素交集

    const listIntersection = (firstList, ...args) => {
    if (!Array.isArray(firstList) || !args.length) {
    return firstList;
      }
    return firstList.filter(item => args.every(list => list.includes(item)));
    };
    listIntersection([1, 2], [3, 4]);
    // []

    listIntersection([2, 2], [3, 4]);
    // []

    listIntersection([3, 2], [3, 4]);
    // [3]

    listIntersection([3, 4], [3, 4]);
    // [3, 4]

    3. 按下标重新组合数组

    const zip = (firstList, ...args) => {
    if (!Array.isArray(firstList) || !args.length) {
    return firstList
      };
    return firstList.map((value, index) => {
    const newArgs = args.map(arg => arg[index]).filter(arg => arg !== undefined);
    const newList = [value, ...newArgs];
    return newList;
      });
    };
    zip(['a', 'b'], [1, 2], [true, false]);
    // [['a', 1, true], ['b', 2, false]]

    zip(['a', 'b', 'c'], [1, 2], [true, false]);
    // [['a', 1, true], ['b', 2, false], ['c']]

    4. 按下标组合数组为对象

    const zipObject = (keys, values = {}) => {
    const emptyObject = Object.create({});
    if (!Array.isArray(keys)) {
    return emptyObject;
      };
    return keys.reduce((acc, cur, index) => {
    acc[cur] = values[index];
    return acc;
      }, emptyObject);
    };
    zipObject(['a', 'b'], [1, 2])
    // { a: 1, b: 2 }
    zipObject(['a', 'b'])
    // { a: undefined, b: undefined }

    5. 检查对象属性的值

    const checkValue = (obj = {}, objRule = {}) => {
    const isObject = obj => {
    return Object.prototype.toString.call(obj) === '[object Object]';
      };
    if (!isObject(obj) || !isObject(objRule)) {
    return false;
      }
    return Object.keys(objRule).every(key => objRule[key](obj[key]));
    };

    const object = { a: 1, b: 2 };

    checkValue(object, {
    b: n => n > 1,
    })
    // true

    checkValue(object, {
    b: n => n > 2,
    })
    // false

    6. 获取对象属性

    const get = (obj, path, defaultValue) => {
    if (!path) {
    return;
      };
    const pathGroup = Array.isArray(path) ? path : path.match(/([^[.\]])+/g);
    return pathGroup.reduce((prevObj, curKey) => prevObj && prevObj[curKey], obj) || defaultValue;
    };

    const obj1 = { a: { b: 2 } }
    const obj2 = { a: [{ bar: { c: 3 } }] }

    get(obj1, 'a.b')
    // 2
    get(obj2, 'a[0].bar.c')
    // 3
    get(obj2, ['a', '0', 'bar', 'c'])
    // 2
    get(obj1, 'a.bar.c', 'default')
    // default
    get(obj1, 'a.bar.c', 'default')
    // default

    7. 将特殊符号转成字体符号

    const escape = str => {
    const isString = str => {
    return Object.prototype.toString.call(str) === '[string Object]';
      };
    if (!isString(str)) {
    return str;
      }
    return (str.replace(/&/g, '&')
    .replace(/"/g, '"')
    .replace(/'/g, '&#x27;')
    .replace(/</g, '<')
    .replace(/>/g, '>')
    .replace(/\//g, '&#x2F;')
    .replace(/\\/g, '&#x5C;')
    .replace(/`/g, '&#96;'));
    };

    8. 利用注释创建一个事件监听器

    class EventEmitter {
    #eventTarget;
    constructor(content = '') {
    const comment = document.createComment(content);
    document.documentElement.appendChild(comment);
    this.#eventTarget = comment;
      }
    on(type, listener) {
    this.#eventTarget.addEventListener(type, listener);
      }
    off(type, listener) {
    this.#eventTarget.removeEventListener(type, listener);
      }
    once(type, listener) {
    this.#eventTarget.addEventListener(type, listener, { once: true });
      }
    emit(type, detail) {
    const dispatchEvent = new CustomEvent(type, { detail });
    this.#eventTarget.dispatchEvent(dispatchEvent);
      }
    };

    const emmiter = new EventEmitter();
    emmiter.on('biy', () => {
    console.log('hello world');
    });
    emmiter.emit('biu');
    // hello world

    9. 生成随机的字符串

    const genRandomStr = (len = 1) => {
    let result = '';
    for (let i = 0; i < len; ++i) {
    result += Math.random().toString(36).substr(2)
      }
    return result.substr(0, len);
    }
    genRandomStr(3)
    // u2d
    genRandomStr()
    // y
    genRandomStr(10)
    // qdueun65jb

    10. 判断是否是指定的哈希值

    const isHash = (type = '', str = '') => {
    const isString = str => {
    return Object.prototype.toString.call(str) === '[string Object]';
      };
    if (!isString(type) || !isString(str)) {
    return str;
      };
    const algorithms = {
    md5: 32,
    md4: 32,
    sha1: 40,
    sha256: 64,
    sha384: 96,
    sha512: 128,
    ripemd128: 32,
    ripemd160: 40,
    tiger128: 32,
    tiger160: 40,
    tiger192: 48,
    crc32: 8,
    crc32b: 8,
      };
    const hash = new RegExp(`^[a-fA-F0-9]{${algorithms[type]}}$`);
    return hash.test(str);
    };

    isHash('md5', 'd94f3f016ae679c3008de268209132f2');
    // true
    isHash('md5', 'q94375dj93458w34');
    // false

    isHash('sha1', '3ca25ae354e192b26879f651a51d92aa8a34d8d3');
    // true
    isHash('sha1', 'KYT0bf1c35032a71a14c2f719e5a14c1');
    // false

    后记

    如果你喜欢探讨技术,或者对本文有任何的意见或建议,非常欢迎加鱼头微信好友一起探讨,当然,鱼头也非常希望能跟你一起聊生活,聊爱好,谈天说地。

    全文完

    作者:酸菜鱼+黄焖鸡

    来源:https://blog.51cto.com/u_15291238/4538068

    收起阅读 »

    尤大亲自解释vue3源码中为什么不使用?.可选链式操作符?

    vue
    阅读本文🦀 1.什么是可选链式操作符号 2.为什么vue3源码中不使用可选链式操作符 什么是可选链式操作符号❓ 可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之...
    继续阅读 »

    阅读本文🦀


    1.什么是可选链式操作符号


    2.为什么vue3源码中不使用可选链式操作符


    什么是可选链式操作符号❓


    可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之处在于,在引用为空(nullish ) (null 或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined


    当尝试访问可能不存在的对象属性时,可选链操作符将会使表达式更短、更简明。在探索一个对象的内容时,如果不能确定哪些属性必定存在,可选链操作符也是很有帮助的。


    const adventurer = {
    name: 'Alice',
    cat: {
    name: 'Dinah'
    }
    };

    const dogName = adventurer.dog?.name;
    console.log(dogName);
    // expected output: undefined

    console.log(adventurer.someNonExistentMethod?.());
    // expected output: undefined


    短路效应


    如果 ?. 左边部分不存在,就会立即停止运算(“短路效应”)。


    所以,如果后面有任何函数调用或者副作用,它们均不会执行。


    let user = null; 
    let x = 0;
    user?.sayHi(x++);
    // 没有 "sayHi",因此代码执行没有触达 x++ alert(x); // 0,值没有增加

    Vue3源码中为什么不采用这么方便的操作符


    image-20211114120351836


    看看这样是不是代码更简洁了,但是为什么这个PR没有被合并呢


    来自尤大的亲自解释


    image-20211114120545284


    (我们有意避免在代码库中使用可选链,因为我们的目标是 ES2016,而 TS 会将其转换为更加冗长的内容)


    从尤大的话中我们可以得知由于Vu3打包后的代码是基于ES2016的,虽然我们在编写代码时看起来代码比较简洁了,实际打包之后反而更冗余了,这样会增大包的体积,影响Vu3的加载速度。由此可见一个优秀的前端框架真的要考虑的东西很多,语法也会考虑周到~✨



    作者:速冻鱼
    链接:https://juejin.cn/post/7033167068895641637

    收起阅读 »

    想知道一个20k级别前端在项目中是怎么使用LocalStorage的吗?

    前言 大家好,我是林三心,用最通俗的话,讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天就给大家唠一下嗑,讲一下,怎么样使用localStorage、sessionStorage,才能更规范,更高大上,更能让人眼前一亮。 用处 在平时的开发中,lo...
    继续阅读 »

    前言


    大家好,我是林三心,用最通俗的话,讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天就给大家唠一下嗑,讲一下,怎么样使用localStorage、sessionStorage,才能更规范,更高大上,更能让人眼前一亮。


    用处


    在平时的开发中,localStorage、sessionStorage的用途是非常的多的,在我们的开发中发挥着非常重要的作用:



    • 1、登录完成后token的存储

    • 2、用户部分信息的存储,比如昵称、头像、简介

    • 3、一些项目通用参数的存储,例如某个id、某个参数params

    • 4、项目状态管理的持久化,例如vuex的持久化、redux的持久化

    • 5、项目整体的切换状态存储,例如主题颜色、icon风格、语言标识

    • 6、等等、、、、、、、、、、、、、、、、、、、、、、、、、、


    普通使用


    那么,相信我们各位平时使用都是这样的(拿localStorage举例)


    1、基础变量


    // 当我们存基本变量时
    localStorage.setItem('基本变量', '这是一个基本变量')
    // 当我们取值时
    localStorage.getItem('基本变量')
    // 当我们删除时
    localStorage.removeItem('基本变量')

    2、引用变量


    // 当我们存引用变量时
    localStorage.setItem('引用变量', JSON.stringify(data))
    // 当我们取值时
    const data = JSON.parse(localStorage.getItem('引用变量'))
    // 当我们删除时
    localStorage.removeItem('引用变量')

    3、清空


    localStorage.clear()

    暴露出什么问题?


    1、命名过于简单



    • 1、比如我们存用户信息会使用user作为 key 来存储

    • 2、存储主题的时候用theme 作为 key 来存储

    • 3、存储令牌时使用token作为 key 来存储


    其实这是很有问题的,咱们都知道,同源的两个项目,它们的localStorage是互通的。


    我举个例子吧比如我现在有两个项目,它们在同源https://www.sunshine.com下,这两个项目都需要往localStorage中存储一个 key 为name的值,那么这就会造成两个项目的name互相顶替的现象,也就是互相污染现象


    截屏2021-11-10 下午10.19.09.png


    2、时效性


    咱们都知道localStorage、sessionStorage这两个的生命周期分别是



    • localStorage:除非手动清除,否则一直存在

    • sessionStorage:生命结束于当前标签页的关闭或浏览器的关闭


    其实平时普通的使用时没什么问题的,但是给某些指定缓存加上特定的时效性,是非常重要的!比如某一天:



    • 后端:”兄弟,你一登录我就把token给你“

    • 前端:”好呀,那你应该会顺便判断token过期没吧?“

    • 后端:”不行哦,放在你前端判断过期呗“

    • 前端:”行吧。。。。。“


    那这时候,因为需要在前端判断过期,所以咱们就得给token设置一个时效性,或者是1天,或者是7天


    截屏2021-11-10 下午10.48.50.png


    3、隐秘性


    其实这个好理解,你们想想,当咱们把咱们想缓存的东西,存在localStorage、sessionStorage中,在开发过程中,确实有利于咱们的开发,咱们想看的时候也是一目了然,点击Application就可以看到。


    但是,一旦产品上线了,用户也是可以看到缓存中的东西的,而咱们肯定是会想:有些东西可以让用户看到,但是有些东西我不想让你看到


    截屏2021-11-10 下午11.02.24.png


    或者咱们在做状态管理持久化时,需要把数据先存在localStorage中,这个时候就很有必要对缓存进行加密了。


    解决方案


    1、命名规范


    我个人的看法是项目名 + 当前环境 + 项目版本 + 缓存key,如果大家有其他规则的,可以评论区告诉林三心,让林三心学学


    截屏2021-11-11 下午9.12.32.png


    2、expire定时


    思路:设置缓存key时,将value包装成一个对象,对象中有相应的时效时段,当下一次想获取缓存值时,判断有无超时,不超时就获取value,超时就删除这个缓存


    截屏2021-11-11 下午9.33.00.png


    3、crypto加密


    加密很简单,直接使用crypto-js进行对数据的加密,使用这个库里的encrypt、decrypyt进行加密、解密


    截屏2021-11-11 下午9.43.16.png


    实践


    其实实践的话比较简单啦,无非就是四步



    • 1、与团队商讨一下key的格式

    • 2、与团队商讨一下expire的长短

    • 3、与团队商讨一下使用哪个库来对缓存进行加密(个人建议crypto-js

    • 4、代码实施(不难,我这里就不写了)


    结语


    有人可能觉得没必要,但是严格要求自己其实是很有必要的,平时严格要求自己,才能做到每到一个公司都能更好的做到向下兼容难度。


    如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群 想进学习群,摸鱼群,请点击这里摸鱼,我会定时直播模拟面试,简历指导,答疑解惑


    image.png


    作者:Sunshine_Lin
    链接:https://juejin.cn/post/7033749571939336228

    收起阅读 »

    巧用渐变实现高级感拉满的背景光动画

    实现 这个效果想利用 CSS 完全复制是比较困难的。CSS 模拟出来的光效阴影相对会 Low 一点,只能说是尽量还原。 其实每组光都基本是一样的,所以我们只需要实现其中一组,就几乎能实现了整个效果。 观察这个效果: 它的核心其实就是角向渐变 -- conic...
    继续阅读 »

    141609598-e0a1e420-2967-4ce4-8086-bfef1233f5f6.gif


    实现


    这个效果想利用 CSS 完全复制是比较困难的。CSS 模拟出来的光效阴影相对会 Low 一点,只能说是尽量还原。


    其实每组光都基本是一样的,所以我们只需要实现其中一组,就几乎能实现了整个效果。


    观察这个效果:



    它的核心其实就是角向渐变 -- conic-gradient(),利用角向渐变,我们可以大致实现这样一个效果:


    <div></div>

    div {
    width: 1000px;
    height: 600px;
    background:
    conic-gradient(
    from -45deg at 400px 300px,
    hsla(170deg, 100%, 70%, .7),
    transparent 50%,
    transparent),
    linear-gradient(-45deg, #060d5e, #002268);
    }

    看看效果:



    有点那意思了。当然,仔细观察,渐变的颜色并非是由一种颜色到透明就结束了,而是颜色 A -- 透明 -- 颜色 B,这样,光源的另一半并非就不会那么生硬,改造后的 CSS 代码:


    div {
    width: 1000px;
    height: 600px;
    background:
    conic-gradient(
    from -45deg at 400px 300px,
    hsla(170deg, 100%, 70%, .7),
    transparent 50%,
    hsla(219deg, 90%, 80%, .5) 100%),
    linear-gradient(-45deg, #060d5e, #002268);
    }

    我们在角向渐变的最后多加了一种颜色,得到观感更好的一种效果:



    emm,到这里,我们会发现,仅仅是角向渐变 conic-gradient() 是不够的,它无法模拟出光源阴影的效果,所以必须再借助其他属性实现光源阴影的效果。


    这里,我们会很自然的想到 box-shadow。这里有个技巧,利用多重 box-shadow, 实现 Neon 灯的效果。


    我们再加个 div,通过它实现光源阴影:


    <div class="shadow"></div>

    .shadow {
    width: 200px;
    height: 200px;
    background: #fff;
    box-shadow:
    0px 0 .5px hsla(170deg, 95%, 80%, 1),
    0px 0 1px hsla(170deg, 91%, 80%, .95),
    0px 0 2px hsla(171deg, 91%, 80%, .95),
    0px 0 3px hsla(171deg, 91%, 80%, .95),
    0px 0 4px hsla(171deg, 91%, 82%, .9),
    0px 0 5px hsla(172deg, 91%, 82%, .9),
    0px 0 10px hsla(173deg, 91%, 84%, .9),
    0px 0 20px hsla(174deg, 91%, 86%, .85),
    0px 0 40px hsla(175deg, 91%, 86%, .85),
    0px 0 60px hsla(175deg, 91%, 86%, .85);
    }


    OK,光是有了,但问题是我们只需要一侧的光,怎么办呢?裁剪的方式很多,这里,我介绍一种利用 clip-path 进行对元素任意空间进行裁切的方法:


    .shadow {
    width: 200px;
    height: 200px;
    background: #fff;
    box-shadow: .....;
    clip-path: polygon(-100% 100%, 200% 100%, 200% 500%, -100% 500%);
    }

    原理是这样的:



    这样,我们就得到了一侧的光:



    这里,其实 CSS 也是有办法实现单侧阴影的(你所不知道的 CSS 阴影技巧与细节),但是实际效果并不好,最终采取了上述的方案。


    接下来,就是利用定位、旋转等方式,将上述单侧光和角向渐变重叠起来,我们就可以得到这样的效果:


    image


    这会,已经挺像了。接下来要做的就是让整个图案,动起来。这里技巧也挺多的,核心还是利用了 CSS @Property,实现了角向渐变的动画,并且让光动画和角向渐变重叠起来。


    我们需要利用 CSS @Property 对代码渐变进行改造,核心代码如下:


    <div class="wrap">
    <div class="shadow"></div>
    </div>

    @property --xPoint {
    syntax: '<length>';
    inherits: false;
    initial-value: 400px;
    }
    @property --yPoint {
    syntax: '<length>';
    inherits: false;
    initial-value: 300px;
    }

    .wrap {
    position: relative;
    margin: auto;
    width: 1000px;
    height: 600px;
    background:
    conic-gradient(
    from -45deg at var(--xPoint) var(--yPoint),
    hsla(170deg, 100%, 70%, .7),
    transparent 50%,
    hsla(219deg, 90%, 80%, .5) 100%),
    linear-gradient(-45deg, #060d5e, #002268);
    animation: pointMove 2.5s infinite alternate linear;
    }

    .shadow {
    position: absolute;
    top: -300px;
    left: -330px;
    width: 430px;
    height: 300px;
    background: #fff;
    transform-origin: 100% 100%;
    transform: rotate(225deg);
    clip-path: polygon(-100% 100%, 200% 100%, 200% 500%, -100% 500%);
    box-shadow: ... 此处省略大量阴影代码;
    animation: scale 2.5s infinite alternate linear;
    }

    @keyframes scale {
    50%,
    100% {
    transform: rotate(225deg) scale(0);
    }
    }

    @keyframes pointMove {
    100% {
    --xPoint: 100px;
    --yPoint: 0;
    }
    }

    这样,我们就实现了完整的一处光的动画:



    我们重新梳理一下,实现这样一个动画的步骤:



    1. 利用角向渐变 conic-gradient 搭出基本框架,并且,这里也利用了多重渐变,角向渐变的背后是深色背景色;

    2. 利用多重 box-shadow 实现光及阴影的效果(又称为 Neon 效果)

    3. 利用 clip-path 对元素进行任意区域的裁剪

    4. 利用 CSS @Property 实现渐变的动画效果


    剩下的工作,就是重复上述的步骤,补充其他渐变和光源,调试动画,最终,我们就可以得到这样一个简单的模拟效果:



    由于原效果是 .mp4,无法拿到其中的准确颜色,无法拿到阴影的参数,其中颜色是直接用的色板取色,阴影则比较随意的模拟了下,如果有源文件,准确参数,可以模拟的更逼真。


    完整的代码你可以戳这里:CodePen -- iPhone 13 Pro Gradient


    作者:chokcoco
    链接:https://juejin.cn/post/7033952765151805453

    收起阅读 »

    vite对浏览器的请求做了什么

    工作原理:type="module" 浏览器中ES Module原生native支持。 如果浏览器支持type="module" ,我i们可以使用es6模块化的方式编写。浏览器会把我们需要导入的文件再发一次http请求,再发到服务器上。 开...
    继续阅读 »

    工作原理:

    • type="module" 浏览器中ES Module原生native支持。 如果浏览器支持type="module" ,我i们可以使用es6模块化的方式编写。浏览器会把我们需要导入的文件再发一次http请求,再发到服务器上。 开发阶段不需要打包
    • 第三方依赖预打包
    • 启动一个开发服务器处理资源请求

    一图详解vite原理:


    6F2(QRN))B}6D@~KQN8CYD0.png


    浏览器做的什么事啊


    宿主文件index.html


    <script type="module" src="/src/main.js"></script>

    浏览器获取到宿主文件中的资源后,发现还要再去请求main.js文件。会再向服务端发送一次main.js的资源请求。


    image.png


    main.js


    在main中,可以发现,浏览器又再次发起对vue.js?v=d253a66cApp.vue?t=1637479953836两个文件的资源请求。


    服务器会将App.vue中的内容进行编译然后返回给浏览器,下图可以看出logo图片和文字都被编译成_hoisted_ 的静态节点。


    image.png
    从请求头中,也可以看出sfc文件已经变成浏览器可以识别的js文件(app.vue文件中要存在script内容才会编译成js)。对于浏览器来说,执行的就是一段js代码。


    image.png


    其他裸模块


    如果vue依赖中还存在其他依赖的话,浏览器依旧会再次发起资源请求,获取相应资源。


    了解一下预打包


    对于第三方依赖(裸模块)的加载,vite对其提前做好打包工作,将其放到node_modules/.vite下。当启动项目的时候,直接从该路径下下载文件。


    1637397635556.png
    通过上图,可以看到再裸模块的引入时,路径发生了改变。


    服务器做的什么事啊


    总结一句话:服务器把特殊后缀名的文件进行处理返回给前端展示


    我们可以模拟vite的devServe,使用koa中间件启动一个本地服务。


    // 引入依赖
    const Koa = require('koa')
    const app = new Koa()
    const fs = require('fs')
    const path = require('path')
    const compilerSfc = require('@vue/compiler-sfc')
    const compilerDom = require('@vue/compiler-dom')

    app.use(async (ctx) => {
    const { url, query } = ctx.request
    // 处理请求资源代码都写这
    })
    zaiz都h这z都he在
    app.listen(3001, () => {
    console.log('dyVite start!!')
    })

    请求首页index.html


     if (url === '/') {
    const p = path.join(__dirname, './index.html') // 绝对路径
    // 首页
    ctx.type = 'text/html'
    ctx.body = fs.readFileSync(p, 'utf8')
    }

    1637475203111.png


    看到上面这张图,就知道我们的宿主文件已经请求成功了。只是浏览器又给服务端发送的一个main.js文件的请求。这时,我们还需要判断处理一下main.js文件。


    请求以.js结尾的文件


    我们处理上述情况后,emmmm。。。发现main中还是存在好多其他资源请求。


    基础js文件


    main文件:


    console.log(1)

    处理main:


    else if (url.endsWith('.js')) {
       // 响应js请求
       const p = path.join(__dirname, url)
       ctx.type = 'text/javascript'
       ctx.body = rewriteImport(fs.readFileSync(p, 'utf8')) // 处理依赖函数
    }

    对main中的依赖进行处理


    你以为main里面就一个输出吗?太天真了。这样的还能处理吗?


    main文件:


    import { createApp, h } from 'vue'
    createApp({ render: () => h('div', 'helllo dyVite!') }).mount('#app')

    emmm。。。应该可以!


    我们可以将main中导入的地址变成相对地址。

    在裸模块路径添加上/@modules/。再去识别/@modules/的文件即(裸模块文件)。


    // 把能读出来的文件地址变成相对地址
    // 正则替换 重写导入 变成相对地址
    // import { createApp } from 'vue' => import { createApp } from '/@modules/vue'
    function rewriteImport(content) {
    return content.replace(/ from ['|"](.*)['|"]/g, function (s0, s1) {
    // s0匹配字符串,s1分组内容
    // 是否是相对路径
    if (s1.startsWith('./') || s1.startsWith('/') || s1.startsWith('../')) {
    // 直接返回
    return s0
    } else {
    return ` from '/@modules/${s1}'`
    }
    })
    }

    对于第三方依赖,vite内部是使用预打包请求自己服务器/node_modules/.vite/下的内部资源。
    我们可以简单化一点,将拿到的依赖名去客户端下的node_modules下拿相应的资源。


      else if (url.startsWith('/@modules/')) {
    // 裸模块的加载
    const moduleName = url.replace('/@modules/', '')
    const pre![1637477009328](imgs/1637477009328.png)![1637477009368](imgs/1637477009368.png)的地址
    const module = require(prefix + '/package.json').module
    const filePath = path.join(prefix, module) // 拿到文件加载的地址
    // 读取相关依赖
    const ret = fs.readFileSync(filePath, 'utf8')
    ctx.type = 'text/javascript'
    ctx.body = rewriteImport(ret) //依赖内部可能还存在依赖,需要递归
    }

    在main中进行render时,会报下图错误:


    1637477015346.png


    我们加载的文件都是服务端执行的库,内部可能会产生node环境的代码,需要判断一下环境变量。如果开发时,会输出一些警告信息,但是在前端是没有的。所以我们需要mock一下,告诉浏览器我们当前的环境。


    给html加上process环境变量。


      <script>
       window.process = { env: { NODE_ENV: 'dev' } }
     </script>

    此时main文件算是加载出来了。


    但是这远远打不到我们的目的啊!


    我们需要的是可以编译vue文件的服务器啊!


    处理.vue文件


    main.js文件:


    import { createApp, h } from 'vue'
    import App from './App.vue'
    createApp(App).mount('#app')

    在vue文件中,它是模块化加载的。


    1637477806326.png


    我们需要在处理vue文件的时候,对.vue后面携带的参数做处理。


    在此,我们简化只考虑template和sfc情况。


    else if (url.indexOf('.vue') > -1) {
    // 处理vue文件 App.vue?vue&type=style&index=0&lang.css
    // 读取vue内容
    const p = path.join(__dirname, url.split('?')[0])
    // compilerSfc解析sfc 获得ast
    const ret = compilerSfc.parse(fs.readFileSync(p, 'utf8'))
    // App.vue?type=template
    // 如果请求没有query.type 说明是sfc
    if (!query.type) {
    // 处理内部的script
    const scriptContent = ret.descriptor.script.content
    // 将默认导出配置对象转为常量
    const script = scriptContent.replace(
    'export default ',
    'const __script = ',
    )
    ctx.type = 'text/javascript'
    ctx.body = `
    ${rewriteImport(script)}
    // template解析转换为单独请求一个资源
    import {render as __render} from '${url}?type=template'
    __script.render = __render
    export default __script
    `
    } else if (query.type === 'template') {
    const tpl = ret.descriptor.template.content
    // 编译包含render模块
    const render = compilerDom.compile(tpl, { mode: 'module' }).code
    ctx.type = 'text/javascript'
    ctx.body = rewriteImport(render)
    }
    }

    处理图片路径


    直接从客户端读取返回。


     else if (url.endsWith('.png')) {
       ctx.body = fs.readFileSync('src' + url)
    }


    作者:ClyingDeng
    链接:https://juejin.cn/post/7033713960784248868

    收起阅读 »

    基于echarts 24种数据可视化展示,填充数据就可用,动手能力强的还可以DIY

    前言我们先跟随百度百科了解一下什么是“数据可视化 [1]”。   数据可视化,是关于数据视觉表现形式的科学技术研究。   其中,这种数据的视觉表现形式被定义为,一种以某种概要形式抽提出来的信息,包括相应信息单位的各种属性和变量。   它是一个处于不断演变之中...
    继续阅读 »

    前言

    我们先跟随百度百科了解一下什么是“数据可视化 [1]”。



      数据可视化,是关于数据视觉表现形式的科学技术研究。


      其中,这种数据的视觉表现形式被定义为,一种以某种概要形式抽提出来的信息,包括相应信息单位的各种属性和变量。


      它是一个处于不断演变之中的概念,其边界在不断地扩大。


    主要指的是技术上较为高级的技术方法,而这些技术方法允许利用图形、图像处理计算机视觉以及用户界面,通过表达、建模以及对立体、表面、属性以及动画的显示,对数据加以可视化解释。


    与立体建模之类的特殊技术方法相比,数据可视化所涵盖的技术方法要广泛得多。



      大家对展厅显示、客户导流、可视化汇报工作、对数据结果进行图形分析等这些业务场景都不陌生。


    很多后端大多都只提供接口数据,并没有去构建前端显示页面,一来是不专业(各种特效、自适应等),二来是公司有前端,用不到后端来写。


      但是暂时用到不代表我们不用,用的时候写不来怎么办?下面介绍24种数据可视化的demo,直接下载下来填充数据就可以跑起来,不满足的还可以DIY(演示地址+下载地址),yyds。


    演示地址

    注意:演示中如果有加载失败的,是环境问题,下载下来运行就好了。


    演示地址:https://www.xiongze.net/viewdata/index.html [2]


    现有的24种如下:


    大数据展示系统、物流数据概况系统、物流订单系统、物流信息系统、办税渠道监控平台、车辆综合管控平台、


    电子商务公共服务中心、各行业程序员中心、简洁大数据统计中心、警务平台大数据统计、农业监测大数据指挥舱、


    农业监控数据平台、社会治理运行分析云图、水质监测大数据中心、水质情况实时监测预警系统、


    物联网大数据统计平台、消防监控预警、销售数据报表中心、医疗大数据中心、营业数据统中心、


    智慧旅游综合服务平台、智慧社区内网比对平台、智慧物流服务中心、政务大数据共享交换平台。



    echarts图表库:Echarts提供了常规的折线图、柱状图、散点图、饼图、k线图,用于统计的盒形图,用于地理数据可视化的地图、热力图、线图,用于关系数据可视化的关系图、treemap、旭日图,多维数据可视化的平行坐标,还有用于 BI 的漏斗图,仪表盘,并且支持图与图之间的混搭。



    下面demo里面的图标颜色、样式都可以在 echarts官网-文档-配置项手册里面进行查看, 是支持通过修改里面的配置项里面的属性来达到项目需求,我们可以去进行查看修改。


    pic_a12bf32a.png

    下载地址

    Git下载链接:https://gitee.com/xiongze/viewdata.git [3]


    百度网盘下载链接:https://pan.baidu.com/s/1jgwK6BvrS2rmbkrtW2MpYA提取码:xion


    Demo示例(部分)

    1、总览

    pic_e6090ae5.png

    pic_d4d67ce3.png

    pic_f40b7776.png

    2、物流信息展示

    pic_8757023a.png

    3、车辆综合管控平台

    pic_cacec7a0.png

    4、农业监测大数据指挥舱

    pic_72002bf9.png

    5、水质情况实时监控预警中心

    pic_ab9ecf3a.png

    6、消防监控预警中心

    pic_94a47e47.png

    7、医疗大数据中心

    pic_9744b569.png

    8、物联网平台数据中心

    pic_50685f14.png

    更多……

    总共24种,这里就不一一展示了,大家下载下来就可以玩了。


    可视化应用

      数据可视化的开发和大部分项目开发一样,也是根据需求来根据数据维度或属性进行筛选,根据目的和用户群选用表现方式。同一份数据可以可视化成多种看起来截然不同的形式。



    • 有的可视化目标是为了观测、跟踪数据,所以就要强调实时性、变化、运算能力,可能就会生成一份不停变化、可读性强的图表。
    • 有的为了分析数据,所以要强调数据的呈现度、可能会生成一份可以检索、交互式的图表
    • 有的为了发现数据之间的潜在关联,可能会生成分布式的多维的图表。
    • 有的为了帮助普通用户或商业用户快速理解数据的含义或变化,会利用漂亮的颜色、动画创建生动、明了,具有吸引力的图表。
    • 还有的被用于教育、宣传或政治,被制作成海报、课件,出现在街头、广告手持、杂志和集会上。这类可视化拥有强大的说服力,使用强烈的对比、置换等手段,可以创造出极具冲击力自指人心的图像。在国外许多媒体会根据新闻主题或数据,雇用设计师来创建可视化图表对新闻主题进行辅助。

      数据可视化的应用价值,其多样性和表现力吸引了许多从业者,而其创作过程中的每一环节都有强大的专业背景支持。无论是动态还是静态的可视化图形,都为我们搭建了新的桥梁,让我们能洞察世界的究竟、发现形形色色的关系,感受每时每刻围绕在我们身边的信息变化,还能让我们理解其他形式下不易发掘的事物。


    参考文献

    [1]百度百科:数据可视化
    [2]演示地址:https://www.xiongze.net/viewdata/index.html
    [3]下载链接:https://gitee.com/xiongze/viewdata.git
    [4]数据可视化概念



    作者:熊泽-学习中的苦与乐
    来源:https://www.cnblogs.com/xiongze520/p/15588852.html


    收起阅读 »

    CommonJS和ES6 Module究竟是什么

    对于前端模块化总是稀里糊涂,今天深入学习一下前端模块化,彻底弄懂CommonJs和ES6 Module,希望本文可以给你带来帮助。 CommonJS 模块 CommonJS中规定每个文件是一个模块。将一个JS文件通过script标签插入页面与封装成Common...
    继续阅读 »

    对于前端模块化总是稀里糊涂,今天深入学习一下前端模块化,彻底弄懂CommonJs和ES6 Module,希望本文可以给你带来帮助。


    CommonJS


    模块


    CommonJS中规定每个文件是一个模块。将一个JS文件通过script标签插入页面与封装成CommonJS模块最大的不同在于,前者的顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境;而后者形成一个属于模块自身的作用域,所有的变量及函数只能自己访问,对外不可见。


    导出


    导出是一个模块向外暴露自身的唯一方式。在commonJS中,通过modul
    e.exports可以导出模块中的内容
    。下面的代码导出了一个对象,包含name和add属性。


    module.exports = {
    name: 'calculater',
    add: function(a, b){
    return a+b;
    }
    }

    为了书写方便,CommonJS也支持直接使用exports。


    exports.name = 'calculater';
    exports.add = function(a, b){
    return a+b;
    }

    exports可以理解为


    var module = {
    exports:{}
    };
    var exports = module.exports;

    注意错误的用法:



    1. 不要给exports直接赋值,否则导出会失效。如下代码,对exports赋值,使其指向新的对象。module.exports却仍然是原来的空对象,因此name属性并不会被导出。


    exports = {
    name: 'calculater'
    }


    1. 不恰当的把module.exports和exports混用。如下代码,先通过exports导出add属性,然后将module.exports重新赋值为另一个对象,将导致add属性丢失,最后导出只有name。


    exports.add = function(a,b){
    return a+b;
    }
    module.exports = {
    name: 'calculater'
    }

    导入


    在CommonJs中,使用require进行模块导入。


    const calculator = require('./calculator.js')
    let sum = calculator.add(2,3)

    注意:

    1. require的模块是第一次被加载,这时会首先执行该模块,然后导出内容
    2. require的模块曾被加载过,这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果。
    3. 对于不需要获取导出内容的模块,直接使用require即可。
    4. require函数可以接收表达式,借助这个特性可以动态地指定模块加载路径。

    const moduleName = ['a.js', 'b.js'];
    moduleNames.forEach(name => {
    require('./'+name)
    })

    ES6 Module


    模块


    ES6 Module是ES语法的一部分,它也是将每个文件作为一个模块,每个模块拥有自身的作用域。


    导出


    在ES6 Module中使用export命令来导出模块。export有两种形式:

    • 命名导出
    • 默认导出

    一个模块可以有多个命名导出,它有两种不同的写法:


    //写法1,将变量的声明和导出写在一行
    export const name = 'calculator'
    export const add = function(a, b){return a+b}

    //写法2,先进行变量的声明,然后在用同一个export语句导出。
    const name = 'calculator'
    const add = function(a, b){return a+b}
    export {name, add}

    与命名导出不同,模块的默认导出只能有一个。


    export default {
    name: 'calculator',
    add: function(a, b){
    return a+b
    }
    }

    导入


    ES6 Module中使用import语法导入模块。


    加载带有命名导出的模块

    有两种方式



    1. import后面要跟一对大括号,将导入的变量名包裹起来。并且这些变量名要与导出的变量名完全一致。


    //calculator.js
    const name = 'calculator'
    const add = function(a, b){return a+b}
    export {name, add}

    //index.js
    import {name, add} from './calculator.js'
    add(2,3)


    1. 采用整体导入的方式, 使用import * as myModule可以把所有导入的变量作为属性值添加到myModule中,从而减少对当前作用域的影响。


    import * as calculator from './calculator.js'
    console.log(calculator.add(2,3))
    console.log(calculator.name)

    加载默认导出的模块

    import后面直接跟变量名,并且这个名字可以自由指定


    //calculator.js
    export default {
    name: 'calculator',
    add: function(a, b){
    return a+b
    }
    }
    //index.js
    import calculator from './calculator.js'
    calculator.add(2,3)

    两种导入方式混合起来

    import React, {Component} from 'react'

    这里的React对应的是该模块的默认导出,Component则是其命名导出中的一个变量。


    CommonJS和ES6 Module的区别


    动态和静态

    • CommonJS是动态的模块结构,模块依赖关系的建立发生在代码的运行阶段
    • ES Module是静态的模块结构,在编译阶段就可以分析模块的依赖关系。


    相比于CommonJS,ES6 Module有如下优势:

    1. 死代码监测和排除
    2. 模块变量和类型检查
    3. 编译器优化

    值拷贝和动态映射


    在导入一个模块时,对于CommonJs来说,获取的是一份导出值的拷贝。而在ES6 Module中则是值的动态映射,并且这个映射是只读的。


    总结

  • CommonJS使用Module.exports或exports导出
  • CommonJS使用require()函数导入,该函数返回一个对象,包含导出的变量。
  • ES6 Module使用export导出,包括命名导出或者默认导出。
  • 命名导出是export后面跟一个大括号,括号里面包含导出的变量
  • 命名导出的另一种方式是export和变量声明在一行。
  • 默认导出是export default,只能有一个默认导出
  • ES6 Module导入使用import
  • 加载带有命名导出的模块,import后面要跟一对大括号,将导入的变量名包裹起来。并且这些变量名要与导出的变量名完全一致。
  • 采用整体导入的方式, 使用import * as 可以把所有导入的变量作为属性值添加到中,从而减少对当前作用域的影响。
  • 加载默认导出的模块,import后面直接跟变量名,并且这个名字可以自由指定。

  • 作者:邓惠子本尊

    链接:https://juejin.cn/post/7033651418934444063

    收起阅读 »

    如何从性能角度选择数组的遍历方式

    前言 本文讲述了JS常用的几种数组遍历方式以及性能分析对比。 如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~ 数组的方法 JavaScript发展到现在已经提供了许多数组的方法,下面这张图涵盖...
    继续阅读 »

    前言


    本文讲述了JS常用的几种数组遍历方式以及性能分析对比。


    如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~


    敖丙.png


    数组的方法


    JavaScript发展到现在已经提供了许多数组的方法,下面这张图涵盖了数组大部分的方法,这篇文章主要说一说数组的遍历方法,以及各自的性能,方法这么多,如何挑选性能最佳的方法对我们的开发有非常大的帮助。


    数组.png


    数组遍历的方法


    for


    标准的for循环语句,也是最传统的循环语句


    var arr = [1,2,3,4,5]
    for(var i=0;i<arr.length;i++){
    console.log(arr[i])
    }

    最简单的一种遍历方式,也是使用频率最高的,性能较好,但还能优化


    优化版for循环语句


    var arr = [1,2,3,4,5]
    for(var i=0,len=arr.length;i<len;i++){
    console.log(arr[i])
    }

    使用临时变量,将长度缓存起来,避免重复获取数组长度,尤其是当数组长度较大时优化效果才会更加明显。


    这种方法基本上是所有循环遍历方法中性能最高的一种


    forEach


    普通forEach


    对数组中的每一元素运行给定的函数,没有返回值,常用来遍历元素


    var arr5 = [10,20,30]
    var result5 = arr5.forEach((item,index,arr)=>{
    console.log(item)
    })
    console.log(result5)
    /*
    10
    20
    30
    undefined 该方法没有返回值
    */

    数组自带的foreach循环,使用频率较高,实际上性能比普通for循环弱


    原型forEach


    由于foreach是Array型自带的,对于一些非这种类型的,无法直接使用(如NodeList),所以才有了这个变种,使用这个变种可以让类似的数组拥有foreach功能。


    const nodes = document.querySelectorAll('div')
    Array.prototype.forEach.call(nodes,(item,index,arr)=>{
    console.log(item)
    })

    实际性能要比普通foreach弱


    for...in


    任意顺序遍历一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。


    一般常用来遍历对象,包括非整数类型的名称和继承的那些原型链上面的属性也能被遍历。像 Array和 Object使用内置构造函数所创建的对象都会继承自Object.prototype和String.prototype的不可枚举属性就不能遍历了.


    var arr = [1,2,3,4,5]
    for(var i in arr){
    console.log(i,arr[i])
    } //这里的i是对象属性,也就是数组的下标
    /**
    0 1
    1 2
    2 3
    3 4
    4 5 **/

    大部分人都喜欢用这个方法,但它的性能却不怎么好


    for...of(不能遍历对象)



    在可迭代对象(具有 iterator 接口)(Array,Map,Set,String,arguments)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句,不能遍历对象



    let arr=["前端","南玖","ssss"];
    for (let item of arr){
    console.log(item)
    }
    //前端 南玖 ssss

    //遍历对象
    let person={name:"南玖",age:18,city:"上海"}
    for (let item of person){
    console.log(item)
    }
    // 我们发现它是不可以的 我们可以搭配Object.keys使用
    for(let item of Object.keys(person)){
    console.log(person[item])
    }
    // 南玖 18 上海

    这种方式是es6里面用到的,性能要好于forin,但仍然比不上普通for循环


    map



    map: 只能遍历数组,不能中断,返回值是修改后的数组。



    let arr=[1,2,3];
    const res = arr.map(item=>{
    return item+1
    })
    console.log(res) //[2,3,4]
    console.log(arr) // [1,2,3]

    every


    对数组中的每一运行给定的函数,如果该函数对每一项都返回true,则该函数返回true


    var arr = [10,30,25,64,18,3,9]
    var result = arr.every((item,index,arr)=>{
    return item>3
    })
    console.log(result) //false

    some


    对数组中的每一运行给定的函数,如果该函数有一项返回true,就返回true,所有项返回false才返回false


    var arr2 = [10,20,32,45,36,94,75]
    var result2 = arr2.some((item,index,arr)=>{
    return item<10
    })
    console.log(result2) //false

    reduce


    reduce()方法对数组中的每个元素执行一个由你提供的reducer函数(升序执行),将其结果汇总为单个返回值


    const array = [1,2,3,4]
    const reducer = (accumulator, currentValue) => accumulator + currentValue;

    // 1 + 2 + 3 + 4
    console.log(array1.reduce(reducer));

    filter


    对数组中的每一运行给定的函数,会返回满足该函数的项组成的数组


    // filter  返回满足要求的数组项组成的新数组
    var arr3 = [3,6,7,12,20,64,35]
    var result3 = arr3.filter((item,index,arr)=>{
    return item > 3
    })
    console.log(result3) //[6,7,12,20,64,35]

    性能测试


    工具测试


    使用工具测试性能分析结果如下图所示


    性能测试1.png


    手动测试


    我们也可以自己用代码测试:


    //测试函数
    function clecTime(fn,fnName){
    const start = new Date().getTime()
    if(fn) fn()
    const end = new Date().getTime()
    console.log(`${fnName}执行耗时:${end-start}ms`)
    }

    function forfn(){
    let a = []
    for(var i=0;i<arr.length;i++){
    // console.log(i)
    a.push(arr[i])
    }
    }
    clecTime(forfn, 'for') //for执行耗时:106ms

    function forlenfn(){
    let a = []
    for(var i=0,len=arr.length;i<len;i++){
    a.push(arr[i])
    }
    }
    clecTime(forlenfn, 'for len') //for len执行耗时:95ms

    function forEachfn(){
    let a = []
    arr.forEach(item=>{
    a.push[item]
    })
    }
    clecTime(forEachfn, 'forEach') //forEach执行耗时:201ms

    function forinfn(){
    let a = []
    for(var i in arr){
    a.push(arr[i])
    }
    }
    clecTime(forinfn, 'forin') //forin执行耗时:2584ms (离谱)

    function foroffn(){
    let a = []
    for(var i of arr){
    a.push(i)
    }
    }
    clecTime(foroffn, 'forof') //forof执行耗时:221ms

    // ...其余可自行测试

    结果分析


    经过工具与手动测试发现,结果基本一致,数组遍历各个方法的速度:传统的for循环最快,for-in最慢



    for-len > for > for-of > forEach > map > for-in



    javascript原生遍历方法的建议用法:



    • for循环遍历数组

    • for...in遍历对象

    • for...of遍历类数组对象(ES6)

    • Object.keys()获取对象属性名的集合


    为何for… in会慢?


    因为for … in语法是第一个能够迭代对象键的JavaScript语句,循环对象键({})与在数组([])上进行循环不同,引擎会执行一些额外的工作来跟踪已经迭代的属性。因此不建议使用for...in来遍历数组



    作者:南玖
    链接:https://juejin.cn/post/7033578966887694373

    收起阅读 »

    async/await 优雅永不过时

    引言 async/await是非常棒的语法糖,可以说他是解决异步问题的最终解决方案。从字面意思来理解。async 是异步的意思,而 await 是 等待 ,所以理解 async用于申明一个function是异步的,而 await 用于等待一个异步方法执行完成...
    继续阅读 »

    引言



    async/await是非常棒的语法糖,可以说他是解决异步问题的最终解决方案。从字面意思来理解。async 是异步的意思,而 await 是 等待 ,所以理解 async用于申明一个function是异步的,而 await 用于等待一个异步方法执行完成。



    src=http___pic.962.net_up_2019-12_15767448543514326.png&refer=http___pic.962.jpeg


    async作用



    async声明function是一个异步函数,返回一个promise对象,可以使用 then 方法添加回调函数。async函数内部return语句返回的值,会成为then方法回调函数的参数。



    async function test() {
    return 'test';
    }
    console.log(test); // [AsyncFunction: test] async函数是[`AsyncFunction`]构造函数的实例
    console.log(test()); // Promise { 'test' }

    // async返回的是一个promise对象
    test().then(res=>{
    console.log(res); // test
    })

    // 如果async函数没有返回值 async函数返回一个undefined的promise对象
    async function fn() {
    console.log('没有返回');
    }
    console.log(fn()); // Promise { undefined }

    // 可以看到async函数返回值和Promise.resolve()一样,将返回值包装成promise对象,如果没有返回值就返回undefined的promise对象

    await



    await 操作符只能在异步函数 async function 内部使用。如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果,也就是说它会阻塞后面的代码,等待 Promise 对象结果。如果等待的不是 Promise 对象,则返回该值本身。



    async function test() {
    return new Promise((resolve)=>{
    setTimeout(() => {
    resolve('test 1000');
    }, 1000);
    })
    }
    function fn() {
    return 'fn';
    }

    async function next() {
    let res0 = await fn(),
    res1 = await test(),
    res2 = await fn();
    console.log(res0);
    console.log(res1);
    console.log(res2);
    }
    next(); // 1s 后才打印出结果 为什么呢 就是因为 res1在等待promise的结果 阻塞了后面代码。

    错误处理



    如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject



    async function test() {
    await Promise.reject('错误了')
    };

    test().then(res=>{
    console.log('success',res);
    },err=>{
    console.log('err ',err);
    })
    // err 错误了


    防止出错的方法,也是将其放在try...catch代码块之中。



    async function test() {
    try {
    await new Promise(function (resolve, reject) {
    throw new Error('错误了');
    });
    } catch(e) {
    console.log('err', e)
    }
    return await('成功了');
    }


    多个await命令后面的异步操作,如果不存在继发关系(即互不依赖),最好让它们同时触发。



    let foo = await getFoo();
    let bar = await getBar();
    // 上面这样写法 getFoo完成以后,才会执行getBar

    // 同时触发写法 ↓

    // 写法一
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);

    // 写法二
    let fooPromise = getFoo();
    let barPromise = getBar();
    let foo = await fooPromise;
    let bar = await barPromise;

    async/await优点



    async/await的优势在于处理由多个Promise组成的 then 链,在之前的Promise文章中提过用then处理回调地狱的问题,async/await相当于对promise的进一步优化。
    假设一个业务,分多个步骤,且每个步骤都是异步的,而且依赖上个步骤的执行结果。



    // 假设表单提交前要通过俩个校验接口

    async function check(ms) { // 模仿异步
    return new Promise((resolve)=>{
    setTimeout(() => {
    resolve(`check ${ms}`);
    }, ms);
    })
    }
    function check1() {
    console.log('check1');
    return check(1000);
    }
    function check2() {
    console.log('check2');
    return check(2000);
    }

    // -------------promise------------
    function submit() {
    console.log('submit');
    // 经过俩个校验 多级关联 promise传值嵌套较深
    check1().then(res1=>{
    check2(res1).then(res2=>{
    /*
    * 提交请求
    */
    })
    })
    }
    submit();

    // -------------async/await-----------
    async function asyncAwaitSubmit() {
    let res1 = await check1(),
    res2 = await check2(res1);
    console.log(res1, res2);
    /*
    * 提交请求
    */
    }



    原理



    async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。



    async function fn(args) {
    // ...
    }

    // 等同于

    function fn(args) {
    return spawn(function* () {
    // ...
    });
    }

    /*
    * Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。
    * 异步操作需要暂停的地方,都用 yield 语句注明
    * 调用 Generator 函数,返回的是指针对象(这是它和普通函数的不同之处),。调用指针对象的 next 方法,会移动内部指针。
    * next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
    */

    // 了解generator的用法
    function* Generator() {
    yield '1';
    yield Promise.resolve(2);
    return 'ending';
    }

    var gen = Generator(); // 返回指针对象 Object [Generator] {}

    let res1 = gen.next();
    console.log(res1); // 返回当前阶段的值 { value: '1', done: false }

    let res2 = gen.next();
    console.log(res2); // 返回当前阶段的值 { value: Promise { 2 }, done: false }

    res2.value.then(res=>{
    console.log(res); // 2
    })

    let res3 = gen.next();
    console.log(res3); // { value: 'ending', done: true }

    let res4 = gen.next();
    console.log(res4); // { value: undefined, done: true }



    Generator实现async函数



    // 接受一个Generator函数作为参数
    function spawn(genF) {
    // 返回一个函数
    return function() {
    // 生成指针对象
    const gen = genF.apply(this, arguments);
    // 返回一个promise
    return new Promise((resolve, reject) => {
    // key有next和throw两种取值,分别对应了gen的next和throw方法
    // arg参数则是用来把promise resolve出来的值交给下一个yield
    function step(key, arg) {
    let result;

    // 监控到错误 就把promise给reject掉 外部通过.catch可以获取到错误
    try {
    result = gen[key](arg)
    } catch (error) {
    return reject(error)
    }

    // gen.next() 返回 { value, done } 的结构
    const { value, done } = result;

    if (done) {
    // 如果已经完成了 就直接resolve这个promise
    return resolve(value)
    } else {
    // 除了最后结束的时候外,每次调用gen.next()
    return Promise.resolve(
    // 这个value对应的是yield后面的promise
    value
    ).then((val)=>step("next", val),(err) =>step("throw", err))
    }
    }
    step("next")
    })
    }
    }


    测试



    function fn(nums) {
    return new Promise(resolve => {
    setTimeout(() => {
    resolve(nums)
    }, 1000)
    })
    }
    // async 函数
    async function testAsync() {
    let res1 = await fn(1);
    console.log(res1); // 1
    let res2 = await fn(2);
    console.log(res2); // 2
    return res2;
    }
    let _res = testAsync();
    console.log('testAsync-res',_res); // Promise
    _res.then(v=>console.log('testAsync-res',v)) // 2

    // Generator函数
    function* gen() {
    let res1 = yield fn(3);
    console.log(res1); // 3
    let res2 = yield fn(4);
    console.log(res2); // 4
    // let res3 = yield Promise.reject(5);
    // console.log(res3);
    return res2;
    }

    let _res2 = spawn(gen)();
    console.log('gen-res',_res2); // Promise

    _res2
    .then(v=>console.log('gen-res',v)) // 4
    .catch(err=>{console.log(err)}) // res3 执行会抛出异常



    总结



    async/await语法糖可以让异步代码变得更清晰,可读性更高,所以快快卷起来吧。Generator有兴趣的可以了解一下。


    作者:小撕夜
    链接:https://juejin.cn/post/7033647059378896903

    收起阅读 »

    当老婆又让我下载一个腾讯视频时

    我们结婚了! 是的,这次不是女朋友啦,是老婆了! 时隔将近一个月,老婆又让我给她下载腾讯视频,如果按照上次探索的内容来下载的话,倒是可以一步步下载,合并,不过很麻烦,程序员不都是为了解决麻烦的吗,这么麻烦的步骤,有没有简单点呢。有!当然有,有很多简单的工具,...
    继续阅读 »

    我们结婚了!


    是的,这次不是女朋友啦,是老婆了!


    WechatIMG58.jpeg


    时隔将近一个月,老婆又让我给她下载腾讯视频,如果按照上次探索的内容来下载的话,倒是可以一步步下载,合并,不过很麻烦,程序员不都是为了解决麻烦的吗,这么麻烦的步骤,有没有简单点呢。有!当然有,有很多简单的工具,上一期很多朋友给我推荐了各种工具,这里我没有一一查看,我可以列举出来,有需要的同学可以尝试看看,不想尝试的也可以看看我下面为了偷懒准备的方法。


    心路历程


    最初,我是想着把我之前的步骤,用无头浏览器加载一遍,然后用代码去下载ts片段,然后在机器上用ffmpeg进行合并,但是仿佛还是有些许麻烦,然后我就去npm搜了一下关键词:m3u8tomp4


    image.png


    m3u8-to-mp4


    于是我点击了第一个包:m3u8-to-mp4


    image.png


    纳尼?这个包就一个版本,用了3年,而且周下载量还不少


    image.png


    于是我想着这个包要么就是很牛逼,一次性解决了m3u8转mp4的问题,一劳永逸,所以3年没更新过了,要么就是作者忘记了自己还有这个包


    于是我就用了这个3年没人维护没人更新的包。


    用法也很简单,就copy example 就好了。代码如下:



    var m3u8ToMp4 = require("m3u8-to-mp4");
    var converter = new m3u8ToMp4();
    (async function() {
    var url = "https://apd-666945ea97106754c57813479384d30c.v.smtcdns.com/omts.tc.qq.com/AofRtrergNwkAhpHs4RrxH2_9DWLWSG8xjDMZDQoFGyY/uwMROfz2r55kIaQXGdGnC2deOm68BrdPrRewQlOzrMAbixNO/svp_50001/cKAgRbCb6Re4BpHkI-IlK_KN1VJ8gQVK2sZtkHEY3vQUIlxVz7AtWmVJRifZrrPfozBS0va-SSJFhQhOFSKVNmqVi165fCQJoPl8V5QZBcGZBDpSIfrpCImJKryoZOdR5C0oGYkzIW77I4his7UkPY9Iwmf1QWjaHwNV2hpKv3aD9ysL_-YByA/szg_9276_50001_0bc3uuaa2aaafmaff4e3ijqvdjodbwsqadka.f304110.ts.m3u8?ver=4"
    await converter
    .setInputFile(url)
    .setOutputFile("dummy.mp4")
    .start();
    console.log("File converted");
    })();

    视频地址是 v.qq.com/x/page/v331…


    然后视频就转换成功了,哇哦!


    so easy ! so beautiful!


    原理


    带着好奇,我想看下这个包是如何进行转换的


    于是我点进去m3u8-to-mp4这个包文件


    包文件内容如下


    image.png


    只有一个文件?


    然后我打开了index.js ,只有64行😂


    全部代码如下


    /**
    * @description M3U8 to MP4 Converter
    * @author Furkan Inanc
    * @version 1.0.0
    */

    let ffmpeg = require("fluent-ffmpeg");

    /**
    * A class to convert M3U8 to MP4
    * @class
    */
    class m3u8ToMp4Converter {
    /**
    * Sets the input file
    * @param {String} filename M3U8 file path. You can use remote URL
    * @returns {Function}
    */
    setInputFile(filename) {
    if (!filename) throw new Error("You must specify the M3U8 file address");
    this.M3U8_FILE = filename;

    return this;
    }

    /**
    * Sets the output file
    * @param {String} filename Output file path. Has to be local :)
    * @returns {Function}
    */
    setOutputFile(filename) {
    if (!filename) throw new Error("You must specify the file path and name");
    this.OUTPUT_FILE = filename;

    return this;
    }

    /**
    * Starts the process
    */
    start() {
    return new Promise((resolve, reject) => {
    if (!this.M3U8_FILE || !this.OUTPUT_FILE) {
    reject(new Error("You must specify the input and the output files"));
    return;
    }

    ffmpeg(this.M3U8_FILE)
    .on("error", error => {
    reject(new Error(error));
    })
    .on("end", () => {
    resolve();
    })
    .outputOptions("-c copy")
    .outputOptions("-bsf:a aac_adtstoasc")
    .output(this.OUTPUT_FILE)
    .run();
    });
    }
    }

    module.exports = m3u8ToMp4Converter;


    大致看了下这个包做的内容,就是检测并设置了输入链接,和输出文件名,然后调用了fluent-ffmpeg这个库


    ???


    站在巨人的肩膀上吗,自己就包了一层😂


    接着看fluent-ffmpeg这个包,是如何实现转换的


    image.png


    然后我们在这个包文件夹下面搜索.run方法,用来定位到具体执行的地方


    image.png


    凭借多年的cv经验,感觉应该是processor.js这个文件里的,然后我们打开这个文件,定位到该方法处


    image.png


    往下看代码,我注意到了这段代码


    image.png


    因为都是基于ffmpeg这个大爹来做的工具,所以最底层也都是去调用ffmpeg的command


    image.png


    这几个if判断都是对结果进行捕获异常,那么我们在这个核心代码的地方打个端点看下


    image.png


    貌似是调用了几个命令行参数


    于是我就有了一个大胆的想法!


    image.png


    是的,我手动在终端将这个命令拼接起来,用我的本地命令去跑应该也没问题的吧,于是我尝试了一下


    image.png


    没想到还成功了,其实成功是必然的,因为都是借助来ffmpeg这个包,只不过我是手动去操作,框架是代码去拼接这个命令而已


    剩余的时间里,我看了看fluent-ffmpeg的其他代码,它做的东西比较多,比如去本查找ffmpeg的绝对路径啊,对ffmpeg的结果进行捕获异常信息等...



    作者:小松同学哦
    链接:https://juejin.cn/post/7033652317958176799

    收起阅读 »

    【前端工程化】- 结合代码实践,全面学习前端工程化

    前言前端工程化,简而言之就是软件工程 前端,以自动化的形式呈现。就个人理解而言:前端工程化,从开发阶段到代码发布生产环境,包含了以下几个内容:开发构建测试部署性能规范 下面我们根据上述几个内容,选择有代表性的几个方面进行深入学习前端工程化。脚手架脚手...
    继续阅读 »

    前言

    前端工程化,简而言之就是软件工程 前端,以自动化的形式呈现。就个人理解而言:前端工程化,从开发阶段到代码发布生产环境,包含了以下几个内容:

    • 开发
    • 构建
    • 测试
    • 部署
    • 性能
    • 规范

    image.png 下面我们根据上述几个内容,选择有代表性的几个方面进行深入学习前端工程化。


    脚手架

    脚手架是什么?(What)

    现在流行的前端脚手架基本上都是基于NodeJs编写,比如我们常用的Vue-CLI,比较火的create-react-app,还有Dva-CLI等。

    脚手架存在的意义?(Why)

    随着前端工程化的概念越来越深入人心,脚手架的出现就是为减少重复性工作而引入的命令行工具,摆脱ctrl cctrl v,此话怎讲? 现在新建一个前端项目,已经不是在html头部引入css,尾部引入js那么简单的事了,css都是采用Sass或则Less编写,在js中引入,然后动态构建注入到html中;除了学习基本的jscss语法和热门框架,还需要学习构建工具webpackbabel这些怎么配置,怎么起前端服务,怎么热更新;为了在编写过程中让编辑器帮我们查错以及更加规范,我们还需要引入ESlint;甚至,有些项目还需要引入单元测试(Jest)。对于一个更入门的人来说,这无疑会让人望而却步。而前端脚手架的出现,就让事情简单化,一键命令,新建一个工程,再执行两个npm命令,跑起一个项目。在入门时,无需关注配置什么的,只需要开心的写代码就好。

    如何实现一个新建项目脚手架(基于koa)?(How)

    先梳理下实现思路

    我们实现脚手架的核心思想就是自动化思维,将重复性的ctrl cctrl v创建项目,用程序来解决。解决步骤如下:

    1. 创建文件夹(项目名)
    2. 创建 index.js
    3. 创建 package.json
    4. 安装依赖

    1. 创建文件夹

    创建文件夹前,需要先删除清空:


    // package.json
    {
    ...
    "scripts": {
    "test": "rm -rf ./haha && node --experimental-modules index.js"
    }
    ...
    }

    创建文件夹:我们通过引入 nodejsfs 模块,使用 mkdirSync API来创建文件夹。


    // index.js
    import fs from 'fs';

    function getRootPath() {
    return "./haha";
    }

    // 生成文件夹
    fs.mkdirSync(getRootPath());

    2. 创建 index.js


    创建 index.js:使用 nodejsfs 模块的 writeFileSync API 创建 index.js 文件:


    // index.js
    fs.writeFileSync(getRootPath() + "/index.js", createIndexTemplate(inputConfig));

    接着我们来看看,动态模板如何生成?我们最理想的方式是通过配置来动态生成文件模板,那么具体来看看 createIndexTemplate 实现的逻辑吧。


    // index.js
    import fs from 'fs';
    import { createIndexTemplate } from "./indexTemplate.js";

    // input
    // process
    // output
    const inputConfig = {
    middleWare: {
    router: true,
    static: true
    }
    }
    function getRootPath() {
    return "./haha";
    }
    // 生成文件夹
    fs.mkdirSync(getRootPath());
    // 生成 index.js 文件
    fs.writeFileSync(getRootPath() + "/index.js", createIndexTemplate(inputConfig));

    // indexTemplate.js
    import ejs from "ejs";
    import fs from "fs";
    import prettier from "prettier";// 格式化代码
    // 问题驱动
    // 模板
    // 开发思想 - 小步骤的开发思想
    // 动态生成代码模板
    export function createIndexTemplate(config) {
    // 读取模板
    const template = fs.readFileSync("./template/index.ejs", "utf-8");

    // ejs渲染
    const code = ejs.render(template, {
    router: config.middleware.router,
    static: config.middleware.static,
    port: config.port,
    });

    // 返回模板
    return prettier.format(code, {
    parser: "babel",
    });
    }

    // template/index.ejs
    const Koa = require("koa");
    <% if (router) { %>
    const Router = require("koa-router");
    <% } %>


    <% if (static) { %>
    const serve = require("koa-static");
    <% } %>

    const app = new Koa();

    <% if (router) { %>
    const router = new Router();
    router.get("/", (ctx) => {
    ctx.body = "hello koa-setup-heihei";
    });
    app.use(router.routes());
    <% } %>

    <% if (static) { %>
    app.use(serve(__dirname + "/static"));
    <% } %>

    app.listen(<%= port %>, () => {
    console.log("open server localhost:<%= port %>");
    });

    3. 创建 package.json


    创建 package.json 文件,实质是和创建 index.js 类似,都是采用动态生成模板的思路来实现,我们来看下核心方法 createPackageJsonTemplate 的实现代码:


    // packageJsonTemplate.js
    function createPackageJsonTemplate(config) {
    const template = fs.readFileSync("./template/package.ejs", "utf-8");

    const code = ejs.render(template, {
    packageName: config.packageName,
    router: config.middleware.router,
    static: config.middleware.static,
    });

    return prettier.format(code, {
    parser: "json",
    });
    }

    // template/package.ejs
    {
    "name": "<%= packageName %>",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "dependencies": {
    "koa": "^2.13.1"
    <% if (router) { %>
    ,"koa-router": "^10.1.1"
    <% } %>

    <% if (static) { %>
    ,"koa-static": "^5.0.0"
    }
    <% } %>
    }

    4. 安装依赖


    要自动安装依赖,我们可以使用 nodejsexeca 库执行 yarn 安装命令:


    execa("yarn", {
    cwd: getRootPath(),
    stdio: [2, 2, 2],
    });

    至此,我们已经用 nodejs 实现了新建项目的脚手架了。最后我们可以重新梳理下可优化点将其升级完善。比如将程序配置升级成 GUI 用户配置(用户通过手动选择或是输入来传入配置参数,例如项目名)。




    编译构建

    编译构建是什么?


    构建,或者叫作编译,是前端工程化体系中功能最繁琐、最复杂的模块,承担着从源代码转化为宿主浏览器可执行的代码,其核心是资源的管理。前端的产出资源包括JS、CSS、HTML等,分别对应的源代码则是:



    • 领先于浏览器实现的ECMAScript规范编写的JS代码(ES6/7/8...)。

    • LESS/SASS预编译语法编写的CSS代码。

    • Jade/EJS/Mustache等模板语法编写的HTML代码。


    以上源代码是无法在浏览器环境下运行的,构建工作的核心便是将其转化为宿主可执行代码,分别对应:



    • ECMAScript规范的转译。

    • CSS预编译语法转译。

    • HTML模板渲染。


    那么下面我们就一起学习下如今3大主流构建工具:Webpack、Rollup、Vite。


    Webpack


    image.png


    Webpack原理


    想要真正用好 Webpack 编译构建工具,我们需要先来了解下它的工作原理。Webpack 编译项目的工作机制是,递归找出所有依赖模块,转换源码为浏览器可执行代码,并构建输出bundle。具体工作流程步骤如下:



    1. 初始化参数:取配置文件和shell脚本参数并合并

    2. 开始编译:用上一步得到的参数初始化compiler对象,执行run方法开始编译

    3. 确定入口:根据配置中的entry,确定入口文件

    4. 编译模块:从入口文件出发,递归遍历找出所有依赖模块的文件

    5. 完成模块编译:使用loader转译所有模块,得到转译后的最终内容和依赖关系

    6. 输出资源:根据入口和模块依赖关系,组装成一个个chunk,加到输出列表

    7. 输出完成:根据配置中的output,确定输出路径和文件名,把文件内容写入输出目录(默认是dist


    Webpack实践


    1. 基础配置


    【entry】



    入口配置,webpack 编译构建时能找到编译的入口文件,进而构建内部依赖图。



    【output】



    输出配置,告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。



    【loader】



    模块转换器,loader 可以处理浏览器无法直接运行的文件模块,转换为有效模块。比如:css-loader和style-loader处理样式;url-loader和file-loader处理图片。



    【plugin】



    插件,解决 loader 无法实现的问题,在 webpack 整个构建生命周期都可以扩展插件。比如:打包优化,资源管理,注入环境变量等。



    下面是 webpack 基本配置的简单示例:


    const path = require("path");

    module.exports = {
    mode: "development",
    entry: "./src/index.js",
    output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
    },
    devServer: {
    static: "./dist",
    },
    module: {
    rules: [
    {
    // 匹配什么样子的文件
    test: /\.css$/i,
    // 使用loader , 从后到前执行
    use: ["style-loader", "css-loader"],
    }
    ],
    },
    };


    参考webpack官网:webpack.docschina.org/concepts/

    (注意:使用不同版本的 webpack 切换对应版本的文档哦)



    2. 性能优化


    • 编译速度优化

    【检测编译速度】


    寻找检测编译速度的工具,比如 speed-measure-webpack-plugin插件 ,用该插件分析每个loader和plugin执行耗时具体情况。


    【优化编译速度该怎么做呢?】




    1. 减少搜索依赖的时间



    • 配置 loader 匹配规则 test/include/exclue,缩小搜索范围,即可减少搜索时间



    1. 减少解析转换的时间



    • noParse配置,精准过滤不用解析的模块

    • loader性能消耗大的,开启多进程



    1. 减少构建输出的时间



    • 压缩代码,开启多进程



    1. 合理使用缓存策略



    • babel-loader开启缓存

    • 中间模块启用缓存,比如使用 hard-source-webpack-plugin


    具体优化措施可参考:webpack性能优化的一段经历|项目复盘



    • 体积优化

    【检测包体积大小】


    寻找检测构建后包体积大小的工具,比如 webpack-bundle-analyzer插件 ,用该插件分析打包后生成Bundle的每个模块体积大小。


    【优化体积该怎么做呢?】




    1. bundle去除第三方依赖

    2. 擦除无用代码 Tree Shaking


    具体优化措施参考:webpack性能优化的一段经历|项目复盘



    Rollup


    Rollup概述


    Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。并且可以对代码模块使用新的标准化格式,比如CommonJSes module


    Rollup原理


    我们先来了解下 Rollup 原理,其主要工作机制是:



    1. 确定入口文件

    2. 使用 Acorn 读取解析文件,获取抽象语法树 AST

    3. 分析代码

    4. 生成代码,输出


    Rollup 相对 Webpack 而言,打包出来的包会更加轻量化,更适用于类库打包,因为内置了 Tree Shaking 机制,在分析代码阶段就知晓哪些文件引入并未调用,打包时就会自动擦除未使用的代码。



    Acorn 是一个 JavaScript 语法解析器,它将 JavaScript 字符串解析成语法抽象树 AST 如果想了解 AST 语法树可以点下这个网址astexplorer.net/



    Rollup实践


    【input】



    入口文件路径



    【output】



    输出文件、输出格式(amd/es6/iife/umd/cjs)、sourcemap启用等。



    【plugin】



    各种插件使用的配置



    【external】



    提取外部依赖



    【global】



    配置全局变量



    下面是 Rollup 基础配置的简单示例:


    import commonjs from "@rollup/plugin-commonjs";
    import resolve from "@rollup/plugin-node-resolve";
    // 解析json
    import json from '@rollup/plugin-json'
    // 压缩代码
    import { terser } from 'rollup-plugin-terser';
    export default {
    input: "src/main.js",
    output: [{
    file: "dist/esmbundle.js",
    format: "esm",
    plugins: [terser()]
    },{
    file: "dist/cjsbundle.js",
    format: "cjs",
    }],
    // commonjs 需要放到 transform 插件之前,
    // 但是又个例外, 是需要放到 babel 之后的
    plugins: [json(), resolve(), commonjs()],
    external: ["vue"]
    };

    Vite

    Vite概述


    Vite,相比 Webpack、Rollup 等工具,极大地改善了前端开发者的开发体验,编译速度极快。


    Vite原理


    为什么 Vite 开发编译速度极快?我们就先来探究下它的原理吧。
    image.png
    由上图可见,Vite 原理是利用现代主流浏览器支持原生的 ESM 规范,配合 server 做拦截,把代码编译成浏览器支持的。
    image.png


    Vite实践体验


    我们可以搭建一个Hello World版的Vite项目来感受下飞快的开发体验:



    注意:Vite 需要 Node.js 版本 >= 12.0.0。



    使用 NPM:


    $ npm init vite@latest

    使用 Yarn:


    $ yarn create vite

    image.png
    上图是Vite项目的编译时间,363ms,开发秒级编译的体验,真的是棒棒哒!


    3种构建工具综合对比





































    WebpackRollupVite
    编译速度一般较快最快
    HMR热更新支持需要额外引入插件支持
    Tree Shaking需要额外配置支持支持
    适用范围项目打包类库打包不考虑兼容性的项目



    测试

    当我们前端项目越来越庞大时,开发迭代维护成本就会越来越高,数十个模块相互调用错综复杂,为了提高代码质量和可维护性,就需要写测试了。下面就给大家具体介绍下前端工程经常做的3类测试。

    单元测试


    单元测试,是对最小可测试单元(一般为单个函数、类或组件)进行检查和验证。

    做单元测试的框架有很多,比如 Mocha断言库ChaiSinonJest等。我们可以先选择 jest 来学习,因为它集成了 Mochachaijsdomsinon 等功能。接下来,我们一起看看 jest 怎么写单元测试吧?



    1. 根据正确性写测试,即正确的输入应该有正常的结果。

    2. 根据错误性写测试,即错误的输入应该是错误的结果。


    以验证求和函数为例:


    // add函数
    module.exports = (a,b) => {
    return a+b;
    }

    // 正确性测试验证
    const add = require('./add.js');

    test('should 1+1 = 2', ()=> {
    // 准备测试数据 -> given
    const a = 1;
    const b = 1;
    // 触发测试动作 -> when
    const r = add(a,b);
    // 验证 -> then
    expect(r).toBe(2);
    })

    image.png


    // 错误性测试验证
    test('should 1+1 = 2', ()=> {
    // 准备测试数据 -> given
    const a = 1;
    const b = 2;
    // 触发测试动作 -> when
    const r = add(a,b)
    // 验证 -> then
    expect(r).toBe(2);
    })

    image.png


    组件测试

    组件测试,主要是针对某个组件功能进行测试,这就相对困难些,因为很多组件涉及了DOM操作。组件测试,我们可以借助组件测试框架来做,比如使用 Cypress(它可以做组件测试,也可以做 e2e 测试)。我们就先来看看组件测试怎么做?


    以 vue3 组件测试为例:



    1. 我们先建好 vue3 + vite 项目,编写测试组件

    2. 再安装 cypress 环境

    3. cypress/component 编写组件测试脚本文件

    4. 执行 cypress open-ct 命令,启动 cypress component testing 的服务运行 xx.spec.js 测试脚本,便能直观看到单个组件自动执行操作逻辑


    // Button.vue 组件

    <template>
    <div>Button测试</div>
    </template>
    <script>
    export default {
    }
    </script>
    <style>
    </style>

    // cypress/plugin/index.js 配置

    const { startDevServer } = require('@cypress/vite-dev-server')
    // eslint-disable-next-line no-unused-vars
    module.exports = (on, config) => {
    // `on` is used to hook into various events Cypress emits
    // `config` is the resolved Cypress config
    on('dev-server:start', (options) => {
    const viteConfig = {
    // import or inline your vite configuration from vite.config.js
    }
    return startDevServer({ options, viteConfig })
    })
    return config;
    }

    // cypress/component/Button.spec.js Button组件测试脚本

    import { mount } from "@cypress/vue";
    import Button from "../../src/components/Button.vue";

    describe("Button", () => {
    it("should show button", () => {
    // 挂载button
    mount(Button);

    cy.contains("Button");
    });
    });

    e2e测试


    e2e 测试,也叫端到端测试,主要是模拟用户对页面进行一系列操作并验证其是否符合预期。我们同样也可以使用 cypress 来做 e2e 测试,具体怎么做呢?


    以 todo list 功能验证为例:



    1. 我们先建好 vue3 + vite 项目,编写测试组件

    2. 再安装 cypress 环境

    3. cypress/integration 编写组件测试脚本文件

    4. 执行 cypress open 命令,启动 cypress 的服务,选择 xx.spec.js 测试脚本,便能直观看到模拟用户的操作流程


    // cypress/integration/todo.spec.js todo功能测试脚本

    describe('example to-do app', () => {
    beforeEach(() => {
    cy.visit('https://example.cypress.io/todo')
    })

    it('displays two todo items by default', () => {
    cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
    cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
    })

    it('can add new todo items', () => {
    const newItem = 'Feed the cat'
    cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)

    cy.get('.todo-list li')
    .should('have.length', 3)
    .last()
    .should('have.text', newItem)
    })

    it('can check off an item as completed', () => {
    cy.contains('Pay electric bill')
    .parent()
    .find('input[type=checkbox]')
    .check()

    cy.contains('Pay electric bill')
    .parents('li')
    .should('have.class', 'completed')
    })

    context('with a checked task', () => {
    beforeEach(() => {
    cy.contains('Pay electric bill')
    .parent()
    .find('input[type=checkbox]')
    .check()
    })

    it('can filter for uncompleted tasks', () => {
    cy.contains('Active').click()

    cy.get('.todo-list li')
    .should('have.length', 1)
    .first()
    .should('have.text', 'Walk the dog')

    cy.contains('Pay electric bill').should('not.exist')
    })

    it('can filter for completed tasks', () => {
    // We can perform similar steps as the test above to ensure
    // that only completed tasks are shown
    cy.contains('Completed').click()

    cy.get('.todo-list li')
    .should('have.length', 1)
    .first()
    .should('have.text', 'Pay electric bill')

    cy.contains('Walk the dog').should('not.exist')
    })

    it('can delete all completed tasks', () => {
    cy.contains('Clear completed').click()

    cy.get('.todo-list li')
    .should('have.length', 1)
    .should('not.have.text', 'Pay electric bill')

    cy.contains('Clear completed').should('not.exist')
    })
    })
    })

    e2e.gif




    总结

    本文前言部分通过开发、构建、性能、测试、部署、规范六个方面,较全面地梳理了前端工程化的知识点,正文则主要介绍了在实践项目中落地使用的前端工程化核心技术点。

    希望本文能够帮助到正在学前端工程化的小伙伴构建完整的知识图谱~


    作者:小铭子
    来源:https://juejin.cn/post/7033355647521554446
    收起阅读 »

    【vue自定义组件】实现一个污染日历

    vue
    前言 佛祖保佑, 永无bug。Hello 大家好!我是海的对岸! 实际开发中,碰到一个日历的需求,这个日历的需求中要加入定制的业务,网上没有现成的,手动实现了一下,整理记录下。 动画效果: 实现 实现背景 工作上碰到一个需求,需要有一个可以在日历上能看到每天...
    继续阅读 »

    前言


    佛祖保佑, 永无bug。Hello 大家好!我是海的对岸!


    实际开发中,碰到一个日历的需求,这个日历的需求中要加入定制的业务,网上没有现成的,手动实现了一下,整理记录下。


    动画效果:


    calendar.gif


    实现


    实现背景


    工作上碰到一个需求,需要有一个可以在日历上能看到每天的污染情况的状态,因此,我们梳理下需求:



    1. 要有一个日历组件

    2. 要在这个日历组件中追加自己的业务逻辑


    简单拎一下核心代码的功能


    实现日历模块


    大体上日历就是看某个月有多少多少天,拆分下,如下所示:
    image.png


    再对比这我们的效果图,日历上还要有上个月的末尾几天


    image.png


    实现上个月的末尾几天


    monthFisrtDay() {
    // 所指的星期中的某一天,使用本地时间。返回值是 0(周日) 到 6(周六) 之间的一个整数
    // eslint-disable-next-line radix
    const currDT = (parseInt(this.year.substr(0, 4)) + '/' + parseInt((this.month).replace('月', '')) + '/1');
    let currWeek = new Date(currDT).getDay();
    return ++currWeek || 7;
    },
    // 刷新日历 获得上个月的结尾天数 <=7
    refreshCalendar() {
    this.nunDays = [];
    const lastDays = [];
    const lastMon = (this.month).replace('月', '') * 1 - 1;
    let lastDay = new Date(new Date(this.year.substr(0, 4), lastMon).getTime() - 8.64e7).getDate();
    for (let i = 1; i < this.monthFisrtDay(); i += 1) {
    lastDays.unshift(lastDay);
    lastDay -= 1;
    }
    this.nunDays = lastDays;
    },

    实现每个月的实际天数


    // 展示 日历数据
    getDatas() {
    if (this.dealDataFinal && this.dealDataFinal.length > 0) {
    // console.log(this.dealDataFinal);
    this.list = [];
    const datas = this.dealDataFinal;
    const dataMap = {};
    if (datas.length > 0) {
    datas.forEach((item) => {
    item.level -= 1;
    item.dateStr = item.tstamp.substr(0, 10);
    item.date = item.tstamp.substr(8, 2);
    dataMap[item.date] = item;
    });
    }

    const curDay = new Date().getDate();
    for (let i = 1; i <= this.monthDays; i += 1) {
    let currColor = this.lvls[6];
    let dateStr = String(i);
    let isCurDay = false;
    if (i == curDay) {
    isCurDay = true; // 表示刚好是今天(该日期 和网络上的今天是同一天)
    }
    dateStr = '0' + dateStr;
    dateStr = dateStr.substr(dateStr.length - 2);
    const dataObj = dataMap[dateStr];
    if (dataObj) {
    if (dataObj.level >= 0 && dataObj.level <= 5) {
    currColor = this.lvls[dataObj.level].color;
    } else {
    currColor = this.lvls[6].color;
    }

    this.list.push({
    date: i,
    curDay: isCurDay,
    color: currColor,
    datas: dataObj,
    checkedColor: undefined, // 选中颜色
    });
    } else {
    this.list.push({
    date: i,
    curDay: isCurDay,
    color: this.lvls[6].color,
    datas: {},
    checkedColor: undefined, // 选中颜色
    });
    }
    }
    // console.log(this.list);
    } else {
    this.clearCalendar();
    }
    },
    // 清除上一次的记录
    clearCalendar() {
    this.list = [];
    for (let i = 1; i <= this.monthDays; i += 1) {
    this.list.push({
    date: i,
    color: this.lvls[6].color,
    datas: {},
    });
    }
    },

    实现日历之后,追加业务


    定义业务上的字段


    data() {
    return {
    ...
    lvls: [
    { title: '优', color: '#00e400' },
    { title: '良', color: '#ffff00' },
    { title: '轻度污染', color: '#ff7e00' },
    { title: '中度污染', color: '#ff0000' },
    { title: '重度污染', color: '#99004c' },
    { title: '严重污染', color: '#7e0023' },
    { title: '未知等级', color: '#cacaca' },
    ],
    list: [], // 当前月的所有天数
    dealDataFinal: [], // 处理接口数据之后获得的最终的数组
    ...
    curYearMonth: '', // 当前时间 年月
    choseYearMonth: '', // 选择的时间 年月
    };
    },

    定义业务上的方法


    // 加载等级
    loadImgType(value) {
    let imgUrl = 0;
    switch (value) {
    case '优':
    imgUrl = 1;
    break;
    case '良':
    imgUrl = 2;
    break;
    case '轻':
    imgUrl = 3;
    break;
    case '中':
    imgUrl = 4;
    break;
    case '重':
    imgUrl = 5;
    break;
    case '严':
    imgUrl = 6;
    break;
    default:
    imgUrl = 0;
    break;
    }
    return imgUrl;
    },

    因为展示效果,用到的是css,css用的比较多,这里就不一段一段的解读了,总而言之,就是日元素不同状态的样式展示,通过前面设置的等级方法,来得到不同的返回参数,进而展示出不同参数对应的不同颜色样式。


    最后会放出日历组件的完整代码。


    完整代码


    <template>
    <div class="right-content">
    <div style="height: 345px;">
    <div class="" style="padding: 0px 15px;">
    <el-select v-model="year" style="width: 119px;" popper-class="EntDate">
    <el-option v-for="item in years" :value="item" :label="item" :key="item"></el-option>
    </el-select>
    <el-select v-model="month" style="width: 119px; margin-left: 10px;" popper-class="EntDate">
    <el-option v-for="item in mons" :value="item" :label="item" :key="item"></el-option>
    </el-select>
    <div class="r-inline">
    <span class="searchBtn" @click="qEQCalendar">查询</span>
    </div>
    </div>
    <div class="calendar" element-loading-spinner="el-icon-loading"
    element-loading-background="rgba(0, 0, 0, 0.6)">
    <div class="day-title clearfix">
    <div class="day-tt" v-for="day in days" :key="day">{{day}}</div>
    </div>
    <div class="clearfix" style="padding-top: 10px;">
    <div :class="{'date-item': true, 'is-last-month': true,}" v-for="(item, index) in nunDays" :key="index + 'num'">
    <div class="day">{{item}}</div>
    </div>
    <div :class="{'date-item': true, 'is-last-month': false, 'isPointer': isPointer}"
    v-for="(item, index) in list" :key="index" @click="queryDeal(item)">
    <div v-if="item.curDay && (curYearMonth === choseYearMonth)" class="day" :style="{border:'2px dashed' + item.color}"
    :class="{'choseDateItemI': item.checkedColor === '#00e400',
    'choseDateItemII': item.checkedColor === '#ffff00', 'choseDateItemIII': item.checkedColor === '#ff7e00', 'choseDateItemIV': item.checkedColor === '#ff0000',
    'choseDateItemV': item.checkedColor === '#99004c', 'choseDateItemVI': item.checkedColor === '#7e0023', 'choseDateItemVII': item.checkedColor === '#cacaca'}"
    >

    </div>
    <div v-else class="day" :style="{border:'2px solid' + item.color}"
    :class="{'choseDateItemI': item.checkedColor === '#00e400',
    'choseDateItemII': item.checkedColor === '#ffff00', 'choseDateItemIII': item.checkedColor === '#ff7e00', 'choseDateItemIV': item.checkedColor === '#ff0000',
    'choseDateItemV': item.checkedColor === '#99004c', 'choseDateItemVI': item.checkedColor === '#7e0023', 'choseDateItemVII': item.checkedColor === '#cacaca'}"
    >
    {{item.date}}
    </div>
    </div>
    </div>
    </div>
    </div>
    </div>
    </template>

    <script>
    const today = new Date();
    const years = [];
    const year = today.getFullYear();
    for (let i = 2018; i <= year; i += 1) {
    years.push(`${i}年`);
    }
    export default {
    props: {
    rightData2: {
    type: Object,
    defaul() {
    return undefined;
    },
    },
    isPointer: {
    type: Boolean,
    default() {
    return false;
    },
    },
    },
    watch: {
    rightData2(val) {
    this.dealData(val);
    },
    calendarData(val) {
    this.dealData(val);
    },
    },
    data() {
    return {
    pointInfo: {
    title: 'xxx污染日历',
    },
    days: ['日', '一', '二', '三', '四', '五', '六'],
    year: year + '年',
    years,
    month: (today.getMonth() + 1) + '月',
    mons: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
    lvls: [
    { title: '优', color: '#00e400' },
    { title: '良', color: '#ffff00' },
    { title: '轻度污染', color: '#ff7e00' },
    { title: '中度污染', color: '#ff0000' },
    { title: '重度污染', color: '#99004c' },
    { title: '严重污染', color: '#7e0023' },
    { title: '未知等级', color: '#cacaca' },
    ],
    list: [], // 当前月的所有天数
    dealDataFinal: [], // 处理接口数据之后获得的最终的数组
    nunDays: [],
    testDays: ['日', '一', '二', '三', '四', '五', '六'],
    calendarData: null,
    curYearMonth: '', // 当前时间 年月
    choseYearMonth: '', // 选择的时间 年月
    };
    },
    computed: {
    // 获取 select框中展示的具体月份应对应的月数
    monthDays() {
    const lastyear = (this.year).replace('年', '') * 1;
    const lastMon = (this.month).replace('月', '') * 1;
    const monNum = new Date(lastyear, lastMon, 0).getDate();
    // return this.$mp.dateFun.GetMonthDays(this.year.substr(0, 4), lastMon);
    return monNum;
    },
    },
    methods: {
    monthFisrtDay() {
    // 所指的星期中的某一天,使用本地时间。返回值是 0(周日) 到 6(周六) 之间的一个整数
    // eslint-disable-next-line radix
    const currDT = (parseInt(this.year.substr(0, 4)) + '/' + parseInt((this.month).replace('月', '')) + '/1');
    let currWeek = new Date(currDT).getDay();
    return ++currWeek || 7;
    },
    // 刷新日历 获得上个月的结尾天数 <=7
    refreshCalendar() {
    this.nunDays = [];
    const lastDays = [];
    const lastMon = (this.month).replace('月', '') * 1 - 1;
    let lastDay = new Date(new Date(this.year.substr(0, 4), lastMon).getTime() - 8.64e7).getDate();
    for (let i = 1; i < this.monthFisrtDay(); i += 1) {
    lastDays.unshift(lastDay);
    lastDay -= 1;
    }
    this.nunDays = lastDays;
    },
    // 展示 日历数据
    getDatas() {
    if (this.dealDataFinal && this.dealDataFinal.length > 0) {
    // console.log(this.dealDataFinal);
    this.list = [];
    const datas = this.dealDataFinal;
    const dataMap = {};
    if (datas.length > 0) {
    datas.forEach((item) => {
    item.level -= 1;
    item.dateStr = item.tstamp.substr(0, 10);
    item.date = item.tstamp.substr(8, 2);
    dataMap[item.date] = item;
    });
    }

    const curDay = new Date().getDate();
    for (let i = 1; i <= this.monthDays; i += 1) {
    let currColor = this.lvls[6];
    let dateStr = String(i);
    let isCurDay = false;
    if (i == curDay) {
    isCurDay = true; // 表示刚好是今天(该日期 和网络上的今天是同一天)
    }
    dateStr = '0' + dateStr;
    dateStr = dateStr.substr(dateStr.length - 2);
    const dataObj = dataMap[dateStr];
    if (dataObj) {
    if (dataObj.level >= 0 && dataObj.level <= 5) {
    currColor = this.lvls[dataObj.level].color;
    } else {
    currColor = this.lvls[6].color;
    }

    this.list.push({
    date: i,
    curDay: isCurDay,
    color: currColor,
    datas: dataObj,
    checkedColor: undefined, // 选中颜色
    });
    } else {
    this.list.push({
    date: i,
    curDay: isCurDay,
    color: this.lvls[6].color,
    datas: {},
    checkedColor: undefined, // 选中颜色
    });
    }
    }
    // console.log(this.list);
    } else {
    this.clearCalendar();
    }
    },
    clearCalendar() {
    this.list = [];
    for (let i = 1; i <= this.monthDays; i += 1) {
    this.list.push({
    date: i,
    color: this.lvls[6].color,
    datas: {},
    });
    }
    },

    // 处理接口返回的日历数据
    dealData(currDS) {
    const tempData = [];
    if (('dates' in currDS) && ('level' in currDS) && ('levelName' in currDS) && ('values' in currDS)) {
    if (currDS.dates.length > 0 && currDS.level.length > 0 && currDS.levelName.length > 0 && currDS.values.length > 0) {
    for (let i = 0; i < currDS.dates.length; i++) {
    const temp = {
    tstamp: currDS.dates[i],
    level: currDS.level[i],
    levelName: currDS.levelName[i],
    value: currDS.values[i],
    grade: this.loadImgType(currDS.levelName[i]),
    week: this.testDays[new Date(currDS.dates[i]).getDay()], // currDS.dates[i]: '2020-03-31'
    };
    tempData.push(temp);
    }
    // this.dealDataFinal = tempData.filter(item => item.grade>0);
    this.dealDataFinal = tempData;
    this.refreshCalendar();
    this.getDatas();
    } else {
    this.dealDataFinal = null;
    this.getDatas();
    }
    } else {
    this.dealDataFinal = null;
    this.getDatas();
    }
    },
    // 加载等级
    loadImgType(value) {
    let imgUrl = 0;
    switch (value) {
    case '优':
    imgUrl = 1;
    break;
    case '良':
    imgUrl = 2;
    break;
    case '轻':
    imgUrl = 3;
    break;
    case '中':
    imgUrl = 4;
    break;
    case '重':
    imgUrl = 5;
    break;
    case '严':
    imgUrl = 6;
    break;
    default:
    imgUrl = 0;
    break;
    }
    return imgUrl;
    },
    // (右边)区域环境质量日历
    qEQCalendar() {
    this.curYearMonth = new Date().getFullYear() + '-' + (new Date().getMonth() + 1);
    this.choseYearMonth = this.year.substr(0, 4) + '-' + this.month.substr(0, 1);
    this.calendarData = {
    dates: [
    '2020-07-01',
    '2020-07-02',
    '2020-07-03',
    '2020-07-04',
    '2020-07-05',
    '2020-07-06',
    '2020-07-07',
    '2020-07-08',
    '2020-07-09',
    '2020-07-10',
    '2020-07-11',
    '2020-07-12',
    '2020-07-13',
    '2020-07-14',
    '2020-07-15',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    ],
    level: [
    1,
    4,
    2,
    3,
    1,
    4,
    4,
    3,
    1,
    4,
    2,
    2,
    4,
    1,
    3,
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    ],
    levelName: [
    '优',
    '中度污染',
    '良',
    '轻度污染',
    '优',
    '中度污染',
    '中度污染',
    '轻度污染',
    '优',
    '中度污染',
    '良',
    '良',
    '中度污染',
    '优',
    '轻度污染',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    ],
    values: [
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '65',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    ],
    };
    // this.$axios.get('api/sinoyd-airquality/airquality/gis/calendar?year=' + parseInt(this.year.substr(0, 4)) + '&month=' + parseInt((this.month).replace('月', '')))
    // .then((res) => {
    // if (res.status == 200) {
    // this.calendarData = res.data.data;
    // } else {
    // this.calendarData = null;
    // }
    // }, () => {
    // this.calendarData = null;
    // });
    },
    // 设置选中之后的逻辑
    queryDeal(item) {
    if (this.isPointer) {
    console.log(item);
    // 设置选中之后的效果
    if (this.list && this.list.length) {
    const tempList = [...this.list];
    tempList.forEach((singleObj) => {
    singleObj.checkedColor = undefined;
    if (item.date === singleObj.date) {
    singleObj.checkedColor = singleObj.color;
    }
    });
    this.list = tempList;
    }
    }
    },
    },
    mounted() {
    this.qEQCalendar();
    },
    };
    </script>

    <style>
    .EntDate{
    background-color: rgba(2, 47, 79, 0.8) !important;
    border: 1px solid rgba(2, 47, 79, 0.8) !important;
    }
    .EntDate /deep/ .popper__arrow::after{
    border-bottom-color: rgba(2, 47, 79, 0.8) !important;
    }
    .EntDate /deep/ .el-scrollbar__thumb{
    background-color: rgba(2, 47, 79, 0.8) !important;
    }
    .el-select-dropdown__item.hover, .el-select-dropdown__item:hover{
    background-color: transparent !important;
    }
    </style>

    <style lang="scss" scoped>
    .r-inline{
    display: inline-block;
    }
    .right-content{
    width: 380px;
    margin: 7px;
    border-radius: 9px;
    background-color: rgba(2, 47, 79, 0.8);
    }
    .day-title {
    border-bottom: 2px solid #03596f;
    padding: 1px 0 10px;
    height: 19px;
    .day-tt {
    float: left;
    text-align: center;
    color: #ffffff;
    width: 48px;
    }
    }
    .date-item {
    float: left;
    text-align: center;
    color: #fff;
    width: 34px;
    // padding: 2px 2px;
    padding: 4px 4px;
    margin: 0px 3px;
    &.is-last-month {
    color: #7d8c8c;
    }
    .day {
    border-radius: 17px;
    padding: 3px;
    height: 25px;
    line-height: 25px;
    text-shadow: #000 0.5px 0.5px 0.5px, #000 0 0.5px 0, #000 -0.5px 0 0, #000 0 -0.5px 0;
    background-color: #173953;
    }
    }
    .calendar{
    padding: 0px 6px;
    }
    .lvls {
    padding: 0px 6px 6px 13px;
    }
    .lvl-t-item {
    float: left;
    font-size:10px;
    padding-right: 3px;
    .lvl-t-ico {
    height: 12px;
    width: 12px;
    display: inline-block;
    margin-right: 5px;
    }
    .lvl-tt {
    color: #5b5e5f;
    }
    }
    // ================================================================================================= 日期框样式
    ::v-deep .el-input__inner {
    background-color: transparent;
    border-radius: 4px;
    border: 0px solid #DCDFE6;
    color: #Fcff00;
    font-size: 19px;
    font-weight: bolder;
    }
    ::v-deep .el-select .el-input .el-select__caret {
    color: #fcff00;
    font-weight: bolder;
    }
    // ================================================================================================= 日期框的下拉框样式
    .el-select-dropdown__item{
    background-color: rgba(2, 47, 79, 0.8);
    color: white;
    &:hover{
    background-color: rgba(2, 47, 79, 0.8);
    color: #5de6f8;
    cursor: pointer;
    }
    }
    .searchBtn {
    cursor: pointer;
    width: 60px;
    height: 28px;
    display: inline-block;
    background-color: rgba(2, 47, 79, 0.8);
    color: #a0daff;
    text-align: center;
    border: 1px solid #a0daff;
    border-radius: 5px;
    margin-left: 15px;
    line-height: 28px;
    }

    .isPointer{
    cursor: pointer;
    }
    .choseDateItemI{
    border: 2px solid #00e400 !important;
    box-shadow: #00e400 0px 0px 9px 2px;
    }
    .choseDateItemII{
    border: 2px solid #ffff00 !important;
    box-shadow: #ffff00 0px 0px 9px 2px;
    }
    .choseDateItemIII{
    border: 2px solid #ff7e00 !important;
    box-shadow: #ff7e00 0px 0px 9px 2px;
    }
    .choseDateItemIV{
    border: 2px solid #ff0000 !important;
    box-shadow: #ff0000 0px 0px 9px 2px;
    }
    .choseDateItemV{
    border: 2px solid #99004c !important;
    box-shadow: #99004c 0px 0px 9px 2px;
    }
    .choseDateItemVI{
    border: 2px solid #7e0023 !important;
    box-shadow: #7e0023 0px 0px 9px 2px;
    }
    .choseDateItemVII{
    border: 2px solid #cacaca !important;
    box-shadow: #cacaca 0px 0px 9px 2px;
    }
    </style>
    作者:海的对岸
    链接:https://juejin.cn/post/7033038877485072397

    收起阅读 »

    生成 UUID 的三种方式及测速对比!

    通用唯一识别码(英语:Universally Unique Identifier,缩写:UUID)是用于计算机体系中以识别信息的一个 128 位标识符,通常表现为一串 32 位十六进制数字。 UUID 用于解决 ID 唯一的问题! 然而,如何确保唯一,这本身...
    继续阅读 »

    通用唯一识别码(英语:Universally Unique Identifier,缩写:UUID)是用于计算机体系中以识别信息的一个 128 位标识符,通常表现为一串 32 位十六进制数字。


    image.png


    UUID 用于解决 ID 唯一的问题!


    然而,如何确保唯一,这本身就是一项挑战!


    如何保证所生成 ID 只有一个副本?如何保证两个 ID 之间没有相关性?唯一性和随机性之间怎么取舍......


    (OS:看过本瓜之前写的《理解 P/NP 问题时,我产生了一种已经触碰到人类认知天花板的错觉?!》这篇文章的朋友,应该知道:或许这个世界上没有随机这个东西?任何随机都能被量子计算算清楚,上帝到底掷骰子吗?没人知道......)


    是否有真正的随机,先按下不表,


    基于目前的算力精度,现在各种 UUID 生成器和不同版本的处理方式能最大限度的确保 ID 不重复,重复 UUID 码概率接近零,可以忽略不计。


    本篇带来 3 种 UUID 生成器! 👍👍👍


    UUID


    基于 RFC4122 标准创建的 UUID,它有很多版本:v1,v2..v5;


    uuid v1是使用主机 MAC 地址和当前日期和时间的组合生成的,这种方式意味着 uuid 是匿名的。


    uuid v4 是随机生成的,没有内在逻辑,组合方式非常多(2¹²⁸),除非每秒生成数以万亿计的 ID,否则几乎不可能产生重复,如果你的应用程序是关键型任务,仍然应该添加唯一性约束,以避免 v4 冲突。


    uuid v5与 v1 v4不同,它通过提供两条输入信息(输入字符串和命名空间)生成的,这两条信息被转换为 uuid;


    特性:

    • 完善;
    • 跨平台;
    • 安全:加密、强随机性;
    • 体积小:零依赖,占用空间小;
    • 良好的开源库支持:uuid command line


    上手:


    import { v4 as uuidv4 } from 'uuid';

    let uuid = uuidv4();

    console.log(uuid) // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'

    Crypto.randomUUID


    Node.js API Crypto 提供 **randomUUID()** 方法,基于 RFC 4122 V4 生成随机数;


    上手:


    let uuid = crypto.randomUUID();

    console.log(uuid); // ⇨ "36b8f84d-df4e-4d49-b662-bcde71a8764f"

    Nano ID


    Nano ID 有 3 个 api:

    1. normal (blocking); 普通
    2. asynchronous;异步
    3. non-secure;非安全

    默认情况下,Nano ID 使用符号(A-Za-z0-9-),并返回一个包含 21 个字符的 ID(具有类似于UUID v4的重复概率)。


    特性:

    • 体积小:130 bytes (压缩后);
    • 零依赖;
    • 生成更快;
    • 安全:
    • 更短,只要 21 位;
    • 方便移植,支持 20 种编程语言.


    上手:


    import { nanoid } from 'nanoid'

    let uuid = nanoid();

    console.log(uuid) // ⇨ "V1StGXR8_Z5jdHi6B-myT"

    Nano IDnpm 下载趋势:


    image.png


    测速


    我们不妨来对比以上所提 3 种生成 UUID 的方式速度差异:


    // test-uuid-gen.js
    const { v4 as uuidv4 } = require('uuid');

    for (let i = 0; i < 10_000_000; i++) {
    uuidv4();
    }

    // test-crypto-gen.js
    const { randomUUID } = require('crypto');

    for (let i = 0; i < 10_000_000; i++) {
    randomUUID();
    }

    // test-nanoid-gen.js
    const { nanoid } = require('nanoid');

    for (let i = 0; i < 10_000_000; i++) {
    nanoid();
    }

    借助 hyperfine


    调用测试:hyperfine ‘node test-uuid-gen.js’ ‘node test-crypto-gen.js’ ‘node test-nanoid-gen.js’


    运行结果:


    img


    我们可以看到, 第二种 randomUUID() 比第三种 nanoid 快 4 倍左右,比第一种 uuid 快 12 倍左右~



    作者:掘金安东尼
    链接:https://juejin.cn/post/7033221241100042271

    收起阅读 »

    老板:你来弄一个团队代码规范!?

    一、背景 9月份换了一个新部门,部门成立时间不长,当时组内还没有统一的代码规范(部分工程用了规范,部分没有,没有统一的收口) 小组的技术栈框架有Vue,React,Taro,Nuxt,用Typescript,算是比较杂了,结合到部门后续还可能扩展其他技术栈,我...
    继续阅读 »

    一、背景


    9月份换了一个新部门,部门成立时间不长,当时组内还没有统一的代码规范(部分工程用了规范,部分没有,没有统一的收口)


    小组的技术栈框架有VueReactTaroNuxt,用Typescript,算是比较杂了,结合到部门后续还可能扩展其他技术栈,我们从0-1实现了一套通用的代码规范


    到现在小组内也用起来几个月了,整个过程还算是比较顺利且快速,最近得空分享出来~


    ⚠️本篇文章不会讲基础的具体的规范,而是从实践经验讲怎么制定规范以及落地规范


    image.png


    二、为什么要代码规范


    就不说了...大家懂的~
    image.png


    不是很了解的话,指路


    三、确定规范范围


    首先,跟主管同步,团队需要一个统一的规范,相信主管也在等着人来做起来


    第一步收集团队的技术栈情况,确定规范要包括的范围


    把规范梳理为三部分ESLintStyleLintCommitLint,结合团队实际情况分析如下



    • ESLint:团队统一用的TypeScript,框架用到了VueReactTaro、还有Nuxt

    • StyleLint:团队统一用的Less

    • CommitLint:git代码提交规范


    image.png
    当然,还需考虑团队后续可能会扩展到的技术栈,以保证实现的时候确保可扩展性


    四、调研业内实现方案


    常见以下3种方案




    1. 团队制定文档式代码规范,成员都人为遵守这份规范来编写代码



      靠人来保证代码规范存在不可靠,且需要人为review代码不规范,效率低





    2. 直接使用业内已有成熟规范,比如css使用StyleLint官方推荐规范stylelint-config-standard、stylelint-order,JavaScript使用ESLint推荐规范eslint:recommended等



      a) 开源规范往往不能满足团队需求,可拓展性差; b) 业内提供的规范都是独立的(stylint只提供css代码规范,ESLint只提供JavaScript规范),是零散的,对于规范初始化或升级存在成本高、不可靠问题(每个工程需要做人为操作多个步骤)





    3. 基于StyleLint、ESLint制定团队规范npm包,使用团队制定规范库



      a) 该方案解决可扩展性差的问题,但是第二点中的(b)问题依旧存在





    五、我们的技术方案


    整体技术思路图如下图,提供三个基础包@jd/stylelint-config-selling@jd/eslint-config-selling@jd/commitlint-config-selling分别满足StyleLintESLintCommitLint



    1. @jd/stylelint-config-selling包括css、less、saas(团队暂未使用到)

    2. @jd/eslint-config-selling包括Vue、React、Taro、Next、nuxt(团队暂未使用到)...,还包括后续可能会扩展到需要自定义的ESLint插件或者解析器

    3. @jd/commitlint-config-selling统一使用git


    向上提供一个简单的命令行工具,交互式初始化init、或者更新update规范


    image.png


    几个关键点


    1、用lerna统一管理包


    lerna是一个管理工具,用于管理包含多个软件包(package)的 JavaScript项目,业内已经广泛使用了,不了解的可以自己找资料看下

    项目结构如下图

    image.png


    2、三个基础包的依赖包都设置为生产依赖dependencies


    如下图,包@jd/eslint-config-selling的依赖包都写在了生产依赖,而不是开发依赖

    image.png
    解释下:
    开发依赖&生产依赖



    • 开发依赖:业务工程用的时候不会下载开发依赖中的包,业内常见的规范如standardairbnb都是写在开发依赖

      • 缺点:业务工程除了安装@jd/eslint-config-selling外,需要自己去安装前置依赖包,如eslint、根据自己选择的框架安装相关前置依赖包如使用的Vue需要安装eslint-plugin-vue...使用成本、维护升级成本较高

      • 优点:按需安装包,开发时不会安装多余的包(Lint相关的包在业务工程中都是开发依赖,所以只会影响开发时)



    • 生产依赖:业务工程用的时候会下载这些包

      • 优点:安装@jd/eslint-config-selling后,无需关注前置依赖包

      • 缺点:开发时会下载@jd/eslint-config-selling中所有写在生产依赖的包,即使有些用不到,比如你使用的是React,却安装了eslint-plugin-vue




    3、提供简单的命令行


    这个比较简单,提供交互式命令,支持一键初始化或者升级3种规范,就不展开说了


    不会的,指路中高级前端必备:如何设计并实现一个脚手架



    组里现在还没有项目模版脚手架,后续有的话需要把规范这部分融进去



    六、最重要的一点


    什么是一个好的规范?

    基本每个团队的规范都是不一样的,团队各成员都认同并愿意遵守的规范


    所以确定好技术方案后,涉及到的各个规范,下图,我们在小组内分工去制定,比如几个人去制定styleLint的,几个人制定Vue的...


    然后拉会评审,大家统一通过的规范才敲定
    image.png
    最后以开源的方式维护升级,使用过程中,遇到规范不合适的问题,提交issue,大家统一讨论确定是否需要更改规范


    写在结尾


    以上就是我们团队在前端规范落地方面的经验~


    作者:jjjona0215
    链接:https://juejin.cn/post/7033210664844066853
    收起阅读 »

    如何优雅的使用枚举功能——Constants

    背景 在项目中,或多或少的会遇到使用枚举/快码/映射/字典,它们一般长这个样子。(PS:我不知道怎么称呼这个玩意) 在一些需要展示的地方,会使用下面的代码来展示定义。 <div>{{ statusList[status] }}</div&g...
    继续阅读 »

    背景


    在项目中,或多或少的会遇到使用枚举/快码/映射/字典,它们一般长这个样子。(PS:我不知道怎么称呼这个玩意)



    在一些需要展示的地方,会使用下面的代码来展示定义。


    <div>{{ statusList[status] }}</div>

    而在代码中,又会使用下面的形式进行判断。这样写会让代码里充斥着许多的 'draft' 字符串,非常不利于管理。


    if (status === 'draft') {
    // do sth...
    }

    基于这种情况,在使用时会先声明一个变量。


    const DRAFT = 'draft'

    if (status === DRAFT) {
    // do sth...
    }

    为了应对整个项目都会使用到的情况,会这样处理。


    export const statusList = {
    draft: '草稿',
    pending: '待处理',
    }

    export const statusKeys = {
    draft: 'draft',
    pending: 'pending',
    }

    看了隔壁后端同事的代码,在 Java 里,枚举的定义及使用一般是如下形式。于是我就有了写这个工具类的想法。


    public enum Status {
    DRAFT('draft', '草稿');

    Status(String code, String name) {
    this.code = code;
    this.name = name;
    }

    public String getCode() {
    return code;
    }

    public String getName() {
    return name;
    }
    }

    public void aFunction() {
    const draftCode = Status.DRAFT.getCode();
    }

    Constants


    直接上代码


    const noop = () => {}

    class Constants {
    constructor(obj) {
    Object.keys(obj).forEach((key) => {
    const initValue = obj[key];
    if (initValue instanceof Object) {
    console.error(`Warnning: this util only support primitive values, current value is ${JSON.stringify(initValue)}`)
    // throw new Error(`Warnning: this util only support primitive values, current value is ${JSON.stringify(initValue)}`)
    }
    const newKey = `_${key}`;
    this[newKey] = initValue;
    Object.defineProperty(this, key, {
    configurable : true,
    enumerable : true,
    get: function() {
    const value = this[newKey];
    const constructorOfValue = value.constructor;
    const entry = [key, value];
    ['getKey', 'getValue'].forEach((item, index) => {
    constructorOfValue.prototype[item] = () => {
    constructorOfValue.prototype.getKey = noop;
    constructorOfValue.prototype.getValue = noop;
    return entry[index];
    }
    })
    return value;
    },
    set: function(newValue) {
    this[newKey] = newValue;
    }
    })
    });
    }
    }

    测试


    const testValues = {
    draft: '草稿',
    id: 1,
    money: 1.2,
    isTest: true,
    testObj: {},
    testArray: [],
    }
    const constants = new Constants(testValues)

    const test = (result, expect) => {
    const isExpected = result === expect
    if (isExpected) {
    console.log(`PASS: The result is ${result}`)
    } else {
    console.error(`FAIL: the result is ${result}, should be ${expect}`)
    }
    }

    test(constants.draft, '草稿')
    test(constants.draft.getKey(), 'draft')
    test(constants.draft.getValue(), '草稿')

    test(constants.id, 1)
    test(constants.id.getKey(), 'id')
    test(constants.id.getValue(), 1)

    test(constants.money, 1.2)
    test(constants.money.getKey(), 'money')
    test(constants.money.getValue(), 1.2)

    test(constants.isTest, true)
    test(constants.isTest.getKey(), 'isTest')
    test(constants.isTest.getValue(), true)
    a = 'test'
    test(a.getKey(), undefined)
    test(a.getValue(), undefined)


    作者:Wetoria
    链接:https://juejin.cn/post/7033220309386395679

    收起阅读 »

    CSS mask 实现鼠标跟随镂空效果

    偶然在某思看到这样一个问题,如何使一个div的部分区域变透明而其他部分模糊掉?,最后实现效果是这样的 进一步,还能实现任意形状的镂空效果 鼠标经过的地方清晰可见,其他地方则是模糊的。 可能一开始无从下手,不要急,可以先从简单的、类似的效果开始,一步一步尝试...
    继续阅读 »

    偶然在某思看到这样一个问题,如何使一个div的部分区域变透明而其他部分模糊掉?,最后实现效果是这样的


    237330258-6181fcdb471cf


    进一步,还能实现任意形状的镂空效果


    Kapture 2021-11-20 at 13.44.26


    鼠标经过的地方清晰可见,其他地方则是模糊的。


    可能一开始无从下手,不要急,可以先从简单的、类似的效果开始,一步一步尝试,一起看看吧。


    一、普通半透明的效果


    比如平时开发中碰到更多的可能是一个半透明的效果,有点类似于探照灯(鼠标外面的地方是半透明遮罩,看起来会暗一点)。如下:


    image-20211117200548416


    那先从这种效果开始吧,假设有这样一个布局:


    <div class="wrap" id="img">
    <img class="prew" src="https://tva1.sinaimg.cn/large/008i3skNgy1gubr2sbyqdj60xa0m6tey02.jpg">
    </div>

    那么如何绘制一个镂空的圆呢?先介绍一种方法


    其实很简单,只需要一个足够大的投影就可以了,原理如下


    image-20211117195737723


    这里可以用伪元素::before来绘制,结构更加精简。用代码实现就是


    .wrap::before{
    content:'';
    position: absolute;
    width: 100px;
    height: 100px;
    border-radius: 50%;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%); /*默认居中*/
    box-shadow: 0 0 0 999vw rgba(0, 0, 0, .5); /*足够大的投影*/
    }

    可以得到这样的效果


    image-20211117200548416


    二、借助 CSS 变量传递鼠标位置


    按照以往的经验,可能会在 js 中直接修改元素的 style 属性,类似这样


    img.addEventListener('mousemove', (ev) => {
    img.style.left = '...';
    img.style.top = '...';
    })

    但是这样交互与业务逻辑混杂在一起,不利于后期维护。其实,我们只需要鼠标的坐标,在 CSS 中也能完全实现跟随的效果。


    这里借助 CSS 变量,那一切就好办了!假设鼠标的坐标是 [--x,--y](范围是[0, 1]),那么遮罩的坐标就可以使用 calc计算了


    .wrap::before{
    left: calc(var(--x) * 100%);
    top: calc(var(--y) * 100%);
    }

    然后鼠标坐标的获取可以使用 JS 来计算,也比较容易,如下


    img.addEventListener('mousemove', (ev) => {
    img.style.setProperty('--x', ev.offsetX / ev.target.offsetWidth);
    img.style.setProperty('--y', ev.offsetY / ev.target.offsetHeight);
    })

    这样,半透明效果的镂空效果就完成了


    Kapture 2021-11-17 at 20.26.27


    完整代码可以访问: backdrop-shadow (codepen.io)


    三、渐变也能实现半透明的效果


    除了上述阴影扩展的方式,CSS 径向渐变也能实现这样的效果


    绘制一个从透明到半透明的渐变,如下


    .wrap::before{
    content: '';
    position: absolute;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    background: radial-gradient( circle at center, transparent 50px, rgba(0,0,0,.5) 51px);
    }

    可以得到这样的效果


    image-20211117200548416


    然后,把鼠标坐标映射上去就可以了。从这里就可以看出 CSS 变量的好处,无需修改 JS,只需要在CSS中修改渐变中心点的位置就可以实现了


    .wrap::before{
    background: radial-gradient( circle at calc(var(--x) * 100% ) calc(var(--y) * 100% ), transparent 50px, rgba(0,0,0,.5) 51px);
    }

    Kapture 2021-11-18 at 19.51.30


    四、背景模糊的效果尝试


    CSS 中有一个专门针对背景(元素后面区域)的属性:backdrop-filter。使用方式和 filter完全一致!


    backdrop-filter: blur(10px);

    下面是 MDN 中的一个示意效果


    image-20211119191341911


    backdrop-filter是让当前元素所在区域后面的内容模糊,要想看到效果,需要元素本身半透明或者完全透明;而filter是让当前元素自身模糊。有兴趣的可以查看这篇文章: CSS backdrop-filter简介与苹果iOS毛玻璃效果 « 张鑫旭-鑫空间-鑫生活 (zhangxinxu.com)


    需要注意的是,这种模糊与背景的半透明度没有任何关系,哪怕元素本身是透明的,仍然会有效果。例如下面是去除背景后的效果 ,整块都是模糊的


    image-20211119193956128


    如果直接运用到上面的例子会怎么样呢?


    1. 阴影实现


    在上面第一个例子中添加 backdrop-filter


    .wrap::before{
    content:'';
    position: absolute;
    width: 100px;
    height: 100px;
    border-radius: 50%;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%); /*默认居中*/
    box-shadow: 0 0 0 999vw rgba(0, 0, 0, .5); /*足够大的投影*/
    backdrop-filter: blur(5px)
    }

    得到效果如下


    Kapture 2021-11-19 at 19.20.57


    可以看到圆形区域是模糊的,正好和希望的效果相反。其实也好理解,只有圆形区域才是真实的结构,外面都是阴影,所以最后作用的范围也只有圆形部分


    2. 渐变实现


    现在在第二个例子中添加 backdrop-filter


    .wrap::before{
    content: '';
    position: absolute;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    background: radial-gradient( circle at calc(var(--x) * 100% ) calc(var(--y) * 100% ), transparent 50px, rgba(0,0,0,.5) 51px);
    backdrop-filter: blur(5px)
    }

    效果如下


    Kapture 2021-11-19 at 19.31.22


    已经全部都模糊了,只是圆形区域外暗一些。由于::before的尺寸占据整个容器,所以整个背后都变模糊了,圆形外部比较暗是因为半透明渐变的影响。


    总之还是不能满足我们的需求,需要寻求新的解决方式。


    五、CSS MASK 实现镂空


    与其说是让圆形区域不模糊,还不如说是把那块区域给镂空了。就好比之前是一整块磨砂玻璃,然后通过 CSS MASK 打了一个圆孔,这样透过圆孔看到后面肯定是清晰的。


    可以对第二个例子稍作修改,通过径向渐变绘制一个透明圆,剩余部分都是纯色的遮罩层,示意如下


    image-20211120113029155


    用代码实现就是


    .wrap::before{
    content: '';
    position: absolute;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    -webkit-mask: radial-gradient( circle at calc(var(--x, .5) * 100% ) calc(var(--y, .5) * 100% ), transparent 50px, #000 51px);
    background: rgba(0,0,0,.3);
    backdrop-filter: blur(5px)
    }

    这样就实现了文章开头的效果


    237330258-6181fcdb471cf


    完整代码可以查看:backdrop-mask (codepen.io)


    六、CSS MASK COMPOSITE 实现更丰富的镂空效果


    除了使用径向渐变绘制遮罩层以外,还可以通过 CSS MASK COMPOSITE(遮罩合成)的方式来实现。标准关键值如下(firefox支持):


    /* Keyword values */
    mask-composite: add; /* 叠加(默认) */
    mask-composite: subtract; /* 减去,排除掉上层的区域 */
    mask-composite: intersect; /* 相交,只显示重合的地方 */
    mask-composite: exclude; /* 排除,只显示不重合的地方 */

    遮罩合成是什么意思呢?可以类比 photoshop 中的形状合成,几乎是一一对应的


    image-20211120123004278


    -webkit-mask-composite 与标准下的值有所不同,属性值非常多,如下(chorme 、safari 支持)


    -webkit-mask-composite: clear; /*清除,不显示任何遮罩*/
    -webkit-mask-composite: copy; /*只显示上方遮罩,不显示下方遮罩*/
    -webkit-mask-composite: source-over;
    -webkit-mask-composite: source-in; /*只显示重合的地方*/
    -webkit-mask-composite: source-out; /*只显示上方遮罩,重合的地方不显示*/
    -webkit-mask-composite: source-atop;
    -webkit-mask-composite: destination-over;
    -webkit-mask-composite: destination-in; /*只显示重合的地方*/
    -webkit-mask-composite: destination-out;/*只显示下方遮罩,重合的地方不显示*/
    -webkit-mask-composite: destination-atop;
    -webkit-mask-composite: xor; /*只显示不重合的地方*/

    是不是一脸懵?这里做了一个对应的效果图,如果不太熟练,使用的时候知道有这样一个功能,然后对着找就行了


    image-20211120130421281


    回到这里,可以绘制一整块背景和一个圆形背景,然后通过遮罩合成排除(mask-composite: exclude)打一个孔就行了,实现如下


    .wrap::before{
    content: '';
    position: absolute;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    -webkit-mask: url("data:image/svg+xml,%3Csvg width='50' height='50' viewBox='0 0 50 50' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='25' cy='25' r='25' fill='%23C4C4C4'/%3E%3C/svg%3E"), linear-gradient(red, red);
    -webkit-mask-size: 50px, 100%;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: calc(var(--x, .5) * 100% + var(--x, .5) * 100px - 50px ) calc(var(--y, .5) * 100% + var(--y, .5) * 100px - 50px ), 0;
    -webkit-mask-composite: xor; /*只显示不重合的地方, chorem 、safari 支持*/
    mask-composite: exclude; /* 排除,只显示不重合的地方, firefox 支持 */
    background: rgba(0,0,0,.3);
    backdrop-filter: blur(5px)
    }

    需要注意-webkit-mask-position中的计算,这样也能很好的实现这个效果


    237330258-6181fcdb471cf


    完整代码可以查看:backdrop-mask-composite (codepen.io)


    你可能已经发现,上述例子中的圆是通过 svg 绘制的,还用到了遮罩合成,看着好像更加繁琐了。其实呢,这是一种更加万能的解决方式,可以带来无限的可能性。比如我需要一个星星⭐️的镂空效果,很简单,先通过一个绘制软件画一个


    image-20211120131056453


    然后把这段 svg 代码转义一下,这里推荐使用张鑫旭老师的SVG在线压缩合并工具


    image-20211120131335734


    替换到刚才的例子中就可以了


    .wrap::before{
    content: '';
    position: absolute;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    -webkit-mask: url("data:image/svg+xml,%3Csvg width='96' height='91' viewBox='0 0 96 91' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M48 0l11.226 34.55h36.327l-29.39 21.352L77.39 90.45 48 69.098 18.61 90.451 29.837 55.9.447 34.55h36.327L48 0z' fill='%23C4C4C4'/%3E%3C/svg%3E"), linear-gradient(red, red);
    -webkit-mask-size: 50px, 100%;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: calc(var(--x, .5) * 100% + var(--x, .5) * 100px - 50px ) calc(var(--y, .5) * 100% + var(--y, .5) * 100px - 50px ), 0;
    -webkit-mask-composite: xor; /*只显示不重合的地方, chorem 、safari 支持*/
    mask-composite: exclude; /* 排除,只显示不重合的地方, firefox 支持 */
    background: rgba(0,0,0,.3);
    backdrop-filter: blur(5px)
    }

    星星镂空实现效果如下


    Kapture 2021-11-20 at 13.35.28


    完整代码可以查看:backdrop-star (codepen.io)


    再比如一个心形❤,实现效果如下


    Kapture 2021-11-20 at 13.44.26


    完整代码可以查看:backdrop-heart (codepen.io)


    只有想不到,没有做不到



    作者:XboxYan
    链接:https://juejin.cn/post/7033188994641100831
    收起阅读 »

    微信小程序如何确保每个页面都已经登陆

    现状 一个微信小程序中,有首页,有个人页面,还有一些列表页面,详情页面等等,这些页面大部分是可以分享的。当分享出去的页面被一个另一个用户打开的时候,这个页面怎么确保这个用户已经登陆了呢? 网上有很多方案是在请求封装里面加一道拦截,如果没有token,就先调用登...
    继续阅读 »

    现状


    一个微信小程序中,有首页,有个人页面,还有一些列表页面,详情页面等等,这些页面大部分是可以分享的。当分享出去的页面被一个另一个用户打开的时候,这个页面怎么确保这个用户已经登陆了呢?


    网上有很多方案是在请求封装里面加一道拦截,如果没有token,就先调用登陆请求获取token后,再继续。
    这种方案没毛病,只要注意一点,当一个页面有多个请求同时触发时,当所有请求拦截后,放到一个数组里面,在获取token成功后,遍历数组一个个请求就行。


    但这个需求再复杂一点,比如连锁便利店小程序,大部分页面都需要有一个门店(因为需要根据门店获取当前门店商品的库存、价格等),这个门店是根据当前的定位来调用后台接口获得的,这个时候如果在请求里进行封装就太麻烦了。


    解决方案


    首先,我们注意到,登陆,获取定位与我们的页面请求是异步的,我们需要保证页面请求是在登陆和获取定位之后,但要是我们每个页面都写一个遍,可维护性就太差了。所以我们可以抽离出一个方法来做这件事。
    所以代码就这样了:


    const app = getApp()
    Page({
    data: {
    logs: []
    },
    onLoad() {
    app.commonLogin(()=>{
    // 处理页页面请求
    })
    }
    })

    做到这里好像是解决我们的问题,但再想一想,如果还想做更多的事,比如说每个页面的onShareAppMessage统一处理,但我又不想在每个页面再写一遍,另外,我又想自己对每个页面实现一个watch,怎么做?


    进一步解决方案


    我们可以看到微信小程序,每个页面是一个Page(),那么我们可以给这个Page外面加一层壳子,我们可以有一个MyPage来替换这个Page,废话不多说,上代码:


    tool.js 相关代码


    /**
    * 处理合并参数
    */
    handlePageParamMerge(arg) {
    let numargs = arg.length; // 获取被传递参数的数值。
    let data = {}
    let page = {}
    for (let ix in arg) {
    let item = arg[ix]
    if (item.data && typeof (item.data) === 'object') {
    data = Object.assign(data, item.data)
    }
    if (item.methods && typeof (item.methods) === 'object') {
    page = Object.assign(page, item.methods)
    } else {
    page = Object.assign(page, item)
    }
    }
    page.data = data
    return page
    }

    /***
    * 合并页面方法以及数据, 兼容 {data:{}, methods: {}} 或 {data:{}, a:{}, b:{}}
    */
    mergePage() {
    return this.handlePageParamMerge(arguments)
    }

    /**
    * 处理组件参数合并
    */
    handleCompParamMerge(arg) {
    let numargs = arg.length; // 获取被传递参数的数值。
    let data = {}
    let options = {}
    let properties = {}
    let methods = {}
    let comp = {}
    for (let ix in arg) {
    let item = arg[ix]
    // 合并组件的初始数据
    if (item.data && typeof (item.data) === 'object') {
    data = Object.assign(data, item.data)
    }
    // 合并组件的属性列表
    if (item.properties && typeof (item.properties) === 'object') {
    properties = Object.assign(properties, item.properties)
    }
    // 合组件的方法列表
    if (item.methods && typeof (item.methods) === 'object') {
    methods = Object.assign(methods, item.methods)
    }
    if (item.options && typeof (item.options) === 'object') {
    options = Object.assign(options, item.options)
    }
    comp = Object.assign(comp, item)
    }
    comp.data = data
    comp.options = options
    comp.properties = properties
    comp.methods = methods
    return comp
    }

    /**
    * 组件混合 {properties: {}, options: {}, data:{}, methods: {}}
    */
    mergeComponent() {
    return this.handleCompParamMerge(arguments)
    }

    /***
    * 合成带watch的页面
    */
    newPage() {
    let options = this.handlePageParamMerge(arguments)
    let that = this
    let app = getApp()

    //增加全局点击登录判断
    if (!options.publicCheckLogin){
    options.publicCheckLogin = function (e) {
    let pages = getCurrentPages()
    let page = pages[pages.length - 1]
    let dataset = e.currentTarget.dataset
    let callback = null

    //获取回调方法
    if (dataset.callback && typeof (page[dataset.callback]) === "function"){
    callback = page[dataset.callback]
    }
    // console.log('callback>>', callback, app.isRegister())
    //判断是否登录
    if (callback && app.isRegister()){
    callback(e)
    }
    else{
    wx.navigateTo({
    url: '/pages/login/login'
    })
    }
    }
    }

    const { onLoad } = options
    options.onLoad = function (arg) {
    options.watch && that.setWatcher(this)
    onLoad && onLoad.call(this, arg)
    }

    const { onShow } = options
    options.onShow = function (arg) {
    if (options.data.noAutoLogin || app.isRegister()) {
    onShow && onShow.call(this, arg)
    //页面埋点
    app.ga({})
    }
    else {
    wx.navigateTo({
    url: '/pages/login/login'
    })
    }
    }

    return Page(options)
    }

    /**
    * 合成带watch等的组件
    */
    newComponent() {
    let options = this.handleCompParamMerge(arguments)
    let that = this
    const { ready } = options
    options.ready = function (arg) {
    options.watch && that.setWatcher(this)
    ready && ready.call(this, arg)
    }
    return Component(options)
    }

    /**
    * 设置监听器
    */
    setWatcher(page) {
    let data = page.data;
    let watch = page.watch;
    Object.keys(watch).forEach(v => {
    let key = v.split('.'); // 将watch中的属性以'.'切分成数组
    let nowData = data; // 将data赋值给nowData
    for (let i = 0; i < key.length - 1; i++) { // 遍历key数组的元素,除了最后一个!
    nowData = nowData[key[i]]; // 将nowData指向它的key属性对象
    }

    let lastKey = key[key.length - 1];
    // 假设key==='my.name',此时nowData===data['my']===data.my,lastKey==='name'
    let watchFun = watch[v].handler || watch[v]; // 兼容带handler和不带handler的两种写法
    let deep = watch[v].deep; // 若未设置deep,则为undefine
    this.observe(nowData, lastKey, watchFun, deep, page); // 监听nowData对象的lastKey
    })
    }

    /**
    * 监听属性 并执行监听函数
    */
    observe(obj, key, watchFun, deep, page) {
    var val = obj[key];
    // 判断deep是true 且 val不能为空 且 typeof val==='object'(数组内数值变化也需要深度监听)
    if (deep && val != null && typeof val === 'object') {
    Object.keys(val).forEach(childKey => { // 遍历val对象下的每一个key
    this.observe(val, childKey, watchFun, deep, page); // 递归调用监听函数
    })
    }
    var that = this;
    Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: true,
    set: function (value) {
    if (val === value) {
    return
    }
    // 用page对象调用,改变函数内this指向,以便this.data访问data内的属性值
    watchFun.call(page, value, val); // value是新值,val是旧值
    val = value;
    if (deep) { // 若是深度监听,重新监听该对象,以便监听其属性。
    that.observe(obj, key, watchFun, deep, page);
    }
    },
    get: function () {
    return val;
    }
    })
    }

    页面代码:


    app.tool.newPage({
    data: {
    // noAutoLogin: false
    },
    onShow: function () {
    // 在这里写页面请求逻辑
    }
    }

    最后


    代码是在线上跑了很久的,tool里的newPage封装,你可以根据自己的需求进行添加。总之,我这里是提供一种思路,如有更佳,欢迎分享。


    作者:盗道
    链接:https://juejin.cn/post/7026544177844355103

    收起阅读 »

    你写过的所有代码都逃不过这两方面:API 和抽象

    作为前端,你可能开发过 Electron 桌面应用、小程序、浏览器上的 web 应用、基于 React Native 等跨端引擎的 app,基于 Node.js 的工具或者服务等各种应用,这些都是 JS 的不同的 runtime,开发也都是基于前端那套技术。 ...
    继续阅读 »

    作为前端,你可能开发过 Electron 桌面应用、小程序、浏览器上的 web 应用、基于 React Native 等跨端引擎的 app,基于 Node.js 的工具或者服务等各种应用,这些都是 JS 的不同的 runtime,开发也都是基于前端那套技术。


    面对这么多的细分领域,作为前端工程师的你是否曾迷茫过:这么多技术我该学什么?他们中有没有什么本质的东西呢?


    其实所有的这些技术,你写过的所有代码,都可以分为两个方面: api 和 抽象。


    api


    不同平台提供的 api 不同,支持的能力不同:


    浏览器提供了 dom api、支持了 css 的渲染,还提供了音视频、webgl 等相关 api,这些 api 是我们开发前端应用的基础。


    Node.js 提供了操作系统能力的 api,比如进程、线程、网络、文件等,这些 api 是我们开发工具链或后端应用的基础。


    React Native 等跨端引擎支持了 css 的渲染,还提供了设备能力的 api,比如照相机、闪光灯、传感器、GPS 等 api,这是我们开发移动 app 的基础。


    Electron 集成了 Chromium 和 Node.js,同时还提供了桌面相关的 api。


    小程序支持了 css 的渲染之外,还提供了一些宿主 app 能力的 api。


    此外,还有很多的 runtime,比如 vscode 插件、sketch 插件等,都有各自能够使用的 api。


    不同的 JS runtime 提供了不同 api 给上层应用,这是应用开发的基础,也是应用开发的能力边界。


    抽象


    基于 runtime 提供的 api 我们就能完成应用的功能开发,但是复杂场景下往往会做一些抽象。


    比如浏览器上的前端应用主要是把数据通过 dom api 和 css 渲染出来,并做一些交互,那么我们就抽象出了数据驱动的前端框架,抽象出了组件、状态、数据流等概念。之后就可以把不同的需求抽象为不同的组件、状态。


    经过层层抽象之后,开发复杂前端应用的时候代码更容易维护、成本更低。


    比如基于 Node.js 的 fs、net、http 等 api 我们就能实现 web server,但是对于复杂的企业级应用,我们通过后端框架做 MVC 的抽象,抽象出控制器、服务、模型、视图等概念。之后的后端代码就可以把需求抽象为不同的控制器和服务。


    经过 MVC 的抽象之后,后端应用的分层更清晰、更容易维护和扩展。


    复杂的应用需要在 api 的基础上做一些抽象。我们往往会用框架做一层抽象,然后自己再做一层抽象,经过层层抽象之后的代码是更容易维护和扩展的。这也就是所谓的架构。


    如何深入 api 和抽象


    api


    api 是对操作系统能力或不同领域能力的封装。


    比如 Node.js 的进程、线程、文件、网络的 api 是对操作系统能力的封装,想深入它们就要去学习操作系统的一些原理。


    而 webgl、音视频等 api 则分别是对图形学、音视频等领域的能力的封装,想要深入它们就要去学习这些领域的一些原理。


    个人觉得我们知道 api 提供了什么能力就行,没必要过度深入 api 的实现原理。


    抽象


    抽象是基于编程语言的编程范式,针对不同目标做的设计。


    Javascript 提供了面向对象、函数式等编程范式,那么就可以基于对象来做抽象,使用面向对象的各种设计模式,或者基于函数式那一套。这是抽象的基础。


    抽象是根据不同的目标来做的。


    前端领域主要是要分离 dom 操作和数据,把页面按照功能做划分,所以根据这些目标就做了 mvvm 和组件化的抽象。


    后端领域主要是要做分层、解耦等,于是就做了 IOC、MVC 等抽象。


    可以看到,抽象是基于编程语言的范式,根据需求做的设计,好的框架一定是做了满足某种管理代码的需求的抽象。


    想要提升抽象、架构设计能力的话,可以学习下面向对象的设计模式,或者函数式等编程范式。研究各种框架是如何做的抽象。


    总结


    不同平台提供了不同的 api,这是应用开发的基础和边界。复杂应用往往要在 api 基础上做层层抽象,一般会用框架做一层抽象,自己再做一层抽象,目标是为了代码划分更清晰,提升可维护性和可扩展性。


    其实我们写过的所有代码,都可以分为 api 和抽象这两方面。


    深入 API 原理的话要深入操作系统和各领域的知识。提升抽象能力的话,可以学习面向对象的设计模式或者函数式等编程范式。


    不管你现在做哪个平台之上的应用开发,刚开始都是要先学习 api 的,之后就是要理解各种抽象了:框架是怎么抽象的,上层又做了什么抽象。


    API 保证下限,抽象可以提高上限。而且抽象能力或者说架构能力是可以迁移的,是程序员最重要的能力之一。


    作者:zxg_神说要有光
    链接:https://juejin.cn/post/7031931672538906637

    收起阅读 »

    线性表

    由于我是搞前端的为了更友好的描述数据结构,所以全部代码示例都是用TypeScript来编写。 1、线性表类型 1.顺序存储结构(数组) 2.链式存储结构(链表) 1.1、顺序存储 一般指数组,内部数据的存储单元在内存中相邻 优势: 查询很快,时间复杂度为...
    继续阅读 »

    由于我是搞前端的为了更友好的描述数据结构,所以全部代码示例都是用TypeScript来编写。


    1、线性表类型



    • 1.顺序存储结构(数组)

    • 2.链式存储结构(链表)


    1.1、顺序存储


    一般指数组,内部数据的存储单元在内存中相邻



    优势: 查询很快,时间复杂度为O(1)


    劣势:



    1. 元素增、删操作时间复杂度为O(n)

    2. 使用时需要提前确定长度




    1. 需要占据连续内存空间


    1.2、链式存储


    n 个数据元素的有限序列,通常为链式,叫作线性链表或链表。链表中的元素为结点,结点是一块内存空间存储一条数据。结点通常由两个部分组成:



    • 节点存储的数据

    • 指向下一个节点的指针



    来看一下链表的typescript实现


    class ListNode {
    val: number
    next: ListNode | null
    constructor(val?: any, next?: ListNode | null) {
    this.val = val
    this.next = (next===undefined ? null : next)
    }
    }

    2、链表类型


    链表类型大体分为下列:



    • 带头、不带头

    • 单向、双向




    • 循环、非循环


    2.1 带头不带头


    链表都有头指针,带头结点的链表头指针指向的是头结点,不带头结点的头指针直接指向首元结点。


    首元节点:链表中用来存储元素节点的第一个节点


    带头:



    不带头:



    操作差异: 删除和新增操作中,无论操作位置,带头结点的链表不需要修改头指针的值,而不带头结点的有时候需要。清空操作中,带头结点的保留头结点,不带头结点的要销毁。


    结构差异: 带头链表不论链表是否为空,均含有一个头结点,不带头单链表均无结点。


    一般使用链表都为带头链表


    2.2、双向链表


    单项链表中,仅有一个指针指向下一个节点的位置,双向链表中,每个节点有有个指针:



    • pre:指向上一个节点位置

    • next:指向下一个节点位置


    双向链表节点图:



    双向链表节点数据结构:


    class TwoWayListNode {
    val: number
    pre: ListNode | null
    next: ListNode | null
    constructor(val?: any, pre?: ListNode | null, next?: ListNode | null) {
    this.val = val
    this.next = (next===undefined ? null : next)
    }
    }

    双向链表图:



    // 简单的来实现一下上述结构,实际使用时可自行封装统的类
    const p1 = new TwoWayListNode('p1', null, null)
    const p2 = new TwoWayListNode('p2', null, null)
    const p3 = new TwoWayListNode('p3', null, null)

    p1.next = p2
    p2.pre = p1
    p2.next = P3

    2.3、循环链表


    链表中最后一个节点指向头节点



    // 简单的来实现一下上述结构,实际使用时可自行封装统的类
    const p1 = new ListNode('p1', null)
    const p2 = new ListNode('p2', null)
    const p3 = new ListNode('p3', null)

    p1.next = p2
    p2.pre = p1
    p2.next = P3
    p3.next = p1

    以此类推还有双向项循环链表,这里就不展开了


    3、线性表数据处理分析


    3.1、顺序存储操作


    查询:


    由于顺序存储中数据按照逻辑顺序依次放入连续的存储单元中,所以在顺序表结构中很容易实现查询操作,直接通过下标去拿即可。时间复杂度为O(1)


    插入:


    在顺序存储结构中, 插入尾部的时间复杂度为O(1),其他位置时间复杂度为O(n)。


    如下图想要在3位置插入一条数据 "六",需要将"四", "五" 位置的数据依次向后移动一个单位





    删除:


    在顺序存储结构中, 删除尾部的时间复杂度为O(1),其他位置时间复杂度为O(n)。


    如下图想删除数据 "六",先将3位置的数据设置为空,"四", "五" 位置的数据依次向前移动一个单位



    3.2 链式存储


    插入:



    // p1 -> p2 -> p3 -> p4 
    p1.next = p5
    p5.next = p2





    删除:



    // p1 -> p2 -> p3 -> p4 
    p1.next = p3
    p2.next = null

    查询:


    链表中查找只能从链表的头指针出发,顺着连标指针逐个结点查询,直到查到想要的结果为止,时间复杂度O(n)


    作者:siegaii
    链接:https://juejin.cn/post/7031868181203386405

    收起阅读 »

    【小程序实战】- 将图片优化进行到底

    背景 前端的性能优化,图片优化是必不可少的重要环节,大部分网站页面的构成都少不了图片的渲染。尤其在电商类项目,往往存在大量的图片,如 banner 广告图,菜单导航图,商品列表图等。图片加载数量多以及图片体积过大往往会影响页面加载速度,造成不良的用户体验。 优...
    继续阅读 »

    背景


    前端的性能优化,图片优化是必不可少的重要环节,大部分网站页面的构成都少不了图片的渲染。尤其在电商类项目,往往存在大量的图片,如 banner 广告图,菜单导航图,商品列表图等。图片加载数量多以及图片体积过大往往会影响页面加载速度,造成不良的用户体验。


    优化方案


    基于上述问题的主要问题是图片数量和图片体积,所以应该怎么提高图片加载速度,提升用户体验。其实图片优化有非常多且优秀的方案,都可以从中借鉴,最后我们对图片进行不同方向的整体优化。


    image-20211021191413342.png


    使用合适的图片格式


    目前广泛应用的 WEB 图片格式有 JPEG/JPG、PNG、GIF、WebP、Base64、SVG 等,这些格式都有各自的特点,以下大概简单总结如下:


    WEB图片格式.png


    使用合适的图片格式通常可以带来更小的图片字节大小,通过合理压缩率,可以减少图片大小,且不影响图片质量。


    降低网络传输


    小程序使用腾讯云图片服务器,提供很多图片处理功能,比如图片缩放、图片降质,格式转换,图片裁剪、图片圆角等功能。这些功能可以通过在图片URL中添加规定参数就能实现,图片服务器会根据参数设置提前将图片处理完成并保存到CDN服务器,这样大大的减少图片传输大小。


    目前后台接口下发返回的图片 URL 都是未设置图片参数预处理,比如一张 800x800 尺寸高清的商品图,体积大概300k 左右,这样就很容易导致图片加载和渲染慢、用户流量消耗大,严重影响了用户体验。所以我们结合腾讯云的图片处理功能,网络图片加载前,先检测是否是腾讯云域名的图片URL,如果域名匹配,对图片URL进行预处理,预处理包括添加缩放参数添加降质参数添加WebP参数的方式减少图片网络传输大小


    我们先看一张通过图片服务器是腾讯云图片处理能力,通过设置图片缩放/降质/WebP,一张尺寸800x800,体积246KB图片,最后输出生成25.6KB,图片体积足足减少了80%,效果显著。


    image-20211021203109404.png


    图片缩放

    目前业务后台都是原图上传,原始图尺寸可能比客户端实际显示的尺寸要大,一方面导致图片加载慢,另一方面导致用户流量的浪费,其中如果是一张很大尺寸图片加载也会影响渲染性能,会让用户感觉卡顿,影响用户体验。通过添加缩放参数的方式,指定图片服务器下发更小和更匹配实际显示size的图片尺寸。


    图片降质

    图片服务器支持图片质量,取值范围 0-100,默认值为原图质量,通过降低图片质量可以减少图片大小,但是质量降低太多也会影响图片的显示效果,网络默认降图片质量参数设置为85,同时通过小程序提供的:wx.getNetworkTypewx.onNetworkStatusChangeoffNetworkStatusChange的接口监听网络状态变化来获取当前用户的网络类型networkType,比如用户当前使用的4G网络,则图片质量会动态设置为80,对于大部分业务情况,一方面可以大幅减少图片下载大小和保证用户使用体验,另一方面节省用户浏览 ,目前添加图片降质参数至少可以减少30-40%的图片大小。


    /**
    * 设置网络情况
    */
    const setNetwork = (res: Record<string, any>) => {
    const { isConnected = true, networkType = 'wifi' } = res;

    this.globalData.isConnected = isConnected;
    this.globalData.networkType = networkType.toLowerCase();
    this.events.emit(EventsEnum.UPDATE_NETWORK, networkType);
    };

    wx.getNetworkType({ success: (res) => setNetwork(res) });
    wx.offNetworkStatusChange((res) => setNetwork(res));
    wx.onNetworkStatusChange((res) => setNetwork(res));

    /**
    * 根据网络环境设置不同质量图片
    */
    const ImageQuality: Record<string, number> = {
    wifi: 85,
    '5g': 85,
    '4g': 80,
    '3g': 60,
    '2g': 60,
    };

    /**
    * 获取图片质量
    */
    export const getImageQuality = () => ImageQuality[getApp().globalData.networkType ?? 'wifi'];

    使用 WebP

    前面简单介绍不同的图片格式都有各自的优缺点和使用场景,其中 WebP 图片格式提供有损压缩与无损压缩的图片格式。按照Google官方的数据,与PNG相比,WebP无损图像的字节数要少26%WebP有损图像比同类JPG图像字节数少25-34%。现如今各大互联网公司的产品都已经使用了,如淘宝、京东和美团等。


    这里放一个 WebP 示例链接(GIF、PNG、JPG 转 Webp),直观感受 WebP 在图片大小上的优势。


    image-20211020191505147.png


    在移动端中 WebP的兼容性,大部分数用户都已经支持了 Can I use... Support tables for HTML5, CSS3, etc


    image-20211020131150424.png


    针对png/jpg图片格式,自动添加WebP参数,转成WebP图片格式。虽然WebP相比png/jpg图片解码可能需要更长时间,但相对网络传输速度提升还是很大。目前 ios 13系统版本有不少用户量的占比,小程序端获取当前系统版本,降级处理不添加WebP参数。


    // 检查是否支持webp格式
    const checkSupportWebp = () => {
    const { system } = wx.getSystemInfoSync();
    const [platform, version] = system.split(' ');

    if (platform.toLocaleUpperCase() === PlatformEnum.IOS) {
    return Number(version.split('.')[0]) > IOS_VERSION_13;
    }

    return true; // 默认支持webp格式
    };


    提示:由于目前图片服务器并不支持、SVG、GIFWebP,并没有做处理



    优化效果


    测试我们小程序首页列表接口加载图片,来对比优化前后的效果


    切片.png


    经过我们通过使用腾讯云图片服务器的图片处理功能,以及动态处理图片格式的方式,减少图片体积,提高图片加载速度,带来的收益比非常可观的


    图片懒加载


    懒加载是一种性能优化的方式,将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载,对于页面加载性能上会有很大的提升,也提高了用户体验。


    实现原理


    使用小程序提供Intersection Observer API,监听某些节点是否可以被用户看见、有多大比例可以被用户看见。这样我们就能判断图片元素是否在可是范围中,进行图片加载。


    我们基于小程序的Intersection Observer API,封装一个监听模块曝光 IntersectionObserver函数工具,提供以下用法


    import IntersectionObserver from 'utils/observer/observer';

    const ob = new IntersectionObserver({
    selector: '.goods-item', // 指定监听的目标节点元素
    observeAll: true, // 是否同时观测多个目标节点
    context: this, // 小程序 this 对象实例
    delay: 200, // 调用 onFinal 方法的间隔时间,默认 200ms
    onEach: ({ dataset }) => {
    // 每一次触发监听调用时,触发 onEach 方法,可以对数据进行一些过滤处理
    const { key } = dataset || {};
    return key;
    },
    onFinal: (data) => {
    // 在触发监听调用一段时间 delay 后,会调用一次 onFinal 方法,可以进行埋点上报
    if (!data) return;
    console.log('module view data', data);
    },
    });

    // 内置函数方法,如下:
    ob.connect(); // 开始监听
    ob.disconnect(); // 停止监听
    ob.reconnect(); // 重置监听

    然后在我们的FreeImage图片组件,添加可视区域加载图片的功能,以下是部分代码


    import IntersectionObserver from 'utils/observer';

    Component({
    properties: {
    src: String,
    /**
    * 是否开启可视区域加载图片
    */
    observer: {
    type: Boolean,
    value: false,
    },
    ....
    },

    data: {
    isObserver: false,
    ...
    },

    lifetimes: {
    attached() {
    // 开启可视区域加载图片
    if (this.data.observer) {
    this.createObserver();
    }
    },
    },
    methods: {
    ...

    /**
    * 监听图片是否进入可视区域
    */
    createObserver() {
    const ob = new IntersectionObserver({
    selector: '.free-image',
    observeAll: true,
    context: this,
    onFinal: (data = []) => {
    data.forEach((item: any) => {
    this.setData({
    isObserver: true,
    });
    ob.disconnect(); // 取消监听
    });
    },
    });

    ob.connect(); // 开始监听
    }
    }
    })

    <free-image observer src="{{ src }}" />

    优化效果


    测试我们小程序首页列表,使用图片懒加载的效果


    27a0b7a88a6e18665fa1ff33b3726b68.gif


    通过使用图片懒加载的功能,减少图片数量的加载,有效提高页面加载性能。在上述我们已经对图片体积进行优化过,所以在我们小程序中,只有在网络情况较差的情况下,才会自动开启图片懒加载功能。


    优化请求数


    我们项目中有很多本地图片资源,比如一些 icon 图标、标签类切图、背景图、图片按钮等。而小程序分包大小是有限制:整个小程序所有分包大小不超过 20M,而单个分包/主包大小不能超过 2M。所以为了减轻小程序体积,本地图片资源需要进行调整,比如图片压缩、上传到 CDN 服务器。这样能减少了小程序主包大小,而大部分图片都在腾讯云 CDN 服务器中,虽然可以加速资源的请求速度,当页面打开需要同时下载大量的图片的话,就会严重影响了用户的使用体验。


    针对此问题,需要找到权衡点来实现来优化请求数,首先我们把图片资源进行分类,以及使用场景,最后确定我们方案如下:



    • 较大体积的图片,选择上传到 CDN 服务器

    • 单色图标使用 iconfont 字体图标,多彩图标则使用svg格式

    • 标签类的图片,则生成雪碧图之后上传到 CDN 服务器

    • 图片体积小于10KB,结合使用场景,则考虑base64 ,比如一张图片体积为3KB的背景图,由于小程序css background不支持本地图片引入,可以使用 base64 方式实现


    其他策略


    大图检测

    实现大图检测机制,及时发现图片不符合规范的问题,当发现图片尺寸太大,不符合商品图尺寸标准时会进行上报。在小程序开发版/体验版中,当我们设置开启Debug模式,图片组件FreeImage会自动检测到大图片时,显示当前图片尺寸、以及设置图片高亮/翻转的方式提醒运营同学和设计同学进行处理



    加载失败处理

    使用腾讯云图片处理功能,URL预处理转换后得新 URL,可能会存在少量图片不存在的异常场景导致加载失败。遇到图片加载失败时,我们还是需要重新加载原始图片 URL, 之后会将错误图片 URL 上报到监控平台,方便之后调整 URL 预处理转换规则,同时也发现一部分错误的图片 URL 推动业务修改。


    这是我们图片组件FreeImage 处理图片加载失败,以下是部分代码


    onError(event: WechatMiniprogram.TouchEvent) {
    const { src, useCosImage } = this.data;

    this.setData({
    loading: false,
    error: true,
    lazy: 'error',
    });

    // 判断是否腾讯云服务的图片
    if (useCosImage) {
    wx.nextTick(() => {
    // 重新加载原生图片
    this.setData({
    formattedSrc: src, // src 是原图地址
    });
    });
    }

    // 上报图片加载失败
    app.aegis.report(AegisEnum.IMAGE_LOAD_FAIL, {
    src,
    errMsg: event?.detail.errMsg,
    });

    this.triggerEvent('error', event.detail);
    }

    图片请求数检查

    使用小程序开发者工具的体验评分功能,体验评分是一项给小程序的体验好坏打分的功能,它会在小程序运行过程中实时检查,分析出一些可能导致体验不好的地方,并且定位出哪里有问题,以及给出一些优化建议。


    image-20211024170719264.png


    通过体验评分的结果,可以分析我们存在短时间内发起太多的图片请求,以及存在图片太大而有效显示区域较小。所以根据分析的结果,开发需要合理控制数量,可考虑使用雪碧图技术、拆分域名或在屏幕外的图片使用懒加载等。


    上传压缩

    图片在上传前在保持可接受的清晰度范围内同时减少文件大小,进行合理压缩。现如今有很多不错的图片压缩插件工具,就不在详情介绍了。


    推荐一个比较优秀的图片压缩网站:TinyPNG使用智能有损压缩技术将您的 WebP, PNG and JPEG 图片的文件大小降低


    作者:稻草叔叔
    链接:https://juejin.cn/post/7031851192481218574

    收起阅读 »

    代码写得好,Reduce 方法少不了,我用这10例子来加深学习!

    数组中的 reduce 犹如一只魔法棒,通过它可以做一些黑科技一样的事情。语法如下: reduce(callback(accumulator, currentValue[, index, array])[,initialValue]) reduce 接受两个参...
    继续阅读 »

    数组中的 reduce 犹如一只魔法棒,通过它可以做一些黑科技一样的事情。语法如下:


    reduce(callback(accumulator, currentValue[, index, array])[,initialValue])

    reduce 接受两个参数,回调函数和初识值,初始值是可选的。回调函数接受4个参数:积累值、当前值、当前下标、当前数组。


    如果 reduce的参数只有一个,那么积累值一开始是数组中第一个值,如果reduce的参数有两个,那么积累值一开始是出入的 initialValue 初始值。然后在每一次迭代时,返回的值作为下一次迭代的 accumulator 积累值。


    今天的这些例子的大多数可能不是问题的理想解决方案,主要的目的是想说介绍如何使用reduce来解决问题。


    求和和乘法


    // 求和
    [3, 5, 4, 3, 6, 2, 3, 4].reduce((a, i) => a + i);
    // 30

    // 有初始化值
    [3, 5, 4, 3, 6, 2, 3, 4].reduce((a, i) => a + i, 5 );
    // 35

    // 如果看不懂第一个的代码,那么下面的代码与它等价
    [3, 5, 4, 3, 6, 2, 3, 4].reduce(function(a, i){return (a + i)}, 0 );

    // 乘法
    [3, 5, 4, 3, 6, 2, 3, 4].reduce((a, i) => a * i);

    查找数组中的最大值


    如果要使用 reduce 查找数组中的最大值,可以这么做:


    [3, 5, 4, 3, 6, 2, 3, 4].reduce((a, i) => Math.max(a, i), -Infinity);

    上面,在每一次迭代中,我们返回累加器和当前项之间的最大值,最后我们得到整个数组的最大值。


    如果你真想在数组中找到最大值,不要有上面这个,用下面这个更简洁:


    Math.max(...[3, 5, 4, 3, 6, 2, 3, 4]);

    连接不均匀数组


    let data = [
    ["The","red", "horse"],
    ["Plane","over","the","ocean"],
    ["Chocolate","ice","cream","is","awesome"],
    ["this","is","a","long","sentence"]
    ]
    let dataConcat = data.map(item=>item.reduce((a,i)=>`${a} ${i}`))

    // 结果
    ['The red horse',
    'Plane over the ocean',
    'Chocolate ice cream is awesome',
    'this is a long sentence']

    在这里我们使用 map 来遍历数组中的每一项,我们对所有的数组进行还原,并将数组还原成一个字符串。


    移除数组中的重复项


    let dupes = [1,2,3,'a','a','f',3,4,2,'d','d']
    let withOutDupes = dupes.reduce((noDupes, curVal) => {
    if (noDupes.indexOf(curVal) === -1) { noDupes.push(curVal) }
    return noDupes
    }, [])

    检查当前值是否在累加器数组上存在,如果没有则返回-1,然后添加它。


    当然可以用 Set 的方式来快速删除重复值,有兴趣的可以自己去谷歌一下。


    验证括号


    [..."(())()(()())"].reduce((a,i)=> i==='('?a+1:a-1,0);
    // 0

    [..."((())()(()())"].reduce((a,i)=> i==='('?a+1:a-1,0);
    // 1

    [..."(())()(()()))"].reduce((a,i)=> i==='('?a+1:a-1,0);
    // -1

    这是一个很酷的项目,之前在力扣中有刷到。


    按属性分组


    let obj = [
    {name: 'Alice', job: 'Data Analyst', country: 'AU'},
    {name: 'Bob', job: 'Pilot', country: 'US'},
    {name: 'Lewis', job: 'Pilot', country: 'US'},
    {name: 'Karen', job: 'Software Eng', country: 'CA'},
    {name: 'Jona', job: 'Painter', country: 'CA'},
    {name: 'Jeremy', job: 'Artist', country: 'SP'},
    ]
    let ppl = obj.reduce((group, curP) => {
    let newkey = curP['country']
    if(!group[newkey]){
    group[newkey]=[]
    }
    group[newkey].push(curP)
    return group
    }, [])

    这里,我们根据 country 对第一个对象数组进行分组,在每次迭代中,我们检查键是否存在,如果不存在,我们创建一个数组,然后将当前的对象添加到该数组中,并返回组数组。


    你可以用它做一个函数,用一个指定的键来分组对象。


    扁平数组


    let flattened = [[3, 4, 5], [2, 5, 3], [4, 5, 6]].reduce(
    (singleArr, nextArray) => singleArr.concat(nextArray), [])

    // 结果:[3, 4, 5, 2, 5, 3, 4, 5, 6]

    这只是一层,如果有多层,可以用递归函数来解决,但我不太喜欢在 JS 上做递归的东西😂。


    一个预定的方法是使用.flat方法,它将做同样的事情


    [ [3, 4, 5],
    [2, 5, 3],
    [4, 5, 6]
    ].flat();

    只有幂的正数


    [-3, 4, 7, 2, 4].reduce((acc, cur) => {
    if (cur> 0) {
    let R = cur**2;
    acc.push(R);
    }
    return acc;
    }, []);

    // 结果
    [16, 49, 4, 144]

    反转字符串


    const reverseStr = str=>[...str].reduce((a,v)=>v+a)

    这个方法适用于任何对象,不仅适用于字符串。调用reverseStr("Hola"),输出的结果是aloH


    二进制转十进制


    const bin2dec = str=>[...String(str)].reduce((acc,cur)=>+cur+acc*2,0)

    // 等价于

    const bin2dec = (str) => {
    return [...String(str)].reduce((acc,cur)=>{
    return +cur+acc*2
    },0)
    }

    为了说明这一点,让我们看一个例子:(10111)->1+(1+(1+(0+(1+0*2)*2)*2)*2)*2


    ~完,我是刷碗智,励志等退休后,要回家摆地摊的人,我们下期见!




    代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug


    作者:前端小智
    链接:https://juejin.cn/post/7032061650479874061

    收起阅读 »

    面试官:请你实现一下JS重载?可不是TS重载哦!

    一位同学:“如何实现JS重载?”我:“JS有重载吗?不是TS才有吗?”一位同学:“有的,这是网易一道面试题”我:“好吧我想想哈!”什么是重载我第一次看到重载这个词还是在以前学习Java的时候,我一直觉得JavaScript是没有重载的,直到TypeScript...
    继续阅读 »
    • 一位同学:“如何实现JS重载?”
    • 我:“JS有重载吗?不是TS才有吗?”
    • 一位同学:“有的,这是网易一道面试题”
    • 我:“好吧我想想哈!”

    image.png

    什么是重载

    我第一次看到重载这个词还是在以前学习Java的时候,我一直觉得JavaScript是没有重载的,直到TypeScript的出现,所以我一直觉得JavaScript没有重载,TypeScript才有,但是现在看来我是错的。

    我理解的重载是:同样的函数,不同样的参数个数,执行不同的代码,比如:

    /*
    * 重载
    */
    function fn(name) {
    console.log(`我是${name}`)
    }

    function fn(name, age) {
    console.log(`我是${name},今年${age}岁`)
    }

    function fn(name, age, sport) {
    console.log(`我是${name},今年${age}岁,喜欢运动是${sport}`)
    }

    /*
    * 理想结果
    */
    fn('林三心') // 我是林三心
    fn('林三心', 18) // 我是林三心,今年18岁
    fn('林三心', 18, '打篮球') // 我是林三心,今年18岁,喜欢运动是打篮球

    但是直接在JavaScript中这么写,肯定是不行的,咱们来看看上面代码的实际执行结果,可以看到,最后一个fn的定义,把前面两个都给覆盖了,所以没有实现重载的效果

    我是林三心,今年undefined岁,喜欢运动是undefined
    我是林三心,今年18岁,喜欢运动是undefined
    我是林三心,今年18岁,喜欢运动是打篮球

    我的做法

    其实,想要实现理想的重载效果,我还是有办法的,我可以只写一个fn函数,并在这个函数中判断arguments类数组的长度,执行不同的代码,就可以完成重载的效果

    function fn() {
    switch (arguments.length) {
    case 1:
    var [name] = arguments
    console.log(`我是${name}`)
    break;
    case 2:
    var [name, age] = arguments
    console.log(`我是${name},今年${age}岁`)
    break;
    case 3:
    var [name, age, sport] = arguments
    console.log(`我是${name},今年${age}岁,喜欢运动是${sport}`)
    break;
    }
    }

    /*
    * 实现效果
    */
    fn('林三心') // 我是林三心
    fn('林三心', 18) // 我是林三心,今年18岁
    fn('林三心', 18, '打篮球') // 我是林三心,今年18岁,喜欢运动是打篮球

    但是那位同学说,网易的面试官好像觉得这么实现可以是可以,但是还有没有更好的实现方法,我就懵逼了。

    高端做法

    image.png

    经过了我的一通网上查找资料,发现了一种比较高端的做法,可以利用闭包来实现重载的效果。这个方法在JQuery之父John Resig写的《secrets of the JavaScript ninja》中,这种方法充分的利用了闭包的特性!

    function addMethod(object, name, fn) {
    var old = object[name]; //把前一次添加的方法存在一个临时变量old里面
    object[name] = function () { // 重写了object[name]的方法
    // 如果调用object[name]方法时,传入的参数个数跟预期的一致,则直接调用
    if (fn.length === arguments.length) {
    return fn.apply(this, arguments);
    // 否则,判断old是否是函数,如果是,就调用old
    } else if (typeof old === "function") {
    return old.apply(this, arguments);
    }
    }
    }

    addMethod(window, 'fn', (name) => console.log(`我是${name}`))
    addMethod(window, 'fn', (name, age) => console.log(`我是${name},今年${age}岁`))
    addMethod(window, 'fn', (name, age, sport) => console.log(`我是${name},今年${age}岁,喜欢运动是${sport}`))

    /*
    * 实现效果
    */

    window.fn('林三心') // 我是林三心
    window.fn('林三心', 18) // 我是林三心,今年18岁
    window.fn('林三心', 18, '打篮球') // 我是林三心,今年18岁,喜欢运动是打篮球

    结语

    如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群 想进学习群,摸鱼群,请点击这里摸鱼,我会定时直播模拟面试,简历指导,答疑解惑

    image.png


    作者:Sunshine_Lin
    链接:https://juejin.cn/post/7031525301414805518

    收起阅读 »

    建议收藏!!VueRouter原理和ReactRouter原理

    简述 其实Vue和React在很多地方,底层原理和语法上差别并不是很大。底层原理更多的是相同的。就比如说React有JSX,Vue有Template。其实就可以理解成一个东西,就是写法不同。文章 【今天学习了吗?hash 路由和 history 路由简介】简单...
    继续阅读 »

    简述


    其实Vue和React在很多地方,底层原理和语法上差别并不是很大。底层原理更多的是相同的。就比如说React有JSX,Vue有Template。其实就可以理解成一个东西,就是写法不同。文章 【今天学习了吗?hash 路由和 history 路由简介】简单介绍了vue的两种路由模式,但是其背后的原理是什么呢?这里和React路由一起介绍一下!希望对读者有所帮助 ~~~


    更新视图但不重新请求页面,是前端路由原理的核心之一!!


    Hash模式

    hash 虽然出现在 url 中,但不会被包括在 http 请求中,它是用来指导浏览器动作的,对服务器端完全无用,因此,改变 hash 不会重新加载页面。

    可以为 hash 的改变添加监听事件:window.addEventListener('hashchange',callBack)

    每一次改变 hash(window.localtion.hash),都会在浏览器访问历史中增加一个记录。利用 hash 的以上特点,就可以来实现前端路由"更新视图但不重新请求页面"的功能了。



    我们就可以通过 hashchange 去处理一些特殊的操作,执行一些情况下才会执行的代码。而 Vue / React 应用的正是这一原理。通过不同的 路由去调用不同的 函数/JS 去生成不同的页面代码。



    举个栗子:


    // 这是一个hash模式的网址例子
    http://www.xxx.com/#/abcd123

    function callBack(e) {
    // 通过event对象去获取当前的路由,来判断下一步要进行的一些操作,当然这里不止包含Dom,
    // 其他的操作也是可以的
    console.log(e.oldURL)
    console.log(e.newURL)
    }
    window.addEventListener('hashchange',callBack)


    目前hash模式支持最低版本是IE8,这也就是为什么都说hash模式的兼容性更好了。其实 React 和 Vue 的hash模式的路由底层就是这么简单的。



    History模式


    History模式,即http://www.xxxx.com/abc/dcd


    这种模式会造成浏览器重新请求服务器路由,首先去获取服务器相应的path下的文件。若没有则会造成 404 Not Found! 当然这种形式需要服务端进行配合,将路由重新重定向到我们的打包出来的index.html文件上。


    History模式其实就是ES6中的新增BOM对象History。Vue 和 React 设计的也很巧妙,完美的使用了ES6的新增属性。ES6新增的BOM对象History如下:


    20316322-a9b1585b2694c9b6.webp


    proto里面包含了replaceState 和 pushState方法。replaceState 和 pushState 其实就是vue中的 replace 和 push ,不过就是Vue的作者将其再进行了封装了。


    History 存储历史记录是 队列存储 的,也可以理解为一个数组。它也是有 length 属性的。
    我们平时操作 go(num) 其实调用的就是这个History队列里面的历史数据,并找到相应的索引进行一个跳转。


    因为IE9才支持ES6,所以History模式并不支持IE9以下版本。所以说Hash模式的兼容更好。


    以上就是 Vue 和 React 两种路由的底层原理了。


    作者:不是Null
    链接:https://juejin.cn/post/7031820537676611614
    收起阅读 »

    关于web中的颜色表示方法,你知道多少?

    想要表示web中的各种颜色,大家首先想到的大概就是用十六进制或者RGB来表示。但在实际web中,是远不止这两种的。今天这篇文章就和大家聊一聊,在web中颜色的各种表示方法。 以如下代码为例,大家可以复制代码看看效果: HTML <div class="b...
    继续阅读 »

    想要表示web中的各种颜色,大家首先想到的大概就是用十六进制或者RGB来表示。但在实际web中,是远不止这两种的。今天这篇文章就和大家聊一聊,在web中颜色的各种表示方法。


    以如下代码为例,大家可以复制代码看看效果:


    HTML


    <div class="box">
    <div class="one"></div>
    <div class="two"></div>
    <div class="three"></div>
    </div>

    CSS


    .box {
    width: 200px;
    height: 200px;
    padding: 20px 20px;
    display: flex;
    justify-content: space-between;
    }
    .box > div {
    width: 50px;
    height: 50px;
    border-radius: 4px;
    }

    英文单词


    HTML 和 CSS 颜色规范中预定义了 140+ 个颜色名称,可以点进这里进行查看。直接用英文单词的好处是直接明了,缺点是140+个单词确实难记,也不能包含所有的颜色。


    .one { background-color: red; }
    .two { background-color: green; }
    .three { background-color: blue; }

    十六进制


    十六进制表示颜色:#RRGGBB ,这里的十六进制实质就是RGB的十六进制表示法,每两位表示RR(红色)、GG(绿色)和 BB(蓝色)三色通道的色阶。所有值必须在 00 到 FF 之间。


    .one { background-color: #00FFFF; }
    .two { background-color: #FAEBD7; }
    .three { background-color: #7FFFD4; }

    对于类似于 #00FFFF 的颜色格式也可以缩写为 #0FF


    .one { background-color: #0FF; }

    如果需要带上透明度,还可以像下面这样增加两个额外的数字:


    .one { background-color: #00FFFF80; }

    RGB


    rgb() 函数中,CSS语法如下:


    rgb(red, green, blue)

    每个参数 red, green, blue 定义颜色的强度,可以是 0 到 255 之间的整数或百分比值(从 0% 到 100%)


    .one { background-color: rgb(112,128,144); }
    .two { background-color: rgb(30%,10%,60%); }
    .three { background-color: rgb( 0,139,139); }
    复制代码

    十六进制和RGB的原理都是利用了光的三原色:红色,绿色,蓝色。利用这三种颜色就能组合出上千万种颜色。简单的计算一下,256级的RGB色彩总共能组合出约1678万种色彩,即256×256×256=16777216种。至于为什么是256级,因为 0 也是数值之一。


    RGBA


    RGBA就是在RGB之上扩展了一个 Alpha 通道 ,指定对象的不透明度。


    .one { background-color: rgba(112,128,144, 0.5); }
    .two { background-color: rgb(30%,10%,60%, 0.2); }
    .three { background-color: rgb( 0,139,139, 0.5); }

    HSL


    HSL 分别代表 色相(hue)、饱和度(saturation)和亮度(lightness),是一种将RGB色彩模型中的点在圆柱坐标系中的表示法


    CSS语法如下:


    hsl(hue, saturation, lightness)


    • 色相:色轮上的度数(从 0 到 360)- 0(或 360)是红色,120 是绿色,240 是蓝色。

    • 饱和度:一个百分比值; 0% 表示灰色阴影,而 100% 是全彩色。

    • 亮度:一个百分比; 0% 是黑色,100% 是白色。


    例子:


    .one { background-color: hsl(20, 100%, 50%); }
    .two { background-color: hsl(130, 100%, 25%); }
    .three { background-color: hsl(240, 80%, 80%); }

    HSLA


    HSLA 和 HSL 的关系与 RGBA 和 RGB 的关系类似,HSLA 颜色值在 HSL 颜色值上扩展 Alpha 通道 - 指定对象的不透明度。


    CSS语法如下:


    hsla(hue, saturation, lightness, alpha)

    例子:


    .one { background-color: hsla(20, 100%, 50%, 0.5); }
    .two { background-color: hsla(130, 100%, 25%, 0.75); }
    .three { background-color: hsla(240, 80%, 80%,0.4); }
    复制代码

    opacity


    opacity 属性设置一个元素了透明度级别。


    CSS语法如下:


    opacity: value|inherit;

    它与 RGBA 中的 A 在行为上有一定的区别:opacity 同时影响子元素的样式,而 RGBA 则不会。感兴趣的可以试一试。


    关键字


    除了 <color>s 的各种数字语法之外,CSS还定义了几组关于颜色的关键字,这些关键字都有各自的有点和用例。这里介绍一下两个特殊的关键字 transparentcurrentcolor


    transparent


    transparen 指定透明黑色,如果一个元素覆盖在另外一个元素之上,而你想显示下面的元素;或者你不希望某元素拥有背景色,同时又不希望用户对浏览器的颜色设置影响到您的设计。 transparent 就能派上用场了。


    在CSS1中,transparent 是作为 background-color 的一个值来用的,在后续的 CSS2 和 CSS3 中, transparent 可以用在任何一个有 color 值的属性上了。


    .one { 
    background-color: transparent;
    color: transparent;
    border-color: transparent;
    }

    currentcolor


    currentcolor 关键字可以引用元素的 color 属性值。


    .one { 
    color: red;
    border: 1px solid currentcolor;
    }

    相当于


    .one { 
    color: red;
    border: 1px solid red;
    }

    下面介绍的这些目前主流浏览器还没有很好的支持,但是已经列为CSS4标准了,所以了解一下也是挺好的。


    HWB


    hwb() 函数表示法根据颜色的色调、白度和黑度来表示给定的颜色。也可以添加 alpha 组件来表示颜色的透明度。


    语法如下:


    hwb[a](H W B[/ A])

    例子:


    hwb(180 0% 0%)
    hwb(180 0% 0% / .5)
    hwb(180, 0%, 0%, .5); /* 使用逗号分隔符 */

    目前只有Safari支持。


    Lab、Lch


    lab() 函数表示法表示 CIE L * a * b * 颜色空间中的给定颜色,L* 代表亮度,取值范围是[0,100]; a* 代表从绿色到红色的分量,取值范围是[127,-128]; b* 代表从蓝色到黄色的分量 ,取值范围是[127,-128]。理论上可以展示出人类可以看到的全部颜色范围。


    语法如下:


    lab(L a b [/ A])

    例子:


    lab(29.2345% 39.3825 20.0664);
    lab(52.2345% 40.1645 59.9971);

    lch() 函数表示法表示CIE LCH 颜色空间中给定的颜色,采用了同 L * a * b * 一样的颜色空间,但它采用L表示明度值,C表示饱和度值,H表示色调角度值的柱形坐标。


    语法如下:


    lch(L C H [/ A])

    例子:


    lch(29.2345% 44.2 27);
    lch(52.2345% 72.2 56.2);

    关于常用颜色空间的概念,可以自行查询,或者点击这篇文章进行了解。


    color()


    color() 函数表示法允许在特定的颜色空间中指定颜色。


    语法如下:


    color( [ [<ident> | <dashed-ident>]? [ <number-percentage>+ | <string> ] [ / <alpha-value> ]? ] )

    例子:


    color(display-p3 -0.6112 1.0079 -0.2192);
    color(profoto-rgb 0.4835 0.9167 0.2188);这里可以了解一下色域标准

    CMYK


    CMYK印刷四色模式



    印刷四色模式,是彩色印刷时采用的一种套色模式,利用色料的三原色混色原理,加上黑色油墨,共计四种颜色混合叠加,形成所谓“全彩印刷”。四种标准颜色是:C:Cyan = 青色,又称为‘天蓝色’或是‘湛蓝’M:Magenta = 品红色,又称为‘洋红色’;Y:Yellow = 黄色;K:blacK=黑色。此处缩写使用最后一个字母K而非开头的B,是为了避免与Blue混淆。CMYK模式是减色模式,相对应的RGB模式是加色模式。



    电脑显示屏使用 RGB 颜色值显示颜色,而打印机通常使用 CMYK 颜色值显示颜色。在CSS4标准中,计划利用 device-cmyk() 函数来实现。


    语法如下:


    device-cmyk() = device-cmyk( <cmyk-component>{4} [ / <alpha-value> ]? , <color>? )
    <cmyk-component> = <number> | <percentage>

    例子:


    device-cmyk(0 81% 81% 30%);
    device-cmyk(0 81% 81% 30% / .5);
    作者:xmanlin
    链接:https://juejin.cn/post/7031700587120951310

    收起阅读 »

    使用这11个代码,可以大大地简化我们的代码。

    1.避免 if 过长 如果判断值满足多个条件,我们可能会这么写: if (value === 'a' || value === 'b' || value === 'c') { ... } 像这样如果有多个条件,if 条件就会很我,可读性降低,我们可以这样简化:...
    继续阅读 »

    1.避免 if 过长


    如果判断值满足多个条件,我们可能会这么写:


    if (value === 'a' || value === 'b' || value === 'c') { ... }

    像这样如果有多个条件,if 条件就会很我,可读性降低,我们可以这样简化:


    if (['a', 'b', 'c'].includes(value)) { ... }

    2.双!操作符将任何变量转换为布尔值


    !(NOT)运算符可以使用两次!!,这样可以将任何变量转换为布尔值(像布尔函数),当你需要在处理它之前检查某个值时非常方便。


    const toto = null

    !!toto // false
    Boolean(toto) // false

    if (!!toto) { } // toto is not null or undefined

    3.可选项 (?)


    在 JS 中,我们需要经常检查对象的某些属性是否存在,然后才能再处理它,不然会报错。 早期我们可能会这么干:


    const toto = { a: { b: { c: 5 } } }

    if (!!toto.a && !!toto.a.b && !!toto.a.b.c) { ... } // toto.a.b.c exist

    如果对象嵌套很深,我们这写法就难以阅读,这时可以使用?来简化:



    if (!!toto.a?.b?.c) { ... } // toto.a.b.c exist

    // 如果键不存在,返回 `undefined`。
    const test = toto.a?.b?.c?.d // undefined

    4. 如果if中返回值时, 就不要在写 else


    经常会看到这种写法:


    if (...) {
    return 'toto'
    } else {
    return 'tutu'
    }

    如果if有返回值了,可以这样写:


    if (...) {
    return 'toto'
    }

    return 'tutu'

    5.避免forEach,多使用filtermapreduceeverysome


    作为初学者,我们使用了很多forEach函数,但 JS 为我们提供了很多选择,而且这些函数是FP(函数式编程)。


    filter


    filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。


    const toto = [1, 2, 3, 4]

    // 过滤奇数
    const evenValue = toto.filter(currentValue => {
    return currentValue % 2 == 0
    }) // [2, 4]

    map


    map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。


    const toto = [1, 2, 3, 4]

    const valueMultiplied = toto.map(currentValue => {
    return currentValue * 2
    }) // [2, 4, 6, 8]

    reduce


    reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。


    const toto = [1, 2, 3, 4]

    const sum = toto.reduce((accumulator, currentValue) => {
    return accumulator += currentValue
    }, 0) // 10

    Some & Every


    some() 方法测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个Boolean类型的值。


    every() 方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。


    什么时候使用?


    所有项目都符合一个条件可以用 every


    const toto = [ 2, 4 ]

    toto.every(val => val % 2 === 0) // true

    const falsyToto = [ 2, 4, 5 ]

    falsyToto.every(val => val % 2 === 0) // false

    只要一个符合条件就行,用some


    const toto = [ 2, 4, 5 ]

    toto.some(val => val % 2 !== 0) // return true

    6.不要使用 delete 来删除属性


    从一个对象中 delete 一个属性是非常不好的(性能不好),此外,它还会产生很多副作用。


    但是如果你需要删除一个属性,你应该怎么做?


    可以使用函数方式创建一个没有此属性的新对象,如下所示:


    const removeProperty = (target, propertyToRemove) => {
    const { [propertyToRemove]: _, ...newTarget } = target
    return newTarget
    }
    const toto = { a: 55, b: 66 }
    const totoWithoutB = removeProperty(toto, 'b') // { a: 55 }

    7.仅当对象存在时才向其添加属性


    有时,如果对象已经定义了属性,我们需要向对象添加属性,我们可能会这样写:


    const toto = { name: 'toto' }
    const other = { other: 'other' }
    // The condition is not important
    const condition = true

    if (condition) {
    other.name = toto.name
    }

    ❌不是很好的代码


    ✅ 可以用一些更优雅的东西!


    const condition = true

    const other = {
    other: 'other',
    ...condition && { name: 'toto' }
    }

    8. 使用模板字符串


    在 JS 中学习字符串时,我们需要将它们与变量连接起来


    const toto = 'toto'
    const message = 'hello from ' + toto + '!' // hello from toto!

    如果还有其它变量,我们就得写很长的表达式,这时可以使用模板字符串来优化。


    const toto = 'toto'
    const message = `hello from ${toto}!` // hello from toto!

    9. 条件简写


    当条件为 true 时,执行某些操作,我们可能会这样写:


    if(condition){
    toto()
    }

    这种方式可以用 && 简写:


    condition && toto()

    10.设置变量的默认值


    如果需要给一个变量设置一个默认值,可以这么做:


    let toto

    console.log(toto) //undefined

    toto = toto ?? 'default value'

    console.log(toto) //default value

    toto = toto ?? 'new value'

    console.log(toto) //default value

    11.使用 console timer


    如果需要知道一个函数的执行时间,可以这么做:


    for (i = 0; i < 100000; i++) {
    // some code
    }
    console.timeEnd() // x ms




    作者:前端小智
    链接:https://juejin.cn/post/7031691510533849124

    收起阅读 »

    【白话前端】从一个故事说明白“浏览器缓存”

    一则小故事 小明常去图书馆借阅英文杂志回家看,由于单词量少,他同时需要借阅一本《英汉词典》; 起初,和图书管理员不熟,每次他都要在图书馆借英文杂志和《英汉词典》,放在书包里背回家;这个过程,暂且将其称为“不缓存”; 后来,小明发现图书管理员竟是妈妈的...
    继续阅读 »

    一则小故事



    小明常去图书馆借阅英文杂志回家看,由于单词量少,他同时需要借阅一本《英汉词典》;




    起初,和图书管理员不熟,每次他都要在图书馆借英文杂志和《英汉词典》,放在书包里背回家;这个过程,暂且将其称为“不缓存”;




    后来,小明发现图书管理员竟是妈妈的好朋友和好邻居王叔叔,经过相认后,王叔叔对小明说:“你每次都要借阅《英汉词典》,我直接借你一整年,在一年内你可以将它放在家里,不需要每次到图书馆来借阅。”小明听了非常高兴,因为他的书包可以轻上一大截;可以持有《英汉词典》一整年的过程,暂且称为“强缓存”;




    再后来,小明发现图书管理员王叔叔经常去家里做客,两人关系也愈发亲密;小明问:“王叔叔,英文杂志的更新总是很不规律,我经常去了图书馆,英文杂志却未更新,我借到的依然是上一期的杂志,有啥办法让我少跑路吗?”




    王叔叔笑着说:“这还不简单?每次你准备去借阅之前,先把你手里当前持有的杂志期号(etag)用短信发给我,如果图书馆没有更新,我就给你一个304的暗号,你就还是接着读家里那本;如果有了更新,我给你一个200的暗号,你再来图书馆拿书就行;”这个过程,暂且被称为“协商缓存”



    逐渐装逼


    不缓存


    不缓存是最容易理解的缓存策略,也最不容易出错,只要每一次刷新页面都去服务器取数据即可;但同样的,不缓存意味着页面加载速度会更慢


    要设置不缓存也很容易,只需要将资源文件的Response Header中的Cache-Control设为no-store即可;



    Cache-Control: no-store



    cache-control 属性之一:可缓存性



    强缓存


    对于已知的几乎不会发生变化的资源,可以通过调整策略,使得浏览器在限定时间内,直接从本地缓存获取,这就是所谓的强缓存;

    要配置静态资源的强缓存,通常需要发送的缓存头如下:



    Cache-Control:public, max-age=31536000



    以下是强缓存常用的两种属性↓;


    cache-control 属性之:可缓存性



    cache-control 属性之: 到期



    协商缓存


    其实上面故事里关于协商缓存的描述,有一点是非常不准确的,那就是对于浏览器而言,小明发送给王叔叔的不是所谓的“杂志期号”,而是杂志的散列(hash);而这个hash,自然也是王叔叔(服务器端)告诉小明(客户端)的;


    在真实情况下,浏览器的协商缓存要触发,只有两种情况:



    1.Cache-Control 的值为 no-cache (不强缓存)



    or



    2.max-age 过期了 (强缓存,但总有过期的时候)



    只有在这两种情况下满足其中至少一种时,才会进入协商缓存的过程;


    因此,常规的协商缓存,通常分为以下几步:



    step1

    浏览器第一次发起请求,request上并没有相应请求头;

    (小明第一次去图书馆借书)




    step2

    服务器第一次返回资源,response上带上了两个属性:
    etag: "33a64df"

    last-modified: Mon, 12 Dec 2020 12:12:12 GMT

    (王叔叔借给小明一本书,并告诉小明这本杂志的编号,以及它的发刊日期)




    step3

    浏览器第二次发起请求,request上携带了上一次请求返回的内容:

    if-none-matched: "33a64df"

    if-modified-since: Mon, 12 Dec 2020 12:12:12 GMT

    (小明第二次借书,先进行了询问:上一次借我的那本的编号和上一次更改后是否有变动?)




    step4

    服务器发现资源没有改变,于是返回了304状态码;

    浏览器直接在本地读取缓存;

    (王叔叔说:还没来新货,你先读着上次借的那本吧)




    step5

    浏览器第三次发起请求,request上携带了上一次请求返回的内容:

    if-none-matched: "33a64df"

    if-modified-since: Mon, 12 Dec 2020 12:12:12 GMT

    (小明第三次借书,先进行了询问:上一次借我的那本的编号和上一次更改后是否有变动?)




    step6

    服务器检查之后发现,文件已经发生了变化,于是将新的资源、编号、最后变更时间一起返回给了客户端;并返回了200状态码;
    if-none-matched: "sd423dss"

    if-modified-since: Mon, 30 Dec 2020 12:12:12 GMT

    (王叔叔说:来了来了,最新一期的杂志编号、发刊日期如下,这是杂志本身,也一起给你;)



    上面过程展示了一次协商缓存生效的过程;


    如何在项目中使用?


    正常来说,一个前端单页应用(SPA)的项目结构大概如下:


    ├─favicon.ico
    ├─index.html

    ├─css
    │ └───app.fb0c6e1c.css

    ├─img
    │ └───logo.82b9c7a5.png

    └─js
    ├───app.febf7357.js
    └───chunk-vendors.5a5a5781.js

    从命名上可以发现,文件大概分两类:



    1. index.html & favicon.ico 都属于固定命名,通常情况下名称不会再发生改变;

    2. css/js/image/ttf 等文件,则通常会以 {name}.{hash}.{suffix}的方式进行命名;


    name-with-hash.png


    当文件发生变化时,其命名规则,可天然保证文件hash跟着发生变化,从而保证文件的路径发生变化;


    因此,针对以上场景,通常情况下可以按以下方式制定缓存策略



    1. index.html 和 favicon.ico 设置为“不缓存”或者“协商缓存”(必要不大);

    2. 名称中带hash的文件(如css/js/image/ttf),可以直接使用“强缓存”策略

    作者:春哥的梦想是摸鱼
    链接:https://juejin.cn/post/7030781324650610695

    收起阅读 »

    2021 年你需要知道的 CSS 工程化技术

    目前整个 CSS 工具链、工程化领域的主要方案如下: 而我们技术选型的标准如下: 开发速度快 开发体验友好 调试体验友好 可维护性友好 扩展性友好 可协作性友好 体积小 有最佳实践指导 目前主要需要对比的三套方案: Less/Sass + PostCS...
    继续阅读 »

    目前整个 CSS 工具链、工程化领域的主要方案如下:


    image.png


    而我们技术选型的标准如下:



    • 开发速度快

    • 开发体验友好

    • 调试体验友好

    • 可维护性友好

    • 扩展性友好

    • 可协作性友好

    • 体积小

    • 有最佳实践指导


    目前主要需要对比的三套方案:



    • Less/Sass + PostCSS 的纯 CSS c侧方案

    • styled-components / emotion 的纯 CSS-in-JS 侧方案

    • TailwindCSS 的以写辅助类为主的 HTML 侧方案


    纯 CSS 侧方案


    介绍与优点




    维护状态:一般




    Star 数:16.7K




    支持框架:无框架限制




    项目地址:github.com/less/less.j…



    Less/Sass + PostCSS 这种方案在目前主流的组件库和企业级项目中使用很广,如 ant-design 等


    它们的主要作用如下:



    • 为 CSS 添加了类似 JS 的特性,你也可以使用变量、mixin,写判断等

    • 引入了模块化的概念,可以在一个 less 文件中导入另外一个 less 文件进行使用

    • 兼容标准,可以快速使用 CSS 新特性,兼容浏览器 CSS 差异等


    这类工具能够与主流的工程化工具一起使用,如 Webpack,提供对应的 loader 如 sass-loader,然后就可以在 React/Vue 项目中建 .scss 文件,写 sass 语法,并导入到 React 组件中生效。


    比如我写一个组件在响应式各个断点下的展示情况的 sass 代码:


    .component {

    width: 300px;

    @media (min-width: 768px) {

    width: 600px;

    @media (min-resolution: 192dpi) {

    background-image: url(/img/retina2x.png);

    }

    }

    @media (min-width: 1280px) {

    width: 800px;

    }

    }

    或导入一些用于标准化浏览器差异的代码:


    @import "normalize.css"; 



    // component 相关的其他代码

    不足


    这类方案的一个主要问题就是,只是对 CSS 本身进行了增强,但是在帮助开发者如何写更好的 CSS、更高效、可维护的 CSS 方面并没有提供任何建议。



    • 你依然需要自己定义 CSS 类、id,并且思考如何去用这些类、id 进行组合去描述 HTML 的样式

    • 你依然可能会写很多冗余的 Less/Sass 代码,然后造成项目的负担,在可维护性方面也有巨大问题


    优化



    • 可以引入 CSS 设计规范:BEM 规范,来辅助用户在整个网页的 HTML 骨架以及对应的类上进行设计

    • 可以引入 CSS Modules,将 CSS 文件进行 “作用域” 限制,确保在之后维护时,修改一个内容不会引起全局中其他样式的效果


    BEM 规范


    B (Block)、E(Element)、M(Modifier),具体就是通过块、元素、行为来定义所有的可视化功能。


    拿设计一个 Button 为例:


    /* Block */

    .btn {}



    /* 依赖于 Block 的 Element */

    .btn__price {}



    /* 修改 Block 风格的 Modifier */

    .btn--orange {}

    .btn--big {}

    遵循上述规范的一个真实的 Button:


    <a href="#">

    <span>$3</span>

    <span>BIG BUTTON</span>

    </a>

    可以获得如下的效果:



    CSS Modules


    CSS Modules 主要为 CSS 添加局部作用域和模块依赖,使得 CSS 也能具有组件化。


    一个例子如下:


    import React from 'react';

    import style from './App.css';



    export default () => {

    return (

    <h1 className={style.title}>

    Hello World

    </h1>

    );

    };

    .title {

    composes: className;

    color: red;

    }

    上述经过编译会变成如下 hash 字符串:


    <h1>

    Hello World

    </h1>

    ._3zyde4l1yATCOkgn-DBWEL {

    color: red;

    }

    CSS Modules 可以与普通 CSS、Less、Sass 等结合使用。


    纯 JS 侧方案


    介绍与优点




    维护状态:一般




    Star 数:35.2K




    支持框架:React ,通过社区支持 Vue 等框架




    项目地址:github.com/styled-comp…



    使用 JS 的模板字符串函数,在 JS 里面写 CSS 代码,这带来了两个认知的改变:



    • 不是在根据 HTML,然后去写 CSS,而是站在组件设计的角度,为组件写 CSS,然后应用组件的组合思想搭建大应用

    • 自动提供类似 CSS Modules 的体验,不用担心样式的全局污染问题


    同时带来了很多 JS 侧才有的各种功能特性,可以让开发者用开发 JS 的方式开发 CSS,如编辑器自动补全、Lint、编译压缩等。


    比如我写一个按钮:


    const Button = styled.button`

    /* Adapt the colors based on primary prop */

    background: ${props => props.primary ? "palevioletred" : "white"};

    color: ${props => props.primary ? "white" : "palevioletred"};



    font-size: 1em;

    margin: 1em;

    padding: 0.25em 1em;

    border: 2px solid palevioletred;

    border-radius: 3px;

    `;



    render(

    <div>

    <Button>Normal</Button>

    <Button primary>Primary</Button>

    </div>

    );

    可以获得如下效果:



    还可以扩展样式:


    // The Button from the last section without the interpolations

    const Button = styled.button`

    color: palevioletred;

    font-size: 1em;

    margin: 1em;

    padding: 0.25em 1em;

    border: 2px solid palevioletred;

    border-radius: 3px;

    `;



    // A new component based on Button, but with some override styles

    const TomatoButton = styled(Button)`

    color: tomato;

    border-color: tomato;

    `;



    render(

    <div>

    <Button>Normal Button</Button>

    <TomatoButton>Tomato Button</TomatoButton>

    </div>

    );

    可以获得如下效果:



    不足


    虽然这类方案提供了在 JS 中写 CSS,充分利用 JS 的插值、组合等特性,然后应用 React 组件等组合思想,将组件与 CSS 进行细粒度绑定,让 CSS 跟随着组件一同进行组件化开发,同时提供和组件类似的模块化特性,相比 Less/Sass 这一套,可以复用 JS 社区的最佳实践等。


    但是它仍然有一些不足:



    • 仍然是是对 CSS 增强,提供非常大的灵活性,开发者仍然需要考虑如何去组织自己的 CSS

    • 没有给出一套 “有观点” 的最佳实践做法

    • 在上层也缺乏基于 styled-components 进行复用的物料库可进行参考设计和使用,导致在初始化使用时开发速度较低

    • 在 JS 中写 CSS,势必带来一些本属于 JS 的限制,如 TS 下,需要对 Styled 的组件进行类型注释

    • 官方维护的内容只兼容 React 框架,Vue 和其他框架都由社区提供支持


    整体来说不太符合团队协作使用,需要人为总结最佳实践和规范等。


    优化



    • 寻求一套写 CSS 的最佳实践和团队协作规范

    • 能够拥有大量的物料库或辅助类等,提高开发效率,快速完成应用开发


    偏向 HTML 侧方案


    介绍与优点




    维护状态:积极




    Star 数:48.9K




    支持框架:React、Vue、Svelte 等主流框架




    项目地址:github.com/tailwindlab…



    典型的是 TailwindCSS,一个辅助类优先的 CSS 框架,提供如 flexpt-4text-centerrotate-90 这样实用的类名,然后基于这些底层的辅助类向上组合构建任何网站,而且只需要专注于为 HTML 设置类名即可。


    一个比较形象的例子可以参考如下代码:


    <button>Decline</button>

    <button>Accept</button>

    上述代码应用 BEM 风格的类名设计,然后设计两个按钮,而这两个类名类似主流组件库里面的 Button 的不同状态的设计,而这两个类又是由更加基础的 TailwindCSS 辅助类组成:


    .btn {

    @apply text-base font-medium rounded-lg p-3;

    }



    .btn--primary {

    @apply bg-rose-500 text-white;

    }



    .btn--secondary {

    @apply bg-gray-100 text-black;

    }

    上面的辅助类包含以下几类:



    • 设置文本相关: text-basefont-mediumtext-whitetext-black

    • 设置背景相关的:bg-rose-500bg-gray-100

    • 设置间距相关的:p-3

    • 设置边角相关的:rounded-lg


    通过 Tailwind 提供的 @apply 方法来对这些辅助类进行组合构建更上层的样式类。


    上述的最终效果展示如下:



    可以看到 TailwindCSS 将我们开发网站的过程抽象成为使用 Figma 等设计软件设计界面的过程,同时提供了一套用于设计的规范,相当于内置最佳实践,如颜色、阴影、字体相关的内容,一个很形象的图片可以说明这一点:



    TailwindCSS 为我们规划了一个元素可以设置的属性,并且为每个属性给定了一组可以设置的值,这些属性+属性值组合成一个有机的设计系统,非常便于团队协作与共识,让我们开发网站就像做设计一样简单、快速,但是整体风格又能保持一致。


    TailwindCSS 同时也能与主流组件库如 React、Vue、Svelte 结合,融入基于组件的 CSS 设计思想,但又只需要修改 HTML 上的类名,如我们设计一个食谱组件:


    // Recipes.js

    import Nav from './Nav.js'

    import NavItem from './NavItem.js'

    import List from './List.js'

    import ListItem from './ListItem.js'



    export default function Recipes({ recipes }) {

    return (

    <div className="divide-y divide-gray-100">

    <Nav>

    <NavItem href="/featured" isActive>Featured</NavItem>

    <NavItem href="/popular">Popular</NavItem>

    <NavItem href="/recent">Recent</NavItem>

    </Nav>

    <List>

    {recipes.map((recipe) => (

    <ListItem key={recipe.id} recipe={recipe} />

    ))}

    </List>

    </div>

    )

    }



    // Nav.js

    export default function Nav({ children }) {

    return (

    <nav className="p-4">

    <ul className="flex space-x-2">

    {children}

    </ul>

    </nav>

    )

    }



    // NavItem.js

    export default function NavItem({ href, isActive, children }) {

    return (

    <li>

    <a

    href={href}

    className={`block px-4 py-2 rounded-md ${isActive ? 'bg-amber-100 text-amber-700' : ''}`}

    >

    {children}

    </a>

    </li>

    )

    }



    // List.js

    export default function List({ children }) {

    return (

    <ul className="divide-y divide-gray-100">

    {children}

    </ul>

    )

    }



    //ListItem.js

    export default function ListItem({ recipe }) {

    return (

    <article className="p-4 flex space-x-4">

    <img src={recipe.image} alt="" className="flex-none w-18 h-18 rounded-lg object-cover bg-gray-100" width="144" height="144" />

    <div className="min-w-0 relative flex-auto sm:pr-20 lg:pr-0 xl:pr-20">

    <h2 className="text-lg font-semibold text-black mb-0.5">

    {recipe.title}

    </h2>

    <dl className="flex flex-wrap text-sm font-medium whitespace-pre">

    <div>

    <dt className="sr-only">Time</dt>

    <dd>

    <abbr title={`${recipe.time} minutes`}>{recipe.time}m</abbr>

    </dd>

    </div>

    <div>

    <dt className="sr-only">Difficulty</dt>

    <dd> · {recipe.difficulty}</dd>

    </div>

    <div>

    <dt className="sr-only">Servings</dt>

    <dd> · {recipe.servings} servings</dd>

    </div>

    <div className="flex-none w-full mt-0.5 font-normal">

    <dt className="inline">By</dt>{' '}

    <dd className="inline text-black">{recipe.author}</dd>

    </div>

    <div>

    <dt className="text-amber-500">

    <span className="sr-only">Rating</span>

    <svg width="16" height="20" fill="currentColor">

    <path d="M7.05 3.691c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.372 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.539 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.783.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.363-1.118L.98 9.483c-.784-.57-.381-1.81.587-1.81H5.03a1 1 0 00.95-.69L7.05 3.69z" />

    </svg>

    </dt>

    <dd>{recipe.rating}</dd>

    </div>

    </dl>

    </div>

    </article>

    )

    }

    上述食谱的效果如下:



    可以看到我们无需写一行 CSS,而是在 HTML 里面应用各种辅助类,结合 React 的组件化设计,既可以轻松完成一个非常现代化且好看的食谱组件。


    除了上面的特性,TailwindCSS 在响应式、新特性支持、Dark Mode、自定义配置、自定义新的辅助类、IDE 方面也提供非常优秀的支持,除此之外还有基于 TailwindCSS 构建的物料库 Tailwind UI ,提供各种各样成熟、好看、可用于生产的物料库:



    因为需要自定的 CSS 不多,而需要自定义的 CSS 可以定义为可复用的辅助类,所以在可维护性方面也是极好的。


    不足



    • 因为要引入一个额外的运行时,TailwindCSS 辅助类到 CSS 的编译过程,而随着组件越来越多,需要编译的工作量也会变大,所以速度会有影响

    • 过于底层,相当于给了用于设计的最基础的指标,但是如果我们想要快速设计网站,那么可能还需要一致的、更加上层的组件库

    • 相当于引入了一套框架,具有一定的学习成本和使用成本


    优化



    • Tailwind 2.0 支持 JIT,可以大大提升编译速度,可以考虑引入

    • 基于 TailwindCSS,设计一套符合自身风格的上层组件库、物料库,便于更加快速开发

    • 提前探索、学习和总结一套教程与开发最佳实践

    • 探索 styled-components 等结合 TailwindCSS 的开发方式



    作者:程序员巴士
    链接:https://juejin.cn/post/7030790310590447630

    收起阅读 »

    如何在TS里使用命名空间,来组织你的代码

    前言 关于命名空间,官方有个说明,大概是这么个意思: 为了与ECMAScript 2015里的术语保持一致,从TypeScript 1.5开始,“外部模块”称为“模块”,而“内部模块”称做“命名空间”。 为了避免新的使用者被相似的名称所迷惑,建议: 任何使用...
    继续阅读 »

    前言


    关于命名空间,官方有个说明,大概是这么个意思:


    为了与ECMAScript 2015里的术语保持一致,从TypeScript 1.5开始,“外部模块”称为“模块”,而“内部模块”称做“命名空间”。


    为了避免新的使用者被相似的名称所迷惑,建议:



    任何使用 module关键字来声明一个内部模块的地方都应该使用namespace关键字来替换



    具体的使用下面会讲到


    使用命名空间


    使用命名空间的方式,其实非常简单,格式如下:


    namespace X {}

    具体的使用可以看看下面这个例子(例子来源TS官方文档)


    我们定义几个简单的字符串验证器,假设会使用它们来验证表单里的用户输入或验证外部数据


    interface StringValidator {
    isAcceptable(s: string): boolean;
    }

    let lettersRegexp = /^[A-Za-z]+$/;
    let numberRegexp = /^[0-9]+$/;

    class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
    return lettersRegexp.test(s);
    }
    }

    class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
    return s.length === 5 && numberRegexp.test(s);
    }
    }

    // Some samples to try
    let strings = ["Hello", "98052", "101"];

    // Validators to use
    let validators: { [s: string]: StringValidator; } = {};
    validators["ZIP code"] = new ZipCodeValidator();
    validators["Letters only"] = new LettersOnlyValidator();

    // Show whether each string passed each validator
    for (let s of strings) {
    for (let name in validators) {
    let isMatch = validators[name].isAcceptable(s);
    console.log(`'${ s }' ${ isMatch ? "matches" : "does not match" } '${ name }'.`);
    }
    }

    现在我们是把所有的验证器都放在一个文件里


    但是,随着更多验证器的加入,我们可能会担心与其它对象产生命名冲突。因此我们使用命名空间来组织我们的代码


    如下使用命名空间:


    namespace Validation {
    export interface StringValidator {
    isAcceptable(s: string): boolean;
    }

    const lettersRegexp = /^[A-Za-z]+$/;
    const numberRegexp = /^[0-9]+$/;

    export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
    return lettersRegexp.test(s);
    }
    }

    export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
    return s.length === 5 && numberRegexp.test(s);
    }
    }
    }

    // Some samples to try
    let strings = ["Hello", "98052", "101"];

    // Validators to use
    let validators: { [s: string]: Validation.StringValidator; } = {};
    validators["ZIP code"] = new Validation.ZipCodeValidator();
    validators["Letters only"] = new Validation.LettersOnlyValidator();

    // Show whether each string passed each validator
    for (let s of strings) {
    for (let name in validators) {
    console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
    }

    如上代码,把所有与验证器相关的类型都放到一个叫做Validation的命名空间里。 因为我们想让这些接口和类在命名空间之外也是可访问的,所以需要使用 export。 相反的,变量 lettersRegexpnumberRegexp是实现的细节,不需要导出,因此它们在命名空间外是不能访问的


    有个问题是,如果只是一个文件,当应用变得越来越大的时候,会变得难以维护,因此我们根据需要,可选的将单文件分离到不同的文件中


    下节我们会继续讲到这个问题,关于多文件的命名空间,并且我们会将上例中的单文件分割成多个文件。欢迎关注


    END


    以上就是本文的所有内容,如有问题,欢迎指正~


    作者:LBJ
    链接:https://juejin.cn/post/7031021973966684191

    收起阅读 »

    【灵魂拷问】当面试官问你JavaScript预编译

    (一) 前言 在腾讯字节等其他大厂的面试中,JavaScript预编译是经常会被问到的问题,本文将带大家了解JS预编译中的具体过程 (二)编译执行步骤 传统编译语言编译步骤 对传统编译型语言来说,其编译步骤一般为:词法分析->语法分析->代码生成,...
    继续阅读 »

    (一) 前言


    在腾讯字节等其他大厂的面试中,JavaScript预编译是经常会被问到的问题,本文将带大家了解JS预编译中的具体过程


    (二)编译执行步骤


    传统编译语言编译步骤


    对传统编译型语言来说,其编译步骤一般为:词法分析->语法分析->代码生成,下面让我们来分别介绍这3个过程。



    1. 词法分析


    这个过程会将代码分隔成一个个语法单元,比如var a = 520;这段代码通常会被分解为vara=520这4个词法单元。



    1. 语法分析


    这个过程是将词法单元整合成一个多维数组,即抽象语法树(AST),以下面代码为例


    if(typeof a == "undefined" ){ 
    a = 0;
    } else {
    a = a;
    }
    alert(a);

    语法树.jpg


    当JavaScript解释器在构造语法树的时候,如果发现无法构造,就会报语法错误(syntaxError),并结束整个代码块的解析。



    1. 代码生成


    这个过程是将抽象语法树AST转变为可执行的机器代码,让计算机能读懂执行。


    JavaScript编译步骤


    比起传统的只有3个步骤的语言的编译器,JavaScript引擎要复杂的多,但总体来看,JavaScript编译过程只有下面三个步骤:
    1. 语法分析
    2. 预编译
    3. 解释执行


    (三)预编译详解


    预编译概述


    JavaScript预编译发生在代码片段执行前的几微秒(甚至更短!),预编译分为两种,一种是函数预编译,另一种是全局预编译,全局预编译发生在页面加载完成时执行,函数预编译发生在函数执行的前一刻。预编译会创建当前环境的执行上下文。


    函数的预编译执行四部曲



    1. 创建Activation Object(以下简写为AO对象);

    2. 找形参和变量声明,将变量声明和形参作为AO的属性名,值为underfined;

    3. 将实参和形参值统一;

    4. 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体。


    案例代码


    //请问下面的console.log()输出什么?
    function fn(a) {
    //console.log(a);
    var a = 123//变量赋值
    //console.log(a);
    function a() { }//函数声明
    //console.log(a);
    var b = function () { }//变量赋值(函数表达式)
    //console.log(b);
    function d() { }//函数声明
    }
    fn(1)//函数调用

    根据上面的四部曲,对代码注解后,我们可以很轻松的知道四个console.log()输出什么,让我们来看下AO的变化



    1. 创建AO对象


    AO{
    //空对象
    }


    1. 找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefined;


    AO{
    a: undefined
    b: undefined
    }


    1. 将实参和形参值统一;


    AO{
    a: 1,
    b: undefined
    }


    1. 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体。


    AO{
    a: function(){}
    b: undefined
    d: function(){}
    }

    最后,下面是完整的预编译过程


    AO:{
    a:undefined -> 1 -> function a(){}
    b:undefined
    d:function d(){}
    }


    全局的预编译执行三部曲



    1. 创建Global Object(以下简写为GO对象);

    2. 找形参和变量声明,将变量声明和形参作为GO的属性名,值为undefined;

    3. 在全局里找函数声明,将函数名作为GO对象的属性名,值赋予函数体。


    案例代码


    global = 100;
    function fn() {
    //console.log(global);
    global = 200;
    //console.log(global);
    var global = 300;
    }
    fn();

    根据全局预编译三部曲我们可以知道他的GO变化过程



    1. 创建GO对象


    GO{
    // 空对象
    }


    1. 找形参和变量声明,将变量声明和形参作为GO的属性名,值为underfined


    GO: {
    global: undefined
    }


    1. 在全局里找函数声明,将函数名作为GO对象的属性名,值赋予函数体


    GO: {
    global: undefined
    fn: function() { }
    }


    注意这里函数声明会带来函数自己的AO,预编译过程继续套用四部曲即可



    (四)总结


    当遇到面试官问你预编译过程时,可以根据上面的内容轻松解答,同时面试时也会遇到很多问你console.log()输出值的问题,也可以用上面的公式获取正确答案。


    作者:橙玉米
    链接:https://juejin.cn/post/7030370931478364196

    收起阅读 »

    面试题:实现小程序平台的并发双工 rpc 通信

    前几天面试的时候遇到一道面试题,还是挺考验能力的。 题目是这样的: rpc 是 remote procedure call,远程过程调用,比如一个进程调用另一个进程的某个方法。很多平台提供的进程间通信机制都封装成了 rpc 的形式,比如 electron 的 ...
    继续阅读 »

    前几天面试的时候遇到一道面试题,还是挺考验能力的。


    题目是这样的:


    rpc 是 remote procedure call,远程过程调用,比如一个进程调用另一个进程的某个方法。很多平台提供的进程间通信机制都封装成了 rpc 的形式,比如 electron 的 remote 模块。


    小程序是双线程机制,两个线程之间要通信,提供了 postMessage 和 addListener 的 api。现在要在两个线程都会引入的 common.js 文件里实现 rpc 方法,支持并发的 rpc 通信。


    达到这样的使用效果:


    const res = await rpc('method', params);

    这道题是有真实应用场景的题目,比一些逻辑题和算法题更有意思一些。


    实现思路


    两个线程之间是用 postMessage 的 api 来传递消息的:



    • 在 rpc 方法里用 postMessage 来传递要调用的方法名和参数

    • 在 addListener 里收到调用的时候,调用 api,然后通过 postMessage 返回结果或者错误


    我们先实现 rpc 方法,通过 postMessage 传递消息,返回一个 promise:


    function rpc(method, params) {
    postMessage(JSON.stringify({
    method,
    params
    }));

    return new Promise((resolve, reject) => {

    });
    }

    这个 promise 什么时候 resolve 或者 reject 呢? 是在 addListener 收到消息后。那就要先把它存起来,等收到消息再调用 resolve 或 reject。


    为了支持并发和区分多个调用通道,我们加一个 id。


    let id = 0;
    function genId() {
    return ++id;
    }

    const channelMap = new Map();

    function rpc(method, params) {
    const curId = genId();

    postMessage(JSON.stringify({
    id: curId,
    method,
    params
    }));

    return new Promise((resolve, reject) => {
    channelMap.set(curId, {
    resolve,
    reject
    });
    });
    }

    这样,就通过 id 来标识了每一个远程调用请求和与它关联的 resolve、reject。


    然后要处理 addListener,因为是双工的通信,也就是通信的两者都会用到这段代码,所以要区分一下是请求还是响应。


    addListener((message) => {
    const { curId, method, params, res}= JSON.parse(message);
    if (res) {
    // 处理响应
    } else {
    // 处理请求
    }
    });

    处理请求就是调用方法,然后返回结果或者错误:


    try {
    const data = global[method](...params);
    postMessage({
    id
    res: {
    data
    }
    });
    } catch(e) {
    postMessage({
    id,
    res: {
    error: e.message
    }
    });
    }

    处理响应就是拿到并调用和 id 关联的 resolve 和 reject:


    const { resolve, reject  } = channelMap.get(id);
    if(res.data) {
    resolve(res.data);
    } else {
    reject(res.error);
    }

    全部代码是这样的:


    let id = 0;
    function genId() {
    return ++id;
    }

    const channelMap = new Map();

    function rpc(method, params) {
    const curId = genId();

    postMessage(JSON.stringify({
    id: curId,
    method,
    params
    }));

    return new Promise((resolve, reject) => {
    channelMap.set(curId, {
    resolve,
    reject
    });
    });
    }

    addListener((message) => {
    const { id, method, params, res}= JSON.parse(message);
    if (res) {
    const { resolve, reject } = channelMap.get(id);
    if(res.data) {
    resolve(res.data);
    } else {
    reject(res.error);
    }
    } else {
    try {
    const data = global[method](...params);
    postMessage({
    id
    res: {
    data
    }
    });
    } catch(e) {
    postMessage({
    id,
    res: {
    error: e.message
    }
    });
    }
    }
    });

    我们实现了最开始的需求:



    • 实现了 rpc 方法,返回一个 promise

    • 支持并发的调用

    • 两个线程都引入这个文件,支持双工的通信


    其实主要注意的有两个点:



    • 要添加一个 id 来关联请求和响应,这在 socket 通信的时候也经常用

    • resolve 和 reject 可以保存下来,后续再调用。这在请求取消,比如 axios 的 cancelToken 的实现上也有应用


    这两个点的应用场景还是比较多的。


    总结


    rpc 是远程过程调用,是跨进程、跨线程等场景下通信的常见封装形式。面试题是小程序平台的双线程的场景,在一个公共文件里实现双工的并发的 rpc 通信。


    思路文中已经讲清楚了,主要要注意的是 promise 的 resolve 和 reject 可以保存下来后续调用,通过添加 id 来标识和关联一组请求响应。


    作者:zxg_神说要有光
    链接:https://juejin.cn/post/7030803556282155022

    收起阅读 »

    localStorage灵魂五问。 5M?? 10M !!!

    灵魂五问 localStorage 存储的键值采用什么字符编码 5M 的单位是什么 localStorage 键占不占存储空间 localStorage的键的数量,对写和读性能的影响 写个方法统计一个localStorage已使用空间 我们挨个解答,之后给...
    继续阅读 »

    灵魂五问



    1. localStorage 存储的键值采用什么字符编码

    2. 5M 的单位是什么

    3. localStorage 键占不占存储空间

    4. localStorage的键的数量,对写和读性能的影响

    5. 写个方法统计一个localStorage已使用空间


    我们挨个解答,之后给各位面试官又多了一个面试题。


    我们常说localStorage存储空间是5M,请问这个5M的单位是什么?


    localStorage 存储的键值采用什么字符编码?


    打开相对权威的MDN localStorage#description



    The keys and the values stored with localStorage are always in the UTF-16 DOMString format, which uses two bytes per character. As with objects, integer keys are automatically converted to strings.



    翻译成中文:



    localStorage 存储的键和值始终采用 UTF-16 DOMString 格式,每个字符使用两个字节。与对象一样,整数键将自动转换为字符串。



    答案: UTF-16


    MDN这里描述的没有问题,也有问题,因为UTF-16,每个字符使用两个字节,是有前提条件的,就是码点小于0xFFFF(65535), 大于这个码点的是四个字节。


    这是全文的关键。


    5M 的单位是什么


    5M的单位是什么?


    选项:



    1. 字符的个数

    2. 字节数

    3. 字符的长度值

    4. bit 数

    5. utf-16编码单元


    以前不知道,现代浏览器,准确的应该是 选项3,字符的长度 ,亦或 选项5, utf-16编码单元


    字符的个数,并不等于字符的长度,这一点要知道:


    "a".length // 1
    "人".length // 1
    "𠮷".length // 2
    "🔴".length // 2

    现代浏览器对字符串的处理是基于UTF-16 DOMString


    但是说5M字符串的长度,显然有那么点怪异。


    而根据 UTF-16编码规则,要么2个字节,要么四个字节,所以不如说是 10M 的字节数,更为合理。


    当然,2个字节作为一个utf-16的字符编码单元,也可以说是 5M 的utf-16的编码单元。


    我们先编写一个utf-16字符串计算字节数的方法:非常简单,判断码点决定是2还是4


    function sizeofUtf16Bytes(str) {
    var total = 0,
    charCode,
    i,
    len;
    for (i = 0, len = str.length; i < len; i++) {
    charCode = str.charCodeAt(i);
    if (charCode <= 0xffff) {
    total += 2;
    } else {
    total += 4;
    }
    }
    return total;
    }

    我们再根绝10M的字节数来存储


    我们留下8个字节数作为key,8个字节可是普通的4个字符换,也可是码点大于65535的3个字符,也可是是组合。


    下面的三个组合,都是可以的,



    1. aaaa

    2. aa🔴

    3. 🔴🔴


    在此基础上增加任意一个字符,都会报错异常异常。


    const charTxt = "人";
    let count = (10 * 1024 * 1024 / 2) - 8 / 2;
    let content = new Array(count).fill(charTxt).join("");
    const key = "aa🔴";
    localStorage.clear();
    try {
    localStorage.setItem(key, content);
    } catch (err) {
    console.log("err", err);
    }

    const sizeKey = sizeofUtf16Bytes(key);
    const contentSize = sizeofUtf16Bytes(content);
    console.log("key size:", sizeKey, content.length);
    console.log("content size:", contentSize, content.length);
    console.log("total size:", sizeKey + contentSize, content.length + key.length);

    现代浏览器的情况下:


    所以,说是10M的字节数,更为准确,也更容易让人理解。


    如果说5M,那其单位就是字符串的长度,而不是字符数。


    答案: 字符串的长度值, 或者utf-16的编码单元


    更合理的答案是 10M字节空间。


    localStorage 键占不占存储空间


    我们把 key和val各自设置长 2.5M的长度


    const charTxt = "a";
    let count = (2.5 * 1024 * 1024);
    let content = new Array(count).fill(charTxt).join("");
    const key = new Array(count).fill(charTxt).join("");
    localStorage.clear();
    try {
    console.time("setItem")
    localStorage.setItem(key, content);
    console.timeEnd("setItem")
    } catch (err) {
    console.log("err code:", err.code);
    console.log("err message:", err.message)
    }

    执行正常。


    我们把content的长度加1, 变为 2.5 M + 1, key的长度依旧是 2.5M的长度


    const charTxt = "a";
    let count = (2.5 * 1024 * 1024);
    let content = new Array(count).fill(charTxt).join("") + 1;
    const key = new Array(count).fill(charTxt).join("");
    localStorage.clear();
    try {
    console.time("setItem")
    localStorage.setItem(key, content);
    console.timeEnd("setItem")
    } catch (err) {
    console.log("err code:", err.code);
    console.log("err message:", err.message)
    }

    image.png


    产生异常,存储失败。 至于更多异常详情吗,参见 localstorage_功能检测


    function storageAvailable(type) {
    var storage;
    try {
    storage = window[type];
    var x = '__storage_test__';
    storage.setItem(x, x);
    storage.removeItem(x);
    return true;
    }
    catch(e) {
    return e instanceof DOMException && (
    // everything except Firefox
    e.code === 22 ||
    // Firefox
    e.code === 1014 ||
    // test name field too, because code might not be present
    // everything except Firefox
    e.name === 'QuotaExceededError' ||
    // Firefox
    e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
    // acknowledge QuotaExceededError only if there's something already stored
    (storage && storage.length !== 0);
    }
    }

    答案: 占空间


    键的数量,对读写的影响


    我们500 * 1000键,如下


    let keyCount = 500 * 1000;

    localStorage.clear();
    for (let i = 0; i < keyCount; i++) {
    localStorage.setItem(i, "");
    }

    setTimeout(() => {
    console.time("save_cost");
    localStorage.setItem("a", "1");
    console.timeEnd("save_cost");
    }, 2000)


    setTimeout(() => {
    console.time("read_cost");
    localStorage.getItem("a");
    console.timeEnd("read_cost");

    }, 2000)

    // save_cost: 0.05615234375 ms
    // read_cost: 0.008056640625 ms

    你单独执行保存代码:


    localStorage.clear();    
    console.time("save_cost");
    localStorage.setItem("a", "1");
    console.timeEnd("save_cost");
    // save_cost: 0.033203125 ms

    可以多次测试, 影响肯定是有的,也仅仅是数倍,不是特别的大。


    反过来,如果是保存的值表较大呢?


    const charTxt = "a";
    const count = 5 * 1024 * 1024 - 1
    const val1 = new Array(count).fill(charTxt).join("");

    setTimeout(() =>{
    localStorage.clear();
    console.time("save_cost_1");
    localStorage.setItem("a", val1);
    console.timeEnd("save_cost_1");
    },1000)


    setTimeout(() =>{
    localStorage.clear();
    console.time("save_cost_2");
    localStorage.setItem("a", "a");
    console.timeEnd("save_cost_2");
    },1000)

    // save_cost_1: 12.276123046875 ms
    // save_cost_2: 0.010009765625 ms

    可以多测试很多次,单次值的大小对存的性能影响非常大,读取也一样,合情合理之中。


    所以尽量不要保存大的值,因为其是同步读取,纯大数据,用indexedDB就好。


    答案:键的数量对读取性能有影响,但是不大。值的大小对性能影响更大,不建议保存大的数据。


    写个方法统计一个localStorage已使用空间


    现代浏览器的精写版本:


    function sieOfLS() {
    return Object.entries(localStorage).map(v => v.join('')).join('').length;
    }

    测试代码:


    localStorage.clear();
    localStorage.setItem("🔴", 1);
    localStorage.setItem("🔴🔴🔴🔴🔴🔴🔴🔴", 1111);
    console.log("size:", sieOfLS()) // 23
    // 🔴*9 + 1 *5 = 2*9 + 1*5 = 23

    html的协议标准


    WHATWG 超文本应用程序技术工作组 的localstorage 协议定了localStorage的方法,属性等等,并没有明确规定其存储空间。也就导致各个浏览器的最大限制不一样。


    其并不是ES的标准。


    页面的utf-8编码


    我们的html页面,经常会出现 <meta charset="UTF-8">
    告知浏览器此页面属于什么字符编码格式,下一步浏览器做好解码工作。


    <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>容器</title>
    </head>

    这和localStorage的存储没有半毛钱的关系。


    localStorage扩容


    localStorage的空间是 10M的字节数,一般情况是够用,可是人总是有贪欲。
    真达到了空间限制,怎么弄?


    localStorage扩容就是一个话题。


    作者:云的世界
    链接:https://juejin.cn/post/7030585901524713508

    收起阅读 »

    Vue新玩具VueUse

    vue
    什么是 VueUse VueUse 是一个基于 Composition API 的实用函数集合。通俗的来说,这就是一个工具函数包,它可以帮助你快速实现一些常见的功能,免得你自己去写,解决重复的工作内容。以及进行了基于 Composition API 的封装。让...
    继续阅读 »

    什么是 VueUse


    VueUse 是一个基于 Composition API 的实用函数集合。通俗的来说,这就是一个工具函数包,它可以帮助你快速实现一些常见的功能,免得你自己去写,解决重复的工作内容。以及进行了基于 Composition API 的封装。让你在 vue3 中更加得心应手。


    简单上手


    安装 VueUse


    npm i @vueuse/core

    使用 VueUse


    // 导入
    import { useMouse, usePreferredDark, useLocalStorage } from '@vueuse/core'

    export default {
    setup() {
    // tracks mouse position
    const { x, y } = useMouse()

    // is user prefers dark theme
    const isDark = usePreferredDark()

    // persist state in localStorage
    const store = useLocalStorage(
    'my-storage',
    {
    name: 'Apple',
    color: 'red',
    },
    )

    return { x, y, isDark, store }
    }
    }

    上面从 VueUse 当中导入了三个函数, useMouseusePreferredDarkuseLocalStorageuseMouse 是一个监听当前鼠标坐标的一个方法,他会实时的获取鼠标的当前的位置。usePreferredDark 是一个判断用户是否喜欢深色的方法,他会实时的判断用户是否喜欢深色的主题。useLocalStorage 是一个用来持久化数据的方法,他会把数据持久化到本地存储中。


    还有我们熟悉的 防抖节流


    import { throttleFilter, debounceFilter, useLocalStorage, useMouse } from '@vueuse/core'

    // 以节流的方式去改变 localStorage 的值
    const storage = useLocalStorage('my-key', { foo: 'bar' }, { eventFilter: throttleFilter(1000) })

    // 100ms后更新鼠标的位置
    const { x, y } = useMouse({ eventFilter: debounceFilter(100) })

    还有还有在 component 中使用的函数


    <script setup>
    import { ref } from 'vue'
    import { onClickOutside } from '@vueuse/core'

    const el = ref()

    function close () {
    /* ... */
    }

    onClickOutside(el, close)
    </script>

    <template>
    <div ref="el">
    Click Outside of Me
    </div>
    </template>

    上面例子中,使用了 onClickOutside 函数,这个函数会在点击元素外部时触发一个回调函数。也就是这里的 close 函数。在 component 中就是这么使用


    <script setup>
    import { OnClickOutside } from '@vueuse/components'

    function close () {
    /* ... */
    }
    </script>

    <template>
    <OnClickOutside @trigger="close">
    <div>
    Click Outside of Me
    </div>
    </OnClickOutside>
    </template>


    注意⚠️ 这里的 OnClickOutside 函数是一个组件,不是一个函数。需要package.json 中安装了 @vueuse/components



    还还有全局状态共享的函数


    // store.js
    import { createGlobalState, useStorage } from '@vueuse/core'

    export const useGlobalState = createGlobalState(
    () => useStorage('vue-use-local-storage'),
    )

    // component.js
    import { useGlobalState } from './store'

    export default defineComponent({
    setup() {
    const state = useGlobalState()
    return { state }
    },
    })

    这样子就是一个简单的状态共享了。扩展一下。传一个参数,就能改变 store 的值了。


    还有关于 fetch, 下面👇就是一个简单的请求了。


    import { useFetch } from '@vueuse/core'

    const { isFetching, error, data } = useFetch(url)

    它还有很多的 option 参数,可以自定义。


    // 100ms超时
    const { data } = useFetch(url, { timeout: 100 })

    // 请求拦截
    const { data } = useFetch(url, {
    async beforeFetch({ url, options, cancel }) {
    const myToken = await getMyToken()

    if (!myToken)
    cancel()

    options.headers = {
    ...options.headers,
    Authorization: `Bearer ${myToken}`,
    }

    return {
    options
    }
    }
    })

    // 响应拦截
    const { data } = useFetch(url, {
    afterFetch(ctx) {
    if (ctx.data.title === 'HxH')
    ctx.data.title = 'Hunter x Hunter' // Modifies the resposne data

    return ctx
    },
    })

    作者:我只是一个小菜鸡
    链接:https://juejin.cn/post/7029699344596992031

    收起阅读 »

    token过期自动跳转到登录页面

    vue
    这几天项目提测,测试给我提了个bug,说token过期,路由应该自动跳转到登陆页面,让用户重新登录。先说下一些前置条件, 1:我公司的token时效在生产环境设置为一个小时,当token过期,所有接口都直接返回 2:每次路由跳转都会对token进行判断,设置了...
    继续阅读 »

    这几天项目提测,测试给我提了个bug,说token过期,路由应该自动跳转到登陆页面,让用户重新登录。先说下一些前置条件,
    1:我公司的token时效在生产环境设置为一个小时,当token过期,所有接口都直接返回
    2:每次路由跳转都会对token进行判断,设置了一个全局的beforeEach钩子函数,如果token存在就跳到你所需要的页面,否则就直接跳转到登录页面,让用户登录重新存取token


    接口返回的信息
    {
    code:10009,
    msg:'token过期',
    data:null
    }
    全局的路由钩子函数
    router.beforeEach(async(to, from, next) => {
    //获取token
    // determine whether the user has logged in
    const hasToken = getToken()

    if (hasToken) {
    //token存在,如果当前跳转的路由是登录界面
    if (to.path === '/login') {
    // if is logged in, redirect to the home page
    next({ path: '/' })
    NProgress.done()
    } else {
    //在这里,就拉去用户权限,判断用户是否有权限访问这个路由
    } catch (error) {
    // remove token and go to login page to re-login
    await store.dispatch('user/resetToken')
    Message.error(error || 'Has Error')
    next(`/login?redirect=${to.path}`)
    NProgress.done()
    }
    } else {
    //token不存在
    if (whiteList.indexOf(to.path) !== -1) {
    //如果要跳转的路由在白名单里,则跳转过去
    next()
    } else {
    //否则跳转到登录页面
    next(`/login?redirect=${to.path}`)
    NProgress.done()
    }
    }
    })

    所以我直接在对所有的请求进行拦截,当响应的数据返回的code是10009,就直接清空用户信息,重新加载页面。我对代码简化了下,因为用户在登录时就会把token,name以及权限信息存在store/user.js文件里,所以只要token过期,把user文件的信息清空。这样,在token过期后,刷新页面或者跳转组件时,都会调用全局的beforeEach判断,当token信息不存在就会直接跳转到登录页面


    import axios from 'axios'
    import { MessageBox, Message } from 'element-ui'
    import store from '@/store'
    import { getToken } from '@/utils/auth'

    const service = axios.create({
    baseURL: process.env.VUE_APP_BASE_API,
    timeout: 5000
    })
    //发送请求时把token携带过去
    service.interceptors.request.use(
    config => {
    if (store.getters.token) {
    config.headers['sg-token'] = getToken()
    }
    return config
    },
    error => {
    console.log(error)
    return Promise.reject(error)
    }
    )

    service.interceptors.response.use(
    response => {
    console.log(response.data)
    const res = response.data

    // token过期,重返登录界面
    if (res.code === 10009) {
    store.dispatch('user/logout').then(() => {
    location.reload(true)
    })
    }
    return res
    },
    error => {
    console.log('err' + error) // for debug
    Message({
    message: error.msg,
    type: 'error',
    duration: 5 * 1000
    })
    return Promise.reject(error)
    }
    )

    export default service

    好啦,关于token的分享就到这里了,以上代码根据你们项目的情况换成你们的数据,有错误欢迎指出来!


    作者:阿狸要吃吃的
    链接:https://juejin.cn/post/6947970204320137252

    收起阅读 »

    Vue3,我决定不再使用Vuex

    vue
    在开发基于Vue3的项目中发现我们可以不再依赖Vuex也能很方便的来管理数据,只需要通过Composition Api可以快捷的建立简单易懂的全局数据存储. 创建State 通过reactive我们来创建state,暴露的IState是用来方便其他文件来接受S...
    继续阅读 »

    在开发基于Vue3的项目中发现我们可以不再依赖Vuex也能很方便的来管理数据,只需要通过Composition Api可以快捷的建立简单易懂的全局数据存储.


    创建State


    通过reactive我们来创建state,暴露的IState是用来方便其他文件来接受State对象


    import { reactive } from 'vue'

    export interface IState {
    code: string
    token: string
    user: any
    }

    export const State: IState = {
    code: '',
    token: '',
    user: {}
    }

    export function createState() {
    return reactive(State)
    }


    创建Action


    我们来创建Action来作为我们修改State的方法


    import { reactive } from 'vue'
    import { IState } from './state'

    function updateCode(state: IState) {
    return (code: string) => {
    state.code = code
    }
    }

    function updateToken(state: IState) {
    return (token: string) => {
    state.token = token
    }
    }

    function updateUser(state: IState) {
    return (user: any) => {
    state.user = user
    }
    }

    /**
    * 创建Action
    * @param state
    */
    export function createAction(state: IState) {
    return {
    updateToken: updateToken(state),
    updateCode: updateCode(state),
    updateUser: updateUser(state)
    }
    }

    通过暴露的IState我们也可以实现对State的代码访问.


    创建Store


    创建好StateAction后我们将它们通过Store整合在一起.


    import { reactive, readonly } from 'vue'
    import { createAction } from './action'
    import { createState } from './state'

    const state = createState()
    const action = createAction(state)

    export const useStore = () => {
    const store = {
    state: readonly(state),
    action: readonly(action)
    }

    return store
    }

    这样我们就可以在项目中通过调用useStore访问和修改State,因为通过useStore返回的State是通过readonly生成的,所以就确认只有Action可以对其进行修改.


    // 访问state
    const store = useStore()
    store.state.code

    // 调用action
    const store = useStore()
    store.action.updateCode(123)

    这样我们就离开了Vuex并创建出了可是实时更新的数据中心.


    持久化存储


    很多Store中的数据还是需要实现持久化存储,来保证页面刷新后数据依然可用,我们主要基于watch来实现持久化存储


    import { watch, toRaw } from 'vue'

    export function createPersistStorage<T>(state: any, key = 'default'): T {
    const STORAGE_KEY = '--APP-STORAGE--'

    // init value
    Object.entries(getItem(key)).forEach(([key, value]) => {
    state[key] = value
    })

    function setItem(state: any) {
    const stateRow = getItem()
    stateRow[key] = state
    const stateStr = JSON.stringify(stateRow)
    localStorage.setItem(STORAGE_KEY, stateStr)
    }

    function getItem(key?: string) {
    const stateStr = localStorage.getItem(STORAGE_KEY) || '{}'
    const stateRow = JSON.parse(stateStr) || {}
    return key ? stateRow[key] || {} : stateRow
    }

    watch(state, () => {
    const stateRow = toRaw(state)
    setItem(stateRow)
    })

    return readonly(state)
    }

    通过watchtoRaw我们就实现了statelocalstorage的交互.


    只需要将readonly更换成createPersistStorage即可


    export const useStore = () => {
    const store = {
    state: createPersistStorage<IState>(state),
    action: readonly(action)
    }

    return store
    }

    这样也就实现了对Store数据的持久化支持.


    作者:程序员紫菜苔
    链接:https://juejin.cn/post/6898504898380464142

    收起阅读 »