大概介绍
- 浏览器端的前端打包工具
- 主要用于在浏览器中使用 npm 包,最终会转换为 commonJS (require) 类似方式,在浏览器使用
- 方便模块细分,每个模块自成,通过
require引用其他模块 - 基于流
Stream - 旧时代产物,尽管也能勉强处理 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
debug 为 true 时,会将 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 参数(文档未描述)。参见:
browserField
如果在项目中 package.json 中配置 browser 字段
"browser": {
"ccc": "./src/ccc.js"
},
在代码源文件中,require('ccc') 会自动被处理成 require('./src/ccc.js')
功能和 paths 类似,但是其主要用来替换原有的模块,而不是 alias 作用
而优先级方面,paths 的设置更高。不过,在使用上需要尽量避免paths 和 browserField 设置相同模块的情况,以免造成一些歧义和不可控的现象
为 false 时,将忽略 package.json 中这个字段
builtins
设置要使用的内置函数列表,默认情况下为 lib/builtins.js
可为 false、array、object
如果设置为 false,不会进行任何 Node 相关内容的设置
如果设置为 array,可以设置为 lib/builtins.js 对应的 key name,例如: ['assert', 'buffer'] 代表只将此两部分作为内置内容
如果设置为 Object,将直接替换掉默认的 lib/builtins.js,而采用用户的配置
bare
例如:
console.log(__dirname)
console.log(process.env)
默认为 false,会将 __dirname、process.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,只在 bare 为 false 时作用。例如:
console.log(__dirname)
console.log(process.env)
会进行模块扫描,上方 __dirname 和 process.env 的设置,是先通过检测,后设置,不设置其他多余内容。但是这样,检测的时间会长一些
如果将其设置为 false,类似 bare 设置为 true,不会进行 __dirname 和 process.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,代表内置的 process、buffer 是否可以设置进去
例如:
{
detectGlobals: true,
insertGlobal: true,
bundleExternal: false,
insertGlobalVars: {
xxx: function () {
return '1111';
}
},
}
即使 detectGlobals, insertGlobal 都为 true,也不会进行 process 和 buffer 的设置
方法
b.add(file, opts)
同 opts.entries 参数
b.require(file, opts)
同 opts.require 参数
b.ignore(file)、b.external(file)、b.exclude(file)
这三个方法,都是用来将 打包文件内的某个/某几个模块 移除编译内容,参数可为 string、array
三个方法的区别,文档也没说清(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]);
理解大概是:
ignore代表忽略,如果内部引用了,会将其模块作为作为空模块处理,模块的位置还在exclude会将该模块直接移除,并且如果require了的话,值为undefinedexternal会将该模块移除,但是对应的模块引入,还是会在运行时进行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, buf,buf 为文件 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 内容是完全一致的
其他工具
更多的工具,可见 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 的方式进行处理,因为:
- babel 7 支持了 typescript 的转换
- 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 缺陷
-
gulp 没有相关
require引用处理的能力 -
如果单纯只用 gulp,相应模块之间的拆分,只能通过全局变量的方式进行管理,相对来说比较混乱。例如:
// a module window.lib.someFunc = xxx // b module window.lib.someFunc() // 打包 gulp.src(['a.js', 'b.js'])
browserify 优势与缺陷
优势
-
而如果用 browserify,因为模块之间的引用,通过
require完成。与 node 模块编写方式一致,此外,入口文件只需要一个。// a module exports.someFunc = xxx // b module const { someFunc } = require('./a') someFunc() browserify(['./b']) .bundle() .pipe(fs.createWriteStream('./build/b.js'))
缺陷
- 以
js作为入口文件,缺乏css、html等处理能力
一起使用
而因为他们都是基于流的处理,因此可以通过流相关的工具,例如 vinyl-source-stream、through2 进行相应转换,来达到共用的目的
结语
基于其与 gulp 的比较,个人认为 最好的方式 是在 gulp 中集成 browserify 功能(将 browserify 作为 gulp 的一个扩展),只用于 require 相关处理
这样,可以结合双方的优点,并且避免了双方的缺陷
而像 babel、typescript 相关转换以及 sourcemap、minify 等等功能,交给 gulp 相关插件
gulp 对应的 browserify 插件:gulp-bro
代码也比较简单,仅仅是对于流进行了相应转换 gulp-bro 源码
最后,此文对 browserify 做的部分介绍,相关配置、插件其实已经过时而没有太大存在和深究的意义
此外,
- browserify 的配置很多,而且很多都重复功能
- browserify 基本上也只有浏览器前端才会需要使用,也就没必要用到太多的无用配置。例如:
bare, builtins, detectGlobals, insertGlobals, insertGlobalVars等等配置都应该移除 - 原
gulp-browserify已经停止维护,像其他的一些 browserify 工具,也都很少再有更新