你可能不知道的動態組件玩法🍉

○ 背景

知道的大佬請輕錘😂。javascript

這篇是做者在公司作了活動架構升級後,產出的主文的前導篇,考慮到本文相對獨立,所以抽離出單獨成文。css

題目爲動態組件,但爲了好理解能夠叫作遠程加載動態組件,後面統一簡化稱爲「遠程組件」。html

具體是怎麼玩呢?彆着急,聽我慢慢道來,看完後會感慨Vue組件還能這麼玩🐶,還會學會一個Stylelint插件,配有DEMO,以及隱藏在最後的彩蛋。前端

做者曾所在我司廣告事業部,廣告承載方式是以刮刮卡、大轉盤等活動頁進行展現,而後用戶參與出廣告券彈層。vue

旁白說:遠程組件其實在可視化低代碼平臺也有相似應用,而咱們這裏也是利用了相似思路實現解耦了活動頁和券彈層。繼續主題...java

image.png

遺留系統早先版本是一個活動就綁定一個彈層,1對1的綁定關係。node

image.png

如今的場景是一個活動可能出不一樣樣式的彈層,這得把綁定關係解除。咱們須要多對多,就是一個活動頁面能夠對應多個廣告券彈層,也能夠一個廣告券彈層對應多個活動頁面。webpack

咱們能夠在本地預先寫好幾個彈層,根據條件選擇不一樣的彈層,能夠知足一個活動對多個彈層。git

而咱們的需求是讓活動頁面對應無數種彈層,而不是多種,因此不可能把全部彈層都寫在本地。所以怎麼辦呢?github

image.png

所以咱們要根據所需,而後經過判斷所需的彈層,遠端返回對應的代碼。其實就是咱們主題要講到的遠程組件

講得容易,該怎麼作呢?

○ 遠程組件核心

Pure版本

若是是Pure JS、CSS組成的彈層,很天然的咱們想到,經過動態的插入JS腳本和CSS,就能組成一個彈層。所以把編譯好的JS、CSS文件能夠存放在遠端CDN。

image.png

看上圖,咱們能夠看到彈窗出來以前,瀏覽器把CSS、JS下載下來了,而後根據既定代碼拼裝成一個彈層。

// CSS插入
<link rel="stylesheet" href="//yun.xxx.com/xxx.css">

// JS的動態插入

<script type="text/javascript"> var oHead = document.querySelector('.modal-group'); var oScript = document.createElement('script'); oScript.type = "text/javascript"; oScript.src = "//yun.xxx.com/xxx.js"; oHead.appendChild(oScript); </script>
複製代碼

經過上面可知,JS、CSS方式能實現Pure版本的遠程組件,而在Vue環境下能實現嗎。若是按照Pure JS、CSS動態插入到Vue活動下,也是能夠很粗糙的實現的。

但有沒有更優雅的方式呢?

image.png

Vue版本

選型這篇不細討論了,後續的主篇會講爲何選擇Vue。

上述是遺留系統的方式,若是咱們要技術棧遷移到Vue,也須要對遠程組件遷移,咱們須要改造它。

讓咱們來回顧下Vue的一些概念。

組件形式

「對象組件」

一個彈窗,其實咱們能夠經過一個Vue組件表示,咱們想把這個組件放到CDN,直接下載這個文件,而後在瀏覽器環境運行它可行嗎?咱們來嘗試下。

基於Vue官方文檔,咱們能夠把以下的選項對象傳入Vue,經過new Vue來建立一個組件。

{
  mounted: () => {
   console.log('加載')
  },
  template: "<div v-bind:style=\"{ color: 'red', fontSize: '12' + 'px' }\">Home component</div>"
}
複製代碼

藉助於包含編譯器的運行時版本,咱們能夠處理字符串形式的Template。

-- 運行時-編譯器-vs-只包含運行時

若是你須要在客戶端編譯模板 (好比傳入一個字符串給Template選項,或掛載到一個元素上並以其 DOM 內部的 HTML 做爲模板),就將須要加上編譯器,即完整版

彷佛找到了新世界的大門。

image.png

咱們確實是能夠經過這種形式實現Template、Script、CSS了,但對於開發同窗,字符串形式的Template、內嵌的CSS,開發體驗不友好。

image.png

「單文件組件」

這個時候很天然地想到SFC - 單文件組件。

文件擴展名爲.vue的**single-file components (單文件組件)**爲以上全部問題提供瞭解決方法 -- Vue文檔。 image.png

但怎麼樣才能讓一個.vue組件從遠端下載下來,而後在當前活動Vue環境下運行呢?這是個問題,因爲.vue文件瀏覽器是識別不了的,但.js文件是能夠的。

咱們先想一下,.vue文件是最終被轉換成了什麼?

image.png (圖片來源:1.03-vue文件的轉換 - 簡書

經過轉換,實際變成了一個JS對象。因此怎麼才能把.vue轉換成.js呢?

有兩種方式,一種經過運行時轉換,咱們找到了http-vue-loader。經過Ajax獲取內容,解析Template、CSS、Script,輸出一個JS對象。

image.png

而考慮到性能和兼容性,咱們選擇預編譯,經過CSS預處理器、HTML模版預編譯器。

Vue的官方提供了vue-loader,它會解析文件,提取每一個語言塊,若有必要會經過其它 loader 處理,最後將他們組裝成一個 ES Module,它的默認導出是一個 Vue.js 組件選項的對象。這指的是什麼意思呢?官方提供選項對象形式的組件DEMO。

有了理論支持,如今須要考慮下實踐啦,用什麼編譯?

image.png

怎麼構建

因爲webpack編譯後會帶了不少關於模塊化相關的無用代碼,因此通常小型的庫會選擇rollup,這裏咱們也選擇rollup。

// rollup.config.js
import vue from 'rollup-plugin-vue'
import commonjs from 'rollup-plugin-commonjs'

export default {
  input: './skin/SkinDemo.vue',
  output: {
    format: 'iife',
    file: './dist/rollup.js',
    name: 'MyComponent'
  },
  plugins: [
    commonjs(),
    vue()
  ]
}
複製代碼

經過rollup-plugin-vue,咱們能夠把.vue文件轉成.js, rollup編譯輸出的iife形式js。

image.png

能夠看到script、style、template分別被處理成對應的片斷,經過整合計算,這些片斷會生成一個JS對象,保存爲.js文件。下圖就是一個組件選項的對象。

image.png

能夠經過項目:github.com/fly0o0/remo…,嘗試下rollup文件夾下的構建,具體看README說明。

咱們已經有了一個 Vue.js 組件選項的對象,怎麼去讓它掛載到對應的Vue App上呢?

image.png

掛載方式

回想以前通讀Vue入門文檔,遇到一個動態組件的概念,但當時並不太理解它的使用場景。 image.png

動態組件是能夠不固定具體的組件,根據規則替換不一樣的組件。從文檔上看出,支持一個組件的選項對象。

最終實現

首先須要構建.vue文件,而後經過Ajax或動態Script去加載遠端JS。因爲Ajax會有跨域限制,因此這裏咱們選擇動態Script形式去加載。

而咱們剛纔使用Rollup導出的方式是把內容掛載在一個全局變量上。那就知道了,經過動態Script插入後,就有一個全局變量MyComponent,把它掛載在動態組件,最終就能把組件顯示在頁面上了。

具體怎麼操做?欠缺哪些步驟,首先咱們須要一個加載遠程.js組件的函數。

// 加載遠程組件js

function cleanup(script){
  if (script.parentNode) script.parentNode.removeChild(script)
  script.onload = null
  script.onerror = null
  script = null
}

function scriptLoad(url) {
  const target = document.getElementsByTagName('script')[0] || document.head

  let script = document.createElement('script')
  script.src = url
  target.parentNode.insertBefore(script, target)

  return new Promise((resolve, reject) => {
    script.onload = function () {
      resolve()
      cleanup(script)
    }
    script.onerror = function () {
      reject(new Error('script load failed'))
      cleanup(script)
    }
  })
}

export default scriptLoad
複製代碼

而後把加載下來的組件,掛載在對應的動態組件上。

<!-- 掛載遠程組件 -->

<template>
  <component
    class="remote-test"
    :is="mode">
  </component>
</template>

<script>
import scriptLoad from "./scriptLoad"

export default {
  name: "Remote",
  data() {
    return {
      mode: "",
    };
  },
  mounted() {
    this.mountCom(this.url)
  },
  methods: {
    async mountCom(url) {
      // 下載遠程js
      await scriptLoad(url)

      // 掛載在mode
      this.mode = window.MyComponent

      // 清除MyComponent
      window.MyComponent = null
    },
  }
}
</script>
複製代碼

基本一個Vue的遠程組件就實現了,但發現還存在一個問題。

image.png

全局變量MyComponent須要約定好,但要實現比較好的開發體驗來講,應該儘可能減小約定。

導出方式

怎麼解決呢?因爲咱們導出是使用的IIFE方式,其實Rollup還支持UMD方式,包含了Common JS和AMD兩種方式。

咱們經過配置Rollup支持UMD。

// rollup.config.js
import vue from 'rollup-plugin-vue'
import commonjs from 'rollup-plugin-commonjs'

export default {
  input: './skin/SkinDemo.vue',
  output: {
    format: 'umd',
    file: './dist/rollup.js',
    name: 'MyComponent'
  },
  plugins: [
    commonjs(),
    vue()
  ]
}
複製代碼

能夠看到構建完畢後,支持三種方式導出。 image.png

咱們能夠模擬node環境,命名全局變量exports、module,就能夠在module.exports變量上拿到導出的組件。

image.png

具體實現核心代碼以下。

<!-- 掛載遠程組件 -->

<template>
  <component
    class="remote-test"
    :is="mode">
  </component>
</template>

<script>
import scriptLoad from "./scriptLoad"

export default {
  name: "Remote",
  data() {
    return {
      mode: "",
    };
  },
  mounted() {
    this.mountCom(this.url)
  },
  methods: {
    async mountCom(url) {
      // 模擬node環境
      window.module = {}
      window.exports = {}

      // 下載遠程js
      await scriptLoad(url)

      // 掛載在mode
      this.mode = window.module.exports

      // 清除
      delete window.module
      delete window.exports
    },
  }
}
</script>
複製代碼

終於搞定了Vue版本的遠程組件加載的方式。

image.png

接下來得想想,怎麼處理遠程組件(彈層)的設計了。

小結

經過使用Vue動態組件實現了遠程組件功能,取代了老架構。image.png

能夠經過如下地址去嘗試一下遠程組件彈層,按照項目的README操做一下。會獲得如下遠程組件彈層。

項目地址:github.com/fly0o0/remo…

image.png

○ 遠程組件(彈層)設計

遠程組件已達成,這部分主要是對遠程彈層組件的一些設計。

對於遠程單組件自己來講,只須要根據數據渲染視圖,根據用戶行爲觸發業務邏輯,整個代碼邏輯是這樣的。

須要考慮組件複用、組件通信、組件封裝、樣式層級等方向。

首先咱們先看看組件複用。

爲了方便統一管理和減小冗餘代碼,咱們通常寫一些相似的組件會抽取一部分能夠公共的組件,例如按鈕等。

但遠程單組件代碼和頁面端代碼是分離的啊(能夠理解爲兩個webpack入口打包出的產物),咱們得想一想公共組件須要放在哪裏了。

image.png

組件複用

如今能夠發現有三種狀況,咱們利用枚舉法嘗試想一遍。

打包 📦

公共組件和遠程組件打包一塊兒

放在一塊兒確定不合適,不只會引發遠程組件變大,還不能讓其餘遠程組件複用。往下考慮再看看。 ​

image.png

公共組件單獨打包

遠程組件、公共組件分別單獨打包,這樣也是不利的,因爲遠程組件抽離的公共組件少於5個,並且代碼量較少,單獨做爲一層打包,會多一個後置請求,影響遠程組件的第一時間展現。 ​

繼續考慮再看看。 image.png 公共組件和頁面核心庫打包一塊兒

把公共組件和頁面核心庫打包到一塊兒,避免後面遠程組件用到時候再加載,能夠提高遠程組件的展現速度。 image.png

所以最終敲定選擇最後種,把公共組件和頁面核心庫打包在一塊兒。

若是把遠程組件.js和公共組件分開了,那咱們該怎麼才能使用公共組件啊?😂

image.png

註冊 🔑

回顧下Vue官方文檔,Vue.component它能夠提供註冊組件的能力,而後在全局能引用到。咱們來試試吧。

公共組件例如按鈕、關閉等,須要經過如下途徑去註冊。

一個按鈕組件

// 本地頁面端(本地是相較於在遠端CDN)

<!-- 按鈕組件 -->
<template>
  <button type="button" class="btn" @click="use">
  </button>
</template>

<script>
export default {
  name: 'Button',
  inject: ['couponUseCallback'],
  methods: {
    use() {
      this.couponUseCallback && this.couponUseCallback()
    }
  }
}
</script>
複製代碼

一個關閉組件

// 本地頁面端(本地是相較於在遠端CDN)

<!-- 關閉組件 -->
<template>
  <span @click="close" class="close"></span>
</template>

<script>
export default {
  name: "CouponClose",
  inject: ["couponCloseCallback"],
  methods: {
    close() {
      this.couponCloseCallback && this.couponCloseCallback();
    },
  },
};
</script>

<style lang="less" scoped>
.close {
  &.gg {
    background-image: url("//yun.tuisnake.com/h5-mami/dist/close-gg.png") !important;
    background-size: 100% !important;
    width: 92px !important;
    height: 60px !important;
  }
}
</style>
複製代碼

經過Vue.component全局註冊公共組件,這樣在遠程組件中咱們就能夠直接調用了。

// 本地頁面端(本地是相較於在遠端CDN)

<script>
  Vue.component("CpButton", Button);
  Vue.component("CpClose", Close);
</script>
複製代碼

解決了公共組件複用的問題,後面須要考慮下遠程組件和頁面容器,還有不一樣類型的遠程組件之間的通信問題。

image.png

組件通信

能夠把頁面容器理解爲父親、遠程組件理解爲兒子,二者存在父子組件跨級雙向通信,這裏的父子也包含了爺孫和爺爺孫的狀況,所以非props能夠支持。那怎麼處理?

能夠經過在頁面核心庫中向遠程組件 provide 自身,遠程組件中 inject 活動實例,實現事件的觸發及回調。

那不一樣類型的遠程組件之間怎麼辦呢,使用Event Bus,能夠利用頂層頁面實例做爲事件中心,利用 on 和 emit 進行溝通,下降不一樣類別遠程組件之間的耦合度。

image.png

組件封裝

如今有個組件封裝的問題,先看個例子,基本就大概有了解了。

現有3個嵌套組件,以下圖。如今須要從頂層組件Main.vue給底層組件RealComponent的一個count賦值,而後監聽RealComponent的input組件的事件,若是有改變通知Main.vue裏的方法。怎麼作呢?

image.png

跨層級通訊,有多少種方案能夠選擇?

  1. 咱們使用vuex來進行數據管理,對於這個需求太重。
  2. 自定義vue bus事件總線(如上面提到的),無明顯依賴關係的消息傳遞,若是傳遞組件所需的props不太合適。
  3. 經過props一層一層傳遞,但須要傳遞的事件和屬性較多,增長維護成本。

而還有一種方式能夠經過 a t t r s attrs和 listeners,實現跨層級屬性和事件「透傳」。

主組件

// Main.vue

<template>
<div> <h2>組件Main 數據項:{{count}}</h2> <ComponentWrapper @changeCount="changeCount" :count="count"> </ComponentWrapper> </div>
</template>
<script> import ComponentWrapper from "./ComponentWrapper"; export default { data() { return { count: 100 }; }, components: { ComponentWrapper }, methods: { changeCount(val) { console.log('Top count', val) this.count = val; } } }; </script>
複製代碼

包裝用的組件

有的時候咱們爲了對真實組件進行一些功能增長,這時候就須要用到包裝組件(特別是對第三方組件庫進行封裝的時候)。

// ComponentWrapper.vue

<template>
  <div> <h3>組件包裹層</h3> <RealComponent v-bind="$attrs" v-on="$listeners"></RealComponent> </div>
</template>
<script> import RealComponent from "./RealComponent"; export default { inheritAttrs: false, // 默認就是true components: { RealComponent } }; </script>
複製代碼

真正的組件

// RealComponent.vue

<template>
  <div> <h3>真實組件</h3> <input v-model="myCount" @input="inputHanlder" /> </div>
</template>
<script> export default { data() { return { myCount: 0 } }, created() { this.myCount = this.$attrs.count; // 在組件Main中傳遞過來的屬性 console.info(this.$attrs, this.$listeners); }, methods: { inputHanlder() { console.log('Bottom count', this.myCount) this.$emit("changeCount", this.myCount); // 在組件Main中傳遞過來的事件,經過emit調用頂層的事件 // this.$listeners.changeCount(this.myCount) // 或者經過回調的方式 } } }; </script>
複製代碼

從例子中迴歸本文裏來,咱們要面對的場景是以下這樣。

遠程組件其實有兩層,一層是本地(頁面內),一層是遠端(CDN)。本地這層只是作封裝用的,能夠理解爲只是包裝了一層,沒有實際功能。這時候能夠理解爲本地這一層組件就是包裝層,包裝層主要作了導入遠程組件的功能沒辦法去除,須要利用上面的特性去傳遞信息給遠程組件。

樣式層級

遠程組件在本文能夠簡單理解爲遠端的彈層組件,公司業務又涉及到不一樣的彈層類別,每種彈層類別可能會重疊。

約定z-index

所以劃分 0~90 爲劃分十層,後續可根據實際狀況增長數值,設定各遠程組件容器只能在規定層級內指定 z-index。

// const.js
const FLOOR = {
  MAIN: 0,   // 主頁面容器
  COUPON_MODAL: 20,  // 廣告彈層
  OTHER_MODAL: 30, // 其餘彈層
  ERROR_MODAL: 90,
  ...
}
複製代碼

設置每種遠程組件即彈層的包裹層。

// CouponModalWrapper.vue
<script>
<template> <div class="modal-wrapper" :style="{'z-index': FLOOR.COUPON_MODAL}" @touchmove.prevent> <slot></slot> </div> </template>

// OtherModalWrapper.vue
<template> <div class="modal-wrapper" :style="{'z-index': FLOOR.OTHER_MODAL}" @touchmove.prevent> <slot></slot> </div> </template>

// 這裏只是爲了表意簡單,實際上兩個Wrapper.vue能夠合併
複製代碼

而後每類別各自引入對應的彈層包裹層。

// 每類別公共組件有一個

// CouponModal2.vue 
<template>
  <CouponModalWrapper> ... </CouponModalWrapper>
</template>
  
// OtherModal2.vue 
<template> <OtherModalWrapper> ... </OtherModalWrapper> </template>
複製代碼

經過這種約定的方式,能夠避免一些問題,但假如真的有人想搗亂怎麼辦?

image.png

彆着急,有辦法的。

藉助stylelint

思路是這樣的,每類別的遠程組件是單獨有對應的主文件夾,能夠爲這個文件夾定義最高和最小可容許的z-index,那該怎麼作呢?

不知道你們有使用過自動加-webkit等前綴的插件 - autoprefixer沒有,它實際上是基於一款postcss工具作的。而咱們常常用做css校驗格式的工具stylelint也是基於它開發的。

這時候咱們想到,能不能經過stylelint的能力,進行約束呢,咱們發現找了官方文檔並無咱們想要的API。

咱們須要本身開發一個stylelint插件,來看看一個基本的stylelint插件的插件。

image.png

stylelint經過stylelint.createPlugin方法,接受一個函數,返回一個函數。

const stylelint = require('stylelint');
const ruleName = 'plugin/z-index-range-plugin';

function rule(options) {
  // options傳入的配置
  return (cssRoot, result) => {
    // cssRoot即爲postcss對象
   };
}

module.exports = stylelint.createPlugin(
  ruleName,
  rule
);
複製代碼

函數中能夠拿到PostCSS對象,能夠利用PostCSS對代碼進行解析成AST、遍歷、修改、AST變代碼等操做。

有一些咱們可用的概念。 ​

  • rule,選擇器,好比.class { z-index: 99 }。
  • decl,屬性,好比z-index: 99。

咱們須要檢查z-index的值,所以須要遍歷CSS檢查z-index。咱們能夠調用cssRoot.walkDecls對作遍歷:

// 遍歷
cssRoot.walkDecls((decl) => {
  // 獲取屬性定義
  if (decl) { 
    // ... 
  } 
});
複製代碼

前置基礎知識差很少夠用了。

image.png

假如咱們要檢測一個兩個文件夾下的.css文件的z-index是否合乎規矩。

咱們設置好兩個模塊stylelint配置文件下的z-index範圍。

這裏咱們能夠看到stylelint配置文件,兩個css文件。

├── .stylelintrc.js
├── module1
│   └── index.css
├── module2
│   └── index2.css
複製代碼

stylelint配置文件

// .stylelintrc.js
module.exports = {
  "extends": "stylelint-config-standard",
  // 自定義插件
  "plugins": ["./plugin.js"],
  "rules": {
    // 自定義插件的規則
    "plugin/z-index-range-plugin": {
      // 設置的範圍,保證各模塊不重複
      "module1": [100, 199],
      "module2": [200, 299]
    }
  }
}
複製代碼

CSS測試文件

/* module1/index.css */
.classA {
  color: red;
  width: 99px;
  height: 100px;
  z-index: 99;
}

/* module2/index.css */
.classB {
    color: red;
    width: 99px;
    height: 100px;
    z-index: 200;
}

複製代碼

咱們要達到的目的是,運行以下命令,會讓module1/index.css報錯,說z-index小於預期。

npx stylelint "*/index.css"
複製代碼

因而乎咱們完成了以下代碼,達成了預期目的。

const stylelint = require('stylelint');
const ruleName = 'plugin/z-index-range-plugin';

function ruleFn(options) {
  return function (cssRoot, result) {

    cssRoot.walkDecls('z-index', function (decl) {
      // 遍歷路徑
      const path = decl.source.input.file
      // 提取文件路徑裏的模塊信息
      const match = path.match(/module\d/)
      // 獲取文件夾
      const folder = match?.[0]
      // 獲取z-index的值
      const value = Number(decl.value);
      // 獲取設定的最大值、最小值
      const params = {
        min: options?.[folder]?.[0],
        max: options?.[folder]?.[1],
      }

      if (params.max && Math.abs(value) > params.max) {
        // 調用 stylelint 提供的report方法給出報錯提示
        stylelint.utils.report({
          ruleName,
          result,
          node: decl,
          message: `Expected z-index to have maximum value of ${params.max}.`
        });
      }

      if (params.min && Math.abs(value) < params.min) {
        // 調用 stylelint 提供的report方法給出報錯提示
        stylelint.utils.report({
          ruleName,
          result,
          node: decl,
          message: `Expected z-index to have minimum value of ${params.min}.`
        });
      }
    });
  };
}

module.exports = stylelint.createPlugin(
  ruleName,
  ruleFn
);

module.exports.ruleName = ruleName;
複製代碼

能夠嘗試項目:github.com/fly0o0/styl…,試一試感覺一下🐶。

這樣基本一個遠程彈層的設計就完成了。

但仍是遇到了些問題,艱難😂。

image.png

○ 遇到的問題

咱們興沖沖的打算髮上線了,結果報錯了🐶。報的錯是webpackJsonp不是一個function。

不要慌,先吃個瓜鎮靜鎮靜。webpackJsonp是作什麼的呢?

異步加載的例子

先看下如下例子,經過import的按需異步加載特性加載了test.js,如下例子基於Webpack3構建。

// 異步加載 test.js
import('./test').then((say) => {
  say();
});
複製代碼

而後生成了異步加載文件 0.bundle.js。

// 異步加載的文件,0.bundle.js
webpackJsonp(
  // 在其它文件中存放着的模塊的 ID
  [0],
  // 本文件所包含的模塊
  [
    // test.js 所對應的模塊
    (function (module, exports) {
      function ;(content) {
        console.log('i am test')
      }

      module.exports = say;
    })
  ]
);
複製代碼

和執行入口文件 bundle.js。

// 執行入口文件,bundle.js
(function (modules) {
  /*** * webpackJsonp 用於從異步加載的文件中安裝模塊。 * */
  window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
    var moduleId, chunkId, i = 0, resolves = [], result;
    for (; i < chunkIds.length; i++) {
      chunkId = chunkIds[i];
      if (installedChunks[chunkId]) {
        resolves.push(installedChunks[chunkId][0]);
      }
      installedChunks[chunkId] = 0;
    }
    for (moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId];
      }
    }
    while (resolves.length) {
      resolves.shift()();
    }
  };

  // 模擬 require 語句
  function __webpack_require__(moduleId) {
  }

  /** * 用於加載被分割出去的,須要異步加載的 Chunk 對應的文件 */
  __webpack_require__.e = function requireEnsure(chunkId) {
    // ... 省略代碼
    return promise;
  };

  return __webpack_require__(__webpack_require__.s = 0);
})
(
  [
    // main.js 對應的模塊
    (function (module, exports, __webpack_require__) {
      // 經過 __webpack_require__.e 去異步加載 show.js 對應的 Chunk
      __webpack_require__.e(0).then(__webpack_require__.bind(null, 1)).then((show) => {
        // 執行 show 函數
        show('Webpack');
      });
    })
  ]
);
複製代碼

能夠看出webpackJsonp的做用是加載異步模塊文件。但爲何會報webpackJsonp不是一個函數呢?

開始排查問題

咱們開始檢查構建出的源碼,發現咱們的webpackJsonp並非一個函數,而是一個數組(現已知Webpack4,當時排查時候不知道)。 ​

咱們發現異步文件加載的時候確實是變成了數組,經過push去增長一個異步模塊到系統裏。

// 異步加載的文件

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[/* chunk id */ 0], {
  "./src/async.js": (function(module, __webpack_exports__, __webpack_require__) {

	//...

}))
複製代碼

且在執行入口文件也發現了webpackJsonp被定義爲了數組。

// 執行入口文件,bundle.js中的核心代碼 

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
複製代碼

確實咱們構建出的源碼的webpackJsonp是一個數組,確實不是一個函數了,感受找到了一點線索。但爲何會webpackJsonp會函數形式去使用呢? ​

咱們懷疑報錯處有問題,開始排查報錯處,發現對應的文件確實是用webpackJsonp看成函數去調用的,這是什麼狀況?🤔️ ​

這時咱們注意到報錯的都是老架構下的遠程組件,是否是在老架構的項目裏會有什麼蛛絲馬跡? ​

咱們開始探索老架構,這時候發現老架構是使用的webpack3,而咱們新架構是使用webpack4構建的。難道是這裏出了問題?💡 ​

因而咱們用webpack3從新構建了下老架構的遠程組件,發現webpackJsonp對應的確實是函數,如上一節「異步加載的例子」裏所示。

因此定位到了緣由,webpack4和webpack3分別構建了新老兩種的異步遠程組件,webpackJsonp在版本4下是數組,而在版本3下面是函數。 ​

image.png

細心的同窗可能已經發現上面的圖在以前出現過,webpack4構建的入口文件去加載webpack3構建的異步組件,就出現了章節頭出現的webpackJsonp不是函數的錯誤。

image.png

好好想想,大概有幾個方案。

  1. 批量去修改webpack3構建出來的異步組件中webpackJsonp的命名,而後在容器頁面入口裏自定義異步加載能力(webpackJsonp功能)的函數。
  2. 從新去用webpack4構建全部遺留的老架構webpack3構建出來的異步組件。
  3. 搜尋是否有官方支持,畢竟這是一個webpack4從webpack3的過來的breack changes。

第一個方案工做量有點大,且怎麼保證異步組件和入口文件同步修改完畢呢? 第二個方案工做量也很大,對於全部老架構的異步組件都得更新,且更新後的可靠性堪憂,萬一有遺漏。 第三個方案看起來是最靠譜的。

image.png

因而在第三個方案的方向下,開始作了搜尋。 ​

咱們經過webpack4源碼全局搜尋webpackJsonp,發現了jsonpFunction。經過官方文檔找到了jsonpFunction是能夠自定義webpack4的webpackJsonp的名稱。好比能夠改爲以下。

output: {
  // 自定義名稱
  jsonpFunction: 'webpack4JsonpIsArray'
},
複製代碼

這樣後,webpackJsonp就不是一個數組了,而是未定義了。所以咱們須要在咱們的公共代碼庫裏提供webpackJsonp函數版本的定義。如異步加載的例子小節所提到的。

// webpackJsonp函數版
!(function (n) {
  window.webpackJsonp = function (t, u, i) {
		//... 
  }
}([]))
複製代碼

以此來提供入口頁面能加載webpack3構建的異步文件的能力。

○ 演進

咱們還對遠程組件彈層作了一些演進,因爲跟本文關聯度不大,只作一些簡單介紹。

圖片壓縮問題

券彈層的券有PNG、JPG、GIF格式,須要更快的展示速度,所以咱們作了圖片壓縮的統一服務。

image.png

gif處理策略:github.com/kornelski/g… png處理策略:pngquant.org

效率問題

有規律的遠程組件,可經過搭建工具處理,所以咱們構建了可視化低代碼建站工具,有感興趣的同窗留言,我考慮寫一篇😂 。

image.png

○ 擴展閱讀

遠程組件

yangjunlong/compile-vue-demo: 編譯vue單文件組件

Webpack異步加載

Webpack 是怎樣運行的?(二) - 知乎

Webpack原理-輸出文件分析 - 騰訊Web前端 IMWeb 團隊社區 | blog | 團隊博客

相關文章
相關標籤/搜索