如何在 webpack 中引入未模块化的库,如 Zepto

来源:互联网 发布:vmware 安装mac os 编辑:程序博客网 时间:2024/05/18 00:11

前言

原文链接: https://sebastianblade.com/how-to-import-unmodular-library-like-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 functionfunction __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.jsimport $ 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 来主动暴露需要的对象,使其模块化。

这样的方法适用于所有的库,不过最好的解决办法是从根本上让这些让这些库支持模块化。

参考

  • webpack.ProvidePlugin with zepto
  • exports-loader · webpack指南

阅读全文
0 0
原创粉丝点击