browserify 中文文档与使用教程

大概介绍

  1. 浏览器端的前端打包工具
  2. 主要用于在浏览器中使用 npm 包,最终会转换为 commonJS (require) 类似方式,在浏览器使用
  3. 方便模块细分,每个模块自成,通过 require 引用其他模块
  4. 基于流 Stream
  5. 旧时代产物,尽管也能勉强处理 css(CSS bundlers),html(brfs),但是不太友好,且年久失修

阅读此篇,大概可以较好使用 browserify,以及将其用在合适的地方

此外,文中带 删除线 的内容,因相对的内容过时,阅读意义不大,可简单跳过

大概分析

以 nums.js,demo.js,build 文件做大概分析

nums.js

1
2
3
var uniq = require('uniq'); // uniq 为 npm 依赖包
var nums = [ 5, 2, 1, 3, 2, 5, 4, 2, 0, 1 ];
module.exports = nums

demo.js

1
2
const nums = require('./nums')
console.log(nums)

build

1
2
3
4
5
const browserify = require('browserify')
const fs = require('fs')
browserify(['./src/demo'])
.bundle()
.pipe(fs.createWriteStream('./build/demo.js'))

build 后文件

通过 detective 进行依赖查找,后落地为以下文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({
1:[function(require,module,exports){
"use strict"

// 为避免内容过长,此部分略去

module.exports = unique

},{}],
2:[function(require,module,exports){
const nums = require('./nums')
console.log(nums)
},{"./nums":3}],
3:[function(require,module,exports){
var uniq = require('uniq');
var nums = [ 5, 2, 1, 3, 2, 5, 4, 2, 0, 1 ];

module.exports = nums
},{"uniq":1}],
},
{},
[2]);

build 后文件运转方式

上面的编译后文件,顶部的压缩代码,来源于 browser-pack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// modules are defined as an array
// [ module function, map of requireuires ]
//
// map of requireuires is short require name -> numeric require
//
// anything defined in a previous bundle is accessed via the
// orig method which is the requireuire for previous bundles

(function() {

function outer(modules, cache, entry) {
// Save the require from previous bundle to this closure if any
var previousRequire = typeof require == "function" && require;

function newRequire(name, jumped){
if(!cache[name]) {
if(!modules[name]) {
// if we cannot find the module within our internal map or
// cache jump to the current global require ie. the last bundle
// that was added to the page.
var currentRequire = typeof require == "function" && require;
if (!jumped && currentRequire) return currentRequire(name, true);

// If there are other bundles on this page the require from the
// previous one is saved to 'previousRequire'. Repeat this as
// many times as there are bundles until the module is found or
// we exhaust the require chain.
if (previousRequire) return previousRequire(name, true);
var err = new Error('Cannot find module \'' + name + '\'');
err.code = 'MODULE_NOT_FOUND';
throw err;
}
var m = cache[name] = {exports:{}};
modules[name][0].call(m.exports, function(x){
var id = modules[name][1][x];
return newRequire(id ? id : x);
},m,m.exports,outer,modules,cache,entry);
}
return cache[name].exports;
}
for(var i=0;i<entry.length;i++) newRequire(entry[i]);

// Override the current require with this new one
return newRequire;
}

return outer;

})()

上方的编译代码,即相当于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// step 1
newRequire(2)

// step 2
(function(require,module,exports){
const nums = require('./nums')
console.log(nums)
}).call({}, function(x){
// name is 2
// x is './nums'
// id is 3
var id = modules[name][1][x];
return newRequire(id ? id : x);

// 至于说,为什么这里要增加 outer, modules, cache, entry 这样的无用参数
// 可参考:https://github.com/browserify/browser-pack/issues/82
// 简单理解为:有其他用途
}, { exports:{} }, {}, outer, modules, cache, entry)

// step 3, 4, 5, etc...

以此类推,通过 newRequire 以及相应的 modules 编号,达到代码执行的目的。

多入口文件编译

1
2
3
4
5
6
7
const browserify = require('browserify')
const fs = require('fs');

// 指定入口即可
['./src/demo-1', './src/demo-2'].forEach(f => {
browserify(f).bundle().pipe(fs.createWriteStream(f.replace('./src', './build') + '.js'))
})

配置

entries

同 browserify 第一个参数,files

basedir

如果 entries / files 为 stream,需要指定 basedir 来让 browserify 可以处理内容中的相对路径

默认为 . 即当前脚本运行目录

require

数组,通过模块名或文件路径指定需要打包到bundle中的其他模块

1
2
3
4
browserify('demo-4', {
basedir: './src',
require: ['./demo-3-module-2'],
})

适用于一些全局的处理,又没有模块依赖的内容

例如 ./demo-3-module-2 内容:

1
2
3
window.demo3Func = function (n) {
return n + 1
}

通过 参数 opts.require 方式引入,那么所有被打包的文件,都会有此部分代码

debug

debugtrue 时,会将 sourcemap 添加到包的末尾

ignoreMissing

默认为 false,即如果 require 的模块不存在时,会报错;如果设置为false,即忽略报错

noParse

一个数组,跳过数组中每个文件的所有 require 和全局解析

适用于jquery或threejs等巨型、无需解析的库,避免解析耗时过长

1
2
3
{
noParse: ['jquery']
}

transform

一个数组,用于内容的相应转换。例如使用 uglifyify

1
2
3
4
5
6
7
browserify(f, {
transform: ['babelify'], // babel 配置在 .babelrc 中指定
})
// or
browserify(f, {
transform: [['babelify', { presets: ['@babel/preset-env'] }]],
})

数组的元素可以为字符串,或者数组(该数组第一项为使用的transform组件,第二项为该组件配置项)。

下方 plugin 等,同理。

ignoreTransform

一个数组,用于过滤不需要做 transform 的 transform 控件

1
2
3
4
browserify(f, {
transform: [['babelify', { presets: ['@babel/preset-env'] }]],
ignoreTransform: ['babelify'],
})

也就不会进行 babelify transform

这个参数没什么作用,其实如果不想进行转换,不把它放入 transform 内就好,不需要多此一举在 transform 中添加,又在 ignoreTransform 定义不进行转换

plugin

插件数组。主要用于一些更高级的插件配置,增强 browserify 功能。

详见:plugins 或者下方一些示例

extensions

参数可为字符串或数组。默认是 ['.js', '.json'],可以补充 .ts, .jsx 等等

paths

一个目录数组,用于在查找未使用相对路径引用的模块时浏览搜索,可以是绝对的或相对于basedir。调用browserify命令时,等效设置 NODE_PATH 环境变量

1
2
3
4
5
6
7
8
9
10
11
browserify('./src/demo', {
basedir: './',
transform: [['babelify', { presets: ['@babel/preset-env'] }]],
paths: ['src'], // ./src 下的模块引用都不需要使用相对路径引用
})

browserify('./demo', {
basedir: './src',
transform: [['babelify', { presets: ['@babel/preset-env'] }]],
paths: ['test'], // ./src/test 下的模块引用都不需要使用相对路径引用
})

例如:原来 require('./a') 可以直接写为 require('a')

commondir

没什么作用的参数,要么不传递,要么传递 false

目前看 browserify index.js:616 只会在 builtIns 为数组时,会将 basedir 设置为 /

可能对 sourcemap 有一点影响,其他没什么作用

fullPaths

布尔值,默认为 false,参考上方的分析代码,对应模块会被标记为数字 id,例如: ./nums: 0

如果设置为 true,不会转换为 id,而是以绝对路径形式展示。例如:"./nums":"/Users/xxx/xxx/xxx/browserify-demo/src/nums.js"。官网文档描述其对于保留生成包的原始路径很有用,但是如果在生产环境下,需要设置为 false,否则可能会暴露一些信息

standalone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// beep.js
var shout = require('./shout.js');
module.exports = function beep() {
console.log(shout('beep'));
}

// shout.js
module.exports = function shout(str) {
return str.toUpperCase() + '!';
};

// build.js
var fs = require('fs');
var browserify = require('browserify');
var b = browserify('./src/beep.js', { standalone: 'beep-boop' });
b.bundle().pipe(fs.createWriteStream('./build/demo-5.js'));

如果使用 standalone: 'beep-boop' 最终打包出来的内容,就不是 browser-pack 做的包裹,而是

这个 umd/template 做的包裹,主要用来处理 requireJS 类似的调用方式,包括在全局下增加 变量。例如上方最终在全局下增加的 beepBoop(驼峰) 变量。

所以作为 standalone(“独立”)的模块,就目前9012年来说,没有什么意义。

参考:Standalone Browserify Builds

externalRequireName

文档不全,没什么用途,不要使用。需要搭配 prelude 参数(文档未描述)。参见:

  1. test/multi_bundle_unique
  2. opts.externalRequireName does not work in standalone mode

browserField

如果在项目中 package.json 中配置 browser 字段

1
2
3
"browser": {
"ccc": "./src/ccc.js"
},

在代码源文件中,require('ccc') 会自动被处理成 require('./src/ccc.js')

功能和 paths 类似,但是其主要用来替换原有的模块,而不是 alias 作用

而优先级方面,paths 的设置更高。不过,在使用上需要尽量避免pathsbrowserField 设置相同模块的情况,以免造成一些歧义和不可控的现象

false 时,将忽略 package.json 中这个字段

builtins

设置要使用的内置函数列表,默认情况下为 lib/builtins.js

可为 falsearrayobject

如果设置为 false,不会进行任何 Node 相关内容的设置

如果设置为 array,可以设置为 lib/builtins.js 对应的 key name,例如: ['assert', 'buffer'] 代表只将此两部分作为内置内容

如果设置为 Object,将直接替换掉默认的 lib/builtins.js,而采用用户的配置

详见:builtins 代码逻辑

bare

例如:

1
2
console.log(__dirname)
console.log(process.env)

默认为 false,会将 __dirnameprocess.env 设置为浏览器可运行的内容 (包含 builtins )。process.env 会被设置为 node-process

设置为 true 时,同 builtins = false, commondir = false。创建一个不包含 Node builtins 的bundle,并且不设置除 __dirname__filename 之外的全局 Node 变量。即 process.env 还是 process.env。而这样的处理,如果模块内有使用相关 Node 模块,浏览器端运行会直接报错

node

默认为 false

设置为 true 时,创建一个在 Node 中运行的bundle,不使用浏览器版本的依赖项。与传递 {bare:true,browserField:false} 相同。这个参数,一般也用不上,如果在 node 运行,也便不需要 browserify

detectGlobals

默认为 true,只在 barefalse 时作用。例如:

1
2
console.log(__dirname)
console.log(process.env)

会进行模块扫描,上方 __dirnameprocess.env 的设置,是先通过检测,后设置,不设置其他多余内容。但是这样,检测的时间会长一些

如果将其设置为 false,类似 bare 设置为 true,不会进行 __dirnameprocess.env 的设置

insertGlobals

默认为 false,即不直接设置所有 Node 相关的内容,而是通过 detectGlobals=true 按需设置

如果设置为 true,会始终插入 Node 相关内容,而不做相应模块分析检测。提高了效率,但是打出来的包,内容也更大。但是detectGlobals 必须为 true 才能工作

其他一些作用的需要条件详见:globalTr

insertGlobalVars

会被作为 opts.vars 传递给 insert-module-globals

格式可参考 defaultVars

1
2
3
4
5
6
7
8
{
detectGlobals: true,
insertGlobalVars: {
forTest: function () {
return '1111';
}
},
}

注意:detectGlobals 需要为 true,这样才能检测文件内的 forTest 变量,并做相应设置

bundleExternal

默认为 true,代表内置的 processbuffer 是否可以设置进去

详见:index.js#L608

例如:

1
2
3
4
5
6
7
8
9
10
{
detectGlobals: true,
insertGlobal: true,
bundleExternal: false,
insertGlobalVars: {
xxx: function () {
return '1111';
}
},
}

即使 detectGlobals, insertGlobal 都为 true,也不会进行 processbuffer 的设置

方法

b.add(file, opts)

opts.entries 参数

b.require(file, opts)

opts.require 参数

b.ignore(file)b.external(file)b.exclude(file)

这三个方法,都是用来将 打包文件内的某个/某几个模块 移除编译内容,参数可为 stringarray

三个方法的区别,文档也没说清(browserify 文档太过简略)。大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const $ = require('jquery')
$('body').css('background', 'red')

// ignore
({
1:[function(require,module,exports){
},{}],
2:[function(require,module,exports){
const $ = require('jquery')
$('body').css('background', 'red')
},{"jquery":1}],
}, {},[2]);

// exclude
({
1:[function(require,module,exports){
const $ = require('jquery')
$('body').css('background', 'red')
},{"jquery":undefined}]
}, {},[1]);

// external
({
1:[function(require,module,exports){
const $ = require('jquery')
$('body').css('background', 'red')
},{"jquery":"jquery"}]
},{},[1]);

理解大概是:

  1. ignore 代表忽略,如果内部引用了,会将其模块作为作为空模块处理,模块的位置还在
  2. exclude 会将该模块直接移除,并且如果 require了的话,值为 undefined
  3. external 会将该模块移除,但是对应的模块引入,还是会在运行时进行 require(name) 的形式,由全局的 require 进行其他模块依赖引入

这么看,也只有 external 具备一定的实用性

b.transform(tr, opts={})

opts.transform

1
2
3
4
5
6
b.transform('babelify', { presets: ['@babel/preset-env'] })

// 同
browserify('./src/demo', {
transform: [['babelify', { presets: ['@babel/preset-env'] }]],
})

b.plugin(plugin, opts)

opts.plugin

b.bundle(cb)

将内容以及内容内部的 reuqire 内容,一并打包进一个文件内

创建了一个可读流,用于 pipe 进可写流文件。例如:

b.bundle().pipe(fs.createWriteStream('./build/demo-7.js'))

callback 为可选项,参数为 err, bufbuf 为文件 buffer 内容,因此也就可以基于 buf 内容进行一些其他处理

b.pipeline

一个属性,使用 labeled-stream-splicer,个人简单理解为将内容拆分为不同的分段,通过流的方式进行传递

一般来讲,如果不是写插件之类东西,单纯使用 browserify 层面上来说,用不到

对应的,browserify 内置了一些 label

1
2
3
4
5
6
7
8
9
10
11
12
13
'record' - save inputs to play back later on subsequent bundle() calls
'deps' - module-deps
'json' - adds module.exports= to the beginning of json files
'unbom' - remove byte-order markers
'unshebang' - remove #! labels on the first line
'syntax' - check for syntax errors
'sort' - sort the dependencies for deterministic bundles
'dedupe' - remove duplicate source contents
'label' - apply integer labels to files
'emit-deps' - emit 'dep' event
'debug' - apply source maps
'pack' - browser-pack
'wrap' - apply final wrapping, require= and a newline and semicolon

可以通过 b.pipeline.get(label) 的方式获取,并对其进行相应的处理

b.reset(opts)

将流恢复到 bundle() 前的状态,主要用于需要多次 bundle() 的场景

实际每次 bundle() 调用后,reset() 都会自动执行,所以这个方法在实际使用过程中,可能也没有太大的用处

1
2
3
4
5
6
7
8
9
10
11
12
13
var b = browserify('./src/beep.js', {
debug: true,
commondir: false,
builtins: [],
});

b
.transform('uglifyify', { global: true })
.bundle().pipe(fs.createWriteStream('./build/demo-6.js'));

setTimeout(() => {
b.bundle().pipe(fs.createWriteStream('./build/demo-7.js'));
}, 2000)

上方 打包出来 demo-6、demo-7 内容是完全一致的

或者参考 browserify/test/reset.js

其他工具

更多的工具,可见 awesome-browserify#tools,此处取一部分代表性内容

budo

启动 http 服务器,进行相应 browserify 打包

1
budo ./beep.js --live --open

如果 index.html 内,引用 <script src="./beep.js"></script>,最终启动的服务便是这个 index.html,以及实时打包的 http://127.0.0.1:9966/beep.js 文件

内容类似如下:

1
2
<script type="text/javascript" src="/budo/livereload.js" async="" defer=""></script>
<script src="beep.js"></script>

envify

process.env 添加环境变量替换

1
2
3
4
5
6
b.transform(envify({
// 将 process.env 其他字段设置为 undefined
_: 'purge',
// process.env.NODE_ENV 会被自动替换为 'development',而不是在运行时获取 process.env.NODE_ENV 值
NODE_ENV: 'development'
}))

babelify

因为 browserify 只处理文件相关依赖引入,不处理文件的 es6 转换,因此如果需要使用 es6、es7 语法,需要经过 babelify 进行转换

1
2
3
4
5
6
['./src/demo-1', './src/demo-2'].forEach(f => {
browserify(f)
.transform('babelify', { presets: ['@babel/preset-env'] })
.bundle()
.pipe(fs.createWriteStream(f.replace('./src', './build') + '.js'))
})

tsify

因为 browserify 只处理文件相关依赖引入,如果想要使用 typescript 编写浏览器端代码,需要进行相应转换

但是原则上,其实也可以通过 gulp-babel 的方式进行处理,因为:

  1. babel 7 支持了 typescript 的转换
  2. gulp-babel 也是基于流

或者,通过 gulp-typescript 进行处理,理由同上

uglifyify

代码丑化

1
2
3
4
5
6
['./src/demo-1', './src/demo-2'].forEach(f => {
browserify(f)
.transform('uglifyify', { global: true })
.bundle()
.pipe(fs.createWriteStream(f.replace('./src', './build') + '.js'))
})

tinyify

1
2
3
4
5
b.plugin('tinyify', {
env: {
PUBLIC_PATH: 'https://mywebsite.surge.sh/'
}
})

以下用到的插件的整合版本

1
2
3
4
5
6
7
8
9
b
.transform('unassertify', { global: true })
.transform('envify', { global: true })
.transform('uglifyify', { global: true })
.plugin('common-shakeify')
.plugin('browser-pack-flat/plugin')
.bundle()
.pipe(require('minify-stream')({ sourceMap: false }))
.pipe(fs.createWriteStream('./output.js'))

watchify

检测改动,自动编译

作为插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const b = browserify('./demo-2', {
basedir: 'src',
debug: true,
paths: ['./'],
fullPaths: true,
plugin: [['watchify', {
delay: 100,
ignoreWatch: ['**/node_modules/**'],
poll: false
}]]
})

b.on('update', bundle)
bundle()

function bundle(x) {
console.log(x)
b
.transform('babelify', { presets: ['@babel/preset-env'] })
.transform('uglifyify', { global: true, sourceMap: true })
.bundle()
.on('error', console.error)
.pipe(fs.createWriteStream('./build/demo-5.js'))
}

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const b = watchify(browserify('./demo-2', {
basedir: 'src',
debug: true,
paths: ['./'],
fullPaths: true,
}), {
delay: 100,
ignoreWatch: ['**/node_modules/**'],
poll: false
})

b.on('update', bundle)
bundle()

function bundle(x) {
console.log(x)
b
.transform('babelify', { presets: ['@babel/preset-env'] })
.transform('uglifyify', { global: true, sourceMap: true })
.bundle()
.on('error', console.error)
.pipe(fs.createWriteStream('./build/demo-5.js'))
}

附:Fast browserify builds with watchify

css-modulesify

如其名 css-modulesify,使用它可以在 js 中 require css 内容

brfs

使用 brfs,可以达到 js 中 require html 类似的效果

1
2
3
4
5
var html = fs.readFileSync(__dirname + '/robot.html', 'utf8');

// 最终转换为

var html = "<b>beep boop</b>\n";

browserify-hmr

browserify 本身是基于流,效率比较高。热更新的用处不大,而且根据 README.md 内作者描述,此插件还是存在不少问题

factor-bundle

拆包:将 x、y 共用部分,打包进 common.js,有一定的实用性

1
2
3
browserify([ './files/x.js', './files/y.js' ])
.plugin('factor-bundle', { outputs: [ 'bundle/x.js', 'bundle/y.js' ] })
.bundle().pipe(fs.createWriteStream('bundle/common.js'))

和 gulp 对比

gulp 缺陷

  1. gulp 没有相关 require 引用处理的能力
  2. 如果单纯只用 gulp,相应模块之间的拆分,只能通过全局变量的方式进行管理,相对来说比较混乱。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    // a module
    window.lib.someFunc = xxx

    // b module
    window.lib.someFunc()

    // 打包
    gulp.src(['a.js', 'b.js'])

browserify 优势与缺陷

优势
  1. 而如果用 browserify,因为模块之间的引用,通过 require 完成。与 node 模块编写方式一致,此外,入口文件只需要一个。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    // a module
    exports.someFunc = xxx

    // b module
    const { someFunc } = require('./a')
    someFunc()

    browserify(['./b'])
    .bundle()
    .pipe(fs.createWriteStream('./build/b.js'))
缺陷
  1. js 作为入口文件,缺乏 csshtml 等处理能力

一起使用

而因为他们都是基于流的处理,因此可以通过流相关的工具,例如 vinyl-source-streamthrough2 进行相应转换,来达到共用的目的

结语

基于其与 gulp 的比较,个人认为 最好的方式 是在 gulp 中集成 browserify 功能(将 browserify 作为 gulp 的一个扩展),只用于 require 相关处理

这样,可以结合双方的优点,并且避免了双方的缺陷

而像 babel、typescript 相关转换以及 sourcemap、minify 等等功能,交给 gulp 相关插件

gulp 对应的 browserify 插件:gulp-bro

代码也比较简单,仅仅是对于流进行了相应转换 gulp-bro 源码

最后,此文对 browserify 做的部分介绍,相关配置、插件其实已经过时而没有太大存在和深究的意义

此外,

  1. browserify 的配置很多,而且很多都重复功能
  2. browserify 基本上也只有浏览器前端才会需要使用,也就没必要用到太多的无用配置。例如:bare, builtins, detectGlobals, insertGlobals, insertGlobalVars 等等配置都应该移除
  3. gulp-browserify 已经停止维护,像其他的一些 browserify 工具,也都很少再有更新