最近公司內部想搭建一個私有的 npm
倉庫,用於將平時用到次數至關頻繁的工具或者組件獨立出來,方便單獨管理,隨着項目的規模變大,數量變多,單純的複製粘粘無疑在優雅以及實用性上都沒法知足咱們的需求,因此進一步模塊化是必然的。vue
可是一個組件庫的創建實際上是一個很是麻煩的過程,基礎 webpack 的配置不用多說,接着你還要配合增長一些 es-lint 之類的工具來規範化團隊成員的代碼。在開發過程當中,你天然須要一個目錄來承載使用示例,方便 dev 這個組件,隨後呢,你還得創建一個打包規範,發佈到私有 npm
倉庫中。node
如此一來,必然大大下降咱們的積極性,因此不如建立一個用於創建模塊包的腳手架工具,方便咱們項目的初始化。webpack
tips:最終成品在底部git
這裏簡單說起一下 私有 npm
的搭建。github
npm i verdaccio -g
pm2 start verdaccio
推薦配合 nrm 使用 快速切換倉庫地址web
verdaccio githubvue-cli
還整個意大利名,屬實洋氣。npm
在進入正題以前,我先介紹一些要點和工具,有了這寫關鍵點,寫起來其實就至關簡單了。element-ui
你們有沒有想過一些全局安裝的工具,他是如何作到在命令行裏面自由調用的呢?json
事實上這個東西是 npm 提供的連接功能
// package.json { "name": "lucky-for-you", "bin": { "lucky": "bin/lucky" } }
當這樣的一個模塊被髮布以後,一旦有人使用 -g 參數全局安裝
sudo npm i luck-for-you -g/usr/local/bin/lucky -> /usr/local/lib/node_modules/luckytiger-package-cli/bin/lucky # npm 幫你進行連接
npm 事實上會幫你進行一次連接,連接到你操做系統的 Path 之中,從而但你敲出 Lucky 這個命令的時候,能從 path 中成功找到對應的程序
另一點就是用於連接執行的文件 通常在開頭都要加上以下內容,讓 bash 可以正確識別該文件應該如何執行
#!/usr/bin/env node // 意味使用 node 運行該文件 // next script
tj 大神的做品,能夠方便的書寫命令行工具。可以自動生成幫助命令
const program = require('commander'); program.version('0.0.1').usage('<command> [options]'); program .command('create <app-name>') .description('建立一個全新的 npm 組件模塊') .action((name, cmd) => { const options = cleanArgs(cmd); require('../lib/create')(name, options); }); // 用戶未輸入完整命令 輸出幫助 if (!process.argv.slice(2).length) { program.outputHelp(); } program.parse(process.argv);
事實上當我第一次使用 vue-cli3.0 的時候,裏面的命令行表單真是很是驚豔,翻了 vue-cli3 的源碼 找到了這款工具,用於命令行的表單。可以更加直觀的配置選項。
inquirer .prompt([ { type: 'list', name: 'template', message: 'template: 請選擇項目起始模板', choices: [ { key: '1', name: 'JavaScript Library - 適用於普通 JS 庫', value: 'js-lib', }, { key: '2', name: 'Vue-components - 適用於 Vue 組件庫', value: 'vue-component', }, ], }, { type: 'input', name: 'author', message: 'author: 請輸入你的名字', validate: function(value) { return !!value; }, }, { type: 'input', name: 'desc', message: 'desc: 請輸入項目描述', validate: function(value) { return !!value; }, }, { type: 'confirm', name: 'confirm', message: 'confirm: 完成配置了?', default: false, }, ]) .then(answers => { console.log(answers.template); console.log(answers.author); console.log(answers.desc); });
還有不少的表單類型,我這裏幾個最簡單的 list + input + confirm 就足夠了。
如今開始分享個人構建流程。因爲代碼量比較大,挨個文件帖出來沒有什麼必要,因此我這裏只作簡單介紹,具體的能夠查看個人 github項目。
我把個人 cli 工具大體分爲兩部分 template模板 + 建立器
z
建立器的主要功能是吸取用戶的可選項,基於模板進行復制+渲染。Vue-cli3.0對於這部分操做會更加複雜,他把模板裏面具體的功能都抽象成了一個 Plugin,能夠按需組建模板,對於面向廣泛大衆固然是更好的。
可是我這個項目由於是公司內部用,因此不太須要太過泛化的設計,一個模板直接解決一個問題,簡化模型就能夠了。好比一個模板用於建立 Vue 的組件庫,一個模板用於建立 React 的組件庫,還有一個模板用於建立JavaScript 的工具函數類庫。
如此一來咱們的 template模板
建立器
在必定程度上能夠作到解耦,也就是說往後須要更多類型的模板,不須要修改建立器部分的代碼。
├── README.md ├── bin │ └── lucky #主程序 ├── lib │ ├── copy.js #複製 │ └── create.js #主建立器 ├── package-lock.json ├── package.json ├── templates │ ├── config.js #模板配置 解耦 │ ├── js-lib #預設模板1 │ └── vue-component #預設模板2 ├── utils # 工具目錄 │ └── dir.js
{ "name": "luckytiger-package-cli", "version": "1.1.14", "description": "package-cli", "bin": { "lucky": "bin/lucky" }, "scripts": { "lucky": "node bin/lucky", "bootstarp": "cnpm i && cd ./templates/js-lib/ && cnpm i && cd ../vue-component/ && cnpm i ", "dev:js-lib": "cd templates/js-lib && npm run dev", "dev:vue-component": "cd templates/vue-component && npm run dev", "dev:create": "rm -rf test-app && node bin/lucky create test-app", "clear": "sudo rm -rf node_modules && sudo rm -rf templates/js-lib/node_modules && sudo rm -rf templates/vue-component/node_modules" }, "author": "zhangzhengyi", "license": "ISC", "dependencies": { "chalk": "^2.4.2", "commander": "^2.20.0", "ejs": "^2.6.2", "inquirer": "^6.4.1", "validate-npm-package-name": "^3.0.0" } }
配置了一些腳本 方便快速 DEV 模板的效果。
這樣運行
npm run dev:js-lib
就能查看和開發 js-lib 這個模板
bin/lucky
#!/usr/bin/env node const program = require('commander') program.version('0.0.1').usage('<command> [options]') program .command('create <app-name>') .description('建立一個全新的 npm 組件模塊') .action((name, cmd) => { const options = cleanArgs(cmd) require('../lib/create')(name, options) }) if (!process.argv.slice(2).length) { program.outputHelp() } program.parse(process.argv) // commander passes the Command object itself as options, // extract only actual options into a fresh object. function cleanArgs(cmd) { const args = {} cmd.options.forEach(o => { const key = camelize(o.long.replace(/^--/, '')) // if an option is not present and Command has a method with the same name // it should not be copied if (typeof cmd[key] !== 'function' && typeof cmd[key] !== 'undefined') { args[key] = cmd[key] } }) return args }
這個文件主要是作一下基本的命令設置 利用了 commander這個庫
若是用戶調用了建立命令,就會轉發給 lib/create.js
處理
lib/cerate.js
const path = require('path') const inquirer = require('inquirer') const validateProjectName = require('validate-npm-package-name') const chalk = require('chalk') const copy = require('./copy') const fs = require('fs') const dir = require('../utils/dir') const templates = require('../templates/config') async function create(projectName, options) { const cwd = options.cwd || process.cwd() const inCurrent = projectName === '.' const name = inCurrent ? path.relative('../', cwd) : projectName const targetDir = path.resolve(cwd, projectName || '.') const result = validateProjectName(name) if (!result.validForNewPackages) { console.error(chalk.red(`無效的項目名: "${name}"`)) result.errors && result.errors.forEach(err => { console.error(chalk.red.dim('Error: ' + err)) }) result.warnings && result.warnings.forEach(warn => { console.error(chalk.red.dim('Warning: ' + warn)) }) return } if (!dir.isDir(targetDir)) { fs.mkdirSync(targetDir) } else { console.error(chalk.red(`該目錄下已經存在該文件夾 請刪除或者修改項目名`)) return } const answers = await inquirer.prompt([ { type: 'list', name: 'template', message: 'template: 請選擇項目模板', choices: templates.map((v, i) => ({ key: i, name: v.name, value: v.dir })) }, { type: 'input', name: 'author', message: 'author: 請輸入你的名字', validate: function(value) { return !!value } }, { type: 'input', name: 'desc', message: 'desc: 請輸入項目描述', validate: function(value) { return !!value } }, { type: 'confirm', name: 'confirm', message: 'confirm: 完成配置了?', default: false } ]) // 啓動複製流程 const sourceDir = path.resolve(__dirname, '..', 'templates', answers.template) console.log(chalk.blue(`🚀 開始建立...`)) try { await copy({ from: sourceDir, to: targetDir, renderData: { desc: answers.desc, author: answers.author, name: projectName }, ignore: ['node_modules', 'package.json'] }) } catch (e) { console.error(chalk.red(e)) return } console.log(chalk.green('🎉 建立完畢!')) console.log() console.log(chalk.cyan(` $ cd ${projectName}`)) console.log(chalk.cyan(` $ npm i && npm run dev`)) } module.exports = create
這裏主要作了幾件事
這裏面 chalk 這個庫可以輸出帶顏色的命令行,美觀一點。
我把模板的一些配置信息都放到了 templates/config.js
中,目的是爲了解耦
//templates/config.js module.exports = [ { name: 'JavaScript Library - 適用於普通 JS 庫', dir: 'js-lib' }, { name: 'Vue-components - 適用於 Vue 組件庫', dir: 'vue-component' } ]
接下來讓咱們看看複製流程
lib/copy
const fs = require('fs') const path = require('path') const dir = require('../utils/dir') const ejs = require('ejs') async function copy({ from, to, renderData, ignore = [] }) { let files = fs.readdirSync(from) // 區分 文件 和 目錄 let rFiles = [] let dirs = [] for (const fileName of files) { if (dir.isDir(path.resolve(from, fileName))) { dirs.push(fileName) } else { rFiles.push(fileName) } } // 複製並編譯文件 rFiles.forEach(fileName => { // 須要忽略 if (ignore.some(v => v === fileName)) { return } let content = fs.readFileSync(path.resolve(from, fileName), 'utf-8') // 該文件須要調用 ejs 模板引擎進行編譯 if (/ejs$/.test(fileName)) { content = ejs.render(content, renderData) fileName = fileName.replace('.ejs', '') } fs.writeFileSync(path.resolve(to, fileName), content) }) // 遞歸複製 目錄 dirs.forEach(dirName => { // 須要忽略 if (ignore.some(v => v === dirName)) { return } const fromDir = path.resolve(from, dirName) const toDir = path.resolve(to, dirName) if (!dir.isDir(toDir)) { fs.mkdirSync(toDir) } copy({ from: fromDir, to: toDir, renderData, ignore }) }) } module.exports = copy
copy 是一個遞歸複製文件和目錄的結構,深度優先。
其中他擁有四個參數源文件夾,目標文件夾,渲染數據,忽略列表。
咱們的模板實際上是須要一些按需渲染內容的能力的,好比生成的 package.json 應該擁有用戶建立時填寫的項目名,建立者,描述等等信息。因此我這裏採用了 EJS 模板引擎進行渲染,全部以.ejs 結尾的文件,都將通過引擎+渲染數據的渲染,接着再輸出,好比 package.json.ejs
另外作了一些忽略的設計,緣由是某些文件在開發模板的過程當中須要,實際生成的時候須要進行過濾。
所有采用同步 API,由於咱們的文件都是比較小的,而且不是服務器上用,阻塞一下也沒有問題。
個人這裏設計了兩個預設模板,分別是 Vue-component 組件庫模板 另一個是 JS 庫的模板(示例一樣基於 Vue)。若是大家有相似的 需求能夠去看看。這兩個模板都是先用 vue-cli3.0生成以後進行改裝。
改裝的目的就是爲了更加契合組件庫這一需求,跟普通的項目不太同樣,組件庫須要在 DEV 模式下對組件進行測試和開發,而後必須擁有單獨打包這個組件的能力,接着進行發佈。
具體能夠直接看代碼
構建的過程當中有些坑須要注意
模板內部應該擁有兩個 package.json 文件
package.json 用於模板的 DEV 模式package.json.ejs 用於建立時的最終導出
而且不要在 package.json 裏面使用 files 字段作文件 publish 白名單,這會致使你的 cli 工具沒法正常發佈整個模板(這個應該是模板內部的 package.json 與整個 cli 工具的 package.json 產生了覆蓋關係)。
模板內部的.gitignore文件加個.ejs
一樣是 cli publish 的時候沒法正常 上傳模板裏面的.gitignore 文件,因此加個 ejs 可讓他假裝成普通文件。
因此我以爲 npm包 的嵌套是否是太容易產生干擾了一點。
這裏推薦你們寫組件庫的時候,能夠手寫一下 TS 的類型聲明 types,在 VSCode 下能得到很是好的代碼提示效果。
首先你須要在組件庫的 package.json 裏面添加一個屬性
{ "typings": "types/index.d.ts", }
我這裏寫一個簡單的函數
// 最終導出 export default { say (name) { return `your name: ${name}` } }
// index.d.ts function say(name: String): String export default { say }
這樣 VSCode 就能在你使用這個模塊的時候,給你更加健全的提示。
這裏額外提醒下,通過個人研究,element-ui 這樣的組件庫,能有 props 的提示是由於人家 vetur 組件專門給開的後門,寫 types 只能擁有 JS 層面的提示,寫 Vue-template 的時候依舊沒有,期待後續可以支持。