摘要:隨著應(yīng)用的復(fù)雜程度越來越高,很多公司越來越重視前端單元測試。最后我們可以利用覆蓋率來看下用例的覆蓋程度是否足夠一般來說不用刻意追求,根據(jù)實際情況來定單元測試是測試驅(qū)動開發(fā)的基礎(chǔ)。
隨著 Web 應(yīng)用的復(fù)雜程度越來越高,很多公司越來越重視前端單元測試。我們看到的大多數(shù)教程都會講單元測試的重要性、一些有代表性的測試框架 api 怎么使用,但在實際項目中單元測試要怎么下手?測試用例應(yīng)該包含哪些具體內(nèi)容呢?
本文從一個真實的應(yīng)用場景出發(fā),從設(shè)計模式、代碼結(jié)構(gòu)來分析單元測試應(yīng)該包含哪些內(nèi)容,具體測試用例怎么寫,希望看到的童鞋都能有所收獲。
完整的代碼內(nèi)容在 這里 (各位童鞋覺得好幫忙去給個 哈)。
該項目采用 react 技術(shù)棧,用到的主要框架包括:react、redux、react-redux、redux-actions、reselect、redux-saga、seamless-immutable、antd。
應(yīng)用場景介紹這個應(yīng)用場景從 UI 層來講主要由兩個部分組成:
工具欄,包含刷新按鈕、關(guān)鍵字搜索框
表格展示,采用分頁的形式瀏覽
看到這里有的童鞋可能會說:切!這么簡單的界面和業(yè)務(wù)邏輯,還是真實場景嗎,還需要寫神馬單元測試嗎?
別急,為了保證文章的閱讀體驗和長度適中,能講清楚問題的簡潔場景就是好場景不是嗎?慢慢往下看。
設(shè)計模式與結(jié)構(gòu)分析在這個場景設(shè)計開發(fā)中,我們嚴格遵守 redux 單向數(shù)據(jù)流 與 react-redux 的最佳實踐,并采用 redux-saga 來處理業(yè)務(wù)流,reselect 來處理狀態(tài)緩存,通過 fetch 來調(diào)用后臺接口,與真實的項目沒有差異。
分層設(shè)計與代碼組織如下所示:
中間 store 中的內(nèi)容都是 redux 相關(guān)的,看名稱應(yīng)該都能知道意思了。
具體的代碼請看 這里。
單元測試部分介紹先講一下用到了哪些測試框架和工具,主要內(nèi)容包括:
jest ,測試框架
enzyme ,專測 react ui 層
sinon ,具有獨立的 fakes、spies、stubs、mocks 功能庫
nock ,模擬 HTTP Server
如果有童鞋對上面這些使用和配置不熟的話,直接看官方文檔吧,比任何教程都寫的好。
接下來,我們就開始編寫具體的測試用例代碼了,下面會針對每個層面給出代碼片段和解析。那么我們先從 actions 開始吧。
為使文章盡量簡短、清晰,下面的代碼片段不是每個文件的完整內(nèi)容,完整內(nèi)容在 這里 。actions
業(yè)務(wù)里面我使用了 redux-actions 來產(chǎn)生 action,這里用工具欄做示例,先看一段業(yè)務(wù)代碼:
import { createAction } from "redux-actions"; import * as type from "../types/bizToolbar"; export const updateKeywords = createAction(type.BIZ_TOOLBAR_KEYWORDS_UPDATE); // ...
對于 actions 測試,我們主要是驗證產(chǎn)生的 action 對象是否正確:
import * as type from "@/store/types/bizToolbar"; import * as actions from "@/store/actions/bizToolbar"; /* 測試 bizToolbar 相關(guān) actions */ describe("bizToolbar actions", () => { /* 測試更新搜索關(guān)鍵字 */ test("should create an action for update keywords", () => { // 構(gòu)建目標 action const keywords = "some keywords"; const expectedAction = { type: type.BIZ_TOOLBAR_KEYWORDS_UPDATE, payload: keywords }; // 斷言 redux-actions 產(chǎn)生的 action 是否正確 expect(actions.updateKeywords(keywords)).toEqual(expectedAction); }); // ... });
這個測試用例的邏輯很簡單,首先構(gòu)建一個我們期望的結(jié)果,然后調(diào)用業(yè)務(wù)代碼,最后驗證業(yè)務(wù)代碼的運行結(jié)果與期望是否一致。這就是寫測試用例的基本套路。
我們在寫測試用例時盡量保持用例的單一職責(zé),不要覆蓋太多不同的業(yè)務(wù)范圍。測試用例數(shù)量可以有很多個,但每個都不應(yīng)該很復(fù)雜。
reducers接著是 reducers,依然采用 redux-actions 的 handleActions 來編寫 reducer,這里用表格的來做示例:
import { handleActions } from "redux-actions"; import Immutable from "seamless-immutable"; import * as type from "../types/bizTable"; /* 默認狀態(tài) */ export const defaultState = Immutable({ loading: false, pagination: { current: 1, pageSize: 15, total: 0 }, data: [] }); export default handleActions( { // ... /* 處理獲得數(shù)據(jù)成功 */ [type.BIZ_TABLE_GET_RES_SUCCESS]: (state, {payload}) => { return state.merge( { loading: false, pagination: {total: payload.total}, data: payload.items }, {deep: true} ); }, // ... }, defaultState );
這里的狀態(tài)對象使用了 seamless-immutable
對于 reducer,我們主要測試兩個方面:
對于未知的 action.type ,是否能返回當(dāng)前狀態(tài)。
對于每個業(yè)務(wù) type ,是否都返回了經(jīng)過正確處理的狀態(tài)。
下面是針對以上兩點的測試代碼:
import * as type from "@/store/types/bizTable"; import reducer, { defaultState } from "@/store/reducers/bizTable"; /* 測試 bizTable reducer */ describe("bizTable reducer", () => { /* 測試未指定 state 參數(shù)情況下返回當(dāng)前缺省 state */ test("should return the default state", () => { expect(reducer(undefined, {type: "UNKNOWN"})).toEqual(defaultState); }); // ... /* 測試處理正常數(shù)據(jù)結(jié)果 */ test("should handle successful data response", () => { /* 模擬返回數(shù)據(jù)結(jié)果 */ const payload = { items: [ {id: 1, code: "1"}, {id: 2, code: "2"} ], total: 2 }; /* 期望返回的狀態(tài) */ const expectedState = defaultState .setIn(["pagination", "total"], payload.total) .set("data", payload.items) .set("loading", false); expect( reducer(defaultState, { type: type.BIZ_TABLE_GET_RES_SUCCESS, payload }) ).toEqual(expectedState); }); // ... });
這里的測試用例邏輯也很簡單,依然是上面斷言期望結(jié)果的套路。下面是 selectors 的部分。
selectorsselector 的作用是獲取對應(yīng)業(yè)務(wù)的狀態(tài),這里使用了 reselect 來做緩存,防止 state 未改變的情況下重新計算,先看一下表格的 selector 代碼:
import { createSelector } from "reselect"; import * as defaultSettings from "@/utils/defaultSettingsUtil"; // ... const getBizTableState = (state) => state.bizTable; export const getBizTable = createSelector(getBizTableState, (bizTable) => { return bizTable.merge({ pagination: defaultSettings.pagination }, {deep: true}); });
這里的分頁器部分參數(shù)在項目中是統(tǒng)一設(shè)置,所以 reselect 很好的完成了這個工作:如果業(yè)務(wù)狀態(tài)不變,直接返回上次的緩存。分頁器默認設(shè)置如下:
export const pagination = { size: "small", showTotal: (total, range) => `${range[0]}-${range[1]} / ${total}`, pageSizeOptions: ["15", "25", "40", "60"], showSizeChanger: true, showQuickJumper: true };
那么我們的測試也主要是兩個方面:
對于業(yè)務(wù) selector ,是否返回了正確的內(nèi)容。
緩存功能是否正常。
測試代碼如下:
import Immutable from "seamless-immutable"; import { getBizTable } from "@/store/selectors"; import * as defaultSettingsUtil from "@/utils/defaultSettingsUtil"; /* 測試 bizTable selector */ describe("bizTable selector", () => { let state; beforeEach(() => { state = createState(); /* 每個用例執(zhí)行前重置緩存計算次數(shù) */ getBizTable.resetRecomputations(); }); function createState() { return Immutable({ bizTable: { loading: false, pagination: { current: 1, pageSize: 15, total: 0 }, data: [] } }); } /* 測試返回正確的 bizTable state */ test("should return bizTable state", () => { /* 業(yè)務(wù)狀態(tài) ok 的 */ expect(getBizTable(state)).toMatchObject(state.bizTable); /* 分頁默認參數(shù)設(shè)置 ok 的 */ expect(getBizTable(state)).toMatchObject({ pagination: defaultSettingsUtil.pagination }); }); /* 測試 selector 緩存是否有效 */ test("check memoization", () => { getBizTable(state); /* 第一次計算,緩存計算次數(shù)為 1 */ expect(getBizTable.recomputations()).toBe(1); getBizTable(state); /* 業(yè)務(wù)狀態(tài)不變的情況下,緩存計算次數(shù)應(yīng)該還是 1 */ expect(getBizTable.recomputations()).toBe(1); const newState = state.setIn(["bizTable", "loading"], true); getBizTable(newState); /* 業(yè)務(wù)狀態(tài)改變了,緩存計算次數(shù)應(yīng)該是 2 了 */ expect(getBizTable.recomputations()).toBe(2); }); });
測試用例依然很簡單有木有?保持這個節(jié)奏就對了。下面來講下稍微有點復(fù)雜的地方,sagas 部分。
sagas這里我用了 redux-saga 處理業(yè)務(wù)流,這里具體也就是異步調(diào)用 api 請求數(shù)據(jù),處理成功結(jié)果和錯誤結(jié)果等。
可能有的童鞋覺得搞這么復(fù)雜干嘛,異步請求用個 redux-thunk 不就完事了嗎?別急,耐心看完你就明白了。
這里有必要大概介紹下 redux-saga 的工作方式。saga 是一種 es6 的生成器函數(shù) - Generator ,我們利用他來產(chǎn)生各種聲明式的 effects ,由 redux-saga 引擎來消化處理,推動業(yè)務(wù)進行。
這里我們來看看獲取表格數(shù)據(jù)的業(yè)務(wù)代碼:
import { all, takeLatest, put, select, call } from "redux-saga/effects"; import * as type from "../types/bizTable"; import * as actions from "../actions/bizTable"; import { getBizToolbar, getBizTable } from "../selectors"; import * as api from "@/services/bizApi"; // ... export function* onGetBizTableData() { /* 先獲取 api 調(diào)用需要的參數(shù):關(guān)鍵字、分頁信息等 */ const {keywords} = yield select(getBizToolbar); const {pagination} = yield select(getBizTable); const payload = { keywords, paging: { skip: (pagination.current - 1) * pagination.pageSize, max: pagination.pageSize } }; try { /* 調(diào)用 api */ const result = yield call(api.getBizTableData, payload); /* 正常返回 */ yield put(actions.putBizTableDataSuccessResult(result)); } catch (err) { /* 錯誤返回 */ yield put(actions.putBizTableDataFailResult()); } }
不熟悉 redux-saga 的童鞋也不要太在意代碼的具體寫法,看注釋應(yīng)該能了解這個業(yè)務(wù)的具體步驟:
從對應(yīng)的 state 里取到調(diào)用 api 時需要的參數(shù)部分(搜索關(guān)鍵字、分頁),這里調(diào)用了剛才的 selector。
組合好參數(shù)并調(diào)用對應(yīng)的 api 層。
如果正常返回結(jié)果,則發(fā)送成功 action 通知 reducer 更新狀態(tài)。
如果錯誤返回,則發(fā)送錯誤 action 通知 reducer。
那么具體的測試用例應(yīng)該怎么寫呢?我們都知道這種業(yè)務(wù)代碼涉及到了 api 或其他層的調(diào)用,如果要寫單元測試必須做一些 mock 之類來防止真正調(diào)用 api 層,下面我們來看一下 怎么針對這個 saga 來寫測試用例:
import { put, select } from "redux-saga/effects"; // ... /* 測試獲取數(shù)據(jù) */ test("request data, check success and fail", () => { /* 當(dāng)前的業(yè)務(wù)狀態(tài) */ const state = { bizToolbar: { keywords: "some keywords" }, bizTable: { pagination: { current: 1, pageSize: 15 } } }; const gen = cloneableGenerator(saga.onGetBizTableData)(); /* 1. 是否調(diào)用了正確的 selector 來獲得請求時要發(fā)送的參數(shù) */ expect(gen.next().value).toEqual(select(getBizToolbar)); expect(gen.next(state.bizToolbar).value).toEqual(select(getBizTable)); /* 2. 是否調(diào)用了 api 層 */ const callEffect = gen.next(state.bizTable).value; expect(callEffect["CALL"].fn).toBe(api.getBizTableData); /* 調(diào)用 api 層參數(shù)是否傳遞正確 */ expect(callEffect["CALL"].args[0]).toEqual({ keywords: "some keywords", paging: {skip: 0, max: 15} }); /* 3. 模擬正確返回分支 */ const successBranch = gen.clone(); const successRes = { items: [ {id: 1, code: "1"}, {id: 2, code: "2"} ], total: 2 }; expect(successBranch.next(successRes).value).toEqual( put(actions.putBizTableDataSuccessResult(successRes))); expect(successBranch.next().done).toBe(true); /* 4. 模擬錯誤返回分支 */ const failBranch = gen.clone(); expect(failBranch.throw(new Error("模擬產(chǎn)生異常")).value).toEqual( put(actions.putBizTableDataFailResult())); expect(failBranch.next().done).toBe(true); });
這個測試用例相比前面的復(fù)雜了一些,我們先來說下測試 saga 的原理。前面說過 saga 實際上是返回各種聲明式的 effects ,然后由引擎來真正執(zhí)行。所以我們測試的目的就是要看 effects 的產(chǎn)生是否符合預(yù)期。那么effect 到底是個神馬東西呢?其實就是字面量對象!
我們可以用在業(yè)務(wù)代碼同樣的方式來產(chǎn)生這些字面量對象,對于字面量對象的斷言就非常簡單了,并且沒有直接調(diào)用 api 層,就用不著做 mock 咯!這個測試用例的步驟就是利用生成器函數(shù)一步步的產(chǎn)生下一個 effect ,然后斷言比較。
從上面的注釋 3、4 可以看到,redux-saga 還提供了一些輔助函數(shù)來方便的處理分支斷點。
這也是我選擇 redux-saga 的原因:強大并且利于測試。
api 和 fetch 工具庫接下來就是api 層相關(guān)的了。前面講過調(diào)用后臺請求是用的 fetch ,我封裝了兩個方法來簡化調(diào)用和結(jié)果處理:getJSON() 、postJSON() ,分別對應(yīng) GET 、POST 請求。先來看看 api 層代碼:
import { fetcher } from "@/utils/fetcher"; export function getBizTableData(payload) { return fetcher.postJSON("/api/biz/get-table", payload); }
業(yè)務(wù)代碼很簡單,那么測試用例也很簡單:
import sinon from "sinon"; import { fetcher } from "@/utils/fetcher"; import * as api from "@/services/bizApi"; /* 測試 bizApi */ describe("bizApi", () => { let fetcherStub; beforeAll(() => { fetcherStub = sinon.stub(fetcher); }); // ... /* getBizTableData api 應(yīng)該調(diào)用正確的 method 和傳遞正確的參數(shù) */ test("getBizTableData api should call postJSON with right params of fetcher", () => { /* 模擬參數(shù) */ const payload = {a: 1, b: 2}; api.getBizTableData(payload); /* 檢查是否調(diào)用了工具庫 */ expect(fetcherStub.postJSON.callCount).toBe(1); /* 檢查調(diào)用參數(shù)是否正確 */ expect(fetcherStub.postJSON.lastCall.calledWith("/api/biz/get-table", payload)).toBe(true); }); });
由于 api 層直接調(diào)用了工具庫,所以這里用 sinon.stub() 來替換工具庫達到測試目的。
接著就是測試自己封裝的 fetch 工具庫了,這里 fetch 我是用的 isomorphic-fetch ,所以選擇了 nock 來模擬 Server 進行測試,主要是測試正常訪問返回結(jié)果和模擬服務(wù)器異常等,示例片段如下:
import nock from "nock"; import { fetcher, FetchError } from "@/utils/fetcher"; /* 測試 fetcher */ describe("fetcher", () => { afterEach(() => { nock.cleanAll(); }); afterAll(() => { nock.restore(); }); /* 測試 getJSON 獲得正常數(shù)據(jù) */ test("should get success result", () => { nock("http://some") .get("/test") .reply(200, {success: true, result: "hello, world"}); return expect(fetcher.getJSON("http://some/test")).resolves.toMatch(/^hello.+$/); }); // ... /* 測試 getJSON 捕獲 server 大于 400 的異常狀態(tài) */ test("should catch server status: 400+", (done) => { const status = 500; nock("http://some") .get("/test") .reply(status); fetcher.getJSON("http://some/test").catch((error) => { expect(error).toEqual(expect.any(FetchError)); expect(error).toHaveProperty("detail"); expect(error.detail.status).toBe(status); done(); }); }); /* 測試 getJSON 傳遞正確的 headers 和 query strings */ test("check headers and query string of getJSON()", () => { nock("http://some", { reqheaders: { "Accept": "application/json", "authorization": "Basic Auth" } }) .get("/test") .query({a: "123", b: 456}) .reply(200, {success: true, result: true}); const headers = new Headers(); headers.append("authorization", "Basic Auth"); return expect(fetcher.getJSON( "http://some/test", {a: "123", b: 456}, headers)).resolves.toBe(true); }); // ... });
基本也沒什么復(fù)雜的,主要注意 fetch 是 promise 返回,jest 的各種異步測試方案都能很好滿足。
剩下的部分就是跟 UI 相關(guān)的了。
容器組件容器組件的主要目的是傳遞 state 和 actions,看下工具欄的容器組件代碼:
import { connect } from "react-redux"; import { getBizToolbar } from "@/store/selectors"; import * as actions from "@/store/actions/bizToolbar"; import BizToolbar from "@/components/BizToolbar"; const mapStateToProps = (state) => ({ ...getBizToolbar(state) }); const mapDispatchToProps = { reload: actions.reload, updateKeywords: actions.updateKeywords }; export default connect(mapStateToProps, mapDispatchToProps)(BizToolbar);
那么測試用例的目的也是檢查這些,這里使用了 redux-mock-store 來模擬 redux 的 store :
import React from "react"; import { shallow } from "enzyme"; import configureStore from "redux-mock-store"; import BizToolbar from "@/containers/BizToolbar"; /* 測試容器組件 BizToolbar */ describe("BizToolbar container", () => { const initialState = { bizToolbar: { keywords: "some keywords" } }; const mockStore = configureStore(); let store; let container; beforeEach(() => { store = mockStore(initialState); container = shallow(); }); /* 測試 state 到 props 的映射是否正確 */ test("should pass state to props", () => { const props = container.props(); expect(props).toHaveProperty("keywords", initialState.bizToolbar.keywords); }); /* 測試 actions 到 props 的映射是否正確 */ test("should pass actions to props", () => { const props = container.props(); expect(props).toHaveProperty("reload", expect.any(Function)); expect(props).toHaveProperty("updateKeywords", expect.any(Function)); }); });
很簡單有木有,所以也沒啥可說的了。
UI 組件這里以表格組件作為示例,我們將直接來看測試用例是怎么寫。一般來說 UI 組件我們主要測試以下幾個方面:
是否渲染了正確的 DOM 結(jié)構(gòu)
樣式是否正確
業(yè)務(wù)邏輯觸發(fā)是否正確
下面是測試用例代碼:
import React from "react"; import { mount } from "enzyme"; import sinon from "sinon"; import { Table } from "antd"; import * as defaultSettingsUtil from "@/utils/defaultSettingsUtil"; import BizTable from "@/components/BizTable"; /* 測試 UI 組件 BizTable */ describe("BizTable component", () => { const defaultProps = { loading: false, pagination: Object.assign({}, { current: 1, pageSize: 15, total: 2 }, defaultSettingsUtil.pagination), data: [{id: 1}, {id: 2}], getData: sinon.fake(), updateParams: sinon.fake() }; let defaultWrapper; beforeEach(() => { defaultWrapper = mount(); }); // ... /* 測試是否渲染了正確的功能子組件 */ test("should render table and pagination", () => { /* 是否渲染了 Table 組件 */ expect(defaultWrapper.find(Table).exists()).toBe(true); /* 是否渲染了 分頁器 組件,樣式是否正確(mini) */ expect(defaultWrapper.find(".ant-table-pagination.mini").exists()).toBe(true); }); /* 測試首次加載時數(shù)據(jù)列表為空是否發(fā)起加載數(shù)據(jù)請求 */ test("when componentDidMount and data is empty, should getData", () => { sinon.spy(BizTable.prototype, "componentDidMount"); const props = Object.assign({}, defaultProps, { pagination: Object.assign({}, { current: 1, pageSize: 15, total: 0 }, defaultSettingsUtil.pagination), data: [] }); const wrapper = mount( ); expect(BizTable.prototype.componentDidMount.calledOnce).toBe(true); expect(props.getData.calledOnce).toBe(true); BizTable.prototype.componentDidMount.restore(); }); /* 測試 table 翻頁后是否正確觸發(fā) updateParams */ test("when change pagination of table, should updateParams", () => { const table = defaultWrapper.find(Table); table.props().onChange({current: 2, pageSize: 25}); expect(defaultProps.updateParams.lastCall.args[0]) .toEqual({paging: {current: 2, pageSize: 25}}); }); });
得益于設(shè)計分層的合理性,我們很容易利用構(gòu)造 props 來達到測試目的,結(jié)合 enzyme 和 sinon ,測試用例依然保持簡單的節(jié)奏。
總結(jié)以上就是這個場景完整的測試用例編寫思路和示例代碼,文中提及的思路方法也完全可以用在 Vue 、Angular 項目上。完整的代碼內(nèi)容在 這里 (重要的事情多說幾遍,各位童鞋覺得好幫忙去給個 哈)。
最后我們可以利用覆蓋率來看下用例的覆蓋程度是否足夠(一般來說不用刻意追求 100%,根據(jù)實際情況來定):
單元測試是 TDD 測試驅(qū)動開發(fā)的基礎(chǔ)。從以上整個過程可以看出,好的設(shè)計分層是很容易編寫測試用例的,單元測試不單單只是為了保證代碼質(zhì)量:他會逼著你思考代碼設(shè)計的合理性,拒絕面條代碼
借用 Clean Code 的結(jié)束語:
2005 年,在參加于丹佛舉行的敏捷大會時,Elisabeth Hedrickson 遞給我一條類似 Lance Armstrong 熱銷的那種綠色腕帶。這條腕帶上面寫著“沉迷測試”(Test Obsessed)的字樣。我高興地戴上,并自豪地一直系著。自從 1999 年從 Kent Beck 那兒學(xué)到 TDD 以來,我的確迷上了測試驅(qū)動開發(fā)。不過跟著就發(fā)生了些奇事。我發(fā)現(xiàn)自己無法取下腕帶。不僅是因為腕帶很緊,而且那也是條精神上的緊箍咒。那腕帶就是我職業(yè)道德的宣告,也是我承諾盡己所能寫出最好代碼的提示。取下它,仿佛就是違背了這些宣告和承諾似的。
所以它還在我的手腕上。在寫代碼時,我用余光瞟見它。它一直提醒我,我做了寫出整潔代碼的承諾。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/96720.html
摘要:我們很容易發(fā)現(xiàn),過濾器可以比喻成一張濾網(wǎng)。這究竟是怎么回事啊我們可以這樣理解過濾器不單單只有一個,那么我們怎么管理這些過濾器呢在中就使用了鏈式結(jié)構(gòu)。第一種方式在文件中配置用于注冊過濾器用于為過濾器指定一個名字,該元素的內(nèi)容不能為空。 什么是過濾器 過濾器是Servlet的高級特性之一,也別把它想得那么高深,只不過是實現(xiàn)Filter接口的Java類罷了! 首先,我們來看看過濾器究竟Web...
摘要:動態(tài)地代理,可以猜測一下它的含義,在運行時動態(tài)地對某些東西代理,代理它做了其他事情。所以動態(tài)代理的內(nèi)容重點就是這個。所以下一篇我們來細致了解下的到底是怎么使用動態(tài)代理的。 之前講了《零基礎(chǔ)帶你看Spring源碼——IOC控制反轉(zhuǎn)》,本來打算下一篇講講Srping的AOP的,但是其中會涉及到Java的動態(tài)代理,所以先單獨一篇來了解下Java的動態(tài)代理到底是什么,Java是怎么實現(xiàn)它的。 ...
摘要:甲乙交易活動不需要雙方見面,避免了雙方的互不信任造成交易失敗的問題。這就是的核心思想。統(tǒng)一配置,便于修改。帶參數(shù)的構(gòu)造函數(shù)創(chuàng)建對象首先,就要提供帶參數(shù)的構(gòu)造函數(shù)接下來,關(guān)鍵是怎么配置文件了。 前言 前面已經(jīng)學(xué)習(xí)了Struts2和Hibernate框架了。接下來學(xué)習(xí)的是Spring框架...本博文主要是引入Spring框架... Spring介紹 Spring誕生: 創(chuàng)建Spring的...
摘要:瀏覽器緩存作為性能優(yōu)化的重要一環(huán),對于前端而言,重要性不言而喻。根據(jù)瀏覽器發(fā)送的修改時間和服務(wù)端的修改時間進行比對,一致的話代表資源沒有改變,服務(wù)端返回正文為空的響應(yīng),讓瀏覽器中緩存中讀取資源,這就大大減小了請求的消耗。 瀏覽器緩存作為性能優(yōu)化的重要一環(huán),對于前端而言,重要性不言而喻。以前總是一知半解的,所以這次好好整理總結(jié)了一下。 1、緩存機制 首先我們來總體感知一下它的匹配流程,如...
摘要:目錄前言架構(gòu)安裝第一個爬蟲爬取有道翻譯創(chuàng)建項目創(chuàng)建創(chuàng)建解析運行爬蟲爬取單詞釋義下載單詞語音文件前言學(xué)習(xí)有一段時間了,當(dāng)時想要獲取一下百度漢字的解析,又不想一個個漢字去搜,復(fù)制粘貼太費勁,考慮到爬蟲的便利性,這篇文章是介紹一個爬蟲框架, 目錄 前言 架構(gòu) 安裝 第一個爬蟲:爬取有道翻譯 創(chuàng)建項目 創(chuàng)建Item 創(chuàng)建Spider 解析 運行爬蟲-爬取單詞釋義 下載單詞語音文件 ...
閱讀 3855·2023-04-25 20:00
閱讀 3240·2021-09-22 15:09
閱讀 577·2021-08-25 09:40
閱讀 3500·2021-07-26 23:38
閱讀 2259·2019-08-30 15:53
閱讀 1157·2019-08-30 13:46
閱讀 2844·2019-08-29 16:44
閱讀 2106·2019-08-29 15:32