CSS SandBox

来源:https://www.cnblogs.com/dtux/archive/2022/06/22/16399387.html
-Advertisement-
Play Games

引言 本篇文章主要介紹的是關於CSS Sandbox的一些事情,為什麼要介紹這個呢?在我們日常的開發中,樣式問題其實一直是一個比較耗時的事情,一方面我們根據 UI 稿不斷的去調整,另一方面隨著項目越來越大可能哪一次開發就發現——誒,我的樣式怎麼不起作用了,亦或是怎麼被另一個樣式所覆蓋了。原因可能有很 ...


引言

本篇文章主要介紹的是關於CSS Sandbox的一些事情,為什麼要介紹這個呢?在我們日常的開發中,樣式問題其實一直是一個比較耗時的事情,一方面我們根據 UI 稿不斷的去調整,另一方面隨著項目越來越大可能哪一次開發就發現——誒,我的樣式怎麼不起作用了,亦或是怎麼被另一個樣式所覆蓋了。原因可能有很多:

  • 不規範的命名導致重覆
  • 為了簡單,直接添加全局樣式的修改
  • 樣式的不合理復用
  • 多個項目合併時,每個子項目都有自己的獨立樣式和配置,可能在自己項目中不存在這樣的問題,但是合併以後互相影響造成了樣式污染
  • 第三方框架引入
  • ……

CSS Sandbox正式為了隔離樣式,從而解決樣式污染的問題

應用場景

通過上述我們瞭解了樣式污染產生的原因,從中我們也可以總結一下哪些場景時我們需要使用CSS Sandbox進行樣式隔離呢

  • 微前端場景下的父子以及子子應用
  • 大型項目以及複雜項目的樣式衝突
  • 第三方框架以及自定義主題樣式的覆蓋
  • ……

常見的解決方案

既然說了這麼多樣式污染產生的原因和應用場景,那我們該如何解決他們呢,目前有以下幾種解決方案,其實解決的核心還是不變的——使CSS選擇器作用的Dom元素唯一

file

Tips:當我們在實際的開發中可以根據項目的實際情況進行選擇

CSS in JS

看名字是不是感覺很高級,直譯下就是用 JS 去寫 CSS 樣式,而不是寫在單獨的樣式文件里。例如:

<p style='color:red'>css in js</p>

這和我們傳統的開發思想很不一樣,傳統的開發原則是關註點分離,就比如我們常說的不寫行內樣式行內腳本,即 HTML、JS、CSS 都寫在對應的文件里。

關於 CSS in JS 不是一個新興的技術,他的熱度主要出現於一些 Web 框架的發展,比如說:React,它所支持的 jsx 語法,可以讓我們在一個文件中同時寫 js、html 和 css,並且組件內部管理自己的樣式、邏輯,組件化開發的思想深入人心。

const style = {
	color: 'red'
}

ReactDOM.render(
  <p style={style}>
     css in js
  </h1>,
  document.getElementById('main')
);

每個組件的樣式由自身的 style 決定,不依賴也不影響外部,從這一點來看確實實現了樣式隔離的效果。

關於Css in js的庫也有很多,比如說:

其中 styled-components 會動態生成一個選擇器

import styled from 'styled-components'

function App() {
  const Title = styled.h1`
    font-size: 1.5em;
    text-align: center;
    color: palevioletred;
  `;

  return (
    <div>
      <Title>Hello World, this is my first styled component!</Title>
    </div>
  );
}

優缺點

| 優點 | • 沒有作用域的樣式污染問題(主要指的是通過寫內行樣式以及生成唯一的 CSS 選擇器)

• 減少了無用樣式的堆積,刪除組件即刪除對應的樣式

• 通過導出定義的樣式變數方便進行復用和重構 |
| --- | --- |
| 缺點 | • 內聯樣式不支持偽類和選擇器等寫法
• 代碼的可讀性比較差,違背了關註點分離的原則

• 運行時會消耗性能,動態生成 CSS(我們在寫 CSS 時其實還是 js)

• 不能結合一些 CSS 預處理器,無法進行預編譯 |

樣式約定

通過約應用的命名首碼實現統一的開發和維護,比如說 BEM 的命名方式,通過對塊、元素以及修飾符三者的命名來規範的描述一個組件

.dropdown-menu__item-button--disabled

優缺點

| 優點 | • 樣式隔離
• 語義化強,組件可讀性高 |
| --- | --- |
| 缺點 | • 命名太長
• 依賴於開發者的命名 |

預處理器

通過 CSS 預處理器可以處理很多獨特的語法格式,比如:

  • 可嵌套性
body {
	with: 20px;
	p {
		color: red;
	}
}
  • 父選擇器
body {
	with: 20px;
	&:hover {
		with: 30px;
	}
}
  • 屬性繼承
.dev {
	width: 200px;
}

span {
	.dev
}

通過這些特殊的語法讓 CSS 更容易解讀和維護

一些常見的市場上的預處理器

  • Sass
  • Less
  • Stylus
  • PostCss

優缺點

| 優點 | • 可讀性較好,方便理解和維護 DOM 結構
• 利用嵌套等方式,也可以大幅度解決樣式污染的問題 |
| --- | --- |
| 缺點 | •需要增加額外的包,藉助相關編譯工具 |

Tips:通常與類似於 BEM 的命名方式結合,可以達到提高開發效率,增強可讀性以及復用的效果

CSS Module

顧名思義就是將 CSS 進行模塊化處理,編譯好後可以避免樣式被污染的問題,不過依賴於Webpack需要配置css-loader等打包工具,以下是我在create-react-app創建的項目中運行,由於其已經在 webpack 配置了css-loader,因此在此篇文章中不展示具體配置

index.ts 文件

import style from './style.module.css'

function App() {

  return (
    <div>
      <p className={style.text}>Css Module</p>
    </div>
  );
}

style.module.css 文件

.text {
  color: red;
}

// 等同於
:local(.text) {
    color: blue;
}

// 還有一種全局模式,此時不會進行編譯
:global(.text) {
    color: blue;
}

file

打包工具會同時把 style.text 以及 text 編譯成獨一無二的值

優缺點

| 優點 | • 學習成本較低,不依賴於人工約束

• 基本上能 100%解決樣式污染問題

• 方便實現模塊的復用 |
| --- | --- |
| 缺點 | • 只能在構建時使用,依賴於 css-loader 等

• 可讀性差,在控制台調試時出現 hash 值不方便調試 |

Shadow DOM

它可以將一個隱藏且獨立的 DOM 附加到一個元素上。當我們用 Shadow DOM 包裹一個元素後,其內樣式不會對外部樣式造成影響,外部樣式也不會對其內部造成影響

file

// 創建一個shadow dom,我這裡是通過ref去拿附著的節點,一般可以用document去拿
import './App.css'; // 定義了shadow-text的樣式

function App() {
  const divRef = useRef(null)

  useEffect(() => {
    if(divRef?.current) {
      const { current } = divRef
      const shadow = current.attachShadow({mode: 'open'}); // mode用來控制能否用js獲取shaow dom內的元素
      shadow.innerHTML = '<p className="shadow-text">Here is some new text</p>';
    }
  }, [])

  return (
    <div>
      <div ref={divRef} className='shadow-host'></div>
    </div>
  );
}

file

外部樣式無法影響 shadow dom 內部的樣式

我們再來看下 shadow dom 內部得樣式會影響外部樣式嗎?

function App() {
  useEffect(() => {
    if(divRef?.current) {
      const { current } = divRef
      const shadow = current.attachShadow({mode: 'open'});
      shadow.innerHTML = '<style>.shadow-h1 { color: red } </style><p class="shadow-h1">Here is some new text</p>';
      
    }
  }, [])

  return (
    <div>
      <Title>Hello World, this is my first styled component!</Title>
      <h1 className='shadow-h1'>lalla1</h1>
      <div ref={divRef} className='shadow-host'></div>
    </div>
  );
}

file

但是也有例外,除了[:focus-within](https://developer.mozilla.org/zh-CN/docs/Web/CSS/:focus-within)

import { useEffect, useRef } from 'react'
import './App.css'; // .shadow-host:focus-within { background-color: yellow;}

function ShadowExample() {
  const divRef = useRef(null)

  useEffect(() => {
    if(divRef?.current) {
      const { current } = divRef
      const shadow = current.attachShadow({mode: 'open'});
      shadow.innerHTML = '<input class="shadow-h1"/>';
      
    }
  }, [])

  return (
    <div>
      <p>Css Module</p>
      <div ref={divRef} className='shadow-host'></div>
    </div>
  );
}

export default ShadowExample;

file

問題

正由於shadow dom內的樣式只會應用於內部,如果我們在 shadow dom 內部用了類似於antdModal這些創建於document.body下的彈窗或者其他組件時,無法應用於antd的樣式,需要把antd的樣式放到上一層中。

優缺點

| 優點 | • 不需要引入額外的包,瀏覽器原生支持

• 嚴格隔離 |
| --- | --- |
| 缺點 | • 在某些場景下可能出現樣式失效的問題,如上問題中的 shadow dom 內創建了全局的 Modal |

淺析 QianKun 中的 CSS SandBox

上面我們講解了一些實現樣式隔離的基本方案,那作為一個比較成熟的微前端框架QianKun中又是怎麼實現樣式隔離方案的呢,以下的源碼解析是在v2.6.3的版本上研究的,首先通過看文檔可以發現

file

在 QianKun 中 CSS SandBox 有兩種模式:

  • strictStyleIsolation——嚴格沙箱模式
  • experimentalStyleIsolation——實驗性沙箱模式

strictStyleIsolation

需要註意的是該方案不是一個無腦的解決方案,開啟後需要進行一定的適配

下麵我們來詳細介紹下該模式:

我們設置strictStyleIsolationtrue時,QianKun採用的是Shadow DOM方案,核心就是為每個微應用包裹上一個 Shadow DOM 節點。接下來我們看下是怎麼實現的

先來個流程圖我們有個大致的概念:

file

  • **registerMicroApps**:註冊子應用,同時調用 single-spa 中的registerApplication進行註冊
  • **loadApp**:載入子應用,初始化載入子應用的 Dom 結構,創建樣式沙箱和 JS 沙箱等,同時返回不同階段的生命周期
  • **createElement**:樣式沙箱的具體實現,主要分為兩種strictStyleIsolationexperimentalStyleIsolation

file

file

registerMicroApps:註冊子應用

export function registerMicroApps<T extends ObjectType>(apps: Array<RegistrableApp<T>>,lifeCycles?: FrameworkLifeCycles<T>,) {
...
    registerApplication({
      name,
      app: async () => {
        ...
        // 載入微應用的具體方法,暴露bootstrap、mount、unmount等生命周期以及一些其他配置信息
        const { mount, ...otherMicroAppConfigs } = (
          await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
        )();
				...
      },
      // 子應用的激活條件
      activeWhen: activeRule
			...
    });
  });
}

調用 single-spa 的 registerApplication 對應用進行註冊,並且在應用激活的時候調用 app 的回調,其中最主要的是loadApp載入微應用的具體方法

一些參數的說明:

  • apps:微應用的註冊信息

file

  • lifeCycles:微應用的一些生命周期鉤子

file

loadApp:載入子應用

function loadApp (app: LoadableApp<T>, configuration: FrameworkConfiguration = {},lifeCycles?: FrameworkLifeCycles<T>) {
...
/**
 * 將操作權交給主應用控制,返回結果涉及CSS SandBox和JS SandBox
 * template --template的為link替換為style註釋script的HTML模版
 * execScripts --腳本執行器,讓指定的腳本(scripts)在規定的上下文環境中執行,只做瞭解暫時不講
 * assetPublicPath -- 靜態資源地址,只做瞭解暫時不講
 */
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);

// 給子應用包裹一層div後的子應用html模版, 是模版字元串的形式
const appContent = getDefaultTplWrapper(appInstanceId)(template);

let initialAppWrapperElement: HTMLElement | null = createElement(
    appContent,
		// 是否開啟了嚴格模式
    strictStyleIsolation,
		// 是否開啟實驗性的樣式隔離
    scopedCSS,
		// 根據應用名生成的唯一值,唯一則為appName,不唯一則為appName_[count]為具體數量,重覆會count++
    appInstanceId,
  );
...
// 下麵還有一些生命周期的處理方法
}

Q1:到現在不知道還有沒有人記得我們開啟嚴格樣式模式是需要做啥?

!!!把子應用的 Dom 結構放到 Shadow dom 中與主應用隔離,防止樣式污染

Q2:那我們咋拿到子應用的 Dom 結構呢?

沒錯就是通過import-html-entry庫的import-html-entry方法,有興趣給看下關於import-html-entry 解析

file

沒錯我們拿到了templateexecScriptsassetPublicPath,這裡我們不對後兩個進行講解,聚焦到template上:

對比下子應用原來的 HTML 結構

file

可以發現我們拿到的templatelink標簽變成style標簽註釋了script的 HTML 模版,其中就有我們需要的子應用的 Dom 結構。

拿到以後 QianKun 里又在template上包裹了一層 Div 形成一個新的 HTML 結構的模版字元串,這是為什麼呢?主要是為了在主應用中標識該節點下的內容為子應用,當然在後面我們也需要它進行特別的處理,這個後面講到的時候再說。因此我們現在拿到的appContent長成這個樣子:

file

這個 div 的 id 是唯一的哈!!!

那我們現在是不是已經做好了前期準備,現在我們需要進入最後一個步驟,把子應用的這個 Dom 結構掛載到一個 shadow dom 上,這就要用到createElement方法。

進入createElement方法前我們先來看下目前的參數值:

  • appContent:包裹了一層 id 唯一的 div,具體如上所示
  • strictStyleIsolation:true
  • scopedCSS:false
  • appInstanceId:react16

createElement:添加 shadow dom

那我們現在如何去創建一個 shadow dom,在前面關於 shadow dom 的講解中我們知道,創建一個 shadow dom 我們需要兩個東西:

一、掛載的 Dom 節點

二、需要添加到 shadow dom 的內容

那我們從哪裡去找呢,根據傳進來的參數吧,我們無疑是要對appContent進行處理了,回顧下appContent有什麼,包裹了一層 div 的子應用的 HTML 模版是吧,自然而然的我們就可以以外面的 div 為掛載的 dom 節點,拿子應用的 HTML 模版為需要添加到 shadow dom 的內容,即:

file

但是問題又來了, 目前的appContent是模版字元串嘞,我們咋辦?這邊 QianKun 的處理方案是:

file

這隻是個大致流程,下麵讓我們跟著這樣的思想看下代碼里處理:

function createElement(appContent: string,strictStyleIsolation: boolean,scopedCSS: boolean,appInstanceId: string) {
...
const containerElement = document.createElement('div');
  containerElement.innerHTML = appContent;
  const appElement = containerElement.firstChild as HTMLElement;
	// 嚴格樣式沙箱模式
  if (strictStyleIsolation) {
    if (!supportShadowDOM) {
      console.warn(
        '[qiankun]: As current browser not support shadow dom, your strictStyleIsolation configuration will be ignored!',
      );
    } else {
      const { innerHTML } = appElement;
      appElement.innerHTML = '';
      let shadow: ShadowRoot;

			// 創建shadow dom節點
      if (appElement.attachShadow) {
        shadow = appElement.attachShadow({ mode: 'open' });
      } else {
        // 相容低版本
        shadow = (appElement as any).createShadowRoot();
      }
      shadow.innerHTML = innerHTML;
    }
  }
...
// 此處省略了開啟experimentalStyleIsolation的處理方法
...
return appElement;
}

這裡有個很有意思的是:

appContent 以 innerHTML 變成 dom 結構後,HTML 模版中的<html><head>以及<body>會被去掉

file

最後我們再來看下子應用掛載到主應用的 Dom 結構:

file

筆者在實踐的過程中也遇到了一些問題:

1、微應用中使用相對路徑引入圖片出現載入資源 404 的問題,這邊筆者沒有進行過多的嘗試可以參考下官方的:https://qiankun.umijs.org/zh/faq#為什麼微應用載入的資源會-404

2、還有一個問題就是 react 中動態打開 Modal 失效的問題,原因可以看下‣,大概看了下和 React 的事件機制有關,即使是設置彈窗預設開啟,也會出現之前上面提到的,樣式丟失的問題

experimentalStyleIsolation

我們設置experimentalStyleIsolationtrue時,QianKun採用的是Runtime css transformer 動態載入/卸載樣式表方案,為子應用的樣式表增加一個特殊的選擇器從而限定影響範圍,類似以下結構:

// 假設應用名是 react16
<style>
	.app-main {
	  font-size: 14px;
	}
</style>

<style>
	div[data-qiankun="react16"] .app-main {
	  font-size: 14px;
	}
<style>

先來通過流程圖瞭解下大致流程

file

createElement:給最外層增加 data-qiankun 屬性,並且獲取所有 style 標簽

function createElement(appContent: string, strictStyleIsolation: boolean, scopedCSS: boolean,appInstanceId: string) {
...
if (scopedCSS) {
		// 給最外層設置data-qiankun的屬性
    const attr = appElement.getAttribute(css.QiankunCSSRewriteAttr);
    if (!attr) {
      appElement.setAttribute(css.QiankunCSSRewriteAttr, appInstanceId);
    }
		// 獲取所有的style標簽,進行遍歷
    const styleNodes = appElement.querySelectorAll('style') || [];
    forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
      css.process(appElement!, stylesheetElement, appInstanceId);
    });
  }
...
}

export const QiankunCSSRewriteAttr = 'data-qiankun';

我們來看下設置完屬性後的屬性後的 appElement

file

styleNodes

file

css.process 詳細處理

/**
* 實例化ScopedCSS
* 生成根元素屬性選擇器[data-qiankun="應用名"]首碼
*/
export const process = (
  appWrapper: HTMLElement,
  stylesheetElement: HTMLStyleElement | HTMLLinkElement,
  appName: string,
): void => {
  // 實例化ScopedCSS
  if (!processor) {
    processor = new ScopedCSS();
  }
	...
	// 一些空值的處理
  const mountDOM = appWrapper;
  if (!mountDOM) {
    return;
  }

  const tag = (mountDOM.tagName || '').toLowerCase();

  if (tag && stylesheetElement.tagName === 'STYLE') {
		// 生成首碼,根元素標簽名[data-qiankun="應用名"]
    const prefix = `${tag}[${QiankunCSSRewriteAttr}="${appName}"]`;
    processor.process(stylesheetElement, prefix);
  }
};
  • prefix:
    div[data-qiankun="react16"]
  • stylesheetElement:

file

進入 processor.process 看看對它進行了什麼操作

// 重寫樣式選擇器以及對於空的style節點設置MutationObserver監聽,原因可能存在動態增加樣式的情況
process(styleNode: HTMLStyleElement, prefix: string = '') {
		// 當style標簽有內容時進行操作
    if (styleNode.textContent !== '') {
			// styleNode.textContent為style標簽內的內容
      const textNode = document.createTextNode(styleNode.textContent || '');
			// swapNode為創建的空的style標簽
      this.swapNode.appendChild(textNode);
			// 獲取樣式表
      const sheet = this.swapNode.sheet as any;
			// 從樣式表獲取cssRules該值是標準的,把樣式規則從偽數組轉化成數組
      const rules = arrayify<CSSRule>(sheet?.cssRules ?? []);
			// 通過遍歷和正則重寫每個選擇器的首碼
      const css = this.rewrite(rules, prefix);
			// 將處理後的重寫後的css放入原來的styleNode中
      styleNode.textContent = css;
      // 清理工具人swapNode
      this.swapNode.removeChild(textNode);
      return;
    }

		//對空的樣式節點進行監聽,可能存在動態插入的問題
    const mutator = new MutationObserver((mutations) => {
      for (let i = 0; i < mutations.length; i += 1) {
				// mutation為變更的每個記錄MutationRecord
        const mutation = mutations[i];

				// 判斷該節點是否應處理過
        if (ScopedCSS.ModifiedTag in styleNode) {
          return;
        }

        if (mutation.type === 'childList') {
          const sheet = styleNode.sheet as any;
          const rules = arrayify<CSSRule>(sheet?.cssRules ?? []);
          const css = this.rewrite(rules, prefix);

          styleNode.textContent = css;
          // 增加處理節點的標識
          (styleNode as any)[ScopedCSS.ModifiedTag] = true;
        }
      }
    });

    // 監聽當前的style標簽,當styleNode為空的時候,以及變更的時候,比如引入的antd樣式文件
    mutator.observe(styleNode, { childList: true });
  }

Q1:為什麼在style標簽有內容的時候使用this.swapNode這個工具人,而在監聽的時候不使用?

還記得我們是需要乾什麼嗎?

改寫style標簽內的樣式規則

因此這裡就通過style.sheet.cssRules方式去獲取 style 標簽里的每一條規則進行重寫,我們來看下sheet樣式表的數據結構

file

通過這個結構我們其實下一步想要做的事情很清楚了

就是重寫每一條cssRules並且通過字元串拼接賦值給style標簽

file

但是我們得註意兩點:

  • 選擇器不同我們的處理方式也不同
  • 對選擇器的匹配規則的處理

讓我們看看 rewrite 具體進行了什麼操作,這裡主要分為兩塊

private rewrite(rules: CSSRule[], prefix: string = '') {
    let css = '';

    rules.forEach((rule) => {
      switch (rule.type) {
				// 普通選擇器類型
        case RuleType.STYLE:
          css += this.ruleStyle(rule as CSSStyleRule, prefix);
          break;
				// @media選擇器類型
        case RuleType.MEDIA:
          css += this.ruleMedia(rule as CSSMediaRule, prefix);
          break;
				// @supports選擇器類型
        case RuleType.SUPPORTS:
          css += this.ruleSupport(rule as CSSSupportsRule, prefix);
          break;
        default:
          css += `${rule.cssText}`;
          break;
      }
    });

    return css;
  }
  • 二是進行正則替換

特殊的

// 處理類似於@media screen and (min-width: 900px) {}
private ruleMedia(rule: CSSMediaRule, prefix: string) {
  const css = this.rewrite(arrayify(rule.cssRules), prefix);
  return `@media ${rule.conditionText} {${css}}`;
}

// 處理類似於@supports (display: grid) {}
private ruleSupport(rule: CSSSupportsRule, prefix: string) {
  const css = this.rewrite(arrayify(rule.cssRules), prefix);
  return `@supports ${rule.conditionText} {${css}}`;
}

普通的

// prefix為"div[data-qiankun="react16"]"
private ruleStyle(rule: CSSStyleRule, prefix: string) {
		// 根選擇器,比如body、html以及:root
    const rootSelectorRE = /((?:[^\w\-.#]|^)(body|html|:root))/gm;
		// 根組合選擇器,類似於 html body{...}
    const rootCombinationRE = /(html[^\w{[]+)/gm;

		// 獲取選擇器
    const selector = rule.selectorText.trim();

		// 獲取樣式文本,比如"html { font-family: sans-serif; line-height: 1.15; text-size-adjust: 100%; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); }"
    let { cssText } = rule;
	   // 對根選擇器(body、html、:root)進行判斷,替換成prefix
    if (selector === 'html' || selector === 'body' || selector === ':root') {
      return cssText.replace(rootSelectorRE, prefix);
    }

    // 對於根組合選擇器進行匹配
    if (rootCombinationRE.test(rule.selectorText)) {
      const siblingSelectorRE = /(html[^\w{]+)(\+|~)/gm;
			// 對於非標準的兄弟選擇器轉換時進行忽略,置空處理
      if (!siblingSelectorRE.test(rule.selectorText)) {
        cssText = cssText.replace(rootCombinationRE, '');
      }
    }

    // 普通選擇器匹配
    cssText = cssText.replace(/^[\s\S]+{/, (selectors) =>
			// selectors為類似於.link{
      selectors.replace(/(^|,\n?)([^,]+)/g, (item, p, s) => {
				// 處理類似於div,body,span { ... },含有根元素的
        if (rootSelectorRE.test(item)) {
          return item.replace(rootSelectorRE, (m) => {
            const whitePrevChars = [',', '('];
						// 將其中的根元素替換為首碼保留,或者(
            if (m && whitePrevChars.includes(m[0])) {
              return `${m[0]}${prefix}`;
            }
						// 直接把根元素替換成首碼
            return prefix;
          });
        }

        return `${p}${prefix} ${s.replace(/^ */, '')}`;
      }),
    );

    return cssText;
  }

動態添加樣式的思考

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • JavaScript 介紹 Javascript 語言誕生主要是完成頁面的數據驗證。因此它運行在客戶端,需要運行瀏覽器來解析執行 JavaScript 代碼。 JS 是 Netscape 網景公司的產品,最早取名為 LiveScript;為了吸引更多 java 程式員。更名為 JavaScript。 ...
  • watch是監聽某個變數或者屬性的變化,並執行相應的回調函數,通常是一個變數的變化決定多個變數的變化,watch可以進行非同步操作,但不支持緩存。一個偵聽器對應一條數據,當偵聽的數據發生變化是,watch才會執行對應的方法(一對一) computed計算屬性是依賴已有的變數來計算一個目標變數,大多數情 ...
  • 1、所謂數據響應式就是能夠使數據變化可以被檢測並對這種變化做出響應的機制。 2、MVVM框架要解決的一個核心問題是連接數據層和視圖層,通過數據驅動應用,數據變化,視圖更新,要做到這點的就需要對數據做響應式處理,這樣一旦數據發生變化就可以立即做出更新處理。 3、以vue為例說明,通過數據響應式加上虛擬 ...
  • 路由懶載入:有效拆分App尺寸,訪問時才非同步載入 keep-alive組件緩存:避免重覆創建組件實例,且能保留緩存組件狀態 使用v-show復用DOM:避免重覆創建組件 v-for遍歷避免同時使用v-if 長列表性能優化:如果是大數據長列表,可採用虛擬滾動(不會將所有內容渲染,只是把可是區域渲染), ...
  • 泛型 指在定義函數、介面或類的時候,不預先指定具體的類型,而在使用的時候再指定具體類型的一種特性。 引入 下麵創建一個函數, 實現功能: 根據指定的數量 count 和數據 value , 創建一個包含 count 個 value 的數組 不用泛型的話,這個函數可能是下麵這樣: function c ...
  • 前言 : 組件系統是Vue.js其中一個重要的概念,它提供了一種抽象,讓我們可以使用獨立可復用的小組件來構建大型應用,任意類型的應用界面都可以抽象為一個組件樹。 現在基於vue的UI組件庫有很多,比如iview,element-ui等。但有時候這些組件庫滿足不了我們的開發需求,這時候我們就需要自己寫 ...
  • Vue 3.x基礎 模版 <template> // html </template> <script setup> // setup API // ... </script> <style> // css </style> setup API 變數(響應式) // 基本數據類型 let refVa ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 1. ref的使用 ref 接受一個原始值,返回一個具有響應式的對象,對象有一個value屬性,其值就是所傳遞的原始值。 ref是做的一個拷貝關係,修改對象msg的值,不會影響對象obj,視圖會發生變化。 import { ref } f ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...