browserify 中文文档与使用教程

5,844 阅读14分钟

大概介绍

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

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

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

大概分析

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

nums.js

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

demo.js

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

build

const browserify = require('browserify')
const fs = require('fs')
browserify(['./src/demo'])
	.bundle()
	.pipe(fs.createWriteStream('./build/demo.js'))

build 后文件

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

(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

// 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;

})()

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

// 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 编号,达到代码执行的目的。

多入口文件编译

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中的其他模块

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

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

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

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

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

debug

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

ignoreMissing

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

noParse

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

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

{
  noParse: ['jquery']
}

transform

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

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

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

下方 plugin 等,同理。

ignoreTransform

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

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 环境变量

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

// 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 字段

"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

例如:

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 时作用。例如:

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

{
  detectGlobals: true,
  insertGlobalVars: {
    forTest: function () {
      return '1111';
    }
  },
}

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

bundleExternal

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

详见:index.js#L608

例如:

{
  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 文档太过简略)。大概如下:

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

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

'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() 都会自动执行,所以这个方法在实际使用过程中,可能也没有太大的用处

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 打包

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

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

内容类似如下:

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

envify

process.env 添加环境变量替换

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 进行转换

['./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

代码丑化

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

tinyify

b.plugin('tinyify', {
  env: {
    PUBLIC_PATH: 'https://mywebsite.surge.sh/'
  }
})

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

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

检测改动,自动编译

作为插件:

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'))
}

或者

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 类似的效果

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,有一定的实用性

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,相应模块之间的拆分,只能通过全局变量的方式进行管理,相对来说比较混乱。例如:

    // a module
    window.lib.someFunc = xxx
    
    // b module
    window.lib.someFunc()
    
    // 打包
    gulp.src(['a.js', 'b.js'])
    

browserify 优势与缺陷

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

    
    // 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 工具,也都很少再有更新