原文地址:github.com/Nealyang/Pe…前端
腳手架實際上是大多數前端都不陌生的東西,基於前面寫過的兩篇文章:node
大概呢,就是介紹下,目前個人幾個項目頁面的代碼組織形式。react
用了幾個項目後,發現也挺順手,遂想着要不搞個 cli
工具,統一下源碼的目錄結構吧。git
這樣不只能夠減小一個機械的工做同時也可以統一源碼架構。同窗間維護項目的陌生感也會有所下降。的確是有一部分提效的不是。雖然咱們大多數頁面都走的大黃蜂搭建🥺。。。github
cli
工具其實就一些基本的命令運行、CV
大法,沒有什麼技術深度。web
#!/usr/bin/env node
'use strict'; const currentNodeVersion = process.versions.node; const semver = currentNodeVersion.split('.'); const major = semver[0]; if (major < 10) { console.error( 'You are running Node ' + currentNodeVersion + '.\n' + 'pmCli requires Node 10 or higher. \n' + 'Please update your version of Node.' ); process.exit(1); } require('../packages/initialization')(); 複製代碼
這裏是入口文件,比較簡單,就是配置個入口,順便校驗 node
的版本號shell
這個文件主要是配置一些命令,其實也比較簡單,你們從 commander
裏面查看本身須要的配置,而後配置出來就能夠了npm
就是根據本身需求去配置這裏就不贅述了,除了以上,就如下兩點實現:json
// 建立工程
program .usage("[command]") .command("init") .option("-f,--force", "overwrite current directory") .description("initialize your project") .action(initProject); // 新增頁面 program .command("add-page <page-name>") .description("add new page") .action(addPage); // 新增模塊 program .command("add-mod [mod-name]") .description("add new mod") .action(addMod); // 添加/修改 .pmConfig.json program .command("modify-config") .description("modify/add config file (.pmCli.config)") .action(modifyCon); program.parse(process.argv); 複製代碼
所謂兜底就是輸入 pm-cli
後沒有跟任何命令segmentfault
在說 init
以前呢,這裏有個技術背景。就是咱們的 rax
工程,基於 def 平臺初始化出來的,因此說自帶一個腳手架。可是咱們在源碼開發中呢,會對其進行一些改動。爲了不認知重複呢,init
我分爲兩個功能:
init projectName
從 0 建立一個
def init rax projectName
項目
這裏咱們在一個空目錄中進行演示
至於這裏的一些問題的交互就不介紹了,就是inquirer
配置的一些問題而已。沒有太大的參考價值 。
入口方法較爲簡單,其實就是區分當前運行 pm-cli init
究竟是基於已有項目初始化,仍是新建一個 rax
項目 ,判斷依據也很是簡單,就是判斷當前目錄下是否有 package.json
來
雖然這麼判斷感受是草率了點,可是,你細品也確實如此!對於有 package.json 的當前目錄,我還會去校驗別的不是。
若是當前目錄存在 package.json,那麼我認爲你是一個項目,想在此項目中,初始化拍賣源碼架構的配置。因此我會去判斷當前項目是否已經初始化過了。
fs.existsSync(path.resolve(CURR_DIR, `./${PM_CLI_CONFIG_FILE_NAME}`))
複製代碼
也就是這個PM_CLI_CONFIG_FILE_NAME
的內容。那麼則給出提示。畢竟不須要重複初始化嘛。若是你想強行再初始化一次,也能夠!
pm-cli init -f
複製代碼
準備工做坐在前期,最終運行的功能都在 run
方法裏面。
這裏還有個功能函數很是的通用,也就提早拿出來講了吧。
const dirList = fs.readdirSync(CURR_DIR);
checkNameValidate(projectName, dirList); 複製代碼
/** * 校驗名稱合法性 * @param {string} name 傳入的名稱 modName/pageName * @param {Array}} validateNameList 非法名數組 */ const checkNameValidate = (name, validateNameList = []) => { const validationResult = validatePageName(name); if (!validationResult.validForNewPackages) { console.error( chalk.red( `Cannot create a mod or page named ${chalk.green( `"${name}"` )} because of npm naming restrictions:\n` ) ); [ ...(validationResult.errors || []), ...(validationResult.warnings || []), ].forEach((error) => { console.error(chalk.red(` * ${error}`)); }); console.error(chalk.red("\nPlease choose a different project name.")); process.exit(1); } const dependencies = [ "rax", "rax-view", "rax-text", "rax-app", "rax-document", "rax-picture", ].sort(); validateNameList = validateNameList.concat(dependencies); if (validateNameList.includes(name)) { console.error( chalk.red( `Cannot create a project named ${chalk.green( `"${name}"` )} because a page with the same name exists.\n` ) + chalk.cyan( validateNameList.map((depName) => ` ${depName}`).join("\n") ) + chalk.red("\n\nPlease choose a different name.") ); process.exit(1); } }; 複製代碼
其實就是校驗名稱合法性以及排除重名。這個工具函數能夠直接 CV。
如上的流程圖,咱們已經走到run
方法了,剩下的就是裏面的一些判斷。
const packageObj = fs.readJSONSync(path.resolve(CURR_DIR, "./package.json"));
// 判斷是 rax 項目 if ( !packageObj.dependencies || !packageObj.dependencies.rax || !packageObj.name ) { handleError("必須在 rax 1.0 項目中初始化"); } // 判斷 rax 版本 let raxVersion = packageObj.dependencies.rax.match(/\d+/) || []; if (raxVersion[0] != 1) { handleError("必須在 rax 1.0 項目中初始化"); } if (!isMpaApp(CURR_DIR)) { handleError(`不支持非 ${chalk.cyan('MPA')} 應用使用 pmCli`); } 複製代碼
由於這些判斷也不是很是的具備參考價值,這裏就簡單跳過了,而後在重點介紹下一些公共方法的編寫。
/** * 判斷目標項目是否爲 ts,並建立配置文件 */ function addTsconfig() { let distExist, srcExist; let disPath = path.resolve("./tsconfig.json"); let srcPath = path.resolve(__dirname, "../../ts.json"); try { distExist = fs.existsSync(disPath); } catch (error) { handleError("路徑解析發生錯誤 code:0024,請聯繫@一凨"); } if (distExist) return; try { srcExist = fs.existsSync(srcPath); } catch (error) { handleError("路徑解析發生錯誤 code:1233,請聯繫@一凨"); } if (srcExist) { // 本地存在 console.log( chalk.red(`編碼語言請採用 ${chalk.underline.red("Typescript")}`) ); spinner.start("正在爲您建立配置文件:tsconfig.json"); fs.copy(srcPath, disPath) .then(() => { console.log(); spinner.succeed("已爲您建立 tsconfig.json 配置文件"); }) .catch((err) => { handleError("tsconfig 建立失敗,請聯繫@一凨"); }); } else { handleError("路徑解析發生錯誤 code:2144,請聯繫@一凨"); } } 複製代碼
上面的代碼你們都能讀的懂,粘貼這一段代碼的目的就是,但願你們寫cli 的時候,必定要多考慮邊界狀況,存在性判斷,以及一些異常兜底。避免沒必要要的 bug 產生
/** * 重寫項目中的 app.json * @param {string} distAppJson app.json 路徑 */ function rewriteAppJson(distAppPath) { try { let distAppJson = fs.readJSONSync(distAppPath); if ( distAppJson.routes && Array.isArray(distAppJson.routes) && distAppJson.routes.length === 1 ) { distAppJson.routes[0] = Object.assign({}, distAppJson.routes[0], { title: "阿里拍賣", spmB: "B碼", spmA: "A碼", }); fs.writeJSONSync(path.resolve(CURR_DIR, "./src/app.json"), distAppJson, { spaces: 2, }); } } catch (error) { handleError(`重寫 ${chalk.cyan("app.json")}出錯了,${error}`); } } 複製代碼
別的重寫方法就不粘貼了,由於也是比較枯燥且重複的。下面說一下公共方法和用處吧
const templateProjectPath = path.resolve(__dirname, `../temps/project`);
// 下載模板 await downloadTempFromRep(projectTempRepo, templateProjectPath); 複製代碼
/** *從遠程倉庫下載模板 * @param {string} repo 遠程倉庫地址 * @param {string} path 路徑 */ const downloadTempFromRep = async (repo, srcPath) => { if (fs.pathExistsSync(srcPath)) fs.removeSync(`${srcPath}`); await seriesAsync([`git clone ${repo} ${srcPath}`]).catch((err) => { if (err) handleError(`下載模板出錯:errorCode:${err},請聯繫@一凨`); }); if(fs.existsSync(path.resolve(srcPath,'./.git'))){ spinner.succeed(chalk.cyan('模板目錄下 .git 移除')); fs.remove(path.resolve(srcPath,'./.git')); } }; 複製代碼
下載模板這裏我直接用的 shell 腳本,由於這裏涉及到不少權限的問題。
// execute a single shell command where "cmd" is a string
exports.exec = function (cmd, cb) { // this would be way easier on a shell/bash script :P var child_process = require("child_process"); var parts = cmd.split(/\s+/g); var p = child_process.spawn(parts[0], parts.slice(1), { stdio: "inherit" }); p.on("exit", function (code) { var err = null; if (code) { err = new Error( 'command "' + cmd + '" exited with wrong status code "' + code + '"' ); err.code = code; err.cmd = cmd; } if (cb) cb(err); }); }; // execute multiple commands in series // this could be replaced by any flow control lib exports.seriesAsync = (cmds) => { return new Promise((res, rej) => { var execNext = function () { let cmd = cmds.shift(); console.log(chalk.blue("run command: ") + chalk.magenta(cmd)); shell.exec(cmd, function (err) { if (err) { rej(err); } else { if (cmds.length) execNext(); else res(null); } }); }; execNext(); }); }; 複製代碼
/** * 拷貝頁面s * @param {array} filesArr 文件數組,二維數組 * @param {function} errorCb 失敗回調函數 * @param {成功回調函數} successCb 成功回調函數 */ const copyFiles = (filesArr, errorCb, successCb) => { try { filesArr.map((filePathArr) => { if (filePathArr.length !== 2) throw "配置文件讀寫錯誤!"; fs.copySync(filePathArr[0], filePathArr[1]); spinner.succeed(chalk.cyan(`${path.basename(filePathArr[1])} 初始化完成`)); }); } catch (error) { console.log(error); errorCb(error); } }; 複製代碼
在將遠程代碼拷貝到源碼目錄 temps/
下,進行一波修改後,仍是須要 copy
到項目目錄中的,因此這裏封裝了一個方法。
配置文件是我爲了標識出當前項目,是否爲 pmCli
初始化所得。由於在addPage
的時候,page
中的一些頁面會使用到外部的組件,好比 loadingPage
如上,initProject:true|false
用來標識當前倉庫。
[pageName]
用來表示有哪些頁面是用 pmCli
新建的。屬性 type:'simpleSource'|'withContext'|'customStateManage'
則用來告訴後續 add-mod
到底添加哪一種類型的模塊。
同時呢,對內容進行了加密,由於配置頁面,是放在用戶的項目下的
const crypto = require('crypto');
function aesEncrypt(data) { const cipher = crypto.createCipher('aes192', 'PmCli'); var crypted = cipher.update(data, 'utf8', 'hex'); crypted += cipher.final('hex'); return crypted; } function aesDecrypt(encrypted) { const decipher = crypto.createDecipher('aes192', 'PmCli'); var decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } module.exports = { aesEncrypt, aesDecrypt } 複製代碼
基本上如上,初始化項目的功能就介紹完了,後面的功能都是換湯不換藥的這些操做。我們蜻蜓點水,提個要點。
上面的功能,其實就是跟 initProject
裏面的代碼類似,就是一些「業務」狀況的判斷不一樣而已。
其實模塊的新增也沒有特別的技術點。先選擇頁面列表,而後讀取.pmCli.config
中的頁面的類型。根據類型去新增頁面
function run(modName) {
// 新增模塊,須要定位當前位置 modifiedCurrPathAndValidatePro(CURR_DIR); // 選擇可以新增模塊的頁面 pageList = Object.keys(pmCliConfigFileContent).filter((val) => { return val !== "initProject"; }); if (pageList.length === 0) { handleError(); } inquirer.prompt(getQuestions(pageList)).then((answer) => { const { pageName } = answer; // modName 重名判斷 try { checkNameValidate( modName, fs.readdirSync( path.resolve(CURR_DIR, `./src/pages/${pageName}/components`) ) ); } catch (error) { console.log("讀取當前頁面模塊列表失敗", error); } let modType = pmCliConfigFileContent[pageName].type; inquirer.prompt(getInsureQuestions(modType)).then(async (ans) => { if (!ans.insure) { modType = ans.type; } const distPath = path.resolve( CURR_DIR, `./src/pages/${pageName}/components` ); const tempPath = path.resolve(__dirname, "../temps/mod"); // 下載模板 await downloadTempFromRep(modTempRepo, tempPath); try { if (fs.existsSync(distPath)) { console.log(chalk.cyanBright(`開始進行模塊初始化`)); let copyFileArr = [ [ path.resolve(tempPath, `./${modType}`), path.resolve(distPath, `./${modName}`), ], ]; if(modType === 'customStateManage'){ copyFileArr = [ [ path.resolve(tempPath,`./${modType}/mod-com`), path.resolve(distPath,`./${modName}`) ], [ path.resolve(tempPath,`./${modType}/mod-com.d.ts`), path.resolve(distPath,`../types/${modName}.d.ts`) ], [ path.resolve(tempPath,`./${modType}/mod-com.reducer.ts`), path.resolve(distPath,`../reducers/${modName}.reducer.ts`) ], ] } copyFiles(copyFileArr, (err) => { handleError(`拷貝配置文件失敗`, err); }); if (!ans.insure) { console.log(); console.log( chalk.underline.red( ` 請確認頁面:${pageName},在 .pmCli.config 中的類型` ) ); console.log(); } modAddEndConsole(modName,modType); } else { handleError("本地文件目錄有問題"); } } catch (error) { handleError("讀取文件目錄出錯,請聯繫@一凨"); } }); }); } 複製代碼
在添加模塊的時候,我還作了我的性化處理。防止好心人覺得要到 cd
到指定 pages
下才能 addMod
,因此我支持只要你在 src
、pages
或者項目根目錄下,均可以執行 add-mod
/** * 糾正當前路徑到項目路徑下,主要是爲了防止用戶在當前頁面新建模塊 */ const modifiedCurrPathAndValidatePro = (proPath) => { const configFilePath = path.resolve(CURR_DIR, `./${PM_CLI_CONFIG_FILE_NAME}`); try { if (fs.existsSync(configFilePath)) { pmCliConfigFileContent = JSON.parse( aesDecrypt(fs.readFileSync(configFilePath, "utf-8")) ); if (!isTrue(pmCliConfigFileContent.initProject)) { handleError(`配置文件:${PM_CLI_CONFIG_FILE_NAME}被篡改,請聯繫@一凨`); } } else if ( path.basename(CURR_DIR) === "pages" || path.basename(CURR_DIR) === "src" ) { CURR_DIR = path.resolve(CURR_DIR, "../"); modifiedCurrPathAndValidatePro(CURR_DIR); } else { handleError(`當前項目並不是${chalk.cyan("pm-cli")}初始化,不可以使用該命令`); } } catch (error) { handleError("讀取項目配置文件失敗", error); } }; 複製代碼
由於以前介紹過源碼的頁面架構,同時我也應用到了項目開發中。開發 pmCli
的時候,又新增了新增了配置文件,存在本地仍是加密的。那麼豈不是我以前的項目須要新增頁面還不能用這個 pmCli
?
因此,就新增了這個功能:
modify-config
:
pmCli
,沒有則新建,有,則修改
fs-extra
+
shell
就能玩起來,很是簡單
所謂工欲善其事必先利其器,在 cli
避免不了使用很是多的工具,這裏我主要是使用一些開源包以及從 CRA
裏面 copy 過來的方法。
homePage:https://github.com/tj/commander.js
node.js 命令行接口的完整解決方案
homePage:https://github.com/SBoudrias/Inquirer.js
交互式命令行用戶界面的組件
homePage:https://github.com/jprichardson/node-fs-extra
fs
模塊自帶文件模塊的外部擴展模塊
homePage:https://github.com/npm/node-semver
用於對版本的一些操做
homePage:https://github.com/chalk/chalk
在命令行中給文本添加顏色的組件
spinners、sparklines、progress bars圖樣顯示組件
homPage:https://github.com/nathanpeck/clui
homePage:https://gitlab.com/flippidippi/download-git-repo
Node
下載並提取一個git倉庫(GitHub,GitLab,Bitbucket)
homePage:https://github.com/sindresorhus/ora
命令行加載效果,同上一個相似
homePage:https://github.com/shelljs/shelljs
Node
跨端運行 shell 的組件
homePage:https://github.com/npm/validate-npm-package-name
用於檢查包名的合法性
homePage:https://github.com/yaronn/blessed-contrib
命令行可視化組件
原本這些工具打算單獨寫一篇文章的,可是堆 list 的文章的確不是頗有用。容易忘主要是,因此這裏就帶過了。功能和效果,你們自行查看和測試吧。而後 CRA
中的比較不錯的方法,我也在文章末尾列出來了。關於 CRA
的源碼閱讀,也能夠查看我以往的文章:github/Nealyang
commander
:概述一下,
Node
命令接口,也就是能夠用它代管
Node
命令。
npm地址
envinfo
:能夠打印當前操做系統的環境和指定包的信息。
npm地址
fs-extra
:外部依賴,
Node
自帶文件模塊的外部擴展模塊
npm地址
semver
:外部依賴,用於比較
Node
版本
npm地址
checkAppName()
:用於檢測文件名是否合法,
isSafeToCreateProjectIn()
:用於檢測文件夾是否安全
shouldUseYarn()
:用於檢測
yarn
在本機是否已經安裝
checkThatNpmCanReadCwd()
:用於檢測
npm
是否在正確的目錄下執行
checkNpmVersion()
:用於檢測
npm
在本機是否已經安裝了
validate-npm-package-name
:外部依賴,檢查包名是否合法。
npm地址
printValidationResults()
:函數引用,這個函數就是我說的特別簡單的類型,裏面就是把接收到的錯誤信息循環打印出來,沒什麼好說的。
execSync
:引用自
child_process.execSync
,用於執行須要執行的子進程
cross-spawn
:
Node
跨平臺解決方案,解決在
windows
下各類問題。用來執行
node
進程。
npm地址
dns
:用來檢測是否可以請求到指定的地址。
npm地址
全棧前端交流羣 | ||