整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          CSS3 Grid布局實現Loading動畫效果

          擊右上方紅色按鈕關注“web秀”,讓你真正秀起來

          前言

          以前說頁面是動態,基本都是說數據是從數據庫查詢的,不是寫死在html代碼里面的。現在的說頁面動態,一般會問:你是說數據,還是效果動態?

          CSS3 Grid布局實現Loading動畫效果

          好的前端動畫效果,能給用戶帶來非常舒適的體驗效果,更甚者,有用戶覺的你這個動畫效果非常nice,反復操作,就為看你這個動畫。停留時間,預覽量上了,帶來的收益也不是一丁點吧。

          當然也不用為了動畫,而額外的來制作動畫效果。比如一個彈框,可以直接漸變出現的,你還加了飛了一圈出現,那就是不必要的動畫了。

          所以恰大好處的動畫效果,能帶來非常不錯的效果。

          下面我們來學習如果只做一些簡單的動畫效果:

          CSS3 Grid布局實現Loading動畫效果

          grid布局

          CSS3 Grid布局實現Loading動畫效果

          上圖的動畫,就是一個簡單的loading加載效果,而且是一個3x3的九宮格。是因為旋轉才變成一個菱形的樣子。我們先來畫出3x3的九宮格:

          html

          <div class="loading">
           <span></span>
           <span></span>
           <span></span>
           <span></span>
           <span></span>
           <span></span>
           <span></span>
           <span></span>
           <span></span>
          </div>
          

          這里用9個span來做每個格子元素。

          css

          body {
           margin: 0;
           height: 100vh; /*=100%*/
           display: flex; /*flex布局*/
           align-items: center; /*flex布局:垂直居中*/
           justify-content: center; /*flex布局:水平居中*/
           background-color: black;
          }
          .loading {
           width: 10em;
           height: 10em;
           display: grid; /*grid布局*/
           grid-template-columns: repeat(3, 1fr);
           grid-gap: 0.5em; /*grid 每個item之間的間距*/
          }
          /**
          * --name 是css中定義變量的方式
          * 可以直接用 var(--name) 使用
          */
          .loading span {
           background-color: var(--color); /*背景顏色*/
          }
          .loading span:nth-child(2n+2) {
           /*n=0: 2*/
           /*n=1: 4*/
           /*n=2: 6*/
           /*n=3: 8*/
           /*n=4: 10(不存在)*/
           --color: #f13f84;
          }
          .loading span:nth-child(4n+3) {
           /*n=0: 3*/
           /*n=1: 7*/
           /*n=2: 11(不存在)*/
           --color: #46b0ff;
          }
          .loading span:nth-child(4n+1) {
           /*n=0: 1*/
           /*n=1: 5*/
           /*n=2: 9*/
           /*n=3: 13(不存在)*/
           --color: #44bb44;
          }
          

          CSS3 Grid布局實現Loading動畫效果

          grid-template-columns: 該屬性是基于 網格列. 的維度,去定義網格線的名稱和網格軌道的尺寸大小。

          repeat: 表示網格軌道的重復部分,以一種更簡潔的方式去表示大量而且重復列的表達式。

          有了九宮格布局后,我們直接旋轉這個loading元素,制作動畫。

          CSS3動畫

          .loading {
           ...
           transform: rotate(45deg); /*旋轉45°*/
          }
          .loading span {
           background-color: var(--color);
           /**
           * 動畫名字是blinking
           * 動畫整個時間是2s
           * 每個元素的執行延時時間 var(--delay)
           * 動畫的速度曲線 由慢到快 ease-in-out
           * 永久執行 infinite
           */
           animation: blinking 2s var(--delay) ease-in-out infinite;
           animation-fill-mode: backwards;
          }
          /**
          * 每個元素執行動畫延時時間變量
          */
          .loading span:nth-child(7) {
           --delay: 0s;
          }
          .loading span:nth-child(4n+4) {
           --delay: 0.2s;
          }
          .loading span:nth-child(4n+1) {
           --delay: 0.4s;
          }
          .loading span:nth-child(4n+2) {
           --delay: 0.6s;
          }
          .loading span:nth-child(3) {
           --delay: 0.8s;
          }
          /**
          * 動畫效果
          */
          @keyframes blinking {
           0%, 20% {
           transform: rotate(0deg) scale(0);
           }
           40%, 80% {
           /*
           * 旋轉一圈rotate(1turn)[轉、圈(Turns)。一個圓共1圈]
           * 縮放 scale 如果大于1就代表放大;如果小于1就代表縮小
           */
           transform: rotate(1turn) scale(1);
           }
           100% {
           transform: rotate(2turn) scale(0);
           }
          }
          

          animation語法

          animation: name duration timing-function delay iteration-count direction;
          

          1、animation-name 規定需要綁定到選擇器的 keyframe 名稱。

          2、animation-duration 規定完成動畫所花費的時間,以秒或毫秒計。

          3、animation-timing-function 規定動畫的速度曲線。

          4、animation-delay 規定在動畫開始之前的延遲。

          5、animation-iteration-count 規定動畫應該播放的次數。

          6、animation-direction 規定是否應該輪流反向播放動畫。

          CSS3 Grid布局實現Loading動畫效果

          動畫的速度曲線

          1、linear 規定以相同速度開始至結束的過渡效果(等于 cubic-bezier(0,0,1,1))。(勻速)

          2、ease 規定慢速開始,然后變快,然后慢速結束的過渡效果(cubic-bezier(0.25,0.1,0.25,1))(相對于勻速,中間快,兩頭慢)。

          3、ease-in 規定以慢速開始的過渡效果(等于 cubic-bezier(0.42,0,1,1))(相對于勻速,開始的時候慢,之后快)。

          4、ease-out 規定以慢速結束的過渡效果(等于 cubic-bezier(0,0,0.58,1))(相對于勻速,開始時快,結束時候間慢,)。

          5、ease-in-out 規定以慢速開始和結束的過渡效果(等于 cubic-bezier(0.42,0,0.58,1))(相對于勻速,(開始和結束都慢)兩頭慢)。

          6、cubic-bezier(n,n,n,n) 在 cubic-bezier 函數中定義自己的值。可能的值是 0 至 1 之間的數值。

          CSS3 Grid布局實現Loading動畫效果

          總結

          CSS3動畫基礎知識可以看看 《如何快速上手基礎的CSS3動畫》 這篇文章,里面用更小的示例,講述了CSS3動畫的每個屬性。CSS3動畫,無外乎就是animation、transform、transition等屬性的使用,記住他們每個的作用特效就可以了。


          喜歡小編或者覺得小編文章對你有幫助的,可以點擊一波關注哦!

          最近有個圖表需求,怎么配置也配置不好,十分頭疼。所以想借著這個問題手寫實現一個交互體驗還不錯的曲線圖,支持開場動畫、自動根據父盒子寬度適配、比echarts更全的配置項,分區線段的可以更好的自定義等。 效果如下

          更新一波! github.com/ccj-007/spa… 有興趣的可以看看源碼
          同時已經發布到了npm庫 www.npmjs.com/package/spa…

          起源

            visualMap: {
              type: 'piecewise',
              show: false,
              dimension: 0,
              seriesIndex: 0,
              pieces: [
                {
                  gt: 1,
                  lt: 3,
                  color: 'rgba(0, 0, 180, 0.4)'
                },
                {
                  gt: 5,
                  lt: 7,
                  color: 'rgba(0, 0, 180, 0.4)'
                }
              ]
            },
          
          

          這里摘抄的是echarts官網的示例,顏色無法更高的自定義程度, 這種情況想做漸變不太行, 但是分區是實現了,手動狗頭,讓我們看看另外一個示例

            series: [
              {
                type: 'line',
                smooth: 0.6,
                symbol: 'none',
                lineStyle: {
                  color: '#5470C6',
                  width: 5
                },
                markLine: {
                  symbol: ['none', 'none'],
                  label: { show: false },
                  data: [{ xAxis: 1 }, { xAxis: 3 }, { xAxis: 5 }, { xAxis: 7 }]
                },
                areaStyle: {},
                data: [
                  ['2019-10-10', 200],
                  ['2019-10-11', 560],
                  ['2019-10-12', 750],
                  ['2019-10-13', 580],
                  ['2019-10-14', 250],
                  ['2019-10-15', 300],
                  ['2019-10-16', 450],
                  ['2019-10-17', 300],
                  ['2019-10-18', 100]
                ]
              }
            ]
          

          這種情況分區的線段顏色有了,但是漸變卻不能區分,只能統一一個區域的漸變。

          所以在我們平常開發,折線圖或者曲線圖一般用用echarts綽綽有余了,但是總有這么幾個配置讓人抓狂,比如要分區,分區的填充色要做漸變、線段要漸變、要支持hover并更改填充色、label更新等。雖然用echarts的markArea能實現一部分,但是看著那幾個抓狂的api,既然追求完美落地,那就硬著頭皮手寫個吧

          曲線圖

          分析思路

          我們從總的canvas繪圖思路來看,首先要分成3層,紅色區域代表輔助層(軸、標注、輔助線、圖例等)、綠色區域圖表層(折線、曲線等)、藍色區域標簽層(label數據展示卡片等)。為什么要分層,就是為了后期管理圖層能更容易,不然做個動畫、清理畫布也是很麻煩的事情。

          如何做適配

          這里有個細節就是canvas一定要設置width、height而不是canvas.style.width,在窗口縮放場景下會有問題。這是最關鍵的一點,其次我們傳入的axisX和axisY的data一定要知道他只是個份數,我們要映射到的是份數,這樣比如1000px寬的屏幕,我們取10份是100px、500px的屏幕,取10份是50px。我們一般只要考慮寬度的縮放。

          考慮縮放

            // 計算 Y 軸坐標比例尺 ratioY
            maxY = Math.max.apply(null, concatData);
            minY = Math.min.apply(null, concatData);
            rangeY = maxY - minY;
            // 數據和坐標范圍的比值
            ratioY = (height - 2 * margin) / rangeY;
            // 計算 X 軸坐標比例尺和步長
            count = concatData.length;
            rangeX = width - 2 * margin;
            xk = 1, xkVal = xk * margin
            dataLen = data.length
            ratioX = rangeX / (count - dataLen);
            stepX = ratioX;
          

          繪制坐標軸

          /**
           * 繪制坐標軸
           */
          function drawAxis() {
            ctx.beginPath();
            ctx.moveTo(margin, margin);
            ctx.lineTo(margin, height - margin);
            ctx.lineTo(width - margin + 2, height - margin);
            ctx.setLineDash([3, 3])
            ctx.strokeStyle = '#aaa'
            ctx.stroke();
            ctx.setLineDash([1])
            const yLen = newOpt.axisY.data.length
            const xLen = newOpt.axisX.data.length
          
            // 繪制 Y 軸坐標標記和標簽
            for (let i = 0; i < yLen; i++) {
              let y = (rangeY * i) / (yLen - 1) + minY;
              let yPos = height - margin - (y - minY) * ratioY;
          
              if (i) {
                ctx.beginPath();
                ctx.moveTo(margin, yPos);
                ctx.lineTo(width - margin, yPos);
                ctx.strokeStyle = '#ddd'
                ctx.stroke();
              }
          
              ctx.beginPath();
              ctx.stroke();
              newYs = []
              for (const val of options.axisY.data) {
                newYs.push(options.axisY.format(val))
              }
              ctx.fillText(newYs[i] + '', margin - 15 - options.axisY.right, yPos + 5);
              firstEnding && axisYList.push(yPos + 5)
            }
          
            // 繪制 X 軸坐標標簽
            for (let i = 0; i < xLen; i++) {
              let x = i * stepX;
              let xPos = (margin + x);
              if (i) {
                ctx.beginPath();
                ctx.moveTo(xPos, height - margin);
                ctx.lineTo(xPos, margin);
                ctx.strokeStyle = '#ddd'
                ctx.stroke();
              }
              newXs = []
              for (const val of options.axisX.data) {
                newXs.push(options.axisX.format(val))
              }
              ctx.fillText(newXs[i], xPos - 1, height - margin + 10 + options.axisX.top);
              firstEnding && axisXList.push(xPos - 1)
            }
          }
          

          繪制曲線入口

          /**
           * 繪制單組曲線
           * @param data 
           */
          function drawLine(data: any) {
            const { points, id, rgba, lineColor, hoverRgba } = data
            startAreaX = endAreaX
            startAreaY = endAreaY
            // 分割區
            if (firstEnding) {
              areaList.push({ x: startAreaX, y: startAreaY })
            }
          
            function darwColorOrLine(lineMode: boolean) {
              // 繪制折線
              ctx.beginPath();
              ctx.moveTo(id ? margin + endAreaX - xkVal : margin + endAreaX, height - margin - (points[0] - minY) * ratioY);
              ctx.lineWidth = 2
              ctx.setLineDash([0, 0])
          
              let x = 0, y = 0, translateX = 0
              if (id) {
                translateX -= 20
              }
              for (let i = 0; i < points.length; i++) {
                x = i * stepX + margin + endAreaX + translateX
                y = height - margin - (points[i] - minY) * ratioY;
          
                let x0 = (i - 1) * stepX + margin + endAreaX + translateX;
                let y0 = height - margin - (points[i - 1] - minY) * ratioY;
                let xc = x0 + stepX / 2;
                let yc = (y0 + y) / 2;
                if (i === 0) {
                  prePointPosX = x
                  prePointPosY = y
                  ctx.lineTo(x, y);
                  // 這里需要提前考慮是否是線、還是曲線
                  if (!(prePointPosX === x && prePointPosY === y)) {
                    pointList.push({ type: 'line', start: { x: prePointPosX, y: prePointPosY }, end: { x: x, y: y } })
                  }
                } else {
                  ctx.bezierCurveTo(xc, y0, xc, y, x, y);
                  pointList.push({ type: 'curve', start: { x: prePointPosX, y: prePointPosY }, end: { x: x, y: y }, control1: { x: xc, y: y0 }, control2: { x: xc, y: y } })
                }
                prePointPosX = x
                prePointPosY = y
                if (i === points.length - 1) {
                  endAreaX = x
                  endAreaY = y
          
                  if (firstEnding && id === newOpt.data.length - 1) {
                    areaList.push({ x: x, y: y })
                  }
                }
              }
              ctx.strokeStyle = lineColor
              ctx.stroke()
          
              lineMode && ctx.beginPath()
          
              // 右側閉合點
              ctx.lineTo(endAreaX, height - margin)
              // 左側閉合點
              ctx.lineTo(margin + startAreaX, height - margin)
              let startClosePointX = id ? startAreaX : margin + startAreaX
              // 交接閉合點
              ctx.lineTo(startClosePointX, height - margin)
              ctx.strokeStyle = 'transparent'
              lineMode && ctx.stroke();
            }
            darwColorOrLine(false)
            // 漸變
            const gradient = ctx.createLinearGradient(200, 110, 200, 290);
          
            if (isHover && areaId === id) {
              gradient.addColorStop(0, `rgba(${hoverRgba[1][0]}, ${hoverRgba[1][1]}, ${hoverRgba[1][2]}, 1)`);
              gradient.addColorStop(1, `rgba(${hoverRgba[0][0]}, ${hoverRgba[0][1]}, ${hoverRgba[0][2]}, 1)`);
            } else {
              gradient.addColorStop(0, `rgba(${rgba[1][0]}, ${rgba[1][1]}, ${rgba[1][2]}, 1)`);
              gradient.addColorStop(1, `rgba(${rgba[0][0]}, ${rgba[0][1]}, ${rgba[0][2]}, 0)`);
            }
          
            ctx.fillStyle = gradient;
            ctx.fill();
          }
          /**
           * 繪制所有組的曲線
           */
          function startDrawLines() {
            const { data, series } = newOpt
            for (let i = 0; i < data.length; i++) {
              drawLine({ points: data[i], id: i, rgba: series[i].rgba, hoverRgba: series[i].hoverRgba, lineColor: series[i].lineColor })
            }
            firstEnding = false  //由于是不斷繪制,我們需要得到第一次渲染完的我們想要的數組,防止數據被污染
          }
          
          

          這里要注意的是我們的分區一定是線段的閉合,然后通過fillStyle填充顏色。所以你需要在結束點后再lineTo做3次到我的起始點。addColorStop來做漸變。

          繪制貝塞爾曲線

          x = i * stepX + margin + endAreaX + translateX
          y = height - margin - (points[i] - minY) * ratioY;
          let x0 = (i - 1) * stepX + margin + endAreaX + translateX;
          let y0 = height - margin - (points[i - 1] - minY) * ratioY;
          let xc = x0 + stepX / 2;
          let yc = (y0 + y) / 2;
          // ....
          ctx.bezierCurveTo(xc, y0, xc, y, x, y);
          

          具體api不過多闡述,但是我們要知道一個控制點我們的曲線是只有一個方向的,如果兩個控制點,意味著我們曲線可以最多有2個方向。而我們的圖表是分上下需要平滑過渡過去的,這個時候必須用兩個控制點的。

          bezierCurveTo原理

          要想實現bezierCurveTo,其實就是計算得到路徑經過的所有點,而這個更方便我們后期在路徑上的點的獲取。下面的計算會比較復雜,其實就是套用三次貝塞爾曲線的公式罷了

          function getBezierCurvePoints(startX: number, startY: number, cp1X: number, cp1Y: number, cp2X: number, cp2Y: number, endX: number, endY: number, steps: number) {
            let points = [];
          
            // 使用二次貝塞爾曲線近似三次貝塞爾曲線
            let q1x = startX + (cp1X - startX) * 2 / 3;
            let q1y = startY + (cp1Y - startY) * 2 / 3;
            let q2x = endX + (cp2X - endX) * 2 / 3;
            let q2y = endY + (cp2Y - endY) * 2 / 3;
          
            // 采樣曲線上的所有點
            for (let i = 0; i <= steps; i++) {
              let t = i / steps;
              let x = (1 - t) * (1 - t) * (1 - t) * startX +
                3 * t * (1 - t) * (1 - t) * q1x +
                3 * t * t * (1 - t) * q2x +
                t * t * t * endX;
              let y = (1 - t) * (1 - t) * (1 - t) * startY +
                3 * t * (1 - t) * (1 - t) * q1y +
                3 * t * t * (1 - t) * q2y +
                t * t * t * endY;
          
              points.push({ x: +x.toFixed(2), y: +y.toFixed(2) });
            }
          
            return points;
          }
          
          

          三次貝塞爾曲線的公式原理

          公式的推導數學好的大佬可以研究研究,但是他的計算過程還是要知道的,我們可以看到t的值從0到1代表曲線的開始端點和結束端點。t控制著Q1、Q2、Q3的百分比的分別在p1p2、p2p3、p3p4線段的位置,同理也是r1、r2對應的位置,然后再得出r1和r2中的相對位置。所以根本就是t的偏移量在不斷劃分的線段中的位置。t從0到1的所有的點的集合就是構造曲線的集合。

          同時我們根據這個原理,通過Ramer Douglas Peucker 算法可以得出線段的細分,控制曲線是否圓滑。

          如何實現點在路徑上游走

          我們之前能得到曲線上的所有點,只要計算我的clientX是否在路徑點的集合中對應的那個點篩選出來,然后在遮罩層繪制一個圓圈以及輔助線。

          function getAllPoints(segments: PointList) {
            let points = [];
            let lastPoint = null;
          
            // 遍歷所有線段的控制點和終點,將這些點的坐標存儲到數組中
            for (let i = 0; i < segments.length; i++) {
              let segment = segments[i];
              let pointsCount = 50; // 點的數量
              // 如果是直線,則使用lineTo方法連接線段的終點
              if (segment.type === "line") {
                let x0 = segment.start.x;
                let y0 = segment.start.y;
                let x1 = segment.end.x;
                let y1 = segment.end.y;
                for (let j = 0; j <= pointsCount; j++) {
                  let t = j / pointsCount;
                  let x = x0 + (x1 - x0) * t;
                  let y = y0 + (y1 - y0) * t;
                  points.push({ x: +x.toFixed(2), y: +y.toFixed(2) });
                }
                // 如果是曲線,則使用貝塞爾曲線的方法繪制曲線,并將曲線上的所有點的坐標存儲到數組中
              } else if (segment.type === "curve") {
                let x0 = segment.start.x;
                let y0 = segment.start.y;
                let x1 = segment.control1.x;
                let y1 = segment.control1.y;
                let x2 = segment.control2.x;
                let y2 = segment.control2.y;
                let x3 = segment.end.x;
                let y3 = segment.end.y;
                const point = getBezierCurvePoints(x0, y0, x1, y1, x2, y2, x3, y3, pointsCount)
                points.push(...point);
              }
              // 更新線段的起點
              lastPoint = segment.end;
            }
            return points
          }
          
          

          label的數據計算、區間的計算

          我們可以看到填充色和label的數值都已經變化,這里需要監聽下全局的坐標是否在圖表內,如果在內部,就計算pointX = clientX - dom.offsetLeft - dom.margin, y坐標同理。

          /**
           * label顯示
           * @param clientX 
           * @param clientY 
           */
          function drawTouchPoint(clientX: number, clientY: number) {
            cx = clientX, cy = clientY
          
            // 計算當前區間位置
            for (let i = 0; i < areaList.length - 1; i++) {
              const pre = areaList[i].x;
              const after = areaList[i + 1].x;
          
              if (cx > pre && cx < after) {
                areaId = i
              }
            }
            // 計算交叉位置,得到對應的x軸位置,從option的data中取對應的title
            for (let i = 0; i < axisXList.length - 1; i++) {
              const pre = axisXList[i];
              const after = axisXList[i + 1];
              if (cx > pre && cx < after) {
                curInfo.x = i
              }
            }
            for (let i = 0; i < axisYList.length - 1; i++) {
              const max = axisYList[i];
              const min = axisYList[i + 1];
              if (cy < max && cy > min) {
                curInfo.y = i + 1
              }
            }
          
            let crossPoint = pathPoints.find((item: Pos) => {
              const orderNum = .5
              if (Math.abs(item.x - clientX) <= orderNum) {
                return item
              }
            }) as Pos | undefined
            if (crossPoint && canvas) {
              dotCtx.clearRect(0, 0, canvas.width, canvas.height);
          
              dotCtx.beginPath()
              dotCtx.setLineDash([2, 4]);
              dotCtx.moveTo(crossPoint.x, margin)
              dotCtx.lineTo(crossPoint.x, height - margin)
              dotCtx.strokeStyle = '#000'
              dotCtx.stroke()
          
              drawArc(dotCtx, crossPoint.x, crossPoint.y, 5)
          
              //label
              if (!isLabel) {
                labelDOM = document.createElement("div");
                labelDOM.id = 'canvasTopBox'
                labelDOM.innerHTML = ""
                container && container.appendChild(labelDOM)
                isLabel = true
              } else {
                if (labelDOM) {
                  let t = crossPoint.y + labelDOM.offsetHeight > canvas.height - margin ? canvas.height - margin - labelDOM.offsetHeight : crossPoint.y - labelDOM.offsetHeight * .5
                  labelDOM.style.left = crossPoint.x + 20 + 'px'
                  labelDOM.style.top = t + 'px'
                  labelDOM.innerHTML = `
                   <div class='label'>
                     <div class='label-left' style='backGround: ${newOpt.series[areaId].lineColor}'>
                     </div>
                    <div class='label-right'>
                      <div class='label-text'>人數:${newYs[curInfo.y]} </div>
                      <div class='label-text'>訂單數:${newXs[curInfo.x]} </div>
                    </div>
                   </div>
                  `
                } else {
                }
              }
            }
          }
          

          遮罩動畫

          核心原理就是通過clearRect,下面的代碼是從右向左遮罩,所以這里可以直接transfrom: rotate(-180deg)就可以了。

          function drawAnimate() {
            markCtx.clearRect(0, 0, width, height);
          
            markCtx.fillStyle = "rgba(255, 255, 255, 1)"
            markCtx.fillRect(0, 0, width, height);
          
            markCtx.clearRect(
              (width - maskWidth),
              (height - maskHeight),
              maskWidth,
              maskHeight
            );
          
            // 更新遮罩區域大小
            maskWidth += 20;
            maskHeight += 20;
            if (maskWidth < width) {
              animateId = requestAnimationFrame(drawAnimate);
            } else {
              cancelAnimationFrame(animateId)
              watchEvent()
            }
          }
          
          

          option配置入口

          export const options = {
            layout: {
              w: 0,
              h: 0,
              root: '#container',
              m: 30
            },
            data: [[40, 60, 40, 80, 10, 50, 80, 0, 50, 30, 20], [20, 30, 60, 40, 30, 10, 30, 20, 0, 30, 40, 20], [20, 30, 20, 40, 20, 10, 10, 30, 0, 30, 50, 20]],
            axisX: {
              data: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32],
              format(param: string | number) {
                return param + 'w'
              },
              top: 4,
            },
            axisY: {
              data: [0, 20, 40, 60, 80],
              format(param: string | number) {
                return param + '人'
              },
              right: 10,
            },
            series: [
              {
                rgba: [[55, 162, 255], [116, 21, 219]],
                hoverRgba: [[55, 162, 255], [116, 21, 219]],
                lineColor: 'blue'
              },
              {
                rgba: [[255, 0, 135], [135, 0, 157]],
                hoverRgba: [[255, 0, 135], [135, 0, 157]],
                lineColor: 'purple'
              },
              {
                rgba: [[255, 190, 0], [224, 62, 76]],
                hoverRgba: [[255, 190, 0], [224, 62, 76]],
                lineColor: 'orange'
              }
            ]
          }
          
          


          總結

          canvas的核心就是點的處理,在一些曲線銜接、路徑的獲取會比較復雜,同時如何管理好圖層是很重要的,本曲線圖底部是輔助圖層不做變化,曲線是需要做動畫的話,最好就單獨做個圖層,頂部在來個遮罩做標簽等元素,為了更方便做自定義,我們也沒必要用canvas繪制,直接dom或svg渲染就行。


          原文鏈接:https://juejin.cn/post/7224886702883258424
          著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

          Web 開發中,有時我們需要實現不同頁面之間的數據傳遞和事件觸發,比如一個頁面打開了另一個頁面,然后在新的頁面中操作后需要更新原來的頁面的內容。這種場景在電商、支付、社交等領域都很常見,那么如何用js來實現不同頁面之間的交互呢?本文提供幾種常見的方法供大家學習參考!

          一、localStorage

          在 Web Storage 中,每一次將一個值存儲到本地存儲時,都會觸發一個 storage 事件,通過 localStorage 結合 window.addEventListener('storage', cb) 完成 A、B 標簽頁間通信。

          // A標簽頁
          localStorage.setItem('send-msg', JSON.stringify({
              name: 'hzd',
              age: '18',
          }))
          
          // B標簽頁
          window.addEventListener('storage', (data) => {
              try {
                  console.log(data)
                  const msg = JSON.parse(data.newValue)
              } catch (err) {
                  // 處理錯誤
              }
          })
          


          在控制臺打印一下 data 的值,可以看到挺多信息:

          二、BroadcastChannel

          BroadcastChannel 通信方式的原理就是一個命名管道,它允許讓指定的同源下瀏覽器不同的窗口來訂閱它。

          每個 BroadcastChannel 對象都需要使用一個唯一的名稱來標識通道,這個名稱在同一域名下的不同頁面之間必須是唯一的,它允許同一域名下的不同頁面之間進行通信。

          通過 postMessage 方法,一個頁面可以將消息發送到頻道中,而其他頁面則可以監聽 message 事件來接收這些消息。通過這種方式是短線了一種實時通信的機制,可以在不同的頁面之間傳遞信息,實現頁面間的即時交流。如下圖所示:

          // A頁面
          const bc = new BroadcastChannel("test_channel");
          bc.postMessage("This is a test message.");
          
          
          // B頁面
          const bc = new BroadcastChannel("test_channel");
          bc.onmessage = (event) => {
            console.log(event);
          };
          


          三、postMessage

          postMessage 是 H5 引入的 API,該方法允許來自不同源的腳本采用異步方式進行有效的通信,可以實現跨文本文檔、多窗口、跨域消息傳遞,多用于窗口間數據通信,這也使它成為跨域通信的一種有效的解決方案。

          下面看兩個簡單的使用例子:

          示例一:

          // 發送端:
          
          <button id="btn">發送消息</button>
          
          <script>
            let device = window.open('http://localhost:63342/signal_communication/postMessage/receive.html')
          
            document.getElementById('btn').addEventListener('click', event => {
              device.postMessage('發送一條消息')
            })
          </script>
          


          // 接收端:
          
          <script>
            window.addEventListener('message', event => {
              console.log(event)
            })
          </script>
          


          示例二:

          // 發送端:
          
          <div>
              <input id="text" type="text" value="Runoob" />
              <button id="sendMessage" >發送消息</button>
          </div>
          <iframe id="receiver" src="https://c.runoob.com/runoobtest/postMessage_receiver.html" width="300" height="360">
              <p>你的瀏覽器不支持 iframe。</p>
          </iframe>
          <script>
          window.onload = function() {
              let receiver = document.getElementById('receiver').contentWindow;
              let btn = document.getElementById('sendMessage');
              btn.addEventListener('click', function (e) {
                  e.preventDefault();
                  let val = document.getElementById('text').value;
                  receiver.postMessage("Hello "+val+"!", "https://c.runoob.com");
              });
          }
          </script>
          


          // 接收端:
          
          <div id="recMessage">Hello World!</div>
          
          <script>
          window.onload = function() {
              let messageEle = document.getElementById('recMessage');
              window.addEventListener('message', function (e) {  // 監聽 message 事件
                  alert(e.origin);
                  if (e.origin !== "https://www.runoob.com") {  // 驗證消息來源地址
                      return;
                  }
                  messageEle.innerHTML = "從"+ e.origin +"收到消息: " + e.data;
              });
          }
          </script>
          


          四、SharedWorker

          SharedWorker 是一種在 Web 瀏覽器中使用的 Web API,它允許不同的瀏覽上下文,如不同的瀏覽器標簽頁之間共享數據和執行代碼。它可以用于在多個瀏覽上下文之間建立通信通道,以便它們可以共享信息和協同工作。

          與普通的 Worker 不同,SharedWorker 可以在多個瀏覽上下文中實例化,而不僅限于一個單獨的瀏覽器標簽頁或框架。這使得多個瀏覽上下文可以共享同一個后臺線程,從而更有效地共享數據和資源,而不必在每個標簽頁或框架中都創建一個獨立的工作線程。

          <!-- a.html -->
          <script>
            let index = 0;
            const worker = new SharedWorker("worker.js");
          
            setInterval(() => {
              worker.port.postMessage(`moment ${index++}`);
            }, 1000);
          </script>
          
          <!-- b.html -->
          <script>
            const worker = new SharedWorker("worker.js");
          
            worker.port.start();
            setInterval(() => {
              worker.port.postMessage("php是世界上最好的語言");
            }, 1000);
          
            worker.port.onmessage = function (e) {
              if (e.data) {
                console.log(e.data);
              }
            };
          </script>
          


          創建一個 worker.js 文件,并編寫以下代碼:

          let data = "";
          
          self.onconnect = (e) => {
            const port = e.ports[0];
          
            port.onmessage = function (e) {
              if (e.data === "php是世界上最好的語言") {
                port.postMessage(data);
                data = "";
              } else {
                data = e.data;
              }
            };
          };
          


          最終代碼運行效果如下圖所示:

          五、Service Worker

          Service Worker 它是一種服務工作線程,是一種在瀏覽器背后運行的腳本,用于處理網絡請求和緩存等任務。它是一種在瀏覽器與網絡之間的中間層,允許開發者攔截和控制頁面發出的網絡請求,以及管理緩存,從而實現離線訪問、性能優化和推送通知等功能。

          它在瀏覽器背后獨立運行與網頁分開,這意味著即使用戶關閉了網頁,Service Worker 仍然可以運行。可以用于實現推送通知功能。它可以注冊為推送消息的接收者,當服務器有新的通知要發送時,Service Worker 可以顯示通知給用戶,即使網頁沒有打開。

          要想使用,首先我們創建兩個不同的 html 文件分別代表不同的頁面,創建一個 Service Worker 文件,并且使用 live server 開啟一個本地服務器:

          <!-- a.html -->
          
          <!DOCTYPE html>
          <html lang="en">
            <head>
              <meta charset="UTF-8" />
              <meta name="viewport" content="width=device-width, initial-scale=1.0" />
              <title>Document</title>
            </head>
            <body>
              <script>
                navigator.serviceWorker.register("worker.js").then(() => {
                  console.log("注冊成功");
                });
          
                setInterval(() => {
                  navigator.serviceWorker.controller.postMessage({
                    value: `moment ${new Date()}`,
                  });
                }, 3000);
          
                navigator.serviceWorker.onmessage = function (e) {
                  console.log(e.data.value);
                };
              </script>
            </body>
          </html>
          
          <!-- b.html -->
          
          <!DOCTYPE html>
          <html lang="en">
            <head>
              <meta charset="UTF-8" />
              <meta name="viewport" content="width=device-width, initial-scale=1.0" />
              <title>Document</title>
            </head>
            <body>
              <script>
                navigator.serviceWorker.register("worker.js").then(() => {
                  console.log("注冊成功");
                });
          
                setInterval(() => {
                  navigator.serviceWorker.controller.postMessage({
                    value: `moment ${new Date()}`,
                  });
                }, 3000);
          
                navigator.serviceWorker.onmessage = function (e) {
                  console.log(e.data.value);
                };
              </script>
            </body>
          </html>
          


          創建一個 worker.js 文件并編寫以下代碼:

          // worker.js
          self.addEventListener("message", function (e) {
            e.waitUntil(
              self.clients.matchAll().then(function (clients) {
                if (!clients || clients.length === 0) {
                  return;
                }
                clients.forEach(function (client) {
                  client.postMessage(e.data);
                });
              })
            );
          });
          


          最終代碼運行如下圖所示:

          你所編寫的 Service Worker 將遵守以下生命周期:

          1. 注冊: 在網頁的 JavaScript 代碼中調用 navigator.serviceWorker.register() 方法來注冊一個 Service Worker
          2. 安裝: 當 Service Worker 文件被下載并首次運行時,會觸發 install 事件。在 install 事件中,你可以緩存靜態資源,如 HTML、CSS、JavaScript 文件,以便在離線時使用
          3. 激活: 安裝成功后,Service Worker 并不會立即接管頁面的網絡請求。它需要等到之前的所有頁面都關閉,或者在下次頁面加載時才會激活()
          4. 就是因為編寫的 worker 代碼不生效,一直刷新都不生效,直到我關機重啟了才生效的......
          5. 控制: 一旦 Service Worker 被激活,它就開始控制在其作用域內的頁面。它可以攔截頁面發出的網絡請求,并根據緩存策略返回緩存的內容
          6. 更新: 當你更新 Service Worker 文件并再次注冊時,會觸發一個新的 install 事件。你可以在新的 install 事件中更新緩存,然后在下次頁面加載時進行激活,以確保新的 Service Worker 被使用
          7. 解除注冊: 如果你不再需要,可以通過調用 navigator.serviceWorker.unregister() 來解除注冊

          六、IndexDB

          IndexedDB 是一種在瀏覽器中用于存儲和管理大量結構化數據的 Web API。它提供了一種持久性存儲解決方案,允許 Web 應用程序在客戶端存儲數據,以便在不同會話、頁面加載或瀏覽器關閉之間保留數據。

          與傳統的 cookie 或 localStorage 等存儲方式不同,IndexedDB 更適合存儲復雜的、結構化的數據,例如對象、數組、鍵值對等。這使得它特別適用于應用程序需要存儲大量數據、執行高級查詢或支持離線工作的情況。

          要實現跨標簽通信,如下代碼所示:

          <!-- a.html -->
          <script>
            let index = 0;
            // 打開或創建 IndexedDB 數據庫
            const request = indexedDB.open("database", 1);
          
            request.onupgradeneeded = (event) => {
              const db = event.target.result;
              const objectStore = db.createObjectStore("dataStore", {
                keyPath: "key",
              });
            };
          
            request.onsuccess = (event) => {
              const db = event.target.result;
              const transaction = db.transaction(["dataStore"], "readwrite");
              const objectStore = transaction.objectStore("dataStore");
          
              // 存儲數據
          
              objectStore.put({ key: "supper", value: `moment` });
          
              transaction.oncomplete = () => {
                db.close();
              };
            };
          </script>
          
          <!-- b.html -->
          <script>
            // 打開相同的 IndexedDB 數據庫
            const request = indexedDB.open("database", 1);
          
            request.onsuccess = (event) => {
              const db = event.target.result;
              const transaction = db.transaction(["dataStore"], "readonly");
              const objectStore = transaction.objectStore("dataStore");
          
              // 獲取數據
              const getRequest = objectStore.get("supper");
          
              getRequest.onsuccess = (event) => {
                const data = event.target.result;
                if (data) {
                  console.log(data.value);
                }
              };
          
              transaction.oncomplete = () => {
                db.close();
              };
            };
          </script>
          


          最終代碼運行如下圖所示:

          七、cookie

          <!-- a.html -->
          <script>
            let index = 0;
            setInterval(() => {
              document.cookie = `supper=moment ${index++}`;
            }, 1000);
          </script>
          
          <!-- b.html -->
          <script>
            console.log("cookie 的值為: ", document.cookie);
          
            setInterval(() => {
              console.log("cookie 的值發生了變化: ", document.cookie);
            }, 1000);
          </script>
          


          具體代碼運行效果如下圖所示:


          作者:前端掘金者H
          鏈接:https://juejin.cn/post/7268602250653319202


          主站蜘蛛池模板: 亚洲国产一区二区a毛片| 色欲综合一区二区三区| 99久久精品国产高清一区二区| 亚洲老妈激情一区二区三区| 一本一道波多野结衣AV一区| 少妇激情AV一区二区三区 | 无码人妻精品一区二区蜜桃百度| 国产一区二区三区播放| 一区二区在线免费观看| 亚欧色一区W666天堂| 国产精品亚洲一区二区三区久久 | 精品福利一区二区三区免费视频| 国产精品福利一区二区| 精品国产一区二区三区av片| 亚洲一区二区三区深夜天堂| 亚洲av午夜福利精品一区| 亚洲国产精品一区二区九九| 亚洲综合一区无码精品| 亚洲Av无码国产一区二区| 久久91精品国产一区二区| 国产伦精品一区二区三区视频小说 | 色一情一乱一区二区三区啪啪高| 精品一区二区久久久久久久网精| 日本成人一区二区三区| 国产精品亚洲专区一区| 动漫精品一区二区三区3d| 无码毛片一区二区三区视频免费播放| 一本AV高清一区二区三区| 中文字幕不卡一区| 精品少妇一区二区三区视频| 久久精品国产一区二区三区日韩| 日本精品一区二区三区四区| 精品国产一区二区三区在线| 国产成人av一区二区三区不卡 | 成人免费观看一区二区| 久久se精品一区精品二区| 中文字幕在线观看一区二区三区| 精品久久综合一区二区| 一区二区视频在线观看| 亚洲免费一区二区| 久夜色精品国产一区二区三区|