使用Browserify配合jQuery进行编程的超级指南


Posted in Javascript onJuly 28, 2015

引言
1. manually

以前,我新开一个网页项目,然后想到要用jQuery,我会打开浏览器,然后找到jQuery的官方网站,点击那个醒目的“Download jQuery”按钮,下载到.js文件,然后把它丢在项目目录里。在需要用到它的地方,这样用<script>引入它:

<script src="path/to/jquery.js"></script>

2. Bower

后来,我开始用Bower这样的包管理工具。所以这个过程变成了:先打开命令行用bower安装jQuery。

bower install jquery

再继续用<script>引入它。

<script src="bower_components/jquery/dist/jquery.js"></script>

3. npm&Browserify

现在,我又有了新的选择,大概是这样:

命令行用npm安装jQuery。

npm install jquery

在需要用到它的JavaScript代码里,这样引入它:

var $ = require("jquery");

没错,这就是使用npm的包的一般方法。但特别的是,这个npm的包是我们熟知的jquery,而它将用在浏览器中。

Browserify,正如其名字所体现的动作那样,让原本属于服务器端的Node及npm,在浏览器端也可使用。

显然,上面的过程还没结束,接下来是Browserify的工作(假定上面那段代码所在的文件叫main.js):

browserify main.js -o bundle.js

最后,用<script>引用Browserify生成的bundle.js文件。

<script src="bundle.js"></script>

这就是依托Browserify建立起来的第三选择。

等下,怎么比以前变复杂了?
CommonJS风格的模块及依赖管理

其实,在这个看起来更复杂的过程中,require()具有非凡的意义。

Browserify并不只是一个让你轻松引用JavaScript包的工具。它的关键能力,是JavaScript模块及依赖管理。(这才是为师的主业)

就模块及依赖管理这个问题而言,已经有RequireJS和国内的Sea.js这些优秀的作品。而现在,Browserify又给了我们新的选择。
Browserify参照了Node中的模块系统,约定用require()来引入其他模块,用module.exports来引出模块。在我看来,Browserify不同于RequireJS和Sea.js的地方在于,它没有着力去提供一个“运行时”的模块加载器,而是强调进行预编译。预编译会带来一个额外的过程,但对应的,你也不再需要遵循一定规则去加一层包裹。因此,相比较而言,Browserify提供的组织方式更简洁,也更符合CommonJS规范。

像写Node那样去组织你的JavaScript,Browserify会让它们在浏览器里正常运行的。
安装及使用
命令行形式

命令行形式是官方贴出来的用法,因为看起来最简单。

Browserify本身也是npm,通过npm的方式安装:

npm install -g browserify

这里-g的参数表示全局,所以可以在命令行内直接使用。接下来,运行browserify命令到你的.js文件(比如entry.js):

browserify entry.js -o bundle.js

Browserify将递归分析你的代码中的require(),然后生成编译后的文件(这里的bundle.js)。在编译后的文件内,所有JavaScript模块都已合并在一起且建立好了依赖关系。最后,你在html里引用这个编译后的文件(喂,和引言里的一样啊):

<script src="bundle.js"></script>

有关这个编译命令的配置参数,请参照node-browserify#usage。如果你想要做比较精细的配置,命令行形式可能会不太方便。这种时候,推荐结合Gulp使用。
+ Gulp形式

结合Gulp使用时,你的Browserify只安装在某个项目内:

npm install browserify --save-dev

建议加上后面的--save-dev以保存到你项目的package.json里。

接下来是gulpfile.js的部分,下面是一个简单示例:

var gulp = require("gulp");
var browserify = require("browserify");
var sourcemaps = require("gulp-sourcemaps");
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');

gulp.task("browserify", function () {
 var b = browserify({
  entries: "./javascripts/src/main.js",
  debug: true
 });

 return b.bundle()
  .pipe(source("bundle.js"))
  .pipe(buffer())
  .pipe(sourcemaps.init({loadMaps: true}))
  .pipe(sourcemaps.write("."))
  .pipe(gulp.dest("./javascripts/dist"));
});

可以看到,Browserify是独立的,我们需要直接使用它的API,并将它加入到Gulp的任务中。

在上面的代码中,debug: true是告知Browserify在运行同时生成内联sourcemap用于调试。引入gulp-sourcemaps并设置loadMaps: true是为了读取上一步得到的内联sourcemap,并将其转写为一个单独的sourcemap文件。vinyl-source-stream用于将Browserify的bundle()的输出转换为Gulp可用的vinyl(一种虚拟文件格式)流。vinyl-buffer用于将vinyl流转化为buffered vinyl文件(gulp-sourcemaps及大部分Gulp插件都需要这种格式)。

这样配置好之后,直接运行gulp browserify就可以得到结果了,可能像这样:

使用Browserify配合jQuery进行编程的超级指南

如果你的代码比较多,可能像上图这样一次编译需要1s以上,这是比较慢的。这种时候,推荐使用watchify。它可以在你修改文件后,只重新编译需要的部分(而不是Browserify原本的全部编译),这样,只有第一次编译会花些时间,此后的即时变更刷新则十分迅速。

有关更多Browserify + Gulp的示例,请参考Gulp Recipes。
特性及简要原理

使用Browserify来组织JavaScript,有什么要注意的地方吗?

要回答这个问题,我们先看看Browserify到底做了什么。下面是一个比较详细的例子。

项目内现在用到2个.js文件,它们存在依赖关系,其内容分别是:
name.js

module.exports = "aya";

main.js

var name = require("./name");

console.log("Hello! " + name);

然后对main.js运行Browserify,得到的bundle.js的文件内容是这样的:
bundle.js

(function e(t, n, r) {
 // ...
})({
 1: [function (require, module, exports) {
  var name = require("./name");

  console.log("Hello! " + name);
 }, {"./name": 2}],
 2: [function (require, module, exports) {
  module.exports = "aya";
 }, {}]
}, {}, [1])

//# sourceMappingURL=bundle.js.map

请先忽略掉省略号里的部分。然后,它的结构就清晰多了。可以看到,整体是一个立即执行的函数(IIFE),该函数接收了3个参数。其中第1个参数比较复杂,第2、3个参数在这里分别是{}和[1]。
模块map

第1个参数是一个Object,它的每一个key都是数字,作为模块的id,每一个数字key对应的值是长度为2的数组。可以看出,前面的main.js中的代码,被function(require, module, exports){}这样的结构包装了起来,然后作为了key1数组里的第一个元素。类似的,name.js中的代码,也被包装,对应到key2。

数组的第2个元素,是另一个map对应,它表示的是模块的依赖。main.js在key1,它依赖name.js,所以它的数组的第二个元素是{"./name": 2}。而在key2的name.js,它没有依赖,因此其数组第二个元素是空Object{}。

因此,这第1个复杂的参数,携带了所有模块的源码及其依赖关系,所以叫做模块map。
包装

前面提到,原有的文件中的代码,被包装了起来。为什么要这样包装呢?

因为,浏览器原生环境中,并没有require()。所以,需要用代码去实现它(RequireJS和Sea.js也做了这件事)。这个包装函数提供的3个参数,require、module、exports,正是由Browserify实现了特定功能的3个关键字。
缓存

第2个参数几乎总是空的{}。它如果有的话,也是一个模块map,表示本次编译之前被加载进来的来自于其他地方的内容。现阶段,让我们忽略它吧。
入口模块

第3个参数是一个数组,指定的是作为入口的模块id。前面的例子中,main.js是入口模块,它的id是1,所以这里的数组就是
[1]。数组说明其实还可以有多个入口,比如运行多个测试用例的场景,但相对来说,多入口的情况还是比较少的。
实现功能

还记得前面忽略掉的省略号里的代码吗?这部分代码将解析前面所说的3个参数,然后让一切运行起来。这段代码是一个函数,来自于browser-pack项目的prelude.js。令人意外的是,它并不复杂,而且写有丰富的注释,很推荐你自行阅读。
所以,到底要注意什么?

到这里,你已经看过了Browserify是如何工作的。是时候回到前面的问题了。首先,在每个文件内,不再需要自行包装。

你可能已经很习惯类似下面这样的写法:

;(function(){
 // Your code here.
}());

但你已经了解到,Browserify的编译会将你的代码封装在局部作用域内,所以,你不再需要自己做这个事情,像这样会更好:

// Your code here.

类似的,如果你想用"use strict";启用严格模式,直接写在外面就可以了,这表示在某个文件的代码范围内启用严格模式。

其次,保持局部变量风格。我们很习惯通过window.jQuery和window.$这样的全局变量来访问jQuery这样的库,但如果使用Browserify,它们都应只作为局部变量:

var $ = require("jquery");

$("#alice").text("Hello!");

这里的$就只存在于这个文件的代码范围内(独立的作用域)。如果你在另一个文件内要使用jQuery,需要按照同样的方式去require()。

然而,新的问题又来了,既然jQuery变成了这种局部变量的形式,那我们熟悉的各种jQuery插件要如何使用呢?
browserify-shim

你一定熟悉这样的jQuery插件使用方式:

<script src="jquery.js"></script>
<script src="jquery.plugin.js"></script>
<script>
 // Now the jQuery plugin is available.
</script>

很多jQuery插件是这样做的:默认window.jQuery存在,然后取这个全局变量,把自己添加到jQuery中。显然,这在Browserify的组织方式里是没法用的。

为了让这样的“不兼容Browserify”(其实是不兼容CommonJS)的JavaScript模块(如插件)也能为Browserify所用,于是有了browserify-shim。

下面,以jQuery插件jquery.pep.js为例,请看browserify-shim的使用方法。
使用示例

安装browserify-shim:

npm install browserify-shim --save-dev

然后在package.json中做如下配置:

"browserify": {
 "transform": [ "browserify-shim" ]
},
"browser": {
 "jquery.pep" : "./vendor/jquery.pep.js"
},
"browserify-shim": {
 "jquery.pep" : { "depends": ["jquery:jQuery"] }
}

最后是.js中的代码:

var $ = require("jquery");
require("jquery.pep");

$(".move-box").pep();

完成!到此,经过Browserify编译后,将可以正常运行这个jQuery插件。

这是一个怎样的过程呢?

在本例中,jQuery使用的是npm里的,而jquery.pep.js使用的是一个自己下载的文件(它与很多jQuery插件一样,还没有发布到npm)。查看jquery.pep.js源码,注意到它用了这样的包装:

;(function ( $, window, undefined ) {
 // ...
}(jQuery, window));

可以看出,它默认当前环境中已存在一个变量jQuery(如果不存在,则报错)。package.json中的"depends": ["jquery:jQuery"]是为它添加依赖声明,前一个jquery表示require("jquery"),后一个jQuery则表示将其命名为jQuery(赋值语句)。这样,插件代码运行的时候就可以正常找到jQuery变量,然后将它自己添加到jQuery中。

实际上,browserify-shim的配置并不容易。针对代码包装(尽管都不兼容CommonJS,但也存在多种情况)及使用场景的不同,browserify-shim有不同的解决方案,本文在此只介绍到这。

关于配置的更多说明,请参照browserify-shim官方文档。更多参考可以查看browserify shim recipes。此外,如果你觉得browserify-shim有些难以理解或者对它的原理也有兴趣,推荐阅读这篇Stack Overflow上的回答。

当然,对于已经处理了CommonJS兼容的库或插件(比如已经发布到npm),browserify-shim是不需要的。
其实还有的更多transform

在前面browserify-shim的例子中,"browserify": {"transform": [ "browserify-shim" ]}其实是Browserify的配置。可以看出,browserify-shim只是Browserify的其中一种transform。在它之外,还有很多的transform可用,分别应对不同的需求,使Browserify的体系更为完善。

比如,还记得本文引言里的Bower吗?debowerify可以让通过Bower安装的包也可以用require()引用。npm和bower同为包管理工具,Browserify表示你们都是我的翅膀。
一点提示

Browserify是静态分析编译工具,因此不支持动态require()。例如,下面这样是不可以的:

var lang = "zh_cn";
var i18n = require("./" + lang);

文档资料

有关Browserify更详细的说明文档,请看browserify-handbook。
结语

我觉得Browserify很有趣,它用了这样一个名字,让你觉得它好像只是一个Node的浏览器端转化工具。为此,它还完成了Node中大部分核心库的浏览器端实现。但实际上,它走到了更远的地方,并在JavaScript模块化开发这个重要的领域中,创立了一个全新的体系。

喜欢CommonJS的简洁风格?请尝试Browserify!

Javascript 相关文章推荐
FireFox JavaScript全局Event对象
Jun 14 Javascript
javascript动态的改变IFrame的高度实现自动伸展
Oct 12 Javascript
通过JS判断联网类型和连接状态的实现代码
Apr 01 Javascript
JS实现新浪博客左侧的Blog管理菜单效果代码
Oct 22 Javascript
Vue.js基础知识汇总
Apr 27 Javascript
15个非常实用的JavaScript代码片段
Dec 18 Javascript
微信小程序 向左滑动删除功能的实现
Mar 10 Javascript
详解angularjs实现echart图表效果最简洁教程
Nov 29 Javascript
微信小程序button组件使用详解
Jan 31 Javascript
vue项目中实现的微信分享功能示例
Jan 21 Javascript
node中使用log4js4.x版本记录日志的方法
Aug 20 Javascript
24个解决实际问题的ES6代码片段(小结)
Feb 02 Javascript
使用AmplifyJS组件配合JavaScript进行编程的指南
Jul 28 #Javascript
JavaScript编程中的Promise使用大全
Jul 28 #Javascript
javascript+html5实现绘制圆环的方法
Jul 28 #Javascript
学习Bootstrap组件之下拉菜单
Jul 28 #Javascript
深入了解JavaScript中的Symbol的使用方法
Jul 28 #Javascript
深入理解JavaScript中的箭头函数
Jul 28 #Javascript
解析JavaScript的ES6版本中的解构赋值
Jul 28 #Javascript
You might like
php使用CURL不依赖COOKIEJAR获取COOKIE的方法
2015/06/17 PHP
100行PHP代码实现socks5代理服务器
2016/04/28 PHP
PHP使用函数用法详解
2018/09/30 PHP
关于laravel 数据库迁移中integer类型是无法指定长度的问题
2019/10/09 PHP
解决laravel(5.5)访问public报错的问题
2019/10/12 PHP
一个刚完成的layout(拖动流畅,不受iframe影响)
2007/08/17 Javascript
js类的静态属性和实例属性的理解
2009/10/01 Javascript
js 自制滚动条的小例子
2013/03/16 Javascript
jquery实现可拖动DIV自定义保存到数据的实例
2013/11/20 Javascript
Javascript基础教程之数据类型转换
2015/01/18 Javascript
Jquery 实现grid绑定模板
2015/01/28 Javascript
JS获取复选框的值,并传递到后台的实现方法
2016/05/30 Javascript
js关于getImageData跨域问题的解决方法
2016/10/14 Javascript
JavaScript实现图片切换效果
2017/08/12 Javascript
web前端开发中常见的多列布局解决方案整理(一定要看)
2017/10/15 Javascript
JS中精巧的自动柯里化实现方法
2017/12/12 Javascript
Angular父子组件通过服务传参的示例方法
2018/10/31 Javascript
vue子路由跳转实现tab选项卡
2019/07/24 Javascript
Vue 数组和对象更新,但是页面没有刷新的解决方式
2019/11/09 Javascript
element-ui 远程搜索组件el-select在项目中组件化的实现代码
2019/12/04 Javascript
python里将list中元素依次向前移动一位
2014/09/12 Python
深入理解python对json的操作总结
2017/01/05 Python
python装饰器深入学习
2018/04/06 Python
Python实现的绘制三维双螺旋线图形功能示例
2018/06/23 Python
Anaconda2 5.2.0安装使用图文教程
2018/09/19 Python
selenium+python自动化测试之鼠标和键盘事件
2019/01/23 Python
python实现字符串完美拆分split()的方法
2019/07/16 Python
Pycharm远程连接服务器并实现代码同步上传更新功能
2020/02/25 Python
详解Anaconda安装tensorflow报错问题解决方法
2020/11/01 Python
python爬虫如何解决图片验证码
2021/02/14 Python
德国家具折扣店:POCO
2020/02/28 全球购物
自我评价格式
2014/01/06 职场文书
大学生志愿者活动总结
2014/06/27 职场文书
推广普通话宣传标语口号
2015/12/26 职场文书
高三英语教学反思
2016/03/03 职场文书
Win11 Build 22000.51版本文件资源管理器“命令栏”和上下文菜单有什么新变化?
2021/11/21 数码科技