前端汇总 --005

336 阅读50分钟

一、 Tailwind:最流行的原子化 CSS 框架

定义一些细粒度的 class,叫做原子 class,然后在 html 里直接引入这些原子化的 class。

1.1 实战

  • 通过 crerate-react-app 创建一个 react 项目:
npx create-react-app tailwind-test
  • 1.11 进入 tailwind-test 目录,执行
npm install -D tailwindcss
npx tailwindcss init
  • 1.12 安装 tailwindcss 依赖,创建 tailwindcss 配置文件。
"./src/**/*.{js,jsx}"

image.png

tailwind 实际上是一个 postcss 插件,因为 cra 内部已经做了 postcss 集成 tailwind 插件的配置,这一步就不用做了:

image.png

  • 1.13 在入口 app.css 里加上这三行代码:

image.png 这三行分别是引入 tailwind 的基础样式、组件样式、工具样式的。

之后就可以在组件里用 tailwind 提供的 class 了:

import './App.css';

function App() {
  return (
    <div className='text-base p-1 border border-black border-solid'>guang</div>
  );
}

export default App;
  • 1.14 执行 npm run start 把开发服务跑起来。
npm run start

可以看到,它正确的加上了样式:

image.png

  • tailwind 提供的所有内置原子 class 都可以配置 这里的 p-1 是 padding:0.25rem,你也可以在配置文件里修改它的值:

image.png

.text-base 是 font-size、line-height 两个样式,这种通过数组配置:

image.png

所有 tailwind 提供的所有内置原子 class 都可以配置。

但这些都是全局的更改,有的时候你想临时设置一些值,可以用 [] 语法。

比如 text-[14px],它就会生成 font-size:14px 的样式

hover 时的样式,在 tailwind 里怎么指定呢? 很简单,这样写:

hover:text-[30px]

生成代码: image.png

此外,写响应式的页面的时候,我们要指定什么宽度的时候用什么样式,这个用 tailwind 怎么写呢?

image.png

生成代码: image.png 断点位置自然也是可以配置的:

image.png

1.2 tailwind css 提供的 vscode 插件来解决:

Tailwind CSS IntelliSense

image.png 安装这个 Tailwind CSS IntelliSense 之后的体验是这样的:

image.png 智能提示,可以查看它对应的样式。

1.3 内置 class 不能满足我的需求

其实上面那个 @layer 和 @apply 就能扩展内置原子 class。

但如果你想跨项目复用,那可以开发个 tailwind 插件

const plugin = require('tailwindcss/plugin');

module.exports = plugin(function({ addUtilities }) {
    addUtilities({
        '.guang': {
            background: 'blue',
            color: 'yellow'
        },
        '.guangguang': {
            'font-size': '70px'
        }
    })
})

在 tailwind.config.js 里引入:

image.png 这样就可以用这个新加的原子 class 了:

image.png tailwind 的 class 名和我已有的 class 冲突了咋办?

通过加 prefix 解决:

image.png 不过这样所有的原子 class 都得加 prefix 了:

image.png

1.4 总结

tailwind 是一个流行的原子化 css 框架。

传统 css 写法是定义 class,然后在 class 内部写样式,而原子化 css 是预定义一些细粒度 class,通过组合 class 的方式完成样式编写。

tailwind 用起来很简单:

所有预定义的 class 都可以通过配置文件修改值,也可以通过 aaa-[14px] 的方式定义任意值的 class。

所有 class 都可以通过 hover:xxx、md:xxx 的方式来添加某个状态下的样式,响应式的样式,相比传统的写法简洁太多了。

它的优点有很多,我个人最喜欢的就是不用起 class 的名字了,而且避免了同样的样式在多个 class 里定义多次导致代码重复,并且局部作用于某个标签,避免了全局污染。

它可以通过 @layer、@apply 或者插件的方式扩展原子 class,支持 prefix 来避免 class 名字冲突。

tailwind 本质上就是一个 postcss 插件,通过 AST 来分析 css 代码,对 css 做增删改,并且可以通过 extractor 提取 js、html 中的 class,之后基于这些来生成最终的 css 代码。

二、 vite打包性能优化

2.1 拆分包

rollupOptions: {
  output: {
    manualChunks(id) {
      if (id.includes("node_modules")) {
        // 让每个插件都打包成独立的文件
        return id .toString() .split("node_modules/")[1] .split("/")[0] .toString(); 
      }
    }
  }
}

2.2 去除debugger

terserOptions: {
  compress: {
    drop_console: true,
    drop_debugger: true
  }
}

2.3 CDN 加速

内容分发网络(Content Delivery Network,简称 CDN)就是让用户从最近的服务器请求资源,提升网络请求的响应速度。同时减少应用打包出来的包体积,利用浏览器缓存,不会变动的文件长期缓存。(不建议使用第三方cdn,这里做学习讨论使用) install:

npm i rollup-plugin-external-globals -D
npm i vite-plugin-html -D
<head>
    <%- vuescript %>
</head>

配置:

import { createHtmlPlugin } from 'vite-plugin-html'

rollupOptions: {
  // 告诉打包工具 在external配置的 都是外部依赖项  不需要打包
  external: ['vue'],
  plugins: [
    externalGlobals({
      // "在项目中引入的变量名称":"CDN包导出的名称,一般在CDN包中都是可见的"
      vue: 'Vue'
    })
  ]
}

plugins: [
    createHtmlPlugin({
      minify: true,
      inject: {
        data: {
          vuescript: '<script src="https://cdn.jsdelivr.net/npm/vue@3.2.37"></script>'
        }
      }
    })
]

2.4 按需导入

{}引入:

import _ from 'lodash-es'; // 你将会把整个lodash的库引入到项目
import { cloneDeep } from 'lodash-es'; // 你将会把引入cloneDeep引入到项目

2.5 文件压缩(后端应设置响应头 content-encoding: gzip )

install:

npm install vite-plugin-compression -D

配置:

// build.rollupOptions.plugins[]
viteCompression({
  verbose: true, // 是否在控制台中输出压缩结果
  disable: false,
  threshold: 10240, // 如果体积大于阈值,将被压缩,单位为b,体积过小时请不要压缩,以免适得其反
  algorithm: 'gzip', // 压缩算法,可选['gzip'' brotliccompress ''deflate ''deflateRaw']
  ext: '.gz',
  deleteOriginFile: true // 源文件压缩后是否删除(我为了看压缩后的效果,先选择了true)
})

当请求静态资源时,服务端发现请求资源为gzip的格式时,应该设置响应头 content-encoding: gzip 。因为浏览器解压也需要时间,所以代码体积不是很大的话不建议使用 gzip 压缩。

2.6 图片压缩

install:

yarn add vite-plugin-imagemin -D

or

npm i vite-plugin-imagemin -D

配置:

import viteImagemin from 'vite-plugin-imagemin'

plugin: [
    viteImagemin({
      gifsicle: {
        optimizationLevel: 7,
        interlaced: false
      },
      optipng: {
        optimizationLevel: 7
      },
      mozjpeg: {
        quality: 20
      },
      pngquant: {
        quality: [0.8, 0.9],
        speed: 4
      },
      svgo: {
        plugins: [
          {
            name: 'removeViewBox'
          },
          {
            name: 'removeEmptyAttrs',
            active: false
          }
        ]
      }
    })
]

viteImagemin在国内比较难安装,容易出现报错,可以尝试下面几种解决方案。

2.61 viteImagemin报错

  1. 使用 yarn 在 package.json 内配置(推荐)

    "resolutions": {
        "bin-wrapper": "npm:bin-wrapper-china"
     }
    
  2. 使用 npm,在电脑 host 文件加上如下配置即可

    199.232.4.133 raw.githubusercontent.com
    
  3. 使用 cnpm 安装(不推荐)

2.7 总结

// vite.config.js
import { defineConfig } from 'vite'
import { createHtmlPlugin } from 'vite-plugin-html'
import viteImagemin from 'vite-plugin-imagemin'
import externalGlobals from 'rollup-plugin-external-globals'
import { visualizer } from 'rollup-plugin-visualizer'
import viteCompression from 'vite-plugin-compression'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    visualizer({ open: true }),
    // 将下面的添加到plugin下
    createHtmlPlugin({
      minify: true,
      inject: {
        data: {
          vuescript: '<script src="https://cdn.jsdelivr.net/npm/vue@3.2.25"></script>',
          demiScript: '<script src="//cdn.jsdelivr.net/npm/vue-demi@0.13.7"></script>',
          elementPlusScript: `
            <link href="https://cdn.jsdelivr.net/npm/element-plus@2.2.22/dist/index.min.css" rel="stylesheet">
            <script src="https://cdn.jsdelivr.net/npm/element-plus@2.2.22/dist/index.full.min.js"></script>
          `,
          echartsSciprt: '<script src="https://cdn.jsdelivr.net/npm/echarts@5.0.2/dist/echarts.min.js"></script>'
        }
      }
    }),
    viteImagemin({
      gifsicle: {
        optimizationLevel: 7,
        interlaced: false
      },
      optipng: {
        optimizationLevel: 7
      },
      mozjpeg: {
        quality: 20
      },
      pngquant: {
        quality: [0.8, 0.9],
        speed: 4
      },
      svgo: {
        plugins: [
          {
            name: 'removeViewBox'
          },
          {
            name: 'removeEmptyAttrs',
            active: false
          }
        ]
      }
    })
  ],
  build: {
    target: 'es2020',
    minify: 'terser',
    // rollup 配置
    rollupOptions: {
      output: {
        chunkFileNames: 'js/[name]-[hash].js', // 引入文件名的名称
        entryFileNames: 'js/[name]-[hash].js', // 包的入口文件名称
        assetFileNames: '[ext]/[name]-[hash].[ext]', // 资源文件像 字体,图片等
        manualChunks(id) {
          if (id.includes('node_modules')) {
            return 'vendor'
          }
        }
      },
      //  告诉打包工具 在external配置的 都是外部依赖项  不需要打包
      external: ['vue', 'element-plus', 'echarts'],
      plugins: [
        externalGlobals({
          vue: 'Vue',
          'element-plus': 'ElementPlus',
          echarts: 'echarts',
          'vue-demi': 'VueDemi'
        }),
        viteCompression({
          verbose: true, // 是否在控制台中输出压缩结果
          disable: false,
          threshold: 10240, // 如果体积大于阈值,将被压缩,单位为b,体积过小时请不要压缩,以免适得其反
          algorithm: 'gzip', // 压缩算法,可选['gzip'' brotliccompress ''deflate ''deflateRaw']
          ext: '.gz',
          deleteOriginFile: false // 源文件压缩后是否删除
        })
      ]
    },
    terserOptions: {
      compress: {
        // 生产环境时移除console
        drop_console: true,
        drop_debugger: true
      }
    }
  }
})

三、 js事件机制

3.1 js事件流

js事件流一共分为三个阶段

  • 从 window上 事件触发处 传播,遇到 注册的捕获事件 就会触发。
  • 传播到事件触发处,触发注册的事件。
  • 从 事件触发处 window上 传播,遇到 注册的冒泡事件 触发。

来张图让你更好的理解:

image.png

3.2 阻止默认事件

当我们想只触发其中的一个或多个事件,而其它事件不触发时,我们可以使用stopPropagation()stopImmediatePropagation()函数。

stopPropagation() : 终止默认事件传播到其他容器

stopImmediatePropagation() : 终止默认事件传播到其他容器上 和 自己这个容器的其他事件

3.3 事件代理

事件代理是js事件机制一个重要的应用

  • 只需要把事件绑定到父元素上,占用的内存更小
  • 可以动态给添加的元素绑定监听事件,不需要每添加一个元素就重新绑定一次。

四、 Grid布局

CSS Gird布局也叫二维网格布局系统,可用于布局页面主要的区域布局或小型组件。网格是一组相交的水平线和垂直线,它定义了网格的列和行。我们可以指定将网格元素放置在与这些行和列相关的位置上。

4.1 一维布局 和 二维布局

像流布局和Flex布局,他们都是一维布局。一维布局一次只能处理一个维度上的元素布局,一行或者一列。回想一下,流布局和Flex布局的兄弟节点,是不是都是按照行或者列来显示的。

网格布局是二维布局,可以同时处理行和列上的布局。使用网格布局,兄弟节点可以被指定布局到网格的某个位置。

4.2 Grid布局中的概念

4.21 网格容器

我们通过在元素上声明 display:griddisplay:inline-grid 来创建一个网格容器。一旦我们这样做,这个元素的所有直系子元素将成为网格元素。网格容器的设置和flex容器的设置类似。

在网格容器节点行,我们可以通过grid-template-columnsgrid-template-rows指定当前容器的行和列。

如下面的代码,就会生成一个2行3列的效果。

<div class="grid-box-1">
  <div>1</div>
  <div>2</div>
  <div>3</div>
  <div>4</div>
  <div>5</div>
</div>
.grid-box-1 {
  border: 1px solid #999;
  width: 300px;
  height: 200px;
  display: grid;
  margin: 20px;
  grid-template-columns: 1fr 1fr 1fr;  /* 指定 3 列*/
  grid-template-rows: 1fr 1fr; /* 指定 2行 */
}
.grid-box-1 > div {
  background-color: bisque;
  border-radius: 4px;
  border: 1px solid #ccc;
}

4.22 网格轨道

通过grid-template-columnsgrid-template-rows指定当前容器的行和列后,这里的行和列就是网格轨道。但在实际页面中,由于页面的内容不确定,内容可能会超过grid-template-columnsgrid-template-rows指定的网格轨道个数,这个时候网格将会在隐式网格中创建行和列。按照默认,这些轨道将自动定义尺寸,所以会根据它里面的内容改变尺寸。

你也可以在隐式网格中用 grid-auto-rowsgrid-auto-columns 属性来定义一个设置大小尺寸的轨道。 还是拿上面的例子,如果Grid的子节点大于6个,就会出现隐式网格。

<div class="grid-box-2">
  <div>1</div>
  <div>2</div>
  <div>3</div>
  <div>4</div>
  <div>5</div>
  <div>6</div>
  <div>7</div>
  <div>8</div>
  <div>9</div>
  <div>10</div>
  <div>11</div>
</div>
/* 隐式网格 */
.grid-box-2 {
  border: 1px solid #999;
  width: 300px;
  height: 200px;
  display: grid;
  margin: 20px;
  grid-template-columns: 1fr 1fr 1fr;
  grid-template-rows: 1fr 1fr;
}
.grid-box-2 > div {
  background-color: bisque;
  border-radius: 4px;
  border: 1px solid #ccc;
}

上面的代码中,如果Grid的子节点超过了6个,父节点的高度是200,那么超出指定网格轨道的子节点就是隐式网格,隐式网格不会根据网格轨道的样式来进行设置。比如例子中就是,隐式网格的高度是内容的高度,父节点剩余的空间会分配给两个指定的网格轨道,轨道1:1比例分配。 当然,我们也可以设置隐式网格的样式,通过grid-auto-rowsgrid-auto-columns可以对隐式网格的行列进行设置。如下面的例子,设置了隐式网格轨道的行高为40,那么显示网格高度就是60((200-40*2) / 2)。

.grid-box-2 {
  grid-auto-rows: 40px; /* 添加隐式网格的行高30px */
}

4.23 网格线

设置网格轨道时,Grid会为我们创建带编号的网格线来让我们来定位每一个网格元素。还是拿上面的代码做例子,他的网格线编号会是下图中的顺序(负数表示从后往前的编号,不含隐式网格对应的网格线)。注意:网格线的编号顺序取决于文章的书写模式。

.grid-box-3 > div:nth-of-type(1) {
  grid-column-start: 1;
  grid-column-end: 4;
  grid-row-start: 1;
  grid-row-end: 2; /* 如果只占一行或一列,grid-xx-end属性可以不用写 */

  /* 等同于下面的代码 */
  grid-area: 1 / 1 / 2 / 4;  /* 这里的顺序是:row-start / column-start / row-end / column-end */ 
}
.grid-box-3 > div:nth-of-type(2) {
  grid-row: 2 / 4; /* grid-row 是 grid-row-start 和 grid-row-end的缩写 */
}
.grid-box-3 > div:nth-of-type(3) {
  grid-column: 2 / span 2; /* span表示占据几行,这里表示从2开始,占据2行,也就是网格线2到4 */
}
.grid-box-3 > div:nth-of-type(6) {
  grid-column: 1 / span 3;
}

image.png

4.24 网格单元和网格间距

一个网格单元是在一个网格元素中最小的单位,比如上面的例子中,4行3列,那么网格布局的父元素就被划分成4*3=12个网格单元,子级元素将会排列在每个事先定义好的网格单元中。网格元素可以向行或着列的方向扩展一个或多个单元,并且会创建一个网格区域。网格区域的形状应该是一个矩形(也就是说你不可能创建出一个类似于“L”形的网格区域)。

网格单元之间可以通过grid-column-gapgrid-row-gap或者grid-gap设置网格间距。下面的例子就是把网格列间距设置为2px,网格行间距设置为6px。

现在改成column-gaprow-gapgap了。gap的顺序是row-gap column-gap

.grid-box-4 {
  /* 等同于 gap: 6px 2px; */
  grid-column-gap: 2px;
  grid-row-gap: 6px;
}

4.25 Grid布局细节

4.251 使用repeat设置行列

我们再设置行列时,会遇到很多列的情况,如果一列一列去指定,太麻烦了。这个时候我们可以通过repeat函数来设置。

.grid-box-6 {
  /* 等同于 grid-template-columns: 1fr 1fr 1fr; */
	grid-template-columns: repeat(3, 1fr);
}

4.26 不确定容器尺寸下的自动填充

有的时候,父容器的尺寸是不确定的,我们需要把子元素往父容器中逐个填充,这个时候我们可以利用auto-fillgrid-template-columns: repeat(auto-fill, 50px);表示,每一列都是50px,但是具体有几列,需要根据子元素填充的情况来定。能放下8列,就放8列,不够9列的部分空白。

/* Grid 容器尺寸不固定,自动适配子元素 */
.grid-box-6 {
  border: 1px solid #999;
  display: grid;
  margin: 20px;
  grid-template-columns: repeat(auto-fill, 50px);
}
.grid-box-6 > div {
  height: 50px;
  background-color: bisque;
  border-radius: 4px;
  border: 1px solid #ccc;
}

如果不希望后面有空白呢,这个时候就需要子节点有适当的宽度适配。子节点不再是固定宽度,而是通过minmax函数指定最小值。如果容器的行不够整数,那么就按照1:1的比例去适当增宽子节点。

.grid-box-6 {
  grid-template-columns: repeat(auto-fill, minmax(50px, 1fr)) ;
}

4.27 网格项目重叠(通过设置z-index来改变覆盖顺序)

4.28 网格线的命名

使用Chrome Dev Tools布局查看,可以看到命名的网格线名字。

.grid-box-7 {
  grid-template-columns: [main-start] 1fr [content-start] 1fr [content-end] 1fr [main-end];
  grid-template-rows: [main-start] 40px [content-start] 40px [content-end] 40px [main-end];
}
.grid-box-7 > div:nth-of-type(1) {
  grid-column-start: main-start;
  grid-column-end: main-end;
  grid-row-start: main-start;
  grid-row-end: content-start;
}
.grid-box-7 > div:nth-of-type(2) {
  grid-column: main-start / content-start;
  grid-row: content-start / main-end;
}

4.29 填充缺口

有的场景下,由于子元素宽度的不确定性,会出现空格,前面通过了grid-template-columns: repeat(auto-fill, minmax(50px, 1fr))解决了部分情况。但是如果网格项目的宽度是不变的,但是顺序可变。这个时候,我们就可以通过grid-auto-flow来解决。

grid-auto-flow是控制自动布局算法怎样运作的属性,它能精确指定在网格中被自动布局的元素怎样排列。它有3个属性值:column``,row(默认),dense。可以看出来,如果是columns网格项目就是先把一列排满,再填如第二列。row就是先填满一行,因为这个是默认值,所以前面的例子都是先填满一行,再填下一行。

那这里的dense呢,它指定自动布局算法使用一种“稠密”堆积算法,如果后面出现了稍小的元素,则会试图去填充网格中前面留下的空白。这样做会填上稍大元素留下的空白,但同时也可能导致原来出现的次序被打乱。

这样我们就可以利用grid-auto-flow: dense来解决空白问题。(不能完美解决,只能让空白变小。)

比如下面这个例子,第一个子节点占了三列,第二个子节点占了2列,那么第一行就会空一个。添加了grid-auto-flow: row dense后,就能自动匹配能填充到这个空间的子节点。(grid-auto-flow: dense也是同样的效果)

.grid-box-9 {
  width: 200px;
  display: grid;
  gap: 2px;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 50px;
  /* grid-auto-flow: row dense; */
}
.grid-box-9 > div:nth-of-type(1){
  grid-column-end: span 3;
}
.grid-box-9 > div:nth-of-type(2n){
  grid-column-end: span 2;
  grid-row-end: span 2;
}

4.30 元素的对齐

和flex类似,Grid布局有以下容器属性用于对齐:justify-contentjustify-itemalign-contentalign-item,另外,Grid还增加了place-contentplace-item用于缩写。子元素也有三个用于对齐属性:justify-selfalign-selfplace-self

简单记忆:justify是在垂直方向对齐方式,align是水平方向对齐对齐,place是前面两个属性的缩写,先alignjustifycontent是容器子元素的对齐,item是子元素所在自己空间的对齐;self是子元素的属性,用于覆盖父容器对应的item样式。

4.301 justify-items: 垂直方向(列维度)的子元素在自己空间的对齐

4.302 align-items: 水平方向(行维度)的子元素在自己空间的对齐

4.303 justify-content: 垂直方向上,子元素在容器空间中的对齐

4.304 align-content: 水平方向上,子元素在容器空间中的对齐

4.305 justify-self 和 align-self

justify-self:子元素属性,垂直方向上的对齐方式,覆盖父元素中justify-items的值。具体的值和效果,同justify-items

align-self: 子元素属性,水平方向上的对齐方式,覆盖父元素中align-items的值。具体的值和效果,同align-items

4.4 容器属性

属性名说明属性值
grid-template-rows grid-template-columns是基于网格行或者网格列的维度,去定义网格线的名称和网格轨道的尺寸大小。 grid-template-rows:设置行 grid-template-columns:设置列grid-template-rows:基于网格行的轨道。 grid-template-columns:基于网格列的轨道。 none:这个关键字表示不明确的网格。所有的行和其大小都将由grid-auto-rows属性隐式的指定。 长度:非负值的长度大小。px,rem,百分比等。 fr:非负值,用单位 fr 来定义网格轨道大小的弹性系数。 minmax函数: 表示当前网格轨道的最大最小值。minmax(auto, 100px)表示最大值100px,自动最小值。 max-content:表示以网格项的最大的内容来占据网格轨道。 min-content:表示以网格项的最小的内容来占据网格轨道。 auto:如果该网格轨道为最大时,该属性等同于 max-content,为最小时,则等同于min-content。 fit-content函数:盒子会使用可用的空间,但永远不会超过max-content。 repeat函数:表示网格轨道的重复部分,以一种更简洁的方式去表示大量而且重复行的表达式。
grid-auto-rows grid-auto-columns指定隐式创建的行轨道大小。 grid-auto-rows:隐式行的大小。 grid-auto-columns:隐式列的大小。长度:px,百分比等。 fr:非负值,用单位 fr 来定义网格轨道大小的弹性系数。 minmax函数:表示当前网格轨道的最大最小值。minmax(auto, 100px)表示最大值100px,自动最小值。 max-content:表示以网格项的最大的内容来占据网格轨道。 min-content:表示以网格项的最小的内容来占据网格轨道。 auto:如果该网格轨道为最大时,该属性等同于 max-content,为最小时,则等同于min-content
grid-template-areas需要和网格布局容器的子元素的grid-area属性配合使用。参考上文中的“网格模板区域”小节。指定当前网格容器的子元素的网格区域。不能有L型区域或者分散的同名区域。多个字符串:每一个给定的字符串会生成一行,一个字符串中用空格分隔的每一个单元会生成一列。多个同名的,跨越相邻行或列的单元称为网格区块。非矩形的网格区块是无效的。
grid-templateCSS属性简写。包含了grid-template-rows,grid-template-columns,grid-template-areasgrid-template-rows / grid-template-columns : 同时指定网格行和网格列。比如:100px 1fr / 50px 1fr 2fr表示2行3列。 网格区域 + 行 / 列。
grid-auto-flow控制着自动布局算法怎样运作,精确指定在网格中被自动布局的元素怎样排列。row:指定自动布局算法按照通过逐行填充来排列元素,在必要时增加新行。 column:指定自动布局算法通过逐列填充来排列元素,在必要时增加新列。 dense:指定自动布局算法使用一种“稠密”堆积算法,如果后面出现了稍小的元素,则会试图去填充网格中前面留下的空白。这样做会填上稍大元素留下的空白,但同时也可能导致原来出现的次序被打乱。 row dense:行优先的“稠密”堆积算法。 column dense:列优先的行有限的“稠密”堆积算法。
grid-row-gap row-gap指定网格行的间距。长度:px,百分比等。
grid-column-gap column-gap指定网格列的间距。长度:px,百分比等。
grid-gap gap指定网格行列的间距。会有两个值,第一个是行间距,第二个是列间距。如果没有列间距,那么表示列间距和行间距一样。长度:px,百分比等。
gridCSS属性简写。包含了grid-templategrid-gapgrid-auto-flow值的组合方式有点复杂,不建议用吧,还是分开写代码可读性更好。
justify-items垂直方向(列维度)的子元素在自己空间的对齐start:对齐到最开始位置。 center:居中对齐 end:对齐到最末尾。 stretch:拉伸子元素到撑满整个容器
align-items水平方向(行维度)的子元素在自己空间的对齐start:对齐到最开始位置。 center:居中对齐 end:对齐到最末尾。 stretch:拉伸子元素到撑满整个容器 baseline:按照子元素的baseline对齐
place-itemsjustify-items和 align-items两个的缩写。先align-items再justify-items。
justify-content垂直方向上,子元素在容器空间中的对齐start:对齐到最开始位置。 center:居中对齐 end:对齐到最末尾。 space-between:多余空间平均分布在子元素中间。 space-around:多余空间平均环绕在子元素中间 space-evenly:多余空间平均分布子元素和容器边框之间。 stretch:拉伸子元素到撑满整个容器
align-content水平方向上,子元素在容器空间中的对齐justify-content
place-contentjustify-content和 align-content两个的缩写。先align-content再justify-content。

4.5 项目属性

属性名说明属性值
grid-column-start grid-column-end grid-column指定当前网格区域的占据的列范围。 grid-column-start指定开始的网格线。 grid-column-end指定结束的网格线。 grid-column是前面两个的缩写。顺序是<grid-column-start> / <grid-column-end>网格线:可以是网格线的数字,也可以是网格线的名字。 span:占据的网格列个数。 比如span 3表示从当前列开始,占据3列。
grid-row-start grid-row-end grid-row指定当前网格区域的占据的行范围。 grid-column-start指定开始的网格线。 grid-column-end指定结束的网格线。 grid-column是前面两个的缩写。<grid-column-start> / <grid-column-end>网格线:可以是网格线的数字,也可以是网格线的名字。 span:占据的网格行个数。 比如span 3表示从当前行开始,占据3行。
grid-area指定项目放在哪一个区域。有两种情况, 一种是指定名字,根据grid-template-area中的位置,进行指定。 另一种是指定网格线。名字:根据grid-template-area中的设置的位置,进行定位。 网格线:指定4根网格线来确定网格区域。比如:1 / 2 / 3 / 4。顺序是:grid-row-startgrid-column-startgrid-row-endgrid-column-end
justify-self当前元素的垂直对齐方式。用于覆盖父容器指定的justify-itemsstart:对齐到最开始位置。 center:居中对齐 end:对齐到最末尾。 stretch:拉伸子元素到撑满整个容器
align-self当前元素的水平对齐方式。用于覆盖父容器指定的align-itemsstart:对齐到最开始位置。 center:居中对齐 end:对齐到最末尾。 stretch:拉伸子元素到撑满整个容器 baseline:按照子元素的baseline对齐
place-selfjustify-selfalign-self两个的缩写。先align-selfjustify-self

五、HTTP的请求分为两种简单请求非简单请求

5.1 简单请求

简单请求要满足两个条件:

  1. 请求方法为:HEADGETPOST

  2. header中只能包含以下请求头字段:

    • Accept

    • Accept-Language

    • Content-Language

    • Content-Type: 所指的媒体类型值仅仅限于下列三者之一

      • text/plain
      • multipart/form-data
      • application/x-www-form-urlencoded

5.11 浏览器的不同处理方式

对于简单请求来说,如果请求跨域,那么浏览器会放行让请求发出。浏览器会发出cors请求,并携带origin。此时不管服务端返回的是什么,浏览器都会把返回拦截,并检查返回的responseheader中有没有Access-Control-Allow-Origin,这个头部信息的值通常为请求的Origin值,表示允许该来源的请求说明资源是共享的,可以拿到。如果Origin头部信息的值为"*"(表示允许来自任何来源的请求)但这种情况下需要谨慎使用,因为它存在安全隐患。如果没有这个头信息,说明服务端没有开启资源共享,浏览器会认为这次请求失败终止这次请求,并且报错。

5.2 非简单请求

只要不满足简单请求的条件,都认为是非简单请求。

发出非简单cors请求,浏览器会做一个http的查询请求(预检请求)也就是optionsoptions请求会按照简单请求来处理。那么为什么会做一次options请求呢?

检查服务器是否支持跨域请求,并且确认实际请求的安全性。预检请求的目的是为了保护客户端的安全,防止不受信任的网站利用用户的浏览器向其他网站发送恶意请求。 预检请求头中除了携带了origin字段还包含了两个特殊字段:

  • Access-Control-Request-Method: 告知服务器实际请求使用的HTTP方法
  • Access-Control-Request-Headers:告知服务器实际请求所携带的自定义首部字段。 比如:
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

以上报文中就可以看到,使用了OPTIONS请求,浏览器根据上面的使用的请求参数来决定是否需要发送,这样服务器就可以回应是否可以接受用实际的请求参数来发送请求。Access-Control-Request-Method告知服务器,实际请求将使用 POST 方法。Access-Control-Request-Headers告知服务器,实际请求将携带两个自定义请求标头字段:X-PINGOTHER 与 Content-Type。服务器据此决定,该实际请求是否被允许。

什么时候会触发预检请求呢?

    1. 发送跨域请求时,请求头中包含了一些非简单请求的头信息,例如自定义头(custom header)等;
    1. 发送跨域请求时,使用了 PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH等请求方法。

5.3 实际场景。(借鉴文章CORS 简单请求+预检请求(彻底理解跨域)的两张图)

image.png

image.png

六、ES2020 - ES2023

image.png

6.1 ES2023

6.11 数组被修改时返回副本

Array 和 TypedArray 有很多方法(比如 sort/splice 等)会改变数组自身,比如:

const array = [3, 2, 1];
const sortedArray = array.sort();
// [1, 2, 3]
console.log(sortedArray);
// 原数组也变成了 [1, 2, 3]
console.log(array);

如果不希望改变数组自身,可以这样做:

const array = [3, 2, 1];
const sortedArray = array.toSorted();
// [1, 2, 3]
console.log(sortedArray);
// 原数组不变 [3, 2, 1]
console.log(array);

类似的方法还有这些:

T.prototype.toReversed() -> T
T.prototype.toSorted(compareFn) -> t
T.prototype.toSpliced(start, deleteCount, ...items) -> T
T.prototype.with(index, value) -> T

with 是干什么的?看个例子就明白了:

const array = [1, 2, 3];
const newArray = array.with(1, false);

// [1, false, 3]
console.log(newArray);
// 原数组不变 [1, 2, 3]
console.log(array);

6.12 WeakMap 支持 Symbol 作为 key

WeakMap 原本只支持 object 类型的 key,现在支持了 Symbol 类型作为 key。

const weak = new WeakMap();
weak.set(Symbol('symbol1'), {});

6.13 Hashbang 语法

Hashbang 也叫 Shebang,是一个由井号和叹号构成的字符序列 #!,用来指定使用哪种解释器执行此文件:

// hashbang.js

#!/usr/bin/env node
console.log('hashbang');


// nohashbang.js

console.log('no hashbang')

在终端执行,没有 Hashbang 时,需要使用 node 指令才能执行:

image.png

6.14 从尾部查找

涉及到两个函数 findLast / findLastIndex

const array = [1, 2, 3]
array.findLast(n => n.value % 2 === 1); 
array.findLastIndex(n => n.value % 2 === 1); 

6.2 ES2022

6.21 异常链

直接看示例,通过 err1.cause 可以拿到 err0 ;如果这个异常被重新抛出了很多次,那通过 err1.cause.cause.... 就能拿到所有相关的异常。

function willThrowError() {
    try {
        // do something
    } catch (err0) {
        throw new Error('one error', { cause: err })
    }
}

try {
    willThrowError()
} catch (err1) {
  // 通过 err1.cause 就能拿到 err0 了
}

6.22 类静态代码块

静态代码块,用来实现复杂的静态属性/方法的初始化逻辑:

// 没有静态代码块时,初始化逻辑与类定义是分离开的
class C {
  static x = ...;
  static y;
  static z;
}

try {
  const obj = doSomethingWith(C.x);
  C.y = obj.y
  C.z = obj.z;
}
catch {
  C.y = ...;
  C.z = ...;
}

// 使用了静态代码块,代码逻辑更收敛
class C {
  static x = ...;
  static y;
  static z;
  static {
    try {
      // 这里的 this 代表 C 而不是 C 的实例
      const obj = doSomethingWith(this.x);
      this.y = obj.y;
      this.z = obj.z;
    }
    catch {
      this.y = ...;
      this.z = ...;
    }
  }
}

还有个特殊的能力:访问私有属性

let getPrivateField;
class D {
    #privateField;
    constructor(v) {
        this.#privateField = v;
    }
    static {
        getPrivateField = (d) => d.#privateField;
    }
}
getPrivateField(new D('private value'));
// → private value

6.23 Object.hasOwn

我们经常会看到这种用法,尤其是在开源库里:

let hasOwnProperty = Object.prototype.hasOwnProperty

if (hasOwnProperty.call(object, "foo")) {
  console.log("has property foo")
}

如果使用 Object.hasOwn,可以简化成:

if (Object.hasOwn(object, "foo")) {
  console.log("has property foo")
}

6.24 .at 返回指定索引的元素

可索引类型(String/Array/TypedArray)可以通过 at 来读取指定索引的元素,而且支持传入负数

// 返回 4
[1, 2, 3, 4].at(-1)

6.25 判断私有属性是否存在(in)

可以通过 in 关键字来判断

class C {
    #brand = 0;
  
    static isC(obj: C) {
      return #brand in obj;
    }
  }

6.26 顶层 await

不需要 async 包裹,也可以使用 await,比如:

function fn() {
    return Promise.resolve();
}

// 以前需要通过 IIFE 实现
(async function () {
    await fn();
})();

// 支持顶层 await 后,可以直接调用
await fn();

6.27 正则表达式切片

通过正则匹配,我们只能拿到匹配到的字符串,通过切片可以拿到这些字符串在原字符串的位置

const re1 = /a+(z)?/d;
const s1 = "xaaaz";
const m1 = re1.exec(s1);

image.png

6.3 ES2021

6.31 数值分隔符

如果数字比较长,可读性就比较差,通过分隔符 _ 可以提高可读性:

// 1000000
console.log(1_000_000)
// 分隔符只要不是数字的第一位和最后一位,放到任何位置都是有效的
1_000000000_1_2_3

6.32 逻辑运算符赋值

和算术运算符赋值是类似的,但是逻辑运算符还会存在短路能力,可以对比看一下:

// 算数运算符
let x = 1;
// x 是 2
x += 1;

// 逻辑运算符
let x = 1;
// x 依然是 1,因为 x 是真值,所以被短路,相当于 x = 1 || 3
x ||= 3;

6.33 WeakRef

弱引用,可以引用某个对象,而且不会阻止该对象被垃圾回收。

let obj = { name: 'obj1' };
let weakRef = new WeakRef(obj);
// 获取引用的对象
// 如果引用对象被回收,拿到的就是 undefined
weakRef.deref();

6.34 Promise.any

和其他几个函数做个对比。

Promise.any

只要有一个输入 resolve 了,那么它就会 resolve:

Promise.any([
  Promise.reject('reject 1'),
  Promise.reject('reject 2'),
  Promise.reject('reject 3'),
  Promise.resolve('1'),
  Promise.resolve('2'),
]).then(
  first => {
    // 只要有一个 resolve,就会执行到这里
    // 打印的是 1
    console.log(first);
  },
  error => {
    // 所有都 reject 时,才会走到这里
    console.log(error);
  },
);

Promise.allSettled

无论输入的 promise 是 reject 还是 resolve,都会汇总给 then

Promise.allSettled([
  Promise.resolve('1'),
  Promise.resolve('2'),
  Promise.reject('e1'),
  Promise.resolve('3'),
  Promise.resolve('4'),
])
  .then(res => {
    console.log('then', res);
  });

image

Promise.all

只要有一个 reject,那么就会 reject。下面的示例会走到 catch:

Promise.all([
  Promise.resolve('1'),
  Promise.resolve('2'),
  Promise.reject('e1'),
  Promise.resolve('3'),
])
  .then(res => {
    console.log('then', res);
  })
  .catch(err => {
    // 打印 catch e1
    console.log('catch', err);
  });

Promise.race

如果有一个 reject(或 resolve)了,那么就会 reject(或 resolve)

function delay(type: 'resolve' | 'reject', timeout: number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (type === 'reject') {
        reject(type);
      } else {
        resolve(type);
      }
    }, timeout);
  });
}
// 打印 then resolve
Promise.race([delay('resolve', 1000), delay('reject', 2000)])
  .then(res => {
    console.log('then', res);
  })
  .catch(err => {
    console.log('catch', err);
  });
  
 // 打印 catch reject
Promise.race([delay('resolve', 2000), delay('reject', 1000)])
  .then(res => {
    console.log('then', res);
  })
  .catch(err => {
    console.log('catch', err);
  });

6.35 String.prototype.replaceAll

替换所有匹配的字符串(类似于replace(/' '/gi,''))

// 12c12d12
'abcabdab'.replaceAll('ab', '12')
// 12cabdab
'abcabdab'.replace('ab', '12')

6.4 ES2020

6.41 import.meta

提供了与宿主相关的模块元信息。

6.42 空值合并运算符

只有在左操作数为 null 或 undefined 时,才会返回右操作数

// "default"
let a = undefined || "default"
// "default"
let a = undefined ?? "default"

// "default"
let a = null || "default"
// "default"
let a = null ?? "default"

// "default"
let a = "" || "default"
// ""
let a = "" ?? "default"

// "default"
let a = 0 || "default"
// 0
let a = 0 ?? "default"

6.43 可选链

// 不用可选链
const street = user.address && user.address.street;
// 用可选链
const street = user.address?.street

6.44 BigInt

可以表示大于2的53次方(js Number 类型可表示的最大数字)的数字

// 数字尾部加个 n
const theBiggestInt = 9007199254740991n;
// 或者直接调 BigInt 构造
const alsoHuge = BigInt(9007199254740991);

6.45 import()

运行时动态加载模块。

6.46 String.prototype.matchAll

match 只能返回匹配的字符串,但是无法返回组信息;matchAll 可以返回匹配的字符串,以及组信息。

image.png

七、 Dom中的各种距离

7.1 JS Dom各种距离释义

7.11 window.devicePixelRatio

  • 像素 屏幕中最小的色块,每个色块称之为一个像素(Pixel)

image.png

image.png

  • 设备像素比

设备像素比的定义是:

window.devicePixelRatio =显示设备物理像素分辨率显示设备CSS像素分辨率\frac{显示设备物理像素分辨率}{显示设备CSS像素分辨率}显示设备CSS像素分辨率显示设备物理像素分辨率​

根据设备像素比的定义, 如果知道显示设备横向的css像素值,根据上面的公式,就能计算出显示设备横向的物理像素值。

显示设备宽度物理像素值= window.screen.width * window.devicePixelRatio;
  • window.devicePixelRatio 是由什么决定的 ?

发现是由笔记本电脑屏幕的缩放设置决定的,如果设置成100%, 此时window.screen.width与笔记本电脑的显示器分辨率X轴方向的数值一致,都是1920(如右侧图所示), 此时屏幕上的字会变得比较小,比较伤视力。

  • 逻辑像素是为了解决什么问题?

逻辑像素是为了解决屏幕相同,分辨率不同的两台显示设备, 显示同一张图片大小明显不一致的问题。

7.12 document.body、document.documentElement和window.screen的宽高区别

7.121 差别是很容易辨别的,如下图所示:

  • document.body -- body标签的宽高
  • document.documentElement -- 网页可视区域的宽高(不包括滚动条)
  • window.screen -- 屏幕的宽高

7.122 屏幕高度和屏幕可用高度区别如下:

屏幕可用高度=屏幕高度-屏幕下方任务栏的高度:

window.screen.availHeight = window.screen.height - 系统任务栏高度

7.13 scrollWidth, scrollLeft, clientWidth关系

scrollWidth(滚动宽度,包含滚动条的宽度)=scrollLeft(左边卷去的距离)+clientWidth(可见部分宽度);
// 同理
scrollHeight(滚动高度,包含滚动条的高度)=scrollTop(上边卷去的距离)+clientHeight(可见部分高度);

需要注意的是,上面这三个属性,都取的是溢出元素的父级元素属性。而不是溢出元素本身。本例中溢出元素是body(document.body),其父级元素是html(document.documentElement)。另外,

溢出元素的宽度(document.body.scrollWidth)=父级元素的宽度(document.documentElement.scrollWidth) - 滚动条的宽度(在谷歌浏览器上滚动条的宽度是19px)

7.14 元素自身和父级元素的scrollWidth和scrollLeft关系?

从下图可以看出:

  • 元素自身没有X轴偏移量,元素自身的滚动宽度不包含滚动条
  • 父级元素有X轴偏移量, 父级元素滚动宽度包含滚动条

image.png

7.15 offsetWidth和clientWidth的关系?

offsetWidth和clientWidth的共同点是都包括 自身宽度+padding , 不同点是offsetWidth包含border

如下图所示:

  • rect元素的clientWidth=200px(自身宽度) + 20px(左右padding) = 220px
  • rect元素的offsetWidth=200px(自身宽度) + 20px(左右padding) + 2px(左右boder) = 222px

7.16 event.clientX,event.clientY, event.offsetX 和 event.offsetY 关系

代码如下,给rect元素添加一个mousedown事件,打印出事件源的各种位置值。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> -->
    <title>JS Dom各种距离</title>
    <style>
        html,
        body {
            margin: 0;
        }

        body {
            width: 200px;
            padding: 10px;
            border: 10px solid blue;
        }

        .rect {
            height: 50px;
            background-color: green;
        }
    </style>
</head>

<body>

    <div id="rect" class="rect"></div>


</body>
<script>
    const rectDom = document.querySelector('#rect');

    rectDom.addEventListener('mousedown', ({ offsetX, offsetY, clientX, clientY, pageX, pageY, screenX, screenY }) => {
        console.log({ offsetX, offsetY, clientX, clientY, pageX, pageY, screenX, screenY });
    })
</script>

</html>

我们通过y轴方向的高度值,了解一下这几个属性的含义。 绿色块的高度是50px, 我们找个特殊的位置(绿色块的右小角)点击一下,如下图所示:

  • offsetY=49, 反推出这个值是相对于元素自身的顶部的距离
  • clientY=69, body标签的border-top是10,paiding是10, 反推出这个值是相对网页可视区域顶部的距离
  • screenY=140,目测肯定是基于浏览器窗口,

所以它们各自的含义,就很清楚了。

image.png

事件源属性表示的距离
event.offsetX、event.offsetY鼠标相对于事件源元素(srcElement)的X,Y坐标,
event.clientX、event.clientY鼠标相对于浏览器窗口可视区域的X,Y坐标(窗口坐标),可视区域不包括工具栏和滚动偏移量。
event.pageX、event.pageY鼠标相对于文档坐标的x,y坐标,文档坐标系坐标 = 视口坐标系坐标 + 滚动的偏移量
event.screenX、event.screenY鼠标相对于用户显示器屏幕左上角的X,Y坐标
  • pageX和clientX的关系

我们点击下图绿色块的右下角,把pageX和clientX值打印出来。如下图所示:

  • 可视区域的宽度是360,点击点的clientX=359(由于是手动点击,有误差也正常)
  • 水平方向的偏移量是56
  • pageX是415,360+56=416,考虑到点击误差,可以推算出 ele.pageX = ele.clientX + ele.scrollLeft

image.png

7.17 getBoundingClientRect获取的top,bottom,left,right的含义

从下图可以看出,上下左右这四个属性,都是相对于浏览器可视区域左上角而言的。

从下图可以看出,当有滚动条出现的时候,right的值是359.6,而不是360+156(x轴的偏移量), 说明通过getBoundingClientRect获取的属性值是不计算滚动偏移量的,是相对浏览器可视区域而言的。

image.png

7.18 movementX和movementY的含义?

MouseEvent.movementX/movementX是一个相对偏移量。返回当前位置与上一个mousemove事件之间的水平/垂直距离。以当前位置为基准, 鼠标向左移动, movementX就是负值,向右移动,movementX就是正值。鼠标向上移动,movementY就是负值,向下移动,movementY就是正值。数值上,它们等于下面的计算公式。 这两个值在设置拖拽距离的时候高频使用,用起来很方便。

curEvent.movementX = curEvent.screenX - prevEvent.screenX;
curEvent.movementY = curEvent.screenY - prevEvent.screenY;

7.2 想移动元素,mouse和drag事件怎么选?

mouse事件相对简单,只有mousedown(开始),mousemove(移动中),mouseup(结束)三种。与之对应的移动端事件是touch事件,也是三种touchstart(手指触摸屏幕), touchmove(手指在屏幕上移动), touchend(手指离开屏幕)。

相对而言, drag事件就要丰富一些。

  • 被拖拽元素事件
事件名触发时机触发次数
dragstart拖拽开始时触发一次1
drag拖拽开始后反复触发多次
dragend拖拽结束后触发一次1
  • 目标容器事件
事件名触发时机触发次数
dragenter被拖拽元素进入目标时触发一次1
dragover被拖拽元素在目标容器范围内时反复触发多次
drop被拖拽元素在目标容器内释放时(前提是设置了dropover事件)1

想要移动一个元素,该如何选择这两种事件类型呢? 选择依据是:

类型选择依据
mouse事件1. 要求丝滑的拖拽体验 2. 无固定的拖拽区域 3. 无需传数据
drag事件1. 拖拽区域有范围限制 2. 对拖拽流畅性要求不高 3. 拖拽时需要传数据

7.3 现在让我们写个拖拽效果

image.png

<!DOCTYPE html>
<html lang="en">

<head>
        
    <meta charset="UTF-8" />    
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />   
    <title>移动小鸟</title>
    <style>
        body {
            margin: 0;
            font-size: 0;
            position: relative;
            height: 100vh;
        }

        .bird {
            position: absolute;
            width: 100px;
            height: 100px;
            cursor: grab;
            z-index: 10;
        }
    </style>
</head>

<body>
    <img class="bird" src="./bird.png" alt="" />  
</body>

<script>
    let evtName = getEventName();
    // 鼠标指针相对于浏览器可视区域的偏移
    let offsetX = 0, offsetY = 0;
    // 限制图片可以X和Y轴可以移动的最大范围,防止溢出
    let limitX = 0, limitY = 0;

    // 确保图片加载完
    window.onload = () => {
        const bird = document.querySelector(".bird");
        const { width, height } = bird;

        limitX = document.documentElement.clientWidth - width;
        limitY = document.documentElement.clientHeight - height;

        bird.addEventListener(evtName.start, (event) => {
            // 监听鼠标指针相对于可视窗口移动的距离
            // 注意移动事件要绑定在document元素上,防止移动过快,位置丢失
            document.addEventListener(evtName.move, moveAt);
        });

        // 鼠标指针停止移动时,释放document上绑定的移动事件
        // 不然白白产生性能开销
        document.addEventListener(evtName.end, () => {
            document.removeEventListener(evtName.move, moveAt);
        })

        // 移动元素
        function moveAt({ movementX, movementY }) {
            const { offsetX, offsetY } = getSafeOffset({ movementX, movementY });

            window.requestAnimationFrame(() => {
                bird.style.cssText = `left:${offsetX}px;top:${offsetY}px;`;
            });
        };
    };

    // 获取安全的偏移距离
    const getSafeOffset = ({ movementX, movementY }) => {
        // //距上次鼠标位置的X,Y方向的偏移量
        offsetX += movementX;
        offsetY += movementY;

        // 防止拖拽元素被甩出可视区域
        if (offsetX > limitX) {
            offsetX = limitX;
        }

        if (offsetX < 0) {
            offsetX = 0;
        }

        if (offsetY > limitY) {
            offsetY = limitY;
        }

        if (offsetY < 0) {
            offsetY = 0;
        }

        // console.log({ movementX, movementY, offsetX, offsetY });
        return { offsetX, offsetY };
    }

    // 区分是移动端还是PC端移动事件
    function getEventName() {
        if ("ontouchstart" in window) {
            return {
                start: "touchstart",
                move: "touchmove",
                end: "touchend",
            };
        } else {
            return {
                start: "pointerdown",
                move: "pointermove",
                end: "pointerup",
            };
        }
    }
</script>

</html>

7.4 问题反馈

在chrome浏览器上发现一个奇怪的现象,设置的border值是整数,计算出来的值却带有小数

原因:这篇文章解释说,浏览器可能只能渲染具有整数物理像素的border值,不是整数物理像素的值时,计算出的是近似border值。

在设备像素比是window.devicePixelRatio=1.25的情况下, 1px对应的是1.25物理像素, 1.25*4的倍数才是整数,所以设置的逻辑像素是4的整数倍数,显示的渲染计算值与设置值一致。但是padding,margin,width/height却不遵循同样的规则。

八、Flutter - 为什么选择 Dart

8.1. 多编译模式

首先,最最重要的一点就是Dart多样化的编译方式。

  • 一般来说,静态类型语言(例如 C / C++)主要是以 AOT(Ahead Of Time)的方式编译,
  • 而动态类型语言(例如 JavaScript)则是以 JIT(Just In Time)的方式编译。

而 Dart 作为一个静态类型语言,不但是众多编程语言中少数同时支持 AOT 和 JIT 的语言,而且可以转译成 JavaScript,甚至还可以作为直译式语言,在 Dart VM 上直接执行。支持 JIT 编译让 Flutter 在开发阶段能够通过 Hot Reload 进行极为快速的迭代,而支持 AOT 编译则让 Flutter 能在部署时产生启动快、执行快且可预期的 ARM Code。通过 Dart 实现的 Stateful Hot Reload 不仅大幅提升了 Flutter 的开发效率,而且进一步影响了 Flutter UI 的编写方式。

8.2. 单一 UI 语言

不论是 iOS 或 Android,编写 UI 时大多都会使用各自的模板语言(xib / XML),并搭配 IDE 提供的工具进行快速编辑和预览。当然也有人因为各种因素选择直接使用代码来编写 UI,但相对的就必须经过冗长的编译过程才能看到修改的结果。Stateful Hot Reload 彻底解决了这个问题。

Dart 是世上少数同时拥有 JIT 和 AOT 的语言,在实现了 Hot Reload 的同时也能兼顾到发布时的性能。

8.3. 快速对象分配及回收

首先,Flutter UI 设计受到了 React 很大的启发,它是陈述式以及复合式的。不同于React的是,Dart作为面向对象语言,自然是以Class而非Function作为复合的主要元素。最后,Flutter不但采取了复合,而且还进一步实现成了激进式复合(Aggressive composability) 。套一个简单的背景色,你得复合一个DecoratedBox;想要居中,你要再复合一个Center;想加入点击事件,再一个GestureDetector。这每一层都是一个需要被分配的对象。这样的设计让 Flutter 在绘制 UI 时必须不断分配和回收大量微小又短命的对象,而 Dart 独特的对象分配和回收机制正好适合这个任务,使其不会在 GC 时卡住 Flutter 引以为傲的 60FPS。


总结来说,未来 Dart 在语法特性上很可能会逐渐赶上 C#、Swift、Kotlin 等其他强大的语言,但同时又保有最重要的多编译模式。试想如果当初 Flutter 团队选择的是 Kotlin,能有多少影响力请 Kotlin 团队放下所有 issue / feature,全力开发能支持极快速 Stateful Hot Reload 的 JIT / AOT 双编译模式?我想可以说 Flutter 团队不是选择了 Dart,而是选择了一个能让它尽情培养,年轻但潜力无穷的编程语言。

九、React新文档8条小结

9.1. 换个角度认识Props与State

PropsState是React中两个略有相似的概念。在一个React组件中,它们作为数据来源可能同时存在,但它们之间有着巨大不同:

    1. Props更像函数中的参数。它主要用于组件之间的信息传递,它可以是任意类型的数据:数字、文本、函数、甚至于组件等等。
    1. State更像组件中的内存。它可以保存组件内部的状态,组件通过跟踪这些状态,从而渲染更新。
import React, { useState } from 'react';

// 父组件
const ParentComponent = () => {
  const [count, setCount] = useState(0); // 使用state来追踪count的值

  return (
    <div>
      <ChildComponent age={25} />
      <p>Count: {count}</p>
    </div>
  );
};

// 子组件
const ChildComponent = (props) => {
  const { age } = props; // 使用props来获取父组件传递的数据

  return (
    <div>
      <p>Age: {age}</p>
    </div>
  );
};

9.2. 不要嵌套定义组件

嵌套定义组件会导致渲染速度变慢,也更容易出现BUG。 我们在嵌套定义组件的情况下,当父组件更新时,内部嵌套的子组件也会重新生成并渲染,子组件内部的state也会丢失。针对这种情况,我们可以通过两种方式来解决:

    1. 为子组件包上useMemo,避免不必要的更新;
    1. 不再嵌套定义,将子组件提至父组件外,通过Props传参。

这里更推荐第二种方法,因为这样代码更少,结构也更清晰,组件迁移维护都会更方便一些。

//🔴 Bad Case
export default function Gallery() {
  function Profile() {
    // ...
  }
  // ...
}
//✅  Good Case
function Profile() {
  // ...
}

export default function Gallery() {
  // ...
}

9.3. 尽量不要使用匿名函数组件

因为类似export default () => {}的匿名函数组件书写虽然方便,但会让debug变得更困难。 如下是两种不同类型组件出错时的控制台的表现:

    1. 具名组件出错时的提示,可直接的指出错的函数组件名称: image.png
    1. 匿名函数出错时的提示,无法直观确定页面内哪个组件出了错: image.png

9.4. 使用逻辑运算符&&编写JSX时,左侧最好不要是数字

运算符&&在JSX中的表现与JS略有不同:

  1. 在JS中,只有在&&左侧的值0nullundefinedfalse''等假值时,才会返回右侧值;
  2. 在JSX中,React对于falsenullundefined并不会做渲染处理,所以进行&&运算后,如果左侧值为假被返回后,会出现与直觉不同的渲染结果:可能会碰到页面上莫名奇妙出现了一个0的问题。为了避免出现这种情况,可以在书写JSX时,为左侧的值加上!!来进行强制类型转换。
const flag = 0
//🔴 Bad Case
{
    flag && <div>123</div>
}
//✅  Good Case 1
{
    !!flag && <div>123</div>
}
//✅  Good Case 2
{
    flag > 0 && <div>123</div>
}

关于JSX对各种常见假值的渲染,这里进行了总结:

  1. nullundefinedfalse:这些值在JSX中会被视为空,不会渲染任何内容。它们会被忽略,并且不会生成对应的DOM元素。
  2. 0NaN:这些值会在页面上渲染为字符串"0"、"NaN"。
  3. ''[]{}:空字符串会被渲染为什么都没有,不会显示任何内容。

9.5.全写的 Fragment标签上可以添加属性key

在map循环添加组件时,需要为组件添加key来优化性能。如果待循环的组件有多个时,可以在外层包裹<Fragment>...</Fragment>标签,在其上添加key添加在<Fragment>...</Fragment>来避免创建额外的组件。

const list = [1,2,3]
//🔴 Bad Case
//不能添加key
{
    list.map(v=><> <div>1-1</div>  <div>1-2</div> </>)
}
//🔴 Bad Case
//创建了额外的div节点
{
    list.map(v=><div key={v}> <div>1-1</div>  <div>1-2</div> <div/>)
}
//✅  Good Case
{
    list.map(v=><Fragment key={v}> <div>1-1</div>  <div>1-2</div> </Fragment>)
}

注意简写的Fragment标签<>...</>上不支持添加key

9.6. 可以使用updater function,来在下一次渲染之前多次更新同一个state

React中处理状态更新时采用了批处理机制,在下一次渲染之前多次更新同一个state,可能不会达到我们想要的效果。如下是一个简单例子:

// 按照直觉一次点击后button中的文字应展示为3,但实际是1
function Demo(){
    const [a,setA] = useState(0)
    
    function handler(){
        setA(a + 1);
        setA(a + 1);
        setA(a + 1);
    }
    
    return <button onclick={handler}>{a}</button>
}

在某些极端场景下,我们可能希望state的值能够及时发生变化,这时便可以采用setA(n => n + 1)(这种形式被称为updater function)来进行更新:

// 一次点击后a的值会被更新为3
function Demo(){
    const [a,setA] = useState(0)
    
    function handler(){
        setA(n => n + 1);
        setA(n => n + 1);
        setA(n => n + 1);
    }
    
    return <button onclick={handler}>{a}</button>
}

9.7. 管理状态的一些原则

更加结构化的状态有助于提高我们组件的健壮性,以下是一些管理组件内状态时可以参考的原则:

  1. 精简相关状态:如果在更新某个状态时总是需要同步更新其他状态变量,可以考虑将它们合并为一个状态变量。
  2. 避免矛盾状态:避免出现两个或多个状态的值互相矛盾的情况。
  3. 避免冗余状态:一个状态可以由其余状态计算而来时,其不应单独作为一个状态来进行管理。
  4. 避免状态重复:当相同的数据在多个状态变量之间或嵌套对象中重复时,很难使它们保持同步。尽可能减少重复。
  5. 避免深度嵌套状态:嵌套层级过深的状态更新时相对麻烦许多,尽量保持状态扁平化。

9.8. 使用useSyncExternalStore订阅外部状态

useSyncExternalStore是一个React18中新提供的一个Hook,可以用于订阅外部状态。 它的使用方式如下:

import { useSyncExternalStore } from 'react';

function MyComponent() {
  const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
  // ...
}

useSyncExternalStore接受三个参数:

  • subscribe:一个订阅函数,用于订阅存储,并返回一个取消订阅的函数。
  • getSnapshot:一个从存储中获取数据快照的函数。在存储未发生变化时,重复调用getSnapshot应该返回相同的值。当存储发生变化且返回值不同时,React会重新渲染组件。
  • getServerSnapshot(可选):一个在服务器端渲染时获取存储初始快照的函数。它仅在服务器端渲染和在客户端进行服务器呈现内容的hydration过程中使用。如果省略该参数,在服务器端渲染组件时会抛出错误。

举一个具体的例子,我们可能需要获取浏览器的联网状态,并期望在浏览器网络状态发生变化时做一些处理。使用useSyncExternalStore可以很大程度上简化我们的代码,同时使逻辑更加清晰:

//🔴 Bad Case
function useOnlineStatus() {
  // Not ideal: Manual store subscription in an Effect
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function updateState() {
      setIsOnline(navigator.onLine);
    }

    updateState();

    window.addEventListener('online', updateState);
    window.addEventListener('offline', updateState);
    return () => {
      window.removeEventListener('online', updateState);
      window.removeEventListener('offline', updateState);
    };
  }, []);
  return isOnline;
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}
function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

// ✅ GoodCase
function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe, // React won't resubscribe for as long as you pass the same function
    () => navigator.onLine, // How to get the value on the client
    () => true // How to get the value on the server
  );
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

十、 package.json

10.1 前言

package.json作为项目的清单文件,记录了项目所依赖的各种包以及自定义的脚本、版本等信息,是现代前端项目中最为重要的文件之一。

生成package.json,使用npm init,然后依次输入选项或者一直按回车,就可以自动化生成一个package.json。如果觉得麻烦,运行npm init -y就可以默认生成一个package.json。

{
  "name": "project", // 项目名称
  "version": "1.0.0", // 项目版本
  "description": "", // 项目描述
  "main": "index.js", // 项目入口文件
  "scripts": { // 指定运行脚本命令的 npm 命令行缩写
    "test": "echo "Error: no test specified" && exit 1"
  },
  "author": "",// 作者
  "license": "ISC" // 许可证
}

10.2 项目概况

10.21 nameversion

如果你想要在npm发布一个包,"name"和"version"是必填的,并且将会是作为你的包的唯一id。

10.22 descriptionkeywords

"description"是一个项目描述,接受一个字符串。
"keywords"是一个数组,数组里面可以放多个项目关键词。
两者都是为了方便让npm搜索,也方便开发者更容易了解该项目的意义。

10.23 homepage, repositorybugs

"homepage" 是这个项目的首页路径,方便开发者访问这个包的主页获取更多信息。

"homepage": "https://github.com/owner/project#readme"

"repository" 是这个项目的代码仓库,方便贡献者找到代码仓库。

"repository": "git+https://github.com/username/repo-name.git"
"repository": {
  "type": "git",
  "url": "git+https://github.com/username/repo-name.git"
}

"bugs" 是为了方便开发者对项目的bug提出建议或意见。

"bugs": { "url" : "https://github.com/owner/project/issues", "email" : "project@hostname.com" }

10.24 unpkgjsdelivr

CDN方式下,引入当前npm包的链接。

"unpkg": "lib/index.iife.js",
"jsdelivr": "lib/index.iife.js",

10.3 项目环境

10.31 engines

package.json中的engines字段用于指定npm包运行所需的环境。它包含了node, npm, yarn等环境的版本要求。engines的格式是:

json
    "engines": {
      "node": ">=10.13.0",
      "npm": ">=6.4.1",
      "yarn": "^1.9.4"
    }

engines字段主要是确保npm包在安装后可以正确运行所需的环境。如果环境版本不匹配,npm install时会显示警告信息。让其他开发者在使用该包之前可以确保拥有匹配的环境,避免因版本不兼容导致的错误或警告。

10.32 os

指定操作系统

{ "os": [ "darwin", "linux" ] } // 适用
{ "os": [ "!linux" ] } // 禁止

10.33 cpu

{ "cpu" : [ "x64", "ia32" ] } # 适用
{ "cpu" : [ "!arm", "!mips" ] } # 禁止

10.4 项目分析

10.41 main

"main" 定义了项目的入口文件,默认值是根目录的"index.js"。

10.42 module

性质等同于main字段。module用于ES6规范的模块,只要支持ES6,会优先使用module入口。
这样,代码也可以启用tree shaking机制。

"module": "es/index.mjs",

10.43 bin

很多包都有一个可执行命令,并且希望被安装到环境变量中。npm 安装依赖的时候, npm会通过node_modules/.bin 将 bin 里面的文件暴露出来,当你通过 npx myapp的时候可以直接运行该命令。

{
    "bin": {
        "myapp": "./cli.js" 
    } 
}

10.44 dependenciesdevDependencies

10.441 两者的作用和区别

两者都是为了记录需要的依赖以及版本。

devDependencies用于开发环境下依赖的模块,生产环境不需要被打入包内。运行命令行npm i xxx --save-dev,会被安装到 "devDependencies"

dependencies依赖的包不仅开发环境能使用,生产环境也能使用。运行命令行npm i xxx,会被安装到 "devDependencies"

总结: 通常情况下, devDependencies一般是放置一些代码规范工具、打包工具、编译器等,而dependencies一般是放置组件库、框架等与页面相关的插件。

10.442 指定不同版本

{ 
    "dependencies": {
        "foo": "1.0.0 - 2.9999.9999", // 1.0.1-2.9999.9999版本(和下面的表达差不多意思)
        "bar": ">=1.0.2 <2.1.2", // 大于或等于1.0.2版本小于2.1.2版本
        "baz": ">1.0.2 <=2.3.4", // 大于1.0.2版本小于等于2.3.4版本
        "boo": "2.0.1", // 指定版本
        "qux": "<1.0.0 || >=2.3.1 <2.4.5", // 小于1.0.0或者大于2.3.1且小于2.4.5
        "til": "~1.2.3", // ~ 匹配1.2.x的所有小版本,但不会匹配到1.3.x
        "elf": "^1.2.3", // 匹配1.x.x的所有大版本,但不会匹配2.x.x
        "two": "2.x", // 匹配所有2.x开头版本
        "thr": "3.3.x", // 匹配3.3.x版本
        "lat": "latest", // 最新版本
        "dyl": "file:../dyl" 
    } 
}

10.442 指定url的包

开发过程中,有可能你需要直接使用本地的包或者git仓库的包进行调试,又不希望频繁上传到npm包管理器这么麻烦。

<protocol>://[<user>[:<password>]@]<hostname>[:<port>][:][/]<path>[#<commit-ish> | #semver:<semver>]

可以根据version或者commit-ish来进行精确下载。

10.443 git示例

{ 
    "dependencies": {
        "bar": "git+ssh://git@github.com:npm/cli.git#v1.0.27",
        "foo": "git+ssh://git@github.com:npm/cli#semver:^5.0",
        "baz": "git+https://isaacs@github.com/npm/cli.git",
        "boo": "git://github.com/npm/cli.git#v1.0.27"
    } 
}

10.444 本地文件示例

{ 
    "dependencies": {
        "bar": "file:../foo/bar" 
    } 
}

10.5 项目运行

10.51 scripts

10.511 scripts字段的作用

scripts 属性是一个字典,对象的属性名为可以通过 npm run 运行命令,属性值可执行命令。{"xxx": "vite"},在这里使用终端执行npm run xxx

如果你没有全局安装vite,直接去执行vite命令行,终端是会报错的。但这里通过npm run xxx执行命令并没有报错,是因为我们在安装依赖的时候,是通过npm i xxx 来执行的,例如 npm i vite,npm 在 安装这个依赖的时候,就会node_modules/.bin/ 目录中创建好vite 为名的几个可执行文件了(上面的bin也提到了)。

image.png

.bin 目录,这个目录不是任何一个 npm 包。而是一个脚本。所以我们执行scripts里面的命令时,虽然在全局找不到对应的可运行命令,但也可以通过.bin文件夹下找到对应的脚本。

10.512 自定义脚本的写法

可以通过自定义脚本直接运行一些命令行提高自己的开发效率,或者运行脚本来打包配置项目。

{
  "scripts": {
      "clean": "rm -rf dist",
      "update": "sh ./update.sh"
  }
}

10.6 项目规范

10.61 huskylint-staged

10.611 husky的作用

husky是一个可以给git hooks添加执行自定义脚本的工具。它可以在git事件触发时(commit, push, rebase等)自动执行我们所配置的脚本,从而实现lint校验,测试运行,构建打包等工作流程。

10.612 配合git事件触发脚本

husky的安装和配置也很简单,主要有以下几个步骤:

  1. 安装husky包:npm install husky --save-dev
  2. 在package.json中添加git hooks脚本,如:"pre-commit": "npm run lint"
  3. 添加husky配置到package.json,如:"husky": { "hooks": { "pre-commit": "npm run lint"} }
  4. git commit触发pre-commit钩子,从而自动执行npm run lint进行lint校验。

10.613 lint-staged的作用

lint-staged是一个在git暂存文件(staged files)上运行代码格式规范的工具。它的主要作用是:

  1. 只校验暂存文件的改动(diff),而不是整个项目。这样可以提高lint效率,并且避免提交未修改的文件产生的lint错误。
  2. 支持在git pre-commit钩子中使用,结合husky可以实现commit前自动lint校验。
  3. 支持多种文件类型(JavaScript, CSS, Markdown 等)和多个linters。如ESLint, Stylelint, Prettier等。4. 提供了简单的配置方式,可以轻松地在package.json中完成配置。

10.614 lint-staged配合husky

  1. 安装lint-staged和相关的代码规范工具:npm install lint-staged eslint prettier -D
  2. 在package.json中添加lint-staged配置:"lint-staged": {"*.js": "eslint --fix",}
  3. 安装husky并配置pre-commit hook:"husky": { "hooks": { "pre-commit": "npm run lint"} }
  4. git add 添加文件并commit,pre-commit钩子就会自动执行lint-staged lint校验。

所以,lint-staged、husky是一个非常实用的工具,让我们的代码质量管理可以更加自动化。理解lint-staged、husky的作用和配置方式,可以很好地提高我们的前端工程化水平。

10.62types

项目如果是用TypeScript写的,则需要types字段,对外暴露相关的类型定义。


"types": "lib/index.d.ts",

10.7 项目打包配置

10.71 private

"private" 配置一个布尔值,当为true的时候,npm将会拒绝发布它。这个是防止私人储存库意外发布的情况。

10.72 sideEffects

"sideEffects" 用于告知打包工具(webpack),当前项目无副作用,可以使用tree shaking优化。

副作用 是指,该函数的调用过程中,是否对主函数(调用者)产生了附加影响,例如修改了函数外的变量或参数,我们就认为该函数是 有副作用 的函数。

"sideEffects": [
    "a.js",
    "b.js"
],

10.73 exports

"exports" 允许在使用模块请求时声明应该使用哪个模块。当指定exports字段时,只有这些模块请求可用,请求其他模块将报错。

{
  "exports": {
    ".": "./main.js",
    "./sub/path": "./secondary.js",
    "./prefix/": "./directory/",
    "./prefix/deep/": "./other-directory/",
    "./other-prefix/*": "./yet-another/*/*.js"
  }
}
// 示例:
import _ from 'my-project' // 将会指向./main.js
import path from 'my-project/sub/path' // 将会指向./secondary.js

10.74 files

"files" 用于保留项目在npm发布时的文件或文件夹,在node_modules/下将会只留下files声明的文件或文件夹。你也可以提供一个.npmignore文件,让npm发布的时候规定哪些文件被忽略,就像.gitignore一样。

"files": [
    "lib",
    "bin",
    "scripts"
],

参考文献: