Webpack 中集成 CoffeeScript:coffee-loader 配置、编译产物与源码级解析
【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through "loaders", modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack
本文以 webpack 官方示例examples/coffee-script为主线,完整讲解如何在 webpack 中通过coffee-loader加载 CoffeeScript 文件:包括规则配置、resolve.extensions扩展名解析、入口 JS 与 CoffeeScript 模块互相引用的完整示例,以及最终编译产物dist/output.js的逐段解读。读完后,你可以掌握 webpack “任意语言 → JS 模块” 的 loader 转换机制、产物中 module 注释块与 runtime 的作用,并能用仓库内测试用例验证同类 loader 的接入方式。
示例结构:一个入口 + 两个 CoffeeScript 模块
整个示例位于 examples/coffee-script 目录,由四个文件构成:
- example.js:JS 入口文件,负责引入 CoffeeScript 模块;
- cup1.coffee:导出一个对象,同时以两种方式引用
cup2; - cup2.coffee:最简模块,执行一条
console.log并导出数字42; - webpack.config.js:配置
coffee-loader与扩展名解析。
入口文件的内容非常简单,它通过 CommonJS 的require引入 CoffeeScript 模块:
console.log(require("./cup1"));值得注意的是,require("./cup1")并未写文件扩展名,而实际目标文件是cup1.coffee——这依赖下文resolve.extensions的配置。
cup1.coffee:对象字面量与嵌套 require
cup1.coffee利用了 CoffeeScript 的缩进对象字面量语法,导出了一个对象,其中external与again两个字段分别以显式扩展名和无扩展名两种方式引入同一个cup2模块:
module.exports = cool: "stuff" answer: 42 external: require "./cup2.coffee" again: require "./cup2"这段代码是理解扩展名解析机制的关键:
require "./cup2.coffee":请求中已带.coffee扩展名,直接按规则匹配;require "./cup2":请求不带扩展名,webpack 会依次尝试resolve.extensions中列出的后缀,直到找到./cup2.coffee。
cup2.coffee:副作用与默认导出
console.log "yeah coffee-script" module.exports = 42编译后它等价于先执行console.log("yeah coffee-script"),再把module.exports整体替换为42。这个“直接给module.exports赋值”的写法会影响 webpack 对该模块的 exports 分析(详见产物解析一节中的CommonJS bailout注释)。
webpack 配置:loader 规则与扩展名解析
示例的 webpack.config.js 全部配置只有两个核心块,完整内容如下:
"use strict"; /** @type {import("webpack").Configuration} */ const config = { // mode: "development" || "production", module: { rules: [ { test: /\.coffee$/, loader: "coffee-loader" } ] }, resolve: { extensions: [".web.coffee", ".web.js", ".coffee", ".js"] } }; module.exports = config;module.rules:按后缀匹配 loader
test: /\.coffee$/是模块匹配的正则,凡是请求路径以.coffee结尾的模块都会命中该规则;loader: "coffee-loader"指定由 npm 包coffee-loader处理命中的文件。它在 webpack 仓库中作为依赖声明在 package.json 的devDependencies中(版本^5.0.0),其作用是把 CoffeeScript 源码编译成等价的 JavaScript,输出给 webpack 的解析器继续处理。
这正是 webpack “通过 loaders,模块可以是 CommonJS、AMD、ES6 模块、CSS、图片、JSON、Coffeescript、LESS 等任意自定义格式”这一设计理念的最直接体现:webpack 本身不理解 CoffeeScript 语法,而是把.coffee文件先交给 loader 转译为 JS,之后一切(解析依赖、tree-shaking、打包)都按标准 JS 模块流程进行。
resolve.extensions:无扩展名 require 的解析顺序
resolve: { extensions: [".web.coffee", ".web.js", ".coffee", ".js"] }extensions定义了require不带后缀时的尝试顺序。对于cup1.coffee中的require "./cup2",解析过程依次为./cup2.web.coffee→./cup2.web.js→./cup2.coffee→./cup2.js,第三个候选命中。这也解释了为什么require "./cup2.coffee"与require "./cup2"在产物中指向同一个模块(module id2),而不会重复打包。
.web.coffee/.web.js这类前缀扩展名是 webpack 平台条件解析的约定(与x.web.js平台文件约定一致),允许为 web 环境提供专门的同名文件。
运行构建与产物 dist/output.js 逐段解析
在仓库中,examples 统一由 examples/buildAll.js 驱动构建(对每个示例目录执行node build.js),构建会分别以 development 与 production 两种模式编译,并自动把结果回填进示例目录下的README.md(模板见 examples/coffee-script/template.md)。
编译产物dist/output.js(development 模式)的核心结构如下,即原 README 展示的完整输出。
模块 1:cup1.coffee 的打包形态
/******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ([ /* 0 */, /* 1 */ /*!*********************!*\ !*** ./cup1.coffee ***! \*********************/ /*! default exports */ /*! export again [provided] [no usage info] [missing usage info prevents renaming] */ /*! export answer [provided] [no usage info] [missing usage info prevents renaming] */ /*! export cool [provided] [no usage info] [missing usage info prevents renaming] */ /*! export external [provided] [no usage info] [missing usage info prevents renaming] */ /*! other exports [not provided] [no usage info] */ /*! runtime requirements: module, __webpack_require__ */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { module.exports = { cool: "stuff", answer: 42, external: __webpack_require__(/*! ./cup2.coffee */ 2), again: __webpack_require__(/*! ./cup2 */ 2) }; /***/ }), /* 2 */ /*!*********************!*\ !*** ./cup2.coffee ***! \*********************/ /*! unknown exports (runtime-defined) */ /*! runtime requirements: module */ /*! CommonJS bailout: module.exports is used directly at 3:0-14 */ /***/ ((module) => { console.log("yeah coffee-script"); module.exports = 42; /***/ }) /******/ ]);这里有几个值得关注的实现细节:
- CoffeeScript 已被完全转译。产物中没有任何 CoffeeScript 残留:缩进对象字面量变成了标准 JS 对象字面量,
console.log "yeah coffee-script"变成了带括号的console.log("yeah coffee-script")。这正是 loader 链的第一道转换,webpack 本身拿到的是纯 JS。 __webpack_modules__数组按模块索引组织。/* 0 */为空位,/* 1 */是./cup1.coffee,/* 2 */是./cup2.coffee。每个条目是一个接收(module, exports, __webpack_require__)的函数。- exports 注释块记录了静态分析结果。
/*! export again [provided] ... */等注释是 webpack 对cup1.coffee编译后代码做 exports 分析的结果:它识别出该模块通过module.exports = { ... }提供again、answer、cool、external四个具名导出;[no usage info]说明在 development 模式下没有收集使用信息,因此不会重命名这些导出。 CommonJS bailout注释。cup2.coffee因直接对module.exports赋值(module.exports = 42),webpack 无法在编译期精确推断其导出结构,于是标记为unknown exports (runtime-defined)并触发 bailout——即放弃对该模块做精细的 exports 优化,改为运行时读取。注释中的3:0-14指向触发 bailout 的源码位置。- 两条 require 解析到同一模块。
__webpack_require__(2)同时服务./cup2.coffee与./cup2两个请求,印证了前文对resolve.extensions的解析分析。
webpack runtime:模块缓存与 require 函数
产物中的 runtime 部分是一个折叠区块,核心代码如下(摘自原 README 的/* webpack runtime code */):
/************************************************************************/ /******/ // The module cache /******/ const __webpack_module_cache__ = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ const cachedModule = __webpack_module_cache__[moduleId]; /******/ if (cachedModule !== undefined) { /******/ return cachedModule.exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ const module = __webpack_module_cache__[moduleId] = { /******/ // no module.id needed /******/ // no module.loaded needed /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ __webpack_modules__moduleId; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /************************************************************************/__webpack_require__是打包后模拟 Node.jsrequire的运行时函数:先查缓存,未命中则创建{ exports: {} }模块对象并执行对应模块函数,最后返回module.exports。这也解释了为什么console.log("yeah coffee-script")这个副作用只执行一次——第二次通过__webpack_require__(2)访问时直接命中缓存,模块函数不会重跑。
入口模块的 IIFE 包装
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk. (() => { /*!********************!*\ !*** ./example.js ***! \********************/ /*! unknown exports (runtime-defined) */ /*! runtime requirements: __webpack_require__ */ console.log(__webpack_require__(/*! ./cup1 */ 1)); })();整个 bundle 被外层(() => { // webpackBootstrap ... })()包裹,入口模块自身再被一层 IIFE 隔离,避免入口文件的顶层变量污染 chunk 内其他模块的作用域。console.log(require("./cup1"))被改写为console.log(__webpack_require__(1)),其中1即cup1.coffee的模块索引。
构建统计:Unoptimized 与 Production mode
原 README 同时记录了 development(未优化)与 production 两种模式的编译统计输出。
Unoptimized(development)
asset output.js 2.55 KiB [emitted] (name: main) chunk (runtime: main) output.js (main) 206 bytes [entry] [rendered] > ./example.js main dependent modules 175 bytes [dependent] 2 modules ./example.js 31 bytes [built] [code generated] [used exports unknown] entry ./example.js main webpack X.X.X compiled successfully- 产物体积 2.55 KiB,包含完整可读的 runtime 与未压缩的模块代码;
[used exports unknown]与产物注释中的[no usage info]呼应:development 模式不收集导出使用情况,因此不做基于用法的优化。
Production mode
asset output.js 300 bytes [emitted] [minimized] (name: main) chunk (runtime: main) output.js (main) 206 bytes [entry] [rendered] > ./example.js main dependent modules 175 bytes [dependent] 2 modules ./example.js 31 bytes [built] [code generated] [no exports used] entry ./example.js main webpack X.X.X compiled successfullyproduction 模式下产物仅 300 bytes 且带[minimized]标记:minimizer 压缩了 runtime 与模块代码,并且 stats 中入口模块标记为[no exports used]——因为example.js的导出确实无人消费。这个体积对比直观展示了 optimization 开关的作用。
仓库内的佐证:coffee-loader 的测试用例
webpack 仓库的测试套件对coffee-loader的接入做了直接验证,位于 test/cases/loaders/coffee-loader/index.js:
it("should handle the coffee loader correctly", function() { expect(require("!coffee-loader!../_resources/script.coffee")).toBe("coffee test"); expect(require("../_resources/script.coffee")).toBe("coffee test"); }); it("should handle literate coffee script correctly", function() { expect(require("!coffee-loader?literate=1!./script.coffee.md")).toBe("literate coffee test"); }); it("should generate valid code with cheap-source-map", function() { require("!coffee-loader!./module-only.coffee"); });这个测试文件提供了三点与示例互补的实现事实:
- loader 请求语法:测试用
!coffee-loader!./script.coffee这种 loader 显式前缀请求文件,而 examples/coffee-script/webpack.config.js 则是用module.rules隐式匹配——两条路径殊途同归,loader 最终都接收同一段 CoffeeScript 源码并输出 JS。 - loader 参数传递:
!coffee-loader?literate=1!./script.coffee.md表明 loader 支持查询参数(此处开启 literate CoffeeScript 模式处理 markdown 中的代码),这对应了 loader 文档约定的loader?query!path写法。 - source-map 兼容:
should generate valid code with cheap-source-map用例确认 coffee-loader 输出与cheap-source-map等 devtool 兼容。
延伸:loader 前缀写法与 source-map 示例
除module.rules外,webpack 还支持在 entry 或require中直接以 loader 前缀处理文件。examples/source-map/webpack.config.js 展示了这一用法:
entry: { bundle: "coffee-loader!./example.coffee" },即把coffee-loader作为 loader 链前缀直接写在入口请求中,对example.coffee执行同样的 CoffeeScript → JS 转换;该示例同时遍历eval、source-map、cheap-module-source-map等十余种devtool值,验证转译产物在各类 source map 配置下的有效性。
小结
- webpack 通过
module.rules中test: /\.coffee$/+loader: "coffee-loader"的组合,把 CoffeeScript 文件统一转译为 JS 后再进入标准打包流程;resolve.extensions让require "./cup2"这类无扩展名请求按约定顺序命中.coffee文件; - 编译产物中,每个模块以
/*! ***! ... */注释块标注路径、exports 分析结果与CommonJS bailout等优化决策,__webpack_require__+ 模块缓存构成运行时,入口模块以 IIFE 隔离; - development 与 production 模式在 stats 上的差异(
[used exports unknown]vs[no exports used]、2.55 KiB vs 300 bytes)体现了优化开关对产物体积与导出分析的影响; - 若需要验证 loader 是否工作,可参考 test/cases/loaders/coffee-loader/index.js 中
!coffee-loader!前缀请求、?literate=1参数传递与 source-map 兼容三类测试手法,并在 examples/coffee-script 目录本地运行构建复现本文全部产物与统计输出。
【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through "loaders", modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考