起因
場景一:
當前專案經歷了刀耕火種地開發, 之後接入了 cli 工具集中管理打包, 那麼專案中的依賴,
和 cli 工具中的依賴重合度是多少, 並且他的的版本是否相同, 是否有冗餘程式碼
場景二:
專案中某一個庫升級了, 他依賴了 A 庫的 V3 版本, 同時當前專案依賴的是 A 庫 V2版本, 這個時候打包很明顯, 就會將這一個包的不同版本同時打入
場景三:
當前 deps 中有對應的依賴庫, 但是業務程式碼中並未使用到
由於上述的場景, 我們需要一個工具來解決這些情況
思考?
這些場景改如何解決, 解決的方案是什麼
針對場景三來說, 現在已經有一個庫: depcheck
簡單的原理: 通過檢測專案中的檔案 import
或者 require
和依賴進行對比, 最後生成依賴列表
想要一定的配置
(通過實際的呼叫, 發現還存在一定的問題: 在子模組中的程式碼未能被檢測, 同時關於依賴中的 babel 配置外掛檢測也是同樣的)
而場景一和二就和三不太一樣了, 他是已有庫, 但是略有重複, 所有需要針對庫進行檢測
目前計劃是通過 node 指令碼來執行
- 檢查 node_modules 或者 lock 檔案中, 是否存在同一庫的多個版本
- node_modules 檔案層級太多, lock 檔案是他的一層對映, 考慮從這裡入手
- 確保 lock 檔案是最新的(這一層比較麻煩, 沒標識來保證, 明確就確保此檔案是否存在即可)
- 開啟本地網站, 針對結果的視覺化顯示(經過實際的操作, 這一場景放棄, 具體原因放下下方詳述)
開發
這裡我們首先解決場景一的問題
場景一
在上面的思考中針對此場景已經了一解決方案了, 即 depcheck
場景, 但是他的配置需要重新編寫:
check 配置更新
const options = {
ignoreBinPackage: false, // ignore the packages with bin entry
skipMissing: false, // skip calculation of missing dependencies
ignorePatterns: [
// files matching these patterns will be ignored
'sandbox',
'dist',
'bower_components',
'tsconfig.json'
],
ignoreMatches: [
// ignore dependencies that matches these globs
'grunt-*',
],
parsers: {
// the target parsers
'**/*.js': depcheck.parser.es6,
'**/*.jsx': depcheck.parser.jsx,
'**/*.ts': depcheck.parser.typescript,
// 這裡 ts 型別可能會出問題, 但是經過實際的執行和文件說明是沒問題的
'**/*.tsx': [depcheck.parser.typescript, depcheck.parser.jsx],
},
detectors: [
// the target detectors
depcheck.detector.requireCallExpression,
depcheck.detector.requireResolveCallExpression,
depcheck.detector.importDeclaration,
depcheck.detector.exportDeclaration,
depcheck.detector.gruntLoadTaskCallExpression,
depcheck.detector.importCallExpression,
depcheck.detector.typescriptImportEqualsDeclaration,
depcheck.detector.typescriptImportType,
],
// specials: [
// // Depcheck API在選項中暴露了特殊屬性,它接受一個陣列,以指定特殊分析器。
// ],
// 這裡將會覆蓋原本的 package.json 的解析
// package: {
// },
};
之後再呼叫配置:
// 預設即當前路徑
const check = (path = process.cwd()) => depcheck(path ,options)
最後加上列印結果:
console.log('Unused dependencies:')
unused.dependencies.forEach(name=>{
console.log(chalk.greenBright(`* ${name}`))
})
console.log('Unused devDependencies:');
unused.devDependencies.forEach(name=>{
console.log(chalk.greenBright(`* ${name}`))
})
呼叫結果的例子展示:
場景二
指令技術選型:
- commander
推薦最多的, 同時也是下載量最多的, 下載量 8kw+
- package-lock.json
針對的 lock 檔案, 預設 npm
及其對應的解析, 現在還有 yarn
, pnpm
比較流行, 但是
一般在伺服器上打包時都用使用 npm
指令
指令的開發
計劃中的指令
- check // 預設場景一的操作
- check json // 解析 .lock 檔案, 同時列印佔用空間的包
- check json -d // 將結果列印成檔案
第一步
指令的定義:
const main = () => {
const program = new commander.Command();
program.command('check')
.description('檢查使用庫')
.action((options) => {
// 顯示一個 loading
const spinner = ora('Loading check').start();
// check
check()
}).command('json').description('解析 lock檔案').option('-d, --doc', '解析 lock 檔案, 將結果儲存')
.action(async (options) => {
// 顯示 loading
const spinner = ora('Loading check').start();
// 執行指令碼
// 額外判斷 options.open
deepCheck(spinner, options)
})
program.parse();
}
第二步 解析檔案
首先我們通過 fs 來獲取檔案內容:
const lockPath = path.resolve('package-lock.json')
const data = fs.readFileSync(lockPath, 'utf8')
針對 lock 資料解析:
const allPacks = new Map();
Object.keys(allDeps).forEach(name => {
const item = allDeps[name]
if (item.dev) {
// dev 的暫時忽略掉
return
}
if (item.requires) {
// 和item.dependencies中的操作類似
setCommonPack(item.requires, name, item.dependencies)
}
if (item.dependencies) {
Object.keys(item.dependencies).forEach(depsName => {
const depsItem = item.dependencies[depsName]
if (!allPacks.has(depsName)) {
allPacks.set(depsName, [])
}
const packArr = allPacks.get(depsName);
packArr.push({
location: `${name}/node_modules/${depsName}`,
version: depsItem.version,
label: 'reDeps', // 標識為重複的依賴
size: getFileSize(`./node_modules/${name}/node_modules/${depsName}`)
})
allPacks.set(depsName, packArr)
})
}
})
最後通過一個迴圈來計算出暫用空間最大的包:
// 建立一個排序資料, push 之後自動根據 size 排序
let topSizeIns = createTopSize()
allPacks.forEach((arr, name, index) => {
if(arr.length <= 1){
return
}
let localSize = 0
arr.forEach((item, itemIndex) => {
const size = Number(item.size)
localSize += size
})
topSizeIns.push({items: arr, size: localSize})
})
// 最後列印結果, 輸出可選擇文件
if (options.doc) {
fs.writeFileSync(`deepCheck.json`, `${JSON.stringify(mapChangeObj(allPacks), null, 2)}`, {encoding: 'utf-8'})
}
// 列印 top5
console.log(chalk.yellow('佔用空間最大的 5 個重複庫:'))
topSizeIns.arr.forEach(itemObj => {
const common = itemObj.items.find(it => it.label === 'common')
console.log(chalk.cyan(`${common.location}--${itemObj.size.toFixed(2)}KB`));
itemObj.items.forEach(it => {
console.log(`* ${it.location}@${it.version}--size:${it.size}KB`)
})
})
第三步
圖形化方案(已經棄用)
先說說實現方案:
- 轉換json 生成的資料至圖表需要的資料
- 啟動本地服務, 引用 echart 和資料
資料轉換:
let nodes = []
let edges = []
packs.forEach((arr, name, index) => {
let localSize = 0
arr.forEach((item, itemIndex) => {
const size = Number(item.size)
nodes.push({
x: Math.random() * 1000,
y: Math.random() * 1000,
id: item.location,
name: item.location,
symbolSize: size > max ? max : size,
itemStyle: {
color: getRandomColor(),
},
})
localSize += size
})
topSizeIns.push({items: arr, size: localSize})
const common = arr.find(it => it.label === 'common')
if (common) {
arr.forEach(item => {
if (item.label === 'common') {
return
}
edges.push({
attributes: {},
size: 1,
source: common.location,
target: item.location,
})
})
}
})
啟動服務:
服務並沒有使用三方庫, 而是新增了一個node http 服務:
var mineTypeMap = {
html: 'text/html;charset=utf-8',
htm: 'text/html;charset=utf-8',
xml: "text/xml;charset=utf-8",
// 省略其他
}
const createServer = () => {
const chartData = fs.readFileSync(getFile('deepCheck.json'), 'utf8')
http.createServer(function (request, response) {
// 解析請求,包括檔名
// request.url
if (request.url === '/') {
// 從檔案系統中讀取請求的檔案內容
const data = fs.readFileSync(getFile('tools.html'))
response.writeHead(200, {'Content-Type': 'text/html'});
// 這裡是使用的類似服務端資料的方案, 當然也可以使用引入 json 的方案來解決
const _data = data.toString().replace(new RegExp('<%chartData%>'), chartData)
// 響應檔案內容
response.write(_data);
response.end();
} else {
const targetPath = decodeURIComponent(getFile(request.url)); //目標地址是基準路徑和檔案相對路徑的拼接,decodeURIComponent()是將路徑中的漢字進行解碼
console.log(request.method, request.url, baseDir, targetPath)
const extName = path.extname(targetPath).substr(1);
if (fs.existsSync(targetPath)) { //判斷本地檔案是否存在
if (mineTypeMap[extName]) {
response.setHeader('Content-Type', mineTypeMap[extName]);
}
var stream = fs.createReadStream(targetPath);
stream.pipe(response);
} else {
response.writeHead(404, {'Content-Type': 'text/html'});
response.end();
}
}
}).listen(8080);
console.log('Server running at http://127.0.0.1:8080/');
opener(`http://127.0.0.1:8080/`);
}
export default createServer
效果圖:
通過此圖, 可以看到大概問題點所在:
- 依賴包太多, 導致資料顯示雜亂
- 根據包真實尺寸大小顯示圓圈, 其中的差距過大, 大的有幾萬 kb, 小的有幾十kb
圖中暫時閒置了最大 size 200
所以暫時不開啟此功能
總結
當前已構建出包: @grewer/deps-check
可嘗試使用
針對文章一開始提出的三種常見場景, 此包基本上能夠解決了
之後還能提出一些優化點, 比如有些包的替換(moment
替換 dayjs
, lodash
和 lodash.xx
包不能同時存在等等)
這些就需要長期維護管理了