28
本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。
原文地址:https://segmentfault.com/a/1190000006887523
如果您对本系列文章感兴趣,欢迎关注订阅这里:https://segmentfault.com/blog/array_huang

前言

目前前端虽处于百花齐放阶段,angular/react/vue竞相角逐,但毕竟尚未完全成熟,有些需求还是得依靠我们的老大哥jQuery的。

我个人对jQuery并不反感,但我对jQuery生态的停滞不前相当无奈,比如说赫赫有名的bootstrap(特指3代),在webpack上打包还得靠个loader的,太跟不上时势了。况且,bootstrap还算好的,有些jquery插件都有一两年没更新了,连NPM都没上架呢,可偏偏就是找不到它们的替代品,项目又急着要上,这可咋办呐?

别急,今天就教你适配兼容老式jQuery插件。

老式jQuery插件为和不能直接用webpack打包?

如果你把jQuery看做是一个普通的js模块来加载(要用到jQuery的模块统统先require后再使用),那么,当你加载老式jQuery插件时,往往会提示找不到jQuery实例(有时候是提示找不到$),这是为啥呢?

要解释这个问题,就必须先稍微解释一下jQuery插件的机制:jQuery插件是通过jQuery提供的jQuery.fn.extend(object)jQuery.extend(object)这俩方法,来把插件本身实现的方法挂载到jQuery(也即$)这个对象上的。传统引用jQuery及其插件的方式是先用<script>加载jQuery本身,然后再用同样的方法来加载其插件;jQuery会把jQuery对象设置为全局变量(当然也包括了$),既然是全局变量,那么插件们很容易就能找到jQuery对象并挂载自身的方法了。

而webpack作为一个遵从模块化原则的构建工具,自然是要把各模块的上下文环境给分隔开以减少相互间的影响;而jQuery也早已适配了AMD/CMD等加载方式,换句话说,我们在require jQuery的时候,实际上并不会把jQuery对象设置为全局变量。说到这里,问题也很明显了,jQuery插件们找不到jQuery对象了,因为在它们各自的上下文环境里,既没有局部变量jQuery(因为没有适配AMD/CMD,所以就没有相应的require语句了),也没有全局变量jQuery

怎么来兼容老式jQuery插件呢?

方法有不少,下面一个一个来看。

ProvidePlugin + expose-loader

首先来介绍我最为推荐的方法:ProvidePlugin + expose-loader,在我公司的项目,以及我个人的脚手架开源项目webpack-seed里使用的都是这一种方法。

ProvidePlugin的配置是这样的:

  var providePlugin = new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    'window.jQuery': 'jquery',
    'window.$': 'jquery',
  });

ProvidePlugin的机制是:当webpack加载到某个js模块里,出现了未定义且名称符合(字符串完全匹配)配置中key的变量时,会自动require配置中value所指定的js模块。

如上述例子,当某个老式插件使用了jQuery.fn.extend(object),那么webpack就会自动引入jquery(此处我是用NPM的版本,我也推荐使用NPM的版本)。

另外,使用ProvidePlugin还有个好处,就是,你自己写的代码里,再!也!不!用!require!jQuery!啦!毕竟少写一句是一句嘛哈哈哈。

接下来介绍expose-loader,这个loader的作用是,将指定js模块export的变量声明为全局变量。下面来看下expose-loader的配置:

/*
    很明显这是一个loader的配置项,篇幅有限也只能截取相关部分了
    看不明白的麻烦去看本系列的另一篇文章《webpack多页应用架构系列(二):webpack配置常用部分有哪些?》:https://segmentfault.com/a/1190000006863968
 */
{
  test: require.resolve('jquery'),  // 此loader配置项的目标是NPM中的jquery
  loader: 'expose?$!expose?jQuery', // 先把jQuery对象声明成为全局变量`jQuery`,再通过管道进一步又声明成为全局变量`$`
},

你或许会问,有了ProvidePlugin为嘛还需要expose-loader?问得好,如果你所有的jQuery插件都是用webpack来加载的话,的确用ProvidePlugin就足够了;但理想是丰满的,现实却是骨感的,总有那么些需求是只能用<script>来加载的。

externals

externals是webpack配置中的一项,用来将某个全局变量“伪装”成某个js模块的exports,如下面这个配置:

    externals: {
      'jquery': 'window.jQuery',
    },

那么,当某个js模块显式地调用var $ = require('jquery')的时候,就会把window,jQuery返回给它。

与上述ProvidePlugin + expose-loader的方案相反,此方案是先用<script>加载的jQuery满足老式jQuery插件的需要,再通过externals将其转换成符合模块化要求的exports。

我个人并不太看好这种做法,毕竟这就意味着jQuery脱离NPM的管理了,不过某些童鞋有其它的考虑,例如为了加快每次打包的时间而把jQuery这些比较大的第三方库给分离出去(直接调用公共CDN的第三方库?),也算是有一定的价值。

imports-loader

这个方案就相当于手动版的ProvidePlugin,以前我用requireJS的时候也是用的类似的手段,所以我一开始从requireJS迁移到webpack的时候用的也是这种方法,后来知道有ProvidePlugin就马上换了哈。

这里就不详细说明了,放个例子大家看看就懂:

// ./webpack.config.js

module.exports = {
    ...
    module: {
        loaders: [
            {
                test: require.resolve("some-module"),
                loader: "imports?$=jquery&jQuery=jquery", // 相当于`var $ = require("jquery");var jQuery = require("jquery");`
            }
        ]
    }
};

总结

以上的方案其实都属于shimming,并不特别针对jQuery,请举一反三使用。另外,上述方案并不仅用于shimming,比如用上ProvidePlugin来写少几个require,自己多多挖掘,很有乐趣的哈~~

补充

误用externals(2016-10-17更新)

有童鞋私信我,说用了我文章的方案依然提示$ is not a function,在我仔细分析后,发现:

  1. 他用的是我推荐的ProvidePlugin + expose-loader方案,也就是说,他已经把jquery打包进来了。
  2. 但是他又不明就里得配了externals:
  externals: {
    jquery: 'window.jQuery',
  },
  1. 然而实际上他并没有直接用<script>来引用jQuery,因此window.jQuery是个null。
  2. 结果,他的jquery插件获得的$就是个null了。

这里面我们可以看出,externals是会覆盖掉ProvidePlugin的。

但这里有个问题,expose-loader的作用就是设置好window.jQuery和window.$,那window.jQuery怎么会是null呢?我的猜想是:externals在expose-loader设置好window.jQuery前就已经取了window.jQuery的值(null)了。

说了这么多,其实关键意思就是,不要手贱不要手贱不要手贱(重要的事情说三遍)!

示例代码

诸位看本系列文章,搭配我在Github上的脚手架项目食用更佳哦(笑):Array-Huang/webpack-seed(https://github.com/Array-Huang/webpack-seed)

附系列文章目录(同步更新)

本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。
原文地址:https://segmentfault.com/a/1190000006887523
如果您对本系列文章感兴趣,欢迎关注订阅这里:https://segmentfault.com/blog/array_huang

如果觉得我的文章对你有用,请随意赞赏
已赞赏

你可能感兴趣的

59 条评论
cychen7 · 2016年12月09日

请问作者,webpack引入zepto.js,使用了ProvidePlugin和expose
还是报“ $ is not a function”
这样的问题,怎么解决??

回复

array_huang 作者 · 2016年12月09日

你是怎么引zepto的,贴代码吧

回复

cychen7 · 2016年12月12日
多个项目公用资源目录?
├── src                             # 源码目录
│   ├── components                  # 组件
│   │   ├── page_components/        # 公用页面模块(meta、layout等)
│   └── pages/                      # 公用页面,如404,505等
│   ├── vendor/                     # 第三方库文件存放目录(无法在npm上找到的第三方库)
│   ├── fonts/                      # 公用字体存放目录(字体文件、scss文件)
│   ├── sprites/                    # 公用图标(需生成雪碧图)存放目录

webpack-dll.config.js
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const dirVars = require('./dir-vars.config'); // 与业务代码共用同一份路径的配置表

module.exports = {

context: dirVars._srcDir, // 因为启动环境不是在src目录中,所以这里指定entry选项的基础目录
output: {
    path: dirVars.dllDir,
    filename: '[name].js',
    library: '[name]_[chunkhash]', // 当前Dll的所有内容都会存放在这个参数指定变量名的一个全局变量下,注意与DllPlugin的name参数保持一致
},
entry: {
    /*
      指定需要打包的js模块
      或是css/less/图片/字体文件等资源,但注意要在module参数配置好相应的loader
    */
    mdll: [
        'zepto', 'lodash', './vendor/sm', './vendor/main.scss'
    ]
},
plugins: [
    new webpack.DllPlugin({
        context: dirVars.staticRootDir, // 指定一个路径作为上下文环境,需要与DllReferencePlugin的context参数保持一致,建议统一设置为项目根目录
        path: '[name]-manifest.json', // manifest文件的输出路径,manifest本Dll文件中各模块的索引,供DllReferencePlugin读取使用
        name: '[name]_[chunkhash]', // 是dll暴露的对象名,当前Dll的所有内容都会存放在这个参数指定变量名的一个全局变量下,注意与参数output.library保持一致
    }),
    /* 跟业务代码一样,该兼容的还是得兼容 */
    new webpack.ProvidePlugin({
        '$': 'zepto',
        'zepto': 'zepto',
        'window.zepto': 'zepto',
        'window.$': 'zepto',
    }),
    new ExtractTextPlugin('[name].css'), // 打包css/less的时候会用到ExtractTextPlugin
    // new webpack.optimize.UglifyJsPlugin({
    //     compress: {
    //         warnings: false,
    //     },
    // })
],
module: require('./webpack_lib/getModule'), // 沿用业务代码的module配置
resolve: require('./webpack_lib/getResolve')

};
webpack.config.js(dll引用部分)
/ 配置好Dll /

new webpack.DllReferencePlugin({
    context: dirVars.staticRootDir,
    manifest: require('../../mdll-manifest.json')
})

alias.config.js
var path = require('path');
var dirVars = require('../dir-vars');
module.exports = {

dllDir: dirVars.dllDir,
commonCssDir: dirVars._cssDir,
libDir: dirVars.libDir,
// 特殊
'zepto': path.resolve(dirVars.vendorDir, './zepto.js'),
// 正常第三方库
'zepto.js': path.resolve(dirVars.vendorDir, './zepto.js')

};
getModlue.js
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const dirVars = require('../dir-vars.config.js');
const includeDirs = [dirVars._srcDir];

module.exports = {

loaders: [{
    test: /\/zepto\.js$/, //
    loader: 'expose?$!expose?zepto'
}, {
    test: /\.css$/,
    include: [dirVars.vendorDir, dirVars.srcRootDir],
    loader: ExtractTextPlugin.extract('css')
}, {
    test: /\.less$/,
    include: includeDirs,
    loader: ExtractTextPlugin.extract('css!less')
}, {
    test: /\.scss$/,
    include: includeDirs,
    loader: ExtractTextPlugin.extract('css!sass')
}, {
    test: /\.js$/,
    include: includeDirs,
    loader: 'babel-loader',
    query: {
        presets: ['es2015'],
        cacheDirectory: true,
    },
}]

};

回复

array_huang 作者 · 2016年12月12日

$ is not a function的那个库,你是怎么引用的?用<script>吗?

回复

cychen7 · 2016年12月12日

<link rel="stylesheet" type="text/css" href="./stylesheets/mdll.css">

<script type="text/javascript" src="./javascripts/mdll.js"></script>

回复

cychen7 · 2016年12月12日

一开始就报“$ is not a function”。后来我把zepto改成jquery,就没有报了,不管是通过npm安装的jquery还是我本地的jquery

回复

array_huang 作者 · 2016年12月12日

如果是这样的话,你可以考虑换成用npm上的zepto-webpack

回复

array_huang 作者 · 2016年12月12日

另外,这个mdll.js,是打包的Dll文件吗?如果是的话,也是需要把zepto也打包进去的

回复

cychen7 · 2016年12月12日

我打包的这些: 'zepto', 'lodash', './vendor/sm.js', './vendor/main.scss',还需要在入口文件中require引用吗?

回复

array_huang 作者 · 2016年12月12日

如果你没有直接用<script>直接引用某个第三方插件的话,理论上来说是不需要在入口文件里引用的,我的看法还是换个zepto-webpack试试。因为zepto好久没更新了,不支持模块化的,跟jquery差远了

回复

cychen7 · 2016年12月12日

恩,我有尝试改成n-zepto,这次$已经是全局变量了,但是sm这个库中附加到$上的函数又报$.init is not a function。$.init()是sm.js中的 $.init = function() {……}

回复

array_huang 作者 · 2016年12月12日

sm?难道是SUI?哈哈哈兄弟恭喜你踩坑了!直接用<script>来加载zepto吧。另外,建议你如果还没深入,就弃坑这个SUI吧,都没有人继续维护了

回复

cychen7 · 2016年12月12日

-_-!是SUI,那就真的是入坑了。我们项目没有用webpack打包,现在维护起来很麻烦。

回复

array_huang 作者 · 2016年12月12日

我也有个项目用的是SUI,最后直接用<script>在最前面加载好zepto和sui。另外,如果你有遵循commonjs的zepto插件库,可以用externals的方案:https://segmentfault.com/a/11...

回复

mr_sai · 2016年12月15日

Webpack打包代码,即使代码只有一句,打包出来的文件都有70多kb,请问同学们这正常吗?

回复

array_huang 作者 · 2016年12月15日

你可以看看打包生成的文件里面都有些啥嘛

回复

跑火车 · 2017年02月08日

{
test: require.resolve('jquery'),
loader: 'expose?$!expose?jQuery',
},
在webpack配置文件里,module里添加了上面这一项,不起作用。但在页面的文件内写 require("expose-loader?jQuery!jquery"); 倒是起作用了。

请问这是怎么回事?

回复

0

因为你只是配置了loader,但只要没有require('jquery'),其实就都还没有加载jquery

array_huang 作者 · 2017年02月08日
0

@array_huang 谢谢,是这样的。但又有另外一个问题:我按照你的方法利用 DllReferencePlugin 和 dll 关联起来,即便require('jquery')也不起作用。我注释掉 plugins 里的 new webpack.DllReferencePlugin 就可以了,这是如何解决?

跑火车 · 2017年02月08日
0

你发问题具体描述一下吧,我没碰过这个情况

array_huang 作者 · 2017年02月08日
行者在路上 · 2017年03月11日

请问我需要加载swiper插件,但会闪一下才会有样式我需要怎么配置
entry: {

    main: [
        './src/swiper/swiper-3.4.1.min.css',
        './src/swiper/swiper-3.4.1.min.js',
        './src/js/main.js',
    ]
},

回复

0

闪一下才会有样式是因为,你的打包方式造成要先执行了js才会把css放到页面上去。解决的方法是,用webpack生成html文件,然后把css用extract-text-webpack-plugin打包成独立的css文件,这样webpack就会自动在生成的html文件里加载这个css文件

array_huang 作者 · 2017年03月11日
scream_leen · 2017年03月15日

有了ProvidePlugin为嘛还需要expose-loader?
其实这句还应该倒过来问
有了expose-loader为嘛还需要ProvidePlugin?

回复

0

expose-loader和ProvidePlugin在用途上既没有重复也没有冲突。你可以不用ProvidePlugin,那你就需要保证在引入jquery插件之前先引入jquery。如果用了ProvidePlugin,那就没有这个烦恼了,根本就不需要引入jquery了。

array_huang 作者 · 2017年03月15日
科西嘉乡巴佬 · 2017年04月06日

有一点我不明白,设置了全局的jquery变量之后,jquery最终是如何发布到静态资源目录的,你在footer里面的script src引入的jquery这个是怎么实现的。

回复

0

我是找了个地方,统一用file-loader把所有需要搬运的文件搬到build目录

array_huang 作者 · 2017年04月06日
载入中...