黑色技术

黑色技术 查看完整档案

广州编辑南海东软信息技术学院  |  计算机应用技术 编辑无业游民  |  web前端 编辑 github.com/yyman001 编辑
编辑

追寻着HTML5的步伐,请不要放弃~

个人动态

黑色技术 提出了问题 · 11月6日

git 修改某个commit 内容

某个commit 中的修改代码遗漏, 如何修改指定commit,如图
image.png

想 修改refactor: api/dmEditor/source/character/static2d
提交内容,不产生新的commit.
好像使用git rebase 后无法执行提交,好像并未找到BCompare

$ git rebase -i bd9a0d8c67ebc9be512bb175ee8713b9179e2216
hint: Waiting for your editor to close the file... error: cannot spawn BCompare: No such file or directory
error: unable to start editor 'BCompare'
git commit --amend
hint: Waiting for your editor to close the file... error: cannot spawn BCompare: No such file or directory
error: unable to start editor 'BCompare'
Please supply the message using either -m or -F option.

关注 3 回答 2

黑色技术 赞了文章 · 10月29日

前端项目自动化部署——超详细教程(Jenkins、Github Actions)

本教程主要讲解了怎么使用 Jenkins 和 Github Actions 部署前端项目。

  1. 第一部分是使用 Gitea 配置局域网 git 服务器,再使用 Jenkins 将 Gitea 下的项目部署到局域网服务器。
  2. 第二部分是使用 Github Actions 将 Github 项目部署到 Github Page 和阿里云。

阅读本教程并不需要你提前了解 Jenkins 和 Github Actions 的知识,只要按照本教程的指引,就能够实现自动化部署项目。

PS:本人所用电脑操作系统为 windows,即以下所有的操作均在 windows 下运行。其他操作系统的配置大同小异,不会有太大差别。

Gitea + Jenkins 自动构建前端项目并部署到服务器

Gitea 用于构建 Git 局域网服务器,Jenkins 是 CI/CD 工具,用于部署前端项目。

配置 Gitea

  1. 下载 Gitea,选择一个喜欢的版本,例如 1.13,选择 gitea-1.13-windows-4.0-amd64.exe 下载。
  2. 下载完后,新建一个目录(例如 gitea),将下载的 Gitea 软件放到该目录下,双击运行。
  3. 打开 localhost:3000 就能看到 Gitea 已经运行在你的电脑上了。
  4. 点击注册,第一次会弹出一个初始配置页面,数据库选择 SQLite3。另外把 localhost 改成你电脑的局域网地址,例如我的电脑 IP 为 192.168.0.118

在这里插入图片描述
在这里插入图片描述

  1. 填完信息后,点击立即安装,等待一会,即可完成配置。
  2. 继续点击注册用户,第一个注册的用户将会成会管理员。
  3. 打开 Gitea 的安装目录,找到 custom\conf\app.ini,在里面加上一行代码 START_SSH_SERVER = true。这时就可以使用 ssh 进行 push 操作了。

在这里插入图片描述

  1. 如果使用 http 的方式无法克隆项目,请取消 git 代理。
git config --global --unset http.proxy
git config --global --unset https.proxy

配置 Jenkins

  1. 需要提前安装 JDK,JDK 安装教程网上很多,请自行搜索。
  2. 打开 Jenkins 下载页面。

在这里插入图片描述

  1. 安装过程中遇到 Logon Type 时,选择第一个。

在这里插入图片描述

  1. 端口默认为 8080,这里我填的是 8000。安装完会自动打开 http://localhost:8000 网站,这时需要等待一会,进行初始化。
  2. 按照提示找到对应的文件(直接复制路径在我的电脑中打开),其中有管理员密码。

在这里插入图片描述

  1. 安装插件,选择第一个。

在这里插入图片描述

  1. 创建管理员用户,点击完成并保存,然后一路下一步。

在这里插入图片描述

  1. 配置完成后自动进入首页,这时点击 Manage Jenkins -> Manage plugins 安装插件。

在这里插入图片描述

  1. 点击 可选插件,输入 nodejs,搜索插件,然后安装。
  2. 安装完成后回到首页,点击 Manage Jenkins -> Global Tool Configuration 配置 nodejs。如果你的电脑是 win7 的话,nodejs 版本最好不要太高,选择 v12 左右的就行。

在这里插入图片描述

创建静态服务器

  1. 建立一个空目录,在里面执行 npm init -y,初始化项目。
  2. 执行 npm i express 下载 express。
  3. 然后建立一个 server.js 文件,代码如下:
const express = require('express')
const app = express()
const port = 8080

app.use(express.static('dist'))

app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`)
})

它将当前目录下的 dist 文件夹设为静态服务器资源目录,然后执行 node server.js 启动服务器。

由于现在没有 dist 文件夹,所以访问网站是空页面。
在这里插入图片描述
不过不要着急,一会就能看到内容了。

自动构建 + 部署到服务器

  1. 下载 Jenkins 提供的 demo 项目 building-a-multibranch-pipeline-project,然后在你的 Gitea 新建一个仓库,把内容克隆进去,并提交到 Gitea 服务器。

在这里插入图片描述

  1. 打开 Jenkins 首页,点击 新建 Item 创建项目。

在这里插入图片描述

  1. 选择源码管理,输入你的 Gitea 上的仓库地址。

在这里插入图片描述

  1. 你也可以尝试一下定时构建,下面这个代码表示每 5 分钟构建一次。

在这里插入图片描述

  1. 选择你的构建环境,这里选择刚才配置的 nodejs。

在这里插入图片描述

  1. 点击增加构建步骤,windows 要选 execute windows batch command,linux 要选 execute shell

  1. 输入 npm i && npm run build && xcopy .\build\* G:\node-server\dist\ /s/e/y,这行命令的作用是安装依赖,构建项目,并将构建后的静态资源复制到指定目录 G:\node-server\dist\ 。这个目录是静态服务器资源目录。

在这里插入图片描述

  1. 保存后,返回首页。点击项目旁边的小三角,选择 build now

在这里插入图片描述

  1. 开始构建项目,我们可以点击项目查看构建过程。

在这里插入图片描述

  1. 构建成功,打开 http://localhost:8080/ 看一下结果。

在这里插入图片描述
在这里插入图片描述

  1. 由于刚才设置了每 5 分钟构建一次,我们可以改变一下网站的内容,然后什么都不做,等待一会再打开网站看看。

在这里插入图片描述

  1. 把修改的内容提交到 Gitea 服务器,稍等一会。打开网站,发现内容已经发生了变化。

在这里插入图片描述

使用 pipeline 构建项目

使用流水线构建项目可以结合 Gitea 的 webhook 钩子,以便在执行 git push 的时候,自动构建项目。

  1. 点击首页右上角的用户名,选择设置

在这里插入图片描述

  1. 添加 token,记得将 token 保存起来。

在这里插入图片描述

  1. 打开 Jenkins 首页,点击 新建 Item 创建项目。

在这里插入图片描述

  1. 点击构建触发器,选择触发远程构建,填入刚才创建的 token。

在这里插入图片描述

  1. 选择流水线,按照提示输入内容,然后点击保存

在这里插入图片描述

  1. 打开 Jenkins 安装目录下的 jenkins.xml 文件,找到 <arguments> 标签,在里面加上 -Dhudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION=true。它的作用是关闭 CSRF 验证,不关的话,Gitea 的 webhook 会一直报 403 错误,无法使用。加好参数后,在该目录命令行下输入 jenkins.exe restart 重启 Jenkins。

在这里插入图片描述

  1. 回到首页,配置全局安全选项。勾上匿名用户具有可读权限,再保存。

在这里插入图片描述
在这里插入图片描述

  1. 打开你的 Gitea 仓库页面,选择仓库设置

在这里插入图片描述

  1. 点击管理 web 钩子,添加 web 钩子,钩子选项选择 Gitea
  2. 目标 URL 按照 Jenkins 的提示输入内容。然后点击添加 web 钩子

在这里插入图片描述
在这里插入图片描述

  1. 点击创建好的 web 钩子,拉到下方,点击测试推送。不出意外,应该能看到推送成功的消息,此时回到 Jenkins 首页,发现已经在构建项目了。

在这里插入图片描述

  1. 由于没有配置 Jenkinsfile 文件,此时构建是不会成功的。所以接下来需要配置一下 Jenkinsfile 文件。将以下代码复制到你 Gitea 项目下的 Jenkinsfile 文件。jenkins 在构建时会自动读取文件的内容执行构建及部署操作。
pipeline {
    agent any
    stages {
        stage('Build') {
            steps {  // window 使用 bat, linux 使用 sh
                bat 'npm i'
                bat 'npm run build'
            }
        }
        stage('Deploy') {
            steps {
                bat 'xcopy .\\build\\* D:\\node-server\\dist\\ /s/e/y' // 这里需要改成你的静态服务器资源目录
            }
        }
    }
}
  1. 每当你的 Gitea 项目执行 push 操作时,Gitea 都会通过 webhook 发送一个 post 请求给 Jenkins,让它执行构建及部署操作。

在这里插入图片描述

小结

如果你的操作系统是 Linux,可以在 Jenkins 打包完成后,使用 ssh 远程登录到阿里云,将打包后的文件复制到阿里云上的静态服务器上,这样就能实现阿里云自动部署了。具体怎么远程登录到阿里云,请看下文中的 《Github Actions 部署到阿里云》 一节。

Github Actions 自动构建前端项目并部署到服务器

如果你的项目是 Github 项目,那么使用 Github Actions 也许是更好的选择。

部署到 Github Page

接下来看一下如何使用 Github Actions 部署到 Github Page。

在你需要部署到 Github Page 的项目下,建立一个 yml 文件,放在 .github/workflow 目录下。你可以命名为 ci.yml,它类似于 Jenkins 的 Jenkinsfile 文件,里面包含的是要自动执行的脚本代码。

这个 yml 文件的内容如下:

name: Build and Deploy
on: # 监听 master 分支上的 push 事件
  push:
    branches:
      - master
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest # 构建环境使用 ubuntu
    steps:
      - name: Checkout
        uses: actions/checkout@v2.3.1  
        with:
          persist-credentials: false

      - name: Install and Build # 下载依赖 打包项目
        run: |
          npm install
          npm run build

      - name: Deploy # 将打包内容发布到 github page
        uses: JamesIves/github-pages-deploy-action@3.5.9 # 使用别人写好的 actions
        with:  # 自定义环境变量
          ACCESS_TOKEN: ${{ secrets.VUE_ADMIN_TEMPLATE }} # VUE_ADMIN_TEMPLATE 是我的 secret 名称,需要替换成你的
          BRANCH: master
          FOLDER: dist
          REPOSITORY_NAME: woai3c/woai3c.github.io # 这是我的 github page 仓库
          TARGET_FOLDER: github-actions-demo # 打包的文件将放到静态服务器 github-actions-demo 目录下

上面有一个 ACCESS_TOKEN 变量需要自己配置。

  1. 打开 Github 网站,点击你右上角的头像,选择 settings

在这里插入图片描述

  1. 点击左下角的 developer settings

在这里插入图片描述

  1. 在左侧边栏中,单击 Personal access tokens(个人访问令牌)

在这里插入图片描述

  1. 单击 Generate new token(生成新令牌)

在这里插入图片描述

  1. 输入名称并勾选 repo

在这里插入图片描述

  1. 拉到最下面,点击 Generate token,并将生成的 token 保存起来。

在这里插入图片描述

  1. 打开你的 Github 项目,点击 settings

在这里插入图片描述
点击 secrets->new secret
在这里插入图片描述
创建一个密钥,名称随便填(中间用下划线隔开),内容填入刚才创建的 token。
在这里插入图片描述

在这里插入图片描述
将上文代码中的 ACCESS_TOKEN: ${{ secrets.VUE_ADMIN_TEMPLATE }} 替换成刚才创建的 secret 名字,替换后代码如下 ACCESS_TOKEN: ${{ secrets.TEST_A_B }}。保存后,提交到 Github。

以后你的项目只要执行 git push,Github Actions 就会自动构建项目并发布到你的 Github Page 上。

Github Actions 的执行详情点击仓库中的 Actions 选项查看。

在这里插入图片描述
在这里插入图片描述
具体详情可以参考一下我的 demo 项目 github-actions-demo

构建成功后,打开 Github Page 网站,可以发现内容已经发布成功。

在这里插入图片描述

Github Actions 部署到阿里云

初始化阿里云服务器

  1. 购买阿里云服务器,选择操作系统,我选的 ubuntu
  2. 在云服务器管理控制台选择实例->更多->密钥->重置实例密码(一会登陆用)
  3. 选择远程连接->VNC,会弹出一个密码,记住它,以后远程连接要用(ctrl + alt + f1~f6 切换终端,例如 ctrl + alt + f1 是第一个终端)
  4. 进入后是一个命令行 输入 root(默认用户名),密码为你刚才重置的实例密码
  5. 登陆成功, 更新安装源 sudo apt-get update && sudo apt-get upgrade -y
  6. 安装 npm sudo apt-get install npm
  7. 安装 npm 管理包 sudo npm install -g n
  8. 安装 node 最新稳定版 sudo n stable

创建一个静态服务器

mkdir node-server // 创建 node-server 文件夹
cd node-server // 进入 node-server 文件夹
npm init -y // 初始化项目
npm i express
touch server.js // 创建 server.js 文件
vim server.js // 编辑 server.js 文件

将以下代码输入进去(用 vim 进入文件后按 i 进行编辑,保存时按 esc 然后输入 :wq,再按 enter),更多使用方法请自行搜索。

const express = require('express')
const app = express()
const port = 3388 // 填入自己的阿里云映射端口,在网络安全组配置。

app.use(express.static('dist'))

app.listen(port, '0.0.0.0', () => {
    console.log(`listening`)
})

执行 node server.js 开始监听,由于暂时没有 dist 目录,先不要着急。

注意,监听 IP 必须为 0.0.0.0 ,详情请看部署Node.js项目注意事项

阿里云入端口要在网络安全组中查看与配置。

在这里插入图片描述

创建阿里云密钥对

请参考创建SSH密钥对绑定SSH密钥对 ,将你的 ECS 服务器实例和密钥绑定,然后将私钥保存到你的电脑(例如保存在 ecs.pem 文件)。

打开你要部署到阿里云的 Github 项目,点击 setting->secrets。

在这里插入图片描述
点击 new secret
在这里插入图片描述
secret 名称为 SERVER_SSH_KEY,并将刚才的阿里云密钥填入内容。

在这里插入图片描述
点击 add secret 完成。

在你项目下建立 .github\workflows\ci.yml 文件,填入以下内容:

name: Build app and deploy to aliyun
on:
  #监听push操作
  push:
    branches:
      # master分支,你也可以改成其他分支
      - master
jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1
    - name: Install Node.js
      uses: actions/setup-node@v1
      with:
        node-version: '12.16.2'
    - name: Install npm dependencies
      run: npm install
    - name: Run build task
      run: npm run build
    - name: Deploy to Server
      uses: easingthemes/ssh-deploy@v2.1.5
      env:
          SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
          ARGS: '-rltgoDzvO --delete'
          SOURCE: dist # 这是要复制到阿里云静态服务器的文件夹名称
          REMOTE_HOST: '118.190.217.8' # 你的阿里云公网地址
          REMOTE_USER: root # 阿里云登录后默认为 root 用户,并且所在文件夹为 root
          TARGET: /root/node-server # 打包后的 dist 文件夹将放在 /root/node-server

保存,推送到 Github 上。

以后只要你的项目执行 git push 操作,就会自动执行 ci.yml 定义的脚本,将打包文件放到你的阿里云静态服务器上。

这个 Actions 主要做了两件事:

  1. 克隆你的项目,下载依赖,打包。
  2. 用你的阿里云私钥以 SSH 的方式登录到阿里云,把打包的文件上传(使用 rsync)到阿里云指定的文件夹中。

如果还是不懂,建议看一下我的 demo

ci.yml 配置文件讲解

  1. name,表示这个工作流程(workflow)的名称。
  2. on,表示监听的意思,后面可以加上各种事件,例如 push 事件。

下面这段代码表示要监听 master 分支的 push 事件。当 Github Actions 监听到 push 事件发生时,它就会执行下面 jobs 定义的一系列操作。

name: Build app and deploy to aliyun
on:
  #监听push操作
  push:
    branches:
      # master分支,你也可以改成其他分支
      - master
jobs:
...
  1. jobs,看字面意思就是一系列的作业,你可以在 jobs 字段下面定义很多作业,例如 job1job2 等等,并且它们是并行执行的。
jobs:
  job1:
      ...
  job2:
      ...
  job3:
    ...

回头看一下 ci.yml 文件,它只有一个作业,即 build,作业的名称是自己定义的,你叫 good 也可以。

  1. runs-on,表示你这个工作流程要运行在什么操作系统上,ci.yml 文件定义的是最新稳定版的 ubuntu。除了 ubuntu,它还可以选择 Mac 或 Windows。

  1. steps,看字面意思就是一系列的步骤,也就是说这个作业由一系列的步骤完成。例如先执行 step1,再执行 step2...

setps 步骤讲解

setps 其实是一个数组,在 YAML 语法中,以 - 开始就是一个数组项。例如 ['a', 'b', 'c'] 用 YAML 语法表示为:

- a
- b
- c

所以 setps 就是一个步骤数组,从上到下开始执行。从 ci.yml 文件来看,每一个小步骤都有几个相关选项:

  1. name,小步骤的名称。
  2. uses,小步骤使用的 actions 库名称或路径,Github Actions 允许你使用别人写好的 Actions 库。
  3. run,小步骤要执行的 shell 命令。
  4. env,设置与小步骤相关的环境变量。
  5. with,提供参数。

综上所述,ci.yml 文件中的 setps 就很好理解了,下面从头到尾解释一边:

    steps:
    - uses: actions/checkout@v1
    - name: Install Node.js
      uses: actions/setup-node@v1
      with:
        node-version: '12.16.2'
    - name: Install npm dependencies
      run: npm install
    - name: Run build task
      run: npm run build
    - name: Deploy to Server
      uses: easingthemes/ssh-deploy@v2.1.5
      env:
          SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
          ARGS: '-rltgoDzvO --delete'
          SOURCE: dist # 这是要复制到阿里云静态服务器的文件夹名称
          REMOTE_HOST: '118.190.217.8' # 你的阿里云公网地址
          REMOTE_USER: root # 阿里云登录后默认为 root 用户,并且所在文件夹为 root
          TARGET: /root/node-server # 打包后的 dist 文件夹将放在 /root/node-server
  1. 使用 actions/checkout@v1 库克隆代码到 ubuntu 上。
  2. 使用 actions/setup-node@v1 库安装 nodejs,with 提供了一个参数 node-version 表示要安装的 nodejs 版本。
  3. ubuntushell 上执行 npm install 下载依赖。
  4. 执行 npm run build 打包项目。
  5. 使用 easingthemes/ssh-deploy@v2.1.5 库,这个库的作用就是用 SSH 的方式远程登录到阿里云服务器,将打包好的文件夹复制到阿里云指定的目录上。

env 上可以看到,这个 actions 库要求我们提供几个环境变量:

  1. SSH_PRIVATE_KEY: 阿里云密钥对中的私钥(需要你提前写在 github secrets 上),
  2. ARGS: '-rltgoDzvO --delete',没仔细研究,我猜是复制完文件就删除掉。
  3. SOURCE:打包后的文件夹名称
  4. REMOTE_HOST: 阿里云公网 IP 地址
  5. REMOTE_USER: 阿里云服务器的用户名
  6. TARGET: 你要拷贝到阿里云服务器指定目录的名称

如果你想了解一下其他 actions 库的实现,可以直接复制 actions 库的名称去搜索引擎搜索一下,例如搜索 actions/checkout 的结果为:

都看到这了,给个赞再走吧。

参考资料

更多文章,敬请关注

查看原文

赞 90 收藏 74 评论 6

黑色技术 赞了文章 · 10月29日

《前端每日实战》第177号作品:多张图片的鼠标悬停和滑动特效

image

一种引起浏览者探索兴趣的方法是,页面打开之后并不马上把所有内容都呈现给用户,而是隐藏其中的一部分内容,其他内容则需要用户交互之后才展示出来。这种方式很合适那些小众的、要营造艺术氛围的网站,通过特效来展现后续内容,有一种与用户对话的感觉。本作品就是采用这样的方式,当页面加载之后先把图片遮住,然后当鼠标移动到元素之上时,图片才展现出来。

效果预览

按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。

https://codepen.io/comehope/pen/MWejLqY

源代码下载

每日前端实战系列的全部源代码请从 github 下载:

https://github.com/comehope/front-end-daily-challenges

代码解读

一、DOM 结构

容器名为 .container,其中包含一个名为 .item 的元素。
.item 元素则包含 3 个子元素,.picture 表示图片本身,.title 是图片上的文字,.mask 是用来制作遮罩效果的元素。
作品完成时,会有多个 .item 元素,但此时我们先只展示 1 张图片,待效果完成之后,再增加其他图片。

<div class="container">
    <div class="item">
        <img class="picture" data-original="images/toggle.png">
        <span class="title">Toggle</span>
        <div class="mask"></div>
    </div>
</div>

本作品用到的4张图片可从下列地址下载。
https://assets.codepen.io/947...
https://assets.codepen.io/947...
https://assets.codepen.io/947...
https://assets.codepen.io/947...

二、基础布局

设置页面背景色为深灰色,令容器居中。

body {
    background-color: #222;
    margin: 0;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
}

设置图片尺寸,用相对单位 em

.item {
    width: 18em;
    height: 12em;
}

.item .picture {
    width: 100%;
}

效果如下图:
image

三、图片遮罩特效

因为先处理遮罩效果,所以把暂时用不到的文字隐藏起来,避免干扰。

.item .title {
    display: none;
}

利用 .mask 元素为图片增加遮罩。遮罩大小是 20em * 20em 的一个大圆,背景色先暂用半透明的醒目的黄色,便于在开发过程中观察。

.item {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
}

.item .mask {
    position: absolute;
    width: 20em;
    height: 20em;
    background-color: hsla(60, 100%, 50%, 0.7);
    border-radius: 50%;
}

效果如下图:
image

上面只是测试了遮罩的大小,把刚才的代码注释掉,改用 box-shadow 实现我们真正需要的遮罩效果。这个遮罩层尺寸是 50em * 50em,远远大于图片本身,但它的大部分区域是内阴影,在内阴影之内才透出遮罩下方的图片来。
内阴影的尺寸是 15em,这是内阴影的半径,所以内阴影的直径是 30em,用遮罩元素的宽高 50em 减去遮罩的 30em,剩下的就是 20em,和刚才测试的遮罩大小是一样的。

.item .mask {
    /*width: 20em;*/
    /*height: 20em;*/
    /*background-color: hsla(60, 100%, 50%, 0.7);*/
    width: 50em;
    height: 50em;
    color: hsla(60, 100%, 50%, 0.7);
    box-shadow: inset 0 0 0 15em;
}

效果如下图:
image

加上鼠标悬停效果试一下。注意,这里元素上的内阴影尺寸设置为 25em,这是内阴影的半径尺寸,那么阴影的直径就是 50em,和遮罩本身的尺寸是一样大的,这表示在默认情况下,整张图片都被内阴影遮住了;而鼠标悬停时,内阴影变小,就显示出了遮罩下方的图片。另外为遮罩层增加了 pointer-events: none 属性,它的作用是避免遮罩层响应鼠标事件。

.item .mask {
    box-shadow: inset 0 0 0 25em;
    transition: box-shadow 0.3s;
    pointer-events: none;
}

.item:hover .mask {
    box-shadow: inset 0 0 0 15em;
}

再下来制作鼠标滑动时遮罩跟随的效果。
先把遮罩移到图片的左上方。遮罩的高是 50emtop: -25em 就是令遮罩的水平中线与图片顶边对齐;同理,left: -25em 则是令遮罩的垂直中线与图片的左边对齐,两者叠加,就是遮罩的中心与图片的左上角对齐。

.item .mask {
    top: -25em;
    left: -25em;
}

增加脚本,为 .item 元素绑定 mousemove 事件,令鼠标在 .item 元素上滑动时,带动 .mask 元素滑动。

window.onload = init

function init() {
    let items = document.querySelectorAll('.item')
    items.forEach((item) => {
        item.addEventListener('mousemove', e => {
            let mask = item.querySelector('.mask')
            mask.style.transform = 'translate(' + e.offsetX + 'px, ' + e.offsetY + 'px)'
        })
    })
}

至此,主要的效果已经完成了,接下来再增强一下效果。
稍加大图片的原始尺寸,在鼠标悬停时恢复图片大小,这样的效果是在鼠标进入图片区域时,图片能“扭曲抖动”一下,加强互动的效果。

.item .picture {
    transform: scale(1.1);
    transition: 0.3s;
}

.item:hover .picture {
    transform: scale(1);
}

鼠标悬停和滑动效果完成,下面这几行代码是一些收尾工作。
通过 overflow: hidden 属性隐藏掉图片之外的部分、容器加一点圆角、遮罩的颜色改用不透明的灰色。

.container {
    border-radius: 0.3em;
}

.item {
    overflow: hidden;
}

.item .mask {
    /*color: hsla(60, 100%, 50%, 0.7);*/
    color: #333;
}

效果如下图:
image

四、文字布局和特效

接下来处理文字。
先把文字显示出来,除了注释掉 display: none 之外,还要设置它的 z-index,令它显示在遮罩层的上方,再有也要取消它的鼠标事件,防止它影响鼠标滑动效果。

.item .title {
    /*display: none;*/
    position: absolute;
    color: #777;
    z-index: 1;
    pointer-events: none;
}

设置文字样式。

.item .title {
    font-family: sans-serif;
    font-weight: bold;
    text-transform: uppercase;
}

增加文字特效,当鼠标滑入图片时,隐藏文字。

.item .title {
    transition: 0.2s;
}

.item:hover .title {
    opacity: 0;
}

效果如下图:
image

至此,单图图片的效果都完成了。

五、将特效应用到多张图片

增加多个 .item 元素。

<div class="container">
    <div class="item">
        <img class="picture" data-original="images/toggle.png">
        <span class="title">Toggle</span>
        <div class="mask"></div>
    </div>
    <!-- 此处再增加3个 .item 元素,代码略 -->
</div>

grid 布局把图片排列成田字格形状。

.container {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    grid-gap: 1em;
}

效果如下图:
image

大功告成!

关于作者

张偶,网络笔名 @comehope,20世纪末触网,被 Web 的无穷魅力所俘获,自此始终战斗在 Web 开发第一线。

《前端每日实战》专栏是我近年来实践项目式学习的笔记,以项目驱动学习,展现从灵感闪现到代码实现的完整过程,亦可作为前端开发的练手习题和开发参考。

拙作《CSS3 艺术》一书已由人民邮电出版社出版,全彩印刷,用100多个生动美观的实例,系统地剖析了 CSS 与视觉效果相关的重要语法,并含有近10小时的视频演示讲解。京东/天猫/当当有售。

查看原文

赞 11 收藏 6 评论 1

黑色技术 赞了文章 · 9月27日

手摸手带你 Docker 从入门到实践

bg

在下最近遇到要在服务器上安装 Mysql、Nginx、EasyMock 等工具的场景,这里记录一下我使用 Docker 安装的过程,希望也能在类似的场景中帮助到大家~

本文前备知识需要一些 Linux 的一些基本命令,推介先看一下 <半小时搞会 CentOS 入门必备基础知识> 这篇文章。

CentOS 版本: 7.6

Nginx 版本: 1.16.1

Docker 版本: 19.03.12

你多学一样本事,就少说一句求人的话

1. 介绍

1.1 出现的原因

前后端开发到测试到生产的过程中,经常会遇到一个问题,明明我在本地跑没问题,为什么到测试环境或者生产环境就报错了了呢,常常这是因为开发、测试、生产的环境与配置不同导致的。

折腾过环境配置的人都明白其中麻烦,换一台系统、虚拟机、机器,就又要重来一次,费力费时。由于环境和配置的原因,各种奇奇怪怪因为环境和配置的 Bug,总是像打地鼠游戏里面的地鼠一样不断冒出来 🐹

Docker

Docker 对这个问题给出了一个很好的解决方案,通过镜像将除了系统之外所需要的系统环境由下而上打包,达到服务跨平台的无缝运作。也就是说,安装的时候,把特定的环境一模一样地搬过来,从而解决「在我的电脑上能跑,在 xx 环境就跑不了」的情况。

另外一个重要的原因,就是轻量,基于容器的虚拟化,Docker 的镜像仅包含业务运行所需的 runtime 环境,一个 CentOS/Ubuntu 基础镜像仅 170M,因为轻量一个宿主机可以轻松安装数百个容器。

1.2 是什么

Docker 是基于 Go 语言实现的云开源项目,从 2013 年发布到现在一直广受关注。Docker 可以让你像使用集装箱一样快速的组合成应用,并且可以像运输标准集装箱一样,尽可能的屏蔽代码层面的差异。它将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。

程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。

本文就不对比虚拟机跟 Docker 的区别和优劣了,每个文章都有,说烂了,想了解的话可以百度一下 😂,我这里就不多说了,下面直接看看怎么安装怎么用起来吧。

2. 安装 & 配置

2.1 Mac 下安装

在下直接使用 Homebrew Cask 来安装,Mac 下:

# Homebrew 安装
$ braw cask install docker

即可,安装完输入命令,直接报错!

➜  ~ docker
zsh: command not found: docker  # 报错

遇到这个报错别担心,安装完之后要在应用列表里面双击 Docker 应用,输入密码之后就可以使用这个命令了 😅。

2.2 CentOS 下安装

Docker 要求 CentOS 版本必须在 6.5 及以上才可以安装。

# 安装
$ sudo yum install yum-utils device-mapper-persistent-data lvm2
$ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
$ sudo yum install docker-ce

# 开启 Docker
$ sudo systemctl start docker

在 Windows 上可以直接下载安装包来安装,或者 Mac 上不使用 Homebrew 也可以去官网直接下载安装包来安装,百度一下到处都是安装方法,其他的就不用多说。

3. 简单配置并跑起来

3.1 配置镜像加速

在 MacOS 的 Docker 配置 Perferences -> Docker Engine 或者 Windows 的 Settings -> Deamon 中的 JSON 中增加一项 registry-mirrors 如下

Docker镜像加速配置

配置完之后在命令行中 docker info 就可以查看到我们配置的镜像加速地址了。

➜  ~ sudo docker info
...
 Registry Mirrors:
  https://reg-mirror.qiniu.com/
  http://hub-mirror.c.163.com/
  https://registry.docker-cn.com/
...

如果你的系统的 Docker 没有客户端,比如 CentOS 中,可以直接修改 deamon 配置文件:

# 修改/创建 docker 的 deamon 配置文件
$ sudo vi /etc/docker/daemon.json

# 修改为如下配置
{
  "experimental": false,
  "debug": true,
  "registry-mirrors": [
    "https://reg-mirror.qiniu.com",
    "http://hub-mirror.c.163.com",
    "https://registry.docker-cn.com"
  ]
}

# 修改完 :wq 重启
$ sudo systemctl restart docker

3.2 Hello World !

然后就可以快乐跑起来我们第一个 Docker 指令 Hello World 了

Docker跑起Helloworld

Good start ! 🎉

4. 镜像 & 容器 & 仓库

镜像和容器的关系就像类和类的实例,一个镜像可以同时跑多个容器,单个容器实例又可以创建新的镜像。如下图:

镜像容器仓库

下面解释一下这个图里面出现的元素

概念说明
Docker 镜像 Images用于创建 Docker 容器的只读模板,比如 Ubuntu 16.04系统、Nginx 1.16.0 等,是一个特殊的文件系统,包括容器运行时需要的程序、库、资源、参数等,但不包含任何动态数据,内容在构建后也不会被改变,一个镜像可以创建多个容器
Docker 容器 Container容器是独立运行、相互隔离的一个或一组应用,是镜像创建的运行实例,实质是进程,可以看作为一个简易版的 Linux 环境 + 运行在其中的应用程序
Docker 客户端 Client客户端通过命令行或者其他工具使用 Docker SDK (https://docs.docker.com/devel... 与 Docker 的守护进程通信
Docker 主机 Host一个物理或者虚拟的机器用于执行 Docker 守护进程和容器
Docker 仓库 Repository集中存放镜像文件的地方,分为公有仓库和私有仓库。
Docker 注册服务器 Registry是一个集中存储、分发镜像的服务,官方的叫 Docker Hub。一个 Docker Registry 中可包含多个仓库,每个仓库可以包含多个标签 Tag 的镜像,不同的标签对应不同的版本
Docker MachineDocker Machine 是一个简化 Docker 安装的命令行工具,通过一个简单的命令行即可在相应的平台上安装Docker,比如 VirtualBox、 Digital Ocean、Microsoft Azure

容器的生命周期图示

容器的生命周期

容器的五个核心状态,也就是图中色块表示的:Created、Running、Paused、Stopped、Deleted:

  1. Created:容器已经被创建,容器所需的相关资源已经准备就绪,但容器中的程序还未处于运行状态。
  2. Running:容器正在运行,也就是容器中的应用正在运行。
  3. Paused:容器已暂停,表示容器中的所有程序都处于暂停 ( 不是停止 ) 状态。
  4. Stopped:容器处于停止状态,占用的资源和沙盒环境都依然存在,只是容器中的应用程序均已停止。
  5. Deleted:容器已删除,相关占用的资源及存储在 Docker 中的管理信息也都已释放和移除。

本文主要关注于使用,就不太赘述这些状态的切换等,下面直接上手。

5. 基本使用

5.1 操作命令

# 开启 Docker 开机自启动
$ sudo systemctl enable docker

# 关闭 Docker 开机自启动
$ sudo systemctl disable docker

5.2 镜像命令

# 去下载镜像,先从本地找,没有去镜像,最后没有去 hub,标签不写默认为 lastest
$ docker pull [镜像名]:[标签Tag]

# 列出本机的所有 image 文件,-a 显示本地所有镜像(包括中间镜像),-q 只显示镜像ID,--digests 显示镜像的摘要信息
$ docker image ls
$ docker images

# 删除 image 文件, -f 强制删除镜像
$ docker rmi [镜像名][:标签Tag]
$ docker rmi [镜像名1][:标签Tag] [镜像名2][:标签Tag]    # 删多个
$ docker rmi $(docker ps -a -q)    # 删全部,后面是子命令

# 查询镜像名称,--no-trunc 显示完整的镜像描述,--filter=stars=30 列出star不少于指定值的镜像,--filter=is-automated=true 列出自动构建类型的镜像
$ docker search [关键字]

# 下载镜像,标签 tag 不写默认为 lastest,也可以自己加比如 :3.2.0
$ docker pull [镜像名][:标签Tag]

5.3 容器命令

# 列出本机正在运行的容器,-a 列出本机所有容器包括终止运行的容器,-q 静默模式只显示容器编号,-l 显示最近创建的容器
$ docker container ls     # 等价于下面这个命令
$ docker ps

# 新建并启动容器
$ docker run [option] [容器名] 

# 启动容器
$ docker start [容器ID]/[容器Names]

# 重启容器
$ docker restart [容器ID]/[容器Names]

# 终止容器运行
$ docker kill [容器ID]  # 强行终止,相当于向容器里面的主进程发出 SIGKILL 信号,那些正在进行中的操作会全部丢失
$ docker kill $(docker ps -a -q) # 强行终止所有容器
$ docker stop [容器ID]  # 从容终止,相当于向容器里面的主进程发出 SIGTERM 信号,然后过一段时间再发出 SIGKILL 信号
$ docker stop $(docker ps -a -q) # 终止所有容器

# 终止运行的容器文件,依然会占据硬盘空间,可以使用 docker container rm 命令删除,-f 强制删除可以删除正在运行的容器
$ docker rm [容器ID]
$ docker rm `docker ps -aq`    # 删除所有已经停止的容器,因为没停止的rm删不了需要加-f

# 查看容器的输出,-t加入时间戳,-f跟随最新日志打印,--tail数字显示最后多少条,如果docker run时,没有使用-it,就要用这个命令查看输出
$ docker logs [容器ID]

# 查看容器进程信息
$ docker top [容器ID]/[容器Names]
$ docker port [容器ID]/[容器Names]

# 退出容器
$ exit           # 容器退出
ctrl + p + q     # 容器退出,快捷键

# 进入容器
$ docker attach [容器ID]      # 退出容器时会让容器停止,本机的输入直接输到容器中
$ docker exec -it [容器ID]    # 退出容器时不会让容器停止,在已运行的容器中执行命令,不创建和启动新的容器

# 设置容器在docker启动时自动启动
$ docker container update --restart=always [容器名字]

这里要特别说一下 docker runoption,因为最常用:

  1. --name 为容器指定一个名称;
  2. -d 容器启动后进入后台,并返回容器 ID,即启动守护式容器;
  3. -P 随机端口映射;
  4. -p 80:8080 将本地 80 端口映射到容器的 8080 端口;
  5. bash 容器启动以后,内部第一个执行的命令。这里启动 bash,保证用户可以使用 Shell;
  6. -i 以交互模式运行容器,通常与 -t 同时使用;
  7. -t 为容器重新分配一个伪输入终端,容器的 Shell 会映射到当前的 Shell,然后在本机窗口输入的命令,就会传入容器,通常与 -i 同时使用;
  8. --rm 在容器终止运行后自动删除容器文件;
  9. --restart=always 设置容器自启动;
  10. -v /xxx:/yyy 映射命令,把本机的 xxx 目录映射到容器中的 yyy 目录,也就是说改变本机的 xxx 目录下的内容, 容器 yyy 目录中的内容也会改变;

比如我在 CentOS 下跑起来一个 CentOS 的 Docker 容器:

# 下载
$ docker pull centos

# 在上面下载的 centos 镜像基础上,新建一个容器名为 mycentos0901 的 centos 实例,并进入这个容器的 bash
$ docker run -it --name mycentos0901 0d120b6ccaa8

[root@169c9fffeecd /]   # 进入容器,下面输入命令,注意这里 root 后面的一串 ID
$ ls       # 可以看到centos的根目录文件列表
$ docker   # bash: docker: command not found 这个容器没有安装docker

是不是很神奇,我们可以在一开始的 CentOS 下面执行 docker ps 来查看容器列表:

image-20200901225909737

你会发现上面那个 ID,正是下面列表中跑起来的这个容器的 ID,镜像的 ID 也是我们前面 pull 下来的 CentOS 镜像 ID,名字也是我们起的 mycentos0901

如果 docker run 之后报 Conflict. The container name "xxxx" is already in use by container 就直接运行 docker rm $(docker ps -a -q) 删除已停止的容器,或者精确删除 docker rm [containerID] 也可以,就可以了。

5.4 几个常见场景的命令使用

守护式启动容器

使用 centos 以后台模式启动一个容器 docker run -d --name mycentos0903 0d120b6ccaa8,启动之后 docker ps -a 查看,发现容器并不在运行中,这是因为 Docker 的运行机制:Docker 容器后台运行,必须有一个前台进程

容器运行的命令如果不是那些一直挂起的命令,比如 toptail ,运行结束会自动退出。所以为了让容器持续在后台运行,那么需要将运行的程序以前台进程的形式运行。

比如这里在后台运行一个命令,这个命令一直在打印 docker run -d centos /bin/sh -c "while true; do echo hello zzyy; sleep 2; done",然后我们 logs 查看一下:

docker_logs

退出容器后对容器操作

退出容器后可以通过 exec 方法对正在运行的容器进行操作:

image-20200911142617186

在容器中拷贝文件到外部

拷贝文件使用 cp 命令

$ docker cp [容器ID]/[容器Names]:[要拷贝的文件目录] [本机目录]   # 容器文件拷贝到本机
$ docker cp [本机目录] [容器ID]/[容器Names]:[要拷贝的文件目录]   # 本机文件拷贝到容器

cp 不仅能把容器中的文件/文件夹拷贝到本机,也可以把本机中的文件/文件夹拷贝到容器。

演示一下,这里先到容器里面创建一个无聊的文件 xixi.txt,然后拷贝到本机:

image-20200921210352644

实用的时候,我们可以拷贝配置、日志等文件到本地。

6. 安装 MySQL

# 查询镜像
$ docker search mysql

# 下载镜像,实测没配置镜像加速的时候会比较慢,配置了就好一些
$ docker pull mysql

# 查看镜像
$ docker images

# 创建并运行容器
$ docker run -d -p 3307:3306 -e MYSQL_ROOT_PASSWORD=888888 -v /Users/sherlocked93/Personal/configs/mysql.d:/etc/mysql/conf.d --name localhost-mysql mysql

稍微解释一下上面的参数:

  1. -p 3307:3306 将本机的 3307 端口映射到 mysql 容器的 3306 端口,根据需要自行更改;
  2. -e MYSQL_ROOT_PASSWORD=<string> 设置远程登录的 root 用户密码;
  3. --name <string> 可选,设置容器别名;
  4. -v xxx/mysql.d:/etc/mysql/conf.d 将本地目录下设置文件夹映射到容器的 /etc/mysql/conf.d
  5. -v xxx/logs:/logs 将本机指定目录下的 logs 目录挂载到容器的 /logs
  6. -v xxx/data:/var/lib/mysql 将主机制定目录下的 data 目录挂载到容器的 /var/lib/mysql

运行截图:

安装Mysql

然后去 Navicat 中就可以连接到 MySQL 了。

这也太爽了!真的是几行命令就装好了啊,比之前真是快乐多了 😂

7. 安装 Nginx

Nginx 的安装和其他的类似,如果你还不太了解 Nginx 如何使用,可以参看 <Nginx 从入门到实践,万字详解> 这篇文章,看完基本就了解如何使用和配置了。

# 查询/下载镜像
$ docker search nginx
$ docker pull nginx

image-20200922203203685

然后创建一个临时的容器,目的是把默认配置拷贝到本机,我这里把配置文件放到 /mnt 目录下,主要是三个配置文件夹:

  1. /etc/nginx 放置 Nginx 配置文件;
  2. /var/log/nginx/ 放置 Nginx 日志文件;
  3. /usr/share/nginx/html/ 放置 Nginx 前端静态文件都放在这个文件夹;

分别把这几个目录都拷贝到本机的 /mnt 文件夹下的 nginxnginx_logshtml 文件夹。

刚刚创建的临时容器没用了 docker rm -f [临时容器ID] 把临时容器干掉,然后 docker run 重新创建 Nginx 容器:

$ docker run -d --name localhost-nginx -p 8082:80 \
-v /mnt/nginx:/etc/nginx \
-v /mnt/nginx_logs:/var/log/nginx \
-v /mnt/html:/usr/share/nginx/html \
--privileged=true nginx

--privileged=true 表示容器内部对挂载的目录拥有读写等特权。

其他配置刚刚上面之前已经讲过,应该不用讲了。

image-20200922204931582

然后在你自己浏览器上就可以访问了,如果是云服务器,记得开放对应端口。

8. 安装 Easy Mock

因为 Easy Mock 依赖 Redis 和 MongoDB,因此本地环境使用 docker-compose 来搭建 Easy Mock 应该算是最佳实践了。

安装 docker-compose

官方文档:https://docs.docker.com/compose/install/

首先你得确定拥有 docker 环境,如果你是 Windows / Mac 用户,那么安装客户端,就会自带 docker-compose 了。

因为本次我们是在云服务器 CentOS7.6 上搭建,所以我们需要自行安装 docker-compose,运行如下命令,下载当前稳定版本的 docker-compose

$ sudo curl -L "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

修改文件权限为可执行文件

$ sudo chmod +x /usr/local/bin/docker-compose

验证是否安装成功

$ docker-compose version

编写 docker-compose.yml 配置文件

可以参考官方文档给出的部署文档,也可以参考我下面的配置过程。

首先新建文件 docker-compose.yml 并将下面 docker-compose 文件内容复制进入 docker-compose.yml,然后将内容中注释位置替换为自己需要的本地地址

version: '3'

services:
  mongodb:
    image: mongo:3.4.1
    volumes:
      #  /apps/easy-mock/data/db 是数据库文件存放地址,根据需要修改为本地地址
      - '/apps/easy-mock/data/db:/data/db'
    networks:
      - easy-mock
    restart: always

  redis:
    image: redis:4.0.6
    command: redis-server --appendonly yes
    volumes:
      #  /apps/easy-mock/data/redis 是 redis 数据文件存放地址,根据需要修改为本地地址
      - '/apps/easy-mock/data/redis:/data'
    networks:
      - easy-mock
    restart: always

  web:
    image: easymock/easymock:1.6.0
    # easy-mock 官方给出的文件,这里是 npm start,这里修改为 npm run dev
    command: /bin/bash -c "npm run dev:server"
    ports:
      - 7300:7300  # 改为你自己期望的映射
    volumes:
      # 日志地址,根据需要修改为本地地址
      - '/apps/easy-mock/logs:/home/easy-mock/easy-mock/logs'
    networks:
      - easy-mock
    restart: always

networks:
  easy-mock:

启动 Easy Mock

在 docker-compose 文件目录下,运行如下命令:

$ docker-compose up -d

如果遇到 easymock docker 实例报文件权限错误

Error: EACCES: permission denied....

要在项目根目录执行以下命令

$ chmod 777 /yourfile/logs

然后就可以通过浏览器上的 你的域名.com:7300 访问到 easy-mock 了!

如果你觉得域名后面跟着端口号挺难看的,你可以通过配置 Nginx 的二级域名来访问你部署的 easy-mock,配置二级域名的方法参见 这篇文章

9. 可视化管理

关于可视化查询工具,这里就简单推介一个 LazyDocker,由于是在终端运行的,而且支持键盘操作和鼠标点击,就挺骚气的,有了这个一些查询语句可以少打几次了。

lzd

安装比较简单,运行下面的命令:

$ docker run --rm -it -v \
/var/run/docker.sock:/var/run/docker.sock \
-v ~/.config/lazydocker:/.config/jesseduffield/lazydocker \
lazyteam/lazydocker

可以设置一个终端的 alias

$ alias lzd='docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock -v ~/.config/lazydocker:/.config/jesseduffield/lazydocker lazyteam/lazydocker'

然后你在终端输入 lzd 就可以浏览你的镜像、容器、日志、配置、状态等等内容了。

10. 结语

由于在下目前使用 Docker 的主要场景是 MySQL、Nginx 之类工具的安装,所以本文所介绍的内容也大多属于这个场景。

篇幅原因 Docker 还有一些内容本文没有介绍,但上面的内容已基本满足日常的使用,其他 Docker 的内容可以关注一下在下的后续文章~


网上的帖子大多深浅不一,甚至有些前后矛盾,在下的文章都是学习过程中的总结,如果发现错误,欢迎留言指出~

参考文档:

  1. Empowering App Development for Developers | Docker 官方网站
  2. Docker核心技术(基础篇)
  3. Docker安装mysql
  4. Docker文档
  5. Docker-compose文档
  6. 使用 docker 运行 easy-mock - 知乎
  7. docker-compose easy-mock - 简书
  8. 使用 docker 运行 easy-mock | CodingDiary
  9. easymock官方docker仓库:easy-mock/easy-mock-docker
  10. 使用docker安装nginx

作者其他高赞文章:

  1. JS 中可以提升幸福度的小技巧
  2. Vue 使用中的小技巧
  3. Nginx 从入门到实践,万字详解!
  4. 半小时搞会 CentOS 入门必备基础知识
  5. 手摸手 Webpack 多入口配置实践
  6. 前端路由跳转基本原理

PS:本人博客地址 Github - SHERlocked93/blog,也欢迎大家关注我的公众号【前端下午茶】,一起加油吧~

查看原文

赞 32 收藏 23 评论 4

黑色技术 赞了文章 · 9月23日

🦄️ Web 站点暗色模式探索

本文存在一些DEMO,适合在 PC 端阅览

最近发布了自己的新博客 https://xlbd.me , 博客站点设计了暗色模式风格,但是当时只是基于媒体查询 perfers-color-schema 实现的跟随系统偏好设置切换主题风格,本次带来了可用户自定义的浅色/暗色主题风格切换功能,同时兼容跟随系统偏好设置切换主题风格。

跟随系统偏好设置切换

macOS Mojave 10.14+ 开始提供了外观设置选项,支持设置 浅色 / 深色 外观

image

macOS Catalina 10.15+ 开始可以设置 浅色 / 深色 / 自动 外观

image

用户随时都可以设定自己的系统外观,或者让系统在一天中从白天到晚上自动调整外观。随之而来的,perfers-color-schema CSS 媒体查询特性用于检测用户是否有将系统的主题色设置为浅色或者暗色。其值为 light / dark ,看一个例子:

image

代码很简单,声明两个媒体查询,编写相应媒体查询下色值即可。

/* 表示用户设定了浅色主题时,页面背景色为白色 */
@media (prefers-color-scheme: light) {
  body { 
    background: white;
  
}

/* 表示用户设定了暗色主题时,页面背景色为黑色 */
@media (prefers-color-scheme: dark) {
  body { 
    background: black;
  }
}

https://codepen.io/xiaoluoboding/pen/wvMJZGG

⚠️ 打开 CODEPEN 查看 demo

自定义主题切换

CSS 媒体查询 perfers-color-schema 已经帮我们实现了系统自动切换主题风格,虽然我们比较提倡在夜晚浏览网页或者使用APP时,用户看到的是暗色风格的UI界面,这样比较护眼。但是如果设计一个可让用户可选的主题开关,交互上会更好,用户也可以自由掌控到底想以什么主题浏览页面。

基于 CSS Variables 的实现方案

一、定义 CSS 变量

将 CSS 变量编写在根伪类 :root 里,它的作用域是整个 HTML文档,任何地方都能访问定义好的变量。下面我们分别定义浅色 / 暗色两套 CSS 变量:

:root {
  --color-light: rgba(0, 0, 0, .75);
  --color-dark: rgba(255, 255, 255, .75);
  --background-light: #f0f2f4;
  --background-dark: #242424;
  --border-light: rgba(0, 0, 0, .33);
  --border-dark: rgba(255, 255, 255, .33);
}

二、添加开关逻辑

开关逻辑核心功能其实就是标示当前用户的选择,我们需要一个状态来记录用户行为,使用 localStorage 保存状态;在 body 标签上设置一个属性 data-user-color-schema ,动态改变它的值。将这个属性作为选择器来使用。

const APP_THEME = 'user-color-scheme'

// set init theme mode as dark
localStorage.setItem(APP_THEME, 'dark')

const toggleButton = document.querySelector('.toggle-btn')

/**
 * if user select the theme, then use the given theme
 */
const applyTheme = givenTheme => {
  let currentTheme = givenTheme || localStorage.getItem(APP_THEME)
  
  if(currentTheme) {
    document.body.setAttribute('data-user-color-scheme', currentTheme)
  }
}

toggleButton.addEventListener('click', e => {
  e.preventDefault()
  
  applyTheme(toggleTheme())
})

三、动态 CSS 变量实现模式切换

不同选择器可以覆盖 CSS 变量的值,利用这个特性,可以获得动态的 CSS 变量。

:root {
  --color-light: rgba(0, 0, 0, .75);
  --color-dark: rgba(255, 255, 255, .75);
  --background-light: #f0f2f4;
  --background-dark: #242424;
  --border-light: rgba(0, 0, 0, .33);
  --border-dark: rgba(255, 255, 255, .33);
}

[data-user-color-scheme='light'] {
  --color-mode: 'light';
  --text-color: var(--color-light);
  --background-color: var(--background-light);
  --border-color: var(--border-light);
}

[data-user-color-scheme='dark'] {
  --color-mode: 'dark';
  --text-color: var(--color-dark);
  --background-color: var(--background-dark);
  --border-color: var(--border-dark);
}

body {
  padding: 2rem 1rem;
  color: var(--text-color);
  background: var(--background-color);
}

到这里,我们就实现了用户自定义的主题切换功能,看下面例子中,点击切换按钮,body 中的颜色属性已经在动态改变了。

image

如果设置了自定义主题 user-color-schema 切换,那么 perfers-color-schema 的优先级就要降低了。因为用户的选择权要高于系统的偏好设置。

初始化主题模式

上面提到了 user-color-schema 优先级要高于 perfers-color-schema ,那么是不是说明 perfers-color-schema 媒体查询就没什么用了,并且也享受不到跟随系统偏好展示页面的功能了呢,其实我们有一个解决的方法,利用 window.matchMedia() API 来鉴别当前用户的系统外观偏好设置。

// 如果匹配到 perfers-color-schema: dark, 代表当前系统外观偏好设置为暗色
if (window.matchMedia) {
  const colorSchema  = window.matchMedia('(prefers-color-scheme: dark)')
    console.log(colorSchema.matches) // Boolean: true/false
}

通过这个方法我们就可以实现判断用户的系统是否设置了暗色外观,利用这一点我们可以帮助用户默认选择页面渲染模式。

https://codepen.io/xiaoluoboding/pen/eYJWyeM

⚠️ 打开 CODEPEN 查看 demo

"鱼"和"熊掌"两者兼顾

虽然实现了跟随系统初始化主题模式,但是因为 use-color-schema 仍然优先级比 perfers-color-schema ,这时当你切换系统外观偏好的时候,页面是没有跟随改变的。

这时我们需要知道系统偏好何时发生变化,也就是我们期望知道 perfers-color-schema 的值何时发生变化,同样是基于 window.matchMedia() API,看下面例子:

const initTheme = () => {
  if (window.matchMedia) {
    const colorSchema  = window.matchMedia('(prefers-color-scheme: dark)')
    
        // 为媒体查询添加监听器
    colorSchema.addListener(e => {
      console.log(e.matches) // Boolean: true/false
      const currentTheme = e.matches ? 'dark' : 'light'
      localStorage.setItem(APP_THEME, currentTheme)
      toggleButtonMode.innerHTML = e.matches ? 'light' : 'dark'
      applyTheme(currentTheme)
    })
  }
}

https://codepen.io/xiaoluoboding/pen/ExPmJyQ?editors=1010

⚠️ 打开 CODEPEN 查看 demo

默认暗色模式

要实现站点默认是暗色模式,其实很简单,颜色反着写就OK了

body {
  background-color: black;
  color: white;
}

@media (prefers-color-scheme: light) {
  body {
    background-color: white;
    color: black;
  }
}

Vue 3 中使用暗色模式

Vue3 的 composition API (组合式 API)提供了更好的逻辑复用和代码组织。

以下例子,我们可以将暗色模式变为响应式,一旦系统偏好改变了外观改变,我们可以立即获取改变的值。

抽离逻辑

定义 usePerfered

将公共逻辑媒体查询 window.matchMedia.matches 变为响应式进行抽象

// /src/helper/usePerfered.js
import { ref } from 'vue'
import { tryOnMounted, tryOnUnmounted } from './utils'

export function usePerferred (query) {
  let mediaQuery = null

  if (typeof window !== 'undefined') {
    mediaQuery = window.matchMedia(query)
  }

  const matches = ref(mediaQuery ? mediaQuery.matches : false)

  function handler(event) {
    matches.value = event.matches
  }

  tryOnMounted(() => {
    if (!mediaQuery) {
      mediaQuery = window.matchMedia(query)
    }
    handler(mediaQuery)
    mediaQuery.addListener(handler)
  })

  tryOnUnmounted(() => {
    mediaQuery.removeListener(handler)
  })

  return matches
}

组件中使用

我们得到了响应式的数据 isDarkmode ,它的变化会引起页面上的视图更新,可见 composition api 对业务逻辑的抽象是很有帮助的。

// /components/Darkmode.vue
import { reactive, watch, watchEffect, onMounted, toRefs } from 'vue'
import { usePerferred } from '../helper/usePerferred'

export default {
  setup () {
    const state = reactive({
      isDarkmode: usePerferred('(prefers-color-scheme: dark)')
    })
    ...
    return {
      ...toRefs(state)
    }
  }
}

颜色控制

HSLa 介绍

HSL 色相-饱和度-亮度(Hue-saturation-lightness)模式,HSL 相比 RGB 的优点是更加直观:你可以估算你想要的颜色,然后微调。它也更易于创建相称的颜色集合。

比如下图描述了:hsl(200, 100%, 50%) 不同透明度的颜色表现

Image

使用 CSS Variables 定制 HSLa

首先,我们将可控制的(Hue-saturation-lightness)拆分为独立变量,这样做的好处就是我可以单独改变某一个值去控制颜色的变化

:root {
  --text-color-h: 200;
  --text-color-s: 100%;
  --text-color-l: 50%;
}

将拆分的变量组合为 HSL 模式

:root {
  --text-color-h: 200;
  --text-color-s: 100%;
  --text-color-l: 50%;
  --text-color-hsl: var(--text-color-h), var(--text-color-s), var(--text-color-l);
}

基于定义好的 HSL,我们可以扩展颜色的不同透明度的色阶

:root {
  --text-color-h: 200;
  --text-color-s: 100%;
  --text-color-l: 50%;
  --text-color-hsl: var(--text-color-h), var(--text-color-s), var(--text-color-l);
  --text-color: hsla(var(--text-color-hsl), 1);
  --text-color-5: hsla(var(--text-color-hsl), .05);
  --text-color-10: hsla(var(--text-color-hsl), .1);
  --text-color-20: hsla(var(--text-color-hsl), .2);
  --text-color-30: hsla(var(--text-color-hsl), .3);
  --text-color-40: hsla(var(--text-color-hsl), .4);
  --text-color-50: hsla(var(--text-color-hsl), .5);
  --text-color-60: hsla(var(--text-color-hsl), .6);
  --text-color-70: hsla(var(--text-color-hsl), .7);
  --text-color-80: hsla(var(--text-color-hsl), .8);
  --text-color-90: hsla(var(--text-color-hsl), .9);
  --text-color-light: hsl(var(--text-color-h), var(--text-color-s), calc(var(--text-color-l) / .8));
  --text-color-dark: hsl(var(--text-color-h), var(--text-color-s), calc(var(--text-color-l) * .8));
}

使用 HSLa 设定暗色主题

https://codepen.io/xiaoluoboding/pen/wvGxvwB?editors=0100

⚠️ 打开 CODEPEN 查看 demo

参考

查看原文

赞 17 收藏 12 评论 0

黑色技术 赞了文章 · 9月22日

前端如何优雅处理类数组对象?

一、背景介绍

Leo 部门最近来了位前端实习生 Robin,作为师傅,Leo 认真的为 Robin 介绍了公司业务、部门工作等情况,还有前端的新人学习地图。

接下来 Robin 开始一周愉快的学习啦~

一周后,Leo 为 Robin 同学布置了学习作业,开发一个【人员搜索选择】的页面,效果大致如下:

Robin 看完这个效果图后,一脸得意的样子,这确实不难呀~

过几天后,Robin 带着自己写的代码,给 Leo 展示了她的代码,并疑惑的问到:

她将这个“数组”输出到控制台:

Leo 看了看代码:

getUserList(){
   const memberList = $('#MemberList li');
   memberList.map(item => { console.log(item) });
   console.log(memberList);
}

Leo 又问到:

Robin 一脸疑惑,然后 Leo 再原来代码上,加了个 Array.from 方法如下:

getUserList(){
    const memberList = Array.from($('#MemberList li'));
    memberList.map(item => {
        console.log(item)
    })
    console.log(memberList)
}

然后重新执行代码,输出下面结果:

Leo 输出的结果,跟 Robin 说到:

Robin 满脸期待望着师傅,对类数组对象更加充满期待。

二、类数组对象介绍

2.1 概念介绍

所谓 类数组对象,即格式与数组结构类似,拥有 length 属性,可以通过索引来访问或设置里面的元素,但是不能使用数组的方法,就可以归类为类数组对象

举个例子🌰:

const arrLike = {
  0: 'name',
  1: 'age',
  2: 'job',
  length: 3
}

2.2 常见类数组对象

  • arguments 对象;
function f() {
  return arguments;
}
f(1,2,3)

// Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
  • NodeList(比如 document.getElementsByClassName('a') 得到的结果;
document.getElementsByTagName('img')
// HTMLCollection(3) [img, img, img]
  • typedArray(比如 Int32Array);

typedArray 即 类型化数组对象 是一种类似数组的对象,它提供了一种用于访问原始二进制数据的机制。JavaScript引擎会做一些内部优化,以便对数组的操作可以很快。然而,随着Web应用程序变得越来越强大,尤其一些新增加的功能例如:音频视频编辑,访问WebSockets的原始数据等,很明显有些时候如果使用JavaScript代码可以快速方便地通过类型化数组来操作原始的二进制数据将会非常有帮助。 —— 《MDN 类型化数组》

const typedArray = new Uint8Array([1, 2, 3, 4])
// Uint8Array(4) [1, 2, 3, 4]

另外使用 jQuery 获取元素,会被 jQuery 做特殊处理成为 init 类型:

$('img')
// init(3) [img, img, img, prevObject: init(1), context: document, selector: "img"]

当然还有一些不常见的类数组对象,比如“Storage API 返回的结果”,这里就不一一列出。

三、类数组对象属性

下面通过 Robin 代码作为示例,介绍类数组对象的属性:

const memberList = $('#MemberList li');

3.1 读写

// 读取
memberList[0];
// Node: <li>...</li>

// 写入
memberList[0] = document.createElement("div")
memberList[0];
//  Node: <div>...</div>

3.2 长度

memberList.length; 
// 10

3.3 遍历

for (let i = 0;i < memberList.length; i++){
    console.log(memberList[i]);
}

/*
    Node: <li>...</li>
    Node: <li>...</li>
  ... 共10个,省略其他
*/

memberList.map(item => console.log(item));

/*
    0
  ... 共10个,省略其他
*/

但如果是 HTMLCollection 就不能使用 map 咯:

const img = document.getElementsByTagName("img");
img.map(item => console.log(item));

// Uncaught TypeError: img.map is not a function

四、类数组对象处理

Leo 看了看 Robin 处理这个列表的代码:

getUserList(){
    const memberList = $('#MemberList li');
    const result = {
        text: [],
        dom : [],
    };
    memberList.map(item => {
        item = memberList[item]
        // 判断当前节点是否有 checked 类名
    })
    console.log(result)
    this.showToast(`选中成员:${result.text}`);
}

很明显,Robin 并没有对 jQuery 获取到的 memberList 做处理,直接使用,通过索引来获取对应值。
Leo 继续和 Robin 介绍到:

4.1 Array.from

使用 Array.from 来将类数组对象转为数组对象,操作起来非常简单:

getUserList(){
    const memberList = Array.from($('#MemberList li'));
    // 省略其他代码
}

语法如下:

Array.from(arrayLike[, mapFn[, thisArg]])

参数

  1. arrayLike 想要转换成数组的伪数组对象或可迭代对象。
  2. mapFn 可选如果指定了该参数,新数组中的每个元素会执行该回调函数。
  3. thisArg 可选可选参数,执行回调函数 mapFnthis 对象。

返回值
一个新的数组实例。

更多Array.from 介绍可以查看文档。

4.2 Array.prototype.slice.call()

slice() 方法返回一个新的数组对象,这一对象是一个由 beginend 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变

实现代码:

getUserList(){
    const memberList = Array.prototype.slice.call($('#MemberList li'));
    // 省略其他代码
}

更多 Array.prototype.slice 介绍可以查看文档。

4.3 ES6展开运算符

展开语法(Spread syntax), 可以在函数调用/数组构造时, 将数组表达式或者string在语法层面展开;还可以在构造字面量对象时, 将对象表达式按key-value的方式展开。

实现代码:

getUserList(){
    const memberList = [...document.getElementsByTagName("li")];
    // 省略其他代码
}

更多 ES6展开运算符 介绍可以查看文档。

4.4 利用concat+apply

getUserList(){
    const memberList = Array.prototype.concat.apply([], $('#MemberList li'));
    // 省略其他代码
}

五、案例小结

Leo 介绍完这些知识后,Robin 又优化了下自己的代码,涉及到类数组对象操作的核心 js 代码如下:

class SelectMember {
    constructor(){
        this.MockUsers = window.MockUsers;
        this.init();
    }
    init(){
        this.initMemberList('#MemberList', this.MockUsers);
        this.initBindEvent();
    }
      // ... 省略部分代码,保留核心代码
    submitSelect(){
        const memberList = Array.from($('#MemberList li'));
        const result = {
            text: [],
            dom : [],
        };
        memberList.map(item => {
            const hasClass = $(item).children('.round-checkbox').children('span').hasClass(this.selectClassName);
            if(hasClass){
                result.text.push($(item).children('.user-data').children('h4').text());
                result.dom.push(item);
            }
        })
        this.showToast(`选中成员:${result.text}`);
    }
}

let newMember = new SelectMember();

很明显,使用正确方式来处理类数组对象,不仅能使我们代码更加少,减少转换处理,还能提高代码质量。

整个项目的完整代码,可以在我的 github 查看

https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Demo/10.Learn-Array-Liked-Objects/index.html

六、总结

本文我们通过一个实际场景,详细介绍了类数组对象在实际开发中的使用,对于常见的类数组对象,我们还介绍了处理方式,能很大程度减少我们处理类数组对象的操作,将类数组统一转成数组,更加方便对数据的操作。
希望看完本文的你,以后再遇到类数组对象,不会再一脸懵逼咯~~~

查看原文

赞 32 收藏 17 评论 1

黑色技术 赞了回答 · 8月31日

解决typescript中函数类型后的尖括号表示什么?

尖括号表示泛型,可以用来约束内容(这话我感觉说的也难懂)

那举个例子吧,声明一个函数test,我希望她接受一个叫参数nums,而且这个参数是数组,且数组里的内容必须是number,可以这么写:

var test = function(nums: Array<number>) {
    console.log(nums);
};

然后,当调用test函数时,如果传入的参数不是数组,或者数组里不是number类型的值的话,就会错误:

图片描述

图片描述

你这里的返回值,意思是“必须是Promise,但Promiseresolve的值不固定,可以是任意(any)值”

关注 3 回答 2

黑色技术 赞了文章 · 8月6日

使用electron开发了一个excel对比工具

elecrton 是什么?

[摘自官网]
Electron是由Github开发,用HTML,CSS和JavaScript来构建跨平台桌面应用程序的一个开源库。 Electron通过将Chromium和Node.js合并到同一个运行时环境中,并将其打包为Mac,Windows和Linux系统下的应用来实现这一目的。

简而言之:使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用
electron中文官网地址

elecrton 有什么特点,前端选择electron开发桌面应用有什么优势?

  • web 技术
Electron 基于 Chromium 和 Node.js, 让你可以使用 HTML, CSS 和 JavaScript 构建应用。所以对于前端工程师来说有着近水楼台的先天优势。
  • 开源,跨平台
如果您想在linux、mac、window下开发统一UI及功能的桌面应用,使用electron可以方便快捷的build成不同平台的安装包。
  • nodeJs的完美融合
引用npm官网的一句话“Build amazing things”,在项目中可以使用前端的任何框架和npm里面所有的模块,比如:ffi模块调取dll文件使你的应用原生功能更加强大,扩展性更好。

electron与nw.jsd该怎么选择?

项目不需要兼容WinXP ? 果断选择electron : nw.js
*

使用electron-vue 快速搭建项目

[摘自官网]electron-vue 充分利用 vue-cli 作为脚手架工具,加上拥有 vue-loader 的 webpack、electron-packager 或是 electron-builder,以及一些最常用的插件,如vue-router、vuex 等等。

项目从开发到打包安装文件

1.1 项目使用electron-vue快速搭建,开发中使用yarn管理依赖[如果游vpn的话随意]

1.2 项目UI使用vue的UI库,element、iview根据喜好

1.3 使用node-xlsx读取和生成excel文件

2.1 项目目的:对比两个excel文件中某一列相同项,并且支持追加导出功能。

2.2 开发遇到的问题:node-xlsx读取excel为二维数组,使用element table组件需要数组对象的json格式,所以代码里很多for循环,为了优化大文件的效率,建议巧妙使用if判断和for循环优化写法。

2.3 打包遇到的问题:本机开发打包白屏,但是run dev正常。请参考webpack.renderer.config.js第110行 。 第二种:本机打包正常,copy给其他电脑白屏,报各种模块不存在,请使用【npm】打包!!!一切问题迎刃而解。怀疑是yarn安装依赖没有被打包。

3.1 使用NSIS打包electron生成的可执行文件,需要注意的是在 5/8 的时候先移出全部在依次点击第一个添加exe文件,再点击第二个选择打包后exe所在的目录{记得勾选包含子目录},然后编译,就会生成一个exe安装文件。

项目截图





项目地址

安装包下载
github

查看原文

赞 19 收藏 10 评论 5

黑色技术 赞了文章 · 8月5日

想要试试Electron ,不如看看这篇爬坑总结

前言

web端能做的事情很多,但是当涉及到操作系统的时候,可能就有点力不从心了。前段时间在开发一个web系统的时候,就遇到了类似的情况。我们需要获取电脑操作系统的一些信息,比如mac地址等。我们的web系统是完全放在服务器上,通过浏览器来运行的,但是通过web端并不能直接实现我们想要的效果。

问题就是留给人们来解决的。经过同事之间的讨论,因为系统本身并不复杂,而且要进行快速的开发。决定用Electron + 原web系统的页面,来解决涉及操作系统信息的问题。

这篇文章总结了我在使用Electron 时所遇到的一些问题和解决方法。

什么是Electron?

使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用——这是Electron官网的简介

Electron是GitHub开发的一个开源框架。它允许使用Node.js(作为后端)和Chromium(作为前端)完成桌面GUI应用程序的开发。Electron现已被多个开源Web应用程序用于前端与后端的开发,著名项目包括GitHub的Atom和微软的Visual Studio Code。——知乎

可以简单的理解为Electron为web项目套上了Node.js环境的壳,使得我们可以调用Node.js的丰富的API。这样我们可以用JavaScript来写桌面应用,拓展很多我们在web端不能做的事情。

怎么构建Electron应用?

构建Electron应用很简单,可以直接查看官方文档,也可以利用现有的轮子。基本来说可以分为两大类:模板和命令行工具。这两种方式都有各自的优点,具体选用哪种方式要根据自己实际的情况来。就拿我来说,因为我要做的项目原本就有web端的页面了,这些模板基本都不适用,为了赶进度,就直接参考官网入门——打造你的第一个-electron-应用,再加上原有的代码进行构建项目。

下面为一些常用的构建模板与命令行工具

模板

命令行工具

一个例子

下面是一个最基础的Electron项目,后续的代码都是在此基础上进行拓展。

npm install --save-dev electron

一般结构

demo/
├── package.json
├── main.js
└── index.html

package.json

{
  "name": "demo",
  "version": "1.0.0",
  "description": "electornDemo",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "author": "xmanlin",
  "license": "MIT",
  "devDependencies": {
    "electron": "^9.0.0"
  }
}

main.js

const {app, BrowserWindow} = require('electron')

app.on('ready', function createWindow () {
    // 可以创建多个渲染进程
    let win = new BrowserWindow({
        width: 800,
        height: 600,
    })
    
    win.show()

    // 渲染进程中的web页面可以加载本地文件
    win.loadFile('index.html')


    // 记得在页面被关闭后清除该变量,防止内存泄漏
    win.on('closed', function () {
        win = null
    })


})

// 页面全部关闭后关闭主进程,不同平台可能有不同的处理方式
app.on('window-all-closed', () => {
    app.quit()
})

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>demo</title>
</head>
<body>
    <div>一个electron项目</div>
    <script>
    
    </script>
</body>
</html>

什么是主进程和渲染进程?

在Electron中,主进程和渲染进程的概念是十分重要的,具体可以查看官网介绍:主进程和渲染进程

主进程

  • 运行package.json中的main脚本的进程是主进程。
  • 一个electron应用有且只有一个主进程。
  • 主进程可以进行GUI相关的原生API操作。

渲染进程

  • Electron 使用了 Chromium 来展示 web 页面,所以 Chromium 的多进程架构也被使用到。
  • 每个web页面运行在它自己的渲染进程中。
  • 使用BrowserWindow类开启一个渲染进程并将这个实例运行在该进程中,当一个BrowserWindow实例被销毁后,相应的渲染进程也会被终止。
  • 渲染进程中不能调用原生资源,但是渲染进程中同样包含Node.js环境,所以可以引入Node.js

模块,在Node.js支持下,可以在页面中和操作系统进行一些底层交互。

渲染进程之中如何调用Node.js的API?

在Electron5.0版本后,渲染进程默认是不能调用Node.js的API的,经过设置后才可以:

主进程(main.js)

let win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
        nodeIntegration: true,  //设置为true就可以在这个渲染进程中调用Node.js
    }
})

怎样打开开发者工具?

Electron虽然是用了Chromium,但是想要打开开发者工具并不是像浏览器一样,如在windows下默认情况直接按F12。

在Electron中有两种方式打开开发者工具:

第一种是在主进程中进行设置,设置后,启动项目,该渲染进程就默认打开了开发者工具

 win.webContents.openDevTools();

第二种可以在渲染进程窗口的菜单栏进行选择View -> Toggle Developer Tools。(或者直接按照上面的快捷键进行操作)

electron 控制台打印乱码问题?

我们可能在Windows的控制台会出现中文乱码的问题,当我们在Windows的控制台下输入chcp,可以查看到当前字符编码,常见的gb2312的值是936,utf8的值是65001。这种情况下只要对package.json进行设置就能解决。

"start": "chcp 65001 && electron ."

主进程和渲染进程之间如何通信?

主进程和渲染进程之间可以通过ipcRendereripcMain模块通信。

主进程主动向渲染进程发送消息

主进程(main.js)

//主进程向渲染进程发送消息,'did-finish-load':当导航完成时发出事件,onload 事件也完成
win.webContents.on('did-finish-load', () => {
    win.webContents.send('msg', '消息来自主进程')
})

渲染进程(index.html)

<script>
    const {ipcRenderer} = require('electron')
    ipcRenderer.on('msg', (event, message) => {
        console.log(message) // 消息来自主进程
    })
</script>

渲染进程主动向主进程发送消息

渲染进程(index.html)

const {ipcRenderer} = require('electron')
ipcRenderer.send('indexMsg','消息来自渲染进程')

主进程(main.js)

const {ipcMain} = require('electron')
ipcMain.on('indexMsg',(event,msg) => {
    console.log(msg) //消息来自渲染进程
})

渲染进程之间如何通信?

渲染进程之间的通信方式有很多种,下面列出几种:

使用全局共享属性

//主进程
global.sharedObject = {
  user: ''
}

//渲染进程一
const {remote} = require('electron')
remote.getGlobal('sharedObject').user = 'xmanlin'

//渲染进程二
const {remote} = require('electron')
console.log(remote.getGlobal('sharedObject').user) //xmanlin

ipcRenderer.sendTo()

下面是ipcRenderer.sendTo()的参数

ipcRenderer.sendTo(webContentsId, channel, [, arg1][, arg2][, ...])
ipcRenderer.sendTo(windowId, 'ping', 'someThing')
//webContentsId : Number
//channel : String
//...args : any[]

具体用法

主进程(main.js)

//创建一个新的渲染进程
let win2 =  new BrowserWindow({
    width: 800,
    height: 600,
})

//为渲染进程设置唯一id
win2.id = 2

渲染进程1

<script>
    const {ipcRenderer} = require('electron')
    //向id为2的渲染进程发送消息
    ipcRenderer.sendTo(2,'msg1','来自渲染进程1的消息')
</script>

渲染进程2

<script>
    const {ipcRenderer} = require('electron')
    ipcRenderer.on('msg1', (event, message) => {
        console.log(message) // 来自渲染进程1的消息
    })
</script>

利用主进程做消息中转站

//主进程
ipcMain.on('msg1', (event, message) => {
  yourWindow.webContents.send('msg2', message);
}

//渲染进程1
ipcRenderer.send('msg1', '来自渲染进程1的消息')

//渲染进程2
ipcRenderer.on('msg2', (event, message) => {
    console.log(message)  //来自渲染进程1的消息
  }
)

如何对项目进行打包?

打包也是必不可少的一步,这里介绍两种比较成熟的打包工具:electron-packagerelectron-builder。这两个工具主要是对其进行配置。

electron-packager

我们可以利用 electron-packager把我们现有的electron应用打包为exe可执行文件。

先进行安装

npm install electron-packager --save-dev

安装好之后要配置electron-packager的基本命令,下面为官方文档中的基本格式:

electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arch> [optional flags...]

简要的介绍一下各个参数所代表的意思:

  • sourcedir:项目所在路径
  • appname:应用名称(打包后文件的名称)
  • platform:确定了你要构建哪个平台的应用(Windows、Mac 还是 Linux)

    • platform=win32 代表Windows
    • platform=darwin 代表Mac
    • platform=linux 代表Linux
  • arch:决定了使用 x86 还是 x64 还是两个架构都用
  • optional options:可选选项

下面为一个例子,供参考

package.json

"scripts": {
    "build32": "electron-packager ./ appDemo --platform=win32 --arch=ia32  --out=./app --app-version=1.0.0 --overwrite --icon=./favicon.ico",
    "build64": "electron-packager ./ appDemo --platform=win32 --arch=x64  --out=./app --app-version=1.0.0 --overwrite --icon=./favicon.ico"
  }

上面都是打包Windows下的软件,build32打包的为32位,build64打包的为64位。除基本参数外,上面其他参数所代表的意义如下:

  • --out表示打包后生成的文件的目录
  • --app-version表示打包生成文件的版本号
  • --overwrite表示删除原有的打包文件,生成新的打包文件
  • --icon表示打包文件的图标

electron-builder

electron-builder不仅可以打包为exe可执行文件,还可以打包为可安装程序,功能与electron-packager相比也要丰富一些。

官网更为推崇yarn 来进行安装

yarn add electron-builder --dev

当然npm也是可以的

npm install electron-builder --save-dev

然后我们可以进行配置了

"scripts": {
  "pack": "electron-builder --dir",
  "dist": "electron-builder"
},
 "build": {
   "productName": "appDemo", // app中文名称
   "appId": "appDemoId",// app标识
   "directories": { // 打包后输出的文件夹
     "buildResources": "resources",
     "output": "dist/"
   }
   "files": [ // 打包后依然保留的源文件
     "dist/electron",
     "node_modules/",
     "package.json"
   ],
   "mac": { // mac打包配置
     "target": "dmg",
     "icon": "icon.ico"
   },
   "win": { // windows打包配置
     "target": "nsis",
     "icon": "icon.ico"
   },
   "dmg": { // dmg文件打包配置
     "artifactName": "appDemo.dmg",
     "contents": [
       {
         "type": "link",
         "path": "/Applications",
         "x": 410,
         "y": 150
       },
       {
         "type": "file",
         "x": 130,
         "y": 150
       }
     ]
   },
   "nsis": { // nsis文件打包配置
     "oneClick": false,
     "allowToChangeInstallationDirectory": true, // 允许修改安装目录
     "allowElevation": true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。
     "installerIcon": "./build/icons/aaa.ico",// 安装图标
     "uninstallerIcon": "./build/icons/bbb.ico",//卸载图标
     "installerHeaderIcon": "./build/icons/aaa.ico", // 安装时头部图标
     "createDesktopShortcut": true, // 创建桌面图标
     "createStartMenuShortcut": true,// 创建开始菜单图标
     "shortcutName": "xxxx", // 图标名称
     "include": "build/script/installer.nsh", //包含的自定义nsis脚本这个对于构建需求严格得安装过程相当有用。
   },
 }

在使用electron-builder打包时,也可以指定参数

 --mac, -m, -o, --macos   macOS打包
 --linux, -l              Linux打包
 --win, -w, --windows     Windows打包
 --mwl                    同时为macOS,Windows和Linux打包
 --x64                    x64 (64位安装包)
 --ia32                   ia32(32位安装包) 

全部参数可参考Command Line Interface (CLI)

关于NSIS,也可以了解一下这种打包方式:electron 打包流程 electron-packager + NSIS

如何设置窗口默认最大化和全屏?

默认最大化

//主进程(main.js)
let win = new BrowserWindow({show: false})
win.maximize()
win.show()

默认全屏

//主进程(main.js)
let win = new BrowserWindow({fullscreen: true})

如何自定义菜单栏?

可以直接参考这篇文章-使用 Electron 自定义菜单,写的比较详细,也可以直接参考官方文档来。当然我们也可以把自带的菜单栏隐藏掉,然后自己调用写Electron的API写一个菜单栏。例如VSCode就是这么做的,打开VSCode,然后在帮助里面找到切换开发者工具,你可能会发现新世界~。

如何获取操作系统mac地址?

这个主要是调用Node.js的API,就可以获取系统的mac地址。

var os=require("os");
//获取mac地址
var mac = ''
var networkInterfaces=os.networkInterfaces();
for(var i in networkInterfaces){
    for(var j in networkInterfaces[i]){
        if(networkInterfaces[i][j]["family"]==="IPv4" && networkInterfaces[i][j]["mac"]!=="00:00:00:00:00:00" && networkInterfaces[i][j]["address"]!=="127.0.0.1"){
            mac = networkInterfaces[i][j]["mac"]
        }
    }
}

参考

https://www.electronjs.org/docs

https://blog.csdn.net/weixin_...

https://www.jianshu.com/p/62c...

https://blog.csdn.net/qq_3480...

https://segmentfault.com/a/11...

https://juejin.im/post/5cfd2e...

最后

这篇文章是我到目前为止在学习并应用Electron时所遇到的问题以及所找到的解决办法,希望对大家有所帮助。若有不足或错误的地方,欢迎指出~

查看原文

赞 26 收藏 19 评论 3

黑色技术 赞了文章 · 7月1日

从ES6到ES10的新特性万字大总结(不得不收藏)

介绍

ECMAScript是一种由Ecma国际(前身为欧洲计算机制造商协会)在标准ECMA-262中定义的脚本语言规范。这种语言在万维网上应用广泛,它往往被称为JavaScript或JScript,但实际上后两者是ECMA-262标准的实现和扩展。

历史版本

至发稿日为止有九个ECMA-262版本发表。其历史版本如下:

  1. 1997年6月:第一版
  2. 1998年6月:修改格式,使其与ISO/IEC16262国际标准一样
  3. 1999年12月:强大的正则表达式,更好的词法作用域链处理,新的控制指令,异常处理,错误定义更加明确,数据输出的格式化及其它改变
  4. 2009年12月:添加严格模式("use strict")。修改了前面版本模糊不清的概念。增加了getters,setters,JSON以及在对象属性上更完整的反射。
  5. 2011年6月:ECMAScript标5.1版形式上完全一致于国际标准ISO/IEC 16262:2011。
  6. 2015年6月:ECMAScript 2015(ES2015),第 6 版,最早被称作是 ECMAScript 6(ES6),添加了类和模块的语法,其他特性包括迭代器,Python风格的生成器和生成器表达式,箭头函数,二进制数据,静态类型数组,集合(maps,sets 和 weak maps),promise,reflection 和 proxies。作为最早的 ECMAScript Harmony 版本,也被叫做ES6 Harmony。
  7. 2016年6月:ECMAScript 2016(ES2016),第 7 版,多个新的概念和语言特性。
  8. 2017年6月:ECMAScript 2017(ES2017),第 8 版,多个新的概念和语言特性。
  9. 2018年6月:ECMAScript 2018 (ES2018),第 9 版,包含了异步循环,生成器,新的正则表达式特性和 rest/spread 语法。
  10. 2019年6月:ECMAScript 2019 (ES2019),第 10 版。

发展标准

TC39(Technical Committee 39)是一个推动JavaScript发展的委员会,它的成语来自各个主流浏览器的代表成语。会议实行多数决,每一项决策只有大部分人同意且没有强烈反对才能去实现。

TC39成员制定着ECMAScript的未来。

每一项新特性最终要进入到ECMAScript规范里,需要经历5个阶段,这5个阶段如下:

  • Stage 0: Strawperson

    只要是TC39成员或者贡献者,都可以提交想法

  • Stage 1: Proposal

    这个阶段确定一个正式的提案

  • Stage 2: draft

    规范的第一个版本,进入此阶段的提案大概率会成为标准

  • Stage 3: Candidate

    进一步完善提案细则

  • Stage 4: Finished

    表示已准备好将其添加到正式的ECMAScript标准中

由于ES6以前的属性诞生年底久远,我们使用也比较普遍,遂不进行说明,ES6之后的语言风格跟ES5以前的差异比较大,所以单独拎出来做个记录。

ES6(ES2015)

ES6是一次重大的革新,比起过去的版本,改动比较大,本文仅对常用的API以及语法糖进行讲解。

Let 和 Const

在ES6以前,JS只有var一种声明方式,但是在ES6之后,就多了letconst这两种方式。用var定义的变量没有块级作用域的概念,而letconst则会有,因为这三个关键字创建是不一样的。

区别如下:

{
    var a = 10
    let b = 20
    const c = 30
}
a // 10
b // Uncaught ReferenceError: b is not defined
c // c is not defined
let d = 40
const e = 50
d = 60
d // 60
e = 70 // VM231:1 Uncaught TypeError: Assignment to constant variable.
varletconst
变量提升××
全局变量××
重复声明××
重新赋值×
暂时死区×
块作用域×
只声明不初始化×

类(Class)

在ES6之前,如果我们要生成一个实例对象,传统的方法就是写一个构造函数,例子如下:

function Person(name, age) {
    this.name = name
    this.age = age
}
Person.prototype.information = function () {
    return 'My name is ' + this.name + ', I am ' + this.age
}

但是在ES6之后,我们只需要写成以下形式:

class Person {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    information() {
        return 'My name is ' + this.name + ', I am ' + this.age
    }
}

箭头函数(Arrow function)

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的thisargumentssupernew.target。这些函数表达式更适用于那些本来需要匿名函数的地方,并且它们不能用作构造函数。

在ES6以前,我们写函数一般是:

var list = [1, 2, 3, 4, 5, 6, 7]
var newList = list.map(function (item) {
    return item * item
})

但是在ES6里,我们可以:

const list = [1, 2, 3, 4, 5, 6, 7]
const newList = list.map(item => item * item)

看,是不是简洁了不少

函数参数默认值(Function parameter defaults)

在ES6之前,如果我们写函数需要定义初始值的时候,需要这么写:

function config (data) {
    var data = data || 'data is empty'
}

这样看起来也没有问题,但是如果参数的布尔值为falsy时就会出问题,例如我们这样调用config:

config(0)
config('')

那么结果就永远是后面的值

如果我们用函数参数默认值就没有这个问题,写法如下:

const config = (data = 'data is empty') => {}

模板字符串(Template string)

在ES6之前,如果我们要拼接字符串,则需要像这样:

var name = 'kris'
var age = 24
var info = 'My name is ' + this.name + ', I am ' + this.age

但是在ES6之后,我们只需要写成以下形式:

const name = 'kris'
const age = 24
const info = `My name is ${name}, I am ${age}`

解构赋值(Destructuring assignment)

我们通过解构赋值, 可以将属性/值从对象/数组中取出,赋值给其他变量。

比如我们需要交换两个变量的值,在ES6之前我们可能需要:

var a = 10
var b = 20
var temp = a
a = b
b = temp

但是在ES6里,我们有:

let a = 10
let b = 20
[a, b] = [b, a]

是不是方便很多

模块化(Module)

在ES6之前,JS并没有模块化的概念,有的也只是社区定制的类似CommonJS和AMD之类的规则。例如基于CommonJS的NodeJS:

// circle.js
// 输出
const { PI } = Math
exports.area = (r) => PI * r ** 2
exports.circumference = (r) => 2 * PI * r

// index.js
// 输入
const circle = require('./circle.js')
console.log(`半径为 4 的圆的面积是 ${circle.area(4)}`)

在ES6之后我们则可以写成以下形式:

// circle.js
// 输出
const { PI } = Math
export const area = (r) => PI * r ** 2
export const circumference = (r) => 2 * PI * r

// index.js
// 输入
import {
    area
} = './circle.js'
console.log(`半径为 4 的圆的面积是: ${area(4)}`)

扩展操作符(Spread operator)

扩展操作符可以在函数调用/数组构造时, 将数组表达式或者string在语法层面展开;还可以在构造字面量对象时, 将对象表达式按key-value的方式展开。

比如在ES5的时候,我们要对一个数组的元素进行相加,在不使用reduce或者reduceRight的场合,我们需要:

function sum(x, y, z) {
    return x + y + z;
}
var list = [5, 6, 7]
var total = sum.apply(null, list)

但是如果我们使用扩展操作符,只需要如下:

const sum = (x, y, z) => x + y + z
const list = [5, 6, 7]
const total = sum(...list)

非常的简单,但是要注意的是扩展操作符只能用于可迭代对象

如果是下面的情况,是会报错的:

var obj = {'key1': 'value1'}
var array = [...obj] // TypeError: obj is not iterable

对象属性简写(Object attribute shorthand)

在ES6之前,如果我们要将某个变量赋值为同样名称的对象元素,则需要:

var cat = 'Miaow'
var dog = 'Woof'
var bird = 'Peet peet'

var someObject = {
  cat: cat,
  dog: dog,
  bird: bird
}

但是在ES6里我们就方便很多:

let cat = 'Miaow'
let dog = 'Woof'
let bird = 'Peet peet'

let someObject = {
  cat,
  dog,
  bird
}

console.log(someObject)

//{
//  cat: "Miaow",
//  dog: "Woof",
//  bird: "Peet peet"
//}

非常方便

Promise

Promise 是ES6提供的一种异步解决方案,比回调函数更加清晰明了。

Promise 翻译过来就是承诺的意思,这个承诺会在未来有一个确切的答复,并且该承诺有三种状态,分别是:

  1. 等待中(pending)
  2. 完成了 (resolved)
  3. 拒绝了(rejected)

这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 resolved 后,就不能再次改变

new Promise((resolve, reject) => {
  resolve('success')
  // 无效
  reject('reject')
})

当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的

new Promise((resolve, reject) => {
  console.log('new Promise')
  resolve('success')
})
console.log('finifsh')
// new Promise -> finifsh

Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装

Promise.resolve(1)
  .then(res => {
    console.log(res) // => 1
    return 2 // 包装成 Promise.resolve(2)
  })
  .then(res => {
    console.log(res) // => 2
  })

当然了,Promise 也很好地解决了回调地狱的问题,例如:

ajax(url, () => {
    // 处理逻辑
    ajax(url1, () => {
        // 处理逻辑
        ajax(url2, () => {
            // 处理逻辑
        })
    })
})

可以改写成:

ajax(url)
  .then(res => {
      console.log(res)
      return ajax(url1)
  }).then(res => {
      console.log(res)
      return ajax(url2)
  }).then(res => console.log(res))

for...of

for...of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。

例子如下:

const array1 = ['a', 'b', 'c'];

for (const element of array1) {
      console.log(element)
}

// "a"
// "b"
// "c"

Symbol

symbol 是一种基本数据类型,Symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。

每个从Symbol()返回的symbol值都是唯一的。一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的。

例子如下:

const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol('foo');

console.log(typeof symbol1); // "symbol"
console.log(symbol3.toString()); // "Symbol(foo)"
console.log(Symbol('foo') === Symbol('foo')); // false

迭代器(Iterator)/ 生成器(Generator)

迭代器(Iterator)是一种迭代的机制,为各种不同的数据结构提供统一的访问机制。任何数据结构只要内部有 Iterator 接口,就可以完成依次迭代操作。

一旦创建,迭代器对象可以通过重复调用next()显式地迭代,从而获取该对象每一级的值,直到迭代完,返回{ value: undefined, done: true }

虽然自定义的迭代器是一个有用的工具,但由于需要显式地维护其内部状态,因此需要谨慎地创建。生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数, 同时它可以自动维护自己的状态。 生成器函数使用 function*语法编写。 最初调用时,生成器函数不执行任何代码,而是返回一种称为Generator的迭代器。 通过调用生成器的下一个方法消耗值时,Generator函数将执行,直到遇到yield关键字。

可以根据需要多次调用该函数,并且每次都返回一个新的Generator,但每个Generator只能迭代一次。

所以我们可以有以下例子:

function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
    for (let i = start; i < end; i += step) {
        yield i;
    }
}
var a = makeRangeIterator(1,10,2)
a.next() // {value: 1, done: false}
a.next() // {value: 3, done: false}
a.next() // {value: 5, done: false}
a.next() // {value: 7, done: false}
a.next() // {value: 9, done: false}
a.next() // {value: undefined, done: true}

Set/WeakSet

Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。

所以我们可以通过Set实现数组去重

const numbers = [2,3,4,4,2,3,3,4,4,5,5,6,6,7,5,32,3,4,5]
console.log([...new Set(numbers)]) 
// [2, 3, 4, 5, 6, 7, 32]

WeakSet 结构与 Set 类似,但区别有以下两点:

  • WeakSet 对象中只能存放对象引用, 不能存放值, 而 Set 对象都可以。
  • WeakSet 对象中存储的对象值都是被弱引用的, 如果没有其他的变量或属性引用这个对象值, 则这个对象值会被当成垃圾回收掉. 正因为这样, WeakSet 对象是无法被枚举的, 没有办法拿到它包含的所有元素。

所以代码如下:

var ws = new WeakSet()
var obj = {}
var foo = {}

ws.add(window)
ws.add(obj)

ws.has(window) // true
ws.has(foo)    // false, 对象 foo 并没有被添加进 ws 中 

ws.delete(window) // 从集合中删除 window 对象
ws.has(window)    // false, window 对象已经被删除了

ws.clear() // 清空整个 WeakSet 对象

Map/WeakMap

Map 对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。

例子如下,我们甚至可以使用NaN来作为键值:

var myMap = new Map();
myMap.set(NaN, "not a number");

myMap.get(NaN); // "not a number"

var otherNaN = Number("foo");
myMap.get(otherNaN); // "not a number"

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

Map的区别与SetWeakSet的区别相似,具体代码如下:

var wm1 = new WeakMap(),
    wm2 = new WeakMap(),
    wm3 = new WeakMap();
var o1 = {},
    o2 = function(){},
    o3 = window;

wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // value可以是任意值,包括一个对象
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // 键和值可以是任意对象,甚至另外一个WeakMap对象
wm1.get(o2); // "azerty"
wm2.get(o2); // undefined,wm2中没有o2这个键
wm2.get(o3); // undefined,值就是undefined

wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (即使值是undefined)

wm3.set(o1, 37);
wm3.get(o1); // 37
wm3.clear();
wm3.get(o1); // undefined,wm3已被清空
wm1.has(o1);   // true
wm1.delete(o1);
wm1.has(o1);   // false

Proxy/Reflect

Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 Proxy 的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

ProxyReflect是非常完美的配合,例子如下:

const observe = (data, callback) => {
      return new Proxy(data, {
            get(target, key) {
                return Reflect.get(target, key)
            },
            set(target, key, value, proxy) {
                  callback(key, value);
                  target[key] = value;
                    return Reflect.set(target, key, value, proxy)
            }
      })
}

const FooBar = { open: false };
const FooBarObserver = observe(FooBar, (property, value) => {
  property === 'open' && value 
          ? console.log('FooBar is open!!!') 
          : console.log('keep waiting');
});
console.log(FooBarObserver.open) // false
FooBarObserver.open = true // FooBar is open!!!

当然也不是什么都可以被代理的,如果对象带有configurable: falsewritable: false 属性,则代理失效。

Regex对象的扩展

正则新增符号

  • i 修饰符

    // i 修饰符
    /[a-z]/i.test('\u212A') // false
    /[a-z]/iu.test('\u212A') // true
  • y修饰符

    // y修饰符
    var s = 'aaa_aa_a';
    var r1 = /a+/g;
    var r2 = /a+/y;
    
    r1.exec(s) // ["aaa"]
    r2.exec(s) // ["aaa"]
    
    r1.exec(s) // ["aa"]
    r2.exec(s) // null
  • String.prototype.flags

    // 查看RegExp构造函数的修饰符
    var regex = new RegExp('xyz', 'i')
    regex.flags // 'i'
  • unicode模式

    var s = '𠮷'
    /^.$/.test(s) // false
    /^.$/u.test(s) // true
  • u转义

    // u转义
    /\,/ // /\,/
    /\,/u // 报错 没有u修饰符时,逗号前面的反斜杠是无效的,加了u修饰符就报错。
  • 引用

    const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
    RE_TWICE.test('abc!abc') // true
    RE_TWICE.test('abc!ab') // false
    
    const RE_TWICE = /^(?<word>[a-z]+)!\1$/;
    RE_TWICE.test('abc!abc') // true
    RE_TWICE.test('abc!ab') // false

字符串方法的实现改为调用RegExp方法

  • String.prototype.match 调用 RegExp.prototype[Symbol.match]
  • String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
  • String.prototype.search 调用 RegExp.prototype[Symbol.search]
  • String.prototype.split 调用 RegExp.prototype[Symbol.split]

正则新增属性

  • RegExp.prototype.sticky 表示是否有y修饰符

    /hello\d/y.sticky // true
  • RegExp.prototype.flags获取修饰符

    /abc/ig.flags // 'gi'

Math对象的扩展

  • 二进制表示法 : 0b或0B开头表示二进制(0bXX0BXX)
  • 二进制表示法 : 0b或0B开头表示二进制(0bXX0BXX)
  • 八进制表示法 : 0o或0O开头表示二进制(0oXX0OXX)
  • Number.EPSILON : 数值最小精度
  • Number.MIN_SAFE_INTEGER : 最小安全数值(-2^53)
  • Number.MAX_SAFE_INTEGER : 最大安全数值(2^53)
  • Number.parseInt() : 返回转换值的整数部分
  • Number.parseFloat() : 返回转换值的浮点数部分
  • Number.isFinite() : 是否为有限数值
  • Number.isNaN() : 是否为NaN
  • Number.isInteger() : 是否为整数
  • Number.isSafeInteger() : 是否在数值安全范围内
  • Math.trunc() : 返回数值整数部分
  • Math.sign() : 返回数值类型(正数1负数-1零0)
  • Math.cbrt() : 返回数值立方根
  • Math.clz32() : 返回数值的32位无符号整数形式
  • Math.imul() : 返回两个数值相乘
  • Math.fround() : 返回数值的32位单精度浮点数形式
  • Math.hypot() : 返回所有数值平方和的平方根
  • Math.expm1() : 返回e^n - 1
  • Math.log1p() : 返回1 + n的自然对数(Math.log(1 + n))
  • Math.log10() : 返回以10为底的n的对数
  • Math.log2() : 返回以2为底的n的对数
  • Math.sinh() : 返回n的双曲正弦
  • Math.cosh() : 返回n的双曲余弦
  • Math.tanh() : 返回n的双曲正切
  • Math.asinh() : 返回n的反双曲正弦
  • Math.acosh() : 返回n的反双曲余弦
  • Math.atanh() : 返回n的反双曲正切

Array对象的扩展

  • Array.prototype.from:转换具有Iterator接口的数据结构为真正数组,返回新数组。

    console.log(Array.from('foo')) // ["f", "o", "o"]
    console.log(Array.from([1, 2, 3], x => x + x)) // [2, 4, 6]
  • Array.prototype.of():转换一组值为真正数组,返回新数组。

    Array.of(7)       // [7] 
    Array.of(1, 2, 3) // [1, 2, 3]
    
    Array(7)          // [empty, empty, empty, empty, empty, empty]
    Array(1, 2, 3)    // [1, 2, 3]
  • Array.prototype.copyWithin():把指定位置的成员复制到其他位置,返回原数组

    const array1 = ['a', 'b', 'c', 'd', 'e']
    
    console.log(array1.copyWithin(0, 3, 4)) // ["d", "b", "c", "d", "e"]
    
    console.log(array1.copyWithin(1, 3)) // ["d", "d", "e", "d", "e"]
  • Array.prototype.find():返回第一个符合条件的成员

    const array1 = [5, 12, 8, 130, 44]
    
    const found = array1.find(element => element > 10)
    
    console.log(found) // 12
  • Array.prototype.findIndex():返回第一个符合条件的成员索引值

    const array1 = [5, 12, 8, 130, 44]
    
    const isLargeNumber = (element) => element > 13
    
    console.log(array1.findIndex(isLargeNumber)) // 3
  • Array.prototype.fill():根据指定值填充整个数组,返回原数组

    const array1 = [1, 2, 3, 4]
    
    console.log(array1.fill(0, 2, 4)) // [1, 2, 0, 0]
    
    console.log(array1.fill(5, 1)) // [1, 5, 5, 5]
    
    console.log(array1.fill(6)) // [6, 6, 6, 6]
  • Array.prototype.keys():返回以索引值为遍历器的对象

    const array1 = ['a', 'b', 'c']
    const iterator = array1.keys()
    
    for (const key of iterator) {
          console.log(key)
    }
    
    // 0
    // 1
    // 2
  • Array.prototype.values():返回以属性值为遍历器的对象

    const array1 = ['a', 'b', 'c']
    const iterator = array1.values()
    
    for (const key of iterator) {
          console.log(key)
    }
    
    // a
    // b
    // c
  • Array.prototype.entries():返回以索引值和属性值为遍历器的对象

    const array1 = ['a', 'b', 'c']
    const iterator = array1.entries()
    
    console.log(iterator.next().value) // [0, "a"]
    console.log(iterator.next().value) // [1, "b"]
  • 数组空位:ES6明确将数组空位转为undefined或者empty

    Array.from(['a',,'b']) // [ "a", undefined, "b" ]
    [...['a',,'b']] // [ "a", undefined, "b" ]
    Array(3) //  [empty × 3]
    [,'a'] // [empty, "a"]

ES7(ES2016)

Array.prototype.includes()

includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。

代码如下:

const array1 = [1, 2, 3]
console.log(array1.includes(2)) // true

const pets = ['cat', 'dog', 'bat']
console.log(pets.includes('cat')) // true
console.log(pets.includes('at')) // false

幂运算符**

幂运算符**,具有与Math.pow()一样的功能,代码如下:

console.log(2**10) // 1024
console.log(Math.pow(2, 10)) // 1024

模板字符串(Template string)

自ES7起,带标签的模版字面量遵守以下转义序列的规则:

  • Unicode字符以"u"开头,例如\u00A9
  • Unicode码位用"u{}"表示,例如\u{2F804}
  • 十六进制以"x"开头,例如\xA9
  • 八进制以""和数字开头,例如\251

这表示类似下面这种带标签的模版是有问题的,因为对于每一个ECMAScript语法,解析器都会去查找有效的转义序列,但是只能得到这是一个形式错误的语法:

latex`\unicode`
// 在较老的ECMAScript版本中报错(ES2016及更早)
// SyntaxError: malformed Unicode character escape sequence

ES8(ES2017)

async/await

虽然Promise可以解决回调地狱的问题,但是链式调用太多,则会变成另一种形式的回调地狱 —— 面条地狱,所以在ES8里则出现了Promise的语法糖async/await,专门解决这个问题。

我们先看一下下面的Promise代码:

fetch('coffee.jpg')
    .then(response => response.blob())
    .then(myBlob => {
          let objectURL = URL.createObjectURL(myBlob)
          let image = document.createElement('img')
          image.src = objectURL
          document.body.appendChild(image)
    })
    .catch(e => {
          console.log('There has been a problem with your fetch operation: ' + e.message)
    })

然后再看看async/await版的,这样看起来是不是更清晰了。

async function myFetch() {
      let response = await fetch('coffee.jpg')
      let myBlob = await response.blob()

      let objectURL = URL.createObjectURL(myBlob)
      let image = document.createElement('img')
      image.src = objectURL
      document.body.appendChild(image)
}

myFetch()

当然,如果你喜欢,你甚至可以两者混用

async function myFetch() {
      let response = await fetch('coffee.jpg')
      return await response.blob()
}

myFetch().then((blob) => {
      let objectURL = URL.createObjectURL(blob)
      let image = document.createElement('img')
      image.src = objectURL
      document.body.appendChild(image)
})

Object.values()

Object.values()方法返回一个给定对象自身的所有可枚举属性值的数组,值的顺序与使用for...in循环的顺序相同 ( 区别在于 for-in 循环枚举原型链中的属性 )。

代码如下:

const object1 = {
      a: 'somestring',
      b: 42,
      c: false
}
console.log(Object.values(object1)) // ["somestring", 42, false]

Object.entries()

Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for...in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环还会枚举原型链中的属性)。

代码如下:

const object1 = {
      a: 'somestring',
      b: 42
}

for (let [key, value] of Object.entries(object1)) {
      console.log(`${key}: ${value}`)
}

// "a: somestring"
// "b: 42"

padStart()

padStart() 方法用另一个字符串填充当前字符串(重复,如果需要的话),以便产生的字符串达到给定的长度。填充从当前字符串的开始(左侧)应用的。

代码如下:

const str1 = '5'
console.log(str1.padStart(2, '0')) // "05"

const fullNumber = '2034399002125581'
const last4Digits = fullNumber.slice(-4)
const maskedNumber = last4Digits.padStart(fullNumber.length, '*') 
console.log(maskedNumber) // "************5581"

padEnd()

padEnd() 方法会用一个字符串填充当前字符串(如果需要的话则重复填充),返回填充后达到指定长度的字符串。从当前字符串的末尾(右侧)开始填充。

const str1 = 'Breaded Mushrooms'
console.log(str1.padEnd(25, '.')) // "Breaded Mushrooms........"
const str2 = '200'
console.log(str2.padEnd(5)) // "200  "

函数参数结尾逗号(Function parameter lists and calls trailing commas)

在ES5里就添加了对象的尾逗号,不过并不支持函数参数,但是在ES8之后,便开始支持这一特性,代码如下:

// 参数定义
function f(p) {}
function f(p,) {} 

(p) => {}
(p,) => {}

class C {
  one(a,) {},
  two(a, b,) {},
}

var obj = {
  one(a,) {},
  two(a, b,) {},
};

// 函数调用
f(p)
f(p,)

Math.max(10, 20)
Math.max(10, 20,)

但是以下的方式是不合法的:

仅仅包含逗号的函数参数定义或者函数调用会抛出 SyntaxError。 而且,当使用剩余参数的时候,并不支持尾后逗号,例子如下:

function f(,) {} // SyntaxError: missing formal parameter
(,) => {}       // SyntaxError: expected expression, got ','
f(,)             // SyntaxError: expected expression, got ','

function f(...p,) {} // SyntaxError: parameter after rest parameter
(...p,) => {}        // SyntaxError: expected closing parenthesis, got ','

在解构里也可以使用,代码如下:

// 带有尾后逗号的数组解构
[a, b,] = [1, 2]

// 带有尾后逗号的对象解构
var o = {
  p: 42, 
  q: true,
}
var {p, q,} = o

同样地,在使用剩余参数时,会抛出 SyntaxError,代码如下:

var [a, ...b,] = [1, 2, 3] // SyntaxError: rest element may not have a trailing comma

ShareArrayBuffer(因安全问题,暂时在Chrome跟FireFox中被禁用)

SharedArrayBuffer 对象用来表示一个通用的,固定长度的原始二进制数据缓冲区,类似于 ArrayBuffer 对象,它们都可以用来在共享内存(shared memory)上创建视图。与 ArrayBuffer 不同的是,SharedArrayBuffer 不能被分离。

代码如下:

let sab = new SharedArrayBuffer(1024) // 必须实例化
worker.postMessage(sab)

Atomics对象

Atomics对象 提供了一组静态方法用来对 SharedArrayBuffer 对象进行原子操作。

方法如下:

  • Atomics.add() :将指定位置上的数组元素与给定的值相加,并返回相加前该元素的值。
  • Atomics.and():将指定位置上的数组元素与给定的值相与,并返回与操作前该元素的值。
  • Atomics.compareExchange():如果数组中指定的元素与给定的值相等,则将其更新为新的值,并返回该元素原先的值。
  • Atomics.exchange():将数组中指定的元素更新为给定的值,并返回该元素更新前的值。
  • Atomics.load():返回数组中指定元素的值。
  • Atomics.or():将指定位置上的数组元素与给定的值相或,并返回或操作前该元素的值。
  • Atomics.store():将数组中指定的元素设置为给定的值,并返回该值。
  • Atomics.sub():将指定位置上的数组元素与给定的值相减,并返回相减前该元素的值。
  • Atomics.xor():将指定位置上的数组元素与给定的值相异或,并返回异或操作前该元素的值。
  • Atomics.wait():检测数组中某个指定位置上的值是否仍然是给定值,是则保持挂起直到被唤醒或超时。返回值为 "ok"、"not-equal" 或 "time-out"。调用时,如果当前线程不允许阻塞,则会抛出异常(大多数浏览器都不允许在主线程中调用 wait())。
  • Atomics.wake():唤醒等待队列中正在数组指定位置的元素上等待的线程。返回值为成功唤醒的线程数量。
  • Atomics.isLockFree(size):可以用来检测当前系统是否支持硬件级的原子操作。对于指定大小的数组,如果当前系统支持硬件级的原子操作,则返回 true;否则就意味着对于该数组,Atomics 对象中的各原子操作都只能用锁来实现。此函数面向的是技术专家。

Object.getOwnPropertyDescriptors()

Object.getOwnPropertyDescriptors() 方法用来获取一个对象的所有自身属性的描述符。代码如下:

const object1 = {
  property1: 42
}

const descriptors1 = Object.getOwnPropertyDescriptors(object1)

console.log(descriptors1.property1.writable) // true

console.log(descriptors1.property1.value) // 42

// 浅拷贝一个对象
Object.create(
  Object.getPrototypeOf(obj), 
  Object.getOwnPropertyDescriptors(obj) 
)

// 创建子类
function superclass() {}
superclass.prototype = {
  // 在这里定义方法和属性
}
function subclass() {}
subclass.prototype = Object.create(superclass.prototype, Object.getOwnPropertyDescriptors({
  // 在这里定义方法和属性
}))

ES9(ES2018)

for await...of

for await...of 语句会在异步或者同步可迭代对象上创建一个迭代循环,包括 StringArrayArray-like 对象(比如arguments 或者NodeList),TypedArrayMapSet和自定义的异步或者同步可迭代对象。其会调用自定义迭代钩子,并为每个不同属性的值执行语句。

配合迭代异步生成器,例子如下:

async function* asyncGenerator() {
      var i = 0
      while (i < 3) {
            yield i++
      }
}

(async function() {
      for await (num of asyncGenerator()) {
            console.log(num)
      }
})()
// 0
// 1
// 2

模板字符串(Template string)

ES9开始,模板字符串允许嵌套支持常见转义序列,移除对ECMAScript在带标签的模版字符串中转义序列的语法限制。

不过,非法转义序列在"cooked"当中仍然会体现出来。它们将以undefined元素的形式存在于"cooked"之中,代码如下:

function latex(str) { 
 return { "cooked": str[0], "raw": str.raw[0] }
} 

latex`\unicode` // { cooked: undefined, raw: "\\unicode" }

正则表达式反向(lookbehind)断言

首先我们得先知道什么是断言(Assertion)

断言(Assertion)是一个对当前匹配位置之前或之后的字符的测试, 它不会实际消耗任何字符,所以断言也被称为“非消耗性匹配”或“非获取匹配”。

正则表达式的断言一共有 4 种形式:

  • (?=pattern) 零宽正向肯定断言(zero-width positive lookahead assertion)
  • (?!pattern) 零宽正向否定断言(zero-width negative lookahead assertion)
  • (?<=pattern) 零宽反向肯定断言(zero-width positive lookbehind assertion)
  • (?<!pattern) 零宽反向否定断言(zero-width negative lookbehind assertion)

在ES9之前,JavaScript 正则表达式,只支持正向断言。正向断言的意思是:当前位置后面的字符串应该满足断言,但是并不捕获。例子如下:

'fishHeadfishTail'.match(/fish(?=Head)/g) // ["fish"]

反向断言和正向断言的行为一样,只是方向相反。例子如下:

'abc123'.match(/(?<=(\d+)(\d+))$/) //  ["", "1", "23", index: 6, input: "abc123", groups: undefined]

正则表达式 Unicode 转义

正则表达式中的Unicode转义符允许根据Unicode字符属性匹配Unicode字符。 它允许区分字符类型,例如大写和小写字母,数学符号和标点符号。

部分例子代码如下:

// 匹配所有数字
const regex = /^\p{Number}+$/u;
regex.test('²³¹¼½¾') // true
regex.test('㉛㉜㉝') // true
regex.test('ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ') // true

// 匹配所有空格
\p{White_Space}

// 匹配各种文字的所有字母,等同于 Unicode 版的 \w
[\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]

// 匹配各种文字的所有非字母的字符,等同于 Unicode 版的 \W
[^\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]

// 匹配 Emoji
/\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu

// 匹配所有的箭头字符
const regexArrows = /^\p{Block=Arrows}+$/u;
regexArrows.test('←↑→↓↔↕↖↗↘↙⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇧⇩') // true

具体的属性列表可查看:https://developer.mozilla.org...

正则表达式 s/dotAll 模式

在以往的版本里,JS的正则的.只能匹配emoji跟行终结符以外的所有文本,例如:

let regex = /./;

regex.test('\n');       // false
regex.test('\r');       // false
regex.test('\u{2028}'); // false
regex.test('\u{2029}'); // false

regex.test('\v');       // true
regex.test('\f');       // true
regex.test('\u{0085}'); // true

/foo.bar/.test('foo\nbar');     // false
/foo[^]bar/.test('foo\nbar');   // true

/foo.bar/.test('foo\nbar');     // false
/foo[\s]bar/.test('foo\nbar');   // true

但是在ES9之后,JS正则增加了一个新的标志 s 用来表示 dotAll,这可以匹配任意字符。代码如下:

/foo.bar/s.test('foo\nbar');    // true

const re = /foo.bar/s;  //  等价于 const re = new RegExp('foo.bar', 's');
re.test('foo\nbar');    // true
re.dotAll;      // true
re.flags;       // "s"

正则表达式命名捕获组

在以往的版本里,JS的正则分组是无法命名的,所以容易混淆。例如下面获取年月日的例子,很容易让人搞不清哪个是月份,哪个是年份:

const matched = /(\d{4})-(\d{2})-(\d{2})/.exec('2019-01-01')
console.log(matched[0]);    // 2019-01-01
console.log(matched[1]);    // 2019
console.log(matched[2]);    // 01
console.log(matched[3]);    // 01

ES9引入了命名捕获组,允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。代码如下:

const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;

const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // 1999
const month = matchObj.groups.month; // 12
const day = matchObj.groups.day; // 31

const RE_OPT_A = /^(?<as>a+)?$/;
const matchObj = RE_OPT_A.exec('');

matchObj.groups.as // undefined
'as' in matchObj.groups // true

对象扩展操作符

ES6中添加了数组的扩展操作符,让我们在操作数组时更加简便,美中不足的是并不支持对象扩展操作符,但是在ES9开始,这一功能也得到了支持,例如:

var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };

var clonedObj = { ...obj1 };
// 克隆后的对象: { foo: "bar", x: 42 }

var mergedObj = { ...obj1, ...obj2 };
// 合并后的对象: { foo: "baz", x: 42, y: 13 }

上面便是一个简便的浅拷贝。这里有一点小提示,就是Object.assign() 函数会触发 setters,而展开语法则不会。所以不能替换也不能模拟Object.assign()

如果存在相同的属性名,只有最后一个会生效。

Promise.prototype.finally()

finally()方法会返回一个Promise,当promise的状态变更,不管是变成rejected或者fulfilled,最终都会执行finally()的回调。

例子如下:

fetch(url)
      .then((res) => {
        console.log(res)
      })
      .catch((error) => { 
        console.log(error)
      })
      .finally(() => { 
        console.log('结束')
    })

ES10(ES2019)

Array.prototype.flat() / flatMap()

flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

flatMap()map() 方法和深度为1的 flat() 几乎相同.,不过它会首先使用映射函数映射每个元素,然后将结果压缩成一个新数组,这样效率会更高。

例子如下:

var arr1 = [1, 2, 3, 4]

arr1.map(x => [x * 2]) // [[2], [4], [6], [8]]

arr1.flatMap(x => [x * 2]) // [2, 4, 6, 8]

// 深度为1
arr1.flatMap(x => [[x * 2]]) // [[2], [4], [6], [8]]

flatMap()可以代替reduce()concat(),例子如下:

var arr = [1, 2, 3, 4]
arr.flatMap(x => [x, x * 2]) // [1, 2, 2, 4, 3, 6, 4, 8]
// 等价于
arr.reduce((acc, x) => acc.concat([x, x * 2]), []) // [1, 2, 2, 4, 3, 6, 4, 8]

但这是非常低效的,在每次迭代中,它创建一个必须被垃圾收集的新临时数组,并且它将元素从当前的累加器数组复制到一个新的数组中,而不是将新的元素添加到现有的数组中。

String.prototype.trimStart() / trimLeft() / trimEnd() / trimRight()

在ES5中,我们可以通过trim()来去掉字符首尾的空格,但是却无法只去掉单边的,但是在ES10之后,我们可以实现这个功能。

如果我们要去掉开头的空格,可以使用trimStart()或者它的别名trimLeft()

同样的,如果我们要去掉结尾的空格,我们可以使用trimEnd()或者它的别名trimRight()

例子如下:

const Str = '   Hello world!  '
console.log(Str) // '   Hello world!  '
console.log(Str.trimStart()) // 'Hello world!  '
console.log(Str.trimLeft()) // 'Hello world!  '
console.log(Str.trimEnd()) // '   Hello world!'
console.log(Str.trimRight()) // '   Hello world!'

不过这里有一点要注意的是,trimStart()trimEnd()才是标准方法,trimLeft()trimRight()只是别名。

在某些引擎里(例如Chrome),有以下的等式:

String.prototype.trimLeft.name === "trimStart"

String.prototype.trimRight.name === "trimEnd"

Object.fromEntries()

Object.fromEntries() 方法把键值对列表转换为一个对象,它是Object.entries()的反函数。

例子如下:

const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42]
])

const obj = Object.fromEntries(entries)

console.log(obj) // Object { foo: "bar", baz: 42 }

Symbol.prototype.description

description 是一个只读属性,它会返回Symbol对象的可选描述的字符串。与 Symbol.prototype.toString() 不同的是它不会包含Symbol()的字符串。例子如下:

Symbol('desc').toString();   // "Symbol(desc)"
Symbol('desc').description;  // "desc"
Symbol('').description;      // ""
Symbol().description;        // undefined

// 具名 symbols
Symbol.iterator.toString();  // "Symbol(Symbol.iterator)"
Symbol.iterator.description; // "Symbol.iterator"

//全局 symbols
Symbol.for('foo').toString();  // "Symbol(foo)"
Symbol.for('foo').description; // "foo"

String.prototype.matchAll

matchAll() 方法返回一个包含所有匹配正则表达式的结果及分组捕获组的迭代器。并且返回一个不可重启的迭代器。例子如下:

var regexp = /t(e)(st(\d?))/g
var str = 'test1test2'

str.match(regexp) // ['test1', 'test2']
str.matchAll(regexp) // RegExpStringIterator {}
[...str.matchAll(regexp)] // [['test1', 'e', 'st1', '1', index: 0, input: 'test1test2', length: 4], ['test2', 'e', 'st2', '2', index: 5, input: 'test1test2', length: 4]]

Function.prototype.toString() 返回注释与空格

在以往的版本中,Function.prototype.toString()得到的字符串是去掉空白符号的,但是从ES10开始会保留这些空格,如果是原生函数则返回你控制台看到的效果,例子如下:

function sum(a, b) {
      return a + b;
}

console.log(sum.toString())
// "function sum(a, b) {
//         return a + b;
//  }"

console.log(Math.abs.toString()) // "function abs() { [native code] }"

try-catch

在以往的版本中,try-catchcatch后面必须带异常参数,例如:

    // ES10之前
try {
      // tryCode
} catch (err) {
      // catchCode
}

但是在ES10之后,这个参数却不是必须的,如果用不到,我们可以不用传,例如:

try {
      console.log('Foobar')
} catch {
      console.error('Bar')
}

BigInt

BigInt 是一种内置对象,它提供了一种方法来表示大于 253 - 1 的整数。这原本是 Javascript中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。

可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数BigInt()

在以往的版本中,我们有以下的弊端:

// 大于2的53次方的整数,无法保持精度
2 ** 53 === (2 ** 53 + 1)
// 超过2的1024次方的数值,无法表示
2 ** 1024 // Infinity

但是在ES10引入BigInt之后,这个问题便得到了解决。

以下操作符可以和 BigInt 一起使用: +*-**% 。除 >>> (无符号右移)之外的位操作也可以支持。因为 BigInt 都是有符号的, >>> (无符号右移)不能用于 BigIntBigInt 不支持单目 (+) 运算符。

/ 操作符对于整数的运算也没问题。可是因为这些变量是 BigInt 而不是 BigDecimal ,该操作符结果会向零取整,也就是说不会返回小数部分。

BigIntNumber不是严格相等的,但是宽松相等的。

所以在BigInt出来以后,JS的原始类型便增加到了7个,如下:

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol (ES6)
  • BigInt (ES10)

globalThis

globalThis属性包含类似于全局对象 this值。所以在全局环境下,我们有:

globalThis === this // true

import()

静态的import 语句用于导入由另一个模块导出的绑定。无论是否声明了 严格模式,导入的模块都运行在严格模式下。在浏览器中,import 语句只能在声明了 type="module"script 的标签中使用。

但是在ES10之后,我们有动态 import(),它不需要依赖 type="module" 的script标签。

所以我们有以下例子:

const main = document.querySelector("main")
for (const link of document.querySelectorAll("nav > a")) {
      link.addEventListener("click", e => {
            e.preventDefault()

            import('/modules/my-module.js')
              .then(module => {
                    module.loadPageInto(main);
              })
              .catch(err => {
                    main.textContent = err.message;
              })
      })
}

私有元素与方法

在ES10之前,如果我们要实现一个简单的计数器组件,我们可能会这么写:

// web component 写法
class Counter extends HTMLElement {
      get x() { 
              return this.xValue
      }
      set x(value) {
              this.xValue = value
              window.requestAnimationFrame(this.render.bind(this))
      }

      clicked() {
            this.x++
      }

      constructor() {
            super()
            this.onclick = this.clicked.bind(this)
            this.xValue = 0
      }

      connectedCallback() { 
              this.render()
      }

      render() {
            this.textContent = this.x.toString()
      }
}
window.customElements.define('num-counter', Counter)

但是在ES10之后我们可以使用私有变量进行组件封装,如下:

class Counter extends HTMLElement {
      #xValue = 0

      get #x() { 
          return #xValue
      }
      set #x(value) {
            this.#xValue = value
            window.requestAnimationFrame(this.#render.bind(this))
      }

      #clicked() {
            this.#x++
      }

      constructor() {
            super();
            this.onclick = this.#clicked.bind(this)
      }

      connectedCallback() { 
              this.#render()
      }

      #render() {
            this.textContent = this.#x.toString()
      }
}
window.customElements.define('num-counter', Counter)

参考资料

  1. ECMAScript 6 入门
  2. 1.5万字概括ES6全部特性
  3. MDN
  4. ES2018 新特征之:非转义序列的模板字符串
  5. 正则表达式反向(lookbehind)断言
  6. Unicode property escapes
  7. exnext提案
  8. ES7、ES8、ES9、ES10新特性大盘点
  9. Ecma TC39
  10. [[ECMAScript] TC39 process](https://www.jianshu.com/p/b08...
  11. The TC39 Process
查看原文

赞 159 收藏 133 评论 5

认证与成就

  • 获得 23 次点赞
  • 获得 20 枚徽章 获得 0 枚金徽章, 获得 7 枚银徽章, 获得 13 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2013-12-04
个人主页被 595 人浏览