如何在 webpack 中引入未模塊化的庫,如 Zepto

前言

最近我在研究多頁面 webpack 模塊打包的完整解決方案時,發現用 import 導入 Zepto 時,會報 Uncaught TypeError: Cannot read property 'createElement' of undefined 錯誤,導致無法愉快地使用 Zepto。在經過一番調試和搜索後終於找到了解決的辦法,並且對於所有不支持模塊化的庫都可以用這種方法導入模塊。zepto error

原因

Zepto 的源碼:

JavaScript
/* Zepto v1.2.0 - zepto event ajax form ie - zeptojs.com/license */
(function(global, factory) {
  if (typeof define === 'function' && define.amd)
    define(function() { return factory(global) })
  else
    factory(global)
}(this, function(window) {
  var Zepto = (function() {

    // ...

    return $
  })()

  window.Zepto = Zepto
  window.$ <span class="token operator" style="color:rgb(166,127,89)">===</span> undefined <span class="token operator" style="color:rgb(166,127,89)">&amp;&amp;</span> <span class="token punctuation" style="color:rgb(153,153,153)">(</span>window<span class="token punctuation" style="color:rgb(153,153,153)">.</span>$ = Zepto)

  return Zepto
}))

可以看出,它只使用了 AMD 規範的模塊導出方法 define,沒有用 CommonJs 規範的方法 module.exports 來導出模塊,不過這不是造成報錯的原因。

先來看一下 webpack 運行模塊的方法:Execute the module function

再來看一下在 webpack config 中不作任何處理,直接 import $ from 'zepto',經過 webpack 轉換的 Zepto 的模塊閉包:zepto webpack transformed

以上代碼是模塊執行的閉包,化簡一下其實就是 webpack 把 AMD 規範的 define 方法轉換成了 module.export = factory(global),以此來獲取 factory 方法返回的對象。

在模塊加載(import/require)時,webpack 會通過下面這種方法來執行模塊閉包並導入模塊:

JavaScript
// The require function
function __webpack_require__(moduleId) {

  // Check if module is in cache
  if(installedModules[moduleId])
    return installedModules[moduleId].exports;

  // Create a new module (and put it into the cache)
  var module = installedModules[moduleId] = {
    exports: {},
    id: moduleId,
    loaded: false,
    hot: hotCreateModule(moduleId),
    parents: hotCurrentParents,
    children: []
  }

  // Execute the module function
  modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));

  // Flag the module as loaded
  module.loaded = true

  // Return the exports of the module
  return module.exports
}

其核心在於 modules[moduleId].call,它會傳入新初始化的 module.exports 來作爲模塊閉包的上下文(context),並運行模塊閉包來將模塊暴露的對象加入到已加載的模塊對象(installedModules)中。

所以對於 Zepto 來說,它初始化時使用的 this(見下圖)其實就是 module.exports,但這個 module.exports 沒有賦值過任何變量,即 Zepto 初始化使用的 this 爲空對象。

this

所以 factory(global) 中 global 爲空對象,Zepto 運行函數中的 window 也就變成了空對象,而 var document = window.document,這個 document 爲 undefined,因此會造成 document.createElement 會報 TypeError。

解決方法

Bash
$ npm i -D script-loader exports-loader

要用到兩個 loader:exports-loader 和 script-loader。

script-loader

JavaScript
require("script!./zepto.js");  
// => execute file.js once in global context

script-loader 可以在我們 import/require 模塊時,在全局上下文環境中運行一遍模塊 JS 文件(不管 require 幾次,模塊僅運行一次)。

script-loader

script-loader 把我們指定的模塊 JS 文件轉成純字符串,並用 eval.call(null, string) 執行,這樣執行的作用域就爲全局作用域了。

但如果只用 script-loader,我們要導入 Zepto 對象就需要這麼寫:

JavaScript
// entry.js
/*
 * 不能使用 `import $ from 'zepto'`
 * 因爲 zepto.js 執行後返回值爲 undefined
 * 因爲 module.exports 默認初始爲空對象
 * 所以 $ 也爲空對象
 */

$(function () { })

這樣的寫法就是:當 webpack 初始化(webpackBootstrap)時,zepto.js 會在全局作用域下執行一遍,將 Zepto 對象賦值給 window.window 、Zepto 變量就都可用了。

不過這種持續依賴全局對象的實現方法不太科學,還是將對象以 ES6 Module/CommonJs/AMD 方式暴露出來更好。

Note:

如果我們用的庫沒有把對象掛載到全局的話,就沒法作爲模塊使用了(還是趁早提個 PR 模塊化一下吧)。

exports-loader

爲了讓我們的模塊導入更加地「模塊化」,可以 import/require,而不是像上面那麼「與衆不同」,我們還需要 exports-loader 的幫助。

exports-loader 可以導出我們指定的對象:

JavaScript
require('exports?window.Zepto!./zepto.js')  

他的作用就是在模塊閉包最後加一句 module.exports = window.Zepto 來導出我們需要的對象,這樣我們就可以愉快地 import $ from 'zepto' 了。

exports-loader

webpack 配置

廢話說了那麼多,終於可以告訴大家怎麼直接使用這兩個 loader 來「模塊化」Zepto 了:

JavaScript
// webpack.config
{
  // ...
  module: {
    loaders: [{
      test: require.resolve('zepto'),
      loader: 'exports-loader?window.Zepto!script-loader'
    }]
  }
}

這樣我們在頁面入口文件中就可以這麼寫:

JavaScript
// entry.js
import $ from 'zepto'

$(function () {
  // ...
})

Note: 
require.resolve() 是 nodejs 用來查找模塊位置的方法,返回模塊的入口文件,如 zepto 爲 ./node_modules/zepto/dist/zepto.js

其他

如果你不想寫 import $ from 'zepto',並且想用其他變量來代替 Zepto。 
可以使用官方的一個插件:webpack.ProvidePlugin。

webpack.ProvidePlugin 是一個依賴注入類型的插件,可以讓你在使用指定變量時,比如直接使用 $</code>&nbsp;時,自動加載指定的模塊&nbsp;<code style="font-family:Consolas,Monaco,&quot;Andale Mono&quot;,&quot;Ubuntu Mono&quot;,monospace,&quot;PingFang SC&quot;,&quot;Hiragino Sans GB&quot;,STHeiti,&quot;Microsoft Yahei&quot;; font-size:0.85em; padding:1px 3px; white-space:pre-wrap; border:1px solid rgb(227,237,243); background:rgb(247,250,251)">zepto</code>,並將其暴露的對象賦值給&nbsp;<code style="font-family:Consolas,Monaco,&quot;Andale Mono&quot;,&quot;Ubuntu Mono&quot;,monospace,&quot;PingFang SC&quot;,&quot;Hiragino Sans GB&quot;,STHeiti,&quot;Microsoft Yahei&quot;; font-size:0.85em; padding:1px 3px; white-space:pre-wrap; border:1px solid rgb(227,237,243); background:rgb(247,250,251)">$

JavaScript
// webpack.config
{
  // ...
  plugins: [
    new webpack.ProvidePlugin({
      $: 'zepto',
      // 把 Zepto 導入爲 abc 變量也可以
      abc: 'zepto'
    })
    // ...
  ]
}

這樣就可以直接使用賦值了 Zepto 對象的 $/abc 變量了~

JavaScript
// entry.js
$(function () {
  // ...
  abc(document)
})

如果不想這麼麻煩地用兩個 loader 來解決問題,可以把不支持模塊化的庫模塊化,比如用這個 npm 包:webpack-zepto

但這個包已經一年多沒更新了,所以我更推薦上面比較麻煩的做法來確保引入的模塊是最新的

總結

由於我們用 npm 下載的模塊沒有模塊化,因此我們要使用:

  1. script-loader 全局上下文環境中執行模塊 JS 文件;
  2. exports-loader 添加 module.exports 來主動暴露需要的對象,使其模塊化。

這樣的方法適用於所有的庫,不過最好的解決辦法是從根本上讓這些讓這些庫支持模塊化。

參考

發佈了1 篇原創文章 · 獲贊 6 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章