1、什么叫“多个模块且不包含共享模块代码的JS库”?
假设你现在要在npm上发布一个js库,你的库里有module1.js
、module2.js
2个模块,这2个模块都依赖了hex.js
工具模块,如果使用普通的
打包模式打包module1.js
、module2.js
2个模块,那么module1.js
、module2.js
2个模块中都会包含hex.js
工具模块,这会导致
在项目导入这2个模块后打包的体积变大,且有重复内容。
假设项目目录结构如下:
普通打包模式打包后的产物内容如图:
从上面2张图中可以看出打包后的module1.js
、module2.js
2个模块都包含了hex.js
工具模块的代码。
那应该怎么打包才能实现不包含hex.js
工具模块的代码?
2、实现打包多个模块不包含共享模块的代码?
原理很简单:把共享模块的代码当成当成外部扩展(Externals)
webpack
配置文件中可以通过externals
来配置外部扩展,它支持字符串、对象、函数、正则类型的值。符合externals的模块不会打包进最终的产物中。
实现思路描述:
1. 在webpack配置文件中配置`externals`,值为函数
2. 在`externals`函数中将"被请求引入的路径"(即`import xx from './utils.js'`中的路径)为相对路径的模块指定为外部模块即可
3、代码实现
安装依赖:npm install webpack webpack-cli ts-loader terser-webpack-plugin globby -D
其中globby
用来快速获取指定文件夹下的指定多个文件的路径
3.1、模块代码
/src/module1.ts
import Hex from './utils/hex';
import { isEmptyObject, getTimeStamp } from './utils/util';
import { isValidPort, isInt } from './module2';
class ATestModule {
static isValidPort = isValidPort;
static getTimeStamp = getTimeStamp;
static Hex = Hex;
name: string;
constructor(name: string) {
this.name = name;
}
sayName () {
console.log(`我的名字是:${this.name}(输出时间:${getTimeStamp()})`);
}
checkIsEmptyObject (obj: Record<string, any>) {
if (typeof obj !== 'object' || Array.isArray(obj)) {
return true;
}
return isEmptyObject(obj);
}
}
export { ATestModule };
export default ATestModule;
/src/module2.ts
import Hex from './utils/hex';
const intReg = /^\d+$/
/**
* 是否为整型
* @param val 变量s
* @returns {boolean}
*/
export function isInt (val: any) {
if (typeof val != 'string' && typeof val != 'number') {
return false
}
return intReg.test(val + '')
}
/**
* 校验端口是否合法
* @param {number} value 端口号
* @param {number} startPort 开始端口
* @param {number} endPort 结束端口
* @returns {{valid: boolean, msg: string}}
*/
export function isValidPort (value: number, startPort = 0, endPort = 65535) {
let result = {
valid: false,
msg: ''
}
if ((value + '').length == 0) {
result.msg = '请输入端口号'
return result
}
if (!isInt(value)) {
result.msg = '端口必须为正整数'
return result
}
if (value < startPort || value > endPort) {
result.msg = `端口取值区间为${startPort}-${endPort}`
return result
}
result.valid = true
return result
}
/**
* 字节数组转成16进制字符串
* @param {number[]} bytes
* @returns {string}
*/
export function bytesToHex (bytes: number[]) {
return Hex.encode(bytes, 0, bytes.length);
}
export default {
isValidPort,
isInt
};
/src/index.ts
import ATestModule from './module1';
import module2 from './module2';
export { ATestModule };
export default {
ATestModule,
module2
};
/src/utils/hex.ts
function Hex(){}
/**
* 字节数组转16进制字符串
* @param b 字节数组
* @param pos 开始位置,一般为0
* @param len 结束位置,一般为字节数组的长度
* @returns {string}
*/
Hex.encode = function(b: number[], pos: number, len: number): string {
var hexCh = new Array(len*2);
var hexCode = new Array('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F');
for(var i = pos,j = 0;i<len+pos;i++,j++) {
hexCh[j] = hexCode[(b[i]&0xFF)>>4];
hexCh[++j] = hexCode[(b[i]&0x0F)];
}
return hexCh.join('');
}
export {
Hex
};
export default Hex
/src/utils/util.ts
/**
* 判断对象是否为空
* @param obj
* @returns {boolean}
*/
export function isEmptyObject (obj: Record<string, any>): boolean {
let keys = Object.keys(obj);
return keys.length == 0;
};
/**
* 获取格式化后的时间戳
* @returns {string}
*/
export function getTimeStamp (): string {
let date = new Date();
let year = date.getFullYear();
let month = date.getMonth() + 1;
let day = date.getDate();
let hour = date.getHours();
let minute = date.getMinutes();
let second = date.getSeconds();
let milliseconds = date.getMilliseconds();
let result = '';
// month = month < 10 ? ('0' + month) : (month + '');
let dayStr = day < 10 ? ('0' + day) : (day + '');
let hourStr = hour < 10 ? ('0' + hour) : (hour + '');
let monthStr = month < 10 ? ('0' + month) : (month + '');
let minuteStr = minute < 10 ? ('0' + minute) : (minute + '');
let secondStr = second < 10 ? ('0' + second) : (second + '');
let millisecondsStr = milliseconds < 10 ? ('0' + milliseconds) : (milliseconds + '');
result = `${year}-${monthStr}-${dayStr} ${hourStr}:${minuteStr}:${secondStr}:${millisecondsStr}`;
return result;
};
tsconfig.json
{
"compilerOptions": {
"module": "CommonJS",
"target": "ES2015",
"outDir": "./typings",
"lib": [
"ES2015",
"DOM"
],
"esModuleInterop": true,
"removeComments": false,
"noEmitOnError": true,
"isolatedModules": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictNullChecks": true,
"strict": true,
"declaration": true,
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
},
"include": [
"src/**/*"
]
}
3.2、webpack打包配置
在项目根目录创建一个webpack.build-lib.js
,代码如下:
const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
/**
* 获取打包配置
* @param entry 入口文件
* @param output webpack的output输出配置
* @param otherConfig webpack其他配置
*/
function getWebpackConfig (entry, output, otherConfig = {}) {
return {
mode: "production",
entry: entry,
output,
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
module: {
rules: [
{
test: /\.ts$/, // 匹配.ts文件
use: 'ts-loader', // 使用ts-loader处理这些文件
exclude: /node_modules/, // 排除node_modules目录
}
]
},
externals ({ context, request, contextInfo, getResolve }, callback) {
// 将import时相对路径的模块视为外部依赖
if ((request.startsWith('./') || request.startsWith('../')) && (request !== entry)) {
// 给在导入模块时没有后缀的语句添加后缀
if (!request.endsWith('.ts') && !request.endsWith('.js')) {
request += '.js';
}
// 使用 request 路径,将一个 commonjs 模块外部化
return callback(null, 'commonjs2 ' + request);
}
// 继续下一步且不外部化引用
callback();
},
optimization: {
minimize: false, // 禁用最小化(压缩)
/* minimizer: [
new TerserPlugin({
terserOptions: {
// ecma: 2015, // 配置支持 ES Module
compress: {
// arrows: false, // 去除箭头函数
warnings: false,
drop_console: true, // 去除console
drop_debugger: true, // 去除debugger
},
mangle: {
keep_classnames: true, // 保持类名不被压缩
keep_fnames: true, // 保持函数名不被压缩
},
},
// sourceMap: false,
}),
] */
},
...otherConfig
};
}
/**
* d打包单个文件
* @param sourceFilePath 需要打包的文件路径
* @param config webpack配置信息
* @param totalCount 需要打包的文件总数量,可选
* @param currentIndex 当前打包的文件的索引,可选
*/
function buildSingleFile (sourceFilePath, config, totalCount, currentIndex) {
const webpackConfig = getWebpackConfig(sourceFilePath, config);
// console.log('webpackConfig', webpackConfig);
const compiler = webpack(webpackConfig);
return new Promise(function (resolve, reject) {
compiler.run((err, stats) => {
let percent = -1;
if (!isNaN(totalCount) && !isNaN(currentIndex)) {
percent = (currentIndex / totalCount).toFixed(2);
}
if (err) {
console.error(`编译[${sourceFilePath}]失败}` + (percent > -1 ? `,进度:${percent * 100}%` : ''));
console.error(err);
resolve(false);
} else {
console.log(`编译[${sourceFilePath}]成功` + (percent > -1 ? `,进度:${percent * 100}%` : ''));
}
compiler.close((closeErr) => { // 要加这个回调,否则会报错
// ...
// console.log('关闭编译器', closeErr);
});
resolve(true);
});
});
};
const outputDir = 'lib';
/**
* 执行打包
* @returns {Promise<void>}
*/
async function doBuild (outputDir) {
console.log('【执行打包lib操作】');
// 用于模式匹配目录文件
const globby = await import('globby');
// 需要忽略的文件
const needIgnoreFiles = ['!src/**/*.d.ts'];
const configJsAndMarkdownPaths = globby.globbySync(['src/*.js', 'src/*.ts', 'src/**/*.js', 'src/**/*.ts', ...needIgnoreFiles]);
const filesLen = configJsAndMarkdownPaths.length;
console.log('需要打包的文件数量:', filesLen);
const outputFileBasePath = path.resolve(__dirname, outputDir);
const startTime = new Date().getTime();
for ( let index = 0; index < filesLen; index++) {
let filePath = configJsAndMarkdownPaths[index];
const parsedFilePath = path.parse(filePath);
const outputFilePath = outputFileBasePath + parsedFilePath.dir.replace('src', '').replaceAll('/', path.sep);
if (!fs.existsSync(outputFilePath)) {
fs.mkdirSync(outputFilePath, { recursive: true });
}
const config = {
path: outputFilePath,
filename: parsedFilePath.name + '.js', // 打包后的js的文件名称
library: {
// name: 'xxx', // 不需要加name,默认即可
type: 'commonjs2',
// type: 'module',
// export: 'default', // export值不能未default,否则js导出时只能使用export default xxx形式才能生效
}
};
await buildSingleFile('./' + filePath, config, filesLen, index + 1);
}
console.log(`编译耗时:${(new Date().getTime() - startTime)/ 1000}s`);
const typingsDirPath = path.resolve(__dirname, './typings');
// 删除旧的类型描述文件
if (fs.existsSync(typingsDirPath)) {
fs.rmSync(path.resolve(__dirname, './typings'), { recursive: true });
}
try {
// 执行生成d.ts文件
execSync('tsc --declaration --emitDeclarationOnly'/* , {
cwd: join(__dirname, '../')
} */)
} catch (err) {
console.error('生成类型描述文件报错');
console.error(err);
}
// 复制描述文件
copyDir(path.resolve(__dirname, './typings'), path.resolve(__dirname, outputDir));
console.log(`打包完成,耗时:${(new Date().getTime() - startTime)/ 1000}s`);
};
/*
* 复制目录、子目录,及其中的文件
* @param src {String} 要复制的目录
* @param dist {String} 复制到目标目录
*/
function copyDir(src, dist, ignoreDirs){
if(!fs.existsSync(src)){
return;
}
var b = fs.existsSync(dist)
if(!b){
fs.mkdirSync(dist, {recursive: true});//创建目录
}
_copy(src, dist, ignoreDirs);
}
function _copy(src, dist, ignoreDirs = []) {
var paths = fs.readdirSync(src);
paths.forEach(function(p) {
if (ignoreDirs.includes(p)) {
return;
}
var _src = src + '/' +p;
var _dist = dist + '/' +p;
var stat = fs.statSync(_src)
if(stat.isFile()) {// 判断是文件还是目录
fs.writeFileSync(_dist, fs.readFileSync(_src));
} else if(stat.isDirectory()) {
copyDir(_src, _dist, ignoreDirs);// 当是目录是,递归复制
}
});
}
doBuild(outputDir);
3.3、普通模式打包webpack配置
在项目根目录下创建一个webpack.build-sdk.js
,代码如下:
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
mode: "production",
entry: './src/module1.ts',
output: {
path: path.resolve(__dirname, './sdk'),
filename: 'module1.js', // 打包后的js的文件名称
library: {
// name: 'module1',
type: 'commonjs2',
}
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
module: {
rules: [
{
test: /\.ts$/, // 匹配.ts文件
use: 'ts-loader', // 使用ts-loader处理这些文件
exclude: /node_modules/, // 排除node_modules目录
}
]
},
optimization: {
minimize: false, // 禁用最小化(压缩)
/* minimizer: [
new TerserPlugin({
terserOptions: {
// ecma: 2015, // 配置支持 ES Module
compress: {
// arrows: false, // 去除箭头函数
warnings: false,
drop_console: true, // 去除console
drop_debugger: true, // 去除debugger
},
mangle: {
keep_classnames: true, // 保持类名不被压缩
keep_fnames: true, // 保持函数名不被压缩
},
},
// sourceMap: false,
}),
] */
}
};
3.3、在package.json
中添加2条打包命令
- 普通模式打包:
"build:sdk": "webpack --config webpack.build-sdk.js"
- 多个模块共享公共模块打包:
"build:lib": "webpack --config webpack.build-lib.js"
4、使用
多个模块共享公共模块打包:执行npm run build:lib
命令,得到的结果如图:
普通模式打包:执行npm run build:sdk
命令,得到的结果如图:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。