Appearance
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-loader
、eslint-loader
、ts-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
:用于编译产物的模块化方案,可选值有:commonjs
、umd
、module
、jsonp
等,通常选用兼容性更强的umd
方案即可。
对比添加library
配置后输出的结果:
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
打包后,发现体积会非常大
这是因为 Webpack 默认会将所有第三方依赖都打包进产物中,这种逻辑能满足 Web 应用资源合并需求,但在开发 NPM 库时则很可能导致代码冗余。
我们可以使用externals 配置项,将第三方依赖排除在打包系统之外
js
externals: {
// 将 lodash 作为一个外部依赖,不打包到最终的 bundle 中
lodash: {
commonjs: 'lodash',
commonjs2: 'lodash',
amd: 'lodash',
root: '_',
},
},
排除后体积就变小了
至此,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
- webpack 打包时配置
devtool:source-map
- 会发现打包后会出现
.map
文件,打包的js文件最后也有注释
js
//# sourceMappingURL=main.js.map
在Chrome浏览器中的设置可以看到有 map 设置
分析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
- false:不使用source-map
- none:production模式下的默认值
- 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
函数的后面
inline-source-map
会生成sourcemap
,但是source-map
是以DataUrl
添加到main
文件的最后面
cheap-source-map
会生成sourcemap
,但是会更加高效一些(cheap低开销),因为它没有生成列映射(Column Mapping)
因为在开发中,我们只需要行信息通常就可以定位到错误了
cheap-module-source-map
会生成sourcemap
,类似于cheap-source-map
,但是对源自loader
的sourcemap
处理会更好
其实就是如果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、缺省值(不写)