前端 JS 库的构建

lxf2023-03-15 12:45:01

开启AdminJS成长之旅!这是我参与「AdminJS日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

背景

JS 库的使用者可能有不同的客户端环境、不同的技术体系,但希望所使用的 JS 库稳定成熟。

JS 库的开发者一般都是希望使用新技术(兼容性问题),但希望给更多的用户提供 JS 库。

两者似乎有点冲突和矛盾了,那么怎样才能调节两者矛盾呢???

目前市面上,主推荐的做法是引入构建流程

模块化

提到构建,就不得不先说一下模块化。只有对模块化有了清晰的了解,才能助你完成 JS 库的构建。

模块就是一个独立的空间,能引用其他模块,也能被其他模块引用

原始模块

一个函数即可称为一个模块。这个是最常见的,最简单的模块。

ADM

AMD 是一种异步模块加载规范,专为浏览器设计,英文全称 Asynchronous Module Definition,中文名称 异步模块定义

define(id?: String, dependencies?: String[], factory: Function|Object);

上述这个参数的传递逻辑一定要弄清楚。

浏览器并不支持 AMD 模块,在浏览器端,需要借助 RequireJS 才能加载 AMD 模块。

CommonJS

CommonJS 是一种同步模块加载规范,目前主要用于 Node.js 环境中。

define(function(require, exports, module) {})

UMD

UMD 是一种通用模块加载规范,全称 Universal Module Definition,中文名称 通用模块定义。

(function(root, factory) {
    if(typeof define === 'function' && define.amd) {
        // amd
    } else if (typeof exports === 'object') {
        // CommonJS
    } else {
        // 原始模块
    }
})(this, function(root) {
    function functionName () {}
    return functionName
})

UMD 规范是对不同模块规范的简单整合。

ES Module

ES Module 是 ES6 带来的原生系统。目前部分浏览器已经支持直接使用,而不兼容的浏览器可以通过构建工具来使用。

export function functionName() {}

JS 库可提供两个入口文件:

入口文件支持的模块
index.js原始模块、ADM 模块、 CommonJS 模块、 UMD 模块
index.esm.jsES Module

打包方案

打包工具

webpack

首先安装 webpack:

npm install webpack webpack-cli --save-dev

配置 webpack.config.js

const path = require("path");

module.exports = {
    entry: "./index.js",
    output: {
        filename: "index.js",
        path: path.resolve(__dirname, "dist"),
    },
};

执行打包命令

npx webpack

生成 dist/index.js

(function (modules) {
    // 此处是 webpack 生成的 冗余代码
})()

webpack 打包会生成很多冗余代码,对于业务代码来说问题不大,但是对于 JS 库来说就不太友好了。

所以说 webpack 适合业务项目,则 rollup 适合库。也是业界最常见的选择方案。

rollup.js

首先安装 rollup.js

npm install rollup --save-dev

接下来创建如下文件及目录:

前端 JS 库的构建

config 目录下文件内容如下:

// rollup.cjs.js
module.exports = {
  input: 'src/index.js',
  output: {
    file: 'dist/index.cjs.js',
    format: 'cjs'
  }
}

// rollup.esm.js
module.exports = {
  input: 'src/index.js',
  output: {
    file: 'dist/index.esm.js',
    format: 'es'
  }
}

// rollup.umd.js
module.exports = {
  input: 'src/index.js',
  output: {
    file: 'dist/index.umd.js',
    format: 'umd',
    name: 'rollup_umd_test' // 必填
  }
}

package.json 执行的命令:

{
  "name": "rollup",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js",
  "scripts": {
    "build:cjs": "rollup -c config/rollup.cjs.js",
    "build:esm": "rollup -c config/rollup.esm.js",
    "build:umd": "rollup -c config/rollup.umd.js",
    "build": "npm run build:cjs && npm run build:esm && npm run build:umd"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "rollup": "^3.13.0"
  }
}

最后生成的 dist 文件内容如下:

// index.cjs.js
'use strict';

function functionName() {}

exports.functionName = functionName;


// index.esm.js
function functionName() {}

export { functionName };


// index.umd.js
(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
        typeof define === 'function' && define.amd ? define(['exports'], factory) :
            (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.rollup_umd_test = {}));
})(this, (function (exports) {
    'use strict';

    function functionName() {}
    
    exports.functionName = functionName;
}));

技巧:看到很多库,都有自己的 横幅说明 和 页脚说明(banner and footer),比如 Vue 的:

const banner =
  '/*!\n' +
  ` * Vue.js v${version}\n` +
  ` * (c) 2014-${new Date().getFullYear()} Evan You\n` +
  ' * Released under the MIT License.\n' +
  ' */'

const footer = '/* follow me on GitHub! @xxxx */'

所以我们也在 config/rollup.js

const pkg = require('../package.json')

const version = pkg.version
const name = pkg.name
const author = pkg.author

const banner =
  '/*!\n' +
  ` * ${name} v${version}\n` +
  ` * (c) 2016-${new Date().getFullYear()} ${author}\n` +
  ' * Released under the MIT License.\n' +
  ' */'

const footer = `/* follow me on GitHub! @${author} */`

exports.banner = banner
exports.footer = footer

接着修改打包配置:

// config/rollup.esm.js
const rollupConfig = require('./rollup')

module.exports = {
  input: 'src/index.js',
  output: {
    file: 'dist/index.esm.js',
    format: 'es',
    banner: rollupConfig.banner,
    footer: rollupConfig.footer
  }
}

最后得到的打包结果如下:

// dist/index.esm.js
/*!
 * rollup_test v1.0.0
 * (c) 2016-2023 RainyNight9
 * Released under the MIT License.
 */
function functionName() {}

export { functionName };
 /* follow me on GitHub! @RainyNight9 */

这样是不是就有点 cool 了。

rollup 的按需加载

可以通过 rollup 提供的 treeshaking 功能可以自动屏蔽未被使用的功能。

配置项

// rollup.config.js

// can be an array (for multiple inputs)
export default {
	// core input options
	external,
	input, // conditionally required
	plugins,

	// advanced input options
	cache,
	onwarn,
	preserveEntrySignatures,
	strictDeprecations,

	// danger zone
	acorn,
	acornInjectPlugins,
	context,
	moduleContext,
	preserveSymlinks,
	shimMissingExports,
	treeshake,

	// experimental
	experimentalCacheExpiry,
	perf,

	// required (can be an array, for multiple outputs)
	output: {
		// core output options
		dir,
		file,
		format, // required
		globals,
		name,
		plugins,

		// advanced output options
		assetFileNames,
		banner,
		chunkFileNames,
		compact,
		entryFileNames,
		extend,
		footer,
		hoistTransitiveImports,
		inlineDynamicImports,
		interop,
		intro,
		manualChunks,
		minifyInternalExports,
		outro,
		paths,
		preserveModules,
		preserveModulesRoot,
		sourcemap,
		sourcemapBaseUrl,
		sourcemapExcludeSources,
		sourcemapFile,
		sourcemapPathTransform,
		validate,

		// danger zone
		amd,
		esModule,
		exports,
		externalLiveBindings,
		freeze,
		indent,
		namespaceToStringTag,
		noConflict,
		preferConst,
		sanitizeFileName,
		strict,
		systemNullSetters
	},

	watch: {
		buildDelay,
		chokidar,
		clearScreen,
		skipWrite,
		exclude,
		include
	}
};