1、什麼叫“多個模塊且不包含共享模塊代碼的JS庫”?
假設你現在要在npm上發佈一個js庫,你的庫裏有module1.js、module2.js2個模塊,這2個模塊都依賴了hex.js工具模塊,如果使用普通的
打包模式打包module1.js、module2.js2個模塊,那麼module1.js、module2.js2個模塊中都會包含hex.js工具模塊,這會導致
在項目導入這2個模塊後打包的體積變大,且有重複內容。
假設項目目錄結構如下:
普通打包模式打包後的產物內容如圖:
從上面2張圖中可以看出打包後的module1.js、module2.js2個模塊都包含了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命令,得到的結果如圖: