杜尼卜

杜尼卜 查看完整档案

上海编辑湖南人文科技学院  |  计算机软件 编辑潘帕斯  |  Web前端 编辑 zhangbing.site 编辑
编辑

做工程师不做码农、全栈开发工程师、持续学习者

📬微信公众号:前端全栈开发者
📘博客主页:https://zhangbing.site
🎁混饭小铺:https://store.zhangbing.site

个人动态

杜尼卜 赞了文章 · 1月19日

小程序云开发实现一个投票应用

因为最近想实践一下小程序的云开发能力,于是设计开发了一个简单的投票应用,欢迎感兴趣的一起学习交流。

代码仓库 https://github.com/luosijie/m...

由于小程序【个人开发者】不开放【投票】类目,所以就不能在线预览了,我放几张应用的截图

数据库设计

总共用到了3个集合

1. users (用户集合)

基本上直接保存用用户的userInfo数据

{
  "_id":"023ce9555ff068de0314b5521c313ee6",
  "OPENID":"oZK45EoiyFzv...7R64I",
  "nickName":"LSJ",
  "gender":1,
  "language":"zh_CN",
  "city":"Xiamen",
  "province":
  "Fujian",
  "country":"China",
  "avatarUrl":"https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTL9lhZHZdYsMx3mjhZZYbbE5OZhUqUefNtsibkhdrSTIdpdhzv34lYHXtafMjuoibJ8JwTj5VM76CkA/132"
}

2. votes (投票集合)

{
  "_id":"21ded5cb5ff5f0530407988a4e8f18a5", // 唯一id
  "creator":"o-ZK45EoiyFzvevQyQTSZUV7R64I", // 发起人
  "title":"阿斯顿大的as da", // 标题
  "desc":"阿斯顿阿斯顿", // 描述
  "startTime":"2021-1-7", // 开始日期
  "endTime":"2021-1-8", // 结束日期
  "state":"ing" // 状态
}

3. options (选项集合)

{
  "_id":"be7fb3985ff5f05403068303431d580b", // 唯一id
  "vote_id":"21ded5cb5ff5f0530407988a4e8f18a5", // 选项对应的投票_id
  "title":"阿斯顿大的大的", // 标题
  "desc":"撒打算的洒大地上阿斯顿", // 描述
  "image":"http://tmp/2jVXjjLScAyNf0dffe2c5fc6479bee73fe954b64a3e7.png", // 配图
  "users":["o-ZK45EoiyFzvevQyQTSZUV7R64I"] // 该选项的投票者
}

云函数开发

总共写了6个云函数

1. addRecord 新增投票记录

/**
 * 新增投票记录
 * @param {String} title 标题
 * @param {String} desc 描述
 * @param {String} startTime 开始日期
 * @param {String} endTime 结束日期
 * @param {String} anonymous 匿名
 * @param {String} min 允许小投票数
 * @param {String} max 允许最大投票数
 * @param {String} type 投票类型:normal; pk
 * @returns {Object} 包含投票_id
 */
const cloud = require('wx-server-sdk')
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})
const db = cloud.database()

exports.main = async (event, context) => {
  const wxContext = cloud.getWXContext()
  const voteCollection = db.collection('votes')
  const data = {
    creator: wxContext.OPENID, // 发起人
    title: event.title,
    desc: event.desc,
    startTime: event.startTime,
    endTime: event.endTime,
    anonymous: event.anonymous,
    min: event.min,
    max: event.max,
    type: event.type,
    state: 'ing'
  }
  // 集合投票votes:新增记录
  const res = await voteCollection.add({
    data
  })
  // 集合选项options: 新增记录
  const options = event.options
  const optionCollection = db.collection('options')
  const optionPromise = options.map( ele => {
    const option = {
      vote_id: res._id,
      ...ele
    }
    return optionCollection.add({
      data: option
    })
  })
  let resOptions = await Promise.all(optionPromise)
  resOptions = resOptions.map(e =>  e._id)
  // 返回投票结果
  return {
    success: true,
    message: '新增投票成功',
    ...res
  }
}

2.getRecordDetail 获取投票详情

/**
 * 获取投票详情
 * @param {String} _id 投票_id
 * @return {Object} 投票数据
 */
const cloud = require('wx-server-sdk')
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})
const db = cloud.database()

exports.main = async (event, context) => {
  const _id = event._id
  const OPENID = cloud.getWXContext().OPENID
  // 查找集合中的投票数据
  const voteCollection = db.collection('votes')
  // 聚合联表查询
  const voteQuery = await voteCollection
  .aggregate()
  .match({ _id })
  .lookup({
    from: 'users',
    localField: 'creator',
    foreignField: 'OPENID',
    as: 'creator'
  })
  .end()
  let vote = {}
  if (voteQuery && voteQuery.list.length) {
    vote = voteQuery.list[0]
    vote.creator = vote.creator[0]
    // 判断是否当前投票的发起人
    vote.isOwner = vote.creator.OPENID === OPENID

    // 查找集合中的选项数据
    const optionsCollection = db.collection('options')
    const optionsQuary = await optionsCollection
    .aggregate()
    .match({ vote_id: _id })
    .lookup({
      from: 'users',
      localField: 'users',
      foreignField: 'OPENID',
      as: 'users'
    })
    .end()
    vote.options = optionsQuary.list
    // 统计已经投票的人数
    let votedTotal = 0
    vote.options.forEach(e => {
      if (e.users && e.users.length) {
        votedTotal += e.users.length
      }
    })
    vote.votedTotal = votedTotal
    // 计算当前投票的状态
    if (vote.state !== 'end') {
      // 未开始
      if (new Date().getTime() < new Date(vote.startTime).getTime()) {
        vote.state = 'pre'
      }
      // 已过期 = 已结束
      if (new Date().getTime() > new Date(vote.endTime).getTime()) {
        vote.state = 'end'
      }
    }
    return {
      success: true,
      data: vote
    }
  } else {
    return {
      success: false,
      message: '找不到投票信息'
    }
  }
}

3. vote 投票操作

/**
 * 投票操作
 * @param {String} voteId 投票_id
 * @param {String} optionId 选项_id
 * @return {Object} 投票结果
 */
const cloud = require('wx-server-sdk')
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})
const db = cloud.database()

exports.main = async (event, context) => {
  const _id = event.optionId
  const vote_id = event.voteId
  const OPENID = cloud.getWXContext().OPENID
  // 获取当前投票数据对应的所有选项数据
  const options = db.collection('options')
  let voteOptions = await options.where({ vote_id }).get()
  voteOptions = voteOptions.data
  // 判断用户是否投过票
  let curOptionUsers = []
  for (let i = 0; i < voteOptions.length; i++) {
    // 找到选项中所有投过票的用户
    const users = voteOptions[i].users
    if (users && users.length) {
      if (voteOptions[i]._id === _id) {
        curOptionUsers = users
      }
      if (users && users.length) {
        // OPENID重复-说明已经投过票->直接返回
        if (users.indexOf(OPENID) > -1) {
          return {
            success: false,
            message: '您已经投过票了'
          }
        }
      }
    }
  }
  // 没有投票->将当前用户OPENID插入到对应的字段
  curOptionUsers.push(OPENID)
  const res = await options.where({ _id }).update({
    data: {
      users: curOptionUsers
    }
  })
  return {
    success: true,
    data: res,
    message: '投票成功'
  }
}

4. getRecordPage 获取我的投票记录分页

/**
 * 获取我的投票记录分页
 * @param {Number} no 页码
 * @param {Number} size 页数
 * @return {Object} 投票数据列表和总数
 */
const cloud = require('wx-server-sdk')
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})
const db = cloud.database()

exports.main = async (event, context) => {
  const wxContext = cloud.getWXContext()
  const size = event.size
  //获取接口参数
  const no = event.no
  const OPENID = wxContext.OPENID
  const voteCollection = db.collection('votes')
  // 查找集合中的投票数据
  const votes = await voteCollection.aggregate()
  .match({
    creator: OPENID
  })
  .lookup({
    from: 'options',
    localField: '_id',
    foreignField: 'vote_id',
    as: 'options'
  })
  .sort({
    _id: -1
  })
  .skip((no - 1) * size)
  .limit(size)
  .end()
  // 计算总数
  const total = await voteCollection.count()
  let data = votes.list
  // 计算投票状态
  if (data.length) {
    data = data.map(e => {
      if (e.state !== 'end') {
        // 未开始
        if (new Date().getTime() < new Date(e.startTime).getTime()) {
          e.state = 'pre'
        }
        // 已过期 = 已结束
        if (new Date().getTime() > new Date(e.endTime).getTime()) {
          e.state = 'end'
        }
      }
      // 统计已投票人数
      let votedTotal = 0
      const options = e.options
      options.forEach(o => {
        if (o.users && o.users.length) {
          votedTotal += o.users.length
        }
      })
      delete e.options
      return {
        ...e,
        votedTotal
      }
    })
  }
  return {
    total,
    data
  }
}

5. login 登录注册

/**
 * 登录注册
 * @param {String} OPENID 从cloud.getWXContext()中获取
 * @return {Object} 用书数据
 */
const cloud = require('wx-server-sdk')
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})
const db = cloud.database()

exports.main = async (event, context) => {
  const wxContext = cloud.getWXContext()
  // 查找集合中的用户数据
  const userCollection = db.collection('users')
  const users = await userCollection.where({ OPENID: wxContext.OPENID }).get()
  let user
  if (users && users.data.length) {
    // 用户已经存在-直接赋值用户数据
    user = users.data[0]
  } else {
    // 新用户-向数据库插入用户数据
    user = {
      OPENID: wxContext.OPENID,
      ...event.userInfo
    }
    await userCollection.add({
      data: user
    })
  }
  // 返回用户数据-前端用来缓存
  return {
    ...user
  }
}

6. checkImage 校验图片合法性


/**
 * 校验图片合法性
 * @param {*} event.fileID 微信云存储的图片ID
 * @return {Number} 0:校验失败;1:校验通过
 */
const cloud = require('wx-server-sdk')
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})
exports.main = async (event, context) => {
  const contentType = 'image/png'
  const fileID = event.fileID
  try {
    // 根据fileID下载图片
    const file = await cloud.downloadFile({
      fileID
    })
    const value = file.fileContent
    // 调用 imgSecCheck 借口,校验不通过接口会抛错
    // 必要参数 media { contentType, value }
    const result = await cloud.openapi.security.imgSecCheck({
      media: {
        contentType,
        value
      }
    })
    return 1
  } catch (err) {
    return 0
  }
}

前端开发

这次小程序端的开发

采用的是 滴滴前端团队出品的 mpx 框架

因为UI比较简单

这里就不贴代码了

感兴趣的欢迎前往 https://github.com/luosijie/m... 了解

谢谢阅读!

查看原文

赞 5 收藏 2 评论 0

杜尼卜 发布了文章 · 1月7日

使用Web Crypto API的端到端加密聊天

在传输或存储用户数据(尤其是私人对话)时,必须考虑采用加密技术来确保隐私。

通过阅读本教程,您将了解如何仅使用JavaScript和Web Crypto API(一种本地浏览器API)在Web应用程序中对数据进行端到端加密。

请注意,本教程非常基础,并且具有严格的教育意义,可能包含一些简化,不建议使用您自己的加密协议,如果没有在安全专家的帮助下正确使用,所使用的算法可能包含某些“陷阱”

如果您碰巧迷路了,也可以在此GitHub仓库中找到完整的项目。

什么是端到端加密?

端到端加密是一种通信系统,其中唯一能够读取消息的人就是进行通信的人。没有任何窃听者可以访问解密对话所需的加密密钥,甚至是运行消息传递服务的公司也无法访问。

什么是Web Crypto API?

Web Cryptography API定义了一个低级接口,用于与用户代理管理或暴露的加密密钥材料进行交互。API本身对密钥存储的底层实现是不可知的,但提供了一组通用的接口,允许富Web应用执行诸如签名生成和验证、散列和验证、加密和解密等操作,而不需要访问原始密钥材料。

基础知识

在以下步骤中,我们将声明端到端加密所涉及的基本功能。您可以将每个文件复制到 lib 文件夹下的专用 .js 文件中。请注意,由于Web Crypto API的异步特性,它们都是异步函数。

注意:并不是所有的浏览器都能实现我们将使用的算法。说的就是IE和旧版Microsoft Edge。请查看MDN网页文档中的兼容性表:Subtle Crypto - Web APIs

生成密钥对

加密密钥对对于端到端加密至关重要。密钥对由公共密钥私有密钥组成。应用程序中的每个用户都应具有一个密钥对来保护其数据,其他用户可以使用公共组件,而密钥对的所有者只能访问私有组件。您将在下一部分中了解这些功能的作用。

要生成密钥对,我们将使用 window.crypto.subtle.generateKey 方法,并使用具有 JWK格式window.crypto.subtle.exportKey 导出私钥和公钥。可以将其视为序列化密钥以在JavaScript之外使用的一种方法。

generateKeyPair.js

export default async () => {
  const keyPair = await window.crypto.subtle.generateKey(
    {
      name: "ECDH",
      namedCurve: "P-256",
    },
    true,
    ["deriveKey", "deriveBits"]
  );

  const publicKeyJwk = await window.crypto.subtle.exportKey(
    "jwk",
    keyPair.publicKey
  );

  const privateKeyJwk = await window.crypto.subtle.exportKey(
    "jwk",
    keyPair.privateKey
  );

  return { publicKeyJwk, privateKeyJwk };
};

此外,我选择了具有P-256椭圆曲线的ECDH算法,因为它得到了很好的支持,并且在安全性和性能之间达到了适当的平衡。随着新算法的推出,这种偏好会随着时间而改变。

注意:导出私钥可能会导致安全问题,因此必须谨慎处理。本教程集成部分将介绍的让用户复制粘贴的做法,并不是一个很好的做法,只是出于教育目的。

派生密钥

我们将使用在最后一步中生成的密钥对来派生对称加密密钥,该密钥对数据进行加密和解密,并且对于任何两个通信用户都是唯一的。例如,用户A使用他们的私钥和用户B的公钥派生密钥,用户B使用他们的私钥和用户A的公钥派生相同的密钥。没有人可以在不访问至少一个用户私钥的情况下生成派生密匙,因此保证它们的安全非常重要。

在上一步中,我们以JWK格式导出了密钥对。在推导出密钥之前,我们需要使用 window.crypto.subtle.importKey 将这些导入到原始状态。为了导出密钥,我们将使用 window.crypto.subtle.deriveKey

deriveKey.js

export default async (publicKeyJwk, privateKeyJwk) => {
  const publicKey = await window.crypto.subtle.importKey(
    "jwk",
    publicKeyJwk,
    {
      name: "ECDH",
      namedCurve: "P-256",
    },
    true,
    []
  );

  const privateKey = await window.crypto.subtle.importKey(
    "jwk",
    privateKeyJwk,
    {
      name: "ECDH",
      namedCurve: "P-256",
    },
    true,
    ["deriveKey", "deriveBits"]
  );

  return await window.crypto.subtle.deriveKey(
    { name: "ECDH", public: publicKey },
    privateKey,
    { name: "AES-GCM", length: 256 },
    true,
    ["encrypt", "decrypt"]
  );
};

在这种情况下,我选择AES-GCM算法是因为它具有已知的安全性/性能平衡和浏览器可用性。

加密文本

现在,我们可以使用派生密钥对文本进行加密,因此可以安全地传输文本。

在加密之前,我们将文本编码为 Uint8Array,因为这就是加密功能所需要的。我们使用 window.crypto.subtle.encrypt 对该数组进行加密,然后将其 ArrayBuffer 输出返回给 Uint8Array,然后将其转换为字符串并将其编码为Base64。JavaScript使它有点复杂,但这只是将我们的加密数据转换为可传输文本的一种方式。

encrypt.js

export default async (messageJSON, derivedKey) => {
  try {
    const message = JSON.parse(messageJSON);
    const text = message.base64Data;
    const initializationVector = new Uint8Array(message.initializationVector).buffer;

    const string = atob(text);
    const uintArray = new Uint8Array(
      [...string].map((char) => char.charCodeAt(0))
    );
    const algorithm = {
      name: "AES-GCM",
      iv: initializationVector,
    };
    const decryptedData = await window.crypto.subtle.decrypt(
      algorithm,
      derivedKey,
      uintArray
    );

    return new TextDecoder().decode(decryptedData);
  } catch (e) {
    return `error decrypting message: ${e}`;
  }
};

如您所见,AES-GCM算法参数包括一个初始化向量(iv)。对于每一个加密操作,可以是随机的,但绝对必须是唯一的,以保证加密的强度。它包含在信息中,所以它可以用于解密过程,这是下一步。另外,虽然不太可能达到这个数字,但你应该在2³²次使用后丢弃钥匙,因为此时随机IV会重复。

解密文字

现在我们可以使用派生密钥来解密我们收到的任何加密文本,做的事情与加密步骤正好相反。

在解密之前,我们检索初始化向量,将字符串从Base64转换回来,变成一个 Uint8Array,并使用相同的算法定义进行解密。之后,我们对 ArrayBuffer 进行解码,并返回人类可读的字符串。

decrypt.js

export default async (messageJSON, derivedKey) => {
  try {
    const message = JSON.parse(messageJSON);
    const text = message.base64Data;
    const initializationVector = new Uint8Array(message.initializationVector).buffer;

    const string = atob(text);
    const uintArray = new Uint8Array(
      [...string].map((char) => char.charCodeAt(0))
    );
    const algorithm = {
      name: "AES-GCM",
      iv: initializationVector,
    };
    const decryptedData = await window.crypto.subtle.decrypt(
      algorithm,
      derivedKey,
      uintArray
    );

    return new TextDecoder().decode(decryptedData);
  } catch (e) {
    return `error decrypting message: ${e}`;
  }
};

也有可能由于使用了错误的派生密钥或初始化向量,导致这个解密过程失败,这意味着用户没有正确的密钥对来解密他们收到的文本。在这种情况下,我们会返回一个错误信息。

集成到您的聊天应用程序中

而这就是所有需要的加密工作!在下面的章节中,我将解释我是如何使用我们在上面实现的方法来对一个使用Stream Chat强大的React聊天组件构建的聊天应用程序进行端到端加密的。

克隆项目

encrypted-web-chat仓库克隆到本地文件夹中,安装依赖项并运行它。

$ git clone https://github.com/getstream/encrypted-web-chat
$ cd encrypted-web-chat/
$ yarn install
$ yarn start

之后,应打开浏览器选项卡。但是首先,我们需要使用我们自己的Stream Chat API密钥配置项目。

配置Stream Chat Dashboard

GetStream.io上创建帐户,创建一个应用程序,然后选择开发而不是生产。

为简化起见,让我们同时禁用身份验证检查和权限检查。确保点击保存。当您的应用程序在生产中,您应该保持这些启用,并有一个后端为用户提供令牌。

请注意Stream凭据,因为下一步将使用它们在应用程序中初始化聊天客户端。由于我们禁用了身份验证和权限,因此我们现在仅真正需要密钥。不过,在未来,你还是会在你的后台使用密钥来实现认证,为Stream Chat发行用户令牌,这样你的聊天应用就可以有适当的访问控制。

如您所见,我已编辑密钥。最好保留这些凭据的安全性。

更改凭证

src/lib/chatClient.js 中,用您的密钥更改密钥。我们将使用此对象进行API调用并配置聊天组件。

chatClient.js

import { StreamChat } from "stream-chat";

export default new StreamChat("[api_key]");

在此之后,您应该能够测试应用程序。在以下步骤中,您将了解我们定义的函数适用于何处。

设置用户

src/lib/setUser.js 中,我们定义了设置聊天客户端的用户并使用给定的公钥对更新的函数。发送公共密钥对于其他用户来说是必要的,以便获得与我们的用户进行加密和解密通信所需的密钥。

setUser.js

import chatClient from "./chatClient";

export default async (id, keyPair) => {
  const response = await chatClient.setUser(
    {
      id,
      name: id,
      image: `https://getstream.io/random_png/?id=cool-recipe-9&name=${id}`,
    },
    chatClient.devToken(id)
  );

  if (
    response.me?.publicKeyJwk &&
    response.me.publicKeyJwk != JSON.stringify(keyPair.publicKeyJwk)
  ) {
    await chatClient.disconnect();
    throw "This user id already exists with a different key pair. Choose a new user id or paste the correct key pair.";
  }

  await chatClient.upsertUsers([
    { id, publicKeyJwk: JSON.stringify(keyPair.publicKeyJwk) },
  ]);
};

在此函数中,我们导入上一版中定义的 chatClient。它需要一个用户ID和一个密钥对,然后调用 chatClient.setUser 来设置用户。此后,它将检查该用户是否已经具有公共密钥,并且是否与给定密钥对中的公共密钥匹配。如果公钥匹配或不存在,我们将使用给定的公钥更新该用户;如果不是,我们断开连接并显示错误。

发件人组件

src/components/Sender.js 中,我们定义了第一屏,在这里选择我们的用户id,可以使用我们在 generateKey.js 中描述的函数生成一个密钥对,如果这是一个现有的用户,则可以粘贴用户创建时生成的密钥对。

收件人组成

src/components/Recipient.js 中,我们定义了第二个屏幕,在这里我们选择要与之通信的用户的id。该组件将使用 chatClient.queryUsers 获取该用户。该调用的结果将包含用户的公钥,我们将用它来导出加密/解密密钥。

KeyDeriver组件

src/components/KeyDeriver.js 中,我们定义了第三个屏幕,其中密钥是使用我们在 deriveKey.js 中实现的方法派生的,该方法使用发送方(us)的私钥和接收方的公钥。该组件只是一个被动加载屏幕,因为所需的信息已在前两个屏幕中收集。但是如果密钥有问题,它会显示一个错误。

EncryptedMessage组件

src/components/EncryptedMessage.js 中,我们自定义Stream Chat的Message组件,使用我们在 decrypt.js 中定义的方法对消息进行解密,同时提供加密数据和派生密钥。

如果不对Message组件进行此自定义,它将显示如下:

通过包装Stream Chat的 MessageSimple 组件并使用 useEffect 钩子来使用DEcrypt方法修改消息属性来进行自定义。

EncryptedMessageInput组件

src/components/EncryptedMessageInput.js 中,我们自定义Stream Chat的MessageInput组件,以便在发送之前使用我们在 encrypt.js 中定义的方法将写好的消息与原始文本一起加密。

定制是通过包装Stream Chat的 MessageInputLarge 组件并将 overrideSubmitHandler prop设置为一个函数来完成的,该函数在发送到通道之前对文本进行加密。

Chat组件

最后,在 src/components/Chat.js 中,我们使用Stream Chat的组件和我们自定义的Message和EncryptedMessageInput组件构建整个聊天屏幕。

Web Crypto API的后续步骤

恭喜你!您刚刚学习了如何在Web应用程序中实现基本的端到端加密,重要的是要知道这是端对端加密的最基本形式。它缺乏一些额外的调整,可以让它在现实世界中更加弹性,比如随机化填充、数字签名和前向保密等等。此外,对于实际使用而言,获得应用程序安全专业人员的帮助也至关重要。

查看原文

赞 10 收藏 6 评论 0

杜尼卜 赞了文章 · 1月7日

SegmentFault 思否 2020 年度 Top Writer

日新月异的技术革命,数字经济的新一轮爆发,背后是无数开发者夜以继日的付出。他们信奉技术力量,敢于技术创新,践行技术信仰,他们是技术先锋,探索改变世界的方向。

SegmentFault 思否作为中国领先的新一代开发者社区,在 2020 展开了第二届“中国技术先锋”年度评选,并先后发布《中国技术品牌影响力企业》、《中国开源先锋 33 人》及《最受开发者欢迎的技术活动》系列榜单。

而在这些引领着时代变革的先锋力量中,有一股力量不容忽视 —— 他们是社区的基石,也是行业发展、技术发展的源动力。他们是一群活跃在 SegmentFault 思否社区的一群卓越的开发者,他们热衷于分享知识与经验,他们布道技术与未来,他们让众多开发者受益,他们叫「Top Writer」。

SegmentFault 思否根据社区用户行为大数据(如文章 & 问答发布数量、获得声望 & 点赞量等)综合分析,从「技术问答」和「专栏文章」两个维度进行了2020年度「Top Writer」的评选。

话不多说,让我们来一同揭晓评选结果~

image

TopWriter·问答作者积累声望值高票问答
然后去远足15948git所谓的分布式体现在什么地方?
linong17915vue回车聚焦下一个input,动态绑定ref出现,refs拿到为undefined
fefe9695promise then 的回调函数是在什么时候进入微任务队列的?
GhostOfYou3748Linux crontab 没有效果
Meathill12308阅读源码重要吗?有多重要?
木马啊9087用纯css怎么实现A元素+B元素,A是绿色背景,A元素+C元素,A是红色背景?
唯一丶10723null undefined区别
zangeci3264chrome控制台 这种怎么输出的?
asseek8967怎么简写下面的赋值语句
hfhan13061element-ui 中 Cascader 级联选择器有没有什么办法判断它是否被全选
madRain3616js中多个时间,怎么取最小值
水不凉4199关于class中的函数问题
边城42037js 数组内嵌对象(json结构),知道路径怎么去修改内容?
TNT4020java 字符串去掉多余空格和空行
程序媛兔子1333vue项目如何实现导航栏中的前进和后退都要刷新页面?vue项目如何实现导航栏中的前进和后退都要刷新页面?
TopWriter·文章作者积累声望值高票文章
民工哥16954小姐姐用动画图解Git命令,一看就懂!
谭光志4666前端性能优化 24 条建议(2020)
前端小智54379能解决 80% 需求的 10个 CSS动画库
疯狂的技术宅410612020最新:100道有答案的前端面试题(上)
lzg95272036分享8个非常实用的Vue自定义指令
Jason302807-SpringBoot+MyBatis+Spring 技术整合实现商品模块的CRUD操作
杜尼卜9987听说你熟练使用Vue,那这9种Vue技术你掌握了吗?不信你全知道!
Peter谭老师13076深度:从零编写一个微前端框架
敖丙2640Redis 缓存雪崩、击穿、穿透
flydean661八张图彻底了解JDK8 GC调优秘籍-附PDF下载
阿宝哥14032「1.8W字」一份不可多得的 TS 学习指南
小傅哥243012天,这本《重学Java设计模式》PDF书籍下载量9k,新增粉丝1400人,Github上全球推荐榜!
codecraft11291聊聊golang的panic与recover
iyacontrol1236服务网格平台探索性指南
蒋鹏飞3443速度提高几百倍,记一次数据结构在实际工作中的运用

恭喜以上上榜的技术内容创作者!请入选的作者们添加下方思否小姐姐的微信,我们为每位「Top Writer」准备了定制证书和 SegmentFault 2021 限量版卫衣。

也欢迎更多开发者在 SegmentFault 思否社区分享自己的经验与技能,为更多「同路人」答疑解惑、互动交流。如果你希望!自己的内容更快被更多用户看见和关注,欢迎加入思否社区创作者群,交流技术、分享写作经验、获得更多流量。(入群请添加小姐姐微信并发送你的社区账号)

扫我↓ 添加 vivian

image

最后思否小姐姐为各位 Top Writer 和社区活跃的开发者点赞,在 SegmentFault 思否社区活跃的开发者最可爱!2021,我们继续在一起鸭!

查看原文

赞 22 收藏 4 评论 17

杜尼卜 发布了文章 · 1月4日

仅使用CSS就可以提高页面渲染速度的4个技巧

来源:https://blog.zhangbing.site/2020/12/28/improve-page-rendering-speed-using-only-css/

用户喜欢快速的网络应用,他们希望页面加载速度快,功能流畅。如果在滚动时有破损的动画或滞后,用户很有可能会离开你的网站。作为一名开发者,你可以做很多事情来改善用户体验。本文将重点介绍4个可以用来提高页面渲染速度的CSS技巧。

1. Content-visibility

一般来说,大多数Web应用都有复杂的UI元素,它的扩展范围超出了用户在浏览器视图中看到的内容。在这种情况下,我们可以使用内容可见性( content-visibility )来跳过屏幕外内容的渲染。如果你有大量的离屏内容,这将大大减少页面渲染时间。

这个功能是最新增加的功能之一,也是对提高渲染性能影响最大的功能之一。虽然 content-visibility 接受几个值,但我们可以在元素上使用 content-visibility: auto; 来获得直接的性能提升。

让我们考虑一下下面的页面,其中包含许多不同信息的卡片。虽然大约有12张卡适合屏幕,但列表中大约有375张卡。正如你所看到的,浏览器用了1037ms来渲染这个页面。

下一步,您可以向所有卡添加 content-visibility

在这个例子中,在页面中加入 content-visibility 后,渲染时间下降到150ms,这是6倍以上的性能提升。

正如你所看到的,内容可见性是相当强大的,对提高页面渲染时间非常有用。根据我们目前所讨论的东西,你一定是把它当成了页面渲染的银弹。

content-visibility 的限制

然而,有几个领域的内容可视性不佳。我想强调两点,供大家参考。

  • 此功能仍处于试验阶段。截至目前,Firefox(PC和Android版本)、IE(我认为他们没有计划在IE中添加这个功能)和,Safari(Mac和iOS)不支持内容可见性。
  • 与滚动条行为有关的问题。由于元素的初始渲染高度为0px,每当你向下滚动时,这些元素就会进入屏幕。实际内容会被渲染,元素的高度也会相应更新。这将使滚动条的行为以一种非预期的方式进行。

为了解决滚动条的问题,你可以使用另一个叫做 contain-intrinsic-size 的 CSS 属性。它指定了一个元素的自然大小,因此,元素将以给定的高度而不是0px呈现。

.element{
  content-visibility: auto;
  contain-intrinsic-size: 200px;
}

然而,在实验时,我注意到,即使使用 conta-intrinsic-size,如果我们有大量的元素, content-visibility 设置为 auto ,你仍然会有较小的滚动条问题。

因此,我的建议是规划你的布局,将其分解成几个部分,然后在这些部分上使用内容可见性,以获得更好的滚动条行为。

2. Will-change 属性

浏览器上的动画并不是一件新鲜事。通常情况下,这些动画是和其他元素一起定期渲染的。不过,现在浏览器可以使用GPU来优化其中的一些动画操作。

通过will-change CSS属性,我们可以表明元素将修改特定的属性,让浏览器事先进行必要的优化。

下面发生的事情是,浏览器将为该元素创建一个单独的层。之后,它将该元素的渲染与其他优化一起委托给GPU。这将使动画更加流畅,因为GPU加速接管了动画的渲染。

考虑以下CSS类:

// In stylesheet
.animating-element {
  will-change: opacity;
}

// In HTML
<div class="animating-elememt">
  Animating Child elements
</div>

当在浏览器中渲染上述片段时,它将识别 will-change 属性并优化未来与不透明度相关的变化。

根据Maximillian Laumeister所做的性能基准,可以看到他通过这个单行的改变获得了超过120FPS的渲染速度,而最初的渲染速度大概在50FPS。

5

什么时候不是用will-change

虽然 will-change 的目的是为了提高性能,但如果你滥用它,它也会降低Web应用的性能。

  • 使用 will-change 表示该元素在未来会发生变化。因此,如果你试图将 will-change 和动画同时使用,它将不会给你带来优化。因此,建议在父元素上使用 will-change ,在子元素上使用动画。

    .my-class{
      will-change: opacity;
    }
    .child-class{
      transition: opacity 1s ease-in-out;
    }
  • 不要使用非动画元素。当你在一个元素上使用 will-change 时,浏览器会尝试通过将元素移动到一个新的图层并将转换工作交给GPU来优化它。如果您没有任何要转换的内容,则会导致资源浪费。

最后需要注意的是,建议在完成所有动画后,将元素的 will-change 删除。

3.减少渲染阻止时间

今天,许多Web应用必须满足多种形式的需求,包括PC、平板电脑和手机等。为了完成这种响应式的特性,我们必须根据媒体尺寸编写新的样式。当涉及页面渲染时,它无法启动渲染阶段,直到 CSS对象模型(CSSOM)已准备就绪。根据你的Web应用,你可能会有一个大的样式表来满足所有设备的形式因素。

但是,假设我们根据表单因素将其拆分为多个样式表。在这种情况下,我们可以只让主CSS文件阻塞关键路径,并以高优先级下载它,而让其他样式表以低优先级方式下载。
<link rel="stylesheet" href="styles.css">

单一样式表

将其分解为多个样式表后:

<!-- style.css contains only the minimal styles needed for the page rendering -->
<link rel="stylesheet" href="styles.css" media="all" />
<!-- Following stylesheets have only the styles necessary for the form factor -->
<link rel="stylesheet" href="sm.css" media="(min-width: 20em)" />
<link rel="stylesheet" href="md.css" media="(min-width: 64em)" />
<link rel="stylesheet" href="lg.css" media="(min-width: 90em)" />
<link rel="stylesheet" href="ex.css" media="(min-width: 120em)" />
<link rel="stylesheet" href="print.css" media="print" />

如您所见,根据样式因素分解样式表可以减少渲染阻止时间。

4.避免@import包含多个样式表

通过 @import,我们可以在另一个样式表中包含一个样式表。当我们在处理一个大型项目时,使用 @import 可以使代码更加简洁。

关于 @import 的关键事实是,它是一个阻塞调用,因为它必须通过网络请求来获取文件,解析文件,并将其包含在样式表中。如果我们在样式表中嵌套了 @import,就会妨碍渲染性能。
# style.css
@import url("windows.css");
# windows.css
@import url("componenets.css");

与使用 @import 相比,我们可以通过多个 link 来实现同样的功能,但性能要好得多,因为它允许我们并行加载样式表。

总结

除了我们在本文中讨论的4个方面,我们还有一些其他的方法可以使用CSS来提高网页的性能。CSS最近的一个特性: content-visibility,在未来的几年里看起来是如此的有前途,因为它给页面渲染带来了数倍的性能提升。

最重要的是,我们不需要写一条JavaScript语句就能获得所有的性能。

我相信你可以结合以上的一些功能,为终端用户构建性能更好的Web应用。希望这篇文章对你有用,如果你知道什么CSS技巧可以提高Web应用的性能,请在下面的评论中提及。谢谢大家。

查看原文

赞 15 收藏 13 评论 0

杜尼卜 分享了头条 · 2020-12-28

在这篇文章中,我将向您展示使用 Intersection Observer 的另一种延迟加载方式,你将知道为什么 Intersection Observer 更好

赞 1 收藏 2 评论 1

杜尼卜 发布了文章 · 2020-12-28

2021年管理Monorepo代码库的11种出色工具

Vue.js 3.0 源码管理用的Lerna

如今,许多工具可以在20个不同的文件夹中运行“npm install”和“npm run build”。但是,并不是所有的工具都能促进正确的monorepo。

促进一个正确的单体开发意味着要解决一些挑战,比如为分离的模块运行测试和构建过程,能够从项目中独立发布模块,以及管理变更对项目中每个受影响的依赖模块的部分影响。

挑战的清单还在继续,甚至包括“琐碎”的事情,比如你如何管理issues和PRs,这可能会随着你的开发规模而变得困难。

请注意,一个monorepo不是一个整体的应用程序(!) ——它不是一次性构建或部署的,它是一组单独开发的应用程序。

什么是 monorepo?

国庆期间10月5日尤大公开了vue3.0已完成的源码,也是采用了monorepo管理模式,看来monorepo确实有其独到的优势。

monorepo是一种将多个package放在一个repo中的代码管理模式,摒弃了传统的多个package多个repo的模式。

目前 Babel, React, Angular, Ember, Meteor, Jest等许多开源项目都使用该种模式来管理代码。

解决的问题

  • 多个repo难以管理,编辑器需要打开多个项目;
  • 某个模块升级,依赖改模块的其他模块需要手动升级,容易疏漏;
  • 公用的npm包重复安装,占据大量硬盘容量,比如打包工具webpack会在每个项目中安装一次;
  • 对新人友好,一句命令即可完成所有模块的依赖安装,且整个项目模块不用到各个仓库去找;

带来的问题

  • 所有package代码集中在一个项目,单个项目体积较大;
  • 所有package代码对所有人可见,无法做权限管理;

还不知道monorpo的同学可以阅读以下文章:

在这篇综述中,我收集了一些世界上最好的工具来构建一个“monorepo”,你可以在一个项目里面构建多个模块,并且有不错的开发者体验,可以扩展。

这个列表并没有进行排名,旨在根据每个工具的优点来概述其优势。希望能帮助你节省时间,找到合适的工具。

欢迎在下方评论,分享自己的心得。

1. Yarn Workspaces

Yarn Workspaces 的目标是简化与monorepos的工作,以更明确的方式解决 yarn link 的一个主要用例。你的依赖关系可以链接在一起,这意味着你的工作空间可以相互依赖,同时总是使用最新的代码。这也是比 yarn link 更好的机制,因为它只影响你的工作空间树而不是你的整个系统。

Workspaces有助于解决一些问题,使其成为一个很好的单兵装备。

  • 它设置了一个单一的 node_modules,不需要在项目中的不同包中重复或克隆依赖关系。
  • 你的所有项目依赖都将被安装在一起,从而给Yarn更大的空间来更好地优化它们。
  • Yarn将使用一个单一的锁文件,而不是为每个项目使用不同的锁文件,这意味着更少的冲突和更容易的审查。
  • 它允许你改变你的一个软件包的代码,并让使用它的其他软件包立即看到这些变化。对一个包的源代码的任何修改都会立即应用到其他包中。

因此,Yarn Workspaces是一个非常强大的组合,可以和列表中的几乎所有工具,特别是Bit、Nx和Lerna等工具一起使用,作为你的monorepo管理抽象的下层。

不过,你也可以直接用workspaces发布。当一个工作空间被打包到一个存档中时,它会动态地将任何 workspace: 依赖关系替换为一个包的版本,因此您可以将结果包发布到远程注册表,而无需运行中间步骤——消费者将能够像使用任何其他包一样使用发布的工作空间。太酷了!

参考阅读基于lerna和yarn workspace的monorepo工作流

2. Bit

Bit是用于构建模块化项目的下一代工具。这是一种新的、令人兴奋的单仓库方法,在这种方法中,由同一个项目(同一个Bit工作空间)管理的模块实际上分布在不同的范围内,而不考虑仓库。

Bit让你以完全解耦的方式拆分模块的开发,享受简单的、整体的开发体验来协调一切。

使用bit,你可以在你的项目中解耦组件,这样每个组件都是独立开发、构建、测试和发布的。每个组件都是使用特殊的环境进行开发和构建的,这些环境是可扩展和可重用的,这样你就可以快速定制和再次使用它们。

Bit的工作空间管理着项目中所有组件之间的关系。当你对任何组件进行更改时,Bit会单独构建和测试它,并将更改传播到依赖关系图中。

组件可以作为独立的包,批量发布到NPM和/或bit.dev平台,用于协作、消费和文档。

Bit的UI可以帮助你查看你的monorepo的开发情况。当你编写代码时,每个组件都会被记录、测试、构建等,你可以通过实时反馈和热重载直观地看到正在发生的事情。

Bit提供了解耦的开发环境--可重用和可定制的模块,这些模块将独立组件整个生命周期所需的不同服务配置和“捆绑”在一起,如编译、捆绑、测试、磨合、文档等。

Bit的工作空间以简单而全面的方式解耦组件开发

掌握组件图——Bit定义、管理并帮助你利用项目中所有组件之间的关系。

图形驱动的构建——当您对某个组件进行更改时,Bit会自动检测依赖于它的其他组件,并“知道”只构建依赖组件的受影响的图形。

“图形驱动的构建”也意味着,万一一个组件被标记了新的发布版本(在被导出到Bit的云端之前),Bit不仅会在每个受影响的组件上运行构建,而且会确保给它们标记一个新的发布版本。

隔离的测试和构建——每个组件都是在项目外部隔离地构建和测试的,因此您可以确切地看到更改的影响。

组件构建管道——您可以在可重用的管道中构建作业,该管道可应用于项目或所有项目中的所有组件。

批量发布——在Bit monorepo中开发的每个组件都可以作为一个独立的包发布。Bit去掉了配置每个组件的“package.json”和其他设置文件的所有开销。你要做的就是运行'bit tag',这样Bit就会自动给所有修改过的组件打上版本补丁(支持semver规则),然后批量发布修改。

可重复使用的文档模板——每个组件都使用可重复使用和可定制的模板进行文档化,Bit为您自动完成大部分工作。用MDX工作?也许还可以添加一些可视化的例子?没问题。

独立渲染的组合——每个组件都是完全独立渲染的,完全在项目之外渲染,渲染的视觉效果(在编写代码时热重新加载)成为每个组件文档的一部分。

3. NX

NX是一套先进的可扩展的开发工具,适用于monorepos,非常强调现代全栈Web技术。

空NX monorepo

NX的目标是通过CLI(带编辑器插件)提供整体的开发体验,并提供可控代码共享和一致代码生成的功能。它还提供了增量构建,因此它不会在你的每一次提交中重建和重新测试所有内容,从而加快构建时间。

有了Nx,你可以使用你喜欢的框架,集成你可能已经在使用的现代工具。例如,NX可以让你使用与Cypress、Jest、Typescript、Prettier和其他工具的开箱即用的集成。

NX团队还提供了NX云,通过云中的智能计算记忆和更快的构建来帮助使用NX的团队更快地交付。

8

4. Rush

Rush是由微软+开源的一个强大的monorepo基础设施,它的目的是帮助你在一个仓库中构建和发布许多包。

登陆页面和一些组件,两个项目,一个仓库

rush的一些主要功能包括一个单一的NPM安装(也可以和Yarn和pnpm一起使用),所以你可以将所有项目的所有依赖关系安装到一个共同的文件夹中,使用隔离的符号链接为每个项目重新构建一个准确的“node_modules”文件夹。

这也有助于确保没有幻影依赖,所以你不会意外地导入一个在package.json中缺失的库,也不会在node_modules中发现10份lib的依赖重复。

Rush交互式CLI不错

自动本地链接意味着你所有的项目都会自动地相互建立符号链接,当你做了一个改变,你可以看到下游的效果,而不需要发布任何东西,也没有任何 npm link 的麻烦。

Rush独特的安装策略为你的所有项目生成一个快速安装的单一收缩/锁定文件。Rush会检测你的依赖关系图,并以正确的顺序构建你的项目,所以如果两个包之间没有直接的依赖关系,Rush会将它们作为单独的进程并行构建。

如果你只打算使用你的repo中的几个项目,Rush提供了子集和增量构建,所以 rush rebuild --to <project> 只对你的上游依赖进行干净的构建。在你做了修改之后,rush rebuild --from <project> 只对受影响的下游项目进行清理。而 rush build 则提供了强大的跨项目增量构建,Rush甚至可以通过分离项目的版本来处理循环依赖关系。

当你想发布的时候,Rush支持批量发布,所以它会检测哪些包有变化,自动跳转所有相关的版本号,并在每个文件夹中运行 npm publish

Rush还有助于实施和执行发展政策。例如,当创建PR时,你可以要求开发人员提供受影响项目的主要/次要/补丁日志条目,这些条目随后将在发布时汇总到一个变更日志文件中。它还可以帮助你执行诸如发布前的审查、特定的依赖版本等东西。

5. Lerna

Lerna(以多头野兽Hydra的家命名)是一个“用于管理带有多个包的JavaScript项目的工具”。

Lerna的创建是为了解决Babel的多包问题,以优化使用git和npm管理多包仓库的工作流程,它本质上是一种工具和脚本,可以有效地管理和发布许多独立版本的包在一个Git仓库中。

my-lerna-repo/
  package.json
  packages/
    package-1/
      package.json
    package-2/
      package.json

Lerna 的两个主要命令是 lerna bootstraplerna publishbootstrap 会将 repo 中的依赖关系连接在一起,publish 会帮助发布任何更新的包。

您可以使用以下两种模式之一来管理项目:固定(Fixed)或独立(Independent)。

固定模式的Lerna项目是以单一的版本行来操作的,版本是保存在你的项目根目录下的 lerna.json 文件中的 version 键。当您运行 lerna publish 时,如果一个模块在上次发布后被更新,它将被更新到您发布的新版本。这是Babel目前使用的模式。

一个带有Yarn Workspaces的Lerna例子

独立模式Lerna项目允许维护者相互独立地增加包的版本,每次发布时,你都会收到一个提示,提示你每一个已经改变的软件包,以指定它是一个补丁,小的,大的或自定义的变化。独立模式可以让你更具体地更新每个包的版本,对于一组包来说是有意义的。

“lerna.json”文件是一个匹配包含 package.json 的目录的globs列表,这也是lerna识别“叶子”包的方式(相对于管理整个repo的开发依赖和脚本)。例子:

{
  "version": "1.1.3",
  "npmClient": "npm",
  "command": {
    "publish": {
      "ignoreChanges": ["ignored-file", "*.md"],
      "message": "chore(release): publish",
      "registry": "https://npm.pkg.github.com"
    },
    "bootstrap": {
      "ignore": "component-*",
      "npmClientArgs": ["--no-package-lock"]
    }
  },
  "packages": ["packages/*"]
}

即使你不打算发布到NPM,Lerna仍然可以在monorepo中帮助管理版本管理和常见的开发任务。

6. Bazel构建系统 (Google)

谷歌推出了Bazel build system,它是一个类似于Make、Maven和Gradle的开源构建和测试工具,使用的是人类可读的高级构建语言。Bazel支持多种语言的项目,并为多种平台构建输出。它支持大型单一仓库中的大型代码库或跨多个仓库的大型代码库和大量用户。

Uber开发者使用Bazel来构建他们的Go monorepo。Uber用Go编写了大部分的后端服务和库,在2018年,这些服务和库都被归纳到一个大型的Go monorepo中,现在有超过10万个文件。Bazel让这个项目得以扩展,缩短了构建时间,并支持其发展。

这是一个不错的小型开源项目,以Bazel作为演示:thundergolfer/example-bazel-monorepo

Bazel被设计成大规模工作,并支持跨分布式基础设施的增量密封构建,这是大型代码库所必需的。有了Bazel的远程缓存,构建服务器还可以共享它们的构建工件。Bazel缓存所有以前完成的工作,并跟踪对文件内容和构建命令的更改。只有在包或包的依赖关系发生更改时,才构建和测试包。

Bazel可以在Linux、macOS和Windows上运行。Bazel可以从同一个项目为多个平台构建二进制文件和可部署的包,包括桌面、服务器和移动设备。支持许多语言,你可以扩展Bazel来支持任何其他语言或框架。

7. Buck构建系统 (Facebook)

Buck是一个鼓励创建由代码和资源组成的小型可重用模块的构建系统,支持不同平台上的各种语言。

它是由Facebook开发和使用的,作为FB单体的官方构建系统,由于被Uber开发者等团队使用,大大缩短了构建时间,因此名声大噪。而AirbnbEng的团队则将构建速度提高了50%,将应用程序缩小了30%。

Uber凭借buck获得了更好的构建结果

Buck被设计用来构建一个monorepo,而对monorepo设计的支持激发了Buck对cell和项目的支持。

Facebook的经验是,将所有的依赖关系维护在同一个版本库中,可以更容易地确保所有开发者拥有正确的代码版本,并简化了进行原子提交的过程。

Buck常用于Android和iOS开发。

8. Pants构建系统(Twitter)

2014年,Twitter推出了名为Pants的monorepo构建系统。今天,在v2版本上,Pants的目标是成为一个快速、可扩展的构建系统,以适应不断增长的代码库。目前,它的重点是Python,很快就会支持其他语言。

Pants使用细粒度的工作流,并将每个工作单元与副作用隔离,因此可以利用所有可用的内核。Pant的一些最佳特性包括明确的依赖建模、细粒度的无效化、共享结果缓存、并发执行、远程执行,以及通过插件API的可扩展性和可定制性。

Pants引擎是用Rust写的,为的是性能。构建规则是用类型化的Python 3写的,为了熟悉和简单。该引擎的设计使得细粒度的无效化、并发性、密封性、缓存和远程执行自然发生,而无需规则作者的干预。

9. Please构建系统

Please是一个跨语言的构建系统,强调高性能、可移植性、可扩展性和正确性。

请确保构建步骤是在自己的密封环境中执行的,只能访问被赋予权限的文件和env变量。增量构建意味着它只构建它需要的东西,它还提供了任务并行性,以及分布式缓存,以实现大规模的可靠和高性能的构建系统。

Please的目标也是专注于开发体验,所以你可以享受一个常用的CLI,并为使用自动完成的常见任务定义别名。

Please用Go编写,Please提供所有这些用户体验,没有运行时依赖。并且,没有需要处理太多配置的单个大工作区文件。

10. Oao

Oao并不是列表中最成熟、最丰富、最容易使用的工具,但它还是很有趣。它是一个基于Yarn的,有意见的monorepo管理工具,p提供monorepo功能,如安装所有的依赖关系,添加/删除/升级子包的依赖关系,验证版本号,确定更新的子包,一次性发布所有的东西,更新变更日志等。

Oao可以让你在所有子包上运行命令或 package.json 脚本,串行或并行,可选择遵循反向依赖树。而且,它支持yarn workspaces,从整体上优化了monorepo依赖树,简化了bootstrap以及依赖的添加/升级/删除。

支持非单包发布:从oao’s的发布前检查、标签、版本选择、变更日志更新等方面受益,也可以在你的单包、非单包中使用。需要注意的是,Oao使用的是同步版本方案,所以在根级的 package.json 中配置了一个主版本,而子包也将与该版本同步。你可以在这里尝试一下。

11. Bolt

Boltpkg旨在成为一个“超级功能JavaScript项目管理工具”。

Bolt在Yarn的基础上实现了workspaces的概念。Bolt CLI在很大程度上是Yarn CLI的替代品,你可以在任何Yarn项目中使用它。

我们知道,workspaces是嵌套在一个更大的项目/repo中的,每个workspaces都可以有自己的依赖关系,有自己的代码和脚本。workspaces也可以归入子目录进行组织。

使用Bolt,你可以一次安装所有这些包的依赖关系(而且你可以做得非常非常快)。而且,当你从一个工作区指定一个依赖关系到另一个工作区时,它将被链接到源代码。这样,当你去测试你的代码时,你所有的变化都会被一起测试。


原文:https://blog.zhangbing.site/2020/12/28/how-to-implement-lazy-loading-in-react-with-intersection-observer/

查看原文

赞 6 收藏 5 评论 0

杜尼卜 发布了文章 · 2020-12-22

实战:如何在React应用中实现“使用GitHub登录”

我遇到一个场景,该场景需要使用Create-React-App在React 应用中实现“使用Github登录”功能。虽然这听起来很简单,但在尝试做这件事时,你可能会遇到一些麻烦。因此,本文的目的是提供一个指南,帮助你在你的应用程序中实现这样的功能。让我们现在就开始吧!

步骤1:在Github上创建OAuth应用

按照此处提供的步骤登录到你的Github帐户并创建OAuth应用。注意:对于本例,在创建OAuth应用时,如果你在本地运行应用,可以将主页URL设置为http://localhost:3000/,将授权回调URL设置为http://localhost:3000/login。在根目录下创建一个 .env 文件,并设置这些变量:

REACT_APP_CLIENT_ID=你的Client ID
REACT_APP_CLIENT_SECRET=你的Client Secret
REACT_APP_REDIRECT_URI=http://localhost:3000/login

步骤2:创建React应用

继续使用你的首选来创建你的react应用程序,在这个例子中,我们将使用Create-React-App。如果你使用这种模式,你必须删除index.css、App.css、App.test.js和serviceWorker.js等文件。编辑index.js,确保它看起来像这样:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

另外,编辑App.js,确保它看起来像这样:

import React, { createContext, useReducer } from 'react';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "./components/Home";
import Login from "./components/Login";
import { initialState, reducer } from "./store/reducer";


export const AuthContext = createContext();

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <AuthContext.Provider
      value={{
        state,
        dispatch
      }}
    >
    <Router>
      <Switch>
        <Route path="/login" component={Login}/>
        <Route path="/" component={Home}/>
      </Switch>
    </Router>
    </AuthContext.Provider>
  );
}

export default App;

在App.js文件中,导入2个组件(Home.jsLogin.js )。要创建这2个组件,进入src文件夹,创建一个名为component的文件夹,里面有2个文件(Home.js和Login.js)。在根文件夹中,你可以在下面运行此命令来创建它们。

mkdir -p src/components && cd src/components && touch Home.js Login.js

接下来,你会观察到,我们从store导入了状态和reducer,继续并设置一个简单的store,它将保持应用程序状态。要做到这一点,导航到src文件夹中,并创建一个名为store的文件夹,在它里面创建一个名为reducer的子文件夹,并在reducer文件夹里面创建一个index.js文件。在根目录下,你可以运行下面这个命令来创建它们。

mkdir -p src/store/reducer && cd src/store/reducer && touch index.js

Store中index.js文件的内容应如下所示。

export const initialState = {
  isLoggedIn: JSON.parse(localStorage.getItem("isLoggedIn")) || false,
  user: JSON.parse(localStorage.getItem("user")) || null,
  client_id: process.env.REACT_APP_CLIENT_ID,
  redirect_uri: process.env.REACT_APP_REDIRECT_URI,
  client_secret: process.env.REACT_APP_CLIENT_SECRET,
  proxy_url: process.env.REACT_APP_PROXY_URL
};

export const reducer = (state, action) => {
  switch (action.type) {
    case "LOGIN": {
      localStorage.setItem("isLoggedIn", JSON.stringify(action.payload.isLoggedIn))
      localStorage.setItem("user", JSON.stringify(action.payload.user))
      console.log(action.payload.isLoggedIn)
      return {
        ...state,
        isLoggedIn: action.payload.isLoggedIn,
        user: action.payload.user
      };
    }
    case "LOGOUT": {
      localStorage.clear()
      return {
        ...state,
        isLoggedIn: false,
        user: null
      };
    }
    default:
      return state;
  }
};

它包含InitialState对象和一个reducer函数,该函数包含派发的动作以突变状态。

这时,我们就可以在我们的组件上下功夫了。让我们在Login.js上工作,这将是一个简单的组件,它有一个按钮,可以触发Github API的登录请求。

import React, { useState, useEffect, useContext } from "react";
import { Redirect } from "react-router-dom";
import Styled from "styled-components";
import GithubIcon from "mdi-react/GithubIcon";
import { AuthContext } from "../App";


export default function Login() {
  const { state, dispatch } = useContext(AuthContext);
  const [data, setData] = useState({ errorMessage: "", isLoading: false });

  const { client_id, redirect_uri } = state;

  useEffect(() => {
    // After requesting Github access, Github redirects back to your app with a code parameter
    const url = window.location.href;
    const hasCode = url.includes("?code=");

    // If Github API returns the code parameter
    if (hasCode) {
      const newUrl = url.split("?code=");
      window.history.pushState({}, null, newUrl[0]);
      setData({ ...data, isLoading: true });

      const requestData = {
        client_id: state.client_id,
        redirect_uri: state.redirect_uri,
        client_secret: state.client_secret,
        code: newUrl[1]
      };

      const proxy_url = state.proxy_url;

      // Use code parameter and other parameters to make POST request to proxy_server
      fetch(proxy_url, {
        method: "POST",
        body: JSON.stringify(requestData)
      })
        .then(response => response.json())
        .then(data => {
          dispatch({
            type: "LOGIN",
            payload: { user: data, isLoggedIn: true }
          });
        })
        .catch(error => {
          setData({
            isLoading: false,
            errorMessage: "Sorry! Login failed"
          });
        });
    }
  }, [state, dispatch, data]);

  if (state.isLoggedIn) {
    return <Redirect to="/" />;
  }

  return (
    <Wrapper>
      <section className="container">
        <div>
          <h1>Welcome</h1>
          <span>Super amazing app</span>
          <span>{data.errorMessage}</span>
          <div className="login-container">
            {data.isLoading ? (
              <div className="loader-container">
                <div className="loader"></div>
              </div>
            ) : (
              <>
                {
                  // Link to request GitHub access
                }
                <a
                  className="login-link"
                  href={`https://github.com/login/oauth/authorize?scope=user&client_id=${client_id}&redirect_uri=${redirect_uri}`}
                  onClick={() => {
                    setData({ ...data, errorMessage: "" });
                  }}
                >
                  <GithubIcon />
                  <span>Login with GitHub</span>
                </a>
              </>
            )}
          </div>
        </div>
      </section>
    </Wrapper>
  );
}

const Wrapper = Styled.section`
  .container {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    font-family: Arial;
    
    > div:nth-child(1) {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2);
      transition: 0.3s;
      width: 25%;
      height: 45%;
      > h1 {
        font-size: 2rem;
        margin-bottom: 20px;
      }
      > span:nth-child(2) {
        font-size: 1.1rem;
        color: #808080;
        margin-bottom: 70px;
      }
      > span:nth-child(3) {
        margin: 10px 0 20px;
        color: red;
      }
      .login-container {
        background-color: #000;
        width: 70%;
        border-radius: 3px;
        color: #fff;
        display: flex;
        align-items: center;
        justify-content: center;
        > .login-link {
          text-decoration: none;
          color: #fff;
          text-transform: uppercase;
          cursor: default;
          display: flex;
          align-items: center;          
          height: 40px;
          > span:nth-child(2) {
            margin-left: 5px;
          }
        }
        .loader-container {
          display: flex;
          justify-content: center;
          align-items: center;          
          height: 40px;
        }
        .loader {
          border: 4px solid #f3f3f3;
          border-top: 4px solid #3498db;
          border-radius: 50%;
          width: 12px;
          height: 12px;
          animation: spin 2s linear infinite;
        }
        @keyframes spin {
          0% {
            transform: rotate(0deg);
          }
          100% {
            transform: rotate(360deg);
          }
        }
      }
    }
  }
`;

Login.js组件内部,请注意以下重要事项:

  1. 我们导入并利用AuthContext使Store中的全局状态和操作可在此组件中使用。
  2. 当用户点击“用Github登录”按钮时,会向Github API提出请求,对我们的应用进行授权。如果成功的话,Github就会重定向回我们的应用(授权回调URL),并在URL中加入“code”。
  3. 我们利用useEffect hook侦听此“code”何时可用。然后我们从url中收集它,使用code和其他数据,如:client_id,redirect_uri,client_secret,继续通过我们的proxy server(代理服务器)向Github APIs发出请求(一个简单的快递应用,帮助我们绕过CORS错误)。下一步,我将详细讨论代理服务器。
  4. 如果通过代理服务器的认证返回有效的响应,我们就会调度“LOGIN”事件,在我们的存储中设置用户数据和isLoggedIn有效载荷。

让我们更新Home.js组件以显示一些用户数据,例如(头像,姓名,关注者人数等)

import React, { useContext } from "react";
import { Redirect } from "react-router-dom";
import Styled from "styled-components";
import { AuthContext } from "../App";


export default function Home() {
  const { state, dispatch } = useContext(AuthContext);

  if (!state.isLoggedIn) {
    return <Redirect to="/login" />;
  }

  const { avatar_url, name, public_repos, followers, following } = state.user

  const handleLogout = () => {
    dispatch({
      type: "LOGOUT"
    });
  } 

  return (
    <Wrapper>
      <div className="container">
        <button onClick={()=> handleLogout()}>Logout</button>
        <div>
          <div className="content">
            <img data-original={avatar_url} alt="Avatar"/>
            <span>{name}</span>
            <span>{public_repos} Repos</span>
            <span>{followers} Followers</span>
            <span>{following} Following</span>
          </div>
        </div>
      </div>
    </Wrapper>
  );
}

const Wrapper = Styled.section`
.container{
  display: flex;
  flex-direction: column;
  height: 100vh;
  font-family: Arial;
  button{
    all: unset;
    width: 100px;
    height: 35px;
    margin: 10px 10px 0 0;
    align-self: flex-end;
    background-color: #0041C2;
    color: #fff;
    text-align: center;
    border-radius: 3px;
    border: 1px solid #0041C2;
    &:hover{
      background-color: #fff;
      color: #0041C2;
    }
  }
  >div{
    height: 100%;
    width: 100%;
    display: flex;
    font-size: 18px;
    justify-content: center;
    align-items: center;
    .content{
      display: flex;
      flex-direction: column;
      padding: 20px 100px;    
      box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2);
      width: auto;
  
      img{
        height: 150px;
        width: 150px;
        border-radius: 50%;
      }
  
      >span:nth-child(2){
        margin-top: 20px;
        font-weight: bold;
      }
  
      >span:not(:nth-child(2)){
        margin-top: 8px;
        font-size: 14px;
      }
  
    }
  }
}
`;

步骤3:创建代理服务器

最后一步是创建代理服务器,以帮助我们绕过CORS错误。它将是一个简单的express应用程序,我们将在header中启用“Access-Control-Allow-Origin”。我们将使用它来转发请求和接收来自Github API的响应,并将所需的响应发送回客户端(我们的React应用程序)。将这些变量添加到.env文件中:

REACT_APP_PROXY_URL=http://localhost:5000/authenticate
SERVER_PORT=5000

在根文件夹中,创建一个名为server的文件夹,并在其中创建一个index.js文件。

const express = require("express");
const bodyParser = require("body-parser");
const FormData = require("form-data");
const fetch = require("node-fetch");
const { client_id, redirect_uri, client_secret } = require("./config");

const config = require("./config");

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.json({ type: "text/*" }));
app.use(bodyParser.urlencoded({ extended: false }));

// Enabled Access-Control-Allow-Origin", "*" in the header so as to by-pass the CORS error.
app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  next();
});

app.post("/authenticate", (req, res) => {
  const { code } = req.body;

  const data = new FormData();
  data.append("client_id", client_id);
  data.append("client_secret", client_secret);
  data.append("code", code);
  data.append("redirect_uri", redirect_uri);

  // Request to exchange code for an access token
  fetch(`https://github.com/login/oauth/access_token`, {
    method: "POST",
    body: data,
  })
    .then((response) => response.text())
    .then((paramsString) => {
      let params = new URLSearchParams(paramsString);
      const access_token = params.get("access_token");

      // Request to return data of a user that has been authenticated
      return fetch(`https://api.github.com/user`, {
        headers: {
          Authorization: `token ${access_token}`,
        },
      });
    })
    .then((response) => response.json())
    .then((response) => {
      return res.status(200).json(response);
    })
    .catch((error) => {
      return res.status(400).json(error);
    });
});

const PORT = process.env.SERVER_PORT || 5000;
app.listen(PORT, () => console.log(`Listening on ${PORT}`));

附加链接:

https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/

https://www.graphql.college/implementing-github-oauth-flow/

源码

如果你按照上面列出的几个步骤进行操作,则可以在应用程序中无缝集成“使用Github登录”功能。

在此处获取源代码:Github

查看原文

赞 5 收藏 4 评论 0

杜尼卜 发布了文章 · 2020-12-21

自动增长Textareas的最干净技巧

想法是使 <textarea> 更像 <div>,因此它的高度可以扩展以包含当前值。这几乎是奇怪的,没有一个简单的原生解决方案,不是吗?

现在我得到了一个非常好的原生解决方案。

这里是演示,如果你只是想要一个工作的例子

<h1>Auto-Growing <code>&lt;textarea></code></h1>

<form action="#0">

  <label for="text">Text:</label>
  <div class="grow-wrap">
    <textarea name="text" id="text" onInput="this.parentNode.dataset.replicatedValue = this.value"></textarea>
  </div>

</form>
.grow-wrap {
  /* 简单的方法将元素叠加在一起,并根据最高者的高度确定它们的大小。 */
  display: grid;
}
.grow-wrap::after {
  /* 注意奇怪的空间!需要预防跳动行为 */
  content: attr(data-replicated-value) " ";

  /* 这就是textarea文本的表现形式 */
  white-space: pre-wrap;

  /* 隐藏在视图,点击和屏幕阅读器中 */
  visibility: hidden;
}
.grow-wrap > textarea {
  /* 您可以保留此设置,但是在用户调整大小后,它将破坏自动调整大小 */
  resize: none;

  /* Firefox显示增长的滚动条,您可以像这样隐藏。 */
  overflow: hidden;
}
.grow-wrap > textarea,
.grow-wrap::after {
  /* 需要相同的样式! */
  border: 1px solid black;
  padding: 0.5rem;
  font: inherit;

  /* 放在彼此之上 */
  grid-area: 1 / 1 / 2 / 2;
}

body {
  margin: 2rem;
  font: 1rem/1.4 system-ui, sans-serif;
}

label {
  display: block;
}

效果

诀窍是,你要准确地将 <textarea> 的内容复制到一个可以自动展开高度的元素中,并匹配它的大小。

所以你有一个 <textarea>,它不能自动展开高度。

相反,您可以在另一个元素中完全复制该元素的外观,内容和位置,再复制的元素隐藏起来。

现在,这三个元素都是相互联系的。无论哪一个子元素最高,都会把父元素推到那个高度,而另一个子元素也会跟随。这意味着 <textarea> 的最小高度将成为“基础”高度,但是如果复制的文本元素碰巧变高了,所有的东西也会随之变高。

好聪明,我太喜欢了。

您需要确保复制的元素完全相同

相同的字体,相同的填充,相同的页边距,相同的边框...所有内容。这是一个相同的副本,只是在视觉上隐藏了 visibility: hidden;;如果不是完全一样的,那么所有的东西都不会完全正确地生长在一起。

我们还需要在复制的文本上 white-space: pre-wrap; ,因为这就是textareas的表现。

这是最奇怪的部分

在我的演示中,我将 ::after 用于复制的文本。我不确定这是否是最好的方法。对我来说感觉很干净,但是我想知道使用 <div aria-hidden =“ true”> 对于屏幕阅读器是否更安全?

visibility: hidden; 够了吗?无论如何,那不是奇怪的部分。这是奇怪的部分:

content: attr(data-replicated-value) " ";

因为我使用的是伪元素,伪元素是将 data 属性从元素中取出并以额外的空间将内容呈现到页面的行(这是奇怪的部分)。如果你不这样做,最终的结果会让人感觉 "跳脱"。我不能说我完全理解它,但它似乎更好地尊重了跨textarea和文本元素的换行行为。

如果你不想使用伪元素,嘿嘿,我没意见,只要注意跳动的行为即可。

查看原文

赞 4 收藏 3 评论 0

杜尼卜 发布了文章 · 2020-12-21

Vite使Vue CLI过时了吗?

image
Vue生态系统中有一个名为Vite的新构建工具,它的开发服务器比Vue CLI快10-100倍。

这是否意味着Vue CLI已经过时了?在本文中,我将比较这两种构建工具,并说明它们的优缺点,以便你可以决定哪一种适合你的下一个项目。

Vue CLI概述

大多数Vue开发人员都知道,Vue CLI是使用标准构建工具和最佳实践配置快速建立基于Vue的项目的不可或缺的工具。

其主要功能包括:

  • 工程脚手架
  • 带热模块重载的开发服务器
  • 插件系统
  • 用户界面

在本讨论中需要注意的是,Vue CLI是构建在Webpack之上的,因此开发服务器和构建功能和性能都将是Webpack的超集。

Vite概述

与Vue CLI类似,Vite也是一个提供基本项目脚手架和开发服务器的构建工具。

然而,Vite并不是基于Webpack的,它有自己的开发服务器,利用浏览器中的原生ES模块。这种架构使得Vite比Webpack的开发服务器快了好几个数量级。Vite采用Rollup进行构建,速度也更快。

Vite目前还处于测试阶段,看来Vite项目的目的并不是像Vue CLI那样的一体化工具,而是专注于提供一个快速的开发服务器和基本的构建工具。

Vite怎么这么快?

Vite开发服务器至少会比Webpack快10倍左右。对于一个基本的项目来说,与2.5秒相比,开发构建/重新构建的时间相差250ms。

在一个较大的项目中,这种差异会变得更加明显。Webpack开发服务器在构建/重新构建时可能会慢到25-30秒,有时甚至更慢。与此同时,Vite开发服务器可能会以恒定的250ms的速度为同一个项目提供服务。

这显然是开发经验和游戏规则改变的差异,Vite是如何做到这一点的?

Webpack开发服务器架构

Webpack的工作方式是,它通过解析应用程序中的每一个 importrequire ,将整个应用程序构建成一个基于JavaScript的捆绑包,并在运行时转换文件(例如Sass、TypeScript、SFC)。

这都是在服务器端完成的,依赖的数量和改变后构建/重新构建的时间之间有一个大致的线性关系。

Vite开发服务器架构

Vite不捆绑应用服务器端。相反,它依赖于浏览器对JavaScript模块的原生支持(也就是ES模块,是一个比较新的功能)。

浏览器将在需要时通过HTTP请求任何JS模块,并在运行时进行处理。Vite开发服务器将按需转换任何文件(如Sass、TypeScript、SFC)。

这种架构避免了服务器端对整个应用的捆绑,并利用浏览器高效的模块处理,提供了一个明显更快的开发服务器。

提示:当你对应用程序进行code-split和tree-shake动时,Vite的速度会更快,因为它只加载它需要的模块,即使是在开发阶段。这与Webpack不同,在Webpack中,代码拆分只对生产包有利。

Vite的缺点

你可能已经明白了,Vite的主要特点是它的开发服务器快得离谱。

如果没有这个功能,可能就不会再讨论了,因为与Vue CLI相比,它确实没有其他的功能,而且确实有一些缺点。

由于Vite使用了JavaScript模块,所以最好让依赖关系也使用JavaScript模块。虽然大多数现代JS包都提供了这一点,但一些老的包可能只提供CommonJS模块。

Vite可以将CommonJS转换为JavaSript模块,但在一些边缘情况下它可能无法做到。当然,它还需要支持JavaScript模块的浏览器。

与Webpack/Vue CLI不同,Vite无法创建针对旧版浏览器、web components等的捆绑包。

而且,与Vue CLI不同,开发服务器和构建工具是不同的系统,导致在生产与开发中可能出现不一致的行为。

Vue CLI vs Vite总结

Vue CLI 优点Vue CLI 缺点
经历过战斗考验,可靠开发服务器速度与依赖数量成反比
与Vue 2兼容
可以捆绑任何类型的依赖关系
插件生态系统
可以针对不同的目标进行构建
Vite 优点Vite 缺点
开发服务器比Webpack快10-100倍只能针对现代浏览器(ES2015+)
将code-splitting作为优先事项与CommonJS模块不完全兼容
处于测试阶段,仅支持Vue 3
最小的脚手架不包括Vuex、路由器等
不同的开发服务器与构建工具

那么判决结果是什么?

对于有经验的Vue开发来说,Vite是一个很好的选择,因为它的开发服务器速度快得离谱,让Webpack看起来像史前时代。

但是,对于喜欢一些手把手的Vue新开发人员来说,或者,对于使用遗留模块和需要复杂构建的大型项目来说,Vue CLI很可能在目前仍然是必不可少的。

Vite的未来

虽然上面的比较主要集中在Vite和Vue CLI的现状上,但仍有几点需要考虑:

  • 仅当浏览器中的JavaScript模块支持得到改善时,Vite才会有所改善。
  • 随着JS生态系统的追赶,更多的软件包将支持JavaScript模块,减少Vite无法处理的边缘情况。
  • Vite仍处于测试阶段--功能可能会有变化。
  • 有可能Vue CLI最终会结合Vite,这样你就不用再使用其中一个了。
值得注意的是,Vite并不是唯一一个利用浏览器中JavaScript模块的开发服务器项目。还有更著名的Snowpack,甚至可能会挤掉Vite的发展。时间会证明这一点

参考


原文:https://vuejsdevelopers.com/2...
作者:Anthony Gore

查看原文

赞 2 收藏 1 评论 0

杜尼卜 发布了文章 · 2020-12-18

MongoDB + Mongoose与Node.js结合使用的后端开发的最佳实践

image

MongoDB无疑是当今最受欢迎的NoSQL数据库选择之一,它有一个很棒的社区和生态系统。

在本文中,我们将介绍在使用Node.js设置MongoDB和Mongoose时应遵循的一些最佳实践。

1.为什么需要Mongoose?

为了理解我们为什么需要Mongoose,我们先来了解MongoDB(也是一个数据库)在架构层面的工作原理。

  • 你有一个数据库服务器(例如MongoDB社区服务器)
  • 你正在运行一个Node.js脚本(作为一个进程)

MongoDB服务器在TCP套接字上监听(通常),你的Node.js进程可以使用TCP连接来连接它。

但是在TCP之上,MongoDB也有自己的协议来了解客户端(我们的Node.js进程)到底想要数据库做什么。

对于这种通信,我们不需要学习我们在TCP层上要发送的消息,而是借助一个“驱动”软件将其抽象掉,在这里称为MongoDB驱动。MongoDB驱动在这里以npm包的形式提供。

现在请记住,MongoDB 驱动程序负责连接和抽象你的低层通信请求/响应——但作为开发者,这只能让你走到这一步。

因为MongoDB是一个无模式数据库,它为你提供了比初学者所需的更多的功能。更大的权利意味着更大的出错可能性,你需要减少可在代码中制作的错误和破坏表的可能性。

Mongoose是对原生MongoDB驱动(我上面提到的npm包)的一个抽象。

抽象化的一般经验法则(我理解的方式)是,每一个抽象化都会损失一些底层操作能力。但这并不一定意味着它是坏的,有时它能提高生产力1000倍以上,因为你根本不需要完全访问底层API。

一个好的思路是,你在技术上用C语言和Python创建一个实时聊天应用。作为开发人员,Python的例子将更容易和更快地实现,生产率更高。C可能更有效率,但它会在生产力、开发速度、错误、崩溃方面付出巨大的代价。另外,在大多数情况下,你不需要拥有C给你的能力来实现websockets。

同样,使用Mongoose,你可以限制你的底层API访问的范围,但可以释放出很多潜在的收益和良好的DX。

2.如何连接Mongoose + MongoDB

首先,让我们快速看看在2020年应该如何用Mongoose连接到你的MongoDB数据库。

mongoose.connect(DB_CONNECTION_STRING, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    useCreateIndex: true,
    useFindAndModify: false
})

这种连接格式确保你正在使用Mongoose的新URL解析器,而且你没有使用任何废弃的做法。

3.如何执行Mongoose操作

现在我们先来快速讨论一下Mongoose的操作,以及你应该如何执行这些操作。

Mongoose为您提供以下两种选择:

  1. 基于游标的(Cursor-based)查询
  2. 全程检索(Full fetching)查询

3.1 基于游标的(Cursor-based)查询

基于游标的查询是指一次只处理一条记录,同时一次从数据库中获取一条或一批文档。这是一种在有限的内存环境下处理海量数据的有效方式。

想象一下,你要在1GB/1核云服务器上解析总大小为10GB的文档。你不能获取整个集合,因为那在你的系统中不合适。游标是一个很好的(也是唯一的)选择。

3.2 全程检索(Full fetching)查询

这是一种查询类型,你可以一次性获得查询的全部响应。在大多数情况下,这就是你要使用的方法。因此,我们将在这里主要关注这个方法。

4.如何使用Mongoose模型(Models)

模型是Mongoose的超级力量,它们帮助你执行“schema”规则,并为你的Node代码提供无缝集成到数据库调用中。

第一步是定义一个好的模型:

import mongoose from 'mongoose'

const CompletedSchema = new mongoose.Schema(
    {
        type: { type: String, enum: ['course', 'classroom'], required: true },
        parentslug: { type: String, required: true },
        slug: { type: String, required: true },
        userid: { type: String, required: true }
    },
    { collection: 'completed' }
)

CompletedSchema.index({ slug: 1, userid: 1 }, { unique: true })

const model = mongoose.model('Completed', CompletedSchema)
export default model

这是一个直接从codedamn的代码库中删减的例子。这里有一些有趣的事情你应该注意。

  1. 在所有需要的字段中,尽量保持 required: true 。如果你在创建对象时没有使用TypeScript这样的静态类型检查系统来协助你正确地检查属性名,那么这可以为你省去很多麻烦。另外,免费的验证也超级酷。
  2. 定义索引和唯一字段。 unique 属性也可以在模式中添加。索引是一个很广泛的话题,所以我在这里就不深究了。但从大范围来看,它们确实可以帮助你加快很多查询速度。
  3. 明确定义一个集合名。虽然Mongoose可以根据模型的名称自动给出一个集合名称(例如这里的 Completed here),但在我看来这太抽象了。你至少应该知道你的数据库名称和代码库中的集合。
  4. 如果可以,请使用枚举来限制值。

5.如何执行CRUD操作

CRUD是指创建、读取、更新和删除。这是四个基本选项,有了这四个选项,你就可以在数据库中进行任何形式的数据操作。让我们快速看看这些操作的一些例子。

5.1 Create

这简单来说就是在数据库中创建一条新记录。让我们使用我们上面定义的模型来创建一条记录。

try {
    const res = await CompletedSchema.create(record)
} catch(error) {
    console.error(error)
    // handle the error
}

同样,这里有一些提示:

  1. 使用async-await而不是回调(看起来不错,但没有突破性的性能优势)
  2. 在查询周围使用try-catch块,因为你的查询可能会因为一些原因而失败(重复记录、错误的值等)。

5.2 Read

这意味着从数据库中读取现有的值。这听起来很简单,但你应该知道Mongoose的几个小问题。

const res = await CompletedSchema.find(info).lean()
  1. 你能看到那里的 lean() 函数调用吗?它对性能超级有用。默认情况下,Mongoose会处理从数据库中返回的文档,并在其上添加其神奇的方法(例如 .save)。
  2. 当你使用 .lean() 时,Mongoose会返回普通的JSON对象,而不是内存和资源沉重的文档。使查询速度更快,对CPU的消耗也更小。
  3. 然而,如果你确实想要更新数据,你可以省略 .lean() (我们接下来会看到)

5.3 Update

如果你已经有了一个Mongoose文档(没有使用 .lean() 触发),你可以简单地去修改对象属性,然后用 object.save() 保存它。

const doc = await CompletedSchema.findOne(info)
doc.slug = 'something-else'
await doc.save()

记住,这里有两个数据库调用。第一个是在 findOne上,第二个是在 doc.save 上。

如果可以,你应该总是减少访问数据库的请求数量(因为如果比较内存、网络和磁盘,网络几乎总是最慢的)。

在另一种情况下,可以使用如下查询:

const res = await CompletedSchema.updateOne(<condition>, <query>).lean()

并且只会对数据库进行一次调用。

5.4 Delete

使用Mongoose删除也很简单,让我们看看如何删除单个文档:

const res = await CompletedSchema.deleteOne(<condition>)

updateOne 一样 deleteOne 也接受第一个参数作为文档的匹配条件。

还有另一个方法叫 deleteMany,只有当你知道要删除多个文档时才使用。

在任何其他情况下,总是使用 deleteOne 来避免意外的多次删除,特别是当你试图自己执行查询时。

查看原文

赞 11 收藏 8 评论 0

认证与成就

  • 认证信息 前端工程师
  • 获得 2373 次点赞
  • 获得 14 枚徽章 获得 0 枚金徽章, 获得 3 枚银徽章, 获得 11 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-03-20
个人主页被 21.4k 人浏览