前言
最近我在研究多頁面 webpack 模塊打包的完整解決方案時,發現用 import
導入
Zepto 時,會報 Uncaught
TypeError: Cannot read property 'createElement' of undefined
錯誤,導致無法愉快地使用 Zepto。在經過一番調試和搜索後終於找到了解決的辦法,並且對於所有不支持模塊化的庫都可以用這種方法導入模塊。
原因
Zepto 的源碼:
/* 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)">&&</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 運行模塊的方法:
再來看一下在 webpack config 中不作任何處理,直接 import
$ from 'zepto'
,經過 webpack 轉換的 Zepto 的模塊閉包:
以上代碼是模塊執行的閉包,化簡一下其實就是 webpack 把 AMD 規範的 define 方法轉換成了 module.export
= factory(global)
,以此來獲取 factory 方法返回的對象。
在模塊加載(import/require)時,webpack 會通過下面這種方法來執行模塊閉包並導入模塊:
// 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
爲空對象。
所以 factory(global)
中
global 爲空對象,Zepto 運行函數中的 window 也就變成了空對象,而 var
document = window.document
,這個 document
爲 undefined
,因此會造成 document.createElement
會報
TypeError。
解決方法
$ npm i -D script-loader exports-loader
要用到兩個 loader:exports-loader 和 script-loader。
script-loader
require("script!./zepto.js");
// => execute file.js once in global context
script-loader 可以在我們 import/require
模塊時,在全局上下文環境中運行一遍模塊
JS 文件(不管 require
幾次,模塊僅運行一次)。
script-loader 把我們指定的模塊 JS 文件轉成純字符串,並用 eval.call(null, string) 執行,這樣執行的作用域就爲全局作用域了。
但如果只用 script-loader,我們要導入 Zepto 對象就需要這麼寫:
// entry.js
/*
* 不能使用 `import $ from 'zepto'`
* 因爲 zepto.js 執行後返回值爲 undefined
* 因爲 module.exports 默認初始爲空對象
* 所以 $ 也爲空對象
*/
$(function () { })
這樣的寫法就是:當 webpack 初始化(webpackBootstrap)時,zepto.js 會在全局作用域下執行一遍,將 Zepto 對象賦值給 window.
不過這種持續依賴全局對象的實現方法不太科學,還是將對象以 ES6 Module/CommonJs/AMD 方式暴露出來更好。
Note:
如果我們用的庫沒有把對象掛載到全局的話,就沒法作爲模塊使用了(還是趁早提個 PR 模塊化一下吧)。
exports-loader
爲了讓我們的模塊導入更加地「模塊化」,可以 import/require,而不是像上面那麼「與衆不同」,我們還需要 exports-loader 的幫助。
exports-loader 可以導出我們指定的對象:
require('exports?window.Zepto!./zepto.js')
他的作用就是在模塊閉包最後加一句 module.exports
= window.Zepto
來導出我們需要的對象,這樣我們就可以愉快地 import
$ from 'zepto'
了。
webpack 配置
廢話說了那麼多,終於可以告訴大家怎麼直接使用這兩個 loader 來「模塊化」Zepto 了:
// webpack.config
{
// ...
module: {
loaders: [{
test: require.resolve('zepto'),
loader: 'exports-loader?window.Zepto!script-loader'
}]
}
}
這樣我們在頁面入口文件中就可以這麼寫:
// 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> 時,自動加載指定的模塊 <code style="font-family:Consolas,Monaco,"Andale Mono","Ubuntu Mono",monospace,"PingFang SC","Hiragino Sans GB",STHeiti,"Microsoft Yahei"; 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>,並將其暴露的對象賦值給 <code style="font-family:Consolas,Monaco,"Andale Mono","Ubuntu Mono",monospace,"PingFang SC","Hiragino Sans GB",STHeiti,"Microsoft Yahei"; font-size:0.85em; padding:1px 3px; white-space:pre-wrap; border:1px solid rgb(227,237,243); background:rgb(247,250,251)">$
:
// webpack.config
{
// ...
plugins: [
new webpack.ProvidePlugin({
$: 'zepto',
// 把 Zepto 導入爲 abc 變量也可以
abc: 'zepto'
})
// ...
]
}
這樣就可以直接使用賦值了 Zepto 對象的 $/abc
變量了~
// entry.js
$(function () {
// ...
abc(document)
})
如果不想這麼麻煩地用兩個 loader 來解決問題,可以把不支持模塊化的庫模塊化,比如用這個 npm 包:webpack-zepto。
但這個包已經一年多沒更新了,所以我更推薦上面比較麻煩的做法來確保引入的模塊是最新的。
總結
由於我們用 npm 下載的模塊沒有模塊化,因此我們要使用:
- script-loader 全局上下文環境中執行模塊 JS 文件;
- exports-loader 添加 module.exports 來主動暴露需要的對象,使其模塊化。
這樣的方法適用於所有的庫,不過最好的解決辦法是從根本上讓這些讓這些庫支持模塊化。