整合營銷服務商

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

          免費咨詢熱線:

          字符串拼接一定得用MessageFormat#for

          字符串拼接一定得用MessageFormat#format?

          DK拍了拍你:字符串拼接一定記得用MessageFormat#format !

          在日常開發中,我們經常會有格式化的需求,如日期格式化、數字格式化、錢幣格式化等等。

          格式化器的作用似乎跟轉換器的作用類似,但是它們的關注點卻不一樣:

          • 轉換器:將類型S轉換為類型T,關注的是類型而非格式
          • 格式化器: String <-> Java類型。這么一看它似乎和PropertyEditor類似,但是它的關注點是字符串的格式

          Spring有自己的格式化器抽象org.springframework.format.Formatter,但是談到格式化器,必然就會聯想起來JDK自己的java.text.Format體系。為后文做好鋪墊,本文就先介紹下JDK為我們提供了哪些格式化能力。

          版本約定

          • JDK:8

          ?正文

          Java里從來都缺少不了字符串拼接的活,JDK也提供了多種“工具”供我們使用,如:StringBuffer、StringBuilder以及最直接的+號,相信這些大家都有用過。但這都不是本文的內容,本文將講解格式化器,給你提供一個新的思路來拼接字符串,并且是推薦方案。

          JDK內置有格式化器,便是java.text.Format體系。它是個抽象類,提供了兩個抽象方法:

          public abstract class Format implements Serializable, Cloneable {
              public abstract StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos);	
          	public abstract Object parseObject (String source, ParsePosition pos);
          }
          • format:將Object格式化為String,并將此String放到toAppendTo里面
          • parseObject:講String轉換為Object,是format方法的逆向操作

          Java SE針對于Format抽象類對于常見的應用場景分別提供了三個子類實現:

          DateFormat:日期時間格式化

          抽象類。用于用于格式化日期/時間類型java.util.Date。雖然是抽象類,但它提供了幾個靜態方法用于獲取它的實例:

          // 格式化日期 + 時間
          public final static DateFormat getInstance() {
              return getDateTimeInstance(SHORT, SHORT);
          }
          public final static DateFormat getDateTimeInstance(int dateStyle, int timeStyle, Locale aLocale){
              return get(timeStyle, dateStyle, 3, aLocale);
          }
          
          // 格式化日期
          public final static DateFormat getDateInstance(int style, Locale aLocale) {
              return get(0, style, 2, aLocale);
          }
          // 格式化時間
          public final static DateFormat getTimeInstance(int style, Locale aLocale){
              return get(style, 0, 1, aLocale);
          }

          有了這些靜態方法,你可在不必關心具體實現的情況下直接使用:

          /**
           * {@link DateFormat}
           */
          @Test
          public void test1() {
              Date curr=new Date();
          
              // 格式化日期 + 時間
              System.out.println(DateFormat.getInstance().getClass() + "-->" + DateFormat.getInstance().format(curr));
              System.out.println(DateFormat.getDateTimeInstance().getClass() + "-->" + DateFormat.getDateTimeInstance().format(curr));
          
              // 格式化日期
              System.out.println(DateFormat.getDateInstance().getClass() + "-->" + DateFormat.getDateInstance().format(curr));
          
              // 格式化時間
              System.out.println(DateFormat.getTimeInstance().getClass() + "-->" + DateFormat.getTimeInstance().format(curr));
          }

          運行程序,輸出:

          class java.text.SimpleDateFormat-->20-12-25 上午7:19
          class java.text.SimpleDateFormat-->2020-12-25 7:19:30
          class java.text.SimpleDateFormat-->2020-12-25
          class java.text.SimpleDateFormat-->7:19:30

          嗯,可以看到底層實現其實是咱們熟悉的SimpleDateFormat。實話說,這種做法不常用,狠一點:基本不會用(框架開發者可能會用做兜底實現)。

          SimpleDateFormat

          一般來說,我們會直接使用SimpleDateFormat來對Date進行格式化,它可以自己指定Pattern,個性化十足。如:

          @Test
          public void test2() {
              DateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd"); // yyyy-MM-dd HH:mm:ss
              System.out.println(dateFormat.format(new Date()));
          }

          運行程序,輸出:

          2020-12-25

          關于SimpleDateFormat的使用方式不再啰嗦,不會的就可走自行勸退手續了。此處只提醒一點:SimpleDateFormat線程不安全

          說明:JDK 8以后不再建議使用Date類型,也就不會再使用到DateFormat。同時我個人建議:在項目中可強制嚴令禁用

          NumberFormat:數字格式化

          抽象類。用于格式化數字,它可以對數字進行任意格式化,如小數、百分數、十進制數等等。它有兩個實現類:

          類結構和DateFormat類似,也提供了getXXXInstance靜態方法給你直接使用,無需關心底層實現:

          @Test
          public void test41() {
              double myNum=1220.0455;
          
              System.out.println(NumberFormat.getInstance().getClass() + "-->" + NumberFormat.getInstance().format(myNum));
              System.out.println(NumberFormat.getCurrencyInstance().getClass() + "-->" + NumberFormat.getCurrencyInstance().format(myNum));
              System.out.println(NumberFormat.getIntegerInstance().getClass() + "-->" + NumberFormat.getIntegerInstance().format(myNum));
              System.out.println(NumberFormat.getNumberInstance().getClass() + "-->" + NumberFormat.getNumberInstance().format(myNum));
              System.out.println(NumberFormat.getPercentInstance().getClass() + "-->" + NumberFormat.getPercentInstance().format(myNum));
          }

          運行程序,輸出:

          class java.text.DecimalFormat-->1,220.045
          class java.text.DecimalFormat-->¥1,220.05
          class java.text.DecimalFormat-->1,220
          class java.text.DecimalFormat-->1,220.045
          class java.text.DecimalFormat-->122,005%

          這一看就知道DecimalFormat是NumberFormat的主力了。

          DecimalFormat

          Decimal:小數,小數的,十進位的。

          用于格式化十進制數字。它具有各種特性,可以解析和格式化數字,包括:西方數字、阿拉伯數字和印度數字。它還支持不同種類的數字,包括:整數(123)、小數(123.4)、科學記數法(1.23E4)、百分數(12%)和貨幣金額(3)。所有這些都可以進行本地化。

          下面是它的構造器:


          其中最為重要的就是這個pattern(不帶參數的構造器一般不會用),它表示格式化的模式/模版。一般來說我們對DateFormat的pattern比較熟悉,但對數字格式化的模版符號了解甚少。這里我就幫你整理出這個表格(信息源自JDK官網),記得收藏哦:

          說明:Number和Digit的區別:

          Number是個抽象概念,其表達形式可以是數字、手勢、聲音等等。如1024就是個numberDigit是用來表達的單獨符號。如0-9這是個digit就可以用來表示number,如1024就是由1、0、2、4這四個digit組成的

          看了這個表格的符號規則,估計很多同學還是一臉懵逼。不啰嗦了,上干貨

          一、0和#的使用(最常見使用場景)

          這是最經典、最常見的使用場景,甚至來說你有可能職業生涯會用到此場景。

          /**
           * {@link DecimalFormat}
           */
          @Test
          public void test4() {
              double myNum=1220.0455;
          
              System.out.println("===============0的使用===============");
              System.out.println("只保留整數部分:" + new DecimalFormat("0").format(myNum));
              System.out.println("保留3位小數:" + new DecimalFormat("0.000").format(myNum));
              System.out.println("整數部分、小數部分都5位。不夠的都用0補位(整數高位部,小數低位補):" + new DecimalFormat("00000.00000").format(myNum));
          
              System.out.println("===============#的使用===============");
              System.out.println("只保留整數部分:" + new DecimalFormat("#").format(myNum));
              System.out.println("保留2為小數并以百分比輸出:" + new DecimalFormat("#.##%").format(myNum));
          
              // 非標準數字(不建議這么用)
              System.out.println("===============非標準數字的使用===============");
              System.out.println(new DecimalFormat("666").format(myNum));
              System.out.println(new DecimalFormat(".6666").format(myNum));
          }

          運行程序,輸出:

          ===============0的使用===============只保留整數部分:1220
          保留3位小數:1220.045
          整數部分、小數部分都5位。不夠的都用0補位(整數高位部,小數低位補):01220.04550===============#的使用===============只保留整數部分:1220
          保留2為小數并以百分比輸出:122004.55%===============非標準數字的使用===============661220
          1220.666

          通過此案例,大致可得出如下結論:

          • 整數部分:0和#都可用于取出全部整數部分0的個數決定整數部分長度,不夠高位補0;#則無此約束,N多個#是一樣的效果
          • 小數部分:可保留小數點后N位(0和#效果一樣)若小數點后位數不夠,若使用的0那就低位補0,若使用#就不補(該是幾位就是幾位)
          • 數字(1-9):并不建議模版里直接寫1-9這樣的數字,了解下即可

          二、科學計數法E

          如果你不是在證券/銀行行業,這個大概率是用不著的(即使在,你估計也不會用它)。來幾個例子感受一把就成:

          @Test
          public void test5() {
              double myNum=1220.0455;
          
              System.out.println(new DecimalFormat("0E0").format(myNum));
              System.out.println(new DecimalFormat("0E00").format(myNum));
              System.out.println(new DecimalFormat("00000E00000").format(myNum));
              System.out.println(new DecimalFormat("#E0").format(myNum));
              System.out.println(new DecimalFormat("#E00").format(myNum));
              System.out.println(new DecimalFormat("#####E00000").format(myNum));
          }

          運行程序,輸出:

          1E3
          1E03
          12200E-00001
          .1E4
          .1E04
          1220E00000

          三、分組分隔符,

          分組分隔符比較常用,它就是我們常看到的逗號,

          @Test
          public void test6() {
              double myNum=1220.0455;
          
              System.out.println(new DecimalFormat(",###").format(myNum));
              System.out.println(new DecimalFormat(",##").format(myNum));
              System.out.println(new DecimalFormat(",##").format(123456789));
          
              // 分隔符,左邊是無效的
              System.out.println(new DecimalFormat("###,##").format(myNum));
          }

          運行程序,輸出:

          1,220
          12,20
          1,23,45,67,89
          12,20

          四、百分號%

          在展示層面也比較常用,用于把一個數字用%形式表示出來。

          @Test
          public void test42() {
              double myNum=1220.0455;
          
              System.out.println("百分位表示:" + new DecimalFormat("#.##%").format(myNum));
              System.out.println("千分位表示:" + new DecimalFormat("#.##\u2030").format(myNum));
          }

          運行程序,輸出:

          百分位表示:122004.55%
          千分位表示:1220045.5‰

          五、本地貨幣符號¤

          嗯,這個符號¤,鍵盤竟無法直接輸出,得使用軟鍵盤(建議使用copy大法)。

          @Test
          public void test7() {
              double myNum=1220.0455;
          
              System.out.println(new DecimalFormat(",000.00¤").format(myNum));
              System.out.println(new DecimalFormat(",000.¤00").format(myNum));
              System.out.println(new DecimalFormat("¤,000.00").format(myNum));
              System.out.println(new DecimalFormat("¤,000.¤00").format(myNum));
              // 世界貨幣表達形式
              System.out.println(new DecimalFormat(",000.00¤¤").format(myNum));
          }

          運行程序,輸出:

          1,220.05¥
          1,220.05¥
          ¥1,220.05
          1,220.05¥¥
          ¥1,220.05¥
          1,220.05CNY

          注意最后一條結果:如果連續出現兩次,代表貨幣符號的國際代號。

          說明:結果默認都做了Locale本地化處理的,若你在其它國家就不會再是¥人民幣符號嘍

          DecimalFormat就先介紹到這了,其實掌握了它就基本等于掌握了NumberFormat。接下來再簡要看看它另外一個“兒子”:ChoiceFormat。

          ChoiceFormat

          Choice:精選的,仔細推敲的。

          這個格式化器非常有意思:相當于以數字為鍵,字符串為值的鍵值對。使用一組double類型的數組作為鍵,一組String類型的數組作為值,兩數組相同(不一定必須是相同,見示例)索引值的元素作為一對。

          @Test
          public void test8() {
              double[] limits={1, 2, 3, 4, 5, 6, 7};
              String[] formats={"周一", "周二", "周三", "周四", "周五", "周六", "周天"};
              NumberFormat numberFormat=new ChoiceFormat(limits, formats);
          
              System.out.println(numberFormat.format(1));
              System.out.println(numberFormat.format(4.3));
              System.out.println(numberFormat.format(5.8));
              System.out.println(numberFormat.format(9.1));
              System.out.println(numberFormat.format(11));
          }

          運行程序,輸出:

          周一
          周四
          周五
          周天
          周天

          結果解釋:

          1. 4.3位于4和5之間,取值4;5.8位于5和6之間,取值5
          2. 9.1和11均超過了數組最大值(或者說找不到匹配的),則取值最后一對鍵值對

          可能你會想這有什么使用場景???是的,不得不承認它的使用場景較少,本文下面會介紹下它和MessageFormat的一個使用場景。

          如果說DateFormatNumberFormat都用沒什么花樣,主要記住它的pattern語法格式就成,那么就下來這個格式化器就是本文的主菜了,使用場景非常的廣泛,它就是MessageFormat

          MessageFormat:字符串格式化

          MessageFormat提供了一種與語言無關(不管你在中國還是其它國家,效果一樣)的方式生成拼接消息/拼接字符串的方法。使用它來構造顯示給最終用戶的消息。MessageFormat接受一組對象,對它們進行格式化,然后在模式的適當位置插入格式化的字符串。

          先來個最簡單的使用示例體驗一把:

          /**
           * {@link MessageFormat}
           */
          @Test
          public void test9() {
              String sourceStrPattern="Hello {0},my name is {1}";
              Object[] args=new Object[]{"girl", "YourBatman"};
          
              String formatedStr=MessageFormat.format(sourceStrPattern, args);
              System.out.println(formatedStr);
          }

          運行程序,輸出:

          Hello girl,my name is YourBatman

          有沒有中似曾相似的感覺,是不是和String.format()的作用特別像?是的,它倆的用法區別,到底使用稅文下也會討論。

          要熟悉MessageFormat的使用,主要是要熟悉它的參數模式(你也可以理解為pattern)。

          參數模式

          MessageFormat采用{}來標記需要被替換/插入的部分,其中{}里面的參數結構具有一定模式:

          ArgumentIndex[,FormatType[,FormatStyle]] 
          • ArgumentIndex非必須。從0開始的索引值
          • FormatType非必須。使用不同的java.text.Format實現類對入參進行格式化處理。它能有如下值:number:調用NumberFormat進行格式化date:調用DateFormat進行格式化time:調用DateFormat進行格式化choice:調用ChoiceFormat進行格式化
          • FormatStyle非必須。設置FormatType使用的樣式。它能有如下值:short、medium、long、full、integer、currency、percent、SubformPattern(如日期格式、數字格式#.##等)

          說明:FormatType和FormatStyle只有在傳入值為日期時間、數字、百分比等類型時才有可能需要設置,使用得并不多。畢竟:我在外部格式化好后再放進去不香嗎?

          @Test
          public void test10() {
              MessageFormat messageFormat=new MessageFormat("Hello, my name is {0}. I’am {1,number,#.##} years old. Today is {2,date,yyyy-MM-dd HH:mm:ss}");
              // 亦可通過編程式 顯示指定某個位置要使用的格式化器
              // messageFormat.setFormatByArgumentIndex(1, new DecimalFormat("#.###"));
          
              System.out.println(messageFormat.format(new Object[]{"YourBatman", 24.123456, new Date()}));
          }

          運行程序,輸出:

          Hello, my name is YourBatman. I’am 24.12 years old. Today is 2020-12-26 15:24:28

          它既可以直接在模版里指定格式化模式類型,也可以通過API方法set指定格式化器,當然你也可以在外部格式化好后再放進去,三種方式均可,任君選擇。

          注意事項

          下面基于此示例,對MessageFormat的使用注意事項作出幾點強調。

          @Test
          public void test11() {
              System.out.println(MessageFormat.format("{1} - {1}", new Object[]{1})); // {1} - {1}
              System.out.println(MessageFormat.format("{0} - {1}", new Object[]{1})); // 輸出:1 - {1}
              System.out.println(MessageFormat.format("{0} - {1}", new Object[]{1, 2, 3})); // 輸出:1 - 2
          
              System.out.println("---------------------------------");
          
              System.out.println(MessageFormat.format("'{0} - {1}", new Object[]{1, 2})); // 輸出:{0} - {1}
              System.out.println(MessageFormat.format("''{0} - {1}", new Object[]{1, 2})); // 輸出:'1 - 2
              System.out.println(MessageFormat.format("'{0}' - {1}", new Object[]{1, 2})); // {0} - 2
              // 若你數據庫值兩邊都需要''包起來,請你這么寫
              System.out.println(MessageFormat.format("''{0}'' - {1}", new Object[]{1, 2})); // '1' - 2
          
              System.out.println("---------------------------------");
              System.out.println(MessageFormat.format("0} - {1}", new Object[]{1, 2})); // 0} - 2
              System.out.println(MessageFormat.format("{0 - {1}", new Object[]{1, 2})); // java.lang.IllegalArgumentException: Unmatched braces in the pattern.
          }
          1. 參數模式的索引值必須從0開始,否則所有索引值無效
          2. 實際傳入的參數個數可以和索引個數不匹配,不報錯(能匹配上幾個算幾個)
          3. 兩個單引號''才算作一個',若只寫一個將被忽略甚至影響整個表達式謹慎使用單引號'關注'的匹配關系
          4. {}只寫左邊報錯,只寫右邊正常輸出(注意參數的對應關系)

          static方法的性能問題

          我們知道MessageFormat提供有一個static靜態方法,非常方便地的使用:

          public static String format(String pattern, Object ... arguments) {
              MessageFormat temp=new MessageFormat(pattern);
              return temp.format(arguments);
          }

          可以清晰看到,該靜態方法本質上還是構造了一個MessageFormat實例去做格式化的。因此:若你要多次(如高并發場景)格式化同一個模版(參數可不一樣)的話,那么提前創建好一個全局的(非static) MessageFormat實例再執行格式化是最好的,而非一直調用其靜態方法。

          說明:若你的系統非高并發場景,此性能損耗基本無需考慮哈,怎么方便怎么來。畢竟朝生夕死的對象對JVM來說沒啥壓力

          和String.format選誰?

          二者都能用于字符串拼接(格式化)上,撇開MessageFormat支持各種模式不說,我們只需要考慮它倆的性能上差異。

          • MeesageFormat:先分析(模版可提前分析,且可以只分析一次),再在指定位置上插入相應的值分析:遍歷字符串,維護一個{}數組并記錄位置填值
          • String.format:該靜態方法是采用運行時用正則表達式 匹配到占位符,然后執行替換的正則表達式為"%(\d+\$)?([-#+ 0,(\<]*)?(\d+)?(\.\d+)?([tT])?([a-zA-Z%])"根據正則匹配到占位符列表和位置,然后填值

          一說到正則表達式,我心里就發怵,因為它對性能是不友好的,所以孰優孰劣,高下立判。

          說明:還是那句話,沒有絕對的誰好誰壞,如果你的系統對性能不敏感,那就是方便第一

          經典使用場景

          這個就很多啦,最常見的有:HTML拼接、SQL拼接、異常信息拼接等等。

          比如下面這個SQL拼接:

          StringBuilder sb=new StringBuilder();
          sb.append("insert into user (");
          sb.append("		name,");
          sb.append("		accountId,");
          sb.append("		zhName,");
          sb.append("		enname,");
          sb.append("		status");
          sb.append(") values (");
          sb.append("		''{0}'',");
          sb.append("		{1},");
          sb.append("		''{2}'',");
          sb.append("		''{3}'',");
          sb.append("		{4},");
          sb.append(")");
          
          Object[] args={name, accountId, zhName, enname, status};
          
          // 最終SQL
          String sql=MessageFormat.format(sb.toString(), arr);

          你看,多工整。

          說明:如果值是字符串需要'包起來,那么請使用兩邊各兩個包起來

          ?總結

          本文內容介紹了JDK原生的格式化器知識點,主要作用在這三個方面:

          • DateFormat:日期時間格式化
          • NumberFormat:數字格式化
          • MessageFormat:字符串格式化

          Spring是直接面向使用者的框架產品,很顯然這些是不夠用的,并且JDK的格式化器在設計上存在一些弊端。比如經常被吐槽的:日期/時間類型格式化器SimpleDateFormat為毛在java.text包里,而它格式化的類型Date卻在java.util包內,這實為不合適。

          有了JDK格式化器作為基礎,下篇我們就可以浩浩蕩蕩地走進Spring格式化器的大門了,看看它是如何優于JDK進行設計和抽象的。

          作者:YourBatman

          原文鏈接:https://fangshixiang.blog.csdn.net/article/details/111752597

          說起富文本編輯器,我們大都遇到過,甚至使用過,這種所見即所得的書寫方式,以及它靈活的排版,讓我們的創作更加流暢和美觀。其實你可以把它理解成是把word等軟件的功能轉成在瀏覽器里面使用,這樣就能通過其他的一些手段進行管理,并融入到相應系統中。但是由于實現方式和語言等的不同,存在著一些出入。

          比如我現在正在使用的,也就是此刻我寫這篇文章的工具,就是一個富文本編輯器。其實富文本編輯器有很多種,它們的功能類似、產出目的類似、使用方式也類似,只不過在豐富程度上稍有差別,今天的CKEditor5就是其中的一款。

          示意圖

          可以看到,還是很好看的,美而不失實用。它的功能特別多,只不過有一些功能是要收費的,也就是說它只開源了一部分,或者說對于一些更高級的吊吊的功能你需要少買一點零食或者玩具。不過這些基礎功能已經足夠用了,它的可插拔式插件集成功能非常強大。

          示意圖

          就像上面所示,你可以隨意的添加或刪除一個擴展功能,下面有非常多的待繼承插件供你選擇。

          示意圖

          但是像上面這種的,帶有premium的插件,那你就需要支付一定的費用才可以使用啦。

          細心的你相信一眼就看出來了,這就是我們今天要講的內容:從word中導入。

          這是一個高級功能,雖然不是很常用,但是有一些特殊的場景或者需求,我們可能希望從編輯好的word中,通過導入的方式來讓用戶在網頁中繼續編輯它,并盡可能的保留內容和格式。

          一個是自己資金不是很充裕,再一個是想自己去動手做做,因此就決定獨立實現這樣一個功能。自己做的,當然可以隨便免費用。

          示例

          在開始之前,我們先看下做這個功能在完成之后需要滿足的效果,雖然這個功能官網是收費的,但是為了給大家演示,官網也提供了示例,我們先看下官網的成品:

          效果圖

          我們先根據提示,在官網示例上面下載了它提供的一個word,然后用CKEditor5的導入word功能,把這個word導入到編輯器中,解析完成之后就看到了效果,它的還原度很高了,官網應該是特意制作的示例word文件,里邊包含了段落、列表、圖片、表格等等多個技術點。這些都是我們接下來要實現的內容,官網這復雜程度,錢花的挺值。

          為了能讓大家有一個對比,這里我把原版word也展示出來給你們看一下:

          效果圖

          可以對比著感受下,不過還是有一些地方不太一樣的,比如我對這個原文檔做一點點更改。體現就稍微有一點略微的不同,但是這個不是毛病,只是看著有點別扭,我給兩張圖,先來原word的圖,這是我改過的列表:

          示意圖

          再來一張官網導入之后渲染的效果圖:

          示意圖

          主要有:1.列表距左邊的距離。2.列表項之間多出空白。3.不能顯示中文序號。

          實現

          我們要想實現這樣一個插件,首先想到有沒有現成的word轉html的前端或者后端插件,因為富文本編輯器是可以設置內容的,并且這個內容實質就是html代碼,然后再在這個基礎上進行集成開發。

          因為我有自己的node后端,所以如果用后端做的話就找了一些關于node的word轉html插件,一共找到了docx2html、mammoth、word2html等,但是經過測試都不太理想,于是決定放棄,換一個思路,我們可以解析word,然后根據word規范,自己生成出html。

          word是流式文件,能任意編輯并且回顯,那么肯定有一套約定在里邊,能夠保存格式并重新讀取,就看它有沒有開放給我們,幸好,docx這個x就是告訴我們,可以的,因為它就是xml的意思,符合xml規范。

          好了,我們可以找出兩個輔助插件:

          第一個就是用來解壓縮用的adm-zip包。

          第二個就是用來解析xml文件的xml-js包。

          為什么這樣呢?這是因為一個docx文件,就是一個壓縮包,我們把docx文件重命名為zip格式。然后就可以解壓看下里面的內容:

          示意圖

          這就是解壓之后的目錄,里面包含著所有的word內容,我們一會揭開它的面紗。其中一個關鍵目錄就是word文件夾:

          示意圖

          可以看到有很多的xml文件,它們就規定了word的回顯機制和渲染邏輯。

          還有一個media文件夾,我們看下它里面有什么:

          示意圖

          可以明顯的看到有兩張圖片,這兩張圖片就是我們在原word中使用的圖片,它就隱藏在這里。

          另外,其中document.xml文件存儲了整個word的結構和內容,numbering.xml文件規定了列表如何渲染,styles.xml告訴了需要應用哪些樣式。

          我們就以document.xml文件做一個簡單的說明,其余不做過多展開:

          示意圖

          文件前面是對該xml的一些聲明,body中包含了一個個的段落,也就是w:p。其中又包含了多個系列w:r,系列中就存儲著我們的文本,比如上圖紅框中我圈出的部分。

          而且里面還存儲著段落屬性w:pPr和系列屬性w:rPr。我們就是通過對這些一對對的xml標簽,來對word進行解析,找出它的渲染規則。

          首先使用上面提到的兩個包,非常簡單:

          const dir=join(process.cwd(), 'public/temp/word/' + fn)
          const zip=new AdmZip(dir)
          let contentXml=zip.readAsText('word/document.xml')
          const documentData=xml2js(contentXml)
          contentXml=zip.readAsText('word/numbering.xml')
          const numberingData=contentXml ? xml2js(contentXml) : {
            elements: ''
          }
          contentXml=zip.readAsText('word/_rels/document.xml.rels')
          const relsData=xml2js(contentXml)
          contentXml=zip.readAsText('word/styles.xml')
          const styleData=xml2js(contentXml)
          let ent=zip.getEntries()
          let ind=fn.lastIndexOf('.')
          let flag=false
          for(let i=0; i < ent.length; i++) {
            let n=ent[i].entryName
            if(n.substring(0, 11)==='word/media/') {
              flag=true
              zip.extractEntryTo(n, join(process.cwd(), 'public/temp/word/' + fn.substring(0, ind)), false, true)
            }
          }
          return {
            documentXML: documentData?.elements[0]?.elements[0]?.elements,
            numberingXML: numberingData?.elements[0]?.elements,
            relsXML: relsData?.elements[0]?.elements,
            styleXML: styleData?.elements[0]?.elements.slice(2),
            imagePath: fn.substring(0, ind),
          }

          簡單對上面的代碼做一下說明:

          1. 先說返回值,由于我們解析完word之后,需要將xml文件讀取出來,根據語義再轉成html,因此我們需要整個document.xml中的內容,因此返回documentXML,而且還要知道列表的渲染機制,因此也需要返回numberingXML,同樣我們需要獲取到文檔中用了哪些圖片,以及它們的位置,所以要返回relsXML,并且我要把對應的圖片放到另一個地方存儲起來以供使用,所以也要返回imagePath,最后整個文檔的樣式,也就是styleXML也要返回。
          2. 第1行就是獲取到上傳的word路徑,這里是我自己做了一個上傳方法。
          3. 第2行通過adm-zip插件對文件進行解壓和讀取。
          4. 第3行就是指定獲取document.xml文件的內容。
          5. 第4行就是用xml-js對讀取到的內容進行解析,之后的代碼同理,只是去解析不同的文件而已。
          6. 第13行讀取該壓縮文件中的目錄結構。
          7. 第16行至第22行就是找出word里面用到的所有圖片,并將它們存儲在其他位置。

          至此,我們看一下目前解析完成之后,形成的數據結構。

          示意圖

          很好,現在開始集成:

          import { Editor } from '/lib/ckeditor5/ckeditor'
          import loadConfig from './config'
          import filePlugin from './file'
          import './style.scss'
          loadConfig(Editor)
          const container: any=ref(null)
          let richEditor: any=null
          onMounted(()=> {
            Editor.create(container.value, {
              extraPlugins: [filePlugin]
            }).then((editor: any)=> {
              richEditor=editor
            }).catch((error: any)=> {
              console.log(error.stack)
            })
          })

          第1行,導入Editor,也就是我們一會要用的富文本編輯器,然后第9行通過create方法創建它,接收的兩個參數分別表示:渲染的容器與配置的插件。

          因為CKEditor5填入圖片的時候,需要自己手動實現一個插件方法,因此我們要把它配置進來,因為跟咱們要講的內容無關,就不展開了,官方文檔說的很清楚了。

          第5行,我在初始化編輯器之前,先去加載了一些配置,其中一個就是引入word轉pdf的功能,由于CKEditor5插件擴展很容易,直接在Editor的builtinPlugins屬性數據里面加上我們實現的插件就可以,所以我們直接講插件的開發:

          import { ButtonView, Plugin } from '/lib/ckeditor5/ckeditor'
          import { postData } from '@/request'
          import { DocumentWordProcessorReference } from '@/common/svg'
          import { serverUrl } from '@/company'
          import { ElMessage } from 'element-plus'
          import { arrayToMapByKey } from '@/utils'
          let numberingList: any=null
          let relsList: any=null
          let styleList: any=null
          let imageUrl: any=null
          let docInfo: any={
            author: {},
            currentAuthor: '',
            currentIndex: -1
          }
          const colorList=['#d13438', '#0078d4', '#5c2e91', 'chocolate', 'aquamarine', 'lawngreen', 'hotpink', 'darkblue', 'darkslateblue', 'blueviolet', 'firebrick', 'coral', 'darkcyan', 'indigo', 'greenyellow', 'deeppink', 'indianred', 'blue', 'darkgray', 'darkmagenta', 'darkgreen', 'chartreuse', 'darksalmon', 'dimgray', 'crimson', 'darkolivegreen', 'gold', 'aqua', 'lightcoral', 'goldenrod', 'burlywood', 'green', 'darkkhaki', 'forestgreen', 'fushcia', 'darkorchid', 'deepskyblue', 'darkgoldenrod', 'cyan', 'cornflowerblue', 'brown', 'cadetblue', 'darkviolet', 'dodgerblue', 'darkred', 'gray', 'khaki', 'bisque', 'darkorange', 'darkslategray', 'lightblue', 'darkturquoise', 'darkseagreen']
          let BlockType=''

          引入一些必要的組件和方法等,然后定義我們的插件,一定要繼承ckeditor5的Plugin:

          export default class importFromWord extends Plugin {
          }

          然后首先在里面實現它的init方法,做一些初始化操作:

          init() {
            const editor=this.editor
            editor.ui.componentFactory.add('importFromWord', ()=> {
              const button=new ButtonView()
              button.set({
                label: '從word導入',
                icon: DocumentWordProcessorReference,
                tooltip: true
              })
              button.on('execute', ()=> {
                this.input.click()
              })
              return button
            })
          }

          this.editor就是我們之前使用create創建好的編輯器,通過editor.ui.componentFactory.add給工具欄添加一個按鈕,也就是我們要點擊導入word的按鈕。

          示意圖

          這里面用到了ckeditor5的ButtonView按鈕組件生成器,設置它的名稱和圖標,然后添加一個暴露出來的事件,當點擊按鈕的時候,觸發選擇文件彈窗,這個input是我自己寫的一個文件上傳輸入框。

          接下來,我們去構造函數中做一些事情,當實例化這個組件的時候,初始化好我們需要的東西:

          constructor(editor: any) {
              super(editor)
              this.editor=editor
              this.input=document.createElement('input')
              this.input.type='file'
              this.input.style.opacity=0
              this.input.style.display='none'
              this.input.addEventListener('change', (e: any)=> {
                const formData: any=new FormData()
                formData.append("upload", this.input.files[0])
                formData.Headers={'Content-Type':'multipart/form-data'}
                let ms=ElMessage({
                  message: "正在解析...",
                  type: "info",
                })
                postData({
                  service: "lc",
                  url: `file/word`,
                  data: formData,
                }).then(res=> {
                  ms.close()
                  if (res.data) {
                    ElMessage({
                      message: "上傳文件成功",
                      type: "success",
                    })
                    const { documentXML, numberingXML, relsXML, styleXML, imagePath }=res.data
                    numberingList=numberingXML
                    relsList=relsXML
                    styleList=styleXML
                    imageUrl=imagePath
                    markList(documentXML)
                    const html=listToHTML(documentXML)
                    const ckC=this.editor.ui.view?.editable?.element
                    const ckP=this.editor.ui.view?.stickyPanel?.element
                    if(ckC) {
                      let rt=ckC.parentNode.parentNode.parentNode
                      rt.style.setProperty('--content-top', docInfo.paddingTop + 'px')
                      rt.style.setProperty('--content-right', docInfo.paddingRight + 'px')
                      rt.style.setProperty('--content-bottom', docInfo.paddingBottom + 'px')
                      rt.style.setProperty('--content-left', docInfo.paddingLeft + 'px')
                      rt.style.setProperty('--content-width', docInfo.pageWidth - docInfo.paddingLeft - docInfo.paddingRight + 'px')
                    }
                    if(ckP) {
                      let rt=ckP.parentNode.parentNode.parentNode
                      rt.style.setProperty('--sticky-width', docInfo.pageWidth + 'px')
                    }
                    const div=document.createElement('div')
                    div.style.display='none'
                    div.innerHTML=html
                    splitList(div.firstElementChild)
                    insertDivToList(div)
                    document.body.appendChild(div)
                    document.body.removeChild(div)
                    this.editor.setData(div.innerHTML)
                  } else {
                    ElMessage({
                      message: "上傳文件失敗",
                      type: "error",
                    })
                  }
                })
              })
            }

          在這里我們主要做了幾件事:

          首先第4行到第7行定義了一個文件選擇器。

          然后給這個輸入框添加了一個事件。

          第9行到第20行我們讀取到選擇的文件并上傳到服務器進行解析。

          對返回回來的文檔數據,我們首先做一個標記,以方便我們接下來的操作:

          function markList(list: any) {
            let cache: any=[]
            list.forEach((item: any, index: number)=> {
              let isList=false
              if(item.name==='w:p') {
                let pPr=findByName(item.elements, 'w:pPr')
                if(pPr) {
                  let numPr=findByName(pPr.elements, 'w:numPr')
                  if(numPr) {
                    isList=true
                    let ilvl=numPr.elements[0].attributes['w:val']
                    let numId=numPr.elements[1].attributes['w:val']
                    let c=cache.at(-1)
                    numPr.level=ilvl
                    if(c) {
                      if(c.ilvl===ilvl && c.numId===numId) {
                        cache.pop()
                      }else if(c.ilvl===ilvl && c.numId !==numId) {
                        numPr.start=true
                        c.numPr.end=true
                        cache.pop()
                      }else if(c.ilvl < ilvl && c.numId===numId) {
                        numPr.start=true
                        cache.pop()
                      }else if(c.ilvl > ilvl && c.numId===numId) {
                        c.numPr.end=true
                        cache.pop()
                      }else if(c.numId !==numId) {
                        while(c.ilvl >=ilvl) {
                          c.numPr.end=true
                          c=cache.pop()
                          if(!c) {
                            break
                          }
                        }
                      }
                    }else {
                      numPr.start=true
                    }
                    cache.push({
                      ilvl,
                      numId,
                      index,
                      numPr
                    })
                  }
                }
              }
            })
            cache.forEach((c: any)=> {
              c.numPr.end=true
            })
          }

          主要就是對列表進行標記,因為它要做一些特殊化的處理。

          拿到數據之后,我們的核心邏輯都在第33行,實現listToHtml進行處理:

          function listToHTML(list: any) {
            let html=''
            list.forEach((item: any, index: number)=> {
              let info=getContainer(item)
              html +=info
            })
            return html
          }

          遍歷每一項,然后把它們生成的html拼接起來:

          function getContainer(item: any) {
            let html=''
            if(item.name==='w:p') {
              let n=findByName(item.elements, 'w:pPr')
              let el: any=null
              let pEl: any=null
              let attr: any={}
              let style=null
              if(n) {
                let ps=findByName(n.elements, 'w:pStyle')
                if(ps) {
                  let styleId=getAttributeVal(ps)
                  let sy=styleList.find((item: any)=> {
                    return item.attributes['w:styleId']===styleId
                  })
                  let ppr=findByName(sy.elements, 'w:pPr')
                  let rpr=findByName(sy.elements, 'w:rPr')
                  if(ppr) {
                    ppr.elements.forEach((p: any)=> {
                      if(!findByName(n.elements, p.name)) {
                        n.elements.push(p)
                      }
                    })
                  }
                  if(rpr) {
                    let rs=findsByName(item.elements, 'w:r')
                    rs.forEach((r: any)=> {
                      let rr=findByName(r.elements, 'w:rPr')
                      rpr.elements.forEach((p: any)=> {
                        if(!findByName(rr.elements, p.name)) {
                          rr.elements.push(p)
                        }
                      })
                    })
                  }
                }
                let info=getPAttribute(n.elements)
                attr=info.attr
                style=info.style
                if(attr.list) {
                  let s1: any={}
                  let s2: any={}
                  for(let t in info.style) {
                    if(t==='list-style-type') {
                      s1[t]=info.style[t]
                    }else{
                      s2[t]=info.style[t]
                    }
                  }
                  for(let t in info.liStyle) {
                    s1[t]=info.liStyle[t]
                  }
                  if(attr.order) {
                    if(attr.start) {
                      if(attr.level !=='0') {
                        html +='<li style="list-style-type:none;">'
                      }
                      html +='<ol'
                      html +=addStyle(s1)
                      html +='<li>'
                      html +='<p'
                      html +=addStyle(s2)
                    }else {
                      html +='<li>'
                      html +='<p'
                      html +=addStyle(s2)
                    }
                  }else{
                    if(attr.start) {
                      if(attr.level !=='0') {
                        html +='<li style="list-style-type:none;">'
                      }
                      html +='<ul'
                      html +=addStyle(s1)
                      html +='<li>'
                      html +='<p'
                      html +=addStyle(s2)
                    }else {
                      html +='<li>'
                      html +='<p'
                      html +=addStyle(s2)
                    }
                  }
                }else{
                  html +='<p'
                  html +=addStyle(info.style)
                }
              }else{
                el=document.createElement('p')
              }
              item.elements.forEach((r: any)=> {
                if(r.name==='w:ins') {
                  setAuthor(r.attributes['w:author'])
                  r.elements.forEach((ins: any)=> {
                    html +=dealWr(ins, 'ins')
                  })
                }else if(r.name==='w:hyperlink') {
                  r.elements.forEach((hyp: any)=> {
                    html +=dealWr(hyp)
                  })
                }else if(r.name==='w:r') {
                  html +=dealWr(r)
                }else if(r.name==='w:commentRangeStart') {
                  BlockType='comment'
                }else if(r.name==='w:commentRangeEnd') {
                  BlockType=''
                }else if(r.name==='w:del') {
                  setAuthor(r.attributes['w:author'])
                  r.elements.forEach((hyp: any)=> {
                    html +=dealWr(hyp, 'del')
                  })
                }
              })
              if(attr.list) {
                if(attr.order) {
                  if(attr.end) {
                    html +='</p></li></ol>'
                    if(attr.level !=='0') {
                      html +='</li>'
                    }
                  }else {
                    html +='</p></li>'
                  }
                }else{
                  if(attr.end) {
                    html +='</p></li></ul>'
                    if(attr.level !=='0') {
                      html +='</li>'
                    }
                  }else {
                    html +='</p></li>'
                  }
                }
              }else {
                html +='</p>'
              }
            }else if(item.name==='w:tbl') {
              let n=findByName(item.elements, 'w:tblPr')
              if(n) {
                let info=getTableAttribute(n.elements)
                html +='<figure class="table"'
                html +=addStyle(info.figureStyle)
                html +='<table'
                html +=addStyle(info.tableStyle)
                html +='<tbody>'
              }
              item.elements.forEach((r: any)=> {
                if(r.name==='w:tr') {
                  html +=dealWtr(r)
                }
              })
              html +='</tbody></table></figure>'
            }else if(item.name==='w:sectPr') {
              let ps=findByName(item.elements, 'w:pgSz')
              let pm=findByName(item.elements, 'w:pgMar')
              if(ps) {
                docInfo.pageWidth=Math.ceil(ps.attributes['w:w'] / 20 * 96 / 72) + 1
              }
              if(pm) {
                docInfo.paddingTop=pm.attributes['w:top'] / 1440 * 96
                docInfo.paddingRight=pm.attributes['w:right'] / 1440 * 96
                docInfo.paddingBottom=pm.attributes['w:bottom'] / 1440 * 96
                docInfo.paddingLeft=pm.attributes['w:left'] / 1440 * 96
              }
            }
            return html
          }

          做了一些邏輯判斷,和不同標簽的特殊處理。

          在剛才input事件中的第34行到47行,主要是做一些編輯器大小等外觀設置,因為要配置成word中的寬度與邊距。

          還需要考慮到,列表可能不是連續的,中間可能被一些段落所隔開,因此到這里還需要對生成的html中的列表進行分割,并修復索引問題:

          function splitList(el: any) {
            while(el) {
              if(el.tagName==='OL' || el.tagName==='UL') {
                let a=el.querySelectorAll('ol > p, ul > p')
                let path: any=[]
                a.forEach((item: any)=> {
                  let p: any=[]
                  while(item) {
                    p.push(item)
                    item=item.parentNode
                    if(item===el) {
                      break
                    }
                  }
                  path.push(p.reverse())
                })
                let cur=el
                let t: number=0
                path.forEach((p: any)=> {
                  let list=cur.cloneNode(false)
                  let list2=list
                  cur.parentNode.insertBefore(list, cur)
                  p.forEach((l: any, ind: number)=> {
                    let chi=cur.children
                    let t=0
                    for(let i=0; i < chi.length; i++) {
                      if(chi[i] !==l) {
                        list.append(chi[i])
                        t++
                        i--
                      }else{
                        if(cur.tagName==='OL') {
                          let s=cur.getAttribute('start')
                          cur.setAttribute('start', s ? (+s + t) : (t + 1))
                        }
                        if(ind===p.length - 1) {
                          let par=chi[i].parentNode
                          el.parentNode.insertBefore(chi[i], el)
                          if(par.children.length===0) {
                            par.remove()
                          }
                          cur=el
                        }else{
                          cur.setAttribute('start', cur.getAttribute('start') - 1)
                          let cl=chi[i].cloneNode(false)
                          list.append(cl)
                          list=cl
                          cur=chi[i]
                        }
                        break
                      }
                    }
                  })
                })
              }
              el=el.nextElementSibling
            }
          }

          并且由于CKEditor5會對相鄰的列表進行合并等處理,這不是我們想要的,可以在它們中間插入一些div:

          function insertDivToList(div: any) {
            let f=div.firstElementChild
            let k=f.nextElementSibling
            while(k) {
              if(f.tagName==='UL' && k.tagName==='UL') {
                let d=document.createElement('div')
                f=k
                div.insertBefore(d, f)
                k=f.nextElementSibling
              }else if(f.tagName==='OL' && k.tagName==='OL') {
                let d=document.createElement('p')
                d.setAttribute('list-separator', "true")
                f=k
                div.insertBefore(d, f)
                k=f.nextElementSibling
              }else {
                f=k
                k=f.nextElementSibling
              }
            }
          }

          最后我們用this.editor.setData方法,將剛才生成的html設置到編輯器中去。

          到此我們基本就已經把需要的功能實現了。

          效果

          該來看一下我們所做的工作成果了,首先同樣導入CKEditor5官網中的文檔:

          效果圖

          可以看到,內容與格式等,基本跟原word一樣,與CKEditor5官網的示例也相同。然后我們再用另一個剛才修改過的文件測試一下:

          效果圖

          這個是用咱們剛才開發的插件導入的word的效果圖,幾乎與原word一模一樣,也沒有了CKEditor官網中的那幾個小問題。

          至此,我們針對CKEditor5導入word的功能已經開發完畢,同時我又找了各種類型的word測試,均未發現問題,還原度都非常高。

          結語

          感謝docx的規范,使得我們自己解析word成為可能,雖然不可能100%還原word的格式,但是能夠將它導入到我們的富文本編輯器中,以進行二次創作,這對我們來說是非常方便的。

          本次word轉html,并導入富文本編輯器的開發過程,希望能給大家帶來啟發。

          每一次創作都是快樂的,每一次分享也都是有益的,希望能夠幫助到你!

          謝謝

          端路由 前端路由是后來發展到SPA(單頁應用)時才出現的概念。 SPA 就是一個WEB項目只有一個 HTML 頁面,一旦頁面加載完成,SPA 不會因為用戶的操作而進行頁面的重新加載或跳轉。 前端路由在SPA項目中是必不可少的,頁面的跳轉、刷新都與路由有關,通過不同的url顯示相應的頁面。 優點:前后端的徹底分離,不刷新頁面,用戶體驗較好,頁面持久性較好。 后端路由 當在地址欄切換不同的url時,都會向服務器發送一個請求,服務器接收并響應這個請求,在服務端拼接好html文件返回給頁面來展示。 優點:減輕了前端的壓力,html都由后端拼接; 缺點:依賴于網絡,網速慢,用戶體驗很差,項目比較龐大時,服務器端壓力較大, 不能在地址欄輸入指定的url訪問相應的模塊,前后端不分離。 路由模式 前端路由實現起來其實很簡單,本質是監聽 URL 的變化,然后匹配路由規則,在不刷新的情況下顯示相應的頁面。 hash模式(對應HashHistory)

          • 把前端路由的路徑用井號 # 拼接在真實 url 后面的模式,但是會覆蓋錨點定位元素的功能,通過監聽 URL 的哈希部分變化,相應地更新頁面的內容。
          • 前端路由的處理完全在客戶端進行,在路由發生變化時,只會改變 URL 中的哈希部分(井號 # 后面的路徑),且不會向服務器發送新的請求,而是觸發 onhashchange 事件。
          • hash 只有#符號之前的內容才會包含在請求中被發送到后端,如果 nginx 沒有匹配得到當前的 url 也沒關系。hash 永遠不會提交到 server 端。
          • hash值的改變,都會在瀏覽器的訪問歷史中增加一個記錄,所以可以通過瀏覽器的回退、前進按鈕控制hash的切換。
          • hash 路由不會造成 404 頁面的問題,因為所有路由信息都在客戶端進行解析和處理,服務器只負責提供應用的初始 HTML 頁面和靜態資源,不需要關心路由的匹配問題。
          // onhashchage事件,可以在window對象上監聽這個事件
          window.onhashchange=function(event){
            console.log(event.oldURL, event.newURL)
            let hash=location.hash.slice(1)
          }


          • 通過location.hash修改hash值,觸發更新。
          • 通過監聽hashchange事件監聽瀏覽器前進或者后退,觸發更新。

          history模式 (對應HTML5History)

          • 是 html5 新推出的功能,比 Hash url 更美觀
          • 在 history 模式下瀏覽器在刷新頁面時,會按照路徑發送真實的資源請求。如果 nginx 沒有匹配得到當前的 url ,就會出現 404 的頁面。
          • 在使用 history 模式時,需要通過服務端支持允許地址可訪問,如果沒有設置,就很容易導致出現 404 的局面。
          • 改變url: history 提供了 pushState 和 replaceState 兩個方法來記錄路由狀態,這兩個方法只改變 URL 不會引起頁面刷新。
          • 監聽url變化:通過 onpopstate 事件監聽history變化,在點擊瀏覽器的前進或者后退功能時觸發,在onpopstate 事件中根據狀態信息加載對應的頁面內容。
          history.replaceState({}, null, '/b') // 替換路由
          history.pushState({}, null, '/a') // 路由壓棧,記錄瀏覽器的歷史棧 不刷新頁面
          history.back() // 返回
          history.forward() // 前進
          history.go(-2) // 后退2次

          history.pushState 修改瀏覽器地址,而頁面的加載是通過 onpopstate 事件監聽實現,加載對應的頁面內容,完成頁面更新。

          // 頁面加載完畢 first.html
          history.pushState({page: 1}, "", "first.html");
          
          window.onpopstate=function(event) {
            // 根據當前 URL 加載對應頁面
            loadPage(location.pathname); 
          };
          
          // 點擊跳轉到 second.html
          history.pushState({page: 2}, "", "second.html");
          
          function loadPage(url) {
            // 加載 url 對應頁面內容
            // 渲染頁面
          }

          onpopstate 事件是瀏覽器歷史導航的核心事件,它標識了頁面狀態的變化時機。通過監聽這個時機,根據最新的狀態信息更新頁面 當使用 history.pushState() 或 history.replaceState() 方法修改瀏覽器的歷史記錄時,不會直接觸發 onpopstate 事件。 但是,可以在調用這些方法時將數據存儲在歷史記錄條目的狀態對象中, onpopstate 事件在處理程序中訪問該狀態對象。這樣,就可以在不觸發 onpopstate 事件的情況下更新頁面內容,并獲取到相應的狀態值。 history 模式下 404 頁面的處理 在 history 模式下,瀏覽器會向服務器發起請求,服務器根據請求的路徑進行匹配: 如果服務器無法找到與請求路徑匹配的資源或路由處理器,服務器可以返回 /404 路由,跳轉到項目中配置的 404 頁面,指示該路徑未找到。 對于使用歷史路由模式的單頁應用(SPA),通常會在服務器配置中添加一個通配符路由,將所有非靜態資源的請求都重定向到主頁或一個自定義的 404 頁面,以保證在前端處理路由時不會出現真正的 404 錯誤頁面。 在項目中配置對應的 404 頁面:

          export const publicRoutes=[
            {
              path: '/404',
              component: ()=> import('src/views/404/index'),
            },
          ]

          vueRouter Vue Router 是 Vue.js 的官方路由。它與 Vue.js 核心深度集成,允許你在 Vue 應用中構建單頁面應用(SPA),并且提供了靈活的路由配置和導航功能。讓用 Vue.js 構建單頁應用變得輕而易舉。功能包括:

          • 路由映射:可以將 url 映射到 Vue組件,實現不同 url 對應不同的頁面內容。
          • 嵌套路由映射:可以在路由下定義子路由,實現更復雜的頁面結構和嵌套組件的渲染。
          • 動態路由:通過路由參數傳遞數據。你可以在路由配置中定義帶有參數的路由路徑,并通過 $route.params 獲取傳遞的參數。
          • 模塊化、基于組件的路由配置:路由配置是基于組件的,每個路由都可以指定一個 Vue 組件作為其頁面內容,將路由配置拆分為多個模塊,在需要的地方引入。。
          • 路由參數、查詢、通配符:通過路由參數傳遞數據,實現頁面間的數據傳遞和動態展示。
          • 導航守衛:Vue Router 提供了全局的導航守衛和路由級別的導航守衛,可以在路由跳轉前后執行一些操作,如驗證用戶權限、加載數據等。
          • 展示由 Vue.js 的過渡系統提供的過渡效果:可以為路由組件添加過渡效果,使頁面切換更加平滑和有動感。
          • 細致的導航控制:可以通過編程式導航(通過 JavaScript 控制路由跳轉)和聲明式導航(通過 組件實現跳轉)實現頁面的跳轉。
          • 路由模式設置:Vue Router 支持兩種路由模式:HTML5 history 模式或 hash 模式
          • 可定制的滾動行為:當頁面切換時,Vue Router 可以自動處理滾動位置。定制滾動行為,例如滾動到頁面頂部或指定的元素位置。
          • URL 的正確編碼:Vue Router 會自動對 URL 進行正確的編碼

          路由組件

          • **router-link:**通過 router-link 創建鏈接 其本質是a標簽,這使得 Vue Router 可以在不重新加載頁面的情況下更改 URL,處理 URL 的生成以及編碼。
          • **router-view:**router-view 將顯示與 url 對應的組件。

          $router$route $route: 是當前路由信息對象,獲取和當前路由有關的信息。 route 為屬性是只讀的,里面的屬性是 immutable (不可變) 的,不過可以通過 watch 監聽路由的變化。

          fullPath: ""  // 當前路由完整路徑,包含查詢參數和 hash 的完整路徑
          hash: "" // 當前路由的 hash 值 (錨點)
          matched: [] // 包含當前路由的所有嵌套路徑片段的路由記錄 
          meta: {} // 路由文件中自賦值的meta信息
          name: "" // 路由名稱
          params: {}  // 一個 key/value 對象,包含了動態片段和全匹配片段就是一個空對象。
          path: ""  // 字符串,對應當前路由的路徑
          query: {}  // 一個 key/value 對象,表示 URL 查詢參數。跟隨在路徑后用'?'帶的參數

          $router是 vueRouter 實例對象,是一個全局路由對象,通過 this.$router 訪問路由器, 可以獲取整個路由文件或使用路由提供的方法。

          // 導航守衛
          router.beforeEach((to, from, next)=> {
            /* 必須調用 `next` */
          })
          router.beforeResolve((to, from, next)=> {
            /* 必須調用 `next` */
          })
          router.afterEach((to, from)=> {})
          
          動態導航到新路由
          router.push
          router.replace
          router.go
          router.back
          router.forward
          
          

          routes 是 router 路由實例用來配置路由對象 可以使用路由懶加載(動態加載路由)的方式

          • 把不同路由對應的組件分割成不同的代碼塊,當路由被訪問時才去加載對應的組件 即為路由的懶加載,可以加快項目的加載速度,提高效率
          const router=new VueRouter({
            routes: [
              {
                path: '/home',
                name: 'Home',
                component:()=import('../views/home')
          		}
            ]
          })
          
          

          vueRouter的使用

          頁面中路由展示位置

          <div id="app">
            <!-- 添加路由 -->
            <!-- 會被渲染為 <a href="#/home"></a> -->
            <router-link to="/home">Home</router-link>
            <router-link to="/login">Login</router-link>
            <!-- 展示路由的內容 -->
            <router-view></router-view>
          </div>
          
          

          路由模塊 引入 vue-router,使用 Vue.use(VueRouter) 注冊路由插件 定義路由數組,并將數組傳入VueRouter 實例,并將實例暴露出去

          import Vue from 'vue'
          import VueRouter from 'vue-router'
          import { hasVisitPermission, isWhiteList } from './permission'
          
          // 注冊路由組件
          Vue.use(VueRouter)
          
          // 創建路由: 每一個路由規則都是一個對象
          const routers=[
            // path 路由的地址
            // component 路由的所展示的組件
            {
                path: '/',
                // 當訪問 '/'的時候 路由重定向 到新的地址 '/home'
                redirect: '/home',
            },     
            {
                path: '/home',
                component: home,
            },
            {
                path: '/login',
                component: login,
            },
          ],
          
          // 實例化 VueRouter 路由
          const router=new VueRouter({
            mode: 'history',
            base: '/',
            routers
          })
          
          // 路由守衛
          router.beforeEach(async (to, from, next)=> {
            // 清除面包屑導航數據
            store.commit('common/SET_BREAD_NAV', [])
            // 是否白名單
            if (isWhiteList(to)) {
              next()
            } else {
              // 未登錄,先登錄
              try {
                if (!store.state.user.userInfo) {
                  await store.dispatch('user/getUserInfo')
                }
          
                // 登錄后判斷,是否有訪問頁面的權限
                if (!hasVisitPermission(to, store.state.user.userInfo)) {
                  next({ path: '/404' })
                } else {
                  next()
                }
              } catch (err) {
                $error(err)
              }
            }
          })
          
          export default router
          
          

          在 main.js 上掛載路由 將VueRouter實例引入到main.js,并注冊到根Vue實例上

          import router from './router'
          
          new Vue({
            router,
            store,
            render: h=> h(App),
          }).$mount('#app')
          
          

          動態路由 我們經常需要把某種模式匹配到的所有路由,全都映射到同個組件。例如,我們有一個 User 組件,對于所有 ID 各不相同的用戶,都要使用這個組件來渲染。我們可以在 vueRrouter 的路由路徑中使用“動態路徑參數”(dynamic segment) 來達到這個效果。

          • 動態路由的創建,主要是使用 path 屬性過程中,使用動態路徑參數,路徑參數 用冒號 : 表示。

          當一個路由被匹配時,它的 params 的值將在每個組件中以 this.$route.query 的形式暴露出來。因此,我們可以通過更新 User 的模板來呈現當前的用戶 ID:

          const routes=[
            {
              path: '/user/:id'
              name: 'User'
              components: User
          	}
          ]
          
          

          _vue-router _通過配置 _params __query _來實現動態路由

          params 傳參

          • 必須使用 命名路由 name 傳值
          • 參數不會顯示在 url 上
          • 瀏覽器強制刷新時傳參會被清空
          // 傳遞參數
          this.$router.push({
            name: Home,
            params: {
              number: 1 ,
              code: '999'
            }
          })
          // 接收參數
          const p=this.$route.params

          query 傳參

          • 可以用 name 也可以使用 path 傳參
          • 傳遞的參數會顯示在 url 上
          • 頁面刷新是傳參不會丟失
          // 方式一:路由拼接
          this.$router.push('/home?username=xixi&age=18')
          
          // 方式二:name + query 傳參
          this.$router.push({
            name: Home,
            query: {
              username: 'xixi',
              age: 18
          	}
          })
          
          
          // 方式三:path + name 傳參
          this.$router.push({
            path: '/home',
            query: {
              username: 'xixi',
              age: 18
          	}
          })
          
          // 接收參數
          const q=this.$route.query
          
          

          keep-alive keep-alive是vue中的內置組件,能在組件切換過程中將狀態保留在內存中,防止重復渲染DOM。 keep-alive 包裹動態組件時,會緩存不活動的組件實例,而不是銷毀它們。 和 transition 相似,keep-alive 是一個抽象組件:它自身不會渲染一個 DOM 元素,也不會出現在組件的父組件鏈中。 keep-alive 可以設置以下props屬性:

          • include - 字符串或正則表達式。只有名稱匹配的組件會被緩存
          • exclude - 字符串或正則表達式。任何名稱匹配的組件都不會被緩存
          • max - 數字。最多可以緩存多少組件實例

          在不緩存組件實例的情況下,每次切換都會重新 render,執行整個生命周期,每次切換時,重新 render,重新請求,必然不滿足需求。 會消耗大量的性能 keep-alive 的基本使用 只是在進入當前路由的第一次render,來回切換不會重新執行生命周期,且能緩存router-view的數據。 通過 include 來判斷是否匹配緩存的組件名稱: 匹配首先檢查組件自身的 name 選項,如果 name 選項不可用,則匹配它的局部注冊名稱 (父組件 components 選項的鍵值),匿名組件不能被匹配

          <keep-alive>
          	<router-view></router-view>
          </keep-alive>
          
          <keep-alive include="a,b">
            <component :is="view"></component>
          </keep-alive>
          
          <!-- 正則表達式 (使用 `v-bind`) -->
          <keep-alive :include="/a|b/">
            <component :is="view"></component>
          </keep-alive>
          
          <!-- 數組 (使用 `v-bind`) -->
          <keep-alive :include="['a', 'b']">
            <component :is="view"></component>
          </keep-alive>

          路由配置 keepAlive

          在路由中設置 keepAlive 屬性判斷是否需要緩存

          {
            path: 'list',
            name: 'itemList', // 列表頁
            component (resolve) {
              require(['@/pages/item/list'], resolve)
           	},
             meta: {
              keepAlive: true,
              compName: 'ItemList'
              title: '列表頁'
             }
          }
          
          {
            path: 'management/class_detail/:id/:activeIndex/:status',
            name: 'class_detail',
            meta: {
              title: '開班詳情',
              keepAlive: true,
              compName: 'ClassInfoDetail',
              hideInMenu: true,
            },
            component: ()=> import('src/views/classManage/class_detail.vue'),
          },

          使用

          <div id="app" class='wrapper'>
            <keep-alive>
                <!-- 需要緩存的視圖組件 --> 
                <router-view v-if="$route.meta.keepAlive"></router-view>
             </keep-alive>
              <!-- 不需要緩存的視圖組件 -->
             <router-view v-if="!$route.meta.keepAlive"></router-view>
          </div>

          keepAlive 對生命周期的影響 設置緩存后組件加載的生命周期會新增 actived 與 deactived

          • 首次進入組件時也會觸發 actived 鉤子函數:beforeRouteEnter > beforeCreate > created> beforeMount > beforeRouteEnter 的 next 回調> mounted > activated > ... ... > beforeRouteLeave > deactivated
          • 再次進入組件時直接獲取actived的組件內容:beforeRouteEnter >activated > ... ... > beforeRouteLeave > deactivated

          keep-alive 組件監聽 include 及 exclude 的緩存規則,若發生變化則執行 pruneCache (遍歷cache 的name判斷是否需要緩存,否則將其剔除) 且 keep-alive 中沒有 template,而是用了 render,在組件渲染的時候會自動執行 render 函數,

          • 若命中緩存則直接從緩存中拿 vnode 的組件實例,
          • 若未命中緩存且未被緩存過則將該組件存入緩存,
          • 當緩存數量超出最大緩存數量時,刪除緩存中的第一個組件。

          動態路由緩存的的具體表現在:

          • 由動態路由配置的路由只能緩存一份數據。
          • keep-alive 動態路由只有第一個會有完整的生命周期,之后的路由只會觸發 actived 和 deactivated這兩個鉤子。
          • 一旦更改動態路由的某個路由數據,期所有同路由下的動態路由數據都會同步更新。

          如何刪除 keep-alive 中的緩存 vue2 中清除路由緩存

          在組件內可以通過 this 獲取 vuerouter 的緩存
          vm.$vnode.parent.componentInstance.cache

          或者通過 ref 獲取 外級 dom

          添加圖片注釋,不超過 140 字(可選)

          <template>
            <el-container id="app-wrapper">
              <Aside />
              <el-container>
                <el-header id="app-header" height="45px">
                  <Header @removeCacheRoute="removeCacheRoute" />
                </el-header>
                <!-- {{ includeViews }} -->
                <el-main id="app-main">
                  <keep-alive :include="includeViews">
                    <router-view ref="routerViewRef" :key="key" />
                  </keep-alive>
                </el-main>
              </el-container>
            </el-container>
          </template>
          
          <script>
          import Aside from './components/Aside'
          import Header from './components/Header'
          import { mapGetters } from 'vuex'
          export default {
            name: 'Layout',
            components: {
              Aside,
              Header,
            },
            data () {
              return {
              }
            },
            computed: {
              ...mapGetters(['cacheRoute', 'excludeRoute']),
              includeViews () {
                return this.cacheRoute.map(item=> item.compName)
              },
              key () {
                return this.$route.fullPath
              },
            },
            methods: {
              removeCacheRoute (fullPath) {
                const cache=this.$refs.routerViewRef.$vnode.parent.componentInstance.cache
                delete cache[fullPath]
              },
            },
          }
          </script>
          
          

          路由守衛 導航守衛主要用來通過跳轉或取消的方式守衛導航。有多種機會植入路由導航過程中:全局的, 單個路由獨享的, 或者組件級的。 通俗來講:路由守衛就是路由跳轉過程中的一些生命周期函數(鉤子函數),我們可以利用這些鉤子函數幫我們實現一些需求。 路由守衛又具體分為 全局路由守衛獨享守衛組件路由守衛。 全局路由守衛

          • 全局前置守衛router.beforeEach
          • 全局解析守衛:router.beforeResolve
          • 全局后置守衛:router.afterEach

          beforeEach(to,from, next) 在路由跳轉前觸發,參數包括to,from,next 三個,這個鉤子作用主要是用于登錄驗證。 前置守衛也可以理解為一個路由攔截器,也就是說所有的路由在跳轉前都要先被前置守衛攔截。

          
          router.beforeEach(async (to, from, next)=> {
            // 清除面包屑導航數據
            store.commit('common/SET_BREAD_NAV', [])
            // 是否白名單
            if (isWhiteList(to)) {
              next()
            } else {
              // 未登錄,先登錄
              try {
                if (!store.state.user.userInfo) {
                  await store.dispatch('user/getUserInfo')
                  // 登錄后判斷,是否有角色, 無角色 到平臺默認頁
                  if (!store.state.user.userInfo.permissions || !store.state.user.userInfo.permissions.length) {
                    next({ path: '/noPermission' })
                  }
                }
          
                // 登錄后判斷,是否有訪問頁面的權限
                if (!hasVisitPermission(to, store.state.user.userInfo)) {
                  next({ path: '/404' })
                } else {
                  next()
                }
              } catch (err) {
                $error(err)
              }
            }
          })

          beforeResolve(to,from, next) 在每次導航時都會觸發,區別是在導航被確認之前,同時在所有組件內守衛和異步路由組件被解析之后,解析守衛就被正確調用。 即在 beforeEach 和 組件內 beforeRouteEnter 之后,afterEach之前調用。 router.beforeResolve 是獲取數據或執行任何其他操作的理想位置

          router.beforeResolve(async to=> {
            if (to.meta.requiresCamera) {
              try {
                await askForCameraPermission()
              } catch (error) {
                if (error instanceof NotAllowedError) {
                  // ... 處理錯誤,然后取消導航
                  return false
                } else {
                  // 意料之外的錯誤,取消導航并把錯誤傳給全局處理器
                  throw error
                }
              }
            }
          })

          afterEach(to,from)

          和beforeEach相反,他是在路由跳轉完成后觸發,參數包括to, from 由于此時路由已經完成跳轉 所以不會再有next。

          全局后置守衛對于分析、更改頁面標題、聲明頁面等輔助功能以及許多其他事情都很有用。

          router.afterEach((to, from)=> {
          	// 在路由完成跳轉后執行,實現分析、更改頁面標題、聲明頁面等輔助功能
          	sendToAnalytics(to.fullPath)
          })

          獨享路由守衛

          beforeEnter(to,from, next) 獨享路由守衛可以直接在路由配置上定義,但是它只在進入路由時觸發,不會在 params、query 或 hash 改變時觸發。

          const routes=[
            {
              path: '/users/:id',
              component: UserDetails,
              // 在路由配置中定義守衛
              beforeEnter: (to, from,next)=> {
                next()
              },
            },
          ]
          
          

          或是使用數組的方式傳遞給 beforeEnter ,有利于實現路由守衛的重用

          function removeQueryParams(to) {
            if (Object.keys(to.query).length)
              return { path: to.path, query: {}, hash: to.hash }
          }
          
          function removeHash(to) {
            if (to.hash) return { path: to.path, query: to.query, hash: '' }
          }
          
          const routes=[
            {
              path: '/users/:id',
              component: UserDetails,
              beforeEnter: [removeQueryParams, removeHash],
            },
            {
              path: '/about',
              component: UserDetails,
              beforeEnter: [removeQueryParams],
            },
          ]

          組件路由守衛 在組件內使用的鉤子函數,類似于組件的生命周期, 鉤子函數執行的順序包括

          • beforeRouteEnter(to,from, next) -- 進入前
          • beforeRouteUpdate(to,from, next) -- 路由變化時
          • beforeRouteLeave(to,from, next) -- 離開后

          組件內路由守衛的執行時機:

          
          <template>
            ...
          </template>
          export default{
            data(){
              //...
            },
            
            // 在渲染該組件的對應路由被驗證前調用
            beforeRouteEnter (to, from, next) {
              // 此時 不能獲取組件實例 this
              // 因為當守衛執行前,組件實例還沒被創建
              next((vm)=>{
                // next 回調 在 組件 beforeMount 之后執行 此時組件實例已創建,
                // 可以通過 vm 訪問組件實例
                console.log('A組件中的路由守衛==>> beforeRouteEnter 中next 回調 vm', vm)
              )
            },
          
            // 可用于檢測路由的變化
            beforeRouteUpdate (to, from, next) {
              // 在當前路由改變,但是該組件被復用時調用  此時組件已掛載完可以訪問組件實例 `this`
              // 舉例來說,對于一個帶有動態參數的路徑 /foo/:id,在 /foo/1 和 /foo/2 之間跳轉的時候,
              // 由于會渲染同樣的 Foo 組件,因此組件實例會被復用。而這個鉤子就會在這個情況下被調用。
              console.log('組件中的路由守衛==>> beforeRouteUpdate')
              next()
            },
          
            // 在導航離開渲染該組件的對應路由時調用
            beforeRouteLeave (to, from, next) {
              // 可以訪問組件實例 `this`
              console.log('A組件中的路由守衛==>> beforeRouteLeave')
              next()
            }
          }
          <style>
          ...
          </style>

          注意 beforeRouteEnter 是支持給 next 傳遞回調的唯一守衛。對于 beforeRouteUpdate 和 beforeRouteLeave 來說,this 已經可用了,所以不支持 傳遞回調,因為沒有必要了

          路由守衛觸發流程

          頁面加載時路由守衛觸發順序:

          添加圖片注釋,不超過 140 字(可選)


          1. 觸發全局的路由守衛 beforeEach
          2. 組件在路由配置的獨享路由 beforeEnter
          3. 進入組件中的 beforeRouteEnter,此時無法獲取組件對象
          4. 觸發全局解析守衛 beforeResolve
          5. 此時路由完成跳轉 觸發全局后置守衛 afterEach
          6. 組件的掛載 beforeCreate --> created --> beforeMount
          7. 路由守衛 beforeRouterEnter 中的 next回調, 此時能夠獲取到組件實例 vm
          8. 完成組件的掛載 mounted

          當點擊切換路由時: A頁面跳轉至B頁面觸發的生命周期及路由守衛順序:

          添加圖片注釋,不超過 140 字(可選)


          1. 導航被觸發進入其他路由。
          2. 在離開的路由組件中調用 beforeRouteLeave 。
          3. 調用全局的前置路由守衛 beforeEach 。
          4. 在重用的組件里調用 beforeRouteUpdate 守衛。
          5. 調用被激活組件的路由配置中調用 beforeEnter。
          6. 解析異步路由組件。
          7. 在被激活的組件中調用 beforeRouteEnter。
          8. 調用全局的 beforeResolve 守衛。
          9. 導航被確認。
          10. 調用全局后置路由 afterEach 鉤子。
          11. 觸發 DOM 更新,激活組件的創建及掛載 beforeCreate (新)-->created (新)-->beforeMount(新) 。
          12. 調用 beforeRouteEnter 守衛中傳給 next 的回調函數,創建好的組件實例會作為回調函數的參數傳入。
          13. 失活組件的銷毀 beforeDestory(舊)-->destoryed(舊)
          14. 激活組件的掛載 mounted(新)

          路由守衛的觸發順序 beforeRouterLeave-->beforeEach-->beforeEnter-->beforeRouteEnter-->beforeResolve-->afterEach--> beforeCreate (新)-->created (新)-->beforeMount(新) -->beforeRouteEnter中的next回調 -->beforeDestory(舊)-->destoryed(舊)-->mounted(新) 當路由更新時:觸發 beforeRouteUpdate 注意: 但凡涉及到有next參數的鉤子,必須調用next() 才能繼續往下執行下一個鉤子,否則路由跳轉等會停止。 vueRouter 實現原理 vueRouter 實現的原理就是 監聽瀏覽器中 url 的 hash值變化,并切換對應的組件 1.路由注冊 通過vue.use()安裝vue-router插件,會執行install方法,并將Vue當做參數傳入install方法 Vue.use(VueRouter)===VueRouter.install() src/install.js

          export function install (Vue) {
            // 確保 install 調用一次
            if (install.installed && _Vue===Vue) return
            install.installed=true
            // 把 Vue 賦值給全局變量
            _Vue=Vue
            const registerInstance=(vm, callVal)=> {
              let i=vm.$options._parentVnode
              if (isDef(i) && isDef(i=i.data) && isDef(i=i.registerRouteInstance)) {
                i(vm, callVal)
              }
            }
            // 為每個組件混入 beforeCreate 鉤子
            // 在 `beforeCreate` 鉤子執行時 會初始化路由
            Vue.mixin({
              beforeCreate () {
                // 判斷組件是否存在 router 對象,該對象只在根組件上有
                if (isDef(this.$options.router)) {
                  // 根路由設置為自己
                  this._routerRoot=this
                  //  this.$options.router就是掛在根組件上的 VueRouter 實例
                  this._router=this.$options.router
                  // 執行VueRouter實例上的init方法,初始化路由
                  this._router.init(this)
                  // 很重要,為 _route 做了響應式處理
                  //   即訪問vm._route時會先向dep收集依賴, 而修改_router 會觸發組件渲染
                  Vue.util.defineReactive(this, '_route', this._router.history.current)
                } else {
                  // 用于 router-view 層級判斷
                  this._routerRoot=(this.$parent && this.$parent._routerRoot) || this
                }
                registerInstance(this, this)
              },
              destroyed () {
                registerInstance(this)
              }
            })
            
            /* 在Vue的prototype上面綁定 $router,
               這樣可以在任意Vue對象中使用this.$router訪問,同時經過Object.defineProperty,將 $router 代理到 Vue
               訪問this.$router 即訪問this._routerRoot._router */
            Object.defineProperty(Vue.prototype, '$router', {
              get () { return this._routerRoot._router }
            })
          
            /* 同理,訪問this.$route即訪問this._routerRoot._route */
            Object.defineProperty(Vue.prototype, '$route', {
              get () { return this._routerRoot._route }
            })
          
            // 全局注冊組件 router-link 和 router-view
            Vue.component('RouterView', View)
            Vue.component('RouterLink', Link)
          }


          1. 使用 Vue.mixin 為每個組件混入 beforeCreate 鉤子,全局混入添加組件選項 掛載 router 配置項
          2. 通過 defineReactive 為vue實例實現數據劫持 讓_router能夠及時響應頁面更新
          3. 將 router、router 、router、route 代理到 Vue 原型上
          4. 全局注冊 router-view 及 router-link 組件

          2. VueRouter 實例化 在安裝插件后,對 VueRouter 進行實例化。

          //用戶定義的路由配置數組
          const Home={ template: '<div>home</div>' }
          const Foo={ template: '<div>foo</div>' }
          const Bar={ template: '<div>bar</div>' }
          
          // 3. Create the router
          const router=new VueRouter({
            mode: 'hash',
            base: __dirname,
            routes: [
              { path: '/', component: Home }, // all paths are defined without the hash.
              { path: '/foo', component: Foo },
              { path: '/bar', component: Bar }
            ]
          })

          VueRouter 構造函數

          src/index.js

          // VueRouter 的構造函數
          constructor(options: RouterOptions={}) {
              // ...
              // 路由匹配對象 -- 路由映射表
              this.matcher=createMatcher(options.routes || [], this)
          
              // 根據 mode 采取不同的路由方式
              let mode=options.mode || 'hash'
              this.fallback=mode==='history' && !supportsPushState && options.fallback !==false
              if (this.fallback) {
                mode='hash'
              }
              if (!inBrowser) {
                mode='abstract'
              }
              this.mode=mode
          
              switch (mode) {
                case 'history':
                  this.history=new HTML5History(this, options.base)
                  break
                case 'hash':
                  this.history=new HashHistory(this, options.base, this.fallback)
                  break
                case 'abstract':
                  this.history=new AbstractHistory(this, options.base)
                  break
                default:
                  if (process.env.NODE_ENV !=='production') {
                    assert(false, `invalid mode: ${mode}`)
                  }
              }
            }

          在實例化 vueRouter 的過程中 通過 createMatcher 創建路由匹配對象(路由映射表),并且根據 mode 來采取不同的路由方式。

          3.創建路由匹配對象

          src/create-matcher.js

          export function createMatcher (
            routes: Array<RouteConfig>,
            router: VueRouter
          ): Matcher {
              // 創建路由映射表
            const { pathList, pathMap, nameMap }=createRouteMap(routes)
              
            function addRoutes (routes) {
              createRouteMap(routes, pathList, pathMap, nameMap)
            }
            // 路由匹配 找到對應的路由
            function match (
              raw: RawLocation,
              currentRoute?: Route,
              redirectedFrom?: Location
            ): Route {
              //...
            }
          
            return {
              match,
              addRoutes
            }
          }

          createMatcher 函數的作用就是創建路由映射表,然后通過閉包的方式讓 addRoutesmatch函數能夠使用路由映射表的幾個對象,最后返回一個 Matcher 對象。 在createMatcher中通過使用 createRouteMap() 根據用戶配置的路由規則來創建對應的路由映射表,返回對應的 pathList, pathMap, nameMap createRouteMap 構造函數 主要用于創建映射表,根據用戶的路由配置規則創建對應的路由映射表 src/create-route-map.js

          export function createRouteMap (
            routes: Array<RouteConfig>,
            oldPathList?: Array<string>,
            oldPathMap?: Dictionary<RouteRecord>,
            oldNameMap?: Dictionary<RouteRecord>
          ): {
            pathList: Array<string>;
            pathMap: Dictionary<RouteRecord>;
            nameMap: Dictionary<RouteRecord>;
          } {
            // 創建映射表
            const pathList: Array<string>=oldPathList || []
            const pathMap: Dictionary<RouteRecord>=oldPathMap || Object.create(null)
            const nameMap: Dictionary<RouteRecord>=oldNameMap || Object.create(null)
            // 遍歷路由配置,為每個配置添加路由記錄
            routes.forEach(route=> {
              addRouteRecord(pathList, pathMap, nameMap, route)
            })
            // 確保通配符在最后
            for (let i=0, l=pathList.length; i < l; i++) {
              if (pathList[i]==='*') {
                pathList.push(pathList.splice(i, 1)[0])
                l--
                i--
              }
            }
            return {
              pathList,
              pathMap,
              nameMap
            }
          }
          // 添加路由記錄
          function addRouteRecord (
            pathList: Array<string>,
            pathMap: Dictionary<RouteRecord>,
            nameMap: Dictionary<RouteRecord>,
            route: RouteConfig,
            parent?: RouteRecord,
            matchAs?: string
          ) {
            // 獲得路由配置下的屬性
            const { path, name }=route
            const pathToRegexpOptions: PathToRegexpOptions=route.pathToRegexpOptions || {}
            // 格式化 url,替換 / 
            const normalizedPath=normalizePath(
              path,
              parent,
              pathToRegexpOptions.strict
            )
            // 生成記錄對象
            const record: RouteRecord={
              path: normalizedPath,
              regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
              components: route.components || { default: route.component },
              instances: {},
              name,
              parent,
              matchAs,
              redirect: route.redirect,
              beforeEnter: route.beforeEnter,
              meta: route.meta || {},
              props: route.props==null
                ? {}
                : route.components
                  ? route.props
                  : { default: route.props }
            }
          
            if (route.children) {
              // 遞歸路由配置的 children 屬性,添加路由記錄
              route.children.forEach(child=> {
                const childMatchAs=matchAs
                  ? cleanPath(`${matchAs}/${child.path}`)
                  : undefined
                addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
              })
            }
            // 如果路由有別名的話
            // 給別名也添加路由記錄
            if (route.alias !==undefined) {
              const aliases=Array.isArray(route.alias)
                ? route.alias
                : [route.alias]
          
              aliases.forEach(alias=> {
                const aliasRoute={
                  path: alias,
                  children: route.children
                }
                addRouteRecord(
                  pathList,
                  pathMap,
                  nameMap,
                  aliasRoute,
                  parent,
                  record.path || '/' // matchAs
                )
              })
            }
            // 更新映射表
            if (!pathMap[record.path]) {
              pathList.push(record.path)
              pathMap[record.path]=record
            }
            // 命名路由添加記錄
            if (name) {
              if (!nameMap[name]) {
                nameMap[name]=record
              } else if (process.env.NODE_ENV !=='production' && !matchAs) {
                warn(
                  false,
                  `Duplicate named routes definition: ` +
                  `{ name: "${name}", path: "${record.path}" }`
                )
              }
            }
          }

          4.路由初始化 init

          當根組件調用 beforeCreate 鉤子函數時,會執行插件安裝階段注入的 beforeCreate 函數

          beforeCreate () {
            // 在option上面存在router則代表是根組件 
            if (isDef(this.$options.router)) {
              this._routerRoot=this
              this._router=this.$options.router
              // 執行_router實例的 init 方法   在 VueRouter 構造函數中的 init()
              this._router.init(this)
               // 為 vue 實例定義數據劫持   讓 _router 的變化能及時響應頁面的更新
              Vue.util.defineReactive(this, '_route', this._router.history.current)
            } else {
               // 非根組件則直接從父組件中獲取
              this._routerRoot=(this.$parent && this.$parent._routerRoot) || this
            }
            // 通過 registerInstance(this, this)這個方法來實現對router-view的掛載操作:主要用于注冊及銷毀實例
            registerInstance(this, this)
          },

          在根組件中進行掛載,非根組件從父級中獲取,保證全局只有一個 路由實例 初始化時執行,保證頁面再刷新時也會進行渲染

          init() -- vueRouter 構造函數中的路由初始化

          src/index.js

          init(app: any /* Vue component instance */) {
              // 將當前vm實例保存在app中,保存組件實例
              this.apps.push(app)
              // 如果根組件已經有了就返回
              if (this.app) {
                return
              }
              /* this.app保存當前vm實例 */
              this.app=app
              // 賦值路由模式
              const history=this.history
              // 判斷路由模式,以哈希模式為例
              if (history instanceof HTML5History) {
                // 路由跳轉
                history.transitionTo(history.getCurrentLocation())
              } else if (history instanceof HashHistory) {
                // 添加 hashchange 監聽
                const setupHashListener=()=> {
                  history.setupListeners()
                }
                // 路由跳轉
                history.transitionTo(
                  history.getCurrentLocation(),
                  setupHashListener,
                  setupHashListener
                )
              }
              // 該回調會在 transitionTo 中調用
              // 對組件的 _route 屬性進行賦值,觸發組件渲染
              history.listen(route=> {
                this.apps.forEach(app=> {
                  app._route=route
                })
              })
            }

          init() 核心就是進行路由的跳轉,改變 URL 然后渲染對應的組件。 路由初始化:

          1. 在Vue調用init進行初始化時會調用beforeCreate鉤子函數
          2. init方法中調用了transationTo 路由跳轉
          3. 在transationTo方法中又調用了confirmTransation 確認跳轉路由,最終在這里執行了runQueue方法,
          4. runQueue 會把隊列 queue 中的所有函數調用執行,其中就包括 路由守衛鉤子函數 的執行

          5.路由跳轉 transitionTo src/history/base.js

          transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
            // 獲取匹配的路由信息
            const route=this.router.match(location, this.current)
            // 確認切換路由
            this.confirmTransition(route, ()=> {
              // 以下為切換路由成功或失敗的回調
              // 更新路由信息,對組件的 _route 屬性進行賦值,觸發組件渲染
              // 調用 afterHooks 中的鉤子函數
              this.updateRoute(route)
              // 添加 hashchange 監聽
              onComplete && onComplete(route)
              
              // 更新 URL
              this.ensureURL()
              // 只執行一次 ready 回調
              if (!this.ready) {
                this.ready=true
                this.readyCbs.forEach(cb=> { cb(route) })
              }
            }, err=> {
            // 錯誤處理
              if (onAbort) {
                onAbort(err)
              }
              if (err && !this.ready) {
                this.ready=true
                this.readyErrorCbs.forEach(cb=> { cb(err) })
              }
            })
          }
          
          
           updateRoute (route: Route) {
              // 更新當前路由信息  對組件的 _route 屬性進行賦值,觸發組件渲染
              const prev=this.current
              this.current=route
              this.cb && this.cb(route)
              // 路由跳轉完成 調用 afterHooks 中的鉤子函數
              this.router.afterHooks.forEach(hook=> {
                hook && hook(route, prev)
              })
            }

          在路由跳轉前要先匹配路由信息,在確認切換路由后更新路由信息,觸發組件的渲染,最后更新 url

          Matcher 中的 match() 在路由配置中匹配到相應的路由則創建對應的路由信息

          src/create-matcher.js

          function match (
            raw: RawLocation,
            currentRoute?: Route,
            redirectedFrom?: Location
          ): Route {
            // 序列化 url
            // 比如對于該 url 來說 /abc?foo=bar&baz=qux#hello
            // 會序列化路徑為 /abc
            // 哈希為 #hello
            // 參數為 foo: 'bar', baz: 'qux'
            const location=normalizeLocation(raw, currentRoute, false, router)
            const { name }=location
            // 如果是命名路由,就判斷記錄中是否有該命名路由配置
            if (name) {
              const record=nameMap[name]
              // 沒找到表示沒有匹配的路由
              if (!record) return _createRoute(null, location)
              const paramNames=record.regex.keys
                .filter(key=> !key.optional)
                .map(key=> key.name)
              // 參數處理
              if (typeof location.params !=='object') {
                location.params={}
              }
              if (currentRoute && typeof currentRoute.params==='object') {
                for (const key in currentRoute.params) {
                  if (!(key in location.params) && paramNames.indexOf(key) > -1) {
                    location.params[key]=currentRoute.params[key]
                  }
                }
              }
              if (record) {
                location.path=fillParams(record.path, location.params, `named route "${name}"`)
                return _createRoute(record, location, redirectedFrom)
              }
            } else if (location.path) {
              // 非命名路由處理
              location.params={}
              for (let i=0; i < pathList.length; i++) {
               // 查找記錄
                const path=pathList[i]
                const record=pathMap[path]
                // 如果匹配路由,則創建路由
                if (matchRoute(record.regex, location.path, location.params)) {
                  return _createRoute(record, location, redirectedFrom)
                }
              }
            }
            // 沒有匹配的路由 返回空的路由
            return _createRoute(null, location)
          }
          
          
          

          通過matcher的match方法(有name匹配name,沒有就匹配path,然后返回,默認重新生成一條路由返回) 解析用戶的路由配置并按照route類型返回,然后路由切換就按照這個route來。 根據匹配的條件創建路由 _createRoute() src/create-matcher.js

          function _createRoute (
              record: ?RouteRecord,
              location: Location,
              redirectedFrom?: Location
            ): Route {
              // 根據條件創建不同的路由
              if (record && record.redirect) {
                return redirect(record, redirectedFrom || location)
              }
              if (record && record.matchAs) {
                return alias(record, location, record.matchAs)
              }
              return createRoute(record, location, redirectedFrom, router)
            }
          
            return {
              match,
              addRoute,
              getRoutes,
              addRoutes
            }
          }

          createRoute ()

          src/util/route.js

          export function createRoute (
            record: ?RouteRecord,
            location: Location,
            redirectedFrom?: ?Location,
            router?: VueRouter
          ): Route {
            const stringifyQuery=router && router.options.stringifyQuery
          
            let query: any=location.query || {}
            try {
              // 深拷貝
              query=clone(query)
            } catch (e) {}
            // 創建路由對象
            const route: Route={
              name: location.name || (record && record.name),
              meta: (record && record.meta) || {},
              path: location.path || '/',
              hash: location.hash || '',
              query,
              params: location.params || {},
              fullPath: getFullPath(location, stringifyQuery),
              matched: record ? formatMatch(record) : []
            }
            if (redirectedFrom) {
              route.redirectedFrom=getFullPath(redirectedFrom, stringifyQuery)
            }
            // 通過Object.freeze定義的只讀對象 route
            return Object.freeze(route)
          }
          
          
          // 獲得包含當前路由的所有嵌套路徑片段的路由記錄
          // 包含從根路由到當前路由的匹配記錄,從上至下
          function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
            const res=[]
            while (record) {
              res.unshift(record)
              record=record.parent
            }
            return res
          }

          6. 確認跳轉

          至此匹配路由已經完成,我們回到 transitionTo 函數中,接下來執行 confirmTransition

          confirmTransition(route: Route, onComplete: Function, onAbort?: Function) {
            const current=this.current
            // 中斷跳轉路由函數
            const abort=err=> {
              if (isError(err)) {
                if (this.errorCbs.length) {
                  this.errorCbs.forEach(cb=> {
                    cb(err)
                  })
                } else {
                  warn(false, 'uncaught error during route navigation:')
                  console.error(err)
                }
              }
              onAbort && onAbort(err)
            }
            // 如果是相同的路由就不跳轉
            if (
              isSameRoute(route, current) &&
              route.matched.length===current.matched.length
            ) {
              this.ensureURL()
              return abort()
            }
            // 通過對比路由解析出可復用的組件,需要渲染的組件,失活的組件
            const { updated, deactivated, activated }=resolveQueue(
              this.current.matched,
              route.matched
            )
            
            function resolveQueue(
                current: Array<RouteRecord>,
                next: Array<RouteRecord>
              ): {
                updated: Array<RouteRecord>,
                activated: Array<RouteRecord>,
                deactivated: Array<RouteRecord>
              } {
                let i
                const max=Math.max(current.length, next.length)
                for (i=0; i < max; i++) {
                  // 當前路由路徑和跳轉路由路徑不同時跳出遍歷
                  if (current[i] !==next[i]) {
                    break
                  }
                }
                return {
                  // 可復用的組件對應路由
                  updated: next.slice(0, i),
                  // 需要渲染的組件對應路由
                  activated: next.slice(i),
                  // 失活的組件對應路由
                  deactivated: current.slice(i)
                }
            }
            // 導航守衛數組
            const queue: Array<?NavigationGuard>=[].concat(
              // 失活的組件鉤子
              extractLeaveGuards(deactivated),
              // 全局 beforeEach 鉤子
              this.router.beforeHooks,
              // 在當前路由改變,但是該組件被復用時調用
              extractUpdateHooks(updated),
              // 需要渲染組件 enter 守衛鉤子
              activated.map(m=> m.beforeEnter),
              // 解析異步路由組件
              resolveAsyncComponents(activated)
            )
            // 保存路由
            this.pending=route
            // 迭代器,用于執行 queue 中的導航守衛鉤子
            const iterator=(hook: NavigationGuard, next)=> {
            // 路由不相等就不跳轉路由
              if (this.pending !==route) {
                return abort()
              }
              try {
              // 執行鉤子
                hook(route, current, (to: any)=> {
                  // 只有執行了鉤子函數中的 next,才會繼續執行下一個鉤子函數
                  // 否則會暫停跳轉
                  // 以下邏輯是在判斷 next() 中的傳參
                  if (to===false || isError(to)) {
                    // next(false) 
                    this.ensureURL(true)
                    abort(to)
                  } else if (
                    typeof to==='string' ||
                    (typeof to==='object' &&
                      (typeof to.path==='string' || typeof to.name==='string'))
                  ) {
                  // next('/') 或者 next({ path: '/' }) -> 重定向
                    abort()
                    if (typeof to==='object' && to.replace) {
                      this.replace(to)
                    } else {
                      this.push(to)
                    }
                  } else {
                  // 這里執行 next
                  // 通過 runQueue 中的 step(index+1) 執行 next()
                    next(to)
                  }
                })
              } catch (e) {
                abort(e)
              }
            }
            // 經典的同步執行異步函數
            runQueue(queue, iterator, ()=> {
              const postEnterCbs=[]
              const isValid=()=> this.current===route
              // 當所有異步組件加載完成后,會執行這里的回調,也就是 runQueue 中的 cb()
              // 接下來執行 需要渲染組件中的 beforeRouteEnter 導航守衛鉤子
              const enterGuards=extractEnterGuards(activated, postEnterCbs, isValid)
              // beforeResolve 解析路由鉤子
              const queue=enterGuards.concat(this.router.resolveHooks)
              runQueue(queue, iterator, ()=> {
              // 跳轉完成
                if (this.pending !==route) {
                  return abort()
                }
                this.pending=null
                onComplete(route)
                if (this.router.app) {
                  this.router.app.$nextTick(()=> {
                    postEnterCbs.forEach(cb=> {
                      cb()
                    })
                  })
                }
              })
            })
          }
          export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
            const step=index=> {
            // 隊列中的函數都執行完畢,就執行回調函數
              if (index >=queue.length) {
                cb()
              } else {
                if (queue[index]) {
                // 執行迭代器,用戶在鉤子函數中執行 next() 回調
                // 回調中判斷傳參,沒有問題就執行 next(),也就是 fn 函數中的第二個參數
                  fn(queue[index], ()=> {
                    step(index + 1)
                  })
                } else {
                  step(index + 1)
                }
              }
            }
            // 取出隊列中第一個鉤子函數
            step(0)
          }
          
          

          7. 導航守衛

          導航守衛在 確認路由跳轉中出現

          const queue: Array<?NavigationGuard>=[].concat(
              // 失活的組件鉤子
            	/*
               *  找出組件中對應的鉤子函數, 給每個鉤子函數添加上下文對象為組件自身
               *  數組降維,并且判斷是否需要翻轉數組,因為某些鉤子函數需要從子執行到父,
               *  獲得鉤子函數數組
               */ 
              extractLeaveGuards(deactivated),
              // 全局 beforeEach 鉤子, 將函數 push 進 beforeHooks 中。
              this.router.beforeHooks,
              // 在當前路由改變,但是該組件被復用時調用
              extractUpdateHooks(updated),
              // 需要渲染組件 beforeEnter 守衛鉤子
              activated.map(m=> m.beforeEnter),
              // 解析異步路由組件
              resolveAsyncComponents(activated)
          )
          
          

          先執行失活組件 deactivated 的鉤子函數 ,找出對應組件中的鉤子函數

          
          function extractLeaveGuards(deactivated: Array<RouteRecord>): Array<?Function> {
          // 傳入需要執行的鉤子函數名  失活組件觸發 beforeRouteLeave 
            return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
          }
          
          function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
            return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
          }
          
          
          function extractGuards(
            records: Array<RouteRecord>,
            name: string,
            bind: Function,
            reverse?: boolean
          ): Array<?Function> {
            const guards=flatMapComponents(records, (def, instance, match, key)=> {
             // 找出組件中對應的鉤子函數
              const guard=extractGuard(def, name)
              if (guard) {
              // 給每個鉤子函數添加上下文對象為組件自身
                return Array.isArray(guard)
                  ? guard.map(guard=> bind(guard, instance, match, key))
                  : bind(guard, instance, match, key)
              }
            })
            // 數組降維,并且判斷是否需要翻轉數組
            // 因為某些鉤子函數需要從子執行到父
            return flatten(reverse ? guards.reverse() : guards)
          }
          export function flatMapComponents (
            matched: Array<RouteRecord>,
            fn: Function
          ): Array<?Function> {
          // 數組降維
            return flatten(matched.map(m=> {
            // 將組件中的對象傳入回調函數中,獲得鉤子函數數組
              return Object.keys(m.components).map(key=> fn(
                m.components[key],
                m.instances[key],
                m, key
              ))
            }))
          }

          執行全局 beforeEach 鉤子函數, 將函數 push 進 beforeHooks 中。

          beforeEach(fn: Function): Function {
              return registerHook(this.beforeHooks, fn)
          }
          function registerHook(list: Array<any>, fn: Function): Function {
            list.push(fn)
            return ()=> {
              const i=list.indexOf(fn)
              if (i > -1) list.splice(i, 1)
            }
          }
          1. 執行 beforeRouteUpdate 鉤子函數 與 deactivated 實現類似
          2. 執行 beforeEnter 獨享路由鉤子
          3. 解析異步組件
          export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
            return (to, from, next)=> {
              let hasAsync=false
              let pending=0
              let error=null
              // 扁平化數組 獲取 組件中的鉤子函數數組
              flatMapComponents(matched, (def, _, match, key)=> {
              // 判斷是否是異步組件
                if (typeof def==='function' && def.cid===undefined) {
                  // 異步組件
                  hasAsync=true
                  pending++
                  // 成功回調
                  // once 函數確保異步組件只加載一次
                  const resolve=once(resolvedDef=> {
                    if (isESModule(resolvedDef)) {
                      resolvedDef=resolvedDef.default
                    }
                    // 判斷是否是構造函數
                    // 不是的話通過 Vue 來生成組件構造函數
                    def.resolved=typeof resolvedDef==='function'
                      ? resolvedDef
                      : _Vue.extend(resolvedDef)
                  // 賦值組件
                  // 如果組件全部解析完畢,繼續下一步
                    match.components[key]=resolvedDef
                    pending--
                    if (pending <=0) {
                      next()
                    }
                  })
                  // 失敗回調
                  const reject=once(reason=> {
                    const msg=`Failed to resolve async component ${key}: ${reason}`
                    process.env.NODE_ENV !=='production' && warn(false, msg)
                    if (!error) {
                      error=isError(reason)
                        ? reason
                        : new Error(msg)
                      next(error)
                    }
                  })
                  let res
                  try {
                  // 執行異步組件函數
                    res=def(resolve, reject)
                  } catch (e) {
                    reject(e)
                  }
                  if (res) {
                  // 下載完成執行回調
                    if (typeof res.then==='function') {
                      res.then(resolve, reject)
                    } else {
                      const comp=res.component
                      if (comp && typeof comp.then==='function') {
                        comp.then(resolve, reject)
                      }
                    }
                  }
                }
              })
              // 不是異步組件直接下一步
              if (!hasAsync) next()
            }
          }

          異步組件解析后會執行 runQueue 中的回調函數

            // 經典的同步執行異步函數
            runQueue(queue, iterator, ()=> {
              const postEnterCbs=[] // 存放beforeRouteEnter 中的回調函數
              const isValid=()=> this.current===route
              // 當所有異步組件加載完成后,會執行這里的回調,也就是 runQueue 中的 cb()
              // 接下來執行 需要渲染組件中的 beforeRouteEnter 導航守衛鉤子
              const enterGuards=extractEnterGuards(activated, postEnterCbs, isValid)
              // beforeResolve 導航守衛鉤子
              const queue=enterGuards.concat(this.router.resolveHooks)
              runQueue(queue, iterator, ()=> {
              // 跳轉完成
                if (this.pending !==route) {
                  return abort()
                }
                this.pending=null
                onComplete(route)
                if (this.router.app) {
                  this.router.app.$nextTick(()=> {
                    postEnterCbs.forEach(cb=> {
                      cb()
                    })
                  })
                }
              })
            })
          1. 執行 beforeRouterEnter ,因為在 beforeRouterEnter 在路由確認之前組件還未渲染,所以此時無法訪問到組件的 this 。

          但是該鉤子函數在路由確認執行,是唯一一個支持在 next 回調中獲取 this 對象的函數。

          
          // beforeRouteEnter 鉤子函數
          function extractEnterGuards (
            activated: Array<RouteRecord>,
            cbs: Array<Function>,
            isValid: ()=> boolean
          ): Array<?Function> {
            return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key)=> {
              return bindEnterGuard(guard, match, key, cbs, isValid)
            })
          }
          
          function bindEnterGuard (
            guard: NavigationGuard,
            match: RouteRecord,
            key: string,
            cbs: Array<Function>,
            isValid: ()=> boolean
          ): NavigationGuard {
            return function routeEnterGuard (to, from, next) {
              return guard(to, from, cb=> {
                next(cb)
                if (typeof cb==='function') {
                  // 判斷 cb 是否是函數
                  // 是的話就 push 進 postEnterCbs
                  cbs.push(()=> {
                    // #750
                    // if a router-view is wrapped with an out-in transition,
                    // the instance may not have been registered at this time.
                    // we will need to poll for registration until current route
                    // is no longer valid.
                     // 循環直到拿到組件實例
                    poll(cb, match.instances, key, isValid)
                  })
                }
              })
            }
          }
          
          // 該函數是為了解決 issus #750
          // 當 router-view 外面包裹了 mode 為 out-in 的 transition 組件 
          // 會在組件初次導航到時獲得不到組件實例對象
          function poll (
            cb: any, // somehow flow cannot infer this is a function
            instances: Object,
            key: string,
            isValid: ()=> boolean
          ) {
            if (instances[key]) {
              cb(instances[key])
            } else if (isValid()) {
              // setTimeout 16ms 作用和 nextTick 基本相同
              setTimeout(()=> {
                poll(cb, instances, key, isValid)
              }, 16)
            }
          }
          1. 執行 beforeResolve 導航守衛鉤子,如果注冊了全局 beforeResolve 鉤子就會在這里執行。
          2. 導航確認完成后 updateRoute 切換路由,更新路由信息后 調用 afterEach 導航守衛鉤子
          updateRoute (route: Route) {
            // 更新當前路由信息  對組件的 _route 屬性進行賦值,觸發組件渲染
            const prev=this.current
            this.current=route
            this.cb && this.cb(route)  // 實際執行 init傳入的回調, app._route=route 對組件的 _route 屬性進行賦值
            // 路由跳轉完成 調用 afterHooks 中的鉤子函數
            this.router.afterHooks.forEach(hook=> {
              hook && hook(route, prev)
            })
          }

          this.cb 是怎么來的呢? 其實 this.cb 是通過 History.listen 實現的,在VueRouter 的初始化 init 過程中對 this.cb 進行了賦值

          //  History 類中 的listen 方法對this.cb 進行賦值
          listen (cb: Function) {
            this.cb=cb
          }
          
          //  init 中執行了 history.listen,將回調函數賦值給 this.cb
          init (app: any /* Vue component instance */) {
            this.apps.push(app)
            history.listen(route=> {
              this.apps.forEach((app)=> {
                app._route=route
              })
            })
          }
          1. 觸發組件的渲染

          當app._router 發生變化時觸發 vue 的響應式調用render() 將路由相應的組件渲染到中

          app._route=route  

          hash 模式的實現

          hash模式的原理是監聽瀏覽器url中hash值的變化,并切換對應的組件

          class HashHistory extends History  {
             constructor (router: Router, base: ?string, fallback: boolean) {
              super(router, base)
              // check history fallback deeplinking
              if (fallback && checkFallback(this.base)) {
                return
              }
              ensureSlash()
            }
            // 監聽 hash 的變化
            setupListeners () {
              const router=this.router
              const expectScroll=router.options.scrollBehavior
              const supportsScroll=supportsPushState && expectScroll
              
              if (supportsScroll) {
                setupScroll()
              }
              
              window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', ()=> {
                const current=this.current
                if (!ensureSlash()) {
                  return
                }
                // 傳入當前的 hash 并觸發跳轉
                this.transitionTo(getHash(), route=> {
                  if (supportsScroll) {
                    handleScroll(this.router, route, current, true)
                  }
                  if (!supportsPushState) {
                    replaceHash(route.fullPath)
                  }
                })
              })
            }   
          }
          
          // 如果瀏覽器沒有 # 則自動補充 /#/
          function ensureSlash (): boolean {
            const path=getHash()
            if (path.charAt(0)==='/') {
              return true
            }
            replaceHash('/' + path)
            return false
          }
          
          
          export default HashHistory

          如果手動刷新頁面的話,是不會觸發hashchange事件的,也就是找不出組件來,那咋辦呢?刷新頁面肯定會使路由重新初始化,咱們只需要在初始化函數init 上執行一次原地跳轉就行。 router-view 組件渲染 組件渲染的關鍵在于 router-view ,將路由變化時匹配到的組件進行渲染。 routerView是一個函數式組件,函數式組件沒有data,沒有組件實例。 因此使用了父組件中的$createElement函數,用以渲染組件,并且在組件渲染的各個時期注冊了hook 如果被 keep-alive 包裹則直接使用緩存的 vnode 通過 depth 實現路由嵌套, 循環向上級訪問,直到訪問到根組件,得到路由的 depth 深度

          export default {
            name: 'RouterView',
            /* 
              https://cn.vuejs.org/v2/api/#functional
              使組件無狀態 (沒有 data ) 和無實例 (沒有 this 上下文)。他們用一個簡單的 render 函數返回虛擬節點使他們更容易渲染。
            */
            functional: true,
            props: {
              name: {
                type: String,
                default: 'default'
              }
            },
            render (_, { props, children, parent, data }) {
              /* 標記位,標記是route-view組件 */
              data.routerView=true
          
              /* 直接使用父組件的createElement函數  因此router-view渲染的組件可以解析命名槽*/
              const h=parent.$createElement
              /* props的name,默認'default' */
              const name=props.name
              /* option中的VueRouter對象 */
              const route=parent.$route
              /* 在parent上建立一個緩存對象 */
              const cache=parent._routerViewCache || (parent._routerViewCache={})
          
          
              /* 記錄組件深度 用于實現路由嵌套 */
              let depth=0
              /* 標記是否是待用(非alive狀態)) */
              let inactive=false
              /* _routerRoot中中存放了根組件的勢力,這邊循環向上級訪問,直到訪問到根組件,得到depth深度 */
              // 用 depth 幫助找到對應的 RouterRecord
              while (parent && parent._routerRoot !==parent) {
                if (parent.$vnode && parent.$vnode.data.routerView) {
                  // 遇到其他的 router-view 組件則路由深度+1 
                  depth++
                }
                /* 如果_inactive為true,代表是在keep-alive中且是待用(非alive狀態) */
                if (parent._inactive) {
                  inactive=true
                }
                parent=parent.$parent
              }
              /* 存放route-view組件的深度 */
              data.routerViewDepth=depth
          
              /* 如果inactive為true說明在keep-alive組件中,直接從緩存中取 */
              if (inactive) {
                return h(cache[name], data, children)
              }
          
              // depth 幫助 route.matched 找到對應的路由記錄
              const matched=route.matched[depth]
          
              /* 如果沒有匹配到的路由,則渲染一個空節點 */
              if (!matched) {
                cache[name]=null
                return h()
              }
          
              /* 從成功匹配到的路由中取出組件 */
              const component=cache[name]=matched.components[name]
          
              // attach instance registration hook
              // this will be called in the instance's injected lifecycle hooks
              /* 注冊實例的registration鉤子,這個函數將在實例被注入的加入到組件的生命鉤子(beforeCreate與destroyed)中被調用 */
              data.registerRouteInstance=(vm, val)=> {  
                /* 第二個值不存在的時候為注銷 */
                // val could be undefined for unregistration
                /* 獲取組件實例 */
                const current=matched.instances[name]
                if (
                  (val && current !==vm) ||
                  (!val && current===vm)
                ) {
                  /* 這里有兩種情況,一種是val存在,則用val替換當前組件實例,另一種則是val不存在,則直接將val(這個時候其實是一個undefined)賦給instances */
                  matched.instances[name]=val
                }
              }
          
              // also register instance in prepatch hook
              // in case the same component instance is reused across different routes
              ;(data.hook || (data.hook={})).prepatch=(_, vnode)=> {
                matched.instances[name]=vnode.componentInstance
              }
          
              // resolve props
              let propsToPass=data.props=resolveProps(route, matched.props && matched.props[name])
              if (propsToPass) {
                // clone to prevent mutation
                propsToPass=data.props=extend({}, propsToPass)
                // pass non-declared props as attrs
                const attrs=data.attrs=data.attrs || {}
                for (const key in propsToPass) {
                  if (!component.props || !(key in component.props)) {
                    attrs[key]=propsToPass[key]
                    delete propsToPass[key]
                  }
                }
              }
          
              return h(component, data, children)
            }
          }
          
          

          嵌套路由的實現 routerView的render函數通過定義一個depth參數,來判斷當前嵌套的路由是位于matched函數層級,然后取出對應的record對象,渲染器對應的組件。 router-link 組件 router-link 的本質是 a 標簽,在標簽上綁定了click事件,然后執行對應的VueRouter實例的push()實現的

          export default {
            name: 'RouterLink',
            props: {
              to: {
                type: toTypes,
                required: true
              },
              tag: {
                type: String,
                default: 'a'
              },
              exact: Boolean,
              append: Boolean,
              replace: Boolean,  // 當點擊時會調用router.replace()而不是router.push(),這樣導航后不會留下history記錄
              activeClass: String,
              exactActiveClass: String,
              event: {
                type: eventTypes,
                default: 'click'   // 默認為 click 事件
              }
            },
            render (h: Function) {
              // 獲取 $router 實例
              const router=this.$router
              // 獲取當前路由對象
              const current=this.$route
          
              // 要跳轉的地址
              const { location, route, href }=router.resolve(this.to, current, this.append)
              const classes={}
              const globalActiveClass=router.options.linkActiveClass
              const globalExactActiveClass=router.options.linkExactActiveClass
              // Support global empty active class
              const activeClassFallback=globalActiveClass==null
                      ? 'router-link-active'
                      : globalActiveClass
              const exactActiveClassFallback=globalExactActiveClass==null
                      ? 'router-link-exact-active'
                      : globalExactActiveClass
              const activeClass=this.activeClass==null
                      ? activeClassFallback
                      : this.activeClass
              const exactActiveClass=this.exactActiveClass==null
                      ? exactActiveClassFallback
                      : this.exactActiveClass
              const compareTarget=location.path
                ? createRoute(null, location, null, router)
                : route
          
              classes[exactActiveClass]=isSameRoute(current, compareTarget)
              classes[activeClass]=this.exact
                ? classes[exactActiveClass]
                : isIncludedRoute(current, compareTarget)
          
              const handler=e=> {
                //  綁定點擊事件
                //  若設置了 replace 屬性則使用 router.replace 切換路由
                //  否則使用 router.push 更新路由
                if (guardEvent(e)) {
                  if (this.replace) {
                    //  router.replace()  導航后不會留下history記錄
                    router.replace(location)
                  } else {
                    router.push(location)
                  }
                }
              }
          
              const on={ click: guardEvent }  // <router-link> 組件默認都支持的click事件 
              if (Array.isArray(this.event)) {
                this.event.forEach(e=> { on[e]=handler })
              } else {
                on[this.event]=handler
              }
          
              const data: any={
                class: classes     
              }
          
              if (this.tag==='a') {   // 如果是 a 標簽會綁定監聽事件
                data.on=on  // 監聽自身
                data.attrs={ href }
              } else {
                // find the first <a> child and apply listener and href
                const a=findAnchor(this.$slots.default)    // 如果不是 a標簽則會 找到第一個 a 標簽
                if (a) {                                     
                  // in case the <a> is a static node        // 找到第一個 a 標簽
                  a.isStatic=false
                  const extend=_Vue.util.extend
                  const aData=a.data=extend({}, a.data)
                  aData.on=on
                  const aAttrs=a.data.attrs=extend({}, a.data.attrs)
                  aAttrs.href=href
                } else {
                  // doesn't have <a> child, apply listener to self
                  data.on=on      // 如果沒找到 a 標簽就監聽自身  
                }
              }
          
              //最后調用$createElement去創建該Vnode
              return h(this.tag, data, this.$slots.default)  
            }
          }
          
          // 阻止瀏覽器的默認事件,所有的事件都是通過 VueRouter 內置代碼實現的
          function guardEvent (e) {
            // don't redirect with control keys
            if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
            // don't redirect when preventDefault called
            if (e.defaultPrevented) return
            // don't redirect on right click
            if (e.button !==undefined && e.button !==0) return
            // don't redirect if `target="_blank"`
            if (e.currentTarget && e.currentTarget.getAttribute) {
              const target=e.currentTarget.getAttribute('target')
              if (/\b_blank\b/i.test(target)) return
            }
            // this may be a Weex event which doesn't have this method
            if (e.preventDefault) {
              e.preventDefault()
            }
            return true
          }

          何時觸發視圖更新

          在混入 beforeCreate 時 對 _route 作了響應式處理,即訪問vm._route時會先向dep收集依賴

          beforeCreate () {
                // 判斷組件是否存在 router 對象,該對象只在根組件上有
                if (isDef(this.$options.router)) {
                  // 根路由設置為自己
                  this._routerRoot=this
                  //  this.$options.router就是掛在根組件上的 VueRouter 實例
                  this._router=this.$options.router
                  // 執行VueRouter實例上的init方法,初始化路由
                  this._router.init(this)
                  // 很重要,為 _route 做了響應式處理
                  //   即訪問vm._route時會先向dep收集依賴, 而修改 _router 會觸發組件渲染
                  Vue.util.defineReactive(this, '_route', this._router.history.current)
                } else {
                  // 用于 router-view 層級判斷
                  this._routerRoot=(this.$parent && this.$parent._routerRoot) || this
                }
                registerInstance(this, this)
              },
          
          //  訪問vm._route時會先向dep收集依賴
            Object.defineProperty(Vue.prototype, '$router', {
              get () { return this._routerRoot._router }
            })

          訪問 $router 時觸發依賴收集

          • 在組件中使用 this.$router
          • router-link 組件內部

          何時觸發 dep.notify 呢? 路由導航實際執行的history.push方法 會觸發 tansitionTo

            push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
              const { current: fromRoute }=this
              this.transitionTo(location, route=> {
                pushState(cleanPath(this.base + route.fullPath))
                handleScroll(this.router, route, fromRoute, false)
                onComplete && onComplete(route)
              }, onAbort)
            }

          在確認路由后執行回調時會通過 updateRoute 觸發 this.$route 的修改

          updateRoute (route: Route) {
            // 更新當前路由信息  對組件的 _route 屬性進行賦值,觸發組件渲染
            const prev=this.current
            this.current=route
            this.cb && this.cb(route)
            this.router.afterHooks.forEach(hook=> {
              hook && hook(route, prev)
            })
          }

          其中 this.cb 在路由初始化過程中 通過history.listen 保存的

          //  VueRouter 路由初始化時設置的 listen 回調
          history.listen(route=> {
            this.apps.forEach((app)=> {
              //  $router 的更新==>> app._route=route則觸發了set,即觸發dep.notify向watcher派發更新
              app._route=route
            })
          })
          
          
          // history 類中 cb的取值
          listen (cb: Function) {
            this.cb=cb
          }

          當組件重新渲染, vue 通過 router-view 渲染到指定位置 綜上所述 路由觸發組件更新依舊是沿用的vue組件的響應式核心, 在執行transitionTo 前手動觸發依賴收集, 在路由transitionTo 過程中手動觸發更新派發以達到watcher的重新update; 而之所以路由能正確的顯示對應的組件,則得益于路由映射表中保存的路由樹形關系 $router.push 切換路由的過程 vue-router 通過 vue.mixin 方法注入 beforeCreate 鉤子,該混合在 beforeCreate 鉤子中通過 Vue.util.defineReactive() 定義了響應式的 _route 。所謂響應式屬性,即當 _route 值改變時,會自動調用 Vue 實例的 render() 方法,更新視圖。 vm.render()是根據當前的_route 的 path,nam 等屬性,來將路由對應的組件渲染到 router-view 中

          1. $router.push() //顯式調用方法
          2. HashHistory.push() //根據hash模式調用, 設置hash并添加到瀏覽器歷史記錄(window.location.hash=XXX)
          3. History.transitionTo() //==>> const route=this.router.match(location, this.current) 找到當前路由對應的組件
          4. History.confirmTransition() // 確認路由,在確認頁面跳轉后 觸發路由守衛,并執行相應回調
          5. History.updateRoute() //更新路由
          6. {app._route=route} // 路由的更改派發更新 觸發頁面的更新
          7. vm.render() // 在 中進行 render 更新視圖
          8. window.location.hash=route.fullpath (瀏覽器地址欄顯示新的路由的path)

          History.replace() 在 hash 模式下

          replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
            const { current: fromRoute }=this
            this.transitionTo(location, route=> {
              replaceHash(route.fullPath)
              handleScroll(this.router, route, fromRoute, false)
              onComplete && onComplete(route)
            }, onAbort)
          }
          
          
          function replaceHash (path) {
            if (supportsPushState) {
              replaceState(getUrl(path))
            } else {
              window.location.replace(getUrl(path))
            }
          }
          
          

          通過 window.location.replace 替換當前路由,這樣不會將新路由添加到瀏覽器訪問歷史的棧頂,而是替換掉當前的路由。

          history模式下

          replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
            const { current: fromRoute }=this
            this.transitionTo(location, route=> {
              replaceState(cleanPath(this.base + route.fullPath))
              handleScroll(this.router, route, fromRoute, false)
              onComplete && onComplete(route)
            }, onAbort)
          }

          監聽地址欄

          在地址欄修改 url 時 vueRouter 會發生什么變化

          當路由采用 hash 模式時,監聽了瀏覽器 hashChange 事件,在路由發生變化后調用 replaceHash()

            //  監聽 hash 的變化
            setupListeners () {
              const router=this.router
              const expectScroll=router.options.scrollBehavior
              const supportsScroll=supportsPushState && expectScroll
          
              if (supportsScroll) {
                setupScroll()
              }
              
              window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', ()=> {
                const current=this.current
                if (!ensureSlash()) {
                  return
                }
                // 傳入當前的 hash 并觸發跳轉
                this.transitionTo(getHash(), route=> {
                  if (supportsScroll) {
                    handleScroll(this.router, route, current, true)
                  }
                  if (!supportsPushState) {
                    replaceHash(route.fullPath)
                  }
                })
              })
            }
          
          

          在路由初始化的時候會添加事件 setupHashListener 來監聽 hashchange 或 popstate;當路由變化時,會觸發對應的 push 或 replace 方法,然后調用 transitionTo 方法里面的 updateRoute 方法來更新 _route,從而觸發 router-view 的變化。 所以在瀏覽器地址欄中直接輸入路由相當于代碼調用了replace()方法,將路由替換成輸入的 url。 在 history 模式下的路由監聽是在構造函數中執行的,對 HTML5History 的 popstate 事件進行監聽

          window.addEventListener('popstate', e=> {
            const current=this.current
            const location=getLocation(this.base)
            if (this.current===START && location===initLocation) {
              return
            }
          
            this.transitionTo(location, route=> {
              if (supportsScroll) {
                handleScroll(router, route, current, true)
              }
            })
          })

          小結 頁面渲染 1、Vue.use(Router) 注冊 2、注冊時調用 install 方法混入生命周期,定義 router 和 route 屬性,注冊 router-view 和 router-link 組件 3、生成 router 實例,根據配置數組(傳入的routes)生成路由配置記錄表,根據不同模式生成監控路由變化的History對象 4、生成 vue 實例,將 router 實例掛載到 vue 實例上面,掛載的時候 router 會執行最開始混入的生命周期函數 5、初始化結束,顯示默認頁面 路由點擊更新 1、 router-link 綁定 click 方法,觸發 history.push 或 history.replace ,從而觸發 history.transitionTo 方法 2、ransitionTo 用于處理路由轉換,其中包含了 updateRoute 用于更新 _route 3、在 beforeCreate 中有劫持 _route 的方法,當 _route 變化后,觸發 router-view 的變化 地址變化路由更新 1、HashHistory 和 HTML5History 會分別監控 hashchange 和 popstate 來對路由變化作對用的處理 2、HashHistory 和 HTML5History 捕獲到變化后會對應執行 push 或 replace 方法,從而調用 transitionTo 3、然后更新 _route 觸發 router-view 的變化 路由相關問題 1. vue-router響應路由參數的變化

          • 通過 watch 監聽 route 對象
          // 監聽當前路由發生變化的時候執行
          watch: {
            $route(to, from){
              console.log(to.path)
              // 對路由變化做出響應
            }
          }
          • 組件中的 beforeRouteUpdate 路由守衛

          在組件被復用的情況下,在同一組件中路由動態傳參的變化 如: 動態參數的路徑 /foo/:id,在 /foo/1 和 /foo/2 之間跳轉的時候,

          beforeRouteUpdate(to, from, next){
            // to do somethings
          }

          2. keep-alive 緩存后獲取數據

          • beforeRouteEnter

          在每次組件渲染時執行 beforeRouterEnter

          beforeRouteEnter(to, from, next){
              next(vm=>{
                  console.log(vm)
                  // 每次進入路由執行
                  vm.getData()  // 獲取數據
              })
          },
          • actived

          在 keep-alive 組件被激活時都會執行 actived 鉤子

          服務器端渲染期間 avtived 不被調用 


          activated(){
          	this.getData() // 獲取數據
          },

          總結 當時在寫這篇文的時候就是想著盡量能把各個知識點都串聯上,建立完善的知識體系 這不寫著寫著就成了長文, 一旦開始就無法停下,那就硬著頭皮繼續吧 不過這篇長文真的是有夠長的,哈哈哈哈,能堅持看到這里的同學我都感到佩服 如果覺得還有哪里缺失的點可以及時告訴我哦 那么今天就先到這啦


          主站蜘蛛池模板: 国产另类ts人妖一区二区三区| 香蕉免费一区二区三区| 亚洲日本一区二区三区| 国产精品区一区二区三在线播放 | 亚洲av永久无码一区二区三区 | 国产免费伦精品一区二区三区| 一区二区三区在线视频播放| 精品一区二区三区在线视频| 精品国产一区二区三区麻豆| 日本丰满少妇一区二区三区| 国产精品视频一区二区三区经| 国模精品一区二区三区| 呦系列视频一区二区三区| 亚洲综合一区二区国产精品| 国产精品毛片a∨一区二区三区| 日本视频一区在线观看免费| 精品一区二区三区在线观看| 国内精自品线一区91| 亚洲午夜一区二区三区| 无码囯产精品一区二区免费| 国产伦精品一区二区三区视频金莲| 国产午夜精品一区二区三区极品| 色噜噜狠狠一区二区三区| 日本不卡一区二区视频a| 久久精品国产一区二区三| 亚洲国产精品一区二区第一页| 波多野结衣免费一区视频| 乱色熟女综合一区二区三区| 亚洲一区二区三区在线播放| 国产一区二区在线|播放| 日韩一区二区免费视频| 精品人妻一区二区三区四区在线| 丰满人妻一区二区三区视频| 日韩制服国产精品一区| 无码精品人妻一区二区三区免费看 | 国内精品一区二区三区最新| 麻豆视频一区二区三区| 亚洲国产综合无码一区| 国产精品亚洲专区一区 | 亚洲一区二区三区免费在线观看| 成人一区二区三区视频在线观看|