Giter Club home page Giter Club logo

blog's Introduction

issue-blog

Project setup

yarn install

Compiles and hot-reloads for development

yarn serve

Compiles and minifies for production

yarn build

Customize configuration

See Configuration Reference.

blog's People

Watchers

 avatar  avatar

blog's Issues

CSS伪类+filer实现背景图片毛玻璃效果

前言

众所周知,CSS3 中新增了一个filter属性,其中filter:blur()属性可以给图像设置高斯模糊效果,也就是我们常说的 毛玻璃 效果。

但是该属性只能给某个 dom 元素设置,并且包括该 dom 内的所有子元素都会被设置成毛玻璃效果。有时我们只想给背景图添加毛玻璃效果,那么该如何实现呢?

解决办法

我们可以利用 CSS3 中的伪类元素来解决。

原理就是将背景放置在当前元素的伪类元素中,然后将伪类元素设置为毛玻璃效果,这样就不会影响该元素内的其他元素啦。

我们需要使用::before这个伪类元素。

其中,我们需要将元素设置属性position: relative;或者position: absolute;。然后将伪类元素设置为position: absolute;。这样就可以控制伪类元素的大小啦。

如果想对html设置一个全屏背景呐,那么就需要将伪元素的 position 设置为 fixed

node版本管理与npm源管理

使用 NVM 管理不同的 Node.js 版本

安装 NVM

查资料得出,要使用 curl 或 wget 来安装(版本可以选用官网最新版):

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash

或:

wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash

注意:安装完了,重新打开 Terminal(iTerm2) 来重启会话

安装 Node.js

  1. 安装最新版 Node.js,命令:nvm install node
  2. 查看安装效果,命令:nvm use node

查看 Node 列表

nvm ls

使用 nrm 切换 npm 源

由于众所周知的原因,npm 源是很慢的,所以使用 nrm 来快速切换 npm 源

yarn add -global nrm

nrm ls

nrm use [name]

commit message撰写以及自动检查工具

本文主要讲解如何安装commit message 的撰写工具以提高效率,同时安装自动纠错工具来强制规范团队成员的 commit message 格式

Commitizen

Commitizen是一个撰写合格 Commit message 的工具。

安装命令如下。

npm install -g commitizen

然后,在项目目录里,运行下面的命令,使其支持 Angular 的 Commit message 格式。

npm

commitizen init cz-conventional-changelog --save --save-exact   

yarn

commitizen init cz-conventional-changelog --yarn --dev --exact

以后,凡是用到git commit命令,一律改为使用git cz。这时,就会出现选项,用来生成符合格式的 Commit message。

commitlint

我们可以使用commitlint来检查提交的git message是否符合规范

commitlint

安装commitlint

npm install --save-dev @commitlint/{cli,config-conventional}
echo "module.exports = {extends: ['@commitlint/config-conventional']};" > commitlint.config.js

安装husky

husky 是一个可以触发 git 钩子的一个库,方便我们在git操作的各个阶段执行各种命令

npm install --save-dev husky

然后在 文件中添加:

// package.json
{
  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }  
  }
}

这样,在每次提交commit message 之后 commitlint 就会自动检测提交的内容是否符合规范,如果不符合就会提交失败。

conventional-changelog-cli

conventional-changelog-cli 默认推荐的 commit 标准是来自angular项目,除了 angular 标准以外,目前集成了包括 atom, codemirror, ember, eslint, express, jquery 等项目的标准,具体可以根据自己口味来选用。

npm install -D conventional-changelog-cli

基本使用:

conventional-changelog -p angular -i CHANGELOG.md -s

10分钟了解前端图片压缩

10分钟了解前端图片压缩

前言

在前端开发中,我们经常会被产品要求实现一个图片上传的功能。
关于图片上传,一般会考虑的问题就是如果用户上传的图片体积太大怎么办。图片体积太大会带来一些问题:一是上传过程中用户等待时间太长,如果是移动端上传,同时网络状况不太好的情况下,那体验就更差了。二是优化上传的图片体积也能给公司节省不必要的流量支出((/ω\)。

常见的压缩方式及图片格式

有损压缩指在压缩文件大小的过程中,损失了一部分图片的信息,也即降低了图片的质量(即图片被压糊了),并且这种损失是不可逆的。常见的有损压缩手段是按照一定的算法将临近的像素点进行合并。压缩算法不会对图片所有的数据进行编码压缩,而是在压缩的时候,去除了人眼无法识别的图片细节。因此有损压缩可以在同等图片质量的情况下大幅降低图片的体积。例如 jpg 格式的图片使用的就是有损压缩。

无损压缩指的是在压缩图片的过程中,图片的质量没有任何损耗。我们任何时候都可以从无损压缩过的图片中恢复出原来的信息。压缩算法对图片的所有的数据进行编码压缩,能在保证图片的质量的同时降低图片的体积。例如 png、gif 使用的就是无损压缩。

类型 动画 压缩类型 浏览器支持 透明度
GIF 支持 无损压缩 所有 支持
PNG 不支持 无损压缩 所有 支持
  不支持 有损压缩 所有 不支持
WebP 不支持 无损压缩或有损压缩 Chrome、Firefox、Safari、Opera、Edge、Android 支持

压缩思路

  1. input 读取到 image/file ,检查大小。
  2. 使用 FileReader 将其转换为 base64 编码
  3. 新建 img ,使其 src 指向刚刚的 base64
  4. 新建 canvas ,将 img 画到 canvas 上
  5. 利用canvas.drawImage绘制画布、利用 canvas.toDataURL/toBlob 将 canvas 导出为 base64 或 Blob(压缩在此步骤实现)
  6. 将 base64 或 Blob 转化为 File

image

代码实现

// 图片文件转 Base64 编码
const file2DataURL = (file) => {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = function () {
      const dataURL = this.result;
      resolve(dataURL);
    };
  });
};

// dataURL 转 img 对象
const dataURL2Image = (dataURL) => {
  return new Promise((resolve) => {
    const img = new Image();
    img.src = dataURL;
    img.onload = () => {
      resolve(img);
    };
  });
};

// canvas 压缩图片
const canvas2dataURL = (img, quality = 0.8, type) => {
  const canvas = document.createElement("canvas");
  const { width, height } = img;
  canvas.width = width;
  canvas.height = height;
  const context = canvas.getContext("2d");
  context.drawImage(img, 0, 0, width, height);
  const compressedDataURL = canvas.toDataURL(type, quality);
  return compressedDataURL;
};

// dataURL 转 blob 流
const dataURL2Blob = (dataURL, type) => {
  const text = window.atob(dataURL.split(",")[1]);
  const buffer = new ArrayBuffer(text.length);
  const ubuffer = new Uint8Array(buffer);
  for (let i = 0; i < text.length; i++) {
    ubuffer[i] = text.charCodeAt(i);
  }
  const blob = new window.Blob([buffer], { type });
  return blob;
};

// 压缩
const compressImage = async (file, quality = 0.8) => {
  const { type } = file;
  const dataURL = await file2DataURL(file);
  const image = await dataURL2Image(dataURL);
  const compress = canvas2dataURL(image, quality, type);
  const blob = dataURL2Blob(compress, type);
  return blob;
};

理想很丰满,现实很骨感

上面我们已经实现了一个简单的图片压缩函数,主要就是利用了 canvas.toDataURL/toBlob 自带的压缩能力来实现的图片压缩。我们先来看看这两个api的定义:

toDataURL
image

toBlob
image

从定义上我们可以看到,canvas的压缩只支持 jpg和webp格式,也就是说canvas的压缩只支持有损压缩,而像 png这种不支持有损压缩的图片格式反而会出现压缩之后图片变大的情况出现。

png

对于png这样的图片,我们就没办法通过toDataUrl来进行压缩,前面还提到过,canvas还有一个drawImage的api,这个api主要用来绘制画布和图片信息的。我们可以通过将绘制的图片进行缩放,以直接改变图像分辨率的方式来减小图片的大小。不过这种方式对于最终生成的图片质量很难把握。

gif

而对于gif图片来说就更加复杂了,由于gif是由多张图片合成的”动画“,因此也不能采用直接缩放的方式来进行压缩。对于gif的压缩方式主要有两种方案,一种是”抽帧“,顾名思义,也就是通过减少帧数来实现压缩。第二种方式是”减色“,也就是牺牲单个图片的色值来达到压缩的目的。不过这两种方式都是人为的有损压缩,而gif是支持无损压缩的,所以对于gif图片来说,不建议采用这两种方式。具体的实现方案大家可以自行查阅资料

有个更加简便的方式就是:把上传的所有格式的图片都输出成jpg格式,这样就可以愉快的压缩了((/ω\)

其他缺点

  1. 由于我们的压缩能力是canvas提供的,所以canvas的缺点也会影响到我们的压缩能力。经过查阅资料发现,浏览器对于canvas的最大尺寸和最大像素有限制,而且不同浏览器的限制标准不同,甚至同一浏览器在不同硬件设备平台下的限制标准也不同,应该是为了限制内存占用。
    同时浏览器并未提供可返回最大尺寸的api。不过可以使用 canvas-size 来检测当前画布支持是否受支持。
    Maximum size of a element
  2. 由于设备的差异性,如果上传的图片过大,可能会出现压缩卡死的情况。
  3. 由于toDataUrl只是提供了压缩的比例,但我们在实际使用中一般需要将图片压缩到一个最大值以下,因此可能需要进行多次压缩。

因此在实际的使用中,我们需要设置一个值,来限制用户上传的最大图片,或者直接规定压缩出来的最大尺寸,具体要视业务需求来调整。

前人栽树,后人乘凉

在实际的业务中,不建议自己封装图片压缩函数,因为这里面的坑要比想象中多的多。还好已经有一些相对成熟的库,比如compressorjs

完美解决hexo下分类和标签无法显示的问题

概述

今天更换了一个新的主题,之后发现无法正常添加 分类页 和 标签页,经过一下午的研究,终于找到了最完美的解决方案。

解决方案

步骤一

你需要在 hexo 根目录的 source 文件夹下新建一个 tags 文件夹,然后在 tags 文件夹里面新建一个 index.md 文件。快捷命令为:

$ hexo new page "tags"

步骤二

编辑 index.md 文件,内容如下:

---
title: "tags"
type: tags
layout: "tags"
---

重点来了

注意!这里面最重要的就是 layout 选项,后面的参数对应的是你 主题文件夹下 layout 文件夹下第一级的布局文件。比如,我的主题是用 ejs 写的,那么对应的就是 layout/tags.ejs,如果没有,那么就会出现空白的现象!如果你的 tags 文件的命名时 a.ejs,那么你就应该写成 layout: "a"

步骤三

编辑主题配置文件:

nav:
  home: /
  about: /about
  tags: /tags

步骤四

编辑 hexo 配置文件 Directory 选项。

检查一下名称是否对应

# Directory
tag_dir: tags

至此,完美解决。

最重要的就是看一下你的主题文件里有没有标签页或者分类页的布局文件,一般来说都是有的,只是命名和存放的位置可能不同,所以大家要根据实际情况来修改。

自动化注册全局组件与 vuex modules

前言

在 vue 开发中,有很多地方需要做注册处理,非常繁琐。我们可以利用 webpack 的 require.context方法来做自动化注册。

自动化注册 vuex modules

代码如下:

/*----------  自动注册modules  ----------*/
export default function() {
  const requireComponent = require.context(
    // 其组件目录的相对路径
    "./modules",
    // 是否查询其子目录
    true,
    // 匹配js文件
    /\.js$/
  );

  const modules = {};

  requireComponent.keys().forEach(fileName => {
    // 获取配置
    const componentConfig = requireComponent(fileName);

    // 获取命名
    const componentName = fileName
      .split("/")
      .pop()
      .split(".")
      .shift();

    modules[componentName] = componentConfig.default || componentConfig;
  });

  // console.log("modules:", modules);
  return modules;
}

这样放在 modules 文件夹下的 js 文件就会被自动注册。

自动化注册全局组件

我们可以在 components 文件夹下新建一个 baseComponents 文件夹,然后把需要注册为全局的组件放在这里,就会被自动注册为全局组件。

代码如下:

import Vue from "vue";
import upperFirst from "lodash/upperFirst"; //转换字符串string的首字母为大写。
import camelCase from "lodash/camelCase"; //转换字符串string为 驼峰写法。

const requireComponent = require.context(
  // 其组件目录的相对路径
  "../baseComponents",
  // 是否查询其子目录
  true,
  // 匹配基础组件文件名的正则表达式,只匹配字母开头的组件
  /[A-Za-z]\w+\.(vue)$/
);

// console.log(requireComponent.keys());

requireComponent.keys().forEach(fileName => {
  // 获取组件配置
  const componentConfig = requireComponent(fileName);

  // 获取组件的 PascalCase 命名
  const componentName = upperFirst(
    camelCase(
      // 获取和目录深度无关的文件名
      "base-" +
        fileName
          .split("/")
          .pop()
          .replace(/\.\w+$/, "")
    )
  );
  // console.log("自注册全局组件:", componentName); //查看最终的组件名称

  // 全局注册组件
  Vue.component(
    componentName,
    // 如果这个组件选项是通过 `export default` 导出的,
    // 那么就会优先使用 `.default`,
    // 否则回退到使用模块的根。
    componentConfig.default || componentConfig
  );
});

Markdown

请使用 Chrome 浏览器。

请阅读下方文本熟悉工具使用方法,本文可直接拷贝到微信中预览。

1 Markdown Nice 简介

  • 支持自定义样式的 Markdown 编辑器
  • 支持微信公众号、知乎和稀土掘金
  • 欢迎扫码回复「排版」加入推文群

2 主题

https://product.mdnice.com/themes/

欢迎提交主题,提供更多文章示例~~

3 通用语法

3.1 标题

在文字写书写不同数量的#可以完成不同的标题,如下:

一级标题

二级标题

三级标题

3.2 无序列表

无序列表的使用,在符号-后加空格使用。如下:

  • 无序列表 1
  • 无序列表 2
  • 无序列表 3

如果要控制列表的层级,则需要在符号-前使用空格。如下:

  • 无序列表 1
  • 无序列表 2
    • 无序列表 2.1
    • 无序列表 2.2

由于微信原因,最多支持到二级列表

3.3 有序列表

有序列表的使用,在数字及符号.后加空格后输入内容,如下:

  1. 有序列表 1
  2. 有序列表 2
  3. 有序列表 3

3.4 粗体和斜体

粗体的使用是在需要加粗的文字前后各加两个*

而斜体的使用则是在需要斜体的文字前后各加一个*

如果要使用粗体和斜体,那么就是在需要操作的文字前后加三个*。如下:

这个是粗体

这个是斜体

这个是粗体加斜体

注:由于 commonmark 标准,可能会导致加粗与想象不一致,如下

**今天天气好晴朗,**处处好风光。

这个是正常现象,请参考加粗 Issue

3.5 链接

微信公众号仅支持公众号文章链接,即域名为https://mp.weixin.qq.com/的合法链接。使用方法如下所示:

对于该论述,欢迎读者查阅之前发过的文章,你是《未来世界的幸存者》么?

3.6 引用

引用的格式是在符号 > 后面书写文字,文字的内容可以包含标题、链接、图片、粗体和斜体等。

一级引用如下:

一级引用示例

读一本好书,就是在和高尚的人谈话。 ——歌德

Markdown Nice最全功能介绍

这里写图片描述

当使用多个 > 符号时,就会变成多级引用

二级引用如下:

二级引用示例

读一本好书,就是在和高尚的人谈话。 ——歌德

Markdown Nice最全功能介绍

这里写图片描述

三级引用如下:

三级引用示例

读一本好书,就是在和高尚的人谈话。 ——歌德

Markdown Nice最全功能介绍

这里写图片描述

3.7 分割线

可以在一行中用三个以上的减号来建立一个分隔线,同时需要在分隔线的上面空一行。如下:


3.8 删除线

删除线的使用,在需要删除的文字前后各使用两个~,如下:

这是要被删除的内容。

3.9 表格

可以使用冒号来定义表格的对齐方式,如下:

姓名 年龄 工作
小可爱 18 吃可爱多
小小勇敢 20 爬棵勇敢树
小小小机智 22 看一本机智书

宽度过长的表格可以滚动,可在自定义主题中调节宽度:

姓名 年龄 工作 邮箱 手机 姓名 年龄 工作 邮箱 手机
小可爱 18 吃可爱多 [email protected] 18812345678 小可爱 18 吃可爱多 [email protected] 18812345678
小小勇敢 20 爬棵勇敢树 [email protected] 17712345678 小小勇敢 20 爬棵勇敢树 [email protected] 17712345678
小小小机智 22 看一本机智书 [email protected] 16612345678

3.10 图片

插入图片,如果是行内图片则无图例,否则有图例,格式如下:

这里写图片描述

可以通过在图片尾部添加宽度和高度控制图片大小,用法如下:

![同时设置宽度和高度](https://camo.githubusercontent.com/a531e6c62c6695ac6f24123f2b4b0c046d32c39a4db04cbf6fb11258e998feec/68747470733a2f2f66696c65732e6d646e6963652e636f6d2f6c6f676f2e737667 =150x150)

![只设置宽度,推荐使用百分比](https://camo.githubusercontent.com/a531e6c62c6695ac6f24123f2b4b0c046d32c39a4db04cbf6fb11258e998feec/68747470733a2f2f66696c65732e6d646e6963652e636f6d2f6c6f676f2e737667 =40%x)

该语法比较特殊,其他 Markdown 编辑器不完全通用。

支持 jpg、png、gif、svg 等图片格式,其中 svg 文件仅可在微信公众平台中使用,svg 文件示例如下:

  • 支持图片拖拽和截图粘贴到编辑器中上传,上传时使用当前选择的图床。
  • 可使用格式->图片上传本地图片,网站目前支持「图壳」图床,失败率低,但是只可保存一天用于排版

注:仅支持 https 的图片,图片粘贴到微信、知乎或掘金时会自动上传其服务器,不必担心使用上述图床会导致图片丢失

图片还可以和链接嵌套使用,能够实现推荐卡片的效果,用法如下:

Markdown Nice 最全功能介绍

4. 特殊语法

4.1 脚注

支持平台:微信公众号、知乎。

脚注与链接的区别如下所示:

链接:[文字](链接)
脚注:[文字](脚注解释 "脚注名字")

有人认为在大前端时代的背景下,移动端开发(Android、IOS)将逐步退出历史舞台。

全栈工程师在业务开发流程中起到了至关重要的作用。

脚注内容请拉到最下面观看。

4.2 代码块

支持平台:微信公众号、知乎。

如果在一个行内需要引用代码,只要用反引号引起来就好,如下:

Use the printf() function.

在需要高亮的代码块的前一行及后一行使用三个反引号,同时第一行反引号后面表示代码块所使用的语言,如下:

// FileName: HelloWorld.java
public class HelloWorld {
  // Java 入口程序,程序从此入口
  public static void main(String[] args) {
    System.out.println("Hello,World!"); // 向控制台打印一条语句
  }
}

支持以下语言种类:

bash
clojure,cpp,cs,css
dart,dockerfile, diff
erlang
go,gradle,groovy
haskell
java,javascript,json,julia
kotlin
lisp,lua
makefile,markdown,matlab
objectivec
perl,php,python
r,ruby,rust
scala,shell,sql,swift
tex,typescript
verilog,vhdl
xml
yaml

如果想要更换代码主题,可在上方挑选,不支持代码主题自定义。

其中微信代码主题与微信官方一致,有以下注意事项:

  • 带行号且不换行,代码大小与官方一致
  • 需要在代码块处标志语言,否则无法高亮
  • 粘贴到公众号后,用鼠标点代码块内外一次,完成高亮

diff 不能同时和其他语言的高亮同时显示,且需要调整代码主题为微信代码主题以外的代码主题才能看到 diff 效果,使用效果如下:

+ 新增项
- 删除项

其他主题不带行号,可自定义是否换行,代码大小与当前编辑器一致

4.3 数学公式

支持平台:微信公众号、知乎。

行内公式使用方法,比如这个化学公式:$\ce{Hg^2+ ->[I-] HgI2 ->[I-] [Hg^{II}I4]^2-}$

块公式使用方法如下:

$$H(D_2) = -\left(\frac{2}{4}\log_2 \frac{2}{4} + \frac{2}{4}\log_2 \frac{2}{4}\right) = 1$$

矩阵:

$$ \begin{pmatrix} 1 & a_1 & a_1^2 & \cdots & a_1^n \\ 1 & a_2 & a_2^2 & \cdots & a_2^n \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & a_m & a_m^2 & \cdots & a_m^n \\ \end{pmatrix} $$

公式由于微信不支持,目前的解决方案是转成 svg 放到微信中,无需调整,矢量不失真。

目前测试如果公式量过大,在 Chrome 下会存在粘贴后无响应,但是在 Firefox 中始终能够成功。

4.4 TOC

支持平台:微信公众号、知乎。

TOC 全称为 Table of Content,列出全部标题。由于示例标题过多,需要使用将下方代码段去除即可。

[TOC]

由于微信只支持到二级列表,本工具仅支持二级标题和三级标题的显示。

4.5 注音符号

支持平台:微信公众号。

支持注音符号,用法如下:

Markdown Nice 这么好用,简直是{喜大普奔|hē hē hē hē}呀!

4.6 横屏滑动幻灯片

支持平台:微信公众号。

通过<![](url),![](url)>这种语法设置横屏滑动滑动片,具体用法如下:

<蓝1,绿2,红3>

5 其他语法

5.1 HTML

支持原生 HTML 语法,请写内联样式,如下:

橙色居右
橙色居中

5.2 UML

不支持,推荐使用开源工具https://draw.io/制作后再导入图片

5.3 更多文档

更多文档请参考 mdnice 产品主页

ESbuild 介绍

Esbuild 是什么

ESbuild 是一个类似webpack构建工具。它的构建速度是 webpack 的几十倍。

为什么这么快 ?

  1. js是单线程串行,esbuild是新开一个进程,然后多线程并行,充分发挥多核优势
  2. go是纯机器码,肯定要比JIT快
  3. 不使用 AST,优化了构建流程。(也带来了一些缺点,后面会说)

ESbuild 使用介绍

ESbulid 文档: esbuild.github.io/api/

Esbuild 有命令行 ,js 调用, go 调用三种使用方式。这里主要讲利用 js 调用的方式。

利用 esbuild 编译代码

esbuild 提供了 writeFileSync/writeFile 对 code 进行编译, demo 如下

require('fs').writeFileSync('in.ts', 'let x: number =   1')
require('esbuild').buildSync({
  entryPoints: ['in.ts'],
  outfile: 'out.js',
})
复制代码

利用 esbuild 处理 jsx 代码

require('esbuild').transformSync('<div/>', {
  jsxFactory: 'h', //默认为 React.CreateElement,可自定义, 如果你想使用 Vue 的 jsx 写法, 将该值换成为 Vue.CreateElement
  loader: 'jsx', // 将 loader 设置为 jsx 可以编译 jsx 代码
})

// 同上,默认为 React.Fragment , 可换成对应的 Vue.Fragment。
require('esbuild').transformSync('<>x</>', {
  jsxFragment: 'Fragment',
  loader: 'jsx',
})
复制代码

利用 esbuild 压缩代码体积

esbuild 提供了一个 minify 配置允许用户去压缩代码体积,实际 demo 如下

var js = 'fn = obj => { return obj.x }'
require('esbuild').transformSync(js, {
  minify: true,
})

// minify 后
{
  code: 'fn=n=>n.x;\n',
  map: '',
  warnings: []
}
复制代码

处理其他资源

与webpack不同的是,esbuild内置了一些文件处理的loader。 当esbuild解析到某后缀时,会自动使用该loader进行处理。 当然,你也可以手动指定对应的loader处理器,如你想使用jsx loader去处理js文件。可以按下面的实例进行配置。

目前Esbuild 内置了 js,jsx,ts,tsx,css,text,binary,dataurl,file类型的loader

require('esbuild').buildSync({
  entryPoints: ['app.js'],
  bundle: true,
  loader: { '.js': 'jsx' }, // 默认使用 js loader ,手动改为 jsx-loader 
  outfile: 'out.js',
})
复制代码

用esbuild启动一个web server用于调试(支持热更新)

require('esbuild').serve({}, {
  entryPoints: ['app.js'],
  bundle: true,
  outfile: 'out.js',
}).then(server => {
  // Call "stop" on the web server when you're done
  server.stop()
})
复制代码

在Webpack中使用esbuild

在当前前端环境中,直接使用 esbuild 代理 webpack 是不现实的。在目前的主流方案是在 webpack 中使用 esbuild 去做一些代码的 transform (代替 babel-loader)。

Webpack 构建流程

先讲讲 webpack 的构建流程: aotu.io/notes/2020/… Webpack 的构建流程简单来说就是递归编译每一个模块文件,对于不同类型的文件使用不同的 webpack loader 进行处理。我们要做的就是就是将 webpack 中做代码转化的步骤(babel-loader, ts-loader) esbuild-loader 代替。

esbuild-loader

esbuild-loader 就是一个这样的工具。简单讲下 esbuild-loader 如何使用。

首先你需要在 plugin 中引用 esbuild-plugin。 将 esbuild 的相关方法挂载在webpack中 然后就可以正常的配置 webpack 相关配置了。下面讲讲可以拿来干啥

  • 用 esbuild 代替 babel-loader(ts-loader同理) 做代码降级

  • 用 esbuild 代替 ts-loader 处理 ts 代码

  • Esbuild-loader 还可以用于minify代码

esbuild 的缺点

esbuild 同样不是完美的(如果真有那么完美为什么还没有大面积使用呢?),为了保证 esbuild 的编译效率,esbuild 没有提供 AST 的操作能力。所以一些通过 AST 处理代码的 babel-plugin 没有很好的方法过渡到 esbuild 中(说的就是你 babel-plugin-import)。so,如果你的项目使用了 babel-plugin-import, 或者一些自定义的 babel-plugin 。在目前来看是没有很好的迁移方案的。

生产环境使用 esbuild 的可行性

先说结论,在生产环境下使用 esbuild 是可行的。像 snowpack , vite 等构建工具都已经是用了 esbuild 作为代码处理工具(稳定性已经足够)。如果你一定要使用,可以看看是否符合下面标准

  1. 没有使用一些自定义的 babel-plugin (如 babel-plugin-import)
  2. 不需要兼容一些低版本浏览器(esbuild 只能将代码转成 es6)

那你就可以大胆使用 esbuild-loader 为你的项目提效了~

提效成果

可以看到升级完 esbuild 后提效明显。在冷启动阶段,打包速度能优化到 70% 在带 cache 的阶段,速度也能到 50%

使用 socket.io还是使用 websocket

前言

在开发聊天机器人的过程中,我遇到了一个选择,是直接使用 websocket 还是使用 socket.io。

我查找了很多资料,想了解清楚两者的差异,无奈很多文章都是大同小异,没什么有营养的东西,不过最终我还是在 stackoverflow 上找到了我想要的答案

原文解释了几个关于 websocket 和 socket.io 的误区,并给出实验结果。实验结果同学们可以自己看。

下面是作者指出的三个常见的认知误区:

  1. 使用 sokcet.io 要比直接用 websocket 简单很多? NO,直接使用websocket其实是非常方便的。
  2. 浏览器对 Websocket 的支持还不全面?IE10及以上全面支持,假如你要支持IE9那当我没说。
  3. sokcet.io首先尝试用Websocket建立连接,当老的浏览器不支持 Websocket 时,socket.io 会降级进行其他方式的连接?NO,实际情况不是这样的,socket.io 在初始连接的时候,是使用 AJAX 方式进行连接,在交换信息之后确定浏览器支持 Websocket 了,才升级到 Websocket 方式。

从这里可以看到这两者从明显的区别就是,socket.io 无论何时都是先用 AJAX 的方式建立连接的。然后会经过大概 3 个长轮询之后再升级到 Websocket。

比较

根据比较我们可以总结一下两者的使用场景:

Websocket 是一个 Web 标准,它非常轻巧,因为它本身是受浏览器支持的(除了 IE9 及以前版本)。如果你的浏览器支持Websocket,那无疑直接使用Websocket是最好的选择。

socket.io 支持更多的浏览器,如果你需要支持的浏览器不支持 Websocket,那 socket.io 无疑是你的选择。

参考

socket.io 和 websockets 之间的区别

浅谈vue组件通信

概述

组件系统是 vue 的一个重要的功能,由于各个组件分布在不同的文件中,因此如何进行组件之间的通信也成为了我们必须掌握的技能。

父子组件通信

父子组件通信需要用到 props$emit

父组件 -> 子组件

父组件向子组件通信可以使用props来传递变量,具体代码实现如下:

<input v-model="data" type="text" />
<child :name="data"></child>

import child from "./child"; //引入组件
export default {
  data() {
    return {
      data: null
    };
  },
  components: {
    child //注册组件
  }
};
<p></p>
<p v-text="name"></p>; //

export default {
  props: ["name"] //通信变量
};

子组件 -> 父组件

子组件向父组件通信可以使用$emit来传递变量,具体代码实现如下:

<child @getChildData="getData"></child>
<p v-text="child"></p>

import child from "./child";
export default {
  data() {
    return {
      child: null
    };
  },
  components: {
    child
  },
  methods: {
    getData(data) {
      this.child = data;
    }
  }
};
<input v-model="child" @input="setData" type="text" />

export default {
  data() {
    return {
      child: null
    };
  },
  methods: {
    setData() {
      this.$emit("getChildData", this.child);
    }
  }
};

同级组件通信

同级组件不能像父子组件那样直接传值,需要一个中间桥梁。我们需要先新建一个公共文件eventBus.js,然后添加两句代码:

import Vue from "vue";
export default new Vue();

然后在同级组件中引入eventBus.js文件

这里我们演示从 page1page2 通信:

<input @input="changeVal" v-model="inputData" type="text">

import bus from "../assets/eventBus";
export default {
  data() {
    return {
      inputData: null
    };
  },
  methods: {
    changeVal() {
      bus.$emit("newBusVal", this.inputData);
    }
  }
};
<p></p>
<p v-text="msg"></p>;

import bus from "../assets/eventBus";
export default {
  data() {
    return {
      msg: "我是page2的值"
    };
  },
  created() {
    bus.$on("newBusVal", data => {
      this.msg = data;
    });
  }
};

通过$emit$on触发和监听事件,就可以进行通信了。

git commit 规范

在日常的团队项目中,commit message 是十分重要的。因此,规范的commit message也是十分重要的。

Commit message 的作用

格式化的Commit message,有几个好处。

(1)提供更多的历史信息,方便快速浏览。

比如,下面的命令显示上次发布后的变动,每个commit占据一行。你只看行首,就知道某次 commit 的目的。

git log <last tag> HEAD --pretty=format:%s

(2)可以过滤某些commit(比如文档改动),便于快速查找信息。

比如,下面的命令仅仅显示本次发布新增加的功能。

git log <last release> HEAD --grep feature

(3)可以直接从commit生成Change log。

Change Log 是发布新版本时,用来说明与上一个版本差异的文档,详见后文。

Commit message 的格式

每次提交,Commit message 都包括三个部分:Header,Body 和 Footer。

():
// 空一行

// 空一行

其中,Header 是必需的,Body 和 Footer 可以省略。

不管是哪一个部分,任何一行都不得超过72个字符(或100个字符)。这是为了避免自动换行影响美观。

2.1 Header

Header部分只有一行,包括三个字段:type(必需)、scope(可选)和subject(必需)。

(1)type

type用于说明 commit 的类别,只允许使用下面7个标识。

  • feat:新功能(feature)
  • fix:修补bug
  • docs:文档(documentation)
  • style: 格式(不影响代码运行的变动)
  • refactor:重构(即不是新增功能,也不是修改bug的代码变动)
  • test:增加测试
  • chore:构建过程或辅助工具的变动

如果typefeatfix,则该 commit 将肯定出现在 Change log 之中。其他情况(docschorestylerefactortest)由你决定,要不要放入 Change log,建议是不要。

(2)scope

scope用于说明 commit 影响的范围,比如数据层、控制层、视图层等等,视项目不同而不同。

(3)subject

subject是 commit 目的的简短描述,不超过50个字符。

  • 以动词开头,使用第一人称现在时,比如change,而不是changedchanges
  • 第一个字母小写
  • 结尾不加句号(.

2.2 Body

Body 部分是对本次 commit 的详细描述,可以分成多行。下面是一个范例。

More detailed explanatory text, if necessary. Wrap it to
about 72 characters or so.

Further paragraphs come after blank lines.

  • Bullet points are okay, too
  • Use a hanging indent

有两个注意点。

(1)使用第一人称现在时,比如使用change而不是changedchanges

(2)应该说明代码变动的动机,以及与以前行为的对比。

2.3 Footer

Footer 部分只用于两种情况。

(1)不兼容变动

如果当前代码与上一个版本不兼容,则 Footer 部分以BREAKING CHANGE开头,后面是对变动的描述、以及变动理由和迁移方法。

BREAKING CHANGE: isolate scope bindings definition has changed.

To migrate the code follow the example below:

Before:

scope: {
myAttr: 'attribute',
}

After:

scope: {
myAttr: '@',
}

The removed inject wasn't generaly useful for directives so there should be no code using it.

(2)关闭 Issue

如果当前 commit 针对某个issue,那么可以在 Footer 部分关闭这个 issue 。

Closes #234

也可以一次关闭多个 issue 。

Closes #123, #245, #992

2.4 Revert

还有一种特殊情况,如果当前 commit 用于撤销以前的 commit,则必须以revert:开头,后面跟着被撤销 Commit 的 Header。

revert: feat(pencil): add 'graphiteWidth' option

This reverts commit 667ecc1654a317a13331b17617d973392f415f02.

Body部分的格式是固定的,必须写成This reverts commit &lt;hash>.,其中的hash是被撤销 commit 的 SHA 标识符。

如果当前 commit 与被撤销的 commit,在同一个发布(release)里面,那么它们都不会出现在 Change log 里面。如果两者在不同的发布,那么当前 commit,会出现在 Change log 的Reverts小标题下面。

结语

以上就是 commit message 的规范了。但是在实际开发中这样的书写格式其实是很繁琐的,因此我们需要一个半自动化的工具来提高书写效率,同时在合并代码之前自动检查成员的commit message是否符合规范。

在这篇 #9 文章中,我们将实现这一需求。

参考文章

本文基本参照阮一峰老师的Commit message 和 Change log 编写指南 一文书写,之所以分成两篇文章是因为
#9 一文的内容较原文内容有很大的改动与改进。

前端处理\n换行符

前言

在开发 xbot 和 ebot(是我们的智能聊天机器人)的过程中,websocket 返回的信息中有些会包含 \n 换行符。要知道在 HTML 中,是无法识别 \n 换行符的,究其原因是因为 \n是 js 中的换行,而 html 中的换行是<br />

解决

那么如何让 \n 的换行起效果呢,解决办法其实很多。

方法一 innerText

在目前的 HTML 标准下, innerText 会把 \n 替换为 <br>

举个栗子

let a = "\n 1";
let ele = document.createElement("div");
ele.innerText = a;
console.log(ele); //<div><br> 1</div>

方法二 使用 css

我们可以使用 css 属性 white-space来设置。white-space 属性详解

该属性有 6 个值。其中我们可以使用 pre、pre-wrap、pre-line 等值来实现换行

normal

连续的空白符会被合并,换行符会被当作空白符来处理。换行在填充「行框盒子(line boxes)」时是必要。

nowrap

和 normal 一样,连续的空白符会被合并。但文本内的换行无效。

pre

连续的空白符会被保留。在遇到换行符或者
元素时才会换行。

pre-wrap

连续的空白符会被保留。在遇到换行符或者
元素,或者需要为了填充「行框盒子(line boxes)」时才会换行。

pre-line

连续的空白符会被合并。在遇到换行符或者
元素,或者需要为了填充「行框盒子(line boxes)」时会换行。
break-spaces
与 pre-wrap 的行为相同,除了:

  • 任何保留的空白序列总是占用空间,包括在行尾。
  • 每个保留的空格字符后都存在换行机会,包括空格字符之间。
  • 这样保留的空间占用空间而不会挂起,从而影响盒子的固有尺寸(最小内容大小和最大内容大小)。

相比于 innerText 方法,使用 css 处理明显更方便和简单。

使用jquery/cheerio获取没有被标签包裹的文本内容

其实这个问题是我在使用cheerio的时候遇到的,cheerio是在 node 中使用的用来操作 dom 文档的一个库,使用方法和 jQuery 是一样的,属于 jQuery 的子集,所以此问题也适用于 jQuery。在使用 node 做爬虫的时候,经常会遇到一些文本内容是没有被标签包裹的,由于没有被标签包裹,cheerio就无法直接操作该文本,那么该怎么办呢。

进入正题

先看下面的 HTML 片段:

<div>
  <p>
    内容一
    <span>内容二</span>
  </p>
</div>

我们需要获取“内容一”而不需要“内容二”,那么该如何操作呢。

单纯的使用p.text()方法是没有办法实现的,因为这样会获取到p标签下面包含子节点的所有文本内容。

解决办法很简单,先看下面的代码:

var obj = $("div").children("p").clone()
obj.find(':nth-child(n)').remove();
console.log(obj.text());

在这段代码里,我们先将p标签及其子节点克隆了一份(克隆的原因是为了不影响原来的 dom 结构),然后将p标签下面的子节点全部删除,因为“文本一”没有被标签包裹,所以就被保留下来了。

这样一来就获取到了没有被子节点包裹的文本内容了

vue组件v-model与sync修饰符

前言

在 vue 中我们可以通过 v-model 来实现很方便的双向绑定。那你知道 v-model 的原理吗?

其实 v-model 只是一个语法糖,Vue 会默认使用一个名为 value 的 prop,以及名为 input 的事件,即:

  • v-bind:绑定响应式数据
  • 触发 input 事件 并传递数据 (核心和重点)
<input v-model="inputValue" />

等价于:

<input
  v-bind:value="inputValue"
  v-on:input="inputValue = $event.target.value"
/>

既然我们知道了 v-model 的原理,那么我们在自定义组件中如何使用呢

自定义组件的 v-model

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的。model 选项可以用来避免这样的冲突,在 2.2.0+ 版本中,vue 提供了一个 model 选项:

Vue.component("base-checkbox", {
  model: {
    prop: "checked",
    event: "change"
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
});

这样我们就把默认的 prop 改为 checked,将触发事件改为 change。

只要当子组件需要返回的值变动时,通过$emit方法触发 change 事件即可。这也是我们常用的子组件向父组件传递数据的方法。

.sync 修饰符

在 2.3+版本中,vue 提供了.sync 修饰符。

示例:

<comp :foo.sync="bar"></comp>

会被扩展为:

<comp :foo="bar" @update:foo="val => bar = val"></comp>

当子组件需要更新 foo 的值时,它需要显式地触发一个更新事件:

this.$emit("update:foo", newValue);

这样看下来是不是跟 v-model 非常相似呢。

确实两者本质都是一样,并没有任何区别: “监听一个触发事件”="(val) => value = val"。

细微之处的区别:

  1. 只不过 v-model 默认对应的是 input 或者 textarea 等组件的 input 事件,如果在子组件替换这个 input 事件,其本质和.sync 修饰符一模一样。比较单一,不能有多个。
  2. 一个组件可以多个属性用.sync 修饰符,可以同时"双向绑定多个“prop”,而并不像 v-model 那样,一个组件只能有一个。

总结功能作用场景:

  1. v-model 针对更多的是最终操作结果,是双向绑定的结果,是 value,是一种 change 操作。
    比如:输入框的值、多选框的 value 值列表、树结构最终绑定的 id 值列表(ant design 和 element 都是)、等等...
  2. .sync 针对更多的是各种各样的状态,是状态的互相传递,是 status,是一种 update 操作。
    比如:组件 loading 状态、子菜单和树结构展开列表(状态的一种)、某个表单组件内部验证状态、等等....

Cookie, LocalStorage 与 SessionStorage

基本概念

Cookie

Cookie 是小甜饼的意思。顾名思义,cookie 确实非常小,它的大小限制为 4KB 左右。

localStorage

localStorage 是 HTML5 标准中新加入的技术。是一种持久化本地存储方案。并且不会参与浏览器的通信。

sessionStorage

sessionStorage 与 localStorage 的接口类似,但保存数据的生命周期与 localStorage 不同。

异同

特性 Cookie localStorage sessionStorage
生命期 一般由服务器生成,可设置失效时间。如果在浏览器端生成 Cookie,默认是关闭浏览器后失效 除非被清除,否则永久保存 仅在当前会话下有效,关闭页面或浏览器后被清除
存储大小 4K 左右 一般为 5MB 同 localStorage
与服务器通信 每次都会携带在 HTTP 头中,如果使用 cookie 保存过多数据会带来性能问题 仅在客户端(即浏览器)中保存,不参与和服务器的通信 同 localStorage
易用性 需要程序员自己封装,源生的 Cookie 接口不友好 源生接口可以接受,亦可再次封装来对 Object 和 Array 有更好的支持 同 localStorage

应用场景

因为考虑到每个 HTTP 请求都会带着 Cookie 的信息,所以 Cookie 当然是能精简就精简啦,比较常用的一个应用场景就是判断用户是否登录。针对登录过的用户,服务器端会在他登录时往 Cookie 中插入一段加密过的唯一辨识单一用户的辨识码,下次只要读取这个值就可以判断当前用户是否登录啦。曾经还使用 Cookie 来保存用户在电商网站的购物车信息,如今有了 localStorage,似乎在这个方面也可以给 Cookie 放个假了~

而另一方面 localStorage 接替了 Cookie 管理购物车的工作,同时也能胜任其他一些工作。比如 HTML5 游戏通常会产生一些本地数据,localStorage 也是非常适用的。如果遇到一些内容特别多的表单,为了优化用户体验,我们可能要把表单页面拆分成多个子页面,然后按步骤引导用户填写。这时候 sessionStorage 的作用就发挥出来了。

从html字符串提取纯文本

目前常用的有两种方法;

正则过滤

这种办法比较简单粗暴

return htmlString.replace(/<[^>]*>|/g, "");

innerText/textContent 过滤

这种办法是先将 html 字符串转换成 dom 节点,然后用内置的 innerText/textContent 方法提取纯文本。

let ele = document.createElement("div");
ele.innerHTML = htmlString;
return ele.innerText || ele.textContent;

但是这俩种办法都有一个缺点就是无法过滤转义过的 html 字符串。
比如这段字符串&lt;button onclick="console.log('1')"&gt;文本&lt;/button&gt;
解决办法就是把两种方法结合,先用innerText方法过滤,然后再用正则方法过滤。代码如下:

/**
 * @description 从html字符串中提取纯文本
 * @export
 * @param {string} htmlString
 * @returns
 */
function replaceHtmlToText(htmlString) {
  let ele = document.createElement("div");
  ele.innerHTML = htmlString;
  let text = ele.innerText || ele.textContent;
  // 二次过滤
  return text.replace(/<[^>]*>|/g, "");
}

CSRF与XSS的区别以及防护

前言

国际惯例,先上一下维基百科:

CSRF:跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。

XSS:跨站脚本(Cross-site scripting,通常简称为 XSS)是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了 HTML 以及用户端脚本语言。

大话解释

CSRF

充用户发起请求(在用户不知情的情况下),完成一些违背用户意愿的请求(如恶意发帖,删帖,改密码,发邮件等)。

举个栗子:

用户 A 登陆微博之后,点开了用户 B 发布的一条网址 www.weibo.com/hehe/deletepage=30 好了用户 A 的 id=30 号的文章就被删除掉了,你没有插入任何恶意代码,你只是调用了网站本身的 js 代码接口,好了你就完成了一次未经授权的删帖操作。

XSS

通过客户端脚本语言(最常见如:JavaScript)在一个论坛发帖中发布一段恶意的 JavaScript 代码就是脚本注入,如果这个代码内容有请求外部服务器,那么就叫做 XSS!

前端如何防护

CSRF

目前最常用的做法就是 token 防护。由于 CSRF 的原理是冒用 cookie 里面的信息,那这里再加一个 token,由前端手动填充到请求中就可以避免这种情况。

XSS

XSS 的防护理论上比较简单,一是尽量少用 innerHTML 属性。二是在用户输入的地方做 HTML 标签过滤。目前已经有比较成熟的方案xss.js npm

CSS实现宽高等比例自适应矩形

概述

今天遇到一个很有趣的问题:如何实现一个宽度自适应,高度为宽度的一半的矩形

经过搜索引擎的筛选和自己的反复试验,发现使用padding-bottom是最完美的解决方案。

解决方案

首先我们要明白,padding-top/bottommargin-top/bottom都是相对于父元素的宽度来计算的,我们可以利用这一属性来实现我们的需求。

代码如下:

<div class="scale"></div>
.scale {
  width: 100%;
  height: 0;
  padding-bottom: 50%;
}

这其中的关键点就是height: 0;padding-bottom: 50%;

我们将元素的高度由padding撑开,由于padding是根据父元素宽度计算的,所以高度也就变成了相对父元素宽度,同时要将height设置为 0,这是为了将元素高度完全交给padding负责。

最后padding-bottom的值设为width的值一半,就可以实现高度是宽度的一半且自适应啦。

改进

光是这样写还是不够的,因为元素的height为 0,导致该元素里面再有子元素的时候,就无法正常设置高度。所以我们需要用到position: absolute;。代码如下:

<div class="scale">
    <div class="item">
        这里是所有子元素的容器
    </div>
</div>
.scale {
  width: 100%;
  padding-bottom: 56.25%;
  height: 0;
  position: relative; //
}

.item {
  width: 100%;
  height: 100%;
  background-color: aquamarine;
  position: absolute; //
}

继续改进

解决了子元素的问题,那么我们再来看看元素本身。由于我们一开始的需求是宽高比 2:1,这种比较好实现,但是后来需求又想要 16:9 的宽高比,而且宽度不是 100%,那这样计算 padding-bottom的时候就很麻烦了。如何解决呢?

这时候我们需要在外层再套一个父元素,将宽度的控制交给这个父元素来做。

代码如下:

<body>
    <div class="box">
        <div class="scale">
            <div class="item">
                item
            </div>
        </div>
    </div>
</body>
/* box 用来控制宽度 */
.box {
  width: 80%;
}
/* scale 用来实现宽高等比例 */
.scale {
  width: 100%;
  padding-bottom: 56.25%;
  height: 0;
  position: relative;
}
/* item 用来放置全部的子元素 */
.item {
  width: 100%;
  height: 100%;
  background-color: aquamarine;
  position: absolute;
}

如此,就可以完美解决。

在线演示

Markdown测试

马克飞象是一款专为印象笔记(Evernote)打造的Markdown编辑器,通过精心的设计与技术实现,配合印象笔记强大的存储和同步功能,带来前所未有的书写体验。特点概述:

  • 功能丰富 :支持高亮代码块、LaTeX 公式、流程图,本地图片以及附件上传,甚至截图粘贴,工作学习好帮手;
  • 得心应手 :简洁高效的编辑器,提供桌面客户端以及离线Chrome App,支持移动端 Web;
  • 深度整合 :支持选择笔记本和添加标签,支持从印象笔记跳转编辑,轻松管理。

Markdown简介

Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档,然后转换成格式丰富的HTML页面。 —— 维基百科

正如您在阅读的这份文档,它使用简单的符号标识不同的标题,将某些文字标记为粗体或者斜体,创建一个链接或一个脚注1。下面列举了几个高级功能,更多语法请按Cmd + /查看帮助。

代码块

@requires_authorization
def somefunc(param1='', param2=0):
    '''A docstring'''
    if param1 > param2: # interesting
        print 'Greater'
    return (param2 - param1 + 1) or None
class SomeClass:
    pass
>>> message = '''interpreter
... prompt'''

表格

Item Value Qty
Computer 1600 USD 5
Phone 12 USD 12
Pipe 1 USD 234

提示:想了解更多,请查看流程图语法以及时序图语法

复选框

使用 - [ ]- [x] 语法可以创建复选框,实现 todo-list 等功能。例如:

  • 已完成事项
  • 待办事项1
  • 待办事项2

注意:目前支持尚不完全,在印象笔记中勾选复选框是无效、不能同步的,所以必须在马克飞象中修改 Markdown 原文才可生效。下个版本将会全面支持。

印象笔记相关

笔记本和标签

马克飞象增加了@(笔记本)[标签A|标签B]语法, 以选择笔记本和添加标签。 绑定账号后, 输入(自动会出现笔记本列表,请从中选择。

笔记标题

马克飞象会自动使用文档内出现的第一个标题作为笔记标题。例如本文,就是第一行的 欢迎使用马克飞象

快捷编辑

保存在印象笔记中的笔记,右上角会有一个红色的编辑按钮,点击后会回到马克飞象中打开并编辑该笔记。

**注意:**目前用户在印象笔记中单方面做的任何修改,马克飞象是无法自动感知和更新的。所以请务必回到马克飞象编辑。

数据同步

马克飞象通过将Markdown原文以隐藏内容保存在笔记中的精妙设计,实现了对Markdown的存储和再次编辑。既解决了其他产品只是单向导出HTML的单薄,又规避了服务端存储Markdown带来的隐私安全问题。这样,服务端仅作为对印象笔记 API调用和数据转换之用。

隐私声明:用户所有的笔记数据,均保存在印象笔记中。马克飞象不存储用户的任何笔记数据。

离线存储

马克飞象使用浏览器离线存储将内容实时保存在本地,不必担心网络断掉或浏览器崩溃。为了节省空间和避免冲突,已同步至印象笔记并且不再修改的笔记将删除部分本地缓存,不过依然可以随时通过文档管理打开。

**注意:**虽然浏览器存储大部分时候都比较可靠,但印象笔记作为专业云存储,更值得信赖。以防万一,请务必经常及时同步到印象笔记

编辑器相关

设置

右侧系统菜单(快捷键Cmd + M)的设置中,提供了界面字体、字号、自定义CSS、vim/emacs 键盘模式等高级选项。

快捷键

帮助 Cmd + /
同步文档 Cmd + S
创建文档 Cmd + Opt + N
最大化编辑器 Cmd + Enter
预览文档 Cmd + Opt + Enter
文档管理 Cmd + O
系统菜单 Cmd + M

加粗 Cmd + B
插入图片 Cmd + G
插入链接 Cmd + L
提升标题 Cmd + H

关于收费

马克飞象为新用户提供 10 天的试用期,试用期过后需要续费才能继续使用。未购买或者未及时续费,将不能同步新的笔记。之前保存过的笔记依然可以编辑。

反馈与建议


感谢阅读这份帮助文档。请点击右上角,绑定印象笔记账号,开启全新的记录与分享体验吧。

Footnotes

  1. 这是一个示例脚注。请查阅 MultiMarkdown 文档 关于脚注的说明。 限制: 印象笔记的笔记内容使用 ENML 格式,基于 HTML,但是不支持某些标签和属性,例如id,这就导致脚注TOC无法正常点击。

手摸手教你实现一个双飞翼布局

前言

在页面布局中我们经常会遇到一种布局类型就是 一边或者两边等宽,中间自适应,有时我们还需要让中间布局提前渲染。如下图所示

image

说到这种布局我们就不得不提一下圣杯布局和双飞翼布局了。

圣杯布局和双飞翼布局是布局届中经常被提起的布局,实际上这两个布局是同一种布局方式,只是实现的方法略有不同,这里只讲双飞翼布局,因为双飞翼布局实际上是圣杯布局的升级版。

ok,那么如何实现一个双飞翼布局呢?

方法一

最原始的办法就是利用float来实现,代码如下:

<div class="box">
    <div class="center">
        <div class="center-item">center</div>
    </div>
    <div class="left">left</div>
    <div class="right">right</div>
</div>
.center,
.left,
.right {
  float: left; /* 全部左浮动  */
  height: 300px;
}

.center {
  width: 100%;
}

.center-item {
  height: 300px;
  background-color: antiquewhite;
  margin-left: 200px; /*  margin-left为左边div的宽度  */
  margin-right: 300px; /*  margin-right为右边div的宽度 */
}

.left {
  width: 200px;
  background-color: aquamarine;
  margin-left: -100%; /* 向左移动100%的距离 */
}

.right {
  width: 300px;
  background-color: bisque;
  margin-left: -300px; /* 向左移动自身的距离  */
}

方法二

如果不考虑兼容性的话最好用的还是flex布局,实现起来也很简单。代码如下:

<div class="flex-box">
    <div class="center">center</div>
    <div class="left">left</div>
    <div class="right">right</div>
</div>
.flex-box {
  display: flex;
  flex-direction: row; /* 横向排列 */
  align-items: stretch; /* 实现等高 */
  width: 100%;
  height: 300px;
}

.center {
  flex-grow: 1; /* 默认占据剩余空间 */
  order: 1; /* 排序 */
  background-color: antiquewhite;
}

.left {
  order: 0; /* 排序 */
  width: 200px;
  background-color: aquamarine;
}

.right {
  order: 2; /* 排序 */
  width: 300px;
  background-color: bisque;
}

总结

两种实现方法比起来明显 flex 布局要好很多,而且理解起来也更简单,但是兼容性是个问题,所以在有兼容性的时候还需要使用第一种方式。

在vue中使用websocket以及心跳检测

前言

在使用 websocket 的过程中,有时候会遇到网络断开的情况,但是在网络断开的时候服务器端并没有触发 onclose 的事件。这样会有:服务器会继续向客户端发送多余的链接,并且这些数据还会丢失。所以就需要一种机制来检测客户端和服务端是否处于正常的链接状态。因此就有了 websocket 的心跳了。还有心跳,说明还活着,没有心跳说明已经挂掉了。

为什么叫心跳包呢?

它就像心跳一样每隔固定的时间发一次,来告诉服务器,我还活着。

心跳机制是?

心跳机制是每隔一段时间会向服务器发送一个数据包,告诉服务器自己还活着,同时客户端会确认服务器端是否还活着,如果还活着的话,就会回传一个数据包给客户端来确定服务器端也还活着,否则的话,有可能是网络断开连接了。需要重连~

实现

代码如下:

<script>
  export default {
    data() {
      return {
        WebSocket: null,
        timeout: 15 * 1000, //心跳检测间隔 ms
        severTimeout: 3 * 1000, //服务器超时时间,也就是websocket关闭重启时间 ms
        reconnectTimeout: 3 * 1000, //websocket重连时间间隔 ms
        timeoutObj: null,
        serverTimeoutObj: null,
        lockReconnect: false //连接状态标识
      };
    },
    computed: {},
    watch: {},
    mounted() {
      this.initWebSocket();
    },
    beforeDestroy() {
      this.wsClose();
    },
    methods: {
      /*=============================================
    =            WebSocket 推送                   =
    =============================================*/
      //websocket初始化
      initWebSocket() {
        try {
          this.createWebSocket();
        } catch (e) {
          reconnect();
        }
      },
      // websocket实例
      createWebSocket() {
        const url = "";
        //
        this.WebSocket = new WebSocket(url);
        //
        this.WebSocket.onopen = res => {
          this.reset();
          this.wsOnOpen(res);
        };
        this.WebSocket.onmessage = e => {
          this.reset();
          this.wsOnMessage(e);
        };
        this.WebSocket.onerror = err => {
          this.reconnect();
          this.wsOnError(err);
        };
        this.WebSocket.onclose = res => {
          this.reconnect();
          this.wsOnClose(res);
        };
      },

      /* 事件 */
      wsOnOpen(res) {
        window.console.log("ws推送连接成功!");
      },
      wsOnMessage(e) {},
      wsOnClose(res) {
        window.console.log("ws推送已关闭!");
      },

      /* 方法 */
      wsSend(text) {
        text = JSON.stringify(text);
        this.WebSocket.send(text);
      },
      wsClose() {
        this.WebSocket.close();
      },

      /* 心跳检测 */
      start() {
        this.timeoutObj = setTimeout(() => {
          // send内容不可更改!!!发送内容与后端商定
          this.wsSend({ a: 1 });
          this.serverTimeoutObj = setTimeout(() => {
            this.wsClose();
          }, this.severTimeout);
        }, this.timeout);
      },
      reset() {
        this.timeoutObj && clearTimeout(this.timeoutObj);
        this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
        this.start();
      },

      /* 计数,避免websocket重复连接 */
      reconnect() {
        if (this.lockReconnect) {
          return;
        }
        this.lockReconnect = true;
        //没连接上会一直重连,设置延迟避免请求过多
        // tt && clearTimeout(tt);
        setTimeout(() => {
          this.initWebSocket();
          this.lockReconnect = false;
        }, this.reconnectTimeout);
      }

      /*=====  End of WebSocket 推送  ======*/
    }
  };
</script>

其中发送的内容需要与后端商议,在客户端发送心跳包之后,服务端再返回一个心跳包,这样前端就可以分辨心跳包与其他消息推送了。

// 插件版

class HeartBeat {
    lockReconnect = false; //连接状态标识
    timeoutObj = null;
    serverTimeoutObj = null;
    ws = null;

    constructor(config = {}) {
        const {
            url,
            timeout = 5 * 1000, //心跳检测间隔 ms
            severTimeout = 3 * 1000, //服务器超时时间,也就是websocket关闭重启时间 ms
            reconnectTimeout = 3 * 1000, //websocket重连时间间隔 ms
        } = config;


        this.url = url;
        this.timeout = timeout;
        this.severTimeout = severTimeout;
        this.reconnectTimeout = reconnectTimeout;


        this.initWebSocket()
        return this.ws;
    }

    //websocket初始化
    initWebSocket() {
        try {
            this.createWebSocket();
        } catch (e) {
            this.reconnect();
        }
    }

    // websocket实例
    createWebSocket() {
        this.ws = new WebSocket(this.url);
        //
        this.ws.addEventListener('open', res => {
            this.reset();
        })
        this.ws.addEventListener('message', e => {
            this.reset();
        })
        this.ws.addEventListener('error', err => {
            this.reconnect();
        })
        this.ws.addEventListener('close', res => {
            this.reconnect();
        })
    }

    /* 心跳检测 */
    start() {
        this.timeoutObj = setTimeout(() => {
            // send内容不可更改!!!发送内容与后端商定
            this.ws.send(JSON.stringify({
                heartbeat: "ping"
            }));
            this.serverTimeoutObj = setTimeout(() => {
                this.ws.close();
            }, this.severTimeout);
        }, this.timeout);
    }

    reset() {
        this.timeoutObj && clearTimeout(this.timeoutObj);
        this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
        this.start();
    }

    /* 计数,避免websocket重复连接 */
    reconnect() {
        if (this.lockReconnect) {
            return;
        }
        this.lockReconnect = true;
        //没连接上会一直重连,设置延迟避免请求过多
        setTimeout(() => {
            this.initWebSocket();
            this.lockReconnect = false;
        }, this.reconnectTimeout);
    }
}


export default HeartBeat;


// test

// const IM = new HeartBeat({
//     url: 'ws://123.207.136.134:9010/ajaxchattest'
// })

// console.log(IM);

npm发包流程及版本控制

背景

目前项目组内没有一套版本管理规范来对node package进行规范,每个人对版本号的理解差异导致package版本号混乱,本文档将给出一套规范来解决该问题,并且在次规范的基础上给出一套node package发包最佳实践。
如果您对node package的语义化版本号已经非常了解,可直接跳到最佳实践部分开始阅读

文档适用范围:包括所有发布到npm上的node和前端package

语义化版本号说明

major.minor.patch[-prerelease]

版本号组成

node package版本号由四部分组成:major.minor.patch[-prerelease],比如:1.0.2-beta.1,其中prerelease可选。

  • major:代表主版本号,通常在需要提交不能向下兼容的情况下对该版本号进行升级
  • minor:代表次版本号,通常在新增功能时才对该版本号进行升级
  • patch:代表修复版本号,升级该版本号通常代表修复一些bug,但没有新增功能或者存在不向下兼容的功能
  • prerelease:带有该版本号的包通常表示在测试阶段,尚未稳定,通常不建议用户安装。

prerelease说明

alpha、beta、rc

通常我们会看到三种类型的prerelease,分别是:alpha、beta、RC,如:

1.1.0-alpha.1
1.1.0-beta.1
1.1.0-rc.1

每种类型的prerelease都有其特殊的含义,请不要乱用。

  • alpha: 代表内部测试版,会有很多Bug,是比beta更早的版本,一般不建议对外发布
  • beta: 相对alpha版本已有了很大的改进,但还是存在一些缺陷,需要经过多次测试来进一步消除
  • rc:Release Candidate顾名思义就是正式发布的候选版本。和Beta版最大的差别在于Beta阶段会一直加入新的功能,但是到了RC版本,几乎就不会加入新的功能了,而主要着重于除错! RC版本是最终发放给用户的最接近正式版的版本,发行后改正bug就是正式版了,就是正式版之前的最后一个测试版

配合Tag灵活控制版本输出

思考一个问题:npm install ,会安装哪个版本的package 最新版本?
其实node package也有tag的功能,跟git的tag有点类似,目的就是给某个版本的package打标签。通过npm dist-tag ls指令可以查看某个package的所有tag,以vue为例:

> npm dist-tag ls vue
beta: 2.6.0-beta.3
csp: 1.0.28-csp
latest: 2.6.10

tag包括内置类型和自定义类型,其中latest就是内置tag,csp和beta为自定义tag。默认情况下latest指向最新版本的package,当然我们可以手动修改latest指向的版本,这个我们后面讲。每次npm publish发包时都会将latest指向当前发布的package版本。至于beta和csp具体指向哪个版本的package,完全由我们自己决定。那么tag具体有什么作用?

  1. 指定最新的稳定版本

npm install时除了指定某个version的package,也可以指定安装某个tag的package。以vue为例:

image

npm install vue@latest就会安装2.6.10版本。现在回答刚才的问题:“npm install ,会安装哪个版本的package?”答案是latest指向的version。因此,我们在版本迭代时始终让latest指向最新的稳定版本。

  1. 指定最新的公测版本

一般pkg在发新版之前都会发布一些公测版让用户先尝鲜,比如0.0.4-beta.0,一方面是让用户体验新功能,另一方面尽早发现bug修复上线。而在此期间更新版本是相对频繁的,我们不可能每发布一个内测版本都通知内测人员修改版本号,我们可以使用自定义标签解决此类问题。beta tag始终指向最新的带有prerelease的版本。那么用户通过npm install pkg@beta就可以安装最新的内测版。
除了在npm publish时通过--tag参数的方式指定tag,我们还可以通过npm dist-tag add指令增加或者移动tag。

// 方式一
npm publish --tag beta
// 方式二
npm dist-tag add [email protected] beta

有一点需要特别注意:npm publish 时会自动将latest指向最新的版本包括带有prerelease的版本。为了不改变latest总是指向最新稳定版本的属性,请在publish beta版本时使用 --tag beta参数。

最佳实践

参考了目前流行框架(Vue、React、Taro)的版本管理方案,得出以下最佳实践。

约定

为了规范发包流程,我们做如下约定:

  • 第一个稳定版本号为1.0.0
  • beta版本号从0开始,比如:1.0.0-beta.0/2.1.0-beta.0
  • 使用npm version工具进行版本升级
  • prerelease只保留beta
  • 只有 latestbeta 两个标签
  • latest tag永远指向最新的稳定版本
  • beta tag永远指向最新的公测版本
  • 提交beta版本时,npm publish时必须加上 --tag beta 参数
  • npm publish后需要给git仓库打tag,tag名称跟当前版本号一致

版本升级工具

npm version [ | major | minor | patch | premajor | preminor | prepatch | prerelease [--preid=] | from-git]

npm 提供了自动升级版本号的工具:npm version,该工具会自动修改package.json内的版本号并且会自动 git commit, 因此使用该工具时请保持git status是clear的。
假设我们当前版本号为0.0.1,我们需要升patch号:

npm version patch

那么版本号就会变成0.0.2
npm version majornpm version minor同理,具体使用方法参考官方文档
其中npm version prerelease比较特殊,需要扩展说明下。

prerelease 版本升级

npm version prerelease

假设当前版本号为0.0.1,执行 npm version prerelease 后,版本号将变为0.0.2-0,再执行npm version prerelease,版本号将变为0.0.2-1,以此类推。
但是如何升级成类似0.0.2-beta.1的形式?可以尝试使用 --preid 选项,但前提是您本地的npm版本需要大于6.4.0

// npm 6.4.0 以后可以使用 --preid 选项
npm version prerelease --preid=beta

0.0.1将变为0.0.2-beta.0,您也可以选择手动升级:

npm version prerelease 0.0.2-beta.0

这不是明智的选择,我们依然推荐您将npm升级到6.4.0以上的版本,升级方式:

npm i -g npm@latest

案例

第一个beta版本

我们目前有个package名称是ossa,第一个版本之前有两个beta版本,那么项目初始化时确保package.json里版本号为1.0.0-beta.0,publish指令:npm publish --tag beta

image

git tag:

git tag 1.0.0-beta.0

第二个beta版本

接下来会通过npm version prerelease --preid=beta进行beta版本升级,升级后版本号将变为1.0.0-beta.1,publish指令:npm publish --tag beta

image

git tag:

git tag 1.0.0-beta.1

major版

两个beta版本后需要发布稳定版本1.0.0,请使用指令npm version patch,版本号将变为1.0.0,publish指令:npm publish

image

git tag:

git tag 1.0.0

修复版本

接着会发布一个patch版本,请使用指令npm version patch,版本号将变为1.0.1,publish指令:npm publish

image

git tag:

git tag 1.0.1

minor版本

接着会发布一个minor版本,请使用指令npm version minor,版本号将变为1.1.0,publish指令:npm publish

image

git tag:

git tag 1.1.0

minor的beta版

发布1.2.0之前会发布一个1.2.0的beta版,npm version prerelease --preid=beta,publish指令:npm publish --tag beta

image

git tag:

git tag 1.2.0-beta.0

第二个minor的beta版

接下来所有1.2.0的beta版都可以通过npm version prerelease --preid=beta指令自动升级,比如升级到1.2.0-beta.1,publish指令:npm publish --tag beta

image

git tag:

git tag 1.2.0-beta.1

第二个minor版

接着发布1.2.0稳定版,请使用指令npm version minor,版本号将变为1.2.0,publish指令:npm publish

image

git tag:

git tag 1.2.0

第二个major

接着发布2.0.0稳定版,请使用指令npm version major,版本号将变为2.0.0,publish指令:npm publish

image

git tag:

git tag 2.0.0

修正tag指向

如果发包时出现tag指向错误的情况,比如:当前包版本为1.0.0

image

发beta包时没有加--tag beta参数,tag指向将变为:

image

此时,可使用npm dist-tag add指令修改tag指向:

npm dist-tag add [email protected] latest
npm dist-tag add [email protected] beta

修改后tag指向:

image

上面的例子已包括了常见的发包情形,后面的以此类推,请在发包时严格遵守。

参考文章

  1. node-semver
  2. npm-version

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.