整合營銷服務商

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

          免費咨詢熱線:

          一個vue頁面復用方案

          問大家一個問題,曾經的你是否也遇到過,一個項目中有好幾個頁面長得基本相同,但又差那么一點,想用 vue extends 繼承它又不能按需繼承html模板部分,恰好 B 頁面需要用的 A 頁面 80% 的模板,剩下的 20% 由 B 頁面自定義,舉個栗子:

          我們假設這是兩個頁面,B頁面比A頁面多了個p標簽,剩余的東西都一樣,難道僅僅是因為這一個 p標簽就要重新寫一份模板嗎?相信大部分伙伴解決方式是把公共部分抽成一個組件來用,這是一個好的做法。沒錯,但是來了,老板讓你在 標題1、標題2下面分別插入一段內容,這會兒你是不是頭大了?難道只能重寫一份了嗎?當然不是,來開始我們的填坑之路~(當你的業務能用插槽或者組件抽離的方式固然更好,以下內容僅針對當你項目達到一定體量,vue老三套難以處理的情況下采用)

          準備工作

          準備以下工具包:

          • node-html-parser: 將html生成dom樹 官網
          npm install --save node-html-parser
          


          思路

          1. 子頁面提供繼承的父頁面的路徑,如下:
          <template extend="./xxx.vue">
          </template>
          


          1. 子頁面需要通過一個自定義標簽(假設是 extend)的方式,來決定如何拓展父頁面,如下就應該是一個替換的操作,它最少應該具備拓展類型 type 與目標節點 target 屬性。
          <template extend="./xxx.vue">
            <div>
              <extend type="replace" target="#div_1">
                <a>通過replace替換掉父頁面下id為div_1的元素 </a>
              </extend>
            </div>
          </template>
          


          最終它生成的應該是除了 id 為 div_1元素被<a>通過replace替換掉父頁面下id為div_1的元素 </a>替換掉之外,剩下的全部和xxx.vue一樣的頁面。

          梳理需求點

          子頁面繼承父頁面既可以完全繼承,也可以通過某種方式以父頁面為基板,對其進行增、刪、改。方便理解,我們先定義一個自定義標簽 extend,子頁面通過該標簽對其繼承的頁面操刀動手術,為了實現一個比較完善的繼承拓展,extend 標簽需要具備以下屬性:

          Extend Attributes

          參數

          說明

          類型

          可選值

          type

          指定擴展類型

          string

          insert(插入)、replace(替換)、remove(移除)、append(向子集追加)

          position

          指定插入的位置(僅在 type 取值 insert 時生效)

          string

          before(目標前)、after(目標后)

          指定插入的位置(僅在 type 取值 append 時生效,用于指定插入成為第幾個子節點)

          number

          -

          target

          指定擴展的目標

          string


          實現需求

          新建一個vue2的項目,項目結構如下:

          我們的繼承拓展通過自定義loader在編譯的時候實現,進入到src/loader/index.js

          const extend = require('./extend');
          module.exports = function (source) {
               // 當前模塊目錄
               const resourcePath = this.resourcePath;
               // 合并
               const result = new extend(source, resourcePath).mergePage();
               // console.log('result :>> ', result);
               // 返回合并后的內容
               this.callback(null, result);
          };
          


          實現繼承拓展主要邏輯代碼:src/loader/extend.js

          const parser = require('node-html-parser');
          const fs = require('fs');
          const pathFile = require('path');
          /**
           * 通過node-html-parser解析頁面文件重組模板
           * @param {String} source 頁面內容
           * @param {String} resourcePath 頁面目錄
           * @returns {String} 重組后的文件內容
           */
          class Extend {
              constructor(source, resourcePath) {
                  this.source = source;
                  this.resourcePath = resourcePath;
              }
              // 合并頁面
              mergePage() {
                  // 通過node-html-parser解析模板文件
                  const pageAst = parser.parse(this.source).removeWhitespace();
                  // 獲取template標簽extend屬性值
                  const extendPath = pageAst.querySelector('template').getAttribute('extend');
                  if (!extendPath) {
                      return pageAst.toString();
                  }
                  // extendPath文件內容
                  const extendContent = fs.readFileSync(pathFile.resolve(pathFile.dirname(this.resourcePath), extendPath), 'utf-8');
                  // extendContent文件解析
                  const extendAst = parser.parse(extendContent).removeWhitespace();
                  // 獲取頁面文件標簽為extend的元素
                  const extendElements = pageAst.querySelectorAll('extend');
          
                  extendElements.forEach((el) => {
                      // 獲取對應屬性值
                      const type = el.getAttribute('type');
                      const target = el.getAttribute('target');
                      const position = parseInt(el.getAttribute('position'));
          
                      // 匹配模板符合target的元素
                      let templateElements = extendAst.querySelectorAll(target);
          
                      // type屬性為insert
                      if (type === 'insert') {
                          templateElements.forEach((tel) => {
                              // 通過position屬性判斷插入位置 默認為after
                              if (position === 'before') {
                                  el.childNodes.forEach((child) => {
                                      tel.insertAdjacentHTML('beforebegin', child.toString());
                                  });
                              } else {
                                  el.childNodes.forEach((child) => {
                                      tel.insertAdjacentHTML('afterend', child.toString());
                                  });
                              }
                          });
                      }
                      // type屬性為append
                      if (type === 'append') {
                          templateElements.forEach((tel) => {
                             const elNodes = el.childNodes;
                             let tlNodes = tel.childNodes;
                             const len = tlNodes.filter((node) => node.nodeType === 1 || node.nodeType === 3).length;
                              // 未傳position屬性或不為數字、大于len、小于0時默認插入到最后
                              if(isNaN(position) || position > len || position <= 0){
                                  elNodes.forEach((child) => {
                                      tel.insertAdjacentHTML('beforeend', child.toString());
                                  });
                              }else {
                                  tlNodes =  [...tlNodes.slice(0, position-1), ...elNodes, ...tlNodes.slice(position-1)]
                                  tel.set_content(tlNodes);
                              }
                          });
                      }
                      // type屬性為replace
                      if (type === 'replace') {
                          templateElements.forEach((tel) => {
                              tel.replaceWith(...el.childNodes);
                          });
                      }
                      // type屬性為remove
                      if (type === 'remove') {
                          templateElements.forEach((tel) => {
                              tel.remove();
                          });
                      }
                  });
                  // 重組文件內容
                  const template = extendAst.querySelector('template').toString();
                  const script = pageAst.querySelector('script').toString();
                  const style = extendAst.querySelector('style').toString() + pageAst.querySelector('style').toString() 
                  return`${template}${script}${style}`
              }
          
          }
          module.exports = Extend;
          


          好的,自定義loader已經編寫完成,在vue.config.js里面配置好我們的loader

          const { defineConfig } = require('@vue/cli-service')
          module.exports = defineConfig({
            configureWebpack: {
              module: {
                rules: [
                  {
                    test: /\.vue$/,
                    use: [
                      {
                        loader: require.resolve('./src/loader'),
                      },
                    ],
                  },
                ],
              },
            },
          })
          
          


          接下來我們嘗試編寫A頁面和B頁面:

          A.vue:

          <template>
            <div class="template">
                <div id="div_1" class="div">父頁面的div_1</div>
                <div id="div_2" class="div">父頁面的div_2</div>
                <div id="div_3" class="div">父頁面的div_3</div>
                <div id="div_4" class="div">父頁面的div_4</div>
                <div id="div_5" class="div">父頁面的div_5</div>
                <div id="div_6" class="div">父頁面的div_6</div>
                <div id="div_7" class="div">父頁面的div_7</div>
                <div id="div_8" class="div">父頁面的div_8</div>
            </div>
          </template>
          <script>
          export default {
            name: 'COM_A',
            props: {
              msg: String
            }
          }
          </script>
          <style scoped>
          .div {
            color: #42b983;
            font-size: 1.5em;
            margin: 0.5em;
            padding: 0.5em;
            border: 2px solid #42b983; 
            border-radius:  0.2em;
          }
          </style>
          


          B.vue:

          <template extend="./A.vue">
            <div>
              <extend type="insert" target="#div_1" position="after">
                <div id="div_child" class="div">子頁面的div_5</div>
              </extend>
              <extend type="append" target="#div_3" position="2">
                <a> 子頁面通過append插入的超鏈接 </a>
              </extend>
            </div>
          </template>
          <script>
          import A from './A.vue'
          export default {
            name: 'COM_B',
            extends: A,//繼承業務邏輯代碼
            props: {
              msg: String
            }
          }
          </script>
          <style scoped>
          #div_child {
            color: #d68924;
            font-size: 1.5em;
            margin: 0.5em;
            padding: 0.5em;
            border: 2px solid #d68924;
          }
          a {
            color: blue;
            font-size: 0.7em;
          }
          </style>
          


          我們在App.vue下引入B.vue

          <template>
            <div id="app">
              <B/>
            </div>
          </template>
          <script>
          import B from './components/B.vue'
          export default {
            name: 'App',
            components: {
              B
            }
          }
          </script>
          <style>
          #app {
            font-family: Avenir, Helvetica, Arial, sans-serif;
            -webkit-font-smoothing: antialiased;
            -moz-osx-font-smoothing: grayscale;
            text-align: center;
            color: #2c3e50;
            margin-top: 60px;
          }
          </style>
          


          當我們執行編譯的時候,實際上B.vue的編譯結果如下:

          <template>
            <div class="template">
              <div id="div_1" class="div">父頁面的div_1</div>
              <div id="div_child" class="div">子頁面的div_5</div>
              <div id="div_2" class="div">父頁面的div_2</div>
              <div id="div_3" class="div">
                父頁面的div_3
                <a> 子頁面通過append插入的超鏈接 </a>
              </div>
              <div id="div_4" class="div">父頁面的div_4</div>
              <div id="div_5" class="div">父頁面的div_5</div>
              <div id="div_6" class="div">父頁面的div_6</div>
              <div id="div_7" class="div">父頁面的div_7</div>
              <div id="div_8" class="div">父頁面的div_8</div>
            </div>
          </template>
          <script>
          import A from './A.vue'
          export default {
            name: 'COM_B',
            extends: A,//繼承業務邏輯代碼
            props: {
              msg: String
            }
          }
          </script>
          <style scoped>
          .div {
            color: #42b983;
            font-size: 1.5em;
            margin: 0.5em;
            padding: 0.5em;
            border: 2px solid #42b983;
            border-radius: 0.2em;
          }
          </style>
          <style scoped>
          #div_child {
            color: #d68924;
            font-size: 1.5em;
            margin: 0.5em;
            padding: 0.5em;
            border: 2px solid #d68924;
          }
          
          a {
            color: blue;
            font-size: 0.7em;
          }
          </style>
          


          注意我們在B.vue使用了extends繼承了組件A,這里是為了能復用業務邏輯代碼,最后我們運行代碼,頁面輸出為:

          結語

          在真實的項目當中,我們遇到大量重復的頁面但是又有小區別的頁面,是可以通過這種方式減少我們的代碼量,當然也許有更好的辦法,也希望大伙能提出寶貴的建議。

          最后引用一下 @XivLaw 老哥的評論:有很多人說通過cv就能解決,但是當你的業務有成千上萬個頁面是趨同,并且具有相同的基本功能,當界面需要統一調整或者需要進行ui統一管控的時候,cv就成了你的累贅了。 也有朋友說通過組件化和插槽解決,組件化是一個不錯的方案,但是當成千上萬個趨同的界面存在時,插槽并一定能覆蓋所有的業務定制化。 使不使用這種方式,主要看你的業務。

          直白一點說就是:我現在有一千個頁面幾乎一樣,有的頁面是頭部多一點東西,有的是底部,有的是某個按鈕旁邊多一個按鈕,有的是輸入框之間多個輸入框,ui或者界面或者同時需要添加固定功能,需要調整的時候,這一千個頁面要怎么調?


          作者:小小小小_柏
          鏈接:https://juejin.cn/post/7347973138787467274

          目開發中的函數抽離和復用

          在實際的項目開發中,盡可能要做到讓開發者易于理解和后期維護,那么,其中一個最重要的就是必須將重復使用的相同代碼塊或者是差異不明顯的代碼塊抽離出來。在需要使用的地方傳參調用執行。

          這樣做的好處其中一個是,盡可能減少代碼模塊的改動,當項目中有需要拓展或者修改的地方,那就不需要在一個大的函數體里面去修改,而是該對應的模塊,只要確定好輸入的參數和返回值就可以。另一個好處是易于項目的拓展,將通用邏輯抽離出來之后,如果有新增的方法,直接新命名一個新的函數體實現新的邏輯,舊的函數體也可以保留,保證目前線上代碼的兼容性。這就是面向對象編程的開發思想。

          下面是一個簡單的例子:

          這是一段數據上報相關的代碼,在上報之前合并所需參數,之后判斷當前環境再執行上報事件。

          // index.vue
          methods : {
            pageReport(ags) {
              let baseParams = {
                'event_id': this.eventId,
                'event_name': this.eventName,
                'page_id': '123',
              };
              return this.report({ ...baseParams, ...args });
            },
          
            report(args) {
              if (this.isApp) {
                web.Connect.Report({
                  "data": args
                });
              } else {
                web.report(args);
              }
            },
          }

          流程圖為:

          這一段代碼本身是沒有什么問題,但是后來想了一下,如果這個時候做拓展增加上報的場景,比如微信情況下,qq情況下,或者在上報的時候增加了一些其他邏輯,那么,report 這個函數的代碼就會很臃腫,而且也不好理解。所以,可以先把客戶端上報和web上報的函數單獨給抽離出來。

          所以,可以代碼修改成這樣:

          // index.vue
          methods : {
            pageReport(args) {
              let baseParams = {
                'event_id': this.eventId,
                'event_name': this.eventName,
                'page_id': '123',
              };
              return this.reportDistribute({ ...baseParams, ...args });
            },
          
            reportDistribute(args) {
              if (!this.isApp) return this.webReport(args);
              this.appReport(args);
            },
          
            appReport(args) {
              web.Connect.Report({
                "data": args
              });
            },
          
            webReport(ars) {
              web.report(args)
            },
          }

          流程圖為:

          由中間的一個函數做判斷處理,這樣至少可以先保證客戶端上報和web上報這兩個地方不論在邏輯上怎么去改,只要是不涉及到上報本身的邏輯修改,那么就只要修改 reportDistribute 這個函數就可以,可以理解為 appReport 和 webReport 這兩個函數是底層執行代碼,而 reportDistribute 這個就是邏輯層代碼,負責判斷處理調用。

          這樣抽離之后底層負責執行的模塊就會和邏輯層分開,需要拓展的之后只需要增加對應的模塊和定義好需要的數據,使用的時候再去調用既可以了,不會再改到邏輯層上的代碼,同時,任意一個模塊需要修改,那么只要修改對應模塊就可以。

          上面的代碼目前只有兩種情況,客戶端內和Web情況下上報 。雖然是將底層代碼抽離了出來,但如果這個地方如果增加了其他場景的上報比如微信下的,或者是qq下的又或者是微博下的,按照目前的寫法,就是增加 if else,但是這樣并不好理解,代碼的圈復雜度也高,這個時候可以寫一個配置表進行映射 ,代碼如下:

          // index.vue
          methods : {
            appReport(ars) {
              // do something
            },
          
            webReport(ars) {
              // do something
            },
          
            weixinReport(ars) {
              // do something
            },
          
            getReportParams() {
              let isApp = isApp ? '2' : '1';
              if (+isApp === 1 && isWeixin === 1) {
                isApp = 3
              }
              return isApp
            },
          
            reportDistribute(args, type) {
              const config = {
                1: this.webReport,
                2: this.appReport,
                3: this.weixinReport
              }
              config[type](args);
            },
          
            reportInit(args) {
              return this.reportDistribute(args, this.getReportParams());
            },
          
            pageReport(args) {
              let baseParams = {
                'event_id': this.eventId,
                'event_name': this.eventName,
                'page_id': '123',
              };
              return this.reportInit({ ...baseParams, ...args });
            },
          }

          流程圖為:

          這樣不管再新增多少情況的上報,只要在 reportDistribute 里面增加一個配置,在 getReportParams 中多增加一個返回值,就可以完成配置,整一段代碼清晰可維護。

          接著,這一段代碼負責底層邏輯的一些映射相關的配置,這些一般改動的是比較少的,那么可以在把這一些抽離出來單獨放在一個 config 文件里面,而不是全都擠在一個 vue 頁面的 methods 中。

          首先,將映射配置相關和負責底層執行的函數抽離出來,放在一個page-config.js 文件中,再將接口export 出去。 代碼如下:

          第一個文件 page-config.js ,定義基本配置和映射表。

          // page-config.js 
          
          function appReport(ars) {
            alert(JSON.stringify(ars));
          }
          
          function webReport(ars) {
            alert(JSON.stringify(ars));
          }
          
          function weixinReport(ars) {
            alert(JSON.stringify(ars));
          }
          
          const baseParams = {
            eventId: '1',
            eventName: 'test-page',
            pageId: 'index',
          };
          
          const pageConfig = {
            appReport: appReport,
            webReport: webReport,
            weixinReport: weixinReport,
          };
          export { pageConfig, baseParams };

          之后如果需要做拓展,直接在這個配置文件里面加內容即可。

          再是第二個文件 index.vue 文件

          // index.vue 
          import { pageConfig, baseParams } from './page-config';
          export default {
            methods: {
              getReportParams() {
                let isApp = this.isApp() ? 'appReport' : 'webReport';
                if (+isApp === 'webReport' && this.isWeixin === true) {
                  isApp = 'weixinReport'
                }
                return isApp
              },
          
              // add function
              pageGo(args) {
                return this.eventDistribute('link', 'https://www.google.com');
              },
          
              eventDistribute(type, args) {
                return pageConfig[type](args);
              },
          
              pageReport(args) {
                return this.eventDistribute(this.getReportParams(), { ...baseParams, ...args });
              },
            }
          }

          函數抽離之后,這里以后的拓展需要改到的只有 getReportParams 函數,這里面可能會返回新的數值。

          這里注意一點是,之前的 reportDistribute 函數改成了 eventDistribute,這樣做的目的是更加的通用,因為一個項目不單單是只有上報事件,肯定還有其他功能邏輯,例如這時候需要再加一個網頁跳轉的事件,那么就可以向上面的代碼一樣 ,增加一個 pageGo調用 eventDistribute 。

          eventDistribute 函數通用第一個參數區分執行的函數類型和第二個參數進行傳參執行對應函數。

          整理的流程圖改為:

          接下來,也可以再對 page-config 做一點優化,因為,上面舉例的事件類型也只有兩大類一個是頁面跳轉,另一個是上報事件。那么,要考慮再多其他類型的情況,也不太好記的請。所以,可以對底層的映射配置再做一點優化,比如,通過參數的名稱進行對應的函數執行,代碼如下:

          methods : {
            getReportParams() {
              let isApp = this.isApp ? "report.appReport" : "report.webReport";
              if (isApp === "report.webReport" && this.isWeixin) {
                isApp = "report.weixinReport";
              }
              return isApp;
            },
          
            // add function
            pageGo(args) {
              return this.eventDistribute("go.link", "https://www.google.com");
            },
          
            eventDistribute(type, args) {
              return pageConfig(type, args);
            },
          
            pageReport(args) {
              return this.eventDistribute(this.getReportParams(), {
                ...baseParams,
                ...args,
              });
            },
          },

          把事件類型分類,再使用小數點隔開 report.appReport

          // page-config.js 
          function appReport(ars) {
            alert(JSON.stringify(ars));
          }
          
          function webReport(ars) {
            alert(JSON.stringify(ars));
          }
          
          function weixinReport(ars) {
            alert(JSON.stringify(ars));
          }
          
          function link(url) {
            window.location.href = url;
          }
          
          const baseParams = {
            eventId: '1',
            eventName: 'test-page',
            pageId: 'index',
          };
          
          const pageEvent = (name, args) => {
            eval(name + "(" + JSON.stringify(args) + ")");
          };
          
          const pageFunConfig = {
            'report': pageEvent,
            'go': pageEvent
          };
          
          const pageConfig = (type, args) => {
            const eventType = type.split('.')[0];
            const eventName = type.split('.')[1];
            pageFunConfig[eventType](eventName, args);
          }
          
          export { pageConfig, baseParams };

          切割出函數名稱之后執行函數。流程圖如下:

          上面的代碼只是利用了上報這個行為做了一個例子,并不是說一定就是要這樣寫,更多的是一種將代碼抽離達到多次服用和容易維護的目的。

          如果小伙伴有更好的方法和建議,麻煩請在我的公眾號留言,謝謝!

          -----

          利寶閣確實有點東西。


          歡迎關注各位小伙伴我的公眾號 @GavinUI

          中1, 2, 3, 4 表示優先級

          CSS 選擇器及其優先級

          選擇器

          格式

          優先級權重

          !important

          10000


          內聯樣式

          1000


          id 選擇器

          #id

          100

          類選擇器

          #classname

          10

          屬性選擇器

          a[ref=“eee”]

          10

          偽類選擇器

          li:last-child

          10

          標簽選擇器

          div

          1

          偽元素選擇器

          li::after

          1

          相鄰兄弟選擇器

          h1+p

          0

          子選擇器

          ul>li

          0

          后代選擇器

          li a

          0

          通配符選擇器

          *

          0

          注意事項:

          • !important 聲明的樣式的優先級最高;
          • 如果優先級相同,則最后出現的樣式生效;
          • 繼承得到的樣式的優先級最低;
          • 通用選擇器(*)、子選擇器(>)和相鄰同胞選擇器(+)并不在這四個等級中,它們的權值都為 0 ;
          • 樣式表的來源不同時,優先級順序為:內聯樣式 > 內部樣式 > 外部樣式 > 瀏覽器用戶自定義樣式 > 瀏覽器默認樣式。

          CSS 中可繼承與不可繼承屬性有哪些

          可繼承:

          字體系列 font-family font-weight font-size

          文本系列 color text-align line-height

          可見系列 如 visibility

          由于屬性太多,這里只列舉常見的可繼承的屬性

          display 的屬性值及其作用

          屬性值

          作用

          none

          元素不顯示,并且會從文檔流中移除。

          block

          塊類型。默認寬度為父元素寬度,可設置寬高,換行顯示。

          inline

          行內元素類型。默認寬度為內容寬度,不可設置寬高,同行顯示。

          inline-block

          默認寬度為內容寬度,可以設置寬高,同行顯示。

          list-item

          像塊類型元素一樣顯示,并添加樣式列表標記。

          table

          此元素會作為塊級表格來顯示。

          inherit

          規定應該從父元素繼承 display 屬性的值。

          display 的 block、inline 和 inline-block 的區別

          block:


          主站蜘蛛池模板: 日韩AV无码一区二区三区不卡毛片| 午夜无码视频一区二区三区| 无码人妻一区二区三区免费看 | 亚洲综合色一区二区三区小说| 久久久久国产一区二区| 成人免费视频一区二区三区 | 中文字幕无码免费久久9一区9| 亚洲国产视频一区| 亚洲综合色一区二区三区| 亚洲AV无码一区二区三区在线观看| 国产午夜精品一区二区三区嫩草| 亚洲一区二区三区高清视频| 国产一区二区三区视频在线观看| 亚洲AV日韩AV天堂一区二区三区 | 任你躁国语自产一区在| 狠狠综合久久AV一区二区三区| 国产A∨国片精品一区二区| 91午夜精品亚洲一区二区三区 | 日韩一区二区a片免费观看| 日韩一区二区在线观看视频| 一区二区三区四区免费视频| 亚洲av午夜福利精品一区| 无码少妇一区二区性色AV| 国产婷婷色一区二区三区深爱网| 亚洲AV无码一区二区三区系列| 狠狠色婷婷久久一区二区三区 | 亚洲AV日韩综合一区| 老熟女高潮一区二区三区| 精品无码人妻一区二区三区品| 中文字幕一区二区视频| 国产亚洲情侣一区二区无码AV| 国产成人精品一区二三区在线观看| 福利片免费一区二区三区| 无码精品久久一区二区三区 | 无码精品人妻一区二区三区AV| 日本一区二区三区在线视频| 国产精品一区二区三区99| 日本一区二区免费看| ...91久久精品一区二区三区| 中文字幕一区二区三匹| 精品国产亚洲一区二区三区在线观看 |