ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Browserify 实战:让浏览器直接加载 Node.js 的 CommonJS 模块

Browserify 实战:让浏览器直接加载 Node.js 的 CommonJS 模块
  • 教程
  • 文档

【免费下载链接】jstutorial

Javascript tutorial book

项目地址:https://gitcode.com/gh_mirrors/js/jstutorial
点击查看免费下载

随着 JavaScript 程序日益模块化,代码的复用与组织成为一个核心问题。在 ECMAScript 6 推出官方的模块处理方案之前,社区在实践中形成了两大模块规范:AMD 规范面向浏览器的异步加载,CommonJS 规范面向 Node.js 环境的同步加载。Browserify 的出现打通了二者之间的壁垒——它是一个基于 Node.js 的打包工具,能够把原本运行在服务器端的 CommonJS 模块改写为浏览器可以直接加载的脚本。阅读本文,你将掌握 Browserify 的安装、基本打包、前端模块管理、模块导出、实时生成以及与服务端框架集成的完整实战方案。

背景:浏览器端为什么需要 Browserify

在 ES6 之前,JavaScript 模块化存在两套并行的方案:

  • AMD 规范(Asynchronous Module Definition):针对模块的异步加载,主要用于浏览器端,代表作是 RequireJS,参见仓库中的 RequireJS和AMD规范;
  • CommonJS 规范:针对模块的同步加载,主要用于服务器端,即 Node.js 环境,参见仓库中的 CommonJS规范。

Node.js 采用 CommonJS 规范后,每个文件就是一个模块,有自己的作用域,通过module.exports对外输出接口,通过require()加载其他模块。这种写法清晰、易于维护,但浏览器环境无法直接运行——浏览器没有 Node.js 内置的require函数与模块解析机制。

Browserify 正是一个 Node.js 模块,它主要用于改写现有的 CommonJS 模块,使得浏览器端也可以使用这些模块。它会顺着require()调用链遍历所有依赖,把它们打包进同一个脚本文件,浏览器加载该脚本后,模块间的依赖关系便已内置,无需再逐个加载。

安装 Browserify

在全局环境下安装 Browserify:

$ npm install -g browserify

安装后,browserify命令即可在任意目录使用。若要查看当前版本,可以执行browserify --version。此外,Browserify 也可以作为项目依赖安装(如npm install --save-dev browserify),便于纳入构建流程,这一点在后面的"与构建工具的集成"一节中还会展开。

基本用法:将 CommonJS 模块打包成浏览器脚本

从入口文件生成 bundle

先看一个最简单的例子。假定有一个非常简单的 CommonJS 模块文件foo.js:

// foo.js module.exports = function(x) { console.log(x); };

它通过module.exports输出一个函数——注意,当模块的输出是单一值时,必须使用module.exports而不是exports,这一点在 CommonJS规范 一文中有详细说明。

然后,还有一个main.js文件,用来加载foo模块:

// main.js var foo = require("./foo"); foo("Hi");

使用 Browserify,将main.js转化为浏览器可以加载的脚本compiled.js。这里有三种等价的命令行写法:

browserify main.js > compiled.js # 或者 browserify main > compiled.js # 或者 browserify main.js -o compiled.js

说明:

  • browserify <entry>的入口文件参数可以省略.js后缀;
  • 使用重定向符号>与使用-o选项效果相同,后者是--outfile的简写,输出更直观。

之所以把转化后的文件称为compiled.js,是因为该文件不仅包括main.js,还包括它所依赖的foo.js,两者打包在一起,保证了浏览器加载时的依赖关系。随后,只需在 HTML 中引入这一个文件:

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

在浏览器中运行compiled.js,控制台会显示Hi。

从底层原理看,Browserify 打包的本质是把 Node.js 的require/module.exports机制移植到浏览器:它把每个模块的代码包裹进一个函数作用域,再在 bundle 内部实现一张模块注册表,入口模块执行时按需调用这些函数。这相当于在浏览器端重建了 CommonJS规范 中描述的模块加载语义,只是将同步的文件系统读取替换为打包时的一次性解析。

打包第三方库:Backbone 实例

再看一个把服务器端backbone模块转为客户端模块的例子。先安装backbone和它所依赖的jQuery模块:

npm install backbone jquery

然后新建main.js:

// main.js var Backbone = require('backbone'); var $ = Backbone.$ = require('jquery/dist/jquery')(window); var AppView = Backbone.View.extend({ render: function(){ $('main').append('<h1>Browserify is a great tool.</h1>'); } }); var appView = new AppView(); appView.render();

代码要点:

  • require('backbone')直接按 CommonJS 方式引入第三方库;
  • jQuery 通过require('jquery/dist/jquery')(window)取到,并赋给Backbone.$,让 Backbone 使用打包进来的这份 jQuery;
  • AppView与appView.render()的用法可对照仓库中的 MVC框架与Backbone.js 一文。

接着,使用 Browserify 将main.js转为app.js:

browserify main.js -o app.js

app.js就可以直接插入 HTML 网页了:

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

注意:只要插入app.js一个文件就可以了,完全不需要再加载backbone.js和jQuery。这正是 Browserify 的核心价值:依赖关系被固化在打包结果中,页面无需关心模块的加载顺序与个数。

管理前端模块:用 npm 安装纯前端组件

Browserify 的主要作用是将 CommonJS 模块转为浏览器可以调用的格式,但纯粹的前端模块,也可以用它打包——这等于让 npm 成为前端组件的包管理器。

首先,新建一个项目目录,添加package.json文件:

{ "name": "demo", "version": "1.0.0" }

接着,新建index.html:

<!doctype html> <html> <head> <title>npm and jQuery demo</title> </head> <body> <span class="title-tipso tipso_style" title="This is a loaded TIPSO!"> Roll over to see the tip </span> <script src="./bundle.js"/> </body> </html>

上面代码中的bundle.js,就是 Browserify 打包后将生成的文件。

然后,安装jquery和它的插件:

$ npm install --save jquery tipso

--save参数会把jquery和tipso写入package.json的dependencies字段,便于他人通过npm install复现环境。

接着,新建一个文件entry.js:

global.jQuery = require('jquery'); require('tipso'); jQuery(function(){ jQuery('.title-tipso').tipso(); });

上面的文件中,第一行之所以要把jQuery写成global的属性,是为了转码之后,它可以变成一个全局变量——许多 jQuery 插件在初始化时会查找全局的jQuery或$,通过global.jQuery = require('jquery')显式挂载,插件才能正确工作。这一手法与 CommonJS规范 中"如果想在多个文件分享变量,必须定义为global对象的属性"的说明相吻合。

最后,执行 Browserify 打包:

$ browserify entry.js --debug > bundle.js

--debug参数表示在打包后的文件中加入 source map 以便除错。有了 source map,浏览器开发者工具中的报错信息可以映射回entry.js等原始源码文件,而非混淆后的 bundle 内容。

这时,在浏览器中打开index.html,脚本已经可以运行,tipso插件的提示效果即可出现。

通过 CDN 加载:browserify-shim

如果不希望将jQuery一起打包,而是通过 CDN 加载,可以使用browserify-shim模块。它的思路与 RequireJS 中的shim配置类似(参见 RequireJS和AMD规范):把"不符合 CommonJS 规范、依赖全局变量"的库显式声明为外部依赖,从而在打包时跳过它们,运行时从全局环境(如 CDN 引入的<script>)中获取。

打包插件自带的 CSS:parcelify

另外一个问题是,某些 jQuery 插件还带有自带的 CSS 文件,这时可以安装parcelify模块:

$ npm install -g parcelify

然后,在package.json中写入规则,声明 CSS 文件的位置:

"style": [ "./node_modules/tipso/src/tipso.css" ]

接着,运行parcelify进行 CSS 打包:

$ parcelify entry.js -c bundle.css

最后,将打包后的 CSS 文件插入index.html:

<link rel="stylesheet" href="bundle.css" />

这样,JS 与 CSS 都通过 npm 统一管理、统一打包,前端组件从"手动下载、手动引入"变成了"声明依赖、一条命令构建"。

生成前端模块:-r参数

有时,我们只是希望将 Node.js 的模块移植到浏览器,使得浏览器端可以调用。这时,可以采用 Browserify 的-r参数(--require的简写):

browserify -r through -r ./my-file.js:my-module > bundle.js

上面代码将through和my-file.js(后面的冒号表示指定模块名为my-module)都做成了模块,可以在其他<script>标签中调用:

<script src="bundle.js"></script> <script> var through = require('through'); var myModule = require('my-module'); /* ... */ </script>

可以看到,-r参数的另一个作用,就是为浏览器端提供require方法。打包出的 bundle 会在全局暴露require,页面内联脚本也能像 Node.js 一样按模块名加载其中注册的模块。这在开发"多个页面共享一套打包产物"的场景中尤其有用:只打包一次,各页面通过require按需取用。

脚本文件的实时生成

Browserify 还可以实时生成脚本文件。下面是一个服务器端脚本,启动 Web 服务器之后,外部用户每次访问这个脚本,它的内容是实时生成的:

var browserify = require('browserify'); var http = require('http'); http.createServer(function (req, res) { if (req.url === '/bundle.js') { res.setHeader('content-type', 'application/javascript'); var b = browserify(__dirname + '/main.js').bundle(); b.on('error', console.error); b.pipe(res); } else res.writeHead(404, 'not found') });

代码要点:

  • browserify(__dirname + '/main.js')以编程方式创建打包实例;
  • .bundle()返回一个可读流,这正是 Browserify 遵循 Node.js Stream 设计的地方——b.pipe(res)把打包结果直接流入 HTTP 响应;
  • .on('error', console.error)监听打包过程中的错误,避免服务崩溃;
  • 每次请求都会重新打包,适合开发调试;生产环境一般仍建议预生成静态文件。

注意,服务器端脚本中引入的是browserify这个 npm 包本身,因此在此场景下,Browserify 应作为项目依赖安装(如npm install --save-dev browserify),而非仅全局安装。

browserify-middleware:运行时动态转换

上面是将服务器端模块直接转为客户端脚本,然后在网页中调用这个转化后的脚本文件。还有一种思路是,在运行时动态转换模块,这就需要用到browserify-middleware模块。

比如,网页中需要加载app.js,它是从main.js转化过来的:

<!-- index.html --> <script src="app.js"></script>

你可以在服务器端静态生成一个app.js文件,也可以让它动态生成。使用browserify-middleware模块,配合 Express,服务器端脚本要像下面这样写:

var browserify = require('browserify-middleware'); var express = require('express'); var app = express(); app.get('/app.js', browserify('./client/main.js')); app.get('/', function(req, res){ res.render('index.html'); });

这里的核心是app.get('/app.js', browserify('./client/main.js')):浏览器请求/app.js时,browserify-middleware会在服务器端按需把./client/main.js及其依赖动态打包成 JavaScript 返回。开发时免去了每次改动源码都要重新执行命令行打包的步骤;生产环境也可配合缓存机制,既保留模块化开发的便利,又不牺牲响应性能。这一思路也可推广到 Koa、Connect 等中间件风格的 Node.js 框架,仓库中的 Express框架 与 Koa框架 文档可作为服务端集成的参考。

与构建工具的集成

通过 npm scripts 管理打包命令

Browserify 最常见的落地方式,是把它写进package.json的scripts字段,用npm run统一管理构建流程。仓库的 npm模块 中给出了典型配置:

"scripts": { "watch": "watchify client/main.js -o public/app.js -v", "build": "browserify client/main.js -o public/app.js", "start": "npm run watch & nodemon server.js", "test": "node test/all.js" }

其中watchify是 Browserify 的伴生工具,会在源码变化时自动增量重建。执行npm run build即等同于运行browserify client/main.js -o public/app.js。

npm run会自动在环境变量$PATH中添加node_modules/.bin目录,因此只要把browserify安装为项目的devDependencies,scripts中就能直接书写browserify命令,无需全局安装。

用管道组合压缩

Browserify 的输出是标准输出流,可以借助 Linux 管道与其他工具串联。仓库的 npm模块 还展示了如何把打包与压缩一步完成:

"build-js": "browserify browser/main.js | uglifyjs -mc > static/bundle.js"

这条命令先将browser/main.js打包,再经uglifyjs -mc(mangle + compress)压缩,最终写入static/bundle.js。类似的流式写法也可以把多个npm run命令用&&或&串接成完整的构建链。

作为 devDependencies 声明

在package.json的devDependencies字段中声明 Browserify,是整个 npm 生态的标准做法。仓库的 package.json文件 中列举了两种典型形态:

{ "devDependencies": { "browserify": "~13.0.0", "karma-browserify": "~5.0.1" } }

以及更早期的固定版本写法"browserify": "2.36.1",并常与grunt-browserify搭配使用。版本号前的~(波浪号)表示安装 13.x 的最新版本,不改变大版本号与次要版本号;如需指定精确版本,可直接书写版本号。此外,本项目仓库自身的 package.json 与 Gruntfile.js 展示了基于 Grunt 的构建组织方式(uglify 压缩 JS、cssmin 压缩 CSS),读者可以在此基础上替换或补充grunt-browserify任务,把 Browserify 纳入 Grunt 的watch自动构建流程。

小结

Browserify 解决的核心问题是:把 Node.js 的 CommonJS 模块生态引入浏览器。通过本文你可以掌握:

  1. 基本打包:browserify main.js -o compiled.js,一个命令生成包含全部依赖的 bundle;
  2. 前端模块管理:借助 npm 安装jquery、tipso等前端组件,通过entry.js组织入口、--debug生成 source map,再用browserify-shim、parcelify处理 CDN 外置与 CSS 打包;
  3. 模块导出:-r参数让 bundle 在浏览器端暴露require方法,供多个页面按需调用;
  4. 实时生成:以编程方式调用browserify().bundle()输出流,或借助browserify-middleware在 Express 中按请求动态打包;
  5. 工程化集成:把 Browserify 写入devDependencies与scripts,结合管道压缩与 watch 工具,纳入完整构建流程。

理解 Browserify 的原理,也有助于理解其后继者(如 webpack、rollup 等打包工具)共通的"依赖图 + 模块注册表"设计思想。配合仓库中 CommonJS规范、RequireJS和AMD规范、npm模块 与 package.json文件 几篇文档,可以构建起对 JavaScript 模块化从规范到工具、从 Node 到浏览器的完整认知。

  • 教程
  • 文档

【免费下载链接】jstutorial

Javascript tutorial book

项目地址:https://gitcode.com/gh_mirrors/js/jstutorial
点击查看免费下载

相关推荐

上一篇:终极指南:如何用MatterGen从零开始生成稳定的无机材料结构
下一篇:Koel快捷键终极指南:10个必备键盘操作技巧优化音乐体验

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进