广工小成

广工小成 查看完整档案

广州编辑广东工业大学  |  软件工程 编辑某大厂  |  前端开发 编辑 xiaocheng123.github.io/ 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

广工小成 关注了用户 · 1月13日

木易杨 @muyiyang_5bd9ae9ac7993

我是木易杨,蚂蚁高级前端工程师,跟着我每周重点攻克一个面试重难点。

关注 212

广工小成 发布了文章 · 1月8日

useEffect引起的React Hooks深入了解

前言

参考文章 react源码useEffect 完整指南呕心沥血,一文看懂 react hooksreact官网

在进入正式阅读之前,最好先思考一下下面的问题:

  1. React Hooks真的有生命周期吗?
  2. React Hooks的函数里面定义的函数或者变量会被缓存吗,这样下次再调用组件的时候就可以不用重新声明了。
  3. 为什么我的useEffect有时候拿到了之前的值。

React Hooks函数式渲染

我非常喜欢参考文章里面说的,如果你想要学好React Hooks,那么你摒弃掉之前组件的想法可能会更加好

为什么我会这么讲?

React Hooks会璞归真,其实就是将我们之前封装好的组件对象重新变回来了我们原始的代码模式。让你只要考虑执行过程中的栈,堆和队列

函数式渲染与生命周期的关系

在React Hooks里面,当我们声明一个组件时

import React from 'react';

function App() {
  return (
    <div className="App">
      我是React Hooks
    </div>
  );
}

export default App;

我们可以很明显的看到,这就是一个函数嘛,只是加入JSX的写法,返回了组件而已。是的,你没有理解错,这应该也是React Hooks的创始人的想法,不比Vue,虽然轻量,但是封装好了一切,导致可能前端就是学习框架去了~

但是,React不也是组件化的框架吗?是的,当代码增多

import React, { useState, useEffect } from 'react';

function App() {
  const [name, setName] = useState('hello');

  useEffect(() => {
    console.log(name)
  })

  return (
    <div className="App" onClick={() => {setName(name + 'world')}}>
      我是{name}
    </div>
  );
}

export default App;

让我们来大胆的猜想一下,当这段代码运行完了之后,页面会发生什么?

// 第一次渲染
-----函数开始-----
useState定义了一个为'name'的state
useEffect定义了一个函数,他没有任何的依赖项
返回一个JSX显示到我们页面上
页面加载成功,发现我们有个useEffect,发现他不依赖任何属性,他就要运行,于是他从此次函数中拿到name-->打印hello
-----函数结束-----

// 当点击我们的文本
点击事件回调修改我们的state,告诉我们相应的组件,你需要更新了
-----函数开始-----
从我们的state中拿出修改后的'name',这时候name为helloworld
第二次渲染了,useEffect已经被声明过了,不理他了
返回一个JSX显示到我们页面上
页面加载成功,发现我们有个useEffect,发现他不依赖任何属性,他就要运行,于是他从此次函数中拿到name-->打印helloworld
-----函数结束-----

想必,看完了这段例子之后,你对React Hooks也有了一定的理解了。其实就是使用useState去存储我们需要存储的数据,当他更新的时候刷新页面,当页面刷新的时候,我们再使用useEffect来进行我们需要的操作。

这样看的话,useEffect岂不就是相当于我们之前的componentDidMount + componentDidUpdate

我可以很负责任的告诉你,不是的,useEffect是我们更新页面的副作用,当我们对他加上了依赖项之后,他就会在页面加载完了之后,检查依赖项是否有变化来进行决定是否要运行自己,例如:

import React, { useState, useEffect } from 'react';

function App() {
  const [name, setName] = useState('hello');
  const [myName, setMyName] = useState('my Hello')

  useEffect(() => {
    console.log('我是第一个副作用' + name)
  })

  useEffect(() => {
    console.log('我是第二个副作用' + name)
  }, [])
  
  useEffect(() => {
    console.log('我是第三个副作用' + name)
  }, [myName])

  useEffect(() => {
    console.log('我是第四个副作用' + name)
  }, [name])

  return (
    <div className="App" onClick={() => {setName(name + 'world')}}>
      我是{name}
    </div>
  );
}

export default App;

这时候我们执行完了之后和点击文本之后会发生什么呢?

// 第一次渲染
-----函数开始-----
...(省略相同步骤)
页面加载完毕
发现没有依赖项,打印:我是第一个副作用hello
发现是首次渲染,打印:我是第二个副作用hello
发现是首次渲染,打印:我是第三个副作用hello
发现是首次渲染,打印:我是第四个副作用hello
-----函数开始-----

// 点击我们的文本
更新state
-----函数开始-----
...(省略相同步骤)
发现没有依赖项,打印:我是第一个副作用helloworld
发现依赖项还是空,与上次相同,不打印
发现myName没有更新,与上次相同,不打印
发现name更新,打印:我是第四个副作用helloworld
-----函数结束-----

详情可参考例子:https://codesandbox.io/s/reac...

函数式渲染的特点

上面的例子可能还没能够理解为什么是函数式渲染,接下来这一次你可能就能意会到,而且是开发中可能经常出现的问题,看下面的例子

function Counter() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
      <button onClick={handleAlertClick}>
        Show alert
      </button>
    </div>
  );
}

在该例子中,我们点击了Show alert之后,再去点击Click me,发现弹出来的是了Show alert时候的count,详情可参考例子:https://codesandbox.io/s/pric...

为什么会出现这样的情况呢

因为Counter是一个函数,在点击Show alert,此时的count为当前值,在我们每次点击Click me的时候Counter函数都会运行一次。这样是不是就能理解了呢

附录:

  1. 一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留,所以定义的函数会被清除
  2. React hook更新 state 变量总是替换它而不是合并它。跟class不一样
  3. useEffect只有一个参数的时候=componentDidMount+componentDidUpdate
  4. useEffect它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制
  5. return一个函数,运行就会清理,因为 useEffect 默认就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理
  6. 在条件语句违反Hook的规则原因如下
// 🔴 在条件语句中使用 Hook 违反第一条规则
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }
  
// 第二次调用
useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm)  // 🔴 此 Hook 被忽略!
useState('Poppins')        // 🔴 2 (之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle)     // 🔴 3 (之前为 4)。替换更新标题的 effect 失败
查看原文

赞 8 收藏 5 评论 4

广工小成 发布了文章 · 1月8日

极为细致的Vue的Diff流程详解——以流程图表达

网上看了一些diff的算法,但是感觉看完之后,还是那么的一知半解,为什么一个简单的diff算法,不能直接画个流程图就简单的明了了呢,说动就动,下面的是本人基于vue版本2.6.11源码为各位读友进行的解析

Vue的diff流程图

流程前说明

  1. 由于diff的过程是对vnode(虚拟dom)树进行层级比较,所以以同一层级作为例子
  2. 下面将旧节点列表的起始和终止节点称为OS(OldStarVnode)和OE(OldEndVnode),用index标志遍历过程OS和OE的变化。即OS和OE的index称为OSIndex和OEIndex。同理得新节点的为NS和NE,NSIndex和NEIndex,如下图

image

主流程

如下图:

image

文字版描述一下就是:

  1. 判断是否遍历完,未遍历则开始2,否则,如果遍历完了旧节点列表,则未遍历的新节点则创建并且增加到节点列表,如果遍历完了新节点列表,则未遍历的旧节点在节点列表里面删除
  2. 对旧节点的OS和OE进行判空,如果为空,则跳过该节点,继续从1开始;否则继续3
  3. 对OS,OE,NS,NE进行两两比较,如果相等,则更新节点并且指针向下一个移动,继续从1开始;否则继续4
  4. 判断NS是否有key,有key则判断NS是否在旧节点列表里面找到key一样的进行更新;否则创建NS并且插入节点列表

updateChildren进行diff算法源码

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }

附,源码中部分工具函数的解释:

isUndef 对节点进行判空

function isUndef (v) {
  return v === undefined || v === null
}

sameVnode对节点进行判断是否相等

  1. 判断新旧节点的key
  2. 判断新旧节点的属性(tag,isComment表示是否是注释节点,isDef表示是否为非空节点,sameInputType表示是否同个Input节点)是否一致
  3. 判断新旧节点的加载函数asyncFactory是否一致
function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

patchVnode更新节点

patchVnode更新节点主要做以下事情,代码比较长就不贴了,影响读者,需要可以直接阅读源码:

  1. 判断vnode和oldvnode是否相等,相等直接返回
  2. 处理静态节点的情况
  3. 对vnode如果是可patch的情形进行调用update
  4. 对vnode进行判断是否是根节点(即文本节点),如果是,则进行5,否则则对其子节点进行遍历更新
  5. 判断vnode和oldvnode文本是否一样: 不一样则替换节点文本
查看原文

赞 12 收藏 7 评论 3

广工小成 关注了用户 · 2020-12-01

everlastlucas @everlastlucas

关注 1

广工小成 发布了文章 · 2020-11-18

极其有用的DOM API——MutationObserver监听节点变化详解

今天来介绍一个很有用的DOM API——MutationObserver

使用背景

页面或者某个父类DOM需要监听子节点的变化,来进行统一回调,这个变化包括了:

  1. 特定属性名称的变化,例如class等
  2. 属性的变化
  3. 整个DOM树中子节点的变化

MutationObserver介绍

这里采用了MDN的官方介绍,MutationObserver接口提供了监视对DOM树所做更改的能力。它被设计为旧的Mutation Events功能的替代品,该功能是DOM3 Events规范的一部分。

MutationObserver主要包括了三个方法,具体可结合下面示例:

  • disconnect()——阻止 MutationObserver 实例继续接收的通知
  • observe()——配置MutationObserver在DOM更改匹配给定选项
  • takeRecords()——从MutationObserver的通知队列中删除所有待处理的通知

实例&示例

通过MutationObserver()即可构造一个实例,下面为使用示例

// 选择需要观察变动的节点
const targetNode = document.getElementById('some-id');

// 观察器的配置(需要观察什么变动)
const config = {
    attributes: true, // 开启监听属性
    childList: true, // 开启监听子节点
    subtree: true // 开启监听子节点下面的所有节点
};

// 当观察到变动时执行的回调函数
const callback = function(mutationsList, observer) {
    // Use traditional 'for loops' for IE 11
    for(let mutation of mutationsList) {
        if (mutation.type === 'childList') {
            console.log('A child node has been added or removed.');
        }
        else if (mutation.type === 'attributes') {
            console.log('The ' + mutation.attributeName + ' attribute was modified.');
        }
    }
};

// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);

// 以上述配置开始观察目标节点
observer.observe(targetNode, config);

// 之后,可停止观察
observer.disconnect();

config的值介绍

上文的config为一个MutationObserverInit字典,描述了MutationObserver的配置,我们有以下可选属性进行配置(没有必选属性)

属性介绍
attributeFilter要监视的特定属性名称的数组。如果未包含此属性,则对所有属性的更改都会触发变动通知。无默认值。
attributeOldValue当监视节点的属性改动时,将此属性设为 true 将记录任何有改动的属性的上一个值。无默认值。
attributes设为 true 以观察受监视元素的属性值变更。默认值为 false。
characterData设为 true 以监视指定目标节点或子节点树中节点所包含的字符数据的变化。无默认值。
characterDataOldValue设为 true 以在文本在受监视节点上发生更改时记录节点文本的先前值。无默认值。
childList设为 true 以监视目标节点(如果 subtree 为 true,则包含子孙节点)添加或删除新的子节点。默认值为 false。
subtree设为 true 以将监视范围扩展至目标节点整个节点树中的所有节点。MutationObserverInit 的其他值也会作用于此子树下的所有节点,而不仅仅只作用于目标节点。默认值为 false。

callback介绍

callback
一个回调函数,每当被指定的节点或子树以及配置项有Dom变动时会被调用。回调函数拥有两个参数:一个是描述所有被触发改动的 MutationRecord 对象数组,另一个是调用该函数的MutationObserver 对象。

参考文章:

https://developer.mozilla.org...

附API的适配性:

image

查看原文

赞 7 收藏 3 评论 0

广工小成 收藏了文章 · 2020-11-16

实现本地跨域存储

什么是跨域?

先看一下 URL 有哪些部分组成,如下:

https://github.com:80/gauseen/blog?issues=1#note
\___/  \________/ \_/ \_________/ \______/ \___/
  |         |      |       |          |      |
protocol   host   port  pathname    search  hash

protocol(协议)、host(域名)、port(端口)有一个地方不同都会产生跨域现象,也被称为客户端同源策略

本地存储受同源策略限制

客户端(浏览器)出于安全性考虑,无论是 localStorage 还是 sessionStorage 都会受到同源策略限制。

那么如何实现跨域存储呢?

window.postMessage()

想要实现跨域存储,先找到一种可跨域通信的机制,没错,就是 postMessage,它可以安全的实现跨域通信,不受同源策略限制。

语法:

otherWindow.postMessage('message', targetOrigin, [transfer])
  • otherWindow 窗口的一个引用,如:iframecontentWindow 属性,当前 window 对象,window.open 返回的窗口对象等
  • message 将要发送到 otherWindow 的数据
  • targetOrigin 通过窗口的 targetOrigin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串 "*"(表示无限制)

实现思路

postMessage 可跨域特性,来实现跨域存储。因为多个不同域下的页面无法共享本地存储数据,我们需要找个“中转页面”来统一处理其它页面的存储数据。为了方便理解,画了张时序图,如下:

跨域存储时序图

场景模拟

需求:

有两个不同的域名(http://localhost:6001http://localhost:6002)想共用本地存储中的同一个 token

假设:

http://localhost:6001 对应 client1.html 页面
http://localhost:6002 对应 client2.html 页面
http://localhost:6003 对应 hub.html 中转页面

启动服务:

使用 http-server 启动 3 个本地服务

npm -g install http-server

# 启动 3 个不同端口的服务,模拟跨域现象
http-server -p 6001
http-server -p 6002
http-server -p 6003

简单实现版本

client1.html 页面代码

<body>
  <!-- 开始存储事件 -->
  <button onclick="handleSetItem()">client1-setItem</button>
  <!-- iframe 嵌套“中转页面” hub.html -->
  <iframe data-original="http://localhost:6003/hub.html" frameborder="0" id="hub"></iframe>

  <script>
    const $ = id => document.querySelector(id)
    // 获取 iframe window 对象
    const ifameWin = $('#hub').contentWindow

    let count = 0
    function handleSetItem () {
      let request = {
        // 存储的方法
        method: 'setItem',
        // 存储的 key
        key: 'someKey',
        // 需要存储的数据值
        value: `来自 client-1 消息:${count++}`,
      }
      // 向 iframe “中转页面”发送消息
      ifameWin.postMessage(request, '*')
    }
  </script>
</body>

hub.html 中转页面代码

<body>
  <script>
    // 映射关系
    let map = {
      setItem: (key, value) => window.localStorage['setItem'](key, value),
      getItem: (key) => window.localStorage['getItem'](key),
    }

    // “中转页面”监听 ifameWin.postMessage() 事件
    window.addEventListener('message', function (e) {
      let { method, key, value } = e.data
      // 处理对应的存储方法
      let result = map[method](key, value)
      // 返回给当前 client 的数据
      let response = {
        result,
      }
      // 把获取的数据,传递给 client 窗口
      window.parent.postMessage(response, '*')
    })
  </script>
</body>

client2.html 页面代码

<body>
  <!-- 获取本地存储数据 -->
  <button onclick="handleGetItem()">client2-getItem</button>
  <!-- iframe 嵌套“中转页面” hub.html -->
  <iframe data-original="http://localhost:6003/hub.html" frameborder="0" id="hub"></iframe>

  <script>
    const $ = id => document.querySelector(id)
    // 获取 iframe window 对象
    const ifameWin = $('#hub').contentWindow

    function handleGetItem () {
      let request = {
        // 存储的方法(获取)
        method: 'getItem',
        // 获取的 key
        key: 'someKey',
      }
      // 向 iframe “中转页面”发送消息
      ifameWin.postMessage(request, '*')
    }

    // 监听 iframe “中转页面”返回的消息
    window.addEventListener('message', function (e) {
      console.log('client 2 获取到数据啦:', e.data)
    })
  </script>
</body>

浏览器打开如下地址:

具体效果如下:

跨域存储 Demo 效果演示

改进版本

分成 2 个 js 文件,一个是客户端页面使用 client.js,另一个是中转页面使用 hub.js

// client.js

class Client {
  constructor (hubUrl) {
    this.hubUrl = hubUrl
    // 所有请求的 id 值(累加)
    this.id = 0
    // 所有请求消息映射
    this._requests = {}
    // 获取 iframe window 对象
    this._iframeWin = this._createIframe(this.hubUrl).contentWindow
    this._initListener()
  }
  //
  getItem (key, callback) {
    this._requestFn('getItem', {
      key,
      callback,
    })
  }
  setItem (key, value, callback) {
    this._requestFn('setItem', {
      key,
      value,
      callback,
    })
  }
  _requestFn (method, { key, value, callback }) {
    // 发消息时,请求对象格式
    let req = {
      id: this.id++,
      method,
      key,
      value,
    }
    // 请求 id 和回调函数的映射
    this._requests[req.id] = callback
    // 向 iframe “中转页面”发送消息
    this._iframeWin.postMessage(req, '*')
  }
  // 初始化监听函数
  _initListener () {
    // 监听 iframe “中转页面”返回的消息
    window.addEventListener('message', (e) => {
      let { id, result } = e.data
      // 找到“中转页面”的消息对应的回调函数
      let currentCallback = this._requests[id]
      if (!currentCallback) return
      // 调用并返回数据
      currentCallback(result)
    })
  }
  // 创建 iframe 标签
  _createIframe (hubUrl) {
    const iframe = document.createElement('iframe')
    iframe.src = hubUrl
    iframe.style = 'display: none;'
    window.document.body.appendChild(iframe)
    return iframe
  }
}
// hub.js

class Hub {
  constructor () {
    this._initListener()
    this.map = {
      setItem: (key, value) => window.localStorage['setItem'](key, value),
      getItem: (key) => window.localStorage['getItem'](key),
    }
  }
  // 监听 client ifameWin.postMessage() 事件
  _initListener () {
    window.addEventListener('message', (e) => {
      let { method, key, value, id } = e.data
      // 处理对应的存储方法
      let result = this.map[method](key, value)
      // 返回给当前 client 的数据
      let response = {
        id,
        result,
      }
      // 把获取的数据,发送给 client 窗口
      window.parent.postMessage(response, '*')
    })
  }
}

页面使用:

<!-- client1 页面代码 -->

<body>
  <button onclick="handleGetItem()">client1-GetItem</button>
  <button onclick="handleSetItem()">client1-SetItem</button>

  <script data-original="./lib/client.js"></script>
  <script>
    const crossStorage = new Client('http://localhost:6003/hub.html')
    // 在 client1 中,获取 client2 存储的数据
    function handleGetItem () {
      crossStorage.getItem('client2Key', (result) => {
        console.log('client-1 getItem result: ', result)
      })
    }

    // client1 本地存储
    function handleSetItem () {
      crossStorage.setItem('client1Key', 'client-1 value', (result) => {
        console.log('client-1 完成本地存储')
      })
    }
  </script>
</body>
<!-- hub 页面代码 -->

<body>
  <script data-original="./lib/hub.js"></script>
  <script>
    const hub = new Hub()
  </script>
</body>
<!-- client2 页面代码 -->

<body>
  <button onclick="handleGetItem()">client2-GetItem</button>
  <button onclick="handleSetItem()">client2-SetItem</button>

  <script data-original="./lib/client.js"></script>
  <script>
    const crossStorage = new Client('http://localhost:6003/hub.html')
    // 在 client2 中,获取 client1 存储的数据
    function handleGetItem () {
      crossStorage.getItem('client1Key', (result) => {
       console.log('client-2 getItem result: ', result)
      })
    }
    // client2 本地存储
    function handleSetItem () {
      crossStorage.setItem('client2Key', 'client-2 value', (result) => {
        console.log('client-2 完成本地存储')
      })
    }
  </script>
</body>

总结

以上就实现了跨域存储,也是 cross-storage 开源库的原理。
通过 window.postMessage() api 跨域特性,再配合一个 “中转页面”,来完成所谓的“跨域存储”,实际上并没有真正的在浏览器端实现跨域存储,
这是浏览器的限制,我们无法打破,只能用“曲线救国”的方式,变向来共享存储数据。

所有源码在这里:跨域存储源码

欢迎关注无广告文章、无广告文章、无广告文章公众号:学前端

你的关注、点赞、star 是我最大的动力!谢谢!

参考

查看原文

广工小成 收藏了文章 · 2020-11-14

一名【合格】前端工程师的自检清单

开篇

前端开发是一个非常特殊的行业,它的历史实际上不是很长,但是知识之繁杂,技术迭代速度之快是其他技术所不能比拟的。

winter在他的《重学前端》课程中提到:

到现在为止,前端工程师已经成为研发体系中的重要岗位之一。可是,与此相对的是,我发现极少或者几乎没有大学的计算机专业愿意开设前端课程,更没有系统性的教学方案出现。大部分前端工程师的知识,其实都是来自于实践和工作中零散的学习。

这样是一个非常真实的现状,实际上很多前端开发者都是自学甚至转行过来的,前端入门简单,学习了几个API以后上手做项目也很简单,但是这往往成为了限制自身发展的瓶颈。

只是停留在会用阶段是远远不够的,我们还需要不断探索和深入。现在市面上并不缺少学习教程,技术文章,如果盲目的学习你会发现看过以后的知识留存率会很低,而且发现没有了解到的知识越来越多,这会让人产生焦虑。

实际上,除了坚持学习的强大的自驱力,你还需要一个很简单的学习方法。那就是:建立自己的知识体系。它能帮助你更系统性的学习,同时你也时刻能知道自己哪些地方是不足的。

我会把我工作和学习中接触到的知识全部归纳到我的知识体系中,其中不仅仅包括我已经学过的,还有很多我没有来得及学习的。

这不仅仅是我的知识体系,更是我时刻提醒自己的自检清单。

下面我会把我的自检清单分享给大家,你可以按照清单上的知识检测自己还有哪些不足和提升,我也建议大家建自己的知识体系,这样工作或者学习甚至面试时,你能快速定位到知识清单中的点,如果你有哪些我没归纳到的点,欢迎在评论区告诉我。

一、JavaScript基础

前端工程师吃饭的家伙,深度、广度一样都不能差。

变量和类型

  • 1.JavaScript规定了几种语言类型
  • 2.JavaScript对象的底层数据结构是什么
  • 3.Symbol类型在实际开发中的应用、可手动实现一个简单的Symbol
  • 4.JavaScript中的变量在内存中的具体存储形式
  • 5.基本类型对应的内置对象,以及他们之间的装箱拆箱操作
  • 6.理解值类型和引用类型
  • 7.nullundefined的区别
  • 8.至少可以说出三种判断JavaScript数据类型的方式,以及他们的优缺点,如何准确的判断数组类型
  • 9.可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用
  • 10.出现小数精度丢失的原因,JavaScript可以存储的最大数字、最大安全数字,JavaScript处理大数字的方法、避免精度丢失的方法

原型和原型链

  • 1.理解原型设计模式以及JavaScript中的原型规则
  • 2.instanceof的底层实现原理,手动实现一个instanceof
  • 4.实现继承的几种方式以及他们的优缺点
  • 5.至少说出一种开源项目(如Node)中应用原型继承的案例
  • 6.可以描述new一个对象的详细过程,手动实现一个new操作符
  • 7.理解es6 class构造以及继承的底层实现原理

作用域和闭包

  • 1.理解词法作用域和动态作用域
  • 2.理解JavaScript的作用域和作用域链
  • 3.理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题
  • 4.this的原理以及几种不同使用场景的取值
  • 5.闭包的实现原理和作用,可以列举几个开发中闭包的实际应用
  • 6.理解堆栈溢出和内存泄漏的原理,如何防止
  • 7.如何处理循环的异步操作
  • 8.理解模块化解决的实际问题,可列举几个模块化方案并理解其中原理

执行机制

  • 1.为何try里面放returnfinally还会执行,理解其内部机制
  • 2.JavaScript如何实现异步编程,可以详细描述EventLoop机制
  • 3.宏任务和微任务分别有哪些
  • 4.可以快速分析一个复杂的异步嵌套逻辑,并掌握分析方法
  • 5.使用Promise实现串行
  • 6.Node与浏览器EventLoop的差异
  • 7.如何在保证页面运行流畅的情况下处理海量数据

语法和API

  • 1.理解ECMAScriptJavaScript的关系
  • 2.熟练运用es5es6提供的语法规范,
  • 3.熟练掌握JavaScript提供的全局对象(例如DateMath)、全局函数(例如decodeURIisNaN)、全局属性(例如Infinityundefined
  • 4.熟练应用mapreducefilter 等高阶函数解决问题
  • 5.setInterval需要注意的点,使用settimeout实现setInterval
  • 6.JavaScript提供的正则表达式API、可以使用正则表达式(邮箱校验、URL解析、去重等)解决常见问题
  • 7.JavaScript异常处理的方式,统一的异常处理方案

二、HTML和CSS

HTML

  • 1.从规范的角度理解HTML,从分类和语义的角度使用标签
  • 2.常用页面标签的默认样式、自带属性、不同浏览器的差异、处理浏览器兼容问题的方式
  • 3.元信息类标签(headtitlemeta)的使用目的和配置方法
  • 4.HTML5离线缓存原理
  • 5.可以使用Canvas APISVG等绘制高性能的动画

CSS

  • 1.CSS盒模型,在不同浏览器的差异
  • 2.CSS所有选择器及其优先级、使用场景,哪些可以继承,如何运用at规则
  • 3.CSS伪类和伪元素有哪些,它们的区别和实际应用
  • 4.HTML文档流的排版规则,CSS几种定位的规则、定位参照物、对文档流的影响,如何选择最好的定位方式,雪碧图实现原理
  • 5.水平垂直居中的方案、可以实现6种以上并对比它们的优缺点
  • 6.BFC实现原理,可以解决的问题,如何创建BFC
  • 7.可使用CSS函数复用代码,实现特殊效果
  • 8.PostCSSSassLess的异同,以及使用配置,至少掌握一种
  • 9.CSS模块化方案、如何配置按需加载、如何防止CSS阻塞渲染
  • 10.熟练使用CSS实现常见动画,如渐变、移动、旋转、缩放等等
  • 11.CSS浏览器兼容性写法,了解不同API在不同浏览器下的兼容性情况
  • 12.掌握一套完整的响应式布局方案

手写

  • 1.手写图片瀑布流效果
  • 2.使用CSS绘制几何图形(圆形、三角形、扇形、菱形等)
  • 3.使用纯CSS实现曲线运动(贝塞尔曲线)
  • 4.实现常用布局(三栏、圣杯、双飞翼、吸顶),可是说出多种方式并理解其优缺点

三、计算机基础

关于编译原理,不需要理解非常深入,但是最基本的原理和概念一定要懂,这对于学习一门编程语言非常重要

编译原理

  • 1.理解代码到底是什么,计算机如何将代码转换为可以运行的目标程序
  • 2.正则表达式的匹配原理和性能优化
  • 3.如何将JavaScript代码解析成抽象语法树(AST)
  • 4.base64的编码原理
  • 5.几种进制的相互转换计算方法,在JavaScript中如何表示和转换

网络协议

  • 1.理解什么是协议,了解TCP/IP网络协议族的构成,每层协议在应用程序中发挥的作用
  • 2.三次握手和四次挥手详细原理,为什么要使用这种机制
  • 3.有哪些协议是可靠,TCP有哪些手段保证可靠交付
  • 4.DNS的作用、DNS解析的详细过程,DNS优化原理
  • 5.CDN的作用和原理
  • 6.HTTP请求报文和响应报文的具体组成,能理解常见请求头的含义,有几种请求方式,区别是什么
  • 7.HTTP所有状态码的具体含义,看到异常状态码能快速定位问题
  • 8.HTTP1.1HTTP2.0带来的改变
  • 9.HTTPS的加密原理,如何开启HTTPS,如何劫持HTTPS请求
  • 10.理解WebSocket协议的底层原理、与HTTP的区别

设计模式

  • 1.熟练使用前端常用的设计模式编写代码,如单例模式、装饰器模式、代理模式等
  • 2.发布订阅模式和观察者模式的异同以及实际应用
  • 3.可以说出几种设计模式在开发中的实际应用,理解框架源码中对设计模式的应用

四、数据结构和算法

据我了解的大部分前端对这部分知识有些欠缺,甚至抵触,但是,如果突破更高的天花板,这部分知识是必不可少的,而且我亲身经历——非常有用!

JavaScript编码能力

  • 1.多种方式实现数组去重、扁平化、对比优缺点
  • 2.多种方式实现深拷贝、对比优缺点
  • 3.手写函数柯里化工具函数、并理解其应用场景和优势
  • 4.手写防抖和节流工具函数、并理解其内部原理和应用场景
  • 5.实现一个sleep函数

手动实现前端轮子

  • 1.手动实现call、apply、bind
  • 2.手动实现符合Promise/A+规范的Promise、手动实现async await
  • 3.手写一个EventEmitter实现事件发布、订阅
  • 4.可以说出两种实现双向绑定的方案、可以手动实现
  • 5.手写JSON.stringifyJSON.parse
  • 6.手写一个模版引擎,并能解释其中原理
  • 7.手写懒加载下拉刷新上拉加载预加载等效果

数据结构

  • 1.理解常见数据结构的特点,以及他们在不同场景下使用的优缺点
  • 2.理解数组字符串的存储原理,并熟练应用他们解决问题
  • 3.理解二叉树队列哈希表的基本结构和特点,并可以应用它解决问题
  • 4.了解的基本结构和使用场景

算法

  • 1.可计算一个算法的时间复杂度和空间复杂度,可估计业务逻辑代码的耗时和内存消耗
  • 2.至少理解五种排序算法的实现原理、应用场景、优缺点,可快速说出时间、空间复杂度
  • 3.了解递归和循环的优缺点、应用场景、并可在开发中熟练应用
  • 4.可应用回溯算法贪心算法分治算法动态规划等解决复杂问题
  • 5.前端处理海量数据的算法方案

五、运行环境

我们需要理清语言和环境的关系:

ECMAScript描述了JavaScript语言的语法和基本对象规范

浏览器作为JavaScript的一种运行环境,为它提供了:文档对象模型(DOM),描述处理网页内容的方法和接口、浏览器对象模型(BOM),描述与浏览器进行交互的方法和接口

Node也是JavaScript的一种运行环境,为它提供了操作I/O、网络等API

浏览器API

  • 1.浏览器提供的符合W3C标准的DOM操作API、浏览器差异、兼容性
  • 2.浏览器提供的浏览器对象模型 (BOM)提供的所有全局API、浏览器差异、兼容性
  • 3.大量DOM操作、海量数据的性能优化(合并操作、DiffrequestAnimationFrame等)
  • 4.浏览器海量数据存储、操作性能优化
  • 5.DOM事件流的具体实现机制、不同浏览器的差异、事件代理
  • 6.前端发起网络请求的几种方式及其底层实现、可以手写原生ajaxfetch、可以熟练使用第三方库
  • 7.浏览器的同源策略,如何避免同源策略,几种方式的异同点以及如何选型
  • 8.浏览器提供的几种存储机制、优缺点、开发中正确的选择
  • 9.浏览器跨标签通信

浏览器原理

  • 1.各浏览器使用的JavaScript引擎以及它们的异同点、如何在代码中进行区分
  • 2.请求数据到请求结束与服务器进行了几次交互
  • 3.可详细描述浏览器从输入URL到页面展现的详细过程
  • 4.浏览器解析HTML代码的原理,以及构建DOM树的流程
  • 5.浏览器如何解析CSS规则,并将其应用到DOM树上
  • 6.浏览器如何将解析好的带有样式的DOM树进行绘制
  • 7.浏览器的运行机制,如何配置资源异步同步加载
  • 8.浏览器回流与重绘的底层原理,引发原因,如何有效避免
  • 9.浏览器的垃圾回收机制,如何避免内存泄漏
  • 10.浏览器采用的缓存方案,如何选择和控制合适的缓存方案

Node

  • 1.理解Node在应用程序中的作用,可以使用Node搭建前端运行环境、使用Node操作文件、操作数据库等等
  • 2.掌握一种Node开发框架,如ExpressExpressKoa的区别
  • 3.熟练使用Node提供的APIPathHttpChild Process等并理解其实现原理
  • 4.Node的底层运行原理、和浏览器的异同
  • 5.Node事件驱动、非阻塞机制的实现原理

六、框架和类库

轮子层出不穷,从原理上理解才是正道

TypeScript

  • 1.理解泛型接口等面向对象的相关概念,TypeScript对面向对象理念的实现
  • 2.理解使用TypeScript的好处,掌握TypeScript基础语法
  • 3.TypeScript的规则检测原理
  • 4.可以在ReactVue等框架中使用TypeScript进行开发

React

  • 1.Reactvue 选型和优缺点、核心架构的区别
  • 2.ReactsetState的执行机制,如何有效的管理状态
  • 3.React的事件底层实现机制
  • 4.React的虚拟DOMDiff算法的内部实现
  • 5.ReactFiber工作原理,解决了什么问题
  • 6.React RouterVue Router的底层实现原理、动态加载实现原理
  • 7.可熟练应用React API、生命周期等,可应用HOCrender propsHooks等高阶用法解决问题
  • 8.基于React的特性和原理,可以手动实现一个简单的React

Vue

  • 1.熟练使用VueAPI、生命周期、钩子函数
  • 2.MVVM框架设计理念
  • 3.Vue双向绑定实现原理、Diff算法的内部实现
  • 4.Vue的事件机制
  • 5.从template转换成真实DOM的实现机制

多端开发

  • 1.单页面应用(SPA)的原理和优缺点,掌握一种快速开发SPA的方案
  • 2.理解Viewportemrem的原理和用法,分辨率、pxppidpidp的区别和实际应用
  • 3.移动端页面适配解决方案、不同机型适配方案
  • 4.掌握一种JavaScript移动客户端开发技术,如React Native:可以搭建React Native开发环境,熟练进行开发,可理解React Native的运作原理,不同端适配
  • 5.掌握一种JavaScriptPC客户端开发技术,如Electron:可搭建Electron开发环境,熟练进行开发,可理解Electron的运作原理
  • 6.掌握一种小程序开发框架或原生小程序开发
  • 7.理解多端框架的内部实现原理,至少了解一个多端框架的使用

数据流管理

  • 1.掌握ReactVue传统的跨组件通信方案,对比采用数据流管理框架的异同
  • 2.熟练使用Redux管理数据流,并理解其实现原理,中间件实现原理
  • 3.熟练使用Mobx管理数据流,并理解其实现原理,相比Redux有什么优势
  • 4.熟练使用Vuex管理数据流,并理解其实现原理
  • 5.以上数据流方案的异同和优缺点,不情况下的技术选型

实用库

  • 1.至少掌握一种UI组件框架,如antd design,理解其设计理念、底层实现
  • 2.掌握一种图表绘制框架,如Echart,理解其设计理念、底层实现,可以自己实现图表
  • 3.掌握一种GIS开发框架,如百度地图API
  • 4.掌握一种可视化开发框架,如Three.jsD3
  • 5.工具函数库,如lodashunderscoremoment等,理解使用的工具类或工具函数的具体实现原理

开发和调试

  • 1.熟练使用各浏览器提供的调试工具
  • 2.熟练使用一种代理工具实现请求代理、抓包,如charls
  • 3.可以使用AndroidIOS模拟器进行调试,并掌握一种真机调试方案
  • 4.了解VueReact等框架调试工具的使用

七、前端工程

前端工程化:以工程化方法和工具提高开发生产效率、降低维护难度

项目构建

  • 1.理解npmyarn依赖包管理的原理,两者的区别
  • 2.可以使用npm运行自定义脚本
  • 3.理解BabelESLintwebpack等工具在项目中承担的作用
  • 4.ESLint规则检测原理,常用的ESLint配置
  • 5.Babel的核心原理,可以自己编写一个Babel插件
  • 6.可以配置一种前端代码兼容方案,如Polyfill
  • 7.Webpack的编译原理、构建流程、热更新原理,chunkbundlemodule的区别和应用
  • 8.可熟练配置已有的loadersplugins解决问题,可以自己编写loadersplugins

nginx

  • 1.正向代理与反向代理的特点和实例
  • 2.可手动搭建一个简单的nginx服务器、
  • 3.熟练应用常用的nginx内置变量,掌握常用的匹配规则写法
  • 4.可以用nginx实现请求过滤、配置gzip、负载均衡等,并能解释其内部原理

开发提速

  • 1.熟练掌握一种接口管理、接口mock工具的使用,如yapi
  • 2.掌握一种高效的日志埋点方案,可快速使用日志查询工具定位线上问题
  • 3.理解TDDBDD模式,至少会使用一种前端单元测试框架

版本控制

  • 1.理解Git的核心原理、工作流程、和SVN的区别
  • 2.熟练使用常规的Git命令、git rebasegit stash等进阶命令
  • 3.可以快速解决线上分支回滚线上分支错误合并等复杂问题

持续集成

  • 1.理解CI/CD技术的意义,至少熟练掌握一种CI/CD工具的使用,如Jenkins
  • 2.可以独自完成架构设计、技术选型、环境搭建、全流程开发、部署上线等一套完整的开发流程(包括Web应用、移动客户端应用、PC客户端应用、小程序、H5等等)

八、项目和业务

后端技能

  • 1.了解后端的开发方式,在应用程序中的作用,至少会使用一种后端语言
  • 2.掌握数据最终在数据库中是如何落地存储的,能看懂表结构设计、表之间的关联,至少会使用一种数据库

性能优化

  • 1.了解前端性能衡量指标、性能监控要点,掌握一种前端性能监控方案
  • 2.了解常见的WebApp性能优化方案
  • 3.SEO排名规则、SEO优化方案、前后端分离的SEO
  • 4.SSR实现方案、优缺点、及其性能优化
  • 5.Webpack的性能优化方案
  • 6.Canvas性能优化方案
  • 7.ReactVue等框架使用性能优化方案

前端安全

  • 1.XSS攻击的原理、分类、具体案例,前端如何防御
  • 2.CSRF攻击的原理、具体案例,前端如何防御
  • 3.HTTP劫持、页面劫持的原理、防御措施

业务相关

  • 1.能理解所开发项目的整体业务形态、业务目标、业务架构,可以快速定位线上业务问题
  • 2.能理解所开发项目整体的技术架构、能快读的根据新需求进行开发规划、能快速根据业务报警、线上日志等定位并解决线上技术问题
  • 3.可以将自己的想法或新技术在业务中落地实践,尽量在团队中拥有一定的不可替代性

九、学习提升

vczh大神在知乎问题【如何能以后达到温赵轮三位大神的水平?】下的回答:

这十几年我一共做了三件事:

  • 1、不以赚钱为目的选择学习的内容;
  • 2、以自己是否能造出轮子来衡量学习的效果;
  • 3、坚持每天写自己的代码,前10年每天至少6个小时,不包含学习和工作的时间。
上面几点可能有点难,第一点我就做不到,但是做到下面绩点还是比较容易的。

关于写博客说明下,能给别人讲明白的知识会比自己学习掌握的要深刻许多

  • 1.拥有自己的技术博客,或者在一些博客平台上拥有自己的专栏
  • 2.定期的将知识进行总结,不断完善自己的知识体系
  • 3.尽量将自己的知识转换成真实的产出,不要仅仅停留在书面理解层面,更重要的是实际应用
  • 4.坚持输出自己的代码,不要盲目的扎进公司业

十、技术之外

这部分可能比上面九条加起来重要!
  • 1.了解互联网人员术语:CEOCTOCOOCFOPMQAUIFEDEVDBAOPS
  • 2.了解互联网行业术语:B2BB2CC2CO2O
  • 3.掌握互联网行业沟通、问答、学习的
  • 4.有一定的"PPT"能力
  • 5.有一定的理财意识,至少了解储蓄、货币基金、保险、指数基金、股票等基本的理财知识
  • 6.掌握在繁重的工作和长期的电脑辐射的情况下保持健康的方法,建立正确的养生知识体系

十一、资源推荐

有了知识体系,在阅读一篇技术文章的时候就很容易把它归类,我一直以来就是这样做的。

事实证明,在阅读文章或书籍时,有目的和归类的阅读比"随便看看"后的只是留存率要高很多。

每阅读到一篇好的文章或者书籍,我都会收藏并归类到我的知识体系中。

下面是一些我觉得还不错的文章、博客或者书籍教程等等,分享给大家,资源不多,但都是精品。

学习一门知识,最好先阅读官方文档,把所有的API大概浏览一遍,再继续看大佬们总结的进阶知识,什么东西是搬运过来的,什么是干货,一目了然。

语言基础

计算机基础

数据结构和算法

运行环境

框架和类库

前端工程

项目和业务

学习提升

另外推荐我一直在关注的几位大佬的个人博客:

技术之外

其实在这个信息发达的时代最不缺的就是资源,如何从众多的资源中获取到真正精华的部分,是非常重要的,资源在于精不在于多,强烈建议在保证深度的情况下再保证广度。

小结

希望你阅读本篇文章后可以达到以下几点:

  • 从知识清单中找到自己的知识盲点与欠缺
  • 具有知识体系化的思想,开始建立自己的知识体系
  • 阅读文章时将知识归类到知识体系中,并不断完善自己的知识体系
  • 从文章中获取到了有用的资源

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。

如果你有什么好的知识、资源推荐,欢迎在评论区留言。

想阅读更多优质文章、下载文章中思维导图源文件、可关注我的github博客,后续的文章我也会按照知识清单来写,你的star✨、点赞和关注是我持续创作的动力!

推荐关注我的微信公众号【code秘密花园】,每天推送高质量文章,我们一起交流成长。

查看原文

广工小成 发布了文章 · 2020-11-10

H5常遇见的问题———移动端1px解决(完整版)

 在某个夜黑风高的晚上,程序员小A正在开开心心的准备收拾东西回家。这时候,手机突然震动了一下。小A下意识的想到,肯定是大事不好了。果不其然,是设计大佬发来消息了。。。

设计大佬:小A,怎么这个0.5dp(0.5dp=1px)的边框比实际的还粗啊

小A:好的,我现在去看一下(没道理啊,我明明记得我写了1px的,不可能会粗啊,难道我忘了吗)

阅读完代码之后....

小A:设计大佬,我这里已经写了1px了,他就是1px了,不信你看看,会粗一点可能是手机h5会把1px画粗一点吧

设计大佬:但是很多h5页面也有1px的边框啊

小A:好的,那我再去调研一下(大佬说话,不得不低调,还是先别着急,可能另有转机呢)

经过一番调研后....

小A顿然醒悟,果然1px的问题很多人都遇到,网上也有很多方案,但是看起来乱七八糟的,又没讲明原理,万一下次再碰到,那该怎么办呢。这是他有了一个idea,不如自己整理一下,也方便大家记忆

1px原理篇

在讲原理之前,先跟大家说一个概念,就是设备像素比DPR(devicePixelRatio)是什么

DPR = 设备像素 / CSS像素(某一方向上)

这句话看起来很难理解,可以结合下面这张图(1px在各个DPR上面的展示),一般我们h5拿到的设计稿都是750px的,但是如果在DPR为2的屏幕上,手机的最小像素却是要用2 * 2px来进行绘制,这也就导致了为什么1px会比较粗了。

image

解决方法

解决办法有很多种,在这里帮大家比较下方案:

方案优点缺点
使用0.5px实现代码简单,使用css即可IOS及Android老设备不支持
使用border-image实现兼容目前所有机型修改颜色不方便
通过 viewport + rem 实现一套代码,所有页面和0.5px一样,机型不兼容
使用伪类 + transform实现兼容所有机型不支持圆角
box-shadow模拟边框实现兼容所有机型box-shadow不在盒子模型,需要注意预留位置

以上的方案在网上都可以找到示例,我这里只提供两个本人经常使用的方案:

box-shadow

box-shadow是本人最常用的,除了在Android4.4以下发现小于1p的shadow无法显示之外,其他的都是好的

// 下边框
box-shadow: 0 1px #E9E9E9;

// 全边框
box-shadow: 0 -1px #D9D9D9, 1px 0 #D9D9D9, 0 1px #D9D9D9, -1px 0 #D9D9D9;

// 其他的可以看看API更深入了解这个API

使用伪类 + transform实现

目前京东的h5网页就是使用使用伪类 + transform实现

// 左边框,如果需要修改边框位置,可以修改元素top,left,right,bottom的值即可
&::before {
    position: absolute;
    top: 0;
    left: 0;
    content: '\0020';
    width: 100%;
    height: 1px;
    border-top: 1px solid #E9E9E9;
    transform-origin: 0 0;
    overflow: hidden;
}

@media (-webkit-min-device-pixel-ratio: 1.5) and (-webkit-max-device-pixel-ratio: 2.49) {
    &::before {
      transform: scaleY(0.5);
    }
}

@media (-webkit-min-device-pixel-ratio: 2.5) {
    &::before {
      transform: scaleY(0.33333);
    }
}

市面上其他网页的处理方案

参考了下目前的前沿技术某东和某宝的页面

发现某宝是使用div+width来进行实现,因此推断某宝应该是使用了通过 viewport + rem + div 的方法

image

某东如上面的使用了伪类 + transform实现

image

总结

设计师有时候很严格也是件好事

参考资料:

https://main.m.taobao.com/

https://m.jd.com/

https://juejin.im/post/684490...

https://www.kelede.win/posts/...

查看原文

赞 23 收藏 15 评论 2

广工小成 收藏了文章 · 2020-11-06

前端都该懂的浏览器工作原理,你懂了吗?

前言

在我们面试过程中,面试官经常会问到这么一个问题,那就是从在浏览器地址栏中输入URL到页面显示,浏览器到底发生了什么?这个问题看起来是老生常谈,但是这个问题回答的好坏,确实可以很好的反映出面试者知识的广度和深度。

本文从浏览器角度来告诉你,URL后输入后按回车,浏览器内部究竟发生了什么,读完本文后,你将了解到:

  • 浏览器内有哪些进程,这些进程都有些什么作用
  • 浏览器地址输入URL后,内部的进程、线程都做了哪些事
  • 我们与浏览器交互时,内部进程是怎么处理这些交互事件的

原文地址 欢迎star

浏览器架构

在讲浏览器架构之前,先理解两个概念,进程线程

进程(process)是程序的一次执行过程,是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,线程(thread)是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

简单的说呢,进程可以理解成正在执行的应用程序,而线程呢,可以理解成我们应用程序中的代码的执行器。而他们的关系可想而知,线程是跑在进程里面的,一个进程里面可能有一个或者多个线程,而一个线程,只能隶属于一个进程。

大家都知道,浏览器属于一个应用程序,而应用程序的一次执行,可以理解为计算机启动了一个进程,进程启动后,CPU会给该进程分配相应的内存空间,当我们的进程得到了内存之后,就可以使用线程进行资源调度,进而完成我们应用程序的功能。

而在应用程序中,为了满足功能的需要,启动的进程会创建另外的新的进程来处理其他任务,这些创建出来的新的进程拥有全新的独立的内存空间,不能与原来的进程内向内存,如果这些进程之间需要通信,可以通过IPC机制(Inter Process Communication)来进行。

进程1

很多应用程序都会采取这种多进程的方式来工作,因为进程和进程之间是互相独立的它们互不影响,也就是说,当其中一个进程挂掉了之后,不会影响到其他进程的执行,只需要重启挂掉的进程就可以恢复运行。

浏览器的多进程架构

假如我们去开发一个浏览器,它的架构可以是一个单进程多线程的应用程序,也可以是一个使用IPC通信的多进程应用程序。

不同的浏览器使用不同的架构,下面主要以Chrome为例,介绍浏览器的多进程架构。

在Chrome中,主要的进程有4个:

  • 浏览器进程 (Browser Process):负责浏览器的TAB的前进、后退、地址栏、书签栏的工作和处理浏览器的一些不可见的底层操作,比如网络请求和文件访问。
  • 渲染进程 (Renderer Process):负责一个Tab内的显示相关的工作,也称渲染引擎。
  • 插件进程 (Plugin Process):负责控制网页使用到的插件
  • GPU进程 (GPU Process):负责处理整个应用程序的GPU任务

进程关系

这4个进程之间的关系是什么呢?

首先,当我们是要浏览一个网页,我们会在浏览器的地址栏里输入URL,这个时候Browser Process会向这个URL发送请求,获取这个URL的HTML内容,然后将HTML交给Renderer ProcessRenderer Process解析HTML内容,解析遇到需要请求网络的资源又返回来交给Browser Process进行加载,同时通知Browser Process,需要Plugin Process加载插件资源,执行插件代码。解析完成后,Renderer Process计算得到图像帧,并将这些图像帧交给GPU ProcessGPU Process将其转化为图像显示屏幕。

进程关系

多进程架构的好处

Chrome为什么要使用多进程架构呢?

第一,更高的容错性。当今WEB应用中,HTML,JavaScript和CSS日益复杂,这些跑在渲染引擎的代码,频繁的出现BUG,而有些BUG会直接导致渲染引擎崩溃,多进程架构使得每一个渲染引擎运行在各自的进程中,相互之间不受影响,也就是说,当其中一个页面崩溃挂掉之后,其他页面还可以正常的运行不收影响。

浏览器容错性

第二,更高的安全性和沙盒性(sanboxing)。渲染引擎会经常性的在网络上遇到不可信、甚至是恶意的代码,它们会利用这些漏洞在你的电脑上安装恶意的软件,针对这一问题,浏览器对不同进程限制了不同的权限,并为其提供沙盒运行环境,使其更安全更可靠

第三,更高的响应速度。在单进程的架构中,各个任务相互竞争抢夺CPU资源,使得浏览器响应速度变慢,而多进程架构正好规避了这一缺点。

多进程架构优化

之前的我们说到,Renderer Process的作用是负责一个Tab内的显示相关的工作,这就意味着,一个Tab,就会有一个Renderer Process,这些进程之间的内存无法进行共享,而不同进程的内存常常需要包含相同的内容。

浏览器的进程模式

为了节省内存,Chrome提供了四种进程模式(Process Models),不同的进程模式会对 tab 进程做不同的处理。

  • Process-per-site-instance (default) - 同一个 site-instance 使用一个进程
  • Process-per-site - 同一个 site 使用一个进程
  • Process-per-tab - 每个 tab 使用一个进程
  • Single process - 所有 tab 共用一个进程

这里需要给出 site 和 site-instance 的定义

  • site 指的是相同的 registered domain name(如: google.com ,bbc.co.uk)和scheme (如:https://)。比如a.baidu.com和b.baidu.com就可以理解为同一个 site(注意这里要和 Same-origin policy 区分开来,同源策略还涉及到子域名和端口)。
  • site-instance 指的是一组 connected pages from the same site,这里 connected 的定义是 can obtain references to each other in script code 怎么理解这段话呢。满足下面两中情况并且打开的新页面和旧页面属于上面定义的同一个 site,就属于同一个 site-instance

    • 用户通过<a target="_blank">这种方式点击打开的新页面
    • JS代码打开的新页面(比如 window.open)

理解了概念之后,下面解释四个进程模式

首先是Single process,顾名思义,单进程模式,所有tab都会使用同一个进程。接下来是Process-per-tab ,也是顾名思义,每打开一个tab,会新建一个进程。而对于Process-per-site,当你打开 a.baidu.com 页面,在打开 b.baidu.com 的页面,这两个页面的tab使用的是共一个进程,因为这两个页面的site相同,而如此一来,如果其中一个tab崩溃了,而另一个tab也会崩溃。

Process-per-site-instance 是最重要的,因为这个是 Chrome 默认使用的模式,也就是几乎所有的用户都在用的模式。当你打开一个 tab 访问 a.baidu.com ,然后再打开一个 tab 访问 b.baidu.com,这两个 tab 会使用两个进程。而如果你在 a.baidu.com 中,通过JS代码打开了 b.baidu.com 页面,这两个 tab 会使用同一个进程

默认模式选择

那么为什么浏览器使用Process-per-site-instance作为默认的进程模式呢?

Process-per-site-instance兼容了性能与易用性,是一个比较中庸通用的模式。

  • 相较于 Process-per-tab,能够少开很多进程,就意味着更少的内存占用
  • 相较于 Process-per-site,能够更好的隔离相同域名下毫无关联的 tab,更加安全

导航过程都发生了什么

前面我们讲了浏览器的多进程架构,讲了多进程架构的各种好处,和Chrome是怎么优化多进程架构的,下面从用户浏览网页这一简单的场景,来深入了解进程和线程是如何呈现我们的网站页面的。

网页加载过程

之前我们我们提到,tab以外的大部分工作由浏览器进程Browser Process负责,针对工作的不同,Browser Process 划分出不同的工作线程:

  • UI thread:控制浏览器上的按钮及输入框;
  • network thread:处理网络请求,从网上获取数据;
  • storage thread: 控制文件等的访问;

浏览器进程线程

第一步:处理输入

当我们在浏览器的地址栏输入内容按下回车时,UI thread会判断输入的内容是搜索关键词(search query)还是URL,如果是搜索关键词,跳转至默认搜索引擎对应都搜索URL,如果输入的内容是URL,则开始请求URL。

处理输入

第二步:开始导航

回车按下后,UI thread将关键词搜索对应的URL或输入的URL交给网络线程Network thread,此时UI线程使Tab前的图标展示为加载中状态,然后网络进程进行一系列诸如DNS寻址,建立TLS连接等操作进行资源请求,如果收到服务器的301重定向响应,它就会告知UI线程进行重定向然后它会再次发起一个新的网络请求。

开始导航

第三步:读取响应

network thread接收到服务器的响应后,开始解析HTTP响应报文,然后根据响应头中的Content-Type字段来确定响应主体的媒体类型(MIME Type),如果媒体类型是一个HTML文件,则将响应数据交给渲染进程(renderer process)来进行下一步的工作,如果是 zip 文件或者其它文件,会把相关数据传输给下载管理器。

与此同时,浏览器会进行 Safe Browsing 安全检查,如果域名或者请求内容匹配到已知的恶意站点,network thread 会展示一个警告页。除此之外,网络线程还会做 CORB(Cross Origin Read Blocking)检查来确定那些敏感的跨站数据不会被发送至渲染进程。

第四步:查找渲染进程

各种检查完毕以后,network thread 确信浏览器可以导航到请求网页,network thread 会通知 UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页的渲染。

查找渲染进程

浏览器为了对查找渲染进程这一步骤进行优化,考虑到网络请求获取响应需要时间,所以在第二步开始,浏览器已经预先查找和启动了一个渲染进程,如果中间步骤一切顺利,当 network thread 接收到数据时,渲染进程已经准备好了,但是如果遇到重定向,这个准备好的渲染进程也许就不可用了,这个时候会重新启动一个渲染进程。

第五步:提交导航

到了这一步,数据和渲染进程都准备好了,Browser Process 会向 Renderer Process 发送IPC消息来确认导航,此时,浏览器进程将准备好的数据发送给渲染进程,渲染进程接收到数据之后,又发送IPC消息给浏览器进程,告诉浏览器进程导航已经提交了,页面开始加载。

提交导航

这个时候导航栏会更新,安全指示符更新(地址前面的小锁),访问历史列表(history tab)更新,即可以通过前进后退来切换该页面。

第六步:初始化加载完成

当导航提交完成后,渲染进程开始加载资源及渲染页面(详细内容下文介绍),当页面渲染完成后(页面及内部的iframe都触发了onload事件),会向浏览器进程发送IPC消息,告知浏览器进程,这个时候UI thread会停止展示tab中的加载中图标。

网页渲染原理

导航过程完成之后,浏览器进程把数据交给了渲染进程,渲染进程负责tab内的所有事情,核心目的就是将HTML/CSS/JS代码,转化为用户可进行交互的web页面。那么渲染进程是如何工作的呢?

渲染进程中,包含线程分别是:

  • 一个主线程(main thread)
  • 多个工作线程(work thread)
  • 一个合成器线程(compositor thread)
  • 多个光栅化线程(raster thread)

浏览器进程中线程

不同的线程,有着不同的工作职责。

构建DOM

当渲染进程接受到导航的确认信息后,开始接受来自浏览器进程的数据,这个时候,主线程会解析数据转化为DOM(Document Object Model)对象。

DOM为WEB开发人员通过JavaScript与网页进行交互的数据结构及API。

子资源加载

在构建DOM的过程中,会解析到图片、CSS、JavaScript脚本等资源,这些资源是需要从网络或者缓存中获取的,主线程在构建DOM过程中如果遇到了这些资源,逐一发起请求去获取,而为了提升效率,浏览器也会运行预加载扫描(preload scanner)程序,如果HTML中存在imglink等标签,预加载扫描程序会把这些请求传递给Browser Process的network thread进行资源下载。

加载子资源

JavaScript的下载与执行

构建DOM过程中,如果遇到<script>标签,渲染引擎会停止对HTML的解析,而去加载执行JS代码,原因在于JS代码可能会改变DOM的结构(比如执行document.write()等API)

不过开发者其实也有多种方式来告知浏览器应对如何应对某个资源,比如说如果在<script> 标签上添加了 asyncdefer 等属性,浏览器会异步的加载和执行JS代码,而不会阻塞渲染。

样式计算 - Style calculation

DOM树只是我们页面的结构,我们要知道页面长什么样子,我们还需要知道DOM的每一个节点的样式。主线程在解析页面时,遇到<style>标签或者<link>标签的CSS资源,会加载CSS代码,根据CSS代码确定每个DOM节点的计算样式(computed style)。

计算样式是主线程根据CSS样式选择器(CSS selectors)计算出的每个DOM元素应该具备的具体样式,即使你的页面没有设置任何自定义的样式,浏览器也会提供其默认的样式。

样式计算

布局 - Layout

DOM树和计算样式完成后,我们还需要知道每一个节点在页面上的位置,布局(Layout)其实就是找到所有元素的几何关系的过程。

主线程会遍历DOM 及相关元素的计算样式,构建出包含每个元素的页面坐标信息及盒子模型大小的布局树(Render Tree),遍历过程中,会跳过隐藏的元素(display: none),另外,伪元素虽然在DOM上不可见,但是在布局树上是可见的。

layout

绘制 - Paint

布局 layout 之后,我们知道了不同元素的结构,样式,几何关系,我们要绘制出一个页面,我们要需要知道每个元素的绘制先后顺序,在绘制阶段,主线程会遍历布局树(layout tree),生成一系列的绘画记录(paint records)。绘画记录可以看做是记录各元素绘制先后顺序的笔记。

paint

合成 - Compositing

文档结构、元素的样式、元素的几何关系、绘画顺序,这些信息我们都有了,这个时候如果要绘制一个页面,我们需要做的是把这些信息转化为显示器中的像素,这个转化的过程,叫做光栅化(rasterizing)。

那我们要绘制一个页面,最简单的做法是只光栅化视口内(viewport)的网页内容,如果用户进行了页面滚动,就移动光栅帧(rastered frame)并且光栅化更多的内容以补上页面缺失的部分,如下:

最简单的光栅化过程

Chrome第一个版本就是采用这种简单的绘制方式,这一方式唯一的缺点就是每当页面滚动,光栅线程都需要对新移进视图的内容进行光栅化,这是一定的性能损耗,为了优化这种情况,Chrome采取一种更加复杂的叫做合成(compositing)的做法。

那么,什么是合成?合成是一种将页面分成若干层,然后分别对它们进行光栅化,最后在一个单独的线程 - 合成线程(compositor thread)里面合并成一个页面的技术。当用户滚动页面时,由于页面各个层都已经被光栅化了,浏览器需要做的只是合成一个新的帧来展示滚动后的效果罢了。页面的动画效果实现也是类似,将页面上的层进行移动并构建出一个新的帧即可。

合成的光栅化过程

为了实现合成技术,我们需要对元素进行分层,确定哪些元素需要放置在哪一层,主线程需要遍历渲染树来创建一棵层次树(Layer Tree),对于添加了 will-change CSS 属性的元素,会被看做单独的一层,没有 will-change CSS属性的元素,浏览器会根据情况决定是否要把该元素放在单独的层。

layer tree

你可能会想要给页面上所有的元素一个单独的层,然而当页面的层超过一定的数量后,层的合成操作要比在每个帧中光栅化页面的一小部分还要慢,因此衡量你应用的渲染性能是十分重要的一件事情。

一旦Layer Tree被创建,渲染顺序被确定,主线程会把这些信息通知给合成器线程,合成器线程开始对层次数的每一层进行光栅化。有的层的可以达到整个页面的大小,所以合成线程需要将它们切分为一块又一块的小图块(tiles),之后将这些小图块分别进行发送给一系列光栅线程(raster threads)进行光栅化,结束后光栅线程会将每个图块的光栅结果存在GPU Process的内存中。

光栅线程创建图块的位图并发送给GPU

为了优化显示体验,合成线程可以给不同的光栅线程赋予不同的优先级,将那些在视口中的或者视口附近的层先被光栅化。

当图层上面的图块都被栅格化后,合成线程会收集图块上面叫做绘画四边形(draw quads)的信息来构建一个合成帧(compositor frame)。

  • 绘画四边形:包含图块在内存的位置以及图层合成后图块在页面的位置之类的信息。
  • 合成帧:代表页面一个帧的内容的绘制四边形集合

以上所有步骤完成后,合成线程就会通过IPC向浏览器进程(browser process)提交(commit)一个渲染帧。这个时候可能有另外一个合成帧被浏览器进程的UI线程(UI thread)提交以改变浏览器的UI。这些合成帧都会被发送给GPU从而展示在屏幕上。如果合成线程收到页面滚动的事件,合成线程会构建另外一个合成帧发送给GPU来更新页面。

合成线程构建出合成帧,合成帧会被发送给浏览器进程然后再发送给GPU

合成的好处在于这个过程没有涉及到主线程,所以合成线程不需要等待样式的计算以及JavaScript完成执行。这就是为什么合成器相关的动画最流畅,如果某个动画涉及到布局或者绘制的调整,就会涉及到主线程的重新计算,自然会慢很多。

浏览器对事件的处理

当页面渲染完毕以后,TAB内已经显示出了可交互的WEB页面,用户可以进行移动鼠标、点击页面等操作了,而当这些事件发生时候,浏览器是如何处理这些事件的呢?

以点击事件(click event)为例,让鼠标点击页面时候,首先接受到事件信息的是Browser Process,但是Browser Process只知道事件发生的类型和发生的位置,具体怎么对这个点击事件进行处理,还是由Tab内的Renderer Process进行的。Browser Process接受到事件后,随后便把事件的信息传递给了渲染进程,渲染进程会找到根据事件发生的坐标,找到目标对象(target),并且运行这个目标对象的点击事件绑定的监听函数(listener)。

点击事件从浏览器进程路由到渲染进程

渲染进程中合成器线程接收事件

前面我们说到,合成器线程可以独立于主线程之外通过已光栅化的层创建组合帧,例如页面滚动,如果没有对页面滚动绑定相关的事件,组合器线程可以独立于主线程创建组合帧,如果页面绑定了页面滚动事件,合成器线程会等待主线程进行事件处理后才会创建组合帧。那么,合成器线程是如何判断出这个事件是否需要路由给主线程处理的呢?

由于执行 JS 是主线程的工作,当页面合成时,合成器线程会标记页面中绑定有事件处理器的区域为非快速滚动区域(non-fast scrollable region),如果事件发生在这些存在标注的区域,合成器线程会把事件信息发送给主线程,等待主线程进行事件处理,如果事件不是发生在这些区域,合成器线程则会直接合成新的帧而不用等到主线程的响应。

非快速滚动区域有用户事件发生

而对于非快速滚动区域的标记,开发者需要注意全局事件的绑定,比如我们使用事件委托,将目标元素的事件交给根元素body进行处理,代码如下:

document.body.addEventListener('touchstart', event => {
  if (event.target === area) {
    event.preventDefault()
  }
})

在开发者角度看,这一段代码没什么问题,但是从浏览器角度看,这一段代码给body元素绑定了事件监听器,也就意味着整个页面都被编辑为一个非快速滚动区域,这会使得即使你的页面的某些区域没有绑定任何事件,每次用户触发事件时,合成器线程也需要和主线程通信并等待反馈,流畅的合成器独立处理合成帧的模式就失效了。

当整个页面都是非快速滚动区域时页面的事件处理示意图

其实这种情况也很好处理,只需要在事件监听时传递passtive参数为 true,passtive会告诉浏览器你既要绑定事件,又要让组合器线程直接跳过主线程的事件处理直接合成创建组合帧。

document.body.addEventListener('touchstart', 
event => {
    if (event.target === area) {
        event.preventDefault()
    }
 }, {passive: true});

查找事件的目标对象(event target)

当合成器线程接收到事件信息,判定到事件发生不在非快速滚动区域后,合成器线程会向主线程发送这个时间信息,主线程获取到事件信息的第一件事就是通过命中测试(hit test)去找到事件的目标对象。具体的命中测试流程是遍历在绘制阶段生成的绘画记录(paint records)来找到包含了事件发生坐标上的元素对象。

当整个页面都是非快速滚动区域时页面的事件处理示意图

浏览器对事件的优化

一般我们屏幕的帧率是每秒60帧,也就是60fps,但是某些事件触发的频率超过了这个数值,比如wheel,mousewheel,mousemove,pointermove,touchmove,这些连续性的事件一般每秒会触发60~120次,假如每一次触发事件都将事件发送到主线程处理,由于屏幕的刷新速率相对来说较低,这样使得主线程会触发过量的命中测试以及JS代码,使得性能有了没必要是损耗。

事件淹没了屏幕刷新的时间轴,导致页面很卡顿

出于优化的目的,浏览器会合并这些连续的事件,延迟到下一帧渲染是执行,也就是requestAnimationFrame之前。

和之前相同的事件轴,可是这次事件被合并并延迟调度了

而对于非连续性的事件,如keydown,keyup,mousedown,mouseup,touchstart,touchend等,会直接派发给主线程去执行。

总结

浏览器的多进程架构,根据不同的功能划分了不同的进程,进程内不同的使命划分了不同的线程,当用户开始浏览网页时候,浏览器进程进行处理输入、开始导航请求数据、请求响应数据,查找新建渲染进程,提交导航,之后渲染又进行了解析HTML构建DOM、构建过程加载子资源、下载并执行JS代码、样式计算、布局、绘制、合成,一步一步的构建出一个可交互的WEB页面,之后浏览器进程又接受页面的交互事件信息,并将其交给渲染进程,渲染进程内主进程进行命中测试,查找目标元素并执行绑定的事件,完成页面的交互。

本文大部分内容也是对inside look at modern web browser系列文章的整理、解读和翻译吧,整理过程还是收获非常大的,希望读者读了本文只有有所启发吧。

相关参考链接

查看原文

广工小成 收藏了文章 · 2020-08-20

从零搭建 Node.js 企业级 Web 服务器(零):静态服务

前言

过去 5 年,我前后在菜鸟网络和蚂蚁金服做开发工作,一方面支撑业务团队开发各类业务系统,另一方面在自己的技术团队做基础技术建设。期间借着 Node.js 的锋芒做了不少 Web 系统,有的至今生气蓬勃、有的早已夭折淡出。在实践中,蚂蚁的 Chair 与淘系的 Midway 给了我不少启发,也借鉴了不少 bad case。思考过身边团队、自己团队、国外团队的各种案例之后发现,搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项

在接下来的一段时间里,我会以如何 “从零搭建 Node.js 企业级 Web 服务器” 为主题,将自己的所见所闻、所思所想详尽地记录下来,每个章节最后会附上实现本章内容的源码,希望可以帮助正在学习和了解 Node.js 的朋友对 Web 服务器领域获得更清晰的理解和洞见。

阅读提示:

  • 本文着重表述 Web 后端技术相关内容,Web 前端内容采用 JavaScript Modules 进行演示。
  • 本文需要读者具备基础的编程能力以及对计算机网络的基本了解,一些常用术语限于篇幅不再展开。

准备环境

安装 Node.js

Node.js 发布版本分为 Current 和 LTS 两类,前者每 6 个月迭代一个大版本,快速提供新增功能和问题修复,后者以 30 个月为一个大周期,为生产环境提供稳定依赖。当前 Node.js 最新 LTS 版本为 12.18.2,本文以此版本作为运行环境,可以在官网下载并安装。

安装完成后,在命令行输入 node --version 查看输出是否为 v12.8.2,如果一致,那么安装成功。

另外,有兴趣的读者可以尝试通过 nvm / nvm-windows 管理多个 Node.js 版本,此处不是本文重点不再展开。

安装 Yarn

Node.js 提供了自己的包管理器 npm,npm 默认从海外官方的 registry 拉取包信息速度比较慢,需要执行以下命令设置使用国内镜像地址:

$ npm config set registry http://r.cnpmjs.org/

通过 npm 可以全局或本地安装依赖包,当本地安装时 npm 会根据 package.json 安装最新的依赖包,同时自动生成并更新 package-lock.json 文件,这就引发了一个问题:如果一个依赖包在 package.json 标记版本范围内发布了有问题的新版本,那么我们自己的项目也会跟着出问题。

为了解决这个问题,就引入了第三方包管理器 yarn,通过以下命令安装:

$ npm i -g yarn

相比 npm,yarn 会严格按照自动生成的 yarn.lock 本地安装依赖包,只有增删依赖或者 package.json 标记版本发生不可兼容的变化时才会更新 yarn.lock,这就彻底杜绝了上述 npm 的问题。考虑到企业级 Web 服务器对稳定性的要求,yarn 是必要的。

安装 Docker

一般来讲,做一个 Web 服务器会有两种部署选择,要么是传统的包部署,要么是容器镜像部署,后者较前者在编排上更方便,结合 Kubernetes 可以做到很好的伸缩,是当前发展的趋势。Docker 作为主流容器技术必须熟练掌握,可以在官网下载并安装。

写一个静态资源服务器

初始化工程

准备好了环境就可以开始编码了,先新建工程根目录,然后进入并初始化 package.json 与目录结构:

$ mkdir 00-static   # 新建工程根目录
$ cd 00-static      # 进入工程根目录

$ yarn init -y      # 初始化 package.json
yarn init v1.22.4
success Saved package.json

$ mkdir src         # 新建 src 目录存放核心逻辑
$ mkdir public      # 新建 public 目录存放静态资源

$ tree -L 1         # 展示当前目录内容结构
.
├── package.json
├── public
└── src

Express 还是 Koa?

Express 与 Koa 均是 Node.js 服务端基础框架。Express 发布于 2010 年,凭借出色的中间件机制在开源社区积累了大量的成熟模块,现在是 OpenJS 基金会 At-Large 级别项目。Koa 发布于 2013 年,相比 Express 具备了更加完善的中间件机制以及编程体验,但在开源社区模块积累的质与量上还有一定差距。在此比较几个常用模块:

模块名称功能简介Express / KoaStarContributersUsed by最近提交时间
passport认证登录Express17.7k33385k2020-06-10
koa-passport认证登录Koa737214.7k2019-07-13
connect-redis会话存储Express2.3k5126.3k2020-07-10
koa-redis会话存储Koa310132.7k2020-01-16
helmet网络安全Express7.2k25136.4k2020-07-11
koa-helmet网络安全Koa546244.1k2020-06-03

上表整理自 Github 截止 2020 年 7 月 20 日的数据。

相比 Koa 模块,Express 模块普遍在星数(Star)、贡献者数(Contributers)、使用数(Used by)上高出一个层次,同时 Express 模块的贡献者更热心于维护与更新,Koa 尽管在国内受到过一些追捧,但在更全面的考量下 Express 才是更稳健的选择。在工程根目录执行以下命令安装:

$ yarn add express  # 本地安装 Express
# ...
info Direct dependencies
└─ express@4.17.1
# ...

$ tree -L 1         # 展示当前目录内容结构
.
├── node_modules
├── package.json
├── public
├── src
└── yarn.lock

静态服务

现在可以开始写应用逻辑了,本章先做一个静态资源服务器,以 public 目录为静态资源目录:

// src/server.js
const express = require('express');
const { resolve } = require('path');
const { promisify } = require('util');

const server = express();
const port = parseInt(process.env.PORT || '9000');
const publicDir = resolve('public');

async function bootstrap() {
  server.use(express.static(publicDir));
  await promisify(server.listen.bind(server, port))();
  console.log(`> Started on port ${port}`);
}

bootstrap();
<!-- public/index.html -->
<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <h1>It works!</h1>
  </body>
</html>
$ tree -L 2 -I node_modules   # 展示除了 node_modules 之外的目录内容结构
.
├── package.json
├── public
│   └── index.html
├── src
│   └── server.js
└── yarn.lock

逻辑写好之后在 package.json 中设置启动脚本:

{
  "name": "00-static",
  "version": "1.0.0",
-  "main": "index.js",
+  "scripts": {
+    "start": "node src/server.js"
+  },
  "license": "MIT",
  "dependencies": {
    "express": "^4.17.1"
  }
}

然后就可以启动应用了:

$ yarn start
> Started on port 9000

访问 http://localhost:9000/ 即可看到 index.html 内容:

765bb72ad6058d32b641116e26f8d7338cf949f9.jpg

使用容器

接下来通过 Docker 对做好的静态资源服务器进行容器化,新建以下配置文件:

# Dockerfile
FROM node:12.18.2-slim

WORKDIR /usr/app/00-static
COPY . .
RUN yarn

EXPOSE 9000
CMD yarn start
# .dockerignore
node_modules
$ tree -L 1 -a  # 展示包括 . 开头的全部目录内容结构
.
├── .dockerignore
├── Dockerfile
├── node_modules
├── package.json
├── public
├── src
└── yarn.lock

然后构建镜像并启动容器:

$ # 构建容器镜像,命名为 00-static,标签为 1.0.0
$ docker build -t 00-static:1.0.0 .
# ...
Successfully tagged 00-static:1.0.0

$ # 以镜像 00-static:1.0.0 运行容器,命名为 00-static
$ docker run -p 9090:9000 -d --name 00-static 00-static:1.0.0

$ docker logs 00-static   # 查看 00-static 容器的日志
> Started on port 9000

$ docker stats 00-static  # 查看 00-static 容器的状态
CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS
43c451232fa5        00-static           0.03%               37.41MiB / 1.945GiB   1.88%               8.52kB / 3.35kB     0B / 0B             24

访问 http://localhost:9090/ 即可与之前一样看到 index.html 内容:

e369752861fb54057d2683cca41c81eb01071368.jpg

本章源码

host1-tech/nodejs-server-examples - 00-static

更多阅读

从零搭建 Node.js 企业级 Web 服务器(零):静态服务
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
从零搭建 Node.js 企业级 Web 服务器(二):校验
从零搭建 Node.js 企业级 Web 服务器(三):中间件
从零搭建 Node.js 企业级 Web 服务器(四):异常处理
从零搭建 Node.js 企业级 Web 服务器(五):数据库访问
从零搭建 Node.js 企业级 Web 服务器(六):会话
从零搭建 Node.js 企业级 Web 服务器(七):认证登录
从零搭建 Node.js 企业级 Web 服务器(八):网络安全
从零搭建 Node.js 企业级 Web 服务器(九):配置项
从零搭建 Node.js 企业级 Web 服务器(十):日志
从零搭建 Node.js 企业级 Web 服务器(十一):定时任务
从零搭建 Node.js 企业级 Web 服务器(十二):远程调用
从零搭建 Node.js 企业级 Web 服务器(十三):断点调试与性能分析
从零搭建 Node.js 企业级 Web 服务器(十四):自动化测试
从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望

查看原文

认证与成就

  • 获得 413 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-01-09
个人主页被 5.5k 人浏览