注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

当老婆又让我下载一个腾讯视频时

我们结婚了! 是的,这次不是女朋友啦,是老婆了! 时隔将近一个月,老婆又让我给她下载腾讯视频,如果按照上次探索的内容来下载的话,倒是可以一步步下载,合并,不过很麻烦,程序员不都是为了解决麻烦的吗,这么麻烦的步骤,有没有简单点呢。有!当然有,有很多简单的工具,...
继续阅读 »

我们结婚了!


是的,这次不是女朋友啦,是老婆了!


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

收起阅读 »

TypeScript 函数的重载

函数的重载 什么是函数重载呢?允许函数接收不同数量或类型的参数时,做出不同的处理。比如说这个例子: function double(x: number | string): number | string { if (typeof x === 'num...
继续阅读 »

函数的重载


什么是函数重载呢?允许函数接收不同数量或类型的参数时,做出不同的处理。比如说这个例子:


function double(x: number | string): number | string {
if (typeof x === 'number') {
return x * 2;
} else {
return x + ', ' + x;
}
}

本来这个函数,期望输入是 number,输出就是 number。或者输入是 string,输出就是 string。但是我们使用这种联合类型来书写的话,就会存在一个问题。


image-20211011230002191.png


那如何解决这个问题呢?我们可以使用函数的重载


function double(x: number): number; // 输入是 number 类型,输出也是 number 类型
function double(x: string): string;
function double(x: number | string): number | string {
if (typeof x === 'number') {
return x * 2;
} else {
return x + ', ' + x;
}
}

let d = double(1);

image-20211011230140623.png
需要注意的是,函数重载是从上往下匹配,如果有多个函数定义,有包含关系的话,需要把精确的,写在最前面。


习题-根据函数重载知识,完善下面代码块


function paramType (param: ______): string;
function paramType (param: string): string;
function paramType (param: string | number): string {
return typeof param;
};

paramType('panda');
paramType(10);

答案:number


解析:


重载允许一个函数接收不同数量或类型的参数,然后做不同处理。


// 函数声明
function paramType (param: ______): string;
function paramType (param: string): string;
// 函数实现
function paramType (param: string | number): string {
return typeof param;
};

在函数实现中参数的类型为 string | number,故答案为 number;


资料-高阶函数


在维基百科中对于高阶函数的定义是这样的:



在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:



  • 接受一个或多个函数作为输入

  • 输出一个函数



在 JavaScript 的世界中,大家应该都有听说过「函数是一等公民」( first-class citizens )的说法。这里实际上是指在 JavaScript 中,函数被视为 Object 类型,我们可以将函数( function )作为参数传递给其他函数,并且也可以将函数作为其他函数的返回值传递。


因为函数是对象,所以 JavaScript 天然就支持高阶函数的写法。


或许你对高阶函数感到陌生,但实际上在日常的代码编写中,我们一定都用到过高阶函数。


接受一个或多个函数作为输入


我们在日常的学习和开发中,一定遇到过使用回调函数( callback )的场景。回调函数是在完成所有其他操作后,在操作结束时执行的函数。我们通常将回调函数作为最后一个参数,用匿名函数的形式传入。在拥有异步操作的场景中,支持回调函数的传入是至关重要的。


例如我们发送一个 Ajax 请求,我们通常需要在服务器响应完成后进行一些操作,同时在 Web 端,一些需要等待用户响应的行为,如点击、键盘输入等场景也需要用到回调函数。我们看下面的例子


let $submitButton = document.querySelector('#submit-button');

$submitButton.addEventListener('click', function() {
alert('您点击了提交按钮!');
});

这里我们通过将匿名函数作为参数的形式将它传递了 addEventListener 函数。我们也可以改造一下:


let $submitButton = document.querySelector('#submit-button');

let showAlert = function() {
alert('您点击了提交按钮!');
}

$submitButton.addEventListener('click', showAlert);

请注意,这里我们给 addEventListener 传递参数的时候,使用的是 showAlert 而不是 showAlert()。在没有括号的时候,我们传递的是函数本身,而有括号的话,我们传递的是函数的执行结果。


这里将具名函数( named function )作为参数传递给其他函数的能力也为我们使用纯函数(pure functions )提供了很大的想象空间,我们可以定义一个小型的 纯函数库 ,其中的每个纯函数都可以作为参数被复用至多处。


将函数作为结果返回


我们来假想一种场景:假如你拥有一个个人网站,在里面写了很多篇文章。在你的文章中经常介绍你的个人网站,网址是 myblog.com,后来你的站点域名变成了 my-blog.com。这时候你需要将文章中的 myblog.com 替换为 my-blog.com。你或许会这样做:


let replaceSiteUrl = function(text) {
return text.replace(/myblog\.com/ig, 'my-blog.com');
}

在域名变更后,你又想更改网站名称,你可能会这么做:


let replaceSiteName = function(text) {
return text.replace(/MySite/ig, 'MyBlog');
}

上述做法是行之有效的,但是你或许会烦于每次信息变更都要写一个新的函数来适配,而且上述两段代码看起来相似度极高。这时候我们可以考虑使用 高阶函数 来复用这段代码:


let replaceText = function(reg, newText, source){
return function(source) {
return source.replace(reg, newText);
}
}

let replaceSiteUrl = replaceText(/myblog\.com/ig, 'my-blog.com');

console.log(replaceSiteUrl('My site url is https://myblog.com')); // My site url is https://my-blog.com

在上述代码中,我们用到了 JavaScript 函数并不关心他们收到多少个参数的特性,如果没有传递会自动忽略并且认为是 undefined。


总结


高阶函数看起来并没有那么神秘,它是我们日常很自然而然就用到的场景。高阶函数可以帮助我们将一些通用的场景抽象出来,达到多处复用的结果。这也是一种良好的编程习惯。


链接:https://juejin.cn/post/7029481950691737630

收起阅读 »

js 有哪些内置对象

全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。 这里说的全局的对象是说在全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。 js 中的内置对象主要指的是...
继续阅读 »

全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。


这里说的全局的对象是说在全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。



js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般我们经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。



标准内置对象的分类


(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。


   例如 Infinity、NaN、undefined、null 字面量

(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。


   例如 eval()、parseFloat()、parseInt() 等

(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。


   例如 Object、Function、Boolean、Symbol、Error 等

(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。


   例如 Number、Math、Date 

(5)字符串,用来表示和操作字符串的对象。


   例如 String、RegExp

(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。


例如 Array

(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。


   例如 Map、Set、WeakMap、WeakSet

(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。


   例如 SIMD 等

(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。


   例如 JSON 等

(10)控制抽象对象


   例如 Promise、Generator 等

(11)反射


   例如 Reflect、Proxy

(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。


   例如 Intl、Intl.Collator 等

(13)WebAssembly


(14)其他


例如 arguments

链接:https://juejin.cn/post/7029486810745012260

收起阅读 »

为什么的我的z-index不生效了??

最近开发时遇到了一个有趣的现象z-index&transform等连用造成了z-index不生效,因此想借此机会记录一下学习成果。本篇文章偏概念性,请在专业人士的监督下食用。Stacking Context 层叠上下文这是 HTML 中的一个三维概念(...
继续阅读 »

最近开发时遇到了一个有趣的现象z-index&transform等连用造成了z-index不生效,因此想借此机会记录一下学习成果。

本篇文章偏概念性,请在专业人士的监督下食用。

Stacking Context 层叠上下文

这是 HTML 中的一个三维概念(举个不太合适的🌰类似皮影戏?)用图片更容易明白。

假设这是我们看到的一个页面(左图),但是实际上他是通过这样展现的(右图),我们肉眼所见的画面可能是多个层叠加展示的,由此就会涉及到不同层的层级问题。

stacking context - mdn - 链接

Stacking Context 的创建

这里只简单介绍一下常用的 Stacking Context 。

  1. 天生具有 Stacking Context 的元素: HTML

  2. 常用混搭款:

    1. position的值为relative/absolute && z-index !== 'auto'会产生 Stacking Context ;
    2. position的值为fixed/sticky
    3. display: flex/inline-flex/grid && z-index !== 'auto'
    4. transform/filter !== 'none'
    5. opacity < 1
    6. -webkit-overflow-scrolling: touch

Stacking Context 的关系

  1. Stacking Context 可以嵌套,但内部的 Stacking Context 将受制于外部的 Stacking Context;
  2. Stacking Context 和兄弟元素相互独立;
  3. 元素的层叠次序是被包含在父元素的 Stacking Context 中的;
  4. 通常来说,最底层的 Stacking Context 是 <HTML>标签创建的。

层叠次序

在一个层叠上下文内,不同元素的层叠次序如下图所示(由内到外):

  1. 最底层的是当前层叠上下文的装饰性内容,如背景颜色、边框等;
  2. 其次是负值的 z-index;
  3. 然后是布局相关的内容,如块状盒子、浮动盒子;
  4. 接着是内容相关的元素,如inline水平盒子;
  5. 再接着是 z-index:auto/0/不依赖 z-index 的(子)层叠上下文;
  6. 最上面的就是 z-index 值为正的元素;

概括来说,z-index为正 > z-index:auto/z-index:0/不依赖z-index的层叠上下文 > 内容 > 布局 > 装饰。

选自张鑫旭《CSS世界》图7-7

回应标题:为什么我的 z-index 不生效了?

这个标题内容非常的宽泛,我们提供如下的解题思路:

  1. 是否配合使用了可用的 postion / 弹性布局 / grid布局?
    1. 没配合自然不生效。
  2. z-index: -1; 为什么没有生效?
    1. 检查你对应的父元素是否也创建了 Stacking Context,大概率是的,根据 #层叠顺序那一章可以知道,负的z-index的次序是高于当前层叠上下文的背景的;
    2. 解决方案:取消父元素的 Stacking Context / 元素外包裹一层新的元素。

作者:vivi_chen
链接:https://juejin.cn/post/7028858045882957838

收起阅读 »

【译】3 个能优化网站可用性但被忽视的细节

根据 Adobe 的调查显示,给定 15 分钟的时间浏览内容,三分之二的用户更愿意将时间花费在视觉上吸引人的内容。用户也希望网站能在至少 5 秒内加载。因此,设计一个速度快、满意度高的网站(或应用)应成为每个设计师关注的重点。 视觉设计是很难被忽视的,因为我们...
继续阅读 »

根据 Adobe 的调查显示,给定 15 分钟的时间浏览内容,三分之二的用户更愿意将时间花费在视觉上吸引人的内容用户也希望网站能在至少 5 秒内加载。因此,设计一个速度快、满意度高的网站(或应用)应成为每个设计师关注的重点。


视觉设计是很难被忽视的,因为我们作为设计师,喜欢设计视觉上吸引人的东西。虽然美学非常重要,但在有限的时间内,设计师往往倾向于放弃可用性。优化应用/网站的可用性需要你深入地了解客户的目标。网站的可以性可以通过不同的方式来衡量,举例如下:



  1. UI 的清晰度有多高?

  2. 页面上的“障碍“有多少?

  3. 导航是否遵循逻辑结构?


让我们谈谈这些可以提高可用性的细节。


1. 更少的选择


作出选择是耗费精力的,所以为用户理清或甚至是排除不必要的障碍能减少所谓的分析瘫痪。分析瘫痪是指用户因为要考虑的选择太多而感到困惑或沮丧。



根据心理学家 Mark Lepper 和 Sheena Iyengar 的研究指出,更多的选择往往会导致更少的销售额。他们分析了 754 名消费者在面临多种选择时的行为,研究是这样进行的:


在第一天,他们在一家高档食品超市中摆了 24 种果酱,但在第二天,桌子上只摆有 6 种果酱。结果显示摆有 24 种果酱的桌子收到了更多的关注,但第二天的销量却比第一天的来得更好。这个现象可以用一句话来说明:“选择泛滥(spoilt for choice)”。


过量的选择将导致分析瘫痪。用户无法做出决策,因为他们面临太多的选择了。当你考虑那些各种各样的果酱时,它们之中可能某些比较便宜,而另一些可能味道更好等等。我们的大脑将尝试“解码”哪个选择最物有所值。这需要时间和思考,所以结果是转换的速度降低了,甚至导致用户放弃做出选择。



深入阅读:希克定律。希克定律指出,做出决定所需的时间会随着选项的数量增加而增加。该定律证实了减少选择数量能提高转化率这个概念。正如他们所说,“少即是多”



解决方法:个性化的内容


预期设计(由 Spotify,Netflix 和 Google 采用)能帮助使用者减少决策疲劳,其中人工智能(AI)能用于预测用户想要什么。应用和网站展示的“最受欢迎”的栏目就是例子之一,背后的逻辑是:因为其他的用户对这件商品感兴趣,所以你也可能对它感兴趣。


对于零售网站来说,另一种方式是整合“最畅销的商品”或“心愿单”,例如亚马逊的“购买此商品的客户也购买了……”推荐引擎。



冷知识:亚马逊的推荐引擎占其总收入的 30%。人工智能根据用户搜索历史和购物篮中的商品来预测用户想要购买的商品。




2. 极简导航


对于包含多个类别和子类的的网站,导航应成为用户体验(UX)的重中之重,尤其是在移动设备上。移动端网站难以导航,更容易导致分析瘫痪。


为了提升可用性,菜单中包含的项目数量应维持在 7 个以内(这同样适用于下拉菜单)。这样做还能更容易地指示用户所在的位置,降低用户跳出率。


为什么?因为用户时常会忘记他们之前在做什么,尤其是当他们打开多个标签页的时候!


3. 在导航中显示当前位置


进度追踪器能指示用户在界面中的当前位置。根据 Kantar 和 Lightspeed Research 的研究指出,这些指示器能提高用户参与度和客户满意度。


典型的网络冲浪者(或应用程序用户)通常会在一时之内打开多个标签页(或应用程序),因此他们很容易忘记在某个标签页中未完成的任务。有时侯,分析瘫痪的困境是由用户自己造成的!


设计师应该意识到他们的应用程序或网站不会是用户使用的唯一应用程序或网站,当用户面临太多打开着的标签页时,这通常会导致健忘。一个标注用户所在位置的指示器是非常有帮助的。否则,用户可能不仅会忘记他们在做什么,而且会完全不再注意它。


解决方法:面包屑导航


面包屑用于表示用户在哪里,以及他们来自哪里。你可能听说过《汉赛尔和格莱特》这个经典童话故事,这对兄妹用面包屑帮助他们找到回家的路,也避免了他们在森林中绕圈子。


面包屑导航能描述用户的路径。你在亚马逊,NewEgg 和其他的一些需要展示大量内容的线上零售网站都能看到这一点。这能帮助用户记得他们上次所在的位置(如果他们中途因任何原因离开屏幕),并帮助他们在遇到死胡同时找到回去的路。



结论


总的来说,你可以通过帮助用户专注于重要的事情来有效的提高网站的可用性; 温和地引导他们,在必要时进行总结,并优化用户体验以确保用户能找到他们想找的东西。


作者:披着狼皮的羊_
链接:https://juejin.cn/post/7028491022107672613

收起阅读 »

setTimeout的执行你真的了解吗?

setTimeout的创建和执行 我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。 首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了...
继续阅读 »

setTimeout的创建和执行


我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。


首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了保证setTimeout能够在规定的时间内执行,setTimeout创建的任务不会被添加到消息队列里,与此同时,浏览器还维护了另外一个队列叫做延迟消息队列,该队列就是用来存放延迟任务,setTimeout创建的任务会被存放于此,同时它会被记住创建时间,延迟执行时间。


然后我们看下具体例子:


setTimeout(function showName() { console.log('showName') }, 1000)
setTimeout(function showName() { console.log('showName1') }, 1000)
console.log('martincai')

以上例子执行是这样:



  • 1.从消息队列中取出宏任务进行执行(首次任务直接执行)

  • 2.执行setTimeout,此时会创建一个延迟任务,延迟任务的回调函数是showName,发起时间是当前的时间,延迟时间是第二个参数1000ms,然后该延迟任务会被推入到延迟任务队列

  • 3.执行console.log('martincai')代码

  • 4.从延迟队列里去筛选所有已过期任务(当前时间 >= 发起时间 + 延迟时间),然后依次执行


所以我们可以看到showName和showName1同时执行,原因是两个延迟任务都已经过期


循环源码:


void MainTherad(){
for(;;){
// 执行消息队列中的任务
Task task = task_queue.takeTask();
ProcessTask(task);

// 执行延迟队列中的任务
ProcessDelayTask()

if(!keep_running) // 如果设置了退出标志,那么直接退出线程循环
break;
}
}

删除延迟任务


clearTimeout是windows下提供的原生方法,用于删除特定的setTimeout的延迟任务,我们在定义setTimeout的时候返回值就是当前任务的唯一id值,那么clearTimeout就会拿着id在延迟消息队列里查找对应的任务,将其踢出队列即可


setTimeout的几个注意点:



  1. setTimeout会受到消息队列里的宏任务的执行时间影响,上面我们可以看到延迟消息队列的任务会在消息队列的弹出的当前任务执行完之后再执行,所以当前任务的执行时间会阻碍到setTimeout的延迟任务的执行时间


  function showName() {
setTimeout(function show() {
console.log('show')
}, 0)
for (let i = 0; i <= 5000; i++) {}
}
showName()

这里去执行一遍可以发现setTimeout并不是在0ms左右执行,中间会有明显的延迟,因为setTimeout在执行的时候首先会将任务放入到延迟消息队列里,等到showName执行完之后,才会去延迟队列里去查找已过期的任务,这里setTimeout任务会被showName耽误



  1. setTimeout嵌套下会有4ms的延迟


Chrome会把嵌套5层以上的setTimeout后当作阻塞方法,在第6次调用setTimeout的时候会自动将延时器更改为至少4ms的延迟时间



  1. 未激活的页面的setTimeout更改为至少1000ms


当前tab页面不在active状态的时候,setTimeout的延迟至少会被更改1000ms,这样做是为了减少性能消耗和电量消耗



  1. 延迟时间有最大值


目前Chrome、Firefox等主流浏览器都是用32bit去存储延时时间,所以最大值是2的31次方 - 1


  setTimeout(() => {
console.log(1)
}, 2 ** 31)

以上代码会立即执行


链接:https://juejin.cn/post/7028836586745757710
收起阅读 »

从22行有趣的源码库中,我学到了 callback promisify 化的 Node.js 源码实现

我们经常会在本地git仓库切换tags,或者git仓库切换tags。那么我们是否想过如果获取tags呢。本文就是学习 remote-git-tags 这个22行代码的源码库。源码不多,但非常值得我们学习。 阅读本文,你将学到: 1. Node 加载采用什么模块...
继续阅读 »

我们经常会在本地git仓库切换tags,或者git仓库切换tags。那么我们是否想过如果获取tags呢。本文就是学习 remote-git-tags 这个22行代码的源码库。源码不多,但非常值得我们学习。


阅读本文,你将学到:


1. Node 加载采用什么模块
2. 获取 git 仓库所有 tags 的原理
3. 学会调试看源码
4. 学会面试高频考点 promisify 的原理和实现
5. 等等

刚开始先不急着看上千行、上万行的源码。源码长度越长越不容易坚持下来。看源码讲究循序渐进。比如先从自己会用上的百来行的开始看。


我之前在知乎上回答过类似问题。


一年内的前端看不懂前端框架源码怎么办?


简而言之,看源码


循序渐进
借助调试
理清主线
查阅资料
总结记录

2. 使用


import remoteGitTags from 'remote-git-tags';

console.log(await remoteGitTags('https://github.com/lxchuan12/blog.git'));
//=> Map {'3.0.5' => '6020cc35c027e4300d70ef43a3873c8f15d1eeb2', …}

3. 源码



Get tags from a remote Git repo



这个库的作用是:从远程仓库获取所有标签。


原理:通过执行 git ls-remote --tags repoUrl (仓库路径)获取 tags


应用场景:可以看有哪些包依赖的这个包。
npm 包描述信息


其中一个比较熟悉的是npm-check-updates



npm-check-updates 将您的 package.json 依赖项升级到最新版本,忽略指定的版本。



还有场景可能是 github 中获取所有 tags 信息,切换 tags 或者选定 tags 发布版本等,比如微信小程序版本。


看源码前先看 package.json 文件。


3.1 package.json


// package.json
{
// 指定 Node 以什么模块加载,缺省时默认是 commonjs
"type": "module",
"exports": "./index.js",
// 指定 nodejs 的版本
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"scripts": {
"test": "xo && ava"
}
}

众所周知,Node 之前一直是 CommonJS 模块机制。 Node 13 添加了对标准 ES6 模块的支持。


告诉 Node 它要加载的是什么模块的最简单的方式,就是将信息编码到不同的扩展名中。
如果是 .mjs 结尾的文件,则 Node 始终会将它作为 ES6 模块来加载。
如果是 .cjs 结尾的文件,则 Node 始终会将它作为 CommonJS 模块来加载。


对于以 .js 结尾的文件,默认是 CommonJS 模块。如果同级目录及所有目录有 package.json 文件,且 type 属性为module 则使用 ES6 模块。type 值为 commonjs 或者为空或者没有 package.json 文件,都是默认 commonjs 模块加载。


关于 Node 模块加载方式,在《JavaScript权威指南第7版》16.1.4 Node 模块 小节,有更加详细的讲述。此书第16章都是讲述Node,感兴趣的读者可以进行查阅。


3.2 调试源码


# 推荐克隆我的项目,保证与文章同步,同时测试文件齐全
git clone https://github.com/lxchuan12/remote-git-tags-analysis.git
# npm i -g yarn
cd remote-git-tags && yarn
# VSCode 直接打开当前项目
# code .

# 或者克隆官方项目
git clone https://github.com/sindresorhus/remote-git-tags.git
# npm i -g yarn
cd remote-git-tags && yarn
# VSCode 直接打开当前项目
# code .

用最新的VSCode 打开项目,找到 package.jsonscripts 属性中的 test 命令。鼠标停留在test命令上,会出现 运行命令调试命令 的选项,选择 调试命令 即可。


调试如图所示:


调试如图所示


VSCode 调试 Node.js 说明如下图所示:


VSCode 调试 Node.js 说明


跟着调试,我们来看主文件。


3.3 主文件仅有22行源码


// index.js
import {promisify} from 'node:util';
import childProcess from 'node:child_process';

const execFile = promisify(childProcess.execFile);

export default async function remoteGitTags(repoUrl) {
const {stdout} = await execFile('git', ['ls-remote', '--tags', repoUrl]);
const tags = new Map();

for (const line of stdout.trim().split('\n')) {
const [hash, tagReference] = line.split('\t');

// Strip off the indicator of dereferenced tags so we can override the
// previous entry which points at the tag hash and not the commit hash
// `refs/tags/v9.6.0^{}` → `v9.6.0`
const tagName = tagReference.replace(/^refs\/tags\//, '').replace(/\^{}$/, '');

tags.set(tagName, hash);
}

return tags;
}

源码其实一眼看下来就很容易懂。


3.4 git ls-remote --tags


支持远程仓库链接。


git ls-remote 文档


如下图所示:


ls-remote


获取所有tags git ls-remote --tags https://github.com/vuejs/vue-next.git


把所有 tags 和对应的 hash值 存在 Map 对象中。


3.5 node:util


Node 文档



Core modules can also be identified using the node: prefix, in which case it bypasses the require cache. For instance, require('node:http') will always return the built in HTTP module, even if there is require.cache entry by that name.



也就是说引用 node 原生库可以加 node: 前缀,比如 import util from 'node:util'


看到这,其实原理就明白了。毕竟只有22行代码。接着讲述 promisify


4. promisify


源码中有一段:


const execFile = promisify(childProcess.execFile);

promisify 可能有的读者不是很了解。


接下来重点讲述下这个函数的实现。


promisify函数是把 callback 形式转成 promise 形式。


我们知道 Node.js 天生异步,错误回调的形式书写代码。回调函数的第一个参数是错误信息。也就是错误优先。


我们换个简单的场景来看。


4.1 简单实现


假设我们有个用JS加载图片的需求。我们从 这个网站 找来图片。


examples
const imageSrc = 'https://www.themealdb.com/images/ingredients/Lime.png';

function loadImage(src, callback) {
const image = document.createElement('img');
image.src = src;
image.alt = '公众号若川视野专用图?';
image.style = 'width: 200px;height: 200px';
image.onload = () => callback(null, image);
image.onerror = () => callback(new Error('加载失败'));
document.body.append(image);
}

我们很容易写出上面的代码,也很容易写出回调函数的代码。需求搞定。


loadImage(imageSrc, function(err, content){
if(err){
console.log(err);
return;
}
console.log(content);
});

但是回调函数有回调地狱等问题,我们接着用 promise 来优化下。


4.2 promise 初步优化


我们也很容易写出如下代码实现。


const loadImagePromise = function(src){
return new Promise(function(resolve, reject){
loadImage(src, function (err, image) {
if(err){
reject(err);
return;
}
resolve(image);
});
});
};
loadImagePromise(imageSrc).then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
});

但这个不通用。我们需要封装一个比较通用的 promisify 函数。


4.3 通用 promisify 函数


function promisify(original){
function fn(...args){
return new Promise((resolve, reject) => {
args.push((err, ...values) => {
if(err){
return reject(err);
}
resolve(values);
});
// original.apply(this, args);
Reflect.apply(original, this, args);
});
}
return fn;
}

const loadImagePromise = promisify(loadImage);
async function load(){
try{
const res = await loadImagePromise(imageSrc);
console.log(res);
}
catch(err){
console.log(err);
}
}
load();

需求搞定。这时就比较通用了。


这些例子在我的仓库存放在 examples 文件夹中。可以克隆下来,npx http-server .跑服务,运行试试。


examples


跑失败的结果可以把 imageSrc 改成不存在的图片即可。


promisify 可以说是面试高频考点。很多面试官喜欢考此题。


接着我们来看 Node.js 源码中 promisify 的实现。


4.4 Node utils promisify 源码


github1s node utils 源码


源码就暂时不做过多解释,可以查阅文档。结合前面的例子,其实也容易理解。


utils promisify 文档


const kCustomPromisifiedSymbol = SymbolFor('nodejs.util.promisify.custom');
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs');

let validateFunction;

function promisify(original) {
// Lazy-load to avoid a circular dependency.
if (validateFunction === undefined)
({ validateFunction } = require('internal/validators'));

validateFunction(original, 'original');

if (original[kCustomPromisifiedSymbol]) {
const fn = original[kCustomPromisifiedSymbol];

validateFunction(fn, 'util.promisify.custom');

return ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
value: fn, enumerable: false, writable: false, configurable: true
});
}

// Names to create an object from in case the callback receives multiple
// arguments, e.g. ['bytesRead', 'buffer'] for fs.read.
const argumentNames = original[kCustomPromisifyArgsSymbol];

function fn(...args) {
return new Promise((resolve, reject) => {
ArrayPrototypePush(args, (err, ...values) => {
if (err) {
return reject(err);
}
if (argumentNames !== undefined && values.length > 1) {
const obj = {};
for (let i = 0; i < argumentNames.length; i++)
obj[argumentNames[i]] = values[i];
resolve(obj);
} else {
resolve(values[0]);
}
});
ReflectApply(original, this, args);
});
}

ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original));

ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
value: fn, enumerable: false, writable: false, configurable: true
});
return ObjectDefineProperties(
fn,
ObjectGetOwnPropertyDescriptors(original)
);
}

promisify.custom = kCustomPromisifiedSymbol;

5. ES6+ 等知识


文中涉及到了Mapfor of、正则、解构赋值。


还有涉及封装的 ReflectApplyObjectSetPrototypeOfObjectDefinePropertyObjectGetOwnPropertyDescriptors 等函数都是基础知识。



作者:若川
链接:https://juejin.cn/post/7028731182216904740

收起阅读 »

3D 穿梭效果?使用 CSS 轻松搞定

背景 周末在家习惯性登陆 Apex,准备玩几盘。在登陆加速器的过程中,发现加速器到期了。 我一直用的腾讯网游加速器,然而点击充值按钮,提示最近客户端升级改造,暂不支持充值(这个操作把我震惊了~)。只能转头下载网易 UU 加速器。 打开 UU 加速器首页,映入眼...
继续阅读 »

背景


周末在家习惯性登陆 Apex,准备玩几盘。在登陆加速器的过程中,发现加速器到期了。


我一直用的腾讯网游加速器,然而点击充值按钮,提示最近客户端升级改造,暂不支持充值(这个操作把我震惊了~)。只能转头下载网易 UU 加速器


打开 UU 加速器首页,映入眼帘的是这样一幅画面:


11.gif


瞬间,被它这个背景图吸引。


出于对 CSS 的敏感,盲猜了一波这个用 CSS 实现的,至少也应该是 Canvas。打开控制台,稍微有点点失望,居然是一个 .mp4文件:



再看看 Network 面板,这个 .mp4 文件居然需要 3.5M?



emm,瞬间不想打游戏了。这么个背景图,CSS 不能搞定么


使用 CSS 3D 实现星际 3D 穿梭效果


这个技巧,我在 奇思妙想 CSS 3D 动画 | 仅使用 CSS 能制作出多惊艳的动画? 也有提及过,感兴趣的可以一并看看。


假设我们有这样一张图形:



这张图先放着备用。在使用这张图之前,我们会先绘制这样一个图形:


<div class="g-container">
<div class="g-group">
<div class="item item-right"></div>
<div class="item item-left"></div>
<div class="item item-top"></div>
<div class="item item-bottom"></div>
<div class="item item-middle"></div>
</div>
</div>

body {
background: #000;
}
.g-container {
position: relative;
}
.g-group {
position: absolute;
width: 100px;
height: 100px;
left: -50px;
top: -50px;
transform-style: preserve-3d;
}
.item {
position: absolute;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, .5);
}
.item-right {
background: red;
transform: rotateY(90deg) translateZ(50px);
}
.item-left {
background: green;
transform: rotateY(-90deg) translateZ(50px);
}
.item-top {
background: blue;
transform: rotateX(90deg) translateZ(50px);
}
.item-bottom {
background: deeppink;
transform: rotateX(-90deg) translateZ(50px);
}
.item-middle {
background: rgba(255, 255, 255, 0.5);
transform: rotateX(180deg) translateZ(50px);
}

一共设置了 5 个子元素,不过仔细看 CSS 代码,其中 4 个子元素都设置了 rotateX/Y(90deg/-90deg),也就是绕 X 轴或者 Y 轴旋转了 90°,在视觉上是垂直屏幕的一张平面,所以直观视觉上我们是不到的,只能看到一个平面 .item-middle


我将 5 个子 item 设置了不同的背景色,结果如下:



现在看来,好像平平无奇,确实也是。


不过,见证奇迹的时候来了,此时,我们给父元素 .g-container 设置一个极小的 perspective,譬如,设置一个 perspective: 4px,看看效果:


.g-container {
position: relative;
+ perspective: 4px;
}
// ...其余样式保持不变

此时,画风骤变,整个效果就变成了这样:



由于 perspective 生效,原本的平面效果变成了 3D 的效果。接下来,我们使用上面准备好的星空图,替换一下上面的背景颜色,全部都换成同一张图,神奇的事情发生了:



由于设置的 perspective 非常之下,而每个 item 的 transform: translateZ(50px) 设置的又比较大,所以图片在视觉上被拉伸的非常厉害。但是整体是充满整个屏幕的。


接下来,我们只需要让视角动起来,给父元素增加一个动画,通过控制父元素的 translateZ() 进行变化即可:


.g-container{
position: relative;
perspective: 4px;
perspective-origin: 50% 50%;
}

.g-group{
position: absolute;
// ... 一些定位高宽代码
transform-style: preserve-3d;
+ animation: move 8s infinite linear;
}

@keyframes move {
0%{
transform: translateZ(-50px) rotate(0deg);
}
100%{
transform: translateZ(50px) rotate(0deg);
}
}


看看,神奇美妙的星空穿梭的效果就出来了,Amazing:



美中不足之处在于,动画没能无限衔接上,开头和结尾都有很大的问题。


当然,这难不倒我们,我们可以:



  1. 通过叠加两组同样的效果,一组比另一组通过负的 animation-delay 提前行进,使两组动画衔接起来(一组结束的时候另外一组还在行进中)

  2. 再通过透明度的变化,隐藏掉 item-middle 迎面飞来的突兀感

  3. 最后,可以通过父元素的滤镜 hue-rotate 控制图片的颜色变化


我们尝试修改 HTML 结构如下:


<div class="g-container">
<div class="g-group">
<div class="item item-right"></div>
<div class="item item-left"></div>
<div class="item item-top"></div>
<div class="item item-bottom"></div>
<div class="item item-middle"></div>
</div>
<!-- 增加一组动画 -->
<div class="g-group">
<div class="item item-right"></div>
<div class="item item-left"></div>
<div class="item item-top"></div>
<div class="item item-bottom"></div>
<div class="item item-middle"></div>
</div>
</div>


修改后的核心 CSS 如下:


.g-container{
perspective: 4px;
position: relative;
// hue-rotate 变化动画,可以让图片颜色一直变换
animation: hueRotate 21s infinite linear;
}

.g-group{
transform-style: preserve-3d;
animation: move 12s infinite linear;
}
// 设置负的 animation-delay,让第二组动画提前进行
.g-group:nth-child(2){
animation: move 12s infinite linear;
animation-delay: -6s;
}
.item {
background: url(https://z3.ax1x.com/2021/08/20/fLwuMd.jpg);
background-size: cover;
opacity: 1;
// 子元素的透明度变化,减少动画衔接时候的突兀感
animation: fade 12s infinite linear;
animation-delay: 0;
}
.g-group:nth-child(2) .item {
animation-delay: -6s;
}
@keyframes move {
0%{
transform: translateZ(-500px) rotate(0deg);
}
100%{
transform: translateZ(500px) rotate(0deg);
}
}
@keyframes fade {
0%{
opacity: 0;
}
25%,
60%{
opacity: 1;
}
100%{
opacity: 0;
}
}
@keyframes hueRotate {
0% {
filter: hue-rotate(0);
}
100% {
filter: hue-rotate(360deg);
}
}

最终完整的效果如下,星空穿梭的效果,整个动画首尾相连,可以一直无限下去,几乎没有破绽,非常的赞:



上述的完整代码,你可以猛击这里:CSS 灵感 -- 3D 宇宙时空穿梭效果


这样,我们就基本还原了上述见到的网易 UU 加速器首页的动图背景。


更进一步,一个图片我都不想用


当然,这里还是会有读者吐槽,你这里不也用了一张图片资源么?没有那张星空图行不行?这张图我也懒得去找。


当然可以,CSS YYDS。这里我们尝试使用 box-shadow,去替换实际的星空图,也是在一个 div 标签内实现,借助了 SASS 的循环函数:


<div></div>

@function randomNum($max, $min: 0, $u: 1) {
@return ($min + random($max)) * $u;
}

@function randomColor() {
@return rgb(randomNum(255), randomNum(255), randomNum(255));
}

@function shadowSet($maxWidth, $maxHeight, $count) {
$shadow : 0 0 0 0 randomColor();

@for $i from 0 through $count {
$x: #{random(10000) / 10000 * $maxWidth};
$y: #{random(10000) / 10000 * $maxHeight};


$shadow: $shadow, #{$x} #{$y} 0 #{random(5)}px randomColor();
}

@return $shadow;
}

body {
background: #000;
}

div {
width: 1px;
height: 1px;
border-radius: 50%;
box-shadow: shadowSet(100vw, 100vh, 500);
}

这里,我们用 SASS 封装了一个函数,利用多重 box-shadow 的特性,在传入的大小的高宽内,生成传入个数的点。


这样,我们可以得到这样一幅图,用于替换实际的星空图:



我们再把上述这个图,替换实际的星空图,主要是替换 .item 这个 class,只列出修改的部分:


// 原 CSS,使用了一张星空图
.item {
position: absolute;
width: 100%;
height: 100%;
background: url(https://z3.ax1x.com/2021/08/20/fLwuMd.jpg);
background-size: cover;
animation: fade 12s infinite linear;
}

// 修改后的 CSS 代码
.item {
position: absolute;
width: 100%;
height: 100%;
background: #000;
animation: fade 12s infinite linear;
}
.item::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 1px;
height: 1px;
border-radius: 50%;
box-shadow: shadowSet(100vw, 100vh, 500);
}

这样,我们就实现了这样一个效果,在不借助额外资源的情况下,使用纯 CSS 实现上述效果:



CodePen Demo -- Pure CSS Galaxy Shuttle 2


通过调整动画的时间,perspective 的值,每组元素的 translateZ() 变化距离,可以得到各种不一样的观感和效果,感兴趣的读者可以基于我上述给的 DEMO 自己尝试尝试。


作者:chokcoco
链接:https://juejin.cn/post/7028757824695959588

收起阅读 »

freeze、seal、preventExtensions对比

在Object常用的方法中,Object.freeze和Object.seal对于初学者而言,是两个较为容易混淆的概念,常常傻傻分不清两者的区别和应用场景 概念 先看看两者定义 Object.freeze在MDN中的定义 Object.freeze() 方法...
继续阅读 »

Object常用的方法中,Object.freezeObject.seal对于初学者而言,是两个较为容易混淆的概念,常常傻傻分不清两者的区别和应用场景


概念


先看看两者定义


Object.freeze在MDN中的定义



Object.freeze() 方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。



Object.seal在MDN中的定义



Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变。



从两者定义可以得到两者差异:Object.freeze核心是冻结,强调的是不可修改。Object.seal核心是封闭,强调的是不可配置,不影响老的属性值修改


差异


定义一个对象,接下来的对比围绕这个对象进行


"use strict";
const obj = {
name: "nordon"
};

使用Object.freeze


Object.freeze(obj);
Object.isFrozen(obj); // true
obj.name = "wy";

使用Object.isFrozen可以检测数据是否被Object.freeze冻结,返回一个Boolean类型数据


此时对冻结之后的数据进行修改,控制台将会报错:Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'


使用Object.seal


Object.seal(obj);
Object.isSealed(obj); // true
obj.name = "wy";

使用Object.isSealed可以检测数据是否被Object.seal封闭,返回一个Boolean类型数据


此时修改name是成功的,此时的obj也成功被修改


注意:若是不开启严格模式,浏览器会采用静默模式,不会在控制台抛出异常信息


共同点


主要以Object.seal演示


不可删除


delete obj.name;

控制台将会抛出异常:Uncaught TypeError: Cannot delete property 'name' of #<Object>


不可配置


可以修改原有的属性值


Object.defineProperty(obj, 'name', {
value: 'wy'
})

不可增加新的属性值


Object.defineProperty(obj, "age", {
value: 12,
});

控制台将会抛出异常:Uncaught TypeError: Cannot define property age, object is not extensible


深层嵌套


两者对于深层嵌套的数据都表现为:无能为力


定义一个嵌套的对象


"use strict";
const obj = {
name: "nordon",
info: {
foo: 'bar'
}
};

对于obj而言,无论是freeze还是seal,操作info内部的数据都无法做到对应的处理


obj.info.msg = 'msg'

数据源obj被修改,不受冻结或者冰封的影响


若是想要做到嵌套数据的处理,需要递归便利数据源处理,此操作需要注意:数据中包含循环引用时,将会触发无限循环


preventExtensions


最后介绍一下Object.preventExtensions,为何这个方法没有放在与Object.freezeObject.seal一起对比呢?因为其和seal基本可保持一致,唯一的区别就是可以delete属性,因此单独放在最后介绍


看一段代码


"use strict";
const obj = {
name: "nordon",
};

Object.preventExtensions(obj);
Object.isExtensible(obj); // false, 代表其不可扩展

delete obj.name;

作者:Nordon
链接:https://juejin.cn/post/7028389571561947172

收起阅读 »

【喵猫秀秀秀】用CSS向你展示猫立方!!

前言 这次,我们用vue2+scss,带大家来实现一个六面体的猫立方。 本次的逻辑我们不适用任何的js代码,仅仅只依靠css来完成。 所以,通过本片文章,你可以收获一些css动画相关的技巧。 先看看效果 预习 本次我们要用到的知识点 transform ...
继续阅读 »

前言


这次,我们用vue2+scss,带大家来实现一个六面体的猫立方。


本次的逻辑我们不适用任何的js代码,仅仅只依靠css来完成。


所以,通过本片文章,你可以收获一些css动画相关的技巧。


先看看效果


cat3D.gif


预习


本次我们要用到的知识点



  1. transform


解释:transform 属性向元素应用 2D 或 3D 转换。该属性允许我们对元素进行旋转、缩放、移动或倾斜。


要用哪个,可以对着这个表格查


image.png



  1. transform-style


解释:transform--style属性指定嵌套元素是怎样在三维空间中呈现。


注意:  使用此属性必须先使用 transform 属性.



  1. transition


解释:transition 属性是一个简写属性,用于设置四个过渡属性:



  • transition-property

  • transition-duration

  • transition-timing-function

  • transition-delay


注释:请始终设置 transition-duration 属性,否则时长为 0,就不会产生过渡效果。


分析


我们先拆解下这个猫3D的特点,它有以下特点



  1. 它一直在不停的转

  2. 它由两个六面体组成,外面一个,里面一个

  3. 鼠标靠近外面的六面体,六面体的六个面会往外扩,露出里面的小六面体


开始


1.因为我们做的是六面体,有2个六面体,一个在里面,一个在外面。2个六面体,12个面,先准备12张猫主子的图片。


image.png



  1. 然后我们新建img3D.vue文件,开干


image.png


步骤一


先来完成第一个特点不停的转


cat3D1.gif
代码如下:


<template>
<div>
<div class="container">
</div>
</div>
</template>

<script>

</script>

<style lang="scss" scoped>
* {
margin: 0px;
padding: 0px;
}

.container {
position: relative;
margin: 0px auto;
margin-top: 9%;
margin-left: 42%;
width: 200px;
height: 200px;
transform: rotateX(-30deg) rotateY(-80deg);
transform-style: preserve-3d;
animation: rotate 15s infinite;
/* 我这里用一个边框线来更容易看到效果 */
border: 1px solid red;
}

/* 旋转立方体 */
@keyframes rotate {
from {
transform: rotateX(0deg) rotateY(0deg);
}

to {
transform: rotateX(360deg) rotateY(360deg);
}
}
</style>

步骤二


弄外面的六面体,并且六面体在鼠标悬停的时候,需要往外扩
效果如下:


cat3D2.gif


<template>
<div>
<div class="container">
<!-- 外层立方体 -->
<div class="outer-container">
<div class="outer-top">
<img :src="cat1" />
</div>
<div class="outer-bottom">
<img :src="cat2" />
</div>
<div class="outer-front">
<img :src="cat3" />
</div>
<div class="outer-back">
<img :src="cat4" />
</div>
<div class="outer-left">
<img :src="cat5" />
</div>
<div class="outer-right">
<img :src="cat6" />
</div>
</div>
</div>
</div>
</template>

<script>
import cat1 from "@/assets/cats/cat1.png";
import cat2 from "@/assets/cats/cat2.png";
import cat3 from "@/assets/cats/cat3.png";
import cat4 from "@/assets/cats/cat4.png";
import cat5 from "@/assets/cats/cat5.png";
import cat6 from "@/assets/cats/cat6.png";

export default {
data() {
return {
cat1,
cat2,
cat3,
cat4,
cat5,
cat6,
};
},
};
</script>

<style lang="scss" scoped>
...
/* 设置图片可以在三维空间里展示 */
.container .outer-container {
transform-style: preserve-3d;
}
/* 设置图片尺寸固定宽高200px */
.outer-container img {
width: 200px;
height: 200px;
}

/* 外层立方体样式 */
.outer-container .outer-top,
.outer-container .outer-bottom,
.outer-container .outer-right,
.outer-container .outer-left,
.outer-container .outer-front,
.outer-container .outer-back {
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 200px;
border: 1px solid #fff;
opacity: 0.3;
transition: all 0.9s;
}

/* 全靠transform 属性撑起来了外面的六面体 */
.outer-top {
transform: rotateX(90deg) translateZ(100px);
}

.outer-bottom {
transform: rotateX(-90deg) translateZ(100px);
}

.outer-front {
transform: rotateY(0deg) translateZ(100px);
}

.outer-back {
transform: translateZ(-100px) rotateY(180deg);
}

.outer-left {
transform: rotateY(90deg) translateZ(100px);
}

.outer-right {
transform: rotateY(-90deg) translateZ(100px);
}

/* 鼠标悬停 外面的六面体,六条边都往外扩出去,并且透明度变浅了,猫猫图片更清晰了 */
.cube:hover .outer-top {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(90deg) translateZ(200px);
}

.container:hover .outer-bottom {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(-90deg) translateZ(200px);
}

.container:hover .outer-front {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(0deg) translateZ(200px);
}

.container:hover .outer-back {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: translateZ(-200px) rotateY(180deg);
}

.container:hover .outer-left {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(90deg) translateZ(200px);
}

.container:hover .outer-right {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(-90deg) translateZ(200px);
}
</style>


步骤三


弄里面的六面体,这个六边形比较简单,没有移入移出,鼠标悬停等的样式效果
效果如下:


cat3D.gif
代码如下:


<template>
<div>
<div class="container">
<!-- 外层立方体 -->
...
<!-- 内层立方体 -->
<div class="inner-container">
<div class="inner-top">
<img :src="cat7" />
</div>
<div class="inner-bottom">
<img :src="cat8" />
</div>
<div class="inner-front">
<img :src="cat9" />
</div>
<div class="inner-back">
<img :src="cat10" />
</div>
<div class="inner-left">
<img :src="cat11" />
</div>
<div class="inner-right">
<img :src="cat12" />
</div>
</div>
</div>
</div>
</template>

<script>
...
import cat7 from "@/assets/cats/cat7.png";
import cat8 from "@/assets/cats/cat8.png";
import cat9 from "@/assets/cats/cat9.png";
import cat10 from "@/assets/cats/cat10.png";
import cat11 from "@/assets/cats/cat11.png";
import cat12 from "@/assets/cats/cat12.png";

export default {
data() {
return {
...
cat7,
cat8,
cat9,
cat10,
cat11,
cat12,
};
},
};
</script>

<style lang="scss" scoped>
...
/* 设置图片可以在三维空间里展示 */
.container .outer-container,
.container .inner-container {
transform-style: preserve-3d;
}

...

/* 设置里面的六面体样式 */
/* 嵌套的内层立方体样式 */
.inner-container > div {
position: absolute;
top: 35px;
left: 35px;
width: 130px;
height: 130px;
}
/* 里面的六面体尺寸宽高设置130px */
.inner-container img {
width: 130px;
height: 130px;
}

.inner-top {
transform: rotateX(90deg) translateZ(65px);
}

.inner-bottom {
transform: rotateX(-90deg) translateZ(65px);
}

.inner-front {
transform: rotateY(0deg) translateZ(65px);
}

.inner-back {
transform: translateZ(-65px) rotateY(180deg);
}

.inner-left {
transform: rotateY(90deg) translateZ(65px);
}

.inner-right {
transform: rotateY(-90deg) translateZ(65px);
}
</style>

最后完整代码:


<template>
<div>
<div class="container">
<!-- 外层立方体 -->
<div class="outer-container">
<div class="outer-top">
<img :src="cat1" />
</div>
<div class="outer-bottom">
<img :src="cat2" />
</div>
<div class="outer-front">
<img :src="cat3" />
</div>
<div class="outer-back">
<img :src="cat4" />
</div>
<div class="outer-left">
<img :src="cat5" />
</div>
<div class="outer-right">
<img :src="cat6" />
</div>
</div>
<!-- 内层立方体 -->
<div class="inner-container">
<div class="inner-top">
<img :src="cat7" />
</div>
<div class="inner-bottom">
<img :src="cat8" />
</div>
<div class="inner-front">
<img :src="cat9" />
</div>
<div class="inner-back">
<img :src="cat10" />
</div>
<div class="inner-left">
<img :src="cat11" />
</div>
<div class="inner-right">
<img :src="cat12" />
</div>
</div>
</div>
</div>
</template>

<script>
import cat1 from "@/assets/cats/cat1.png";
import cat2 from "@/assets/cats/cat2.png";
import cat3 from "@/assets/cats/cat3.png";
import cat4 from "@/assets/cats/cat4.png";
import cat5 from "@/assets/cats/cat5.png";
import cat6 from "@/assets/cats/cat6.png";
import cat7 from "@/assets/cats/cat7.png";
import cat8 from "@/assets/cats/cat8.png";
import cat9 from "@/assets/cats/cat9.png";
import cat10 from "@/assets/cats/cat10.png";
import cat11 from "@/assets/cats/cat11.png";
import cat12 from "@/assets/cats/cat12.png";

export default {
data() {
return {
cat1,
cat2,
cat3,
cat4,
cat5,
cat6,
cat7,
cat8,
cat9,
cat10,
cat11,
cat12,
};
},
};
</script>

<style lang="scss" scoped>
* {
margin: 0px;
padding: 0px;
}

.container {
position: relative;
margin: 0px auto;
margin-top: 9%;
margin-left: 42%;
width: 200px;
height: 200px;
transform: rotateX(-30deg) rotateY(-80deg);
transform-style: preserve-3d;
animation: rotate 15s infinite;
/* 我这里用一个边框线来更容易看到效果 */
/* border: 1px solid red; */
}

/* 旋转立方体 */
@keyframes rotate {
from {
transform: rotateX(0deg) rotateY(0deg);
}

to {
transform: rotateX(360deg) rotateY(360deg);
}
}
/* 设置图片可以在三维空间里展示 */
.container .outer-container,
.container .inner-container {
transform-style: preserve-3d;
}
/* 设置图片尺寸固定宽高200px */
.outer-container img {
width: 200px;
height: 200px;
}

/* 外层立方体样式 */
.outer-container .outer-top,
.outer-container .outer-bottom,
.outer-container .outer-right,
.outer-container .outer-left,
.outer-container .outer-front,
.outer-container .outer-back {
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 200px;
border: 1px solid #fff;
opacity: 0.3;
transition: all 0.9s;
}

/* 全靠transform 属性撑起来了外面的六面体 */
.outer-top {
transform: rotateX(90deg) translateZ(100px);
}

.outer-bottom {
transform: rotateX(-90deg) translateZ(100px);
}

.outer-front {
transform: rotateY(0deg) translateZ(100px);
}

.outer-back {
transform: translateZ(-100px) rotateY(180deg);
}

.outer-left {
transform: rotateY(90deg) translateZ(100px);
}

.outer-right {
transform: rotateY(-90deg) translateZ(100px);
}

// 鼠标悬停 外面的六面体,六条边都往外扩出去,并且透明度变浅了,猫猫图片更清晰了
.cube:hover .outer-top {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(90deg) translateZ(200px);
}

.container:hover .outer-bottom {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(-90deg) translateZ(200px);
}

.container:hover .outer-front {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(0deg) translateZ(200px);
}

.container:hover .outer-back {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: translateZ(-200px) rotateY(180deg);
}

.container:hover .outer-left {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(90deg) translateZ(200px);
}

.container:hover .outer-right {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(-90deg) translateZ(200px);
}

/* 设置里面的六面体样式 */
/* 嵌套的内层立方体样式 */
.inner-container > div {
position: absolute;
top: 35px;
left: 35px;
width: 130px;
height: 130px;
}
/* 里面的六面体尺寸宽高设置130px */
.inner-container img {
width: 130px;
height: 130px;
}

.inner-top {
transform: rotateX(90deg) translateZ(65px);
}

.inner-bottom {
transform: rotateX(-90deg) translateZ(65px);
}

.inner-front {
transform: rotateY(0deg) translateZ(65px);
}

.inner-back {
transform: translateZ(-65px) rotateY(180deg);
}

.inner-left {
transform: rotateY(90deg) translateZ(65px);
}

.inner-right {
transform: rotateY(-90deg) translateZ(65px);
}
</style>


都看到这里了,求各位观众大佬们点个赞再走吧,你的赞对我非常重要



收起阅读 »

一款强大到没朋友的图片编辑插件,爱了爱了!

前言 最近用户提出了一个新的需求,老师可以批改学生的图片作业,需要对图片进行旋转、缩放、裁剪、涂鸦、标注、添加文本等。乍一听,又要掉不少头发。有没有功能强大的插件实现以上功能,让我有更多的时间去阻止女票双十一剁手呢?答案当然是有的。 效果展示涂鸦 裁剪 ...
继续阅读 »

前言


最近用户提出了一个新的需求,老师可以批改学生的图片作业,需要对图片进行旋转、缩放、裁剪、涂鸦、标注、添加文本等。乍一听,又要掉不少头发。有没有功能强大的插件实现以上功能,让我有更多的时间去阻止女票双十一剁手呢?答案当然是有的。


效果展示

涂鸦



涂鸦2.jpg


裁剪


裁剪.jpg


标注


标注2.jpg


旋转


旋转2.jpg


滤镜


1636088844(1).jpg


是不是很强大!还有众多功能我就不一一展示了。那么还等什么,跟我一起用起来吧~


安装


npm i tui-image-editor
// or
yarn add tui-image-editor

使用

快速体验



复制以下代码,将插件引入到自己的项目中。


<template>
<div class="drawing-container">
<div id="tui-image-editor"></div>
</div>

</template>
<script>
import "tui-image-editor/dist/tui-image-editor.css";
import "tui-color-picker/dist/tui-color-picker.css";
import ImageEditor from "tui-image-editor";
export default {
data() {
return {
instance: null,
};
},
mounted() {
this.init();
},
methods: {
init() {
this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);
document.getElementsByClassName("tui-image-editor-main")[0].style.top = "45px"; // 图片距顶部工具栏的距离
},
},
};
</script>


<style lang="scss" scoped>
.drawing-container {
height: 900px;
}
</style>


可以看到活生生的图片编辑工具就出现了,是不是很简单:


初始效果.jpg


国际化


由于是老外开发的,默认的文字描述都是英文,这里我们先汉化一下:


const locale_zh = {
ZoomIn: "放大",
ZoomOut: "缩小",
Hand: "手掌",
History: '历史',
Resize: '调整宽高',
Crop: "裁剪",
DeleteAll: "全部删除",
Delete: "删除",
Undo: "撤销",
Redo: "反撤销",
Reset: "重置",
Flip: "镜像",
Rotate: "旋转",
Draw: "画",
Shape: "形状标注",
Icon: "图标标注",
Text: "文字标注",
Mask: "遮罩",
Filter: "滤镜",
Bold: "加粗",
Italic: "斜体",
Underline: "下划线",
Left: "左对齐",
Center: "居中",
Right: "右对齐",
Color: "颜色",
"Text size": "字体大小",
Custom: "自定义",
Square: "正方形",
Apply: "应用",
Cancel: "取消",
"Flip X": "X 轴",
"Flip Y": "Y 轴",
Range: "区间",
Stroke: "描边",
Fill: "填充",
Circle: "圆",
Triangle: "三角",
Rectangle: "矩形",
Free: "曲线",
Straight: "直线",
Arrow: "箭头",
"Arrow-2": "箭头2",
"Arrow-3": "箭头3",
"Star-1": "星星1",
"Star-2": "星星2",
Polygon: "多边形",
Location: "定位",
Heart: "心形",
Bubble: "气泡",
"Custom icon": "自定义图标",
"Load Mask Image": "加载蒙层图片",
Grayscale: "灰度",
Blur: "模糊",
Sharpen: "锐化",
Emboss: "浮雕",
"Remove White": "除去白色",
Distance: "距离",
Brightness: "亮度",
Noise: "噪音",
"Color Filter": "彩色滤镜",
Sepia: "棕色",
Sepia2: "棕色2",
Invert: "负片",
Pixelate: "像素化",
Threshold: "阈值",
Tint: "色调",
Multiply: "正片叠底",
Blend: "混合色",
Width: "宽度",
Height: "高度",
"Lock Aspect Ratio": "锁定宽高比例",
};

this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);

效果如下:


汉化.jpg


自定义样式


默认风格为暗黑系,如果想改成白底,或者想改变按钮的大小、颜色等样式,可以使用自定义样式。


const customTheme = {
"common.bi.image": "", // 左上角logo图片
"common.bisize.width": "0px",
"common.bisize.height": "0px",
"common.backgroundImage": "none",
"common.backgroundColor": "#f3f4f6",
"common.border": "1px solid #333",

// header
"header.backgroundImage": "none",
"header.backgroundColor": "#f3f4f6",
"header.border": "0px",

// load button
"loadButton.backgroundColor": "#fff",
"loadButton.border": "1px solid #ddd",
"loadButton.color": "#222",
"loadButton.fontFamily": "NotoSans, sans-serif",
"loadButton.fontSize": "12px",
"loadButton.display": "none", // 隐藏

// download button
"downloadButton.backgroundColor": "#fdba3b",
"downloadButton.border": "1px solid #fdba3b",
"downloadButton.color": "#fff",
"downloadButton.fontFamily": "NotoSans, sans-serif",
"downloadButton.fontSize": "12px",
"downloadButton.display": "none", // 隐藏

// icons default
"menu.normalIcon.color": "#8a8a8a",
"menu.activeIcon.color": "#555555",
"menu.disabledIcon.color": "#ccc",
"menu.hoverIcon.color": "#e9e9e9",
"submenu.normalIcon.color": "#8a8a8a",
"submenu.activeIcon.color": "#e9e9e9",

"menu.iconSize.width": "24px",
"menu.iconSize.height": "24px",
"submenu.iconSize.width": "32px",
"submenu.iconSize.height": "32px",

// submenu primary color
"submenu.backgroundColor": "#1e1e1e",
"submenu.partition.color": "#858585",

// submenu labels
"submenu.normalLabel.color": "#858585",
"submenu.normalLabel.fontWeight": "lighter",
"submenu.activeLabel.color": "#fff",
"submenu.activeLabel.fontWeight": "lighter",

// checkbox style
"checkbox.border": "1px solid #ccc",
"checkbox.backgroundColor": "#fff",

// rango style
"range.pointer.color": "#fff",
"range.bar.color": "#666",
"range.subbar.color": "#d1d1d1",

"range.disabledPointer.color": "#414141",
"range.disabledBar.color": "#282828",
"range.disabledSubbar.color": "#414141",

"range.value.color": "#fff",
"range.value.fontWeight": "lighter",
"range.value.fontSize": "11px",
"range.value.border": "1px solid #353535",
"range.value.backgroundColor": "#151515",
"range.title.color": "#fff",
"range.title.fontWeight": "lighter",

// colorpicker style
"colorpicker.button.border": "1px solid #1e1e1e",
"colorpicker.title.color": "#fff",
};

this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
theme: customTheme, // 自定义样式
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);

效果如下:


自定义样式.jpg


按钮优化


通过自定义样式,我们看到右上角的 Load 和 Download 按钮已经被隐藏了,接下来我们再隐藏掉其他用不上的按钮(根据业务需要),并添加一个保存图片的按钮。


<template>
<div class="drawing-container">
<div id="tui-image-editor"></div>
<el-button class="save" type="primary" size="small" @click="save">保存</el-button>
</div>

</template>

// ...
methods: {
init() {
this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
menu: ["resize", "crop", "rotate", "draw", "shape", "icon", "text", "filter"], // 底部菜单按钮列表 隐藏镜像flip和遮罩mask
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
theme: customTheme, // 自定义样式
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);
document.getElementsByClassName("tui-image-editor-main")[0].style.top ="45px"; // 调整图片显示位置
document.getElementsByClassName("tie-btn-reset tui-image-editor-item help") [0].style.display = "none"; // 隐藏顶部重置按钮
},
// 保存图片,并上传
save() {
const base64String = this.instance.toDataURL(); // base64 文件
const data = window.atob(base64String.split(",")[1]);
const ia = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
ia[i] = data.charCodeAt(i);
}
const blob = new Blob([ia], { type: "image/png" }); // blob 文件
const form = new FormData();
form.append("image", blob);
// upload file
},
}

<style lang="scss" scoped>
.drawing-container {
height: 900px;
position: relative;
.save {
position: absolute;
right: 50px;
top: 15px;
}
}
</style>

效果如下:


按钮优化.jpg


可以看到顶部的重置按钮,以及底部的镜像和遮罩按钮都已经不见了。右上角多了一个我们自己的保存按钮,点击按钮,可以获取到 base64 文件和 blob 文件。


完整代码


<template>
<div class="drawing-container">
<div id="tui-image-editor"></div>
<el-button class="save" type="primary" size="small" @click="save">保存</el-button>
</div>

</template>
<script>
import 'tui-image-editor/dist/tui-image-editor.css'
import 'tui-color-picker/dist/tui-color-picker.css'
import ImageEditor from 'tui-image-editor'
const locale_zh = {
ZoomIn: '放大',
ZoomOut: '缩小',
Hand: '手掌',
History: '历史',
Resize: '调整宽高',
Crop: '裁剪',
DeleteAll: '全部删除',
Delete: '删除',
Undo: '撤销',
Redo: '反撤销',
Reset: '重置',
Flip: '镜像',
Rotate: '旋转',
Draw: '画',
Shape: '形状标注',
Icon: '图标标注',
Text: '文字标注',
Mask: '遮罩',
Filter: '滤镜',
Bold: '加粗',
Italic: '斜体',
Underline: '下划线',
Left: '左对齐',
Center: '居中',
Right: '右对齐',
Color: '颜色',
'Text size': '字体大小',
Custom: '自定义',
Square: '正方形',
Apply: '应用',
Cancel: '取消',
'Flip X': 'X 轴',
'Flip Y': 'Y 轴',
Range: '区间',
Stroke: '描边',
Fill: '填充',
Circle: '圆',
Triangle: '三角',
Rectangle: '矩形',
Free: '曲线',
Straight: '直线',
Arrow: '箭头',
'Arrow-2': '箭头2',
'Arrow-3': '箭头3',
'Star-1': '星星1',
'Star-2': '星星2',
Polygon: '多边形',
Location: '定位',
Heart: '心形',
Bubble: '气泡',
'Custom icon': '自定义图标',
'Load Mask Image': '加载蒙层图片',
Grayscale: '灰度',
Blur: '模糊',
Sharpen: '锐化',
Emboss: '浮雕',
'Remove White': '除去白色',
Distance: '距离',
Brightness: '亮度',
Noise: '噪音',
'Color Filter': '彩色滤镜',
Sepia: '棕色',
Sepia2: '棕色2',
Invert: '负片',
Pixelate: '像素化',
Threshold: '阈值',
Tint: '色调',
Multiply: '正片叠底',
Blend: '混合色',
Width: '宽度',
Height: '高度',
'Lock Aspect Ratio': '锁定宽高比例'
}

const customTheme = {
"common.bi.image": "", // 左上角logo图片
"common.bisize.width": "0px",
"common.bisize.height": "0px",
"common.backgroundImage": "none",
"common.backgroundColor": "#f3f4f6",
"common.border": "1px solid #333",

// header
"header.backgroundImage": "none",
"header.backgroundColor": "#f3f4f6",
"header.border": "0px",

// load button
"loadButton.backgroundColor": "#fff",
"loadButton.border": "1px solid #ddd",
"loadButton.color": "#222",
"loadButton.fontFamily": "NotoSans, sans-serif",
"loadButton.fontSize": "12px",
"loadButton.display": "none", // 隐藏

// download button
"downloadButton.backgroundColor": "#fdba3b",
"downloadButton.border": "1px solid #fdba3b",
"downloadButton.color": "#fff",
"downloadButton.fontFamily": "NotoSans, sans-serif",
"downloadButton.fontSize": "12px",
"downloadButton.display": "none", // 隐藏

// icons default
"menu.normalIcon.color": "#8a8a8a",
"menu.activeIcon.color": "#555555",
"menu.disabledIcon.color": "#ccc",
"menu.hoverIcon.color": "#e9e9e9",
"submenu.normalIcon.color": "#8a8a8a",
"submenu.activeIcon.color": "#e9e9e9",

"menu.iconSize.width": "24px",
"menu.iconSize.height": "24px",
"submenu.iconSize.width": "32px",
"submenu.iconSize.height": "32px",

// submenu primary color
"submenu.backgroundColor": "#1e1e1e",
"submenu.partition.color": "#858585",

// submenu labels
"submenu.normalLabel.color": "#858585",
"submenu.normalLabel.fontWeight": "lighter",
"submenu.activeLabel.color": "#fff",
"submenu.activeLabel.fontWeight": "lighter",

// checkbox style
"checkbox.border": "1px solid #ccc",
"checkbox.backgroundColor": "#fff",

// rango style
"range.pointer.color": "#fff",
"range.bar.color": "#666",
"range.subbar.color": "#d1d1d1",

"range.disabledPointer.color": "#414141",
"range.disabledBar.color": "#282828",
"range.disabledSubbar.color": "#414141",

"range.value.color": "#fff",
"range.value.fontWeight": "lighter",
"range.value.fontSize": "11px",
"range.value.border": "1px solid #353535",
"range.value.backgroundColor": "#151515",
"range.title.color": "#fff",
"range.title.fontWeight": "lighter",

// colorpicker style
"colorpicker.button.border": "1px solid #1e1e1e",
"colorpicker.title.color": "#fff",
};
export default {
data() {
return {
instance: null
}
},
mounted() {
this.init()
},
methods: {
init() {
this.instance = new ImageEditor(document.querySelector('#tui-image-editor'), {
includeUI: {
loadImage: {
path: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image',
name: 'image'
},
menu: ['resize', 'crop', 'rotate', 'draw', 'shape', 'icon', 'text', 'filter'], // 底部菜单按钮列表 隐藏镜像flip和遮罩mask
initMenu: 'draw', // 默认打开的菜单项
menuBarPosition: 'bottom', // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
theme: customTheme // 自定义样式
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600 // canvas 最大高度
})
document.getElementsByClassName('tui-image-editor-main')[0].style.top = '45px' // 调整图片显示位置
document.getElementsByClassName(
'tie-btn-reset tui-image-editor-item help'
)[0].style.display = 'none' // 隐藏顶部重置按钮
},
// 保存图片,并上传
save() {
const base64String = this.instance.toDataURL() // base64 文件
const data = window.atob(base64String.split(',')[1])
const ia = new Uint8Array(data.length)
for (let i = 0; i < data.length; i++) {
ia[i] = data.charCodeAt(i)
}
const blob = new Blob([ia], { type: 'image/png' }) // blob 文件
const form = new FormData()
form.append('image', blob)
// upload file
}
}
}
</script>


<style lang="scss" scoped>
.drawing-container {
height: 900px;
position: relative;
.save {
position: absolute;
right: 50px;
top: 15px;
}
}
</style>


总结


以上就是 tui.image-editor 的基本使用方法,相比其他插件,tui.image-editor 的优势是功能强大,简单易上手。


插件固然好用,但本人也发现一个小 bug,当放大图片,用手掌拖动显示位置,再点击重置按钮时,图片很可能就消失不见了。解决办法有两个,一是改源码,在重置之前,先调用 resetZoom 方法,还原缩放比列;二是自己做一个重置按钮,点击之后调用 this.init 方法重新进行渲染。



收起阅读 »

超详细讲解页面加载过程

说一说从输入URL到页面呈现发生了什么?(知识点) ❝ 这个题可以说是面试最常见也是一道可以无限难的题了,一般面试官出这道题就是为了考察你的前端知识的深度与广度。 ❞ 1.浏览器接受URL开启网络请求线程(涉及到:浏览器机制,线程与进程等) 2.开启网络线...
继续阅读 »

说一说从输入URL到页面呈现发生了什么?(知识点)




这个题可以说是面试最常见也是一道可以无限难的题了,一般面试官出这道题就是为了考察你的前端知识的深度与广度。




1.浏览器接受URL开启网络请求线程(涉及到:浏览器机制,线程与进程等)


2.开启网络线程到发出一个完整的http请求(涉及到:DNS解析,TCP/IP请求,5层网络协议等)


3.从服务器接收到请求到对应后台接受到请求(涉及到:负载均衡,安全拦截,后台内部处理等)


4.后台与前台的http交互(涉及到:http头,响应码,报文结构,cookie等)


5.缓存问题(涉及到:http强缓存与协商缓存等)(请看上一篇文章[这些浏览器面试题,看看你能回答几个?](juejin.cn/post/702653…


6.浏览器接受到http数据包后的解析流程(涉及到html词法分析,解析成DOM树,解析CSS生成CSSOM树,合并生成render渲染树。然后layout布局,painting渲染,复合图层合成,GPU绘制,等)


在浏览器地址栏输入URL


当我们在浏览器地址栏输入URL地址后,浏览器会开一个线程来对我们输入的URL进行解析处理。


浏览器中的各个进程及作用:(多进程)



  • 浏览器进程:负责管理标签页的创建销毁以及页面的显示,资源下载等。

  • 第三方插件进程:负责管理第三方插件。

  • GPU进程:负责3D绘制与硬件加速(最多一个)。

  • 渲染进程:负责页面文档解析(HTML,CSS,JS),执行与渲染。(可以有多个)


DNS域名解析


为什么需要DNS域名解析?


因为我们在浏览器中输入的URL通常是一个域名,并不会直接去输入IP地址(纯粹因为域名比IP好记),但我们的计算机并不认识域名,它只知道IP,所以就需要这一步操作将域名解析成IP。


URL组成部分



  • protocol:协议头,比如http,https,ftp等;

  • host:主机域名或者IP地址;

  • port:端口号;

  • path:目录路径;

  • query:查询的参数;

  • hash:#后边的hash值,用来定位某一个位置。


解析过程



  • 首先会查看浏览器DNS缓存,有的话直接使用浏览器缓存

  • 没有的话就查询计算机本地DNS缓存(localhost)

  • 还没有就询问递归式DNS服务器(就是网络提供商,一般这个服务器都会有自己的缓存)

  • 如果依然没有缓存,那就需要通过 根域名服务器 和TLD域名服务器 再到对应的 权威DNS服务器 找记录,并缓存到 递归式服务器,然后 递归服务器 再将记录返回给本地


「⚠️注意:」




DNS解析是非常耗时的,如果页面中需要解析的域名过多,是非常影响页面性能的。考虑使用dns与加载或减少DNS解析进行优化。




发送HTTP请求


拿到了IP地址后,就可以发起HTTP请求了。HTTP请求的本质就是TCP/IP的请求构建。建立连接时需要**「3次握手」进行验证,断开链接也同样需要「4次挥手」**进行验证,保证传输的可靠性


3次握手



  • 第一次握手:客户端发送位码为 SYN = 1(SYN 标志位置位),随机产生初始序列号 Seq = J 的数据包到服务器。服务器由 SYN = 1(置位)知道,客户端要求建立联机。

  • 第二次握手:服务器收到请求后要确认联机信息,向客户端发送确认号Ack = (客户端的Seq +1,J+1),SYN = 1,ACK = 1(SYN,ACK 标志位置位),随机产生的序列号 Seq = K 的数据包。

  • 第三次握手:客户端收到后检查 Ack 是否正确,即第一次发送的 Seq +1(J+1),以及位码ACK是否为1。若正确,客户端会再发送 Ack = (服务器端的Seq+1,K+1),ACK = 1,以及序号Seq为服务器确认号J 的确认包。服务器收到后确认之前发送的 Seq(K+1) 值与 ACK= 1 (ACK置位)则连接建立成功。


3次握手.gif


「直白理解:」


(客户端:hello,你是server么?服务端:hello,我是server,你是client么 客户端:yes,我是client 建立成功之后,接下来就是正式传输数据。)


4次挥手



  • 客户端发送一个FIN Seq = M(FIN置位,序号为M)包,用来关闭客户端到服务器端的数据传送。

  • 服务器端收到这个FIN,它发回一个ACK,确认序号Ack 为收到的序号M+1。

  • 服务器端关闭与客户端的连接,发送一个FIN Seq = N 给客户端。

  • 客户端发回ACK 报文确认,确认序号Ack 为收到的序号N+1。


4次挥手.gif


「直白理解:」


(主动方:我已经关闭了向你那边的主动通道了,只能被动接收了 被动方:收到通道关闭的信息 被动方:那我也告诉你,我这边向你的主动通道也关闭了 主动方:最后收到数据,之后双方无法通信)


五层网络协议


1、应用层(DNS,HTTP):DNS解析成IP并发送http请求;


2、传输层(TCP,UDP):建立TCP连接(3次握手);


3、网络层(IP,ARP):IP寻址;


4、数据链路层(PPP):封装成帧;


5、物理层(利用物理介质传输比特流):物理传输(通过双绞线,电磁波等各种介质)。


「OSI七层框架:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层」


服务器接收请求做出响应


HTTP 请求到达服务器,服务器进行对应的处理。 最后要把数据传给浏览器,也就是返回网络响应。


跟请求部分类似,网络响应具有三个部分:响应行、响应头和响应体。


响应完成之后怎么办?TCP 连接就断开了吗?


不一定。这时候要判断Connection字段, 如果请求头或响应头中包含Connection: Keep-Alive, 表示建立了持久连接,这样TCP连接会一直保持,之后请求统一站点的资源会复用这个连接。否则断开TCP连接, 请求-响应流程结束。


状态码


状态码是由3位数组成,第一个数字定义了响应的类别,且有五种可能取值:



  • 1xx:指示信息–表示请求已接收,继续处理。

  • 2xx:成功–表示请求已被成功接收、理解、接受。

  • 3xx:重定向–要完成请求必须进行更进一步的操作。

  • 4xx:客户端错误–请求有语法错误或请求无法实现。

  • 5xx:服务器端错误–服务器未能实现合法的请求。 平时遇到比较常见的状态码有:200, 204, 301, 302, 304, 400, 401, 403, 404, 422, 500(分别表示什么请自行查找)。


服务器返回相应文件


请求成功后,服务器会返回相应的网页,浏览器接收到响应成功的报文后便开始下载网页,至此,网络通信结束。


浏览器解析渲染页面


浏览器在接收到HTML,CSS,JS文件之后,它是如何将页面渲染在屏幕上的?


render.png


解析HTML构建DOM Tree


浏览器在拿到服务器返回的网页之后,首先会根据顶部定义的DTD类型进行对应的解析,解析过程将被交给内部的GUI渲染线程来处理。


「DTD(Document Type Definition)文档类型定义」


常见的文档类型定义


//HTML5文档定义
<!DOCTYPE html>
//用于XHTML 4.0 的严格型 
<!DOCTYPE HTMLPUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> 
//用于XHTML 4.0 的过渡型 
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 
//用于XHTML 1.0 的严格型 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
//用于XHTML 1.0 的过渡型
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

HTML解释器的工作就是将网络或者本地磁盘获取的HTML网页或资源从字节流解释成DOM树🌲结构


HTML解释器.png


通过上图可以清楚的了解这一过程:首先是字节流,经过解码之后是字符流,然后通过词法分析器会被解释成词语(Tokens),之后经过语法分析器构建成节点,最后这些节点被组建成一颗 DOM 树。


对于线程化的解释器,字符流后的整个解释、布局和渲染过程基本会交给一个单独的渲染线程来管理(不是绝对的)。由于 DOM 树只能在渲染线程上创建和访问,所以构建 DOM 树的过程只能在渲染线程中进行。但是,从字符串到词语这个阶段可以交给单独的线程来做,Chrome 浏览器使用的就是这个思想。在解释成词语之后,Webkit 会分批次将结果词语传递回渲染线程。


这个过程中,如果遇到的节点是 JS 代码,就会调用 JS引擎 对 JS代码进行解释执行,此时由于 JS引擎GUI渲染线程 的互斥,GUI渲染线程 就会被挂起,渲染过程停止,如果 JS 代码的运行中对DOM树进行了修改,那么DOM的构建需要从新开始


如果节点需要依赖其他资源,图片/CSS等等,就会调用网络模块的资源加载器来加载它们,它们是异步的,不会阻塞当前DOM树的构建


如果遇到的是 JS 资源URL(没有标记异步),则需要停止当前DOM的构建,直到 JS 的资源加载并被 JS引擎 执行后才继续构建DOM


解析CSS构建CSSOM Tree


CSS解释器会将CSS文件解释成内部表示结构,生成CSS规则树,这个过程也是和DOM解析类似的,CSS 字节转换成字符,接着词法解析与法解析,最后构成 CSS对象模型(CSSOM) 的树结构


构建渲染树(Render Tree)


DOM TreeCSSOM Tree都构建完毕后,接着将它们合并成渲染树(Render Tree)渲染树 只包含渲染网页所需的节点,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上。


渲染(布局,绘制,合成)



  • 计算CSS样式 ;

  • 构建渲染树 ;

  • 布局,主要定位坐标和大小,是否换行,各种position overflow z-index属性 ;

  • 绘制,将图像绘制出来。


这个过程比较复杂,涉及到两个概念: reflow(回流)和repain(重绘)。DOM节点中的各个元素都是以盒模型的形式存在,这些都需要浏览器去计算其位置和大小等,这个过程称为relow;当盒模型的位置,大小以及其他属性,如颜色,字体,等确定下来之后,浏览器便开始绘制内容,这个过程称为repain。页面在首次加载时必然会经历reflow和repain。reflow和repain过程是非常消耗性能的,尤其是在移动设备上,它会破坏用户体验,有时会造成页面卡顿。所以我们应该尽可能少的减少reflow和repain。


这里Reflow和Repaint的概念是有区别的:


(1)Reflow:即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树。


(2)Repaint:即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了。


回流的成本开销要高于重绘,而且一个节点的回流往往回导致子节点以及同级节点的回流, 所以优化方案中一般都包括,尽量避免回流。


「回流一定导致重绘,但重绘不一定会导致回流」


「合成(composite)」


最后一步合成( composite ),这一步骤浏览器会将各层信息发送给GPU,GPU将各层合成,显示在屏幕上


普通图层和复合图层


可以简单的这样理解,浏览器渲染的图层一般包含两大类:普通图层以及复合图层


首先,普通文档流内可以理解为一个复合图层(这里称为默认复合层,里面不管添加多少元素,其实都是在同一个复合图层中)


其次,absolute布局(fixed也一样),虽然可以脱离普通文档流,但它仍然属于默认复合层


然后,可以通过硬件加速的方式,声明一个新的复合图层,它会单独分配资源 (当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘)


可以简单理解下:「GPU中,各个复合图层是单独绘制的,所以互不影响」,这也是为什么某些场景硬件加速效果一级棒


可以Chrome源码调试 -> More Tools -> Rendering -> Layer borders中看到,黄色的就是复合图层信息。



收起阅读 »

VS Code settings.json 10 个高(装)阶(杯)配置!

1. 隐藏活动栏 VS Code 左侧图标列表是“活动栏”,我们可以点击图标跳转到各个模块,我们可以通过配置 workbench.activityBar.visible 来控制活动栏的显示; 如果你想恢复显示,可以自定义快捷键来再次显示这块空间; 如何设置...
继续阅读 »

1. 隐藏活动栏


VS Code 左侧图标列表是“活动栏”,我们可以点击图标跳转到各个模块,我们可以通过配置 workbench.activityBar.visible 来控制活动栏的显示;


image.png


如果你想恢复显示,可以自定义快捷键来再次显示这块空间;


image.png


如何设置快捷键:keybindings


我们可以用 Ctrl+B 来隐藏/显示文件资源管理器,用 Ctrl+Alt+B 来隐藏/显示活动栏;


虽然,你也可以在命令面板 Ctrl+Shift+P 中搜索,不过使用快捷键就更有装杯效果~


活动栏在隐藏状态下,我们也可以通过快捷键跳转到不同的工作空间,比如 Ctrl+Shift+E(跳转到文件资源管理器)、Ctrl+Shift+X(跳转到扩展)、Ctrl+Shift+H(搜索和替换)等


2. AI 编码


GitHub Copilot 是 VS Code 的一个扩展,可在你编写代码时生成片段代码;


由于它是人工智能、机器学习,有可能会产生一些你不喜欢的代码,但是请别仇视它,毕竟 AI 编码是未来趋势!


image.png


处于隐私考虑,建议不要在工作中使用 Copilot,但是可以在个人项目中使用它,有趣又有用,尤其是对于单元测试;


可以在 settings.json 中配置 Copilot;


3. 字体与缩放


这个不多做解释,根据自己的需求进行文字大小及缩放比例的配置;


image.png


当然,你不一定要在 settings.json 中去编写这个配置,也可以在可选项及输入配置窗口进行配置。


4. 无拖拽/删除确认


如果你对自己的编程技能足够自信,或者对 VS Code 的 Ctrl+Z 足够自信,你可以配置取消删除确认;因为拖拽/删除确认有时也会干扰思路~


image.png


image.png


5. 自更新绝对路径


VS Code 的最佳功能之一是它的文件导入很友善,使用绝对路径,例如:@/components/Button../../Button 更让人舒适;


当移动文件重新组织目录时,希望 VS Code 能自动更新文件的路径?你可以配置它们:


image.png


请注意,您需要在 .tsconfig/.jsconfig 文件中配置路径才能使用绝对路径导入。


6. 保存执行


配置过 ESLint 保存修正的应该都知道这个配置。这个非常强大,出了 fixAll,还能 addMissingImports 补充缺少的 Imports,或者其它你想在保存后执行的行为;


image.png


这个配置就像是编程魔法~


7. CSS 格式化


你可能已经在使用 Stylelint 了,如果没有,请在配置中设置它!


image.png


另一个设置是 editor.suggest.insertMode,当设置为“replace”时,意味着——当你选择一个提示并按 Tab 或 Enter 时,将替换整个文本为提示,这非常有用。


8. 开启 Emmet


你可能熟悉 Emmet —— Web 开发人员必备工具包,如果没有,请设置它;虽然它内置于 VS Code,但必须手动配置启用;


image.png


9. Tailwind CSS


Tailwind CSS 是一个功能类优先的 CSS 框架,它集成了诸如 flexpt-4text-center 和 rotate-90 这样的的类,它们能直接在脚本标记语言中组合起来,构建出任何设计。


虽然它目前尚未内置在 VS Code 中,但可作为免费的 VS Code 扩展进行安装使用,还可以配置附加设置增强它的功能!


image.png


10. 单击打开文件


VS Code 默认用户界面,有个奇怪的现象,它需要双击才能从文件资源管理器中打开文件。


单击一下得到的是奇怪的“预览”模式,当你单击下一个文件时,第一个文件就会消失。这就像只有一个标签。


image.png


需要进行这个配置,关闭后,单击将在新选项卡中打开文件。问题解决了~


将配置用 Settings Sync 进行同步,去哪都能个性化、自定义!酷的!


image.png




以上就是本篇分享,你有啥压箱底的 VS Code-settings.json 配置吗?欢迎评论留言,分享交流 (#^.^#)



收起阅读 »

总结 scripts 阻塞 HTML 解析

看了一些类似文章,里面有这样一些表述:解析 HTML,DOM 解析等;我们统一下表述:下载完 HTML 文件后,浏览器会解析 HTML,目的是为了构建 DOM 结构 或 生成 DOM 树。 内联 scripts <html> <head&...
继续阅读 »

看了一些类似文章,里面有这样一些表述:解析 HTML,DOM 解析等;我们统一下表述:下载完 HTML 文件后,浏览器会解析 HTML,目的是为了构建 DOM 结构 或 生成 DOM 树


内联 scripts


<html>
<head></head>
<body>
 <script>
console.log('irene')
 </script>
</body>
</html>

解析 HTML 过程中遇到 内联 scripts 会暂停解析,先执行 scripts,然后继续解析 HTML。


普通外联 scripts


<script src="index.js"></script>

解析 HTML 过程中遇到 普通外联 scripts 会暂停解析,发送请求并执行 scripts,然后继续解析 HTML。如下图所示,绿色表示 HTML 解析;灰色表示 HTML 解析暂停;蓝色表示 scripts 下载;粉色表示 scripts 执行。


image.png


defer scripts


<script defer src="index.js"></script>

解析 HTML 过程中遇到 defer scripts 不会停止解析,scripts 也会并行下载;等整个 HTML 解析完成后按引用 scripts 的顺序执行。defer scripts 在 DOMContentLoaded 事件触发之前执行。defer 属性只能用于外联 scripts。


image.png
浏览器并行下载多个 defer scripts,文件小的 scripts 很可能先下载完,defer 属性除了告诉浏览器不去阻塞 HTML 解析,同时还保证了defer scripts 的相对顺序。即使 small.js 先下载完,它还是得等到 long.js 执行完再去执行。


async scripts


<script async src="index.js"></script>

解析 HTML 过程中遇到 async scripts 不会停止解析,scripts 也会并行下载;scripts 下载完之后开始执行,阻塞 HTML 解析。async scripts 的执行顺序和它的引用顺序不一定相同。async scripts 可能在 DOMContentLoaded 事件触发之前或之后执行。如果 HTML 先解析完 async scripts 才下载完成,此时 DOMContentLoaded 事件已经触发, async scripts 很有可能来不及监听 DOMContentLoaded 事件。async 属性只能用于外联 scripts。


image.png
浏览器并行下载多个 async scripts,文件小的 scripts 很可能先下载完,先下载完就先执行了,它无法保证按 async scripts 的引用顺序执行。


defer VS async


在实践中,defer 用于需要整个 DOM 或其相对执行顺序很重要的 scripts。而 async 则用于独立的 scripts,如计数器或广告,而它们的相对执行顺序并不重要。


dynamic scripts


let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script);

脚本一旦被 append 到文档中就开始下载,动态脚本在默认情况下表现的像 async scripts,即先下载完先执行;可以显示设置 script.async = false,这样 scripts 的执行顺序就会和 defer scripts 表现的一致。


这两篇文章中,文一说 defer scripts 会阻塞 HTML 解析,文二说 defer scripts 不会阻塞 HTML 解析。其实两者的想法是一致的:即 defer scripts 的下载不会阻塞 HTML 解析,且执行是在构建完 DOM 之后;之所以有两种不同的表述是因为文一定义阻塞 HTML 解析的标准:是否在 DOMContentLoaded 之前执行,在之前执行就是阻塞 HTML 解析,否则就是不会;defer scripts 是在构建完 DOM 之后,DOMContentLoaded 之前执行的,所有文一认为 defer scripts 会阻塞 HTML 解析。文二说 defer scripts 不会阻塞 HTML 解析就很好理解了。


作者:小被子
链接:https://juejin.cn/post/7027673904927735822

收起阅读 »

手把手教你封装一个日期格式化的工具函数

最近还是在做那个练习的小项目,做完接收数据并渲染到页面上的时候,发现后端小伙伴又在给我找活干了欸,单纯的渲染这当然是小kiss啦,可这个字段是个什么东西? "createTime" : "2021-01-17T13:32:06.381Z", "lastLogi...
继续阅读 »

最近还是在做那个练习的小项目,做完接收数据并渲染到页面上的时候,发现后端小伙伴又在给我找活干了欸,单纯的渲染这当然是小kiss啦,可这个字段是个什么东西?


"createTime" : "2021-01-17T13:32:06.381Z",
"lastLoginTime" : "2021-01-17T13:32:06.381Z"

直接CV到百度,查出来这一串是一种时间格式,下面放上它的解释:



T表示分隔符,Z表示的是UTC.
UTC:世界标准时间,在标准时间上加上8小时,即东八区时间,也就是北京时间。


另:还有别的时间格式和时间戳,想了解的小伙伴可以百度了解一下哦,免得跟我一样,看到了才想着去百度了解,事先了解一下,没坏处的。



了解完了,现在我应该做的,就是将这个时间变成我们大家看得懂的那种格式,并将它渲染到页面上。


开始上手


JavaScript中,处理日期和时间,当然要用到我们的Date对象,所以我们先来写出这个函数的雏形:


const formateDate = (value)=>{
let date = new Date(value)
}

下面要做的应该是定义日期的格式了,这里我用的是yyyy-MM-dd hh:mm:ss


let fmt = 'yyyy-MM-dd hh:mm:ss'

因为年月日时分秒这里都是两位或者两位以上的,所以在获取的时候我是这样定义的:


const o = {
'y+': date.getFullYear(),
'M+': date.getMonth()+1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}

首先先解释一下getMonth()+1,去查看Date文档就知道,这个函数的返回是0-11,我们正常月份都是1-12,所以加上1,才是正确的月份。


定义了规则之后,我们循环它,应该就可以得到我们想要的结果了吧。


for(let k in o ){
if(new RegExp(`(${k})`).test(fmt)){
const val = o[k] + ''
fmt = fmt.replace(RegExp.$1,val)
}
}

我们继续来解释一下代码,首先fmt.replace是代表我们要做一个替换,RegExp.$1就是获取到上面的值表达式内容,将这个内容,换成val中的值,之所以上面加了一个空字符串,是为了将val变成字符串的形式,以防再出纰漏。


$1.png


我们渲染上去,看看结果如何?


秒未补零.png


日期被我们成功的转化为了,我们能看得懂的东西,但是我们可以看到,秒这里,只有一位,也就是说,在秒只有个位数的情况下,我们应该给予它一个补零的操作。



不光是秒,其他也应该是这个道理哦!



关于补零


补零的话,有两种方式,先来说说笨笨的这种吧:


我们去判断这个字符串的长度,如果是1,我们就加个零,如果不是1,那么就不用加。


var a = '6'
a.length = 1?'0'+a:a // '06'

再来说个略微比这个高级一点的:


我们需要两位,所以直接给字符串补上两个零,再用substr去分割一下字符串,就能得到我们想要的了。


var b = '6'
var result = ('00'+b).substr(b.length) // '06'

那么我们去改一下上面的代码,就得到了下面的函数:


const formateDate = (value)=>{
let date = new Date(value)
let fmt = 'yyyy-MM-dd hh:mm:ss'
const o = {
'y+': date.getFullYear(),
'M+': date.getMonth()+1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}
for(let k in o ){
if(new RegExp(`(${k})`).test(fmt)){
const val = o[k] + ''
fmt = fmt.replace(RegExp.$1,RegExp.$1.length==1?val:('00'+val).substr(val.length))
}
}
return fmt
}

在刷新一下网页,看看我们成功了没!


补零结束.png


成功是成功了,但是我们发现,前面的年竟然被干掉了,他也变成了两位的样子,这可不行啊,我们定义的年份格式可是四位的。


这可咋整.webp


但是别慌,这个只需要把年份单独的去做判断,不与其他2位的格式一起进行操作就能解决啦,所以我们最终的函数是这样的:


const formateDate = (value)=>{
let date = new Date(value)
let fmt = 'yyyy-MM-dd hh:mm:ss'
if(/(y+)/.test(fmt)){
fmt = fmt.replace(RegExp.$1,date.getFullYear())
}
const o = {
'M+': date.getMonth()+1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}
for(let k in o ){
if(new RegExp(`(${k})`).test(fmt)){
const val = o[k] + ''
fmt = fmt.replace(RegExp.$1,RegExp.$1.length==1?val:('00'+val).substr(val.length))
}
}
return fmt
}

看一下结果吧:


image.png


如果再严谨一点,可以再给函数加个参数,传递一个rule,这样方便我们后期进行调整数据格式,在定义格式的时候用||就好了。


let fmt = '传入的新格式' || '默认的格式
收起阅读 »

用CSS告诉你为何大橘为重!!

本期我们通过vite+scss去完成一个橘猫心情变化的创意动画,这里的逻辑我们将不使用任何js代码,仅依靠css来完成,所以,通过本期的动画,你可以到一些css动画和绘制的一些技巧。废话不多说,先康康效果~ 还比较可爱吧。当鼠标(鱼)移入出,橘子闷闷不乐,无...
继续阅读 »

本期我们通过vite+scss去完成一个橘猫心情变化的创意动画,这里的逻辑我们将不使用任何js代码,仅依靠css来完成,所以,通过本期的动画,你可以到一些css动画和绘制的一些技巧。废话不多说,先康康效果~


VID_20211030_184225.gif


还比较可爱吧。当鼠标(鱼)移入出,橘子闷闷不乐,无精打采的。但当鼠标(鱼)移入,橘子一看见最喜欢的鱼立马就开心了,连天气都变好了,对,这只橘子就是这么馋,变成胖橘是有原因的。


好了,我们马上就要进入正文了,我们会从基础搭建,太阳,云,猫的绘制和动画去了解制作这个动画的流程。


正文


1.搭建与结构


yarn add vite sass sass-loader

我们是用vite和sass去完成项目的构建,和样式的书写,所以我们先安装下他们。


<div id="app">
<div class="warrper">
<div class="sun"></div>
<div class="cloud"></div>
<div class="cat">
<div class="eye left"><div class="eye-hide"></div></div>
<div class="eye right"><div class="eye-hide"></div></div>
<div class="nose"></div>
<div class="mouth"></div>
</div>
</div>
</div>

在html我们先写出结构来。div#app作为主界面去填满一屏,而div.warrper就作为主要内容的展示区域也就是那个圆圈。然后,在圆圈里面我们放太阳div.sun,云朵div.cloud,猫div.cat,当然猫里面还有眼睛鼻子嘴巴这些,至于猫的耳朵就用两个伪类做个三角形去实现。


2.变量与界面


$cat:rgb(252, 180, 125);

:root{
--bgColor:rgb(81, 136, 168);
--eyeHideTop:0px;
--cloudLeft:45%;
--mouthRadius:10px 10px 0 0;
}
#app{
width: 100%;
height: 100vh;
position: relative;
display: flex;
justify-content: center;
align-items: center;
background-image: repeating-linear-gradient(0deg, hsla(340,87%,75%,0.2) 0px, hsla(340,87%,75%,0.2) 30px,transparent 30px, transparent 60px),repeating-linear-gradient(90deg, hsla(340,87%,75%,0.2) 0px, hsla(340,87%,75%,0.2) 30px,transparent 30px, transparent 60px),linear-gradient(90deg, rgb(255,255,255),rgb(255,255,255));
}

.warrper{
width: 320px;
height: 320px;
border-radius: 50%;
border: 10px solid white;
position: relative;
overflow: hidden;
background-color: var(--bgColor);
transition: background-color 1s linear;
cursor:url("./assets/fish.png"),default;
&:hover{
--bgColor:rgb(178, 222, 247);
--eyeHideTop:-20px;
--cloudLeft:100%;
--mouthRadius:0 0 10px 10px;
}
}

我们先定义猫的主色调,还有一些要变化的颜色和距离,因为我们移入将通过css3去改变这些属性,来达到某些动画的实现。


我们期望的是,当鼠标移入圆圈后,天空变晴,云朵退散,猫开心充满精神,所以,bgColor:天空颜色,eyeHideTop猫的眼皮y轴距离,cloudLeft云朵x轴偏移距离,mouthRadius猫嘴巴的圆角值。目前来说,当鼠标移入div.warrper后,这些值都会发生变化。另外,我自定义了鼠标图标移入圆圈变成了一条鱼(即cursor:url(图片地址))。这里的hover后的值是我事先算好的,如果大家重新开发别的动画可以一边做一边算。


微信截图_20211030200310.png


3.太阳与云朵


.sun{
width: 50px;
height: 50px;
position: absolute;
background-color: rgb(255, 229, 142);
border:7px solid rgb(253, 215, 91);
border-radius: 50%;
left: 55%;
top: 14%;
box-shadow: 0 0 6px rgb(255, 241, 48);
}

太阳我们就画个圆圈定好位置,然后用box-shadow投影去完成一点发光的效果。


微信截图_20211030200343.png


然后,我们再开始画云朵~


.cloud{
width: 100px;
height: 36px;
background-color: white;
position: absolute;
transition: left .6s linear;
left: var(--cloudLeft);
top: 23%;
border-radius: 36px;
animation: bouncy 2s ease-in-out infinite;
&::before{
content: '';
width: 50px;
height: 50px;
background-color: white;
border-radius: 50%;
position: absolute;
top: -23px;
left: 18px;
}
&::after{
content: '';
width: 26px;
height: 26px;
background-color: white;
border-radius: 50%;
position: absolute;
top: -16px;
left: 56px;
}
}

@keyframes bouncy {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}

云朵很简单,我们就是画一个圆角矩形,然后用两个伪类画一个大圆和小圆叠在一起就非常像云了,另外,我们再加个animation动画,让他时大时小,有动的感觉。


微信截图_20211030200357.png


4.橘猫与动画


.cat{
width: 180px;
height: 160px;
background-color: $cat;
position: absolute;
bottom: -20px;
left: 50%;
margin-left: -90px;
animation: wait 2s ease-in-out infinite;
&::after,
&::before{
content: '';
display: block;
border-style: solid;
border-width: 20px 30px;
position: absolute;
top: -30px;
}
&::after{
right: 0;
border-color: transparent $cat $cat transparent;
}
&::before{
left: 0;
border-color: transparent transparent $cat $cat;
}
.eye{
width: 42px;
height: 42px;
border-radius: 50%;
position: absolute;
top: 30px;
background:white;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
.eye-hide{
height: 20px;
position: absolute;
top: var(--eyeHideTop);
left: -2px;
right:-2px;
background-color: $cat;
transition: top .5s ease-in-out;
z-index: 2;
}
&::before{
content: "";
height: 36px;
width: 36px;
background-color:black;
border-radius: 50%;
}
&::after{
content: "";
width: 24px;
height: 24px;
background-color: white;
border-radius: 50%;
position: absolute;
right: 0px;
top: 0px;
}
&.left{
left: 24px;
}
&.right{
right: 24px;
}
}
.nose{
width: 0;
height: 0;
border-top: 7px solid rgb(248, 226, 226);
border-left: 7px solid transparent;
border-right: 7px solid transparent;
position: absolute;
left: 50%;
margin-left: -7px;
top: 70px;
}
.mouth{
width: 26px;
height: 20px;
background-color: rgb(255, 217, 217);
position: absolute;
top: 85px;
left: 50%;
margin-left: -13px;
border-radius: var(--mouthRadius);
transition: border-radius .2s linear;
overflow: hidden;
&::after,
&::before{
content: "";
position: absolute;
display: block;
top: 0;
border-top: 7px solid white;
border-left: 2px solid transparent;
border-right: 2px solid transparent;
}
&::after{
right: 5px;
}
&::before{
left: 5px;
}
}
}

@keyframes wait{
0% {
bottom: -20px;
}
50% {
bottom: -25px;
}
100% {
bottom: -20px;
}
}

我们可以实现分解出,耳朵(伪类)+ 一双眼睛 + 鼻子 + 嘴(包含两颗尖牙) = 猫。


通过以上代码就不难看出主要都是在使用绝对定位来完成,面部器官的摆放。绝大部分都是css基础代码来实现的。唯一可以注意的点,就是耳朵这个三角形,我们是通过伪类实现,将它不设置宽高,而主是通过border-width+boder-color这个技巧去绘制出三角形的,算是个css小技巧吧,后面的鼻子和嘴巴里的尖牙都是这个小技巧来实现的。


另外,还要说的是那双眼睛,我们用先填充白底再分别用伪类去实现里面的黑底圆和白色小圆,肯定有同学问了为什么不用border是实现白色圆框,就不用浪费一个伪类去完成黑底圆了?因为我们用了overflow: hidden,他多余隐藏的内容是border以下的元素,而border边框可以无损,那么他的伪类能盖不住他的border,这样显得眼皮垂下的圆圈还是很大不自然,所以我们又造了一个伪类去实现他的黑底,让外圆不使用border了。


剩下的就是做一个等待的animation动画给猫,让他上下移动着,来实现不停的呼吸的效果。


微信截图_20211030200539.png


这样一直无精打采的橘猫就完成了。因为在第一部分,我们事先已经把移入后改变的变量算好了,现在把鼠标移入,效果就出现咯~


微信截图_20211030200546.png


结语


讲到这里我们就已经完成了这个动画了,不得不说,看见食物这么激动不愧都叫他胖橘!


这里有我这个动画【I Like Fish】codepen地址可以看到演示和代码,有兴趣的小伙伴可以康康。


本期还是比较侧重基础和动画创意的,主要是新手向,大佬勿喷,经常用css写写动画挺有意思的,不仅可以熟悉基本功,而且会迸发出很多创意来,也是一种锻炼自己的学习方式吧,多练习下,大家一起加油鸭~



收起阅读 »

你需要知道的 19 个 console 实用调试技巧

众所周知,浏览器的开发者工具为我们提供了强大的调试系统,可以用来查看DOM树结构、CSS样式调试、动画调试、JavaScript代码断点调试等。今天我们就来看看console调试的那些实用的调试技巧。 如今,我们项目的开发通常会使用React、Vue等前端框...
继续阅读 »

众所周知,浏览器的开发者工具为我们提供了强大的调试系统,可以用来查看DOM树结构、CSS样式调试、动画调试、JavaScript代码断点调试等。今天我们就来看看console调试的那些实用的调试技巧。


如今,我们项目的开发通常会使用React、Vue等前端框架,前端调试也变得更加有难度,除了使用React Dev Tools,Vue Dev Tools等插件之外,我们使用最多的就是console.log(),当然多数情况下,console.log()就能满足我们的需求,但是当数据变得比较复杂时,console.log()就显得有些单一。其实console对象为我们提供了很多打印的方法,下面是console对象包含的方法(这里使用的是Chrome浏览器,版本为 95.0.4638.54(正式版本) (arm64)):


image.png


console 对象提供了浏览器控制台调试的接口,我们可以从任何全局对象中访问到它,如果你平时只是用console.log()来输出一些变量,那你可能没有用过console那些强大的功能。下面带你用console玩玩花式调试。


一、基本打印


1. console.log()


console.log()就是最基本、最常用的用法了。它可以用在JavaScript代码的任何地方,然后就可以浏览器的控制台中看到打印的信息。其基本使用方法如下:


let name = "CUGGZ";
let age = 18;
console.log(name) // CUGGZ
console.log(`my name is: ${name}`) // CUGGZ
console.log(name, age) // CUGGZ 18
console.log("message:", name, age) // message: CUGGZ 18

除此之外,console.log()还支持下面这种输出方式:


let name = "CUGGZ";
let age = 18;
let height = 180;
console.log('Name: %s, Age: %d', name, age) // Name: CUGGZ, Age: 18
console.log('Age: %d, Height: %d', age, height) // Age: 18, Height: 180

这里将后面的变量赋值给了前面的占位符的位置,他们是一一对应的。这种写法在复杂的输出时,能保证模板和数据分离,结构更加清晰。不过如果是简单的输出,就没必要这样写了。在console.log中,支持的占位符格式如下:



  • 字符串:%s

  • 整数:%d

  • 浮点数:%f

  • 对象:%o或%O

  • CSS样式:%c


可以看到,除了最基本的几种类型之外,它还支持定义CSS样式:


let name = "CUGGZ";
console.log('My Name is %cCUGGZ', 'color: skyblue; font-size: 30px;')

打印结果如下(好像并没有什么卵用):


image.png


这个样式打印可能有用的地方就是打印图片,用来查看图片是否正确:


console.log('%c ','background-image:url("http://iyeslogo.orbrand.com/150902Google/005.gif");background-size:120% 120%;background-repeat:no-repeat;background-position:center center;line-height:60px;padding:30px 120px;');

打印结果如下:


image.png


严格地说,console.log()并不支持打印图片,但是可以使用CSS的背景图来打印图片,不过并不能直接打印,因为是不支持设置图片的宽高属性,所以就需要使用line-heigh和padding来撑开图片,使其可以正常显示出来。


我们可以使用console.log()来打印字符画,就像知乎的这样:


image.png


可以使用字符画在线生成工具,将生成的字符粘贴到console.log()即可。在线工具:mg2txt。我的头像生成效果如下,中间的就是生成的字符:


image.png


除此之外,可以看到,当占位符表示一个对象时,有两种写法:%c或者%C,那它们两个有什么区别呢?当我们指定的对象是普通的object对象时,它们两个是没有区别的,如果是DOM节点,那就有有区别了,来看下面的示例:


image.png


可以看到,使用 %o 打印的是DOM节点的内容,包含其子节点。而%O打印的是该DOM节点的对象属性,可以根据需求来选择性的打印。


2. console.warn()


console.warn() 方法用于在控制台输出警告信息。它的用法和console.log是完全一样的,只是显示的样式不太一样,信息最前面加一个黄色三角,表示警告:


const app = ["facebook", "google", "twitter"];
console.warn(app);

打印样式如下:


image.png


3. console.error()


console.error()可以用于在控制台输出错误信息。它和上面的两个方法的用法是一样的,只是显示样式不一样:


const app = ["facebook", "google", "twitter"];
console.error(app)

image.png


需要注意,console.exception() 是 console.error() 的别名,它们功能是相同的。


当然,console.error()还有一个console.log()不具备的功能,那就是打印函数的调用栈:


function a() {
b();
}
function b() {
console.error("error");
}
function c() {
a();
}
c();

打印结果如下:


image.png


可以看到,这里打印出来了函数函数调用栈的信息:b→a→c。


console对象提供了专门的方法来打印函数的调用栈(console.trace()),这个下面会介绍到。


4. console.info()


console.info()可以用来打印资讯类说明信息,它和console.log()的用法一致,打印出来的效果也是一样的:


image.png


二、打印时间


1. console.time() & console.timeEnd()


如果我们想要获取一段代码的执行时间,就可以使用console对象的console.time() 和console.timeEnd()方法,来看下面的例子:


console.time();

setTimeout(() => {
console.timeEnd();
}, 1000);

// default: 1001.9140625 ms

它们都可以传递一个参数,该参数是一个字符串,用来标记唯一的计时器。如果页面只有一个计时器时,就不需要传这个参数 ,如果有多个计时器,就需要使用这个标签来标记每一个计时器:


console.time("timer1");
console.time("timer2");

setTimeout(() => {
console.timeEnd("timer1");
}, 1000);

setTimeout(() => {
console.timeEnd("timer2");
}, 2000);

// timer1: 1004.666259765625 ms
// timer2: 2004.654052734375 ms

2. console.timeLog()


这里的console.timeLog()上面的console.timeEnd()类似,但是也有一定的差别。他们都需要使用console.time()来启动一个计时器。然后console.timeLog()就是打印计时器当前的时间,而console.timeEnd()是打印计时器,直到结束的时间。下面来看例子:


console.time("timer");

setTimeout(() => {
console.timeLog("timer")
setTimeout(() => {
console.timeLog("timer");
}, 2000);
}, 1000);

// timer: 1002.80224609375 ms
// timer: 3008.044189453125 ms

而使用console.timeEnd()时:


console.time("timer");

setTimeout(() => {
console.timeEnd("timer")
setTimeout(() => {
console.timeLog("timer");
}, 2000);
}, 1000);


打印结果如下:


image.png


可以看到,它会终止当前的计时器,所以里面的timeLog就无法在找到timer计数器了。
所以两者的区别就在于,是否会终止当前的计时。


三、分组打印


1. console.group() & console.groupEnd()


这两个方法用于在控制台创建一个信息分组。 一个完整的信息分组以 console.group() 开始,console.groupEnd() 结束。来看下面的例子:


console.group();
console.log('First Group');
console.group();
console.log('Second Group')
console.groupEnd();
console.groupEnd();

打印结果如下:


image.png


再来看一个复杂点的:


console.group("Alphabet")
console.log("A");
console.log("B");
console.log("C");
console.group("Numbers");
console.log("One");
console.log("Two");
console.groupEnd("Numbers");
console.groupEnd("Alphabet");

打印结果如下:


image.png


可以看到,这些分组是可以嵌套的。当前我们需要调试一大堆调试输出,就可以选择使用分组输出,


2. console.groupCollapsed()


console.groupCollapsed()方法类似于console.group(),它们都需要使用console.groupEnd()来结束分组。不同的是,该方法默认打印的信息是折叠展示的,而group()是默认展开的。来对上面的例子进行改写:


console.groupCollapsed("Alphabet")
console.log("A");
console.log("B");
console.log("C");
console.groupCollapsed("Numbers");
console.log("One");
console.log("Two");
console.groupEnd("Numbers");
console.groupEnd("Alphabet");

其打印结果如下:


image.png


可以看到,和上面方法唯一的不同就是,打印的结果被折叠了,需要手动展开来看。


四、打印计次


1. console.count()


可以使用使用console.count()来获取当前执行的次数。来看下面的例子:


for (i = 0; i < 5; i++) {
console.count();
}

// 输出结果如下
default: 1
default: 2
default: 3
default: 4
default: 5

它也可以传一个参数来进行标记(如果为空,则为默认标签default):


for (i = 0; i < 5; i++) {
console.count("hello");
}

// 输出结果如下
hello: 1
hello: 2
hello: 3
hello: 4
hello: 5

这个方法主要用于一些比较复杂的场景,有时候一个函数被多个地方调用,就可以使用这个方法来确定是否少调用或者重复调用了该方法。


2. console.countReset()


顾名思义,console.countReset()就是重置计算器,它会需要配合上面的console.count()方法使用。它有一个可选的参数label:



  • 如果提供了参数label,此函数会重置与label关联的计数,将count重置为0。

  • 如果省略了参数label,此函数会重置默认的计数器,将count重置为0。


console.count(); 
console.count("a");
console.count("b");
console.count("a");
console.count("a");
console.count();
console.count();

console.countReset();
console.countReset("a");
console.countReset("b");

console.count();
console.count("a");
console.count("b");

打印结果如下:


default:1
a:1
b:1
a:2
a:3
default:2
default:3
default:1
a:1
b:1

五、其他打印


1. console.table()


我们平时使用console.log较多,其实console对象还有很多属性可以使用,比如console.table(),使用它可以方便的打印数组对象的属性,打印结果是一个表格。console.table() 方法有两个参数,第一个参数是需要打印的对象,第二个参数是需要打印的表格的标题,这里就是数组对象的属性值。来看下面的例子:


const users = [ 
{
"first_name":"Harcourt",
"last_name":"Huckerbe",
"gender":"Male",
"city":"Linchen",
"birth_country":"China"
},
{
"first_name":"Allyn",
"last_name":"McEttigen",
"gender":"Male",
"city":"Ambelókipoi",
"birth_country":"Greece"
},
{
"first_name":"Sandor",
"last_name":"Degg",
"gender":"Male",
"city":"Mthatha",
"birth_country":"South Africa"
}
]

console.table(users, ['first_name', 'last_name', 'city']);

打印结果如下:


image.png


通过这种方式,可以更加清晰的看到数组对象中的指定属性。


除此之外,还可以使用console.table()来打印数组元素:


const app = ["facebook", "google", "twitter"];
console.table(app);

打印结果如下:
image.png
通过这种方式,我们可以更清晰的看到数组中的元素。


需要注意,console.table() 只能处理最多1000行,因此它可能不适合所有数据集。但是也能适用于多数场景了。


2. console.clear()


console.clear() 顾名思义就是清除控制台的信息。当清空控制台之后,会打印一句:“Console was clered”:


image.png


当然,我们完全可以使用控制台的清除键清除控制台:


image.png


3. console.assert()


console.assert()方法用于语句断言,当断言为 false时,则在信息到控制台输出错误信息。它的语法如下:


console.assert(expression, message)

它有两个参数:



  • expression: 条件语句,语句会被解析成 Boolean,且为 false 的时候会触发message语句输出;

  • message: 输出语句,可以是任意类型。



该方法会在expression条件语句为false时,就会打印message信息。当在特定情况下才输出语句时,就可以使用console.assert()方法。


比如,当列表元素的子节点数量大于等于100时,打印错误信息:


console.assert(list.childNodes.length < 100, "Node count is > 100");

其输出结果如下图所示:


image.png


4. console.trace()


console.trace()方法可以用于打印当前执行的代码在堆栈中的调用路径。它和上面的console.error()的功一致,不过打印的样式就和console.log()是一样的了。来看下面的例子:


function a() {
b();
}
function b() {
console.trace();
}
function c() {
a();
}
c();

打印结果如下:


image.png


可以看到,这里输出了调用栈的信息:b→a→c,这个堆栈信息是从调用位置开始的。


5. console.dir()


console.dir()方法可以在控制台中显示指定JavaScript对象的属性,并通过类似文件树样式的交互列表显示。它的语法如下:


console.dir(object);

它的参数是一个对象,最终会打印出该对象所有的属性和属性值。


在多数情况下,使用consoledir()和使用console.log()的效果是一样的。但是当打印元素结构时,就会有很大的差异了,console.log()打印的是元素的DOM结构,而console.dir()打印的是元素的属性:


image.png


image.png


6. console.dirxml()


console.dirxml()方法用于显示一个明确的XML/HTML元素的包括所有后代元素的交互树。 如果无法作为一个element被显示,那么会以JavaScript对象的形式作为替代。 它的输出是一个继承的扩展的节点列表,可以让你看到子节点的内容。其语法如下:


console.dirxml(object);

该方法会打印输出XML元素及其后代元素,对于XML和HTML元素调用console.log()和console.dirxml()是等价的。


image.png


7. console.memory


console.memory是console对象的一个属性,而不是一个方法。它可以用来查看当前内存的使用情况,如果使用过多的console.log()会占用较多的内存,导致浏览器出现卡顿情况。


image.png



收起阅读 »

淦,为什么 "???".length !== 3

不知道你是否遇到过这样的疑惑,在做表单校验长度的需求中,发现不同字符 length 可能大小不一。比如标题中的 "𠮷" length 是 2(需要注意📢,这并不是一个中文字!)。 '吉'.length // 1 '𠮷'.length // 2 '❤'.le...
继续阅读 »

不知道你是否遇到过这样的疑惑,在做表单校验长度的需求中,发现不同字符 length 可能大小不一。比如标题中的 "𠮷" length 是 2(需要注意📢,这并不是一个中文字!)。


'吉'.length
// 1

'𠮷'.length
// 2

'❤'.length
// 1

'💩'.length
// 2

要解释这个问题要从 UTF-16 编码说起。


UTF-16


ECMAScript® 2015 规范中可以看到,ECMAScript 字符串使用的是 UTF-16 编码。



定与不定: UTF-16 最小的码元是两个字节,即使第一个字节可能都是 0 也要占位,这是固定的。不定是对于基本平面(BMP)的字符只需要两个字节,表示范围 U+0000 ~ U+FFFF,而对于补充平面则需要占用四个字节 U+010000~U+10FFFF



在上一篇文章中,我们有介绍过 utf-8 的编码细节,了解到 utf-8 编码需要占用 1~4 个字节不等,而使用 utf-16 则需要占用 2 或 4 个字节。来看看 utf-16 是怎么编码的。


UTF-16 的编码逻辑


UTF-16 编码很简单,对于给定一个 Unicode 码点 cp(CodePoint 也就是这个字符在 Unicode 中的唯一编号):



  1. 如果码点小于等于 U+FFFF(也就是基本平面的所有字符),不需要处理,直接使用。

  2. 否则,将拆分为两个部分 ((cp – 65536) / 1024) + 0xD800((cp – 65536) % 1024) + 0xDC00 来存储。



Unicode 标准规定 U+D800...U+DFFF 的值不对应于任何字符,所以可以用来做标记。



举个具体的例子:字符 A 的码点是 U+0041,可以直接用一个码元表示。


'\u0041'
// -> A

A === '\u0041'
// -> true

Javascript 中 \u 表示 Unicode 的转义字符,后面跟着一个十六进制数。


而字符 💩 的码点是 U+1f4a9,处于补充平面的字符,经过 👆 公式计算得到两个码元 55357, 56489 这两个数字用十六进制表示为 d83d, dca9,将这两个编码结果组合成代理对。


'\ud83d\udca9'
// -> '💩'

'💩' === '\ud83d\udca9'
// -> true

由于 Javascript 字符串使用 utf-16 编码,所以可以正确将代理对 \ud83d\udca9 解码得到码点 U+1f4a9


还可以使用 \u + {},大括号中直接跟码点来表示字符。看起来长得不一样,但他们表示的结果是一样的。


'\u0041' === '\u{41}'
// -> true

'\ud83d\udca9' === '\u{1f4a9}'
// -> true


可以打开 Dev Tool 的 console 面板,运行代码验证结果。



所以为什么 length 判断会有问题?


要解答这个问题,可以继续查看 规范,里面提到:在 ECMAScript 操作解释字符串值的地方,每个元素都被解释为单个 UTF-16 代码单元。



Where ECMAScript operations interpret String values, each element is interpreted as a single UTF-16 code unit.



所以像💩 字符实际上占用了两个 UTF-16 的码元,也就是两个元素,所以它的 length 属性就是 2。(这跟一开始 JS 使用 USC-2 编码有关,当初以为 65536 个字符就可以满足所有需求了)


但对于普通用户而言,这就完全没办法理解了,为什么明明只填了一个 '𠮷',程序上却提示占用了两个字符长度,要怎样才能正确识别出 Unicode 字符长度呢?


我在 Antd Form 表单使用的 async-validator 包中可以看到下面这段代码


const spRegexp = /[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]/g;

if (str) {
val = value.replace(spRegexp, '_').length;
}

当需要进行字符串长度的判断时,会将码点范围在补充平面的字符全部替换为下划线,这样长度判断就和实际显示的一致了!!!


ES6 对 Unicode 的支持


length 属性的问题,主要还是最初设计 JS 这门语言的时候,没有考虑到会有这么多字符,认为两个字节就完全可以满足。所以不止是 length,字符串常见的一些操作在 Unicode 支持上也会表现异常


下面的内容将介绍部分存在异常的 API 以及在 ES6 中如何正确处理这些问题。


for vs for of


例如使用 for 循环打印字符串,字符串会按照 JS 理解的每个“元素”遍历,辅助平面的字符将会被识别成两个“元素”,于是出现“乱码”。


var str = '👻yo𠮷'
for (var i = 0; i < str.length; i ++) {
console.log(str[i])
}

// -> �
// -> �
// -> y
// -> o
// -> �
// -> �

而使用 ES6 的 for of 语法就不会。


var str = '👻yo𠮷'
for (const char of str) {
console.log(char)
}

// -> 👻
// -> y
// -> o
// -> 𠮷

展开语法(Spread syntax)


前面提到了使用正则表达式,将辅助平面的字符替换的方式来统计字符长度。使用展开语法也可以得到同样的效果。


[...'💩'].length
// -> 1

slice, split, substr 等等方法也存在同样的问题。


正则表达式 u


ES6 中还针对 Unicode 字符增加了 u 描述符。


/^.$/.test('👻')
// -> false

/^.$/u.test('👻')
// -> true

charCodeAt/codePointAt


对于字符串,我们还常用 charCodeAt 来获取 Code Point,对于 BMP 平面的字符是可以适用的,但是如果字符是辅助平面字符 charCodeAt 返回结果就只会是编码后第一个码元对于的数字。


'羽'.charCodeAt(0)
// -> 32701
'羽'.codePointAt(0)
// -> 32701

'😸'.charCodeAt(0)
// -> 55357
'😸'.codePointAt(0)
// -> 128568

而使用 codePointAt 则可以将字符正确识别,并返回正确的码点。


String.prototype.normalize()


由于 JS 中将字符串理解成一串两个字节的码元序列,判断是否相等是根据序列的值来判断的。所以可能存在一些字符串看起来长得一模一样,但是字符串相等判断结果确是 false


'café' === 'café'
// -> false

上面代码中第一个 café 是有 cafe 加上一个缩进的音标字符\u0301组成的,而第二个 café 则是由一个 caf + é 字符组成的。所以两者虽然看上去一样,但码点不一样,所以 JS 相等判断结果为 false


'cafe\u0301'
// -> 'café'

'cafe\u0301'.length
// -> 5

'café'.length
// -> 4

为了能正确识别这种码点不一样,但是语意一样的字符串判断,ES6 增加了 String.prototype.normalize 方法。


'cafe\u0301'.normalize() === 'café'.normalize()
// -> true

'cafe\u0301'.normalize().length
// -> 4

总结


这篇文章主要是我最近重新学习编码的学习笔记,由于时间仓促 && 水平有限,文章中必定存在大量不准确的描述、甚至错误的内容,如有发现还请善意指出。❤️



收起阅读 »

「如何优雅的不写注释?」每个工程师都要不断追求的漫漫长路

引言 ✨ 作为一个软件开发者,我们需要进行协同,在这个协同的过程中我们需要让其他开发者可以读懂我们的代码,所以注释可能就变成了一个重要的代码解读标注手段。 说到这,大家可能就会感知到注释确实很重要,他可以是开发者有效沟通的渠道,但是我在这里想表达的是注释其实并...
继续阅读 »

引言 ✨


作为一个软件开发者,我们需要进行协同,在这个协同的过程中我们需要让其他开发者可以读懂我们的代码,所以注释可能就变成了一个重要的代码解读标注手段。


说到这,大家可能就会感知到注释确实很重要,他可以是开发者有效沟通的渠道,但是我在这里想表达的是注释其实并不是银弹,也不是最好的手段,在我的眼里,注释更多承担的是一个兜底的作用。


于是便有了本篇文章,我想通过更多亲身经历和书籍参考来阐述证明我的观点:



  • 什么是好注释

  • 什么是坏注释

  • 怎么不通过注释提高编码可读性「易于上手版」

  • 挖一个关于前端架构的大坑「待我能力提升」



注意我并不是贬低使用注释,而是想给大家推销一种思想,即更多的用代码阐述自己的想法,把注释作为一种保底手段,而不是银弹。而且我切实的知道很多历史因素导致代码极其难以阅读,利用注释去表达信息也是没有办法的。但是希望大家读完本文之后可以更多去考量如何让代码更可读,而不是怎么写注释



不堪回首的摸爬滚打 🌧️


image.png


我毕业工作这一年以来,一直在不断加深对整洁编码的理解,我也经历过迷惑和自我怀疑。但是通过不断的实践,思考,回顾,我渐渐的有了自己的理解。


希望大家在阅读完成我的经历与摸爬滚打后,有所收获,如果觉得没啥参考性,就当作看了一场平平无奇的故事。


求学 🎓


曾经在求学过程中,大家的老师应该都会鼓励大家去多些注释去阐述你的编码思想,我便一直认为写注释是一个标准,是我在开发中必须要去做的事情。


然而现在我回过头去看曾经老师的教诲,我会觉得是出于以下三点考虑:



  • 对于一个计算机初学者,写注释可以让你去梳理你的编码思路

  • 写注释的时候也会对代码逻辑进行思考,类比伪代码

  • 很现实的一点,帮助老师理解你的代码,毕竟有的代码不写注释老师都不知道判卷的时候该给分还是不给分


实习 🧱


之后我便开始了我的第一段职业生涯,大四实习,我来到我现在所在的公司呆了两个月,做的也是比较简单的工作,改一改前人留下的遗产「bug」,到即将返校的时候,我接到了一个开发任务,当时我开发了一天,晚上进行 code-review,我记得当时我信心满满,给每个子方法和关键变量都写了注释,本来信心满满,但是最后我的代码被前辈批到自闭,其中不乏关于代码设计和命名相关的建议,也是在这次 review 中我听到了一句足以毁灭我学习生涯所建立的世界观的话:



好的代码是不需要注释的



工作 💻


工作之后,最开始我接触的项目也是一个恶臭代码的重灾区了,充斥着各种难以阅读的代码逻辑,但是那时候我还没有一个评判好坏的能力,一度去怀疑自己。



是不是我太菜了,才读不懂别人那些高端的代码



没错,我真的这么想过,甚至十分的自我怀疑,但是后期我经历了几件事情让我对这个自我怀疑的想法消除了:



  • 在导师的指导下对代码的反复 code-review 并重写



当时我们发现该项目存在需求遗漏,于是这个需求便来到了我的头上,即使项目紧急,导师还是给我细心 review,最后这个功能我重写了三四次,也让我对什么样的代码是好的有了一个粗略的概念。




  • 对某一个模块的完全重新设计与编码



经历了从设计到评审,再到编码,最后 review 的过程。




  • 相关书籍的阅读



本篇文章不做书籍推荐了,只是表达对于如何整洁编码是存在很多前人经验与指导原则存在的,新人可以优先阅读《代码整洁之道




  • 丰富的自我实践与思考



在我后来参与产线内部平台建设,负责安全运维大模块,负责冬奥会项目过程中也是不断的在追求整洁编码。思考,实践,回顾,一直伴随着我的职业道路,对于代码如何编写的更整洁也渐渐有了自己的想法。



在工作的过程中,我对于“好的代码是不需要注释的”这句话的理解也在不断加深。当然,如果对于某些难以处理的遗留问题,注释也是一个不错的方法对其进行注解描述。


总结 🔍


最开始我觉得注释是必要的,后经过经验的积累,前辈的教导,自己的学习,不断的思考与回顾,到现在有了自己的一套思想。当然我不会去说我的思想是正确的,可能过几年之后我会回来打我自己的脸,其实想法的改变,也能代表一种成长吧~


有关注释的杂七杂八 🌲


image.png



别给糟糕的代码加注释,重新写吧




  • 什么也比不上放置良好的注释来的有用

  • 什么也不会比乱七八糟的注释更有本事搞乱一个模块

  • 什么也不会比陈旧,提供错误信息的注释更具破坏性


若编程语言有足够的表达力,或者我们长于用这些语言来表达意图,就不那么需要注释 —— 也许根本不需要。


上面的话引自《代码整洁之道》。但是从事这个行业越久我越无法否认其正确性,我们必须要知道的一件事是代码具有实时性,即你现在项目中的代码总是当前最新的,否则也无法正确运行。然而上面的注释我们根本无法知道是什么时候写的,不具备实时性。



  • 代码是在变动,演化的。然而注释并不能随之变动

  • 程序员不会长期维护注释

  • 注释会撒谎,而代码不会

  • 不准确的注释比没注释坏的多

  • ...


所以我的想法很坚定,注释无法美化糟糕的代码,与其花时间为糟糕的代码编写解释不如花时间把糟糕的代码变得整洁。



用代码来阐述思想一直是最好的办法。



当然总有些注释是必须的或是有利的,还有一些注释是有害的,下面和大家聊一聊什么是好注释,什么是坏注释。


好注释 🌈



  • 法律信息



比如版权或者著作权的声明




  • 提供信息的注释



比如描述一个方法的返回值,但是其实可以利用函数名来传达信息




  • 阐释



把某些晦涩难懂的参数或者返回值翻译成某种可读的形式,更好的方式是让参数和返回值自身就足够清楚




  • TODO 注释



这个可能大家都会经常用,用来记录我们哪里还有任务没有完成




  • 放大



比如一个看似不起眼却很重要的变量,我们可以用注释凸显它的重要性




  • ...


坏注释 😈



  • 喃喃自语



只有作者读的懂的注释,当你打算开始写注释,就要讲清楚原委,和读者有良好的沟通




  • 多余的注释



有的注释写不写没啥作用,很简单的方法都要写注释,甚至读代码都比看注释快




  • 误导性注释



程序员都已经够辛苦了,你还要用注释欺骗人家




  • 循规式注释



要求每个方法每个变量都要有注释,很多废话只会扰乱读者




  • 位置标记



比如打了一堆 ****** 来标注位置,这个我上学的时候经常干




  • 废话注释



毫无作用的废话




  • 注释掉的代码



很多读者会想,代码依然留在那一定有原因,最后不敢删除畏手畏脚




  • 信息过多的注释



注释中包含很多无关的细节,其实对读者完全没有必要




  • ...


优雅的不写注释 🌿


image.png


首先我再次阐述之前说过的话,编码实际上是一种社会行为,是需要沟通的。而如何让我们不借助注释来阐述我们的思想,其实是需要我们长期探索并在实践中积累经验的,从我的经验与视角出发,其实让我们的代码库更加整洁其实主要从以下两个方面考量:



  • 整洁编码

  • 前端架构


下面我分开来讲~



注意,编码不是一个人的事情,在我眼里如何做到团队成员编码风格的相近才是最具成效且需要长期努力的任务,也是相对理想且难以做到的。正所谓,就算我们写的代码很烂,但是烂的我们的成员可以相互理解,也是一种优秀「瞎说的,哈哈哈,代码可维护性还是要团队成员一起追求的」。



整洁编码 📚


首先我先引用几位前辈的话,带大家感受一下,什么样的代码是整洁的:



  • Bjarne:我喜欢优雅和高效的代码,代码的逻辑应当直接了当,叫缺陷难以隐藏们。尽量减少依赖关系,使之便于维护,依据某种分层战略完善错误处理代码,性能调至最优,省得引诱别人做没有规矩的优化,搞出一堆混乱出来,整洁的代码只做好一件事。

  • Grady: 整洁的代码简单直接,整洁的代码从不隐藏设计者的意图,充满干净利落的抽象和直截了当的控制语句。


对于整洁编码可以先简单总结:



  • 尽量减少依赖关系,便于维护

  • 简单直接,充满了干净利落逻辑处理和直截了当的控制语句。

  • 能够全部运行通过,并配有单元测试和验收测试

  • 没有重复的代码

  • 写的代码能够完全提现我们的设计理念「这个可以通过类、方法、属性的命名,代码逻辑编码的清晰来体现



在我们日常编码中,命名和函数可以说是我们最常接触的,也是最能影响我们代码整洁度的。于是本文中,我将围绕这两个方向为大家介绍几种易于上手的整洁编码方案。



下文参考我之前写过的一篇文章:关于整洁代码与重构的摸爬滚打


命名 🌟



  • 只要命名需要通过注释来补充,就表示我们的命名还是存在问题

  • 所有的命名都要有实际意义,命名会告诉你它为什么存在,它做什么事情,应该怎么用


比如列举一段曾经上学的时候可能写出的代码:


#include <stdio.h>  
int main(){
printf("Hello, C! \n");
int i = 10;
int m = 1;
for(int j = 0; j < i; j+=m){
for(int n = 0; n< i-j;n++){
printf("*");
}
printf("\n");
}
return 0;
}

我们看这里命名都是一大堆 i,m,n,j之类的根本不知道这些变量用来干嘛,其实这段代码最后仅仅打印出来的是 * 组成的直角三角形。但是当时写代码我确实就是这样,i, j,m,n等等字母用了一遍,也不包含什么语义上的东西,变量命名就是字母表里面选。


当然现在的命名就高端多了,开始从词典里面找了,还要排列组合,比如 getUserisAdmin。语义上提升了,通过命名我们也可以直观的判断这个方法是干嘛的,这个变量是干嘛的。


这样看其实命名是很有学问的事情,下面我开始列举几点命名中可以快速提升代码整洁度的方法:



  • 避免引起误导



不要用不该作为变量的专有名词来为变量命名,一组账号accountList ,却不是List类型,也是存在误导。命名过于相似:比如 XYZHandlerForAORBORC 和XYZControllerForAORBORDORC,谁能一眼就看出来呢~




  • 做有意义的区分


let fn = (array1,array2) =>{ 
for(let i =0 ;i<array1.length;i++){
array2[i] = array1[i];
}
}


比如上面 array1array2 就不是有意义的区分,这只是一个赋值操作,完全可以是 sourceArrayDesArray

再比如 起的名字:userInfouserData都是这种的,我们很难读懂这两个有啥子区别,这种区分也没啥意义,说白了这只是单词拼写的区分,而不是在语义上区分开了。




  • 使用读的出来的名称



编程是社会活动,免不了与人交流对话,使用难以轻松读出来的声音会导致你的思想难于传达。并且人类的大脑中有专门处理语言的区域,可以辅助你理解问题,不加以运用简直暴殄天物。简单举个例子:getYMDHMS,这个方法就是获取时间,然而就是难以阅读,是不好的命名。




  • 使用可以搜索的名称



之前的代码,我用个 i 作为变量。如果代码很长,我这想要追踪一下这个i的变化,简直折磨。同理我不喜欢直接以 valuedatainfo 等单词直接做变量,因为他们经常以其他变量的组成部分出现,难以追踪。




  • 程序中有意义的数字或者字符串应该用常量进行替换,方便查找


export const DEFAULT_ORDERBY = '-updateTime' 
export const DEFAULT_CHECKEDNUM = 0


比如采用上面的方式,既可以让代码更加语义化也方便集中修改




  • 类名和对象名应为名词或名词词组,方法名应为动词或动词词组



比如我们常用的 updatexxxfilteredXXX 都是这样的命名规则




  • 属性命名添加有用必要的语境,但是短名称如果足够用清楚,就比长名称好,别添加不必要的语境

  • 每个概念对应一个词



比如 taglabelticketworkOrder 各种混着用岂不是乱糟糟的,这读者容易混淆,也会为以后造成负担,也可能会隐藏一些 bug。所以我们在项目开发前可以确定一个名词术语表来避免这种情况发生。




  • ...


函数 🌟



大师写代码是在讲故事,而不是在写程序。




  • 短小:20封顶最佳

  • 函数的缩进层级尽可能的少

  • 函数参数尽量少

  • 使用具有描述性的函数名



当然函数越短小,功能越集中,就越便于取好名字




  • 抽取异常处理,即 try-catch 就是一个函数 ,函数应该只做一件事,错误处理就是一件事

  • 标识参数丑陋不堪


const updateList = (flag) {
if(flag){
// ...
} else {
// ...
}
}


比如一个方法,定义成上面这个样子,我们很难通过方法定义直接了解其能力以及参数的含义。




  • 函数名是动词,参数是名词,并保证顺序



比如 saveField(name)assertExpectedEqualsActual(expected,actual)




  • 无副作用



比如一个方法名是 updateList,后来者应该顺理成章的认为这个方法只会更新列表,然而开发者在这个方法中加入了其他的逻辑,可能导致后来者在使用这个方法后导致副作用,而代码报错无法正常运行。




  • 重复是软件中一切邪恶的根源,拒绝重复的代码

  • ...


写代码和写文章一样,先去想你要写什么,最后再去打磨,初稿也许粗糙无序,那就要斟酌推敲,直到达成心中的样子。编程艺术也是语言设计的艺术。


前端架构 🎋



本人现在工作一年有余,一年半不足,对于前端架构并不能很好的输出给大家,所以在此给大家先挖一个大坑,本章节中如有错误理解,请大家不吝赐教,与我探讨交流,感谢。



首先,我先解释一下我为什么要把前端架构放在这样的一篇文章中,其实是存在两条原因:



  • 从个人开发角度来看,优秀的前端架构可以增强代码的维护性



试想一个组织结构恶臭的项目,一定会影响你的阅读的,杂乱不堪的组件划分原则,不清晰的边界通通都会成为巨大的阻力。




  • 最近换了组,到了天擎终端平台组,新的 leader 也分享了很多关于组件化的经验与理解



浅薄无知的小寒草🌿,在线求鞭策。



那么,大家在提到前端架构的时候,会想到什么呢,我反正会想到以下几点:



  • 组件化

  • 架构模式

  • 规范 / 约定

  • 分层

  • ...


下面我逐条来讲~


架构模式 ✨


组件化我先跳过,最后再说,先说说架构模式,大家脑子里一定会想到 MVVMMVC 等模式,比如我们常用的 Vue 框架中的 MVVM ,以及普遍在 Spring 那一套中被提及并在在 nest.js 中有所应用的 MVC。但是关于架构模式前端说的可能还是相对较少,我的水平也有限,而且说起来可能就会跑题了,于是也不在本文过多赘述。


规范&约定 ✨


关于规范或者约定,常见的包括:



  • 目录结构

  • 命名规范

  • 术语表

  • ...


其实这几点我们很好理解,我们会通过约定或者脚手架等方式来规范化我们的目录结构,使我们同一个产线下项目的目录结构保证一致。以及我们在开发前的设计阶段可能也需要出具一份术语表,这个前文也听到过一个含义用一个确定的词来表示,否则可能会导致代码的混乱。


关于命名规范,首先我们需要去约定一个统一的命名规则,我们常见的是变量命名为小驼峰,文件命名为连字符。但是这个命名规范其实我们可以做的事情不止这些,比如我说几个例子:



  • 前端命名规范是小驼峰,服务端命名是下划线,我们怎么处理让前端编码中屏蔽掉命名规则差异。

  • 同一个含义我们可以用很多命名来表示,比如:handleStaffUpdate / updateStaff。在项目初期我们完全可以对其进行约束使用哪种命名风格,以让我们项目一致性加强。

  • ...


分层 ✨


关于分层,大家的差异可能会比较大,比如我们可能会把我们的前端项目分为以下几层:



  • 业务层

  • 服务层

  • 模型层「可能有也可能没有」


业务层就是我们比较熟悉的,各种业务代码。


服务层「server」不知道大家的项目中有没有,我们项目使用 grpc 接口,由于接口粒度较高,我们通常会在 server 层对接口再次处理,合并,或者在这个层去完成一些服务端不合理设计的屏蔽。


模型层「model」不常有,但是一些复杂的又需要复用的逻辑可能有这个层,就相当于逻辑的抽象,脱离于视图,之后如果我们需要复用这里的逻辑,而视图不同,我们就可以使用这个 model


合理的分层可以让我们的项目更清晰,减少代码冗杂,提升可维护性。


组件化 ✨


其实组件化一直都是前端架构中的大课题,首先我们可以通过组件化能得到什么,其实最重要的可能就是:



  • 复用


不知道大家的项目有没有统计代码复用率,我们是有的,而且这也是前端工程质量很重要的一个指标。然而在追求组件化的过程中其实我们很少会拥有一个衡量标准:



  • 什么情况需要拆分组件

  • 什么情况不需要拆分组件


团队对这个问题没有一个统一认知的情况下很容易造成:



  • 五花八门的组件拆分原则导致代码结构混乱

  • 无效的组件拆分导致文件过多,维护困难

  • 过深的组件嵌套层级「经历过的人一定会对此深恶痛绝」

  • ...


其实我最开始的时候也喜欢把组件按照很细的粒度进行拆分,想的是总会有用到的时候嘛,但是从架构整洁的角度出发,过细或者过于粗糙的组件拆分都会导致维护困难,复用困难等问题,现在的我可能更会从复用性角度出发:



  • 这个东西会不会复用


只从复用性考量很容易的就会把组件区分为两大类:



  • 需要复用的组件

  • 几乎不会被复用的组件



注意我没有说什么组件是肯定不会被复用的,而是几乎不会被复用。



所以我们就可以坐下来思考,把我们工作中常见的场景拎出来,过一遍,因为我们工作的业务场景不同,所以我肯定还是以我的业务场景出发,那么我可以把我的组件分成几种:



  • page 组件

  • layout 组件

  • 业务组件


其中我认为,page 组件是几乎不会复用的组件,layout 组件和业务组件在我眼里是可以复用的组件。



这只是很粗糙的的区分,之后还有很多问题:



  • 如何把业务组件写的好用

  • 如何确定一个组件的边界

  • ...


这些我们就要从消费者角度考量了。



当然其实组件化也可以和分层一起考虑,因为组件其实也会有层级,比如:



  • 基础 ui 组件[参考element-ui]

  • 基础业务组件


基础业务组件也可以按照是否跨模块等原则继续进行分层,这个可以按照大家的业务场景自行考量。


总结 ✨


从实际经验出发,合理的架构确实是项目易于维护「从而优雅的不写注释🌿」,而这是一个自顶向下分析决策的过程,本章节篇幅有限,加上我水平有限,无法在此过多赘述,还请大家持续期待我的分享。


结束语 ☀️


image.png


那么本篇文章就结束了,涵盖了我个人经历上的摸爬滚打,解析什么样的注释是好的,什么样的注释是坏的,并从编码整洁度与前端架构的角度出发来考量如何提升代码的可维护性。以此来论述我的观点:



注释不是维护代码的银弹,而便于维护的代码需要从整洁编码前端架构两个「或者更多」层面入手。



我工作的时间不长也不短了,已经一年出头了,我一直秉承着编码是社会性工作,需要协同合作,代码的可维护性也是一名职业软件工程师需要持续追求的观点。


思考,实践,回顾的过程没有停歇,我在此也希望大家多思考,作为一名工程师我们需要追求的不仅仅只有:



收起阅读 »

Node 之一个进程的死亡

人固有一死,一个 Node 进程亦是如此,总有万般不愿也无法避免。从本篇文章我们看看一个进程灭亡时如何从容离去。 一个 Node 进程,除了提供 HTTP 服务外,也绝少不了跑脚本的身影。跑一个脚本拉取配置、处理数据以及定时任务更是家常便饭。在一些重要流程中能...
继续阅读 »

人固有一死,一个 Node 进程亦是如此,总有万般不愿也无法避免。从本篇文章我们看看一个进程灭亡时如何从容离去。


一个 Node 进程,除了提供 HTTP 服务外,也绝少不了跑脚本的身影。跑一个脚本拉取配置、处理数据以及定时任务更是家常便饭。在一些重要流程中能够看到脚本的身影:



  1. CI,用以测试、质量保障及部署等

  2. Cron,用以定时任务

  3. Docker,用以构建镜像


如果在这些重要流程中脚本出错无法及时发现问题,将有可能引发更加隐蔽的问题。如果在 HTTP 服务出现问题时,无法捕获,服务异常是不可忍受的。


最近观察项目镜像构建,会偶尔发现一两个镜像虽然构建成功,但容器却跑不起来的情况究其原因,是因为 一个 Node 进程灭亡却未曾感知到的问题


Exit Code



什么是 exit code?



exit code 代表一个进程的返回码,通过系统调用 exit_group 来触发。


POSIX 中,0 代表正常的返回码,1-255 代表异常返回码,在业务实践中,一般主动抛出的错误码都是 1。在 Node 应用中调用 API process.exitCode = 1 来代表进程因期望外的异常而中断退出。


这里有一张关于异常码的附表 Appendix E. Exit Codes With Special Meanings


异常码在操作系统中随处可见,以下是一个关于 cat 进程的异常以及它的 exit code,并使用 strace 追踪系统调用。


$ cat a
cat: a: No such file or directory

# 使用 strace 查看 cat 的系统调用
# -e 只显示 write 与 exit_group 的系统调用
$ strace -e write,exit_group cat a
write(2, "cat: ", 5cat: ) = 5
write(2, "a", 1a) = 1
write(2, ": No such file or directory", 27: No such file or directory) = 27
write(2, "\n", 1
) = 1
exit_group(1) = ?
+++ exited with 1 +++

strace 追踪进程显示的最后一行可以看出,该进程的 exit code 是 1,并把错误信息输出到 stderr (stderr 的 fd 为2) 中


如何查看 exit code


strace 中可以来判断进程的 exit code,但是不够方便过于冗余,更无法第一时间来定位到异常码。


有一种更为简单的方法,通过 echo $? 来确认返回码


$ cat a
cat: a: No such file or directory

$ echo $?
1

$ node -e "preocess.exit(52)"
$ echo $?
52

未曾感知的痛苦何在: throw new ErrorPromise.reject 区别


以下是两段代码,第一段抛出一个异常,第二段 Promise.reject,两段代码都会如下打印出一段异常信息,那么两者有什么区别?


function error () {
throw new Error('hello, error')
}

error()

// Output:

// /Users/shanyue/Documents/note/demo.js:2
// throw new Error('hello, world')
// ^
//
// Error: hello, world
// at error (/Users/shanyue/Documents/note/demo.js:2:9)

async function error () {
return new Error('hello, error')
}

error()

// Output:

// (node:60356) UnhandledPromiseRejectionWarning: Error: hello, world
// at error (/Users/shanyue/Documents/note/demo.js:2:9)
// at Object.<anonymous> (/Users/shanyue/Documents/note/demo.js:5:1)
// at Module._compile (internal/modules/cjs/loader.js:701:30)
// at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)

在对上述两个测试用例使用 echo $? 查看 exit code,我们会发现 throw new Error()exit code 为 1,而 Promise.reject() 的为 0。


从操作系统的角度来讲,exit code 为 0 代表进程成功运行并退出,然而此时即使有 Promise.reject,操作系统也会视为它执行成功。


这在 DockerfileCI 中执行脚本时将留有安全隐患。


Dockerfile 在 Node 镜像构建时的隐患


当使用 Dockerfile 构建镜像或者 CI 时,如果进程返回非0返回码,构建就会失败。


这是一个浅显易懂的含有 Promise.reject() 问题的镜像,我们从这个镜像来看出问题所在。


FROM node:12-alpine

RUN node -e "Promise.reject('hello, world')"

构建镜像过程如下,最后两行提示镜像构建成功:即使在构建过程打印出了 unhandledPromiseRejection 信息,但是镜像仍然构建成功。


$ docker build -t demo .
Sending build context to Docker daemon 33.28kB
Step 1/2 : FROM node:12-alpine
---> 18f4bc975732
Step 2/2 : RUN node -e "Promise.reject('hello, world')"
---> Running in 79a6d53c5aa6
(node:1) UnhandledPromiseRejectionWarning: hello, world
(node:1) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:1) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Removing intermediate container 79a6d53c5aa6
---> 09f07eb993fe
Successfully built 09f07eb993fe
Successfully tagged demo:latest

但如果是在 node 15 镜像内,镜像会构建失败,至于原因以下再说。


FROM node:15-alpine

RUN node -e "Promise.reject('hello, world')"

$ docker build -t demo .
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM node:15-alpine
---> 8bf655e9f9b2
Step 2/2 : RUN node -e "Promise.reject('hello, world')"
---> Running in 4573ed5d5b08
node:internal/process/promises:245
triggerUncaughtException(err, true /* fromPromise */);
^

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "hello, world".] {
code: 'ERR_UNHANDLED_REJECTION'
}
The command '/bin/sh -c node -e "Promise.reject('hello, world')"' returned a non-zero code: 1

Promise.reject 脚本解决方案


能在编译时能发现的问题,绝不要放在运行时。所以,构建镜像或 CI 中需要执行 node 脚本时,对异常处理需要手动指定 process.exitCode = 1 来提前暴露问题


runScript().catch(() => {
process.exitCode = 1
})

在构建镜像时,Node 也有关于异常解决方案的建议:



(node:1) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag --unhandled-rejections=strict (see nodejs.org/api/cli.htm…). (rejection id: 1)



根据提示,--unhandled-rejections=strict 将会把 Promise.reject 的退出码设置为 1,并在将来的 node 版本中修正 Promise 异常退出码。


而下一个版本 Node 15.0 已把 unhandled-rejections 视为异常并返回非0退出码。


$ node --unhandled-rejections=strict error.js 

Signal


在外部,如何杀死一个进程?答:kill $pid


而更为准确的来说,一个 kill 命令用以向一个进程发送 signal,而非杀死进程。大概是杀进程的人多了,就变成了 kill。



The kill utility sends a signal to the processes specified by the pid operands.



每一个 signal 由数字表示,signal 列表可由 kill -l 打印


# 列出所有的 signal
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

这些信号中与终端进程接触最多的为以下几个,其中 SIGTERM 为 kill 默认发送信号,SIGKILL 为强制杀进程信号


在 Node 中,process.on 可以监听到可捕获的退出信号而不退出。以下示例监听到 SIGINT 与 SIGTERM 信号,SIGKILL 无法被监听,setTimeout 保证程序不会退出


console.log(`Pid: ${process.pid}`)

process.on('SIGINT', () => console.log('Received: SIGINT'))
// process.on('SIGKILL', () => console.log('Received: SIGKILL'))
process.on('SIGTERM', () => console.log('Received: SIGTERM'))

setTimeout(() => {}, 1000000)

运行脚本,启动进程,可以看到该进程的 pid,使用 kill -2 97864 发送信号,进程接收到信号并未退出


$ node signal.js
Pid: 97864
Received: SIGTERM
Received: SIGTERM
Received: SIGTERM
Received: SIGINT
Received: SIGINT
Received: SIGINT

容器中退出时的优雅处理


当在 k8s 容器服务升级时需要关闭过期 Pod 时,会向容器的主进程(PID 1)发送一个 SIGTERM 的信号,并预留 30s 善后。如果容器在 30s 后还没有退出,那么 k8s 会继续发送一个 SIGKILL 信号。如果古时皇帝白绫赐死,教你体面。


其实不仅仅是容器,CI 中脚本也要优雅处理进程的退出。


当接收到 SIGTERM/SIGINT 信号时,预留一分钟时间做未做完的事情。


async function gracefulClose(signal) {
await new Promise(resolve => {
setTimout(resolve, 60000)
})

process.exit()
}

process.on('SIGINT', gracefulClose)
process.on('SIGTERM', gracefulClose)

这个给脚本预留时间是比较正确的做法,但是如果是一个服务有源源不断的请求过来呢?那就由服务主动关闭吧,调用 server.close() 结束服务


const server = http.createServer(handler)

function gracefulClose(signal) {
server.close(() => {
process.exit()
})
}

process.on('SIGINT', gracefulClose)
process.on('SIGTERM', gracefulClose)

总结



  1. 当进程结束的 exit code 为非 0 时,系统会认为该进程执行失败

  2. 通过 echo $? 可查看终端上一进程的 exit code

  3. Node 中 Promise.reject 时 exit code 为 0

  4. Node 中可以通过 process.exitCode = 1 显式设置 exit code

  5. 在 Node12+ 中可以通过 node --unhandled-rejections=strict error.js 执行脚本,视 Promise.rejectexit code 为 1,在 Node15 中修复了这一个问题

  6. Node 进程退出时需要优雅退出

  7. k8s 关闭 POD 时先发一个 SIGTERM 信号,留 30s 时间处理未完成的事,如若 POD 没有正常退出,30s 过后发送 SIGKILL 信号


收起阅读 »

CSS 奇技淫巧 | 巧妙实现文字二次加粗再加边框

需求背景 - 文字的二次加粗 今天遇到这样一个有意思的问题: 在文字展示的时候,利用了 font-weight: bold 给文字进行加粗,但是觉得还是不够粗,有什么办法能够让文字更粗一点呢? emm,不考虑兼容性的话,答案是可以利用文字的 -webkit...
继续阅读 »

需求背景 - 文字的二次加粗


今天遇到这样一个有意思的问题:



  1. 在文字展示的时候,利用了 font-weight: bold 给文字进行加粗,但是觉得还是不够粗,有什么办法能够让文字更粗一点呢?


emm,不考虑兼容性的话,答案是可以利用文字的 -webkit-text-stroke 属性,给文字二次加粗。


MDN - webkit-text-stroke: 该属性为文本字符添加了一个边框(笔锋),指定了边框的颜色, 它是 -webkit-text-stroke-width-webkit-text-stroke-color 属性的缩写。


看下面的 DEMO,我们可以利用 -webkit-text-stroke,给文字二次加粗:


<p>文字加粗CSS</p>
<p>文字加粗CSS</p>
<p>文字加粗CSS</p>
<p>文字加粗CSS</p>

p {
font-size: 48px;
letter-spacing: 6px;
}
p:nth-child(2) {
font-weight: bold;
}
p:nth-child(3) {
-webkit-text-stroke: 3px red;
}
p:nth-child(4) {
-webkit-text-stroke: 3px #000;
}

对比一下下面 4 种文字,最后一种利用了 font-weight: bold-webkit-text-stroke,让文字变得更为



CodePen Demo -- font-weight: bold 和 -webkit-text-stroke 二次加粗文字


如何给二次加粗的文字再添加边框?


OK,完成了上述第一步,事情还没完,更可怕的问题来了。


现在文字要在二次加粗的情况下,再添加一个不同颜色的边框。


我们把原本可能可以给文字添加边框的 -webkit-text-stroke 属性用掉了,这下事情变得有点棘手了。这个问题也可以转变为,如何给文字添加 2 层不同颜色的边框?


当然,这也难不倒强大的 CSS(SVG),让我们来尝试下。


尝试方法一:使用文字的伪元素放大文字


第一种尝试方法,有点麻烦。我们可以对每一个文字进行精细化处理,利用文字的伪元素稍微放大一点文字,将原文字和访达后的文字贴合在一起。



  1. 将文字拆分成一个一个独立元素处理

  2. 利用伪元素的 attr() 特性,利用元素的伪元素实现同样的字

  3. 放大伪元素的字

  4. 叠加在原文字之下


上代码:


<ul>
<li data-text="文">文</li>
<li data-text="字">字</li>
<li data-text="加">加</li>
<li data-text="粗">粗</li>
<li data-text="C">C</li>
<li data-text="S">S</li>
<li data-text="S">S</li>
</ul>

ul {
display: flex;
flex-wrap: nowrap;
}

li {
position: relative;
font-size: 64px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 3px #000;

&::before {
content: attr(data-text);
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
color: red;
-webkit-text-stroke: 3px #f00;
z-index: -1;
transform: scale(1.15);
}
}

可以简单给上述效果加个动画,一看就懂:



CodePen Demo -- 利用伪元素给加粗文字添加边框


看着不错,但是实际上仔细观察,边框效果很粗糙,文字每一处并非规则的被覆盖,效果不太能接受:



尝试方法二:利用 text-shadow 模拟边框


第一种方法宣告失败,我们继续尝试第二种方式,利用 text-shadow 模拟边框。


我们可以给二次加粗的文字添加一个文字阴影:


<p>文字加粗CSS</p>

p {
font-size: 48px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 1px #000;
text-shadow: 0 0 2px red;
}

看看效果:


image


好吧,这和边框差的也太远了,它就是阴影。


不过别着急,text-shadow 是支持多重阴影的,我们把上述的 text-shadow 多叠加几次:


p {
font-size: 48px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 1px #000;
- text-shadow: 0 0 2px red;
+ text-shadow: 0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red;
}


Wow,不仔细看的话,利用这种叠加多层 text-shadow 的方式,还真的非常像边框!


当然,如果我们放大来看,瑕疵就比较明显了,还是能看出是阴影:



CodePen Demo -- 利用 text-shadow 给文字添加边框


尝试方法四:利用多重 drop-shadow()


在尝试了 text-shadow 之后,自然而然的就会想到多重 filter: drop-shadow(),主观上认为会和多重 text-shadow 的效果应该是一致的。


不过,实践出真知。


在实际测试中,发现利用 filter: drop-shadow() 的效果比多重 text-shadow 要好,模糊感会弱一些:


p {
font-weight: bold;
-webkit-text-stroke: 1px #000;
filter:
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red);
}

效果如下:


image


我们甚至可以利用它制作文字二次加粗后的多重边框:


p {
font-weight: bold;
-webkit-text-stroke: 1px #000;
filter:
drop-shadow(0 0 0.2px red)
// 重复 N 次
drop-shadow(0 0 0.2px red)
drop-shadow(0 0 0.25px blue)
// 重复 N 次
drop-shadow(0 0 0.25px blue);
}

效果如下:



然而,在不同屏幕下(高清屏和普通屏),drop-shadow() 的表现效果差别非常之大,实则也难堪重用。


我们没有办法了吗?不,还有终极杀手锏 SVG。


尝试方法四:利用 SVG feMorphology 滤镜给文字添加边框


其实利用 SVG 的 feMorphology 滤镜,可以非常完美的实现这个需求。


这个技巧,我在 有意思!不规则边框的生成方案 这篇文章中也有提及。


借用 feMorphology 的扩张能力给不规则图形添加边框


直接上代码:


<p>文字加粗CSS</p>

<svg width="0" height="0">
<filter id="dilate">
<feMorphology in="SourceAlpha" result="DILATED" operator="dilate" radius="2"></feMorphology>
<feFlood flood-color="red" flood-opacity="1" result="flood"></feFlood>
<feComposite in="flood" in2="DILATED" operator="in" result="OUTLINE"></feComposite>

<feMerge>
<feMergeNode in="OUTLINE" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</svg>

p {
font-size: 64px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 2px #000;
filter: url(#dilate);
}

效果如下:



我们可以通过 SVG feMorphology 滤镜中的 radius 控制边框大小,feFlood 滤镜中的 flood-color 控制边框颜色。并且,这里的 SVG 代码可以任意放置,只需要在 CSS 中利用 filter 引入即可。


本文不对 SVG 滤镜做过多的讲解,对 SVG 滤镜原理感兴趣的,可以翻看我上述提到的文章。


至此,我们就完美的实现了在已经利用 font-weight: bold-webkit-text-stroke 的基础上,再给文字添加不一样颜色的边框的需求。


放大了看,这种方式生成的边框,是真边框,不带任何的模糊:



CodePen Demo -- 利用 SVG feMorphology 滤镜给文字添加边框


最后


OK,本文到此结束,介绍了一些 CSS 中的奇技淫巧去实现文字二次加粗后加边框的需求,实际需求中,如果不是要求任意字都要有这个效果,其实我更推荐切图大法,高保真,不丢失细节。


当然,可能还有更便捷更有意思的解法,欢迎在评论区不吝赐教。


希望本文对你有所帮助 :)


想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄


更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。


如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。


作者:chokcoco
链接:https://juejin.cn/post/7023940690476269605

收起阅读 »

大道至简,繁在人心:在浏览器控制台安装npm包是什么操作?

  我们都知道,npm 是 JavaScript 世界的包管理工具,并且是 Node.js 平台的默认包管理工具。通过 npm 可以安装、共享、分发代码,管理项目依赖关系。虽然作为命令行工具的 npm 近年来逐渐式微,但是作为广泛使用的存储库的 npm,却依然...
继续阅读 »

  我们都知道,npm 是 JavaScript 世界的包管理工具,并且是 Node.js 平台的默认包管理工具。通过 npm 可以安装、共享、分发代码,管理项目依赖关系。虽然作为命令行工具的 npm 近年来逐渐式微,但是作为广泛使用的存储库的 npm,却依然如日中天,还是世界上最大的软件注册表


  通常,我们通过npm install xxx在 React、Vue、Angular 等现代前端项目中安装依赖,但是前端项目在本质上还是运行在浏览器端的 HTML、JavaScript 和 CSS,那么,我们有办法在浏览器控制台直接安装 npm 包并使用吗?


  如果你对这个问题感兴趣,不妨跟着我通过本文一探究竟,也许最终你会发现:越是“复杂”的东西,其原理越趋向“简单”


通过 <script /> 引入 cdn 资源


  在浏览器控制台安装 npm 包,看起来是个天马行空的想法,让人觉得不太切实际。如果我换一个方式进行提问:如何在浏览器/HTML 中引入 JavaScript 呢?也许你马上就有了答案:<script />标签。没错,我们的第一步就是通过 <script />标签在 HTML 页面上引入 cdn 资源。


  那么,又该如果在控制台在页面上插入<script />标签来引入 CDN 资源呢?这个问题可难不倒你


// 在页面中插入<script />标签
const injectScript = (url) => {
const script = document.createElement('script');
script.src = url;
document.body.appendChild(script);
};

  我们还得在资源引入后以及出现错误时,给用户一些提示:


script.onload = () => {
console.log(pkg_name_origin, ' 安装成功。');
};
script.onerror = () => {
console.log(pkg_name_origin, ' 安装失败。');
};

  这么以来,我们就可以直接在控制台引入 cdn 资源了,你可以再额外补充一些善后工作的处理逻辑,比如把<script />标签移除。当然,你也完全可以通过创建<link />标签来引入css样式库,这里不过多赘述。


根据包名安装 npm 包


  上面实现了通过<script /> 引入 cdn 资源,但是我们安装 npm 包一般都是通过npm install后面直接跟包名来完成的,显然单靠<script />的方式难以达到我们的饿预期,那么,有没有一种方式,可以将我们的包名直接转换成 cdn 资源地址呢?


  答案当然是:有。否则我写个屁啊 🤔,cdnjs就提供了这样的能力。


  cdnjs 提供了一个简单的 API,允许任何人快速查询 CDN 上的资源。具体使用读者可参考官方链接,这里给出一个根据包名查询 CDN 资源链接的示例,可以直接在浏览器地址栏打开这个链接查看:https://api.cdnjs.com/libraries?search=jquery,这是一个 get 请求,你将看到类似下面的页面,数组的第一项为名称/功能最相近的资源的最新 CDN 资源地址


jquery


  是以,根据包名搜索 cdn 资源 URL 便有如下的实现:


const cdnjs = async (name) => {
const searchPromise = await fetch(
`https://api.cdnjs.com/libraries?search=${name}`,
// 不显示referrer的任何信息在请求头中
{ referrerPolicy: 'no-referrer' }
);
const { results, total } = await searchPromise.json();
if (total === 0) {
console.error('Sorry, ', name, ' not found, please try another keyword.');
return;
}

// 取结果中最相关的一条
const { name: exactName, latest: url } = results[0];
if (name !== exactName) {
// 如果名称和你传进来的不一样
console.log(name, ' not found, import ', exactName, ' instead.');
}
// 通过<script />标签插入
injectScript(url);
};

安装特定版本的 npm 包


  我们在 npm 中还可以通过类似npm install jquery@3.5.1的语法安装特定版本的 npm 包,而 cdnjs 只能返回特定版本的详细信息(不含 cdn 资源链接)。


  UNPKG在此时可以帮我们一个大忙。unpkg 是一个快速的全球内容分发网络,适用于 npm 上的所有内容。使用它可以使用以下 URL 快速轻松地从任何包加载任何文件unpkg.com/:package@:version/:file


  例如,访问https://unpkg.com/jquery@3.5.1会自动重定向到https://unpkg.com/jquery@3.5.1/dist/jquery.js,并返回v3.5.1版本的jQuery文件内容(如果不带版本号,会返回最新的资源):


jquery_unpkg


  也就是说,我们可以将https://unpkg.com/包名直接丢给<script />标签来加载资源:


const unpkg = (name) => {
injectScript(`https://unpkg.com/${name}`);
};

完整代码


  将上面的代码简单整理,并通过一个统一的入口方法npmInstall进行调用:


// 存储原始传入的名称
let pkg_name_origin = null;
const npmInstall = (originName) => {
// Trim string
const name = originName.trim();
pkg_name_origin = name;
// 三种引入方式
// 如果是一个有效的URL,直接通过<script />标签插入
if (/^https?:\/\//.test(name)) return injectScript(name);
// 如果指定了版本,尝试使用unpkg加载
if (name.indexOf('@') !== -1) return unpkg(name);
// 否则,尝试使用cdnjs搜索
return cdnjs(name);
};

// 在页面中插入<script />标签
const injectScript = (url) => {
const script = document.createElement('script');
script.src = url;
script.onload = () => {
console.log(pkg_name_origin, ' 安装成功。');
};
script.onerror = () => {
console.log(pkg_name_origin, ' 安装失败。');
};
document.body.appendChild(script);
// document.body.removeChild(script);
};

const unpkg = (name) => {
injectScript(`https://unpkg.com/${name}`);
};

const cdnjs = async (name) => {
const searchPromise = await fetch(
`https://api.cdnjs.com/libraries?search=${name}`,
// 不显示referrer的任何信息在请求头中
{ referrerPolicy: 'no-referrer' }
);
const { results, total } = await searchPromise.json();
if (total === 0) {
console.error('Sorry, ', name, ' not found, please try another keyword.');
return;
}

// 取结果中最新的一条
const { name: exactName, latest: url } = results[0];
if (name !== exactName) {
console.log(name, ' not found, import ', exactName, ' instead.');
}
// 通过<script />标签插入
injectScript(url);
};

  我们可以使用类似npmInstall('moment')的方式在控制台进行调用:


console


  下面这些调用方式自然也是支持的:


npmInstall('jquery'); // 直接引入
npmInstall('jquery@2'); // 指定版本
npmInstall('https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'); // cdn地址

不每次都写这些函数行不行


  看了上面的操作,确实很简单,但是也许你会说:每次要使用时,我都得在控制台定义和调用函数,有些麻烦,不每次都写这些函数行不行?那自然是行的啦,你完全可以自己写一个浏览器插件,将这些JS代码注入页面,详情可参考7分钟学会写一个浏览器插件——突破某SDN未登录禁止复制的限制


  如果你实在不想写,其实有人已经为你写好了,那便是Console Importer,它可以让你的浏览器控制台成为更强大的实验场



  • 使用示例:


import



  • 效果图:


Console Importer



链接:Console Importer | Chrome 插件地址



可以干什么


  那么,本文介绍的方法和工具到底有什么用呢?


  平时开发中,我们经常会想要在项目里尝试一些操作或者验证一些库的方法、打印结果,通过本文的学习,以后你完全可以直接在控制台引入loadsh、moment、jQuery、React 等来进行使用和验证,减少在项目中进行console.log验证后再删除的频次。



  • 你可以通过引入jQuery方便的进行一些项目、页面中的DOM操作;

  • 你可以通过引入axios进行一些简单的接口请求;

  • 你可以通过引入moment.js来验证一些时间格式化方法的使用;

  • 你可以通过引入loadsh并调用它的方法完成一些便捷的计算;

  • ...


可以学到什么


unpkg


  unpkg 是一个内容源自 npm 的前端常用全球快速 CDN,它能以快速、简洁、优雅的方式提供任意包、任意文件的访问,在流行的类库、框架文档中常常能看到它的身影。使用方式一般是unpkg.com/:package@:version/:file。或者更简洁一点:https://unpkg.com/包名,包名包含版本号时,你将获得对应版本的js文件,不包含版本号时,你将获得这个库的最新版js文件。


cdnjs


  cdnjs是一种免费的开源 CDN 服务,受到超过 12.5% 的网站的信任,每月处理超过 2000 亿次请求,由 Cloudflare 提供支持。它类似 Google CDN 和微软CDN服务,但是速度比这二者更加快。CDNJS 上提供了众多 JavaScript 库,你可以直接在网页上引用这些 JS 文件,实现用户浏览网站的最佳速度体验。


  你还可以通过它的查询APIhttps://api.cdnjs.com/libraries?search=xxx进行特定库的cdn地址的查找,这个API还会给你返回一些你所查询的库的替代品


大道至简,繁在人心


  越是“复杂”的东西,其原理也许越是趋向“简单”,大道至简,繁在人心,祝每一个努力攀登者,终能豁然开朗,释然于心。


作者:獨釣寒江雪
链接:https://juejin.cn/post/7023916328637431816

收起阅读 »

微信小程序统一分享,全局接管页面分享消息的一些技巧

分享功能非常的重要,当某一个功能或文章打动用户的时候,能把这个小程序分享出去,就能带来裂变传播的效果。 全局接管分享事件 而随着功能越来越多,页面越来越多,每一个页面都需要添加分享的回调方法吗? onShareAppMessage: function () {...
继续阅读 »

分享功能非常的重要,当某一个功能或文章打动用户的时候,能把这个小程序分享出去,就能带来裂变传播的效果。


全局接管分享事件


而随着功能越来越多,页面越来越多,每一个页面都需要添加分享的回调方法吗?


onShareAppMessage: function () {
return {
title: '分享的标题',
path: '分享的页面路径'
}
},

有没有办法能全局统一接管分享呢?写一次,所有页面就都可以分享了。


能!


由于onShareAppMessage是一个函数,在用户点击右上角...时触发,或者<button open-type='share'>时触发。所以我们只要在这之前替换掉这个函数就可以了。


通过wx.onAppRoute(cb)这个方法,我们可以监听到微信小程序页面栈的变化。


//在小程序启动时添加全局路由变化的监听
onLaunch(){
wx.onAppRoute(()=>{
console.log('page stack changed');
console.log(getCurrentPages());
});
}

onAppRoute会在页面栈改变后被触发,这个时候通过getCurrentPages()方法,我们可以拿到小程序中全部的页面栈。


数组最后一个就是当前页面


image.png


现在直接给当前页面这个对象赋值onShareAppMessage即可


var pages = getCurrentPages();
var curPage = pages[pages.length-1];

curPage.onShareAppMessage=()=>{
return {
title:"被接管了"
}
}

再分享时我们就会发现被接管了


image.png


获取当前页面的地址


page参数不传的话,默认转发出去就是当前页面的地址。当然通过curPage.route也可以获取该页面地址。


var pages = getCurrentPages();
var curPage = pages[pages.length-1];

curPage.onShareAppMessage=()=>{
return {
title:"被接管了",
page:curPage.route
}
}

小技巧


如果就这样分享出去,用户打开的时候,就会直接展示这个分享的页面。直接返回,或者左滑屏幕,都会直接退出到聊天界面。用户主动分享一次产生的裂变不容易,我希望这个分享带来的价值最大化,让接到分享的微信用户看到更多页面的话怎么办呢?


永远先进首页,首页检查启动参数后再跳转相关页面


curPage.onShareAppMessage=()=>{
return {
title:"被接管了",
page:"/pages/home/home?url="+curPage.route
}
}


作者:大帅老猿
链接:https://juejin.cn/post/7024046727820738573

收起阅读 »

我阅读源码的五步速读法

阅读代码是程序员最重要的技能之一,我们每天都在读同事的代码或者第三方库的代码,那怎么高效的阅读代码呢?分享下我的源码阅读方法。 我的阅读源码的方法分为五步: 第一步,通过文档和测试用例了解代码的功能 阅读源码前要先了解代码的功能,可以通过文档或者测试用例,了解...
继续阅读 »

阅读代码是程序员最重要的技能之一,我们每天都在读同事的代码或者第三方库的代码,那怎么高效的阅读代码呢?分享下我的源码阅读方法。


我的阅读源码的方法分为五步:


第一步,通过文档和测试用例了解代码的功能


阅读源码前要先了解代码的功能,可以通过文档或者测试用例,了解代码做了什么,输入和输出是什么。


了解功能是阅读源码的基础,后面才会有方向感。


第二步,自己思考功能的实现方式


知道了源码有啥功能之后,要先思考下如果自己实现会怎么做。有个大概的思路就行。


如果想不通可以看下源码用到了哪些依赖库,这些依赖库都有啥功能,再想下应该怎么实现。


如果还想不通也没关系,重要的是要先自己思考下实现方式。


第三步,粗读源码理清实现思路


你已经有了一个大概的实现思路,然后再去读源码,看下它是怎么实现的。和你思路类似的地方很快就可以掠过去,而且印象也很深,和你思路不一样的地方,通过读代码搞清楚它的实现思路。


这一步不用关心细节,知道某段代码是干啥的就行,关键是和自己的思路做 diff,理清它的整体实现思路。


第四步,通过 debugger 理清实现细节


粗读源码理清了实现思路之后,对于一些部分的具体实现可能还不是很清楚,这时候就可以通过 debugger 来断点调试了。


构造一个能触发该功能的测试用例,在关心的代码处打一个断点,通过 debugger 运行代码。


这时候你已经知道这部分代码是干啥的了,单步调试也很容易理清每一条语句的功能,这样一条语句一条语句的搞懂之后,你就很容易能把这部分代码的实现细节理清楚。


这样一部分一部分的通过 debugger 理清细节实现之后,你就对整体代码的思路和细节的实现都有了比较好的掌握。


第五步,输出文章来讲述源码实现思路


当你觉得对源码的实现有了比较好的掌握的时候,可以输出一篇文章的方式来讲述源码的整体思路。


因为可能会有一些部分是你没注意到的,而在输出的过程中,会进行更全面的思考,这时候如果发现了一些没有读到的点,可以再通过前面几步去阅读源码,直到能清晰易懂的把源码的实现讲清楚。这样才算真正的把代码读懂了。


这就是我觉得比较高效的阅读源码的方法。


总结


我阅读源码的方法分为五步:



  1. 通过文档和测试用例了解代码的功能

  2. 自己思考功能的实现方式

  3. 粗读源码理清实现思路

  4. 通过 debugger 理清实现细节

  5. 输出文章来讲述源码实现思路


这五步缺一不可:



  • 缺了第一步,不了解功能就开始读源码,那读代码会没有方向感

  • 缺了第二步,不经过思考直接读源码,理解代码实现思路的效率会降低

  • 缺了第三步,不理清整体思路就开始 debugger,会容易陷入细节,理不清整体的思路

  • 缺了第四步,不 debugger 只大概理下整体思路,这样不能从细节上真正理清楚

  • 缺了第五步,不通过输出文章来检验,那是否真的理清了整体思路和实现细节是没底的


当然,这是我个人的阅读源码的方法,仅供参考。


作者:zxg_神说要有光
链接:https://juejin.cn/post/7024084789929967646

收起阅读 »

复杂web动画,不慌,选择 web Animations API

说动前端动画,我们熟知的有两种 CSS 动画 (requestAnimation/setTimeout/setInterval + 属性改变) 动画 当然有人可能会说canvas动画,从运动本质了还是第二种。 今天说的是第三种 Web Animations...
继续阅读 »

说动前端动画,我们熟知的有两种



  1. CSS 动画

  2. (requestAnimation/setTimeout/setInterval + 属性改变) 动画


当然有人可能会说canvas动画,从运动本质了还是第二种。


今天说的是第三种 Web Animations API, 也有简称为 WAAPI 的。


与纯粹的声明式CSS不同,JavaScript还允许我们动态地将属性值设置为持续时间。 对于构建自定义动画库和创建交互式动画,Web动画API可能是完成工作的完美工具。


举两个栗子


落球


点击之后,球体下落


ballFall2.gif


const ballEl = document.querySelector(".ball");
ballEl.addEventListener("click", function () {
let fallAni = ballEl.animate({
transform: ['translate(0, 0)', 'translate(20px, 8px)', 'translate(50px, 200px)']
}, {
easing: "cubic-bezier(.68,.08,.89,-0.05)",
duration: 2000,
fill: "forwards"
})
});

直播的世界消息或者弹幕


这是一个我们项目中一个实际的例子, 直播的弹幕。

我们需要消息先运动到屏幕中间,消息最少需要在停留2秒,如果消息过长,消息还需要 匀速滚动 ,之后再滑出屏幕。



  1. 滑入

  2. 暂停,如果消息过长,消息还需要匀速滚动

  3. 滑出


难点就在于,暂停阶段,消息滚动的时间并不是确定的,需要计算。 这个时候,纯CSS3的动画,难度就有些高了,采用 Web Animations API,天然的和JS亲和,那就简单多了。


先看看效果
longDan2.gif


shortDan.gif


代码也就简单的分为三段:滑入,暂停,滑出。

因为其天然支持Promise, 代码很简洁,逻辑也很清晰。


async function startAnimate() {
// 滑入
const totalWidth = stageWidth + DANMU_WITH;
const centerX = stageWidth * 0.5 - DANMU_WITH * 0.5;
const kfsIn = {
transform: [`translateX(${totalWidth}px)`, `translateX(${centerX}px)`]
}
await danmuEl.animate(kfsIn, {
duration: 2000,
fill: 'forwards',
easing: 'ease-out'
}).finished;

// 暂停部分
const contentEl = danmuEl.querySelector(".danmu-content");
const itemWidth = contentEl.getBoundingClientRect().width;
const gapWidth = Math.max(0, itemWidth - DANMU_WITH);
const duration = Math.max(0, Math.floor(gapWidth / 200) * 1000);

const translateX = duration > 0 ? gapWidth : 0;
const kfsTxt = {
transform: [`translateX(0px)`, `translateX(-${gapWidth}px)`]
};
await contentEl.animate(kfsTxt, {
duration,
delay: 2000,
fill: 'forwards',
easing: 'linear',
}).finished;

// 滑出
const kfsOut = {
transform: [`translateX(${centerX}px)`, `translateX(-${DANMU_WITH}px)`]
};
await danmuEl.animate(kfsOut, {
duration: 2000,
fill: "forwards",
easing: 'ease-in'
}).finished;

if (danmuEl) {
stageEl.removeChild(danmuEl);
}
isAnimating = false
}

web Animations API 两个核心的对象



  1. KeyframeEffect 描述动画属性

  2. Animation 控制播放


KeyframeEffect


描述动画属性的集合,调用keyframesAnimation Effect Timing Properties。 然后可以使用 Animation 构造函数进行播放。


其有三种构建方式,着重看第二种,参数后面说。



new KeyframeEffect(target, keyframes);

new KeyframeEffect(target, keyframes, options)

new KeyframeEffect(source)



当然我们可以显示的去创建 KeyframeEffect, 然后交付给Animation去播放。 但是我们通常不需要这么做, 有更加简单的API, 这就是接后面要说的 Element.animate


看一个KeyframeEffect复用的例子,new KeyframeEffect(kyEffect)基于当前复制,然后多处使用。


const box1ItemEl = document.querySelector(".box1");
const box2ItemEl = document.querySelector(".box2");

const kyEffect = new KeyframeEffect(null, {
transform: ['translateX(0)', 'translateX(200px)']
},
{ duration: 3000, fill: 'forwards' })

const ky1 = new KeyframeEffect(kyEffect);
ky1.target = box1ItemEl;

const ky2 = new KeyframeEffect(kyEffect);
ky2.target = box2ItemEl;

new Animation(ky1).play();
new Animation(ky2).play();


kf2.gif


Animation


提供播放控制、动画节点或源的时间轴。 可以接受使用 KeyframeEffect 构造函数创建的对象作为参数。


const box1ItemEl = document.querySelector(".box1");

const kyEffect = new KeyframeEffect(box1ItemEl, {
transform: ['translateX(0)', 'translateX(200px)']
},
{ duration: 3000, fill: 'forwards' })

const ani1 = new Animation(kyEffect);
ani1.play();

ani1.gif


常用的方法



Animation 事件监听


监听有两种形式:



  1. event 方式


因其继承于EventTarget,所有依旧有两种形式


animation.onfinish = function() {
element.remove();
}

animation.addEventListener("finish", function() {
element.remove();
}


  1. Promise形式


animation.finished.then(() =>
element.remove()
)

比如一个很有用的场景,所有动画完成后:


Promise.all( element.getAnimations().map(ani => ani.finished)
).then(function() {
// do something cool
})

常用事件回调




便捷的 Element.animate


任何 Element都具备该方法, 其语法:



animate(keyframes, options)



其参数和 new KeyframeEffect(target, keyframes, options)的后两个参数基本一样, 返回的是一个Animation对象。


第一个参数 keyframes


keyframes有两种形式,一种是数组形式,一种是对象形式。


数组形式


一组对象(关键帧) ,由要迭代的属性和值组成。

关键帧的偏移可以通过提供一个offset来指定 ,值必须是在 [0.0, 1.0] 这个区间内,且须升序排列。简单理解就是进度的百分比的小数值。


element.animate([ { opacity: 1 },
{ opacity: 0.1, offset: 0.7 },
{ opacity: 0 } ],
2000);

并非所有的关键帧都需要设置offset。 没有指定offset的关键帧将与相邻的关键帧均匀间隔。


对象形式


一个包含key-value键值的对象需要包含动画的属性和要循环变化的值数组


element.animate({
opacity: [ 0, 0.9, 1 ],
offset: [ 0, 0.8 ], // [ 0, 0.8, 1 ] 的简写
easing: [ 'ease-in', 'ease-out' ],
}, 2000);

第二个参数 options


new KeyframeEffect(target, keyframes, options)的第三个参数基本一致,但是多了一个可选属性,就是id,用来标记动画,也方便 在Element.getAnimations结果中精确的查找。





















































后续四个特性相对高级,掌握好了可以玩出花来,本章主要讲基本知识,后续会出高级版本。


更多细节可以参见 KeyframeEffect


Element.getAnimations


我们通过Element.animate或者创建Animation给Element添加很多动画,通过这个方法可以获得所有Animation的实例。


在需要批量修改参数,或者批量停止动画的时候,那可是大杀器。


比如批量暂停动画:


box1ItemEl.getAnimations()
.forEach(el=> el.pause()) // 暂停全部动画

优势



  1. 相对css动画更加灵活

  2. 相对requestAnimation/setTimeout/setInterval 动画,性能更好,代码更简洁

  3. 天然支持Promise,爽爽爽!!!


你有什么理由拒绝她呢?


对比 CSS Animation


动画参数属性键对照表


参数设置值上的区别



  1. duration 参数只支持毫秒

  2. 迭代次数无限使用的是 JS的Infinity,不是字符串 "infinite"

  3. 默认动画的贝塞尔是linear,而不是css的ease


兼容性


整体还不错,Safari偏差。

如果不行, 加个垫片 web-animations-js


我们在实际的桌面项目上已经使用,非常灵活, nice!
image.png


总结


web Animations API 和 css动画,不是谁替换谁。结合使用,效果更佳。


复杂的逻辑动画,因为web Animations API和JS天然的亲和力,是更优的选择。



收起阅读 »

小程序原理 及 优化

小程序使用的是双线程 在这种架构中,视图层使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境 > 两者都是独立的模块,并不具备数据直接共享的通道。视图层和逻辑层的数据传输,要由 Native 的 JS...
继续阅读 »

小程序使用的是双线程



在这种架构中,视图层使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境 >




两者都是独立的模块,并不具备数据直接共享的通道。视图层和逻辑层的数据传输,要由 NativeJSBrigde 做中转



小程序的启动过程



  • 1、小程序初始化: 微信初始化小程序环境:包括 Js 引擎WebView 进行初始化,并注入公共基础库。 这步是微信做的,在用户打开小程序之前就已经准备好了,是小程序运行环境预加载。

  • 2、下载小程序代码包 对小程序业务代码包进行下载:下载的不是小程序的源代码,而是编译、压缩、打包之后的代码。

  • 3、加载小程序代码包 对下载完成对代码包进行注入执行。 此时,app.js、页面所在的 Js 文件和所有其他被require 的 Js 文件会被自动执行一次,小程序基础库会完成所有页面的注册。

  • 4、初始化小程序首页 拉取数据,从逻辑层传递到视图层,进行渲染


setData 的工作原理



  • 1、调用setData方法;

  • 2、逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,将待传输数据转换成字符串并拼接到特定的JS脚本, 并通过 evaluateJavascript 执行脚本将数据传输到渲染层。

  • 3、渲染层接收到后, WebView JS 线程会对脚本进行编译,得到待更新数据后进入渲染队列等待 WebView 线程空闲时进行页面渲染。

  • 4、WebView 线程开始执行渲染时,将 data setData 数据套用在WXML 片段上,得到一个新节点树。经过新虚拟节点树与当前节点树的 diff 对比,将差异部分更新到UI视图。最后,将 setData 数据合并到 data 中,并用新节点树替换旧节点树,用于下一次重渲染


小程序官方性能指标



  • 1、首屏时间不超过 5 秒

  • 2、渲染时间不超过 500ms

  • 3、每秒调用 setData 的次数不超过 20 次

  • 4、setData 的数据在 JSON.stringify 后不超过 256kb

  • 5、页面 WXML 节点少于 1000 个,节点树深度少于 30 层子节点数不大于 60 个

  • 6、所有网络请求都在 1 秒内返回结果;


小程序优化


1、分包并且使用


分包预加载(通过配置 preloadRule) 将访问率低的页面放入子包里,按需加载;启动时需要访问的页面及其依赖的资源文件应放在主包中。 不需要等到用户点击到子包页面后在下载子包,而是可以根据后期数据,做子包预加载,将用户在当先页可能点击的子包页面先加载,当用户点击后直接跳转;可以根据用户网络类型来判断的,当用户处于网络条件好时才预加载;


image.png


2、采用独立分包技术(感觉开普勒黄金流程源码可以独立分包)


主包+子包的方式,,如果要跳到子包里,还是会加载主包然后加载子包;采用独立分包技术,区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源;


3、异步请求可以在页面onLoad就加载


4、注意利用缓存


利用storage API, 对变动频率比较低的异步数据进行缓存,二次启动时,先利用缓存数据进行初始化渲染,然后后台进行异步数据的更新


5、及时反馈


及时对需要用户等待的交互操作进行反馈,避免用户以为小程序卡了 先反馈,再请求。比如说,点赞的按钮,可以先改变按钮的样式,再 发起异步请求。


6、可拆分的部分尽量使用自定义组件


自定义组件的更新并不会影响页面上其他元素的更新,各个组件具有独立的逻辑空间、数据、样式环境及 setData 调用


7、避免不当的使用onPageScroll


避免在onPageScroll 中执行复杂的逻辑,避免在onPageScroll中频繁使用setData,避免在onPageScroll中 频繁查询几点信息(selectQuery


8、减少在代码包中直接嵌入的资源文件;图片放在cdn,使用适当的图片格式


9、setData 优化


(1)与界面渲染无关的数据最好不要设置在 data 中,可以考虑设置在 page 对象的其他字段下;


this.setData({ 
a: '与渲染有关的字符串',
b: '与渲染无关的字符串'
})
// 可以优化为
this.setData({
a: '与渲染有关的字符串'
})
this.b = '与渲染无关的字符串'

(2)不要过于频繁调用 setData,将多次 setData 合并成一次 setData 调用


(3)数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示数据结构比较复杂包含长字符串,则不应使用setData来设置这些数据


(4)列表局部更新 在更新列表的某一个数据时。不要用 setData 进行全部数据的刷新。查找对应 id 的那条数据的下标(index是不会改变的),用 setData 进行局部刷新


this.setData({ 
`list[${index}]` = newList[index]
})

(5)切勿在后台页面进行setData(就是不要再页面跳转后使用setData) 页面跳转后,代码逻辑还在执行,此时多个webview是共享一个js进程;后台的setData操作会抢占前台页面的渲染资源;


10、避免过多的页面节点数


页面初始渲染时,渲染树的构建、计算节点几何信息以及绘制节点到屏幕的时间开销都跟页面节点数量成正相关关系,页面节点数量越多,渲染耗时越长。


每次执行 setData 更新视图,WebView JS 线程都要遍历节点树计算新旧节点数差异部分。当页面节点数量越多,计算的时间开销越大,减少节点树节点数量可以有效降低重渲染的时间开销。


11、事件使用不当


(1)去掉不必要的事件绑定(WXML中的bindcatch),从而减少通信的数据量和次数;
(2)事件绑定时需要传输targetcurrentTargetdataset,因而不要在节点的data前缀属性中放置过大的数据


12、逻辑后移,精简业务逻辑


就比如咱们生成分享图片,再比如领取新人券的时候将是否是新人是否符合风控条件和最终领券封装为一个接口


13、数据预拉取(重要


小程序官方为开发者提供了一个在小程序冷启动时提前拉取第三方接口的能力 developers.weixin.qq.com/miniprogram… 预拉取能够在小程序冷启动的时候通过微信后台提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间,从而提升小程序的打开速度


14、跳转时预拉取


可以在发起跳转前(如 wx.navigateTo 调用前),提前请求下一个页面的主接口并存储在全局 Promise 对象中,待下个页面加载完成后从 Promise 对象中读取数据即可


15、非关键渲染数据延迟请求


小程序会发起一个聚合接口请求来获取主体模块的数据,而非主体模块的数据则从另一个接口获取,通过拆分的手段来降低主接口的调用时延,同时减少响应体的数据量,缩减网络传输时间。


16、分屏渲染


在 主体模块 的基础上再度划分出 首屏模块 和 非首屏模块(比如京挑好货的猜你喜欢模块),在所有首屏模块都渲染完成后才会渲染非首屏模块和非主体模块,以此确保首屏内容以最快速度呈现


17、接口聚合,请求合并(主要解决小程序中针对 API 调用次数的限制)


在小程序中针对 API 调用次数的限制: wx.request (HTTP 连接)的最大并发限制是 10 个; wx.connectSocket (WebSocket 连接)的最大并发限制是 5 个;


18、事件总线,替代组件间数据绑定的通信方式


通过事件总线(EventBus),也就是发布/订阅模式,来完成由父向子的数据传递


19、大图裁剪为多块加载


20、长列表优化


(1)不要每次加载更多的时候 都用concat
每获取到新一页数据时,就把它们concatlist上去,这样就会导致每次setData时的list越来越长越来越长,渲染速度也就越来越慢
(2)分批setData,减少一次setData的数量。不要一次性setData list,而是把每一页一批一批地set Data到这个list中去


this.setData({ 
['feedList[' + (page - 1) + ']']: newVal,
})

(3)运用官方的 IntersectionObserver.relativeToViewport 将超出或者没进入可视区的 部分卸载掉(适用于一次加载很多的列表数据,超出了两屏高度所展示的内容)


image.png


this.extData.listItemContainer.relativeToViewport({ top: showNum * windowHeight, bottom: showNum * windowHeight }) 
.observe(`#list-item-${this.data.skeletonId}`, (res) => {
let { intersectionRatio } = res
if (intersectionRatio === 0) {
console.log('【卸载】', this.data.skeletonId, '超过预定范围,从页面卸载')
this.setData({
showSlot: false
})
} else {
console.log('【进入】', this.data.skeletonId, '达到预定范围,渲染进页面')
this.setData({
showSlot: true,
height: res.boundingClientRect.height
})
}
})

21、合理运用函数的防抖与节流,防止出现重复点击及重复请求出现 为避免频繁setData和渲染,做了防抖函数,时间是600ms


作者:甘草倚半夏
链接:https://juejin.cn/post/7023671521075806244

收起阅读 »