国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

React 單元測試策略及落地

nifhlheimr / 3410人閱讀

摘要:寫好的單元測試,對開發(fā)速度項目維護有莫大的幫助。我認(rèn)為單元測試的上下文存在于敏捷中。接下來一小節(jié),就可以正式進入如何做的環(huán)節(jié)了如何寫好單元測試。前面說到,我們對單元測試寄予

寫好的單元測試,對開發(fā)速度、項目維護有莫大的幫助。前端的測試工具一直推陳出新,而測試的核心、原則卻少有變化。與產(chǎn)品代碼一并交付可靠的測試代碼,是每個專業(yè)開發(fā)者應(yīng)該不斷靠近的一個理想之地。本文就圍繞測試講講,為什么我們要做測試,什么是好的測試和原則,以及如何在一個 React 項目中落地這些測試策略。

本文使用的測試框架、斷言工具是 jest。文章不打算對測試框架、語法本身做過多介紹,因為已有很多文章。本文假定讀者已有一定基礎(chǔ),至少熟悉語法,但并不假設(shè)讀者寫過單元測試。在介紹什么是好的單元測試時,我會簡單介紹一個好的單元測試的結(jié)構(gòu)。

Github 討論:https://github.com/linesh-sim...

原文地址:https://blog.linesh.tw/#/post...

目錄

為什么要做單元測試

單元測試的上下文

測試策略:測試金字塔

如何寫好單元測試:好測試的特征

有且僅有一個失敗的理由

表達力極強

快、穩(wěn)定

React 單元測試策略及落地

React 應(yīng)用的單元測試策略

actions 測試

reducer 測試

selector 測試

saga 測試

來自官方的錯誤姿勢

正確姿勢

component 測試

業(yè)務(wù)型組件 - 分支渲染

業(yè)務(wù)型組件 - 事件調(diào)用

功能型組件 - children 型高階組件

utils 測試

總結(jié)

未盡話題 & 歡迎討論

為什么要做單元測試

雖然關(guān)于測試的文章有很多,關(guān)于 React 的文章也有很多,但關(guān)于 React 應(yīng)用之詳細單元測試的文章還比較少。而且更多的文章都更偏向于對工具本身進行講解,只講「我們可以這么測」,卻沒有回答「我們?yōu)槭裁匆@么測」、「這么測究竟好不好」的問題。這幾個問題上的空白,難免使人得出測試無用、測試成本高、測試使開發(fā)變慢的錯誤觀點,導(dǎo)致在「質(zhì)量內(nèi)建」已漸入人心的今日,很多人仍然認(rèn)為測試是二等公民,是成本,是錦上添花。這一點上,我的態(tài)度一貫鮮明:不僅要寫測試,還要把單元測試寫好;不僅要有測試前移質(zhì)量內(nèi)建的意識,還要有基于測試進行快速反饋快速開發(fā)的能力。沒自動化測試的代碼不叫完成,不能驗收。

「為什么我們需要做單元測試」,這是一個關(guān)鍵的問題。每個人都有自己關(guān)于該不該做測試、該怎么做、做到什么程度的看法,試圖面面俱到、左右逢源地評價這些看法是不可能的。我們需要一個視角,一個談?wù)搯卧獪y試的上下文。做單元測試當(dāng)然有好處,但本文不會從有什么好處出發(fā)來談,而是談,在我們在意的這個上下文中,不做單元測試會有什么問題。

那么我們談?wù)搯卧獪y試的上下文是什么呢?不做單元測試我們會遇到什么問題呢?

單元測試的上下文

先說說問題。最大的一個問題是,不寫單元測試,你就不敢重構(gòu),就只能看著代碼腐化。代碼質(zhì)量談不上,持續(xù)改進談不上,個人成長更談不上。始終是原始的勞作方式。

再說說上下文。我認(rèn)為單元測試的上下文存在于「敏捷」中。現(xiàn)代企業(yè)數(shù)字化競爭日益激烈,業(yè)務(wù)端快速上線、快速驗證、快速失敗的思路對技術(shù)端的響應(yīng)力提出了更高的要求:更快上線更頻繁上線持續(xù)上線。怎么樣衡量這個「更快」呢?那就是第一圖提到的 lead time,它度量的是一個 idea 從提出并被驗證,到最終上生產(chǎn)環(huán)境面對用戶獲取反饋的時間。顯然,這個時間越短,軟件就能越快獲得反饋,對價值的驗證就越快發(fā)生。這個結(jié)論對我們寫不寫單元測試有什么影響呢?答案是,不寫單元測試,你就快不起來。為啥呢?因為每次發(fā)布,你都要投入人力來進行手工測試;因為沒有測試,你傾向于不敢隨意重構(gòu),這又導(dǎo)致代碼逐漸腐化,復(fù)雜度使得你的開發(fā)速度降低。

再考慮到以下兩個大事實:人員會流動,應(yīng)用會變大。人員一定會流動,需求一定會增加,再也沒有任何人能夠了解任何一個應(yīng)用場景。因此,意圖依賴人、依賴手工的方式來應(yīng)對響應(yīng)力的挑戰(zhàn)首先是低效的,從時間維度上來講也是不現(xiàn)實的。那么,為了服務(wù)于「高響應(yīng)力」這個目標(biāo),我們就需要一套自動化的測試套件,它能幫我們提供快速反饋、做質(zhì)量的守衛(wèi)者。唯解決了人工、質(zhì)量的這一環(huán),效率才能穩(wěn)步提升,團隊和企業(yè)的高響應(yīng)力才可能達到。

那么在「響應(yīng)力」這個上下文中來談要不要單元測試,我們就可以很有根據(jù)了,而不是開發(fā)爽了就用,不爽就不用這樣含糊的答案:

如果你說我的業(yè)務(wù)部門不需要頻繁上線,并且我有足夠的人力來覆蓋手工測試,那你可以不用單元測試

如果你說我是個小項目小部門不需要多高的響應(yīng)力,每天摸摸魚就過去了,那你可以不用單元測試

如果你說我不在意代碼腐化,并且我也不做重構(gòu),那你可以不用單元測試

如果你說我不在意代碼質(zhì)量,好幾個沒有測試保護的 if-else 裸奔也不在話下,腦不好還做什么程序員,那你可以不用單元測試

如果你說我確有快速部署的需求,但我們不 care 質(zhì)量問題,出回歸問題就修,那你可以不用單元測試

除此之外,你就需要寫單元測試。如果你想隨時整理重構(gòu)代碼,那么你需要寫單元測試;如果你想有自動化的測試套件來幫你快速驗證提交的完整性,那么你需要寫單元測試;如果你是個長期項目有人員流動,那么你需要寫單元測試;如果你不想花大量的時間在記住業(yè)務(wù)場景和手動測試應(yīng)用上,那么你就需要單元測試。

至此,我們從「響應(yīng)力」這個上下文中,回答了「為什么我們需要寫單元測試」的問題。接下來可以談下一個問題了:「為什么是單元測試」。

測試策略:測試金字塔

上面我直接從高響應(yīng)力談到單元測試,可能有的同學(xué)會問,高響應(yīng)力這個事情我認(rèn)可,也認(rèn)可快速開發(fā)的同時,質(zhì)量也很重要。但是,為了達到「保障質(zhì)量」的目的,不一定得通過測試呀,也不一定得通過單元測試鴨。

這是個好的問題。為了達到保障質(zhì)量這個目標(biāo),測試當(dāng)然只是其中一個方式,穩(wěn)定的自動化部署、集成流水線、良好的代碼架構(gòu)、組織架構(gòu)的必要調(diào)整等,都是必須跟上的設(shè)施。我從未認(rèn)為單元測試是解決質(zhì)量問題的銀彈,多方共同提升才可能起到效果。但相反,也很難想象單元測試都沒有都寫不好的項目,能有多高的響應(yīng)力。

即便我們談自動化測試,未必也不可能全部都是寫單元測試。我們對自動化測試套件寄予的厚望是,它能幫我們安全重構(gòu)已有代碼保存業(yè)務(wù)上下文快速回歸。測試種類多種多樣,為什么我要重點談單元測試呢?因為這篇文章主題就是談單元測試啊…它寫起來相對最容易、運行速度最快、反饋效果又最直接。下面這個圖,想必大家都有所耳聞:

這就是有名的測試金字塔。對于一個自動化測試套件,應(yīng)該包含種類不同、關(guān)注點不同的測試,比如關(guān)注單元的單元測試、關(guān)注集成和契約的集成測試和契約測試、關(guān)注業(yè)務(wù)驗收點的端到端測試等。正常來說,我們會受到資源的限制,無法應(yīng)用所有層級的測試,效果也未必最佳。因此,我們需要有策略性地根據(jù)收益-成本的原則,考慮項目的實際情況和痛點來定制測試策略:比如三方依賴多的項目可以多寫些契約測試,業(yè)務(wù)場景多、復(fù)雜或經(jīng)常回歸的場景可以多寫些端到端測試,等。但不論如何,整個測試金字塔體系中,你還是應(yīng)該擁有更多低層次的單元測試,因為它們成本相對最低,運行速度最快(通常是毫秒級別),而對單元的保護價值相對更大。

以上是對「為什么我們需要的是單元測試」這個問題的回答。接下來一小節(jié),就可以正式進入如何做的環(huán)節(jié)了:「如何寫好單元測試」。

關(guān)于測試金字塔的補充閱讀:測試金字塔實戰(zhàn)。

如何寫好單元測試:好測試的特征

寫單元測試僅僅是第一步,下面還有個更關(guān)鍵的問題,就是怎樣寫出好的、容易維護的單元測試。好的測試有其特征,雖然它并不是什么新的東西,但總需要時時拿出來溫故知新。很多時候,同學(xué)感覺測試難寫、難維護、不穩(wěn)定、價值不大等,可能都是因為單元測試寫不好所導(dǎo)致的。那么我們就來看看,一個好的單元測試,應(yīng)該遵循哪幾點原則。

首先,我們先來看個簡單的例子,一個最簡單的 JavaScript 的單元測試長什么樣:

// production code
const computeSumFromObject = (a, b) => {
  return a.value + b.value
}

// testing code
it("should return 5 when adding object a with value 2 and b with value 3", () => {
  // given - 準(zhǔn)備數(shù)據(jù)
  const a = { value: 2 }
  const b = { value: 3 }

  // when - 調(diào)用被測函數(shù)
  const result = computeSumFromObject(a, b)

  // then - 斷言結(jié)果
  expect(result).toBe(5)
})

以上就是一個最簡答的單元測試部分。但麻雀雖小,五臟基本全,它揭示了單元測試的一個基本結(jié)構(gòu):準(zhǔn)備輸入數(shù)據(jù)、調(diào)用被測函數(shù)、斷言輸出結(jié)果。任何單元測試都可以遵循這樣一個骨架,它是我們常說的 given-when-then 三段式。

為什么說單元測試說來簡單,做到卻不簡單呢?除了遵循三段式,顯然我們還需要遵循一些其他的原則。前面說到,我們對單元測試寄予了幾點厚望,下面就來看看,它如何能達到我們期望的效果,以此來反推單元測試的特征:

安全重構(gòu)已有代碼 -> 應(yīng)該有且僅有一個失敗的理由不關(guān)注內(nèi)部實現(xiàn)

保存業(yè)務(wù)上下文 -> 表達力極強

快速回歸 -> 穩(wěn)定

下面來看看這三個原則都是咋回事:

有且僅有一個失敗的理由

有且僅有一個失敗的理由,這個理由是什么呢?是 「當(dāng)輸入不變時,當(dāng)且僅當(dāng)被測業(yè)務(wù)代碼功能被改動了」時,測試才應(yīng)該掛掉。為什么這會支持我們重構(gòu)呢,因為重構(gòu)的意思是,在不改動軟件外部可觀測行為的基礎(chǔ)上,調(diào)整軟件內(nèi)部實現(xiàn)的一種手段。也就是說,當(dāng)我被測的代碼輸入輸出沒變時,任我怎么倒騰重構(gòu)代碼的內(nèi)部實現(xiàn),測試都不應(yīng)該掛掉。這樣才能說是支持了重構(gòu)。有的單元測試寫得,內(nèi)部實現(xiàn)(比如數(shù)據(jù)結(jié)構(gòu))一調(diào)整,測試就掛掉,盡管它的業(yè)務(wù)本身并沒修改,這樣怎么支持重構(gòu)呢?不怪得要反過來罵測試成本高,沒有用。一般會出現(xiàn)這種情況,可能是因為是先寫完代碼再補的測試,或者對代碼的接口和抽象不明確所導(dǎo)致。

另外,還有一些測試(比如下文要看到的 saga 官方推薦的測試),它需要測試實現(xiàn)代碼的執(zhí)行次序。這也是一種「關(guān)注內(nèi)部實現(xiàn)」的測試,這就使得除了業(yè)務(wù)目標(biāo)外,還有「執(zhí)行次序」這個因素可能使測試掛掉。這樣的測試也是很脆弱的。

表達力極強

表達力極強,講的是兩方面:

看到測試時,你就知道它測的業(yè)務(wù)點是啥

測試掛掉時,能清楚地知道業(yè)務(wù)、期望數(shù)據(jù)與實際輸出的差異

這些表達力體現(xiàn)在許多方面,比如測試描述、數(shù)據(jù)準(zhǔn)備的命名、與測試無關(guān)數(shù)據(jù)的清除、斷言工具能提供的比對等。空口無憑,請大家在閱讀后面測試落地時時常對照。

快、穩(wěn)定

不快的單元測試還能叫單元測試嗎?一般來講,一個沒有依賴、沒有 API 調(diào)用的單元測試,都能在毫秒級內(nèi)完成。那么為了達到快、穩(wěn)定這個目標(biāo),我們需要:

隔離盡量多的依賴。依賴少,速度就快,自然也更穩(wěn)定

將依賴、集成等耗時、依賴三方返回的地方放到更高層級的測試中,有策略性地去做

測試代碼中不要包含邏輯。不然你咋知道是實現(xiàn)掛了還是你的測試掛了呢?

在后面的介紹中,我會將這些原則落實到我們寫的每個單元測試中去。大家可以時時翻到這個章節(jié)來對照,是不是遵循了我們說的這幾點原則,不遵循是不是確實會帶來問題。時時勤拂拭,莫使惹塵埃啊。

React 單元測試策略及落地

React 應(yīng)用的單元測試策略

上個項目上的 React(-Native) 應(yīng)用架構(gòu)如上所述。它涉及一個常見 React 應(yīng)用的幾個層面:組件、數(shù)據(jù)管理、redux、副作用管理等,是一個常見的 React、Redux 應(yīng)用架構(gòu),也是 dva 所推薦的 66%的最佳實踐(redux+saga),對于不同的項目應(yīng)該有一定的適應(yīng)性。架構(gòu)中的不同元素有不同的特點,因此即便是單元測試,我們也有針對性的測試策略:

架構(gòu)層級 測試內(nèi)容 測試策略 解釋
action(creator) 層 是否正確創(chuàng)建 action 對象 一般不需要測試,視信心而定 這個層級非常簡單,基礎(chǔ)設(shè)施搭好以后一般不可能出錯,享受了架構(gòu)帶來的簡單性
reducer 層 是否正確完成計算 對于有邏輯的 reducer 需要 100%覆蓋率 這個層級輸入輸出明確,又有業(yè)務(wù)邏輯的計算在內(nèi),天然屬于單元測試寵愛的對象
selector 層 是否正確完成計算 對于有較復(fù)雜邏輯的 selector 需要 100%覆蓋率 這個層級輸入輸出明確,又有業(yè)務(wù)邏輯的計算在內(nèi),天然屬于單元測試寵愛的對象
saga(副作用) 層 是否獲取了正確的參數(shù)去調(diào)用 API,并使用正確的數(shù)據(jù)存取回 redux 中 對于是否獲取了正確參數(shù)、是否調(diào)用正確的 API、是否使用了正確的返回值保存數(shù)據(jù)、業(yè)務(wù)分支邏輯、異常分支 這五個業(yè)務(wù)點建議 100% 覆蓋 這個層級也有業(yè)務(wù)邏輯,對前面所述的 5 大方面進行測試很有重構(gòu)價值
component(組件接入) 層 是否渲染了正確的組件 組件的分支渲染邏輯要求 100% 覆蓋、交互事件的調(diào)用參數(shù)一般要求 100% 覆蓋、被 redux connect 過的組件不測、純 UI 不測、CSS 一般不測 這個層級最為復(fù)雜,測試策略還是以「代價最低,收益最高」為指導(dǎo)原則進行
UI 層 樣式是否正確 目前不測 這個層級以我目前理解來說,測試較難穩(wěn)定,成本又較高
utils 層 各種幫助函數(shù) 沒有副作用的必須 100% 覆蓋,有副作用的視項目情況自定

對于這個策略,這里做一些其他補充:

關(guān)于不測 redux connect 過的組件這個策略。理由是成本遠高于收益:要犧牲開發(fā)體驗(搞起來沒那么快了),要配置依賴(配置 store、 ,在大型或遺留系統(tǒng)中補測試還很可能遇到 @connect 組件里套 @connect 組件的場景);然后收益也只是可能覆蓋到了幾個極少數(shù)出現(xiàn)的場景。得不償失,果斷不測。

關(guān)于 UI 測試這塊的策略。團隊之前嘗試過 snapshot 測試,對它寄予厚望,理由是成本低,看起來又像萬能藥。不過由于其難以提供精確快照比對,整個工作的基礎(chǔ)又依賴于開發(fā)者盡心做好「確認(rèn)比對」這個事情,很依賴人工耐心又打斷日常的開發(fā)節(jié)奏,導(dǎo)致成本和收益不成正比。我個人目前是持保留態(tài)度的。

關(guān)于 DOM 測試這塊的策略。也就是通過 enzyme 這類工具,通過 css selector 來進行 DOM 渲染方面的測試。這類測試由于天生需要通過 css selector 去關(guān)聯(lián) DOM 元素,除了被測業(yè)務(wù)外 css selector 本身就是掛測試的一個因素。一個 DOM 測試至少有兩個原因可使它掛掉,并不符合我們上面提到的最佳實踐。但這種測試有時又確實有用,后文講組件測試時會專門提到,如何針對它制定適合的策略。

actions 測試

這一層太過簡單,基本都可以不用測試,獲益于架構(gòu)的簡單性。當(dāng)然,如果有些經(jīng)常出錯的 action,再針對性地對這些 action creator 補充測試。

export const saveUserComments = (comments) => ({
  type: "saveUserComments",
  payload: {
    comments,
  },
})
import * as actions from "./actions"

test("should dispatch saveUserComments action with fetched user comments", () => {
  const comments = []
  const expected = {
    type: "saveUserComments",
    payload: {
      comments: [],
    },
  }

  expect(actions.saveUserComments(comments)).toEqual(expected)
})
reducer 測試

reducer 大概有兩種:一種比較簡單,僅一一保存對應(yīng)的數(shù)據(jù)切片;一種復(fù)雜一些,里面具有一些計算邏輯。對于第一種 reducer,寫起來非常簡單,簡單到甚至可以不需要用測試去覆蓋。其正確性基本由簡單的架構(gòu)和邏輯去保證的。下面是對一個簡單 reducer 做測試的例子:

import Immutable from "seamless-immutable"

const initialState = Immutable.from({
  isLoadingProducts: false,
})

export default createReducer((on) => {
  on(actions.isLoadingProducts, (state, action) => {
    return state.merge({
      isLoadingProducts: action.payload.isLoadingProducts,
    })
  })
}, initialState)
import reducers from "./reducers"
import actions from "./actions"

test("should save loading start indicator when action isLoadingProducts is dispatched given isLoadingProducts is true", () => {
  const state = { isLoadingProducts: false }
  const expected = { isLoadingProducts: true }

  const result = reducers(state, actions.isLoadingProducts(true))

  expect(result).toEqual(expected)
})

下面是一個較為復(fù)雜、更具備測試價值的 reducer 例子,它在保存數(shù)據(jù)的同時,還進行了合并、去重的操作:

import uniqBy from "lodash/uniqBy"

export default createReducers((on) => {
  on(actions.saveUserComments, (state, action) => {
    return state.merge({
      comments: uniqBy(
        state.comments.concat(action.payload.comments), 
        "id",
      ),
    })
  })
})
import reducers from "./reducers"
import actions from "./actions"

test(`
  should merge user comments and remove duplicated comments 
  when action saveUserComments is dispatched with new fetched comments
`, () => {
  const state = {
    comments: [{ id: 1, content: "comments-1" }],
  }
  const comments = [
    { id: 1, content: "comments-1" },
    { id: 2, content: "comments-2" },
  ]

  const expected = {
    comments: [
      { id: 1, content: "comments-1" },
      { id: 2, content: "comments-2" },
    ],
  }

  const result = reducers(state, actions.saveUserComments(comments))

  expect(result).toEqual(expected)
})

reducer 作為純函數(shù),非常適合做單元測試,加之一般在 reducer 中做重邏輯處理,此處做單元測試保護的價值也很大。請留意,上面所說的單元測試,是不是符合我們描述的單元測試基本原則:

有且僅有一個失敗的理由:當(dāng)輸入不變時,僅當(dāng)我們被測「合并去重」的業(yè)務(wù)操作不符預(yù)期時,才可能掛掉測試

表達力極強:測試描述已經(jīng)寫得清楚「當(dāng)使用新獲取到的留言數(shù)據(jù)分發(fā) action saveUserComments 時,應(yīng)該與已有留言合并并去除重復(fù)的部分」;此外,測試數(shù)據(jù)只準(zhǔn)備了足夠體現(xiàn)「合并」這個操作的兩條 id 的數(shù)據(jù),而沒有放很多的數(shù)據(jù),形成雜音;

快、穩(wěn)定:沒有任何依賴,測試代碼不包含準(zhǔn)備數(shù)據(jù)、調(diào)用、斷言外的任何邏輯

selector 測試

selector 同樣是重邏輯的地方,可以認(rèn)為是 reducer 到組件的延伸。它也是一個純函數(shù),測起來與 reducer 一樣方便、價值不菲,也是應(yīng)該重點照顧的部分。況且,稍微大型一點的項目,應(yīng)該說必然會用到 selector。原因我講在這里。下面看一個 selector 的測試用例:

import { createSelector } from "reselect"

// for performant access/filtering in React component
export const labelArrayToObjectSelector = createSelector(
  [(store, ownProps) => store.products[ownProps.id].labels],
  (labels) => {
    return labels.reduce(
      (result, { code, active }) => ({
        ...result,
        [code]: active,
      }),
      {}
    )
  }
)
import { labelArrayToObjectSelector } from "./selector"

test("should transform label array to object", () => {
  const store = {
    products: {
      10085: {
        labels: [
          { code: "canvas", name: "帆布鞋", active: false },
          { code: "casual", name: "休閑鞋", active: false },
          { code: "oxford", name: "牛津鞋", active: false },
          { code: "bullock", name: "布洛克", active: true },
          { code: "ankle", name: "高幫鞋", active: true },
        ],
      },
    },
  }
  const expected = {
    canvas: false,
    casual: false,
    oxford: false,
    bullock: true,
    ankle: false,
  }

  const productLabels = labelArrayToObjectSelector(store, { id: 10085 })

  expect(productLabels).toEqual(expected)
})
saga 測試

saga 是負(fù)責(zé)調(diào)用 API、處理副作用的一層。在實際的項目上副作用還有其他的中間層進行處理,比如 redux-thunk、redux-promise 等,本質(zhì)是一樣的,只不過 saga 在測試性上要好一些。這一層副作用怎么測試呢?首先為了保證單元測試的速度和穩(wěn)定性,像 API 調(diào)用這種不確定性的依賴我們一定是要 mock 掉的。經(jīng)過仔細總結(jié),我認(rèn)為這一層主要的測試內(nèi)容有五點:

是否使用正確的參數(shù)(通常是從 action payload 或 redux 中來),調(diào)用了正確的 API

對于 mock 的 API 返回,是否保存了正確的數(shù)據(jù)(通常是通過 action 保存到 redux 中去)

主要的業(yè)務(wù)邏輯(比如僅當(dāng)用戶滿足某些權(quán)限時才調(diào)用 API 等)

異常邏輯

其他副作用是否發(fā)生(比如有時有需要 Emit 的事件、需要保存到 IndexDB 中去的數(shù)據(jù)等)

來自官方的錯誤姿勢

redux-saga 官方提供了一個 util: CloneableGenerator 用以幫我們寫 saga 的測試。這是我們項目使用的第一種測法,大概會寫出來的測試如下:

import chunk from "lodash/chunk"

export function* onEnterProductDetailPage(action) {
  yield put(actions.notImportantAction1("loading-stuff"))
  yield put(actions.notImportantAction2("analytics-stuff"))
  yield put(actions.notImportantAction3("http-stuff"))
  yield put(actions.notImportantAction4("other-stuff"))

  const recommendations = yield call(Api.get, "products/recommended")
  const MAX_RECOMMENDATIONS = 3
  const [products = []] = chunk(recommendations, MAX_RECOMMENDATIONS)

  yield put(actions.importantActionToSaveRecommendedProducts(products))

  const { payload: { userId } } = action
  const { vipList } = yield select((store) => store.credentails)
  if (!vipList.includes(userId)) {
    yield put(actions.importantActionToFetchAds())
  }
}
import { put, call } from "saga-effects"
import { cloneableGenerator } from "redux-saga/utils"
import { Api } from "src/utils/axios"
import { onEnterProductDetailPage } from "./saga"

const product = (productId) => ({ productId })

test(`
  should only save the three recommended products and show ads 
  when user enters the product detail page 
  given the user is not a VIP
`, () => {
  const action = { payload: { userId: 233 } }
  const credentials = { vipList: [2333] }
  const recommendedProducts = [product(1), product(2), product(3), product(4)]
  const firstThreeRecommendations = [product(1), product(2), product(3)]
  const generator = cloneableGenerator(onEnterProductDetailPage)(action)

  expect(generator.next().value).toEqual(
    actions.notImportantAction1("loading-stuff")
  )
  expect(generator.next().value).toEqual(
    actions.notImportantAction2("analytics-stuff")
  )
  expect(generator.next().value).toEqual(
    actions.notImportantAction3("http-stuff")
  )
  expect(generator.next().value).toEqual(
    actions.notImportantAction4("other-stuff")
  )

  expect(generator.next().value).toEqual(call(Api.get, "products/recommended"))
  expect(generator.next(recommendedProducts).value).toEqual(
    firstThreeRecommendations
  )
  generator.next()
  expect(generator.next(credentials).value).toEqual(
    put(actions.importantActionToFetchAds())
  )
})

這個方案寫多了,大家開始感受到了痛點,明顯違背我們前面提到的一些原則:

測試分明就是把實現(xiàn)抄了一遍。這違反上述所說「有且僅有一個掛測試的理由」的原則,改變實現(xiàn)次序也將會使測試掛掉

當(dāng)在實現(xiàn)中某個部分加入新的語句時,該語句后續(xù)所有的測試都會掛掉,并且出錯信息非常難以描述原因,導(dǎo)致常常要陷入「調(diào)試測試」的境地,這也是依賴于實現(xiàn)次序帶來的惡果,根本無法支持「重構(gòu)」這種改變內(nèi)部實現(xiàn)但不改變業(yè)務(wù)行為的代碼清理行為

為了測試兩個重要的業(yè)務(wù)「只保存獲取回來的前三個推薦產(chǎn)品」、「對非 VIP 用戶推送廣告」,不得不在前面先按次序先斷言許多個不重要的實現(xiàn)

測試沒有重點,隨便改點什么都會掛測試

正確姿勢

針對以上痛點,我們理想中的 saga 測試應(yīng)該是這樣:1) 不依賴實現(xiàn)次序;2) 允許僅對真正關(guān)心的、有價值的業(yè)務(wù)進行測試;3) 支持不改動業(yè)務(wù)行為的重構(gòu)。如此一來,測試的保障效率和開發(fā)者體驗都將大幅提升。

于是,我們發(fā)現(xiàn)官方提供了這么一個跑測試的工具,剛好可以用來完美滿足我們的需求:runSaga。我們可以用它將 saga 全部執(zhí)行一遍,搜集所有發(fā)布出去的 action,由開發(fā)者自由斷言其感興趣的 action!基于這個發(fā)現(xiàn),我們推出了我們的第二版 saga 測試方案:runSaga + 自定義拓展 jest 的 expect 斷言。最終,使用這個工具寫出來的 saga 測試,幾近完美:

import { put, call } from "saga-effects"
import { Api } from "src/utils/axios"
import { testSaga } from "../../../testing-utils"
import { onEnterProductDetailPage } from "./saga"

const product = (productId) => ({ productId })

test(`
  should only save the three recommended products and show ads 
  when user enters the product detail page 
  given the user is not a VIP
`, async () => {
  const action = { payload: { userId: 233 } }
  const store = { credentials: { vipList: [2333] } }
  const recommendedProducts = [product(1), product(2), product(3), product(4)]
  const firstThreeRecommendations = [product(1), product(2), product(3)]
  Api.get = jest.fn().mockImplementations(() => recommendedProducts)

  await testSaga(onEnterProductDetailPage, action, store)

  expect(Api.get).toHaveBeenCalledWith("products/recommended")
  expect(
    actions.importantActionToSaveRecommendedProducts
  ).toHaveBeenDispatchedWith(firstThreeRecommendations)
  expect(actions.importantActionToFetchAds).toHaveBeenDispatched()
})

這個測試已經(jīng)簡短了許多,沒有了無關(guān)斷言的雜音,依然遵循 given-when-then 的結(jié)構(gòu)。并且同樣是測試「只保存獲取回來的前三個推薦產(chǎn)品」、「對非 VIP 用戶推送廣告」兩個關(guān)心的業(yè)務(wù)點,其中自有簡潔的規(guī)律:

當(dāng)輸入不變時,無論你怎么優(yōu)化內(nèi)部實現(xiàn)、調(diào)整內(nèi)部次序,這個測試關(guān)心的業(yè)務(wù)場景都不會掛,真正做到了測試保護重構(gòu)、支持重構(gòu)的作用

可以僅斷言你關(guān)心的點,忽略不重要或不關(guān)心的中間過程(比如上例中,我們就沒有斷言其他 notImportant 的 action 是否被 dispatch 出去),消除無關(guān)斷言的雜音,提升了表達力

使用了 product 這樣的測試數(shù)據(jù)創(chuàng)建套件(fixtures),精簡測試數(shù)據(jù),消除無關(guān)數(shù)據(jù)的雜音,提升了表達力

自定義的 expect(action).toHaveBeenDispatchedWith(payload) matcher 很有表達力,且出錯信息友好

這個自定義的 matcher 是通過 jest 的 expect.extend 擴展實現(xiàn)的:

expect.extend({
  toHaveBeenDispatched(action) { ... },
  toHaveBeenDispatchedWith(action, payload) { ... },
})

上面是我們認(rèn)為比較好的副作用測試工具、測試策略和測試方案。使用時,需要牢記你真正關(guān)心的業(yè)務(wù)價值點(本節(jié)開始提到的 5 點),以及做到在較為復(fù)雜的單元測試中始終堅守三大基本原則。唯如此,單元測試才能真正提升開發(fā)速度、支持重構(gòu)、充當(dāng)業(yè)務(wù)上下文的文檔。

component 測試

組件測試其實是實踐最多,測試實踐看法和分歧也最多的地方。React 組件是一個高度自治的單元,從分類上來看,它大概有這么幾類:

展示型業(yè)務(wù)組件

容器型業(yè)務(wù)組件

通用 UI 組件

功能型組件

先把這個分類放在這里,待會回過頭來談。對于 React 組件測什么不測什么,我有一些思考,也有一些判斷標(biāo)準(zhǔn):除去功能型組件,其他類型的組件一般是以渲染出一個語法樹為終點的,它描述了頁面的 UI 內(nèi)容、結(jié)構(gòu)、樣式和一些邏輯 component(props) => UI。內(nèi)容、結(jié)構(gòu)和樣式,比起測試,直接在頁面上調(diào)試反饋效果更好。測也不是不行,但都難免有不穩(wěn)定的成本在;邏輯這塊,還是有一測的價值,但需要控制好依賴。綜合「好的單元測試標(biāo)準(zhǔn)」作為原則進行考慮,我的建議是:兩測兩不測。

組件分支渲染邏輯必須測

事件調(diào)用和參數(shù)傳遞一般要測

純 UI 不在單元測試層級測

連接 redux 的高階組件不測

其他的一般不測(比如 CSS,官方文檔有反例)

組件的分支邏輯,往往也是有業(yè)務(wù)含義和業(yè)務(wù)價值的分支,添加單元測試既能保障重構(gòu),還可順便做文檔用;事件調(diào)用同樣也有業(yè)務(wù)價值和文檔作用,而事件調(diào)用的參數(shù)調(diào)用有時可起到保護重構(gòu)的作用。

純 UI 不在單元測試級別測試的原因,純粹就是因為不好斷言。所謂快照測試有意義的前提在于兩個:必須是視覺級別的比對、必須開發(fā)者每次都認(rèn)真檢查。jest 有個 snapshot 測試的概念,但那個 UI 測試是代碼級的比對,不是視覺級的比對,最終還是繞了一圈,去除了雜音還不如看 Git 的 commit diff。每次要求開發(fā)者自覺檢查,既打亂工作流,也難以堅持。考慮到這些成本,我不推薦在單元測試的級別來做 UI 類型的測試。對于我們之前中等規(guī)模的項目,訴諸手工還是有一定的可控性。

連接 redux 的高階組件不測。原因是,connect 過的組件從測試的角度看無非幾個測試點:

mapStateToProps 中是否從 store 中取得了正確的參數(shù)

mapDispatchToProps 中是否地從 actions 中取得了正確的參數(shù)

map 過的 props 是否正確地被傳遞給了組件

redux 對應(yīng)的數(shù)據(jù)切片更新時,是否會使用新的 props 觸發(fā)組件進行一次更新

這四個點,react-redux 已經(jīng)都幫你測過了,已經(jīng)證明 work 了,為啥要重復(fù)測試自尋煩惱呢?當(dāng)然,不測這個東西的話,還是有這么一種可能,就是你 export 的純組件測試都是過的,但是代碼實際運行出錯。窮盡下來主要可能是這幾種問題:

你在 mapStateToProps 中打錯了字或打錯了變量名

你寫了 mapStateToProps 但沒有 connect 上去

你在 mapStateToProps 中取的路徑是錯的,在 redux 中已經(jīng)被改過

第一、二種可能,無視。測試不是萬能藥,不能預(yù)防人主動犯錯,這種場景如果是小步提交發(fā)現(xiàn)起來是很快的,如果不小步提交那什么測試都幫不了你的;如果某段數(shù)據(jù)獲取的邏輯多處重復(fù),則可以考慮將該邏輯抽取到 selector 中并進行多帶帶測試。

第三種可能,確實是問題,但發(fā)生頻率目前看來較低。為啥呢,因為沒有類型系統(tǒng)我們不會也不敢隨意改 redux 的數(shù)據(jù)結(jié)構(gòu)啊…(這侵入性重的框架喲)所以針對這些少量出現(xiàn)的場景,不必要采取錯殺一千的方式進行完全覆蓋。默認(rèn)不測,出了問題或者經(jīng)常可能出問題的部分,再策略性地補上測試進行固定即可。

綜上,@connect 組件不測,因為框架本身已做了大部分測試,剩下的場景出 bug 頻率不高,而施加測試的話提高成本(準(zhǔn)備依賴和數(shù)據(jù)),降低開發(fā)體驗,模糊測試場景,性價比不大,所以強烈建議省了這份心。不測 @connect 過的組件,其實也是 官方文檔 推薦的做法。

然后,基于上面第 1、2 個結(jié)論,映射回四類組件的結(jié)構(gòu)當(dāng)中去,我們可以得到下面的表格,然后發(fā)現(xiàn)…每種組件都要測渲染分支事件調(diào)用,跟組件類型根本沒必然的關(guān)聯(lián)…不過,功能型組件有可能會涉及一些其他的模式,因此又大致分出一小節(jié)來談。

組件類型 / 測試內(nèi)容 分支渲染邏輯 事件調(diào)用 @connect 純 UI
展示型組件 ? ? - ??
容器型組件 ? ? ?? ??
通用 UI 組件 ? ? - ??
功能型組件 ? ? ?? ??
業(yè)務(wù)型組件 - 分支渲染
export const CommentsSection = ({ comments }) => (
  
{comments.length > 0 && (

Comments

)} {comments.map((comment) => ( )}
)

對應(yīng)的測試如下,測試的是不同的分支渲染邏輯:沒有評論時,則不渲染 Comments header。

import { CommentsSection } from "./index"
import { Comment } from "./Comment"

test("should not render a header and any comment sections when there is no comments", () => {
  const component = shallow()

  const header = component.find("h2")
  const comments = component.find(Comment)

  expect(header).toHaveLength(0)
  expect(comments).toHaveLength(0)
})

test("should render a comments section and a header when there are comments", () => {
  const contents = [
    { id: 1, author: "男***8", comment: "價廉物美,相信奧康旗艦店" },
    { id: 2, author: "雨***成", comment: "所以一雙合腳的鞋子..." },
  ]
  const component = shallow()

  const header = component.find("h2")
  const comments = component.find(Comment)

  expect(header.html()).toBe("Comments")
  expect(comments).toHaveLength(2)
})
業(yè)務(wù)型組件 - 事件調(diào)用

測試事件的一個場景如下:當(dāng)某條產(chǎn)品被點擊時,應(yīng)該將產(chǎn)品相關(guān)的信息發(fā)送給埋點系統(tǒng)進行埋點。

export const ProductItem = ({
  id,
  productName,
  introduction,
  trackPressEvent,
}) => (
   trackPressEvent(id, productName)}>
    
      
      <Introduction introduction={introduction} />
    </View>
  </TouchableWithoutFeedback>
)</pre>
<pre>import { ProductItem } from "./index"

test(`
  should send product id and name to analytics system 
  when user press the product item
`, () => {
  const trackPressEvent = jest.fn()
  const component = shallow(
    <ProductItem
      id={100832}
      introduction="iMac Pro - Power to the pro."
      trackPressEvent={trackPressEvent}
    />
  )

  component.find(TouchableWithoutFeedback).simulate("press")

  expect(trackPressEvent).toHaveBeenCalledWith(
    100832,
    "iMac Pro - Power to the pro."
  )
})</pre>
<p>簡單得很吧。這里的幾個測試,在你改動了樣式相關(guān)的東西時,不會掛掉;但是如果你改動了分支邏輯或函數(shù)調(diào)用的內(nèi)容時,它就會掛掉了。而分支邏輯或函數(shù)調(diào)用,恰好是我覺得接近業(yè)務(wù)的地方,所以它們對保護代碼邏輯、保護重構(gòu)是有價值的。當(dāng)然,它們多少還是依賴了組件內(nèi)部的實現(xiàn)細節(jié),比如說 <b>find(TouchableWithoutFeedback)</b>,還是做了「組件內(nèi)部使用了 <b>TouchableWithoutFeedback</b> 組件」這樣的假設(shè),而這個假設(shè)很可能是會變的。也就是說,如果我換了一個組件來接受點擊事件,盡管點擊時的行為依然發(fā)生,但這個測試仍然會掛掉。這就違反了我們所說了「有且僅有一個使測試失敗的理由」。這對于組件測試來說,是不夠完美的地方。</p>
<p>但這個問題無法避免。因為組件本質(zhì)是渲染組件樹,那么測試中要與組件樹關(guān)聯(lián),必然要通過 組件名、id 這樣的 selector,這些 selector 的關(guān)聯(lián)本身就是使測試掛掉的「另一個理由」。但對組件的分支、事件進行測試又有一定的價值,無法避免。所以,我認(rèn)為這個部分還是要用,只不過同時需要一些限制,以控制這些假設(shè)為維護測試帶來的額外成本:</p>

<p>不要斷言組件內(nèi)部結(jié)構(gòu)。像那些 <b>expect(component.find("div > div > p").html().toBe("Content")</b> 的真的就算了吧</p>
<p>正確拆分組件樹。一個組件盡量只負(fù)責(zé)一個功能,不允許堆疊太多的函數(shù)和功能。要符合單一職責(zé)原則</p>

<p>如果你的每個組件都十分清晰直觀、邏輯分明,那么像上面這樣的組件測起來也就很輕松,一般就遵循 <b>shallow</b> -> <b>find(Component)</b> -> 斷言的三段式,哪怕是了解了一些組件的內(nèi)部細節(jié),通常也在可控的范圍內(nèi),維護起來成本并不高。這是目前我覺得平衡了表達力、重構(gòu)意義和測試成本的實踐。</p>
<b>功能型組件 - <b>children</b> 型高階組件</b>
<p>功能型組件,指的是跟業(yè)務(wù)無關(guān)的另一類組件:它是功能型的,更像是底層支撐著業(yè)務(wù)組件運作的基礎(chǔ)組件,比如路由組件、分頁組件等。這些組件一般偏重邏輯多一點,關(guān)心 UI 少一些。其本質(zhì)測法跟業(yè)務(wù)組件是一致的:不關(guān)心 UI 具體渲染,只測分支渲染和事件調(diào)用。但由于它偏功能型的特性,使得它在設(shè)計上常會出現(xiàn)一些業(yè)務(wù)型組件不常出現(xiàn)的設(shè)計模式,如高階組件、以函數(shù)為子組件等。下面分別針對這幾種進行分述。</p>
<pre>export const FeatureToggle = ({ features, featureName, children }) => {
  if (!features[featureName]) {
    return null
  }

  return children
}

export default connect(
  (store) => ({ features: store.global.features })
)(FeatureToggle)</pre>
<pre>import React from "react"
import { shallow } from "enzyme"
import { View } from "react-native"

import FeatureToggles from "./featureToggleStatus"
import { FeatureToggle } from "./index"

const DummyComponent = () => <View />

test("should not render children component when remote toggle is empty", () => {
  const component = shallow(
    <FeatureToggle features={{}} featureName="promotion618">
      <DummyComponent />
    </FeatureToggle>
  )

  expect(component.find(DummyComponent)).toHaveLength(0)
})

test("should render children component when remote toggle is present and stated on", () => {
  const features = {
    promotion618: FeatureToggles.on,
  }

  const component = shallow(
    <FeatureToggle features={features} featureName="promotion618">
      <DummyComponent />
    </FeatureToggle>
  )

  expect(component.find(DummyComponent)).toHaveLength(1)
})

test("should not render children component when remote toggle object is present but stated off", () => {
  const features = {
    promotion618: FeatureToggles.off,
  }

  const component = shallow(
    <FeatureToggle features={features} featureName="promotion618">
      <DummyComponent />
    </FeatureToggle>
  )

  expect(component.find(DummyComponent)).toHaveLength(0)
})</pre>
<b>utils 測試</b>
<p>每個項目都會有 utils。一般來說,我們期望 util 都是純函數(shù),即是不依賴外部狀態(tài)、不改變參數(shù)值、不維護內(nèi)部狀態(tài)的函數(shù)。這樣的函數(shù)測試效率也非常高。測試原則跟前面所說的也并沒什么不同,不再贅述。不過值得一提的是,因為 util 函數(shù)多是數(shù)據(jù)驅(qū)動,一個輸入對應(yīng)一個輸出,并且不需要準(zhǔn)備任何依賴,這使得它非常適合采用參數(shù)化測試的方法。這種測試方法,可以提升數(shù)據(jù)準(zhǔn)備效率,同時依然能保持詳細的用例信息、錯誤提示等優(yōu)點。jest 從 23 后就內(nèi)置了對參數(shù)化測試的支持了,如下:</p>
<pre>test.each([
  [["0", "99"], 0.99, "(整數(shù)部分為0時也應(yīng)返回)"],
  [["5", "00"], 5, "(小數(shù)部分不足時應(yīng)該補0)"],
  [["5", "10"], 5.1, "(小數(shù)部分不足時應(yīng)該補0)"],
  [["4", "38"], 4.38, "(小數(shù)部分不足時應(yīng)該補0)"],
  [["4", "99"], 4.994, "(超過默認(rèn)2位的小數(shù)的直接截斷,不四舍五入)"],
  [["4", "99"], 4.995, "(超過默認(rèn)2位的小數(shù)的直接截斷,不四舍五入)"],
  [["4", "99"], 4.996, "(超過默認(rèn)2位的小數(shù)的直接截斷,不四舍五入)"],
  [["-0", "50"], -0.5, "(整數(shù)部分為負(fù)數(shù)時應(yīng)該保留負(fù)號)"],
])(
  "should return %s when number is %s (%s)",
  (expected, input, description) => {
    expect(truncateAndPadTrailingZeros(input)).toEqual(expected)
  }
)</pre>
<p><script type="text/javascript">showImg("https://segmentfault.com/img/remote/1460000016828940?w=2674&h=1224");</script></p>
<b>總結(jié)</b>
<p>好,到此為止,本文的主要內(nèi)容也就講完了。總結(jié)下來,本文主要覆蓋到的內(nèi)容如下:</p>

<p>單元測試對于任何 React 項目(及其他任何項目)來說都是必須的</p>
<p>我們需要自動化的測試套件,根本目標(biāo)是為了提升企業(yè)和團隊的 IT「響應(yīng)力」</p>
<p>之所以優(yōu)先選擇單元測試,是依據(jù)測試金字塔的成本收益比原則確定得到的</p>
<p>好的單元測試具備三大特征:<strong>有且僅有一個失敗的理由</strong>、<strong>表達力極強</strong>、<strong>快、穩(wěn)定</strong>
</p>
<p>
<p>單元測試也有測試策略:在 React 的典型架構(gòu)下,一個測試體系大概分為六層:組件、action、reducer、selector、副作用層、utils。它們分別的測試策略為:</p>

<p>reducer、selector 的重邏輯代碼要求 100% 覆蓋</p>
<p>utils 層的純函數(shù)要求 100% 覆蓋</p>
<p>副作用層主要測試:<strong>是否拿到了正確的參數(shù)</strong>、<strong>是否調(diào)用了正確的 API</strong>、<strong>是否保存了正確的數(shù)據(jù)</strong>、<strong>業(yè)務(wù)邏輯</strong>、<strong>異常邏輯</strong> 五個層面</p>
<p>組件層兩測兩不測:<strong>分支渲染邏輯必測</strong>、<strong>事件、交互調(diào)用必測</strong>;純 UI(包括 CSS)不測、<b>@connect</b> 過的高階組件不測</p>
<p>action 層選擇性覆蓋:可不測</p>

</p>
<p>其他高級技巧:定制測試工具(<b>jest.extend</b>)、參數(shù)化測試等</p>

<b>未盡話題 & 歡迎討論</b>
<p>講完 React 下的單元測試尚且已經(jīng)這么花費篇幅,文章中難免還有些我十分想提又意猶未盡的地方。比如完整的測試策略、比如 TDD、比如重構(gòu)、比如整潔代碼設(shè)計模式等。如果讀者有由此文章而生發(fā)、而疑慮、而不吐不快的種種興趣和分享,都十分歡迎留下你的想法和指點。寫文交流,樂趣如此。感謝。</p>           
               
                                           
                       
                 </div>
            
                     <div   id="vt7vnz5"   class="mt-64 tags-seach" >
                 <div   id="vp7lbdr"   class="tags-info">
                                                                                                                    
                         <a style="width:120px;" title="GPU云服務(wù)器" href="http://m.specialneedsforspecialkids.com/site/product/gpu.html">GPU云服務(wù)器</a>
                                             
                         <a style="width:120px;" title="云服務(wù)器" href="http://m.specialneedsforspecialkids.com/site/active/kuaijiesale.html?ytag=seo">云服務(wù)器</a>
                                                                                                                                                 
                                      
                     
                    
                                                                                               <a style="width:120px;" title="單元測試" href="http://m.specialneedsforspecialkids.com/yun/tag/danyuanceshi/">單元測試</a>
                                                                                                           <a style="width:120px;" title="單元測試規(guī)范" href="http://m.specialneedsforspecialkids.com/yun/tag/danyuanceshiguifan/">單元測試規(guī)范</a>
                                                                                                           <a style="width:120px;" title="單元測試圖片測試java" href="http://m.specialneedsforspecialkids.com/yun/tag/danyuanceshitupianceshijava/">單元測試圖片測試java</a>
                                                                                                           <a style="width:120px;" title="php單元測試’" href="http://m.specialneedsforspecialkids.com/yun/tag/phpdanyuanceshi/">php單元測試’</a>
                                                         
                 </div>
               
              </div>
             
               <div   id="9t7rnxh"   class="entry-copyright mb-30">
                   <p class="mb-15"> 文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。</p>
                 
                   <p>轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/98783.html</p>
               </div>
                      
               <ul class="pre-next-page">
                 
                                  <li id="lt77j5v"    class="ellipsis"><a class="hpf" href="http://m.specialneedsforspecialkids.com/yun/98782.html">上一篇:JavaScript之閉包與立即執(zhí)行函數(shù)</a></li>  
                                                
                                       <li id="fjpld7z"    class="ellipsis"><a class="hpf" href="http://m.specialneedsforspecialkids.com/yun/98784.html">下一篇:深入 JavaScript 原型繼承原理——babel 編譯碼解讀</a></li>
                                  </ul>
              </div>
              <div   id="t7vlfpb"   class="about_topicone-mid">
                <h3 class="top-com-title mb-0"><span data-id="0">相關(guān)文章</span></h3>
                <ul class="com_white-left-mid atricle-list-box">
                             
                                                                                                    <li>
                                                <div   id="rjrzthf"   class="atricle-list-right">
                          <h2 class="ellipsis2"><a class="hpf" href="http://m.specialneedsforspecialkids.com/yun/8902.html"><b><em>React</em> <em>測試</em>指南</b></a></h2>
                                                     <p class="ellipsis2 good">摘要:單元測試針對程序模塊進行測試。是開源的單元測試工具。一個好的單元測試應(yīng)該具備的條件安全重構(gòu)已有代碼單元測試一個很重要的價值是為重構(gòu)保駕護航。斷言外部依賴單元測試的一個重要原則就是無依賴和隔離。

前端測試金字塔
對于一個 Web 應(yīng)用來說,理想的測試組合應(yīng)該包含大量單元測試(unit tests),部分快照測試(snapshot tests),以及少量端到端測試(e2e tests)。參...</p>
                                                   
                          <div   id="tl5tlz5"   class="com_white-left-info">
                                <div   id="x7zplpb"   class="com_white-left-infol">
                                    <a href="http://m.specialneedsforspecialkids.com/yun/u-254.html"><img src="http://m.specialneedsforspecialkids.com/yun/data/avatar/000/00/02/small_000000254.jpg" alt=""><span id="dllh7lz"    class="layui-hide64">Tecode</span></a>
                                    <time datetime="">2019-05-29 17:12</time>
                                    <span><i class="fa fa-commenting"></i>評論0</span> 
                                    <span><i class="fa fa-star"></i>收藏0</span> 
                                </div>
                          </div>
                      </div>
                    </li> 
                                                                                       <li>
                                                <div   id="vjn5npn"   class="atricle-list-right">
                          <h2 class="ellipsis2"><a class="hpf" href="http://m.specialneedsforspecialkids.com/yun/26973.html"><b>如何用微服務(wù)重構(gòu)應(yīng)用程序</b></a></h2>
                                                     <p class="ellipsis2 good">摘要:以下兩個要點將會對任何微服務(wù)重構(gòu)策略產(chǎn)生重大影響。批量替換通過批發(fā)更換,您可以一次性重構(gòu)整個應(yīng)用程序,直接從單體式轉(zhuǎn)移到一組微服務(wù)器。如果您通過使用破解您的微服務(wù)器,那么每個域?qū)@一個用例,或者更常見的,一組相互關(guān)聯(lián)的用例。

在決定使用微服務(wù)之后,為了將微服務(wù)付諸實踐,也許你已經(jīng)開始重構(gòu)你的應(yīng)用程序或把重構(gòu)工作列入了待辦事項清單。
無論是哪種情況,如果這是你第一次重構(gòu)應(yīng)用程序,那么您...</p>
                                                   
                          <div   id="d7dvnnd"   class="com_white-left-info">
                                <div   id="xldlbnp"   class="com_white-left-infol">
                                    <a href="http://m.specialneedsforspecialkids.com/yun/u-510.html"><img src="http://m.specialneedsforspecialkids.com/yun/data/avatar/000/00/05/small_000000510.jpg" alt=""><span id="7jdjnbb"    class="layui-hide64">KevinYan</span></a>
                                    <time datetime="">2019-06-28 15:53</time>
                                    <span><i class="fa fa-commenting"></i>評論0</span> 
                                    <span><i class="fa fa-star"></i>收藏0</span> 
                                </div>
                          </div>
                      </div>
                    </li> 
                                                                                       <li>
                                                <div   id="7txprhv"   class="atricle-list-right">
                          <h2 class="ellipsis2"><a class="hpf" href="http://m.specialneedsforspecialkids.com/yun/87432.html"><b>前端每周清單第 29 期:Web 現(xiàn)狀分析與優(yōu)化<em>策略</em>、Vue <em>單元</em><em>測試</em>、Headless Chrom</b></a></h2>
                                                     <p class="ellipsis2 good">摘要:前端每周清單第期現(xiàn)狀分析與優(yōu)化策略單元測試爬蟲作者王下邀月熊編輯徐川前端每周清單專注前端領(lǐng)域內(nèi)容,以對外文資料的搜集為主,幫助開發(fā)者了解一周前端熱點分為新聞熱點開發(fā)教程工程實踐深度閱讀開源項目巔峰人生等欄目。

showImg(https://segmentfault.com/img/remote/1460000011008022);
前端每周清單第 29 期:Web 現(xiàn)狀分析與優(yōu)化策略...</p>
                                                   
                          <div   id="7vbdhjv"   class="com_white-left-info">
                                <div   id="l7rvddf"   class="com_white-left-infol">
                                    <a href="http://m.specialneedsforspecialkids.com/yun/u-449.html"><img src="http://m.specialneedsforspecialkids.com/yun/data/avatar/000/00/04/small_000000449.jpg" alt=""><span id="f7f755x"    class="layui-hide64">HackerShell</span></a>
                                    <time datetime="">2019-08-21 12:05</time>
                                    <span><i class="fa fa-commenting"></i>評論0</span> 
                                    <span><i class="fa fa-star"></i>收藏0</span> 
                                </div>
                          </div>
                      </div>
                    </li> 
                                                                                       <li>
                                                <div   id="jn7pn5b"   class="atricle-list-right">
                          <h2 class="ellipsis2"><a class="hpf" href="http://m.specialneedsforspecialkids.com/yun/78853.html"><b>【前端構(gòu)建】WebPack實例與前端性能優(yōu)化</b></a></h2>
                                                     <p class="ellipsis2 good">摘要:感受構(gòu)建工具給前端優(yōu)化工作帶來的便利。多多益處邏輯清晰,程序注重數(shù)據(jù)與表現(xiàn)分離,可讀性強,利于規(guī)避和排查問題構(gòu)建工具層出不窮。其實工具都能滿足需求,關(guān)鍵是看怎么用,工具的使用背后是對前端性能優(yōu)化的理解程度。

這篇主要介紹一下我在玩Webpack過程中的心得。通過實例介紹WebPack的安裝,插件使用及加載策略。感受構(gòu)建工具給前端優(yōu)化工作帶來的便利。
showImg(https://se...</p>
                                                   
                          <div   id="jvxf7zz"   class="com_white-left-info">
                                <div   id="zpjpth5"   class="com_white-left-infol">
                                    <a href="http://m.specialneedsforspecialkids.com/yun/u-126.html"><img src="http://m.specialneedsforspecialkids.com/yun/data/avatar/000/00/01/small_000000126.jpg" alt=""><span id="rtjnft5"    class="layui-hide64">QiShare</span></a>
                                    <time datetime="">2019-08-19 17:48</time>
                                    <span><i class="fa fa-commenting"></i>評論0</span> 
                                    <span><i class="fa fa-star"></i>收藏0</span> 
                                </div>
                          </div>
                      </div>
                    </li> 
                                                                                       <li>
                                                <div   id="vvntbdd"   class="atricle-list-right">
                          <h2 class="ellipsis2"><a class="hpf" href="http://m.specialneedsforspecialkids.com/yun/8023.html"><b>騰訊云運維干貨沙龍-海量運維實踐大曝光 (三)</b></a></h2>
                                                     <p class="ellipsis2 good">摘要:月日,首期沙龍海量運維實踐大曝光在騰訊大廈圓滿舉行。織云高效的實踐是,它是以運維標(biāo)準(zhǔn)化為基石,以為核心的自動化運維平臺。

作者丨周小軍,騰訊SNG資深運維工程師,負(fù)責(zé)社交產(chǎn)品分布式存儲的運維及團隊管理工作。對互聯(lián)網(wǎng)網(wǎng)站架構(gòu)、數(shù)據(jù)中心、云計算及自動化運維等領(lǐng)域有深入研究和理解。
12月16日,首期沙龍海量運維實踐大曝光在騰訊大廈圓滿舉行。沙龍出品人騰訊運維技術(shù)總監(jiān)、復(fù)旦大學(xué)客座講師、De...</p>
                                                   
                          <div   id="h75fz7b"   class="com_white-left-info">
                                <div   id="rphvpdd"   class="com_white-left-infol">
                                    <a href="http://m.specialneedsforspecialkids.com/yun/u-1390.html"><img src="http://m.specialneedsforspecialkids.com/yun/data/avatar/000/00/13/small_000001390.jpg" alt=""><span id="dnfl5tp"    class="layui-hide64">eechen</span></a>
                                    <time datetime="">2019-05-28 17:05</time>
                                    <span><i class="fa fa-commenting"></i>評論0</span> 
                                    <span><i class="fa fa-star"></i>收藏0</span> 
                                </div>
                          </div>
                      </div>
                    </li> 
                                                                           
                </ul>
              </div>
              
               <div   id="rrj5x57"   class="topicone-box-wangeditor">
                  
                  <h3 class="top-com-title mb-64"><span>發(fā)表評論</span></h3>
                   <div   id="z7d5jnz"   class="xcp-publish-main flex_box_zd">
                                      
                      <div   id="rdjntvj"   class="unlogin-pinglun-box">
                        <a href="javascript:login()" class="grad">登陸后可評論</a>
                      </div>                   </div>
               </div>
              <div   id="hxlbh5d"   class="site-box-content">
                <div   id="x77lpbp"   class="site-content-title">
                  <h3 class="top-com-title mb-64"><span>0條評論</span></h3>   
                </div> 
                      <div   id="trtjbdr"   class="pages"></ul></div>
              </div>
           </div>
           <div   id="7jnp5bz"   class="layui-col-md4 layui-col-lg3 com_white-right site-wrap-right">
              <div   id="tfjpv7p"   class=""> 
                <div   id="f7rlpfh"   class="com_layuiright-box user-msgbox">
                    <a href="http://m.specialneedsforspecialkids.com/yun/u-962.html"><img src="http://m.specialneedsforspecialkids.com/yun/data/avatar/000/00/09/small_000000962.jpg" alt=""></a>
                    <h3><a href="http://m.specialneedsforspecialkids.com/yun/u-962.html" rel="nofollow">nifhlheimr</a></h3>
                    <h6>男<span>|</span>高級講師</h6>
                    <div   id="lvzb7dp"   class="flex_box_zd user-msgbox-atten">
                     
                                                                      <a href="javascript:attentto_user(962)" id="attenttouser_962" class="grad follow-btn notfollow attention">我要關(guān)注</a>
      
                                                                                        <a href="javascript:login()" title="發(fā)私信" >我要私信</a>
                     
                                            
                    </div>
                    <div   id="zltzddr"   class="user-msgbox-list flex_box_zd">
                          <h3 class="hpf">TA的文章</h3>
                          <a href="http://m.specialneedsforspecialkids.com/yun/ut-962.html" class="box_hxjz">閱讀更多</a>
                    </div>
                      <ul class="user-msgbox-ul">
                                                  <li><h3 class="ellipsis"><a href="http://m.specialneedsforspecialkids.com/yun/124713.html">頭歌平臺(EduCoder)————軟件測試(黑盒測試)</a></h3>
                            <p>閱讀 3417<span>·</span>2021-11-24 09:38</p></li>
                                                       <li><h3 class="ellipsis"><a href="http://m.specialneedsforspecialkids.com/yun/124029.html">使用WebUploader實現(xiàn)圖片上傳</a></h3>
                            <p>閱讀 3194<span>·</span>2021-11-22 09:34</p></li>
                                                       <li><h3 class="ellipsis"><a href="http://m.specialneedsforspecialkids.com/yun/120734.html">虛擬主機技術(shù)是什么意思-什么是虛擬主機?</a></h3>
                            <p>閱讀 2108<span>·</span>2021-09-22 16:03</p></li>
                                                       <li><h3 class="ellipsis"><a href="http://m.specialneedsforspecialkids.com/yun/114453.html">CSS 搞事技巧:border+transparent</a></h3>
                            <p>閱讀 2368<span>·</span>2019-08-29 18:37</p></li>
                                                       <li><h3 class="ellipsis"><a href="http://m.specialneedsforspecialkids.com/yun/113623.html">webpack4 + react 搭建多頁面應(yīng)用</a></h3>
                            <p>閱讀 380<span>·</span>2019-08-29 16:15</p></li>
                                                       <li><h3 class="ellipsis"><a href="http://m.specialneedsforspecialkids.com/yun/109839.html">如何使頁面交互更流暢</a></h3>
                            <p>閱讀 1770<span>·</span>2019-08-26 13:56</p></li>
                                                       <li><h3 class="ellipsis"><a href="http://m.specialneedsforspecialkids.com/yun/108552.html">前端防XSS攻擊——模板字面量(模板字符串)之模板標(biāo)簽的應(yīng)用</a></h3>
                            <p>閱讀 866<span>·</span>2019-08-26 12:21</p></li>
                                                       <li><h3 class="ellipsis"><a href="http://m.specialneedsforspecialkids.com/yun/108338.html">小程序input控件</a></h3>
                            <p>閱讀 2207<span>·</span>2019-08-26 12:15</p></li>
                                                
                      </ul>
                </div>

                   <!-- 文章詳情右側(cè)廣告-->
              
  <div   id="r7j77vf"   class="com_layuiright-box">
                  <h6 class="top-com-title"><span>最新活動</span></h6> 
           
         <div   id="7hxbdth"   class="com_adbox">
                    <div   id="7jnhbz5"   class="layui-carousel" id="right-item">
                      <div carousel-item>
                                                                                                                       <div>
                          <a href="http://m.specialneedsforspecialkids.com/site/active/kuaijiesale.html?ytag=seo"  rel="nofollow">
                            <img src="http://m.specialneedsforspecialkids.com/yun/data/attach/240625/2rTjEHmi.png" alt="云服務(wù)器">                                 
                          </a>
                        </div>
                                                <div>
                          <a href="http://m.specialneedsforspecialkids.com/site/product/gpu.html"  rel="nofollow">
                            <img src="http://m.specialneedsforspecialkids.com/yun/data/attach/240807/7NjZjdrd.png" alt="GPU云服務(wù)器">                                 
                          </a>
                        </div>
                                                                   
                    
                        
                      </div>
                    </div>
                      
                    </div>                    <!-- banner結(jié)束 -->
              
<div   id="jjptnzz"   class="adhtml">

</div>
                <script>
                $(function(){
                    $.ajax({
                        type: "GET",
                                url:"http://m.specialneedsforspecialkids.com/yun/ad/getad/1.html",
                                cache: false,
                                success: function(text){
                                  $(".adhtml").html(text);
                                }
                        });
                    })
                </script>                </div>              </div>
           </div>
        </div>
      </div> 
    </section>
    <!-- wap拉出按鈕 -->
     <div   id="xxbrxzj"   class="site-tree-mobile layui-hide">
      <i class="layui-icon layui-icon-spread-left"></i>
    </div>
    <!-- wap遮罩層 -->
    <div   id="v7pvpb5"   class="site-mobile-shade"></div>
    
       <!--付費閱讀 -->
       <div   class="fbhnt57"   id="payread">
         <div   id="jv77hth"   class="layui-form-item">閱讀需要支付1元查看</div>  
         <div   id="5ptnfj7"   class="layui-form-item"><button class="btn-right">支付并查看</button></div>     
       </div>
      <script>
      var prei=0;

       
       $(".site-seo-depict pre").each(function(){
          var html=$(this).html().replace("<code>","").replace("</code>","").replace('<code class="javascript hljs" codemark="1">','');
          $(this).attr('data-clipboard-text',html).attr("id","pre"+prei);
          $(this).html("").append("<code>"+html+"</code>");
         prei++;
       })
           $(".site-seo-depict img").each(function(){
             
            if($(this).attr("src").indexOf('data:image/svg+xml')!= -1){
                $(this).remove();
            }
       })
     $("LINK[href*='style-49037e4d27.css']").remove();
       $("LINK[href*='markdown_views-d7a94ec6ab.css']").remove();
layui.use(['jquery', 'layer','code'], function(){
  $("pre").attr("class","layui-code");
      $("pre").attr("lay-title","");
       $("pre").attr("lay-skin","");
  layui.code(); 
       $(".layui-code-h3 a").attr("class","copycode").html("復(fù)制代碼 ").attr("onclick","copycode(this)");
      
});
function copycode(target){
    var id=$(target).parent().parent().attr("id");
  
                  var clipboard = new ClipboardJS("#"+id);

clipboard.on('success', function(e) {


    e.clearSelection();
    alert("復(fù)制成功")
});

clipboard.on('error', function(e) {
    alert("復(fù)制失敗")
});
}
//$(".site-seo-depict").html($(".site-seo-depict").html().slice(0, -5));
</script>
  <link rel="stylesheet" type="text/css" href="http://m.specialneedsforspecialkids.com/yun/static/js/neweditor/code/styles/tomorrow-night-eighties.css">
    <script src="http://m.specialneedsforspecialkids.com/yun/static/js/neweditor/code/highlight.pack.js" type="text/javascript"></script>
    <script src="http://m.specialneedsforspecialkids.com/yun/static/js/clipboard.js"></script>

<script>hljs.initHighlightingOnLoad();</script>

<script>
    function setcode(){
        var _html='';
    	  document.querySelectorAll('pre code').forEach((block) => {
        	  var _tmptext=$.trim($(block).text());
        	  if(_tmptext!=''){
        		  _html=_html+_tmptext;
        		  console.log(_html);
        	  }
    		 
    		  
    		 
      	  });
    	 

    }

</script>

<script>
function payread(){
  layer.open({
      type: 1,
      title:"付費閱讀",
      shadeClose: true,
      content: $('#payread')
    });
}
// 舉報
function jupao_tip(){
  layer.open({
      type: 1,
      title:false,
      shadeClose: true,
      content: $('#jubao')
    });

}
$(".getcommentlist").click(function(){
var _id=$(this).attr("dataid");
var _tid=$(this).attr("datatid");
$("#articlecommentlist"+_id).toggleClass("hide");
var flag=$("#articlecommentlist"+_id).attr("dataflag");
if(flag==1){
flag=0;
}else{
flag=1;
//加載評論
loadarticlecommentlist(_id,_tid);
}
$("#articlecommentlist"+_id).attr("dataflag",flag);

})
$(".add-comment-btn").click(function(){
var _id=$(this).attr("dataid");
$(".formcomment"+_id).toggleClass("hide");
})
$(".btn-sendartcomment").click(function(){
var _aid=$(this).attr("dataid");
var _tid=$(this).attr("datatid");
var _content=$.trim($(".commenttext"+_aid).val());
if(_content==''){
alert("評論內(nèi)容不能為空");
return false;
}
var touid=$("#btnsendcomment"+_aid).attr("touid");
if(touid==null){
touid=0;
}
addarticlecomment(_tid,_aid,_content,touid);
})
 $(".button_agree").click(function(){
 var supportobj = $(this);
         var tid = $(this).attr("id");
         $.ajax({
         type: "GET",
                 url:"http://m.specialneedsforspecialkids.com/yun/index.php?topic/ajaxhassupport/" + tid,
                 cache: false,
                 success: function(hassupport){
                 if (hassupport != '1'){






                         $.ajax({
                         type: "GET",
                                 cache:false,
                                 url: "http://m.specialneedsforspecialkids.com/yun/index.php?topic/ajaxaddsupport/" + tid,
                                 success: function(comments) {

                                 supportobj.find("span").html(comments+"人贊");
                                 }
                         });
                 }else{
                	 alert("您已經(jīng)贊過");
                 }
                 }
         });
 });
 function attenquestion(_tid,_rs){
    	$.ajax({
    //提交數(shù)據(jù)的類型 POST GET
    type:"POST",
    //提交的網(wǎng)址
    url:"http://m.specialneedsforspecialkids.com/yun/favorite/topicadd.html",
    //提交的數(shù)據(jù)
    data:{tid:_tid,rs:_rs},
    //返回數(shù)據(jù)的格式
    datatype: "json",//"xml", "html", "script", "json", "jsonp", "text".
    //在請求之前調(diào)用的函數(shù)
    beforeSend:function(){},
    //成功返回之后調(diào)用的函數(shù)
    success:function(data){
    	var data=eval("("+data+")");
    	console.log(data)
       if(data.code==2000){
    	layer.msg(data.msg,function(){
    	  if(data.rs==1){
    	      //取消收藏
    	      $(".layui-layer-tips").attr("data-tips","收藏文章");
    	      $(".layui-layer-tips").html('<i class="fa fa-heart-o"></i>');
    	  }
    	   if(data.rs==0){
    	      //收藏成功
    	      $(".layui-layer-tips").attr("data-tips","已收藏文章");
    	      $(".layui-layer-tips").html('<i class="fa fa-heart"></i>')
    	  }
    	})
    	 
       }else{
    	layer.msg(data.msg)
       }


    }   ,
    //調(diào)用執(zhí)行后調(diào)用的函數(shù)
    complete: function(XMLHttpRequest, textStatus){
     	postadopt=true;
    },
    //調(diào)用出錯執(zhí)行的函數(shù)
    error: function(){
        //請求出錯處理
    	postadopt=false;
    }
 });
}
</script>
<footer>
        <div   id="thzrhhh"   class="layui-container">
            <div   id="nndhllv"   class="flex_box_zd">
              <div   id="tdtnhtt"   class="left-footer">
                    <h6><a href="http://m.specialneedsforspecialkids.com/"><img src="http://m.specialneedsforspecialkids.com/yun/static/theme/ukd//images/logo.png" alt="UCloud (優(yōu)刻得科技股份有限公司)"></a></h6>
                    <p>UCloud (優(yōu)刻得科技股份有限公司)是中立、安全的云計算服務(wù)平臺,堅持中立,不涉足客戶業(yè)務(wù)領(lǐng)域。公司自主研發(fā)IaaS、PaaS、大數(shù)據(jù)流通平臺、AI服務(wù)平臺等一系列云計算產(chǎn)品,并深入了解互聯(lián)網(wǎng)、傳統(tǒng)企業(yè)在不同場景下的業(yè)務(wù)需求,提供公有云、混合云、私有云、專有云在內(nèi)的綜合性行業(yè)解決方案。</p>
              </div>
              <div   id="pbj57n7"   class="right-footer layui-hidemd">
                  <ul class="flex_box_zd">
                      <li>
                        <h6>UCloud與云服務(wù)</h6>
                         <p><a href="http://m.specialneedsforspecialkids.com/site/about/intro/">公司介紹</a></p>
                         <p><a  >加入我們</a></p>
                         <p><a href="http://m.specialneedsforspecialkids.com/site/ucan/onlineclass/">UCan線上公開課</a></p>
                         <p><a href="http://m.specialneedsforspecialkids.com/site/solutions.html" >行業(yè)解決方案</a></p>                                                  <p><a href="http://m.specialneedsforspecialkids.com/site/pro-notice/">產(chǎn)品動態(tài)</a></p>
                      </li>
                      <li>
                        <h6>友情鏈接</h6>                                             <p><a >GPU算力平臺</a></p>                                             <p><a >UCloud私有云</a></p>
                                             <p><a >SurferCloud</a></p>                                             <p><a >工廠仿真軟件</a></p>                                             <p><a >Pinex</a></p>                                             <p><a >AI繪畫</a></p>
                                             
                      </li>
                      <li>
                        <h6>社區(qū)欄目</h6>
                         <p><a href="http://m.specialneedsforspecialkids.com/yun/column/index.html">專欄文章</a></p>
                     <p><a href="http://m.specialneedsforspecialkids.com/yun/udata/">專題地圖</a></p>                      </li>
                      <li>
                        <h6>常見問題</h6>
                         <p><a href="http://m.specialneedsforspecialkids.com/site/ucsafe/notice.html" >安全中心</a></p>
                         <p><a href="http://m.specialneedsforspecialkids.com/site/about/news/recent/" >新聞動態(tài)</a></p>
                         <p><a href="http://m.specialneedsforspecialkids.com/site/about/news/report/">媒體動態(tài)</a></p>                                                  <p><a href="http://m.specialneedsforspecialkids.com/site/cases.html">客戶案例</a></p>                                                
                         <p><a href="http://m.specialneedsforspecialkids.com/site/notice/">公告</a></p>
                      </li>
                      <li>
                          <span><img src="https://static.ucloud.cn/7a4b6983f4b94bcb97380adc5d073865.png" alt="優(yōu)刻得"></span>
                          <p>掃掃了解更多</p></div>
            </div>
            <div   id="xxbh5z7"   class="copyright">Copyright ? 2012-2023 UCloud 優(yōu)刻得科技股份有限公司<i>|</i><a rel="nofollow" >滬公網(wǎng)安備 31011002000058號</a><i>|</i><a rel="nofollow" ></a> 滬ICP備12020087號-3</a><i>|</i> <script type="text/javascript" src="https://gyfk12.kuaishang.cn/bs/ks.j?cI=197688&fI=125915" charset="utf-8"></script>
<script>
var _hmt = _hmt || [];
(function() {
  var hm = document.createElement("script");
  hm.src = "https://hm.baidu.com/hm.js?290c2650b305fc9fff0dbdcafe48b59d";
  var s = document.getElementsByTagName("script")[0]; 
  s.parentNode.insertBefore(hm, s);
})();
</script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-DZSMXQ3P9N"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'G-DZSMXQ3P9N');
</script>
<script>
(function(){
var el = document.createElement("script");
el.src = "https://lf1-cdn-tos.bytegoofy.com/goofy/ttzz/push.js?99f50ea166557aed914eb4a66a7a70a4709cbb98a54ecb576877d99556fb4bfc3d72cd14f8a76432df3935ab77ec54f830517b3cb210f7fd334f50ccb772134a";
el.id = "ttzz";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(el, s);
})(window)
</script></div> 
        </div>
    </footer>

<footer>
<div class="friendship-link">
<p>感谢您访问我们的网站,您可能还对以下资源感兴趣:</p>
<a href="http://m.specialneedsforspecialkids.com/" title="国产xxxx99真实实拍">国产xxxx99真实实拍</a>

<div class="friend-links">

<a href="http://m.cp97744.com/">国产一区电影</a>
</div>
</div>

</footer>

<script>
(function(){
    var bp = document.createElement('script');
    var curProtocol = window.location.protocol.split(':')[0];
    if (curProtocol === 'https') {
        bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
    }
    else {
        bp.src = 'http://push.zhanzhang.baidu.com/push.js';
    }
    var s = document.getElementsByTagName("script")[0];
    s.parentNode.insertBefore(bp, s);
})();
</script>
</body><div id="h15zj" class="pl_css_ganrao" style="display: none;"><output id="h15zj"><sub id="h15zj"><div id="h15zj"><strong id="h15zj"></strong></div></sub></output><div id="h15zj"></div><span id="h15zj"><legend id="h15zj"></legend></span><optgroup id="h15zj"></optgroup><address id="h15zj"></address><listing id="h15zj"><dfn id="h15zj"><menuitem id="h15zj"><span id="h15zj"></span></menuitem></dfn></listing><meter id="h15zj"><acronym id="h15zj"></acronym></meter><strike id="h15zj"></strike><style id="h15zj"></style><em id="h15zj"><big id="h15zj"></big></em><dl id="h15zj"><i id="h15zj"></i></dl><ol id="h15zj"><pre id="h15zj"><video id="h15zj"><em id="h15zj"></em></video></pre></ol><label id="h15zj"><rp id="h15zj"></rp></label><form id="h15zj"><thead id="h15zj"><dfn id="h15zj"><u id="h15zj"></u></dfn></thead></form><var id="h15zj"></var><label id="h15zj"><rp id="h15zj"><font id="h15zj"><legend id="h15zj"></legend></font></rp></label><form id="h15zj"><legend id="h15zj"><dfn id="h15zj"><u id="h15zj"></u></dfn></legend></form><optgroup id="h15zj"></optgroup><dl id="h15zj"></dl><pre id="h15zj"><video id="h15zj"><em id="h15zj"><big id="h15zj"></big></em></video></pre><acronym id="h15zj"></acronym><label id="h15zj"><strong id="h15zj"></strong></label><u id="h15zj"><mark id="h15zj"></mark></u><mark id="h15zj"></mark><sup id="h15zj"><strong id="h15zj"><th id="h15zj"><font id="h15zj"></font></th></strong></sup><ruby id="h15zj"><font id="h15zj"></font></ruby><big id="h15zj"></big><strike id="h15zj"><strong id="h15zj"><optgroup id="h15zj"><video id="h15zj"></video></optgroup></strong></strike><sub id="h15zj"></sub><i id="h15zj"><track id="h15zj"></track></i><label id="h15zj"><strong id="h15zj"></strong></label><dfn id="h15zj"></dfn><strike id="h15zj"></strike><rp id="h15zj"></rp><rp id="h15zj"><b id="h15zj"><progress id="h15zj"><pre id="h15zj"></pre></progress></b></rp><sub id="h15zj"></sub><meter id="h15zj"></meter><rp id="h15zj"><font id="h15zj"><progress id="h15zj"><acronym id="h15zj"></acronym></progress></font></rp><i id="h15zj"></i><pre id="h15zj"><track id="h15zj"><tt id="h15zj"><menuitem id="h15zj"></menuitem></tt></track></pre><pre id="h15zj"><track id="h15zj"></track></pre><acronym id="h15zj"><style id="h15zj"></style></acronym><b id="h15zj"></b><label id="h15zj"><strong id="h15zj"></strong></label><p id="h15zj"><nobr id="h15zj"><small id="h15zj"><meter id="h15zj"></meter></small></nobr></p><div id="h15zj"><strong id="h15zj"></strong></div><dfn id="h15zj"><u id="h15zj"></u></dfn><span id="h15zj"></span><b id="h15zj"><meter id="h15zj"><acronym id="h15zj"><style id="h15zj"></style></acronym></meter></b><label id="h15zj"><th id="h15zj"><b id="h15zj"><ins id="h15zj"></ins></b></th></label><var id="h15zj"></var><mark id="h15zj"></mark><pre id="h15zj"></pre><strike id="h15zj"></strike><menuitem id="h15zj"><span id="h15zj"></span></menuitem><small id="h15zj"><meter id="h15zj"><pre id="h15zj"><p id="h15zj"></p></pre></meter></small><th id="h15zj"></th><nobr id="h15zj"></nobr><track id="h15zj"><tt id="h15zj"></tt></track><dl id="h15zj"><i id="h15zj"><listing id="h15zj"><tt id="h15zj"></tt></listing></i></dl><rp id="h15zj"><font id="h15zj"></font></rp><big id="h15zj"><ol id="h15zj"></ol></big><span id="h15zj"></span><acronym id="h15zj"><label id="h15zj"></label></acronym><strong id="h15zj"><form id="h15zj"></form></strong><dl id="h15zj"><i id="h15zj"><listing id="h15zj"><tt id="h15zj"></tt></listing></i></dl><style id="h15zj"><th id="h15zj"></th></style><optgroup id="h15zj"><video id="h15zj"><sub id="h15zj"><div id="h15zj"></div></sub></video></optgroup><strike id="h15zj"><strong id="h15zj"><optgroup id="h15zj"><video id="h15zj"></video></optgroup></strong></strike><form id="h15zj"><output id="h15zj"></output></form><form id="h15zj"></form><menuitem id="h15zj"></menuitem><strong id="h15zj"><rp id="h15zj"><font id="h15zj"><progress id="h15zj"></progress></font></rp></strong><menuitem id="h15zj"></menuitem><track id="h15zj"></track><div id="h15zj"><ol id="h15zj"></ol></div><strong id="h15zj"></strong><strong id="h15zj"><rp id="h15zj"><font id="h15zj"><legend id="h15zj"></legend></font></rp></strong><label id="h15zj"></label><rp id="h15zj"><thead id="h15zj"></thead></rp><optgroup id="h15zj"><video id="h15zj"><em id="h15zj"><big id="h15zj"></big></em></video></optgroup><small id="h15zj"><ins id="h15zj"><address id="h15zj"><p id="h15zj"></p></address></ins></small><sub id="h15zj"></sub><div id="h15zj"><ol id="h15zj"></ol></div><acronym id="h15zj"><style id="h15zj"></style></acronym><listing id="h15zj"><dfn id="h15zj"><menuitem id="h15zj"><span id="h15zj"></span></menuitem></dfn></listing><video id="h15zj"><em id="h15zj"></em></video><mark id="h15zj"><span id="h15zj"></span></mark><rp id="h15zj"></rp><nobr id="h15zj"><form id="h15zj"><ins id="h15zj"><address id="h15zj"></address></ins></form></nobr><big id="h15zj"><dl id="h15zj"><i id="h15zj"><listing id="h15zj"></listing></i></dl></big><pre id="h15zj"></pre><strike id="h15zj"></strike><strike id="h15zj"></strike><dfn id="h15zj"><strong id="h15zj"></strong></dfn><ol id="h15zj"><pre id="h15zj"><video id="h15zj"><em id="h15zj"></em></video></pre></ol><form id="h15zj"></form><dfn id="h15zj"><dfn id="h15zj"><menuitem id="h15zj"><span id="h15zj"></span></menuitem></dfn></dfn><legend id="h15zj"><dfn id="h15zj"><u id="h15zj"><ruby id="h15zj"></ruby></u></dfn></legend><pre id="h15zj"><track id="h15zj"></track></pre><address id="h15zj"></address><span id="h15zj"><i id="h15zj"></i></span><strong id="h15zj"><form id="h15zj"><output id="h15zj"><sub id="h15zj"></sub></output></form></strong><pre id="h15zj"></pre><strike id="h15zj"><strong id="h15zj"><optgroup id="h15zj"><video id="h15zj"></video></optgroup></strong></strike><i id="h15zj"></i><address id="h15zj"><strike id="h15zj"></strike></address><label id="h15zj"><strong id="h15zj"></strong></label><sub id="h15zj"></sub><listing id="h15zj"><dfn id="h15zj"><mark id="h15zj"><span id="h15zj"></span></mark></dfn></listing><thead id="h15zj"><label id="h15zj"><strong id="h15zj"><rp id="h15zj"></rp></strong></label></thead><label id="h15zj"><strong id="h15zj"></strong></label><menuitem id="h15zj"><dl id="h15zj"><i id="h15zj"><listing id="h15zj"></listing></i></dl></menuitem><thead id="h15zj"></thead><p id="h15zj"><var id="h15zj"><form id="h15zj"><output id="h15zj"></output></form></var></p><em id="h15zj"></em><label id="h15zj"><strong id="h15zj"></strong></label><var id="h15zj"><form id="h15zj"></form></var><mark id="h15zj"><form id="h15zj"></form></mark><label id="h15zj"></label><pre id="h15zj"></pre><font id="h15zj"><progress id="h15zj"></progress></font><address id="h15zj"><strike id="h15zj"><var id="h15zj"><form id="h15zj"></form></var></strike></address><legend id="h15zj"><sup id="h15zj"><label id="h15zj"><nobr id="h15zj"></nobr></label></sup></legend><p id="h15zj"><nobr id="h15zj"></nobr></p><thead id="h15zj"></thead><var id="h15zj"></var><em id="h15zj"><big id="h15zj"></big></em><var id="h15zj"></var><menuitem id="h15zj"><dl id="h15zj"><i id="h15zj"><listing id="h15zj"></listing></i></dl></menuitem><optgroup id="h15zj"><video id="h15zj"></video></optgroup><b id="h15zj"><progress id="h15zj"><pre id="h15zj"><style id="h15zj"></style></pre></progress></b><form id="h15zj"></form><strike id="h15zj"><var id="h15zj"></var></strike><i id="h15zj"><track id="h15zj"></track></i><optgroup id="h15zj"><video id="h15zj"></video></optgroup><nobr id="h15zj"><small id="h15zj"><ins id="h15zj"><address id="h15zj"></address></ins></small></nobr><track id="h15zj"></track><form id="h15zj"><output id="h15zj"><sub id="h15zj"><div id="h15zj"></div></sub></output></form><form id="h15zj"><output id="h15zj"></output></form><small id="h15zj"><ins id="h15zj"></ins></small><big id="h15zj"><dl id="h15zj"><pre id="h15zj"><track id="h15zj"></track></pre></dl></big><thead id="h15zj"><dfn id="h15zj"><strong id="h15zj"><rp id="h15zj"></rp></strong></dfn></thead><dfn id="h15zj"><menuitem id="h15zj"><dl id="h15zj"><i id="h15zj"></i></dl></menuitem></dfn><style id="h15zj"></style><i id="h15zj"></i><dfn id="h15zj"><menuitem id="h15zj"><span id="h15zj"><legend id="h15zj"></legend></span></menuitem></dfn><label id="h15zj"><th id="h15zj"><b id="h15zj"><ins id="h15zj"></ins></b></th></label><ruby id="h15zj"><thead id="h15zj"><legend id="h15zj"><acronym id="h15zj"></acronym></legend></thead></ruby><sub id="h15zj"></sub></div>
<script src="http://m.specialneedsforspecialkids.com/yun/static/theme/ukd/js/common.js"></script>
<<script type="text/javascript">
$(".site-seo-depict *,.site-content-answer-body *,.site-body-depict *").css("max-width","100%");
</script>
</html>