UniRx精講(二):獨立的 Update &UniRx 的基本語法格式

獨立的 Update

在 UniRx 簡介的時候,筆者講了一種比較麻煩的情況:就是在 MonoBehaviour 的 Update 中摻雜了大量互相無關的邏輯,導致代碼非常不容易閱讀。

這種情況我們平時在項目開發中非常常見,代碼如下:

private void Update()
{
	if (A)
	{
		...
	}

	if (B)
	{
		...
		if (D)
		{
			...
		}
		else {}
	}

	switch (C)
	{
		...
	}

	if (Input.GetMouseButtonUp(0))
	{
		...
	}
}

Update 方法中代碼冗長,而且干擾視線,非常影響閱讀。

而使用 UniRx 則可以改善這個問題。

void Start()
{
	// A 邏輯,實現了 xx
	Observable.EveryUpdate()
			.Subscribe(_ => 
			{
				if (A)
				{
					...
				}
			}).AddTo(this);
	

	// B 邏輯,實現了 xx
	Observable.EveryUpdate()
			.Subscribe(_ =>
			{
				if (B)
				{
					...
					if (D)
					{
						...
					}
				else {}
				}
			}).AddTo(this);

	// C 邏輯,實現了 xx
	Observable.EveryUpdate()
			.Subscribe(_ =>
			{
				switch (C)
				{
					...
				}
			}).AddTo(this);

	// 鼠標點擊檢測邏輯
	Observable.EveryUpdate()
			.Subscribe(_ => {
			{
				if (Input.GetMouseButtonUp(0))
				{
					...
				}
			}).AddTo(this);
}

雖然在代碼長度上沒有任何改善,但是最起碼,這些 Update 邏輯互相之間獨立了。
狀態跳轉、延時等等這些經常在 Update 里實現的邏輯,都可以使用以上這種方式獨立。

使用 UniRx 可以對我們工程中的代碼進行了改善,而筆者接觸 UniRx 之後,就再也沒有使用過 Update 方法了。

不過以上的這種 UniRx 使用方式,是比較初級的,而這種使用方式,隨着對 UniRx 的深入學習,也會漸漸淘汰,因為等我們入門之後,會學習更好的實現方式。

今天的內容就這些。

知識地圖

UniRx 的基本語法格式

在之前的兩篇文章中,我們學習了 UniRx 的 Timer 和 Update 這兩個 API,但是對代碼的工作原理還沒有進行過介紹。在這篇文章中,我們就來試着理解一下 UniRx 的代碼工作原理及 UniRx 的基本語法格式。

先搬出來第一篇文章中 Delay 的實現代碼:

/****************************************************************************
 * http://liangxiegame.com liangxie
 ****************************************************************************/
 
using System;
using UniRx;
using UnityEngine;

namespace UniRxLesson
{
	public class DelayExample : MonoBehaviour
	{
		private void Start()
		{
			Observable.Timer(TimeSpan.FromSeconds(2.0f)).Subscribe(_ =>
			{
				Debug.Log("延時兩秒"); 
				
			}).AddTo(this);
		}
	}
}

代碼中的 Observable.XXX().Subscribe() 是非常經典的 UniRx 格式。只要理解了這種格式就可以看懂大部分的 UniRx 的用法了。

首先解決代碼中的詞彙問題:

  • Observable:可觀察的,是形容詞,它形容後邊的詞(Timer)是可觀察的,我們可以直接把 Observable 後邊的詞理解成發布者。
  • Timer:定時器,名詞,被 Observable 修飾,所以是發布者,是事件的發送方。
  • Subscribe:訂閱,是動詞,它訂閱誰呢?當然是前邊的 Timer,這裏可以理解成訂閱者,也就是事件的接收方。
  • AddTo:添加到,這個我們暫時不用理解得太深刻,只需要知道它是與 MonoBehaviour 進行生命周期綁定即可。

以上的代碼,連起來則是:可被觀察(監聽)的.Timer().訂閱()
理順了之後應該是:訂閱可被觀察的定時器。

其概念關係很容易理解。

  • Timer 是可觀察的。
  • 可觀察的才能被訂閱。
Observable.XXX().Subscribe();

這行代碼我們可以理解為:可被觀察(監聽)的 XX,註冊。

以上筆者從發布者和訂閱者這個角度進行了簡單的介紹,以便大家理解。
但是 UniRx 的側重點,不是發布者和訂閱者這兩個概念如何使用,而是事件從發布者到訂閱者之間的過程如何處理。
所以這兩個點不重要,重要的是兩點之間的線,也就是事件的傳遞過程。

這裏先不說得太深入,在入門之後,會用很大的篇幅去深入介紹這些概念的。

今天的 UniRx 的基本語法格式的介紹就到這裏,我們下一篇再見,拜拜~

知識地圖

更多內容
QFramework 地址:https://github.com/liangxiegame/QFramework
QQ 交流群:623597263
涼鞋的主頁:https://liangxiegame.com/zhuanlan
關注公眾號:liangxiegame 獲取第一時間更新通知及更多的免費內容。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

【溫故知新】 編程原則和方法論

寫了這麼多年代碼,依舊做不好一個項目

做好一個項目是人力、產品、業務、技術、運營的結合,可能還疊加一點時機的因素,就我們碼農而言,工作就是搬磚,實現產品, 給業務提供支撐。
“給祖傳代碼加 BUG 修 BUG”,“拿起鍵盤一把梭”這些戲謔程序員的話,聽多了真的會讓程序員麻木,彷彿大家都是這麼乾的。
從業多年,堆過 shi 山,接手過祖傳代碼, 已經不能沉下氣去查看、調試 shi 山代碼, 說實話,很累。
本人一直推崇寫流暢、自然、可自解釋的代碼,讓優雅成為一種習慣, 給自己留個念想、給後人留個好評。

溫故而知新,聊一聊現代編程幾大常見的編程原則

普世原則
KISS (Keep It Simple Stupid) 保持系統結構簡單可信賴
YAGNI (you aren’t gonna need it) 當前確實需要,再去做
Do The Simplest Things That Could Possibly Work 思考最簡單可行的辦法
Separation of Concerns 關注點分離
Keep Things DRY 保持代碼結構清爽 Don’t repeat yourself
Code For The Maintainer 站在維護者角度寫代碼
Avoid Premature Optimization 避免提前優化
Boy-Scout Rule 清掃戰場:清理口水話註釋、無效代碼
模塊(類)間
Minimise Coupling 低耦合
Law of Demeter Don’t talk to strangers,對象方法只接觸該接觸的對象、字段、入參
Composition Over Inheritance 組合而不是繼承
Orthogonality 正相關,概念上不相關的事物不應在系統中強行相關
Robustness Principle 代碼健壯性
Inversion of Control 控制反轉
模塊(類)
Maximise Cohesion 高內聚
Likov Substitution Principle 里斯替代原則:將程序中對象替換到子類型實例,不會報錯。
Open/Closed Principle 設計的實體對擴展開放,對修改關閉
Single Responsiblity Principle 單一責任原則
Hide Implementation Details 隱藏實施細節
Curly’s Law 柯里定律:為確定目標編寫特定代碼
Encapsulate What Changes 封裝變化
Interface Segregation Principle 接口隔離原則
Command Query Separation 命令查詢分離

KISS
大多數系統保持簡單,會運行的很好。

  • 更少的代碼消耗更好的時間,產生更少的 bug,並且容易修改
  • 複雜業務都是由簡單代碼堆砌而成
  • 完美並不是“沒有什麼東西可以再加”,而是“沒有什麼東西可以被去掉”

YAGNI
YAGNI 代表“you aren’t gonna need it.”,不要自以為是的提前實現某些邊角,直到真正需要的時候,再來做。

  • 提前做明天才需要做的工作,意味着當前迭代中需要花費更多精力
  • 導致代碼膨脹,軟件變得臃腫且複雜

Separation of Concerns
關注點分離是一種將計算機程序分為不同部分的設計原則,這樣每個部分都可以解決一個單獨的關注點。例如應用程序的業務邏輯是一個問題,而用戶界面是另外一個問題,更改用戶界面不應要求更改業務邏輯,反之亦然。

  • 簡化應用程序的開發和維護
  • 如果關注點分離得很好,則各個部分可以重複使用,也可以獨立開發和更新。

Interface Segregation Principle
接口隔離,將胖接口修改為多個小接口,調用接口的代碼應該比實現接口的代碼更依賴於接口。
why:
如果一個類實現了胖接口的所有方法(部分方法在某次調用時並不需要),那麼在該次調用時我們就會發現此時出現了(部分並不需要的方法),而並沒有機制告訴我們我們現在不應該使用這部分方法。
how: 避免胖接口,類永遠不必實現違反單一職責原則的接口。可以根據實際多職責劃分為多接口,類實現多接口后, 在調用時以特定接口指代對象,這樣這個對象只能體現特定接口的方法,以此體現接口隔離。

   public interface IA
    {
        void getA();
    }

    interface IB
    {
        void getB();
    }

    public class Test : IA, IB
    {
        public string Field { get; set; }
        public void getA()
        {
            throw new NotImplementedException();
        }

        public void getB()
        {
            throw new NotImplementedException();
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            IA a = new Test();
            a.getA();       //  在這個調用處只能看到接口IA的方法, 接口隔離
        }
    }

Command Query Separation
命令查詢分離: 操作方法就只寫操作邏輯,查詢方法就只寫查詢邏輯,並以明顯的方法名區分自己的動作。
有了這個原則,程序員可以更加自信地進行編碼:由於查詢方法不會改變狀態,因此可以在任何地方以任何順序使用,使用操作方法時,也心中有數。

End

懂得這麼多道理,卻依舊過不好這一生。前人總結的編程原則和方法論需要在實踐中感悟,束之高閣,則始終不能體會編程的魅力和快感

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

Spring系列.AOP使用

AOP簡介

利用面向對象的方法可以很好的組織代碼,也可以繼承的方式實現代碼重用。但是項目中總是會出現一些重複的代碼,並且不太方便使用繼承的方式把他們重用管理起來,比如說通用日誌打印,事務處理和安全檢查等。我們可以將這些代碼封裝起來,做成通用模塊,但是還是需要在代碼中每處需要的地方進行显示調用,使用起來不方便。這是時候就是利用AOP的時候。

AOP是一種編程範式,用來解決特定的問題,不能解決所有問題,可以看做是OOP的補充,常見的編程範式還有:

  • 面向過程編程;
  • 面向對象編程;
  • 面向函數編程(函數式編程);
  • 事件驅動編程(GUI開發中比較常見);
  • 面向切面編程

AOP的常見使用場景

  • 性能監控,在方法調用前後記錄調用時間,方法執行太長或超時報警;
  • 緩存代理,緩存某方法的返回值,下次執行該方法時,直接從緩存里獲取;
  • 軟件破解,使用AOP修改軟件的驗證類的判斷邏輯;
  • 記錄日誌,在方法執行前後記錄系統日誌;
  • 工作流系統,工作流系統需要將業務代碼和流程引擎代碼混合在一起執行,那麼我們可以使用AOP將其分離,並動態掛接業務;
  • 權限驗證,方法執行前驗證是否有權限執行當前方法,沒有則拋出沒有權限執行異常,由業務代碼捕捉;
  • 事務處理 。

Spring AOP相關概念

  • AOP:這種在運行時(或者編譯時或者加載時),動態地將某些公共代碼切入到類的指定方法、指定位置上的編程思想就是面向切面的編程;
  • 切面(Aspect):A modularization of a concern that cuts across multiple classes。在Spring中切面就是一個標註@AspectJ的類,不要想得太複雜;
  • 連接點(Joinpoint):方法執行過程中的某個點,是在應用執行過程中能夠插入切面的一個點。這個點可以是調用方法時、拋出異常時、甚至修改一個字段時。切面代碼可以利用這些點插入到應用的正常流程之中,並添加新的行為;
  • 通知(advice):描述切面要完成什麼工作,以及在什麼時間點進行工作;
  • Pointcut:用來匹配一組連接點,並且pointcut會關聯advice,在pointcut匹配的連接點執行的時候,advice代碼會被執行;
  • Introduction
  • Target object:被織入切面的對象;
  • AOP proxy : 包裝了切面代碼和target代碼的對象,Spring中支持JDK動態代理和
    CGLIB,默認使用JDK動態代理,但是如果被代理的類沒有實現接口,或者用戶強制使用CGLIB,那麼Spring會使用CGLIB代理;
  • Weaving:將切面代碼添加到目標代碼的過程,織入的類型有編譯時織入,加載時織入和運行時織入(Spring是運行時織入)

SpringAOP可以應用5種類型的通知:

  • 前置通知(Before):在目標方法被調用之前調用通知功能。
  • 後置通知(After):在目標方法完成之後調用通知,此時不會關心方法的輸出是什麼。(不管執行是否成功都執行都執行)
  • 返回通知(After-returning):在目標方法成功執行之後調用通知。
  • 異常通知(After-throwing):在目標方法拋出異常后調用通知。
  • 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用之前和調用之後執行自定義的行為。

Spring AOP相關

開啟Aop


//自動選擇合適的AOP代理
//傳統xml這樣配置:<aop:aspectj-autoproxy/>

//exposeProxy = true屬性設置成true,意思是將動態生成的代理類expose到AopContext的ThreadLocal線程
//可以通過AopContext.currentProxy();獲取到生成的動態代理類。

//proxyTargetClass屬性設置動態代理使用JDK動態代理還是使用CGlib代理,設置成true是使用CGlib代理,false的話是使用JDK動態代理

//注意:如果使用Spring Boot的話,下面的配置可以不需要。AopAutoConfiguration這個自動配置類中已經自動開啟了AOP
//默認使用CGLIB動態代理,Spring Boot配置的優先級高於下面的配置

@Configuration
@EnableAspectJAutoProxy(exposeProxy = true,proxyTargetClass = false)
public class AopConfig {

}


如果使用傳統的配置方式的話,可按如下配置開啟AOP功能。

<aop:aspectj-autoproxy/>

定義一個Aspect

Aspects (classes annotated with @Aspect) can have methods and fields, the same as any other class. They can also contain pointcut, advice, and introduction (inter-type) declarations.

可以使用普通Bean的定義方式,或者加@Aspect註解的方式定義。一旦一個類被標註成切面類,它就不會成為其他切面的代理對象。

定義一個PointCut

切面表達式可以由指示器,通配符和運算符組成

  1. 指示器(Designators)
  • 匹配方法 execution() (重點掌握…)
  • 匹配註解 @target() @args() @within() @annotation()
  • 匹配包/類型 within()
  • 匹配對象 this() bean() target()
  • 匹配參數 args()
  1. Wildcards(通配符)
  • *匹配任意數量的字符
  • +匹配指定類及其子類
  • .. 一般用於匹配任意參數的子包或參數
  1. Operators(運算符)
  • && 與操作符
  • || 或操作符
  • ! 非操作符

下面給出一個定義PointCut的例子

package com.csx.demo.spring.boot.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MyAspect {

    //PointCut匹配的方法必須是Spring中bean的方法
    //Pointcut可以有下列方式來定義或者通過&& || 和!的方式進行組合.
    //下面定義的這些切入點就可以通過&& ||組合

    private static Logger logger = LoggerFactory.getLogger(MyAspect.class);

    //*:代表方法的返回值可以是任何類型
    //整個表達式匹配controller包下面任何的的echo方法,方法入參樂意是任意
    @Pointcut("execution(* com.csx.demo.spring.boot.controller.*.echo(..))")
    public void pointCut1(){}

    //代表echo方法必須有一個參數 參數的類型可以是任意類型
    @Pointcut("execution(* com.csx.demo.spring.boot.controller.*.echo(*))")
    public  void pointCut2(){}

    //代表echo方法必須有兩個參數,第一個類型任意,第二個類型必須是String
    @Pointcut("execution(* com.csx.demo.spring.boot.controller.*.echo(*,String))")
    public void pointCut3(){}

    //contrller包及其子包下面的任意類的任意方法
    //需要注意的是with和@with都是正對包級別的
    @Pointcut("within(com.csx.demo.spring.boot.controller..*)")
    public void pointCut4(){}

    //使用RestController這個註解標註任意類的任意方法
    @Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
    public void pointCut5(){}

    //用法和@Within類似
    @Pointcut("@target(org.springframework.web.bind.annotation.RestController)")
    public void pointCut10(){}

    //MyService這個接口實現類的任何方法
    //如果MyService是一個類的話,那匹配這個類內部的所有方法
    @Pointcut("this(com.csx.demo.spring.boot.service.MyService)")
    public void pointCut6(){}

    @Pointcut("this(com.csx.demo.spring.boot.service.MyServiceImpl)")
    public void pointCut7(){}

    //某個bean內部的所有方法
    @Pointcut("bean(myServiceImpl)")
    public void pointCut8(){}

    //@within和@target針對類的註解,@annotation是針對方法的註解
    //匹配任何標註GetMaping註解的方法
    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    public void pointCut9(){}

    //匹配只有一個參數,參數類型是String的方法
    @Pointcut("args(String)")
    public void pointCut11(){}


    @Before("pointCut1()")
    public void befor(){
        logger.info("前置通知vvvv...");
        logger.info("我要做些事情...");
    }

    @After("pointCut1()")
    public void after(){
        logger.info("後置通知");
    }

    @AfterReturning("pointCut1()")
    public void afterReturn(){
       logger.info("後置返回");
    }

     //目標方法拋出相關異常后通知
    @AfterThrowing("pointCut1()")
    public void afterThrowing(){
        logger.info("後置異常");
    }

    @Around("pointCut1()")
    public void around(ProceedingJoinPoint point) throws Throwable {
        logger.info("環繞通知...");
        logger.info("我要做些事情...");
        point.proceed();
        logger.info("結束環繞通知");
    }

}

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

LeetCode 77,組合挑戰,你能想出不用遞歸的解法嗎?

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天是LeetCode第46篇文章,我們一起來LeetCode中的77題,Combinations(組合)。

這個題目可以說是很精闢了,僅僅用一個單詞的標題就說清楚了大半題意了。這題官方難度是Medium,它在LeetCode當中評價很高,1364人點贊,只有66個反對。通過率53.6%。

題意

題目的題意很簡單,給定兩個整數n和k。n表示從1到n的n個自然數,要求隨機從這n個數中抽取k個的所有組合

樣例

Input: n = 4, k = 2
Output:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

全排列的問題我們已經很熟悉了,那麼獲取組合的問題怎麼做呢?

遞歸

這是一個全組合問題,實際上我們之前做過全排列問題。我們來分析一下排列和組合的區別,可能很多人知道這兩者的區別,但是對於區別本身的理解和認識不是非常深刻。

排列和組合有一個巨大的區別在於,排列會考慮物體擺放的順序。也就是說同樣的元素構成,只要這些元素一些交換順序,那麼就會被視為是不同的排列。然而對於組合來說,是不會考慮物體的擺放順序的。只要是這些元素構成,無論它們怎麼調換擺放順序,都是同一種組合。

我們獲取全排列的時候用的是回溯法,我們當然也可以用回溯法來獲取組合。但問題是,我們怎麼保證獲取到的組合都是元素的組成不同,而不是元素之間的順序不同呢?

為了保證這一點,需要用到一個慣用的小套路,就是通過下標遞增來控制拿取元素的順序。如果我們限定了拿取元素的下標是遞增的,那麼就可以保證每一次拿取到的組合都是獨一無二的。所以我們就把這一點加在回溯法上即可,只要理解了,並不難實現。

在代碼的實現當中,我們用上了閉包,省略了幾個參數的傳遞,整體上來說編碼的難度降低了一些。

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        def dfs(start, cur):
            # 如果當前已經拿到了K個數的組合,直接加入答案
            # 注意要做深拷貝,否則在之後的回溯過程當中變動也會影響結果
            if len(cur) == k:
                ret.append(cur[:])
                return
            
            # 從start+1的位置開始遍歷
            for i in range(start+1, n):
                cur.append(i+1)
                dfs(i, cur)
                # 回溯
                cur.pop()
                
        ret = []
        dfs(-1, [])
        return ret

迭代

這題並不是只有一種做法,我們也可以不用遞歸實現算法。不用遞歸意味着沒有系統幫助我們建棧存儲中間信息了,需要我們自己把迭代過程當中所有變量的關係整理清楚。

我們假設n=8,k=3,那麼在所有合法的組合當中,最小的組合一定是[1,2,3],最大的組合一定是[6,7,8]。如果我們保證組合當中的元素是有序排列的,那麼組合之間的大小關係也是可以確定的。進而我們可以思考設計一種方案,使得我們可以從最小的組合[1,2,3]一直迭代到[6,7,8],並且我們還要保證在迭代的過程當中,組合當中元素的順序不會被打亂。

我們可以想象成這n個數在一根“直尺”上排成了一行,我們有k個滑動框在上面移動。這k個滑動框取值的結果就是n個元素中選取k個的組合,並且由於滑動框之間是不能交錯的,所以保證了這k個值是有序的。我們要做的就是設計一種移動滑動框的算法,使得能夠找到所有的組合情況。

我們可以想象一下,一開始的時候滑動框都聚集在最左邊,我們要移動只能移動最右側的滑動框。我們把滑動框從k移動到了k+1,那麼這個時候它的右側有k-1個滑動框,一共有k個位置。

那麼這個問題其實轉化成了k個元素當中取k-1個組合的子問題。我們把1-k的這個部分看成是新的“直尺”,我們要在其中移動k-1個滑動框獲取所有的組合。首先,我們需要把這k-1個滑動框全部移動到左側,然後再移動其中最右側的滑動框。然後循環往複,直到所有的滑動框都往右移動了一格為止,這其實是一個遞歸的過程。

我們不去深究這個遞歸的整個過程,我們只需要理解清楚其中的幾個關鍵點就可以了。首先,對於每一次遞歸來說,我們只會移動這個遞歸範圍內最右側的滑動框,其次我們清楚每一次遞歸過程中的起始狀態。開始狀態就是所有的滑動框全部集中在“直尺”的最左側,結束狀態就是全部集中在最右側。

我們把上面的邏輯整理一下,假設我們經過一系列操作之後,m個滑動框全部移動到了長度為n的直尺的最右側。這就相當於的組合都已經獲取完了。如果n+1的位置還有滑動框,並且它的右側還可以移動,那麼我們需要將它往右移動一個,到n+2的位置。這個時候剩下的局面就是,為了獲取這些組合,我們需要把這m個滑動框全部再移動到直尺的最左側,重新開始移動。

我們在實現的時候當然沒有滑動框,我們可以用一個數組記錄滑動框當中的元素。

我先用遞歸寫一下這段邏輯:

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        def comb(window, m, ret):
            ret.append(window[:-1])

            # 如果第m位的滑動框不超過直尺的範圍並且m右側的滑動框
            while window[m] < min(n - k + m + 1, window[m+1] - 1):
                # 向右滑動一位
                window[m] += 1
                # 如果m左側還有滑動框,遞歸
                if m > 0:
                    # 把左側的滑動框全部移動到最左側
                    window[:m] = range(1, m+1)
                    comb(window, m-1, ret)
                else:
                    # 否則記錄答案
                    ret.append(window[:-1])

                
        ret = []
        window = list(range(1, k+1))
        # 額外多放一個滑動框作為標兵
        window.append(n+1)
        comb(window, k-1, ret)
        return ret

這種解法的速度比上面正規遞歸的速度快了許多,因為我們遞歸的過程當中做了諸多限制,剪掉了很多無關的情況,相當於做了極致的剪枝。

最關鍵的是上面的這段邏輯我們是可以用循環實現的,所以我們可以用循環來將遞歸的邏輯展開,就得到了下面這段代碼。

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        # 構造滑動框
        window = list(range(1, k + 1)) + [n + 1]
        
        ret, j = [], 0

        while j < k:
            # 添加答案
            ret.append(window[:k])

            j = 0
            # 從最左側的滑動框開始判斷
            # 如果滑動框與它右側滑動框挨着,那麼就將它移動到最左側
            # 因為它右側的滑動框一定會向右移動
            while j < k and window[j + 1] == window[j] + 1:
                window[j] = j + 1
                j += 1
            # 連續挨着最右側的滑動框向右移動一格
            window[j] += 1
            
        return ret

這段代碼雖然非常精鍊,但是很難理解,尤其是你沒能理解上面遞歸實現的話,會更難理解。所以我建議,先把遞歸實現的滑動框的方法理解了,再來理解不含遞歸的這段,會容易一些。

總結

我們通過回溯法求解組合的方法應該是最簡單也是最基礎的,難度也不大。相比之下後面一種方法則要困難許多,我們直接去啃,往往不得要領。既會疑惑為什麼這樣可以保證能獲得所有的組合,又會不明白其中具體的實現邏輯。所以如果想要弄明白第二種方法,一定要從滑動框這個模型出發

從代碼實現的角度來說,滑動框方法的遞歸解法比非遞歸的解法還要困難。因為遞歸條件以及邏輯都比較複雜,還涉及到存儲答案的問題。但是從理解上來說,遞歸的解法更加容易理解一些,非遞歸的算法往往會疑惑於j這個指針的取值。所以如果想要理解算法的話,可以從遞歸的代碼入手,想要實現代碼的話,可以從非遞歸的方法入手。

這道題目非常有意思,值得大家細細思考。

如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

本文使用 mdnice 排版

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

MongoDB 邏輯備份工具mongodump

mongodump是官方提供的一個對數據庫進行邏輯導出的備份工具,導出文件為BSON二進制格式,無法使用文本編輯工具直接查看。mongodump可以導出mongod或者mongos實例的數據,從集群模式來看,可以備份單實例、副本集、分片集集群。

mongodump作為MongoDB官方工具集中的一部分,從版本4.4開始,文檔說明統一到工具分類中:database-tools。本文是基於MongoDB 4.2 社區版本,具體環境如下:

OS:CentOS Linux release 7.6.1810 (Core)

DB version:v4.2.2

因為安裝的二進製版本,所以mongodump可執行文件通過解壓壓縮包就可以得到。

主要選項

mongodump支持的選項不算太多,可以通過--help選項獲得:

mongodump --help
Usage:
  mongodump <options>

Export the content of a running server into .bson files.

選項分為幾個大類:

  • general options:通用選項
  • connection options:連接選項
  • ssl options:安全連接選項
  • authentication options:驗證選項
  • kerberos options:基於kerboeros驗證選項
  • namespace options:命名空間選項
  • uri options:mongodb uri連接串選項
  • query options:查詢選項
  • output options:輸出選項
  • verbosity options:显示選項

general options(通用選項)

--help      # 打印工具使用方式,選項說明。
--version   # 打印工具版本並退出。

connection options(連接選項)

-h, --host=<hostname>   # 指定連接的實例主機名或者IP地址。                                 
    --port=<port>       # 指定連接的實例端口號。                                 

連接選項也可以分為三種實例模式:單實例、副本集、分片集。

  • Standalone(單實例)

只指定選項--host

mongodump --host="192.168.196.128:27017"

同時指定選項--host--port

mongodump --host="192.168.196.128" --port=27017
  • Replica Set(副本集)

指定選項--host

mongodump --host="dbabdSet/192.168.196.128:27017,192.168.196.128:27018,192.168.196.128:27019"

默認情況下,mongodump讀取Primary節點的數據,如果想讀取Secondary節點的數據,則需要使用選項readPreference

mongodump --host="dbabdSet/192.168.196.128:27017,192.168.196.128:27018,192.168.196.128:27019" --readPreference=secondary
  • Sharded Cluster(分片集群)

Replica Set(副本集)相同的指定方式。

注意:

–host選項默認值為:localhost:27017

–port選項默認值為:27017

執行副本集的導出備份時通常單獨連接某個Secondary節點進行導出備份:

mongodump --host="192.168.196.128:27018"

ssl options(安全連接選項)

--ssl                                                
--sslCAFile=<filename>                               
--sslPEMKeyFile=<filename>                            
--sslPEMKeyPassword=<password>                        
--sslCRLFile=<filename>                               
--sslAllowInvalidCertificates          
--sslAllowInvalidHostnames
--sslFIPSMode

關於ssl安全加密協議連接,本文不過多的贅述,詳情可以參考官方文檔:https://docs.mongodb.com/manual/reference/program/mongodump/index.html#cmdoption-mongodump-ssl

authentication options(驗證選項)

主要用於驗證連接實例的用戶的合法性。

-u, --username=<username>                   # 指定連接用戶
-p, --password=<password>                   # 指定連接用戶密碼
--authenticationDatabase=<database-name>    # 指定連接用戶驗證數據庫
--authenticationMechanism=<mechanism>       # 指定連接驗證機制

注意:

如果需要在導出時显示指示輸入密碼,而不是直接寫在選項中,則在指定--username選項的同時,不指定--password或者為--password選項指定一個空值,如:–password “”

如果沒有驗證數據庫,則mongodump假設指定導出的數據庫中包含用戶的驗證信息,如果沒有驗證數據庫並且也沒有指定導出數據庫,則mongodump假設admin數據庫包含用戶的驗證信息。

kerberos options(kerboeros驗證選項)

該選項主要是基於kerberos驗證機制的連接驗證選項,kerberos只有MongoDB企業版才支持,本文基於社區版本,有關kerberos可以參考官方文檔:https://docs.mongodb.com/manual/core/kerberos/

namespace options(命名空間選項)

主要是指定需要邏輯備份的數據庫和集合。

-d, --db=<database-name>             # 指定數據庫
-c, --collection=<collection-name>   # 指定集合                     

注意:

如果沒有指定導出數據庫,則mongodump導出實例中所有的數據庫,對於集合選項做相同的處理。

uri options(uri連接串選項)

從版本3.4.6開始新增加一種連接MongoDB實例的字符串格式,即URI。

--uri=mongodb-uri  # 指定uri連接字符串
  • Standalone(單實例)
--uri="mongodb://192.168.196.128:27017"
# 開始訪問控制驗證
--uri="mongodb://dbabd:admin@192.168.196.128:27017/?authSource=admin"
  • Replica Set(副本集)
--uri="mongodb://192.168.196.128:27017,192.168.196.128:27018,192.168.196.128:27019/?replicaSet=dbabdSet"
# 開始訪問控制驗證
--uri="mongodb://dbabd:admin@192.168.196.128:27017,192.168.196.128:27018,192.168.196.128:27019/?authSource=admin&replicaSet=dbabdSet"
  • Sharded Cluster(分片集群)

Replica Set(副本集)相同的指定方式。

當指定--uri連接串選項時,會與之前有關連接的選項互斥,這些選項包括:

  • –host
  • –port
  • –db
  • –username
  • –password(如果–uri連接串有指定連接密碼的話)
  • –authenticationDatabase
  • –authenticationMechanism

因為–uri連接串中已經包含了以上選項所涉及的部分。

query options(查詢選項)

指定mongodump導出的條件,可以參考db.collections.find()方法的查詢表達式。

-q, --query=<json>       # 指定符合JSON v2拓展格式的查詢語句 如:'{"x":{"$gt":1}}'
    --queryFile=<json>   # 指定包含JSON v2拓民格式查詢語句的文件
--readPreference=<string>|<json>  # 指定優先讀取順序   如:'nearest' 或 '{mode: "nearest", tagSets: [{a: "b"}],maxStalenessSeconds: 123}')
--forceTableScan         # 指定導出時遍歷集合使用的索引

注意:

如果指定--query選項,則必須同時指定--collection選項。

mongodump對於副本集默認優先讀取primary節點,如果需要從secondary,則指定選項–readPreference=secondary

mongodump導出時遍歷集合時默認使用索引_id,如果要使用其他索引,則指定選項--forceTableScan,該選項沒辦法確保mongodump導出基於某個時間點的快照,如果需要創建某一時間點的快照,則使用選項--oplog,該選項不能與選項--query一起使用。

output options(輸出選項)

指定mongodump導出時保存BSON文件的目錄,默認情況下,導出文件保存在執行mongodump當下目錄中的dump目錄里。

-o, --out=<directory-path>  # 指定導出文件保存目錄
--gzip                      # 使用gzip對導出文件進行壓縮
--oplog                     # 指定保存mongodump導出期間的oplog日誌,文件名為oplog.bson
--archive=<file-path>       # 指定導出文件合併歸檔的目的地
--dumpDbUsersAndRoles       # 指定導出數據庫的用戶和角色定義
--excludeCollection=<collection-name>  # 指定導出時排除某個集合,如有多個,需要指定多次
--excludeCollectionsWithPrefix=<collection-prefix>  # 指定導出時排除多個相同命名前綴的集合
-j, --numParallelCollections=  # 指定導出時可以并行的集合數,默認值為4
--viewsAsCollections # 指定導出時將只讀視圖當成集合保存成BSON文件

注意:

當指定選項--oplog時,mongodump在導出過程中同時會保存這一時間點產生的oplog,並保存為oplog.bson文件,使導出的備份是實例基於某個時間點的快照,如果使用mongorestore還原進行oplog回放時,需要指定選項--oplogReplay。如果沒有指定選項--oplog,則無法保證當前的導出在這一時刻的一致性,在導出過程中有對數據庫進行作何的更新操作都會影響導出的文件變化。

如果mongodump指定選項--oplog導出時客戶端執行以下命令會導致導出失敗:

  • renameCollection
  • db.collection.renameCollection()
  • db.collection.aggregate()並且執行操作符$out

當mongodump連接mongos實例進行整個分片集群的導出備份時,指定選項--oplog是沒有生效的,需要對各個分片節點集群單獨導出並指定--oplog

選項--oplog只有在副本集所有成員都開啟oplog功能才會生效。

選項--oplog並不會民出保存oplog的集合。

當mongodump指定選項--oplog導出時,必須包括導出實例所有的數據庫和集合,即不能指定選項--db只導出某個庫或選項--collection只導出某個集合。

verbosity options(显示選項)

指定導出時log輸出的显示的詳細級別。

-v, --verbose=<level>  # 指定日誌輸出詳細級別,如:-vvvvv 或 指定數值
--quiet                # 指定不輸出作何日誌信息

使用示例

備份導出數據庫

mongodump -h"192.168.196.128:27017" -uroot --authenticationDatabase admin -d dbabd -o /data/mongodump/

備份導出數據庫某個集合

mongodump -h"192.168.196.128:27017" -uroot --authenticationDatabase admin -d dbabd -
c orders -o /data/mongodump/

備份導出排除某個集合

# 如果有多個排除集合,則多次指定選項--excludeCollection
mongodump -h"192.168.196.128:27017" -uroot --authenticationDatabase admin -d dbabd -
-excludeCollection=orders -o /data/mongodump/

備份導出文件歸檔到文件

mongodump -h"192.168.196.128:27017" -uroot --authenticationDatabase admin -d dbabd --archive=/data/mongodump/dbabd.archive

備份導出文件進行壓縮

mongodump -h"192.168.196.128:27017" -uroot --authenticationDatabase admin -d dbabd -
-gzip -o /data/mongodump/

備份導出文件進行壓縮並歸檔

mongodump -h"192.168.196.128:27017" -uroot --authenticationDatabase admin -d dbabd --gzip --archive=/data/mongodump/dbabd.archive

指定條件的備份導出

mongodump -h"192.168.196.128:27017" -uroot --authenticationDatabase admin -d dbabd -c orders -q='{"price":{"$in":[25,50]}}' -o /data/mongodump/

注意

mongodump不會導出有關於local數據庫的內容;

mongodump不會導出文檔的索引,而只會導出文檔的數據,在導出文件還原之後需要重建索引;

對於開啟了訪問控制機制的實例,mongodump要執行導出操作的話必須對每個需要導出的數據庫具有find權限,內建角色backup提供了備份所有數據庫的權限,所以可以對用戶直接授予backup角色。

總結

對於MongoDB實例的邏輯備份工具,mongodump是個不二選擇,官方出品,安全穩定性有保障;

mongodump較適合數據量較少的備份,相對於數據量較大的情況,備份效率不是太高;

mongodump導出時比較消耗服務器性能,而且不支持同時導出多個庫,對於導出庫數據的最終一致性選項--oplog也只能是全庫導出才支持。

參考

https://docs.mongodb.com/database-tools/mongodump

https://docs.mongodb.com/manual/reference/program/mongodump/index.html#cmdoption-mongodump-ssl2

https://docs.mongodb.com/manual/core/kerberos/

〖本人水平有限,文中如有錯誤還請留言批評指正!〗

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!

求求你,別問了,Java字符串是不可變的

最近,又有好幾個小夥伴問我這個問題:“二哥,為什麼 Java 的 String 要設計成不可變的啊?”說實話,這也是一道非常經典的面試題,面試官超喜歡問。我之前寫過這方面的文章,現在讀起來似乎不太滿意,所以我決定再啰嗦最後一次,交出一份更滿意的答卷,讓小夥伴們在面試官面前更從容一些,更有底氣一些。

關於不可變對象,還有這樣一個小故事。Java 之父詹姆斯高司令曾在一次採訪中被問及這樣一個問題:“高司令,應該什麼時候使用不可變對象啊?”你猜高司令怎麼回答?

如有可能,我願意任何時候都使用不可變對象。

這就是高司令的答案,那有的小夥伴可能不服,老人家會說中文,你瞎扯吧你。也對哈,那就上英文唄:

I would use an immutable whenever I can.

這下徹底被打服了吧?老人家還說,不可變有着非常強大的功能,比如說,緩存、安全性、高性能等等。

01、什麼是不可變對象

不可變對象在創建后,它的內部狀態會保持不變,這就意味着,一旦我們將一個對象分配給一個變量,就無法再通過任何方式更改對象的狀態了。

關於不可變對象的更多信息,可以查看我之前寫的另外一篇文章——這次要說不明白immutable類,我就怎麼地,看完啥都明白了。你看,寫系列文章的好處就是這樣,不需要重複造輪子,用到的時候直接搬出來套上就行了。

02、為什麼 String 是不可變的

重點來了啊,為什麼 String 是不可變的?原因可以從四個方面說起,緩存、安全性、同步和高性能。

1)字符串常量池

字符串恐怕是 Java 中最常用的數據形式了,如果字符串非要謙虛地說自己是老二,就沒有人敢說自己是老大。

因此,把字符串緩存起來,並且重複使用它們會節省大量堆空間(堆內存用來存儲 Java 中的對象,無論是成員變量、局部變量,還是類變量,它們指向的對象都存儲在堆內存中),因為不同的字符串變量引用的是字符串常量池中的同一個對象。這也正是字符串常量池存在的目的。

字符串常量池是 Java 虛擬機用來存儲字符串的一個特殊的區域,由於字符串是不可變的,因此 Java 虛擬機可以在字符串常量池中只為同一個字符串存儲一個字符串副本來節省空間。

字符串常量池的主要使用方法有兩種:

  • 直接使用雙引號聲明出來的字符串對象會直接存儲在常量池中。
  • 否則,可以使用 String 類提供的 intern() 方法強制將當前字符串放入常量池中——常量池中查詢不到當前字符串。

來看下面這段代碼:

String s1 = "沉默王二";
String s2 = "沉默王二";

System.out.println(s1 == s2); // true

由於字符串常量池的存在,所以兩個不同的變量都指向了池中同一個字符串對象,從而節省了稀缺的內存資源。如果是通過 new 關鍵字創建的對象,則需要新的堆空間。

放心,關於字符串常量池,後面有時間的話,我再單獨寫一篇文章詳細地說一說。

2)安全性

字符串在 Java 應用程序中的使用範圍非常廣,幾乎無處不在,比如說存儲用戶名、密碼、數據庫連接地址等等這些非常敏感的信息,因此,必須要保證 String 類的絕對安全性。

來考慮一下下面這段代碼:

void criticalMethod(String userName) {
    // 檢查用戶名是否合法
    if (!isAlphaNumeric(userName)) {
        throw new SecurityException(); 
    }

    // 初始化數據庫連接
    initializeDatabase();

    // 準備修改用戶狀態
    connection.executeUpdate("UPDATE members SET status = 'active' " +
      " WHERE username = '" + userName + "'");
}

通常情況下,用戶名由客戶端傳遞到服務器端,服務器端接收后要先對用戶名進行檢查,再進行其他操作,因為客戶端傳遞過來的信息不一定值得信任。

如果字符串是可變的,那麼我們在執行 executeUpdate 更新數據庫的時候,就有點不放心,因為即便是安全性檢查通過了,字符串仍然有可能被修改。

在調用 isAlphaNumeric() 方法進行安全性檢查期間,userName 的值仍然有可能被 criticalMethod() 方法的調用者進行篡改,就容易造成 SQL 注入。

但如果字符串是不可變的,這方面的擔憂就不存在了。因為在執行更新之前,字符串的值是確定的,就是我們檢查安全性之後的值。

3)線程安全

由於字符串是不可變的,因此可以在多線程之間共享,如果一個線程把字符串的值修改為另外一個,那麼就會在字符串常量池中創建另外一個字符串,原有的字符串仍然會保持不變。

不過,很遺憾,我還不知道怎麼從代碼層面上去證明這一點,只能純理論 yy 一下。小夥伴誰有辦法的,教教我,在線等的那種。

4)哈希碼

字符串廣泛應用於 HashMap、HashTable、HashSet 等需要哈希碼作為鍵的數據結構中,在對這些哈希表進行操作的時候,需要頻繁調用 hashCode() 方法來獲取鍵的哈希碼。

public V put(K key, V value) {
    return putVal(hash(key), key, value, falsetrue);
}
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

由於字符串是不可變性,這就保證了鍵值的哈希值不會發生改變,因此在第一次調用 String 類的 hashCode() 方法時,就對哈希值進行了緩存,此後,就一直返回相同的值。

/** Cache the hash code for the string */
private int hash; // Default to 0

public int hashCode() {
    int h = hash;
    if (h == 0 && !hashIsZero) {
        h = isLatin1() ? StringLatin1.hashCode(value)
                : StringUTF16.hashCode(value);
        if (h == 0) {
            hashIsZero = true;
        } else {
            hash = h;
        }
    }
    return h;
}

由於哈希值被緩存了,這在另外一種層面上提高了哈希表的訪問性能,因為哈希值不用重新計算了。

假如字符串是可變的,那就意味着哈希碼會有多個,在通過鍵獲取值的時候,就不一定能夠獲取到對的值了。

你看,字符串常量池的存在,哈希碼的存在,在很大程度上提高了程序的性能。

03、總結

好了,我親愛的小夥伴們,以上就是本文的全部內容了。我相信你一定對字符串的不可變性有了充足的了解,由於字符串是不可變的,因此我們可以將它看作是一個特殊的基本數據類型,哪怕是在多線程的環境下,也不用擔心它的值是否會發生改變。

如果覺得文章對你有點幫助,請微信搜索「 沉默王二 」第一時間閱讀。

本文已收錄 GitHub,傳送門~ ,裏面更有大廠面試完整考點,歡迎 Star。

我是沉默王二,一枚有顏值卻靠才華苟且的程序員。關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,嘻嘻

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

Jmeter基礎004—-增加參數化

一、參數化概述

1、參數化概念

      參數化就是動態的獲取並設置數據,當執行批量操作時,如批量插入或批量刪除,之前每執行完一次就需要修改一次,效率太低,參數化可以代替人工獲取並設置數據,安全且高效!

2、Jmeter參數化組件

  • CSV Data Set Config—-CSV數據設置組件
  • 用戶參數
  • 用戶定義的變量
  • 函數

二、參數化實現之CSV Data Set Config

1、概述

     CSV Data Set Config—-CSV數據設置組件,是參數化的實現組件之一,通過這個組件可以動態獲取並設置數據,實現批量操作,如:批量添加操作(執行一次,將多條數據插入到數據庫)。

2、實例1:參數化登錄賬號

      我們錄製的腳本,內容都是固定的,比如手機號、驗證碼都是我們再錄製過程中輸入的,如果我們希望模擬不同用戶登陸,那麼我們並不需要錄製很多個腳本,而只要將腳本中的用戶名、密碼變成變量,而線程執行時,不同線程取得不同的變量值即可。
     下面我們就舉例說明如何參數化登陸賬號。

(1)測試登錄接口:如下圖,新建登陸的HTTP請求並運行,確保登錄接口運行正常。

 

(2)創建一個文本文檔,標準的CSV格式文件,如下圖包括3條數據,每一行數據對應一條登錄信息,不同字段之間使用英文逗號分隔。

(3)線程組右鍵—添加—配置元件—CSV Data Set Config—-CSV,創建一個CSV組件,並聲明數據源、編碼集以及解析格式,如下圖所示:

(4)設置線程組的線程數為3(因為文件中有三條登錄數據),並修改HTTP請求中的參數值,調用CSV數據文件設置中定義的變量,調用格式${變量名},如下圖所示:

 

 (5)運行測試計劃,查看結果樹的運行結果,如下圖所示:

 

 

 3、實例2:批量添加

 實現思想:

 

 

 實現步驟:

(1)創建CSV 數據文件設置,如下圖所示:

 (2)創建HTTP請求,並在請求中調用CSV中定義的變量

 (3)編輯文本文檔,存儲要添加的三條數據

 (4)設置線程組循環次數為3,並運行測試計劃,查看察看結果樹显示。

 

三、參數化之用戶參數

1、用戶參數與CSV參數化的區別

    用戶參數和CSV都是將數據設置進第三方,然後循環讀取數據,區別在於:CSV是將數據設置進外部的文本文檔,而用戶參數是將數據設置進Jmeter內置組件。

2、實現流程  

(1)搭建框架:創建測試計劃、線程組、HTTP請求(請求的JSON數據先不設置)。注意:執行次數是3次(不是設置循環次數,而是設置線程數)

(2)創建Jmeter內置組件存儲要插入的數據:測試計劃右鍵—-添加—-前置處理器—-用戶參數,在用戶參數組件界面添加4個變量、3個用戶,如下圖所示:

 注意:因為此處添加的是用戶,每個用戶對應一個線程 ,添加幾個用戶就應該設置幾個線程,所以這裏設置的是線程組而不是循環次數。

 

(3)將用戶參數組件中的變量名稱設置進HTTP請求的Json數據格式中,調用格式:${變量名},如下圖所示:

 (4)運行測試計劃,查看察看結果樹。

 

四、參數化之用戶定義的變量

1、需求

     當系統執行增刪改查操作時,資源路徑不一定相同,但存在部分相同,如:都是/api/departments/開頭,為了提高編寫路徑的效率,可以將公共路徑定義成變量,然後再在路徑中使用${變量名}調用變量。注:一般定義、存儲全局使用的變量。

2、實現過程

(1)將公共的路徑數據提取出來使用一個組件存儲,如:/api/departments/。測試計劃右鍵—-添加—-配置元件—-用戶定義的變量,創建用戶定義的變量組件,添加自定義變量,如下圖所示:

(2)分別創建HTTP請求,在路徑中公共部分調用定義的路徑變量,非公共部分路徑與原來一致,如下圖所示:

 (3)運行測試計劃,查看結果樹。

 

五、參數化之函數

1、需求

     函數是程序中最基本的封裝單元,封裝了一些常用的功能,比如計數器。在實際應用中當我們需要循環10次查詢信息時,結果數的請求名稱都是一樣的,我們可以使用計數函數添加標號以示區分。

2、實現流程

(1)打開Jmeter內置的函數組件,一共有三種方式:

  •  選項+函數助手對話框
  •  ctrl+shift+F1
  • 工具欄倒數第二個圖標

(2)選擇要使用的函數,給函數傳參,並用Jmeter生成調用格式,如下圖:

 

注:__counter函數的參數:true,每一個用戶單獨一個計數器;false,所有用戶共用一個計數器.

(3)在需要調用函數的位置使用Jmeter生成的調用格式:${_函數名(參數)}

 

 

 (4)運行測試計劃,查看結果樹,如下圖所示:

六、總結

1、參數化—-CSV Data Set Config

     概念:動態獲取並設置數據,操作數據高效安全(程序代替人工)

     實現思想+具體流程:

2、參數化—-用戶參數

實現思想:將數據單獨存儲,然後再將數據讀取到http請求的JSON 數據中

實現流程:

  • 設置執行次數(用戶數)
  • 添加組件用戶參數存儲多條記錄
  • 讀取數據格式: ${變量名 )

3、參數化—-用戶定義的變量

作用:存儲全局性數據 

添加格式:添加用戶定義的變量組件—–鍵和值

調用格式:${變量名}

4、參數化—-函數

概念:程序中的功能單元,封裝了部分實現 

實現: 

  • 打開函數功能模塊
  • 選擇要調用的函數+設置參數+生成調用格式
  • 在需要使用的位置調用即可

5、四種參數化方案比較

  •   CSV和用戶參數使用思想一致,流程上後者更簡單,但是實際應用中,使用CSV居多,因為數據量大時,CSV更方便
  •   用戶定義的變量一般用來存儲全局變量,但是使用場景較少
  •   函數實現更為靈活且內置了好多實現。

總結:最常用的是參數化方法是:CSV+函數

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

Vue結合路由配置遞歸實現菜單欄

作者:小土豆biubiubiu

博客園:https://www.cnblogs.com/HouJiao/

掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d

微信公眾號:土豆媽的碎碎念(掃碼關注,一起吸貓,一起聽故事,一起學習前端技術)

作者文章的內容均來源於自己的實踐,如果覺得有幫助到你的話,可以點贊給個鼓勵或留下寶貴意見

前言

在日常開發中,項目中的菜單欄都是已經實現好了的。如果需要添加新的菜單,只需要在路由配置中新增一條路由,就可以實現菜單的添加。

相信大家和我一樣,有時候會躍躍欲試自己去實現一個菜單欄。那今天我就將自己實現的菜單欄的整個思路和代碼分享給大家。

本篇文章重在總結和分享菜單欄的一個遞歸實現方式代碼的優化菜單權限等不在本篇文章範圍之內,在文中的相關部分也會做一些提示,有個別不推薦的寫法希望大家不要參考哦。

同時可能會存在一些細節的功能沒有處理或者沒有提及到,忘知曉。

最終的效果

本次實現的這個菜單欄包含有一級菜單二級菜單三級菜單這三種類型,基本上已經可以覆蓋項目中不同的菜單需求。

後面會一步一步從易到難去實現這個菜單。

簡單實現

我們都知道到element提供了 NavMenu 導航菜單組件,因此我們直接按照文檔將這個菜單欄做一個簡單的實現。

基本的布局架構圖如下:

菜單首頁-menuIndex

首先要實現的是菜單首頁這個組件,根據前面的布局架構圖並且參考官方文檔,實現起來非常簡單。

<!-- src/menu/menuIndex.vue -->
<template>
    <div id="menu-index">
        <el-container>
            <el-header>
                <TopMenu :logoPath="logoPath" :name="name"></TopMenu>
            </el-header>
            <el-container id="left-container">
                <el-aside width="200px">
                    <LeftMenu></LeftMenu>                    
                </el-aside>
                <el-main>
                    <router-view/>
                </el-main>
            </el-container>
        </el-container>
    </div>
</template>
<script>
import LeftMenu from './leftMenu';
import TopMenu from './topMenu';
export default {
    name: 'MenuIndex',
    components: {LeftMenu, TopMenu},
    data() {
        return {
            logoPath:  require("../../assets/images/logo1.png"),
            name: '員工管理系統'
        }
    }
}
</script>
<style lang="scss">
    #menu-index{
        .el-header{
            padding: 0px;
        }
    }
</style>

頂部菜單欄-topMenu

頂部菜單欄主要就是一個logo產品名稱

邏輯代碼也很簡單,我直接將代碼貼上。

<!-- src/menu/leftMenu.vue -->
<template>
    <div id="top-menu">
        <img class="logo" :src="logoPath" />
        <p class="name">{{name}}</p>
    </div>
</template>
<script>
export default {
    name: 'topMenu',
    props: ['logoPath', 'name']
}
</script>
<style lang="scss" scoped>
    $topMenuWidth: 80px;
    $logoWidth: 50px;
    $bg-color: #409EFF;
    $name-color: #fff;
    $name-size: 18px;
    #top-menu{
        height: $topMenuWidth;
        text-align: left;
        background-color: $bg-color;
        padding: 20px 20px 0px 20px;
        .logo {
            width: $logoWidth;
            display: inline-block;
        }
        .name{
            display: inline-block;
            vertical-align: bottom;
            color: $name-color;
            font-size: $name-size;
        }
    }
</style>

這段代碼中包含了父組件傳遞給子組件的兩個數據。

props: ['logoPath', 'name']

這個是父組件menuIndex傳遞給子組件topMenu的兩個數據,分別是logo圖標的路徑產品名稱

完成后的界面效果如下。

左側菜單欄-leftMenu

首先按照官方文檔實現一個簡單的菜單欄。

<!-- src/menu/leftMenu.vue -->
<template>
    <div id="left-menu">
        <el-menu 
            :default-active="$route.path" 
            class="el-menu-vertical-demo" 
            :collapse="false">
            <el-menu-item index="1">
                <i class="el-icon-s-home"></i>
                <span slot="title">首頁</span>
            </el-menu-item>
            <el-submenu index="2">
                <template slot="title">
                    <i class="el-icon-user-solid"></i>
                    <span slot="title">員工管理</span>
                </template>
                <el-menu-item index="2-1">員工統計</el-menu-item>
                <el-menu-item index="2-2">員工管理</el-menu-item>
            </el-submenu>
            <el-submenu index="3">
                <template slot="title">
                    <i class="el-icon-s-claim"></i>
                    <span slot="title">考勤管理</span>
                </template>
                <el-menu-item index="3-1">考勤統計</el-menu-item>
                <el-menu-item index="3-2">考勤列表</el-menu-item>
                <el-menu-item index="3-2">異常管理</el-menu-item>
            </el-submenu>
            <el-submenu index="4">
                <template slot="title">
                    <i class="el-icon-location"></i>
                    <span slot="title">工時管理</span>
                </template>
                <el-menu-item index="4-1">工時統計</el-menu-item>
                <el-submenu index="4-2">
                    <template slot="title">工時列表</template>
                    <el-menu-item index="4-2-1">選項一</el-menu-item>
                    <el-menu-item index="4-2-2">選項二</el-menu-item>
                </el-submenu>
            </el-submenu>
        </el-menu>
    </div>
</template>
<script>
export default {
    name: 'LeftMenu'
}
</script>
<style lang="scss">
    // 使左邊的菜單外層的元素高度充滿屏幕
    #left-container{
        position: absolute;
        top: 100px;
        bottom: 0px;
        // 使菜單高度充滿屏幕
        #left-menu, .el-menu-vertical-demo{
            height: 100%;
        }
    }
</style>

注意菜單的樣式代碼,設置了絕對定位,並且設置topbottom使菜單高度撐滿屏幕。

此時在看下界面效果。

基本上算是實現了一個簡單的菜單布局。

不過在實際項目在設計的時候,菜單欄的內容有可能來自後端給我們返回的數據,其中包含菜單名稱菜單圖標以及菜單之間的層級關係

總而言之,我們的菜單是動態生成的,而不是像前面那種固定的寫法。因此下面我將實現一個動態生成的菜單,菜單的數據來源於我們的路由配置

結合路由配置實現動態菜單

路由配置

首先,我將項目的路由配置代碼貼出來。

import Vue from 'vue';
import Router from "vue-router";

// 菜單
import MenuIndex from '@/components/menu/menuIndex.vue';

// 首頁
import Index from '@/components/homePage/index.vue';

// 人員統計
import EmployeeStatistics from '@/components/employeeManage/employeeStatistics.vue';
import EmployeeManage from '@/components/employeeManage/employeeManage.vue'

// 考勤
// 考勤統計
import AttendStatistics from '@/components/attendManage/attendStatistics';
// 考勤列表
import AttendList from '@/components/attendManage/attendList.vue';
// 異常管理
import ExceptManage from '@/components/attendManage/exceptManage.vue';

// 工時
// 工時統計
import TimeStatistics from '@/components/timeManage/timeStatistics.vue';
// 工時列表
import TimeList from '@/components/timeManage/timeList.vue';
Vue.use(Router)


let routes = [
    // 首頁(儀錶盤、快速入口)
    {
        path: '/index',
        name: 'index',
        component: MenuIndex,
        redirect: '/index',  
        meta: {
            title: '首頁',    // 菜單標題
            icon: 'el-icon-s-home',  // 圖標
            hasSubMenu: false, // 是否包含子菜單,false 沒有子菜單;true 有子菜單

        },
        children:[
            {
                path: '/index',
                component: Index
            }
        ]
    },
    // 員工管理
    {
        path: '/employee',
        name: 'employee',
        component: MenuIndex,
        redirect: '/employee/employeeStatistics', 
        meta: {
            title: '員工管理',    // 菜單標題
            icon: 'el-icon-user-solid',  // 圖標
            hasSubMenu: true,   // 是否包含子菜單
        },
        children: [
            // 員工統計
            {
                path: 'employeeStatistics',
                name: 'employeeStatistics',
                meta: {
                    title: '員工統計',    // 菜單標題,
                    hasSubMenu: false    // 是否包含子菜單
                },
                component: EmployeeStatistics,
            },
            // 員工管理(增刪改查)
            {
                path: 'employeeManage',
                name: 'employeeManage',
                meta: {
                    title: '員工管理',    // 菜單標題
                    hasSubMenu: false    // 是否包含子菜單
                },
                component: EmployeeManage
            }
        ]
    },
    // 考勤管理
    {
        path: '/attendManage',
        name: 'attendManage',
        component: MenuIndex,
        redirect: '/attendManage/attendStatistics',
        meta: {
            title: '考勤管理',    // 菜單標題
            icon: 'el-icon-s-claim',  // 圖標
            hasSubMenu: true, // 是否包含子節點,false 沒有子菜單;true 有子菜單
        },
        children:[
            // 考勤統計
            {
                path: 'attendStatistics',
                name: 'attendStatistics',
                meta: {
                    title: '考勤統計',    // 菜單標題   
                    hasSubMenu: false    // 是否包含子菜單               
                },
                component: AttendStatistics,
            },
            // 考勤列表
            {
                path: 'attendList',
                name: 'attendList',
                meta: {
                    title: '考勤列表',    // 菜單標題   
                    hasSubMenu: false    // 是否包含子菜單                 
                },
                component: AttendList,
            },
            // 異常管理
            {
                path: 'exceptManage',
                name: 'exceptManage',
                meta: {
                    title: '異常管理',    // 菜單標題  
                    hasSubMenu: false    // 是否包含子菜單                  
                },
                component: ExceptManage,
            }
        ]
    },
    // 工時管理
    {
        path: '/timeManage',
        name: 'timeManage',
        component: MenuIndex,
        redirect: '/timeManage/timeStatistics',
        meta: {
            title: '工時管理',    // 菜單標題
            icon: 'el-icon-message-solid',  // 圖標
            hasSubMenu: true, // 是否包含子菜單,false 沒有子菜單;true 有子菜單
        },
        children: [
            // 工時統計
            {
                path: 'timeStatistics',
                name: 'timeStatistics',
                meta: {
                    title: '工時統計',    // 菜單標題
                    hasSubMenu: false    // 是否包含子菜單        
                },
                component: TimeStatistics
            },
            // 工時列表
            {
                path: 'timeList',
                name: 'timeList',
                component: TimeList,
                meta: {
                    title: '工時列表',    // 菜單標題
                    hasSubMenu: true    // 是否包含子菜單        
                },
                children: [
                    {
                        path: 'options1',
                        meta: {
                            title: '選項一',    // 菜單標題
                            hasSubMenu: false    // 是否包含子菜單        
                        },
                    },
                    {
                        path: 'options2',
                        meta: {
                            title: '選項二',    // 菜單標題
                            hasSubMenu: false    // 是否包含子菜單        
                        },
                    },
                ]
            }
        ]
    },
];
export default new Router({
    routes
})

在這段代碼的最開始部分,我們引入了需要使用的組件,接着就對路由進行了配置。

此處使用了直接引入組件的方式,項目開發中不推薦這種寫法,應該使用懶加載的方式

路由配置除了最基礎的pathcomponent以及children之外,還配置了一個meta數據項。

meta: {
    title: '工時管理',    // 菜單標題
    icon: 'el-icon-message-solid',  // 圖標
    hasSubMenu: true, // 是否包含子節點,false 沒有子菜單;true 有子菜單
}

meta數據包含的配置有菜單標題(title)、圖標的類名(icon)和是否包含子節點(hasSubMenu)。

根據titleicon這兩個配置項,可以展示當前菜單的標題圖標

hasSubMenu表示當前的菜單項是否有子菜單,如果當前菜單包含有子菜單(hasSubMenutrue),那當前菜單對應的標籤元素就是el-submenu;否則當前菜單對應的菜單標籤元素就是el-menu-item

是否包含子菜單是一個非常關鍵的邏輯,我在實現的時候是直接將其配置到了meta.hasSubMenu這個參數裏面。

根據路由實現多級菜單

路由配置完成后,我們就需要根據路由實現菜單了。

獲取路由配置

既然要根據路由配置實現多級菜單,那第一步就需要獲取我們的路由數據。這裏我使用簡單粗暴的方式去獲取路由配置數據:this.$router.options.routes

這種方式也不太適用日常的項目開發,因為無法在獲取的時候對路由做進一步的處理,比如權限控制

我們在組件加載時打印一下這個數據。

// 代碼位置:src/menu/leftMenu.vue
 mounted(){
    console.log(this.$router.options.routes);
}

打印結果如下。

可以看到這個數據就是我們在router.js中配置的路由數據。

為了方便使用,我將這個數據定義到計算屬性中。

// 代碼位置:src/menu/leftMenu.vue
computed: {
    routesInfo: function(){
        return this.$router.options.routes;
    }
}

一級菜單

首先我們來實現一級菜單

主要的邏輯就是循環路由數據routesInfo,在循環的時候判斷當前路由route是否包含子菜單,如果包含則當前菜單使用el-submenu實現,否則當前菜單使用el-menu-item實現。

<!-- src/menu/leftMenu.vue -->
<el-menu 
    :default-active="$route.path" 
    class="el-menu-vertical-demo" 
    :collapse="false">
    <!--  一級菜單 -->
    <!--  循環路由數據  -->
    <!--  判斷當前路由route是否包含子菜單  -->
    <el-submenu 
        v-for="route in routesInfo" 
        v-if="route.meta.hasSubMenu"
        :index="route.path">
        <template slot="title">
            <i :class="route.meta.icon"></i>
            <span slot="title">{{route.meta.title}}</span>
        </template>
    </el-submenu>
    <el-menu-item :index="route.path" v-else> 
        <i :class="route.meta.icon"></i>
        <span slot="title">{{route.meta.title}}</span>
    </el-menu-item>
</el-menu>

結果:

可以看到,我們第一級菜單已經生成了,員工管理考勤管理工時管理這三個菜單是有子菜單的,所以會有一個下拉按鈕。

不過目前點開是沒有任何內容的,接下來我們就來實現這三個菜單下的二級菜單

二級菜單

二級菜單的實現和一級菜單的邏輯是相同的:循環子路由route.children,在循環的時候判斷子路由childRoute是否包含子菜單,如果包含則當前菜單使用el-submenu實現,否則當前菜單使用el-menu-item實現。

那話不多說,直接上代碼。

<!-- src/menu/leftMenu.vue -->
<el-menu 
    :default-active="$route.path" 
    class="el-menu-vertical-demo" 
    :collapse="false">
    <!--  一級菜單 -->
    <!--  循環路由數據  -->
    <!--  判斷當前路由route是否包含子菜單  -->
    <el-submenu 
        v-for="route in routesInfo" 
        v-if="route.meta.hasSubMenu"
        :index="route.path">
        <template slot="title">
            <i :class="route.meta.icon"></i>
            <span slot="title">{{route.meta.title}}</span>
        </template>
        <!-- 二級菜單 -->
        <!-- 循環子路由`route.children` -->
        <!-- 循環的時候判斷子路由`childRoute`是否包含子菜單 -->
        <el-submenu 
            v-for="childRoute in route.children" 
            v-if="childRoute.meta.hasSubMenu"
            :index="childRoute.path">
            <template slot="title">
                <i :class="childRoute.meta.icon"></i>
                <span slot="title">{{childRoute.meta.title}}</span>
            </template>
        </el-submenu>
        <el-menu-item :index="childRoute.path" v-else> 
            <i :class="childRoute.meta.icon"></i>
            <span slot="title">{{childRoute.meta.title}}</span>
        </el-menu-item>
    </el-submenu>
    <el-menu-item :index="route.path" v-else> 
        <i :class="route.meta.icon"></i>
        <span slot="title">{{route.meta.title}}</span>
    </el-menu-item>
</el-menu>

結果如下:

可以看到二級菜單成功實現。

三級菜單

三級菜單就不用多說了,和一級二級邏輯相同,這裏還是直接上代碼。

<!-- src/menu/leftMenu.vue -->
<el-menu 
    :default-active="$route.path" 
    class="el-menu-vertical-demo" 
    :collapse="false">
    <!--  一級菜單 -->
    <!--  循環路由數據  -->
    <!--  判斷當前路由route是否包含子菜單  -->
    <el-submenu 
        v-for="route in routesInfo" 
        v-if="route.meta.hasSubMenu"
        :index="route.path">
        <template slot="title">
            <i :class="route.meta.icon"></i>
            <span slot="title">{{route.meta.title}}</span>
        </template>
        <!-- 二級菜單 -->
        <!-- 循環子路由`route.children` -->
        <!-- 循環的時候判斷子路由`childRoute`是否包含子菜單 -->
        <el-submenu 
            v-for="childRoute in route.children" 
            v-if="childRoute.meta.hasSubMenu"
            :index="childRoute.path">
            <template slot="title">
                <i :class="childRoute.meta.icon"></i>
                <span slot="title">{{childRoute.meta.title}}</span>
            </template>
            <!-- 三級菜單 -->
            <!-- 循環子路由`childRoute.children` -->
            <!-- 循環的時候判斷子路由`child`是否包含子菜單 -->
            <el-submenu 
                v-for="child in childRoute.children" 
                v-if="child.meta.hasSubMenu"
                :index="child.path">
                <template slot="title">
                    <i :class="child.meta.icon"></i>
                    <span slot="title">{{child.meta.title}}</span>
                </template>
            </el-submenu>
            <el-menu-item :index="child.path" v-else> 
                <i :class="child.meta.icon"></i>
                <span slot="title">{{child.meta.title}}</span>
            </el-menu-item>
        </el-submenu>
        <el-menu-item :index="childRoute.path" v-else> 
            <i :class="childRoute.meta.icon"></i>
            <span slot="title">{{childRoute.meta.title}}</span>
        </el-menu-item>
    </el-submenu>
    <el-menu-item :index="route.path" v-else> 
        <i :class="route.meta.icon"></i>
        <span slot="title">{{route.meta.title}}</span>
    </el-menu-item>
</el-menu>

可以看到工時列表下的三級菜單已經显示了。

總結

此時我們已經結合路由配置實現了這個動態的菜單。

不過這樣的代碼在邏輯上相關於三層嵌套for循環,對應的是我們有三層的菜單。

假如我們有四層五層甚至更多層的菜單時,那我們還得在嵌套更多層for循環。很顯然這樣的方式暴露了前面多層for循環的缺陷,所以我們就需要對這樣的寫法進行一個改進。

遞歸實現動態菜單

前面我們一直在說一級二級三級菜單的實現邏輯都是相同的:循環子路由,在循環的時候判斷子路由是否包含子菜單,如果包含則當前菜單使用el-submenu實現,否則當前菜單使用el-menu-item實現。那這樣的邏輯最適合的就是使用遞歸去實現。

所以我們需要將這部分共同的邏輯抽離出來作為一個獨立的組件,然後遞歸的調用這個組件。

邏輯拆分

<!-- src/menu/menuItem.vue -->
<template>
    <div>
        <el-submenu 
            v-for="child in route" 
            v-if="child.meta.hasSubMenu"
            :index="child.path">
            <template slot="title">
                <i :class="child.meta.icon"></i>
                <span slot="title">{{child.meta.title}}</span>
            </template>
        </el-submenu>
        <el-menu-item :index="child.path" v-else> 
            <i :class="child.meta.icon"></i>
            <span slot="title">{{child.meta.title}}</span>
        </el-menu-item>
    </div>
</template>
<script>
export default {
    name: 'MenuItem',
    props: ['route']
}
</script>

需要注意的是,這次抽離出來的組件循環的時候直接循環的是route數據,那這個route數據是什麼呢。

我們先看一下前面三層循環中循環的數據源分別是什麼。

為了看得更清楚,我將前面代碼中一些不相關的內容進行了刪減。

<!-- src/menu/leftMenu.vue -->

<!--  一級菜單 -->
<el-submenu 
    v-for="route in routesInfo" 
    v-if="route.meta.hasSubMenu">
    <!-- 二級菜單 -->
    
    <el-submenu 
        v-for="childRoute in route.children" 
        v-if="childRoute.meta.hasSubMenu">
        
        <!-- 三級菜單 -->
        <el-submenu 
            v-for="child in childRoute.children" 
            v-if="child.meta.hasSubMenu">
          
        </el-submenu>
    
    </el-submenu>
</el-submenu>

從上面的代碼可以看到:

一級菜單循環的是`routeInfo`,即最初我們獲取的路由數據`this.$router.options.routes`,循環出來的每一項定義為`route`

二級菜單循環的是`route.children`,循環出來的每一項定義為`childRoute`

三級菜單循環的是`childRoute.children`,循環出來的每一項定義為`child`

按照這樣的邏輯,可以發現二級菜單三級菜單循環的數據源都是相同的,即前一個循環結果項的children,而一級菜單的數據來源於this.$router.options.routes

前面我們抽離出來的menuItem組件,循環的是route數據,即不管是一層菜單還是二層三層菜單,都是同一個數據源,因此我們需要統一數據源。那當然也非常好實現,我們在調用組件的時候,為組件傳遞不同的值即可。

代碼實現

前面公共組件已經拆分出來了,後面的代碼就非常好實現了。

首先是抽離出來的meunItem組件,實現的是邏輯判斷以及遞歸調用自身

<!-- src/menu/menuItem.vue -->
<template>
    <div>
        <el-submenu 
            v-for="child in route" 
            v-if="child.meta.hasSubMenu"
            :index="child.path">
            <template slot="title">
                <i :class="child.meta.icon"></i>
                <span slot="title">{{child.meta.title}}</span>
            </template>
            <!--遞歸調用組件自身 -->
            <MenuItem :route="child.children"></MenuItem>
        </el-submenu>
        <el-menu-item :index="child.path" v-else> 
            <i :class="child.meta.icon"></i>
            <span slot="title">{{child.meta.title}}</span>
        </el-menu-item>
    </div>
</template>
<script>
export default {
    name: 'MenuItem',
    props: ['route']
}
</script>

接着是leftMenu組件,調用menuIndex組件,傳遞原始的路由數據routesInfo

<!-- src/menu/leftMenu.vue -->
<template>
    <div id="left-menu">
        <el-menu 
            :default-active="$route.path" 
            class="el-menu-vertical-demo"
            :collapse="false">
            <MenuItem :route="routesInfo"></MenuItem>
        </el-menu>
    </div>
</template>
<script>
import MenuItem from './menuItem'
export default {
    name: 'LeftMenu',
    components: { MenuItem }
}
</script>
<style lang="scss">
    // 使左邊的菜單外層的元素高度充滿屏幕
    #left-container{
        position: absolute;
        top: 100px;
        bottom: 0px;
        // 使菜單高度充滿屏幕
        #left-menu, .el-menu-vertical-demo{
            height: 100%;
        }
    }
</style>

最終的結果這裏就不展示了,和我們需要實現的結果是一致的。

功能完善

到此,我們結合路由配置實現了菜單欄這個功能基本上已經完成了,不過這是一個缺乏靈魂的菜單欄,因為沒有設置菜單的跳轉,我們點擊菜單欄還無法路由跳轉到對應的組件,所以接下來就來實現這個功能。

菜單跳轉的實現方式有兩種,第一種是NavMenu組件提供的跳轉方式。

第二種是在菜單上添加router-link實現跳轉。

那本次我選擇的是第一種方式實現跳轉,這種實現方式需要兩個步驟才能完成,第一步是啟用el-menu上的router;第二步是設置導航的index屬性。

那下面就來實現這兩個步驟。

啟用el-menu上的router

<!-- src/menu/leftMenu.vue -->
<!-- 省略其餘未修改代碼-->
<el-menu 
    :default-active="$route.path" 
    class="el-menu-vertical-demo"
    router
    :collapse="false">
    <MenuItem :route="routesInfo">
    </MenuItem>
</el-menu>

設置導航的index屬性

首先我將每一個菜單標題對應需要設置的index屬性值列出來。

index值對應的是每個菜單在路由中配置的path

首頁        

員工管理    
    員工統計  index="/employee/employeeStatistics"
    員工管理  index="/employee/employeeManage"

考勤管理  
    考勤統計  index="/attendManage/attendStatistics"
    考勤列表  index="/attendManage/attendList"
    異常管理  index="/attendManage/exceptManage"

員工統計  
    員工統計  index="/timeManage/timeStatistics"
    員工統計  index="/timeManage/timeList"
        選項一  index="/timeManage/timeList/options1"
        選項二  index="/timeManage/timeList/options2"

接着在回顧前面遞歸調用的組件,導航菜單的index設置的是child.path,為了看清楚child.path的值,我將其添加菜單標題的右側,讓其显示到界面上。

<!-- src/menu/menuItem.vue -->
<!-- 省略其餘未修改代碼-->
<el-submenu 
    v-for="child in route" 
    v-if="child.meta.hasSubMenu"
    :index="child.path">
    <template slot="title">
        <i :class="child.meta.icon"></i>
        <span slot="title">{{child.meta.title}} | {{child.path}}</span>
    </template>
    <!--遞歸調用組件自身 -->
    <MenuItem :route="child.children"></MenuItem>
</el-submenu>
<el-menu-item :index="child.path" v-else> 
    <i :class="child.meta.icon"></i>
    <span slot="title">{{child.meta.title}} | {{child.path}}</span>
</el-menu-item>

同時將菜單欄的寬度由200px設置為400px

<!-- src/menu/menuIndex.vue -->
<!-- 省略其餘未修改代碼-->
<el-aside width="400px">
    <LeftMenu></LeftMenu>                    
</el-aside>

然後我們看一下效果。

可以發現,child.path的值就是當前菜單在路由中配置path值(router.js中配置的path值)。

那麼問題就來了,前面我們整理了每一個菜單標題對應需要設置的index屬性值,就目前來看,現在設置的index值是不符合要求的。不過仔細觀察現在菜單設置的index值和正常值是有一點接近的,只是缺少了上一級菜單的path值,如果能將上一級菜單path值和當前菜單的path值進行一個拼接,就能得到正確的index值了。

那這個思路實現的方式依然是在遞歸時將當前菜單的path作為參數傳遞給menuItem組件。

<!-- src/menu/menuIndex.vue -->
<!--遞歸調用組件自身 -->
<MenuItem 
    :route="child.children" 
    :basepath="child.path">
</MenuItem>

將當前菜單的path作為參數傳遞給menuItem組件之後,在下一級菜單實現時,就能拿到上一級菜單的path值。然後組件中將basepath的值和當前菜單的path值做一個拼接,作為當前菜單的index值。

<!-- src/menu/menuIndex.vue -->
<el-menu-item :index="getPath(child.path)" v-else> 

</el-menu-item>
<script>
import path from 'path'
export default {
    name: 'MenuItem',
    props: ['route','basepath'],
    data(){
        return {
           
        }
    },
    methods :{
        // routepath 為當前菜單的path值
        // getpath: 拼接 當前菜單的上一級菜單的path 和 當前菜單的path
        getPath: function(routePath){
            return path.resolve(this.basepath, routePath);
        }
    }
}
</script>

再看一下界面。

我們可以看到二級菜單的index值已經沒問題了,但是仔細看,發現工時管理工時列表下的兩個三級菜單index值還是有問題,缺少了工時管理這個一級菜單的path

那這個問題是因為我們在調用組件自身是傳遞的basepath有問題。

<!--遞歸調用組件自身 -->
<MenuItem 
    :route="child.children" 
    :basepath="child.path">
</MenuItem>

basepath傳遞的只是上一級菜單的path,在遞歸二級菜單時,index的值是一級菜單的path值+二級菜單的path值;那當我們遞歸三級菜單時,index的值就是二級菜單的path值+三級菜單的path值,這也就是為什麼工時管理-工時列表下的兩個三級菜單index值存在問題。

所以這裏的basepath值在遞歸的時候應該是累積的,而不只是上一級菜單的path值。因此藉助遞歸算法的優勢,basepath的值也需要通過getPath方法進行處理。

<MenuItem 
    :route="child.children" 
    :basepath="getPath(child.path)">
</MenuItem>

最終完整的代碼如下。

<!-- src/menu/menuIndex.vue -->
<template>
    <div>
        <el-submenu 
            v-for="child in route" 
            v-if="child.meta.hasSubMenu"
            :key="child.path"
            :index="getPath(child.path)">
            <template slot="title">
                <i :class="child.meta.icon"></i>
                    <span slot="title">
                        {{child.meta.title}}
                    </span>
            </template>
            <!--遞歸調用組件自身 -->
            <MenuItem 
                :route="child.children" 
                :basepath="getPath(child.path)">
            </MenuItem>
        </el-submenu>
        <el-menu-item :index="getPath(child.path)" v-else> 
            <i :class="child.meta.icon"></i>
            <span slot="title">
                   {{child.meta.title}}
            </span>
        </el-menu-item>
        
    </div>
</template>
<script>
import path from 'path'
export default {
    name: 'MenuItem',
    props: ['route','basepath'],
    data(){
        return {
           
        }
    },
    methods :{
        // routepath 為當前菜單的path值
        // getpath: 拼接 當前菜單的上一級菜單的path 和 當前菜單的path
        getPath: function(routePath){
            return path.resolve(this.basepath, routePath);
        }
    }
}
</script>

刪除其餘用來調試的代碼

最終效果

文章的最後呢,將本次實現的最終效果在此展示一下。

選項一選項二這兩個三級菜單在路由配置中沒有設置component,這兩個菜單隻是為了實現三級菜單,在最後的結果演示中,我已經刪除了路由中配置的這兩個三級菜單

此處在leftMenu組件中為el-menu開啟了unique-opened

menuIndex組件中,將左側菜單欄的寬度改為200px

關於

作者

小土豆biubiubiu

一個努力學習的前端小菜鳥,知識是無限的。堅信只要不停下學習的腳步,總能到達自己期望的地方

同時還是一個喜歡小貓咪的人,家裡有一隻美短小母貓,名叫土豆

博客園

https://www.cnblogs.com/HouJiao/

掘金

https://juejin.im/user/58c61b4361ff4b005d9e894d

微信公眾號

土豆媽的碎碎念

微信公眾號的初衷是記錄自己和身邊的一些故事,同時會不定期更新一些技術文章

歡迎大家掃碼關注,一起吸貓,一起聽故事,一起學習前端技術

作者寄語

小小總結,歡迎大家指導~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

日本也愛野味?每年撲殺7萬頭動物製「野味包」

摘錄自2020年11月1日民視報導

日本高知縣是全國森林覆蓋率最高的地方,常有許多鹿與野豬危害作物,像是吃光果實、拿柚子樹的樹幹磨牙,當地一年農損就超過1億日圓,因此每年必須撲殺7萬頭野生動物。

其中,大多都會進入加工廠,送往餐飲店做成山產料理。但受到疫情影響,餐廳紛紛停業,相關食材賣不出去。於是有業者靈機一動,把這些野味做成寵物食品。甚至有高中社團,研發野味速食包,搶攻年輕人市場。

寵物食品業者說,「這邊是用高知的鹿肉,以及部分野豬肉做成的寵物食品。」人類不吃的骨頭和部分內臟,對狗狗而言卻是營養的大餐,正好搶攻疫情期間的寵物商機。

而高知的商業高中,還有所謂的「野味社團」,致力推銷在地山產料理。除了拍照PO網,還自力開發新產品。野味鹿肉富含鐵質、高蛋白、以及維他命,對銀髮族和小朋友,都是相當好的補充品。肺炎病毒來襲,在地傳統飲食也意外找到新的出路。

生物多樣性
國際新聞
日本
野味
武漢肺炎
動物與大環境變遷

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

上次被目擊是1913年!消失百年「沃茲考氏變色龍」現蹤

摘錄自2020年11月1日自由時報報導

這真的是百年一見!科學家在非洲島國馬達加斯加西北部,發現難為世人所見的變色龍「沃茲考氏變色龍」(Voeltzkow’s chameleon),上一次這種變色龍被目擊的紀錄,是在100多年前。

馬達加斯加和德國的研究人員10月30日宣佈這項發現,他們2018年在馬達加斯加西北部探察時,發現了幾隻活「沃茲考氏變色龍」。巴伐利亞自然歷史陳列館(ZSM)的科學家,在期刊「蠑螈」(Salamandra)發表的報告說,基因分析確認,這些物種是拉波德氏變色龍(Labord’s chameleon)的近親。

上一次發現沃茲考氏變色龍是在1913年,而且,之前也從未有關於雌性「沃茲考氏變色龍」的紀錄。研究人員說,雄性的沃茲考氏變色龍外觀為綠色,相比之下,母變色龍就繽紛得多。研究人員相信,這兩種變色龍都只在雨季生活:孵化、迅速成長、與競爭者較勁,然後交配、死亡,這些過程都在短短幾個月內完成。ZSM爬蟲和兩棲動物典藏研究員格勞(Frank Glaw)說,「這些動物基本上是脊椎動物中的浮游類」。現今大規模森林砍伐正威脅牠們的棲地。

生物多樣性
國際新聞
變色龍
瀕危物種

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心