木匣子

Web/Game/Programming/Life etc.

使用 WebpackJsonp 与 jQuery 进行代码注入

本文本涉及的版本为 Webpack 2.x-3.x,写作时 Webpack 已经发布 4.x 半年左右。

0x00 Webpack

Webpack 是当下最流行的 Web 打包工具,允许前端开者发在项目中进行模块化开发,并将代码打包成一个代码包(bundle),在浏览器上加载使用。但随着引入的第三库越来越多,代码包会越来越大,影响页面加载时间。于是 Webpack 提供了一个分包功能,可以将共享的库拆分(code-spliting)到不同的包中,使得浏览器可以并行加载不同的包,加快页面打开的速度。

0x01 jQuery

jQuery 是一个老牌的前端组件,主要用于提供方便的 Dom 操作和 Ajax 请求。由于在前端项目中大量使用,所以在模块化开发中,常常将它作为全局组件使用。为了必避在每个模块中导入它:import $ from 'jquery';。Webpack 提供了一个叫 webpack.ProvidePlugin 的插件,可以在配置中将 jQuery 全局化,在其它模块中直接使用而无需 import

new webpack.ProvidePlugin({
  $: 'jquery',
});

0x02 webpackJsonp

如果不使用 Webpack 的分包功能,包中的模块全部存在 Webpack 闭包里的 installedModules = {} 对象中,只能通过闭包中的 __webpack_require__(moduleId) 来引用。在这种情况下 jQuery 将与前端逻辑放在一起,并且受到 Webpack 的闭包保护,而无法在网页上的其它代码里引用包中的 jQuery 实例。除非你在代码中强行把 jQuery 导入到 window 命名空间[1]

但是如果启用了 Webpack 的分包功能,Webpack 生成一个 Manifest.js 并向 Window 对象注入一个 webpackJsonp(chunkIds, moreModules, executeModules) 接口用于多个分包之间共享数据。

但是如果仔细阅读这个接口的代码,会发现第三个参数有特别的作用,可以指定 moduleId 并返回该 Module,相当于可以在外部直接 require 包内的模块。

window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
    ...
    if(executeModules) {
        for(i=0; i < executeModules.length; i++) {
            result = __webpack_require__(__webpack_require__.s = executeModules[i]);
        }
    }
    return result;
}

0x03 $.ajax()

在 jQuery 中,前端开发者可以用 $.ajax() / $.getJSON() / $.post() 等接口与服务端通讯。后两者其实是 $.ajax() 的封装。从 jQuery 的文档中可以看到 $.ajax() 的一般用法:

$.ajax({
    url: '/path/to/api-endpoint',
    data: {
        /* request data */
    },
    method: 'GET',
    success: (data) => {
        /* handle response data */
    }
}).done((data) => {
    /* another way to handle response data */
});

以上代码向 /path/to/api-endpoint 请求数据,并带上 data 指定的参数,最后在 success 里处理返回结果。

0x04 $.ajaxPrefilter()

如果有个 API 返回了 {"successful": false},要是我们能拦截返回结果并修改它,我们就可以构造一些数据来影响前端逻辑。而 jQuery 确实提供了一个接口让我们有机会在数据被处理前将它改掉:$.ajaxPrefilter()

$.ajaxPrefilter((options) => {
    const oldSuccess = options.success;
    options.success = (data) => {
        console.log('original', data);
        data.successful = true;
        console.log('injected', data);
        oldSuccess && oldSuccess(data);
    };
});

以上代码向 jQuery 注册了一个 ajax pre-filter,之后的所有 ajax 都被会它预处理。它在将原来的 success 回调进行封装,并修改了服务端返回的结果,从而影响了前端原有的逻辑。除此之外,ajaxPrefilter 还可以用来修改所有 $.ajax() 的 options 对象,包括 data 等。

ajaxPrefilter 这类的拦截器本意是让前端开发者有一些统一的地方对 API 通讯进行一些封装,但是如果被脚本小子利用就另当别论了。

0x05 Case

影响前端逻辑看起来似乎没什么用,但是却很方便来用绕过一些限制,实现一些 Web 自动化,甚至采集用户数据等等。

例如某电视剧网站,最近为了收割微信用户,强制用户观影前需要关注公众号,将观影码发送至公众号才可以进入观看影片。但其接口设计有问题,只是通过 jQuery 不断轮询 API,检查服务端是否收到观影码,如果收到就放行。

  1. 打开 Console 观察,发现不能直接访问 jQuery 对象,但网站使用 Webpack 打包并分包处理;
  2. 通过下载 Manifest.js 可以发现 webpackJsonp(chunkIds, moreModules, executeModules) 接口;
  3. 通过下载 Vendor.js 可以找到混淆过的 jQuery 的 moduleId ;
  4. 通过 let jq = webpackJsonp([],[],[jqModuleId]) 获取包内 jQuery ;
  5. 通过 ajaxPrefilter 注入修改 API 返回的结果;
  6. 下一次轮询后成功进入观影。

0x06 受影响版本

由于 Webpack 的代码是开源的,跟踪提交记录可以看到 webpackJsonp(chunkIds, moreModules, executeModules)2.0.0-beta 版被引入,并在 4.0.0-alpha.0 被移除。所以如果网站使用 Webpack 分包,最好升级到 Webpack 4.x 以上的版本。

另外尽可能不要将 jQuery 暴露到 window 全局空间,可以防范类似 ajaxPrefilter 的修改。

0x07 备注

有个有趣的细节是,如果在 webpack.ProvidePlugin 中添加 {'window.$': 'jquery'},即使在模块中“不小心”写了 window.$ = $ 或者 window['$'] = $,Webpack 会将其修改为 __webpack_provided_window_dot_ = $,从而防止 $ 被暴露到全局空间。


  1. window.$ = $;

MAMP 与 PhpStorm 远程调试

最近接手了一个用 PHP 作后端项目。本来团队里一直都是使用 Vagrant 来统一开发环境的,但是这个项目可能比较老,所以没有整合虚拟机。于是需要手动配置开发环境。配置中途遇到了一点问题,需要断点调试程序,方便追踪错误。本文相关运行环境:

Windows 10
MAMP for windows 3.3.1
Apache 2.2.31
PHP 5.3.23

黑客帝国字幕流效果

翻收藏夹的时候发现压箱底的 yupoo 图床居然还活着,里面放了一些以前折腾过的东西。其中有一个动图是 08 年的时候用 Flash 做的黑客帝国字幕流特效:

matrix rain

第一感觉是:哇塞我居然做过这个!当时高中的时候用的是 Nokia 3110c ,内置塞班 S40 系统。这玩意儿居然支持 Flash 屏保。于是就写了上面这个程序,可惜性能太差,在手机上运行非常卡,最后导出成了屏幕录像凑合着用了。

现在又回来做前端了,想试着在网页上重新实现这个特效。但这么多年过去了,原来的源文件也不知道哪里去了,只好从零开始。

Unity3d 的 GUI 框架分析

本系列的第三篇,以 Unity3d 为例,看看其如何实现基于事件的 GUI 框架。

本文分析的是 uGUI,而非 Unity3d 的 Editor UI 。后者是一种称为立即模式(Immediate Mode)的 UI 交互方式,Unity3d 的编辑器插件以及非常早期的 Unity UI 使用这种方式开发。

与 Cocos2d 或者 Egret 不同的是,Unity3d 是一个基于组件-实体-系统(Entity-Component-System[1][2])框架的游戏引擎。虽然是商业引擎,但 Unity3d 将 UI 的大部分代码托管于 Bitbucket ,详见这里。里面除了最具价值的 RectTransform 和 Canvas 之外,都开源了。花了一些时间将其中比较核心的类图画出来,被它的复杂度震惊了:

unity3d-cls-1

Unity3d 的 UI 框架非常的复杂,由于是基于 ECS 框架,代码非常零散。此外 Unity3d 允许将 GUI 渲染在屏幕甚至是 3D 表面,并且对实体的碰撞检测(hitTest)甚至可以是基于物理的。这需求远超出我的想像,所以它的抽象性非常高。

Egret 的 GUI 框架分析

本系列的第二篇,以 egret 为例,看看其如何实现基于事件的 GUI 框架。

Egret 是另一款基于 html5 的国产开源游戏引擎。其框架结构大量参考 Flash ,并且使用 TypeScript 这种强类型脚本语言,特别适合早期的 ActionScript 3.0 游戏开发者迁移到 Egret 的开发环境。

虽然我并没有用它做过比较大的项目,但是对它的实现也是保持了一点的好奇心。同第一篇一样,我将 egret 框架中与 GUI 有关的类绘制出来,方便理解:

egret-cls-1

通过与上一篇的 Cocos2d-html5 对比,我们可以看到以下概念在 Egret 里有何区别。

Cocos2d-html5 的 GUI 框架分析

本系列的第一篇,以 cocos2d-html5 为例,分析其如何实现基于事件的 GUI 框架。

Cocos2d-x 是当下非常流行的跨平台游戏引擎,其衍生版 Cocos2d-html5 是使用 Javascript 编写并运行于浏览器的 Canvas 的版本,非常符合我的口味。源码可以在这里下载。

经过简单梳理,我将源码中与 GUI 相关的几个类以及主要函数提取并绘制成类图,方便理解:

cocos2d-html5-cls-1

从这张图里可以总结出一些有趣的概念:

浅析事件驱动的 GUI 原理

从中学时期做 Flash 开发,到大学期间学 App 开发,到毕业后当了几年手机游戏开发者,再到现在转行做 Web 前端。在脑海里一直挥洒不去的疑问就是「如何从 0 开始实现一个 GUI 框架」。

期间间断性地尝试过找一些书看,但大部分都是教如何用现成的桌面 GUI 框架(例如 Java Swing,Qt,WPF 等)写一些软件或者 APP 。而非我想要的那种偏实践性的,从概念介绍 GUI 架构并一步一步实现的文献。

不过在经过使用那么多框架后做了各种各样的项目后,其实已经从这些框架中汲取了不少思想。所以想通过对几个游戏引擎的分析,来看看交互程序中的 GUI 是如何工作的。如果有时间,或许可以试着在网页的 Canvas 上实现一个简单的 GUI 框架。

为什么选游戏引擎?因为游戏引擎是非常典型的图形交互程序,而且游戏引擎的 GUI 部分相对于传统应用程序而言更加精炼,容易理解。经过考查,我选了三个游戏引擎,分别是 cocos2d-html5egret-coreunity3d-ui

写在 26 岁末

2015

去年的这个时候,我从厦门勇仕网络离职,准备了一个月雅思。虽然首战口语成绩不理想,但刚过完 26 岁的生日后就迫不及待乘上了厦门直飞墨尔本的航班,与远在他乡的 LP 团聚。

在飞机上的 10 个小时真是无比漫长,晚上飞机的轰鸣声,以及狭小的空间让人难以入睡。不过想着马上能见到亲爱的 LP ,这点小折磨根本算不上什么。

听 LP 描述的墨村,是个很荒凉的地方,半夜出门是看不到活物的,也没有可以吃宵夜的小店。当飞机抵达墨尔本上空的时候是上午,我正好坐在窗边,往下望去,没有看到什么高楼大厦,只见一小撮有点繁华的村中城,也就是 CBD 。

飞机顺利着陆,入关取行李花了一个多小时。LP 已经早早在机场门口等候。随后一起打车回到了不久前她新搬的家,一个离 CBD 非常近的地方。