Skip to content

npm库和source-map

Webpack 同样具有完备的构建 NPM 库的能力。与一般场景相比,构建 NPM 库时需要注意:

  • 正确导出模块内容
  • 不要将第三方包打包进产物中,以免与业务方环境发生冲突
  • 将 CSS 抽离为独立文件,以方便用户自行决定实际用法
  • 始终生成 Sourcemap 文件,方便用户调试

开发一个npm库

开个一个 dev-npm-lib 的库

bash
cd dev-npm-lib
npm init -y

安装webpack

js
pnpm i -D webpack webpack-cli

创建入口文件 src/index.js

js
export const add = (a, b) => a + b

export const minus = (a, b) => a - b

目录结构如下

bash
├─dev-npm-lib
      ├─package.json
      ├─src
  ├─index.js

使用 webpack 构建 npm 库

生成 webpack.config.js

bash
npx webpack init

基础配置

js
const path = require('path');

const isProduction = process.env.NODE_ENV == 'production';

const config = {
    entry: './src/index.js',
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    },
    plugins: [
    ],
    module: {
        rules: [
            // {
            //     test: /\.(js|jsx)$/i,
            //     loader: 'babel-loader',
            // },
            {
                test: /\.css$/i,
                use: ['css-loader'],
            },
        ],
    },
};

module.exports = () => {
    if (isProduction) {
        config.mode = 'production';
    } else {
        config.mode = 'development';
    }
    return config;
};

提示:我们还可以在上例基础上叠加任意 Loader、Plugin,例如: babel-loadereslint-loaderts-loader 等。

上述配置会将代码编译成一个 IIFE 函数,但这并不适用于 NPM 库,我们需要将打包输出改为 UMD 模块化方案

js
output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist'),
    library: {
        name: 'myLib',
        type: 'umd',
    }
},
  • output.library.name:用于定义模块名称,在浏览器环境下使用 script 加载该库时,可直接使用这个名字调用模块
html
<!DOCTYPE html>
<html lang="en">
...
<body>
    <script src="https://examples.com/dist/main.js"></script>
    <script>
        // Webpack 会将模块直接挂载到全局对象上
        window.myLib.add(1, 2)
    </script>
</body>

</html>
  • output.library.type:用于编译产物的模块化方案,可选值有:commonjsumdmodulejsonp 等,通常选用兼容性更强的 umd 方案即可。

对比添加library配置后输出的结果:

image-20240827110418633

UMD 模式启动时会判断运行环境,自动选择当前适用的模块化方案,例如:

js
// ES Module
import { add } from 'dev-npm-lib';

// CommonJS
const { add } = require('dev-npm-lib');

// HTML
<script src="https://examples.com/dist/main.js"></script>
<script>
    // Webpack 会将模块直接挂载到全局对象上
    window.myLib.add(1, 2)
</script>

正确使用第三方包

加入开发的库使用了lodash

js
// src/index.js
import _ from 'lodash'

export const max = _.max

打包后,发现体积会非常大

image-20240827112045509

这是因为 Webpack 默认会将所有第三方依赖都打包进产物中,这种逻辑能满足 Web 应用资源合并需求,但在开发 NPM 库时则很可能导致代码冗余。

我们可以使用externals 配置项,将第三方依赖排除在打包系统之外

js
externals: {
    // 将 lodash 作为一个外部依赖,不打包到最终的 bundle 中
    lodash: {
        commonjs: 'lodash',
        commonjs2: 'lodash',
        amd: 'lodash',
        root: '_',
    },
},

排除后体积就变小了

image-20240827112614930

至此,Webpack 不再打包 lodash 代码,我们可以顺手将 lodash 声明为 peerDependencies

json
// package.json
"peerDependencies": {
    "lodash": "^4.17.21"
}

实践中,多数第三方框架都可以沿用上例方式处理,包括 React、Vue、Angular、Axios、Lodash 等,方便起见,可以直接使用 webpack-node-externals 排除所有 node_modules 模块,使用方法

js
// webpack.config.js
const nodeExternals = require('webpack-node-externals');

const config = {
  // ...
+  externals: [nodeExternals()]
  // ...
};

抽离 css 代码

通常需要使用 mini-css-extract-plugin 插件将样式抽离成单独文件,由用户自行引入。

js
// webpak.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

const config = {
    plugins: [
        new MiniCssExtractPlugin()
    ],
    module: {
        rules: [
            {
                test: /\.css$/i,
                use: [MiniCssExtractPlugin.loader, 'css-loader'],
            },
        ],
    },
}

生成 source-map

Sourcemap 是一种代码映射协议,它能够将经过压缩、混淆、合并的代码还原回未打包状态

接入方法很简单,只需要添加适当的 devtool 配置:

js
const config = {
    devtool: 'source-map'
}

此后,业务方只需使用 source-map-loader 就可以将这段 Sourcemap 信息加载到自己的业务系统中,实现框架级别的源码调试能力。

其他 npm 配置

  • 使用 .npmignore 文件忽略不需要发布到 NPM 的文件;

  • package.json 文件中,使用 main 指定项目入口,同时使用 module 指定 ES Module 模式下的入口,以允许用户直接使用源码版本,例如:

json
{
    "main": "dist/main.js",
}

source-map

代码通常运行在浏览器上时,是通过打包压缩的。当代码报错需要调试时(debug),调试转换后的代码是很困难的。

我们可以利用 source-map 调试这种转换后不一致的代码

  • source-map是从已转换的代码,映射到原始的源文件
  • 使浏览器可以重构原始源并在调试器中显示重建的原始源

如何使用 source-map

  1. webpack 打包时配置 devtool:source-map
  2. 会发现打包后会出现 .map 文件,打包的js文件最后也有注释
js
//# sourceMappingURL=main.js.map

在Chrome浏览器中的设置可以看到有 map 设置

image-20240827141104605

分析map文件

打开 map 文件

json
{
  "version": 3,
  "file": "main.js",
  "mappings": "AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD,O;;;;;;;;;;;ACVA;;;;;;;;;;;ACAA;;;;;;UCAA;UACA;;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;;UAEA;UACA;;UAEA;UACA;UACA;;;;;WCtBA;WACA;WACA;WACA;WACA;WACA,iCAAiC,WAAW;WAC5C;WACA;;;;;WCPA;WACA;WACA;WACA;WACA,yCAAyC,wCAAwC;WACjF;WACA;WACA;;;;;WCPA;;;;;WCAA;WACA;WACA;WACA,uDAAuD,iBAAiB;WACxE;WACA,gDAAgD,aAAa;WAC7D;;;;;;;;;;;;;;;;;ACNsB;AACF;;AAEb,YAAY,mDAAK;;AAEjB;;AAEA",
  "sources": [
    "webpack://myLib/webpack/universalModuleDefinition",
    "webpack://myLib/./src/index.css?62aa",
    "webpack://myLib/external umd {\"commonjs\":\"lodash\",\"commonjs2\":\"lodash\",\"amd\":\"lodash\",\"root\":\"_\"}",
    "webpack://myLib/webpack/bootstrap",
    "webpack://myLib/webpack/runtime/compat get default export",
    "webpack://myLib/webpack/runtime/define property getters",
    "webpack://myLib/webpack/runtime/hasOwnProperty shorthand",
    "webpack://myLib/webpack/runtime/make namespace object",
    "webpack://myLib/./src/index.js"
  ],
  "sourcesContent": [
    "(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"lodash\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"lodash\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"myLib\"] = factory(require(\"lodash\"));\n\telse\n\t\troot[\"myLib\"] = factory(root[\"_\"]);\n})(self, (__WEBPACK_EXTERNAL_MODULE_lodash__) => {\nreturn ",
    "// extracted by mini-css-extract-plugin\nexport {};",
    "module.exports = __WEBPACK_EXTERNAL_MODULE_lodash__;",
    "// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n",
    "// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};",
    "// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};",
    "__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))",
    "// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};",
    "import _ from 'lodash'\nimport './index.css'\n\nexport const max = _.max\n\nexport const add = (a, b) => a + b\n\nexport const minus = (a, b) => a - b\n"
  ],
  "names": [],
  "sourceRoot": ""
}
  • version:当前使用的版本,也就是最新的第三版
  • sources:从哪些文件转换过来的source-map和打包的代码(最初始的文件)
  • names:转换前的变量和属性名称(因为我目前使用的是development模式,所以不需要保留转换前的名称)
  • mappings:source-map用来和源文件映射的信息(比如位置信息等),一串base64VLQ(veriable-length quantity可变长度值)编码
  • file:打包后的文件(浏览器加载的文件)
  • sourceContent:转换前的具体代码信息(和sources是对应的关系)
  • sourceRoot:所有的sources相对的根目录

生成 source-map

Devtool | webpack 中文文档 (docschina.org)

webpack为我们提供了非常多的选项(目前是26个),来处理source-map。选择不同的值,生成的source-map会稍微有差异,打包的过程也会有性能的差异,可以根据不同的情况进行选择。

下面这些值不会生成 source-map

  1. false:不使用source-map
  2. none:production模式下的默认值
  3. eval:development模式下的默认值,不生成 source-map,但是它会在eval执行的代码中,添加 //# sourceURL=**,它会被浏览器在执行时解析,并且在调试面板中生成对应的一些文件目录,方便我们调试代码

eval 效果

js
eval("__webpack_require__.r(__webpack_exports__);
\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {
\n/* harmony export */   add: () => (/* binding */ add),\n/* harmony export */   max: () => (/* binding */ max),
\n/* harmony export */   minus: () => (/* binding */ minus)\n/* harmony export */ });
\n/* harmony import */ var lodash__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! lodash */ \"lodash\");
\n/* harmony import */ var lodash__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(lodash__WEBPACK_IMPORTED_MODULE_0__);
\n/* harmony import */ var _index_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./index.css */ \"./src/index.css\");
\n\n\n\nconst max = (lodash__WEBPACK_IMPORTED_MODULE_0___default().max)\n\nconst add = (a, b) => a + b\n\nconst minus = (a, b) => a - b
\n\n\n//# sourceURL=webpack://myLib/./src/index.js?");
/***/ }),

source-map 值

主要介绍几个常用的

source-map

生成一个独立的source-map文件,并且在main文件中有一个注释,指向source-map文件

开发工具会根据这个注释找到source-map文件,并且解析

js
//# sourceMappingURL=main.js.map

eval-source-map

会生成sourcemap,但是source-map是以DataUrl添加到eval函数的后面

image-20240827143352444

inline-source-map

会生成sourcemap,但是source-map是以DataUrl添加到main文件的最后面

image-20240827143550116

cheap-source-map

会生成sourcemap,但是会更加高效一些(cheap低开销),因为它没有生成列映射(Column Mapping)

因为在开发中,我们只需要行信息通常就可以定位到错误了

image-20240827143716932

cheap-module-source-map

会生成sourcemap,类似于cheap-source-map,但是对源自loadersourcemap处理会更好

其实就是如果loader对我们的源码进行了特殊的处理,比如babel

如何选择:

  • cheap-source-map:适用于不需要经过 loader 处理的项目,或者你更关心编译速度,而不太在意调试的精确度。
  • cheap-module-source-map:适用于使用了 Babel、TypeScript 或其他 loader 的项目,并且你希望在调试时看到更准确的映射到原始源码的行号。

hidden-source-map

会生成sourcemap,但是不会对source-map文件进行引用。相当于删除了打包文件中对sourcemap的引用注释

js
// 将被删除
//# sourceMappingURL=main.js.map

如果我们手动添加进来,那么sourcemap就会生效了

nosources-source-map

会生成sourcemap,但是生成的sourcemap只有错误信息的提示,不会生成源代码文件

浏览器有错误提示的时候,点击错误提示,跳转无法查看源代码

多个值的组合

组合的规则如下:

  • inline-|hidden-|eval:三个值时三选一;
  • nosources:可选值;
  • cheap可选值,并且可以跟随module的值
bash
[inline- | hidden- | eval-][nosources-][cheap-[module-]]source-map

实际开发中的选择

  • 开发阶段:推荐使用 source-map或者cheap-module-source-map
    • 这分别是vue和react使用的值,可以获取调试信息,方便快速开发
  • 测试阶段:推荐使用 source-map或者cheap-module-source-map
    • 测试阶段我们也希望在浏览器下看到正确的错误提示
  • 发布阶段:false、缺省值(不写)

Released under the MIT License.